Parse advanced item descriptions (Ctrl + Alt + C) (#425)

This commit is contained in:
Alexander Drozdov
2021-07-26 17:58:19 +03:00
committed by GitHub
parent 17f683302f
commit 30b6e3d32f
11 changed files with 473 additions and 227 deletions
+1
View File
@@ -29,6 +29,7 @@ module.exports = {
'@typescript-eslint/prefer-readonly': 'off',
'@typescript-eslint/no-floating-promises': 'off',
'@typescript-eslint/no-misused-promises': 'off',
'@typescript-eslint/prefer-reduce-type-parameter': 'off',
// TODO: refactor IPC and enable
'@typescript-eslint/consistent-type-assertions': 'off'
},
+4 -4
View File
@@ -36,15 +36,14 @@ const dict = {
'Synthesised Item': 'Synthesised Item',
'/^Synthesised (.*)$/': /^Synthesised (.*)$/,
'/^Vaal .*$/': /^Vaal .*$/,
'Veiled Prefix': 'Veiled Prefix',
'Veiled Suffix': 'Veiled Suffix',
'VEILED_PREFIX': 'Veiled Prefix',
'VEILED_SUFFIX': 'Veiled Suffix',
'/^Currently has \\d+ Charges$/': /^Currently has \d+ Charges$/,
'/^Spawns a Level (\\d+) Monster when Harvested$/': /^Spawns a Level (\d+) Monster when Harvested$/,
'Right-click this item then left-click the ground to plant it in the Sacred Grove.': 'Right-click this item then left-click the ground to plant it in the Sacred Grove.',
"Combine this with four other different samples in Tane's Laboratory.": "Combine this with four other different samples in Tane's Laboratory.",
'Right-click to add this to your bestiary.': 'Right-click to add this to your bestiary.',
'Right-click to add this prophecy to your character.': 'Right-click to add this prophecy to your character.',
'Added Small Passive Skills grant: ': 'Added Small Passive Skills grant: ',
'/^.* Brain$/': /^.* Brain$/,
'/^.* Eye$/': /^.* Eye$/,
'/^.* Lung$/': /^.* Lung$/,
@@ -76,7 +75,8 @@ const dict = {
'PREFIX_MODIFIER': 'Prefix Modifier',
'SUFFIX_MODIFIER': 'Suffix Modifier',
'CRAFTED_PREFIX': 'Master Crafted Prefix Modifier',
'CRAFTED_SUFFIX': 'Master Crafted Suffix Modifier'
'CRAFTED_SUFFIX': 'Master Crafted Suffix Modifier',
'UNSCALABLE_VALUE': ' — Unscalable Value'
}
export default dict
+4 -4
View File
@@ -38,15 +38,14 @@ const dict: TranslationDict = {
'Synthesised Item': 'Синтезированный предмет',
'/^Synthesised (.*)$/': /^(?:Синтезированный|Синтезированная|Синтезированное|Синтезированные) (.*?)\u00a0*$/,
'/^Vaal .*$/': /^.* ваал$/,
'Veiled Prefix': 'Завуалированный префикс',
'Veiled Suffix': 'Завуалированный суффикс',
'VEILED_PREFIX': 'Завуалированный префикс',
'VEILED_SUFFIX': 'Завуалированный суффикс',
'/^Currently has \\d+ Charges$/': /^Содержит зарядов: \d+$/,
'/^Spawns a Level (\\d+) Monster when Harvested$/': /^При сборе появляется монстр (\d+) уровня$/,
'Right-click this item then left-click the ground to plant it in the Sacred Grove.': 'Щелкните ПКМ, затем ЛКМ по земле в Священной роще, чтобы посадить растение.',
"Combine this with four other different samples in Tane's Laboratory.": 'Объедините эту часть с четырьмя другими в Лаборатории Танэ.',
'Right-click to add this to your bestiary.': 'Нажмите ПКМ, чтобы добавить это в ваш Бестиарий.',
'Right-click to add this prophecy to your character.': 'Нажмите ПКМ, чтобы добавить это пророчество вашему персонажу.',
'Added Small Passive Skills grant: ': 'Добавленные малые пассивные умения даруют: ',
'/^.* Brain$/': /^Мозг: .*$/,
'/^.* Eye$/': /^Глаз: .*$/,
'/^.* Lung$/': /^Лёгкое: .*$/,
@@ -78,7 +77,8 @@ const dict: TranslationDict = {
'PREFIX_MODIFIER': 'Префикс',
'SUFFIX_MODIFIER': 'Суффикс',
'CRAFTED_PREFIX': 'Мастерский префикс',
'CRAFTED_SUFFIX': 'Мастерский суффикс'
'CRAFTED_SUFFIX': 'Мастерский суффикс',
'UNSCALABLE_VALUE': ' — Неизменяемое значение'
}
export default dict
+3 -10
View File
@@ -30,19 +30,12 @@ function priceCheck (lockedMode: boolean) {
if (!lockedMode) {
if (config.get('priceCheckKeyHold') === 'Ctrl') {
robotjs.keyTap('C')
robotjs.keyTap('C', ['Alt'])
} else /* Alt */ {
// not interested in advanced item text (Ctrl + Alt + C)
robotjs.keyToggle('Alt', 'up')
robotjs.keyTap('C', ['Ctrl'])
// restore Alt
robotjs.keyToggle('Alt', 'down')
// cancel alt-visibility
// (using Ctrl key (Alt + Ctrl), to ensure nothing triggers on the user's system)
robotjs.keyTap('Ctrl')
}
} else {
robotjs.keyTap('C', ['Ctrl'])
robotjs.keyTap('C', ['Ctrl', 'Alt'])
}
}
@@ -56,7 +49,7 @@ function itemCheck () {
})
.catch(() => {})
hotkeyPressPosition = screen.getCursorScreenPoint()
robotjs.keyTap('C', ['Ctrl'])
robotjs.keyTap('C', ['Ctrl', 'Alt'])
}
function registerGlobal () {
+5 -1
View File
@@ -1,6 +1,7 @@
import { ItemRarity, ItemInfluence } from './constants'
import { ItemModifier, ModifierType } from './modifiers'
import { ItemCategory } from './meta'
import type { ParsedModifier } from './advanced-mod-desc'
export interface ParsedItem {
rarity: ItemRarity
@@ -33,7 +34,11 @@ export interface ParsedItem {
isMirrored?: boolean
influences: ItemInfluence[]
isSynthesised?: boolean
/**
* @deprecated
*/
modifiers: ItemModifier[]
newMods: ParsedModifier[]
unknownModifiers: Array<{
text: string
type: ModifierType
@@ -47,7 +52,6 @@ export interface ParsedItem {
category?: ItemCategory
icon?: string
rawText: string
isAdvancedDesc: boolean
}
export type HeistJob =
+69 -71
View File
@@ -5,10 +5,12 @@ import {
CLIENT_STRINGS as _$,
ITEM_NAME_REF_BY_TRANSLATED
} from '@/assets/data'
import { ModifierType, sectionToStatStrings, tryFindModifier, getRollOrMinmaxAvg } from './modifiers'
import { ModifierType } from './modifiers'
import { linesToStatStrings, tryParseTranslation, getRollOrMinmaxAvg } from './stat-translations'
import { ItemCategory } from './meta'
import { HeistJob, ParsedItem } from './ParsedItem'
import { magicBasetype } from './magic-name'
import { isModInfoLine, groupLinesByMod, parseModInfoLine, parseModType, ModifierInfo, ParsedModifier, sumStatsFromMods } from './advanced-mod-desc'
const SECTION_PARSED = 1
const SECTION_SKIPPED = 0
@@ -45,7 +47,8 @@ const parsers: ParserFn[] = [
parseMirrored,
parseModifiers,
parseModifiers,
parseModifiers
parseModifiers,
transformToLegacyModifiers
]
export function parseClipboard (clipboard: string) {
@@ -77,7 +80,6 @@ export function parseClipboard (clipboard: string) {
sections.shift()
parsed.rawText = clipboard
parsed.isAdvancedDesc = isAdvancedDescription(lines)
// each section can be parsed at most by one parser
for (const parser of parsers) {
@@ -196,12 +198,12 @@ function parseNamePlate (section: string[]) {
isUnidentified: false,
isCorrupted: false,
modifiers: [],
newMods: [],
unknownModifiers: [],
influences: [],
sockets: {},
extra: {},
rawText: undefined!,
isAdvancedDesc: false
rawText: undefined!
}
return item
}
@@ -429,10 +431,10 @@ function parseWeapon (section: string[], item: ParsedItem) {
isParsed = SECTION_PARSED; continue
}
if (line.startsWith(_$[C.TAG_PHYSICAL_DAMAGE])) {
const [min, max] = line
item.props.physicalDamage = getRollOrMinmaxAvg(line
.substr(_$[C.TAG_PHYSICAL_DAMAGE].length)
.split('-').map(str => parseInt(str, 10))
item.props.physicalDamage = (min + max) / 2
)
isParsed = SECTION_PARSED; continue
}
if (line.startsWith(_$[C.TAG_ELEMENTAL_DAMAGE])) {
@@ -463,80 +465,41 @@ function parseModifiers (section: string[], item: ParsedItem) {
return PARSER_SKIPPED
}
const countBefore = (item.modifiers.length + item.unknownModifiers.length)
if (!section.some(line =>
line.endsWith(C.ENCHANT_LINE) ||
isModInfoLine(line) ||
(line === _$.VEILED_PREFIX || line === _$.VEILED_SUFFIX)
)) {
return SECTION_SKIPPED
}
const statIterator = sectionToStatStrings(section)
let stat = statIterator.next()
while (!stat.done) {
if (parseVeiledNested(stat.value, item)) {
stat = statIterator.next(true)
continue
if (section.some(line => line.endsWith(C.ENCHANT_LINE))) {
const { lines } = parseModType(section)
const modInfo: ModifierInfo = {
type: ModifierType.Enchant,
tags: []
}
parseStatsFromMod(lines, item, { info: modInfo, stats: [] })
} else {
section = section.filter(line => !parseVeiledNested(line, item))
let modType: ModifierType | undefined
// cleanup suffix
if (stat.value.endsWith(C.IMPLICIT_SUFFIX)) {
stat.value = stat.value.slice(0, -C.IMPLICIT_SUFFIX.length)
modType = ModifierType.Implicit
} else if (stat.value.endsWith(C.CRAFTED_SUFFIX)) {
stat.value = stat.value.slice(0, -C.CRAFTED_SUFFIX.length)
modType = ModifierType.Crafted
} else if (stat.value.endsWith(C.ENCHANT_SUFFIX)) {
stat.value = stat.value.slice(0, -C.ENCHANT_SUFFIX.length)
modType = ModifierType.Enchant
} else if (stat.value.endsWith(C.FRACTURED_SUFFIX)) {
stat.value = stat.value.slice(0, -C.FRACTURED_SUFFIX.length)
modType = ModifierType.Fractured
} else {
modType = ModifierType.Explicit
}
const mod = tryFindModifier(stat.value)
if (mod && mod.trade.ids[modType]) {
mod.type = modType
item.modifiers.push(mod)
stat = statIterator.next(true)
} else {
if (
mod != null || // not found on trade, but successfully parsed
modType === ModifierType.Enchant || // has separate section
modType === ModifierType.Implicit || // has separate section
modType === ModifierType.Fractured || // always comes first in section
(modType === ModifierType.Crafted && (
!stat.value.includes('\n') || // not multiline, on section transition from explicit to crafted mods
item.modifiers.some(m => m.type === ModifierType.Crafted) ||
item.unknownModifiers.some(m => m.type === ModifierType.Crafted)
))
) {
item.unknownModifiers.push({
text: stat.value,
type: modType
})
stat = statIterator.next(true)
} else {
stat = statIterator.next(false)
}
for (const { modLine, statLines } of groupLinesByMod(section)) {
const { modType, lines } = parseModType(statLines)
const modInfo = parseModInfoLine(modLine, modType)
parseStatsFromMod(lines, item, { info: modInfo, stats: [] })
}
}
if (countBefore < (item.modifiers.length + item.unknownModifiers.length)) {
item.unknownModifiers.push(...stat.value.map(line => ({
text: line,
type: ModifierType.Explicit
})))
return SECTION_PARSED
}
return SECTION_SKIPPED
return SECTION_PARSED
}
// TODO blocked by https://www.pathofexile.com/forum/view-thread/3148119
function parseVeiledNested (text: string, item: ParsedItem) {
if (text === _$[C.VEILED_SUFFIX]) {
if (text === _$.VEILED_SUFFIX) {
item.extra.veiled = (item.extra.veiled == null ? 'suffix' : 'prefix-suffix')
return true
}
if (text === _$[C.VEILED_PREFIX]) {
if (text === _$.VEILED_PREFIX) {
item.extra.veiled = (item.extra.veiled == null ? 'prefix' : 'prefix-suffix')
return true
}
@@ -686,6 +649,41 @@ function markupConditionParser (text: string) {
return text
}
function isAdvancedDescription (lines: string[]): boolean {
return lines.some(line => line.startsWith('{') && line.endsWith('}'))
function parseStatsFromMod (lines: string[], item: ParsedItem, modifier: ParsedModifier) {
const statIterator = linesToStatStrings(lines)
let stat = statIterator.next()
while (!stat.done) {
const parsedStat = tryParseTranslation(stat.value, modifier.info.type)
if (parsedStat) {
modifier.stats.push(parsedStat)
stat = statIterator.next(true)
} else {
stat = statIterator.next(false)
}
}
item.newMods.push(modifier)
item.unknownModifiers.push(...stat.value.map(line => ({
text: line,
type: modifier.info.type
})))
}
/**
* @deprecated
*/
function transformToLegacyModifiers (_: string[], item: ParsedItem) {
item.modifiers = sumStatsFromMods(item.newMods)
return PARSER_SKIPPED as SectionParseResult // fake parser
}
export function removeLinesEnding (
lines: readonly string[], ending: string
): string[] {
return lines.map(line =>
line.endsWith(ending)
? line.slice(0, -ending.length)
: line
)
}
+160 -9
View File
@@ -1,18 +1,27 @@
import { CLIENT_STRINGS as _$ } from '@/assets/data'
import { CLIENT_STRINGS as _$, STAT_BY_MATCH_STR } from '@/assets/data'
import * as C from './constants'
import { percentRoll } from '@/web/price-check/filters/util'
import type { ParsedStat } from './stat-translations'
import { LegacyItemModifier, ModifierType } from './modifiers'
import { removeLinesEnding } from './Parser'
// TODO "— Unscalable Value"
export interface ParsedModifier {
info: ModifierInfo
stats: ParsedStat[]
}
interface ModifierInfo {
export interface ModifierInfo {
type: ModifierType
generation?: 'suffix' | 'prefix'
name?: string
tier?: number
rank?: number
tags?: string[]
catalystIncr?: number
tags: string[]
rollIncr?: number
}
export function parseModInfoLine (line: string): ModifierInfo {
const [modText, tagsText, catalystText] = line
export function parseModInfoLine (line: string, type: ModifierType): ModifierInfo {
const [modText, tagsText, incrText] = line
.slice(1, -1)
.split('\u2014')
.map(_ => _.trim())
@@ -36,7 +45,149 @@ export function parseModInfoLine (line: string): ModifierInfo {
const tier = Number(match.groups!.tier) || undefined
const rank = Number(match.groups!.rank) || undefined
const tags = tagsText ? tagsText.split(', ') : []
const catalystIncr = parseInt(catalystText, 10) || undefined
const rollIncr = parseInt(incrText, 10) || undefined
return { generation, name, tier, rank, tags, catalystIncr }
return { type, generation, name, tier, rank, tags, rollIncr }
}
export function isModInfoLine (line: string): boolean {
return line.startsWith('{') && line.endsWith('}')
}
interface GroupedModLines {
modLine: string
statLines: string[]
}
export function * groupLinesByMod (lines: string[]): Generator<GroupedModLines, void> {
if (!lines.length || !isModInfoLine(lines[0])) {
return
}
let last: GroupedModLines | undefined
for (const line of lines) {
if (!isModInfoLine(line)) {
last!.statLines.push(line)
} else {
if (last) { yield last }
last = { modLine: line, statLines: [] }
}
}
yield last!
}
export function parseModType (lines: string[]): { modType: ModifierType, lines: string[] } {
let modType: ModifierType
if (lines.some(line => line.endsWith(C.ENCHANT_LINE))) {
modType = ModifierType.Enchant
lines = removeLinesEnding(lines, C.ENCHANT_LINE)
} else if (lines.some(line => line.endsWith(C.IMPLICIT_LINE))) {
modType = ModifierType.Implicit
lines = removeLinesEnding(lines, C.IMPLICIT_LINE)
} else if (lines.some(line => line.endsWith(C.FRACTURED_LINE))) {
modType = ModifierType.Fractured
lines = removeLinesEnding(lines, C.FRACTURED_LINE)
} else if (lines.some(line => line.endsWith(C.CRAFTED_LINE))) {
modType = ModifierType.Crafted
lines = removeLinesEnding(lines, C.CRAFTED_LINE)
} else {
modType = ModifierType.Explicit
}
return { modType, lines }
}
// stat values internally stored as ints,
// this is the most common formatter
const DIV_BY_100 = 2
function applyIncr (mod: ModifierInfo, stat: ParsedStat): ParsedStat | null {
const { rollIncr } = mod
const { roll } = stat
if (!rollIncr || !roll || roll.unscalable) {
return null
}
return {
translation: stat.translation,
roll: {
unscalable: roll.unscalable,
dp: roll.dp,
value: percentRoll(roll.value, rollIncr, (roll.value > 0) ? Math.floor : Math.ceil, roll.dp && DIV_BY_100),
min: percentRoll(roll.min, rollIncr, (roll.min > 0) ? Math.floor : Math.ceil, roll.dp && DIV_BY_100),
max: percentRoll(roll.max, rollIncr, (roll.max > 0) ? Math.floor : Math.ceil, roll.dp && DIV_BY_100)
}
}
}
export function sumStatsFromMods (mods: readonly ParsedModifier[]): LegacyItemModifier[] {
const out: LegacyItemModifier[] = []
const merged: ParsedStat[] = []
mods = mods.map(mod => ({
...mod,
stats: mod.stats.map(stat =>
applyIncr(mod.info, stat) ?? stat
)
}))
for (const modA of mods) {
for (const statA of modA.stats) {
if (merged.includes(statA)) {
continue
}
const dbStatA = STAT_BY_MATCH_STR.get(statA.translation.string)!.stat
const toMerge = mods
.reduce((filtered, modB) => {
if (modB.info.type === modA.info.type) {
const targetStat = modB.stats.find(statB =>
dbStatA.stat.matchers.some(matcher => matcher.string === statB.translation.string)
)
if (targetStat) {
filtered.push({
info: modB.info,
stat: targetStat
})
}
}
return filtered
}, [] as Array<{ info: ModifierInfo, stat: ParsedStat }>)
if (toMerge.length === 1) {
out.push({
stat: dbStatA.stat,
trade: dbStatA.trade,
string: statA.translation.string,
type: modA.info.type,
negate: statA.translation.negate,
value: statA.roll?.value
})
} else {
const rollValue = toMerge.reduce((sum, { stat }) => sum + stat.roll!.value, 0)
const translation =
(dbStatA.stat.matchers.find(m => m.value === rollValue)) ??
((statA.translation.value == null)
? statA.translation
: dbStatA.stat.matchers.find(m => m.value == null && !m.negate)) ??
({ string: `Report bug if you see this text (${statA.translation.string})` })
out.push({
stat: dbStatA.stat,
trade: dbStatA.trade,
string: translation.string,
type: modA.info.type,
negate: translation.negate,
value: rollValue
})
}
merged.push(...toMerge.map(mod => mod.stat))
}
}
return out
}
+4 -9
View File
@@ -45,10 +45,10 @@ export const TAG_EVASION = 'Evasion Rating: '
export const TAG_ENERGY_SHIELD = 'Energy Shield: '
export const TAG_BLOCK_CHANCE = 'Chance to Block: '
export const IMPLICIT_SUFFIX = ' (implicit)'
export const CRAFTED_SUFFIX = ' (crafted)'
export const ENCHANT_SUFFIX = ' (enchant)'
export const FRACTURED_SUFFIX = ' (fractured)'
export const IMPLICIT_LINE = ' (implicit)'
export const CRAFTED_LINE = ' (crafted)'
export const ENCHANT_LINE = ' (enchant)'
export const FRACTURED_LINE = ' (fractured)'
export const CORRUPTED = 'Corrupted'
export const UNIDENTIFIED = 'Unidentified'
@@ -60,8 +60,6 @@ export const MAP_BLIGHTED = '/^Blighted (.*)$/'
export const ITEM_SUPERIOR = '/^Superior (.*)$/'
export const VAAL_GEM = '/^Vaal .*$/'
export const CLUSTER_JEWEL_GRANT = 'Added Small Passive Skills grant: '
export const PROPHECY_HELP = 'Right-click to add this prophecy to your character.'
export const BEAST_HELP = 'Right-click to add this to your bestiary.'
export const METAMORPH_HELP = "Combine this with four other different samples in Tane's Laboratory."
@@ -73,9 +71,6 @@ export const METAMORPH_LUNG = '/^.* Lung$/'
export const METAMORPH_HEART = '/^.* Heart$/'
export const METAMORPH_LIVER = '/^.* Liver$/'
export const VEILED_PREFIX = 'Veiled Prefix'
export const VEILED_SUFFIX = 'Veiled Suffix'
export const PROPHECY_ALVA = 'You will find Alva and complete her mission.'
export const PROPHECY_EINHAR = 'You will find Einhar and complete his mission.'
export const PROPHECY_NIKO = 'You will find Niko and complete his mission.'
+3 -116
View File
@@ -1,5 +1,4 @@
import { STAT_BY_MATCH_STR, Stat, StatMatcher, CLIENT_STRINGS as _$ } from '@/assets/data'
import * as C from './constants'
import type { Stat, StatMatcher } from '@/assets/data'
export enum ModifierType {
Pseudo = 'pseudo',
@@ -11,123 +10,11 @@ export enum ModifierType {
Fractured = 'fractured'
}
export interface ItemModifier extends
export interface LegacyItemModifier extends
Stat,
Pick<StatMatcher, 'string' | 'negate'> {
value?: number
type: ModifierType
}
export function * sectionToStatStrings (section: string[]): Generator<string, string[], boolean> {
const notParsedLines: string[] = []
let idx = 0
let multi = (idx + 1) < section.length
while (idx < section.length) {
let str: string
if (multi) {
if (
section[idx].startsWith(_$[C.CLUSTER_JEWEL_GRANT]) &&
section[idx + 1].startsWith(_$[C.CLUSTER_JEWEL_GRANT])
) {
str = `${section[idx].slice(0, -C.ENCHANT_SUFFIX.length)}\n${section[idx + 1]}`
} else if (
section[idx].endsWith(C.IMPLICIT_SUFFIX) ||
section[idx].endsWith(C.CRAFTED_SUFFIX) ||
section[idx].endsWith(C.ENCHANT_SUFFIX) ||
section[idx].endsWith(C.FRACTURED_SUFFIX)
) {
multi = false
str = section[idx]
} else {
str = `${section[idx]}\n${section[idx + 1]}`
}
} else {
str = section[idx]
}
const isParsed: boolean = yield str
if (isParsed) {
idx += multi ? 2 : 1
multi = (idx + 1) < section.length
} else {
if (multi) {
multi = false
} else {
idx += 1
multi = (idx + 1) < section.length
notParsedLines.push(str)
}
}
}
return notParsedLines
}
const PLACEHOLDER_MAP = [
// 0 #
[[]],
// 1 #
[[0], []],
// 2 #
[[0, 1], [0], [1], []],
// 3 #
[[0, 1, 2], [1, 2], [0, 2], [0, 1], [2], [1], [0]],
// 4 #
[[0, 1, 2, 3], [1, 2, 3], [0, 2, 3], [0, 1, 3], [0, 1, 2], [2, 3], [1, 3], [1, 2], [0, 3], [0, 2], [0, 1]]
]
export function tryFindModifier (stat: string): ItemModifier | undefined {
const matches = [] as string[]
const withPlaceholders = stat
.replace(/(?<![\d#])[+-]?[\d.]+/gm, (value) => {
matches.push(value)
return '#'
})
if (matches.length >= PLACEHOLDER_MAP.length) return
const comboVariants = PLACEHOLDER_MAP[matches.length]
for (const combo of comboVariants) {
let pIdx = -1
const possibleStat = withPlaceholders.replace(/#/gm, () => {
pIdx += 1
if (combo.includes(pIdx)) {
return matches[pIdx]
} else {
return '#'
}
})
const found = STAT_BY_MATCH_STR.get(possibleStat)
if (found) {
let values = matches
.filter((_, idx) => !combo.includes(idx))
.map(str => Number(str) * (found.matcher.negate ? -1 : 1))
if (!values.length && found.matcher.value) {
values = [found.matcher.value]
}
return {
stat: found.stat.stat,
trade: found.stat.trade,
string: found.matcher.string,
negate: found.matcher.negate,
value: values.length ? getRollOrMinmaxAvg(values) : undefined,
type: undefined!
}
}
}
}
export function getRollOrMinmaxAvg (values: number[]): number {
if (values.length === 2) {
return (values[0] + values[1]) / 2
} else {
return values[0]
}
}
export { LegacyItemModifier as ItemModifier }
+209
View File
@@ -0,0 +1,209 @@
import { CLIENT_STRINGS as _$, STAT_BY_MATCH_STR, StatMatcher } from '@/assets/data'
import type { ModifierType } from './modifiers'
// This file is a little messy and scary,
// but that's how stats translations are parsed :-D
export interface ParsedStat {
readonly translation: StatMatcher
roll?: {
unscalable: boolean
dp: boolean
value: number
min: number
max: number
}
}
interface StatString {
string: string
unscalable: boolean
}
export function * linesToStatStrings (lines: string[]): Generator<StatString, string[], boolean> {
const notParsedLines: string[] = []
let reminderString = false
outer:
for (let start = 0; start < lines.length; start += 1) {
if ((lines[start].trim()).startsWith('(')) {
reminderString = true
}
if (reminderString && (lines[start].trim()).endsWith(')')) {
reminderString = false
continue
}
if (reminderString) {
continue
}
for (let end = start; end < lines.length; end += 1) {
let str = lines.slice(start, end + 1).join('\n')
const unscalable = str.endsWith(_$.UNSCALABLE_VALUE)
if (unscalable) {
str = str.slice(0, -_$.UNSCALABLE_VALUE.length)
}
const isParsed: boolean = yield { string: str, unscalable }
if (isParsed) {
continue outer
}
}
notParsedLines.push(lines[start])
}
return notParsedLines
}
const PLACEHOLDER_MAP = [
// 0 # -> max 0 #
[[]],
// 1 # -> max 1 #
[[0], []],
// 2 # -> max 2 #
[[0, 1], [0], [1], []],
// 3 # -> max 2 #
[[0, 1, 2], [1, 2], [0, 2], [0, 1], [2], [1], [0]],
// 4 # -> max 2 #
[[0, 1, 2, 3], [1, 2, 3], [0, 2, 3], [0, 1, 3], [0, 1, 2], [2, 3], [1, 3], [1, 2], [0, 3], [0, 2], [0, 1]]
]
function * _statPlaceholderGenerator (stat: string) {
// combinations to detect stat text range "Victario(Cadiro-Victario)"
// ^^^^^^ ^^^^^^^^
// but ignore (dashed-text) literal inside translation string
function * _firstPass (stat: string): Generator<string, void> {
const matches = [] as string[]
const withPlaceholders = stat
.replace(/(?:(?<!\x20|\d)\((?<min>.[^)-]*)-(?<max>[^)]+)\))/gm, (match, min, max) => {
if (Number.isNaN(Number(min)) || Number.isNaN(Number(max))) {
matches.push(match)
return '#'
} else {
return match
}
})
for (let j = 0; j < 2 ** matches.length; j += 1) {
const replacements: number[] = []
for (let idx = 0; idx < matches.length; idx += 1) {
if ((j & (2 ** idx))) {
replacements.push(idx)
}
}
{
let idx = -1
yield withPlaceholders.replace(/#/gm, () => {
idx += 1
return replacements.includes(idx)
? matches[idx]
: ''
})
}
}
}
for (stat of _firstPass(stat)) {
const matches: Array<{
roll: number
rollStr: string
decimal: boolean
bounds?: { min: number, max: number }
}> = []
const withPlaceholders = stat
.replace(/(?<value>(?<!\d|\))[+-]?\d+(?:\.\d+)?)(?:\((?<min>.[^)-]*)(?:-(?<max>[^)]+))?\))?/gm, (_, roll: string, min?: string, max?: string) => {
if (min != null && max == null) {
// example: Watchstone "# uses remaining"
max = min
}
const captured: typeof matches[number] = {
roll: Number(roll),
rollStr: roll,
decimal: roll.includes('.') || min?.includes('.') || max?.includes('.') || false,
bounds: { min: Number(min), max: Number(max) }
}
matches.push(captured)
if (Number.isNaN(captured.bounds!.min) || Number.isNaN(captured.bounds!.max)) {
captured.bounds = undefined
return (min != null) ? `#(${min}-${max})` : '#'
} else {
return '#'
}
})
if (matches.length < PLACEHOLDER_MAP.length) {
for (const replacements of PLACEHOLDER_MAP[matches.length]) {
let idx = -1
const replaced = withPlaceholders.replace(/#/gm, () => {
idx += 1
return replacements.includes(idx)
? matches[idx].rollStr
: '#'
})
yield {
stat: replaced,
values: matches
.filter((_, idx) => !replacements.includes(idx)) as
Array<Pick<typeof matches[number], 'roll' | 'bounds' | 'decimal'>>
}
}
}
}
}
export function tryParseTranslation (stat: StatString, modType: ModifierType): ParsedStat | undefined {
for (const combination of _statPlaceholderGenerator(stat.string)) {
const found = STAT_BY_MATCH_STR.get(combination.stat)
if (!found || !found.stat.trade.ids[modType]) {
continue
}
if (found.matcher.negate) {
for (const stat of combination.values) {
stat.roll *= -1
if (stat.bounds) {
// do not swap (TODO: or do? can't say from Ventor's Gamble)
stat.bounds.min *= -1
stat.bounds.max *= -1
}
}
}
if (!combination.values.length && found.matcher.value) {
combination.values = [{
roll: found.matcher.value,
decimal: false,
bounds: {
min: found.matcher.value,
max: found.matcher.value
}
}]
}
return {
translation: found.matcher,
roll: combination.values.length
? {
unscalable: stat.unscalable,
dp: found.stat.stat.dp || combination.values.some(stat => stat.decimal),
value: getRollOrMinmaxAvg(combination.values.map(stat => stat.roll)),
min: getRollOrMinmaxAvg(combination.values.map(stat => stat.bounds?.min ?? stat.roll)),
max: getRollOrMinmaxAvg(combination.values.map(stat => stat.bounds?.max ?? stat.roll))
}
: undefined
}
}
}
export function getRollOrMinmaxAvg (values: number[]): number {
if (values.length === 2) {
return (values[0] + values[1]) / 2
} else {
return values[0]
}
}
@@ -26,7 +26,7 @@ export interface RareItemPrice {
export async function requestPoeprices (item: ParsedItem): Promise<RareItemPrice | null> {
const query = querystring({
i: utf8ToBase64(item.rawText),
i: utf8ToBase64(transformItemText(item.rawText)),
l: league.value,
s: 'awakened-poe-trade'
})
@@ -56,7 +56,7 @@ export async function requestPoeprices (item: ParsedItem): Promise<RareItemPrice
export function getExternalLink (item: ParsedItem): string {
const query = querystring({
i: utf8ToBase64(item.rawText),
i: utf8ToBase64(transformItemText(item.rawText)),
l: league.value,
s: 'awakened-poe-trade',
w: 1
@@ -72,7 +72,7 @@ export async function sendFeedback (
const body = new FormData()
body.append('selector', feedback.option)
body.append('feedbacktxt', feedback.text)
body.append('qitem_txt', utf8ToBase64(item.rawText))
body.append('qitem_txt', utf8ToBase64(transformItemText(item.rawText)))
body.append('source', 'awakened-poe-trade')
body.append('min', String(prediction.min))
body.append('max', String(prediction.max))
@@ -98,3 +98,11 @@ function querystring (q: Record<string, any>) {
.map(pair => pair.map(encodeURIComponent).join('='))
.join('&')
}
/**
* @deprecated TODO blocked by poeprices.info not supporting advanced text
*/
function transformItemText (rawText: string) {
// this may not account for all cases
return rawText.replace(/(?<=\d)(\([^)]+\))/gm, '')
}