mirror of
https://github.com/Kvan7/Exiled-Exchange-2.git
synced 2025-12-15 12:35:59 +00:00
944 lines
27 KiB
TypeScript
944 lines
27 KiB
TypeScript
import {
|
|
CLIENT_STRINGS as _$,
|
|
CLIENT_STRINGS_REF as _$REF,
|
|
ITEM_BY_TRANSLATED,
|
|
ITEM_BY_REF,
|
|
STAT_BY_MATCH_STR,
|
|
BaseType
|
|
} from '@/assets/data'
|
|
import { ModifierType, sumStatsByModType } from './modifiers'
|
|
import { linesToStatStrings, tryParseTranslation, getRollOrMinmaxAvg } from './stat-translations'
|
|
import { ItemCategory } from './meta'
|
|
import { IncursionRoom, ParsedItem, ItemInfluence, ItemRarity } from './ParsedItem'
|
|
import { magicBasetype } from './magic-name'
|
|
import { isModInfoLine, groupLinesByMod, parseModInfoLine, parseModType, ModifierInfo, ParsedModifier, ENCHANT_LINE, SCOURGE_LINE } from './advanced-mod-desc'
|
|
import { calcPropPercentile, QUALITY_STATS } from './calc-q20'
|
|
|
|
type SectionParseResult =
|
|
| 'SECTION_PARSED'
|
|
| 'SECTION_SKIPPED'
|
|
| 'PARSER_SKIPPED'
|
|
|
|
type ParserFn = (section: string[], item: ParserState) => SectionParseResult
|
|
type VirtualParserFn = (item: ParserState) => void
|
|
|
|
export interface ParserState extends ParsedItem {
|
|
name: string
|
|
baseType: string | undefined
|
|
infoVariants: BaseType[]
|
|
}
|
|
|
|
const parsers: Array<ParserFn | { virtual: VirtualParserFn }> = [
|
|
parseUnidentified,
|
|
{ virtual: parseSuperior },
|
|
parseSynthesised,
|
|
parseCategoryByHelpText,
|
|
{ virtual: normalizeName },
|
|
{ virtual: parseGemAltQuality },
|
|
parseVaalGemName,
|
|
{ virtual: findInDatabase },
|
|
// -----------
|
|
parseItemLevel,
|
|
parseTalismanTier,
|
|
parseGem,
|
|
parseArmour,
|
|
parseWeapon,
|
|
parseFlask,
|
|
parseStackSize,
|
|
parseCorrupted,
|
|
parseRelic,
|
|
parseInfluence,
|
|
parseMap,
|
|
parseSockets,
|
|
parseHeistBlueprint,
|
|
parseAreaLevel,
|
|
parseAtzoatlRooms,
|
|
parseMirroredTablet,
|
|
parseMirrored,
|
|
parseSentinelCharge,
|
|
parseLogbookArea,
|
|
parseLogbookArea,
|
|
parseLogbookArea,
|
|
parseModifiers, // enchant
|
|
parseModifiers, // scourge
|
|
parseModifiers, // implicit
|
|
parseModifiers, // explicit
|
|
{ virtual: transformToLegacyModifiers },
|
|
{ virtual: parseFractured },
|
|
{ virtual: parseBlightedMap },
|
|
{ virtual: pickCorrectVariant },
|
|
{ virtual: calcBasePercentile }
|
|
]
|
|
|
|
export function parseClipboard (clipboard: string) {
|
|
const lines = clipboard.split(/\r?\n/)
|
|
lines.pop()
|
|
|
|
let sections: string[][] = [[]]
|
|
lines.reduce((section, line) => {
|
|
if (line !== '--------') {
|
|
section.push(line)
|
|
return section
|
|
} else {
|
|
const section: string[] = []
|
|
sections.push(section)
|
|
return section
|
|
}
|
|
}, sections[0])
|
|
sections = sections.filter(section => section.length)
|
|
|
|
if (sections[0][2] === _$.CANNOT_USE_ITEM) {
|
|
sections[0].pop() // remove CANNOT_USE_ITEM line
|
|
sections[1].unshift(...sections[0]) // prepend item class & rarity into second section
|
|
sections.shift() // remove first section where CANNOT_USE_ITEM line was
|
|
}
|
|
const parsed = parseNamePlate(sections[0])
|
|
if (!parsed) {
|
|
return null
|
|
}
|
|
|
|
sections.shift()
|
|
parsed.rawText = clipboard
|
|
|
|
// each section can be parsed at most by one parser
|
|
for (const parser of parsers) {
|
|
if (typeof parser === 'object') {
|
|
parser.virtual(parsed)
|
|
continue
|
|
}
|
|
|
|
for (const section of sections) {
|
|
const result = parser(section, parsed)
|
|
if (result === 'SECTION_PARSED') {
|
|
sections = sections.filter(s => s !== section)
|
|
break
|
|
} else if (result === 'PARSER_SKIPPED') {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
return Object.freeze(parsed)
|
|
}
|
|
|
|
function normalizeName (item: ParserState) {
|
|
if (item.rarity === ItemRarity.Magic) {
|
|
const baseType = magicBasetype(item.name)
|
|
if (baseType) {
|
|
item.name = baseType
|
|
}
|
|
}
|
|
|
|
if (item.rarity === ItemRarity.Normal ||
|
|
item.rarity === ItemRarity.Rare
|
|
) {
|
|
if (item.baseType) {
|
|
if (_$REF.MAP_BLIGHTED.test(item.baseType)) {
|
|
item.baseType = _$REF.MAP_BLIGHTED.exec(item.baseType)![1]
|
|
} else if (_$REF.MAP_BLIGHT_RAVAGED.test(item.baseType)) {
|
|
item.baseType = _$REF.MAP_BLIGHT_RAVAGED.exec(item.baseType)![1]
|
|
}
|
|
} else {
|
|
if (_$REF.MAP_BLIGHTED.test(item.name)) {
|
|
item.name = _$REF.MAP_BLIGHTED.exec(item.name)![1]
|
|
} else if (_$REF.MAP_BLIGHT_RAVAGED.test(item.name)) {
|
|
item.name = _$REF.MAP_BLIGHT_RAVAGED.exec(item.name)![1]
|
|
}
|
|
}
|
|
}
|
|
|
|
if (item.category === ItemCategory.MetamorphSample) {
|
|
if (_$REF.METAMORPH_BRAIN.test(item.name)) {
|
|
item.name = 'Metamorph Brain'
|
|
} else if (_$REF.METAMORPH_EYE.test(item.name)) {
|
|
item.name = 'Metamorph Eye'
|
|
} else if (_$REF.METAMORPH_LUNG.test(item.name)) {
|
|
item.name = 'Metamorph Lung'
|
|
} else if (_$REF.METAMORPH_HEART.test(item.name)) {
|
|
item.name = 'Metamorph Heart'
|
|
} else if (_$REF.METAMORPH_LIVER.test(item.name)) {
|
|
item.name = 'Metamorph Liver'
|
|
}
|
|
}
|
|
}
|
|
|
|
function findInDatabase (item: ParserState) {
|
|
let info: BaseType[] | undefined
|
|
if (item.category === ItemCategory.DivinationCard) {
|
|
info = ITEM_BY_REF('DIVINATION_CARD', item.name)
|
|
} else if (item.category === ItemCategory.CapturedBeast) {
|
|
info = ITEM_BY_REF('CAPTURED_BEAST', item.baseType ?? item.name)
|
|
} else if (item.category === ItemCategory.Gem) {
|
|
info = ITEM_BY_REF('GEM', item.name)
|
|
} else if (item.category === ItemCategory.MetamorphSample) {
|
|
info = ITEM_BY_REF('ITEM', item.name)
|
|
} else if (item.category === ItemCategory.Voidstone) {
|
|
info = ITEM_BY_REF('ITEM', 'Charged Compass')
|
|
} else if (item.rarity === ItemRarity.Unique && !item.isUnidentified) {
|
|
info = ITEM_BY_REF('UNIQUE', item.name)
|
|
} else {
|
|
info = ITEM_BY_REF('ITEM', item.baseType ?? item.name)
|
|
}
|
|
if (!info?.length) {
|
|
throw new Error('UNKNOWN_ITEM')
|
|
}
|
|
if (info[0].unique) {
|
|
info = info.filter(info => info.unique!.base === item.baseType)
|
|
}
|
|
item.infoVariants = info
|
|
// choose 1st variant, correct one will be picked at the end of parsing
|
|
item.info = info[0]
|
|
// same for every variant
|
|
if (!item.category) {
|
|
if (item.info.craftable) {
|
|
item.category = item.info.craftable.category
|
|
} else if (item.info.unique) {
|
|
item.category = ITEM_BY_REF('ITEM',
|
|
item.info.unique.base)![0].craftable!.category
|
|
}
|
|
}
|
|
}
|
|
|
|
function parseMap (section: string[], item: ParsedItem) {
|
|
if (section[0].startsWith(_$.MAP_TIER)) {
|
|
item.mapTier = Number(section[0].slice(_$.MAP_TIER.length))
|
|
return 'SECTION_PARSED'
|
|
}
|
|
return 'SECTION_SKIPPED'
|
|
}
|
|
|
|
function parseBlightedMap (item: ParsedItem) {
|
|
if (item.category !== ItemCategory.Map) return
|
|
|
|
const calc = item.statsByType.find(calc =>
|
|
calc.type === ModifierType.Implicit &&
|
|
calc.stat.ref.startsWith('Area is infested with Fungal Growths'))
|
|
if (calc !== undefined) {
|
|
if (calc.sources[0].contributes!.value === 9) {
|
|
item.mapBlighted = 'Blight-ravaged'
|
|
item.info.icon = ITEM_BY_REF('ITEM', 'Blight-ravaged Map')![0].icon
|
|
} else {
|
|
item.mapBlighted = 'Blighted'
|
|
item.info.icon = ITEM_BY_REF('ITEM', 'Blighted Map')![0].icon
|
|
}
|
|
}
|
|
}
|
|
|
|
function parseFractured (item: ParserState) {
|
|
if (item.newMods.some(mod => mod.info.type === ModifierType.Fractured)) {
|
|
item.isFractured = true
|
|
}
|
|
}
|
|
|
|
function pickCorrectVariant (item: ParserState) {
|
|
if (!item.info.disc) return
|
|
|
|
for (const variant of item.infoVariants) {
|
|
const cond = variant.disc!
|
|
|
|
if (cond.propAR && !item.armourAR) continue
|
|
if (cond.propEV && !item.armourEV) continue
|
|
if (cond.propES && !item.armourES) continue
|
|
|
|
if (cond.mapTier === 'W' && !(item.mapTier! <= 5)) continue
|
|
if (cond.mapTier === 'Y' && !(item.mapTier! >= 6 && item.mapTier! <= 10)) continue
|
|
if (cond.mapTier === 'R' && !(item.mapTier! >= 11)) continue
|
|
|
|
if (cond.hasImplicit && !item.statsByType.some(calc =>
|
|
calc.type === ModifierType.Implicit &&
|
|
calc.stat.ref === cond.hasImplicit!.ref)
|
|
) continue
|
|
|
|
if (cond.hasExplicit && !item.statsByType.some(calc =>
|
|
calc.type === ModifierType.Explicit &&
|
|
calc.stat.ref === cond.hasExplicit!.ref)
|
|
) continue
|
|
|
|
if (cond.sectionText && !item.rawText.includes(cond.sectionText)) continue
|
|
|
|
item.info = variant
|
|
}
|
|
|
|
// it may happen that we don't find correct variant
|
|
// i.e. corrupted implicit on Two-Stone Ring
|
|
}
|
|
|
|
function parseNamePlate (section: string[]) {
|
|
if (section.length < 3 ||
|
|
!section[0].startsWith(_$.ITEM_CLASS) ||
|
|
!section[1].startsWith(_$.RARITY)) {
|
|
return null
|
|
}
|
|
|
|
const item: ParserState = {
|
|
rarity: undefined,
|
|
category: undefined,
|
|
name: markupConditionParser(section[2]),
|
|
baseType: (section.length >= 4) ? markupConditionParser(section[3]) : undefined,
|
|
isUnidentified: false,
|
|
isCorrupted: false,
|
|
newMods: [],
|
|
statsByType: [],
|
|
unknownModifiers: [],
|
|
influences: [],
|
|
info: undefined!,
|
|
infoVariants: undefined!,
|
|
rawText: undefined!
|
|
}
|
|
|
|
const rarityText = section[1].slice(_$.RARITY.length)
|
|
switch (rarityText) {
|
|
case _$.RARITY_CURRENCY:
|
|
item.category = ItemCategory.Currency
|
|
break
|
|
case _$.RARITY_DIVCARD:
|
|
item.category = ItemCategory.DivinationCard
|
|
break
|
|
case _$.RARITY_GEM:
|
|
item.category = ItemCategory.Gem
|
|
break
|
|
case _$.RARITY_NORMAL:
|
|
item.rarity = ItemRarity.Normal
|
|
break
|
|
case _$.RARITY_MAGIC:
|
|
item.rarity = ItemRarity.Magic
|
|
break
|
|
case _$.RARITY_RARE:
|
|
item.rarity = ItemRarity.Rare
|
|
break
|
|
case _$.RARITY_UNIQUE:
|
|
item.rarity = ItemRarity.Unique
|
|
break
|
|
default:
|
|
return null
|
|
}
|
|
|
|
return item
|
|
}
|
|
|
|
function parseInfluence (section: string[], item: ParsedItem) {
|
|
if (section.length <= 2) {
|
|
const countBefore = item.influences.length
|
|
|
|
for (const line of section) {
|
|
switch (line) {
|
|
case _$.INFLUENCE_CRUSADER:
|
|
item.influences.push(ItemInfluence.Crusader)
|
|
break
|
|
case _$.INFLUENCE_ELDER:
|
|
item.influences.push(ItemInfluence.Elder)
|
|
break
|
|
case _$.INFLUENCE_SHAPER:
|
|
item.influences.push(ItemInfluence.Shaper)
|
|
break
|
|
case _$.INFLUENCE_HUNTER:
|
|
item.influences.push(ItemInfluence.Hunter)
|
|
break
|
|
case _$.INFLUENCE_REDEEMER:
|
|
item.influences.push(ItemInfluence.Redeemer)
|
|
break
|
|
case _$.INFLUENCE_WARLORD:
|
|
item.influences.push(ItemInfluence.Warlord)
|
|
break
|
|
}
|
|
}
|
|
|
|
if (countBefore < item.influences.length) {
|
|
return 'SECTION_PARSED'
|
|
}
|
|
}
|
|
return 'SECTION_SKIPPED'
|
|
}
|
|
|
|
function parseCorrupted (section: string[], item: ParsedItem) {
|
|
if (section[0] === _$.CORRUPTED) {
|
|
item.isCorrupted = true
|
|
return 'SECTION_PARSED'
|
|
} else if (section[0] === _$.UNMODIFIABLE) {
|
|
item.isCorrupted = true
|
|
item.isUnmodifiable = true
|
|
return 'SECTION_PARSED'
|
|
}
|
|
return 'SECTION_SKIPPED'
|
|
}
|
|
|
|
function parseRelic (section: string[], item: ParsedItem) {
|
|
if (item.rarity !== ItemRarity.Unique) {
|
|
return 'PARSER_SKIPPED'
|
|
}
|
|
if (section[0] === _$.RELIC_UNIQUE) {
|
|
item.isRelic = true
|
|
return 'SECTION_PARSED'
|
|
}
|
|
return 'SECTION_SKIPPED'
|
|
}
|
|
|
|
function parseUnidentified (section: string[], item: ParsedItem) {
|
|
if (section[0] === _$.UNIDENTIFIED) {
|
|
item.isUnidentified = true
|
|
return 'SECTION_PARSED'
|
|
}
|
|
return 'SECTION_SKIPPED'
|
|
}
|
|
|
|
function parseItemLevel (section: string[], item: ParsedItem) {
|
|
if (section[0].startsWith(_$.ITEM_LEVEL)) {
|
|
item.itemLevel = Number(section[0].slice(_$.ITEM_LEVEL.length))
|
|
return 'SECTION_PARSED'
|
|
}
|
|
return 'SECTION_SKIPPED'
|
|
}
|
|
|
|
function parseTalismanTier (section: string[], item: ParsedItem) {
|
|
if (section[0].startsWith(_$.TALISMAN_TIER)) {
|
|
item.talismanTier = Number(section[0].slice(_$.TALISMAN_TIER.length))
|
|
return 'SECTION_PARSED'
|
|
}
|
|
return 'SECTION_SKIPPED'
|
|
}
|
|
|
|
function parseVaalGemName (section: string[], item: ParserState) {
|
|
if (item.category !== ItemCategory.Gem) return 'PARSER_SKIPPED'
|
|
|
|
// TODO blocked by https://www.pathofexile.com/forum/view-thread/3231236
|
|
if (section.length === 1) {
|
|
let gemName: string | undefined
|
|
if ((gemName = _$.QUALITY_ANOMALOUS.exec(section[0])?.[1])) {
|
|
item.gemAltQuality = 'Anomalous'
|
|
} else if ((gemName = _$.QUALITY_DIVERGENT.exec(section[0])?.[1])) {
|
|
item.gemAltQuality = 'Divergent'
|
|
} else if ((gemName = _$.QUALITY_PHANTASMAL.exec(section[0])?.[1])) {
|
|
item.gemAltQuality = 'Phantasmal'
|
|
} else if (ITEM_BY_TRANSLATED('GEM', section[0])) {
|
|
gemName = section[0]
|
|
item.gemAltQuality = 'Superior'
|
|
}
|
|
if (gemName) {
|
|
item.name = ITEM_BY_TRANSLATED('GEM', gemName)![0].refName
|
|
return 'SECTION_PARSED'
|
|
}
|
|
}
|
|
return 'SECTION_SKIPPED'
|
|
}
|
|
|
|
function parseGem (section: string[], item: ParsedItem) {
|
|
if (item.category !== ItemCategory.Gem) {
|
|
return 'PARSER_SKIPPED'
|
|
}
|
|
if (section[1]?.startsWith(_$.GEM_LEVEL)) {
|
|
// "Level: 20 (Max)"
|
|
item.gemLevel = parseInt(section[1].slice(_$.GEM_LEVEL.length), 10)
|
|
|
|
parseQualityNested(section, item)
|
|
|
|
return 'SECTION_PARSED'
|
|
}
|
|
return 'SECTION_SKIPPED'
|
|
}
|
|
|
|
function parseGemAltQuality (item: ParserState) {
|
|
if (item.category !== ItemCategory.Gem) return
|
|
|
|
let gemName: string | undefined
|
|
if ((gemName = _$REF.QUALITY_ANOMALOUS.exec(item.name)?.[1])) {
|
|
item.gemAltQuality = 'Anomalous'
|
|
} else if ((gemName = _$REF.QUALITY_DIVERGENT.exec(item.name)?.[1])) {
|
|
item.gemAltQuality = 'Divergent'
|
|
} else if ((gemName = _$REF.QUALITY_PHANTASMAL.exec(item.name)?.[1])) {
|
|
item.gemAltQuality = 'Phantasmal'
|
|
} else {
|
|
item.gemAltQuality = 'Superior'
|
|
}
|
|
if (gemName) {
|
|
item.name = gemName
|
|
}
|
|
}
|
|
|
|
function parseStackSize (section: string[], item: ParsedItem) {
|
|
if (item.rarity !== ItemRarity.Normal &&
|
|
item.category !== ItemCategory.Currency &&
|
|
item.category !== ItemCategory.DivinationCard) {
|
|
return 'PARSER_SKIPPED'
|
|
}
|
|
if (section[0].startsWith(_$.STACK_SIZE)) {
|
|
// Portal Scroll "Stack Size: 2[localized separator]448/40"
|
|
const [value, max] = section[0].slice(_$.STACK_SIZE.length).replace(/[^\d/]/g, '').split('/').map(Number)
|
|
item.stackSize = { value, max }
|
|
|
|
return 'SECTION_PARSED'
|
|
}
|
|
return 'SECTION_SKIPPED'
|
|
}
|
|
|
|
function parseSockets (section: string[], item: ParsedItem) {
|
|
if (section[0].startsWith(_$.SOCKETS)) {
|
|
let sockets = section[0].slice(_$.SOCKETS.length).trimEnd()
|
|
|
|
item.sockets = {
|
|
white: (sockets.split('W').length - 1),
|
|
linked: undefined
|
|
}
|
|
|
|
sockets = sockets.replace(/[^ -]/g, '#')
|
|
if (sockets === '#-#-#-#-#-#') {
|
|
item.sockets.linked = 6
|
|
} else if (
|
|
sockets === '# #-#-#-#-#' ||
|
|
sockets === '#-#-#-#-# #' ||
|
|
sockets === '#-#-#-#-#'
|
|
) {
|
|
item.sockets.linked = 5
|
|
}
|
|
return 'SECTION_PARSED'
|
|
}
|
|
return 'SECTION_SKIPPED'
|
|
}
|
|
|
|
function parseQualityNested (section: string[], item: ParsedItem) {
|
|
for (const line of section) {
|
|
if (line.startsWith(_$.QUALITY)) {
|
|
// "Quality: +20% (augmented)"
|
|
item.quality = parseInt(line.slice(_$.QUALITY.length), 10)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
function parseArmour (section: string[], item: ParsedItem) {
|
|
let isParsed: SectionParseResult = 'SECTION_SKIPPED'
|
|
|
|
for (const line of section) {
|
|
if (line.startsWith(_$.ARMOUR)) {
|
|
item.armourAR = parseInt(line.slice(_$.ARMOUR.length), 10)
|
|
isParsed = 'SECTION_PARSED'; continue
|
|
}
|
|
if (line.startsWith(_$.EVASION)) {
|
|
item.armourEV = parseInt(line.slice(_$.EVASION.length), 10)
|
|
isParsed = 'SECTION_PARSED'; continue
|
|
}
|
|
if (line.startsWith(_$.ENERGY_SHIELD)) {
|
|
item.armourES = parseInt(line.slice(_$.ENERGY_SHIELD.length), 10)
|
|
isParsed = 'SECTION_PARSED'; continue
|
|
}
|
|
if (line.startsWith(_$.TAG_WARD)) {
|
|
item.armourWARD = parseInt(line.slice(_$.TAG_WARD.length), 10)
|
|
isParsed = 'SECTION_PARSED'; continue
|
|
}
|
|
if (line.startsWith(_$.BLOCK_CHANCE)) {
|
|
item.armourBLOCK = parseInt(line.slice(_$.BLOCK_CHANCE.length), 10)
|
|
isParsed = 'SECTION_PARSED'; continue
|
|
}
|
|
}
|
|
|
|
if (isParsed === 'SECTION_PARSED') {
|
|
parseQualityNested(section, item)
|
|
}
|
|
|
|
return isParsed
|
|
}
|
|
|
|
function parseWeapon (section: string[], item: ParsedItem) {
|
|
let isParsed: SectionParseResult = 'SECTION_SKIPPED'
|
|
|
|
for (const line of section) {
|
|
if (line.startsWith(_$.CRIT_CHANCE)) {
|
|
item.weaponCRIT = parseFloat(line.slice(_$.CRIT_CHANCE.length))
|
|
isParsed = 'SECTION_PARSED'; continue
|
|
}
|
|
if (line.startsWith(_$.ATTACK_SPEED)) {
|
|
item.weaponAS = parseFloat(line.slice(_$.ATTACK_SPEED.length))
|
|
isParsed = 'SECTION_PARSED'; continue
|
|
}
|
|
if (line.startsWith(_$.PHYSICAL_DAMAGE)) {
|
|
item.weaponPHYSICAL = getRollOrMinmaxAvg(line
|
|
.slice(_$.PHYSICAL_DAMAGE.length)
|
|
.split('-').map(str => parseInt(str, 10))
|
|
)
|
|
isParsed = 'SECTION_PARSED'; continue
|
|
}
|
|
if (line.startsWith(_$.ELEMENTAL_DAMAGE)) {
|
|
item.weaponELEMENTAL =
|
|
line.slice(_$.ELEMENTAL_DAMAGE.length)
|
|
.split(', ')
|
|
.map(element => getRollOrMinmaxAvg(element.split('-').map(str => parseInt(str, 10))))
|
|
.reduce((sum, x) => sum + x, 0)
|
|
|
|
isParsed = 'SECTION_PARSED'; continue
|
|
}
|
|
}
|
|
|
|
if (isParsed === 'SECTION_PARSED') {
|
|
parseQualityNested(section, item)
|
|
}
|
|
|
|
return isParsed
|
|
}
|
|
|
|
function parseLogbookArea (section: string[], item: ParsedItem) {
|
|
if (item.info.refName !== 'Expedition Logbook') return 'PARSER_SKIPPED'
|
|
if (section.length < 3) return 'SECTION_SKIPPED'
|
|
|
|
// skip Area, parse Faction
|
|
const faction = STAT_BY_MATCH_STR(section[1])
|
|
if (!faction) return 'SECTION_SKIPPED'
|
|
|
|
const areaMods: ParsedModifier[] = [{
|
|
info: { tags: [], type: ModifierType.Pseudo },
|
|
stats: [{
|
|
stat: faction.stat,
|
|
translation: faction.matcher
|
|
}]
|
|
}]
|
|
|
|
const { modType, lines } = parseModType(section.slice(2))
|
|
for (const line of lines) {
|
|
const found = STAT_BY_MATCH_STR(line)
|
|
if (found && found.stat.ref === 'Area contains an Expedition Boss (#)') {
|
|
const roll = found.matcher.value!
|
|
areaMods.push({
|
|
info: { tags: [], type: modType },
|
|
stats: [{
|
|
stat: found.stat,
|
|
translation: found.matcher,
|
|
roll: { value: roll, min: roll, max: roll, dp: false, unscalable: true }
|
|
}]
|
|
})
|
|
}
|
|
}
|
|
|
|
if (!item.logbookAreaMods) {
|
|
item.logbookAreaMods = [areaMods]
|
|
} else {
|
|
item.logbookAreaMods.push(areaMods)
|
|
}
|
|
|
|
return 'SECTION_PARSED'
|
|
}
|
|
|
|
function parseModifiers (section: string[], item: ParsedItem) {
|
|
if (
|
|
item.rarity !== ItemRarity.Normal &&
|
|
item.rarity !== ItemRarity.Magic &&
|
|
item.rarity !== ItemRarity.Rare &&
|
|
item.rarity !== ItemRarity.Unique
|
|
) {
|
|
return 'PARSER_SKIPPED'
|
|
}
|
|
|
|
const recognizedLine = section.find(line =>
|
|
line.endsWith(ENCHANT_LINE) ||
|
|
line.endsWith(SCOURGE_LINE) ||
|
|
isModInfoLine(line)
|
|
)
|
|
|
|
if (!recognizedLine) {
|
|
return 'SECTION_SKIPPED'
|
|
}
|
|
|
|
if (isModInfoLine(recognizedLine)) {
|
|
for (const { modLine, statLines } of groupLinesByMod(section)) {
|
|
const { modType, lines } = parseModType(statLines)
|
|
const modInfo = parseModInfoLine(modLine, modType)
|
|
parseStatsFromMod(lines, item, { info: modInfo, stats: [] })
|
|
|
|
if (modType === ModifierType.Veiled) {
|
|
item.isVeiled = true
|
|
}
|
|
}
|
|
} else {
|
|
const { lines } = parseModType(section)
|
|
const modInfo: ModifierInfo = {
|
|
type: recognizedLine.endsWith(ENCHANT_LINE)
|
|
? ModifierType.Enchant
|
|
: ModifierType.Scourge,
|
|
tags: []
|
|
}
|
|
parseStatsFromMod(lines, item, { info: modInfo, stats: [] })
|
|
}
|
|
|
|
return 'SECTION_PARSED'
|
|
}
|
|
|
|
function parseMirrored (section: string[], item: ParsedItem) {
|
|
if (section.length === 1) {
|
|
if (section[0] === _$.MIRRORED) {
|
|
item.isMirrored = true
|
|
return 'SECTION_PARSED'
|
|
}
|
|
}
|
|
return 'SECTION_SKIPPED'
|
|
}
|
|
|
|
function parseFlask (section: string[], item: ParsedItem) {
|
|
// the purpose of this parser is to "consume" flask buffs
|
|
// so they are not recognized as modifiers
|
|
|
|
let isParsed: SectionParseResult = 'SECTION_SKIPPED'
|
|
|
|
for (const line of section) {
|
|
if (_$.FLASK_CHARGES.test(line)) {
|
|
isParsed = 'SECTION_PARSED'; break
|
|
}
|
|
}
|
|
|
|
if (isParsed) {
|
|
parseQualityNested(section, item)
|
|
}
|
|
|
|
return isParsed
|
|
}
|
|
|
|
function parseSentinelCharge (section: string[], item: ParsedItem) {
|
|
if (item.category !== ItemCategory.Sentinel) return 'PARSER_SKIPPED'
|
|
|
|
if (section.length === 1) {
|
|
if (section[0].startsWith(_$.SENTINEL_CHARGE)) {
|
|
item.sentinelCharge = parseInt(section[0].slice(_$.SENTINEL_CHARGE.length), 10)
|
|
return 'SECTION_PARSED'
|
|
}
|
|
}
|
|
return 'SECTION_SKIPPED'
|
|
}
|
|
|
|
function parseSynthesised (section: string[], item: ParserState) {
|
|
if (section.length === 1) {
|
|
if (section[0] === _$.SECTION_SYNTHESISED) {
|
|
item.isSynthesised = true
|
|
if (item.baseType) {
|
|
item.baseType = _$REF.ITEM_SYNTHESISED.exec(item.baseType)![1]
|
|
} else {
|
|
item.name = _$REF.ITEM_SYNTHESISED.exec(item.name)![1]
|
|
}
|
|
return 'SECTION_PARSED'
|
|
}
|
|
}
|
|
|
|
return 'SECTION_SKIPPED'
|
|
}
|
|
|
|
function parseSuperior (item: ParserState) {
|
|
if (
|
|
(item.rarity === ItemRarity.Normal) ||
|
|
(item.rarity === ItemRarity.Magic && item.isUnidentified) ||
|
|
(item.rarity === ItemRarity.Rare && item.isUnidentified) ||
|
|
(item.rarity === ItemRarity.Unique && item.isUnidentified)
|
|
) {
|
|
if (_$REF.ITEM_SUPERIOR.test(item.name)) {
|
|
item.name = _$REF.ITEM_SUPERIOR.exec(item.name)![1]
|
|
}
|
|
}
|
|
}
|
|
|
|
function parseCategoryByHelpText (section: string[], item: ParsedItem) {
|
|
if (section[0] === _$.BEAST_HELP) {
|
|
item.category = ItemCategory.CapturedBeast
|
|
return 'SECTION_PARSED'
|
|
} else if (section[0] === _$.METAMORPH_HELP) {
|
|
item.category = ItemCategory.MetamorphSample
|
|
return 'SECTION_PARSED'
|
|
} else if (section[0] === _$.VOIDSTONE_HELP) {
|
|
item.category = ItemCategory.Voidstone
|
|
return 'SECTION_PARSED'
|
|
}
|
|
|
|
return 'SECTION_SKIPPED'
|
|
}
|
|
|
|
function parseHeistBlueprint (section: string[], item: ParsedItem) {
|
|
if (item.category !== ItemCategory.HeistBlueprint) return 'PARSER_SKIPPED'
|
|
|
|
parseAreaLevelNested(section, item)
|
|
if (!item.areaLevel) {
|
|
return 'SECTION_SKIPPED'
|
|
}
|
|
|
|
item.heist = {}
|
|
|
|
for (const line of section) {
|
|
if (line.startsWith(_$.HEIST_TARGET)) {
|
|
const targetText = line.slice(_$.HEIST_TARGET.length)
|
|
switch (targetText) {
|
|
case _$.HEIST_BLUEPRINT_ENCHANTS:
|
|
item.heist.target = 'Enchants'; break
|
|
case _$.HEIST_BLUEPRINT_GEMS:
|
|
item.heist.target = 'Gems'; break
|
|
case _$.HEIST_BLUEPRINT_REPLICAS:
|
|
item.heist.target = 'Replicas'; break
|
|
case _$.HEIST_BLUEPRINT_TRINKETS:
|
|
item.heist.target = 'Trinkets'; break
|
|
}
|
|
} else if (line.startsWith(_$.HEIST_WINGS_REVEALED)) {
|
|
item.heist.wingsRevealed = parseInt(line.slice(_$.HEIST_WINGS_REVEALED.length), 10)
|
|
}
|
|
}
|
|
|
|
return 'SECTION_PARSED'
|
|
}
|
|
|
|
function parseAreaLevelNested (section: string[], item: ParsedItem) {
|
|
for (const line of section) {
|
|
if (line.startsWith(_$.AREA_LEVEL)) {
|
|
item.areaLevel = Number(line.slice(_$.AREA_LEVEL.length))
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
function parseAreaLevel (section: string[], item: ParsedItem) {
|
|
if (
|
|
item.info.refName !== 'Chronicle of Atzoatl' &&
|
|
item.info.refName !== 'Expedition Logbook' &&
|
|
item.info.refName !== 'Mirrored Tablet'
|
|
) return 'PARSER_SKIPPED'
|
|
|
|
parseAreaLevelNested(section, item)
|
|
|
|
return (item.areaLevel)
|
|
? 'SECTION_PARSED'
|
|
: 'SECTION_SKIPPED'
|
|
}
|
|
|
|
function parseAtzoatlRooms (section: string[], item: ParsedItem) {
|
|
if (item.info.refName !== 'Chronicle of Atzoatl') return 'PARSER_SKIPPED'
|
|
if (section[0] !== _$.INCURSION_OPEN) return 'SECTION_SKIPPED'
|
|
|
|
let state = IncursionRoom.Open
|
|
for (const line of section.slice(1)) {
|
|
if (line === _$.INCURSION_OBSTRUCTED) {
|
|
state = IncursionRoom.Obstructed
|
|
continue
|
|
}
|
|
|
|
const found = STAT_BY_MATCH_STR(line)
|
|
if (found) {
|
|
item.newMods.push({
|
|
info: { tags: [], type: ModifierType.Pseudo },
|
|
stats: [{
|
|
stat: found.stat,
|
|
translation: {
|
|
string: (state === IncursionRoom.Open)
|
|
? found.matcher.string
|
|
: `${_$.INCURSION_OBSTRUCTED} ${found.matcher.string}`
|
|
},
|
|
roll: { value: state, min: state, max: state, dp: false, unscalable: true }
|
|
}]
|
|
})
|
|
} else {
|
|
item.unknownModifiers.push({
|
|
text: line,
|
|
type: ModifierType.Pseudo
|
|
})
|
|
}
|
|
}
|
|
|
|
return 'SECTION_PARSED'
|
|
}
|
|
|
|
function parseMirroredTablet (section: string[], item: ParsedItem) {
|
|
if (item.info.refName !== 'Mirrored Tablet') return 'PARSER_SKIPPED'
|
|
if (section.length < 8) return 'SECTION_SKIPPED'
|
|
|
|
for (const line of section) {
|
|
const found = tryParseTranslation({ string: line, unscalable: true }, ModifierType.Pseudo)
|
|
if (found) {
|
|
item.newMods.push({
|
|
info: { tags: [], type: ModifierType.Pseudo },
|
|
stats: [found]
|
|
})
|
|
} else {
|
|
item.unknownModifiers.push({
|
|
text: line,
|
|
type: ModifierType.Pseudo
|
|
})
|
|
}
|
|
}
|
|
|
|
return 'SECTION_PARSED'
|
|
}
|
|
|
|
function markupConditionParser (text: string) {
|
|
// ignores state set by <<set:__>>
|
|
// always evaluates first condition to true <if:__>{...}
|
|
// full markup: https://gist.github.com/SnosMe/151549b532df8ea08025a76ae2920ca4
|
|
|
|
text = text.replace(/<<set:.+?>>/g, '')
|
|
text = text.replace(/<(if:.+?|elif:.+?|else)>{(.+?)}/g, (_, type: string, body: string) => {
|
|
return type.startsWith('if:')
|
|
? body
|
|
: ''
|
|
})
|
|
|
|
return text
|
|
}
|
|
|
|
function parseStatsFromMod (lines: string[], item: ParsedItem, modifier: ParsedModifier) {
|
|
item.newMods.push(modifier)
|
|
|
|
if (modifier.info.type === ModifierType.Veiled) {
|
|
const found = STAT_BY_MATCH_STR(modifier.info.name!)
|
|
if (found) {
|
|
modifier.stats.push({
|
|
stat: found.stat,
|
|
translation: found.matcher
|
|
})
|
|
} else {
|
|
item.unknownModifiers.push({
|
|
text: modifier.info.name!,
|
|
type: modifier.info.type
|
|
})
|
|
}
|
|
return
|
|
}
|
|
|
|
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.unknownModifiers.push(...stat.value.map(line => ({
|
|
text: line,
|
|
type: modifier.info.type
|
|
})))
|
|
}
|
|
|
|
/**
|
|
* @deprecated
|
|
*/
|
|
function transformToLegacyModifiers (item: ParsedItem) {
|
|
item.statsByType = sumStatsByModType(item.newMods)
|
|
}
|
|
|
|
function calcBasePercentile (item: ParsedItem) {
|
|
if (!item.info.armour) return
|
|
// Base percentile is the same for all defences.
|
|
// Using `AR/EV -> ES -> WARD` order to improve accuracy
|
|
// of calculation (larger rolls = more precise).
|
|
const info = item.info.armour
|
|
if (item.armourAR) {
|
|
item.basePercentile = calcPropPercentile(item.armourAR, info.ar!, QUALITY_STATS.ARMOUR, item)
|
|
} else if (item.armourEV) {
|
|
item.basePercentile = calcPropPercentile(item.armourEV, info.ev!, QUALITY_STATS.EVASION, item)
|
|
} else if (item.armourES) {
|
|
item.basePercentile = calcPropPercentile(item.armourES, info.es!, QUALITY_STATS.ENERGY_SHIELD, item)
|
|
} else if (item.armourWARD) {
|
|
item.basePercentile = calcPropPercentile(item.armourWARD, info.ward!, QUALITY_STATS.WARD, item)
|
|
}
|
|
}
|
|
|
|
export function removeLinesEnding (
|
|
lines: readonly string[], ending: string
|
|
): string[] {
|
|
return lines.map(line =>
|
|
line.endsWith(ending)
|
|
? line.slice(0, -ending.length)
|
|
: line
|
|
)
|
|
}
|