diff --git a/scripts/mods-all.ts b/scripts/mods-all.ts new file mode 100644 index 00000000..45aba3a0 --- /dev/null +++ b/scripts/mods-all.ts @@ -0,0 +1,79 @@ +import { Mod } from '../src/data' +const fs = require('fs') +const nfetch = require('node-fetch') + +interface ApiStats { + result: Array<{ + label: string, + entries: Array<{ + id: string + text: string + type: string + option?: { + options: Array<{ + id: string | number + text: string + }> + } + }> + }> +} + +const SUPPORTED_MODS = ['pseudo', 'explicit', 'implicit', 'enchant', 'crafted'] + +;(async function () { + const response = await nfetch('https://www.pathofexile.com/api/trade/data/stats') + const { result }: ApiStats = await response.json() + + const dupes = new Set() + const MOD_BY_NAME = new Map() + + for (const _ of result) { + for (const rawMod of _.entries) { + if (SUPPORTED_MODS.includes(rawMod.type)) { + const mod = MOD_BY_NAME.get(rawMod.text) + if (mod) { + if (mod.types.find(type => type.name === rawMod.type)) { + dupes.add(rawMod.text) + // invalidate mod + mod.types.find(type => type.name === rawMod.type)!.tradeId = undefined + } else { + mod.types.push({ name: rawMod.type, tradeId: rawMod.id }) + } + } else { + if (!rawMod.option) { + MOD_BY_NAME.set(rawMod.text, { + text: rawMod.text, + types: [{ + name: rawMod.type, + tradeId: rawMod.id + }] + }) + } else { + for (const option of rawMod.option.options) { + MOD_BY_NAME.set(rawMod.text.replace('#', option.text), { + text: rawMod.text, + types: [{ + name: rawMod.type, + tradeId: rawMod.id + }], + option: { + text: option.text, + tradeId: option.id + } + }) + } + } + } + } + } + } + + if (dupes.size) { + console.warn('Dupe mods found, consider to report to GGG') + console.warn(Array.from(dupes)) + console.warn('Dupe mods found, consider to report to GGG') + } + + fs.writeFileSync('./src/data/mods.json', JSON.stringify(Array.from(MOD_BY_NAME), null, 2)) +})() diff --git a/src/components/Parser.ts b/src/components/Parser.ts index 253f7813..10f2a805 100644 --- a/src/components/Parser.ts +++ b/src/components/Parser.ts @@ -14,7 +14,7 @@ import { PREFIX_SUPERIOR, SUFFIX_INFLUENCE } from './parser-constants' -import { Prophecies, ItemisedMonsters } from '../data' +import { Prophecies, ItemisedMonsters, Mods, Mod } from '../data' import { getDetailsId, nameToDetailsId } from './trends/getDetailsId' import { ItemInfo, Prices } from './Prices' @@ -37,6 +37,11 @@ export interface ParsedItem { gemLevel?: number influences: ItemInfluence[] rawText: string + modifiers: Array<{ + mod: Mod + values?: number[] + type: ModifierType + }> computed: { category?: ItemCategory mapName?: string @@ -51,6 +56,14 @@ export enum ItemCategory { ItemisedMonster = 'Itemised Monster' } +export enum ModifierType { + Pseudo = 'pseudo', + Explicit = 'explicit', + Implicit = 'implicit', + Crafted = 'crafted', + Enchant = 'enchant' +} + const SECTION_PARSED = 1 const SECTION_SKIPPED = 0 const PARSER_SKIPPED = -1 @@ -75,7 +88,10 @@ const parsers: ParserFn[] = [ parseCorrupted, parseInfluence, parseMap, - parseSockets + parseSockets, + parseModifiers, // enchant + parseModifiers, // implicit + parseModifiers // explicit ] const parserAfterHooks = new Map([ @@ -106,6 +122,7 @@ export function parseClipboard (clipboard: string) { // need to think how to handle it return null } + sections.shift() // each section can be parsed at most by one parser for (const parser of parsers) { @@ -185,15 +202,18 @@ function parseNamePlate (section: string[]) { case ItemRarity.Magic: case ItemRarity.Rare: case ItemRarity.Unique: - return { + const item : ParsedItem = { rarity, name: section[1].replace(/^(<<.*?>>|<.*?>)+/, ''), // Item from chat "<><><>Beast Grinder" baseType: section[2], isUnidentified: false, isCorrupted: false, - influences: [] as ItemInfluence[], - computed: {} - } as ParsedItem + modifiers: [], + influences: [], + computed: {}, + rawText: undefined! + } + return item default: return null } @@ -306,6 +326,87 @@ function parseQualityNested (section: string[], item: ParsedItem) { } } +function parseModifiers (section: string[], item: ParsedItem) { + const IMPLICIT_SUFFIX = ' (implicit)' + const CRAFTED_SUFFIX = ' (crafted)' + + const countBefore = item.modifiers.length + + for (let line of section) { + let modType: ModifierType | undefined + let mod: Mod | undefined + const values: number[] = [] + + // cleanup suffix + if (line.endsWith(IMPLICIT_SUFFIX)) { + line = line.slice(0, -IMPLICIT_SUFFIX.length) + modType = ModifierType.Implicit + } else if (line.endsWith(CRAFTED_SUFFIX)) { + line = line.slice(0, -CRAFTED_SUFFIX.length) + modType = ModifierType.Crafted + } + + // 1. try to find "as is" + mod = Mods.get(line) + if (!mod) { + // 2. replace + // example: +#%, #%, #, +#, # to #, #-# + line = line.replace(/(? { + values.push(Number(value)) + return '#' + }) + if (values.length !== 1 && values.length !== 2) { + continue + } + // 3. match variants + // @TODO: IMPORTANT! distinguish between local and global mods + const variants = [ + { invertSign: false, text: line }, + { invertSign: false, text: line + ' (Local)' }, + { invertSign: false, text: line.replace('#', '+#') }, + { invertSign: false, text: line.replace('#', '+#') + ' (Local)' }, + { invertSign: true, text: line.replace('#% reduced', '#% increased') }, + { invertSign: true, text: line.replace('#% reduced', '#% increased') + ' (Local)' } + ] + for (const variant of variants) { + mod = Mods.get(variant.text) + if (mod) { + if (variant.invertSign) { + values[0] *= -1 + } + break + } + } + } + + if (mod) { + if (modType == null) { + for (const type of mod.types) { + if ( + type.name !== ModifierType.Pseudo && + type.name !== ModifierType.Implicit && + type.name !== ModifierType.Crafted + ) { + // explicit/enchant + modType = type.name as ModifierType + } + } + } + + item.modifiers.push({ + mod, + type: modType!, + values: values.length ? values : undefined + }) + } + } + + if (countBefore < item.modifiers.length) { + return SECTION_PARSED + } + return SECTION_SKIPPED +} + // -------- function enrichItem (item: ParsedItem) { diff --git a/src/data/index.ts b/src/data/index.ts index 3fe2a8c1..4c3c6ea8 100644 --- a/src/data/index.ts +++ b/src/data/index.ts @@ -1,5 +1,21 @@ import prophecies from './prophecies.json' import monsters from './itemised-monsters.json' +import mods from './mods.json' + +export interface Mod { + text: string + types: Array<{ + name: string + tradeId?: string + // undefined when dupe mod found. Alternative impl is to store + // all tradeIds in array, and apply Logical OR with "min: 1 matched" + }> + option?: { + text: string + tradeId: string | number + } +} export const Prophecies = new Set(prophecies as string[]) export const ItemisedMonsters = new Set(monsters as string[]) +export const Mods = new Map(mods)