7 Commits

11 changed files with 351 additions and 130 deletions

View File

@@ -1,21 +1,36 @@
import csv
import json
import os
import sys
import argparse
from pathlib import Path
path = sys.argv[1] if len(sys.argv) >= 2 else "."
parser = argparse.ArgumentParser(
description="Process unique and set items from game data"
)
parser.add_argument(
"DATA_DIR", help="Path to d2 data dir containing local/ and global/"
)
parser.add_argument("OUTPUT_DIR", help="Path to destination directory")
args = parser.parse_args()
excelpath = Path(args.DATA_DIR) / "global/excel"
outputpath = Path(args.OUTPUT_DIR)
namespath = Path(args.DATA_DIR) / "local/lng/strings/item-names.json"
with namespath.open(encoding="utf-8-sig") as f:
names = json.load(f)
names = {name["Key"]: name["enUS"] for name in names}
category = "Base"
setitems = {}
with open(os.path.join(path, "setitems.txt")) as f:
with (excelpath / "setitems.txt").open() as f:
dr = csv.DictReader(f, delimiter="\t")
for row in dr:
if row["index"] == "Expansion":
category = row["index"]
continue
setitems[row["*ID"]] = {
"name": row["index"],
"set": row["set"],
"name": names[row["index"]],
"set": names[row["set"]],
"itembase": row["item"],
"req_lvl": int(row["lvl req"]),
"ilvl": int(row["lvl"]),
@@ -25,7 +40,7 @@ with open(os.path.join(path, "setitems.txt")) as f:
category = "Base"
uniqueitems = {}
with open(os.path.join(path, "uniqueitems.txt")) as f:
with (excelpath / "uniqueitems.txt").open() as f:
dr = csv.DictReader(f, delimiter="\t")
for row in dr:
if row["index"] in [
@@ -42,7 +57,7 @@ with open(os.path.join(path, "uniqueitems.txt")) as f:
if len(row["lvl req"]) == 0:
continue # deleted uniques
uniqueitems[row["*ID"]] = {
"name": row["index"],
"name": names[row["index"]],
"itembase": row["code"],
"req_lvl": int(row["lvl req"]),
"ilvl": int(row["lvl"]),
@@ -50,10 +65,10 @@ with open(os.path.join(path, "uniqueitems.txt")) as f:
"category": category,
}
with open("uniques.json", "w", newline="\n") as f:
with (outputpath / "uniques.json").open("w", newline="\n") as f:
json.dump(uniqueitems, f, indent=4)
f.write("\n")
with open("sets.json", "w", newline="\n") as f:
with (outputpath / "sets.json").open("w", newline="\n") as f:
json.dump(setitems, f, indent=4)
f.write("\n")

View File

@@ -3,6 +3,7 @@ from flask import Flask, redirect, abort, render_template, request
from pathlib import Path
import shutil
from datetime import datetime
import json
import psutil
from d2warehouse.item import Item, Quality, lookup_basetype
@@ -14,6 +15,7 @@ import re
from d2warehouse.stash import StashFullError
STASH_FILES = {
"softcore": "SharedStashSoftCoreV2.d2i",
"hardcore": "SharedStashHardCoreV2.d2i",
@@ -92,22 +94,26 @@ def storage_count(item: Item, stash: str) -> int | str:
db = get_stash_db(stash)
if item.is_simple:
return db.execute(
"SELECT COUNT(id) FROM item WHERE code = ? AND deleted IS NULL",
"SELECT COUNT(id) FROM item "
"WHERE code = ? AND deleted IS NULL AND socketed_into IS NULL",
(item.code,),
).fetchone()[0]
elif item.quality == Quality.UNIQUE:
return db.execute(
"SELECT COUNT(id) FROM item INNER JOIN item_extra ON id = item_id WHERE code = ? AND unique_id = ? AND deleted IS NULL",
"SELECT COUNT(id) FROM item INNER JOIN item_extra ON id = item_id "
"WHERE code = ? AND unique_id = ? AND deleted IS NULL AND socketed_into IS NULL",
(item.code, item.unique_id),
).fetchone()[0]
elif item.quality == Quality.SET:
return db.execute(
"SELECT COUNT(id) FROM item INNER JOIN item_extra ON id = item_id WHERE code = ? AND set_id = ? AND deleted IS NULL",
"SELECT COUNT(id) FROM item INNER JOIN item_extra ON id = item_id "
"WHERE code = ? AND set_id = ? AND deleted IS NULL AND socketed_into IS NULL",
(item.code, item.set_id),
).fetchone()[0]
elif item.is_runeword:
return db.execute(
"SELECT COUNT(id) FROM item INNER JOIN item_extra ON id = item_id WHERE code = ? AND runeword_id = ? AND deleted IS NULL",
"SELECT COUNT(id) FROM item INNER JOIN item_extra ON id = item_id "
"WHERE code = ? AND runeword_id = ? AND deleted IS NULL and socketed_into IS NULL",
(item.code, item.runeword_id),
).fetchone()[0]
else:
@@ -217,7 +223,9 @@ def list_storage(stash_name: str):
db = get_stash_db(stash_name)
items = {}
rows = db.execute("SELECT id FROM item WHERE deleted IS NULL").fetchall()
rows = db.execute(
"SELECT id FROM item WHERE deleted IS NULL and socketed_into IS NULL"
).fetchall()
for row in rows:
items[row["id"]] = Item.load_from_db(row["id"], db=db)
@@ -238,16 +246,21 @@ def list_storage_category(stash_name: str, category: str):
if category == "uniques":
q = db.execute(
"SELECT id FROM item INNER JOIN item_extra ON id = item_id WHERE deleted IS NULL AND quality = ?",
"SELECT id FROM item INNER JOIN item_extra ON id = item_id "
"WHERE deleted IS NULL AND socketed_into IS NULL AND quality = ?",
(int(Quality.UNIQUE),),
)
elif category == "sets":
q = db.execute(
"SELECT id FROM item INNER JOIN item_extra ON id = item_id WHERE deleted IS NULL AND quality = ?",
"SELECT id FROM item INNER JOIN item_extra ON id = item_id "
"WHERE deleted IS NULL AND socketed_into IS NULL AND quality = ?",
(int(Quality.SET),),
)
elif category == "misc":
q = db.execute("SELECT id FROM item WHERE deleted IS NULL AND is_simple = TRUE")
q = db.execute(
"SELECT id FROM item "
"WHERE deleted IS NULL AND socketed_into IS NULL AND is_simple = TRUE"
)
else:
return "Unexpected category", 400
@@ -260,6 +273,7 @@ def list_storage_category(stash_name: str, category: str):
"list_storage.html",
stash_name=stash_name,
storage_items=items,
category=category,
storage_count=lambda x: storage_count(x, stash_name),
)
@@ -325,14 +339,16 @@ def storage_currency_counts(item_codes: list[str], stash_name: str) -> dict:
for code in item_codes:
currencies[code] = {
"count": db.execute(
"SELECT COUNT(id) FROM item WHERE code = ?", (code,)
"SELECT COUNT(id) FROM item "
"WHERE code = ? AND deleted IS NULL AND socketed_into IS NULL",
(code,),
).fetchone()[0],
"name": lookup_basetype(code)["name"],
}
return currencies
@app.route("/storage/<stash_name>/currency")
@app.route("/currency/<stash_name>")
def storage_currency(stash_name: str):
if stash_name not in DB_FILES:
abort(404)
@@ -343,3 +359,86 @@ def storage_currency(stash_name: str):
return render_template(
"currency.html", runes=runes, gems=gems, keys=keys, essences=essences
)
def load_uniques_data():
"""Return a sorted dictionary of unique item ids indexed by names."""
uniques_path = Path(__file__).resolve().parent.parent / "data/uniques.json"
with uniques_path.open() as f:
data = json.load(f)
name_to_id = {v["name"]: int(k) for k, v in data.items()}
del name_to_id["Amulet of the Viper"]
del name_to_id["Staff of Kings"]
del name_to_id["Horadric Staff"]
del name_to_id["Khalim's Flail"]
del name_to_id["Khalim's Will"]
del name_to_id["Hell Forge Hammer"]
return {name: name_to_id[name] for name in sorted(name_to_id.keys())}
def load_sets_data():
"""Return a sorted dictionary of unique item ids indexed by names."""
uniques_path = Path(__file__).resolve().parent.parent / "data/sets.json"
with uniques_path.open() as f:
data = json.load(f)
name_to_id = {v["name"]: int(k) for k, v in data.items()}
return {name: name_to_id[name] for name in sorted(name_to_id.keys())}
all_uniques = load_uniques_data()
all_sets = load_sets_data()
def get_found_uniques(db):
rows = db.execute(
"SELECT DISTINCT unique_id "
"FROM item INNER JOIN item_extra ON id = item_id "
"WHERE quality = 7 AND deleted IS NULL AND socketed_into IS NULL",
).fetchall()
return {row["unique_id"]: True for row in rows}
def get_found_sets(db):
rows = db.execute(
"SELECT DISTINCT set_id "
"FROM item INNER JOIN item_extra ON id = item_id "
"WHERE quality = 5 AND deleted IS NULL AND socketed_into IS NULL",
).fetchall()
return {row["set_id"]: True for row in rows}
@app.route("/grail/<stash_name>")
def storage_grail(stash_name: str):
if stash_name not in DB_FILES:
abort(404)
db = get_stash_db(stash_name)
# Unique progress
found = get_found_uniques(db)
items = {name: found.get(id, False) for name, id in all_uniques.items()}
count = len(found)
total = len(all_uniques)
uniques = {
"list": items,
"count": count,
"total": total,
"progress": count / total * 100,
}
# Set progress
found = get_found_sets(db)
items = {name: found.get(id, False) for name, id in all_sets.items()}
count = len(found)
total = len(all_sets)
sets = {
"list": items,
"count": count,
"total": total,
"progress": count / total * 100,
}
return render_template(
"grail.html",
uniques=uniques,
sets=sets,
)

View File

@@ -11,3 +11,10 @@ function toggleSelectAll(tabIndex) {
cb.checked = !allSelected;
});
}
function toggleCollected() {
const collected = document.querySelectorAll('.collected');
collected.forEach(item => {
item.classList.toggle('hidden');
});
}

