mirror of
https://github.com/Kvan7/Exiled-Exchange-2.git
synced 2025-12-14 20:15:51 +00:00
* Add config for default rune #347 * Jarngreipr - Wrong roll for life #343 * [PoE2] - Quality gets applied to corrupted items #369 * Addtional pdr not showing tier #371 * [Parse Error] - Unidentified items #360 * Filter to only Normal Rarity #350 * Add note to complex query error specifically #344 * continue script setup refactoring * extract default config into widgets * remove icon special cases * continue moving files * remove another special case * Apply fixes for upstream update * Add options to choose which numbering system to use #357 * Change to use "PoE2 tier numbering" #356 * Life regen tiers broken #361 * Update translations #321 * Add new strings * Add field in settings to cycle through them all #359 * Add this box to some UIs #358 * More fixes for tips * Features/elementalDamage (#387) * Adds ele options * add text and color * dynamic display of ele filters * fill default min value * Filter actually changes now * Add filter into site * Increase ratio slightly * [Parse Error] - Collector's Ritual Precursor Tablet of the Dogma #206 * Fix help text for tablets * Update translations * [Not Recognized Modifier] - Quality (Attribute Modifiers): +20% (augmented) #186 * remove unused setting * version bump * Controlled Metamorphosis is searched incorrectly after the trade site was updated #394 * hide tip 17 for now
1622 lines
43 KiB
TypeScript
1622 lines
43 KiB
TypeScript
import { Result, ok, err } from "neverthrow";
|
|
import {
|
|
CLIENT_STRINGS as _$,
|
|
// CLIENT_STRINGS_REF as _$REF,
|
|
ITEM_BY_TRANSLATED,
|
|
ITEM_BY_REF,
|
|
STAT_BY_MATCH_STR,
|
|
BaseType,
|
|
RUNE_SINGLE_VALUE,
|
|
} from "@/assets/data";
|
|
import { ModifierType, StatCalculated, 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,
|
|
IMPLICIT_LINE,
|
|
RUNE_LINE,
|
|
isModInfoLine,
|
|
groupLinesByMod,
|
|
parseModInfoLine,
|
|
} 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) => Result<never, string> | void;
|
|
|
|
interface ParserState extends ParsedItem {
|
|
name: string;
|
|
baseType: string | undefined;
|
|
infoVariants: BaseType[];
|
|
}
|
|
|
|
const parsers: Array<ParserFn | { virtual: VirtualParserFn }> = [
|
|
parseUnidentified,
|
|
{ virtual: parseSuperior },
|
|
parseSynthesised,
|
|
parseCategoryByHelpText,
|
|
{ virtual: normalizeName },
|
|
parseVaalGemName,
|
|
{ virtual: findInDatabase },
|
|
{ virtual: tempErrLogbooks },
|
|
// -----------
|
|
parseItemLevel,
|
|
parseRequirements,
|
|
parseTalismanTier,
|
|
parseGem,
|
|
parseArmour,
|
|
parseWeapon,
|
|
parseFlask,
|
|
parseJewelery,
|
|
parseCharmSlots,
|
|
parseSpirit,
|
|
parsePriceNote,
|
|
parseHelpText,
|
|
parseStackSize,
|
|
parseCorrupted,
|
|
parseFoil,
|
|
parseInfluence,
|
|
parseMap,
|
|
parseWaystone,
|
|
parseSockets,
|
|
parseRuneSockets,
|
|
parseHeistBlueprint,
|
|
parseAreaLevel,
|
|
parseAtzoatlRooms,
|
|
parseMirroredTablet,
|
|
parseFilledCoffin,
|
|
parseMirrored,
|
|
parseSentinelCharge,
|
|
parseLogbookArea,
|
|
parseLogbookArea,
|
|
parseLogbookArea,
|
|
parseModifiersPoe2, // enchant
|
|
parseModifiersPoe2, // rune
|
|
parseModifiersPoe2, // implicit
|
|
parseModifiersPoe2, // explicit
|
|
{ virtual: transformToLegacyModifiers },
|
|
{ virtual: parseFractured },
|
|
{ virtual: parseBlightedMap },
|
|
{ virtual: applyRuneSockets },
|
|
{ virtual: applyElementalAdded },
|
|
{ virtual: pickCorrectVariant },
|
|
{ virtual: calcBasePercentile },
|
|
];
|
|
|
|
export function parseClipboard(clipboard: string): Result<ParsedItem, string> {
|
|
try {
|
|
const chatRegex = /\[.*?\]|\[.*?\|.*?\]/;
|
|
const isFromChat = chatRegex.test(clipboard);
|
|
if (isFromChat) {
|
|
clipboard = parseAffixStrings(clipboard);
|
|
}
|
|
let sections = itemTextToSections(clipboard);
|
|
|
|
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.isOk()) return parsed;
|
|
|
|
sections.shift();
|
|
parsed.value.rawText = clipboard;
|
|
|
|
// each section can be parsed at most by one parser
|
|
for (const parser of parsers) {
|
|
if (typeof parser === "object") {
|
|
const error = parser.virtual(parsed.value);
|
|
if (error) return error;
|
|
continue;
|
|
}
|
|
for (const section of sections) {
|
|
const result = parser(section, parsed.value);
|
|
if (result === "SECTION_PARSED") {
|
|
sections = sections.filter((s) => s !== section);
|
|
break;
|
|
} else if (result === "PARSER_SKIPPED") {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (parsed.isOk() && isFromChat) {
|
|
parsed.value.fromChat = isFromChat;
|
|
}
|
|
return Object.freeze(parsed);
|
|
} catch (e) {
|
|
console.log(e);
|
|
return err("item.parse_error");
|
|
}
|
|
}
|
|
|
|
function itemTextToSections(text: string) {
|
|
const lines = text.split(/\r?\n/);
|
|
if (lines[lines.length - 1] === "") {
|
|
lines.pop();
|
|
}
|
|
|
|
const sections: string[][] = [[]];
|
|
lines.reduce((section, line) => {
|
|
if (line !== "--------") {
|
|
section.push(line);
|
|
return section;
|
|
} else {
|
|
const section: string[] = [];
|
|
sections.push(section);
|
|
return section;
|
|
}
|
|
}, sections[0]);
|
|
return sections.filter((section) => section.length);
|
|
}
|
|
|
|
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 (_$.MAP_BLIGHTED.test(item.baseType)) {
|
|
item.baseType = _$.MAP_BLIGHTED.exec(item.baseType)![1];
|
|
} else if (_$.MAP_BLIGHT_RAVAGED.test(item.baseType)) {
|
|
item.baseType = _$.MAP_BLIGHT_RAVAGED.exec(item.baseType)![1];
|
|
}
|
|
} else {
|
|
if (_$.MAP_BLIGHTED.test(item.name)) {
|
|
item.name = _$.MAP_BLIGHTED.exec(item.name)![1];
|
|
} else if (_$.MAP_BLIGHT_RAVAGED.test(item.name)) {
|
|
item.name = _$.MAP_BLIGHT_RAVAGED.exec(item.name)![1];
|
|
}
|
|
}
|
|
}
|
|
|
|
if (item.category === ItemCategory.MetamorphSample) {
|
|
if (_$.METAMORPH_BRAIN.test(item.name)) {
|
|
item.name = "Metamorph Brain";
|
|
} else if (_$.METAMORPH_EYE.test(item.name)) {
|
|
item.name = "Metamorph Eye";
|
|
} else if (_$.METAMORPH_LUNG.test(item.name)) {
|
|
item.name = "Metamorph Lung";
|
|
} else if (_$.METAMORPH_HEART.test(item.name)) {
|
|
item.name = "Metamorph Heart";
|
|
} else if (_$.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_TRANSLATED("DIVINATION_CARD", item.name);
|
|
} else if (item.category === ItemCategory.CapturedBeast) {
|
|
info = ITEM_BY_TRANSLATED("CAPTURED_BEAST", item.baseType ?? item.name);
|
|
} else if (item.category === ItemCategory.Gem) {
|
|
info = ITEM_BY_TRANSLATED("GEM", item.name);
|
|
} else if (item.category === ItemCategory.MetamorphSample) {
|
|
info = ITEM_BY_TRANSLATED("ITEM", item.name);
|
|
} else if (item.category === ItemCategory.Voidstone) {
|
|
info = ITEM_BY_TRANSLATED("ITEM", "Charged Compass");
|
|
} else if (item.rarity === ItemRarity.Unique && !item.isUnidentified) {
|
|
info = ITEM_BY_TRANSLATED("UNIQUE", item.name);
|
|
} else {
|
|
info = ITEM_BY_TRANSLATED("ITEM", item.baseType ?? item.name);
|
|
}
|
|
if (!info?.length) {
|
|
return err("item.unknown");
|
|
}
|
|
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_TRANSLATED(
|
|
"ITEM",
|
|
item.info.unique.base,
|
|
)![0].craftable!.category;
|
|
}
|
|
}
|
|
|
|
// Override charm since its flask in trade
|
|
if (item.category === ItemCategory.Charm) {
|
|
item.category = ItemCategory.Flask;
|
|
}
|
|
}
|
|
|
|
function tempErrLogbooks(item: ParserState) {
|
|
if (item.info.refName === "Expedition Logbook") {
|
|
return err("item.parse_error");
|
|
}
|
|
}
|
|
|
|
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 parseWaystone(section: string[], item: ParsedItem) {
|
|
if (section[0].startsWith(_$.WAYSTONE_TIER)) {
|
|
item.mapTier = Number(section[0].slice(_$.WAYSTONE_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[]) {
|
|
let line = section.shift();
|
|
if (!line?.startsWith(_$.ITEM_CLASS)) {
|
|
return err("item.parse_error");
|
|
}
|
|
|
|
line = section.shift();
|
|
let rarityText: string | undefined;
|
|
if (line?.startsWith(_$.RARITY)) {
|
|
rarityText = line.slice(_$.RARITY.length);
|
|
line = section.shift();
|
|
}
|
|
|
|
let name: string;
|
|
if (line != null) {
|
|
name = markupConditionParser(line);
|
|
} else {
|
|
return err("item.parse_error");
|
|
}
|
|
|
|
line = section.shift();
|
|
const baseType = line && markupConditionParser(line);
|
|
|
|
const item: ParserState = {
|
|
rarity: undefined,
|
|
category: undefined,
|
|
name,
|
|
baseType,
|
|
isUnidentified: false,
|
|
isCorrupted: false,
|
|
newMods: [],
|
|
statsByType: [],
|
|
unknownModifiers: [],
|
|
influences: [],
|
|
info: undefined!,
|
|
infoVariants: undefined!,
|
|
rawText: undefined!,
|
|
};
|
|
|
|
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:
|
|
case _$.RARITY_QUEST:
|
|
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;
|
|
}
|
|
|
|
return ok(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].trim() === _$.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 parseFoil(section: string[], item: ParsedItem) {
|
|
if (item.rarity !== ItemRarity.Unique) {
|
|
return "PARSER_SKIPPED";
|
|
}
|
|
if (section[0] === _$.FOIL_UNIQUE) {
|
|
item.isFoil = 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) {
|
|
let prefix = _$.ITEM_LEVEL;
|
|
if (item.info.refName === "Filled Coffin") {
|
|
prefix = _$.CORPSE_LEVEL;
|
|
}
|
|
|
|
for (const line of section) {
|
|
if (line.startsWith(prefix)) {
|
|
item.itemLevel = Number(line.slice(prefix.length));
|
|
return "SECTION_PARSED";
|
|
}
|
|
}
|
|
return "SECTION_SKIPPED";
|
|
}
|
|
|
|
function parseRequirements(section: string[], item: ParsedItem) {
|
|
if (section[0].startsWith(_$.REQUIREMENTS)) {
|
|
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 (ITEM_BY_TRANSLATED("GEM", section[0])) {
|
|
gemName = section[0];
|
|
}
|
|
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 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 parseRuneSockets(section: string[], item: ParsedItem) {
|
|
const categoryMax = getMaxSockets(item.category);
|
|
const armourOrWeapon = categoryMax && isArmourOrWeapon(item.category);
|
|
if (!armourOrWeapon) return "PARSER_SKIPPED";
|
|
if (section[0].startsWith(_$.SOCKETS)) {
|
|
const sockets = section[0].slice(_$.SOCKETS.length).trimEnd();
|
|
const current = sockets.split("S").length - 1;
|
|
item.runeSockets = {
|
|
type: armourOrWeapon,
|
|
empty: 0,
|
|
current,
|
|
normal: categoryMax,
|
|
};
|
|
|
|
return "SECTION_PARSED";
|
|
}
|
|
if (categoryMax) {
|
|
item.runeSockets = {
|
|
type: armourOrWeapon,
|
|
empty: categoryMax,
|
|
current: 0,
|
|
normal: categoryMax,
|
|
};
|
|
}
|
|
return "SECTION_SKIPPED";
|
|
}
|
|
|
|
function parseSockets(section: string[], item: ParsedItem) {
|
|
if (item.category === ItemCategory.Gem && section[0].startsWith(_$.SOCKETS)) {
|
|
let sockets = section[0].slice(_$.SOCKETS.length).trimEnd();
|
|
sockets = sockets.replace(/[^ -]/g, "#");
|
|
|
|
item.gemSockets = {
|
|
number: sockets.split("#").length - 1,
|
|
white: sockets.split("W").length - 1,
|
|
linked: undefined,
|
|
};
|
|
|
|
if (sockets === "#-#-#-#-#-#") {
|
|
item.gemSockets.linked = 6;
|
|
} else if (
|
|
sockets === "# #-#-#-#-#" ||
|
|
sockets === "#-#-#-#-# #" ||
|
|
sockets === "#-#-#-#-#"
|
|
) {
|
|
item.gemSockets.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(_$.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 (line.startsWith(_$.FIRE_DAMAGE)) {
|
|
const fireDamage = line
|
|
.slice(_$.FIRE_DAMAGE.length)
|
|
.split(", ")
|
|
.map((element) =>
|
|
getRollOrMinmaxAvg(
|
|
element.split("-").map((str) => parseInt(str, 10)),
|
|
),
|
|
)
|
|
.reduce((sum, x) => sum + x, 0);
|
|
if (item.weaponELEMENTAL) {
|
|
item.weaponELEMENTAL = fireDamage + item.weaponELEMENTAL;
|
|
} else {
|
|
item.weaponELEMENTAL = fireDamage;
|
|
}
|
|
isParsed = "SECTION_PARSED";
|
|
continue;
|
|
}
|
|
if (line.startsWith(_$.COLD_DAMAGE)) {
|
|
const coldDamage = line
|
|
.slice(_$.COLD_DAMAGE.length)
|
|
.split(", ")
|
|
.map((element) =>
|
|
getRollOrMinmaxAvg(
|
|
element.split("-").map((str) => parseInt(str, 10)),
|
|
),
|
|
)
|
|
.reduce((sum, x) => sum + x, 0);
|
|
if (item.weaponELEMENTAL) {
|
|
item.weaponELEMENTAL = coldDamage + item.weaponELEMENTAL;
|
|
} else {
|
|
item.weaponELEMENTAL = coldDamage;
|
|
}
|
|
isParsed = "SECTION_PARSED";
|
|
continue;
|
|
}
|
|
if (line.startsWith(_$.LIGHTNING_DAMAGE)) {
|
|
const lightningDamage = line
|
|
.slice(_$.LIGHTNING_DAMAGE.length)
|
|
.split(", ")
|
|
.map((element) =>
|
|
getRollOrMinmaxAvg(
|
|
element.split("-").map((str) => parseInt(str, 10)),
|
|
),
|
|
)
|
|
.reduce((sum, x) => sum + x, 0);
|
|
if (item.weaponELEMENTAL) {
|
|
item.weaponELEMENTAL = lightningDamage + item.weaponELEMENTAL;
|
|
} else {
|
|
item.weaponELEMENTAL = lightningDamage;
|
|
}
|
|
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";
|
|
}
|
|
/**
|
|
*
|
|
*
|
|
*
|
|
*
|
|
* Item Class: Gloves
|
|
Rarity: Rare
|
|
Golem Grasp
|
|
Precursor Gauntlets
|
|
--------
|
|
Quality: +20% (augmented)
|
|
Armour: 1030 (augmented)
|
|
--------
|
|
Requirements:
|
|
Level: 78
|
|
Str: 155
|
|
Dex: 68
|
|
Int: 27
|
|
--------
|
|
Sockets: R-R-R-R
|
|
--------
|
|
Item Level: 86
|
|
--------
|
|
+5% chance to Suppress Spell Damage (implicit)
|
|
8% increased Attack Speed (implicit)
|
|
--------
|
|
+32 to Strength
|
|
+101 to Armour
|
|
89% increased Armour
|
|
+98 to maximum Life
|
|
+48% to Fire Resistance
|
|
+29% to Lightning Resistance
|
|
Searing Exarch Item
|
|
Eater of Worlds Item
|
|
|
|
*
|
|
*
|
|
* Item Class: Gloves
|
|
Rarity: Rare
|
|
Golem Grasp
|
|
Precursor Gauntlets
|
|
--------
|
|
Quality: +20% (augmented)
|
|
Armour: 1030 (augmented)
|
|
--------
|
|
Requirements:
|
|
Level: 78
|
|
Str: 155
|
|
Dex: 68
|
|
Int: 27
|
|
--------
|
|
Sockets: R-R-R-R
|
|
--------
|
|
Item Level: 86
|
|
--------
|
|
{ Searing Exarch Implicit Modifier (Lesser) — Attack, Speed }
|
|
8% increased Attack Speed (implicit)
|
|
{ Eater of Worlds Implicit Modifier (Lesser) }
|
|
+5% chance to Suppress Spell Damage (implicit)
|
|
(50% of Damage from Suppressed Hits and Ailments they inflict is prevented) (implicit)
|
|
--------
|
|
{ Prefix Modifier "Plated" (Tier: 3) — Defences, Armour }
|
|
+101(83-101) to Armour
|
|
{ Prefix Modifier "Girded" (Tier: 2) — Defences, Armour }
|
|
89(80-91)% increased Armour
|
|
{ Prefix Modifier "Rotund" (Tier: 3) — Life }
|
|
+98(85-99) to maximum Life
|
|
{ Suffix Modifier "of the Thunderhead" (Tier: 5) — Elemental, Lightning, Resistance }
|
|
+29(24-29)% to Lightning Resistance
|
|
{ Suffix Modifier "of Tzteosh" (Tier: 1) — Elemental, Fire, Resistance }
|
|
+48(46-48)% to Fire Resistance
|
|
{ Suffix Modifier "of the Gorilla" (Tier: 5) — Attribute }
|
|
+32(28-32) to Strength
|
|
Searing Exarch Item
|
|
Eater of Worlds Item
|
|
|
|
*
|
|
*
|
|
* Item Class: Rings
|
|
Rarity: Rare
|
|
Morbid Whorl
|
|
Ruby Ring
|
|
--------
|
|
Requirements:
|
|
Level: 23
|
|
--------
|
|
Item Level: 39
|
|
--------
|
|
+27% to Fire Resistance (implicit)
|
|
--------
|
|
+33 to Accuracy Rating
|
|
+35 to Evasion Rating
|
|
+13% to Lightning Resistance
|
|
38% increased Mana Regeneration Rate
|
|
|
|
|
|
*
|
|
*/
|
|
|
|
export function parseModifiersPoe2(section: string[], item: ParsedItem) {
|
|
if (
|
|
item.rarity !== ItemRarity.Normal &&
|
|
item.rarity !== ItemRarity.Magic &&
|
|
item.rarity !== ItemRarity.Rare &&
|
|
item.rarity !== ItemRarity.Unique
|
|
) {
|
|
return "PARSER_SKIPPED";
|
|
}
|
|
|
|
let foundAnyMods = false;
|
|
|
|
const enchantOrScourgeOrRune = section.find(
|
|
(line) =>
|
|
line.endsWith(ENCHANT_LINE) ||
|
|
line.endsWith(SCOURGE_LINE) ||
|
|
line.endsWith(RUNE_LINE),
|
|
);
|
|
|
|
if (enchantOrScourgeOrRune) {
|
|
const { lines } = parseModType(section);
|
|
const modInfo: ModifierInfo = {
|
|
type: enchantOrScourgeOrRune.endsWith(ENCHANT_LINE)
|
|
? ModifierType.Enchant
|
|
: enchantOrScourgeOrRune.endsWith(SCOURGE_LINE)
|
|
? ModifierType.Scourge
|
|
: ModifierType.Rune,
|
|
tags: [],
|
|
};
|
|
foundAnyMods = parseStatsFromMod(lines, item, { info: modInfo, stats: [] });
|
|
} else {
|
|
for (const statLines of section) {
|
|
let { modType, lines } = parseModType([statLines]);
|
|
if (
|
|
modType === ModifierType.Explicit &&
|
|
item.category === ItemCategory.Relic
|
|
) {
|
|
modType = ModifierType.Sanctum;
|
|
}
|
|
// const modInfo = parseModInfoLine(modLine, modType);
|
|
const found = parseStatsFromMod(lines, item, {
|
|
info: { type: modType, tags: [] },
|
|
stats: [],
|
|
});
|
|
foundAnyMods = found || foundAnyMods;
|
|
|
|
if (modType === ModifierType.Veiled) {
|
|
item.isVeiled = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return foundAnyMods ? "SECTION_PARSED" : "SECTION_SKIPPED";
|
|
}
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
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 applyRuneSockets(item: ParsedItem) {
|
|
// If we have any rune sockets
|
|
if (item.runeSockets) {
|
|
// Count current mods that are of type Rune
|
|
const runeMods = item.newMods.filter(
|
|
(mod) => mod.info.type === ModifierType.Rune,
|
|
);
|
|
const runeStats = item.statsByType.filter(
|
|
(calc) => calc.type === ModifierType.Rune,
|
|
);
|
|
const runes = runeMods
|
|
.map((mod) => {
|
|
const stat = runeStats.find(
|
|
(stat) => stat.sources[0].stat === mod.stats[0],
|
|
);
|
|
if (!stat) return [];
|
|
return runeCount(mod, stat);
|
|
})
|
|
.flat();
|
|
|
|
const potentialEmptySockets =
|
|
Math.max(item.runeSockets.normal, item.runeSockets.current) -
|
|
runes.reduce((x, y) => x + y, 0);
|
|
|
|
item.runeSockets.empty = potentialEmptySockets;
|
|
}
|
|
}
|
|
|
|
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 parseJewelery(section: string[], item: ParsedItem) {
|
|
if (
|
|
item.category !== ItemCategory.Amulet &&
|
|
item.category !== ItemCategory.Ring &&
|
|
item.category !== ItemCategory.Belt
|
|
) {
|
|
return "PARSER_SKIPPED";
|
|
}
|
|
|
|
for (const line of section) {
|
|
if (line.startsWith(_$.QUALITY.substring(0, _$.QUALITY.indexOf(":")))) {
|
|
return "SECTION_PARSED";
|
|
}
|
|
}
|
|
|
|
return "SECTION_SKIPPED";
|
|
}
|
|
|
|
function parseCharmSlots(section: string[], item: ParsedItem) {
|
|
// the purpose of this parser is to "consume" charm slot 1 sections
|
|
// so they are not recognized as modifiers
|
|
if (item.category !== ItemCategory.Belt) return "PARSER_SKIPPED";
|
|
|
|
let isParsed: SectionParseResult = "SECTION_SKIPPED";
|
|
|
|
for (const line of section) {
|
|
if (line.startsWith(_$.CHARM_SLOTS)) {
|
|
isParsed = "SECTION_PARSED";
|
|
break;
|
|
}
|
|
}
|
|
|
|
return isParsed;
|
|
}
|
|
|
|
function parseSpirit(section: string[], item: ParsedItem) {
|
|
// the purpose of this parser is to "consume" Spirit: 100 sections
|
|
// so they are not recognized as modifiers
|
|
if (item.category !== ItemCategory.Sceptre) return "PARSER_SKIPPED";
|
|
|
|
let isParsed: SectionParseResult = "SECTION_SKIPPED";
|
|
|
|
for (const line of section) {
|
|
if (line.startsWith(_$.BASE_SPIRIT)) {
|
|
isParsed = "SECTION_PARSED";
|
|
break;
|
|
}
|
|
}
|
|
|
|
return isParsed;
|
|
}
|
|
|
|
function parsePriceNote(section: string[], item: ParsedItem) {
|
|
let isParsed: SectionParseResult = "SECTION_SKIPPED";
|
|
|
|
for (const line of section) {
|
|
if (line.startsWith(_$.PRICE_NOTE)) {
|
|
isParsed = "SECTION_PARSED";
|
|
break;
|
|
}
|
|
}
|
|
|
|
return isParsed;
|
|
}
|
|
|
|
function parseHelpText(section: string[], item: ParsedItem) {
|
|
if (
|
|
item.category !== ItemCategory.Quiver &&
|
|
item.category !== ItemCategory.Flask &&
|
|
item.category !== ItemCategory.Charm &&
|
|
item.category !== ItemCategory.Waystone &&
|
|
item.category !== ItemCategory.Map &&
|
|
item.category !== ItemCategory.Jewel &&
|
|
item.category !== ItemCategory.Relic &&
|
|
item.category !== ItemCategory.Tablet &&
|
|
item.category !== ItemCategory.TowerAugment
|
|
)
|
|
return "PARSER_SKIPPED";
|
|
|
|
for (const line of section) {
|
|
if (
|
|
line.startsWith(_$.QUIVER_HELP_TEXT) ||
|
|
line.startsWith(_$.FLASK_HELP_TEXT) ||
|
|
line.startsWith(_$.CHARM_HELP_TEXT) ||
|
|
line.startsWith(_$.WAYSTONE_HELP) ||
|
|
line.startsWith(_$.JEWEL_HELP) ||
|
|
line.startsWith(_$.SANCTUM_HELP) ||
|
|
line.startsWith(_$.PRECURSOR_TABLET_HELP)
|
|
) {
|
|
return "SECTION_PARSED";
|
|
}
|
|
}
|
|
return "SECTION_SKIPPED";
|
|
}
|
|
|
|
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 = _$.ITEM_SYNTHESISED.exec(item.baseType)![1];
|
|
} else {
|
|
item.name = _$.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 (_$.ITEM_SUPERIOR.test(item.name)) {
|
|
item.name = _$.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" &&
|
|
item.info.refName !== "Forbidden Tome"
|
|
)
|
|
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 foundAndTier = tryParseTranslation(
|
|
{ string: line, unscalable: true },
|
|
ModifierType.Pseudo,
|
|
);
|
|
if (foundAndTier) {
|
|
const { stat: found } = foundAndTier;
|
|
item.newMods.push({
|
|
info: { tags: [], type: ModifierType.Pseudo },
|
|
stats: [found],
|
|
});
|
|
} else {
|
|
item.unknownModifiers.push({
|
|
text: line,
|
|
type: ModifierType.Pseudo,
|
|
});
|
|
}
|
|
}
|
|
|
|
return "SECTION_PARSED";
|
|
}
|
|
|
|
function parseFilledCoffin(section: string[], item: ParsedItem) {
|
|
if (item.info.refName !== "Filled Coffin") return "PARSER_SKIPPED";
|
|
if (!section.some((line) => line.endsWith(IMPLICIT_LINE)))
|
|
return "SECTION_SKIPPED";
|
|
|
|
const { lines } = parseModType(section);
|
|
const modInfo: ModifierInfo = {
|
|
type: ModifierType.Necropolis,
|
|
tags: [],
|
|
};
|
|
parseStatsFromMod(lines, item, { info: modInfo, stats: [] });
|
|
|
|
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,
|
|
): boolean {
|
|
item.newMods.push(modifier);
|
|
|
|
if (modifier.info.type === ModifierType.Veiled) {
|
|
console.log("mod name:", modifier.info.name);
|
|
const found = STAT_BY_MATCH_STR(modifier.info.name!);
|
|
if (found) {
|
|
modifier.stats.push({
|
|
stat: found.stat,
|
|
translation: found.matcher,
|
|
});
|
|
} else {
|
|
if (item.rarity !== ItemRarity.Unique) {
|
|
item.unknownModifiers.push({
|
|
text: modifier.info.name!,
|
|
type: modifier.info.type,
|
|
});
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
const statIterator = linesToStatStrings(lines);
|
|
let stat = statIterator.next();
|
|
while (!stat.done) {
|
|
const parsedStatAndTier = tryParseTranslation(
|
|
stat.value,
|
|
modifier.info.type,
|
|
item,
|
|
);
|
|
if (parsedStatAndTier) {
|
|
const { stat: parsedStat, tier } = parsedStatAndTier;
|
|
modifier.stats.push(parsedStat);
|
|
if (tier) {
|
|
modifier.info.tier = tier.poe1;
|
|
modifier.info.tierNew = tier.poe2;
|
|
}
|
|
stat = statIterator.next(true);
|
|
} else {
|
|
stat = statIterator.next(false);
|
|
}
|
|
}
|
|
|
|
if (item.rarity !== ItemRarity.Unique) {
|
|
item.unknownModifiers.push(
|
|
...stat.value.map((line) => ({
|
|
text: line,
|
|
type: modifier.info.type,
|
|
})),
|
|
);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @deprecated
|
|
*/
|
|
function transformToLegacyModifiers(item: ParsedItem) {
|
|
item.statsByType = sumStatsByModType(item.newMods);
|
|
}
|
|
|
|
function applyElementalAdded(item: ParsedItem) {
|
|
if (item.weaponELEMENTAL) {
|
|
const knownRefs = new Set<string>([
|
|
"Adds # to # Lightning Damage",
|
|
"Adds # to # Cold Damage",
|
|
"Adds # to # Fire Damage",
|
|
]);
|
|
|
|
item.statsByType.forEach((calc) => {
|
|
if (knownRefs.has(calc.stat.ref)) {
|
|
for (const source of calc.sources) {
|
|
if (calc.stat.ref === "Adds # to # Lightning Damage") {
|
|
if (item.weaponLIGHTNING) {
|
|
item.weaponLIGHTNING =
|
|
source.contributes!.value + item.weaponLIGHTNING;
|
|
} else {
|
|
item.weaponLIGHTNING = source.contributes!.value;
|
|
}
|
|
} else if (calc.stat.ref === "Adds # to # Cold Damage") {
|
|
if (item.weaponCOLD) {
|
|
item.weaponCOLD = source.contributes!.value + item.weaponCOLD;
|
|
} else {
|
|
item.weaponCOLD = source.contributes!.value;
|
|
}
|
|
} else if (calc.stat.ref === "Adds # to # Fire Damage") {
|
|
if (item.weaponFIRE) {
|
|
item.weaponFIRE = source.contributes!.value + item.weaponFIRE;
|
|
} else {
|
|
item.weaponFIRE = source.contributes!.value;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
function calcBasePercentile(item: ParsedItem) {
|
|
const info = item.info.unique
|
|
? ITEM_BY_TRANSLATED("ITEM", item.info.unique.base)![0].armour
|
|
: item.info.armour;
|
|
if (!info) return;
|
|
|
|
// Base percentile is the same for all defences.
|
|
// Using `AR/EV -> ES` order to improve accuracy
|
|
// of calculation (larger rolls = more precise).
|
|
if (item.armourAR && info.ar) {
|
|
item.basePercentile = calcPropPercentile(
|
|
item.armourAR,
|
|
info.ar,
|
|
QUALITY_STATS.ARMOUR,
|
|
item,
|
|
);
|
|
} else if (item.armourEV && info.ev) {
|
|
item.basePercentile = calcPropPercentile(
|
|
item.armourEV,
|
|
info.ev,
|
|
QUALITY_STATS.EVASION,
|
|
item,
|
|
);
|
|
} else if (item.armourES && info.es) {
|
|
item.basePercentile = calcPropPercentile(
|
|
item.armourES,
|
|
info.es,
|
|
QUALITY_STATS.ENERGY_SHIELD,
|
|
item,
|
|
);
|
|
}
|
|
}
|
|
|
|
export function removeLinesEnding(
|
|
lines: readonly string[],
|
|
ending: string,
|
|
): string[] {
|
|
return lines.map((line) =>
|
|
line.endsWith(ending) ? line.slice(0, -ending.length) : line,
|
|
);
|
|
}
|
|
|
|
export function parseAffixStrings(clipboard: string): string {
|
|
return clipboard.replace(/\[([^\]|]+)\|?([^\]]*)\]/g, (_, part1, part2) => {
|
|
return part2 || part1;
|
|
});
|
|
}
|
|
function getMaxSockets(category: ItemCategory | undefined) {
|
|
switch (category) {
|
|
case ItemCategory.BodyArmour:
|
|
case ItemCategory.TwoHandedAxe:
|
|
case ItemCategory.TwoHandedMace:
|
|
case ItemCategory.TwoHandedSword:
|
|
case ItemCategory.Crossbow:
|
|
case ItemCategory.Bow:
|
|
case ItemCategory.Warstaff:
|
|
return 2;
|
|
case ItemCategory.Helmet:
|
|
case ItemCategory.Shield:
|
|
case ItemCategory.Gloves:
|
|
case ItemCategory.Boots:
|
|
case ItemCategory.OneHandedAxe:
|
|
case ItemCategory.OneHandedMace:
|
|
case ItemCategory.OneHandedSword:
|
|
case ItemCategory.Claw:
|
|
case ItemCategory.Dagger:
|
|
case ItemCategory.Focus:
|
|
return 1;
|
|
default:
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
export function isArmourOrWeapon(
|
|
category: ItemCategory | undefined,
|
|
): "armour" | "weapon" | undefined {
|
|
switch (category) {
|
|
case ItemCategory.BodyArmour:
|
|
case ItemCategory.Boots:
|
|
case ItemCategory.Gloves:
|
|
case ItemCategory.Helmet:
|
|
case ItemCategory.Shield:
|
|
case ItemCategory.Focus:
|
|
return "armour";
|
|
case ItemCategory.OneHandedAxe:
|
|
case ItemCategory.OneHandedMace:
|
|
case ItemCategory.OneHandedSword:
|
|
case ItemCategory.Quiver:
|
|
case ItemCategory.Claw:
|
|
case ItemCategory.Dagger:
|
|
case ItemCategory.Wand:
|
|
case ItemCategory.Sceptre:
|
|
case ItemCategory.TwoHandedAxe:
|
|
case ItemCategory.TwoHandedMace:
|
|
case ItemCategory.TwoHandedSword:
|
|
case ItemCategory.Crossbow:
|
|
case ItemCategory.Bow:
|
|
case ItemCategory.Warstaff:
|
|
case ItemCategory.Staff:
|
|
return "weapon";
|
|
default:
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
function runeCount(mod: ParsedModifier, statCalc: StatCalculated): number {
|
|
if (mod.info.type !== ModifierType.Rune) return 0;
|
|
const runeTradeId = statCalc.stat.trade.ids[ModifierType.Rune][0];
|
|
const runeSingle = RUNE_SINGLE_VALUE[runeTradeId];
|
|
|
|
// Calculate how many of this rune are in the item
|
|
const runeAppliedValue = statCalc.sources[0].contributes!.value;
|
|
const runeSingleValue = runeSingle.values[0];
|
|
const totalRunes = Math.floor(runeAppliedValue / runeSingleValue);
|
|
|
|
return totalRunes;
|
|
}
|
|
|
|
export function replaceHashWithValues(template: string, values: number[]) {
|
|
let result = template;
|
|
values.forEach((value: number) => {
|
|
result = result.replace("#", value.toString()); // Replace the first occurrence of #
|
|
});
|
|
return result;
|
|
}
|
|
|
|
// Disable since this is export for tests
|
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
export const __testExports = {
|
|
itemTextToSections,
|
|
};
|