mirror of
https://github.com/Kvan7/Exiled-Exchange-2.git
synced 2026-09-24 21:55:41 +00:00
Parse advanced item descriptions (Ctrl + Alt + C) (#425)
This commit is contained in:
@@ -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'
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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 () {
|
||||
|
||||
@@ -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
@@ -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
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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 }
|
||||
|
||||
@@ -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, '')
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user