diff --git a/.gitignore b/.gitignore index 6268b42..d4f1348 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,6 @@ # Ignore the wiki dump data wiki-dump/ + +# NEU item cache used only by tools/build-craft-recipes.py +tools/.neu-cache/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ae1109..5d62766 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## 2026-09-02 + +- Added **craft prices to the Accessory Path**: every card now shows, next to its Auction-House / bazaar price, what the accessory costs to craft yourself and whether it can be crafted at all. Three chip states — `craft 194.47k` (green when crafting beats buying, with the saving as `−88%`), `craft ≥ 8.07M` (recipe exists but some material is unbuyable, listed on expand), and `no recipe`. Clicking a chip expands the full material list with per-material prices and their source (bazaar / AH / owned / crafted). +- Materials are priced from live markets, cheapest source first: an accessory you already own inside an upgrade chain is free (Wolf Talisman inside a Wolf Ring), then bazaar (insta-buy / buy-order, following the global toggle), then Auction-House lowest BIN, then the material's own recipe recursively. +- Added a **Craft prices** toggle (on by default, remembered per browser) and a **Craft cost (cheapest first)** sort option to the Accessory Path toolbar; the summary row gained a "craftable" counter and the page shows the total you'd save by crafting every step where it is cheaper. +- The cost-per-MP ratio on each path step now uses the cheaper of buy vs craft and is tagged `craft` when crafting wins. +- Added `craft-cost.js` (the recursive pricing engine) and `data/accessory-recipes.json` (~40 KB, recipes for all accessories plus their material closure). Hypixel's items endpoint has no recipe data, so the file is generated by the new `tools/build-craft-recipes.py` from the community NotEnoughUpdates repo — re-run it after major game updates. +- Fixed the Accessory Path sort comparators returning `NaN` (and thus an unstable order) when two unpriced accessories were compared. + ## 2026-06-29 - Added a **Calendar** page: a live SkyBlock clock (date, in-game time, and day-progress bar derived from the Year 1 epoch), an in-game-style month grid with prev/next navigation and a "today" highlight, and an Upcoming Events panel with real-time countdowns. Tracks the Dark Auction and Jacob's Farming Contest (every 3 SkyBlock days), the Traveling Zoo, Spooky Festival, Season of Jerry, New Year Celebration, and the mayor election / new-mayor dates. Inspired by inventivetalent's SkyBlock calendar. diff --git a/README.md b/README.md index 051560e..97f43b0 100644 --- a/README.md +++ b/README.md @@ -50,12 +50,16 @@ shard-market/ ├── nbt.js ← Minimal NBT parser (decodes the gzipped inventory blob) ├── prices.js ← Unified price resolver: bazaar + AH lowest-BIN scan (used by accessories, Sweep, P2W) ├── accessories.js ← Accessory catalog, upgrade families, Magical Power math +├── craft-cost.js ← Recursive crafting-cost engine (bazaar/AH materials → craft price) ├── attributes.js ← Attribute catalog + shards-to-max calculation ├── CHANGELOG.md ← Release notes and contributor credits +├── tools/ +│ └── build-craft-recipes.py ← One-off generator for data/accessory-recipes.json ├── data/ │ ├── fusion-properties.json ← Per-shard metadata (189 shards, from SkyShards) │ ├── fusion-data.json ← Full fusion recipe graph (~2 MB, from SkyShards) -│ └── attribute-desc.json ← Attribute id → rarity/title/effect (from SkyShards) +│ ├── attribute-desc.json ← Attribute id → rarity/title/effect (from SkyShards) +│ └── accessory-recipes.json ← Crafting recipes for accessories + their material closure └── README.md ``` @@ -92,6 +96,53 @@ Fish Bowls, Kuudra organs, Crux Chronomicon → Celestial Starstone, Helianthus and Shady Ring → Seal of the Family are treated as one upgrade path instead of separate missing accessories. +**Craft prices.** Next to every Auction-House / bazaar price the Accessory Path also shows +what the item costs to **craft yourself**, and whether that is possible at all. Flip the +**Craft prices** chip (on by default) and pick **Craft cost (cheapest first)** in the sort +menu to rank the whole path by crafting cost. Click any craft chip to expand the full +material list with per-material prices and where each one comes from. + +### How craft cost is computed + +Hypixel's `/v2/resources/skyblock/items` endpoint has no recipe data, so the recipes are +bundled once at development time in `data/accessory-recipes.json` (≈40 KB) by +`tools/build-craft-recipes.py`, which reads the community +[NotEnoughUpdates repo](https://github.com/NotEnoughUpdates/NotEnoughUpdates-REPO): + +```bash +python tools/build-craft-recipes.py # incremental — caches into tools/.neu-cache/ +python tools/build-craft-recipes.py --refresh # re-download the recipe files +``` + +It starts from every `ACCESSORY` item, then walks the ingredient closure: any material that +is neither on the bazaar nor already fetched is fetched too, so deep chains +(`Bat Talisman → Bat Ring → Bat Artifact`, `Enchanted X → Enchanted X Block`) can be priced. +Re-run it after big game updates. + +At runtime `craft-cost.js` walks that graph and prices every leaf material live: + +| Priority | Source | Notes | +| --- | --- | --- | +| 1 | **Already owned** | the base accessory in an upgrade chain (e.g. your Wolf Talisman inside a Wolf Ring) is free | +| 2 | **Bazaar** | insta-buy or buy-order, following the global bazaar-mode toggle | +| 3 | **Auction House** | lowest BIN, for materials that never list on the bazaar | +| 4 | **Craft it** | the material's own recipe, recursively | + +Each chip reports one of three states: + +- `craft 194.47k` — fully craftable; green when it beats the buy price, with the saving + shown as `−88%`. The tag reads **bazaar mats** when every material comes off the bazaar, + or **bz + AH mat** when at least one has to be bought on the Auction House. +- `craft ≥ 8.07M` — has a recipe but not fully craftable: that much of it is buyable, the + rest (quest drops, dungeon drops, soulbound gear) must be obtained another way, and the + expanded panel lists exactly what. +- `no recipe` — nothing to craft; it has to be bought, dropped or earned. + +What is **not** modelled: NPC shop prices, forge fuel/catalyst costs beyond the listed +inputs, collection and skill requirements (a recipe may exist while you still lack the +Heart of the Mountain tier), and craft-time opportunity cost. Forge recipes show their +duration (`1.5d in the forge`) so you can judge that yourself. + ### Attributes How many **Attribute Shards** you still need to take each attribute to level 10. The report includes the full 189-entry SkyShards catalog, marks attributes you have never diff --git a/craft-cost.js b/craft-cost.js new file mode 100644 index 0000000..5000bff --- /dev/null +++ b/craft-cost.js @@ -0,0 +1,256 @@ +/* ========================================================================= + * craft-cost.js + * + * "What would it cost to craft this myself?" for accessories. + * + * Hypixel's own items endpoint has no recipe data, so recipes are bundled + * statically in data/accessory-recipes.json (generated once from the + * NotEnoughUpdates community repo by tools/build-craft-recipes.py). + * + * The engine walks a recipe tree recursively and prices every leaf material + * from the live market: + * + * 1. material already owned by the player -> 0 (upgrade chains) + * 2. bazaar insta-buy / buy-order (per mode) -> bazaar price + * 3. auction house lowest BIN -> BIN + * 4. the material is itself craftable -> its own craft cost + * + * Every resolved material is the cheapest of the options above; materials that + * resolve to nothing (quest drops, dungeon drops, soulbound gear) are reported + * as `missing`, which is what makes an item "not craftable". + * + * Exposes on window: + * loadCraftRecipes() -> Promise<{recipes, names, loaded, error}> + * createCraftPricer(opts) -> { analyse(id), bestUnit(id), craftOf(id) } + * createCraftPricer().analyse(id) -> { + * hasRecipe, craftable, cost, partialCost, pureBazaar, + * type, duration, out, materials[], missing[], freeInputs[] + * } + * ======================================================================= */ + +"use strict"; + +const CRAFT_RECIPES_URL = "data/accessory-recipes.json?v=20260902-recipes"; + +/* Fallback display names when the Hypixel items API has no record for an + * ingredient id (NEU internal ids that Hypixel never exposes). */ +function prettyItemId(id) { + return String(id || "") + .toLowerCase() + .split("_") + .filter(Boolean) + .map((w) => w.charAt(0).toUpperCase() + w.slice(1)) + .join(" "); +} + +let craftRecipesPromise = null; + +/* Fetch the bundled recipe graph once per page load. Never throws — the site + * must keep working (without craft prices) if the file is missing. */ +function loadCraftRecipes() { + if (craftRecipesPromise) return craftRecipesPromise; + craftRecipesPromise = (async () => { + try { + const res = await fetch(CRAFT_RECIPES_URL); + if (!res.ok) throw new Error(`recipe data HTTP ${res.status}`); + const json = await res.json(); + return { + recipes: json.recipes || {}, + names: json.names || {}, + loaded: true, + error: null, + }; + } catch (e) { + console.warn("[Hypixie] craft recipe data unavailable:", e); + return { recipes: {}, names: {}, loaded: false, error: String(e?.message || e) }; + } + })(); + return craftRecipesPromise; +} + +/* Build a pricer bound to one snapshot of the market. + * + * bazaar — live bazaar products map (state.raw.products) + * bins — Map of AH lowest BINs + * bazaarMode — "instaBuy" | "buyOrder" + * owned — Set/Map of accessory ids the player already owns (cost 0) + * recipes — the bundled recipe graph + * names — NEU display names, used only as a fallback + * nameOf — optional (id) => string, normally the Hypixel items API name + */ +function createCraftPricer(opts = {}) { + const { + bazaar = null, + bins = null, + bazaarMode = "instaBuy", + owned = null, + recipes = {}, + names = {}, + nameOf = null, + maxDepth = 12, + } = opts; + + const ownedIds = + owned instanceof Map ? new Set(owned.keys()) : owned instanceof Set ? new Set(owned) : new Set(); + + const unitMemo = new Map(); + const craftMemo = new Map(); + const inflight = new Set(); + + const label = (id) => { + if (typeof nameOf === "function") { + const n = nameOf(id); + if (n) return n; + } + return names[id] || prettyItemId(id); + }; + + /* Cheapest market price for one unit of `id`, or null. */ + function marketUnit(id) { + const prod = bazaar?.[id]; + if (prod?.quick_status) { + const instaBuy = prod.quick_status.buyPrice || null; + const buyOrder = prod.quick_status.sellPrice || null; + const price = + bazaarMode === "buyOrder" ? (buyOrder ?? instaBuy) : (instaBuy ?? buyOrder); + if (price != null && price > 0) { + return { price, source: "bazaar", sourceLabel: bazaarMode === "buyOrder" ? "buy order" : "insta-buy" }; + } + } + const bin = bins?.get(id); + if (bin != null && bin > 0) return { price: bin, source: "ah", sourceLabel: "lowest BIN" }; + return null; + } + + /* Cheapest way to buy or craft 1 unit of `id`. Owned items are handled by + * the caller (craftOf) because only it knows how many are required. */ + function bestUnit(id, depth = 0) { + if (unitMemo.has(id)) return unitMemo.get(id); + if (inflight.has(id) || depth > maxDepth) return null; // cycle / runaway guard + inflight.add(id); + + let best = null; + try { + const mkt = marketUnit(id); + if (mkt && (!best || mkt.price < best.price)) best = mkt; + + const craft = craftOf(id, depth + 1); + if (craft && craft.craftable && Number.isFinite(craft.cost)) { + const cand = { + price: craft.cost, + source: craft.pureBazaar ? "craft" : "craft-ah", + sourceLabel: craft.pureBazaar ? "crafted" : "crafted (needs AH part)", + }; + if (!best || cand.price < best.price) best = cand; + } + } finally { + inflight.delete(id); + } + + unitMemo.set(id, best); + return best; + } + + /* Raw craft cost of `id` — the cheapest of its known recipes. */ + function craftOf(id, depth = 0) { + if (craftMemo.has(id)) return craftMemo.get(id); + const list = recipes[id]; + if (!list || !list.length || depth > maxDepth) { + const none = { hasRecipe: false, craftable: false, cost: null, partialCost: null, materials: [], missing: [], freeInputs: [] }; + craftMemo.set(id, none); + return none; + } + if (inflight.has(id)) return null; + inflight.add(id); + + let best = null; + try { + for (const r of list) { + const materials = []; + const missing = []; + const freeInputs = []; + let total = 0; + let ok = true; + let usedAh = false; + + for (const [ingId, amount] of Object.entries(r.in || {})) { + /* A player-owned accessory covers the recipe's first copy for free — + * that is the whole point of an upgrade chain (Wolf Talisman → Wolf + * Ring). Any further copies (Scarf's Thesis needs 4 Studies) still + * have to be sourced. */ + const ownedUnits = ownedIds.has(ingId) ? Math.min(amount, 1) : 0; + const toBuy = amount - ownedUnits; + + let subtotal = 0; + let source = ownedUnits ? "owned" : null; + let sourceLabel = ownedUnits ? "you own it" : null; + + if (toBuy > 0) { + const unit = bestUnit(ingId, depth + 1); + if (!unit) { + ok = false; + missing.push({ id: ingId, name: label(ingId), amount: toBuy }); + continue; + } + subtotal = unit.price * toBuy; + if (unit.source === "ah" || unit.source === "craft-ah") usedAh = true; + if (!source) { source = unit.source; sourceLabel = unit.sourceLabel; } + else if (unit.source !== "bazaar") { source = unit.source; sourceLabel = unit.sourceLabel; } + } + + total += subtotal; + if (ownedUnits) freeInputs.push({ id: ingId, name: label(ingId), amount: ownedUnits }); + materials.push({ + id: ingId, + name: label(ingId), + amount, + ownedUnits, + unit: toBuy ? subtotal / toBuy : 0, + subtotal, + source: source || "owned", + sourceLabel: sourceLabel || "you own it", + }); + } + + const out = r.out || 1; + const cost = ok ? total / out : null; + const cand = { + hasRecipe: true, + craftable: ok, + cost, + partialCost: ok ? cost : total / out, + pureBazaar: ok && !usedAh, + type: r.type || "crafting", + duration: r.duration ?? null, + out, + materials: materials.sort((a, b) => b.subtotal - a.subtotal), + missing, + freeInputs, + }; + + if (!best) best = cand; + else if (cand.craftable && (!best.craftable || cand.cost < best.cost)) best = cand; + else if (!cand.craftable && !best.craftable && (cand.missing.length < best.missing.length)) best = cand; + } + } finally { + inflight.delete(id); + } + + if (best) craftMemo.set(id, best); + return best; + } + + /* Display-ready analysis of one accessory. */ + function analyse(id) { + const c = craftOf(String(id || "").toUpperCase(), 0); + if (!c) return null; + if (!c.hasRecipe) return { hasRecipe: false, craftable: false, cost: null, materials: [], missing: [], freeInputs: [] }; + return c; + } + + return { analyse, bestUnit, craftOf, label }; +} + +window.loadCraftRecipes = loadCraftRecipes; +window.createCraftPricer = createCraftPricer; +window.prettyItemId = prettyItemId; diff --git a/data/accessory-recipes.json b/data/accessory-recipes.json new file mode 100644 index 0000000..d20633e --- /dev/null +++ b/data/accessory-recipes.json @@ -0,0 +1 @@ +{"generatedAt":"2026-09-02T10:03:53Z","source":"NotEnoughUpdates/NotEnoughUpdates-REPO (item recipes)","accessoryIds":["ABICASE","ACCRETION_ARTIFACT","ACCRETION_RING","ACCRETION_TALISMAN","AGARIMOO_ARTIFACT","AGARIMOO_RING","AGARIMOO_TALISMAN","ANGUISH_ARTIFACT","ANGUISH_RING","ANGUISH_TALISMAN","ANITA_ARTIFACT","ANITA_RING","ANITA_TALISMAN","APPLICANT_STATEMENT","ARCHAEOLOGIST_COMPASS","ARGOFAY_TRINKET","ARTIFACT_OF_COINS","ARTIFACT_OF_CONTROL","ARTIFACT_OF_SPACE","ARTIFACT_POTION_AFFINITY","ATMOSPHERIC_FILTER","AUTO_RECOMBOBULATOR","BAIT_RING","BAT_ARTIFACT","BAT_PERSON_ARTIFACT","BAT_PERSON_RING","BAT_PERSON_TALISMAN","BAT_RING","BAT_TALISMAN","BEASTMASTER_CREST_COMMON","BEASTMASTER_CREST_EPIC","BEASTMASTER_CREST_LEGENDARY","BEASTMASTER_CREST_RARE","BEASTMASTER_CREST_UNCOMMON","BIG_BRAIN_TALISMAN","BINGO_ARTIFACT","BINGO_HEIRLOOM","BINGO_RELIC","BINGO_RING","BINGO_TALISMAN","BIOANALYSIS_ARTIFACT","BIOANALYSIS_RING","BIOANALYSIS_TALISMAN","BITS_TALISMAN","BLAZE_TALISMAN","BLOOD_DONOR_ARTIFACT","BLOOD_DONOR_RING","BLOOD_DONOR_TALISMAN","BLOOD_GOD_CREST","BLOOD_GOD_SIGIL","BLUERTOOTH_RING","BLUETOOTH_RING","BLUE_GIFT_TALISMAN","BOOK_OF_PROGRESSION","BROKEN_PIGGY_BANK","BUCKET_OF_DYE","BURNING_KUUDRA_CORE","BURSTSTOPPER_ARTIFACT","BURSTSTOPPER_TALISMAN","CAMPFIRE_TALISMAN_1","CAMPFIRE_TALISMAN_10","CAMPFIRE_TALISMAN_11","CAMPFIRE_TALISMAN_12","CAMPFIRE_TALISMAN_13","CAMPFIRE_TALISMAN_14","CAMPFIRE_TALISMAN_15","CAMPFIRE_TALISMAN_16","CAMPFIRE_TALISMAN_17","CAMPFIRE_TALISMAN_18","CAMPFIRE_TALISMAN_19","CAMPFIRE_TALISMAN_2","CAMPFIRE_TALISMAN_20","CAMPFIRE_TALISMAN_21","CAMPFIRE_TALISMAN_22","CAMPFIRE_TALISMAN_23","CAMPFIRE_TALISMAN_24","CAMPFIRE_TALISMAN_25","CAMPFIRE_TALISMAN_26","CAMPFIRE_TALISMAN_27","CAMPFIRE_TALISMAN_28","CAMPFIRE_TALISMAN_29","CAMPFIRE_TALISMAN_3","CAMPFIRE_TALISMAN_4","CAMPFIRE_TALISMAN_5","CAMPFIRE_TALISMAN_6","CAMPFIRE_TALISMAN_7","CAMPFIRE_TALISMAN_8","CAMPFIRE_TALISMAN_9","CANDY_ARTIFACT","CANDY_RELIC","CANDY_RING","CANDY_TALISMAN","CARNIVAL_MASK_BAG","CATACOMBS_EXPERT_RING","CAT_TALISMAN","CENTURY_ARTIFACT","CENTURY_RING","CENTURY_TALISMAN","CHEETAH_TALISMAN","CHUMMING_TALISMAN","COIN_TALISMAN","COMBO_MANIA_TALISMAN","COMPASS_TALISMAN","COPPER_ARTIFACT","COPPER_RING","COPPER_TALISMAN","CRACKED_PIGGY_BANK","CROOKED_ARTIFACT","CROPIE_TALISMAN","CRUX_TALISMAN_1","CRUX_TALISMAN_2","CRUX_TALISMAN_3","CRUX_TALISMAN_4","CRUX_TALISMAN_5","CRUX_TALISMAN_6","CRUX_TALISMAN_7","DANTE_RING","DANTE_TALISMAN","DAY_CRYSTAL","DEFECTIVE_MONITOR","DEVOUR_RING","DRACONIC_ARTIFACT","DRACONIC_RING","DRACONIC_TALISMAN","DULL_SHARK_TOOTH_NECKLACE","DWARVEN_METAL","EMERALD_ARTIFACT","EMERALD_RING","EMPEROR_ARTIFACT","EMPEROR_RING","EMPEROR_TALISMAN","ENDER_ARTIFACT","ENDER_RELIC","ETERNAL_CRYSTAL","ETERNAL_HOOF","EXPERIENCE_ARTIFACT","FARMER_ORB","FARMING_TALISMAN","FEATHER_ARTIFACT","FEATHER_RING","FEATHER_TALISMAN","FERMENTO_ARTIFACT","FIERY_KUUDRA_CORE","FIRE_TALISMAN","FISH_AFFINITY_TALISMAN","FRESHLY_BAKED_ARTIFACT","FRESHLY_BAKED_HEIRLOOM","FRESHLY_BAKED_RELIC","FRESHLY_BAKED_RING","FRESHLY_BAKED_TALISMAN","FRIED_FROZEN_CHICKEN","FROZEN_CHICKEN","FUTURE_CALORIES","GANACHE_CHOCOLATE_SLAB","GARLIC_FLAVORED_GUMMY_BEAR","GENERAL_MEDALLION","GLACIAL_ARTIFACT","GLACIAL_RING","GLACIAL_TALISMAN","GLOSSY_MINERAL_TALISMAN","GOLD_GIFT_TALISMAN","GRATITUDE_ARTIFACT","GRATITUDE_RING","GRATITUDE_TALISMAN","GRAVITY_TALISMAN","GREAT_SPOOK_ARTIFACT","GREAT_SPOOK_RING","GREAT_SPOOK_TALISMAN","GREEN_GIFT_TALISMAN","GRIZZLY_PAW","HANDY_BLOOD_CHALICE","HARMONIOUS_SURGERY_TOOLKIT","HASTE_ARTIFACT","HASTE_RING","HEALING_RING","HEALING_TALISMAN","HEGEMONY_ARTIFACT","HELIANTHUS_RELIC","HOCUS_POCUS_CIPHER","HONED_SHARK_TOOTH_NECKLACE","HONEYCOMB_ARTIFACT","HONEYCOMB_RING","HONEYCOMB_TALISMAN","HUNTER_RING","HUNTER_TALISMAN","INFERNAL_KUUDRA_CORE","INTIMIDATION_ARTIFACT","INTIMIDATION_RELIC","INTIMIDATION_RING","INTIMIDATION_TALISMAN","IQ_POINT","JACOBUS_REGISTER","JAKE_PLUSHIE","JERRY_TALISMAN_BLUE","JERRY_TALISMAN_GOLDEN","JERRY_TALISMAN_GREEN","JERRY_TALISMAN_PURPLE","JUNGLE_AMULET","JUNK_ARTIFACT","JUNK_RING","JUNK_TALISMAN","KING_TALISMAN","KUUDRAS_HEART","KUUDRAS_KIDNEY","KUUDRAS_LUNG","KUUDRA_FOLLOWER_ARTIFACT","KUUDRA_FOLLOWER_RELIC","LARGE_FISH_BOWL","LAVA_TALISMAN","LUCKY_HOOF","LUCK_TALISMAN","LUMBERJACK_ARTIFACT","LUMBERJACK_RING","LUMBERJACK_TALISMAN","LUSH_ARTIFACT","LUSH_RING","LUSH_TALISMAN","LYNX_TALISMAN","MAGIC_8_BALL","MAGNETIC_TALISMAN","MASTER_SKULL_TIER_1","MASTER_SKULL_TIER_10","MASTER_SKULL_TIER_2","MASTER_SKULL_TIER_3","MASTER_SKULL_TIER_4","MASTER_SKULL_TIER_5","MASTER_SKULL_TIER_6","MASTER_SKULL_TIER_7","MASTER_SKULL_TIER_8","MASTER_SKULL_TIER_9","MASTER_THESIS","MEDIUM_FISH_BOWL","MELODY_HAIR","MINERAL_TALISMAN","MINE_TALISMAN","MINIATURIZED_TUBULATOR","MOONGLADE_ARTIFACT","MOONGLADE_RING","MOONGLADE_TALISMAN","MOONLIGHT_CRYSTAL","NETHERRACK_LOOKING_SUNSHADE","NETHER_ARTIFACT","NEW_YEAR_CAKE_BAG","NIBBLE_CHOCOLATE_STICK","NIGHT_CRYSTAL","NIGHT_VISION_CHARM","ODGERS_BRONZE_TOOTH","ODGERS_DIAMOND_TOOTH","ODGERS_GOLD_TOOTH","ODGERS_SILVER_TOOTH","OLD_BOOT","ORGAN_DONOR_ARTIFACT","ORGAN_DONOR_RING","ORGAN_DONOR_TALISMAN","PANDORAS_BOX","PARTY_HAT_CRAB","PARTY_HAT_CRAB_ANIMATED","PARTY_HAT_SLOTH","PERSONAL_COMPACTOR_4000","PERSONAL_COMPACTOR_5000","PERSONAL_COMPACTOR_6000","PERSONAL_COMPACTOR_7000","PERSONAL_DELETOR_4000","PERSONAL_DELETOR_5000","PERSONAL_DELETOR_6000","PERSONAL_DELETOR_7000","PESTHUNTER_ARTIFACT","PESTHUNTER_BADGE","PESTHUNTER_RELIC","PESTHUNTER_RING","PHD_GRIMOIRE","PIGGY_BANK","PIGS_FOOT","POCKET_ESPRESSO_MACHINE","POTATO_RING","POTATO_TALISMAN","POTION_AFFINITY_TALISMAN","POWER_ARTIFACT","POWER_RELIC","POWER_RING","POWER_TALISMAN","PRESSURE_ARTIFACT","PRESSURE_RING","PRESSURE_TALISMAN","PRESTIGE_CHOCOLATE_REALM","PULSE_RING","PUNCHCARD_ARTIFACT","PURPLE_GIFT_TALISMAN","RAGGEDY_SHARK_TOOTH_NECKLACE","RAZOR_SHARP_SHARK_TOOTH_NECKLACE","REAPER_ORB","RED_CLAW_ARTIFACT","RED_CLAW_RING","RED_CLAW_TALISMAN","RELIC_OF_COINS","RESPIRATION_ARTIFACT","RESPIRATION_RING","RESPIRATION_TALISMAN","RICH_CHOCOLATE_CHUNK","RIFT_PRISM","RING_OF_BROKEN_LOVE","RING_OF_COINS","RING_OF_ETERNAL_LOVE","RING_OF_SPACE","RING_POTION_AFFINITY","RUNEBLADE_ARTIFACT","RUNEBLADE_RING","RUNEBLADE_TALISMAN","RUNEBOOK","SAFETY_BADGE","SATELITE","SCARF_GRIMOIRE","SCARF_STUDIES","SCARF_THESIS","SCAVENGER_ARTIFACT","SCAVENGER_RING","SCAVENGER_TALISMAN","SEAL_OF_THE_FAMILY","SEAL_RING","SEAL_TALISMAN","SEA_CREATURE_ARTIFACT","SEA_CREATURE_RING","SEA_CREATURE_TALISMAN","SHADY_RING","SHARP_SHARK_TOOTH_NECKLACE","SHENS_REGALIA","SKELETON_TALISMAN","SMALL_FISH_BOWL","SMOOTH_CHOCOLATE_BAR","SOULFLOW_BATTERY","SOULFLOW_PILE","SOULFLOW_SUPERCELL","SOUL_CAMPFIRE_TALISMAN_1","SOUL_CAMPFIRE_TALISMAN_10","SOUL_CAMPFIRE_TALISMAN_11","SOUL_CAMPFIRE_TALISMAN_12","SOUL_CAMPFIRE_TALISMAN_13","SOUL_CAMPFIRE_TALISMAN_14","SOUL_CAMPFIRE_TALISMAN_15","SOUL_CAMPFIRE_TALISMAN_16","SOUL_CAMPFIRE_TALISMAN_17","SOUL_CAMPFIRE_TALISMAN_18","SOUL_CAMPFIRE_TALISMAN_19","SOUL_CAMPFIRE_TALISMAN_2","SOUL_CAMPFIRE_TALISMAN_20","SOUL_CAMPFIRE_TALISMAN_21","SOUL_CAMPFIRE_TALISMAN_22","SOUL_CAMPFIRE_TALISMAN_23","SOUL_CAMPFIRE_TALISMAN_24","SOUL_CAMPFIRE_TALISMAN_25","SOUL_CAMPFIRE_TALISMAN_26","SOUL_CAMPFIRE_TALISMAN_27","SOUL_CAMPFIRE_TALISMAN_28","SOUL_CAMPFIRE_TALISMAN_29","SOUL_CAMPFIRE_TALISMAN_3","SOUL_CAMPFIRE_TALISMAN_4","SOUL_CAMPFIRE_TALISMAN_5","SOUL_CAMPFIRE_TALISMAN_6","SOUL_CAMPFIRE_TALISMAN_7","SOUL_CAMPFIRE_TALISMAN_8","SOUL_CAMPFIRE_TALISMAN_9","SPEED_ARTIFACT","SPEED_RELIC","SPEED_RING","SPEED_TALISMAN","SPIDER_ARTIFACT","SPIDER_RING","SPIDER_TALISMAN","SPIKED_ATROCITY","SQUASH_RING","STUDENT_STUDIES","SUNSHINE_CRYSTAL","SURVIVOR_CUBE","TALISMAN_OF_SPACE","TARANTULA_RING","TARANTULA_TALISMAN","TEST_BUCKET_PLEASE_IGNORE","TINY_DANCER","TITANIUM_ARTIFACT","TITANIUM_RELIC","TITANIUM_RING","TITANIUM_TALISMAN","TORRHUS_ARTIFACT","TORRHUS_RING","TORRHUS_TALISMAN","TRAPPER_CREST","TREASURE_ARTIFACT","TREASURE_RING","TREASURE_TALISMAN","TWO_IQ_POINT","VACCINE_ARTIFACT","VACCINE_RING","VACCINE_TALISMAN","VAMPIRE_DENTIST_RELIC","VILLAGE_TALISMAN","VOTER_BADGE","VOTER_BADGE_ELITE","VOTER_BADGE_SUPREME","VOTER_BADGE_VIP","WARDING_TRINKET","WEDDING_RING_0","WEDDING_RING_1","WEDDING_RING_2","WEDDING_RING_3","WEDDING_RING_4","WEDDING_RING_5","WEDDING_RING_6","WEDDING_RING_7","WEDDING_RING_8","WEDDING_RING_9","WHITE_GIFT_TALISMAN","WITCH_ARTIFACT","WITCH_RING","WITCH_TALISMAN","WITHER_ARTIFACT","WITHER_RELIC","WOLF_PAW","WOLF_RING","WOLF_TALISMAN","WOOD_TALISMAN","ZOMBIE_ARTIFACT","ZOMBIE_RING","ZOMBIE_TALISMAN"],"names":{"ACCRETION_ARTIFACT":"Accretion Artifact","ACCRETION_TALISMAN":"Accretion Talisman","ANGUISH_ARTIFACT":"Anguish Artifact","ACCRETION_RING":"Accretion Ring","ANITA_TALISMAN":"Anita's Talisman","AGARIMOO_TALISMAN":"Agarimoo Talisman","AGARIMOO_ARTIFACT":"Agarimoo Artifact","ANGUISH_RING":"Anguish Ring","ANITA_ARTIFACT":"Anita's Artifact","ARCHAEOLOGIST_COMPASS":"Archaeologist's Compass","APPLICANT_STATEMENT":"Applicant's Statement","ANITA_RING":"Anita's Ring","ANGUISH_TALISMAN":"Anguish Talisman","AGARIMOO_RING":"Agarimoo Ring","ARTIFACT_OF_COINS":"Artifact of Coins","ARTIFACT_OF_CONTROL":"Artifact of Control","BAT_ARTIFACT":"Bat Artifact","ARTIFACT_POTION_AFFINITY":"Potion Affinity Artifact","BAIT_RING":"Bait Ring","AUTO_RECOMBOBULATOR":"Auto Recombobulator","BAT_PERSON_TALISMAN":"Bat Person Talisman","BAT_PERSON_ARTIFACT":"Bat Person Artifact","BAT_TALISMAN":"Bat Talisman","BEASTMASTER_CREST_EPIC":"Beastmaster Crest","ATMOSPHERIC_FILTER":"Atmospheric Filter","BEASTMASTER_CREST_COMMON":"Beastmaster Crest","BAT_PERSON_RING":"Bat Person Ring","BAT_RING":"Bat Ring","BEASTMASTER_CREST_RARE":"Beastmaster Crest","BEASTMASTER_CREST_UNCOMMON":"Beastmaster Crest","BEASTMASTER_CREST_LEGENDARY":"Beastmaster Crest","BIG_BRAIN_TALISMAN":"Big Brain Talisman","BINGO_ARTIFACT":"Bingo Artifact","BINGO_RELIC":"Bingo Relic","BINGO_RING":"Bingo Ring","BIOANALYSIS_ARTIFACT":"Bioanalysis Artifact","BINGO_TALISMAN":"Bingo Talisman","BIOANALYSIS_RING":"Bioanalysis Ring","BIOANALYSIS_TALISMAN":"Bioanalysis Talisman","BITS_TALISMAN":"Bits Talisman","BLOOD_DONOR_RING":"Blood Donor Ring","BLOOD_DONOR_ARTIFACT":"Blood Donor Artifact","BLAZE_TALISMAN":"Blaze Talisman","BLOOD_GOD_SIGIL":"Blood God Sigil","BLOOD_DONOR_TALISMAN":"Blood Donor Talisman","BLUERTOOTH_RING":"Bluertooth Ring","BLOOD_GOD_CREST":"Blood God Crest","BLUETOOTH_RING":"Bluetooth Ring","BOOK_OF_PROGRESSION":"Book of Progression","BLUE_GIFT_TALISMAN":"Blue Gift Talisman","BROKEN_PIGGY_BANK":"Broken Piggy Bank","BURNING_KUUDRA_CORE":"Burning Kuudra Core","BURSTSTOPPER_TALISMAN":"Burststopper Talisman","CAMPFIRE_TALISMAN_1":"Campfire Initiate Badge I","CAMPFIRE_TALISMAN_12":"Campfire Cultist Badge V","CAMPFIRE_TALISMAN_13":"Campfire Scion Badge I","CAMPFIRE_TALISMAN_15":"Campfire Scion Badge III","CAMPFIRE_TALISMAN_16":"Campfire Scion Badge IV","CAMPFIRE_TALISMAN_17":"Campfire Scion Badge V","CAMPFIRE_TALISMAN_14":"Campfire Scion Badge II","BURSTSTOPPER_ARTIFACT":"Burststopper Artifact","BUCKET_OF_DYE":"Bucket of Dye","CAMPFIRE_TALISMAN_11":"Campfire Cultist Badge IV","CAMPFIRE_TALISMAN_10":"Campfire Cultist Badge III","CAMPFIRE_TALISMAN_19":"Campfire Scion Badge VII","CAMPFIRE_TALISMAN_18":"Campfire Scion Badge VI","CAMPFIRE_TALISMAN_21":"Campfire God Badge I","CAMPFIRE_TALISMAN_2":"Campfire Initiate Badge II","CAMPFIRE_TALISMAN_20":"Campfire Scion Badge VIII","CAMPFIRE_TALISMAN_22":"Campfire God Badge II","CAMPFIRE_TALISMAN_23":"Campfire God Badge III","CAMPFIRE_TALISMAN_25":"Campfire God Badge V","CAMPFIRE_TALISMAN_24":"Campfire God Badge IV","CAMPFIRE_TALISMAN_27":"Campfire God Badge VII","CAMPFIRE_TALISMAN_29":"Campfire God Badge IX","CAMPFIRE_TALISMAN_28":"Campfire God Badge VIII","CAMPFIRE_TALISMAN_26":"Campfire God Badge VI","CAMPFIRE_TALISMAN_3":"Campfire Initiate Badge III","CAMPFIRE_TALISMAN_4":"Campfire Adept Badge I","CAMPFIRE_TALISMAN_6":"Campfire Adept Badge III","CAMPFIRE_TALISMAN_5":"Campfire Adept Badge II","CAMPFIRE_TALISMAN_7":"Campfire Adept Badge IV","CAMPFIRE_TALISMAN_9":"Campfire Cultist Badge II","CAMPFIRE_TALISMAN_8":"Campfire Cultist Badge I","CANDY_ARTIFACT":"Candy Artifact","CANDY_RING":"Candy Ring","CANDY_RELIC":"Candy Relic","CAT_TALISMAN":"Cat Talisman","CARNIVAL_MASK_BAG":"Carnival Mask Bag","CATACOMBS_EXPERT_RING":"Catacombs Expert Ring","CENTURY_RING":"Ring of the Century","CENTURY_TALISMAN":"Talisman of the Century","COIN_TALISMAN":"Talisman of Coins","CHEETAH_TALISMAN":"Cheetah Talisman","COMBO_MANIA_TALISMAN":"Combo Mania","CHUMMING_TALISMAN":"Chumming Talisman","COPPER_ARTIFACT":"Copper Artifact","COPPER_RING":"Copper Ring","CENTURY_ARTIFACT":"Artifact of the Century","COPPER_TALISMAN":"Copper Talisman","CROOKED_ARTIFACT":"Crooked Artifact","CRACKED_PIGGY_BANK":"Cracked Piggy Bank","CROPIE_TALISMAN":"Cropie Talisman","CRUX_TALISMAN_1":"Crux Talisman","CRUX_TALISMAN_2":"Crux Ring","CRUX_TALISMAN_3":"Crux Artifact","CRUX_TALISMAN_4":"Crux Relic","CRUX_TALISMAN_5":"Crux Heirloom","CRUX_TALISMAN_6":"Crux Chronomicon","DANTE_TALISMAN":"Dante's Talisman","CRUX_TALISMAN_7":"Celestial Starstone","DANTE_RING":"Dante's Ring","DAY_CRYSTAL":"Day Crystal","CANDY_TALISMAN":"Candy Talisman","DEVOUR_RING":"Devour Ring","DRACONIC_ARTIFACT":"Draconic Artifact","DEFECTIVE_MONITOR":"Defective Monitor","DRACONIC_TALISMAN":"Draconic Talisman","DRACONIC_RING":"Draconic Ring","DWARVEN_METAL":"Dwarven Metal Talisman","DULL_SHARK_TOOTH_NECKLACE":"Dull Shark Tooth Necklace","EMERALD_ARTIFACT":"Emerald Artifact","EMPEROR_ARTIFACT":"Emperor's Artifact","EMPEROR_RING":"Emperor's Ring","EMPEROR_TALISMAN":"Emperor's Talisman","ENDER_ARTIFACT":"Ender Artifact","ENDER_RELIC":"Ender Relic","ETERNAL_HOOF":"Eternal Hoof","EXPERIENCE_ARTIFACT":"Experience Artifact","FEATHER_ARTIFACT":"Feather Artifact","FEATHER_TALISMAN":"Feather Talisman","FIERY_KUUDRA_CORE":"Fiery Kuudra Core","FIRE_TALISMAN":"Fire Talisman","EMERALD_RING":"Emerald Ring","FERMENTO_ARTIFACT":"Fermento Artifact","FRESHLY_BAKED_ARTIFACT":"Freshly Baked Artifact","FISH_AFFINITY_TALISMAN":"Fish Affinity Talisman","FRESHLY_BAKED_HEIRLOOM":"Freshly Baked Heirloom","FRESHLY_BAKED_RELIC":"Freshly Baked Relic","FRESHLY_BAKED_RING":"Freshly Baked Ring","FRESHLY_BAKED_TALISMAN":"Freshly Baked Talisman","FRIED_FROZEN_CHICKEN":"Fried Frozen Chicken","FROZEN_CHICKEN":"Frozen Chicken","FUTURE_CALORIES":"Future Calories Talisman","GANACHE_CHOCOLATE_SLAB":"Ganache Chocolate Slab","GARLIC_FLAVORED_GUMMY_BEAR":"Perma-Jelled Garlic-Flavored Re-Heated Gummy Polar Bear","GENERAL_MEDALLION":"General's Medallion","GLACIAL_ARTIFACT":"Glacial Artifact","GLACIAL_RING":"Glacial Ring","GLACIAL_TALISMAN":"Glacial Talisman","GLOSSY_MINERAL_TALISMAN":"Glossy Mineral Talisman","GOLD_GIFT_TALISMAN":"Gold Gift Talisman","GRATITUDE_RING":"Gratitude Ring","GRATITUDE_ARTIFACT":"Gratitude Artifact","GRATITUDE_TALISMAN":"Gratitude Talisman","GRAVITY_TALISMAN":"Gravity Talisman","FARMING_TALISMAN":"Farming Talisman","GREAT_SPOOK_ARTIFACT":"Great Spook Artifact","GREAT_SPOOK_RING":"Great Spook Ring","GREAT_SPOOK_TALISMAN":"Great Spook Talisman","FARMER_ORB":"Farmer Orb","GREEN_GIFT_TALISMAN":"Green Gift Talisman","FEATHER_RING":"Feather Ring","HARMONIOUS_SURGERY_TOOLKIT":"Harmonious Surgery Toolkit","HASTE_RING":"Haste Ring","HEALING_TALISMAN":"Healing Talisman","HEGEMONY_ARTIFACT":"Hegemony Artifact","HEALING_RING":"Healing Ring","HELIANTHUS_RELIC":"Helianthus Relic","HOCUS_POCUS_CIPHER":"Hocus-Pocus Cipher","HONED_SHARK_TOOTH_NECKLACE":"Honed Shark Tooth Necklace","HONEYCOMB_ARTIFACT":"Honeycomb Artifact","HONEYCOMB_TALISMAN":"Honeycomb Talisman","HONEYCOMB_RING":"Honeycomb Ring","HUNTER_RING":"Hunter Ring","INFERNAL_KUUDRA_CORE":"Infernal Kuudra Core","HUNTER_TALISMAN":"Hunter Talisman","INTIMIDATION_ARTIFACT":"Intimidation Artifact","INTIMIDATION_RELIC":"Intimidation Relic","INTIMIDATION_TALISMAN":"Intimidation Talisman","JACOBUS_REGISTER":"Jacobus Register","JAKE_PLUSHIE":"Jake's Plushie","IQ_POINT":"IQ Point","JERRY_TALISMAN_GOLDEN":"Golden Jerry Artifact","JUNGLE_AMULET":"Jungle Amulet","JUNK_ARTIFACT":"Junk Artifact","JERRY_TALISMAN_PURPLE":"Purple Jerry Talisman","JUNK_RING":"Junk Ring","JUNK_TALISMAN":"Junk Talisman","KING_TALISMAN":"King Talisman","KUUDRAS_HEART":"Kuudra's Heart","JERRY_TALISMAN_BLUE":"Blue Jerry Talisman","HASTE_ARTIFACT":"Haste Artifact","HANDY_BLOOD_CHALICE":"Handy Blood Chalice","KUUDRAS_LUNG":"Kuudra's Lung","KUUDRAS_KIDNEY":"Kuudra's Kidney","KUUDRA_FOLLOWER_ARTIFACT":"Kuudra Follower Artifact","LARGE_FISH_BOWL":"Large Fish Bowl","LAVA_TALISMAN":"Lava Talisman","LUCKY_HOOF":"Lucky Hoof","LUMBERJACK_ARTIFACT":"Lumberjack Artifact","LUMBERJACK_RING":"Lumberjack Ring","LUSH_ARTIFACT":"Lush Artifact","LUMBERJACK_TALISMAN":"Lumberjack Talisman","LUSH_TALISMAN":"Lush Talisman","LUSH_RING":"Lush Ring","MAGIC_8_BALL":"Magic 8 Ball","MAGNETIC_TALISMAN":"Magnetic Talisman","LYNX_TALISMAN":"Lynx Talisman","INTIMIDATION_RING":"Intimidation Ring","MASTER_SKULL_TIER_1":"Master Skull - Tier 1","MASTER_SKULL_TIER_3":"Master Skull - Tier 3","MASTER_SKULL_TIER_2":"Master Skull - Tier 2","MASTER_SKULL_TIER_5":"Master Skull - Tier 5","MASTER_SKULL_TIER_4":"Master Skull - Tier 4","MASTER_SKULL_TIER_7":"Master Skull - Tier 7","MASTER_SKULL_TIER_6":"Master Skull - Tier 6","JERRY_TALISMAN_GREEN":"Green Jerry Talisman","MASTER_THESIS":"Master's Thesis","KUUDRA_FOLLOWER_RELIC":"Kuudra Follower Relic","MINERAL_TALISMAN":"Mineral Talisman","MEDIUM_FISH_BOWL":"Medium Fish Bowl","MELODY_HAIR":"Melody's Hair","MINE_TALISMAN":"Mine Affinity Talisman","MINIATURIZED_TUBULATOR":"Miniaturized Tubulator","MOONGLADE_ARTIFACT":"Moonglade Artifact","MOONGLADE_RING":"Moonglade Ring","MOONGLADE_TALISMAN":"Moonglade Talisman","NETHERRACK_LOOKING_SUNSHADE":"Netherrack-Looking Sunshade","MOONLIGHT_CRYSTAL":"Moonlight Crystal","NEW_YEAR_CAKE_BAG":"New Year Cake Bag","NIBBLE_CHOCOLATE_STICK":"Nibble Chocolate Stick","NIGHT_CRYSTAL":"Night Crystal","NIGHT_VISION_CHARM":"Night Vision Charm","ODGERS_BRONZE_TOOTH":"Odger's Bronze Tooth","ODGERS_DIAMOND_TOOTH":"Odger's Diamond Tooth","ODGERS_SILVER_TOOTH":"Odger's Silver Tooth","ORGAN_DONOR_ARTIFACT":"Organ Donor Artifact","ORGAN_DONOR_RING":"Organ Donor Ring","ODGERS_GOLD_TOOTH":"Odger's Gold Tooth","ORGAN_DONOR_TALISMAN":"Organ Donor Talisman","PANDORAS_BOX":"Pandora's Box","PERSONAL_COMPACTOR_4000":"Personal Compactor 4000","PERSONAL_COMPACTOR_5000":"Personal Compactor 5000","PERSONAL_COMPACTOR_7000":"Personal Compactor 7000","PERSONAL_DELETOR_4000":"Personal Deletor 4000","PERSONAL_DELETOR_5000":"Personal Deletor 5000","PERSONAL_DELETOR_6000":"Personal Deletor 6000","PESTHUNTER_BADGE":"Pesthunter Badge","PESTHUNTER_ARTIFACT":"Pesthunter Artifact","PESTHUNTER_RELIC":"Pesthunter Relic","PESTHUNTER_RING":"Pesthunter Ring","PHD_GRIMOIRE":"PhD's Grimoire","PERSONAL_DELETOR_7000":"Personal Deletor 7000","PIGGY_BANK":"Piggy Bank","PERSONAL_COMPACTOR_6000":"Personal Compactor 6000","PIGS_FOOT":"Pig's Foot","POCKET_ESPRESSO_MACHINE":"Pocket Espresso Machine","POTATO_RING":"Potato Ring","POTATO_TALISMAN":"Potato Talisman","POTION_AFFINITY_TALISMAN":"Potion Affinity Talisman","POWER_ARTIFACT":"Artifact of Power","POWER_RELIC":"Relic of Power","POWER_RING":"Ring of Power","NETHER_ARTIFACT":"Nether Artifact","POWER_TALISMAN":"Talisman of Power","PRESSURE_RING":"Pressure Ring","PRESSURE_ARTIFACT":"Pressure Artifact","PRESTIGE_CHOCOLATE_REALM":"Prestige Chocolate Realm","PUNCHCARD_ARTIFACT":"Punchcard Artifact","PULSE_RING":"Pulse Ring","PRESSURE_TALISMAN":"Pressure Talisman","PURPLE_GIFT_TALISMAN":"Purple Gift Talisman","RAZOR_SHARP_SHARK_TOOTH_NECKLACE":"Razor-sharp Shark Tooth Necklace","RAGGEDY_SHARK_TOOTH_NECKLACE":"Raggedy Shark Tooth Necklace","RED_CLAW_ARTIFACT":"Red Claw Artifact","REAPER_ORB":"Reaper Orb","RED_CLAW_RING":"Red Claw Ring","RED_CLAW_TALISMAN":"Red Claw Talisman","RESPIRATION_ARTIFACT":"Respiration Artifact","RESPIRATION_RING":"Respiration Ring","RELIC_OF_COINS":"Relic of Coins","RIFT_PRISM":"Rift Prism","RICH_CHOCOLATE_CHUNK":"Rich Chocolate Chunk","RESPIRATION_TALISMAN":"Respiration Talisman","RING_OF_BROKEN_LOVE":"Ring of Broken Love","RING_OF_COINS":"Ring of Coins","RING_OF_ETERNAL_LOVE":"Ring of Eternal Love","RING_POTION_AFFINITY":"Potion Affinity Ring","RUNEBLADE_ARTIFACT":"Runeblade Artifact","RUNEBLADE_TALISMAN":"Runeblade Talisman","SCARF_THESIS":"Scarf's Thesis","SATELITE":"Satelite","SAFETY_BADGE":"Safety Badge","SCARF_STUDIES":"Scarf's Studies","SCARF_GRIMOIRE":"Scarf's Grimoire","SCAVENGER_ARTIFACT":"Scavenger Artifact","SCAVENGER_RING":"Scavenger Ring","SEAL_OF_THE_FAMILY":"Seal of the Family","SEAL_RING":"Seal Ring","SEA_CREATURE_ARTIFACT":"Sea Creature Artifact","SEA_CREATURE_TALISMAN":"Sea Creature Talisman","SEA_CREATURE_RING":"Sea Creature Ring","SEAL_TALISMAN":"Seal Talisman","SHADY_RING":"Shady Ring","SHARP_SHARK_TOOTH_NECKLACE":"Sharp Shark Tooth Necklace","SHENS_REGALIA":"Shen's Regalia","SMOOTH_CHOCOLATE_BAR":"Smooth Chocolate Bar","SOULFLOW_BATTERY":"Soulflow Battery","SOULFLOW_PILE":"Soulflow Pile","SOULFLOW_SUPERCELL":"Soulflow Supercell","SCAVENGER_TALISMAN":"Scavenger Talisman","SOUL_CAMPFIRE_TALISMAN_11":"Soul Campfire Cultist Badge IV","SOUL_CAMPFIRE_TALISMAN_13":"Soul Campfire Scion Badge I","SOUL_CAMPFIRE_TALISMAN_12":"Soul Campfire Cultist Badge V","RUNEBLADE_RING":"Runeblade Ring","SOUL_CAMPFIRE_TALISMAN_15":"Soul Campfire Scion Badge III","RUNEBOOK":"Runebook","SOUL_CAMPFIRE_TALISMAN_10":"Soul Campfire Cultist Badge III","SOUL_CAMPFIRE_TALISMAN_1":"Soul Campfire Initiate Badge I","SMALL_FISH_BOWL":"Small Fish Bowl","SOUL_CAMPFIRE_TALISMAN_18":"Soul Campfire Scion Badge VI","SOUL_CAMPFIRE_TALISMAN_14":"Soul Campfire Scion Badge II","SOUL_CAMPFIRE_TALISMAN_17":"Soul Campfire Scion Badge V","SOUL_CAMPFIRE_TALISMAN_16":"Soul Campfire Scion Badge IV","SOUL_CAMPFIRE_TALISMAN_19":"Soul Campfire Scion Badge VII","SOUL_CAMPFIRE_TALISMAN_20":"Soul Campfire Scion Badge VIII","SOUL_CAMPFIRE_TALISMAN_2":"Soul Campfire Initiate Badge II","SOUL_CAMPFIRE_TALISMAN_22":"Soul Campfire God Badge II","SOUL_CAMPFIRE_TALISMAN_21":"Soul Campfire God Badge I","SOUL_CAMPFIRE_TALISMAN_23":"Soul Campfire God Badge III","SOUL_CAMPFIRE_TALISMAN_24":"Soul Campfire God Badge IV","SOUL_CAMPFIRE_TALISMAN_25":"Soul Campfire God Badge V","SOUL_CAMPFIRE_TALISMAN_26":"Soul Campfire God Badge VI","SOUL_CAMPFIRE_TALISMAN_27":"Soul Campfire God Badge VII","SKELETON_TALISMAN":"Skeleton Talisman","SOUL_CAMPFIRE_TALISMAN_6":"Soul Campfire Adept Badge III","SOUL_CAMPFIRE_TALISMAN_3":"Soul Campfire Initiate Badge III","SOUL_CAMPFIRE_TALISMAN_7":"Soul Campfire Adept Badge IV","SPEED_ARTIFACT":"Speed Artifact","SOUL_CAMPFIRE_TALISMAN_9":"Soul Campfire Cultist Badge II","SOUL_CAMPFIRE_TALISMAN_8":"Soul Campfire Cultist Badge I","SPEED_RING":"Speed Ring","SPEED_RELIC":"Speed Relic","SPEED_TALISMAN":"Speed Talisman","SOUL_CAMPFIRE_TALISMAN_28":"Soul Campfire God Badge VIII","SPIDER_ARTIFACT":"Spider Artifact","SPIDER_RING":"Spider Ring","SPIDER_TALISMAN":"Spider Talisman","SPIKED_ATROCITY":"Spiked Atrocity","STUDENT_STUDIES":"Student's Studies","TITANIUM_ARTIFACT":"Titanium Artifact","SUNSHINE_CRYSTAL":"Sunshine Crystal","SURVIVOR_CUBE":"Survivor Cube","SQUASH_RING":"Squash Ring","TARANTULA_TALISMAN":"Tarantula Talisman","SOUL_CAMPFIRE_TALISMAN_29":"Soul Campfire God Badge IX","TARANTULA_RING":"Tarantula Ring","TEST_BUCKET_PLEASE_IGNORE":"Test Bucket Please Ignore","TINY_DANCER":"Tiny Dancer","TITANIUM_RELIC":"Titanium Relic","TITANIUM_RING":"Titanium Ring","TITANIUM_TALISMAN":"Titanium Talisman","TORRHUS_ARTIFACT":"Torrhus Artifact","TORRHUS_RING":"Torrhus Ring","SOUL_CAMPFIRE_TALISMAN_4":"Soul Campfire Adept Badge I","TORRHUS_TALISMAN":"Torrhus Talisman","TRAPPER_CREST":"Trapper Crest","TREASURE_TALISMAN":"Treasure Talisman","SOUL_CAMPFIRE_TALISMAN_5":"Soul Campfire Adept Badge II","TREASURE_ARTIFACT":"Treasure Artifact","VACCINE_RING":"Vaccine Ring","TWO_IQ_POINT":"2 IQ Points","VAMPIRE_DENTIST_RELIC":"Vampire Dentist Relic","VACCINE_TALISMAN":"Vaccine Talisman","VACCINE_ARTIFACT":"Vaccine Artifact","VILLAGE_TALISMAN":"Village Affinity Talisman","VOTER_BADGE":"Voter's Badge","VOTER_BADGE_ELITE":"Elite Voter's Badge","VOTER_BADGE_SUPREME":"Supreme Voter's Badge","VOTER_BADGE_VIP":"VIP Voter's Badge","WARDING_TRINKET":"Warding Trinket","WEDDING_RING_0":"Shiny Yellow Rock","WEDDING_RING_1":"Yellow Rock of Love","WEDDING_RING_3":"Rubbish Ring of Love","WEDDING_RING_2":"Mediocre Ring of Love","WEDDING_RING_4":"Modest Ring of Love","WEDDING_RING_5":"Refined Ring of Love","WEDDING_RING_6":"Classy Ring of Love","WEDDING_RING_7":"Exquisite Ring of Love","TREASURE_RING":"Treasure Ring","WEDDING_RING_8":"Invaluable Ring of Love","WEDDING_RING_9":"Legendary Ring of Love","WOLF_RING":"Wolf Ring","WHITE_GIFT_TALISMAN":"White Gift Talisman","WITCH_ARTIFACT":"Witch's Artifact","WITCH_RING":"Witch's Ring","WITCH_TALISMAN":"Witch's Talisman","WITHER_ARTIFACT":"Wither Artifact","WITHER_RELIC":"Wither Relic","WOLF_TALISMAN":"Wolf Talisman","ZOMBIE_ARTIFACT":"Zombie Artifact","ZOMBIE_RING":"Zombie Ring","WOOD_TALISMAN":"Wood Affinity Talisman","ZOMBIE_TALISMAN":"Zombie Talisman","WOLF_PAW":"Wolf Paw","SHADOW_CRUX":"Shadow Crux","MILK_BUCKET":"Milk Bucket","FROSTY_CRUX":"Frosty Crux","SHAME_CRUX":"Shy Crux","METAL_HEART":"Living Metal Heart","VOLT_CRUX":"Volt Crux","SPLATTER_CRUX":"Splatter Crux","NEARLY_WHOLE_CARROT":"Nearly-Whole Carrot","TOKEN_OF_THE_CENTURY":"Token of the Century","PRE_DIGESTION_FISH":"Pre-Digestion Fish","EXCEEDINGLY_RARE_ENDER_ARTIFACT_UPGRADER":"Exceedingly Rare Ender Artifact Upgrade","SCRIBE_CRUX":"Scribe Crux","PUFF_CRUX":"Puff Crux","BACTE_FRAGMENT":"Bacte Fragment","SCARE_FRAGMENT":"Scare Fragment","GRIZZLY_BAIT":"Grizzly Salmon","BLOODBADGE":"Bloodbadge","YOUNGITE":"Youngite","TIMITE":"Timite","UNFANGED_VAMPIRE_PART":"Unfanged Vampire Part","OBSOLITE":"Obsolite","LEAVES":"Oak Leaves","SCYTHE_BLADE":"Scythe Blade","HALF_EATEN_CARROT":"Half-Eaten Carrot","CRUXMOTION":"Cruxmotion","COVEN_SEAL":"Coven Seal","LEECH_SUPREME_FRAGMENT":"Leech Supreme Fragment"},"recipes":{"AGARIMOO_TALISMAN":[{"type":"crafting","in":{"AGARIMOO_TONGUE":9},"out":1,"duration":null}],"AGARIMOO_ARTIFACT":[{"type":"crafting","in":{"AGARIMOO_TONGUE":512,"AGARIMOO_RING":1},"out":1,"duration":null}],"AGARIMOO_RING":[{"type":"crafting","in":{"AGARIMOO_TONGUE":96,"AGARIMOO_TALISMAN":1},"out":1,"duration":null}],"ARTIFACT_OF_COINS":[{"type":"crafting","in":{"FRESHLY_MINTED_COINS":16,"RING_OF_COINS":1},"out":1,"duration":null}],"ARTIFACT_POTION_AFFINITY":[{"type":"crafting","in":{"ENCHANTED_NETHER_STALK":256,"RING_POTION_AFFINITY":1},"out":1,"duration":null}],"BAIT_RING":[{"type":"crafting","in":{"ENCHANTED_INK_SACK":288},"out":1,"duration":null}],"BAT_PERSON_ARTIFACT":[{"type":"crafting","in":{"SPOOKY_SHARD":16,"ECTOPLASM":128,"BAT_PERSON_RING":1},"out":1,"duration":null}],"BEASTMASTER_CREST_EPIC":[{"type":"crafting","in":{"GRIFFIN_FEATHER":128,"ENCHANTED_ANCIENT_CLAW":4,"BEASTMASTER_CREST_RARE":1},"out":1,"duration":null}],"BEASTMASTER_CREST_COMMON":[{"type":"crafting","in":{"GRIFFIN_FEATHER":4,"ANCIENT_CLAW":40,"MILK_BUCKET":1},"out":1,"duration":null}],"BAT_PERSON_RING":[{"type":"crafting","in":{"SPOOKY_SHARD":4,"ECTOPLASM":32,"BAT_PERSON_TALISMAN":1},"out":1,"duration":null}],"BEASTMASTER_CREST_RARE":[{"type":"crafting","in":{"GRIFFIN_FEATHER":32,"ANCIENT_CLAW":256,"BEASTMASTER_CREST_UNCOMMON":1},"out":1,"duration":null}],"BEASTMASTER_CREST_UNCOMMON":[{"type":"crafting","in":{"GRIFFIN_FEATHER":8,"ANCIENT_CLAW":80,"BEASTMASTER_CREST_COMMON":1},"out":1,"duration":null}],"BEASTMASTER_CREST_LEGENDARY":[{"type":"crafting","in":{"GRIFFIN_FEATHER":256,"ENCHANTED_ANCIENT_CLAW":32,"BEASTMASTER_CREST_EPIC":1},"out":1,"duration":null}],"BLAZE_TALISMAN":[{"type":"crafting","in":{"MILLENIA_OLD_BLAZE_ASHES":8,"ENCHANTED_BLAZE_ROD":8},"out":1,"duration":null}],"BLUERTOOTH_RING":[{"type":"crafting","in":{"BLUE_RING":128,"GOBLIN_EGG_BLUE":2,"BLUE_SHARK_TOOTH":16,"BLUETOOTH_RING":1},"out":1,"duration":null}],"BLUETOOTH_RING":[{"type":"crafting","in":{"METAL_HEART":32},"out":1,"duration":null}],"BURSTSTOPPER_TALISMAN":[{"type":"crafting","in":{"WHIPPED_MAGMA_CREAM":4,"MOLTEN_POWDER":128,"REFINED_TITANIUM":6},"out":1,"duration":null}],"BURSTSTOPPER_ARTIFACT":[{"type":"crafting","in":{"MOLTEN_POWDER":128,"WHIPPED_MAGMA_CREAM":80,"BURSTSTOPPER_TALISMAN":1},"out":1,"duration":null}],"CATACOMBS_EXPERT_RING":[{"type":"crafting","in":{"WITHER_CATALYST":24},"out":1,"duration":null}],"CENTURY_RING":[{"type":"crafting","in":{"TOKEN_OF_THE_CENTURY":8,"CENTURY_TALISMAN":1},"out":1,"duration":null}],"COIN_TALISMAN":[{"type":"crafting","in":{"EMERALD":20,"GOLD_INGOT":5},"out":1,"duration":null}],"CROPIE_TALISMAN":[{"type":"crafting","in":{"CROPIE":256},"out":1,"duration":null}],"CRUX_TALISMAN_1":[{"type":"crafting","in":{"SHAME_CRUX":8},"out":1,"duration":null}],"CRUX_TALISMAN_2":[{"type":"crafting","in":{"SHADOW_CRUX":16,"CRUX_TALISMAN_1":1},"out":1,"duration":null}],"CRUX_TALISMAN_3":[{"type":"crafting","in":{"VOLT_CRUX":16,"CRUX_TALISMAN_2":1},"out":1,"duration":null}],"CRUX_TALISMAN_4":[{"type":"crafting","in":{"SCRIBE_CRUX":16,"CRUX_TALISMAN_3":1},"out":1,"duration":null}],"CRUX_TALISMAN_5":[{"type":"crafting","in":{"FROSTY_CRUX":24,"CRUX_TALISMAN_4":1},"out":1,"duration":null}],"CRUX_TALISMAN_6":[{"type":"crafting","in":{"SPLATTER_CRUX":32,"CRUX_TALISMAN_5":1},"out":1,"duration":null}],"CRUX_TALISMAN_7":[{"type":"crafting","in":{"PUFF_CRUX":128,"CRUX_TALISMAN_6":1},"out":1,"duration":null}],"DAY_CRYSTAL":[{"type":"crafting","in":{"ENCHANTED_QUARTZ":4,"ENCHANTED_QUARTZ_BLOCK":1},"out":1,"duration":null}],"DEVOUR_RING":[{"type":"crafting","in":{"ENCHANTED_RAW_CHICKEN":2,"ENCHANTED_RAW_SALMON":8,"REVENANT_VISCERA":39},"out":1,"duration":null}],"DRACONIC_ARTIFACT":[{"type":"crafting","in":{"RITUAL_RESIDUE":12,"TESSELLATED_ENDER_PEARL":2,"MITE_GEL":128,"DRACONIC_RING":1},"out":1,"duration":null}],"DRACONIC_TALISMAN":[{"type":"crafting","in":{"RITUAL_RESIDUE":2,"MITE_GEL":96,"ABSOLUTE_ENDER_PEARL":32},"out":1,"duration":null}],"DRACONIC_RING":[{"type":"crafting","in":{"RITUAL_RESIDUE":4,"MITE_GEL":96,"DRACONIC_TALISMAN":1,"TESSELLATED_ENDER_PEARL":1},"out":1,"duration":null}],"DWARVEN_METAL":[{"type":"forge","in":{"REFINED_TUNGSTEN:4.0":1,"REFINED_UMBER:4.0":1,"GLACITE_AMALGAMATION:4.0":1},"out":1.0,"duration":86400}],"DULL_SHARK_TOOTH_NECKLACE":[{"type":"crafting","in":{"NURSE_SHARK_TOOTH":4,"RAGGEDY_SHARK_TOOTH_NECKLACE":1},"out":1,"duration":null}],"EMERALD_ARTIFACT":[{"type":"crafting","in":{"ENCHANTED_EMERALD_BLOCK":36,"EMERALD_RING":1,"AVARICIOUS_CHALICE":1},"out":1,"duration":null}],"ENDER_RELIC":[{"type":"crafting","in":{"ENCHANTED_OBSIDIAN":128,"ENCHANTED_EYE_OF_ENDER":96,"ENDER_ARTIFACT":1,"EXCEEDINGLY_RARE_ENDER_ARTIFACT_UPGRADER":1},"out":1,"duration":null}],"ETERNAL_HOOF":[{"type":"crafting","in":{"SOUL_FRAGMENT":8,"LUCKY_HOOF":1},"out":1,"duration":null}],"EXPERIENCE_ARTIFACT":[{"type":"crafting","in":{"ENCHANTED_LAPIS_LAZULI_BLOCK":9},"out":1,"duration":null}],"FEATHER_ARTIFACT":[{"type":"crafting","in":{"ENCHANTED_FEATHER":128,"FEATHER_RING":1},"out":1,"duration":null}],"FEATHER_TALISMAN":[{"type":"crafting","in":{"FEATHER":108},"out":1,"duration":null}],"FIERY_KUUDRA_CORE":[{"type":"crafting","in":{"BURNING_KUUDRA_CORE":4},"out":1,"duration":null}],"FIRE_TALISMAN":[{"type":"crafting","in":{"ENCHANTED_BLAZE_POWDER":9},"out":1,"duration":null}],"EMERALD_RING":[{"type":"crafting","in":{"ENCHANTED_EMERALD":32},"out":1,"duration":null}],"FERMENTO_ARTIFACT":[{"type":"crafting","in":{"CONDENSED_FERMENTO":8,"SQUASH_RING":1},"out":1,"duration":null}],"FUTURE_CALORIES":[{"type":"crafting","in":{"ENCHANTED_RAW_FISH":384,"MAGMA_FISH_SILVER":48,"PRE_DIGESTION_FISH":1},"out":1,"duration":null}],"GARLIC_FLAVORED_GUMMY_BEAR":[{"type":"crafting","in":{"NEARLY_WHOLE_CARROT":16,"BACTE_FRAGMENT":16,"BLOODBADGE":8,"REHEATED_GUMMY_POLAR_BEAR":16},"out":1,"duration":null}],"GLACIAL_ARTIFACT":[{"type":"crafting","in":{"GLACIAL_FRAGMENT":40,"ENCHANTED_PACKED_ICE":8,"GLACIAL_RING":1},"out":1,"duration":null}],"GLACIAL_RING":[{"type":"crafting","in":{"GLACIAL_FRAGMENT":20,"ENCHANTED_ICE":128,"GLACIAL_TALISMAN":1},"out":1,"duration":null}],"GLOSSY_MINERAL_TALISMAN":[{"type":"crafting","in":{"GLOSSY_GEMSTONE":16,"MINERAL_TALISMAN":1},"out":1,"duration":null}],"GRAVITY_TALISMAN":[{"type":"crafting","in":{"OBSIDIAN":144},"out":1,"duration":null}],"FEATHER_RING":[{"type":"crafting","in":{"ENCHANTED_FEATHER":7,"FEATHER_TALISMAN":1},"out":1,"duration":null}],"HASTE_RING":[{"type":"crafting","in":{"ENCHANTED_COBBLESTONE":128},"out":1,"duration":null}],"HEALING_TALISMAN":[{"type":"crafting","in":{"WATER_LILY":144},"out":1,"duration":null}],"HEALING_RING":[{"type":"crafting","in":{"ENCHANTED_WATER_LILY":4,"HEALING_TALISMAN":1},"out":1,"duration":null}],"HONED_SHARK_TOOTH_NECKLACE":[{"type":"crafting","in":{"BLUE_SHARK_TOOTH":4,"DULL_SHARK_TOOTH_NECKLACE":1},"out":1,"duration":null}],"HUNTER_RING":[{"type":"crafting","in":{"GOLDEN_TOOTH":224,"GRIZZLY_BAIT":1,"HUNTER_TALISMAN":1},"out":1,"duration":null}],"INFERNAL_KUUDRA_CORE":[{"type":"crafting","in":{"FIERY_KUUDRA_CORE":4},"out":1,"duration":null}],"HUNTER_TALISMAN":[{"type":"crafting","in":{"GOLDEN_TOOTH":64,"ENCHANTED_ROTTEN_FLESH":32},"out":1,"duration":null}],"INTIMIDATION_RELIC":[{"type":"crafting","in":{"SCARE_FRAGMENT":8,"INTIMIDATION_ARTIFACT":1},"out":1,"duration":null}],"JERRY_TALISMAN_GOLDEN":[{"type":"crafting","in":{"JERRY_TALISMAN_PURPLE":5},"out":1,"duration":null}],"JERRY_TALISMAN_PURPLE":[{"type":"crafting","in":{"JERRY_TALISMAN_BLUE":5},"out":1,"duration":null}],"JERRY_TALISMAN_BLUE":[{"type":"crafting","in":{"JERRY_TALISMAN_GREEN":5},"out":1,"duration":null}],"HASTE_ARTIFACT":[{"type":"crafting","in":{"ENCHANTED_COBBLESTONE":256,"HASTE_RING":1},"out":1,"duration":null}],"LAVA_TALISMAN":[{"type":"crafting","in":{"ENCHANTED_MAGMA_CREAM":9},"out":1,"duration":null}],"LUSH_ARTIFACT":[{"type":"crafting","in":{"ENCHANTED_LUSH_BERBERIS":512,"LUSH_RING":1},"out":1,"duration":null}],"LUSH_TALISMAN":[{"type":"crafting","in":{"LUSH_BERBERIS":576},"out":1,"duration":null}],"LUSH_RING":[{"type":"crafting","in":{"ENCHANTED_LUSH_BERBERIS":64,"LUSH_TALISMAN":1},"out":1,"duration":null}],"MAGNETIC_TALISMAN":[{"type":"crafting","in":{"EMERALD":128,"REDSTONE":32},"out":1,"duration":null}],"MASTER_SKULL_TIER_3":[{"type":"crafting","in":{"MASTER_SKULL_TIER_2":4},"out":1,"duration":null}],"MASTER_SKULL_TIER_2":[{"type":"crafting","in":{"MASTER_SKULL_TIER_1":4},"out":1,"duration":null}],"MASTER_SKULL_TIER_5":[{"type":"crafting","in":{"MASTER_SKULL_TIER_4":4},"out":1,"duration":null}],"MASTER_SKULL_TIER_4":[{"type":"crafting","in":{"MASTER_SKULL_TIER_3":4},"out":1,"duration":null}],"MASTER_SKULL_TIER_7":[{"type":"crafting","in":{"MASTER_SKULL_TIER_6":4},"out":1,"duration":null}],"MASTER_SKULL_TIER_6":[{"type":"crafting","in":{"MASTER_SKULL_TIER_5":4},"out":1,"duration":null}],"MINERAL_TALISMAN":[{"type":"crafting","in":{"REFINED_MINERAL":9},"out":1,"duration":null}],"NIGHT_CRYSTAL":[{"type":"crafting","in":{"ENCHANTED_QUARTZ":4,"ENCHANTED_QUARTZ_BLOCK":1},"out":1,"duration":null}],"NIGHT_VISION_CHARM":[{"type":"crafting","in":{"ENCHANTED_RED_MUSHROOM":4,"ENCHANTED_BROWN_MUSHROOM":4},"out":1,"duration":null}],"PERSONAL_DELETOR_4000":[{"type":"crafting","in":{"ENCHANTED_IRON":112,"ENCHANTED_LAVA_BUCKET":1,"ENCHANTED_REDSTONE_BLOCK":1},"out":1,"duration":null}],"PERSONAL_DELETOR_5000":[{"type":"crafting","in":{"ENCHANTED_IRON":224,"PERSONAL_DELETOR_4000":1},"out":1,"duration":null}],"PERSONAL_DELETOR_6000":[{"type":"crafting","in":{"ENCHANTED_IRON":448,"PERSONAL_DELETOR_5000":1},"out":1,"duration":null}],"PERSONAL_DELETOR_7000":[{"type":"crafting","in":{"ENCHANTED_IRON_BLOCK":7,"PERSONAL_DELETOR_6000":1},"out":1,"duration":null}],"POTION_AFFINITY_TALISMAN":[{"type":"crafting","in":{"NETHER_STALK":144},"out":1,"duration":null}],"POWER_ARTIFACT":[{"type":"crafting","in":{"GEMSTONE_MIXTURE":32,"POWER_RING":1},"out":1,"duration":null}],"POWER_RELIC":[{"type":"forge","in":{"POWER_ARTIFACT:1.0":1,"PERFECT_PLATE:1.0":1,"GLACITE_AMALGAMATION:32.0":1},"out":1.0,"duration":28800}],"POWER_RING":[{"type":"crafting","in":{"FINE_RUBY_GEM":7,"GEMSTONE_MIXTURE":1,"POWER_TALISMAN":1},"out":1,"duration":null}],"POWER_TALISMAN":[{"type":"crafting","in":{"FLAWED_RUBY_GEM":45},"out":1,"duration":null}],"PULSE_RING":[{"type":"crafting","in":{"ORB_OF_ENERGY":256},"out":1,"duration":null}],"RAZOR_SHARP_SHARK_TOOTH_NECKLACE":[{"type":"crafting","in":{"GREAT_WHITE_SHARK_TOOTH":4,"SHARP_SHARK_TOOTH_NECKLACE":1},"out":1,"duration":null}],"RAGGEDY_SHARK_TOOTH_NECKLACE":[{"type":"crafting","in":{"SHARK_FIN":8,"NURSE_SHARK_TOOTH":1},"out":1,"duration":null}],"REAPER_ORB":[{"type":"crafting","in":{"SCYTHE_BLADE":8},"out":1,"duration":null}],"RED_CLAW_TALISMAN":[{"type":"crafting","in":{"WOLF_TOOTH":8,"ENCHANTED_BONE":1},"out":1,"duration":null}],"RELIC_OF_COINS":[{"type":"crafting","in":{"FRESHLY_MINTED_COINS":64,"ARTIFACT_OF_COINS":1},"out":1,"duration":null}],"RING_OF_COINS":[{"type":"crafting","in":{"FRESHLY_MINTED_COINS":4,"COIN_TALISMAN":1},"out":1,"duration":null}],"RING_POTION_AFFINITY":[{"type":"crafting","in":{"ENCHANTED_NETHER_STALK":8,"POTION_AFFINITY_TALISMAN":1},"out":1,"duration":null}],"SCARF_THESIS":[{"type":"crafting","in":{"SCARF_STUDIES":4},"out":1,"duration":null}],"SATELITE":[{"type":"crafting","in":{"YOUNGITE":64,"TIMITE":32,"OBSOLITE":32,"HIGHLITE":1},"out":1,"duration":null}],"SCARF_GRIMOIRE":[{"type":"crafting","in":{"SCARF_THESIS":4},"out":1,"duration":null}],"SCAVENGER_ARTIFACT":[{"type":"crafting","in":{"BLOOD_SOAKED_COINS":4,"FRESHLY_MINTED_COINS":16,"SCAVENGER_RING":1},"out":1,"duration":null}],"SCAVENGER_RING":[{"type":"crafting","in":{"BLOOD_STAINED_COINS":4,"FRESHLY_MINTED_COINS":8,"SCAVENGER_TALISMAN":1},"out":1,"duration":null}],"SEA_CREATURE_ARTIFACT":[{"type":"crafting","in":{"ENCHANTED_SPONGE":64,"SEA_CREATURE_RING":1},"out":1,"duration":null}],"SEA_CREATURE_TALISMAN":[{"type":"crafting","in":{"SPONGE":18},"out":1,"duration":null}],"SEA_CREATURE_RING":[{"type":"crafting","in":{"SPONGE":6,"ENCHANTED_SPONGE":2,"SEA_CREATURE_TALISMAN":1},"out":1,"duration":null}],"SHARP_SHARK_TOOTH_NECKLACE":[{"type":"crafting","in":{"TIGER_SHARK_TOOTH":4,"HONED_SHARK_TOOTH_NECKLACE":1},"out":1,"duration":null}],"SOULFLOW_BATTERY":[{"type":"crafting","in":{"NULL_OVOID":8,"SOULFLOW_PILE":1},"out":1,"duration":null}],"SOULFLOW_PILE":[{"type":"crafting","in":{"NULL_SPHERE":90},"out":1,"duration":null}],"SOULFLOW_SUPERCELL":[{"type":"crafting","in":{"NULL_OVOID":192,"NULL_ATOM":2,"SOULFLOW_BATTERY":1},"out":1,"duration":null}],"SPEED_ARTIFACT":[{"type":"crafting","in":{"ENCHANTED_SUGAR_CANE":48,"SPEED_RING":1},"out":1,"duration":null}],"SPEED_RING":[{"type":"crafting","in":{"ENCHANTED_SUGAR":96,"SPEED_TALISMAN":1},"out":1,"duration":null}],"SPEED_TALISMAN":[{"type":"crafting","in":{"SUGAR_CANE":108},"out":1,"duration":null}],"SPIDER_ARTIFACT":[{"type":"crafting","in":{"ENCHANTED_EMERALD":32,"TARANTULA_SILK":32,"SPIDER_RING":1},"out":1,"duration":null}],"SPIDER_RING":[{"type":"crafting","in":{"TARANTULA_WEB":64,"SPIDER_TALISMAN":1},"out":1,"duration":null}],"SPIKED_ATROCITY":[{"type":"crafting","in":{"ENCHANTED_INK_SACK":128,"SPIKED_BAIT":128,"BAIT_RING":1},"out":1,"duration":null}],"TITANIUM_ARTIFACT":[{"type":"forge","in":{"REFINED_TITANIUM":12,"TITANIUM_RING":1},"out":1,"duration":129600}],"SQUASH_RING":[{"type":"crafting","in":{"SQUASH":128,"CROPIE_TALISMAN":1},"out":1,"duration":null}],"TITANIUM_RELIC":[{"type":"forge","in":{"TITANIUM_ARTIFACT":1,"REFINED_TITANIUM":20},"out":1,"duration":259200}],"TITANIUM_RING":[{"type":"forge","in":{"TITANIUM_TALISMAN":1,"REFINED_TITANIUM":6},"out":1,"duration":72000}],"TITANIUM_TALISMAN":[{"type":"forge","in":{"REFINED_TITANIUM":2},"out":1,"duration":50400}],"TREASURE_ARTIFACT":[{"type":"crafting","in":{"TREASURE_RING":9},"out":1,"duration":null}],"VACCINE_RING":[{"type":"crafting","in":{"ARACHNE_FANG":8,"VACCINE_TALISMAN":1},"out":1,"duration":null}],"TWO_IQ_POINT":[{"type":"crafting","in":{"IQ_POINT":9},"out":1,"duration":null}],"VAMPIRE_DENTIST_RELIC":[{"type":"crafting","in":{"BLOODBADGE":8,"UNFANGED_VAMPIRE_PART":1},"out":1,"duration":null}],"VACCINE_TALISMAN":[{"type":"crafting","in":{"POISONOUS_POTATO":9},"out":1,"duration":null}],"VACCINE_ARTIFACT":[{"type":"crafting","in":{"POISON_SAMPLE":8,"VACCINE_RING":1},"out":1,"duration":null}],"TREASURE_RING":[{"type":"crafting","in":{"TREASURE_TALISMAN":8},"out":1,"duration":null}],"WOLF_RING":[{"type":"crafting","in":{"ENCHANTED_BONE":14,"WOLF_TALISMAN":1,"WEAK_WOLF_CATALYST":1},"out":1,"duration":null}],"WITHER_RELIC":[{"type":"crafting","in":{"WITHER_CATALYST":8,"WITHER_ARTIFACT":1},"out":1,"duration":null}],"ZOMBIE_ARTIFACT":[{"type":"crafting","in":{"ENCHANTED_IRON":32,"ENCHANTED_DIAMOND":16,"REVENANT_VISCERA":48,"ZOMBIE_RING":1},"out":1,"duration":null}],"ZOMBIE_RING":[{"type":"crafting","in":{"REVENANT_FLESH":64,"ZOMBIE_TALISMAN":1},"out":1,"duration":null}],"WOOD_TALISMAN":[{"type":"crafting","in":{"LEAVES":8,"ENCHANTED_OAK_LOG":1},"out":1,"duration":null}],"NEARLY_WHOLE_CARROT":[{"type":"crafting","in":{"HALF_EATEN_CARROT":80},"out":1,"duration":null}],"BLOODBADGE":[{"type":"crafting","in":{"COVEN_SEAL":64,"BACTE_FRAGMENT":2,"CRUXMOTION":2,"LEECH_SUPREME_FRAGMENT":2},"out":1,"duration":null}],"CRUXMOTION":[{"type":"crafting","in":{"SHAME_CRUX":1,"SHADOW_CRUX":1,"VOLT_CRUX":1,"SCRIBE_CRUX":1,"FROSTY_CRUX":1,"SPLATTER_CRUX":1},"out":1,"duration":null}]}} \ No newline at end of file diff --git a/index.html b/index.html index f3ac9ff..87e54aa 100644 --- a/index.html +++ b/index.html @@ -376,6 +376,7 @@


   <script src= + diff --git a/script.js b/script.js index fbf7827..5cc68a3 100644 --- a/script.js +++ b/script.js @@ -79,7 +79,8 @@ const CONFIG = { /* Accessory page preferences. */ BAZAAR_MODE_STORAGE: "shardmarket.bazaarMode", // "instaBuy" | "buyOrder" PREFER_MAX_STORAGE: "shardmarket.preferMax", // "1" | "0" - ACC_SORT_STORAGE: "shardmarket.accSortKey", // "mp" | "costPerMp" | "price" + ACC_SORT_STORAGE: "shardmarket.accSortKey", // "mp" | "costPerMp" | "price" | "craft" + CRAFT_PRICES_STORAGE: "hypixie.acc.craftPrices", // "1" | "0" — show craft cost next to AH price ATTR_USABLE_ONLY_STORAGE: "shardmarket.attributes.usableOnly", // "1" | "0" /* Filter out dead markets where no shards traded in the past week. */ @@ -166,6 +167,12 @@ const state = { bazaarMode: localStorage.getItem(CONFIG.BAZAAR_MODE_STORAGE) || "instaBuy", preferMax: localStorage.getItem(CONFIG.PREFER_MAX_STORAGE) !== "0", accSortKey: localStorage.getItem(CONFIG.ACC_SORT_STORAGE) || "mp", + /* Craft-price feature (see craft-cost.js). Recipes load lazily; the pricer is + * rebuilt whenever the bazaar, the AH scan or the bazaar mode changes. */ + showCraftPrices: localStorage.getItem(CONFIG.CRAFT_PRICES_STORAGE) !== "0", + craftRecipes: null, // {recipes, names, loaded, error} | null + craftPricer: null, + craftPricerSig: "", attrUsableOnly: localStorage.getItem(CONFIG.ATTR_USABLE_ONLY_STORAGE) === "1", sweepShowCompleted: localStorage.getItem(CONFIG.SWEEP_SHOW_COMPLETED_STORAGE) === "1", minionManualTiers: {}, @@ -1756,6 +1763,154 @@ function accessoryPrice(id) { }); } +/* ------------------------------------------------------------------------ */ +/* Craft-price engine (see craft-cost.js) */ +/* */ +/* Recipes are bundled statically, but the pricer depends on live market */ +/* data, so it is rebuilt whenever the bazaar snapshot, the AH scan, the */ +/* bazaar buy mode or the player's owned accessories change. */ +/* ------------------------------------------------------------------------ */ + +function ensureCraftRecipesLoaded() { + if (state.craftRecipes) return; + if (!window.loadCraftRecipes) return; + state.craftRecipes = { recipes: {}, names: {}, loaded: false, error: null, pending: true }; + window.loadCraftRecipes().then((data) => { + state.craftRecipes = data; + state.craftPricer = null; + if (state.view === "missing" || state.view === "upgrades") renderActiveView(); + }); +} + +function getCraftPricer() { + const data = state.craftRecipes; + if (!data?.loaded || !Object.keys(data.recipes || {}).length) return null; + + const sig = [ + state.raw?.lastUpdated || 0, + state.lowestBins ? state.lowestBins.size : -1, + state.bazaarMode, + state.player.ownedAccessories ? state.player.ownedAccessories.size : -1, + ].join("|"); + + if (state.craftPricer && state.craftPricerSig === sig) return state.craftPricer; + + state.craftPricer = window.createCraftPricer({ + bazaar: state.raw?.products, + bins: state.lowestBins, + bazaarMode: state.bazaarMode, + owned: state.player.ownedAccessories, + recipes: data.recipes, + names: data.names, + nameOf: (id) => state.allItemsById?.get(id)?.name || null, + }); + state.craftPricerSig = sig; + return state.craftPricer; +} + +/* Craft analysis for one accessory, or null when it cannot be computed. */ +function accessoryCraft(id) { + if (!state.showCraftPrices) return null; + const pricer = getCraftPricer(); + if (!pricer) return null; + try { + return pricer.analyse(id); + } catch (e) { + console.warn("[Hypixie] craft cost failed for", id, e); + return null; + } +} + +/* Human-readable forge duration (the recipe data stores seconds). */ +function forgeDurationText(seconds) { + const s = Number(seconds); + if (!Number.isFinite(s) || s <= 0) return null; + if (s < 3600) return `${Math.round(s / 60)}m`; + const h = s / 3600; + return h >= 24 ? `${(h / 24).toFixed(h / 24 >= 10 ? 0 : 1)}d` : `${h.toFixed(h >= 10 ? 0 : 1)}h`; +} + +/* The craft chip shown next to the AH/BIN price, with a click-to-expand + * ingredient breakdown. */ +function accessoryCraftHTML(item, marketPrice) { + const c = accessoryCraft(item.id); + if (!c) return ""; + + if (!c.hasRecipe) { + return ` + no recipe + `; + } + + const modeLabel = state.bazaarMode === "buyOrder" ? "buy orders" : "insta-buy"; + const forgeNote = c.type === "forge" && c.duration + ? `, ${escapeHtml(forgeDurationText(c.duration))} in the forge` + : c.type === "forge" ? ", forge" : ""; + + if (!c.craftable) { + const missingList = c.missing.map((m) => `${m.amount}× ${m.name}`).join(", "); + const partial = Number.isFinite(c.partialCost) && c.partialCost > 0 + ? `≥${escapeHtml(fmtCoins(c.partialCost))}` + : ""; + return `
+ + craft + ${partial} + not craftable + +
+

You can buy ${escapeHtml(fmtCoins(c.partialCost || 0))} of the materials, but these cannot be bought: ${escapeHtml(missingList)}.

+
+
`; + } + + const cheaper = Number.isFinite(marketPrice) && marketPrice > 0 && c.cost < marketPrice; + const savings = cheaper ? marketPrice - c.cost : 0; + const pct = cheaper ? Math.round((savings / marketPrice) * 100) : 0; + const cls = cheaper ? "acc-craft--cheap" : "acc-craft--plain"; + const srcTag = c.pureBazaar ? "bazaar mats" : "bz + AH mat"; + const title = cheaper + ? `Craft it for ${fmtCoins(c.cost)} instead of ${fmtCoins(marketPrice)} — saves ${fmtCoins(savings)} (${pct}%). Materials priced at ${modeLabel}.` + : `Craft cost ${fmtCoins(c.cost)} from live ${modeLabel} material prices${c.pureBazaar ? "" : " (one material is AH-only)"}.`; + + const rows = c.materials.map((m) => { + const srcLabel = + m.source === "owned" ? "owned" : + m.source === "ah" ? "AH" : + m.source === "craft" || m.source === "craft-ah" ? "crafted" : "bazaar"; + return ` +
  • + ${escapeHtml(m.amount)}× ${escapeHtml(m.name)}${ + m.ownedUnits ? ` (${m.ownedUnits} owned)` : "" + } + ${m.subtotal > 0 ? escapeHtml(fmtCoins(m.subtotal)) : "free"} + ${srcLabel} + +
  • `; + }).join(""); + + const freeNote = c.freeInputs.length + ? `

    Priced at 0 because you already own it: ${escapeHtml(c.freeInputs.map((f) => `${f.amount}× ${f.name}`).join(", "))}.

    ` + : ""; + + return `
    + + craft + ${escapeHtml(fmtCoins(c.cost))} + ${escapeHtml(srcTag)} + ${cheaper ? `−${pct}%` : ""} + +
    +

    + ${escapeHtml(c.type === "forge" ? "Forged" : "Crafted")} from ${c.materials.length} material${c.materials.length === 1 ? "" : "s"}${escapeHtml(forgeNote)} · ${escapeHtml(fmtCoins(c.cost))} total${cheaper ? ` · saves ${escapeHtml(fmtCoins(savings))} vs buying` : ""}. +

    +
      ${rows}
    + ${freeNote} +
    +
    `; +} + function buildSweepCatalog(itemsData) { const byId = {}; const resourceItems = itemsData?.items || []; @@ -1984,6 +2139,10 @@ function accessoryActionRow(item, mpLabel, mpValue) { priceTxt = `Auction House`; } + /* Craft-price chip: what it would cost to make this from live bazaar + * materials (and whether that is even possible). */ + const craftHTML = accessoryCraftHTML(item, price.best); + const tierColor = RARITY_COLORS[item.tier] || RARITY_COLORS.UNKNOWN; /* Soulbound items can't be bought — show a wiki link instead of a /ahs cmd. */ const cmdRow = item.soulbound @@ -2016,6 +2175,7 @@ function accessoryActionRow(item, mpLabel, mpValue) { ${item.tier.toLowerCase()} · ${priceTxt} + ${craftHTML ? `·${craftHTML}` : ""} @@ -2103,6 +2263,10 @@ function accessoryToolbarHTML() { Prefer max tier +
    @@ -2111,6 +2275,7 @@ function accessoryToolbarHTML() { +
    ${binsState}
    @@ -2135,6 +2300,16 @@ function bindAccessoryToolbar(container) { }); } + const craftToggle = container.querySelector("#craft-prices-toggle"); + if (craftToggle) { + craftToggle.addEventListener("change", (e) => { + state.showCraftPrices = e.target.checked; + localStorage.setItem(CONFIG.CRAFT_PRICES_STORAGE, state.showCraftPrices ? "1" : "0"); + if (state.showCraftPrices) ensureCraftRecipesLoaded(); + renderActiveView(); + }); + } + const sortSelect = container.querySelector("#acc-sort-select"); if (sortSelect) { sortSelect.addEventListener("change", (e) => { @@ -2195,17 +2370,47 @@ function accessoryPathActions(analysis) { flowHTML: `${escapeHtml(rc.item.name)}${rc.nextRarity.toLowerCase()}`, cardHTML: () => recombActionRow(rc, recombPrice), })), - ]; + ].map(withCraftCost); +} + +/* Attach craft-price data to a path action. + * craftCost — coins to craft it yourself, or Infinity when unknown + * effPrice — cheapest route: craft it when that beats the market, else buy */ +function withCraftCost(action) { + const c = action.item && action.type !== "recomb" ? accessoryCraft(action.item.id) : null; + const craftCost = c && c.craftable && Number.isFinite(c.cost) ? c.cost : Infinity; + const effPrice = Math.min(action.price, craftCost); + return { + ...action, + craft: c || null, + craftCost, + effPrice: Number.isFinite(effPrice) ? effPrice : action.price, + viaCraft: Number.isFinite(craftCost) && craftCost < action.price, + }; +} + +/* Safe numeric comparator — plain subtraction yields NaN when both sides are + * Infinity (unpriced accessories), which makes Array#sort unstable. */ +function cmpNum(a, b) { + if (a === b) return 0; + if (Number.isFinite(a) && Number.isFinite(b)) return a - b; + if (Number.isFinite(a)) return -1; // a priced, b unpriced → a first + if (Number.isFinite(b)) return 1; + return 0; } function sortAccessoryPathActions(actions) { const sorted = [...actions]; - if (state.accSortKey === "price") { - sorted.sort((x, y) => x.price - y.price || y.mpGain - x.mpGain); + if (state.accSortKey === "craft") { + /* Uncraftable steps sink to the bottom instead of poisoning the order. */ + sorted.sort((x, y) => cmpNum(x.craftCost, y.craftCost) || cmpNum(x.effPrice, y.effPrice) || y.mpGain - x.mpGain); + } else if (state.accSortKey === "price") { + sorted.sort((x, y) => cmpNum(x.price, y.price) || y.mpGain - x.mpGain); } else if (state.accSortKey === "costPerMp") { - sorted.sort((x, y) => (x.price / x.mpGain) - (y.price / y.mpGain) || y.mpGain - x.mpGain); + const perMp = (a) => (Number.isFinite(a.price) && a.mpGain > 0 ? a.price / a.mpGain : Infinity); + sorted.sort((x, y) => cmpNum(perMp(x), perMp(y)) || y.mpGain - x.mpGain); } else { - sorted.sort((x, y) => y.mpGain - x.mpGain || x.price - y.price); + sorted.sort((x, y) => y.mpGain - x.mpGain || cmpNum(x.price, y.price)); } return sorted; } @@ -2249,7 +2454,11 @@ function accessoryPathCard(action, index) {
    ${index + 1} ${action.label} - ${Number.isFinite(action.price) ? `${fmtCoins(action.price / action.mpGain)}/MP` : "price unknown"} + ${ + Number.isFinite(action.effPrice) + ? `${fmtCoins(action.effPrice / action.mpGain)}/MP${action.viaCraft ? ` craft` : ""}` + : "price unknown" + }
    ${action.flowHTML}
    ${action.cardHTML()} @@ -2280,6 +2489,11 @@ function renderMissingView() { const pricedActions = actions.filter((x) => Number.isFinite(x.price)); const totalKnownCost = pricedActions.reduce((s, x) => s + x.price, 0); + /* Craft-prices summary: how many steps are cheaper to craft than to buy. */ + if (state.showCraftPrices) ensureCraftRecipesLoaded(); + const craftableSteps = actions.filter((x) => Number.isFinite(x.craftCost)); + const craftSavings = craftableSteps.filter((x) => x.viaCraft).reduce((s, x) => s + (x.price - x.craftCost), 0); + pane.innerHTML = `
    @@ -2298,7 +2512,13 @@ function renderMissingView() {
    ${missingCount}missing
    ${upgradeCount}upgrades
    ${recombCount}recombs
    + ${state.showCraftPrices + ? `
    ${craftableSteps.length}craftable
    ` + : ""}
    + ${state.showCraftPrices && craftSavings > 0 + ? `

    Crafting the ${craftableSteps.filter((x) => x.viaCraft).length} steps where it is cheaper saves about ${fmtCoins(craftSavings)} versus buying them all.

    ` + : ""} ${accessoryToolbarHTML()}
    ${actions.length @@ -5636,6 +5856,10 @@ function init() { startTimeAgoTicker(); loadData(false); + /* Craft-price data is a small static file — warm it up in the background so + * the Accessory page has craft costs ready on first visit. */ + if (state.showCraftPrices) ensureCraftRecipesLoaded(); + /* If the user previously linked an account, auto-load. */ if (state.player.username) { loadPlayerProfiles(state.player.username); diff --git a/style.css b/style.css index 12129a5..5468b56 100644 --- a/style.css +++ b/style.css @@ -1696,6 +1696,129 @@ input[type="number"]:focus, .acc-price { font-family: var(--font-mono); color: var(--text-soft); } .acc-price-src { color: var(--text-faint); font-size: 10.5px; } .acc-price-ah { color: var(--info); font-weight: 600; } + +/* ---------- Craft-price chip (Accessory path) ---------- + * Sits next to the AH/bazaar price and expands into the full material list. */ +.acc-craft { + flex: 0 1 auto; + align-self: center; + min-width: 0; +} +.acc-craft[open] { flex: 1 1 100%; } +.acc-craft > summary { + display: inline-flex; + align-items: center; + gap: 5px; + padding: 2px 8px; + border: 1px solid var(--surface-line); + border-radius: 999px; + background: rgba(255, 255, 255, 0.03); + font-family: var(--font-mono); + font-size: 11.5px; + line-height: 1.6; + color: var(--text-soft); + cursor: pointer; + list-style: none; + user-select: none; + transition: border-color .15s ease, color .15s ease, background .15s ease; +} +.acc-craft > summary::-webkit-details-marker { display: none; } +.acc-craft > summary::before { + content: ""; + width: 0; height: 0; + border-left: 4px solid currentColor; + border-top: 3.5px solid transparent; + border-bottom: 3.5px solid transparent; + opacity: .5; + flex-shrink: 0; + transition: transform .15s ease; +} +.acc-craft[open] > summary::before { transform: rotate(90deg); } +.acc-craft > summary:hover { border-color: var(--text-muted); color: var(--text); } +.acc-craft:not([open]) > .acc-craft-body { display: none; } + +.acc-craft-tag { + font-size: 9.5px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: .06em; + opacity: .7; +} +.acc-craft-val { font-weight: 700; color: var(--text); } +.acc-craft-src { font-size: 10px; color: var(--text-faint); } +.acc-craft-save { font-size: 10.5px; font-weight: 700; color: var(--pos); } + +.acc-craft--cheap > summary { + border-color: rgba(46, 224, 122, 0.45); + background: rgba(46, 224, 122, 0.10); +} +.acc-craft--cheap .acc-craft-val { color: var(--pos); } + +.acc-craft--blocked > summary { + border-style: dashed; + color: var(--text-muted); +} +.acc-craft--blocked .acc-craft-val { color: var(--text-muted); } + +.acc-craft--none { + font-family: var(--font-mono); + font-size: 11px; + font-style: italic; + color: var(--text-faint); +} + +.acc-craft-body { + margin-top: 6px; + padding: 9px 11px; + background: var(--bg-elevated); + border: 1px solid var(--surface-line); + border-radius: var(--r-md); +} +.acc-craft-note { + margin: 0 0 7px; + font-size: 11.5px; + line-height: 1.45; + color: var(--text-muted); +} +.acc-craft-list { + list-style: none; + margin: 0; + padding: 0; + display: flex; + flex-direction: column; + gap: 3px; +} +.acc-craft-list li { + display: flex; + justify-content: space-between; + align-items: baseline; + gap: 12px; + font-size: 11.5px; +} +.acc-craft-ing { color: var(--text-soft); } +.acc-craft-ing-price { + font-family: var(--font-mono); + color: var(--text); + white-space: nowrap; +} +.acc-craft-ing-src { + font-family: var(--font-body); + font-style: normal; + font-size: 9px; + font-weight: 600; + margin-left: 6px; + padding: 1px 4px; + border-radius: 3px; + text-transform: uppercase; + letter-spacing: .04em; +} +.acc-craft-ing-owned { font-style: normal; font-size: 10px; color: var(--text-faint); } +.acc-craft-ing-src--bazaar { background: rgba(255, 255, 255, 0.06); color: var(--text-muted); } +.acc-craft-ing-src--ah { background: rgba(255, 107, 53, 0.16); color: var(--ember); } +.acc-craft-ing-src--owned { background: rgba(46, 224, 122, 0.16); color: var(--pos); } +.acc-craft-ing-src--craft, +.acc-craft-ing-src--craft-ah { background: rgba(120, 160, 255, 0.16); color: #8fb0ff; } + .acc-card-mp { text-align: right; flex-shrink: 0; @@ -1797,7 +1920,7 @@ input[type="number"]:focus, .acc-path-start { color: var(--text-muted); } .acc-path-summary { display: grid; - grid-template-columns: repeat(3, minmax(0, 1fr)); + grid-template-columns: repeat(auto-fit, minmax(110px, 1fr)); gap: 10px; margin: -4px 0 16px; } @@ -1829,6 +1952,26 @@ input[type="number"]:focus, flex-wrap: wrap; } .acc-upgrade-arrow { color: var(--ember); font-weight: 700; } +.acc-path-viacraft { + font-family: var(--font-body); + font-style: normal; + font-size: 9.5px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: .05em; + color: var(--pos); + opacity: .9; +} +.acc-craft-summary { + margin: -6px 0 14px; + padding: 9px 12px; + background: rgba(46, 224, 122, 0.07); + border: 1px solid rgba(46, 224, 122, 0.28); + border-radius: var(--r-md); + font-size: 12.5px; + color: var(--text-soft); +} +.acc-craft-summary strong { color: var(--text); } .acc-card--upgrade .acc-card-main { margin-top: 2px; } /* Gate / loading / empty states */ diff --git a/tools/build-craft-recipes.py b/tools/build-craft-recipes.py new file mode 100644 index 0000000..fa081bc --- /dev/null +++ b/tools/build-craft-recipes.py @@ -0,0 +1,259 @@ +#!/usr/bin/env python3 +"""Build data/accessory-recipes.json for Hypixie. + +Hypixel's own /v2/resources/skyblock/items endpoint does NOT expose crafting +recipes, so we pull them from the community NotEnoughUpdates repo (MIT-ish, +public data) once, at development time, and bundle the result as a static file. +The site itself stays backend-free and does not talk to GitHub at runtime. + +Pipeline + 1. Hypixel items API -> every ACCESSORY item id + 2. Hypixel bazaar API -> which item ids can simply be bought (price leaves) + 3. NEU repo (raw GitHub) -> shaped `recipe` / list `recipes` for each id + 4. Closure crawl: any ingredient that is neither on the bazaar nor already + fetched gets fetched too, so deep chains (Bat Talisman -> Bat Ring -> + Bat Artifact, Enchanted X -> Enchanted X Block, ...) can be priced. + +Usage + python tools/build-craft-recipes.py # incremental (uses .neu-cache) + python tools/build-craft-recipes.py --refresh # ignore cached NEU files +""" + +from __future__ import annotations + +import argparse +import json +import os +import re +import sys +import time +import urllib.error +import urllib.request +from concurrent.futures import ThreadPoolExecutor, as_completed + +ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +CACHE = os.path.join(ROOT, "tools", ".neu-cache") +OUT = os.path.join(ROOT, "data", "accessory-recipes.json") + +ITEMS_URL = "https://api.hypixel.net/v2/resources/skyblock/items" +BAZAAR_URL = "https://api.hypixel.net/v2/skyblock/bazaar" +NEU_URL = "https://raw.githubusercontent.com/NotEnoughUpdates/NotEnoughUpdates-REPO/master/items/{}.json" + +WORKERS = 16 +MAX_ITEMS = 20000 # hard safety cap on how many NEU files we pull +RECIPE_TYPES = {"crafting", "forge"} + +USER_AGENT = "Hypixie-recipe-builder/1.0 (+static site build tooling)" + + +def http_get(url: str, retries: int = 3, timeout: int = 30) -> bytes | None: + for attempt in range(retries): + try: + req = urllib.request.Request(url, headers={"User-Agent": USER_AGENT}) + with urllib.request.urlopen(req, timeout=timeout) as r: + return r.read() + except urllib.error.HTTPError as e: + if e.code == 404: + return None + if attempt == retries - 1: + return None + time.sleep(1.5 * (attempt + 1)) + except Exception: + if attempt == retries - 1: + return None + time.sleep(1.5 * (attempt + 1)) + return None + + +def load_json(path: str): + with open(path, "r", encoding="utf-8") as f: + return json.load(f) + + +def strip_colors(s: str) -> str: + return re.sub(r"§[0-9a-fk-orA-FK-OR]", "", s or "").strip() + + +def parse_entry(raw: str): + """'ENCHANTED_BONE:2' -> ('ENCHANTED_BONE', 2). 'ENCHANTED_BONE' -> ('...',1).""" + if not raw: + return None + raw = raw.strip() + if not raw: + return None + if ":" in raw: + item_id, _, cnt = raw.rpartition(":") + try: + count = int(cnt) + except ValueError: + item_id, count = raw, 1 + else: + item_id, count = raw, 1 + item_id = item_id.strip().upper() + if not item_id or count <= 0: + return None + return item_id, count + + +def extract_recipes(doc: dict): + """Return a list of {type, in: {id: amount}, out: n, duration: sec|None}.""" + out = [] + + shaped = doc.get("recipe") + if isinstance(shaped, dict) and shaped: + ing = {} + for _slot, raw in shaped.items(): + parsed = parse_entry(raw if isinstance(raw, str) else "") + if not parsed: + continue + iid, cnt = parsed + ing[iid] = ing.get(iid, 0) + cnt + if ing: + out.append({"type": "crafting", "in": ing, "out": doc.get("recipeCount") or 1, "duration": None}) + + for r in doc.get("recipes") or []: + if not isinstance(r, dict): + continue + rtype = (r.get("type") or "crafting").lower() + if rtype not in RECIPE_TYPES: + continue + ing = {} + for raw in r.get("inputs") or []: + parsed = parse_entry(raw if isinstance(raw, str) else "") + if not parsed: + continue + iid, cnt = parsed + ing[iid] = ing.get(iid, 0) + cnt + if not ing: + continue + out.append({ + "type": rtype, + "in": ing, + "out": r.get("count") or 1, + "duration": r.get("duration") or None, + }) + + return out + + +def neu_fetch(item_id: str, refresh: bool): + path = os.path.join(CACHE, item_id + ".json") + if os.path.exists(path) and not refresh: + try: + return item_id, load_json(path) + except Exception: + pass + raw = http_get(NEU_URL.format(item_id)) + if raw is None: + with open(path, "w", encoding="utf-8") as f: + f.write("null") + return item_id, None + try: + doc = json.loads(raw.decode("utf-8")) + except Exception: + return item_id, None + with open(path, "w", encoding="utf-8") as f: + json.dump(doc, f, ensure_ascii=False) + return item_id, doc + + +def fetch_many(ids, refresh: bool): + """Fetch a batch of NEU item docs in parallel. Returns {id: doc|None}.""" + res = {} + if not ids: + return res + with ThreadPoolExecutor(max_workers=WORKERS) as ex: + futs = {ex.submit(neu_fetch, i, refresh): i for i in ids} + done = 0 + for fut in as_completed(futs): + iid, doc = fut.result() + res[iid] = doc + done += 1 + if done % 100 == 0: + print(f" ... {done}/{len(ids)}", flush=True) + return res + + +def main() -> int: + ap = argparse.ArgumentParser() + ap.add_argument("--refresh", action="store_true", help="re-download cached NEU item files") + args = ap.parse_args() + + os.makedirs(CACHE, exist_ok=True) + os.makedirs(os.path.dirname(OUT), exist_ok=True) + + print("[1/4] Hypixel items API ...") + raw = http_get(ITEMS_URL, timeout=60) + if raw is None: + print(" ! failed to fetch items API", file=sys.stderr) + return 1 + items = json.loads(raw.decode("utf-8")).get("items") or [] + accessories = [i for i in items if i.get("category") == "ACCESSORY"] + print(f" {len(items)} items, {len(accessories)} accessories") + + print("[2/4] Bazaar products ...") + raw = http_get(BAZAAR_URL, timeout=60) + bazaar_ids = set() + if raw is not None: + bazaar_ids = set((json.loads(raw.decode("utf-8")).get("products") or {}).keys()) + print(f" {len(bazaar_ids)} bazaar products") + + seeds = sorted({a["id"] for a in accessories}) + seen: set[str] = set() + queue: list[str] = list(seeds) + recipes: dict[str, list] = {} + names: dict[str, str] = {} + + print("[3/4] Crawling NEU recipes (closure over non-bazaar ingredients) ...") + depth = 0 + while queue and len(seen) < MAX_ITEMS: + depth += 1 + batch = [i for i in queue if i not in seen] + if not batch: + break + queue = [] + print(f" depth {depth}: {len(batch)} ids to fetch") + docs = fetch_many(batch, args.refresh) + for iid, doc in docs.items(): + seen.add(iid) + if not doc: + continue + dn = strip_colors(doc.get("displayname") or "") + if dn: + names[iid] = dn + rs = extract_recipes(doc) + if not rs: + continue + recipes[iid] = rs + for r in rs: + for ing in r["in"]: + if ing in bazaar_ids: + continue + if ing in seen or ing in queue: + continue + queue.append(ing) + + print(f" fetched {len(seen)} NEU files, {len(recipes)} with usable recipes") + + # Drop recipes whose ingredients reference nothing we can ever resolve. + # (Kept in the file — the runtime decides craftability live.) + payload = { + "generatedAt": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()), + "source": "NotEnoughUpdates/NotEnoughUpdates-REPO (item recipes)", + "accessoryIds": seeds, + "names": names, + "recipes": recipes, + } + with open(OUT, "w", encoding="utf-8") as f: + json.dump(payload, f, ensure_ascii=False, separators=(",", ":")) + + size = os.path.getsize(OUT) + acc_with = sum(1 for i in seeds if i in recipes) + print("[4/4] Wrote data/accessory-recipes.json") + print(f" {size/1024:.0f} KiB | {acc_with}/{len(seeds)} accessories have a recipe" + f" | {len(recipes)} recipe entries total") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())