View File

@@ -5,6 +5,25 @@ body {
color: rgb(240, 240, 240);
}
nav ul {
list-style: none;
margin: 0;
padding: 0;
display: flex;
background-color: #444;
}
nav a {
display: block;
padding: 1rem 1.5rem;
text-decoration: none;
color: rgb(240, 240, 240);
}
nav a:hover {
background-color: #333;
}
.stash-tab {
display: grid;
grid-template-columns: repeat(5, 1fr);
@@ -104,3 +123,28 @@ input[type="checkbox"]:checked + label {
background-color: #444;
color: rgb(240, 240, 240);
}
ul.grail {
list-style: none;
display: flex;
flex-wrap: wrap;
gap: 0.5rem 2rem;
}
ul.grail li {
flex: 0 0 15rem;
padding: 0.5rem;
width: 15rem;
display: flex;
}
.hidden {
display: none !important;
}
.collected {
background-color: #343;
}
.uncollected {
background-color: #433;
}

View File

@@ -2,10 +2,19 @@
<html lang="en">
<head>
<meta charset="utf-8">
<title>Shared Stash</title>
<title>Currency</title>
<link rel="stylesheet" href="/static/style.css" />
<head>
<body>
{% include "menu.html" %}
<nav>
<ul>
<li><a href="/storage/{{stash_name or 'softcore'}}">All</a></li>
<li><a href="/storage/{{stash_name or 'softcore'}}/uniques">Uniques</a></li>
<li><a href="/storage/{{stash_name or 'softcore'}}/sets">Sets</a></li>
<li><a href="/storage/{{stash_name or 'softcore'}}/currency">Currency</a></li>
</ul>
</nav>
<div class="currencies">
<div>
<table>

View File

@@ -0,0 +1,28 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Grail</title>
<link rel="stylesheet" href="/static/style.css" />
<script src="/static/helpers.js"></script>
<head>
<body>
{% include "menu.html" %}
<button type="button" onclick="toggleCollected()">Toggle collected</button>
<h1>Unique Items</h1>
Progress: {{uniques.count}}/{{uniques.total}} ({{uniques.progress | round(1)}}%)
<ul class="grail">
{% for name,collected in uniques.list.items() %}
<li class="{{ 'collected' if collected else 'uncollected' }}">{{name}}</li>
{% endfor %}
</ul>
<h1>Set Items</h1>
Progress: {{sets.count}}/{{sets.total}} ({{sets.progress | round(1)}}%)
<ul class="grail">
{% for name,collected in sets.list.items() %}
<li class="{{ 'collected' if collected else 'uncollected' }}">{{name}}</li>
{% endfor %}
</ul>
</body>
</html>

View File

@@ -7,6 +7,7 @@
<script src="/static/helpers.js"></script>
<head>
<body>
{% include "menu.html" %}
<form action="/stash/{{stash_name}}/store" method="POST">
{% for tab in stash.tabs %}
{% set tabloop = loop %}

View File

@@ -2,13 +2,23 @@
<html lang="en">
<head>
<meta charset="utf-8">
<title>Storage</title>
<title>Storage - {{category or 'all'}}</title>
<link rel="stylesheet" href="/static/style.css" />
<head>
<body>
{% include "menu.html" %}
<nav>
<ul>
<li><a href="/storage/{{stash_name or 'softcore'}}">All</a></li>
<li><a href="/storage/{{stash_name or 'softcore'}}/uniques">Uniques</a></li>
<li><a href="/storage/{{stash_name or 'softcore'}}/sets">Sets</a></li>
<li><a href="/storage/{{stash_name or 'softcore'}}/misc">Misc</a></li>
</ul>
</nav>
<form action="/storage/{{stash_name}}/take" method="POST">
<div>
<!-- TODO: Include item.html -->
There are {{ storage_items | length }} items.
{% for db_id, item in storage_items.items() %}
<div class="storage-item-entry">
<input type="checkbox" name="item_{{db_id}}" id="item_{{db_id}}" value="take" />

View File

@@ -0,0 +1,8 @@
<nav>
<ul>
<li><a href="/stash/{{stash_name or 'softcore'}}">Stash</a></li>
<li><a href="/storage/{{stash_name or 'softcore'}}">Storage</a></li>
<li><a href="/grail/{{stash_name or 'softcore'}}">Grail</a></li>
<li><a href="/currency/{{stash_name or 'softcore'}}">Currency</a></li>
</ul>
</nav>

View File

@@ -397,7 +397,7 @@
},
"44": {
"name": "Berserker's Headgear",
"set": "Berserker's Garb",
"set": "Berserker's Arsenal",
"itembase": "hlm",
"req_lvl": 3,
"ilvl": 5,
@@ -406,7 +406,7 @@
},
"45": {
"name": "Berserker's Hauberk",
"set": "Berserker's Garb",
"set": "Berserker's Arsenal",
"itembase": "spl",
"req_lvl": 3,
"ilvl": 5,
@@ -415,7 +415,7 @@
},
"46": {
"name": "Berserker's Hatchet",
"set": "Berserker's Garb",
"set": "Berserker's Arsenal",
"itembase": "2ax",
"req_lvl": 3,
"ilvl": 5,
@@ -451,7 +451,7 @@
},
"50": {
"name": "Angelic Sickle",
"set": "Angelical Raiment",
"set": "Angelic Raiment",
"itembase": "sbr",
"req_lvl": 12,
"ilvl": 17,
@@ -460,7 +460,7 @@
},
"51": {
"name": "Angelic Mantle",
"set": "Angelical Raiment",
"set": "Angelic Raiment",
"itembase": "rng",
"req_lvl": 12,
"ilvl": 17,
@@ -469,7 +469,7 @@
},
"52": {
"name": "Angelic Halo",
"set": "Angelical Raiment",
"set": "Angelic Raiment",
"itembase": "rin",
"req_lvl": 12,
"ilvl": 17,
@@ -478,7 +478,7 @@
},
"53": {
"name": "Angelic Wings",
"set": "Angelical Raiment",
"set": "Angelic Raiment",
"itembase": "amu",
"req_lvl": 12,
"ilvl": 17,
@@ -612,7 +612,7 @@
"category": "Expansion"
},
"68": {
"name": "Aldur's Gauntlet",
"name": "Aldur's Rhythm",
"set": "Aldur's Watchtower",
"itembase": "9mt",
"req_lvl": 42,
@@ -684,7 +684,7 @@
"category": "Expansion"
},
"76": {
"name": "Tal Rasha's Fire-Spun Cloth",
"name": "Tal Rasha's Fine-Spun Cloth",
"set": "Tal Rasha's Wrappings",
"itembase": "zmb",
"req_lvl": 53,
@@ -711,7 +711,7 @@
"category": "Expansion"
},
"79": {
"name": "Tal Rasha's Howling Wind",
"name": "Tal Rasha's Guardianship",
"set": "Tal Rasha's Wrappings",
"itembase": "uth",
"req_lvl": 71,
@@ -747,7 +747,7 @@
"category": "Expansion"
},
"83": {
"name": "Griswolds's Redemption",
"name": "Griswold's Redemption",
"set": "Griswold's Legacy",
"itembase": "7ws",
"req_lvl": 53,
@@ -882,7 +882,7 @@
"category": "Expansion"
},
"98": {
"name": "Spiritual Custodian",
"name": "Dark Adherent",
"set": "The Disciple",
"itembase": "uui",
"req_lvl": 43,
@@ -909,7 +909,7 @@
"category": "Expansion"
},
"101": {
"name": "Heaven's Taebaek",
"name": "Taebaek's Glory",
"set": "Heaven's Brethren",
"itembase": "uts",
"req_lvl": 81,
@@ -918,7 +918,7 @@
"category": "Expansion"
},
"102": {
"name": "Haemosu's Adament",
"name": "Haemosu's Adamant",
"set": "Heaven's Brethren",
"itembase": "xrs",
"req_lvl": 44,
@@ -963,7 +963,7 @@
"category": "Expansion"
},
"107": {
"name": "Wihtstan's Guard",
"name": "Whitstan's Guard",
"set": "Orphan's Call",
"itembase": "xml",
"req_lvl": 29,
@@ -990,7 +990,7 @@
"category": "Expansion"
},
"110": {
"name": "Hwanin's Seal",
"name": "Hwanin's Blessing",
"set": "Hwanin's Majesty",
"itembase": "mbl",
"req_lvl": 35,
@@ -1071,7 +1071,7 @@
"category": "Expansion"
},
"119": {
"name": "Cow King's Hoofs",
"name": "Cow King's Hooves",
"set": "Cow King's Leathers",
"itembase": "vbt",
"req_lvl": 13,
@@ -1081,7 +1081,7 @@
},
"120": {
"name": "Naj's Puzzler",
"set": "Naj's Ancient Set",
"set": "Naj's Ancient Vestige",
"itembase": "6cs",
"req_lvl": 78,
"ilvl": 43,
@@ -1090,7 +1090,7 @@
},
"121": {
"name": "Naj's Light Plate",
"set": "Naj's Ancient Set",
"set": "Naj's Ancient Vestige",
"itembase": "ult",
"req_lvl": 71,
"ilvl": 43,
@@ -1099,7 +1099,7 @@
},
"122": {
"name": "Naj's Circlet",
"set": "Naj's Ancient Set",
"set": "Naj's Ancient Vestige",
"itembase": "ci0",
"req_lvl": 28,
"ilvl": 43,
@@ -1107,8 +1107,8 @@
"category": "Expansion"
},
"123": {
"name": "McAuley's Paragon",
"set": "McAuley's Folly",
"name": "Sander's Paragon",
"set": "Sander's Folly",
"itembase": "cap",
"req_lvl": 25,
"ilvl": 20,
@@ -1116,8 +1116,8 @@
"category": "Expansion"
},
"124": {
"name": "McAuley's Riprap",
"set": "McAuley's Folly",
"name": "Sander's Riprap",
"set": "Sander's Folly",
"itembase": "vbt",
"req_lvl": 20,
"ilvl": 20,
@@ -1125,8 +1125,8 @@
"category": "Expansion"
},
"125": {
"name": "McAuley's Taboo",
"set": "McAuley's Folly",
"name": "Sander's Taboo",
"set": "Sander's Folly",
"itembase": "vgl",
"req_lvl": 28,
"ilvl": 20,
@@ -1134,8 +1134,8 @@
"category": "Expansion"
},
"126": {
"name": "McAuley's Superstition",
"set": "McAuley's Folly",
"name": "Sander's Superstition",
"set": "Sander's Folly",
"itembase": "bwn",
"req_lvl": 25,
"ilvl": 20,

View File

@@ -24,7 +24,7 @@
"category": "Base"
},
"3": {
"name": "Mindrend",
"name": "Skull Splitter",
"itembase": "mpi",
"req_lvl": 21,
"ilvl": 28,
@@ -40,7 +40,7 @@
"category": "Base"
},
"5": {
"name": "Fechmars Axe",
"name": "Axe of Fechmar",
"itembase": "lax",
"req_lvl": 8,
"ilvl": 11,
@@ -56,7 +56,7 @@
"category": "Base"
},
"7": {
"name": "The Chieftan",
"name": "The Chieftain",
"itembase": "btx",
"req_lvl": 19,
"ilvl": 26,
@@ -72,7 +72,7 @@
"category": "Base"
},
"9": {
"name": "The Humongous",
"name": "Humongous",
"itembase": "gix",
"req_lvl": 29,
"ilvl": 39,
@@ -80,7 +80,7 @@
"category": "Base"
},
"10": {
"name": "Iros Torch",
"name": "Torch of Iro",
"itembase": "wnd",
"req_lvl": 5,
"ilvl": 7,
@@ -88,7 +88,7 @@
"category": "Base"
},
"11": {
"name": "Maelstromwrath",
"name": "Maelstrom",
"itembase": "ywn",
"req_lvl": 14,
"ilvl": 19,
@@ -104,7 +104,7 @@
"category": "Base"
},
"13": {
"name": "Umes Lament",
"name": "Ume's Lament",
"itembase": "gwn",
"req_lvl": 28,
"ilvl": 38,
@@ -168,7 +168,7 @@
"category": "Base"
},
"21": {
"name": "The Generals Tan Do Li Ga",
"name": "The General's Tan Do Li Ga",
"itembase": "fla",
"req_lvl": 21,
"ilvl": 28,
@@ -184,7 +184,7 @@
"category": "Base"
},
"23": {
"name": "Bonesob",
"name": "Bonesnap",
"itembase": "mau",
"req_lvl": 24,
"ilvl": 32,
@@ -200,7 +200,7 @@
"category": "Base"
},
"25": {
"name": "Rixots Keen",
"name": "Rixot's Keen",
"itembase": "ssd",
"req_lvl": 2,
"ilvl": 3,
@@ -216,7 +216,7 @@
"category": "Base"
},
"27": {
"name": "Krintizs Skewer",
"name": "Skewer of Krintiz",
"itembase": "sbr",
"req_lvl": 10,
"ilvl": 14,
@@ -240,7 +240,7 @@
"category": "Base"
},
"30": {
"name": "Griswolds Edge",
"name": "Griswold's Edge",
"itembase": "bsd",
"req_lvl": 17,
"ilvl": 23,
@@ -256,7 +256,7 @@
"category": "Base"
},
"32": {
"name": "Culwens Point",
"name": "Culwen's Point",
"itembase": "wsd",
"req_lvl": 29,
"ilvl": 39,
@@ -280,7 +280,7 @@
"category": "Base"
},
"35": {
"name": "Kinemils Awl",
"name": "Kinemil's Awl",
"itembase": "gis",
"req_lvl": 23,
"ilvl": 31,
@@ -336,7 +336,7 @@
"category": "Base"
},
"42": {
"name": "Irices Shard",
"name": "Spectral Shard",
"itembase": "bld",
"req_lvl": 25,
"ilvl": 34,
@@ -384,7 +384,7 @@
"category": "Base"
},
"48": {
"name": "Dimoaks Hew",
"name": "Dimoak's Hew",
"itembase": "bar",
"req_lvl": 8,
"ilvl": 11,
@@ -448,7 +448,7 @@
"category": "Base"
},
"56": {
"name": "Lazarus Spire",
"name": "Spire of Lazarus",
"itembase": "cst",
"req_lvl": 18,
"ilvl": 24,
@@ -488,7 +488,7 @@
"category": "Base"
},
"61": {
"name": "Rimeraven",
"name": "Raven Claw",
"itembase": "lbw",
"req_lvl": 15,
"ilvl": 20,
@@ -496,7 +496,7 @@
"category": "Base"
},
"62": {
"name": "Piercerib",
"name": "Rogue's Bow",
"itembase": "cbw",
"req_lvl": 20,
"ilvl": 27,
@@ -504,7 +504,7 @@
"category": "Base"
},
"63": {
"name": "Pullspite",
"name": "Stormstrike",
"itembase": "sbb",
"req_lvl": 25,
"ilvl": 34,
@@ -560,7 +560,7 @@
"category": "Base"
},
"70": {
"name": "Doomspittle",
"name": "Doomslinger",
"itembase": "rxb",
"req_lvl": 28,
"ilvl": 38,
@@ -568,7 +568,7 @@
"category": "Base"
},
"71": {
"name": "War Bonnet",
"name": "Biggin's Bonnet",
"itembase": "cap",
"req_lvl": 3,
"ilvl": 4,
@@ -640,7 +640,7 @@
"category": "Base"
},
"80": {
"name": "Blinkbats Form",
"name": "Blinkbat's Form",
"itembase": "lea",
"req_lvl": 12,
"ilvl": 16,
@@ -688,7 +688,7 @@
"category": "Base"
},
"86": {
"name": "Venomsward",
"name": "Venom Ward",
"itembase": "brs",
"req_lvl": 20,
"ilvl": 27,
@@ -736,7 +736,7 @@
"category": "Base"
},
"92": {
"name": "Victors Silk",
"name": "Silks of the Victor",
"itembase": "aar",
"req_lvl": 28,
"ilvl": 38,
@@ -896,7 +896,7 @@
"category": "Base"
},
"112": {
"name": "Lenyms Cord",
"name": "Lenymo",
"itembase": "lbl",
"req_lvl": 7,
"ilvl": 10,
@@ -1016,7 +1016,7 @@
"category": "Base"
},
"127": {
"name": "KhalimFlail",
"name": "Khalim's Flail",
"itembase": "qf1",
"req_lvl": 0,
"ilvl": 0,
@@ -1024,7 +1024,7 @@
"category": "Base"
},
"128": {
"name": "SuperKhalimFlail",
"name": "Khalim's Will",
"itembase": "qf2",
"req_lvl": 0,
"ilvl": 0,
@@ -1056,7 +1056,7 @@
"category": "Expansion"
},
"132": {
"name": "Pompe's Wrath",
"name": "Pompeii's Wrath",
"itembase": "9mp",
"req_lvl": 45,
"ilvl": 53,
@@ -1104,7 +1104,7 @@
"category": "Expansion"
},
"138": {
"name": "The Minataur",
"name": "The Minotaur",
"itembase": "9gi",
"req_lvl": 45,
"ilvl": 53,
@@ -1288,7 +1288,7 @@
"category": "Expansion"
},
"161": {
"name": "The Atlantian",
"name": "The Atlantean",
"itembase": "9wd",
"req_lvl": 42,
"ilvl": 50,
@@ -1496,7 +1496,7 @@
"category": "Expansion"
},
"187": {
"name": "Skullcollector",
"name": "Skull Collector",
"itembase": "8ws",
"req_lvl": 41,
"ilvl": 49,
@@ -1536,7 +1536,7 @@
"category": "Expansion"
},
"192": {
"name": "Whichwild String",
"name": "Witchwild String",
"itembase": "8s8",
"req_lvl": 39,
"ilvl": 47,
@@ -1560,7 +1560,7 @@
"category": "Expansion"
},
"195": {
"name": "Godstrike Arch",
"name": "Goldstrike Arch",
"itembase": "8lw",
"req_lvl": 46,
"ilvl": 54,
@@ -1576,7 +1576,7 @@
"category": "Expansion"
},
"197": {
"name": "Pus Spiter",
"name": "Pus Spitter",
"itembase": "8mx",
"req_lvl": 36,
"ilvl": 44,
@@ -1600,7 +1600,7 @@
"category": "Expansion"
},
"201": {
"name": "Peasent Crown",
"name": "Peasant Crown",
"itembase": "xap",
"req_lvl": 28,
"ilvl": 36,
@@ -1632,7 +1632,7 @@
"category": "Armor"
},
"205": {
"name": "Valkiry Wing",
"name": "Valkyrie Wing",
"itembase": "xhm",
"req_lvl": 44,
"ilvl": 52,
@@ -1656,7 +1656,7 @@
"category": "Armor"
},
"208": {
"name": "Vampiregaze",
"name": "Vampire Gaze",
"itembase": "xh9",
"req_lvl": 41,
"ilvl": 49,
@@ -1680,7 +1680,7 @@
"category": "Armor"
},
"211": {
"name": "Skin of the Flayerd One",
"name": "Skin of the Flayed One",
"itembase": "xla",
"req_lvl": 31,
"ilvl": 39,
@@ -1688,7 +1688,7 @@
"category": "Armor"
},
"212": {
"name": "Ironpelt",
"name": "Iron Pelt",
"itembase": "xtu",
"req_lvl": 33,
"ilvl": 41,
@@ -1696,7 +1696,7 @@
"category": "Armor"
},
"213": {
"name": "Spiritforge",
"name": "Spirit Forge",
"itembase": "xng",
"req_lvl": 35,
"ilvl": 43,
@@ -1776,7 +1776,7 @@
"category": "Armor"
},
"223": {
"name": "Que-Hegan's Wisdon",
"name": "Que-Hegan's Wisdom",
"itembase": "xtp",
"req_lvl": 51,
"ilvl": 59,
@@ -1792,7 +1792,7 @@
"category": "Armor"
},
"225": {
"name": "Mosers Blessed Circle",
"name": "Moser's Blessed Circle",
"itembase": "xml",
"req_lvl": 31,
"ilvl": 39,
@@ -1816,7 +1816,7 @@
"category": "Armor"
},
"228": {
"name": "Kerke's Sanctuary",
"name": "Gerke's Sanctuary",
"itembase": "xow",
"req_lvl": 44,
"ilvl": 52,
@@ -1824,7 +1824,7 @@
"category": "Armor"
},
"229": {
"name": "Radimant's Sphere",
"name": "Radament's Sphere",
"itembase": "xts",
"req_lvl": 50,
"ilvl": 58,
@@ -1872,7 +1872,7 @@
"category": "Armor"
},
"235": {
"name": "Lavagout",
"name": "Lava Gout",
"itembase": "xtg",
"req_lvl": 42,
"ilvl": 50,
@@ -1912,7 +1912,7 @@
"category": "Armor"
},
"240": {
"name": "Wartraveler",
"name": "War Traveler",
"itembase": "xtb",
"req_lvl": 42,
"ilvl": 50,
@@ -1920,7 +1920,7 @@
"category": "Armor"
},
"241": {
"name": "Gorerider",
"name": "Gore Rider",
"itembase": "xhb",
"req_lvl": 47,
"ilvl": 55,
@@ -1944,7 +1944,7 @@
"category": "Armor"
},
"244": {
"name": "Gloomstrap",
"name": "Gloom's Trap",
"itembase": "zmb",
"req_lvl": 36,
"ilvl": 45,
@@ -1960,7 +1960,7 @@
"category": "Armor"
},
"246": {
"name": "Thudergod's Vigor",
"name": "Thundergod's Vigor",
"itembase": "zhb",
"req_lvl": 47,
"ilvl": 55,
@@ -2120,7 +2120,7 @@
"category": "Elite Uniques"
},
"268": {
"name": "Bul Katho's Wedding Band",
"name": "Bul-Kathos' Wedding Band",
"itembase": "rin",
"req_lvl": 58,
"ilvl": 66,
@@ -2256,7 +2256,7 @@
"category": "Class Specific"
},
"286": {
"name": "Cutthroat1",
"name": "Bartuc's Cut-Throat",
"itembase": "9tw",
"req_lvl": 42,
"ilvl": 50,
@@ -2288,7 +2288,7 @@
"category": "Patch 1.10+"
},
"290": {
"name": "Djinnslayer",
"name": "Djinn Slayer",
"itembase": "7sm",
"req_lvl": 65,
"ilvl": 73,
@@ -2312,7 +2312,7 @@
"category": "Patch 1.10+"
},
"293": {
"name": "Gutsiphon",
"name": "Gut Siphon",
"itembase": "6rx",
"req_lvl": 71,
"ilvl": 79,
@@ -2320,7 +2320,7 @@
"category": "Patch 1.10+"
},
"294": {
"name": "Razoredge",
"name": "Razor's Edge",
"itembase": "7ha",
"req_lvl": 67,
"ilvl": 75,
@@ -2328,7 +2328,7 @@
"category": "Patch 1.10+"
},
"296": {
"name": "Demonlimb",
"name": "Demon Limb",
"itembase": "7sp",
"req_lvl": 63,
"ilvl": 71,
@@ -2336,7 +2336,7 @@
"category": "Patch 1.10+"
},
"297": {
"name": "Steelshade",
"name": "Steel Shade",
"itembase": "ulm",
"req_lvl": 62,
"ilvl": 70,
@@ -2352,7 +2352,7 @@
"category": "Patch 1.10+"
},
"299": {
"name": "Deaths's Web",
"name": "Death's Web",
"itembase": "7gw",
"req_lvl": 66,
"ilvl": 74,
@@ -2408,7 +2408,7 @@
"category": "Patch 1.10+"
},
"308": {
"name": "Jadetalon",
"name": "Jade Talon",
"itembase": "7wb",
"req_lvl": 66,
"ilvl": 74,
@@ -2416,7 +2416,7 @@
"category": "Patch 1.10+"
},
"309": {
"name": "Shadowdancer",
"name": "Shadow Dancer",
"itembase": "uhb",
"req_lvl": 71,
"ilvl": 79,
@@ -2424,7 +2424,7 @@
"category": "Patch 1.10+"
},
"310": {
"name": "Cerebus",
"name": "Cerebus' Bite",
"itembase": "drb",
"req_lvl": 63,
"ilvl": 71,
@@ -2440,7 +2440,7 @@
"category": "Patch 1.10+"
},
"312": {
"name": "Souldrain",
"name": "Soul Drainer",
"itembase": "umg",
"req_lvl": 74,
"ilvl": 82,
@@ -2448,7 +2448,7 @@
"category": "Patch 1.10+"
},
"313": {
"name": "Runemaster",
"name": "Rune Master",
"itembase": "72a",
"req_lvl": 72,
"ilvl": 80,
@@ -2456,7 +2456,7 @@
"category": "Patch 1.10+"
},
"314": {
"name": "Deathcleaver",
"name": "Death Cleaver",
"itembase": "7wa",
"req_lvl": 70,
"ilvl": 78,
@@ -2488,7 +2488,7 @@
"category": "Patch 1.10+"
},
"319": {
"name": "Wisp",
"name": "Wisp Projector",
"itembase": "rin",
"req_lvl": 76,
"ilvl": 84,
@@ -2552,7 +2552,7 @@
"category": "Patch 1.10+"
},
"327": {
"name": "Spiritkeeper",
"name": "Spirit Keeper",
"itembase": "drd",
"req_lvl": 67,
"ilvl": 75,
@@ -2576,7 +2576,7 @@
"category": "Patch 1.10+"
},
"330": {
"name": "Darkforge Spawn",
"name": "Darkforce Spawn",
"itembase": "nef",
"req_lvl": 64,
"ilvl": 72,
@@ -2592,7 +2592,7 @@
"category": "Patch 1.10+"
},
"332": {
"name": "Bloodraven's Charge",
"name": "Blood Raven's Charge",
"itembase": "amb",
"req_lvl": 71,
"ilvl": 79,
@@ -2608,7 +2608,7 @@
"category": "Patch 1.10+"
},
"334": {
"name": "Shadowkiller",
"name": "Shadow Killer",
"itembase": "7cs",
"req_lvl": 78,
"ilvl": 85,
@@ -2664,7 +2664,7 @@
"category": "Patch 1.10+"
},
"342": {
"name": "Steelpillar",
"name": "Steel Pillar",
"itembase": "7p7",
"req_lvl": 69,
"ilvl": 77,
@@ -2704,7 +2704,7 @@
"category": "Patch 1.10+"
},
"348": {
"name": "Steel Carapice",
"name": "Steel Carapace",
"itembase": "uul",
"req_lvl": 66,
"ilvl": 74,
@@ -2744,7 +2744,7 @@
"category": "Patch 1.10+"
},
"354": {
"name": "Fathom",
"name": "Death's Fathom",
"itembase": "obf",
"req_lvl": 73,
"ilvl": 81,
@@ -2840,7 +2840,7 @@
"category": "Patch 1.10+"
},
"367": {
"name": "Eschuta's temper",
"name": "Eschuta's Temper",
"itembase": "obc",
"req_lvl": 72,
"ilvl": 80,
@@ -2904,7 +2904,7 @@
"category": "Patch 1.10+"
},
"376": {
"name": "Verdugo's Hearty Cord",
"name": "Verdungo's Hearty Cord",
"itembase": "umc",
"req_lvl": 63,
"ilvl": 71,
@@ -2920,7 +2920,7 @@
"category": "Patch 1.10+"
},
"379": {
"name": "Giantskull",
"name": "Giant Skull",
"itembase": "uh9",
"req_lvl": 65,
"ilvl": 73,
@@ -2928,7 +2928,7 @@
"category": "Patch 1.10+"
},
"380": {
"name": "Ironward",
"name": "Astreon's Iron Ward",
"itembase": "7ws",
"req_lvl": 60,
"ilvl": 68,
@@ -2968,7 +2968,7 @@
"category": "Patch 1.10+"
},
"385": {
"name": "Earthshifter",
"name": "Earth Shifter",
"itembase": "7gm",
"req_lvl": 69,
"ilvl": 77,
@@ -2976,7 +2976,7 @@
"category": "Patch 1.10+"
},
"386": {
"name": "Wraithflight",
"name": "Wraith Flight",
"itembase": "7gl",
"req_lvl": 76,
"ilvl": 84,
@@ -3000,7 +3000,7 @@
"category": "Patch 1.10+"
},
"389": {
"name": "The Reedeemer",
"name": "The Redeemer",
"itembase": "7sc",
"req_lvl": 72,
"ilvl": 80,
@@ -3008,7 +3008,7 @@
"category": "Patch 1.10+"
},
"390": {
"name": "Headhunter's Glory",
"name": "Head Hunter's Glory",
"itembase": "ush",
"req_lvl": 75,
"ilvl": 83,