Compare commits

15 Commits

20 changed files with 157 additions and 1945 deletions

View File

@@ -1,23 +1,8 @@
# Installation Quick & dirty commit of current progress to share with others.
Don't.
# Development installation
Create a virtual environment and install the development set:
``` ```
python -m venv venv python -m venv venv
source venv/bin/activate source venv/bin/activate
pip install --editable .[dev] pip install --editable .[dev]
```
Set your save path and run the webserver to interact with the storage system:
```
export D2SAVE_PATH="/path/to/saves"
flask --app d2warehouse.app run
```
Some debug tooling:
```
d2dump /path/to/stash.d2i d2dump /path/to/stash.d2i
``` ```

View File

@@ -1,28 +0,0 @@
import json
import csv
import os
import sys
path = sys.argv[1] if len(sys.argv) >= 2 else "."
items = {}
item_patches = {
"tbk": {"class": "tome"},
"ibk": {"class": "tome"},
}
with open(os.path.join(path, "skills.json"), encoding="utf-8-sig") as f:
rows = json.load(f)
lookup_table = {}
for entry in rows:
key = entry["Key"]
text = entry["enUS"]
if len(text.strip()) == 0:
continue
lookup_table[key] = text
with open("skills.json", "w", newline="\n") as f:
json.dump(lookup_table, f, indent=4)
f.write("\n")

View File

