From f972a48c86a60f2dbafe1a8e25bf0adcfd295c59 Mon Sep 17 00:00:00 2001 From: Alexander Drozdov Date: Tue, 23 Nov 2021 11:39:25 +0200 Subject: [PATCH] flatten props --- src/parser/ParsedItem.ts | 36 ++++++------ src/parser/Parser.ts | 57 +++++++++--------- .../filters/create-item-filters.ts | 22 +++---- .../filters/create-stat-filters.ts | 2 +- src/web/price-check/filters/interfaces.ts | 2 +- .../filters/pseudo/item-property-unique.ts | 22 +++---- .../filters/pseudo/item-property.ts | 58 +++++++++---------- src/web/price-check/trade/common.ts | 8 +-- src/web/price-check/trends/getDetailsId.ts | 14 ++--- 9 files changed, 110 insertions(+), 111 deletions(-) diff --git a/src/parser/ParsedItem.ts b/src/parser/ParsedItem.ts index 9d7b49ba..2a0d6d9a 100644 --- a/src/parser/ParsedItem.ts +++ b/src/parser/ParsedItem.ts @@ -23,26 +23,25 @@ export interface ParsedItem { name: string baseType: string | undefined itemLevel?: number - props: { - armour?: number - evasion?: number - energyShield?: number - ward?: number - blockChance?: number - critChance?: number - attackSpeed?: number - physicalDamage?: number - elementalDamage?: number - mapBlighted?: true - mapTier?: number - gemLevel?: number - areaLevel?: number - talismanTier?: number - } + armourAR?: number + armourEV?: number + armourES?: number + armourWARD?: number + armourBLOCK?: number + weaponCRIT?: number + weaponAS?: number + weaponPHYSICAL?: number + weaponELEMENTAL?: number + mapBlighted?: true + mapTier?: number + gemLevel?: number + gemAltQuality?: 'Anomalous' | 'Divergent' | 'Phantasmal' | 'Superior' + areaLevel?: number + talismanTier?: number quality?: number - sockets: { + sockets?: { linked?: number // only 5 or 6 - white?: number + white: number } stackSize?: { value: number, max: number } isUnidentified: boolean @@ -59,7 +58,6 @@ export interface ParsedItem { }> extra: { prophecyMaster?: 'Alva' | 'Einhar' | 'Niko' | 'Jun' | 'Zana' - altQuality?: 'Anomalous' | 'Divergent' | 'Phantasmal' | 'Superior' } heistJob?: { name: HeistJob, level: number } category?: ItemCategory diff --git a/src/parser/Parser.ts b/src/parser/Parser.ts index 62da60f0..601c0cf1 100644 --- a/src/parser/Parser.ts +++ b/src/parser/Parser.ts @@ -141,7 +141,7 @@ function normalizeName (item: ParsedItem) { function parseMap (section: string[], item: ParsedItem) { if (section[0].startsWith(_$.MAP_TIER)) { - item.props.mapTier = Number(section[0].substr(_$.MAP_TIER.length)) + item.mapTier = Number(section[0].substr(_$.MAP_TIER.length)) let name = item.baseType || item.name if (_$.MAP_BLIGHTED.test(name)) { @@ -153,7 +153,7 @@ function parseMap (section: string[], item: ParsedItem) { item.name = name } item.category = ItemCategory.Map - item.props.mapBlighted = true + item.mapBlighted = true } return SECTION_PARSED @@ -173,14 +173,12 @@ function parseNamePlate (section: string[]) { category: undefined, name: markupConditionParser(section[2]), baseType: (section.length >= 4) ? markupConditionParser(section[3]) : undefined, - props: {}, isUnidentified: false, isCorrupted: false, newMods: [], statsByType: [], unknownModifiers: [], influences: [], - sockets: {}, extra: {}, rawText: undefined! } @@ -275,7 +273,7 @@ function parseItemLevel (section: string[], item: ParsedItem) { function parseTalismanTier (section: string[], item: ParsedItem) { if (section[0].startsWith(_$.TALISMAN_TIER)) { - item.props.talismanTier = Number(section[0].substr(_$.TALISMAN_TIER.length)) + item.talismanTier = Number(section[0].substr(_$.TALISMAN_TIER.length)) return SECTION_PARSED } return SECTION_SKIPPED @@ -287,14 +285,14 @@ function parseVaalGem (section: string[], item: ParsedItem) { if (section.length === 1) { let gemName: string | undefined if ((gemName = _$.QUALITY_ANOMALOUS.exec(section[0])?.[1])) { - item.extra.altQuality = 'Anomalous' + item.gemAltQuality = 'Anomalous' } else if ((gemName = _$.QUALITY_DIVERGENT.exec(section[0])?.[1])) { - item.extra.altQuality = 'Divergent' + item.gemAltQuality = 'Divergent' } else if ((gemName = _$.QUALITY_PHANTASMAL.exec(section[0])?.[1])) { - item.extra.altQuality = 'Phantasmal' + item.gemAltQuality = 'Phantasmal' } else if (_$.VAAL_GEM.test(section[0])) { gemName = section[0] - item.extra.altQuality = 'Superior' + item.gemAltQuality = 'Superior' } if (gemName) { @@ -311,21 +309,21 @@ function parseGem (section: string[], item: ParsedItem) { } if (section[1]?.startsWith(_$.GEM_LEVEL)) { // "Level: 20 (Max)" - item.props.gemLevel = parseInt(section[1].substr(_$.GEM_LEVEL.length), 10) + item.gemLevel = parseInt(section[1].substr(_$.GEM_LEVEL.length), 10) parseQualityNested(section, item) // don't override if parsed in Vaal name section - if (!item.extra.altQuality) { + if (!item.gemAltQuality) { let gemName: string | undefined if ((gemName = _$.QUALITY_ANOMALOUS.exec(item.name)?.[1])) { - item.extra.altQuality = 'Anomalous' + item.gemAltQuality = 'Anomalous' } else if ((gemName = _$.QUALITY_DIVERGENT.exec(item.name)?.[1])) { - item.extra.altQuality = 'Divergent' + item.gemAltQuality = 'Divergent' } else if ((gemName = _$.QUALITY_PHANTASMAL.exec(item.name)?.[1])) { - item.extra.altQuality = 'Phantasmal' + item.gemAltQuality = 'Phantasmal' } else { - item.extra.altQuality = 'Superior' + item.gemAltQuality = 'Superior' } if (gemName) { item.name = ITEM_NAME_REF_BY_TRANSLATED.get(gemName) || gemName @@ -371,7 +369,10 @@ function parseSockets (section: string[], item: ParsedItem) { if (section[0].startsWith(_$.SOCKETS)) { let sockets = section[0].substr(_$.SOCKETS.length).trimEnd() - item.sockets.white = (sockets.split('W').length - 1) + item.sockets = { + white: (sockets.split('W').length - 1), + linked: undefined + } sockets = sockets.replace(/[^ -]/g, '#') if (sockets === '#-#-#-#-#-#') { @@ -403,23 +404,23 @@ function parseArmour (section: string[], item: ParsedItem) { for (const line of section) { if (line.startsWith(_$.ARMOUR)) { - item.props.armour = parseInt(line.substr(_$.ARMOUR.length), 10) + item.armourAR = parseInt(line.substr(_$.ARMOUR.length), 10) isParsed = SECTION_PARSED; continue } if (line.startsWith(_$.EVASION)) { - item.props.evasion = parseInt(line.substr(_$.EVASION.length), 10) + item.armourEV = parseInt(line.substr(_$.EVASION.length), 10) isParsed = SECTION_PARSED; continue } if (line.startsWith(_$.ENERGY_SHIELD)) { - item.props.energyShield = parseInt(line.substr(_$.ENERGY_SHIELD.length), 10) + item.armourES = parseInt(line.substr(_$.ENERGY_SHIELD.length), 10) isParsed = SECTION_PARSED; continue } if (line.startsWith(_$.TAG_WARD)) { - item.props.ward = parseInt(line.substr(_$.TAG_WARD.length), 10) + item.armourWARD = parseInt(line.substr(_$.TAG_WARD.length), 10) isParsed = SECTION_PARSED; continue } if (line.startsWith(_$.BLOCK_CHANCE)) { - item.props.blockChance = parseInt(line.substr(_$.BLOCK_CHANCE.length), 10) + item.armourBLOCK = parseInt(line.substr(_$.BLOCK_CHANCE.length), 10) isParsed = SECTION_PARSED; continue } } @@ -436,22 +437,22 @@ function parseWeapon (section: string[], item: ParsedItem) { for (const line of section) { if (line.startsWith(_$.CRIT_CHANCE)) { - item.props.critChance = parseFloat(line.substr(_$.CRIT_CHANCE.length)) + item.weaponCRIT = parseFloat(line.substr(_$.CRIT_CHANCE.length)) isParsed = SECTION_PARSED; continue } if (line.startsWith(_$.ATTACK_SPEED)) { - item.props.attackSpeed = parseFloat(line.substr(_$.ATTACK_SPEED.length)) + item.weaponAS = parseFloat(line.substr(_$.ATTACK_SPEED.length)) isParsed = SECTION_PARSED; continue } if (line.startsWith(_$.PHYSICAL_DAMAGE)) { - item.props.physicalDamage = getRollOrMinmaxAvg(line + item.weaponPHYSICAL = getRollOrMinmaxAvg(line .substr(_$.PHYSICAL_DAMAGE.length) .split('-').map(str => parseInt(str, 10)) ) isParsed = SECTION_PARSED; continue } if (line.startsWith(_$.ELEMENTAL_DAMAGE)) { - item.props.elementalDamage = + item.weaponELEMENTAL = line.substr(_$.ELEMENTAL_DAMAGE.length) .split(', ') .map(element => getRollOrMinmaxAvg(element.split('-').map(str => parseInt(str, 10)))) @@ -610,7 +611,7 @@ function parseHeistMission (section: string[], item: ParsedItem) { item.category !== ItemCategory.HeistContract) return PARSER_SKIPPED parseAreaLevelNested(section, item) - if (!item.props.areaLevel) { + if (!item.areaLevel) { return SECTION_SKIPPED } @@ -636,7 +637,7 @@ function parseHeistMission (section: string[], item: ParsedItem) { function parseAreaLevelNested (section: string[], item: ParsedItem) { for (const line of section) { if (line.startsWith(_$.AREA_LEVEL)) { - item.props.areaLevel = Number(line.substr(_$.AREA_LEVEL.length)) + item.areaLevel = Number(line.substr(_$.AREA_LEVEL.length)) break } } @@ -647,7 +648,7 @@ function parseAtzoatlAreaLevel (section: string[], item: ParsedItem) { parseAreaLevelNested(section, item) - return (item.props.areaLevel) + return (item.areaLevel) ? SECTION_PARSED : SECTION_SKIPPED } diff --git a/src/web/price-check/filters/create-item-filters.ts b/src/web/price-check/filters/create-item-filters.ts index 48697688..53bd74ad 100644 --- a/src/web/price-check/filters/create-item-filters.ts +++ b/src/web/price-check/filters/create-item-filters.ts @@ -67,7 +67,7 @@ export function createFilters ( } if (item.name === 'Chronicle of Atzoatl') { filters.areaLevel = { - value: floorToBracket(item.props.areaLevel!, [1, 68, 73, 75, 78, 80]) + value: floorToBracket(item.areaLevel!, [1, 68, 73, 75, 78, 80]) } } return filters @@ -98,7 +98,7 @@ export function createFilters ( } } - if (item.props.mapBlighted) { + if (item.mapBlighted) { filters.mapBlighted = { value: true } } @@ -107,7 +107,7 @@ export function createFilters ( } filters.mapTier = { - value: item.props.mapTier! + value: item.mapTier! } } else if ( item.category === ItemCategory.HeistContract || @@ -122,7 +122,7 @@ export function createFilters ( } filters.areaLevel = { - value: item.props.areaLevel! + value: item.areaLevel! } if (item.heistJob) { @@ -160,14 +160,14 @@ export function createFilters ( } } - if (item.sockets.linked) { + if (item.sockets?.linked) { filters.linkedSockets = { value: item.sockets.linked, disabled: false } } - if (item.sockets.white) { + if (item.sockets?.white) { filters.whiteSockets = { value: item.sockets.white, disabled: false @@ -322,19 +322,19 @@ function createGemFilters (item: ParsedItem, filters: ItemFilters) { if (SPECIAL_SUPPORT_GEM.includes(item.name)) { filters.gemLevel = { - min: item.props.gemLevel!, - max: item.props.gemLevel!, + min: item.gemLevel!, + max: item.gemLevel!, disabled: false } } else { filters.gemLevel = { - min: item.props.gemLevel!, - disabled: item.props.gemLevel! < 16 + min: item.gemLevel!, + disabled: item.gemLevel! < 16 } } filters.altQuality = { - value: item.extra.altQuality!, + value: item.gemAltQuality!, disabled: false } diff --git a/src/web/price-check/filters/create-stat-filters.ts b/src/web/price-check/filters/create-stat-filters.ts index 1257701f..444329c6 100644 --- a/src/web/price-check/filters/create-stat-filters.ts +++ b/src/web/price-check/filters/create-stat-filters.ts @@ -289,7 +289,7 @@ function finalFilterTweaks (ctx: FiltersCreationContext) { if (item.category === ItemCategory.Amulet) { const anointment = ctx.filters.find(filter => filter.statRef === 'Allocates #') if (anointment) { - if (item.props.talismanTier) { + if (item.talismanTier) { anointment.disabled = false } else if (!item.isCorrupted && !item.isMirrored) { anointment.hidden = 'Buyer will likely change anointment' diff --git a/src/web/price-check/filters/interfaces.ts b/src/web/price-check/filters/interfaces.ts index 0773dca4..8988a19b 100644 --- a/src/web/price-check/filters/interfaces.ts +++ b/src/web/price-check/filters/interfaces.ts @@ -54,7 +54,7 @@ export interface ItemFilters { disabled: boolean } altQuality?: { - value: NonNullable + value: NonNullable disabled: boolean } areaLevel?: { diff --git a/src/web/price-check/filters/pseudo/item-property-unique.ts b/src/web/price-check/filters/pseudo/item-property-unique.ts index db9cc71f..43ccd098 100644 --- a/src/web/price-check/filters/pseudo/item-property-unique.ts +++ b/src/web/price-check/filters/pseudo/item-property-unique.ts @@ -27,8 +27,8 @@ const ARMOUR_STATS = new Set([ function armourProps (ctx: FiltersCreationContext) { const { item } = ctx - if (item.props.armour) { - const totalQ20 = Math.floor(propAt20Quality(item.props.armour, QUALITY_STATS.ARMOUR, item)) + if (item.armourAR) { + const totalQ20 = Math.floor(propAt20Quality(item.armourAR, QUALITY_STATS.ARMOUR, item)) ctx.filters.push({ ...internalPropStat( @@ -43,8 +43,8 @@ function armourProps (ctx: FiltersCreationContext) { }) } - if (item.props.evasion) { - const totalQ20 = Math.floor(propAt20Quality(item.props.evasion, QUALITY_STATS.EVASION, item)) + if (item.armourEV) { + const totalQ20 = Math.floor(propAt20Quality(item.armourEV, QUALITY_STATS.EVASION, item)) ctx.filters.push({ ...internalPropStat( @@ -59,8 +59,8 @@ function armourProps (ctx: FiltersCreationContext) { }) } - if (item.props.energyShield) { - const totalQ20 = Math.floor(propAt20Quality(item.props.energyShield, QUALITY_STATS.ENERGY_SHIELD, item)) + if (item.armourES) { + const totalQ20 = Math.floor(propAt20Quality(item.armourES, QUALITY_STATS.ENERGY_SHIELD, item)) ctx.filters.push({ ...internalPropStat( @@ -75,8 +75,8 @@ function armourProps (ctx: FiltersCreationContext) { }) } - if (item.props.ward) { - const totalQ20 = Math.floor(propAt20Quality(item.props.ward, QUALITY_STATS.WARD, item)) + if (item.armourWARD) { + const totalQ20 = Math.floor(propAt20Quality(item.armourWARD, QUALITY_STATS.WARD, item)) ctx.filters.push({ ...internalPropStat( @@ -102,9 +102,9 @@ const WEAPON_STATS = new Set([ function weaponProps (ctx: FiltersCreationContext) { const { item } = ctx - if (item.props.physicalDamage) { - const physQ20 = propAt20Quality(item.props.physicalDamage, QUALITY_STATS.PHYSICAL_DAMAGE, item) - const pdpsQ20 = Math.floor(physQ20 * item.props.attackSpeed!) + if (item.weaponPHYSICAL) { + const physQ20 = propAt20Quality(item.weaponPHYSICAL, QUALITY_STATS.PHYSICAL_DAMAGE, item) + const pdpsQ20 = Math.floor(physQ20 * item.weaponAS!) ctx.filters.push({ ...internalPropStat( diff --git a/src/web/price-check/filters/pseudo/item-property.ts b/src/web/price-check/filters/pseudo/item-property.ts index b0cdf9e0..017f1aef 100644 --- a/src/web/price-check/filters/pseudo/item-property.ts +++ b/src/web/price-check/filters/pseudo/item-property.ts @@ -30,8 +30,8 @@ export const ARMOUR_STATS = new Set([ function armourProps (ctx: FiltersCreationContext) { const { item } = ctx - if (item.props.armour) { - const totalQ20 = Math.floor(propAt20Quality(item.props.armour, QUALITY_STATS.ARMOUR, item)) + if (item.armourAR) { + const totalQ20 = Math.floor(propAt20Quality(item.armourAR, QUALITY_STATS.ARMOUR, item)) ctx.filters.push({ ...internalPropStat( @@ -45,8 +45,8 @@ function armourProps (ctx: FiltersCreationContext) { }) } - if (item.props.evasion) { - const totalQ20 = Math.floor(propAt20Quality(item.props.evasion, QUALITY_STATS.EVASION, item)) + if (item.armourEV) { + const totalQ20 = Math.floor(propAt20Quality(item.armourEV, QUALITY_STATS.EVASION, item)) ctx.filters.push({ ...internalPropStat( @@ -60,8 +60,8 @@ function armourProps (ctx: FiltersCreationContext) { }) } - if (item.props.energyShield) { - const totalQ20 = Math.floor(propAt20Quality(item.props.energyShield, QUALITY_STATS.ENERGY_SHIELD, item)) + if (item.armourES) { + const totalQ20 = Math.floor(propAt20Quality(item.armourES, QUALITY_STATS.ENERGY_SHIELD, item)) ctx.filters.push({ ...internalPropStat( @@ -75,8 +75,8 @@ function armourProps (ctx: FiltersCreationContext) { }) } - if (item.props.ward) { - const totalQ20 = Math.floor(propAt20Quality(item.props.ward, QUALITY_STATS.WARD, item)) + if (item.armourWARD) { + const totalQ20 = Math.floor(propAt20Quality(item.armourWARD, QUALITY_STATS.WARD, item)) ctx.filters.push({ ...internalPropStat( @@ -90,7 +90,7 @@ function armourProps (ctx: FiltersCreationContext) { }) } - if (item.props.blockChance) { + if (item.armourBLOCK) { ctx.filters.push({ ...internalPropStat( 'armour.block', @@ -99,16 +99,16 @@ function armourProps (ctx: FiltersCreationContext) { ), sources: [], disabled: true, - roll: rollToFilter(item.props.blockChance, { neverNegated: true, percent: ctx.searchInRange }) + roll: rollToFilter(item.armourBLOCK, { neverNegated: true, percent: ctx.searchInRange }) }) } if ( - item.props.armour || - item.props.evasion || - item.props.energyShield || - item.props.ward || - item.props.blockChance + item.armourAR || + item.armourEV || + item.armourES || + item.armourWARD || + item.armourBLOCK ) { createHiddenFilters(ctx, ARMOUR_STATS) } @@ -129,13 +129,13 @@ export const WEAPON_STATS = new Set([ function weaponProps (ctx: FiltersCreationContext) { const { item } = ctx - const physQ20 = propAt20Quality(item.props.physicalDamage!, QUALITY_STATS.PHYSICAL_DAMAGE, item) - const pdpsQ20 = Math.floor(physQ20 * item.props.attackSpeed!) + const physQ20 = propAt20Quality(item.weaponPHYSICAL!, QUALITY_STATS.PHYSICAL_DAMAGE, item) + const pdpsQ20 = Math.floor(physQ20 * item.weaponAS!) - const edps = Math.floor((item.props.elementalDamage || 0) * item.props.attackSpeed!) + const edps = Math.floor((item.weaponELEMENTAL || 0) * item.weaponAS!) const dps = pdpsQ20 + edps - if (item.props.elementalDamage) { + if (item.weaponELEMENTAL) { ctx.filters.push({ ...internalPropStat( 'weapon.total_dps', @@ -180,7 +180,7 @@ function weaponProps (ctx: FiltersCreationContext) { ), sources: [], disabled: true, - roll: rollToFilter(item.props.attackSpeed!, { neverNegated: true, dp: 2, percent: ctx.searchInRange }) + roll: rollToFilter(item.weaponAS!, { neverNegated: true, dp: 2, percent: ctx.searchInRange }) }) ctx.filters.push({ @@ -191,14 +191,14 @@ function weaponProps (ctx: FiltersCreationContext) { ), sources: [], disabled: true, - roll: rollToFilter(item.props.critChance!, { neverNegated: true, dp: 1, percent: ctx.searchInRange }) + roll: rollToFilter(item.weaponCRIT!, { neverNegated: true, dp: 1, percent: ctx.searchInRange }) }) if ( - item.props.attackSpeed || - item.props.critChance || - item.props.elementalDamage || - item.props.physicalDamage + item.weaponAS || + item.weaponCRIT || + item.weaponELEMENTAL || + item.weaponPHYSICAL ) { createHiddenFilters(ctx, WEAPON_STATS) } @@ -218,10 +218,10 @@ function createHiddenFilters (ctx: FiltersCreationContext, stats: Set) { function isSingleAttrArmour (item: ParsedItem) { return [ - item.props.armour, - item.props.evasion, - item.props.energyShield, - item.props.ward + item.armourAR, + item.armourEV, + item.armourES, + item.armourWARD ].filter(value => value != null).length === 1 } diff --git a/src/web/price-check/trade/common.ts b/src/web/price-check/trade/common.ts index dafeae45..d86568d9 100644 --- a/src/web/price-check/trade/common.ts +++ b/src/web/price-check/trade/common.ts @@ -53,10 +53,10 @@ export function tradeTag (item: ParsedItem): string | undefined { } if (name) { - if (item.props.mapBlighted) { - name = `Blighted ${name} (Tier ${item.props.mapTier})` - } else if (item.props.mapTier) { - name = `${name} (Tier ${item.props.mapTier})` + if (item.mapBlighted) { + name = `Blighted ${name} (Tier ${item.mapTier})` + } else if (item.mapTier) { + name = `${name} (Tier ${item.mapTier})` } else if (item.extra.prophecyMaster) { name = `${name} (${item.extra.prophecyMaster})` } diff --git a/src/web/price-check/trends/getDetailsId.ts b/src/web/price-check/trends/getDetailsId.ts index 879838df..da43265d 100644 --- a/src/web/price-check/trends/getDetailsId.ts +++ b/src/web/price-check/trends/getDetailsId.ts @@ -24,9 +24,9 @@ export function getDetailsId (item: ParsedItem) { } if (item.category === ItemCategory.Map) { if (item.rarity === ItemRarity.Unique) { - return nameToDetailsId(`${item.name} t${item.props.mapTier}`) + return nameToDetailsId(`${item.name} t${item.mapTier}`) } else { - return nameToDetailsId(`${item.props.mapBlighted ? 'Blighted ' : ''}${item.baseType || item.name} t${item.props.mapTier} ${LATEST_MAP_VARIANT}`) + return nameToDetailsId(`${item.mapBlighted ? 'Blighted ' : ''}${item.baseType || item.name} t${item.mapTier} ${LATEST_MAP_VARIANT}`) } } if (item.category === ItemCategory.CapturedBeast || @@ -58,17 +58,17 @@ function getGemDetailsId (item: ParsedItem) { return 'portal-1' } - let id = item.extra.altQuality === 'Superior' + let id = item.gemAltQuality === 'Superior' ? nameToDetailsId(item.name) - : nameToDetailsId(`${item.extra.altQuality} ${item.name}`) + : nameToDetailsId(`${item.gemAltQuality} ${item.name}`) if ( SPECIAL_SUPPORT_GEM.includes(item.name) || item.name === BRAND_RECALL_GEM || item.name === BLOOD_AND_SAND_GEM || - item.props.gemLevel! >= 20 + item.gemLevel! >= 20 ) { - id += `-${item.props.gemLevel}` + id += `-${item.gemLevel}` } else { id += '-1' } @@ -107,7 +107,7 @@ function getBaseTypeDetailsId (item: ParsedItem) { function getUniqueDetailsId (item: ParsedItem) { let id = nameToDetailsId(`${item.name}${getUniqueVariant(item) || ''} ${item.baseType}`) - if (item.sockets.linked) { + if (item.sockets?.linked) { id += `-${item.sockets.linked}l` } if (item.baseType === 'Ivory Watchstone') {