diff --git a/src/parser/modifiers.ts b/src/parser/modifiers.ts index 57266eb1..f1abf614 100644 --- a/src/parser/modifiers.ts +++ b/src/parser/modifiers.ts @@ -6,11 +6,13 @@ import type { ParsedStat } from './stat-translations' export interface StatCalculated { stat: Stat type: ModifierType - sources: Array<{ - modifier: ParsedModifier - stat: ParsedStat - contributes?: StatRoll - }> + sources: StatSource[] +} + +export interface StatSource { + modifier: ParsedModifier + stat: ParsedStat + contributes?: StatRoll } export interface StatRoll { @@ -61,11 +63,11 @@ export function sumStatsByModType (mods: readonly ParsedModifier[]): StatCalcula } export function statSourcesTotal ( - calc: StatCalculated + sources: StatSource[] ): StatRoll | undefined { - return (calc.sources.length === 1) - ? (calc.sources[0].contributes) - : (calc.sources.reduce((sum, { contributes }) => { + return (sources.length === 1) + ? (sources[0].contributes) + : (sources.reduce((sum, { contributes }) => { sum.value += contributes!.value sum.min += contributes!.min sum.max += contributes!.max diff --git a/src/web/map-check/prepare-map-stats.ts b/src/web/map-check/prepare-map-stats.ts index ad6493c1..6aa5122d 100644 --- a/src/web/map-check/prepare-map-stats.ts +++ b/src/web/map-check/prepare-map-stats.ts @@ -9,7 +9,7 @@ export interface PreparedStat { export function prepareMapStats (item: ParsedItem): PreparedStat[] { return item.statsByType.map(calc => { - const roll = statSourcesTotal(calc) + const roll = statSourcesTotal(calc.sources) const translation = translateStatWithRoll(calc, roll) const prepared = { diff --git a/src/web/price-check/filters/FilterModifier.vue b/src/web/price-check/filters/FilterModifier.vue index 48526685..39dd53bf 100644 --- a/src/web/price-check/filters/FilterModifier.vue +++ b/src/web/price-check/filters/FilterModifier.vue @@ -208,12 +208,14 @@ export default defineComponent({ showSourceInfo: computed(() => props.filter.sources.length && props.filter.option == null && ( + props.filter.tag === FilterTag.Pseudo || + ( props.filter.sources.length >= 2 || props.filter.sources[0].modifier.info.name != null || props.filter.sources[0].modifier.info.tier != null || props.filter.sources[0].modifier.info.rank != null ) - ), + )), inputFocus, toggleFilter } @@ -376,7 +378,6 @@ export default defineComponent({ "Armour: #": "Броня: #", "Evasion Rating: #": "Уклонение: #", "Energy Shield: #": "Энерг. щит: #", - "+#% total to one of Elemental Resistances": "Всего +#% к сопротивлению одной из стихий", "Map is not occupied by Elder Guardian": "Карта не захвачена Хранителем Древнего", "Block: #%": "Блок: #%", "variant": "вариант", @@ -392,8 +393,7 @@ export default defineComponent({ "Buyer will likely change anointment": "Покупатель, скорее всего, поменяет зачарование", "Select only if price-checking as base item for crafting": "Отмечайте, если проверяете цену в качестве базового предмета для крафта", "1 Empty or Crafted Modifier": "1 свободное или ремесленное свойство", - "Select only if item has 6 modifiers (1 of which is crafted) or if it has 5 modifiers": "Выбирайте, только если у предмета 6 свойств (1 из которых ремесленное) или если у него 5 свойств", - "Stat has a relatively small value": "Относительно небольшое значение у мода" + "Select only if item has 6 modifiers (1 of which is crafted) or if it has 5 modifiers": "Выбирайте, только если у предмета 6 свойств (1 из которых ремесленное) или если у него 5 свойств" } } diff --git a/src/web/price-check/filters/create-stat-filters.ts b/src/web/price-check/filters/create-stat-filters.ts index 407cd31e..b2a4fe09 100644 --- a/src/web/price-check/filters/create-stat-filters.ts +++ b/src/web/price-check/filters/create-stat-filters.ts @@ -110,7 +110,7 @@ export function calculatedStatToFilter ( } } - const roll = statSourcesTotal(calc) + const roll = statSourcesTotal(calc.sources) const translation = translateStatWithRoll(calc, roll) const filter: StatFilter = { diff --git a/src/web/price-check/filters/pseudo/attributes.ts b/src/web/price-check/filters/pseudo/attributes.ts deleted file mode 100644 index dcc76ba1..00000000 --- a/src/web/price-check/filters/pseudo/attributes.ts +++ /dev/null @@ -1,121 +0,0 @@ -import { pseudoStat, sumPseudoStats } from './util' -import { FiltersCreationContext } from '../create-stat-filters' -import { rollToFilter } from '../util' -import { STAT_BY_REF, stat } from '@/assets/data' -import { ModifierType } from '@/parser/modifiers' - -const TO_ALL_ATTRS = stat('+# to all Attributes') - -const STR_ATTR = { - pseudo: pseudoStat('+# total to Strength'), - stats: [ - stat('+# to Strength'), - stat('+# to Strength and Intelligence'), - stat('+# to Strength and Dexterity'), - TO_ALL_ATTRS - ] -} - -const DEX_ATTR = { - pseudo: pseudoStat('+# total to Dexterity'), - stats: [ - stat('+# to Dexterity'), - stat('+# to Dexterity and Intelligence'), - stat('+# to Strength and Dexterity'), - TO_ALL_ATTRS - ] -} - -const INT_ATTR = { - pseudo: pseudoStat('+# total to Intelligence'), - stats: [ - stat('+# to Intelligence'), - stat('+# to Strength and Intelligence'), - stat('+# to Dexterity and Intelligence'), - TO_ALL_ATTRS - ] -} - -const ATTRS = [ - STR_ATTR, - DEX_ATTR, - INT_ATTR -] - -const TO_MAXIMUM_LIFE = stat('+# to maximum Life') -const TO_MAXIMUM_MANA = stat('+# to maximum Mana') - -export function filterAttributes (ctx: FiltersCreationContext) { - const attrs: Array<{ - pseudo: ReturnType - total: number - hasFlat: boolean - countToAll: number - }> = [] - - for (const attr of ATTRS) { - const total = sumPseudoStats(ctx.statsByType, attr.stats) - if (total !== undefined) { - const hasNotToAll = ctx.statsByType.some(m => attr.stats.includes(m.stat.ref) && m.stat.ref !== TO_ALL_ATTRS) - const countToAll = ctx.statsByType.filter(m => m.stat.ref === TO_ALL_ATTRS).length - - attrs.push({ pseudo: attr.pseudo, total, hasFlat: hasNotToAll, countToAll }) - } - } - - const isOnlyToTotal = (attrs.length && attrs.every(a => !a.hasFlat)) - if (isOnlyToTotal) { - const totalToAllAttrs = sumPseudoStats(ctx.statsByType, [TO_ALL_ATTRS])! - - ctx.filters.push({ - ...pseudoStat('+# total to all Attributes'), - disabled: true, - sources: [], - roll: rollToFilter(totalToAllAttrs, { neverNegated: true, percent: ctx.searchInRange }) - }) - } else { - for (const attr of attrs) { - ctx.filters.push({ - ...attr.pseudo, - disabled: true, - sources: [], - hidden: (attr.hasFlat || attr.countToAll >= 2) ? undefined : 'Stat has a relatively small value', - roll: rollToFilter(attr.total, { neverNegated: true, percent: ctx.searchInRange }) - }) - } - } - - for (const attr of attrs) { - if (attr.pseudo.text === STR_ATTR.pseudo.text) { - ctx.statsByType.push({ - stat: STAT_BY_REF.get(TO_MAXIMUM_LIFE)!, - type: ModifierType.Explicit, - sources: [ - // TODO - // { - // modifier: undefined, - // stat: undefined, - // contributes: attr.total * (5 / 10) - // } - ] - }) - } - if (attr.pseudo.text === INT_ATTR.pseudo.text) { - ctx.statsByType.push({ - stat: STAT_BY_REF.get(TO_MAXIMUM_MANA)!, - type: ModifierType.Explicit, - sources: [ - // TODO - // { - // modifier: undefined, - // stat: undefined, - // contributes: attr.total * (5 / 10) - // } - ] - }) - } - } - - const statsToRemove = new Set(ATTRS.flatMap(a => a.stats)) - ctx.statsByType = ctx.statsByType.filter(m => !statsToRemove.has(m.stat.ref)) -} diff --git a/src/web/price-check/filters/pseudo/calc-q20.ts b/src/web/price-check/filters/pseudo/calc-q20.ts index 63ccb253..a1236678 100644 --- a/src/web/price-check/filters/pseudo/calc-q20.ts +++ b/src/web/price-check/filters/pseudo/calc-q20.ts @@ -47,7 +47,7 @@ export function propAt20Quality ( let flat = 0 for (const calc of item.statsByType) { - const roll = statSourcesTotal(calc)! + const roll = statSourcesTotal(calc.sources)! if (calc.stat.ref === statRefs.flat) { flat = roll.value } else if (statRefs.incr.includes(calc.stat.ref)) { diff --git a/src/web/price-check/filters/pseudo/index.ts b/src/web/price-check/filters/pseudo/index.ts index 2bdcf6ab..86c80bba 100644 --- a/src/web/price-check/filters/pseudo/index.ts +++ b/src/web/price-check/filters/pseudo/index.ts @@ -1,175 +1,410 @@ -import { FiltersCreationContext } from '../create-stat-filters' -import { rollToFilter } from '../util' -import { pseudoStat, sumPseudoStats } from './util' -import { filterResists } from './resistances' -import { filterAttributes } from './attributes' import { stat, STAT_BY_REF } from '@/assets/data' +import { ModifierType, statSourcesTotal } from '@/parser/modifiers' +import { rollToFilter } from '../util' +import { filterPseudoSources, pseudoStat } from './util' +import type { FiltersCreationContext } from '../create-stat-filters' +import type { StatFilter } from '../interfaces' -export function filterPseudo (ctx: FiltersCreationContext) { - filterResists(ctx) - filterAttributes(ctx) - filterRemainingPseudo(ctx) +const RESISTANCES_INFO = [ + { ref: stat('+#% to all Elemental Resistances'), elements: ['fire', 'cold', 'lightning'] }, + { ref: stat('+#% to Fire Resistance'), elements: ['fire'] }, + { ref: stat('+#% to Cold Resistance'), elements: ['cold'] }, + { ref: stat('+#% to Lightning Resistance'), elements: ['lightning'] }, + { ref: stat('+#% to Fire and Lightning Resistances'), elements: ['fire', 'lightning'] }, + { ref: stat('+#% to Fire and Cold Resistances'), elements: ['fire', 'cold'] }, + { ref: stat('+#% to Cold and Lightning Resistances'), elements: ['cold', 'lightning'] }, + { ref: stat('+#% to Chaos Resistance'), elements: [], chaos: true }, + { ref: stat('+#% to Fire and Chaos Resistances'), elements: ['fire'], chaos: true }, + { ref: stat('+#% to Cold and Chaos Resistances'), elements: ['cold'], chaos: true }, + { ref: stat('+#% to Lightning and Chaos Resistances'), elements: ['lightning'], chaos: true } +] + +const ATTRIBUTES_INFO = [ + { ref: stat('+# to all Attributes'), attributes: ['str', 'dex', 'int'] }, + { ref: stat('+# to Strength'), attributes: ['str'] }, + { ref: stat('+# to Dexterity'), attributes: ['dex'] }, + { ref: stat('+# to Intelligence'), attributes: ['int'] }, + { ref: stat('+# to Strength and Intelligence'), attributes: ['str', 'int'] }, + { ref: stat('+# to Strength and Dexterity'), attributes: ['str', 'dex'] }, + { ref: stat('+# to Dexterity and Intelligence'), attributes: ['dex', 'int'] } +] + +interface PseudoRule { + group?: string + pseudo: string + disabled?: boolean + replaces?: string + stats: Array<{ + ref: string + multiplier?: number + required?: boolean + }> + mutate?: (filter: StatFilter) => void } -function filterRemainingPseudo (ctx: FiltersCreationContext) { - for (const pseudoMod of REST_PSEUDO) { - const total = sumPseudoStats(ctx.statsByType, pseudoMod.stats) - - if (total !== undefined) { - ctx.filters.push({ - ...pseudoMod.pseudo, - disabled: true, - sources: [], - roll: rollToFilter(total, { - neverNegated: true, - dp: pseudoMod.stats.some((ref) => STAT_BY_REF.get(ref)!.dp), - percent: ctx.searchInRange - }) - }) +const PSEUDO_RULES: PseudoRule[] = [ + { + pseudo: stat('+#% total Elemental Resistance'), + disabled: false, + stats: + RESISTANCES_INFO.filter(info => info.elements.length) + .map(info => ({ ref: info.ref, multiplier: info.elements.length })) + }, + { + pseudo: stat('+#% total to Fire Resistance'), + group: 'to_x_ele_res', + stats: + RESISTANCES_INFO.filter(info => info.elements.includes('fire')) + .map(info => ({ ref: info.ref })) + }, + { + pseudo: stat('+#% total to Cold Resistance'), + group: 'to_x_ele_res', + stats: + RESISTANCES_INFO.filter(info => info.elements.includes('cold')) + .map(info => ({ ref: info.ref })) + }, + { + pseudo: stat('+#% total to Lightning Resistance'), + group: 'to_x_ele_res', + stats: + RESISTANCES_INFO.filter(info => info.elements.includes('lightning')) + .map(info => ({ ref: info.ref })) + }, + { + pseudo: stat('+#% total to Chaos Resistance'), + stats: + RESISTANCES_INFO.filter(info => info.chaos === true) + .map(info => ({ ref: info.ref })), + mutate (filter) { + if (filter.sources.length === 1 && + filter.sources[0].modifier.info.type === ModifierType.Crafted + ) { + filter.hidden = 'Crafted Chaos Resistance without Explicit mod has no value' + } else { + filter.disabled = false + } } - } - - const statsToRemove = new Set(REST_PSEUDO.flatMap(a => a.stats)) - ctx.statsByType = ctx.statsByType.filter(m => !statsToRemove.has(m.stat.ref)) -} - -const REST_PSEUDO = [ + }, { - pseudo: pseudoStat('+# total maximum Life'), + pseudo: stat('+# total to all Attributes'), + group: 'to_all_attrs', stats: [ - stat('+# to maximum Life') + { ref: stat('+# to all Attributes') } + // NOTE: not including other sources from `ATTRIBUTES_INFO` ] }, { - pseudo: pseudoStat('+# total maximum Mana'), + pseudo: stat('+# total to Strength'), + group: 'to_x_attr', + stats: + ATTRIBUTES_INFO.filter(info => info.attributes.includes('str')) + .map(info => ({ ref: info.ref })) + }, + { + pseudo: stat('+# total to Dexterity'), + group: 'to_x_attr', + stats: + ATTRIBUTES_INFO.filter(info => info.attributes.includes('dex')) + .map(info => ({ ref: info.ref })) + }, + { + pseudo: stat('+# total to Intelligence'), + group: 'to_x_attr', + stats: + ATTRIBUTES_INFO.filter(info => info.attributes.includes('int')) + .map(info => ({ ref: info.ref })) + }, + { + pseudo: stat('+# total maximum Life'), + disabled: false, stats: [ - stat('+# to maximum Mana') + { ref: stat('+# to maximum Life'), required: true }, + ...ATTRIBUTES_INFO.filter(info => info.attributes.includes('str')) + .map(info => ({ ref: info.ref, multiplier: 5 / 10 })) ] }, { - pseudo: pseudoStat('#% total increased maximum Energy Shield'), + pseudo: stat('+# total maximum Mana'), stats: [ - stat('#% increased maximum Energy Shield') + { ref: stat('+# to maximum Mana'), required: true }, + ...ATTRIBUTES_INFO.filter(info => info.attributes.includes('int')) + .map(info => ({ ref: info.ref, multiplier: 5 / 10 })) ] }, { - pseudo: pseudoStat('+# total maximum Energy Shield'), + pseudo: stat('#% total increased maximum Energy Shield'), stats: [ - stat('+# to maximum Energy Shield') // global + { ref: stat('#% increased maximum Energy Shield') } ] }, { - pseudo: pseudoStat('+#% total Attack Speed'), + pseudo: stat('+# total maximum Energy Shield'), stats: [ - stat('#% increased Attack Speed') // global - // stat('#% increased Attack and Cast Speed') + { ref: stat('+# to maximum Energy Shield') } // global ] }, { - pseudo: pseudoStat('+#% total Cast Speed'), + pseudo: stat('+#% total Attack Speed'), stats: [ - stat('#% increased Cast Speed') - // stat('#% increased Attack and Cast Speed') + { ref: stat('#% increased Attack Speed') } // global + // { ref: stat('#% increased Attack and Cast Speed') } ] }, { - pseudo: pseudoStat('#% total increased Physical Damage'), + pseudo: stat('+#% total Cast Speed'), stats: [ - stat('#% increased Physical Damage') + { ref: stat('#% increased Cast Speed') } + // { ref: stat('#% increased Attack and Cast Speed') } ] }, { - pseudo: pseudoStat('+#% total Critical Strike Chance for Spells'), + pseudo: stat('#% increased Movement Speed'), stats: [ - stat('#% increased Critical Strike Chance for Spells'), - stat('#% increased Global Critical Strike Chance') + { ref: stat('#% increased Movement Speed') } ] }, { - pseudo: pseudoStat('#% increased Movement Speed'), + pseudo: stat('#% total increased Physical Damage'), stats: [ - stat('#% increased Movement Speed') + { ref: stat('#% increased Global Physical Damage') } ] }, { - pseudo: pseudoStat('+#% Global Critical Strike Chance'), + pseudo: stat('+#% Global Critical Strike Chance'), + group: 'global_crit_chance', stats: [ - stat('#% increased Global Critical Strike Chance') + { ref: stat('#% increased Global Critical Strike Chance') } ] }, { - pseudo: pseudoStat('+#% Global Critical Strike Multiplier'), + pseudo: stat('+#% total Critical Strike Chance for Spells'), + replaces: 'global_crit_chance', stats: [ - stat('+#% to Global Critical Strike Multiplier') + { ref: stat('#% increased Critical Strike Chance for Spells'), required: true }, + { ref: stat('#% increased Global Critical Strike Chance') } ] }, { - pseudo: pseudoStat('#% increased Lightning Damage'), + pseudo: stat('+#% Global Critical Strike Multiplier'), stats: [ - stat('#% increased Lightning Damage'), - stat('#% increased Elemental Damage') + { ref: stat('+#% to Global Critical Strike Multiplier') } ] }, { - pseudo: pseudoStat('#% increased Cold Damage'), + pseudo: stat('#% increased Elemental Damage'), + group: 'incr_ele_dmg', stats: [ - stat('#% increased Cold Damage'), - stat('#% increased Elemental Damage') + { ref: stat('#% increased Elemental Damage') } ] }, { - pseudo: pseudoStat('#% increased Fire Damage'), + pseudo: stat('#% increased Lightning Damage'), + replaces: 'incr_ele_dmg', stats: [ - stat('#% increased Fire Damage'), - stat('#% increased Elemental Damage') + { ref: stat('#% increased Lightning Damage'), required: true }, + { ref: stat('#% increased Elemental Damage') } ] }, { - pseudo: pseudoStat('#% increased Elemental Damage with Attack Skills'), + pseudo: stat('#% increased Cold Damage'), + replaces: 'incr_ele_dmg', stats: [ - stat('#% increased Elemental Damage with Attack Skills') + { ref: stat('#% increased Cold Damage'), required: true }, + { ref: stat('#% increased Elemental Damage') } ] }, { - pseudo: pseudoStat('#% increased Spell Damage'), + pseudo: stat('#% increased Fire Damage'), + group: 'incr_fire_dmg', + replaces: 'incr_ele_dmg', stats: [ - stat('#% increased Spell Damage') + { ref: stat('#% increased Fire Damage'), required: true }, + { ref: stat('#% increased Elemental Damage') } ] }, { - pseudo: pseudoStat('#% increased Burning Damage'), + pseudo: stat('#% increased Spell Damage'), + group: 'incr_spell_dmg', stats: [ - stat('#% increased Burning Damage') + { ref: stat('#% increased Spell Damage') } ] }, { - pseudo: pseudoStat('# Life Regenerated per Second'), + pseudo: stat('#% increased Lightning Spell Damage'), + replaces: 'incr_spell_dmg', stats: [ - stat('Regenerate # Life per second') + { ref: stat('#% increased Lightning Spell Damage'), required: true }, + { ref: stat('#% increased Spell Damage') } ] }, { - pseudo: pseudoStat('#% of Life Regenerated per Second'), + pseudo: stat('#% increased Cold Spell Damage'), + replaces: 'incr_spell_dmg', stats: [ - stat('Regenerate #% of Life per second') + { ref: stat('#% increased Cold Spell Damage'), required: true }, + { ref: stat('#% increased Spell Damage') } ] }, { - pseudo: pseudoStat('#% of Physical Attack Damage Leeched as Life'), + pseudo: stat('#% increased Fire Spell Damage'), + replaces: 'incr_spell_dmg', stats: [ - stat('#% of Physical Attack Damage Leeched as Life') + { ref: stat('#% increased Fire Spell Damage'), required: true }, + { ref: stat('#% increased Spell Damage') } ] }, { - pseudo: pseudoStat('#% of Physical Attack Damage Leeched as Mana'), + pseudo: stat('#% increased Elemental Damage with Attack Skills'), + replaces: 'incr_ele_dmg', stats: [ - stat('#% of Physical Attack Damage Leeched as Mana') + { ref: stat('#% increased Elemental Damage with Attack Skills'), required: true }, + { ref: stat('#% increased Elemental Damage') } ] }, { - pseudo: pseudoStat('#% increased Mana Regeneration Rate'), + pseudo: stat('#% increased Burning Damage'), + replaces: 'incr_fire_dmg', stats: [ - stat('#% increased Mana Regeneration Rate') + { ref: stat('#% increased Burning Damage'), required: true }, + { ref: stat('#% increased Fire Damage') }, + { ref: stat('#% increased Elemental Damage') } + ] + }, + { + pseudo: stat('# Life Regenerated per Second'), + stats: [ + { ref: stat('Regenerate # Life per second') } + ] + }, + { + pseudo: stat('#% of Life Regenerated per Second'), + stats: [ + { ref: stat('Regenerate #% of Life per second') } + ] + }, + { + pseudo: stat('#% of Physical Attack Damage Leeched as Life'), + stats: [ + { ref: stat('#% of Physical Attack Damage Leeched as Life') } + ] + }, + { + pseudo: stat('#% of Physical Attack Damage Leeched as Mana'), + stats: [ + { ref: stat('#% of Physical Attack Damage Leeched as Mana') } + ] + }, + { + pseudo: stat('#% increased Mana Regeneration Rate'), + stats: [ + { ref: stat('#% increased Mana Regeneration Rate') } ] } ] + +export function filterPseudo (ctx: FiltersCreationContext) { + const filterByGroup = new Map() + + rulesLoop: + for (const rule of PSEUDO_RULES) { + const sources = filterPseudoSources(ctx.statsByType, ({ stat }, source) => { + const info = rule.stats.find(info => info.ref === stat.ref) + if (!info) return null + + const multi = info.multiplier ?? 1 + return { + ...source, + contributes: { + value: source.contributes!.value * multi, + min: source.contributes!.min * multi, + max: source.contributes!.max * multi + } + } + }) + if (!sources.length) continue + + if (rule.stats.some(s => s.required)) { + for (const stat of rule.stats) { + if (stat.required && !sources.some(source => + source.stat.stat.ref === stat.ref)) { + continue rulesLoop + } + } + } + + const total = statSourcesTotal(sources)! + + const filter = { + ...pseudoStat(rule.pseudo), + disabled: rule.disabled ?? true, + sources: sources, + roll: rollToFilter(total.value, { + neverNegated: true, + // TODO: is this correct/can be improved + dp: rule.stats.some(({ ref }) => STAT_BY_REF.get(ref)!.dp), + percent: ctx.searchInRange + }) + } + + if (rule.mutate) { + rule.mutate(filter) + } + + ctx.filters.push(filter) + + if (rule.replaces && filterByGroup.has(rule.replaces)) { + const replacedFilters = filterByGroup.get(rule.replaces)! + ctx.filters = ctx.filters.filter(filter => !replacedFilters.includes(filter)) + } + + if (rule.group) { + if (filterByGroup.has(rule.group)) { + filterByGroup.get(rule.group)!.push(filter) + } else { + filterByGroup.set(rule.group, [filter]) + } + } + } + + ctx.statsByType = ctx.statsByType.filter(m => + !PSEUDO_RULES.some(rule => rule.stats.some(({ ref }) => m.stat.ref === ref))) + + if (filterByGroup.has('to_x_ele_res')) { + const resFilters = filterByGroup.get('to_x_ele_res')! + + resFilters.sort((a, b) => b.roll!.value - a.roll!.value) + const maxFilter = (resFilters[0]?.roll?.value === resFilters[1]?.roll?.value) + ? undefined + : resFilters[0] + + if (maxFilter) { + maxFilter.hidden = 'Filtering by exact Elemental Resistance unreasonably increases the price' + } + + ctx.filters = ctx.filters.filter(filter => !resFilters.includes(filter) || filter === maxFilter) + } + + if (filterByGroup.has('to_x_attr')) { + const attrFilters = filterByGroup.get('to_x_attr')! + attrFilters.sort((a, b) => b.roll!.value - a.roll!.value) + if (attrFilters.length === 3) { + if (attrFilters.every(f => f.roll!.value === attrFilters[0].roll!.value)) { + // Here `to_all_attrs` filter may or may not (very rare event) exist. + // Anyway we are removing all attribute filters. + ctx.filters = ctx.filters.filter(filter => !attrFilters.includes(filter)) + } else { + if (filterByGroup.has('to_all_attrs')) { + const toAll = filterByGroup.get('to_all_attrs')! + ctx.filters = ctx.filters.filter(filter => !toAll.includes(filter)) + } + + if (attrFilters[1].roll!.value === attrFilters[2].roll!.value) { + ctx.filters = ctx.filters.filter(filter => filter !== attrFilters[1] && filter !== attrFilters[2]) + } else { + ctx.filters = ctx.filters.filter(filter => filter !== attrFilters[2]) + } + } + } + } +} diff --git a/src/web/price-check/filters/pseudo/resistances.ts b/src/web/price-check/filters/pseudo/resistances.ts deleted file mode 100644 index 747fb138..00000000 --- a/src/web/price-check/filters/pseudo/resistances.ts +++ /dev/null @@ -1,131 +0,0 @@ -import { pseudoStat, sumPseudoStats } from './util' -import { FiltersCreationContext } from '../create-stat-filters' -import { rollToFilter } from '../util' -import { stat } from '@/assets/data' -import { ModifierType } from '@/parser/modifiers' -import { FilterTag } from '../interfaces' - -const TO_ALL_RES = stat('+#% to all Elemental Resistances') - -const ELEMENTAL_RES = [ - { - pseudo: pseudoStat('+#% total to Fire Resistance'), - stats: [ - stat('+#% to Fire Resistance'), - stat('+#% to Fire and Lightning Resistances'), - stat('+#% to Fire and Cold Resistances'), - stat('+#% to Fire and Chaos Resistances'), - TO_ALL_RES - ] - }, - { - pseudo: pseudoStat('+#% total to Cold Resistance'), - stats: [ - stat('+#% to Cold Resistance'), - stat('+#% to Fire and Cold Resistances'), - stat('+#% to Cold and Lightning Resistances'), - stat('+#% to Cold and Chaos Resistances'), - TO_ALL_RES - ] - }, - { - pseudo: pseudoStat('+#% total to Lightning Resistance'), - stats: [ - stat('+#% to Lightning Resistance'), - stat('+#% to Fire and Lightning Resistances'), - stat('+#% to Cold and Lightning Resistances'), - stat('+#% to Lightning and Chaos Resistances'), - TO_ALL_RES - ] - } -] - -const CHAOS_RES = { - pseudo: pseudoStat('+#% total to Chaos Resistance'), - stats: [ - stat('+#% to Chaos Resistance'), - stat('+#% to Fire and Chaos Resistances'), - stat('+#% to Cold and Chaos Resistances'), - stat('+#% to Lightning and Chaos Resistances') - ] -} - -export function filterResists (ctx: FiltersCreationContext) { - const resists: Array<{ pseudo: ReturnType, total: number, hasFlat: boolean }> = [] - - for (const eleRes of ELEMENTAL_RES) { - const hasFlat = ctx.statsByType.some(m => - eleRes.stats.includes(m.stat.ref) && m.stat.ref !== TO_ALL_RES) - - const total = sumPseudoStats(ctx.statsByType, eleRes.stats) - if (total !== undefined) { - resists.push({ pseudo: eleRes.pseudo, total, hasFlat }) - } - } - - const totalRes = resists.reduce((a, b) => a + b.total, 0) - - if (resists.length > 0) { - ctx.filters.push({ - ...pseudoStat('+#% total Elemental Resistance'), - disabled: (resists.length === 1), - sources: [], - roll: rollToFilter(totalRes, { neverNegated: true, percent: ctx.searchInRange }) - }) - - const maxRes = Math.max(...resists.map(r => r.total)) - - if ((maxRes / totalRes > 0.67) || resists.filter(r => r.hasFlat).length === 1) { - ctx.filters.push({ - text: '+#% total to one of Elemental Resistances', - statRef: '+#% total to one of Elemental Resistances', - tradeId: ELEMENTAL_RES.flatMap(r => r.pseudo.tradeId), - tag: FilterTag.Pseudo, - disabled: false, - sources: [], - roll: rollToFilter(maxRes, { neverNegated: true, percent: ctx.searchInRange }) - }) - } - } - - if (resists.length === 3) { - const totalToAllRes = Math.min(...resists.map(r => r.total)) - - if ((totalToAllRes * resists.length) / totalRes > 0.8) { - ctx.filters.push({ - ...pseudoStat('+#% total to all Elemental Resistances'), - disabled: false, - sources: [], - roll: rollToFilter(totalToAllRes, { neverNegated: true, percent: ctx.searchInRange }) - }) - } - } - - for (const resist of resists) { - ctx.filters.push({ - ...resist.pseudo, - disabled: true, - hidden: 'Filtering by exact Elemental Resistance unreasonably increases the price', - sources: [], - roll: rollToFilter(resist.total, { neverNegated: true, percent: ctx.searchInRange }) - }) - } - - const chaosTotal = sumPseudoStats(ctx.statsByType, CHAOS_RES.stats) - if (chaosTotal != null) { - const hasNotCrafted = ctx.statsByType.some(mod => - mod.type !== ModifierType.Crafted && - CHAOS_RES.stats.includes(mod.stat.ref)) - - ctx.filters.push({ - ...CHAOS_RES.pseudo, - disabled: true, // NOTE: unlike EleRes it is disabled - hidden: hasNotCrafted ? undefined : 'Crafted Chaos Resistance without Explicit mod has no value', - sources: [], - roll: rollToFilter(chaosTotal, { neverNegated: true, percent: ctx.searchInRange }) - }) - } - - const statsToRemove = new Set([...ELEMENTAL_RES.flatMap(r => r.stats), ...CHAOS_RES.stats]) - ctx.statsByType = ctx.statsByType.filter(m => !statsToRemove.has(m.stat.ref)) -} diff --git a/src/web/price-check/filters/pseudo/util.ts b/src/web/price-check/filters/pseudo/util.ts index 451fcbfb..3b9babb5 100644 --- a/src/web/price-check/filters/pseudo/util.ts +++ b/src/web/price-check/filters/pseudo/util.ts @@ -1,5 +1,5 @@ import { STAT_BY_REF } from '@/assets/data' -import { StatCalculated, ModifierType } from '@/parser/modifiers' +import { StatCalculated, ModifierType, StatSource } from '@/parser/modifiers' import { FilterTag, InternalTradeId } from '../interfaces' export function pseudoStat (ref: string) { @@ -30,3 +30,19 @@ export function sumPseudoStats (stats: StatCalculated[], pseudoRefs: string[]) { : total, undefined) } + +export function filterPseudoSources ( + stats: StatCalculated[], + mapFn: (calc: StatCalculated, source: StatSource) => StatSource | null +): StatSource[] { + const out: StatSource[] = [] + for (const calc of stats) { + for (const source of calc.sources) { + const result = mapFn(calc, source) + if (result) { + out.push(result) + } + } + } + return out +}