WIP: epitaxy pre-switch from baalxp
This commit is contained in:
Binary file not shown.
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
@@ -0,0 +1,114 @@
|
||||
# Convert the stash sale list into the CSV that tools/traderie_lister.py expects.
|
||||
#
|
||||
# python scripts/make_traderie_csv.py # everything sellable
|
||||
# python scripts/make_traderie_csv.py --runes # runes only
|
||||
#
|
||||
# Writes traderie_items.csv, then:
|
||||
# python tools/traderie_lister.py --csv traderie_items.csv --dry-run
|
||||
import csv
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
os.chdir(_root)
|
||||
|
||||
SRC = "stash_sale_list.csv"
|
||||
OUT = "traderie_items.csv"
|
||||
|
||||
COLUMNS = ["name", "qty", "base_tier", "base_item", "ring_variant",
|
||||
"amulet_variant", "ethereal", "pricing", "price_amount",
|
||||
"price_currency"]
|
||||
|
||||
# Runewords need a "Base Item" field. "No base" is the lister's own escape
|
||||
# hatch for when the socketed base does not matter to the listing.
|
||||
RUNEWORDS = {"spirit", "insight", "stealth", "lore", "smoke", "rhyme",
|
||||
"ancients pledge", "leaf", "malice", "steel", "zephyr", "king's grace",
|
||||
"edge", "harmony", "white", "peace", "myth", "black", "honor"}
|
||||
|
||||
# Not tradeable / not worth a listing. Potions are consumables; sub-perfect gems
|
||||
# were already dropped upstream but are re-checked here so this script is safe to
|
||||
# run against any sale list.
|
||||
SKIP_WORDS = ("rejuvenation potion", "scroll of", "tome of", "key of",
|
||||
"chipped", "flawed", "flawless")
|
||||
|
||||
|
||||
def title_case(name: str) -> str:
|
||||
"""Stash tooltips are ALL CAPS; Traderie searches title case.
|
||||
|
||||
Apostrophes matter: "TAL RASHA'S" must become "Tal Rasha's", not
|
||||
"Tal Rasha'S", or the dropdown search misses.
|
||||
"""
|
||||
out = []
|
||||
for word in name.lower().split():
|
||||
if "'" in word:
|
||||
head, _, tail = word.partition("'")
|
||||
out.append(head.capitalize() + "'" + tail)
|
||||
else:
|
||||
out.append(word.capitalize())
|
||||
return " ".join(out)
|
||||
|
||||
|
||||
def main():
|
||||
runes_only = "--runes" in sys.argv
|
||||
with open(SRC, encoding="utf-8") as f:
|
||||
rows = list(csv.DictReader(f))
|
||||
|
||||
seen: dict[str, dict] = {}
|
||||
skipped = 0
|
||||
for r in rows:
|
||||
raw = (r["name"] or "").strip()
|
||||
low = raw.lower()
|
||||
if not raw or any(w in low for w in SKIP_WORDS):
|
||||
skipped += 1
|
||||
continue
|
||||
if runes_only and not low.endswith("rune"):
|
||||
continue
|
||||
|
||||
name = title_case(raw)
|
||||
qty = r.get("qty") or "1"
|
||||
try:
|
||||
qty = int(qty)
|
||||
except ValueError:
|
||||
qty = 1
|
||||
|
||||
# The same item can sit in several cells (2x TARNHELM, 4x SMALL CHARM OF
|
||||
# INERTIA). Traderie wants one listing with a quantity, not N listings.
|
||||
if name in seen:
|
||||
seen[name]["qty"] += qty
|
||||
continue
|
||||
|
||||
base_item = "No base" if name.lower() in RUNEWORDS else ""
|
||||
seen[name] = {
|
||||
"name": name,
|
||||
"qty": qty,
|
||||
"base_tier": "",
|
||||
"base_item": base_item,
|
||||
"ring_variant": "",
|
||||
"amulet_variant": "",
|
||||
"ethereal": "",
|
||||
# You said you would set prices yourself — ask_for_offers means the
|
||||
# listing goes up without committing to a number.
|
||||
"pricing": "ask_for_offers",
|
||||
"price_amount": "",
|
||||
"price_currency": "",
|
||||
}
|
||||
|
||||
out = sorted(seen.values(), key=lambda r: (not r["name"].endswith("Rune"), r["name"]))
|
||||
with open(OUT, "w", newline="", encoding="utf-8") as f:
|
||||
w = csv.DictWriter(f, fieldnames=COLUMNS)
|
||||
w.writeheader()
|
||||
w.writerows(out)
|
||||
|
||||
print(f"wrote {OUT}: {len(out)} listings ({skipped} skipped as untradeable)")
|
||||
runes = [r for r in out if r["name"].endswith("Rune")]
|
||||
print(f" runes: {len(runes)} other: {len(out) - len(runes)}")
|
||||
needs_base = [r["name"] for r in out if r["base_item"]]
|
||||
if needs_base:
|
||||
print(f" runewords given base_item='No base': {', '.join(needs_base)}")
|
||||
print("\nnext:")
|
||||
print(f" python tools/traderie_lister.py --csv {OUT} --dry-run")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,380 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
traderie_lister.py
|
||||
|
||||
Automates creating "sell" listings on Traderie (https://traderie.com) for
|
||||
Diablo II: Resurrected, based on the workflow used to bulk-list ~20 items
|
||||
for account "alexpolo" in September 2026.
|
||||
|
||||
HOW IT WORKS
|
||||
------------
|
||||
Traderie's "Add Listing" form is a React app built on react-select style
|
||||
dropdowns. It does NOT have simple <select> elements, so this script drives
|
||||
it like a human would: click the combobox, wait for the option list to
|
||||
render, click the matching option by visible text.
|
||||
|
||||
The set of fields shown on the form depends on the item you pick:
|
||||
- Runes / plain items: Ladder, Platform, Mode, Game version
|
||||
- Ethereal-capable uniques: + "Ethereal" checkbox
|
||||
- Charms / belts / boots / etc: + "Base Tier" (Normal/Exceptional/Elite)
|
||||
- Rings: + "Ring Variant"
|
||||
- Amulets w/ multiple looks: + "Amulet Variant"
|
||||
- Runewords: + "Base Item" (weapon/armor the rune-
|
||||
word is socketed into; select "No base"
|
||||
if you don't know / don't care)
|
||||
- Jewels (e.g. Rainbow Facet): + a cosmetic gem-color dropdown (any
|
||||
value is fine, it's just the icon
|
||||
color shown in the listing)
|
||||
|
||||
This script fills in whichever of those fields are present and skips
|
||||
whichever aren't, using best-effort text matching.
|
||||
|
||||
LOGIN
|
||||
-----
|
||||
Traderie authenticates via Discord OAuth. This script does NOT attempt to
|
||||
log in for you. Run once with `--setup-login` to open a real browser
|
||||
window, log in manually, and save the session (cookies + localStorage) to
|
||||
`auth_state.json`. Every subsequent run reuses that file headlessly.
|
||||
|
||||
USAGE
|
||||
-----
|
||||
pip install playwright
|
||||
playwright install chromium
|
||||
|
||||
# one-time login capture
|
||||
python traderie_lister.py --setup-login
|
||||
|
||||
# dry run (prints what it would do, doesn't submit)
|
||||
python traderie_lister.py --csv items.csv --dry-run
|
||||
|
||||
# for real
|
||||
python traderie_lister.py --csv items.csv
|
||||
|
||||
CSV FORMAT
|
||||
----------
|
||||
name,qty,base_tier,base_item,ring_variant,amulet_variant,ethereal,pricing,price_amount,price_currency
|
||||
Ral Rune,9,,,,,,ask_for_offers,,
|
||||
Waterwalk,1,Normal,,,,,ask_for_offers,,
|
||||
Spirit,1,,No base,,,,ask_for_offers,,
|
||||
Insight,1,Elite,No base,,,,ask_for_offers,,
|
||||
Dwarf Star,1,,,Orange,,,ask_for_offers,,
|
||||
Tal Rasha's Adjudication,1,,,,Sun,,ask_for_offers,,
|
||||
Nagelring,3,,,,,,fixed,5,Pul Rune
|
||||
|
||||
Notes on columns:
|
||||
name Exact (or close) item name as it appears in Traderie's
|
||||
search. The script picks the first dropdown match, or an
|
||||
exact-text match if one exists among the results.
|
||||
qty Stack amount. Defaults to 1 if blank.
|
||||
base_tier Normal / Exceptional / Elite (only for items that ask).
|
||||
base_item For runewords: the weapon/armor base, or "No base".
|
||||
ring_variant Big Blue / Orange / Crown / Coral / Small Blue, etc.
|
||||
amulet_variant Whatever the dropdown offers (e.g. "Sun", "Dot", "Penta").
|
||||
ethereal "yes"/"true" to check the Ethereal box.
|
||||
pricing "ask_for_offers", "free", or "fixed".
|
||||
price_amount Only used when pricing == fixed.
|
||||
price_currency Only used when pricing == fixed (searched in the "Runes"
|
||||
or "Items You're Looking For" combobox depending on type).
|
||||
|
||||
ACCOUNT-WIDE SETTINGS
|
||||
----------------------
|
||||
Ladder, Platform, and Mode are the same for every listing on a given
|
||||
account/session, so they're passed once as CLI flags rather than per-row:
|
||||
|
||||
--ladder ladder|non_ladder (default: ladder)
|
||||
--platform pc|switch|playstation|xbox (default: pc)
|
||||
--mode softcore|hardcore (default: softcore)
|
||||
--game-version classic|lod|row (default: row = "reign of the warlock")
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import csv
|
||||
import json
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
from playwright.sync_api import sync_playwright, TimeoutError as PWTimeout
|
||||
|
||||
ADD_LISTING_URL = "https://traderie.com/diablo2resurrected/listings/create"
|
||||
AUTH_STATE_FILE = "auth_state.json"
|
||||
|
||||
GAME_VERSION_LABELS = {
|
||||
"classic": "classic (base game)",
|
||||
"lod": "lord of destruction",
|
||||
"row": "reign of the warlock",
|
||||
}
|
||||
PLATFORM_LABELS = {
|
||||
"pc": "PC",
|
||||
"switch": "switch",
|
||||
"playstation": "playstation",
|
||||
"xbox": "xbox",
|
||||
}
|
||||
MODE_LABELS = {
|
||||
"softcore": "softcore",
|
||||
"hardcore": "hardcore",
|
||||
}
|
||||
LADDER_LABELS = {
|
||||
"ladder": "Ladder",
|
||||
"non_ladder": "Non Ladder",
|
||||
}
|
||||
|
||||
|
||||
def log(msg):
|
||||
print(f"[traderie] {msg}", flush=True)
|
||||
|
||||
|
||||
def setup_login():
|
||||
"""Open a real browser, let the user log in via Discord, save the
|
||||
session so future headless runs are already authenticated."""
|
||||
with sync_playwright() as p:
|
||||
browser = p.chromium.launch(headless=False)
|
||||
context = browser.new_context()
|
||||
page = context.new_page()
|
||||
page.goto("https://traderie.com/diablo2resurrected")
|
||||
log("Log in through the browser window that just opened (Discord OAuth).")
|
||||
log("Once you're logged in and can see your avatar in the top-right, "
|
||||
"come back here and press Enter.")
|
||||
input()
|
||||
context.storage_state(path=AUTH_STATE_FILE)
|
||||
log(f"Saved session to {AUTH_STATE_FILE}. You can close the browser.")
|
||||
browser.close()
|
||||
|
||||
|
||||
def select_react_dropdown(page, label_text, option_text, exact=False, timeout=8000):
|
||||
"""Click a react-select style combobox identified by its adjacent
|
||||
<label>/heading text, then click the option whose text matches
|
||||
option_text (case-insensitive substring match unless exact=True)."""
|
||||
# Find the "Select..." control that sits right after the field's label.
|
||||
# Traderie doesn't use <label for=>, so we locate by nearby text.
|
||||
field = page.locator(
|
||||
f"xpath=//*[self::label or self::div or self::span][contains(normalize-space(.), '{label_text}')]"
|
||||
f"/following::div[contains(@class,'select') or .//input][1]"
|
||||
).first
|
||||
field.click(timeout=timeout)
|
||||
page.wait_for_timeout(200)
|
||||
|
||||
if exact:
|
||||
option = page.get_by_text(option_text, exact=True).last
|
||||
else:
|
||||
option = page.locator(f"text=/{option_text}/i").last
|
||||
option.click(timeout=timeout)
|
||||
page.wait_for_timeout(150)
|
||||
|
||||
|
||||
def click_option_in_open_dropdown(page, option_text, exact=False, timeout=8000):
|
||||
"""Assumes a dropdown is already open; clicks the matching option."""
|
||||
if exact:
|
||||
option = page.get_by_text(option_text, exact=True).last
|
||||
else:
|
||||
option = page.locator(f"text=/{option_text}/i").last
|
||||
option.click(timeout=timeout)
|
||||
page.wait_for_timeout(150)
|
||||
|
||||
|
||||
def set_amount(page, qty):
|
||||
amt = page.locator("text=Amount").locator(
|
||||
"xpath=following::input[1]"
|
||||
).first
|
||||
amt.click(click_count=3)
|
||||
amt.fill(str(qty))
|
||||
|
||||
|
||||
def search_and_select_item(page, item_name):
|
||||
"""Types into the 'Search Items...' combobox and clicks the best
|
||||
matching result."""
|
||||
search = page.get_by_placeholder("Search Items...").first
|
||||
search.click()
|
||||
search.fill(item_name)
|
||||
page.wait_for_timeout(1200)
|
||||
|
||||
# Prefer an exact (case-insensitive) text match among results; fall back
|
||||
# to the first result rendered.
|
||||
results = page.locator("div:has-text('" + item_name + "')").all()
|
||||
exact = page.get_by_text(item_name, exact=True)
|
||||
try:
|
||||
exact.first.click(timeout=1500)
|
||||
except PWTimeout:
|
||||
# fall back: click the first row-like result under the input
|
||||
page.locator("xpath=//input[@placeholder='Search Items...']"
|
||||
"/ancestor::div[1]/following::div[1]").first.click()
|
||||
page.wait_for_timeout(300)
|
||||
|
||||
|
||||
def try_set_dropdown_by_label(page, label, value, exact=False):
|
||||
"""Best-effort: find a 'Select...' box under a heading matching
|
||||
`label` and pick `value`. Silently no-ops if the field isn't present
|
||||
on this item's form (e.g. Base Tier on a plain rune)."""
|
||||
try:
|
||||
heading = page.locator(f"text=/^{label}$/i").first
|
||||
heading.wait_for(state="visible", timeout=1500)
|
||||
except PWTimeout:
|
||||
return False
|
||||
|
||||
box = heading.locator("xpath=following::div[contains(@class,'select')][1]").first
|
||||
box.click(timeout=3000)
|
||||
page.wait_for_timeout(200)
|
||||
click_option_in_open_dropdown(page, value, exact=exact)
|
||||
return True
|
||||
|
||||
|
||||
def fill_listing(page, row, ladder, platform, mode, game_version, dry_run=False):
|
||||
name = row["name"].strip()
|
||||
qty = row.get("qty", "").strip() or "1"
|
||||
base_tier = row.get("base_tier", "").strip()
|
||||
base_item = row.get("base_item", "").strip()
|
||||
ring_variant = row.get("ring_variant", "").strip()
|
||||
amulet_variant = row.get("amulet_variant", "").strip()
|
||||
ethereal = row.get("ethereal", "").strip().lower() in ("yes", "true", "1")
|
||||
pricing = (row.get("pricing", "").strip() or "ask_for_offers").lower()
|
||||
price_amount = row.get("price_amount", "").strip()
|
||||
price_currency = row.get("price_currency", "").strip()
|
||||
|
||||
log(f"--- Listing: {name} x{qty} ---")
|
||||
|
||||
if dry_run:
|
||||
log(f" (dry run) would select item '{name}', qty={qty}, "
|
||||
f"base_tier={base_tier or '-'}, base_item={base_item or '-'}, "
|
||||
f"ring_variant={ring_variant or '-'}, amulet_variant={amulet_variant or '-'}, "
|
||||
f"ethereal={ethereal}, pricing={pricing}, "
|
||||
f"price={price_amount or '-'} {price_currency or ''}")
|
||||
return
|
||||
|
||||
page.goto(ADD_LISTING_URL)
|
||||
page.wait_for_selector("text=Item you're trading", timeout=15000)
|
||||
|
||||
search_and_select_item(page, name)
|
||||
page.mouse.wheel(0, 400)
|
||||
page.wait_for_timeout(300)
|
||||
|
||||
set_amount(page, qty)
|
||||
|
||||
if ethereal:
|
||||
try:
|
||||
page.get_by_text("Ethereal", exact=True).first.click(timeout=1500)
|
||||
except PWTimeout:
|
||||
pass
|
||||
|
||||
# Account-wide fields
|
||||
try_set_dropdown_by_label(page, "Ladder", LADDER_LABELS[ladder], exact=True)
|
||||
try_set_dropdown_by_label(page, "Platform", PLATFORM_LABELS[platform], exact=True)
|
||||
try_set_dropdown_by_label(page, "Mode", MODE_LABELS[mode], exact=True)
|
||||
|
||||
# Item-specific fields (no-ops if not present on this item's form)
|
||||
if base_item:
|
||||
try_set_dropdown_by_label(page, "Base Item", base_item)
|
||||
if ring_variant:
|
||||
try_set_dropdown_by_label(page, "Ring Variant", ring_variant, exact=True)
|
||||
if amulet_variant:
|
||||
try_set_dropdown_by_label(page, "Amulet Variant", amulet_variant, exact=True)
|
||||
if base_tier:
|
||||
try_set_dropdown_by_label(page, "Base Tier", base_tier, exact=True)
|
||||
|
||||
try_set_dropdown_by_label(page, "Game version", GAME_VERSION_LABELS[game_version])
|
||||
|
||||
# Pricing
|
||||
page.mouse.wheel(0, 400)
|
||||
page.wait_for_timeout(300)
|
||||
if pricing == "ask_for_offers":
|
||||
page.get_by_text("Ask for Offers", exact=True).first.click()
|
||||
elif pricing == "free":
|
||||
page.get_by_text("Free", exact=True).first.click()
|
||||
elif pricing == "fixed":
|
||||
page.get_by_text("Accept Only Listing Price", exact=True).first.click()
|
||||
# Runes or general items box for the price currency/item
|
||||
price_box = page.get_by_placeholder("Search Runes...")
|
||||
try:
|
||||
price_box.click(timeout=1000)
|
||||
except PWTimeout:
|
||||
price_box = page.get_by_placeholder("Search Items...").last
|
||||
price_box.click()
|
||||
price_box.fill(price_currency)
|
||||
page.wait_for_timeout(1000)
|
||||
page.get_by_text(price_currency, exact=False).first.click()
|
||||
qty_box = page.locator(
|
||||
"xpath=//input[@placeholder='Search Runes...' or @placeholder='Search Items...']"
|
||||
"/following::input[1]"
|
||||
).last
|
||||
if price_amount:
|
||||
qty_box.click(click_count=3)
|
||||
qty_box.fill(price_amount)
|
||||
else:
|
||||
log(f" WARNING: unknown pricing mode '{pricing}', skipping pricing step")
|
||||
|
||||
page.mouse.wheel(0, 600)
|
||||
page.wait_for_timeout(400)
|
||||
|
||||
submit = page.get_by_role("button", name="Add Listing").first
|
||||
submit.click()
|
||||
|
||||
try:
|
||||
page.wait_for_selector("text=Your listing has been submitted", timeout=10000)
|
||||
log(" Submitted OK.")
|
||||
except PWTimeout:
|
||||
log(" WARNING: did not see the submission confirmation message. "
|
||||
"Check the page manually.")
|
||||
|
||||
# Reset for next item
|
||||
try:
|
||||
page.get_by_role("button", name="OK").first.click(timeout=2000)
|
||||
except PWTimeout:
|
||||
pass
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description=__doc__,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
ap.add_argument("--setup-login", action="store_true",
|
||||
help="Open a browser to log in and save the session.")
|
||||
ap.add_argument("--csv", type=str, help="Path to the items CSV.")
|
||||
ap.add_argument("--dry-run", action="store_true",
|
||||
help="Print what would happen without submitting anything.")
|
||||
ap.add_argument("--headless", action="store_true", default=False,
|
||||
help="Run browser headless (default: headed, so you can watch).")
|
||||
ap.add_argument("--ladder", choices=list(LADDER_LABELS), default="ladder")
|
||||
ap.add_argument("--platform", choices=list(PLATFORM_LABELS), default="pc")
|
||||
ap.add_argument("--mode", choices=list(MODE_LABELS), default="softcore")
|
||||
ap.add_argument("--game-version", choices=list(GAME_VERSION_LABELS), default="row")
|
||||
args = ap.parse_args()
|
||||
|
||||
if args.setup_login:
|
||||
setup_login()
|
||||
return
|
||||
|
||||
if not args.csv:
|
||||
ap.error("--csv is required unless using --setup-login")
|
||||
|
||||
rows = list(csv.DictReader(open(args.csv, newline="", encoding="utf-8")))
|
||||
if not rows:
|
||||
log("CSV has no rows, nothing to do.")
|
||||
return
|
||||
|
||||
if not args.dry_run and not Path(AUTH_STATE_FILE).exists():
|
||||
log(f"No {AUTH_STATE_FILE} found. Run with --setup-login first, "
|
||||
f"or use --dry-run to test without logging in.")
|
||||
sys.exit(1)
|
||||
|
||||
with sync_playwright() as p:
|
||||
browser = p.chromium.launch(headless=args.headless)
|
||||
if args.dry_run:
|
||||
context = browser.new_context()
|
||||
else:
|
||||
context = browser.new_context(storage_state=AUTH_STATE_FILE)
|
||||
page = context.new_page()
|
||||
|
||||
for i, row in enumerate(rows, 1):
|
||||
log(f"[{i}/{len(rows)}]")
|
||||
try:
|
||||
fill_listing(page, row, args.ladder, args.platform,
|
||||
args.mode, args.game_version, dry_run=args.dry_run)
|
||||
except Exception as e:
|
||||
log(f" ERROR on '{row.get('name')}': {e!r} -- continuing with next item")
|
||||
time.sleep(0.5)
|
||||
|
||||
browser.close()
|
||||
|
||||
log("Done.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,75 @@
|
||||
name,qty,base_tier,base_item,ring_variant,amulet_variant,ethereal,pricing,price_amount,price_currency
|
||||
Amn Rune,10,,,,,,ask_for_offers,,
|
||||
Cham Rune,1,,,,,,ask_for_offers,,
|
||||
Dol Rune,9,,,,,,ask_for_offers,,
|
||||
El Rune,2,,,,,,ask_for_offers,,
|
||||
Eld Rune,2,,,,,,ask_for_offers,,
|
||||
Eth Rune,9,,,,,,ask_for_offers,,
|
||||
Fal Rune,1,,,,,,ask_for_offers,,
|
||||
Hel Rune,2,,,,,,ask_for_offers,,
|
||||
Io Rune,2,,,,,,ask_for_offers,,
|
||||
Ith Rune,1,,,,,,ask_for_offers,,
|
||||
Ko Rune,1,,,,,,ask_for_offers,,
|
||||
Lem Rune,2,,,,,,ask_for_offers,,
|
||||
Lum Rune,1,,,,,,ask_for_offers,,
|
||||
Mal Rune,1,,,,,,ask_for_offers,,
|
||||
Nef Rune,4,,,,,,ask_for_offers,,
|
||||
Ohm Rune,1,,,,,,ask_for_offers,,
|
||||
Ort Rune,23,,,,,,ask_for_offers,,
|
||||
Ral Rune,9,,,,,,ask_for_offers,,
|
||||
Sol Rune,7,,,,,,ask_for_offers,,
|
||||
Tal Rune,17,,,,,,ask_for_offers,,
|
||||
Thul Rune,6,,,,,,ask_for_offers,,
|
||||
Tir Rune,10,,,,,,ask_for_offers,,
|
||||
Um Rune,4,,,,,,ask_for_offers,,
|
||||
Amber Grand Charm Of Sustenance,1,,,,,,ask_for_offers,,
|
||||
Bitter Brogues,1,,,,,,ask_for_offers,,
|
||||
Burning Grand Charm,1,,,,,,ask_for_offers,,
|
||||
Coral Small Charm Of Incineration,1,,,,,,ask_for_offers,,
|
||||
Death Hold,1,,,,,,ask_for_offers,,
|
||||
Death Mask,1,,,,,,ask_for_offers,,
|
||||
Deep Worldstone Shard,1,,,,,,ask_for_offers,,
|
||||
Dwarf Star,1,,,,,,ask_for_offers,,
|
||||
Eagle Torc,1,,,,,,ask_for_offers,,
|
||||
Eastern Worldstone Shard,1,,,,,,ask_for_offers,,
|
||||
Entrapping Grand Charm,1,,,,,,ask_for_offers,,
|
||||
Expert's Grand Charm,1,,,,,,ask_for_offers,,
|
||||
Fletcher's Grand Charm Of Inertia,1,,,,,,ask_for_offers,,
|
||||
Grim Coil,1,,,,,,ask_for_offers,,
|
||||
Havoc Touch,1,,,,,,ask_for_offers,,
|
||||
Insight,1,,No base,,,,ask_for_offers,,
|
||||
Lapis Small Charm,1,,,,,,ask_for_offers,,
|
||||
Large Charm Of Inertia,1,,,,,,ask_for_offers,,
|
||||
Large Charm Of Vita,2,,,,,,ask_for_offers,,
|
||||
Nagelring,2,,,,,,ask_for_offers,,
|
||||
Northern Worldstone Shard,1,,,,,,ask_for_offers,,
|
||||
Order Noose,1,,,,,,ask_for_offers,,
|
||||
Order Touch,1,,,,,,ask_for_offers,,
|
||||
Perfect Amethyst,1,,,,,,ask_for_offers,,
|
||||
Perfect Diamond,1,,,,,,ask_for_offers,,
|
||||
Perfect Emerald,1,,,,,,ask_for_offers,,
|
||||
Perfect Ruby,1,,,,,,ask_for_offers,,
|
||||
Perfect Sapphire,1,,,,,,ask_for_offers,,
|
||||
Perfect Skull,1,,,,,,ask_for_offers,,
|
||||
Perfect Topaz,1,,,,,,ask_for_offers,,
|
||||
Rainbow Facet,3,,,,,,ask_for_offers,,
|
||||
Rugged Small Charm,1,,,,,,ask_for_offers,,
|
||||
Seraph's Hymn,1,,,,,,ask_for_offers,,
|
||||
Shogukusha's Grand Charm Of Greed,1,,,,,,ask_for_offers,,
|
||||
Sigon's Guard,1,,,,,,ask_for_offers,,
|
||||
Small Charm Of Balance,1,,,,,,ask_for_offers,,
|
||||
Small Charm Of Inertia,4,,,,,,ask_for_offers,,
|
||||
Snake's Small Charm Of Inertia,1,,,,,,ask_for_offers,,
|
||||
Southern Worldstone Shard,1,,,,,,ask_for_offers,,
|
||||
Sparking Grand Charm,1,,,,,,ask_for_offers,,
|
||||
Sparking Grand Charm Of Vita,1,,,,,,ask_for_offers,,
|
||||
Spirit,2,,No base,,,,ask_for_offers,,
|
||||
Stealth,1,,No base,,,,ask_for_offers,,
|
||||
String Of Ears,1,,,,,,ask_for_offers,,
|
||||
Tal Rasha's Adjudication,1,,,,,,ask_for_offers,,
|
||||
Tal Rasha's Horadric Crest,2,,,,,,ask_for_offers,,
|
||||
Tarnhelm,2,,,,,,ask_for_offers,,
|
||||
The Reaper's Toll,1,,,,,,ask_for_offers,,
|
||||
Waterwalk,1,,,,,,ask_for_offers,,
|
||||
Western Worldstone Shard,1,,,,,,ask_for_offers,,
|
||||
Wraith Master,1,,,,,,ask_for_offers,,
|
||||
|
Reference in New Issue
Block a user