From ab83f8177fca5b84041f2c9444910bd035750a25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Posp=C3=AD=C5=A1il?= Date: Tue, 11 Aug 2026 00:37:53 +0200 Subject: [PATCH] A roll can be a name, and a modifier can be longer than the join reached MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CHANGED: An Advanced Mod Descriptions range whose roll is a name is dropped before the wording is looked up, so "Maximum number of Sentinels of Purity (Animated Weapons-Holy Armaments) is Doubled" resolves to the stat trade indexes instead of matching nothing. - The roll is the name in the wording and the parenthesis is the first and last of the list, exactly as "(50-100)" is the first and last of an interval. There is no numeric token in front of it for `scan_numbers` to hang the bounds on, which is why nothing consumed it. - Stripping only ever *adds* candidates, and adds them after the printed form, so "Unique Monsters (Blood-Filled Vessel): #" — which qualifies on shape alone — still resolves as itself. Measured against the 373 shipped vectors: two changed, both that wording and its twin, both gaining trailing candidates only. CHANGED: One modifier may span eight clipboard lines instead of four, so a modifier that enumerates its alternatives is the one filter it is rather than seven unrecognised ones. - Eight is the longest wording in the data. Reaching further is safe because a shorter join always wins: the loop returns at the first join that resolves, so the cap only bounds a failed match. CHANGED: The fixture bundle is sliced from data-20260810.41, which gives a modifier that rolls a name the trade id and the pool it was missing — so the row is ticked as pooled rather than carrying "not in the modifier data for this unique". ADDED: Captures for The Dark Monarch, Replica Dragonfang's Flight and Bound Fate, one per shape: a named range with a space before it, one without and carrying a number of its own, and a seven-line modifier. CHANGED: UNIQUE-MODS.md states that a pool of one modifier is not a mistake, and its limitation counts are re-measured against that release. Co-Authored-By: Claude Opus 5 (1M context) --- UNIQUE-MODS.md | 25 ++-- docs/data-layer.md | 15 ++- scripts/slice-test-bundle.py | 51 +++++++- src/data/stat_matcher.cpp | 12 +- src/data/stat_normalize.cpp | 50 ++++++-- src/data/stat_normalize.hpp | 17 ++- tests/data/bundle/en-items-base.index.bin | Bin 80 -> 104 bytes tests/data/bundle/en-items-name.index.bin | Bin 352 -> 400 bytes tests/data/bundle/en-items-ref.index.bin | Bin 352 -> 400 bytes tests/data/bundle/en-items.ndjson | 6 + tests/data/bundle/en-stats-matcher.index.bin | Bin 624 -> 808 bytes tests/data/bundle/en-stats-ref.index.bin | Bin 504 -> 632 bytes tests/data/bundle/en-stats.ndjson | 20 +++- .../data/bundle/en-unique-mods-name.index.bin | Bin 24 -> 48 bytes tests/data/bundle/en-unique-mods.ndjson | 3 + tests/data/bundle/item-classes.ndjson | 3 + tests/data/items/unique-bound-fate.txt | 31 +++++ tests/data/items/unique-dark-monarch.txt | 30 +++++ .../unique-replica-dragonfangs-flight.txt | 29 +++++ tests/data/stat-normalization-vectors.ndjson | 42 ++++--- tests/item_pricing_test.cpp | 110 ++++++++++++++++++ tests/normalize_test.cpp | 30 +++++ 22 files changed, 430 insertions(+), 44 deletions(-) create mode 100644 tests/data/items/unique-bound-fate.txt create mode 100644 tests/data/items/unique-dark-monarch.txt create mode 100644 tests/data/items/unique-replica-dragonfangs-flight.txt diff --git a/UNIQUE-MODS.md b/UNIQUE-MODS.md index f30d2d8..32b156d 100644 --- a/UNIQUE-MODS.md +++ b/UNIQUE-MODS.md @@ -56,6 +56,15 @@ is built from, so the lookup is by the name the clipboard already gave us. - **`unlisted[]`** — a pool stated in prose but not enumerated, e.g. the general Synthesis implicit pool. Nothing to search; it exists so the app can say what it is leaving out instead of implying the item has nothing more. +- **A pool of one modifier** is not a mistake. Some modifiers roll a **name** rather than a + number — The Dark Monarch doubles the limit of one of sixteen minion types, Replica + Dragonfang's Flight raises one of 287 skill gems, Forbidden Shako supports one of 164 support + gems in one of four slots. The source calls these fixed, and it is right that every copy has + the modifier; what varies is *which wording* it rendered as, which is the whole of what the + copy in hand is worth searching for. They arrive as a pool with `count: [1, 1]` whose `mods` + all share one `mod` id and differ by wording and `tradeId`. This is why a pool can be 656 + entries long, and why a single filter would have been worse than none: it would have claimed + this copy rolled whichever option the game's own data happens to list first. - **entry `mod`** — GGG's own mod id. Stable across patches, useful for debugging and for deduplicating; not needed to build a query. - **entry `implicit: true`** — the mod is an implicit, so its trade id is in the `implicit.` @@ -112,18 +121,18 @@ a user. Never a wrong filter. ## Limitations, stated plainly -- **No `tradeId` on some filters.** 470 wordings resolve to two different trade ids (the known - ambiguous-wording problem) and 695 to none at all. Both are emitted with `ref` and `range` and - no id, so a pool list still matches the count its `hint` states. Display them; do not search - them. -- **393 mods are dropped entirely** because every stat they grant is unsearchable — cosmetic +- **No `tradeId` on some filters.** Of 11,327 filters, 793 carry no id: 434 wordings resolve to + two different trade ids (the known ambiguous-wording problem) and 359 to none at all. Both are + emitted with `ref` and `range` and no id, so a pool list still matches the count its `hint` + states. Display them; do not search them. +- **394 mods are dropped entirely** because every stat they grant is unsearchable — cosmetic footprints, hidden behaviour. A pool list can therefore be shorter than the pool the game rolls. - **Foulborn is not here.** It is Chayula currency, absent from both the client tables and the wiki's pools. Identification already works off the clipboard's `{ Foulborn Unique Modifier }` info line; only the roll's range stays unknown. -- **Forbidden Flame / Forbidden Flesh** get a record with no mods: their one mod grants a hidden - stat and trade searches them through an *option* stat this join does not reach. Needs the - option-stat work, same as pseudo mods. +- **Forbidden Flame / Forbidden Flesh** get a filter with a wording but no `tradeId`: trade + searches them through an option stat over ascendancy notables, which is not one of the two + name tables the expansion above reads. The same shape, a third table. - **Counts can be missing or coarse.** Precursor's Emblem describes several sub-pools in one hint and the source gives no way to tell which mod belongs to which, so it gets pool groups without counts. diff --git a/docs/data-layer.md b/docs/data-layer.md index f27d601..626ff37 100644 --- a/docs/data-layer.md +++ b/docs/data-layer.md @@ -71,9 +71,22 @@ downloaded at runtime from **[JIRPOS/PathOfPriceCheck-Data](https://github.com/J **`NORMALIZATION.md` in the data repo is normative** and this must reproduce it exactly — a divergence does not crash, it silently mismatches a mod and returns a confident wrong price. `normalize_test` replays the conformance vectors shipped with every data release. + A modifier can roll over a **list instead of an interval**, and the game prints that range the + same way — "Maximum number of Sentinels of Purity (Animated Weapons-Holy Armaments) is + Doubled" rolls over the minion skill gems. `scan_numbers` cannot see it: there is no number in + front of the parenthesis to carry the bounds. `strip_named_ranges` drops it, and the roll is + the name left in the wording, which is what trade indexes. It only ever **adds** candidates — + a wording whose parenthesis is genuinely part of it, `Unique Monsters (Blood-Filled Vessel)`, + is enumerated as printed first and never reaches the stripped form. - **`data/stat_matcher`** joins clipboard lines into one modifier and resolves it to a stat and a roll. Mod type is the primary disambiguator: explicit/implicit/fractured/crafted/enchant variants - share a wording and differ only by trade namespace. Two separate negation concepts — + share a wording and differ only by trade namespace. + `kMaxModLines` is **8**, not the two a hybrid needs: a modifier that enumerates its alternatives + is as long as its list — Bound Fate's "Every # seconds, gain one of the following" is seven + lines and the class-connection jewel is eight, which is the longest wording in the data. Below + the true maximum the join can never be built and *every line* of such a modifier is reported + unrecognised on its own. Reaching further is safe because a shorter join always wins: the loop + returns at the first join that resolves, so the cap only bounds a failed match. Two separate negation concepts — `matcher.negate` (the *wording* is inverse; store the roll canonically) and `trade.inverted` (the *trade site* indexes the opposite sign; applied at query-build time, not here). An inverse wording's Advanced Mod Descriptions range is **printed high to low** — diff --git a/scripts/slice-test-bundle.py b/scripts/slice-test-bundle.py index 36f2725..84285f1 100755 --- a/scripts/slice-test-bundle.py +++ b/scripts/slice-test-bundle.py @@ -68,14 +68,16 @@ "Area is influenced by The Shaper", # A two-line implicit, which is one stat and therefore one filter — the map's affixes are # printed in exactly the same shape and must still come out as none. + # The game hardcodes the 20 that trade placeholds, and the record's ref is the game's + # wording — which is what the clipboard prints and what the fixture must key on. "Map contains Baran's Citadel\n" - "Item Quantity increases amount of Rewards Baran drops by #% of its value", + "Item Quantity increases amount of Rewards Baran drops by 20% of its value", # And a map affix, which that plan must leave out without calling it unrecognised. "Monsters have #% chance to Hinder on Hit with Spells", # A blighted map's implicit, both halves of it — the plan searches every implicit a map # has, so leaving one out of the fixture would show up as an unrecognised modifier. "Area is infested with Fungal Growths\n" - "Map's Item Quantity Modifiers also affect Blight Chest count at #% value\n" + "Map's Item Quantity Modifiers also affect Blight Chest count at 50% value\n" "Can be Anointed up to # times", "Natural inhabitants of this area have been removed", # An Expedition Logbook's destinations. The faction and the area are `pseudo.*` stats and @@ -143,6 +145,37 @@ # "Has Red Smoke" — the second affliction of the third capture — is deliberately **not** # here, for the reason Porcupine Goliath and Dialla's Subjugation are not: an effect the # bundle cannot name has to be left out of the search and said out loud. + # The Dark Monarch's four modifiers. The last of them is the only shape in the game whose + # roll is a *name*: one stat per minion skill gem, of which the item printed the range as + # "(Animated Weapons-Holy Armaments)" and the roll as the gem in the wording itself. + "# to maximum Energy Shield", + "# to maximum Energy Shield (Local)", + "# to Level of all Minion Skill Gems", + "#% increased Light Radius", + "Maximum number of Sentinels of Purity is Doubled\n" + "Cannot have Minions other than Sentinels of Purity", + # Replica Dragonfang's Flight, the other shape of a named range: the pool is every skill + # gem, so the wording carries the roll *and* a number, and the parenthesis follows a word + # with no space in front of it. + "#% increased Dexterity", + "#% increased Intelligence", + "# to Level of all Storm Burst Gems", + "#% to all Elemental Resistances", + "#% increased Reservation Efficiency of Skills", + "Items and Gems have #% increased Attribute Requirements", + "# to all Attributes", + # Bound Fate. A modifier that enumerates its alternatives is as long as the list — seven + # lines here — which is what the join has to be able to reach. + "Every 5 seconds, gain one of the following for 5 seconds:\n" + "Your Hits are always Critical Strikes\n" + "Hits against you are always Critical Strikes\n" + "Attacks cannot Hit you\n" + "Attacks against you always Hit\n" + "Your Damage with Hits is Lucky\n" + "Damage of Hits against you is Lucky", + "# to Dexterity", + "# to Intelligence", + "#% increased Stun and Block Recovery", ] ITEMS = [ @@ -235,18 +268,30 @@ # nothing a logbook's category does not — and why a magic one arriving as "Buffered # Expedition Logbook" has to resolve back to this record before anything is sent. "ITEM::Expedition Logbook", + # A unique whose per-unique record states one modifier as a *pool of names* — the minion + # skill gems — rather than as a numeric range, which is the case for a printed range the + # normalizer has to drop before the wording resolves at all. + "UNIQUE::The Dark Monarch", + "ITEM::Lich's Circlet", + "UNIQUE::Replica Dragonfang's Flight", + "ITEM::Onyx Amulet", + "UNIQUE::Bound Fate", + "ITEM::Cloth Belt", ] UNIQUE_MODS = [ "Ralakesh's Impatience", "That Which Was Taken", "Rumi's Concoction", + "The Dark Monarch", + "Replica Dragonfang's Flight", + "Bound Fate", ] ITEM_CLASSES = ["Rings", "Boots", "Gloves", "Body Armours", "Stackable Currency", "Divination Cards", "Jewels", "Utility Flasks", "Maps", "Skill Gems", "Support Gems", "Chart", "Misc Map Items", "Contracts", "Blueprints", - "Sanctum Research", "Expedition Logbooks"] + "Sanctum Research", "Expedition Logbooks", "Helmets", "Amulets", "Belts"] LANG = "en" diff --git a/src/data/stat_matcher.cpp b/src/data/stat_matcher.cpp index 5cfe1eb..612c04e 100644 --- a/src/data/stat_matcher.cpp +++ b/src/data/stat_matcher.cpp @@ -8,9 +8,15 @@ namespace ppc::data { namespace { -/// How many clipboard lines one modifier may span. Hybrids are two; the cap only bounds how -/// far a failed match will keep reaching forward. -constexpr size_t kMaxModLines = 4; +/// How many clipboard lines one modifier may span. Hybrids are two, but a modifier that +/// enumerates its alternatives is as long as the list: Bound Fate's "Every # seconds, gain +/// one of the following" is seven lines and the class-connection jewel is eight, which is +/// the longest wording in the data. Below that the join can never be built and every line of +/// the modifier comes back unrecognised on its own. +/// +/// The cap only bounds how far a *failed* match keeps reaching forward — a shorter join +/// always wins, because the loop returns at the first `end` that resolves. +constexpr size_t kMaxModLines = 8; /// The game appends this to a roll that item level cannot scale. constexpr std::string_view kUnscalableSuffix = " (unscalable value)"; diff --git a/src/data/stat_normalize.cpp b/src/data/stat_normalize.cpp index 5966350..6957b2c 100644 --- a/src/data/stat_normalize.cpp +++ b/src/data/stat_normalize.cpp @@ -72,6 +72,35 @@ std::string strip_empty_parens(std::string_view line) { return out; } +std::string strip_named_ranges(std::string_view line) { + std::string out; + out.reserve(line.size()); + for (size_t i = 0; i < line.size(); ++i) { + if (line[i] != '(') { + out.push_back(line[i]); + continue; + } + const size_t close = line.find(')', i); + // A range that follows a number is that number's own, numeric bounds or not. + const char prev = out.empty() ? '\0' : out.back(); + if (close == std::string_view::npos || is_digit(prev) || prev == ')') { + out.push_back(line[i]); + continue; + } + const std::string_view inner = line.substr(i + 1, close - i - 1); + // The same split as a numeric range: one arbitrary character, then the separator. + const size_t sep = inner.size() > 1 ? inner.find('-', 1) : std::string_view::npos; + if (sep == std::string_view::npos || sep + 1 >= inner.size() || + to_double(inner.substr(0, sep)) || to_double(inner.substr(sep + 1))) { + out.push_back(line[i]); + continue; + } + if (prev == ' ') out.pop_back(); + i = close; + } + return out; +} + std::vector scan_numbers(std::string_view s) { std::vector out; const size_t n = s.size(); @@ -167,17 +196,24 @@ std::string apply_candidate(std::string_view line, std::span } std::vector candidates(std::string_view line) { - const std::string text = strip_empty_parens(line); - std::vector tokens = scan_numbers(text); - if (tokens.size() > kMaxTokens) tokens.resize(kMaxTokens); - std::vector out; const auto push_unique = [&out](std::string c) { if (std::find(out.begin(), out.end(), c) == out.end()) out.push_back(std::move(c)); }; - for (uint32_t keep : masks_for(tokens.size())) - push_unique(apply_candidate(text, tokens, keep)); - push_unique(text); // last resort, so a wording with no numbers still resolves + const auto enumerate = [&](const std::string& text) { + std::vector tokens = scan_numbers(text); + if (tokens.size() > kMaxTokens) tokens.resize(kMaxTokens); + for (uint32_t keep : masks_for(tokens.size())) + push_unique(apply_candidate(text, tokens, keep)); + push_unique(text); // last resort, so a wording with no numbers still resolves + }; + + const std::string text = strip_empty_parens(line); + enumerate(text); + // Only then the named-range form, so a wording whose parenthesis is part of it — + // "Unique Monsters (Blood-Filled Vessel): #" — resolves as printed. + const std::string named = strip_named_ranges(text); + if (named != text) enumerate(named); return out; } diff --git a/src/data/stat_normalize.hpp b/src/data/stat_normalize.hpp index a8a8a87..f450e26 100644 --- a/src/data/stat_normalize.hpp +++ b/src/data/stat_normalize.hpp @@ -26,6 +26,19 @@ struct NumberToken { /// Remove every literal "()" — GGG emits them occasionally. Must run before scanning. std::string strip_empty_parens(std::string_view line); +/// Remove every Advanced Mod Descriptions range whose roll is a *name*. +/// +/// A modifier can roll over a list instead of over an interval — "Maximum number of +/// Sentinels of Purity (Animated Weapons-Holy Armaments) is Doubled" rolls over the minion +/// skill gems, and the parenthesis is the first and last of that list exactly as "(50-100)" +/// is the first and last of an interval. `scan_numbers` cannot see it: there is no numeric +/// token in front of it to carry the bounds. The name itself is the roll and the trade +/// wording spells it out, so the range is dropped rather than placeheld. +/// +/// Only a group whose two halves are both non-numeric, and that does not follow a number, +/// qualifies — a numeric token's own range belongs to that token. +std::string strip_named_ranges(std::string_view line); + /// Every numeric token, in order. /// /// A token starts at a '+', '-' or digit that is *not* preceded by a digit or ')'. That @@ -43,7 +56,9 @@ std::string apply_candidate(std::string_view line, std::span /// Lookup candidates, most generic first, duplicates removed, raw line last. /// /// The order is load-bearing: the generic form wins wherever the data has it, and the -/// literal forms exist only for wordings that do not generalise. +/// literal forms exist only for wordings that do not generalise. A line carrying a named +/// range is enumerated twice, as printed and then stripped, so a wording that really does +/// contain a parenthesis still wins as itself. std::vector candidates(std::string_view line); /// The fully generic candidate — every number replaced by '#'. This is the join key the diff --git a/tests/data/bundle/en-items-base.index.bin b/tests/data/bundle/en-items-base.index.bin index badbe623fea7ee821e6a735df88cfdc75396678b..e66ad6e8087a474dee747d086622a2484c4ff067 100644 GIT binary patch delta 55 zcmWH@5SzOEqI{z)14BTtQd%Js0|SQ!Z%e7-L_R4t7I&V7pXDd2DG59-WP9^iiGksG L-K96;;tUJ`zE%*t delta 53 zcmV-50LuSpP$re9(I0^w002-^B4%*{006pvoQJao008t~&5>CP002Zn$eb7lkv1X- L$9~f67Z(5k;A0V* diff --git a/tests/data/bundle/en-items-name.index.bin b/tests/data/bundle/en-items-name.index.bin index a052016abae757fe32d82a12304ee987efd0133b..539b64798bfd4bbce539035a46c0af9588a03e9d 100644 GIT binary patch delta 96 zcmV-m0H6Qh0+0icFa{yJju-PEkuEA0ScCs6<01e6hOc`tvjqSEV^(`OZzGXjItGZ{ z5MVVTk(P=Eg!2l&n;?@O0V*IZ;GOezApih6@Syb51ONa#WYF~j1ONbp;&JxW7XSd6 CFC$<8 delta 51 zcmbQh{D5hK!9)uUwzl=v2Af$YmRU{Q(J|SBQA5J&!-6lDSr{0sQ!adE0@5vy3cp7e;lz&o$T>PrI$WIP&~>*tEzB8h(yl++?AAG}`R8ugoH^ke z3;8iODn+G{3SlibZr3%bp%PiE{3wOZl52gw@4w%Bzn|yzJii>foX$S!ZHHSVz`9z^ z5FP+_T5mdmOsy{4WjzHL=x}#y^8vU?!FpcrvEzexVH`x%(31g~yWp!5b-2k9*s;a%s7q~JXYg#5ERYUh~$p}u@lpS67 zXa*pude9S-252nPk5u76(9+J+hErrQ^7J6Belx1%IenmfuYROS0eI+NjnFQqhc%-b zZ$Q{17^8*HMQhF5Q9ym|H#1FXpn1(aMI!iQ?wO$fF!PHi^NtXj;e{5OW@%+!g~cc) zn6kz#@7aJuO_`O(9;8&*=%CvQs%>A1Fl)tq8-v8cyEE~64{?^IwolOl%Ui={==fw@ zAnB} delta 480 zcmZ3%_JPHB`-TM!p}GtVoK1a|oz|b{)kAeEcT6M!N z9R>z3-8p7|Sr`~LTU*)$nldmfK9_5oU;;F0*-6_hAqIv6FSpoVz83?gKgM@}8oo{Y7F73^xr+j@j!oFie^7{1}G{Fz{x-IVK781yfA%$;H}0*M}TE zbxWLqK`FxT94HXabgVt+smj2hx%|X=Us(o*wz(SDK|$iF-hD#~7`Sc|6Yh#lKFcI- za~|kD=s<{B~^ie!E&MIcTj-7ZfE;xrNO|k(8l8LXJAyk L`=t3V4H$_4E>xRX diff --git a/tests/data/bundle/en-stats-ref.index.bin b/tests/data/bundle/en-stats-ref.index.bin index 6dd354f9e2d6adc68bf149eac176acd56674157b..b7fdf2139fff9a9029873f2c170e2cbe9a0b4b0b 100644 GIT binary patch delta 557 zcmWNOTSyad6vltkZr(sm=TfY^;C(u?d7YAru^hM{7b;0aF{oZfb5Z!R(L;)wwTjYA zQPi$hhE|v{B~o;(g;}UsDN&IrNoi=M(EmK2bH4BUeJh?T>kdBYYQZlVyw21~l$+si z)3ptVxnSMzv#=W;xB2@u%V5ywlmA>41hocBZIR$z^j2ED1qzBzg(!D|Z_k6!s4OsA z0waFKfk9C&&&{^$W2|a9bmwHLywb(>jgj2rfiN)Jljn|slh?ngCA&bDa9J~v2l1ih z+VL=`R!r_2h=%F<*b>(!2si3V+I?Z>TzqM14it8q%1rs7claEc5r134Dvi^z(3Ye* zD)WZly{cn>M2g_1FZQVbZ_{>ITXmg~YwbUz*5wSSny;`uRX|E|b)`8G5@RyzEQKKd zXR}+<;Acgz{d)ndzHr&CsSpxgb1~Wj9xZ9Ed=|KZ^v@gKY=v2qq4D-Ms2{CuX%2@vGfe|#=h#DI0-W7wvpO2|5NLbpkO#xqGjqRL3d&B!K$jXO zQ!PWwDS~_B{LpANd>rZ=?%xX@W#uCWlAxlx%2|^uk{bq{??NE5MDNz^faXJW?yo|~ zRdvrT5oF_U4!>G23>r*pqgVF`;ikB8nNsu>>svGNIs)3K^50ujmmCslbY-S6e0X7Q bQ3vO1 z6rmLs$RPj#NmF+heKP<6479HrYd8P^WpolDIw$}DR4{Wbg*lO4CLxL}006T)IXqD{ z000!G$vtWk006-0v_2Ll004?G*-{9Ry%!mwIsgD|=$=~18vp=Kz6)b0HUI#i8;4~l zk?kh}w;z)p0TzEhHvj;L?0SF#FaQ8AE{cb`DgXd2`+$+WG5`Sl#LSV$FaQ7-9gC7A z0ssKD^hK5|E&u=&PQsZS9smI8Z&aJ{9smHC(Tt#w8UO%z-8P{yI{*M>yQ-y;A^-q# z%G0r}DF6TxfX%)w6#xL!ymG&C761U*F?7a1F8~0xo9sKr89D#}0$g*+qbmRaPE^Ip z+7|!-B3Mq&k}&`P%!sYdNhJUPDW}NKO&tILhMg$aU^D;#Nhgcg8X=QD0v06CHvj-5 iwrlQJ82|u}IY;wX0{{TK5vcTKAOHZNJUIRHH~;_!oqR$7 diff --git a/tests/data/bundle/en-stats.ndjson b/tests/data/bundle/en-stats.ndjson index 1e4a27f..150c2b8 100644 --- a/tests/data/bundle/en-stats.ndjson +++ b/tests/data/bundle/en-stats.ndjson @@ -21,9 +21,9 @@ {"better":1,"matchers":[{"string":"#% Chance to Block Attack Damage during Effect"}],"ref":"#% Chance to Block Attack Damage during Effect","trade":{"ids":{"explicit":["explicit.stat_2519106214"]}}} {"better":1,"matchers":[{"string":"#% Chance to Block Spell Damage during Effect"}],"ref":"#% Chance to Block Spell Damage during Effect","trade":{"ids":{"explicit":["explicit.stat_215754572"]}}} {"better":1,"matchers":[{"string":"Area is influenced by The Shaper","value":1.0}],"ref":"Area is influenced by The Shaper","trade":{"ids":{"implicit":["implicit.stat_1792283443|1"]}}} -{"better":1,"matchers":[{"string":"Map contains Baran's Citadel\nItem Quantity increases amount of Rewards Baran drops by #% of its value"}],"ref":"Map contains Baran's Citadel\nItem Quantity increases amount of Rewards Baran drops by #% of its value","trade":{"ids":{"implicit":["implicit.stat_2563183002|1"]}}} +{"better":1,"matchers":[{"string":"Map contains Baran's Citadel\nItem Quantity increases amount of Rewards Baran drops by 20% of its value","value":1.0}],"ref":"Map contains Baran's Citadel\nItem Quantity increases amount of Rewards Baran drops by 20% of its value","trade":{"ids":{"implicit":["implicit.stat_2563183002|1"]}}} {"better":1,"matchers":[{"string":"Monsters have #% chance to Hinder on Hit with Spells"},{"string":"Monsters Hinder on Hit with Spells","value":100.0}],"ref":"Monsters have #% chance to Hinder on Hit with Spells","trade":{"ids":{"explicit":["explicit.stat_962720646"],"fractured":["fractured.stat_962720646"]}}} -{"better":1,"matchers":[{"string":"Area is infested with Fungal Growths\nMap's Item Quantity Modifiers also affect Blight Chest count at #% value\nCan be Anointed up to # times"}],"ref":"Area is infested with Fungal Growths\nMap's Item Quantity Modifiers also affect Blight Chest count at #% value\nCan be Anointed up to # times","trade":{"ids":{"implicit":["implicit.stat_299373046"]}}} +{"better":1,"matchers":[{"string":"Area is infested with Fungal Growths\nMap's Item Quantity Modifiers also affect Blight Chest count at 25% value\nCan be Anointed up to 3 times","value":1.0},{"string":"Area is infested with Fungal Growths\nMap's Item Quantity Modifiers also affect Blight Chest count at 50% value\nCan be Anointed up to # times"}],"ref":"Area is infested with Fungal Growths\nMap's Item Quantity Modifiers also affect Blight Chest count at 50% value\nCan be Anointed up to # times","trade":{"ids":{"implicit":["implicit.stat_299373046"]}}} {"better":1,"matchers":[{"string":"Natural inhabitants of this area have been removed"}],"ref":"Natural inhabitants of this area have been removed","trade":{"ids":{"implicit":["implicit.stat_2656027173"]}}} {"better":1,"matchers":[{"string":"Has Logbook Faction: Druids of the Broken Circle"}],"ref":"Has Logbook Faction: Druids of the Broken Circle","trade":{"ids":{"pseudo":["pseudo.pseudo_logbook_faction_druids"]}}} {"better":1,"matchers":[{"string":"Has Logbook Faction: Order of the Chalice"}],"ref":"Has Logbook Faction: Order of the Chalice","trade":{"ids":{"pseudo":["pseudo.pseudo_logbook_faction_order"]}}} @@ -61,3 +61,19 @@ {"better":1,"matchers":[{"string":"Has Gold Mine"}],"ref":"Has Gold Mine","trade":{"ids":{"sanctum":["sanctum.sanctum_effect_50425"]}}} {"better":1,"matchers":[{"string":"Has Enchanted Urn"}],"ref":"Has Enchanted Urn","trade":{"ids":{"sanctum":["sanctum.sanctum_effect_38167"]}}} {"better":1,"matchers":[{"string":"Has Empty Trove"}],"ref":"Has Empty Trove","trade":{"ids":{"sanctum":["sanctum.sanctum_effect_7865"]}}} +{"better":1,"matchers":[{"string":"# to maximum Energy Shield"}],"ref":"# to maximum Energy Shield","trade":{"ids":{"crafted":["crafted.stat_3489782002"],"explicit":["explicit.stat_3489782002"],"fractured":["fractured.stat_3489782002"],"implicit":["implicit.stat_3489782002"]}}} +{"better":1,"matchers":[{"string":"# to maximum Energy Shield (Local)"}],"ref":"# to maximum Energy Shield (Local)","trade":{"ids":{"crafted":["crafted.stat_4052037485"],"explicit":["explicit.stat_4052037485"],"fractured":["fractured.stat_4052037485"],"implicit":["implicit.stat_4052037485"],"scourge":["scourge.stat_4052037485"]}}} +{"better":1,"matchers":[{"string":"# to Level of all Minion Skill Gems"}],"ref":"# to Level of all Minion Skill Gems","trade":{"ids":{"enchant":["enchant.stat_2162097452"],"explicit":["explicit.stat_2162097452"],"fractured":["fractured.stat_2162097452"],"implicit":["implicit.stat_2162097452"]}}} +{"better":1,"matchers":[{"string":"#% increased Light Radius"},{"negate":true,"string":"#% reduced Light Radius"}],"ref":"#% increased Light Radius","trade":{"ids":{"enchant":["enchant.stat_1263695895"],"explicit":["explicit.stat_1263695895"],"fractured":["fractured.stat_1263695895"],"implicit":["implicit.stat_1263695895"],"scourge":["scourge.stat_1263695895"]}}} +{"better":1,"matchers":[{"string":"Maximum number of Sentinels of Purity is Doubled\nCannot have Minions other than Sentinels of Purity","value":10.0}],"ref":"Maximum number of Sentinels of Purity is Doubled\nCannot have Minions other than Sentinels of Purity","trade":{"ids":{"explicit":["explicit.stat_56473917|10"]}}} +{"better":1,"matchers":[{"string":"#% increased Dexterity"},{"negate":true,"string":"#% reduced Dexterity"}],"ref":"#% increased Dexterity","trade":{"ids":{"explicit":["explicit.stat_4139681126"],"fractured":["fractured.stat_4139681126"],"implicit":["implicit.stat_4139681126"],"scourge":["scourge.stat_4139681126"]}}} +{"better":1,"matchers":[{"string":"#% increased Intelligence"},{"negate":true,"string":"#% reduced Intelligence"}],"ref":"#% increased Intelligence","trade":{"ids":{"explicit":["explicit.stat_656461285"],"fractured":["fractured.stat_656461285"],"implicit":["implicit.stat_656461285"],"scourge":["scourge.stat_656461285"]}}} +{"better":1,"matchers":[{"string":"# to Level of all Storm Burst Gems"}],"ref":"# to Level of all Storm Burst Gems","trade":{"ids":{"explicit":["explicit.indexable_skill_160"]}}} +{"better":1,"matchers":[{"string":"#% to all Elemental Resistances"}],"ref":"#% to all Elemental Resistances","trade":{"ids":{"crafted":["crafted.stat_2901986750"],"explicit":["explicit.stat_2901986750"],"fractured":["fractured.stat_2901986750"],"implicit":["implicit.stat_2901986750"],"scourge":["scourge.stat_2901986750"]}}} +{"better":1,"matchers":[{"string":"#% increased Reservation Efficiency of Skills"},{"negate":true,"string":"#% reduced Reservation Efficiency of Skills"}],"ref":"#% increased Reservation Efficiency of Skills","trade":{"ids":{"explicit":["explicit.stat_2587176568","explicit.stat_4202507508"],"implicit":["implicit.stat_2587176568","implicit.stat_4202507508"]}}} +{"better":-1,"matchers":[{"string":"Items and Gems have #% increased Attribute Requirements"},{"negate":true,"string":"Items and Gems have #% reduced Attribute Requirements"}],"ref":"Items and Gems have #% increased Attribute Requirements","trade":{"ids":{"explicit":["explicit.stat_752930724"],"fractured":["fractured.stat_752930724"]}}} +{"better":1,"matchers":[{"string":"# to all Attributes"}],"ref":"# to all Attributes","trade":{"ids":{"crafted":["crafted.stat_1379411836"],"explicit":["explicit.stat_1379411836"],"fractured":["fractured.stat_1379411836"],"implicit":["implicit.stat_1379411836"]}}} +{"better":1,"matchers":[{"string":"Every 5 seconds, gain one of the following for 5 seconds:\nYour Hits are always Critical Strikes\nHits against you are always Critical Strikes\nAttacks cannot Hit you\nAttacks against you always Hit\nYour Damage with Hits is Lucky\nDamage of Hits against you is Lucky"}],"ref":"Every 5 seconds, gain one of the following for 5 seconds:\nYour Hits are always Critical Strikes\nHits against you are always Critical Strikes\nAttacks cannot Hit you\nAttacks against you always Hit\nYour Damage with Hits is Lucky\nDamage of Hits against you is Lucky","trade":{"ids":{"explicit":["explicit.stat_2501671832"]}}} +{"better":1,"matchers":[{"string":"# to Dexterity"}],"ref":"# to Dexterity","trade":{"ids":{"crafted":["crafted.stat_3261801346"],"explicit":["explicit.stat_3261801346"],"fractured":["fractured.stat_3261801346"],"implicit":["implicit.stat_3261801346"],"scourge":["scourge.stat_3261801346"]}}} +{"better":1,"matchers":[{"string":"# to Intelligence"}],"ref":"# to Intelligence","trade":{"ids":{"crafted":["crafted.stat_328541901"],"explicit":["explicit.stat_328541901"],"fractured":["fractured.stat_328541901"],"implicit":["implicit.stat_328541901"],"scourge":["scourge.stat_328541901"]}}} +{"better":1,"matchers":[{"string":"#% increased Stun and Block Recovery"},{"negate":true,"string":"#% reduced Stun and Block Recovery"}],"ref":"#% increased Stun and Block Recovery","trade":{"ids":{"explicit":["explicit.stat_2511217560"],"fractured":["fractured.stat_2511217560"],"implicit":["implicit.stat_2511217560"],"scourge":["scourge.stat_2511217560"]}}} diff --git a/tests/data/bundle/en-unique-mods-name.index.bin b/tests/data/bundle/en-unique-mods-name.index.bin index 189ec5ba9989538dec09c339a31808f4e207aa74..39b2514845d6b0cf8f903f79c1f5252e56af988e 100644 GIT binary patch literal 48 zcmY$~-6y_+b3Y#uV_*OP Ddh8B+ literal 24 ecmexz%tCrI3j;&j`f39PAW)I^uohxvU;qGK=LLQM diff --git a/tests/data/bundle/en-unique-mods.ndjson b/tests/data/bundle/en-unique-mods.ndjson index 60064a0..d1f262e 100644 --- a/tests/data/bundle/en-unique-mods.ndjson +++ b/tests/data/bundle/en-unique-mods.ndjson @@ -1,3 +1,6 @@ {"base":"Riveted Boots","fixed":[{"filters":[{"range":[[15,25]],"ref":"#% to Chaos Resistance","tradeId":"explicit.stat_2923486259"}],"mod":"ChaosResistUnique__7"},{"filters":[{"range":[[15,25]],"ref":"#% to Cold Resistance","tradeId":"explicit.stat_4220027924"}],"mod":"ColdResistUnique__19"},{"filters":[{"range":[[1,1]],"ref":"Corrupted Blood cannot be inflicted on you","tradeId":"explicit.stat_1658498488"}],"mod":"CorruptedBloodImmunityUnique_1"},{"filters":[{"range":[[15,25]],"ref":"#% increased Movement Speed","tradeId":"explicit.stat_2250533757"}],"mod":"MovementVelocityUnique__57"}],"name":"Ralakesh's Impatience","pools":[{"hint":"Random charge modifier","mods":[{"filters":[{"range":[[1,1]],"ref":"Count as having maximum number of Endurance Charges","tradeId":"explicit.stat_1090017486"}],"mod":"CountAsHavingMaxEnduranceChargesUnique__1"},{"filters":[{"range":[[1,1]],"ref":"Count as having maximum number of Frenzy Charges","tradeId":"explicit.stat_2046300872"}],"mod":"CountAsHavingMaxFrenzyChargesUnique__1"},{"filters":[{"range":[[1,1]],"ref":"Count as having maximum number of Power Charges","tradeId":"explicit.stat_800091665"}],"mod":"CountAsHavingMaxPowerChargesUnique__1"}]}]} {"base":"Crimson Jewel","name":"That Which Was Taken","unlisted":["4 random Charm modifiers"]} {"base":"Granite Flask","fixed":[{"filters":[{"range":[[8,12]],"ref":"#% Chance to Block Attack Damage during Effect","tradeId":"explicit.stat_2519106214"}],"mod":"BlockIncreasedDuringFlaskEffectUniqueFlask7"},{"filters":[{"range":[[4,6]],"ref":"#% Chance to Block Spell Damage during Effect","tradeId":"explicit.stat_215754572"}],"mod":"SpellBlockIncreasedDuringFlaskEffectUniqueFlask7"}],"name":"Rumi's Concoction"} +{"base":"Lich's Circlet","fixed":[{"filters":[{"range":[[27,37]],"ref":"#% to Chaos Resistance","tradeId":"explicit.stat_2923486259"}],"mod":"ChaosResistUniqueHelmetInt__1"},{"filters":[{"range":[[1,1]],"ref":"# to Level of all Minion Skill Gems","tradeId":"explicit.stat_2162097452"}],"mod":"GlobalIncreaseMinionSpellSkillGemLevelUnique__4"},{"filters":[{"range":[[-50,-50]],"ref":"#% increased Light Radius","tradeId":"explicit.stat_1263695895"}],"mod":"LightRadiusUnique__10"},{"filters":[{"range":[[50,100]],"ref":"# to maximum Energy Shield","tradeId":"explicit.stat_3489782002"}],"mod":"LocalIncreasedEnergyShieldUniqueHelmetInt_1"}],"name":"The Dark Monarch","pools":[{"count":[1,1],"hint":"Maximum number of random minion type is Doubled / Cannot have Minions other than random minion type","mods":[{"filters":[{"range":[],"ref":"Maximum number of Animated Weapons is Doubled\nCannot have Minions other than Animated Weapons","tradeId":"explicit.stat_56473917|1"}],"mod":"DoubleMinionLimitsUnique_1"},{"filters":[{"range":[],"ref":"Maximum number of Summoned Golems is Doubled\nCannot have Minions other than Summoned Golems","tradeId":"explicit.stat_56473917|2"}],"mod":"DoubleMinionLimitsUnique_1"},{"filters":[{"range":[],"ref":"Maximum number of Summoned Raging Spirits is Doubled\nCannot have Minions other than Summoned Raging Spirits","tradeId":"explicit.stat_56473917|3"}],"mod":"DoubleMinionLimitsUnique_1"},{"filters":[{"range":[],"ref":"Maximum number of Raised Spectres is Doubled\nCannot have Minions other than Raised Spectres","tradeId":"explicit.stat_56473917|4"}],"mod":"DoubleMinionLimitsUnique_1"},{"filters":[{"range":[],"ref":"Maximum number of Raised Spiders is Doubled\nCannot have Minions other than Raised Spiders","tradeId":"explicit.stat_56473917|5"}],"mod":"DoubleMinionLimitsUnique_1"},{"filters":[{"range":[],"ref":"Maximum number of Raised Zombies is Doubled\nCannot have Minions other than Raised Zombies","tradeId":"explicit.stat_56473917|6"}],"mod":"DoubleMinionLimitsUnique_1"},{"filters":[{"range":[],"ref":"Maximum number of Summoned Reapers is Doubled\nCannot have Minions other than Summoned Reapers","tradeId":"explicit.stat_56473917|7"}],"mod":"DoubleMinionLimitsUnique_1"},{"filters":[{"range":[],"ref":"Maximum number of Sentinels of Absolution is Doubled\nCannot have Minions other than Sentinels of Absolution","tradeId":"explicit.stat_56473917|8"}],"mod":"DoubleMinionLimitsUnique_1"},{"filters":[{"range":[],"ref":"Maximum number of Sentinels of Dominance is Doubled\nCannot have Minions other than Sentinels of Dominance","tradeId":"explicit.stat_56473917|9"}],"mod":"DoubleMinionLimitsUnique_1"},{"filters":[{"range":[],"ref":"Maximum number of Sentinels of Purity is Doubled\nCannot have Minions other than Sentinels of Purity","tradeId":"explicit.stat_56473917|10"}],"mod":"DoubleMinionLimitsUnique_1"},{"filters":[{"range":[],"ref":"Maximum number of Summoned Holy Relics is Doubled\nCannot have Minions other than Summoned Holy Relics","tradeId":"explicit.stat_56473917|11"}],"mod":"DoubleMinionLimitsUnique_1"},{"filters":[{"range":[],"ref":"Maximum number of Summoned Phantasms is Doubled\nCannot have Minions other than Summoned Phantasms","tradeId":"explicit.stat_56473917|12"}],"mod":"DoubleMinionLimitsUnique_1"},{"filters":[{"range":[],"ref":"Maximum number of Summoned Skeletons is Doubled\nCannot have Minions other than Summoned Skeletons","tradeId":"explicit.stat_56473917|13"}],"mod":"DoubleMinionLimitsUnique_1"},{"filters":[{"range":[],"ref":"Maximum number of Summoned Spectral Wolves is Doubled\nCannot have Minions other than Summoned Spectral Wolves","tradeId":"explicit.stat_56473917|14"}],"mod":"DoubleMinionLimitsUnique_1"},{"filters":[{"range":[],"ref":"Maximum number of Living Lightning is Doubled\nCannot have Minions other than Living Lightning","tradeId":"explicit.stat_56473917|15"}],"mod":"DoubleMinionLimitsUnique_1"},{"filters":[{"range":[],"ref":"Maximum number of Holy Armaments is Doubled\nCannot have Minions other than Holy Armaments","tradeId":"explicit.stat_56473917|16"}],"mod":"DoubleMinionLimitsUnique_1"}]}]} +{"base":"Onyx Amulet","fixed":[{"filters":[{"range":[[10,16]],"ref":"# to all Attributes","tradeId":"implicit.stat_1379411836"}],"implicit":true,"mod":"AllAttributesImplicitAmulet1"},{"filters":[{"range":[[5,10]],"ref":"#% to all Elemental Resistances","tradeId":"explicit.stat_2901986750"}],"mod":"AllResistancesUnique__29"},{"filters":[{"range":[[-10,-5]],"ref":"Items and Gems have #% increased Attribute Requirements","tradeId":"explicit.stat_752930724"}],"mod":"GlobalItemAttributeRequirementsUnique__3"},{"filters":[{"range":[[5,10]],"ref":"#% increased Reservation Efficiency of Skills"}],"mod":"ReservationEfficiencyUnique__5"}],"name":"Replica Dragonfang's Flight","pools":[{"count":[1,1],"hint":"+3 to Level of all Random Skill Gems","mods":[{"filters":[{"range":[[3,3]],"ref":"# to Level of all Fireball Gems","tradeId":"explicit.indexable_skill_1"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Ice Nova Gems","tradeId":"explicit.indexable_skill_2"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Leap Slam Gems","tradeId":"explicit.indexable_skill_3"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Holy Sweep Gems","tradeId":"explicit.indexable_skill_4"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Ground Slam Gems","tradeId":"explicit.indexable_skill_5"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Cleave Gems","tradeId":"explicit.indexable_skill_6"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Shield Charge Gems","tradeId":"explicit.indexable_skill_7"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Enduring Cry Gems","tradeId":"explicit.indexable_skill_8"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Double Strike Gems","tradeId":"explicit.indexable_skill_9"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Elemental Hit Gems","tradeId":"explicit.indexable_skill_10"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Immortal Call Gems","tradeId":"explicit.indexable_skill_11"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Dual Strike Gems","tradeId":"explicit.indexable_skill_12"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Whirling Blades Gems","tradeId":"explicit.indexable_skill_13"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Frenzy Gems","tradeId":"explicit.indexable_skill_14"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Cold Snap Gems","tradeId":"explicit.indexable_skill_15"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Raise Zombie Gems"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Detonate Dead Gems","tradeId":"explicit.indexable_skill_17"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Caustic Arrow Gems","tradeId":"explicit.indexable_skill_18"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Creeping Frost Gems","tradeId":"explicit.indexable_skill_19"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Ice Shot Gems","tradeId":"explicit.indexable_skill_20"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Static Strike Gems","tradeId":"explicit.indexable_skill_21"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Split Arrow Gems","tradeId":"explicit.indexable_skill_22"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Blood Rage Gems","tradeId":"explicit.indexable_skill_23"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Righteous Fire Gems","tradeId":"explicit.indexable_skill_24"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Discharge Gems","tradeId":"explicit.indexable_skill_25"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Flicker Strike Gems","tradeId":"explicit.indexable_skill_26"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Spark Gems","tradeId":"explicit.indexable_skill_27"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Ice Spear Gems","tradeId":"explicit.indexable_skill_28"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Raise Spectre Gems"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Infernal Blow Gems","tradeId":"explicit.indexable_skill_30"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Glacial Hammer Gems","tradeId":"explicit.indexable_skill_31"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Frost Wall Gems","tradeId":"explicit.indexable_skill_32"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Freezing Pulse Gems","tradeId":"explicit.indexable_skill_33"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Shock Nova Gems","tradeId":"explicit.indexable_skill_34"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Viper Strike Gems","tradeId":"explicit.indexable_skill_35"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Phase Run Gems","tradeId":"explicit.indexable_skill_36"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Explosive Arrow Gems","tradeId":"explicit.indexable_skill_37"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Temporal Chains Gems","tradeId":"explicit.indexable_skill_38"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Elemental Weakness Gems","tradeId":"explicit.indexable_skill_39"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Warlord's Mark Gems","tradeId":"explicit.indexable_skill_40"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Punishment Gems","tradeId":"explicit.indexable_skill_41"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Enfeeble Gems","tradeId":"explicit.indexable_skill_42"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Assassin's Mark Gems","tradeId":"explicit.indexable_skill_43"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Sniper's Mark Gems","tradeId":"explicit.indexable_skill_44"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Despair Gems","tradeId":"explicit.indexable_skill_45"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Lightning Warp Gems","tradeId":"explicit.indexable_skill_46"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Summon Skeletons Gems","tradeId":"explicit.indexable_skill_47"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Glacial Shield Swipe Gems","tradeId":"explicit.indexable_skill_48"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Crushing Fist Gems","tradeId":"explicit.indexable_skill_49"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Swordstorm Gems","tradeId":"explicit.indexable_skill_50"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Heavy Strike Gems","tradeId":"explicit.indexable_skill_51"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Dominating Blow Gems","tradeId":"explicit.indexable_skill_52"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Rain of Arrows Gems","tradeId":"explicit.indexable_skill_53"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Firestorm Gems","tradeId":"explicit.indexable_skill_54"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Lightning Strike Gems","tradeId":"explicit.indexable_skill_55"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Tempest Shield Gems","tradeId":"explicit.indexable_skill_56"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Molten Shell Gems","tradeId":"explicit.indexable_skill_57"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Power Siphon Gems","tradeId":"explicit.indexable_skill_58"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Puncture Gems","tradeId":"explicit.indexable_skill_59"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Lightning Arrow Gems","tradeId":"explicit.indexable_skill_60"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Arc Gems","tradeId":"explicit.indexable_skill_61"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Haste Gems","tradeId":"explicit.indexable_skill_62"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Purity of Elements Gems","tradeId":"explicit.indexable_skill_63"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Vitality Gems","tradeId":"explicit.indexable_skill_64"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Discipline Gems","tradeId":"explicit.indexable_skill_65"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Grace Gems","tradeId":"explicit.indexable_skill_66"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Determination Gems","tradeId":"explicit.indexable_skill_67"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Anger Gems","tradeId":"explicit.indexable_skill_68"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Hatred Gems","tradeId":"explicit.indexable_skill_69"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Wrath Gems","tradeId":"explicit.indexable_skill_70"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Burning Arrow Gems","tradeId":"explicit.indexable_skill_71"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Clarity Gems","tradeId":"explicit.indexable_skill_72"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Shockwave Totem Gems","tradeId":"explicit.indexable_skill_73"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Rejuvenation Totem Gems","tradeId":"explicit.indexable_skill_74"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Conversion Trap Gems","tradeId":"explicit.indexable_skill_75"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Bear Trap Gems","tradeId":"explicit.indexable_skill_76"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Fire Trap Gems","tradeId":"explicit.indexable_skill_77"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Decoy Totem Gems","tradeId":"explicit.indexable_skill_78"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Devouring Totem Gems","tradeId":"explicit.indexable_skill_79"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Ethereal Knives Gems","tradeId":"explicit.indexable_skill_80"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Arctic Armour Gems","tradeId":"explicit.indexable_skill_81"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Holy Flame Totem Gems","tradeId":"explicit.indexable_skill_82"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Flammability Gems","tradeId":"explicit.indexable_skill_83"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Frostbite Gems","tradeId":"explicit.indexable_skill_84"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Conductivity Gems","tradeId":"explicit.indexable_skill_85"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Incinerate Gems","tradeId":"explicit.indexable_skill_86"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Cyclone Gems","tradeId":"explicit.indexable_skill_87"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Searing Bond Gems","tradeId":"explicit.indexable_skill_88"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Reave Gems","tradeId":"explicit.indexable_skill_89"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Lightning Trap Gems","tradeId":"explicit.indexable_skill_90"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Pyroclast Mine Gems","tradeId":"explicit.indexable_skill_91"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Smoke Mine Gems","tradeId":"explicit.indexable_skill_92"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Icicle Mine Gems","tradeId":"explicit.indexable_skill_93"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Stormblast Mine Gems","tradeId":"explicit.indexable_skill_94"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Animate Guardian Gems","tradeId":"explicit.indexable_skill_95"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Spectral Throw Gems","tradeId":"explicit.indexable_skill_96"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Animate Weapon Gems","tradeId":"explicit.indexable_skill_97"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Purity of Fire Gems","tradeId":"explicit.indexable_skill_98"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Purity of Ice Gems","tradeId":"explicit.indexable_skill_99"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Purity of Lightning Gems","tradeId":"explicit.indexable_skill_100"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Storm Call Gems","tradeId":"explicit.indexable_skill_101"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Flameblast Gems","tradeId":"explicit.indexable_skill_102"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Barrage Gems","tradeId":"explicit.indexable_skill_103"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Ball Lightning Gems","tradeId":"explicit.indexable_skill_104"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Summon Raging Spirit Gems","tradeId":"explicit.indexable_skill_105"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Flame Surge Gems","tradeId":"explicit.indexable_skill_106"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Desecrate Gems","tradeId":"explicit.indexable_skill_107"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Flesh Offering Gems","tradeId":"explicit.indexable_skill_108"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Bone Offering Gems","tradeId":"explicit.indexable_skill_109"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Glacial Cascade Gems","tradeId":"explicit.indexable_skill_110"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Convocation Gems","tradeId":"explicit.indexable_skill_111"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Molten Strike Gems","tradeId":"explicit.indexable_skill_112"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Tornado Shot Gems","tradeId":"explicit.indexable_skill_113"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Herald of Ash Gems","tradeId":"explicit.indexable_skill_114"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Herald of Ice Gems","tradeId":"explicit.indexable_skill_115"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Herald of Thunder Gems","tradeId":"explicit.indexable_skill_116"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Poacher's Mark Gems","tradeId":"explicit.indexable_skill_117"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Lightning Tendrils Gems","tradeId":"explicit.indexable_skill_118"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Mirror Arrow Gems","tradeId":"explicit.indexable_skill_119"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Blink Arrow Gems","tradeId":"explicit.indexable_skill_120"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Kinetic Blast Gems","tradeId":"explicit.indexable_skill_121"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Summon Chaos Golem Gems","tradeId":"explicit.indexable_skill_122"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Summon Ice Golem Gems","tradeId":"explicit.indexable_skill_123"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Summon Flame Golem Gems","tradeId":"explicit.indexable_skill_124"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Summon Lightning Golem Gems","tradeId":"explicit.indexable_skill_125"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Ice Crash Gems","tradeId":"explicit.indexable_skill_126"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Rallying Cry Gems","tradeId":"explicit.indexable_skill_127"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Infernal Cry Gems","tradeId":"explicit.indexable_skill_128"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Vigilant Strike Gems","tradeId":"explicit.indexable_skill_129"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Rolling Magma Gems","tradeId":"explicit.indexable_skill_130"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Flame Dash Gems","tradeId":"explicit.indexable_skill_131"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Frost Blades Gems","tradeId":"explicit.indexable_skill_132"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Wild Strike Gems","tradeId":"explicit.indexable_skill_133"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Galvanic Arrow Gems","tradeId":"explicit.indexable_skill_134"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Blast Rain Gems","tradeId":"explicit.indexable_skill_135"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Bladefall Gems","tradeId":"explicit.indexable_skill_136"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Siege Ballista Gems","tradeId":"explicit.indexable_skill_137"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Blade Vortex Gems","tradeId":"explicit.indexable_skill_138"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Contagion Gems","tradeId":"explicit.indexable_skill_139"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Wither Gems","tradeId":"explicit.indexable_skill_140"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Essence Drain Gems","tradeId":"explicit.indexable_skill_141"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Ice Trap Gems","tradeId":"explicit.indexable_skill_142"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Orb of Storms Gems","tradeId":"explicit.indexable_skill_143"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Frost Bomb Gems","tradeId":"explicit.indexable_skill_144"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Summon Stone Golem Gems","tradeId":"explicit.indexable_skill_145"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Eviscerate Gems","tradeId":"explicit.indexable_skill_146"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Snipe Gems","tradeId":"explicit.indexable_skill_147"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Vengeful Cry Gems","tradeId":"explicit.indexable_skill_148"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Earthquake Gems","tradeId":"explicit.indexable_skill_149"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Sunder Gems","tradeId":"explicit.indexable_skill_150"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Lacerate Gems","tradeId":"explicit.indexable_skill_151"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Spirit Offering Gems","tradeId":"explicit.indexable_skill_152"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Frostbolt Gems","tradeId":"explicit.indexable_skill_153"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Vortex Gems","tradeId":"explicit.indexable_skill_154"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Blight Gems","tradeId":"explicit.indexable_skill_155"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Scorching Ray Gems","tradeId":"explicit.indexable_skill_156"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Blade Flurry Gems","tradeId":"explicit.indexable_skill_157"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Charged Dash Gems","tradeId":"explicit.indexable_skill_158"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Dark Bargain Gems","tradeId":"explicit.indexable_skill_159"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Storm Burst Gems","tradeId":"explicit.indexable_skill_160"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Cremation Gems","tradeId":"explicit.indexable_skill_161"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Bodyswap Gems","tradeId":"explicit.indexable_skill_162"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Volatile Dead Gems","tradeId":"explicit.indexable_skill_163"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Unearth Gems","tradeId":"explicit.indexable_skill_164"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Explosive Trap Gems","tradeId":"explicit.indexable_skill_165"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Siphoning Trap Gems","tradeId":"explicit.indexable_skill_166"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Flamethrower Trap Gems","tradeId":"explicit.indexable_skill_167"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Lightning Spire Trap Gems","tradeId":"explicit.indexable_skill_168"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Seismic Trap Gems","tradeId":"explicit.indexable_skill_169"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Vulnerability Gems","tradeId":"explicit.indexable_skill_170"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Tectonic Slam Gems","tradeId":"explicit.indexable_skill_171"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Spectral Shield Throw Gems","tradeId":"explicit.indexable_skill_172"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Herald of Purity Gems","tradeId":"explicit.indexable_skill_173"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Herald of Agony Gems","tradeId":"explicit.indexable_skill_174"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Consecrated Path Gems","tradeId":"explicit.indexable_skill_175"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Smite Gems","tradeId":"explicit.indexable_skill_176"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Scourge Arrow Gems","tradeId":"explicit.indexable_skill_177"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Toxic Rain Gems","tradeId":"explicit.indexable_skill_178"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Summon Holy Relic Gems","tradeId":"explicit.indexable_skill_179"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Winter Orb Gems","tradeId":"explicit.indexable_skill_180"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Storm Brand Gems","tradeId":"explicit.indexable_skill_181"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Armageddon Brand Gems","tradeId":"explicit.indexable_skill_182"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Brand Recall Gems","tradeId":"explicit.indexable_skill_183"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all War Banner Gems","tradeId":"explicit.indexable_skill_184"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Dread Banner Gems","tradeId":"explicit.indexable_skill_185"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Shattering Steel Gems","tradeId":"explicit.indexable_skill_186"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Lancing Steel Gems","tradeId":"explicit.indexable_skill_187"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Purifying Flame Gems","tradeId":"explicit.indexable_skill_188"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Soulrend Gems","tradeId":"explicit.indexable_skill_189"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Bane Gems","tradeId":"explicit.indexable_skill_190"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Divine Ire Gems","tradeId":"explicit.indexable_skill_191"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Wave of Conviction Gems","tradeId":"explicit.indexable_skill_192"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Zealotry Gems","tradeId":"explicit.indexable_skill_193"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Malevolence Gems","tradeId":"explicit.indexable_skill_194"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Precision Gems","tradeId":"explicit.indexable_skill_195"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Steelskin Gems","tradeId":"explicit.indexable_skill_196"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Dash Gems","tradeId":"explicit.indexable_skill_197"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Bladestorm Gems","tradeId":"explicit.indexable_skill_198"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Blood and Sand Gems","tradeId":"explicit.indexable_skill_199"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Berserk Gems","tradeId":"explicit.indexable_skill_200"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Perforate Gems","tradeId":"explicit.indexable_skill_201"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Chain Hook Gems","tradeId":"explicit.indexable_skill_202"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Frostblink Gems","tradeId":"explicit.indexable_skill_203"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Flesh and Stone Gems","tradeId":"explicit.indexable_skill_204"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Pride Gems","tradeId":"explicit.indexable_skill_205"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Cobra Lash Gems","tradeId":"explicit.indexable_skill_206"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Withering Step Gems","tradeId":"explicit.indexable_skill_207"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Venom Gyre Gems","tradeId":"explicit.indexable_skill_208"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Summon Skitterbots Gems","tradeId":"explicit.indexable_skill_209"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Plague Bearer Gems","tradeId":"explicit.indexable_skill_210"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Pestilent Strike Gems","tradeId":"explicit.indexable_skill_211"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Summon Carrion Golem Gems","tradeId":"explicit.indexable_skill_212"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Artillery Ballista Gems","tradeId":"explicit.indexable_skill_213"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Shrapnel Ballista Gems","tradeId":"explicit.indexable_skill_214"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Ensnaring Arrow Gems","tradeId":"explicit.indexable_skill_215"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Stormbind Gems","tradeId":"explicit.indexable_skill_216"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Blade Blast Gems","tradeId":"explicit.indexable_skill_217"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Spellslinger Gems","tradeId":"explicit.indexable_skill_218"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Kinetic Bolt Gems","tradeId":"explicit.indexable_skill_219"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Arcane Cloak Gems","tradeId":"explicit.indexable_skill_220"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Intimidating Cry Gems","tradeId":"explicit.indexable_skill_221"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Ancestral Cry Gems","tradeId":"explicit.indexable_skill_222"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Seismic Cry Gems","tradeId":"explicit.indexable_skill_223"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all General's Cry Gems","tradeId":"explicit.indexable_skill_224"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Arcanist Brand Gems","tradeId":"explicit.indexable_skill_225"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Penance Brand Gems","tradeId":"explicit.indexable_skill_226"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Wintertide Brand Gems","tradeId":"explicit.indexable_skill_227"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Earthshatter Gems","tradeId":"explicit.indexable_skill_228"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Sigil of Power Gems","tradeId":"explicit.indexable_skill_229"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Splitting Steel Gems","tradeId":"explicit.indexable_skill_230"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Flame Wall Gems","tradeId":"explicit.indexable_skill_231"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Blazing Salvo Gems","tradeId":"explicit.indexable_skill_232"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Crackling Lance Gems","tradeId":"explicit.indexable_skill_233"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Void Sphere Gems","tradeId":"explicit.indexable_skill_234"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Frost Shield Gems","tradeId":"explicit.indexable_skill_235"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Hydrosphere Gems","tradeId":"explicit.indexable_skill_236"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Hexblast Gems","tradeId":"explicit.indexable_skill_237"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Exsanguinate Gems","tradeId":"explicit.indexable_skill_238"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Corrupting Fever Gems","tradeId":"explicit.indexable_skill_239"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Petrified Blood Gems","tradeId":"explicit.indexable_skill_240"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Reap Gems","tradeId":"explicit.indexable_skill_241"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Defiance Banner Gems","tradeId":"explicit.indexable_skill_242"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Storm Rain Gems","tradeId":"explicit.indexable_skill_243"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Rage Vortex Gems","tradeId":"explicit.indexable_skill_244"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Shield Crush Gems","tradeId":"explicit.indexable_skill_245"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Summon Reaper Gems","tradeId":"explicit.indexable_skill_246"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Boneshatter Gems","tradeId":"explicit.indexable_skill_247"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Ambush Gems","tradeId":"explicit.indexable_skill_248"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Voltaxic Burst Gems","tradeId":"explicit.indexable_skill_249"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Battlemage's Cry Gems","tradeId":"explicit.indexable_skill_250"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Absolution Gems","tradeId":"explicit.indexable_skill_251"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Eye of Winter Gems","tradeId":"explicit.indexable_skill_252"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Spectral Helix Gems","tradeId":"explicit.indexable_skill_253"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Forbidden Rite Gems","tradeId":"explicit.indexable_skill_254"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Blade Trap Gems","tradeId":"explicit.indexable_skill_255"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Manabond Gems","tradeId":"explicit.indexable_skill_256"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Explosive Concoction Gems","tradeId":"explicit.indexable_skill_257"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Poisonous Concoction Gems","tradeId":"explicit.indexable_skill_258"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Temporal Rift Gems","tradeId":"explicit.indexable_skill_259"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Energy Blade Gems","tradeId":"explicit.indexable_skill_260"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Tornado Gems","tradeId":"explicit.indexable_skill_261"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Soul Link Gems","tradeId":"explicit.indexable_skill_262"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Flame Link Gems","tradeId":"explicit.indexable_skill_263"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Intuitive Link Gems","tradeId":"explicit.indexable_skill_264"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Protective Link Gems","tradeId":"explicit.indexable_skill_265"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Vampiric Link Gems","tradeId":"explicit.indexable_skill_266"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Destructive Link Gems","tradeId":"explicit.indexable_skill_267"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Galvanic Field Gems","tradeId":"explicit.indexable_skill_268"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Lightning Conduit Gems","tradeId":"explicit.indexable_skill_269"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Alchemist's Mark Gems","tradeId":"explicit.indexable_skill_270"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Frozen Legion Gems","tradeId":"explicit.indexable_skill_271"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Volcanic Fissure Gems","tradeId":"explicit.indexable_skill_272"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Automation Gems","tradeId":"explicit.indexable_skill_273"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Autoexertion Gems","tradeId":"explicit.indexable_skill_274"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Divine Retribution Gems","tradeId":"explicit.indexable_skill_275"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Conflagration Gems","tradeId":"explicit.indexable_skill_276"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Kinetic Fusillade Gems"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Kinetic Rain Gems","tradeId":"explicit.indexable_skill_278"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Somatic Shell Gems","tradeId":"explicit.indexable_skill_279"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Wall of Force Gems","tradeId":"explicit.indexable_skill_280"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Thunderstorm Gems","tradeId":"explicit.indexable_skill_281"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Kinetic Fusillade Gems"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Holy Hammers Gems","tradeId":"explicit.indexable_skill_283"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Shield of Light Gems","tradeId":"explicit.indexable_skill_284"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Holy Strike Gems","tradeId":"explicit.indexable_skill_285"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Divine Blast Gems","tradeId":"explicit.indexable_skill_286"}],"mod":"RandomSkillUnique__1"},{"filters":[{"range":[[3,3]],"ref":"# to Level of all Mana-Infused Staff Gems","tradeId":"explicit.indexable_skill_287"}],"mod":"RandomSkillUnique__1"}]}]} +{"base":"Cloth Belt","fixed":[{"filters":[{"range":[[20,30]],"ref":"# to Dexterity","tradeId":"explicit.stat_3261801346"}],"mod":"DexterityUnique__29"},{"filters":[{"range":[[1,1]],"ref":"Every 5 seconds, gain one of the following for 5 seconds:\nYour Hits are always Critical Strikes\nHits against you are always Critical Strikes\nAttacks cannot Hit you\nAttacks against you always Hit\nYour Damage with Hits is Lucky\nDamage of Hits against you is Lucky","tradeId":"explicit.stat_2501671832"}],"mod":"HinekoraButterflyEffectUnique__1"},{"filters":[{"range":[[60,80]],"ref":"# to maximum Life","tradeId":"explicit.stat_3299347043"}],"mod":"IncreasedLifeUnique__120"},{"filters":[{"range":[[20,30]],"ref":"# to Intelligence","tradeId":"explicit.stat_328541901"}],"mod":"IntelligenceUnique__32"},{"filters":[{"range":[[15,25]],"ref":"#% increased Stun and Block Recovery","tradeId":"implicit.stat_2511217560"}],"implicit":true,"mod":"StunRecoveryImplicitBelt1"}],"name":"Bound Fate"} diff --git a/tests/data/bundle/item-classes.ndjson b/tests/data/bundle/item-classes.ndjson index 715f4be..02366e9 100644 --- a/tests/data/bundle/item-classes.ndjson +++ b/tests/data/bundle/item-classes.ndjson @@ -15,3 +15,6 @@ {"id":"HeistBlueprint","itemClass":"Blueprints","tradeCategory":"heistmission.blueprint"} {"id":"ItemisedSanctum","itemClass":"Sanctum Research","tradeCategory":"sanctum.research"} {"id":"ExpeditionLogbook","itemClass":"Expedition Logbooks","tradeCategory":"logbook"} +{"id":"Helmet","itemClass":"Helmets","tradeCategory":"armour.helmet"} +{"id":"Amulet","itemClass":"Amulets","tradeCategory":"accessory.amulet"} +{"id":"Belt","itemClass":"Belts","tradeCategory":"accessory.belt"} diff --git a/tests/data/items/unique-bound-fate.txt b/tests/data/items/unique-bound-fate.txt new file mode 100644 index 0000000..68f8f08 --- /dev/null +++ b/tests/data/items/unique-bound-fate.txt @@ -0,0 +1,31 @@ +Item Class: Belts +Rarity: Unique +Bound Fate +Cloth Belt +-------- +Requirements: +Level: 49 +-------- +Item Level: 82 +-------- +{ Implicit Modifier } +21(15-25)% increased Stun and Block Recovery +-------- +{ Unique Modifier — Attribute } ++23(20-30) to Dexterity +{ Unique Modifier — Attribute } ++25(20-30) to Intelligence +{ Unique Modifier — Life } ++77(60-80) to maximum Life +{ Unique Modifier } +Every 5 seconds, gain one of the following for 5 seconds: +Your Hits are always Critical Strikes +Hits against you are always Critical Strikes +Attacks cannot Hit you +Attacks against you always Hit +Your Damage with Hits is Lucky +Damage of Hits against you is Lucky +-------- +Knowing what will happen +and knowing when it will happen +are two very different things. diff --git a/tests/data/items/unique-dark-monarch.txt b/tests/data/items/unique-dark-monarch.txt new file mode 100644 index 0000000..09285e1 --- /dev/null +++ b/tests/data/items/unique-dark-monarch.txt @@ -0,0 +1,30 @@ +Item Class: Helmets +Rarity: Unique +The Dark Monarch +Lich's Circlet +-------- +Energy Shield: 177 (augmented) +-------- +Requirements: +Level: 84 +Int: 224 (unmet) +-------- +Sockets: W W-W +-------- +Item Level: 86 +-------- +{ Unique Modifier — Defences, Energy Shield } ++51(50-100) to maximum Energy Shield +{ Unique Modifier — Minion, Gem } ++1 to Level of all Minion Skill Gems +{ Unique Modifier — Chaos, Resistance } ++34(27-37)% to Chaos Resistance +{ Unique Modifier } +50% reduced Light Radius +{ Unique Modifier } +Maximum number of Sentinels of Purity (Animated Weapons-Holy Armaments) is Doubled +Cannot have Minions other than Sentinels of Purity (Animated Weapons-Holy Armaments) +-------- +"Hate? You speak to me of hate? You have no idea what your persecution inflicts. +How it chokes the heart. Withers the soul. Judge me, and you judge yourself." +- Saresh, last words, to Sekhema Orbala diff --git a/tests/data/items/unique-replica-dragonfangs-flight.txt b/tests/data/items/unique-replica-dragonfangs-flight.txt new file mode 100644 index 0000000..15512a3 --- /dev/null +++ b/tests/data/items/unique-replica-dragonfangs-flight.txt @@ -0,0 +1,29 @@ +Item Class: Amulets +Rarity: Unique +Replica Dragonfang's Flight +Onyx Amulet +-------- +Requirements: +Level: 56 +-------- +Item Level: 80 +-------- +{ Corruption Implicit Modifier — Attribute } +5(4-6)% increased Dexterity +6(4-6)% increased Intelligence +-------- +{ Unique Modifier } ++3 to Level of all Storm Burst(Fireball-Mana-Infused Staff) Gems +{ Unique Modifier — Elemental, Resistance } ++9(5-10)% to all Elemental Resistances +{ Unique Modifier } +7(5-10)% increased Reservation Efficiency of Skills +{ Unique Modifier } +Items and Gems have 5(10-5)% reduced Attribute Requirements +(Attributes are Strength, Dexterity, and Intelligence) +-------- +"Did we make this? Why do we have no record of it? +We were warned that there would be consequences..." +- Administrator Qotra +-------- +Corrupted diff --git a/tests/data/stat-normalization-vectors.ndjson b/tests/data/stat-normalization-vectors.ndjson index 1e1a0fe..92238d8 100644 --- a/tests/data/stat-normalization-vectors.ndjson +++ b/tests/data/stat-normalization-vectors.ndjson @@ -14,6 +14,8 @@ {"candidates": ["#% increased Rarity of Items found", "100% increased Rarity of Items found"], "generic": "#% increased Rarity of Items found", "line": "100% increased Rarity of Items found"} {"candidates": ["#% to Critical Strike Chance", "+3.5% to Critical Strike Chance"], "generic": "#% to Critical Strike Chance", "line": "+3.5% to Critical Strike Chance"} {"candidates": ["Regenerate # Life per second", "Regenerate 1.4 Life per second"], "generic": "Regenerate # Life per second", "line": "Regenerate 1.4 Life per second"} +{"candidates": ["Maximum number of Sentinels of Purity (Animated Weapons-Holy Armaments) is Doubled\nCannot have Minions other than Sentinels of Purity (Animated Weapons-Holy Armaments)", "Maximum number of Sentinels of Purity is Doubled\nCannot have Minions other than Sentinels of Purity"], "generic": "Maximum number of Sentinels of Purity (Animated Weapons-Holy Armaments) is Doubled\nCannot have Minions other than Sentinels of Purity (Animated Weapons-Holy Armaments)", "line": "Maximum number of Sentinels of Purity (Animated Weapons-Holy Armaments) is Doubled\nCannot have Minions other than Sentinels of Purity (Animated Weapons-Holy Armaments)"} +{"candidates": ["Unique Monsters (Blood-Filled Vessel): #", "Unique Monsters (Blood-Filled Vessel): 5", "Unique Monsters: #", "Unique Monsters: 5"], "generic": "Unique Monsters (Blood-Filled Vessel): #", "line": "Unique Monsters (Blood-Filled Vessel): 5"} {"candidates": ["#% total to Cold Resistance", "7% total to Cold Resistance"], "generic": "#% total to Cold Resistance", "line": "7% total to Cold Resistance"} {"candidates": ["#% total to Fire Resistance", "7% total to Fire Resistance"], "generic": "#% total to Fire Resistance", "line": "7% total to Fire Resistance"} {"candidates": ["#% total to Lightning Resistance", "7% total to Lightning Resistance"], "generic": "#% total to Lightning Resistance", "line": "7% total to Lightning Resistance"} @@ -118,8 +120,8 @@ {"candidates": ["# Fractured Modifiers", "7 Fractured Modifiers"], "generic": "# Fractured Modifiers", "line": "7 Fractured Modifiers"} {"candidates": ["# Notable Passive Skills", "7 Notable Passive Skills"], "generic": "# Notable Passive Skills", "line": "7 Notable Passive Skills"} {"candidates": ["Mirrored Tablet has # Islands", "Mirrored Tablet has 7 Islands"], "generic": "Mirrored Tablet has # Islands", "line": "Mirrored Tablet has 7 Islands"} -{"candidates": ["Unique Monsters (Blood-Filled Vessel): #", "Unique Monsters (Blood-Filled Vessel): 7"], "generic": "Unique Monsters (Blood-Filled Vessel): #", "line": "Unique Monsters (Blood-Filled Vessel): 7"} -{"candidates": ["Non-Unique Monsters (Blood-Filled Vessel): #", "Non-Unique Monsters (Blood-Filled Vessel): 7"], "generic": "Non-Unique Monsters (Blood-Filled Vessel): #", "line": "Non-Unique Monsters (Blood-Filled Vessel): 7"} +{"candidates": ["Unique Monsters (Blood-Filled Vessel): #", "Unique Monsters (Blood-Filled Vessel): 7", "Unique Monsters: #", "Unique Monsters: 7"], "generic": "Unique Monsters (Blood-Filled Vessel): #", "line": "Unique Monsters (Blood-Filled Vessel): 7"} +{"candidates": ["Non-Unique Monsters (Blood-Filled Vessel): #", "Non-Unique Monsters (Blood-Filled Vessel): 7", "Non-Unique Monsters: #", "Non-Unique Monsters: 7"], "generic": "Non-Unique Monsters (Blood-Filled Vessel): #", "line": "Non-Unique Monsters (Blood-Filled Vessel): 7"} {"candidates": ["Quality (Elemental Damage Modifiers): #%", "Quality (Elemental Damage Modifiers): 7%"], "generic": "Quality (Elemental Damage Modifiers): #%", "line": "Quality (Elemental Damage Modifiers): 7%"} {"candidates": ["Quality (Caster Modifiers): #%", "Quality (Caster Modifiers): 7%"], "generic": "Quality (Caster Modifiers): #%", "line": "Quality (Caster Modifiers): 7%"} {"candidates": ["Quality (Attack Modifiers): #%", "Quality (Attack Modifiers): 7%"], "generic": "Quality (Attack Modifiers): #%", "line": "Quality (Attack Modifiers): 7%"} @@ -278,43 +280,43 @@ {"candidates": ["Reflection of Angling (Difficulty #)", "Reflection of Angling (Difficulty 7)"], "generic": "Reflection of Angling (Difficulty #)", "line": "Reflection of Angling (Difficulty 7)"} {"candidates": ["# to maximum Life", "7 to maximum Life"], "generic": "# to maximum Life", "line": "7 to maximum Life"} {"candidates": ["#% to Cold Resistance", "7% to Cold Resistance"], "generic": "#% to Cold Resistance", "line": "7% to Cold Resistance"} -{"candidates": ["#% to Fire Resistance", "7% to Fire Resistance"], "generic": "#% to Fire Resistance", "line": "7% to Fire Resistance"} {"candidates": ["# to maximum Mana", "7 to maximum Mana"], "generic": "# to maximum Mana", "line": "7 to maximum Mana"} +{"candidates": ["#% to Fire Resistance", "7% to Fire Resistance"], "generic": "#% to Fire Resistance", "line": "7% to Fire Resistance"} {"candidates": ["#% to Lightning Resistance", "7% to Lightning Resistance"], "generic": "#% to Lightning Resistance", "line": "7% to Lightning Resistance"} {"candidates": ["# to Dexterity", "7 to Dexterity"], "generic": "# to Dexterity", "line": "7 to Dexterity"} {"candidates": ["# to Strength", "7 to Strength"], "generic": "# to Strength", "line": "7 to Strength"} {"candidates": ["# to maximum Energy Shield", "7 to maximum Energy Shield"], "generic": "# to maximum Energy Shield", "line": "7 to maximum Energy Shield"} -{"candidates": ["#% to all Elemental Resistances", "7% to all Elemental Resistances"], "generic": "#% to all Elemental Resistances", "line": "7% to all Elemental Resistances"} {"candidates": ["Regenerate # Life per second", "Regenerate 7 Life per second"], "generic": "Regenerate # Life per second", "line": "Regenerate 7 Life per second"} {"candidates": ["# to Intelligence", "7 to Intelligence"], "generic": "# to Intelligence", "line": "7 to Intelligence"} +{"candidates": ["#% to all Elemental Resistances", "7% to all Elemental Resistances"], "generic": "#% to all Elemental Resistances", "line": "7% to all Elemental Resistances"} {"candidates": ["#% increased Stun and Block Recovery", "7% increased Stun and Block Recovery"], "generic": "#% increased Stun and Block Recovery", "line": "7% increased Stun and Block Recovery"} +{"candidates": ["#% increased Attack Speed (Local)", "7% increased Attack Speed (Local)"], "generic": "#% increased Attack Speed (Local)", "line": "7% increased Attack Speed (Local)"} {"candidates": ["#% to Chaos Resistance", "7% to Chaos Resistance"], "generic": "#% to Chaos Resistance", "line": "7% to Chaos Resistance"} {"candidates": ["# to maximum Energy Shield (Local)", "7 to maximum Energy Shield (Local)"], "generic": "# to maximum Energy Shield (Local)", "line": "7 to maximum Energy Shield (Local)"} {"candidates": ["# to Accuracy Rating", "7 to Accuracy Rating"], "generic": "# to Accuracy Rating", "line": "7 to Accuracy Rating"} {"candidates": ["#% to Global Critical Strike Multiplier", "7% to Global Critical Strike Multiplier"], "generic": "#% to Global Critical Strike Multiplier", "line": "7% to Global Critical Strike Multiplier"} {"candidates": ["#% increased Cast Speed", "7% increased Cast Speed"], "generic": "#% increased Cast Speed", "line": "7% increased Cast Speed"} -{"candidates": ["#% increased Attack Speed (Local)", "7% increased Attack Speed (Local)"], "generic": "#% increased Attack Speed (Local)", "line": "7% increased Attack Speed (Local)"} {"candidates": ["#% increased Attack Speed", "7% increased Attack Speed"], "generic": "#% increased Attack Speed", "line": "7% increased Attack Speed"} {"candidates": ["# to all Attributes", "7 to all Attributes"], "generic": "# to all Attributes", "line": "7 to all Attributes"} {"candidates": ["#% increased maximum Life", "7% increased maximum Life"], "generic": "#% increased maximum Life", "line": "7% increased maximum Life"} {"candidates": ["Adds # to # Physical Damage (Local)", "Adds 7 to # Physical Damage (Local)"], "generic": "Adds # to # Physical Damage (Local)", "line": "Adds 7 to # Physical Damage (Local)"} {"candidates": ["# to Evasion Rating", "7 to Evasion Rating"], "generic": "# to Evasion Rating", "line": "7 to Evasion Rating"} {"candidates": ["#% increased Global Critical Strike Chance", "7% increased Global Critical Strike Chance"], "generic": "#% increased Global Critical Strike Chance", "line": "7% increased Global Critical Strike Chance"} -{"candidates": ["# to Evasion Rating (Local)", "7 to Evasion Rating (Local)"], "generic": "# to Evasion Rating (Local)", "line": "7 to Evasion Rating (Local)"} {"candidates": ["#% increased Evasion Rating (Local)", "7% increased Evasion Rating (Local)"], "generic": "#% increased Evasion Rating (Local)", "line": "7% increased Evasion Rating (Local)"} +{"candidates": ["# to Evasion Rating (Local)", "7 to Evasion Rating (Local)"], "generic": "# to Evasion Rating (Local)", "line": "7 to Evasion Rating (Local)"} {"candidates": ["#% increased Armour (Local)", "7% increased Armour (Local)"], "generic": "#% increased Armour (Local)", "line": "7% increased Armour (Local)"} {"candidates": ["# to Armour (Local)", "7 to Armour (Local)"], "generic": "# to Armour (Local)", "line": "7 to Armour (Local)"} {"candidates": ["#% increased Energy Shield (Local)", "7% increased Energy Shield (Local)"], "generic": "#% increased Energy Shield (Local)", "line": "7% increased Energy Shield (Local)"} -{"candidates": ["#% increased maximum Energy Shield", "7% increased maximum Energy Shield"], "generic": "#% increased maximum Energy Shield", "line": "7% increased maximum Energy Shield"} -{"candidates": ["#% increased Armour and Evasion (Local)", "7% increased Armour and Evasion (Local)"], "generic": "#% increased Armour and Evasion (Local)", "line": "7% increased Armour and Evasion (Local)"} {"candidates": ["#% increased Light Radius", "7% increased Light Radius"], "generic": "#% increased Light Radius", "line": "7% increased Light Radius"} -{"candidates": ["Adds # to # Lightning Damage (Local)", "Adds 7 to # Lightning Damage (Local)"], "generic": "Adds # to # Lightning Damage (Local)", "line": "Adds 7 to # Lightning Damage (Local)"} -{"candidates": ["#% increased Evasion and Energy Shield (Local)", "7% increased Evasion and Energy Shield (Local)"], "generic": "#% increased Evasion and Energy Shield (Local)", "line": "7% increased Evasion and Energy Shield (Local)"} +{"candidates": ["#% increased maximum Energy Shield", "7% increased maximum Energy Shield"], "generic": "#% increased maximum Energy Shield", "line": "7% increased maximum Energy Shield"} {"candidates": ["#% increased Armour and Energy Shield (Local)", "7% increased Armour and Energy Shield (Local)"], "generic": "#% increased Armour and Energy Shield (Local)", "line": "7% increased Armour and Energy Shield (Local)"} +{"candidates": ["#% increased Critical Strike Chance", "7% increased Critical Strike Chance"], "generic": "#% increased Critical Strike Chance", "line": "7% increased Critical Strike Chance"} +{"candidates": ["#% increased Armour and Evasion (Local)", "7% increased Armour and Evasion (Local)"], "generic": "#% increased Armour and Evasion (Local)", "line": "7% increased Armour and Evasion (Local)"} {"candidates": ["Adds # to # Cold Damage (Local)", "Adds 7 to # Cold Damage (Local)"], "generic": "Adds # to # Cold Damage (Local)", "line": "Adds 7 to # Cold Damage (Local)"} {"candidates": ["Gain # Mana per Enemy Killed", "Gain 7 Mana per Enemy Killed"], "generic": "Gain # Mana per Enemy Killed", "line": "Gain 7 Mana per Enemy Killed"} +{"candidates": ["Adds # to # Lightning Damage (Local)", "Adds 7 to # Lightning Damage (Local)"], "generic": "Adds # to # Lightning Damage (Local)", "line": "Adds 7 to # Lightning Damage (Local)"} +{"candidates": ["#% increased Evasion and Energy Shield (Local)", "7% increased Evasion and Energy Shield (Local)"], "generic": "#% increased Evasion and Energy Shield (Local)", "line": "7% increased Evasion and Energy Shield (Local)"} {"candidates": ["#% increased Stun Duration on Enemies", "7% increased Stun Duration on Enemies"], "generic": "#% increased Stun Duration on Enemies", "line": "7% increased Stun Duration on Enemies"} -{"candidates": ["#% increased Critical Strike Chance", "7% increased Critical Strike Chance"], "generic": "#% increased Critical Strike Chance", "line": "7% increased Critical Strike Chance"} {"candidates": ["# to Armour", "7 to Armour"], "generic": "# to Armour", "line": "7 to Armour"} {"candidates": ["#% increased Global Physical Damage", "7% increased Global Physical Damage"], "generic": "#% increased Global Physical Damage", "line": "7% increased Global Physical Damage"} {"candidates": ["# to Accuracy Rating (Local)", "7 to Accuracy Rating (Local)"], "generic": "# to Accuracy Rating (Local)", "line": "7 to Accuracy Rating (Local)"} @@ -322,26 +324,25 @@ {"candidates": ["Adds # to # Fire Damage (Local)", "Adds 7 to # Fire Damage (Local)"], "generic": "Adds # to # Fire Damage (Local)", "line": "Adds 7 to # Fire Damage (Local)"} {"candidates": ["#% increased maximum Mana", "7% increased maximum Mana"], "generic": "#% increased maximum Mana", "line": "7% increased maximum Mana"} {"candidates": ["#% increased Projectile Speed", "7% increased Projectile Speed"], "generic": "#% increased Projectile Speed", "line": "7% increased Projectile Speed"} +{"candidates": ["#% increased Global Accuracy Rating", "7% increased Global Accuracy Rating"], "generic": "#% increased Global Accuracy Rating", "line": "7% increased Global Accuracy Rating"} {"candidates": ["#% more Monster Life", "7% more Monster Life"], "generic": "#% more Monster Life", "line": "7% more Monster Life"} {"candidates": ["#% chance to Suppress Spell Damage", "7% chance to Suppress Spell Damage"], "generic": "#% chance to Suppress Spell Damage", "line": "7% chance to Suppress Spell Damage"} {"candidates": ["Gain # Life per Enemy Hit with Attacks", "Gain 7 Life per Enemy Hit with Attacks"], "generic": "Gain # Life per Enemy Hit with Attacks", "line": "Gain 7 Life per Enemy Hit with Attacks"} -{"candidates": ["#% increased Global Accuracy Rating", "7% increased Global Accuracy Rating"], "generic": "#% increased Global Accuracy Rating", "line": "7% increased Global Accuracy Rating"} {"candidates": ["#% increased Evasion Rating", "7% increased Evasion Rating"], "generic": "#% increased Evasion Rating", "line": "7% increased Evasion Rating"} {"candidates": ["#% increased Duration", "7% increased Duration"], "generic": "#% increased Duration", "line": "7% increased Duration"} {"candidates": ["#% increased Spell Critical Strike Chance", "7% increased Spell Critical Strike Chance"], "generic": "#% increased Spell Critical Strike Chance", "line": "7% increased Spell Critical Strike Chance"} {"candidates": ["Reflects # Physical Damage to Melee Attackers", "Reflects 7 Physical Damage to Melee Attackers"], "generic": "Reflects # Physical Damage to Melee Attackers", "line": "Reflects 7 Physical Damage to Melee Attackers"} +{"candidates": ["#% increased Monster Damage", "7% increased Monster Damage"], "generic": "#% increased Monster Damage", "line": "7% increased Monster Damage"} {"candidates": ["Minions have #% increased maximum Life", "Minions have 7% increased maximum Life"], "generic": "Minions have #% increased maximum Life", "line": "Minions have 7% increased maximum Life"} {"candidates": ["#% increased Projectile Damage", "7% increased Projectile Damage"], "generic": "#% increased Projectile Damage", "line": "7% increased Projectile Damage"} {"candidates": ["#% chance to Ignite", "7% chance to Ignite"], "generic": "#% chance to Ignite", "line": "7% chance to Ignite"} -{"candidates": ["#% increased Damage over Time", "7% increased Damage over Time"], "generic": "#% increased Damage over Time", "line": "7% increased Damage over Time"} +{"candidates": ["Minions deal #% increased Damage", "Minions deal 7% increased Damage"], "generic": "Minions deal #% increased Damage", "line": "Minions deal 7% increased Damage"} {"candidates": ["#% reduced Enemy Stun Threshold", "7% reduced Enemy Stun Threshold"], "generic": "#% reduced Enemy Stun Threshold", "line": "7% reduced Enemy Stun Threshold"} {"candidates": ["#% increased Damage", "7% increased Damage"], "generic": "#% increased Damage", "line": "7% increased Damage"} -{"candidates": ["Minions deal #% increased Damage", "Minions deal 7% increased Damage"], "generic": "Minions deal #% increased Damage", "line": "Minions deal 7% increased Damage"} {"candidates": ["#% increased Armour", "7% increased Armour"], "generic": "#% increased Armour", "line": "7% increased Armour"} {"candidates": ["Area has #% chance to contain an Ultimatum Encounter", "Area has 7% chance to contain an Ultimatum Encounter"], "generic": "Area has #% chance to contain an Ultimatum Encounter", "line": "Area has 7% chance to contain an Ultimatum Encounter"} {"candidates": ["Your Maps have #% chance to contain an Ultimatum Encounter", "Your Maps have 7% chance to contain an Ultimatum Encounter"], "generic": "Your Maps have #% chance to contain an Ultimatum Encounter", "line": "Your Maps have 7% chance to contain an Ultimatum Encounter"} {"candidates": ["Your Maps have #% chance to contain a Mirror of Delirium", "Your Maps have 7% chance to contain a Mirror of Delirium"], "generic": "Your Maps have #% chance to contain a Mirror of Delirium", "line": "Your Maps have 7% chance to contain a Mirror of Delirium"} -{"candidates": ["#% increased Monster Damage", "7% increased Monster Damage"], "generic": "#% increased Monster Damage", "line": "7% increased Monster Damage"} {"candidates": ["Your Maps have #% chance to contain a Legion Encounter", "Your Maps have 7% chance to contain a Legion Encounter"], "generic": "Your Maps have #% chance to contain a Legion Encounter", "line": "Your Maps have 7% chance to contain a Legion Encounter"} {"candidates": ["#% chance to contain an Expedition Encounter", "7% chance to contain an Expedition Encounter"], "generic": "#% chance to contain an Expedition Encounter", "line": "7% chance to contain an Expedition Encounter"} {"candidates": ["Your Maps have #% chance to contain an Expedition Encounter", "Your Maps have 7% chance to contain an Expedition Encounter"], "generic": "Your Maps have #% chance to contain an Expedition Encounter", "line": "Your Maps have 7% chance to contain an Expedition Encounter"} @@ -351,19 +352,21 @@ {"candidates": ["Your Maps have #% chance to contain Ritual Altars", "Your Maps have 7% chance to contain Ritual Altars"], "generic": "Your Maps have #% chance to contain Ritual Altars", "line": "Your Maps have 7% chance to contain Ritual Altars"} {"candidates": ["Your Maps have #% chance to contain an Abyss", "Your Maps have 7% chance to contain an Abyss"], "generic": "Your Maps have #% chance to contain an Abyss", "line": "Your Maps have 7% chance to contain an Abyss"} {"candidates": ["#% increased Monster Movement Speed", "7% increased Monster Movement Speed"], "generic": "#% increased Monster Movement Speed", "line": "7% increased Monster Movement Speed"} -{"candidates": ["Your Maps have #% chance to contain The Sacred Grove", "Your Maps have 7% chance to contain The Sacred Grove"], "generic": "Your Maps have #% chance to contain The Sacred Grove", "line": "Your Maps have 7% chance to contain The Sacred Grove"} {"candidates": ["#% increased Monster Attack Speed", "7% increased Monster Attack Speed"], "generic": "#% increased Monster Attack Speed", "line": "7% increased Monster Attack Speed"} {"candidates": ["Your Maps have #% chance to contain a Blight Encounter", "Your Maps have 7% chance to contain a Blight Encounter"], "generic": "Your Maps have #% chance to contain a Blight Encounter", "line": "Your Maps have 7% chance to contain a Blight Encounter"} -{"candidates": ["Monsters deal #% extra Physical Damage as Lightning", "Monsters deal 7% extra Physical Damage as Lightning"], "generic": "Monsters deal #% extra Physical Damage as Lightning", "line": "Monsters deal 7% extra Physical Damage as Lightning"} -{"candidates": ["# to Strength and Intelligence", "7 to Strength and Intelligence"], "generic": "# to Strength and Intelligence", "line": "7 to Strength and Intelligence"} +{"candidates": ["Your Maps have #% chance to contain The Sacred Grove", "Your Maps have 7% chance to contain The Sacred Grove"], "generic": "Your Maps have #% chance to contain The Sacred Grove", "line": "Your Maps have 7% chance to contain The Sacred Grove"} {"candidates": ["# to Level of Socketed Gems", "7 to Level of Socketed Gems"], "generic": "# to Level of Socketed Gems", "line": "7 to Level of Socketed Gems"} +{"candidates": ["#% increased Damage over Time", "7% increased Damage over Time"], "generic": "#% increased Damage over Time", "line": "7% increased Damage over Time"} +{"candidates": ["# to Strength and Intelligence", "7 to Strength and Intelligence"], "generic": "# to Strength and Intelligence", "line": "7 to Strength and Intelligence"} {"candidates": ["#% increased Area of Effect", "7% increased Area of Effect"], "generic": "#% increased Area of Effect", "line": "7% increased Area of Effect"} {"candidates": ["#% to Damage over Time Multiplier", "7% to Damage over Time Multiplier"], "generic": "#% to Damage over Time Multiplier", "line": "7% to Damage over Time Multiplier"} {"candidates": ["#% Chance to Block (Shields)", "7% Chance to Block (Shields)"], "generic": "#% Chance to Block (Shields)", "line": "7% Chance to Block (Shields)"} {"candidates": ["# to Dexterity and Intelligence", "7 to Dexterity and Intelligence"], "generic": "# to Dexterity and Intelligence", "line": "7 to Dexterity and Intelligence"} {"candidates": ["# to Strength and Dexterity", "7 to Strength and Dexterity"], "generic": "# to Strength and Dexterity", "line": "7 to Strength and Dexterity"} -{"candidates": ["#% increased Flask Effect Duration", "7% increased Flask Effect Duration"], "generic": "#% increased Flask Effect Duration", "line": "7% increased Flask Effect Duration"} {"candidates": ["#% increased Charges per use", "7% increased Charges per use"], "generic": "#% increased Charges per use", "line": "7% increased Charges per use"} +{"candidates": ["#% chance to Shock", "7% chance to Shock"], "generic": "#% chance to Shock", "line": "7% chance to Shock"} +{"candidates": ["#% increased Mana Reservation Efficiency of Skills", "7% increased Mana Reservation Efficiency of Skills"], "generic": "#% increased Mana Reservation Efficiency of Skills", "line": "7% increased Mana Reservation Efficiency of Skills"} +{"candidates": ["Monsters deal #% extra Physical Damage as Lightning", "Monsters deal 7% extra Physical Damage as Lightning"], "generic": "Monsters deal #% extra Physical Damage as Lightning", "line": "Monsters deal 7% extra Physical Damage as Lightning"} {"candidates": ["Regenerate # Mana per second", "Regenerate 7 Mana per second"], "generic": "Regenerate # Mana per second", "line": "Regenerate 7 Mana per second"} {"candidates": ["Grants # Life per Enemy Hit", "Grants 7 Life per Enemy Hit"], "generic": "Grants # Life per Enemy Hit", "line": "Grants 7 Life per Enemy Hit"} {"candidates": ["Your Maps have #% chance to contain a Smuggler's Cache", "Your Maps have 7% chance to contain a Smuggler's Cache"], "generic": "Your Maps have #% chance to contain a Smuggler's Cache", "line": "Your Maps have 7% chance to contain a Smuggler's Cache"} @@ -371,3 +374,4 @@ {"candidates": ["#% increased Chaos Damage", "7% increased Chaos Damage"], "generic": "#% increased Chaos Damage", "line": "7% increased Chaos Damage"} {"candidates": ["#% increased Monster Cast Speed", "7% increased Monster Cast Speed"], "generic": "#% increased Monster Cast Speed", "line": "7% increased Monster Cast Speed"} {"candidates": ["Your Maps have #% chance to contain Alva", "Your Maps have 7% chance to contain Alva"], "generic": "Your Maps have #% chance to contain Alva", "line": "Your Maps have 7% chance to contain Alva"} +{"candidates": ["#% increased Energy Shield Recharge Rate", "7% increased Energy Shield Recharge Rate"], "generic": "#% increased Energy Shield Recharge Rate", "line": "7% increased Energy Shield Recharge Rate"} diff --git a/tests/item_pricing_test.cpp b/tests/item_pricing_test.cpp index 91a563f..cf35337 100644 --- a/tests/item_pricing_test.cpp +++ b/tests/item_pricing_test.cpp @@ -2496,3 +2496,113 @@ TEST_CASE("an Expedition Logbook is priced on one of its destinations at a time" CHECK(rows == 5); // the faction, the area, and three implicits } } + +TEST_CASE("a modifier that rolls over a list of names resolves as one modifier") { + auto gd = fixture(); + // The Dark Monarch doubles the limit of one minion skill gem out of sixteen, and with + // Advanced Mod Descriptions on the game prints that pool the way it prints a numeric + // range: the roll is the gem named in the wording, the parenthesis is the first and last + // of the list. Left in, neither line matched anything and both came back unrecognised. + const Item it = resolved(*gd, capture("unique-dark-monarch.txt")); + const Derived d = derive(gd.get(), it); + const SearchPlan p = build_plan(*gd, it, d); + + CHECK(p.strategy == Strategy::Unique); + CHECK(p.name == "The Dark Monarch"); + for (const std::string& n : p.notes) + CHECK(n.find("unrecognised modifier") == std::string::npos); + + // Two clipboard lines, one stat, and the option the item rolled is in the trade id. + const StatFilter* doubled = filter_for(p, "explicit.stat_56473917|10"); + REQUIRE(doubled != nullptr); + CHECK(doubled->text.find("(Animated Weapons-Holy Armaments)") != std::string::npos); + CHECK(doubled->text.find("Cannot have Minions other than") != std::string::npos); + + // The range is the pool, not a number: nothing about it is a bound. + CHECK_FALSE(doubled->min.has_value()); + CHECK_FALSE(doubled->max.has_value()); + CHECK_FALSE(doubled->roll_min.has_value()); + CHECK_FALSE(doubled->roll_max.has_value()); + + // And it is the one thing about this copy worth searching for: sixteen minion types roll + // here, so the modifier is pooled, ticked, and carries no "not in the modifier data". + CHECK(doubled->pooled); + CHECK(doubled->enabled); + CHECK(doubled->caveat.empty()); + + // And the numeric range on the same item is untouched by the stripping. + const StatFilter* es = filter_for(p, "explicit.stat_4052037485"); + REQUIRE(es != nullptr); + CHECK(es->roll_min == doctest::Approx(50)); + CHECK(es->roll_max == doctest::Approx(100)); +} + +TEST_CASE("a named range needs no space in front of it, and the pool can be every gem") { + auto gd = fixture(); + // The other shape of the same thing: Replica Dragonfang's Flight raises one skill gem out + // of every skill gem there is, so the wording carries a number *and* a name, and the game + // prints the pool tight against the name — "Storm Burst(Fireball-Mana-Infused Staff)". + const Item it = resolved(*gd, capture("unique-replica-dragonfangs-flight.txt")); + const Derived d = derive(gd.get(), it); + const SearchPlan p = build_plan(*gd, it, d); + + CHECK(p.name == "Replica Dragonfang's Flight"); + for (const std::string& n : p.notes) + CHECK(n.find("unrecognised modifier") == std::string::npos); + + // The gem the copy rolled is the trade filter; the "+3" is not a bound, because the game + // printed no range for it — the parenthesis belongs to the gem, not to the level. + const StatFilter* gem = filter_for(p, "explicit.indexable_skill_160"); + REQUIRE(gem != nullptr); + CHECK(gem->text.find("Storm Burst(Fireball-Mana-Infused Staff)") != std::string::npos); + CHECK_FALSE(gem->min.has_value()); + CHECK_FALSE(gem->max.has_value()); + // 287 gems can roll here, and which one it is is the item's whole price. + CHECK(gem->pooled); + CHECK(gem->enabled); + CHECK(gem->caveat.empty()); + + // The numeric ranges on the same item are untouched, the descending one included: the + // wording is the inverse, so the roll is stored negative and the window follows it down. + const StatFilter* attr = filter_for(p, "explicit.stat_752930724"); + REQUIRE(attr != nullptr); + CHECK(attr->min == doctest::Approx(-6)); + CHECK(attr->max == doctest::Approx(-5)); + + // And both corruption implicits under the one marker are still two modifiers. + CHECK(filter_for(p, "implicit.stat_4139681126") != nullptr); + CHECK(filter_for(p, "implicit.stat_656461285") != nullptr); +} + +TEST_CASE("a modifier that enumerates its alternatives is one modifier, however long") { + auto gd = fixture(); + // Bound Fate's modifier is seven clipboard lines: the promise, then the six things it + // can be. The join cap was four, so it could never be built, and all seven lines came + // back as unrecognised modifiers of their own. + const Item it = resolved(*gd, capture("unique-bound-fate.txt")); + const Derived d = derive(gd.get(), it); + const SearchPlan p = build_plan(*gd, it, d); + + CHECK(p.name == "Bound Fate"); + for (const std::string& n : p.notes) + CHECK(n.find("unrecognised modifier") == std::string::npos); + + const StatFilter* fate = filter_for(p, "explicit.stat_2501671832"); + REQUIRE(fate != nullptr); + CHECK(fate->text.find("Every 5 seconds") != std::string::npos); + CHECK(fate->text.find("Damage of Hits against you is Lucky") != std::string::npos); + + // Every copy has it and it rolls 1..1, so it is correctly left unticked — and, unlike + // before, without claiming the unique's own modifier data has never heard of it. + CHECK_FALSE(fate->enabled); + CHECK_FALSE(fate->pooled); + CHECK(fate->caveat.empty()); + + // Four filters and no more: the six alternatives are inside the one above, not beside it. + CHECK(p.stats.size() == 5); + CHECK(filter_for(p, "explicit.stat_3261801346") != nullptr); // Dexterity + CHECK(filter_for(p, "explicit.stat_328541901") != nullptr); // Intelligence + CHECK(filter_for(p, "explicit.stat_3299347043") != nullptr); // maximum Life + CHECK(filter_for(p, "implicit.stat_2511217560") != nullptr); // the belt's implicit +} + diff --git a/tests/normalize_test.cpp b/tests/normalize_test.cpp index 96f3601..f80f2dd 100644 --- a/tests/normalize_test.cpp +++ b/tests/normalize_test.cpp @@ -106,3 +106,33 @@ TEST_CASE("more than four numbers does not overflow the mask table") { CHECK_FALSE(cs.empty()); CHECK(cs.back() == line); } + +TEST_CASE("a range whose roll is a name is dropped") { + // The pool is the minion skill gems and the roll is the one named in the wording, so + // there is no numeric token for the parenthesis to belong to. Trade indexes one wording + // per gem, spelling it out. + CHECK(strip_named_ranges( + "Maximum number of Sentinels of Purity (Animated Weapons-Holy Armaments) " + "is Doubled") == "Maximum number of Sentinels of Purity is Doubled"); + + // A numeric token's own range belongs to that token, whether or not it reads as numbers. + CHECK(strip_named_ranges("+25(20-30)% to Cold Resistance") == + "+25(20-30)% to Cold Resistance"); + CHECK(strip_named_ranges("64(65-60)% reduced Effect") == "64(65-60)% reduced Effect"); + + // Neither is a parenthesis that is not a range at all. + CHECK(strip_named_ranges("Adds # to # Chaos Damage (Local)") == + "Adds # to # Chaos Damage (Local)"); + CHECK(strip_named_ranges("+51 to maximum Energy Shield (unscalable value)") == + "+51 to maximum Energy Shield (unscalable value)"); +} + +TEST_CASE("stripping a named range only ever adds candidates, and adds them last") { + // "(Blood-Filled Vessel)" is a real wording, not a range, and it has to resolve as + // printed. It qualifies on shape alone, which is why the stripped form is enumerated + // second rather than instead. + CHECK(candidates("Unique Monsters (Blood-Filled Vessel): 7") == + std::vector{"Unique Monsters (Blood-Filled Vessel): #", + "Unique Monsters (Blood-Filled Vessel): 7", + "Unique Monsters: #", "Unique Monsters: 7"}); +}