@@ -13,9 +13,7 @@ path = sys.argv[1] if len(sys.argv) >= 2 else "."
# into one. Same applies for all stats. # into one. Same applies for all stats.
special_stats = { special_stats = {
"firemindam": {"template": "dmg-fire"}, "firemindam": {"template": "dmg-fire"},
"firemaxdam": { "firemaxdam": None,
"template": "fire-max"
}, # Cathan's rule, no other max ele dmg source exists
"lightmindam": {"template": "dmg-ltng"}, "lightmindam": {"template": "dmg-ltng"},
"lightmaxdam": None, "lightmaxdam": None,
"magicmindam": {"template": "dmg-mag"}, "magicmindam": {"template": "dmg-mag"},

View File

@@ -1,36 +1,21 @@
import csv import csv
import json import json
import argparse import os
from pathlib import Path import sys
parser = argparse.ArgumentParser( path = sys.argv[1] if len(sys.argv) >= 2 else "."
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" category = "Base"
setitems = {} setitems = {}
with (excelpath / "setitems.txt").open() as f: with open(os.path.join(path, "setitems.txt")) as f:
dr = csv.DictReader(f, delimiter="\t") dr = csv.DictReader(f, delimiter="\t")
for row in dr: for row in dr:
if row["index"] == "Expansion": if row["index"] == "Expansion":
category = row["index"] category = row["index"]
continue continue
setitems[row["*ID"]] = { setitems[row["*ID"]] = {
"name": names[row["index"]], "name": row["index"],
"set": names[row["set"]], "set": row["set"],
"itembase": row["item"], "itembase": row["item"],
"req_lvl": int(row["lvl req"]), "req_lvl": int(row["lvl req"]),
"ilvl": int(row["lvl"]), "ilvl": int(row["lvl"]),
@@ -40,7 +25,7 @@ with (excelpath / "setitems.txt").open() as f:
category = "Base" category = "Base"
uniqueitems = {} uniqueitems = {}
with (excelpath / "uniqueitems.txt").open() as f: with open(os.path.join(path, "uniqueitems.txt")) as f:
dr = csv.DictReader(f, delimiter="\t") dr = csv.DictReader(f, delimiter="\t")
for row in dr: for row in dr:
if row["index"] in [ if row["index"] in [
@@ -57,7 +42,7 @@ with (excelpath / "uniqueitems.txt").open() as f:
if len(row["lvl req"]) == 0: if len(row["lvl req"]) == 0:
continue # deleted uniques continue # deleted uniques
uniqueitems[row["*ID"]] = { uniqueitems[row["*ID"]] = {
"name": names[row["index"]], "name": row["index"],
"itembase": row["code"], "itembase": row["code"],
"req_lvl": int(row["lvl req"]), "req_lvl": int(row["lvl req"]),
"ilvl": int(row["lvl"]), "ilvl": int(row["lvl"]),
@@ -65,10 +50,10 @@ with (excelpath / "uniqueitems.txt").open() as f:
"category": category, "category": category,
} }
with (outputpath / "uniques.json").open("w", newline="\n") as f: with open("uniques.json", "w", newline="\n") as f:
json.dump(uniqueitems, f, indent=4) json.dump(uniqueitems, f, indent=4)
f.write("\n") f.write("\n")
with (outputpath / "sets.json").open("w", newline="\n") as f: with open("sets.json", "w", newline="\n") as f:
json.dump(setitems, f, indent=4) json.dump(setitems, f, indent=4)
f.write("\n") f.write("\n")

View File

@@ -3,10 +3,9 @@ from flask import Flask, redirect, abort, render_template, request
from pathlib import Path from pathlib import Path
import shutil import shutil
from datetime import datetime from datetime import datetime
import json
import psutil import psutil
from d2warehouse.item import Item, Quality, lookup_basetype from d2warehouse.item import Item, Quality
from d2warehouse.parser import parse_stash from d2warehouse.parser import parse_stash
import d2warehouse.db as base_db import d2warehouse.db as base_db
from d2warehouse.app.db import get_db, close_db from d2warehouse.app.db import get_db, close_db
@@ -15,7 +14,6 @@ import re
from d2warehouse.stash import StashFullError from d2warehouse.stash import StashFullError
STASH_FILES = { STASH_FILES = {
"softcore": "SharedStashSoftCoreV2.d2i", "softcore": "SharedStashSoftCoreV2.d2i",
"hardcore": "SharedStashHardCoreV2.d2i", "hardcore": "SharedStashHardCoreV2.d2i",
@@ -24,59 +22,6 @@ DB_FILES = {
"softcore": "d2warehouse.softcore.sqlite3", "softcore": "d2warehouse.softcore.sqlite3",
"hardcore": "d2warehouse.hardcore.sqlite3", "hardcore": "d2warehouse.hardcore.sqlite3",
} }
CURRENCY_RUNES = [f"r{i + 1:02d}" for i in range(33)]
CURRENCY_GEMS = [
"gcv",
"gfv",
"gsv",
"gzv",
"gpv",
"gcb",
"gfb",
"gsb",
"glb",
"gpb",
"gcg",
"gfg",
"gsg",
"glg",
"gpg",
"gcr",
"gfr",
"gsr",
"glr",
"gpr",
"gcw",
"gfw",
"gsw",
"glw",
"gpw",
"gcy",
"gfy",
"gsy",
"gly",
"gpy",
"skc",
"skf",
"sku",
"skl",
"skz",
]
CURRENCY_KEYS = [
"pk1",
"pk2",
"pk3",
"bey",
"mbr",
"dhn",
]
CURRENCY_ESSENCES = [
"tes",
"ceh",
"bet",
"fed",
"toa",
]
def d2_running() -> bool: def d2_running() -> bool:
@@ -84,7 +29,7 @@ def d2_running() -> bool:
try: try:
if proc.cmdline()[0].endswith("D2R.exe"): if proc.cmdline()[0].endswith("D2R.exe"):
return True return True
except (IndexError, psutil.AccessDenied, psutil.ZombieProcess): except (IndexError, psutil.AccessDenied):
pass pass
return False return False
@@ -94,26 +39,21 @@ def storage_count(item: Item, stash: str) -> int | str:
db = get_stash_db(stash) db = get_stash_db(stash)
if item.is_simple: if item.is_simple:
return db.execute( return db.execute(
"SELECT COUNT(id) FROM item " "SELECT COUNT(id) FROM item WHERE code = ?", (item.code,)
"WHERE code = ? AND deleted IS NULL AND socketed_into IS NULL",
(item.code,),
).fetchone()[0] ).fetchone()[0]
elif item.quality == Quality.UNIQUE: elif item.quality == Quality.UNIQUE:
return db.execute( return db.execute(
"SELECT COUNT(id) FROM item INNER JOIN item_extra ON id = item_id " "SELECT COUNT(id) FROM item INNER JOIN item_extra ON id = item_id WHERE code = ? AND unique_id = ?",
"WHERE code = ? AND unique_id = ? AND deleted IS NULL AND socketed_into IS NULL",
(item.code, item.unique_id), (item.code, item.unique_id),
).fetchone()[0] ).fetchone()[0]
elif item.quality == Quality.SET: elif item.quality == Quality.SET:
return db.execute( return db.execute(
"SELECT COUNT(id) FROM item INNER JOIN item_extra ON id = item_id " "SELECT COUNT(id) FROM item INNER JOIN item_extra ON id = item_id WHERE code = ? AND set_id = ?",
"WHERE code = ? AND set_id = ? AND deleted IS NULL AND socketed_into IS NULL",
(item.code, item.set_id), (item.code, item.set_id),
).fetchone()[0] ).fetchone()[0]
elif item.is_runeword: elif item.is_runeword:
return db.execute( return db.execute(
"SELECT COUNT(id) FROM item INNER JOIN item_extra ON id = item_id " "SELECT COUNT(id) FROM item INNER JOIN item_extra ON id = item_id WHERE code = ? AND runeword_id = ?",
"WHERE code = ? AND runeword_id = ? AND deleted IS NULL and socketed_into IS NULL",
(item.code, item.runeword_id), (item.code, item.runeword_id),
).fetchone()[0] ).fetchone()[0]
else: else:
@@ -223,17 +163,12 @@ def list_storage(stash_name: str):
db = get_stash_db(stash_name) db = get_stash_db(stash_name)
items = {} items = {}
rows = db.execute( rows = db.execute("SELECT id FROM item WHERE deleted IS NULL").fetchall()
"SELECT id FROM item WHERE deleted IS NULL and socketed_into IS NULL"
).fetchall()
for row in rows: for row in rows:
items[row["id"]] = Item.load_from_db(row["id"], db=db) items[row["id"]] = Item.load_from_db(row["id"], db=db)
return render_template( return render_template(
"list_storage.html", "list_storage.html", stash_name=stash_name, storage_items=items
stash_name=stash_name,
storage_items=items,
storage_count=lambda x: storage_count(x, stash_name),
) )
@@ -246,21 +181,16 @@ def list_storage_category(stash_name: str, category: str):
if category == "uniques": if category == "uniques":
q = db.execute( q = db.execute(
"SELECT id FROM item INNER JOIN item_extra ON id = item_id " "SELECT id FROM item INNER JOIN item_extra ON id = item_id WHERE deleted IS NULL AND quality = ?",
"WHERE deleted IS NULL AND socketed_into IS NULL AND quality = ?",
(int(Quality.UNIQUE),), (int(Quality.UNIQUE),),
) )
elif category == "sets": elif category == "sets":
q = db.execute( q = db.execute(
"SELECT id FROM item INNER JOIN item_extra ON id = item_id " "SELECT id FROM item INNER JOIN item_extra ON id = item_id WHERE deleted IS NULL AND quality = ?",
"WHERE deleted IS NULL AND socketed_into IS NULL AND quality = ?",
(int(Quality.SET),), (int(Quality.SET),),
) )
elif category == "misc": elif category == "misc":
q = db.execute( q = db.execute("SELECT id FROM item WHERE deleted IS NULL AND is_simple = TRUE")
"SELECT id FROM item "
"WHERE deleted IS NULL AND socketed_into IS NULL AND is_simple = TRUE"
)
else: else:
return "Unexpected category", 400 return "Unexpected category", 400
@@ -270,11 +200,7 @@ def list_storage_category(stash_name: str, category: str):
items[row["id"]] = Item.load_from_db(row["id"], db=db) items[row["id"]] = Item.load_from_db(row["id"], db=db)
return render_template( return render_template(
"list_storage.html", "list_storage.html", stash_name=stash_name, storage_items=items
stash_name=stash_name,
storage_items=items,
category=category,
storage_count=lambda x: storage_count(x, stash_name),
) )
@@ -331,114 +257,3 @@ def storage_take_items(stash_name: str):
tmp_path.replace(stash_path) tmp_path.replace(stash_path)
return redirect(f"/storage/{stash_name}", code=303) return redirect(f"/storage/{stash_name}", code=303)
def storage_currency_counts(item_codes: list[str], stash_name: str) -> dict:
db = get_stash_db(stash_name)
currencies = {}
for code in item_codes:
currencies[code] = {
"count": db.execute(
"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("/currency/<stash_name>")
def storage_currency(stash_name: str):
if stash_name not in DB_FILES:
abort(404)
runes = storage_currency_counts(CURRENCY_RUNES, stash_name)
gems = storage_currency_counts(CURRENCY_GEMS, stash_name)
keys = storage_currency_counts(CURRENCY_KEYS, stash_name)
essences = storage_currency_counts(CURRENCY_ESSENCES, stash_name)
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

@@ -1,20 +0,0 @@
function toggleSelectAll(tabIndex) {
const tab = document.querySelector(`[data-tab="${tabIndex}"]`);
const checkboxes = tab.querySelectorAll('input[type="checkbox"]');
if (checkboxes.length === 0)
return;
const allSelected = Array.from(checkboxes).every(cb => cb.checked);
checkboxes.forEach(cb => {
cb.checked = !allSelected;
});
}
function toggleCollected() {
const collected = document.querySelectorAll('.collected');
collected.forEach(item => {
item.classList.toggle('hidden');
});
}

View File

@@ -5,25 +5,6 @@ body {
color: rgb(240, 240, 240); 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 { .stash-tab {
display: grid; display: grid;
grid-template-columns: repeat(5, 1fr); grid-template-columns: repeat(5, 1fr);
@@ -31,19 +12,6 @@ nav a:hover {
margin: 0 auto; margin: 0 auto;
} }
.currencies {
display: flex;
gap: 50px
}
.currencies th {
text-align: left;
}
.currencies td {
text-align: right;
}
@media (max-width: 1600px) { @media (max-width: 1600px) {
.stash-tab { .stash-tab {
grid-template-columns: repeat(4, 1fr); grid-template-columns: repeat(4, 1fr);
@@ -66,27 +34,6 @@ nav a:hover {
display: none; display: none;
} }
.storage-item-entry {
position: relative;
width: fit-content;
}
.item-hover {
display: none;
position: absolute;
left: 0;
top: 30px;
z-index: 1000;
background: #222;
border: 1px solid #555;
padding: 8px;
min-width: 300px;
}
.storage-item-entry:hover .item-hover {
display: block;
}
.item .name { .item .name {
font-weight: bold; font-weight: bold;
} }
@@ -123,28 +70,3 @@ input[type="checkbox"]:checked + label {
background-color: #444; background-color: #444;
color: rgb(240, 240, 240); 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

@@ -1,62 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<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>
{% for code,currency in runes.items() %}
<tr>
<th>{{currency.name}}</th>
<td>{{currency.count}}</td>
</tr>
{% endfor %}
</table>
</div>
<div>
<table>
{% for code,currency in gems.items() %}
<tr>
<th>{{currency.name}}</th>
<td>{{currency.count}}</td>
</tr>
{% endfor %}
</table>
</div>
<div>
<table>
{% for code,currency in keys.items() %}
<tr>
<th>{{currency.name}}</th>
<td>{{currency.count}}</td>
</tr>
{% endfor %}
</table>
</div>
<div>
<table>
{% for code,currency in essences.items() %}
<tr>
<th>{{currency.name}}</th>
<td>{{currency.count}}</td>
</tr>
{% endfor %}
</table>
</div>
</div>
</body>
</html>

View File

@@ -1,28 +0,0 @@
<!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

@@ -1,4 +1,6 @@
<ul> <input type="checkbox" id="item_{{tabloop.index0}}_{{itemloop.index0}}" name="item_{{tabloop.index0}}_{{itemloop.index0}}" value="remove" />
<label class="item" for="item_{{tabloop.index0}}_{{itemloop.index0}}">
<ul>
<li class="name color-{{item.color}}">{{item.name}}</li> <li class="name color-{{item.color}}">{{item.name}}</li>
{% if item.quality and item.quality >= 5 %} {% if item.quality and item.quality >= 5 %}
<li class="name color-{{item.color}}">{{item.basename}}</li> <li class="name color-{{item.color}}">{{item.basename}}</li>
@@ -10,4 +12,5 @@
{% endfor %} {% endfor %}
{% endif %} {% endif %}
<li><input class="raw-item" type="text" name="raw item" value="{{item.raw().hex()}}" onfocus="this.select()" readonly></li> <li><input class="raw-item" type="text" name="raw item" value="{{item.raw().hex()}}" onfocus="this.select()" readonly></li>
</ul> </ul>
</label>

View File

@@ -4,21 +4,16 @@
<meta charset="utf-8"> <meta charset="utf-8">
<title>Shared Stash</title> <title>Shared Stash</title>
<link rel="stylesheet" href="/static/style.css" /> <link rel="stylesheet" href="/static/style.css" />
<script src="/static/helpers.js"></script>
<head> <head>
<body> <body>
{% include "menu.html" %}
<form action="/stash/{{stash_name}}/store" method="POST"> <form action="/stash/{{stash_name}}/store" method="POST">
{% for tab in stash.tabs %} {% for tab in stash.tabs %}
{% set tabloop = loop %} {% set tabloop = loop %}
<h2>Tab {{tabloop.index}} <button type="button" onclick="toggleSelectAll({{tabloop.index}})">Select All</button> </h2> <h2>Tab {{tabloop.index}}</h2>
<div class="stash-tab" data-tab="{{tabloop.index}}"> <div class="stash-tab">
{% for item in tab.items %} {% for item in tab.items %}
{% set itemloop = loop %} {% set itemloop = loop %}
<input type="checkbox" id="item_{{tabloop.index0}}_{{itemloop.index0}}" name="item_{{tabloop.index0}}_{{itemloop.index0}}" value="remove" />
<label class="item" for="item_{{tabloop.index0}}_{{itemloop.index0}}">
{% include "item.html" %} {% include "item.html" %}
</label>
{% endfor %} {% endfor %}
</div> </div>
{% endfor %} {% endfor %}

View File

@@ -2,30 +2,18 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<title>Storage - {{category or 'all'}}</title> <title>Storage</title>
<link rel="stylesheet" href="/static/style.css" /> <link rel="stylesheet" href="/static/style.css" />
<head> <head>
<body> <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"> <form action="/storage/{{stash_name}}/take" method="POST">
<div> <div>
<!-- TODO: Include item.html --> <!-- TODO: Include item.html -->
There are {{ storage_items | length }} items.
{% for db_id, item in storage_items.items() %} {% for db_id, item in storage_items.items() %}
<div class="storage-item-entry"> <div>
<input type="checkbox" name="item_{{db_id}}" id="item_{{db_id}}" value="take" /> <input type="checkbox" name="item_{{db_id}}" value="take" />
<label for="item_{{db_id}}">{{item.name}} ({{db_id}})</label> {{item.name}}
<div class="item-hover"> ({{db_id}})
{% include "item.html" %}
</div>
</div> </div>
{% endfor %} {% endfor %}
</div> </div>

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -273,14 +273,6 @@
"save_add": 0, "save_add": 0,
"save_param_bits": null "save_param_bits": null
}, },
"49": {
"text": "+# to Maximum Fire Damage",
"save_bits": [
9
],
"save_add": 0,
"save_param_bits": null
},
"50": { "50": {
"text": "Adds #-# Lightning Damage", "text": "Adds #-# Lightning Damage",
"save_bits": [ "save_bits": [

View File

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

View File

@@ -62,7 +62,7 @@ decode_tree = decodetree(code)
def decode(bits: bitarray, n) -> tuple[str, int]: def decode(bits: bitarray, n) -> tuple[str, int]:
text = "".join(itertools.islice(bits.decode(decode_tree), n)) text = "".join(itertools.islice(bits.iterdecode(decode_tree), n))
length = len(encode(text)) length = len(encode(text))
return text, length return text, length

View File

@@ -31,7 +31,6 @@ _unique_map = None
_set_item_map = None _set_item_map = None
_runeword_map = None _runeword_map = None
_affix_map = None _affix_map = None
_skills_map = None
class Quality(IntEnum): class Quality(IntEnum):
@@ -78,19 +77,9 @@ class Stat:
for val in self.values: for val in self.values:
subst_text = subst_text.replace("#", str(val), 1) subst_text = subst_text.replace("#", str(val), 1)
if param: if param:
subst_text = self.try_add_skill_text(subst_text) subst_text = re.sub(r"\[[^\]]*\]", str(param), subst_text, 1)
return subst_text return subst_text
def try_add_skill_text(self, subst_text: str) -> str | None:
if self.id == 107: # +X to [Skill] ([Class] only)
return re.sub(r"\[[^\]]*\]", lookup_skill_name(self.parameter), subst_text, 1)
elif self.id == 188: # +X to [Skill]
if '[' in subst_text:
return subst_text[:subst_text.find('[')] + lookup_random_skill_tab(self.parameter)
return re.sub(r"\[[^\]]*\]", lookup_random_skill_tab(self.parameter), subst_text, 1)
else:
return re.sub(r"\[[^\]]*\]", str(self.parameter), subst_text, 1)
def txtbits(bits: bitarray) -> str: def txtbits(bits: bitarray) -> str:
txt = "".join(str(b) for b in bits) txt = "".join(str(b) for b in bits)
@@ -557,57 +546,3 @@ def lookup_affix(id: int, prefix: bool) -> dict:
with open(os.path.join(_data_path, "affixes.json")) as f: with open(os.path.join(_data_path, "affixes.json")) as f:
_affix_map = json.load(f) _affix_map = json.load(f)
return _affix_map["prefixes" if prefix else "suffixes"][str(id)] return _affix_map["prefixes" if prefix else "suffixes"][str(id)]
def _get_skills_map():
global _skills_map
if _skills_map is None:
with open(os.path.join(_data_path, "skills.json")) as f:
_skills_map = json.load(f)
return _skills_map
def lookup_skill_name(id: int) -> str:
# FIXME: This hackish way of calculating the key is because we directly index into local/lng/strings/skills.json
# but the actual ID points to global/excel/skills.txt
skills_map = _get_skills_map()
try:
try:
return skills_map[f"Skillname{id + 1}"]
except KeyError:
return skills_map[f"skillname{id}"]
except KeyError:
return f"<Invalid key: skillname{id} or Skillname{id + 1}>"
def lookup_random_skill_tab(id: int) -> str:
# (ClassId * 8) is the id of the first tab for that class
skills_map = _get_skills_map()
class_name = lookup_class(int(id / 8))
two_letter_class_code = lookup_class(int(id / 8))[:2]
tab_index = 3 - id % 8 # for some reason local/lng/strings/skills.json index backwards (0 -> 3, 1 -> 2, ...)
try:
return skills_map[f"SkillCategory{two_letter_class_code}{tab_index}"] + f" ({class_name} only)"
except KeyError:
return f"<Invalid random skill tab: {id}>"
def lookup_class(id: int) -> str:
match id:
case 0:
return "Amazon"
case 1:
return "Sorceress"
case 2:
return "Necromancer"
case 3:
return "Paladin"
case 4:
return "Barbarian"
case 5:
return "Druid"
case 6:
return "Assassin"
case _:
# TODO: In 3.11 replace this with assert_never
assert False, f"{id} Should be unreachable"

View File

@@ -239,12 +239,3 @@ class ParseItemTest(unittest.TestCase):
self.assertAlmostEqual( self.assertAlmostEqual(
lookup_affix(item.suffixes[2], False)["name"], "of Charged Shield" lookup_affix(item.suffixes[2], False)["name"], "of Charged Shield"
) )
def test_cathans_rule(self):
# Cathan's Rule: Only source of fire-max stat
data = bytes.fromhex(
"10008000050054c90448ad74276f910150448c180afc24ff1348fd3f212d85b415d25a48fb0f"
)
data, item = parse_item(data)
self.assertEqual(data, b"")
self.assertEqual(str(item.stats[0]), "+10 to Maximum Fire Damage")