From 1d32c4874fd159e82d58ea5bc48aad00d63bdb88 Mon Sep 17 00:00:00 2001 From: kvan7 Date: Thu, 3 Sep 2026 20:05:05 -0500 Subject: [PATCH] update editor and extraction value --- renderer/public/images/extractor.png | Bin 0 -> 9903 bytes renderer/specs/Parser/augment.test.ts | 257 ++++++++++++++++-- renderer/specs/Parser/items.ts | 78 ++++++ renderer/specs/helper.ts | 67 +++++ renderer/src/parser/Parser.ts | 93 ++++++- renderer/src/parser/utils.ts | 6 + renderer/src/web/background/Prices.ts | 76 ++++++ .../web/price-check/trade/ExtractionValue.vue | 116 ++++++++ .../web/price-check/trade/TradeListing.vue | 5 + renderer/src/web/ui/ItemQuickPrice.vue | 19 +- 10 files changed, 670 insertions(+), 47 deletions(-) create mode 100644 renderer/public/images/extractor.png create mode 100644 renderer/src/parser/utils.ts create mode 100644 renderer/src/web/price-check/trade/ExtractionValue.vue diff --git a/renderer/public/images/extractor.png b/renderer/public/images/extractor.png new file mode 100644 index 0000000000000000000000000000000000000000..1154c4c0e77440e11ad2c204ce2338eb6bf3b80a GIT binary patch literal 9903 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 { beforeEach(async () => { setupTests(); @@ -46,7 +22,7 @@ describe("determineAugments", () => { type: ModifierType.Augment, }, } as unknown as ParsedModifier, - makeCalcStat("#% increased Physical Damage", 36), + [makeCalcStat("#% increased Physical Damage", 36, ModifierType.Augment)], ); expect(result.map((augment) => augment.refName)).toEqual(expectedRef); @@ -61,7 +37,7 @@ describe("determineAugments", () => { type: ModifierType.Augment, }, } as unknown as ParsedModifier, - makeCalcStat("#% to Lightning Resistance", 10), + [makeCalcStat("#% to Lightning Resistance", 10, ModifierType.Augment)], ); expect(result.map((augment) => augment.refName)).toEqual(expectedRef); @@ -98,3 +74,224 @@ describe("BFS", () => { }, ); }); + +describe("parseAugmentSockets", () => { + beforeEach(async () => { + setupTests(); + await init("en"); + }); + + it("should skip for items without sockets", () => { + const item = createTestItem(); + + const result = __testExports.parseAugmentSockets([], item); + expect(result).toEqual("PARSER_SKIPPED"); + }); + + it.each([ + [["Sockets: S"], 1], + [["Sockets: S S"], 2], + [["Sockets: S S S"], 3], + [["Sockets: S S S S"], 4], + [["Sockets: S S S S S"], 5], + ])( + "%#. should add correct augment socket count(%o -> %o)", + (lines, expected) => { + const item = createTestItem(); + item.category = ItemCategory.Gloves; + + const result = __testExports.parseAugmentSockets(lines, item); + expect(result).toEqual("SECTION_PARSED"); + expect(item.augmentSockets?.normal).toEqual(1); + expect(item.augmentSockets?.empty).toEqual(0); + expect(item.augmentSockets?.current).toEqual(expected); + expect( + item.augmentSockets?.augments.every((v) => v === null), + ).toBeTruthy(); + expect(item.augmentSockets?.augments.length).toEqual(expected); + }, + ); + + it("should add sockets on non-socket section", () => { + const item = createTestItem(); + item.category = ItemCategory.Gloves; + + const result = __testExports.parseAugmentSockets(["text"], item); + expect(result).toEqual("SECTION_SKIPPED"); + expect(item.augmentSockets?.normal).toEqual(1); + expect(item.augmentSockets?.empty).toEqual(1); + expect(item.augmentSockets?.current).toEqual(0); + expect(item.augmentSockets?.augments.every((v) => v === null)).toBeTruthy(); + expect(item.augmentSockets?.augments.length).toEqual(1); + }); +}); + +describe("applyAugmentSockets", () => { + beforeEach(async () => { + setupTests(); + await init("en"); + }); + + it("should do nothing on item without sockets", () => { + const item = createTestItem(); + + __testExports.applyAugmentSockets(item); + expect(item).toEqual(createTestItem()); + }); + + it("should set empty on item with no augment stats", () => { + const item = createTestItem(); + item.category = ItemCategory.Gloves; + item.augmentSockets = { + empty: 0, + current: 2, + normal: 2, + augments: [null, null], + }; + + __testExports.applyAugmentSockets(item); + expect(item.augmentSockets.empty).toEqual(2); + expect(item.augmentSockets.current).toEqual(2); + expect(item.augmentSockets.normal).toEqual(2); + expect(item.augmentSockets.augments.every((v) => v === null)).toBeTruthy(); + }); + + it("should apply correct augment to item", () => { + const item = createTestItem(); + item.category = ItemCategory.Bow; + item.augmentSockets = { + empty: 0, + current: 2, + normal: 2, + augments: [null, null], + }; + + const stat = makeCalcStat( + "#% increased Physical Damage", + 18, + ModifierType.Augment, + ); + item.statsByType = [stat]; + item.newMods = [ + { + info: { + type: ModifierType.Augment, + tags: [], + }, + stats: [stat.sources[0].stat], + }, + ]; + + __testExports.applyAugmentSockets(item); + expect(item.augmentSockets.empty).toEqual(1); + expect(item.augmentSockets.current).toEqual(2); + expect(item.augmentSockets.normal).toEqual(2); + expect(item.augmentSockets.augments[0]?.refName).toEqual( + "Greater Iron Rune", + ); + expect(item.augmentSockets.augments[1]).toBeNull(); + }); + + it("should apply multiple different augments to item", () => { + const item = createTestItem(); + item.category = ItemCategory.Bow; + item.augmentSockets = { + empty: 0, + current: 2, + normal: 2, + augments: [null, null], + }; + + const stat1 = makeCalcStat( + "#% increased Physical Damage", + 18, + ModifierType.Augment, + ); + const stat2 = makeCalcStat( + "Bow Attacks fire # additional Arrows", + 1, + ModifierType.Augment, + ); + item.statsByType = [stat1, stat2]; + item.newMods = [ + { + info: { + type: ModifierType.Augment, + tags: [], + }, + stats: [stat1.sources[0].stat, stat2.sources[0].stat], + }, + ]; + + __testExports.applyAugmentSockets(item); + expect(item.augmentSockets.empty).toEqual(0); + expect(item.augmentSockets.current).toEqual(2); + expect(item.augmentSockets.normal).toEqual(2); + expect( + item.augmentSockets.augments.some( + (i) => i?.refName === "Greater Iron Rune", + ), + ).toBeTruthy(); + expect( + item.augmentSockets.augments.some( + (i) => i?.refName === "Countess Seske's Rune of Archery", + ), + ).toBeTruthy(); + }); + + it("should join multiline augments", () => { + const item = createTestItem(); + item.category = ItemCategory.Staff; + item.augmentSockets = { + empty: 0, + current: 2, + normal: 2, + augments: [null, null], + }; + + const stat1 = makeCalcStat( + "Meta Skills gain #% increased Energy", + 40, + ModifierType.Augment, + ); + const stat2 = makeCalcStat( + "#% increased Spirit", + -25, + ModifierType.Augment, + ); + const stat3 = makeCalcStat( + "#% increased Mana Regeneration Rate", + 20, + ModifierType.Augment, + ); + item.statsByType = [stat1, stat2, stat3]; + item.newMods = [ + { + info: { + type: ModifierType.Augment, + tags: [], + }, + stats: [ + stat1.sources[0].stat, + stat2.sources[0].stat, + stat3.sources[0].stat, + ], + }, + ]; + + __testExports.applyAugmentSockets(item); + expect(item.augmentSockets.empty).toEqual(0); + expect(item.augmentSockets.current).toEqual(2); + expect(item.augmentSockets.normal).toEqual(2); + expect( + item.augmentSockets.augments.some( + (i) => i?.refName === "Idol of the Martyr", + ), + ).toBeTruthy(); + expect( + item.augmentSockets.augments.some( + (i) => i?.refName === "Lesser Inspiration Rune", + ), + ).toBeTruthy(); + }); +}); diff --git a/renderer/specs/Parser/items.ts b/renderer/specs/Parser/items.ts index 52efc9f7..380e7f6e 100644 --- a/renderer/specs/Parser/items.ts +++ b/renderer/specs/Parser/items.ts @@ -1333,3 +1333,81 @@ CharmQuality.prefixCount = 1; CharmQuality.suffixCount = 1; // #endregion CharmQuality + +// #region BowThreeAugments +export const BowThreeAugments = new TestItem(`Item Class: Bows +Rarity: Rare +Oblivion Branch +Obliterator Bow +-------- +Quality: +20% (augmented) +Physical Damage: 288-534 (augmented) +Fire Damage: 47-92 (fire) +Critical Hit Chance: 8.45% (augmented) +Attacks per Second: 1.27 (augmented) +-------- +Requires: Level 78, 163 Dex +-------- +Sockets: S S S +-------- +Item Level: 80 +-------- +38% increased Physical Damage (rune) +Bow Attacks fire an additional Arrow (rune) +-------- +{ Implicit Modifier } +50% reduced Projectile Range +-------- +{ Prefix Modifier "Cruel" (Tier: 3) — Damage, Physical, Attack } +151(135-154)% increased Physical Damage +{ Desecrated Prefix Modifier "Razor-sharp" (Tier: 3) — Damage, Physical, Attack } +Adds 21(16-24) to 39(28-42) Physical Damage +{ Prefix Modifier "Blasting" (Tier: 3) — Damage, Elemental, Fire, Attack } +Adds 47(47-59) to 92(74-97) Fire Damage +{ Suffix Modifier "of Renown" (Tier: 2) — Attack, Speed } +15(14-16)% increased Attack Speed +{ Suffix Modifier "of the Vampire" (Tier: 1) — Life, Physical, Attack } +Leeches 9.6(9-9.9)% of Physical Damage as Life +{ Crafted Suffix Modifier "of Calamity" (Tier: 3) — Attack, Critical } ++3.45(3.11-3.8)% to Critical Hit Chance +-------- +Corrupted +`); + +BowThreeAugments.category = ItemCategory.Bow; +BowThreeAugments.rarity = ItemRarity.Rare; +BowThreeAugments.itemLevel = 80; +BowThreeAugments.quality = 20; +BowThreeAugments.weaponPHYSICAL = 411; +BowThreeAugments.weaponFIRE = 69.5; +BowThreeAugments.weaponELEMENTAL = BowThreeAugments.weaponFIRE; +BowThreeAugments.weaponCRIT = 8.45; +BowThreeAugments.weaponAS = 1.27; +BowThreeAugments.requires = { + level: 78, + str: 0, + dex: 163, + int: 0, +}; + +BowThreeAugments.info.refName = "Obliterator Bow"; +BowThreeAugments.sectionCount = 9; +BowThreeAugments.prefixCount = 3; +BowThreeAugments.suffixCount = 3; +BowThreeAugments.implicitCount = 1; +BowThreeAugments.augmentSockets = { + empty: 0, + current: 3, + normal: 2, + augments: [ + createEditorItem("Perfect Iron Rune", "20% increased Physical Damage", 20), + createEditorItem("Greater Iron Rune", "18% increased Physical Damage", 18), + createEditorItem( + "Countess Seske's Rune of Archery", + "Bow Attacks fire an additional Arrow", + 1, + ), + ], +}; + +// #endregion BowThreeAugments diff --git a/renderer/specs/helper.ts b/renderer/specs/helper.ts index 6bf7a557..d5973b6a 100644 --- a/renderer/specs/helper.ts +++ b/renderer/specs/helper.ts @@ -1,4 +1,9 @@ +import { STAT_BY_REF } from "@/assets/data"; +import { ItemCategory } from "@/parser"; +import { ParsedModifier } from "@/parser/advanced-mod-desc"; +import { ModifierType, StatCalculated } from "@/parser/modifiers"; import { createVirtualItem, ParsedItem } from "@/parser/ParsedItem"; +import { ParsedStat } from "@/parser/stat-translations"; import { FilterTag, StatFilter } from "@/web/price-check/filters/interfaces"; export function createTestStatFilter(): StatFilter { @@ -43,9 +48,71 @@ export function createTestItem(): ParsedItem { info: { refName: "test", namespace: "ITEM", + craftable: { + category: ItemCategory.Unknown, + }, name: "", icon: "", tags: [], }, }; } + +export function createParsedStat( + statRef: string, + value: number | [number, number], +): ParsedStat { + const stat = STAT_BY_REF(statRef)!; + + return { + stat, + translation: stat.matchers[0], + roll: { + unscalable: false, + dp: stat.dp || false, + value: Array.isArray(value) ? (value[0] + value[1]) / 2 : value, + min: Array.isArray(value) ? value[0] : value, + max: Array.isArray(value) ? value[1] : value, + }, + }; +} + +export function createParsedModifier( + statRef: string, + value: number | [number, number], + type: ModifierType = ModifierType.Explicit, +): ParsedModifier { + return { + info: { + type, + tags: [], + }, + stats: [createParsedStat(statRef, value)], + }; +} + +export function makeCalcStat( + ref: string, + value: number, + type: ModifierType = ModifierType.Explicit, +): StatCalculated { + const parsedStat = createParsedStat(ref, value); + + return { + stat: parsedStat.stat, + type, + sources: [ + { + contributes: parsedStat.roll, + modifier: { + info: { + type, + tags: [], + }, + stats: [parsedStat], + }, + stat: parsedStat, + }, + ], + }; +} diff --git a/renderer/src/parser/Parser.ts b/renderer/src/parser/Parser.ts index 573eab0b..64ebd20b 100644 --- a/renderer/src/parser/Parser.ts +++ b/renderer/src/parser/Parser.ts @@ -44,6 +44,7 @@ import { calcPropPercentile, QUALITY_STATS } from "./calc-q20"; import { AppConfig } from "@/web/Config"; import { buildEditorItems, getSavedAugments } from "./augment-builder"; import { useAugment } from "@/web/price-check/item-editor/augment"; +import { combinations } from "./utils"; type SectionParseResult = | "SECTION_PARSED" @@ -855,14 +856,16 @@ function parseAugmentSockets(section: string[], item: ParsedItem) { empty: 0, current, normal: categoryMax, - augments: Array(categoryMax).fill(null), + augments: Array(current).fill(null), }; } else { item.augmentSockets = { empty: 0, current, normal: categoryMax, - augments: Array(categoryMax).fill(null), + augments: Array(categoryMax > current ? categoryMax : current).fill( + null, + ), }; } @@ -1322,23 +1325,44 @@ function applyAugmentSockets(item: ParsedItem) { const augmentMods = item.newMods.filter( (mod) => mod.info.type === ModifierType.Augment, ); + console.log(augmentMods); const augmentStats = item.statsByType.filter( (calc) => calc.type === ModifierType.Augment, ); - const augments: Array = augmentMods - .map((mod) => { - const stat = augmentStats.find( - (stat) => stat.sources[0].stat === mod.stats[0], - ); - if (!stat) return []; + console.log(augmentStats); + + let statCombinations = combinations(augmentStats) + .filter((f) => f.length) + .toSorted((a, b) => b.length - a.length); + const augs: BaseType[][] = []; + for (let i = 0; i < statCombinations.length; ) { + const foundAugs = determineAugments(augmentMods[0], statCombinations[i]); + if (foundAugs.length) { + augs.push(foundAugs); + for (const combStat of statCombinations[i]) { + // drop any that we use here + statCombinations = statCombinations.filter( + (f) => f.find((s) => s === combStat) === undefined, + ); + } + i = 0; + continue; + } + i++; + } + + const augments: Array = augs + .map((augGroup) => { return buildEditorItems( - determineAugments(mod, stat), + augGroup, item.category ?? ItemCategory.Unknown, true, ); }) .flat(); + console.log(augments); + if ( augments.length < Math.max(item.augmentSockets.current, item.augmentSockets.normal) @@ -2133,21 +2157,58 @@ function modifiedBfs( function determineAugments( mod: ParsedModifier, - statCalc: StatCalculated, + statCalcs: StatCalculated[], ): BaseType[] { if (mod.info.type !== ModifierType.Augment) return []; - const augmentAppliedValue = statCalc.sources[0].contributes?.value; + const augmentAppliedValue = statCalcs + .map((s) => s.sources[0].contributes?.value) + .filter((v) => v !== undefined); - const augmentTradeId = statCalc.stat.trade.ids[ModifierType.Augment][0]; - const possibleAugments = AUGMENT_DATA_BY_TRADE_ID[augmentTradeId]; + // const augmentTradeId = statCalcs[0].stat.trade.ids[ModifierType.Augment][0]; + // const possibleAugments = AUGMENT_DATA_BY_TRADE_ID[augmentTradeId]; + + const allTradeIds = statCalcs.map( + (c) => c.stat.trade.ids[ModifierType.Augment][0], + ); + const allPossibleAugments = allTradeIds.map( + (id) => AUGMENT_DATA_BY_TRADE_ID[id], + ); + const augmentRefSets = allPossibleAugments.map( + (augGroup) => new Set(augGroup.map((a) => a.refName)), + ); + console.log(augmentRefSets); + + const morePossibleAugments = augmentRefSets.reduce((acc, set) => { + return set.intersection(acc); + }, augmentRefSets[0]); + console.log("morePossibleAugments", morePossibleAugments); + + // intersect all possible augments + const possibleAugments = allPossibleAugments + .map((augGroup) => + augGroup.filter((aug) => morePossibleAugments.has(aug.refName)), + ) + .find((f) => f.length); + + console.log(possibleAugments); if (!possibleAugments) return []; // something like "Raven-Touched" - if (!augmentAppliedValue) { + if (!augmentAppliedValue || !augmentAppliedValue.length) { const singleAugment = possibleAugments[0]; return [ITEM_BY_REF("ITEM", singleAugment.refName)![0]]; } + if (augmentAppliedValue.length > 1) { + const likelyAugment = possibleAugments.find( + (aug) => + new Set(aug.values).intersection(new Set(augmentAppliedValue)).size === + augmentAppliedValue.length, + ); + if (!likelyAugment) return []; + return [ITEM_BY_REF("ITEM", likelyAugment.refName)![0]]; + } + // // Calculate how many of this augment are in the item const availableAugmentValues = possibleAugments .map((augment) => { @@ -2166,7 +2227,7 @@ function determineAugments( // BFS to find all combinations with minimum count const likelyValues = - modifiedBfs(augmentAppliedValue, [], availableAugmentValues) ?? []; + modifiedBfs(augmentAppliedValue[0], [], availableAugmentValues) ?? []; return likelyValues.map((v) => { const augment = possibleAugments.find((aug) => @@ -2206,4 +2267,6 @@ export const __testExports = { parseTrials, determineAugments, modifiedBfs, + parseAugmentSockets, + applyAugmentSockets, }; diff --git a/renderer/src/parser/utils.ts b/renderer/src/parser/utils.ts new file mode 100644 index 00000000..696c0f8d --- /dev/null +++ b/renderer/src/parser/utils.ts @@ -0,0 +1,6 @@ +export function combinations(arr: T[]): T[][] { + return arr.reduce( + (acc, curr) => [...acc, ...acc.map((a) => [...a, curr])], + [[]], + ); +} diff --git a/renderer/src/web/background/Prices.ts b/renderer/src/web/background/Prices.ts index 1bb7a641..87948af2 100644 --- a/renderer/src/web/background/Prices.ts +++ b/renderer/src/web/background/Prices.ts @@ -356,6 +356,78 @@ export const usePoeninja = createGlobalState(() => { return currency; } + function toDivine(value: CurrencyValue): CurrencyValue { + if (value.currency === "div") return value; + if (xchgRate.value) { + if (xchgRateCurrency.value !== value.currency) { + throw new Error( + `Cannot convert ${value.currency} to div, current core currency is ${xchgRateCurrency.value}`, + ); + } + + return { + min: value.min / xchgRate.value, + max: value.max / xchgRate.value, + currency: "div", + }; + } + return value; + } + + function addPrice( + addend1: CurrencyValue, + addend2: CurrencyValue, + ): CurrencyValue { + const addend1Div = toDivine(addend1); + const addend2Div = toDivine(addend2); + + return autoCurrency([ + addend1Div.min + addend2Div.min, + addend1Div.max + addend2Div.max, + ]); + } + + function subtractPrice( + minuend: CurrencyValue, + subtrahend: CurrencyValue, + ): CurrencyValue { + const minuendDiv = toDivine(minuend); + const subtrahendDiv = toDivine(subtrahend); + + return autoCurrency([ + minuendDiv.min - subtrahendDiv.max, + minuendDiv.max - subtrahendDiv.min, + ]); + } + + function comparePrice( + left: CurrencyValue, + op: ">" | ">=" | "==" | "===" | "<=" | "<", + right: CurrencyValue, + ): boolean { + const leftDiv = toDivine(left); + const rightDiv = toDivine(right); + + switch (op) { + case ">": + return leftDiv.min > rightDiv.max; + case ">=": + return leftDiv.min >= rightDiv.max; + case "==": + return leftDiv.min === rightDiv.max && leftDiv.max === rightDiv.min; + case "===": + return ( + left.currency === right.currency && + leftDiv.min === rightDiv.max && + leftDiv.max === rightDiv.min + ); + case "<=": + return leftDiv.max <= rightDiv.min; + case "<": + return leftDiv.max < rightDiv.min; + } + } + setInterval(() => { load(); }, RETRY_INTERVAL_MS); @@ -384,6 +456,10 @@ export const usePoeninja = createGlobalState(() => { initialLoading: () => isLoading.value && !PRICES_DB.length, availableCoreCurrencies: readonly(availableCoreCurrencies), ITEM_DROP, + // math + addPrice, + subtractPrice, + comparePrice, }; }); diff --git a/renderer/src/web/price-check/trade/ExtractionValue.vue b/renderer/src/web/price-check/trade/ExtractionValue.vue new file mode 100644 index 00000000..8ea3536a --- /dev/null +++ b/renderer/src/web/price-check/trade/ExtractionValue.vue @@ -0,0 +1,116 @@ + + diff --git a/renderer/src/web/price-check/trade/TradeListing.vue b/renderer/src/web/price-check/trade/TradeListing.vue index fda4b4a8..bfc16dca 100644 --- a/renderer/src/web/price-check/trade/TradeListing.vue +++ b/renderer/src/web/price-check/trade/TradeListing.vue @@ -92,6 +92,9 @@ + + +
-
+
- +
~ {{ @@ -89,6 +96,14 @@ export default defineComponent({ type: Object as PropType, default: undefined, }, + showImg: { + type: Boolean, + default: true, + }, + showArrow: { + type: Boolean, + default: true, + }, }, setup(props) { const minText = computed(() =>