From 39d61d797d6c449e2a7c5ce8a8eb8391564e7710 Mon Sep 17 00:00:00 2001 From: Jo Blo Date: Wed, 4 Mar 2026 17:01:08 -0500 Subject: [PATCH] initial commit --- .claude/settings.local.json | 25 + .gitignore | 33 + .vscode/extensions.json | 3 + README.md | 5 + index.html | 13 + package-lock.json | 2376 +++++++++++++++++++++ package.json | 25 + postcss.config.ts | 6 + public/vite.svg | 1 + src/App.vue | 91 + src/assets/vue.svg | 1 + src/components/HelloWorld.vue | 41 + src/components/PriceCell.vue | 111 + src/components/filters/CategoryFilter.vue | 31 + src/components/filters/CityFilter.vue | 25 + src/components/filters/FilterBar.vue | 328 +++ src/components/filters/FilterPanel.vue | 90 + src/components/filters/TaxInput.vue | 27 + src/components/filters/TierFilter.vue | 40 + src/components/layout/AppHeader.vue | 20 + src/components/table/ProfitRow.vue | 294 +++ src/components/table/ProfitTable.vue | 47 + src/components/table/TableHeader.vue | 42 + src/components/ui/ErrorBanner.vue | 28 + src/components/ui/LastUpdated.vue | 32 + src/components/ui/LoadingSpinner.vue | 14 + src/components/ui/RefreshButton.vue | 48 + src/composables/useAlbionPrices.ts | 137 ++ src/composables/useAutoRefresh.ts | 55 + src/composables/useCraftingProfit.ts | 131 ++ src/composables/useFilters.ts | 79 + src/composables/useProductionOrder.ts | 79 + src/data/categories.json | 456 ++++ src/data/constants.ts | 24 + src/data/itemTree.ts | 29 + src/data/recipes.json | 2287 ++++++++++++++++++++ src/data/recipes.ts | 80 + src/env.d.ts | 1 + src/main.ts | 5 + src/pages/PricesPage.vue | 115 + src/pages/ProductionPage.vue | 286 +++ src/style.css | 3 + src/types/api.ts | 30 + src/types/crafting.ts | 47 + src/types/filters.ts | 11 + src/utils/formatting.ts | 140 ++ tailwind.config.ts | 13 + tsconfig.app.json | 16 + tsconfig.json | 7 + tsconfig.node.json | 26 + vite.config.ts | 10 + 51 files changed, 7864 insertions(+) create mode 100644 .claude/settings.local.json create mode 100644 .gitignore create mode 100644 .vscode/extensions.json create mode 100644 README.md create mode 100644 index.html create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 postcss.config.ts create mode 100644 public/vite.svg create mode 100644 src/App.vue create mode 100644 src/assets/vue.svg create mode 100644 src/components/HelloWorld.vue create mode 100644 src/components/PriceCell.vue create mode 100644 src/components/filters/CategoryFilter.vue create mode 100644 src/components/filters/CityFilter.vue create mode 100644 src/components/filters/FilterBar.vue create mode 100644 src/components/filters/FilterPanel.vue create mode 100644 src/components/filters/TaxInput.vue create mode 100644 src/components/filters/TierFilter.vue create mode 100644 src/components/layout/AppHeader.vue create mode 100644 src/components/table/ProfitRow.vue create mode 100644 src/components/table/ProfitTable.vue create mode 100644 src/components/table/TableHeader.vue create mode 100644 src/components/ui/ErrorBanner.vue create mode 100644 src/components/ui/LastUpdated.vue create mode 100644 src/components/ui/LoadingSpinner.vue create mode 100644 src/components/ui/RefreshButton.vue create mode 100644 src/composables/useAlbionPrices.ts create mode 100644 src/composables/useAutoRefresh.ts create mode 100644 src/composables/useCraftingProfit.ts create mode 100644 src/composables/useFilters.ts create mode 100644 src/composables/useProductionOrder.ts create mode 100644 src/data/categories.json create mode 100644 src/data/constants.ts create mode 100644 src/data/itemTree.ts create mode 100644 src/data/recipes.json create mode 100644 src/data/recipes.ts create mode 100644 src/env.d.ts create mode 100644 src/main.ts create mode 100644 src/pages/PricesPage.vue create mode 100644 src/pages/ProductionPage.vue create mode 100644 src/style.css create mode 100644 src/types/api.ts create mode 100644 src/types/crafting.ts create mode 100644 src/types/filters.ts create mode 100644 src/utils/formatting.ts create mode 100644 tailwind.config.ts create mode 100644 tsconfig.app.json create mode 100644 tsconfig.json create mode 100644 tsconfig.node.json create mode 100644 vite.config.ts diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..4487b17 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,25 @@ +{ + "permissions": { + "allow": [ + "Bash(npx tsc:*)", + "WebFetch(domain:github.com)", + "WebSearch", + "WebFetch(domain:raw.githubusercontent.com)", + "WebFetch(domain:wiki.albiononline.com)", + "WebFetch(domain:www.tools4albion.com)", + "WebFetch(domain:albiononline2d.com)", + "WebFetch(domain:gist.github.com)", + "WebFetch(domain:forum.albiononline.com)", + "WebFetch(domain:www.albion-online-data.com)", + "WebFetch(domain:gamerifts.com)", + "WebFetch(domain:albionhub.com)", + "WebFetch(domain:hyper3d.ai)", + "WebFetch(domain:albiononlineguideblog.wordpress.com)", + "Bash(node:*)", + "Bash(python3:*)", + "Bash(curl:*)", + "Bash(cat > /tmp/gen_final.py << 'PYEOF'\n#!/usr/bin/env python3\n\"\"\"Generate corrected recipes.json and categories.json from items.json\"\"\"\nimport json, urllib.request, sys\n\nURL = \"https://raw.githubusercontent.com/ao-data/ao-bin-dumps/refs/heads/master/items.json\"\nOUT_RECIPES = \"/home/jbourdon/repos/albion-crafting-calc/src/data/recipes.json\"\nOUT_CATEGORIES = \"/home/jbourdon/repos/albion-crafting-calc/src/data/categories.json\"\n\nprint\\(\"Fetching items.json...\", file=sys.stderr\\)\nreq = urllib.request.Request\\(URL, headers={'User-Agent': 'Mozilla/5.0'}\\)\nwith urllib.request.urlopen\\(req, timeout=120\\) as r:\n raw = json.load\\(r\\)\n\nitems_root = raw['items']\nlookup = {}\nfor type_key, items_list in items_root.items\\(\\):\n if isinstance\\(items_list, list\\):\n for item in items_list:\n if isinstance\\(item, dict\\):\n name = item.get\\('@uniquename',''\\)\n if name: lookup[name] = item\n elif isinstance\\(items_list, dict\\):\n name = items_list.get\\('@uniquename',''\\)\n if name: lookup[name] = items_list\n\ndef resolve_cr\\(item\\):\n cr = item.get\\('craftingrequirements'\\)\n if cr is None: return None\n if isinstance\\(cr, list\\):\n for path in cr:\n reqs = path.get\\('craftresource', []\\)\n if isinstance\\(reqs, dict\\): reqs = [reqs]\n bad = any\\('TOKEN_FAVOR' in r.get\\('@uniquename',''\\) or 'QUESTITEM' in r.get\\('@uniquename',''\\) for r in reqs\\)\n if not bad: return path\n return cr[0]\n return cr\n\ndef extract_ings\\(cr\\):\n reqs = cr.get\\('craftresource', []\\)\n if isinstance\\(reqs, dict\\): reqs = [reqs]\n result = []\n for r in reqs:\n name = r.get\\('@uniquename',''\\)\n qty = int\\(r.get\\('@count', 1\\)\\)\n if 'TOKEN_FAVOR' in name or 'QUESTITEM' in name: continue\n result.append\\(\\(name, qty\\)\\)\n return result\n\ndef templatize\\(item_id\\):\n if item_id.startswith\\('T4_'\\): return 'T{t}_' + item_id[3:]\n if item_id.startswith\\('T3_'\\): return 'T{t-1}_' + item_id[3:]\n return item_id\n\ndef no_enchant\\(item_id\\):\n return 'ARTEFACT' in item_id or 'ALCHEMY_RARE' in item_id\n\ndef make_recipe\\(id_, display_name, category, output_tmpl, enchanted=None\\):\n t4_id = output_tmpl.replace\\('{t}','4'\\)\n item = lookup.get\\(t4_id\\)\n if not item:\n print\\(f\" NOT_FOUND: {t4_id}\", file=sys.stderr\\); return None\n cr = resolve_cr\\(item\\)\n if not cr:\n print\\(f\" NO_CR: {t4_id}\", file=sys.stderr\\); return None\n ings = extract_ings\\(cr\\)\n entry = {\n 'id': id_,\n 'displayName': display_name,\n 'category': category,\n 'outputId': output_tmpl,\n 'ingredients': []\n }\n if enchanted is False:\n entry['enchanted'] = False\n for item_id, qty in ings:\n ing = {'itemId': templatize\\(item_id\\), 'qty': qty}\n if no_enchant\\(item_id\\): ing['noEnchant'] = True\n entry['ingredients'].append\\(ing\\)\n return entry\n\n# ─── Build recipes ────────────────────────────────────────────────────────────\nrecipes = []\nW = 'Weapons'; A = 'Armor'; G = 'Gathering'; F = 'Food'; P = 'Potions'\n\ndef c\\(msg\\): recipes.append\\({'comment': msg}\\)\ndef a\\(id_, name, cat, tmpl, enchanted=None\\):\n r = make_recipe\\(id_, name, cat, tmpl, enchanted\\)\n if r: recipes.append\\(r\\)\n\nc\\('--- Bows -------------------------------------------------------------------'\\)\na\\('bow','Bow',W,'T{t}_2H_BOW'\\)\na\\('longbow','Longbow',W,'T{t}_2H_LONGBOW'\\)\na\\('warbow','Warbow',W,'T{t}_2H_WARBOW'\\)\na\\('whispering-bow','Whispering Bow',W,'T{t}_2H_LONGBOW_UNDEAD'\\)\na\\('wailing-bow','Wailing Bow',W,'T{t}_2H_BOW_HELL'\\)\na\\('bow-of-badon','Bow of Badon',W,'T{t}_2H_BOW_KEEPER'\\)\na\\('mistpiercer','Mistpiercer',W,'T{t}_2H_BOW_AVALON'\\)\na\\('skystrider-bow','Skystrider Bow',W,'T{t}_2H_BOW_CRYSTAL'\\)\n\nc\\('--- Swords \\(Bars + Leather\\) ------------------------------------------------'\\)\na\\('broadsword','Broadsword',W,'T{t}_MAIN_SWORD'\\)\na\\('claymore','Claymore',W,'T{t}_2H_CLAYMORE'\\)\na\\('dual-swords','Dual Swords',W,'T{t}_2H_DUALSWORD'\\)\n\nc\\('--- Axes \\(Planks + Bars\\) ---------------------------------------------------'\\)\na\\('battleaxe','Battleaxe',W,'T{t}_MAIN_AXE'\\)\na\\('great-axe','Great Axe',W,'T{t}_2H_AXE'\\)\na\\('halberd','Halberd',W,'T{t}_2H_HALBERD'\\)\n\nc\\('--- Maces / Hammers --------------------------------------------------------'\\)\na\\('mace','Mace',W,'T{t}_MAIN_MACE'\\)\na\\('hammer','Hammer',W,'T{t}_MAIN_HAMMER'\\)\na\\('great-hammer','Great Hammer',W,'T{t}_2H_HAMMER'\\)\na\\('polehammer','Polehammer',W,'T{t}_2H_POLEHAMMER'\\)\n\nc\\('--- Crossbows \\(Planks + Bars\\) ----------------------------------------------'\\)\na\\('light-crossbow','Light Crossbow',W,'T{t}_MAIN_1HCROSSBOW'\\)\na\\('crossbow','Crossbow',W,'T{t}_2H_CROSSBOW'\\)\na\\('heavy-crossbow','Heavy Crossbow',W,'T{t}_2H_CROSSBOWLARGE'\\)\n\nc\\('--- Daggers \\(Bars + Leather\\) -----------------------------------------------'\\)\na\\('dagger','Dagger',W,'T{t}_MAIN_DAGGER'\\)\na\\('dagger-pair','Dagger Pair',W,'T{t}_2H_DAGGERPAIR'\\)\na\\('claws','Claws',W,'T{t}_2H_CLAWPAIR'\\)\n\nc\\('--- Spears \\(Planks + Bars\\) -------------------------------------------------'\\)\na\\('spear','Spear',W,'T{t}_MAIN_SPEAR'\\)\na\\('pike','Pike',W,'T{t}_2H_SPEAR'\\)\na\\('glaive','Glaive',W,'T{t}_2H_GLAIVE'\\)\n\nc\\('--- Quarterstaves \\(Bars + Leather\\) -----------------------------------------'\\)\na\\('quarterstaff','Quarterstaff',W,'T{t}_2H_QUARTERSTAFF'\\)\na\\('iron-clad-staff','Iron-clad Staff',W,'T{t}_2H_IRONCLADEDSTAFF'\\)\na\\('double-bladed-staff','Double Bladed Staff',W,'T{t}_2H_DOUBLEBLADEDSTAFF'\\)\n\nc\\('--- Fire Staves \\(Planks + Bars\\) --------------------------------------------'\\)\na\\('fire-staff','Fire Staff',W,'T{t}_MAIN_FIRESTAFF'\\)\na\\('great-fire-staff','Great Fire Staff',W,'T{t}_2H_FIRESTAFF'\\)\na\\('inferno-staff','Inferno Staff',W,'T{t}_2H_INFERNOSTAFF'\\)\n\nc\\('--- Arcane Staves \\(Planks + Bars\\) ------------------------------------------'\\)\na\\('arcane-staff','Arcane Staff',W,'T{t}_MAIN_ARCANESTAFF'\\)\na\\('great-arcane-staff','Great Arcane Staff',W,'T{t}_2H_ARCANESTAFF'\\)\na\\('enigmatic-staff','Enigmatic Staff',W,'T{t}_2H_ENIGMATICSTAFF'\\)\n\nc\\('--- Cursed Staves \\(Planks + Bars\\) ------------------------------------------'\\)\na\\('cursed-staff','Cursed Staff',W,'T{t}_MAIN_CURSEDSTAFF'\\)\na\\('great-cursed-staff','Great Cursed Staff',W,'T{t}_2H_CURSEDSTAFF'\\)\na\\('demonic-staff','Demonic Staff',W,'T{t}_2H_DEMONICSTAFF'\\)\n\nc\\('--- Frost Staves \\(Planks + Bars\\) -------------------------------------------'\\)\na\\('frost-staff','Frost Staff',W,'T{t}_MAIN_FROSTSTAFF'\\)\na\\('great-frost-staff','Great Frost Staff',W,'T{t}_2H_FROSTSTAFF'\\)\na\\('glacial-staff','Glacial Staff',W,'T{t}_2H_GLACIALSTAFF'\\)\n\nc\\('--- Holy Staves \\(Planks + Cloth\\) -------------------------------------------'\\)\na\\('holy-staff','Holy Staff',W,'T{t}_MAIN_HOLYSTAFF'\\)\na\\('great-holy-staff','Great Holy Staff',W,'T{t}_2H_HOLYSTAFF'\\)\na\\('divine-staff','Divine Staff',W,'T{t}_2H_DIVINESTAFF'\\)\n\nc\\('--- Nature Staves \\(Planks + Cloth\\) -----------------------------------------'\\)\na\\('nature-staff','Nature Staff',W,'T{t}_MAIN_NATURESTAFF'\\)\na\\('great-nature-staff','Great Nature Staff',W,'T{t}_2H_NATURESTAFF'\\)\na\\('wild-staff','Wild Staff',W,'T{t}_2H_WILDSTAFF'\\)\n\nc\\('--- Shapeshifter Weapons \\(Planks + Leather + Alchemy\\) ----------------------'\\)\na\\('shapeshifter-claws','Shapeshifter Claws',W,'T{t}_2H_SHAPESHIFTER_SET1'\\)\na\\('shapeshifter-spear','Shapeshifter Spear',W,'T{t}_2H_SHAPESHIFTER_SET2'\\)\na\\('shapeshifter-staff','Shapeshifter Staff',W,'T{t}_2H_SHAPESHIFTER_SET3'\\)\n\nc\\('--- War Gloves \\(Bars + Leather\\) --------------------------------------------'\\)\na\\('brawler-gloves','Brawler Gloves',W,'T{t}_2H_KNUCKLES_SET1'\\)\na\\('battle-bracers','Battle Bracers',W,'T{t}_2H_KNUCKLES_SET2'\\)\na\\('ravenstrike-cestus','Ravenstrike Cestus',W,'T{t}_2H_KNUCKLES_SET3'\\)\n\nc\\('--- Off-hands ---------------------------------------------------------------'\\)\na\\('shield','Shield',W,'T{t}_OFF_SHIELD'\\)\na\\('torch','Torch',W,'T{t}_OFF_TORCH'\\)\na\\('tome-of-spells','Tome of Spells',W,'T{t}_OFF_BOOK'\\)\n\n# ─── Plate Armor ──────────────────────────────────────────────────────────────\nc\\('--- Plate Armor — sets 1-3 \\(Bars only\\) ------------------------------------'\\)\nfor sn,sl in [\\('SET1','Soldier'\\),\\('SET2','Knight'\\),\\('SET3','Guardian'\\)]:\n n = sl.lower\\(\\)\n a\\(f'{n}-helmet',f'{sl} Helmet',A,f'T{{t}}_HEAD_PLATE_{sn}'\\)\n a\\(f'{n}-armor',f'{sl} Armor',A,f'T{{t}}_ARMOR_PLATE_{sn}'\\)\n a\\(f'{n}-boots',f'{sl} Boots',A,f'T{{t}}_SHOES_PLATE_{sn}'\\)\n\nc\\('--- Plate Armor — artifact variants \\(Bars + artifact\\) ----------------------'\\)\nfor art,name in [\\('UNDEAD','Graveguard'\\),\\('HELL','Demon'\\),\\('KEEPER','Judicator'\\),\\('FEY','Duskweaver'\\),\\('AVALON','Valor'\\)]:\n n = name.lower\\(\\)\n a\\(f'{n}-helmet',f'{name} Helmet',A,f'T{{t}}_HEAD_PLATE_{art}'\\)\n a\\(f'{n}-armor',f'{name} Armor',A,f'T{{t}}_ARMOR_PLATE_{art}'\\)\n a\\(f'{n}-boots',f'{name} Boots',A,f'T{{t}}_SHOES_PLATE_{art}'\\)\n\n# ─── Leather Armor ────────────────────────────────────────────────────────────\nc\\('--- Leather Armor — sets 1-3 \\(Leather only\\) --------------------------------'\\)\nfor sn,sl in [\\('SET1','Mercenary'\\),\\('SET2','Hunter'\\),\\('SET3','Assassin'\\)]:\n n = sl.lower\\(\\)\n a\\(f'{n}-hood',f'{sl} Hood',A,f'T{{t}}_HEAD_LEATHER_{sn}'\\)\n a\\(f'{n}-jacket',f'{sl} Jacket',A,f'T{{t}}_ARMOR_LEATHER_{sn}'\\)\n a\\(f'{n}-shoes',f'{sl} Shoes',A,f'T{{t}}_SHOES_LEATHER_{sn}'\\)\n\nc\\('--- Leather Armor — artifact variants \\(Leather + artifact\\) -----------------'\\)\nfor art,name in [\\('MORGANA','Hellion'\\),\\('HELL','Specter'\\),\\('UNDEAD','Stalker'\\),\\('FEY','Mistwalker'\\),\\('AVALON','Tenacity'\\)]:\n n = name.lower\\(\\)\n a\\(f'{n}-hood',f'{name} Hood',A,f'T{{t}}_HEAD_LEATHER_{art}'\\)\n a\\(f'{n}-jacket',f'{name} Jacket',A,f'T{{t}}_ARMOR_LEATHER_{art}'\\)\n a\\(f'{n}-shoes',f'{name} Shoes',A,f'T{{t}}_SHOES_LEATHER_{art}'\\)\n\n# ─── Cloth Armor ──────────────────────────────────────────────────────────────\nc\\('--- Cloth Armor — sets 1-3 \\(Cloth only\\) ------------------------------------'\\)\nfor sn,sl,slots in [\\('SET1','Scholar',\\('Cowl','Robe','Sandals'\\)\\),\\('SET2','Cleric',\\('Cowl','Robe','Sandals'\\)\\),\\('SET3','Mage',\\('Cowl','Robe','Sandals'\\)\\)]:\n n = sl.lower\\(\\)\n a\\(f'{n}-cowl',f'{sl} {slots[0]}',A,f'T{{t}}_HEAD_CLOTH_{sn}'\\)\n a\\(f'{n}-robe',f'{sl} {slots[1]}',A,f'T{{t}}_ARMOR_CLOTH_{sn}'\\)\n a\\(f'{n}-sandals',f'{sl} {slots[2]}',A,f'T{{t}}_SHOES_CLOTH_{sn}'\\)\n\nc\\('--- Cloth Armor — artifact variants \\(Cloth + artifact\\) ---------------------'\\)\nfor art,name in [\\('KEEPER','Druid'\\),\\('HELL','Fiend'\\),\\('MORGANA','Cultist'\\),\\('FEY','Feyscale'\\),\\('AVALON','Purity'\\)]:\n n = name.lower\\(\\)\n a\\(f'{n}-cowl',f'{name} Cowl',A,f'T{{t}}_HEAD_CLOTH_{art}'\\)\n a\\(f'{n}-robe',f'{name} Robe',A,f'T{{t}}_ARMOR_CLOTH_{art}'\\)\n a\\(f'{n}-sandals',f'{name} Sandals',A,f'T{{t}}_SHOES_CLOTH_{art}'\\)\n\n# ─── Gathering ────────────────────────────────────────────────────────────────\nc\\('--- Gathering Tools \\(Planks + Bars\\) ----------------------------------------'\\)\na\\('pickaxe','Pickaxe',G,'T{t}_2H_TOOL_PICK'\\)\na\\('sickle','Sickle',G,'T{t}_2H_TOOL_SICKLE'\\)\na\\('skinning-knife','Skinning Knife',G,'T{t}_2H_TOOL_KNIFE'\\)\na\\('lumberjack-axe','Lumberjack Axe',G,'T{t}_2H_TOOL_AXE'\\)\na\\('stonesmasher','Stonesmasher',G,'T{t}_2H_TOOL_HAMMER'\\)\n\n# ─── Food ─────────────────────────────────────────────────────────────────────\nc\\('--- Food \\(enchanted: false\\) ------------------------------------------------'\\)\na\\('seaweed-salad','Seaweed Salad',F,'T{t}_MEAL_SALAD', False\\)\na\\('stew','Stew',F,'T{t}_MEAL_STEW', False\\)\na\\('sandwich','Sandwich',F,'T{t}_MEAL_SANDWICH', False\\)\n\n# ─── Potions ──────────────────────────────────────────────────────────────────\nc\\('--- Potions \\(enchanted: false\\) ---------------------------------------------'\\)\na\\('healing-potion','Healing Potion',P,'T{t}_POTION_HEAL', False\\)\na\\('energy-potion','Energy Potion',P,'T{t}_POTION_ENERGY', False\\)\na\\('cooldown-potion','Cooldown Potion',P,'T{t}_POTION_COOLDOWN', False\\)\n\n# ─── Write recipes.json ───────────────────────────────────────────────────────\nwith open\\(OUT_RECIPES, 'w'\\) as f:\n json.dump\\(recipes, f, indent=2, ensure_ascii=False\\)\n f.write\\('\\\\n'\\)\n\nrecipe_count = sum\\(1 for r in recipes if 'outputId' in r\\)\nprint\\(f\"Wrote {recipe_count} recipes to {OUT_RECIPES}\", file=sys.stderr\\)\n\n# ─── Build categories.json ────────────────────────────────────────────────────\ncategories = [\n {\"label\": \"Weapons\", \"children\": [\n {\"label\": \"Bow\", \"children\": [\n {\"label\": \"Bow\"}, {\"label\": \"Longbow\"}, {\"label\": \"Warbow\"},\n {\"label\": \"Whispering Bow\"}, {\"label\": \"Wailing Bow\"},\n {\"label\": \"Bow of Badon\"}, {\"label\": \"Mistpiercer\"}, {\"label\": \"Skystrider Bow\"}\n ]},\n {\"label\": \"Sword\", \"children\": [\n {\"label\": \"Broadsword\"}, {\"label\": \"Claymore\"}, {\"label\": \"Dual Swords\"}\n ]},\n {\"label\": \"Axe\", \"children\": [\n {\"label\": \"Battleaxe\"}, {\"label\": \"Great Axe\"}, {\"label\": \"Halberd\"}\n ]},\n {\"label\": \"Mace\", \"children\": [\n {\"label\": \"Mace\"}, {\"label\": \"Hammer\"}, {\"label\": \"Great Hammer\"}, {\"label\": \"Polehammer\"}\n ]},\n {\"label\": \"Crossbow\", \"children\": [\n {\"label\": \"Light Crossbow\"}, {\"label\": \"Crossbow\"}, {\"label\": \"Heavy Crossbow\"}\n ]},\n {\"label\": \"Dagger\", \"children\": [\n {\"label\": \"Dagger\"}, {\"label\": \"Dagger Pair\"}, {\"label\": \"Claws\"}\n ]},\n {\"label\": \"Spear\", \"children\": [\n {\"label\": \"Spear\"}, {\"label\": \"Pike\"}, {\"label\": \"Glaive\"}\n ]},\n {\"label\": \"Quarterstaff\", \"children\": [\n {\"label\": \"Quarterstaff\"}, {\"label\": \"Iron-clad Staff\"}, {\"label\": \"Double Bladed Staff\"}\n ]},\n {\"label\": \"Fire Staff\", \"children\": [\n {\"label\": \"Fire Staff\"}, {\"label\": \"Great Fire Staff\"}, {\"label\": \"Inferno Staff\"}\n ]},\n {\"label\": \"Arcane Staff\", \"children\": [\n {\"label\": \"Arcane Staff\"}, {\"label\": \"Great Arcane Staff\"}, {\"label\": \"Enigmatic Staff\"}\n ]},\n {\"label\": \"Cursed Staff\", \"children\": [\n {\"label\": \"Cursed Staff\"}, {\"label\": \"Great Cursed Staff\"}, {\"label\": \"Demonic Staff\"}\n ]},\n {\"label\": \"Frost Staff\", \"children\": [\n {\"label\": \"Frost Staff\"}, {\"label\": \"Great Frost Staff\"}, {\"label\": \"Glacial Staff\"}\n ]},\n {\"label\": \"Holy Staff\", \"children\": [\n {\"label\": \"Holy Staff\"}, {\"label\": \"Great Holy Staff\"}, {\"label\": \"Divine Staff\"}\n ]},\n {\"label\": \"Nature Staff\", \"children\": [\n {\"label\": \"Nature Staff\"}, {\"label\": \"Great Nature Staff\"}, {\"label\": \"Wild Staff\"}\n ]},\n {\"label\": \"Shapeshifter\", \"children\": [\n {\"label\": \"Shapeshifter Claws\"}, {\"label\": \"Shapeshifter Spear\"}, {\"label\": \"Shapeshifter Staff\"}\n ]},\n {\"label\": \"War Gloves\", \"children\": [\n {\"label\": \"Brawler Gloves\"}, {\"label\": \"Battle Bracers\"}, {\"label\": \"Ravenstrike Cestus\"}\n ]},\n {\"label\": \"Off-hand\", \"children\": [\n {\"label\": \"Shield\"}, {\"label\": \"Torch\"}, {\"label\": \"Tome of Spells\"}\n ]}\n ]},\n {\"label\": \"Armor\", \"children\": [\n {\"label\": \"Head\", \"children\": [\n {\"label\": \"Plate\", \"children\": [\n {\"label\": \"Soldier Helmet\"}, {\"label\": \"Knight Helmet\"}, {\"label\": \"Guardian Helmet\"},\n {\"label\": \"Graveguard Helmet\"}, {\"label\": \"Demon Helmet\"}, {\"label\": \"Judicator Helmet\"},\n {\"label\": \"Duskweaver Helmet\"}, {\"label\": \"Valor Helmet\"}\n ]},\n {\"label\": \"Leather\", \"children\": [\n {\"label\": \"Mercenary Hood\"}, {\"label\": \"Hunter Hood\"}, {\"label\": \"Assassin Hood\"},\n {\"label\": \"Hellion Hood\"}, {\"label\": \"Specter Hood\"}, {\"label\": \"Stalker Hood\"},\n {\"label\": \"Mistwalker Hood\"}, {\"label\": \"Tenacity Hood\"}\n ]},\n {\"label\": \"Cloth\", \"children\": [\n {\"label\": \"Scholar Cowl\"}, {\"label\": \"Cleric Cowl\"}, {\"label\": \"Mage Cowl\"},\n {\"label\": \"Druid Cowl\"}, {\"label\": \"Fiend Cowl\"}, {\"label\": \"Cultist Cowl\"},\n {\"label\": \"Feyscale Cowl\"}, {\"label\": \"Purity Cowl\"}\n ]}\n ]},\n {\"label\": \"Chest\", \"children\": [\n {\"label\": \"Plate\", \"children\": [\n {\"label\": \"Soldier Armor\"}, {\"label\": \"Knight Armor\"}, {\"label\": \"Guardian Armor\"},\n {\"label\": \"Graveguard Armor\"}, {\"label\": \"Demon Armor\"}, {\"label\": \"Judicator Armor\"},\n {\"label\": \"Duskweaver Armor\"}, {\"label\": \"Valor Armor\"}\n ]},\n {\"label\": \"Leather\", \"children\": [\n {\"label\": \"Mercenary Jacket\"}, {\"label\": \"Hunter Jacket\"}, {\"label\": \"Assassin Jacket\"},\n {\"label\": \"Hellion Jacket\"}, {\"label\": \"Specter Jacket\"}, {\"label\": \"Stalker Jacket\"},\n {\"label\": \"Mistwalker Jacket\"}, {\"label\": \"Tenacity Jacket\"}\n ]},\n {\"label\": \"Cloth\", \"children\": [\n {\"label\": \"Scholar Robe\"}, {\"label\": \"Cleric Robe\"}, {\"label\": \"Mage Robe\"},\n {\"label\": \"Druid Robe\"}, {\"label\": \"Fiend Robe\"}, {\"label\": \"Cultist Robe\"},\n {\"label\": \"Feyscale Robe\"}, {\"label\": \"Purity Robe\"}\n ]}\n ]},\n {\"label\": \"Boots\", \"children\": [\n {\"label\": \"Plate\", \"children\": [\n {\"label\": \"Soldier Boots\"}, {\"label\": \"Knight Boots\"}, {\"label\": \"Guardian Boots\"},\n {\"label\": \"Graveguard Boots\"}, {\"label\": \"Demon Boots\"}, {\"label\": \"Judicator Boots\"},\n {\"label\": \"Duskweaver Boots\"}, {\"label\": \"Valor Boots\"}\n ]},\n {\"label\": \"Leather\", \"children\": [\n {\"label\": \"Mercenary Shoes\"}, {\"label\": \"Hunter Shoes\"}, {\"label\": \"Assassin Shoes\"},\n {\"label\": \"Hellion Shoes\"}, {\"label\": \"Specter Shoes\"}, {\"label\": \"Stalker Shoes\"},\n {\"label\": \"Mistwalker Shoes\"}, {\"label\": \"Tenacity Shoes\"}\n ]},\n {\"label\": \"Cloth\", \"children\": [\n {\"label\": \"Scholar Sandals\"}, {\"label\": \"Cleric Sandals\"}, {\"label\": \"Mage Sandals\"},\n {\"label\": \"Druid Sandals\"}, {\"label\": \"Fiend Sandals\"}, {\"label\": \"Cultist Sandals\"},\n {\"label\": \"Feyscale Sandals\"}, {\"label\": \"Purity Sandals\"}\n ]}\n ]}\n ]},\n {\"label\": \"Gathering\", \"children\": [\n {\"label\": \"Pickaxe\"}, {\"label\": \"Sickle\"}, {\"label\": \"Skinning Knife\"},\n {\"label\": \"Lumberjack Axe\"}, {\"label\": \"Stonesmasher\"}\n ]},\n {\"label\": \"Food\", \"children\": [\n {\"label\": \"Seaweed Salad\"}, {\"label\": \"Stew\"}, {\"label\": \"Sandwich\"}\n ]},\n {\"label\": \"Potions\", \"children\": [\n {\"label\": \"Healing Potion\"}, {\"label\": \"Energy Potion\"}, {\"label\": \"Cooldown Potion\"}\n ]}\n]\n\nwith open\\(OUT_CATEGORIES, 'w'\\) as f:\n json.dump\\(categories, f, indent=2, ensure_ascii=False\\)\n f.write\\('\\\\n'\\)\n\nprint\\(f\"Wrote categories to {OUT_CATEGORIES}\", file=sys.stderr\\)\nPYEOF\necho \"Script written\")", + "Bash(do:*)" + ] + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..64fe55b --- /dev/null +++ b/.gitignore @@ -0,0 +1,33 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +# Node modules +node_modules + +# Build output +dist +dist-ssr +*.local +temp/ + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +# Environment files +env +env.local +env.*.local diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..a7cea0b --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,3 @@ +{ + "recommendations": ["Vue.volar"] +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..33895ab --- /dev/null +++ b/README.md @@ -0,0 +1,5 @@ +# Vue 3 + TypeScript + Vite + +This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 ` + + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..884971c --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2376 @@ +{ + "name": "albion-crafting-calc", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "albion-crafting-calc", + "version": "0.0.0", + "dependencies": { + "vue": "^3.5.25" + }, + "devDependencies": { + "@types/node": "^24.10.1", + "@vitejs/plugin-vue": "^6.0.2", + "@vue/tsconfig": "^0.8.1", + "autoprefixer": "^10.4.27", + "postcss": "^8.5.6", + "tailwindcss": "^3.4.19", + "typescript": "~5.9.3", + "vite": "^7.3.1", + "vue-tsc": "^3.1.5" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", + "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", + "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", + "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", + "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.2", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.2.tgz", + "integrity": "sha512-izyXV/v+cHiRfozX62W9htOAvwMo4/bXKDrQ+vom1L1qRuexPock/7VZDAhnpHCLNejd3NJ6hiab+tO0D44Rgw==", + "dev": true + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", + "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", + "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", + "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", + "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", + "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", + "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", + "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", + "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", + "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", + "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", + "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", + "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", + "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", + "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", + "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", + "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", + "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", + "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", + "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", + "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", + "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", + "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", + "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", + "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", + "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true + }, + "node_modules/@types/node": { + "version": "24.10.14", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.14.tgz", + "integrity": "sha512-OowOUbD1lBCOFIPOZ8xnMIhgqA4sCutMiYOmPHL1PTLt5+y1XA+g2+yC9OOyz8p+deMZqPZLxfMjYIfrKsPeFg==", + "dev": true, + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@vitejs/plugin-vue": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.4.tgz", + "integrity": "sha512-uM5iXipgYIn13UUQCZNdWkYk+sysBeA97d5mHsAoAt1u/wpN3+zxOmsVJWosuzX+IMGRzeYUNytztrYznboIkQ==", + "dev": true, + "dependencies": { + "@rolldown/pluginutils": "1.0.0-rc.2" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0", + "vue": "^3.2.25" + } + }, + "node_modules/@volar/language-core": { + "version": "2.4.28", + "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.28.tgz", + "integrity": "sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ==", + "dev": true, + "dependencies": { + "@volar/source-map": "2.4.28" + } + }, + "node_modules/@volar/source-map": { + "version": "2.4.28", + "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.28.tgz", + "integrity": "sha512-yX2BDBqJkRXfKw8my8VarTyjv48QwxdJtvRgUpNE5erCsgEUdI2DsLbpa+rOQVAJYshY99szEcRDmyHbF10ggQ==", + "dev": true + }, + "node_modules/@volar/typescript": { + "version": "2.4.28", + "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.28.tgz", + "integrity": "sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw==", + "dev": true, + "dependencies": { + "@volar/language-core": "2.4.28", + "path-browserify": "^1.0.1", + "vscode-uri": "^3.0.8" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.29", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.29.tgz", + "integrity": "sha512-cuzPhD8fwRHk8IGfmYaR4eEe4cAyJEL66Ove/WZL7yWNL134nqLddSLwNRIsFlnnW1kK+p8Ck3viFnC0chXCXw==", + "dependencies": { + "@babel/parser": "^7.29.0", + "@vue/shared": "3.5.29", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.29", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.29.tgz", + "integrity": "sha512-n0G5o7R3uBVmVxjTIYcz7ovr8sy7QObFG8OQJ3xGCDNhbG60biP/P5KnyY8NLd81OuT1WJflG7N4KWYHaeeaIg==", + "dependencies": { + "@vue/compiler-core": "3.5.29", + "@vue/shared": "3.5.29" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.29", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.29.tgz", + "integrity": "sha512-oJZhN5XJs35Gzr50E82jg2cYdZQ78wEwvRO6Y63TvLVTc+6xICzJHP1UIecdSPPYIbkautNBanDiWYa64QSFIA==", + "dependencies": { + "@babel/parser": "^7.29.0", + "@vue/compiler-core": "3.5.29", + "@vue/compiler-dom": "3.5.29", + "@vue/compiler-ssr": "3.5.29", + "@vue/shared": "3.5.29", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.6", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.29", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.29.tgz", + "integrity": "sha512-Y/ARJZE6fpjzL5GH/phJmsFwx3g6t2KmHKHx5q+MLl2kencADKIrhH5MLF6HHpRMmlRAYBRSvv347Mepf1zVNw==", + "dependencies": { + "@vue/compiler-dom": "3.5.29", + "@vue/shared": "3.5.29" + } + }, + "node_modules/@vue/language-core": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-3.2.5.tgz", + "integrity": "sha512-d3OIxN/+KRedeM5wQ6H6NIpwS3P5gC9nmyaHgBk+rO6dIsjY+tOh4UlPpiZbAh3YtLdCGEX4M16RmsBqPmJV+g==", + "dev": true, + "dependencies": { + "@volar/language-core": "2.4.28", + "@vue/compiler-dom": "^3.5.0", + "@vue/shared": "^3.5.0", + "alien-signals": "^3.0.0", + "muggle-string": "^0.4.1", + "path-browserify": "^1.0.1", + "picomatch": "^4.0.2" + } + }, + "node_modules/@vue/reactivity": { + "version": "3.5.29", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.29.tgz", + "integrity": "sha512-zcrANcrRdcLtmGZETBxWqIkoQei8HaFpZWx/GHKxx79JZsiZ8j1du0VUJtu4eJjgFvU/iKL5lRXFXksVmI+5DA==", + "dependencies": { + "@vue/shared": "3.5.29" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.29", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.29.tgz", + "integrity": "sha512-8DpW2QfdwIWOLqtsNcds4s+QgwSaHSJY/SUe04LptianUQ/0xi6KVsu/pYVh+HO3NTVvVJjIPL2t6GdeKbS4Lg==", + "dependencies": { + "@vue/reactivity": "3.5.29", + "@vue/shared": "3.5.29" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.29", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.29.tgz", + "integrity": "sha512-AHvvJEtcY9tw/uk+s/YRLSlxxQnqnAkjqvK25ZiM4CllCZWzElRAoQnCM42m9AHRLNJ6oe2kC5DCgD4AUdlvXg==", + "dependencies": { + "@vue/reactivity": "3.5.29", + "@vue/runtime-core": "3.5.29", + "@vue/shared": "3.5.29", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.29", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.29.tgz", + "integrity": "sha512-G/1k6WK5MusLlbxSE2YTcqAAezS+VuwHhOvLx2KnQU7G2zCH6KIb+5Wyt6UjMq7a3qPzNEjJXs1hvAxDclQH+g==", + "dependencies": { + "@vue/compiler-ssr": "3.5.29", + "@vue/shared": "3.5.29" + }, + "peerDependencies": { + "vue": "3.5.29" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.29", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.29.tgz", + "integrity": "sha512-w7SR0A5zyRByL9XUkCfdLs7t9XOHUyJ67qPGQjOou3p6GvBeBW+AVjUUmlxtZ4PIYaRvE+1LmK44O4uajlZwcg==" + }, + "node_modules/@vue/tsconfig": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@vue/tsconfig/-/tsconfig-0.8.1.tgz", + "integrity": "sha512-aK7feIWPXFSUhsCP9PFqPyFOcz4ENkb8hZ2pneL6m2UjCkccvaOhC/5KCKluuBufvp2KzkbdA2W2pk20vLzu3g==", + "dev": true, + "peerDependencies": { + "typescript": "5.x", + "vue": "^3.4.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + }, + "vue": { + "optional": true + } + } + }, + "node_modules/alien-signals": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-3.1.2.tgz", + "integrity": "sha512-d9dYqZTS90WLiU0I5c6DHj/HcKkF8ZyGN3G5x8wSbslulz70KOxaqCT0hQCo9KOyhVqzqGojvNdJXoTumZOtcw==", + "dev": true + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true + }, + "node_modules/autoprefixer": { + "version": "10.4.27", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.27.tgz", + "integrity": "sha512-NP9APE+tO+LuJGn7/9+cohklunJsXWiaWEfV3si4Gi/XHDwVNgkwr1J3RQYFIvPy76GmJ9/bW8vyoU1LcxwKHA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "browserslist": "^4.28.1", + "caniuse-lite": "^1.0.30001774", + "fraction.js": "^5.3.4", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz", + "integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==", + "dev": true, + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001774", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001774.tgz", + "integrity": "sha512-DDdwPGz99nmIEv216hKSgLD+D4ikHQHjBC/seF98N9CPqRX4M5mSxT9eTV6oyisnJcuzxtZy4n17yKKQYmYQOA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==" + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true + }, + "node_modules/electron-to-chromium": { + "version": "1.5.302", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.302.tgz", + "integrity": "sha512-sM6HAN2LyK82IyPBpznDRqlTQAtuSaO+ShzFiWTvoMJLHyZ+Y39r8VMfHzwbU8MVBzQ4Wdn85+wlZl2TLGIlwg==", + "dev": true + }, + "node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/esbuild": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "dev": true, + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "dev": true, + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/muggle-string": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz", + "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==", + "dev": true + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", + "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/readdirp/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", + "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", + "dev": true, + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.59.0", + "@rollup/rollup-android-arm64": "4.59.0", + "@rollup/rollup-darwin-arm64": "4.59.0", + "@rollup/rollup-darwin-x64": "4.59.0", + "@rollup/rollup-freebsd-arm64": "4.59.0", + "@rollup/rollup-freebsd-x64": "4.59.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", + "@rollup/rollup-linux-arm-musleabihf": "4.59.0", + "@rollup/rollup-linux-arm64-gnu": "4.59.0", + "@rollup/rollup-linux-arm64-musl": "4.59.0", + "@rollup/rollup-linux-loong64-gnu": "4.59.0", + "@rollup/rollup-linux-loong64-musl": "4.59.0", + "@rollup/rollup-linux-ppc64-gnu": "4.59.0", + "@rollup/rollup-linux-ppc64-musl": "4.59.0", + "@rollup/rollup-linux-riscv64-gnu": "4.59.0", + "@rollup/rollup-linux-riscv64-musl": "4.59.0", + "@rollup/rollup-linux-s390x-gnu": "4.59.0", + "@rollup/rollup-linux-x64-gnu": "4.59.0", + "@rollup/rollup-linux-x64-musl": "4.59.0", + "@rollup/rollup-openbsd-x64": "4.59.0", + "@rollup/rollup-openharmony-arm64": "4.59.0", + "@rollup/rollup-win32-arm64-msvc": "4.59.0", + "@rollup/rollup-win32-ia32-msvc": "4.59.0", + "@rollup/rollup-win32-x64-gnu": "4.59.0", + "@rollup/rollup-win32-x64-msvc": "4.59.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.19", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", + "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", + "dev": true, + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.7", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "devOptional": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true + }, + "node_modules/vite": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "dev": true, + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vscode-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz", + "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==", + "dev": true + }, + "node_modules/vue": { + "version": "3.5.29", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.29.tgz", + "integrity": "sha512-BZqN4Ze6mDQVNAni0IHeMJ5mwr8VAJ3MQC9FmprRhcBYENw+wOAAjRj8jfmN6FLl0j96OXbR+CjWhmAmM+QGnA==", + "dependencies": { + "@vue/compiler-dom": "3.5.29", + "@vue/compiler-sfc": "3.5.29", + "@vue/runtime-dom": "3.5.29", + "@vue/server-renderer": "3.5.29", + "@vue/shared": "3.5.29" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-tsc": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-3.2.5.tgz", + "integrity": "sha512-/htfTCMluQ+P2FISGAooul8kO4JMheOTCbCy4M6dYnYYjqLe3BExZudAua6MSIKSFYQtFOYAll7XobYwcpokGA==", + "dev": true, + "dependencies": { + "@volar/typescript": "2.4.28", + "@vue/language-core": "3.2.5" + }, + "bin": { + "vue-tsc": "bin/vue-tsc.js" + }, + "peerDependencies": { + "typescript": ">=5.0.0" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..7f0ae8b --- /dev/null +++ b/package.json @@ -0,0 +1,25 @@ +{ + "name": "albion-crafting-calc", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vue-tsc -b && vite build", + "preview": "vite preview" + }, + "dependencies": { + "vue": "^3.5.25" + }, + "devDependencies": { + "@types/node": "^24.10.1", + "@vitejs/plugin-vue": "^6.0.2", + "@vue/tsconfig": "^0.8.1", + "autoprefixer": "^10.4.27", + "postcss": "^8.5.6", + "tailwindcss": "^3.4.19", + "typescript": "~5.9.3", + "vite": "^7.3.1", + "vue-tsc": "^3.1.5" + } +} diff --git a/postcss.config.ts b/postcss.config.ts new file mode 100644 index 0000000..2e7af2b --- /dev/null +++ b/postcss.config.ts @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/public/vite.svg b/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/App.vue b/src/App.vue new file mode 100644 index 0000000..3e0d478 --- /dev/null +++ b/src/App.vue @@ -0,0 +1,91 @@ + + + diff --git a/src/assets/vue.svg b/src/assets/vue.svg new file mode 100644 index 0000000..770e9d3 --- /dev/null +++ b/src/assets/vue.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/components/HelloWorld.vue b/src/components/HelloWorld.vue new file mode 100644 index 0000000..b58e52b --- /dev/null +++ b/src/components/HelloWorld.vue @@ -0,0 +1,41 @@ + + + + + diff --git a/src/components/PriceCell.vue b/src/components/PriceCell.vue new file mode 100644 index 0000000..a8d9cc9 --- /dev/null +++ b/src/components/PriceCell.vue @@ -0,0 +1,111 @@ + + + diff --git a/src/components/filters/CategoryFilter.vue b/src/components/filters/CategoryFilter.vue new file mode 100644 index 0000000..96d4052 --- /dev/null +++ b/src/components/filters/CategoryFilter.vue @@ -0,0 +1,31 @@ + + + diff --git a/src/components/filters/CityFilter.vue b/src/components/filters/CityFilter.vue new file mode 100644 index 0000000..9ea2562 --- /dev/null +++ b/src/components/filters/CityFilter.vue @@ -0,0 +1,25 @@ + + + diff --git a/src/components/filters/FilterBar.vue b/src/components/filters/FilterBar.vue new file mode 100644 index 0000000..a283c93 --- /dev/null +++ b/src/components/filters/FilterBar.vue @@ -0,0 +1,328 @@ + + + diff --git a/src/components/filters/FilterPanel.vue b/src/components/filters/FilterPanel.vue new file mode 100644 index 0000000..b2e00b0 --- /dev/null +++ b/src/components/filters/FilterPanel.vue @@ -0,0 +1,90 @@ + + + diff --git a/src/components/filters/TaxInput.vue b/src/components/filters/TaxInput.vue new file mode 100644 index 0000000..33e0360 --- /dev/null +++ b/src/components/filters/TaxInput.vue @@ -0,0 +1,27 @@ + + + diff --git a/src/components/filters/TierFilter.vue b/src/components/filters/TierFilter.vue new file mode 100644 index 0000000..37823c2 --- /dev/null +++ b/src/components/filters/TierFilter.vue @@ -0,0 +1,40 @@ + + + diff --git a/src/components/layout/AppHeader.vue b/src/components/layout/AppHeader.vue new file mode 100644 index 0000000..f8c6ffb --- /dev/null +++ b/src/components/layout/AppHeader.vue @@ -0,0 +1,20 @@ + + + diff --git a/src/components/table/ProfitRow.vue b/src/components/table/ProfitRow.vue new file mode 100644 index 0000000..e5e55cd --- /dev/null +++ b/src/components/table/ProfitRow.vue @@ -0,0 +1,294 @@ + + + diff --git a/src/components/table/ProfitTable.vue b/src/components/table/ProfitTable.vue new file mode 100644 index 0000000..61e5677 --- /dev/null +++ b/src/components/table/ProfitTable.vue @@ -0,0 +1,47 @@ + + + diff --git a/src/components/table/TableHeader.vue b/src/components/table/TableHeader.vue new file mode 100644 index 0000000..bb822e6 --- /dev/null +++ b/src/components/table/TableHeader.vue @@ -0,0 +1,42 @@ + + + diff --git a/src/components/ui/ErrorBanner.vue b/src/components/ui/ErrorBanner.vue new file mode 100644 index 0000000..70faf1c --- /dev/null +++ b/src/components/ui/ErrorBanner.vue @@ -0,0 +1,28 @@ + + + diff --git a/src/components/ui/LastUpdated.vue b/src/components/ui/LastUpdated.vue new file mode 100644 index 0000000..2bf220e --- /dev/null +++ b/src/components/ui/LastUpdated.vue @@ -0,0 +1,32 @@ + + + diff --git a/src/components/ui/LoadingSpinner.vue b/src/components/ui/LoadingSpinner.vue new file mode 100644 index 0000000..804bdb8 --- /dev/null +++ b/src/components/ui/LoadingSpinner.vue @@ -0,0 +1,14 @@ + + + diff --git a/src/components/ui/RefreshButton.vue b/src/components/ui/RefreshButton.vue new file mode 100644 index 0000000..20a5ba2 --- /dev/null +++ b/src/components/ui/RefreshButton.vue @@ -0,0 +1,48 @@ + + + diff --git a/src/composables/useAlbionPrices.ts b/src/composables/useAlbionPrices.ts new file mode 100644 index 0000000..372157a --- /dev/null +++ b/src/composables/useAlbionPrices.ts @@ -0,0 +1,137 @@ +import { ref, readonly } from 'vue' +import type { AlbionPriceEntry, AlbionCity, ManualPriceEntry, ManualPriceCache } from '../types/api' + +// ─── Override duration ──────────────────────────────────────────────────────── + +export const OVERRIDE_DURATION_OPTIONS: { label: string; ms: number }[] = [ + { label: '30 min', ms: 30 * 60 * 1000 }, + { label: '1 hour', ms: 60 * 60 * 1000 }, + { label: '2 hours', ms: 2 * 60 * 60 * 1000 }, + { label: '4 hours', ms: 4 * 60 * 60 * 1000 }, + { label: '8 hours', ms: 8 * 60 * 60 * 1000 }, + { label: '24 hours',ms: 24 * 60 * 60 * 1000 }, +] + +const DURATION_STORAGE_KEY = 'albion-override-duration' +const MANUAL_STORAGE_KEY = 'albion-manual-prices' + +function loadOverrideDuration(): number { + try { + const raw = localStorage.getItem(DURATION_STORAGE_KEY) + if (raw) { + const ms = Number(raw) + if (OVERRIDE_DURATION_OPTIONS.some(o => o.ms === ms)) return ms + } + } catch { /* ignore */ } + return 24 * 60 * 60 * 1000 // default 24 hours +} + +function loadManualPricesFromStorage(): ManualPriceCache { + try { + const raw = localStorage.getItem(MANUAL_STORAGE_KEY) + if (raw) { + const obj = JSON.parse(raw) as Record + return new Map(Object.entries(obj)) + } + } catch { /* ignore */ } + return new Map() +} + +function saveManualPricesToStorage(map: ManualPriceCache): void { + try { + const obj: Record = {} + for (const [key, val] of map) obj[key] = val + localStorage.setItem(MANUAL_STORAGE_KEY, JSON.stringify(obj)) + } catch { /* ignore */ } +} + +// ─── Module-level singleton state ──────────────────────────────────────────── + +const manualPrices = ref(loadManualPricesFromStorage()) +const overrideDurationMs = ref(loadOverrideDuration()) + +// ─── Helpers ────────────────────────────────────────────────────────────────── + +function cacheKey(itemId: string, city: string): string { + return `${itemId}::${city}` +} + +function isManualExpired(entry: ManualPriceEntry): boolean { + return Date.now() - new Date(entry.editedAt).getTime() > overrideDurationMs.value +} + +function synthesizeManualEntry( + entry: ManualPriceEntry, + itemId: string, + city: AlbionCity, +): AlbionPriceEntry { + return { + item_id: itemId, + city, + quality: 1, + sell_price_min: entry.sell_price_min, + sell_price_min_date: entry.editedAt, + sell_price_max: entry.sell_price_min, + buy_price_min: 0, + buy_price_max: 0, + } +} + +// ─── Price accessors ────────────────────────────────────────────────────────── + +function getPrice(itemId: string, city: AlbionCity): AlbionPriceEntry | null { + const key = cacheKey(itemId, city) + const manual = manualPrices.value.get(key) + if (manual && !isManualExpired(manual)) { + return synthesizeManualEntry(manual, itemId, city) + } + return null +} + +// ─── Manual price management ────────────────────────────────────────────────── + +function setManualPrice(itemId: string, city: AlbionCity, price: number): void { + const key = cacheKey(itemId, city) + const next = new Map(manualPrices.value) + next.set(key, { sell_price_min: price, editedAt: new Date().toISOString() }) + manualPrices.value = next + saveManualPricesToStorage(next) +} + +function clearManualPrice(itemId: string, city: AlbionCity): void { + const key = cacheKey(itemId, city) + if (!manualPrices.value.has(key)) return + const next = new Map(manualPrices.value) + next.delete(key) + manualPrices.value = next + saveManualPricesToStorage(next) +} + +function isManualPrice(itemId: string, city: AlbionCity): boolean { + const entry = manualPrices.value.get(cacheKey(itemId, city)) + return !!entry && !isManualExpired(entry) +} + +function getManualEntry(itemId: string, city: AlbionCity): ManualPriceEntry | undefined { + const entry = manualPrices.value.get(cacheKey(itemId, city)) + return entry && !isManualExpired(entry) ? entry : undefined +} + +function setOverrideDuration(ms: number): void { + overrideDurationMs.value = ms + try { localStorage.setItem(DURATION_STORAGE_KEY, String(ms)) } catch { /* ignore */ } +} + +// ─── Public API ─────────────────────────────────────────────────────────────── + +export function useAlbionPrices() { + return { + overrideDurationMs: readonly(overrideDurationMs), + getPrice, + setManualPrice, + clearManualPrice, + isManualPrice, + getManualEntry, + setOverrideDuration, + } +} diff --git a/src/composables/useAutoRefresh.ts b/src/composables/useAutoRefresh.ts new file mode 100644 index 0000000..aab483d --- /dev/null +++ b/src/composables/useAutoRefresh.ts @@ -0,0 +1,55 @@ +import { ref, watch, onUnmounted } from 'vue' +import type { Ref } from 'vue' +import { AUTO_REFRESH_INTERVAL_S } from '../data/constants' + +export function useAutoRefresh(fetchFn: () => Promise, enabled: Ref) { + const countdown = ref(AUTO_REFRESH_INTERVAL_S) + + let intervalId: ReturnType | null = null + let tickId: ReturnType | null = null + + function startRefresh() { + stopRefresh() + countdown.value = AUTO_REFRESH_INTERVAL_S + + // Countdown tick every second + tickId = setInterval(() => { + countdown.value = Math.max(0, countdown.value - 1) + }, 1000) + + // Fetch every interval + intervalId = setInterval(async () => { + await fetchFn() + countdown.value = AUTO_REFRESH_INTERVAL_S + }, AUTO_REFRESH_INTERVAL_S * 1000) + } + + function stopRefresh() { + if (intervalId !== null) { + clearInterval(intervalId) + intervalId = null + } + if (tickId !== null) { + clearInterval(tickId) + tickId = null + } + } + + watch( + enabled, + (val) => { + if (val) { + startRefresh() + } else { + stopRefresh() + } + }, + { immediate: true } + ) + + onUnmounted(() => { + stopRefresh() + }) + + return { countdown } +} diff --git a/src/composables/useCraftingProfit.ts b/src/composables/useCraftingProfit.ts new file mode 100644 index 0000000..5aa0fa8 --- /dev/null +++ b/src/composables/useCraftingProfit.ts @@ -0,0 +1,131 @@ +import { computed } from 'vue' +import type { Ref } from 'vue' +import type { CraftingRecipe, ProfitResult, IngredientBreakdown, SortState } from '../types/crafting' +import type { FilterState } from '../types/filters' +import { useAlbionPrices } from './useAlbionPrices' +import { formatItemId } from '../utils/formatting' + +// Returns 0=basic, 1=artifact, 2=avalon, 3=crystal +function variantRank(outputItemId: string): number { + const id = outputItemId.replace(/@\d$/, '') // strip enchantment suffix + if (id.endsWith('_AVALON')) return 2 + if (id.endsWith('_CRYSTAL')) return 3 + if (/_SET[123]$/.test(id)) return 0 + // Artifact suffixes: UNDEAD, HELL, MORGANA, KEEPER, and unique named artifacts + if (/_(?:UNDEAD|HELL|MORGANA|KEEPER|ROYAL|FEY)$/.test(id)) return 1 + return 0 +} + +export function useCraftingProfit( + recipes: CraftingRecipe[], + filters: Ref, + sortState: Ref +) { + const { getPrice } = useAlbionPrices() + + const profitResults = computed(() => { + const f = filters.value + const city = f.city + const rrrFactor = 1 - f.rrr / 100 + + const results: ProfitResult[] = [] + const nameLower = f.nameFilter.trim().toLowerCase() + + for (const recipe of recipes) { + if (!f.tiers.has(recipe.tier)) continue + if (f.enchantments !== null && !f.enchantments.has(recipe.enchantment)) continue + const baseName = recipe.displayName.replace(/^T\d+\.\d /, '') + if (f.selectedItemTypes !== null && !f.selectedItemTypes.has(baseName)) continue + if (nameLower && !recipe.displayName.toLowerCase().includes(nameLower)) continue + + // Calculate material cost from ingredients only + let materialCost = 0 + let missingPrices = false + + // Track oldest price date (for status column) + let oldestDate: string | null = null + function trackDate(date: string) { + if (!oldestDate || date < oldestDate) oldestDate = date + } + + const ingredientBreakdown: IngredientBreakdown[] = [] + + for (const ing of recipe.ingredients) { + const ingEntry = getPrice(ing.itemId, city) + const unitPrice = ingEntry?.sell_price_min ?? 0 + if (ingEntry === null || unitPrice === 0) { + missingPrices = true + ingredientBreakdown.push({ + itemId: ing.itemId, + displayName: formatItemId(ing.itemId), + quantity: ing.quantity, + unitPrice: 0, + totalCost: 0, + }) + } else { + trackDate(ingEntry.sell_price_min_date) + const totalCost = unitPrice * ing.quantity + materialCost += totalCost + ingredientBreakdown.push({ + itemId: ing.itemId, + displayName: formatItemId(ing.itemId), + quantity: ing.quantity, + unitPrice, + totalCost, + }) + } + } + + const effectiveMaterialCost = materialCost * rrrFactor + const priceAgeMs = missingPrices ? null : (oldestDate ? Date.now() - new Date(oldestDate).getTime() : null) + + results.push({ + recipe, + materialCost, + effectiveMaterialCost, + priceAgeMs, + missingPrices, + ingredientBreakdown, + }) + } + + // Sort results + const { field, direction } = sortState.value + results.sort((a, b) => { + let aVal: number | string + let bVal: number | string + + switch (field) { + case 'materialCost': + aVal = a.effectiveMaterialCost + bVal = b.effectiveMaterialCost + break + case 'displayName': + aVal = a.recipe.displayName + bVal = b.recipe.displayName + break + case 'tier': + aVal = a.recipe.tier + bVal = b.recipe.tier + break + case 'variantType': + aVal = variantRank(a.recipe.outputItemId) + bVal = variantRank(b.recipe.outputItemId) + break + default: + aVal = a.effectiveMaterialCost + bVal = b.effectiveMaterialCost + } + + if (typeof aVal === 'string' && typeof bVal === 'string') { + return direction === 'asc' ? aVal.localeCompare(bVal) : bVal.localeCompare(aVal) + } + const diff = (aVal as number) - (bVal as number) + return direction === 'asc' ? diff : -diff + }) + + return results + }) + + return { profitResults } +} diff --git a/src/composables/useFilters.ts b/src/composables/useFilters.ts new file mode 100644 index 0000000..c916e03 --- /dev/null +++ b/src/composables/useFilters.ts @@ -0,0 +1,79 @@ +import { ref } from 'vue' +import type { FilterState } from '../types/filters' +import type { Tier, Enchantment } from '../types/crafting' +import type { AlbionCity } from '../types/api' +import { ENCHANTMENTS } from '../data/constants' + +function loadCity(): AlbionCity { + return (localStorage.getItem('albion-city') as AlbionCity) ?? 'Caerleon' +} + +function loadRrr(): number { + const v = Number(localStorage.getItem('albion-rrr')) + return isNaN(v) || v < 0 || v > 100 ? 15 : v +} + +const filters = ref({ + city: loadCity(), + tiers: new Set([4, 5, 6, 7, 8]), + selectedItemTypes: null, + rrr: loadRrr(), + nameFilter: '', + enchantments: null, +}) + +function setCity(city: AlbionCity) { + filters.value.city = city + localStorage.setItem('albion-city', city) +} + +function toggleTier(tier: Tier) { + const tiers = new Set(filters.value.tiers) + if (tiers.has(tier)) { + tiers.delete(tier) + } else { + tiers.add(tier) + } + filters.value.tiers = tiers +} + +function setSelectedItemTypes(value: Set | null) { + filters.value.selectedItemTypes = value +} + +function setRrr(rate: number) { + filters.value.rrr = Math.max(0, Math.min(100, rate)) + localStorage.setItem('albion-rrr', String(filters.value.rrr)) +} + +function setNameFilter(value: string) { + filters.value.nameFilter = value +} + +function toggleEnchantment(enc: Enchantment) { + const current = filters.value.enchantments + const next = current === null ? new Set(ENCHANTMENTS as Enchantment[]) : new Set(current) + if (next.has(enc)) { + next.delete(enc) + } else { + next.add(enc) + } + filters.value.enchantments = next.size === ENCHANTMENTS.length ? null : next +} + +function resetEnchantments() { + filters.value.enchantments = null +} + +export function useFilters() { + return { + filters, + setCity, + toggleTier, + setSelectedItemTypes, + setRrr, + setNameFilter, + toggleEnchantment, + resetEnchantments, + } +} diff --git a/src/composables/useProductionOrder.ts b/src/composables/useProductionOrder.ts new file mode 100644 index 0000000..9ed2fa8 --- /dev/null +++ b/src/composables/useProductionOrder.ts @@ -0,0 +1,79 @@ +import { ref, computed } from 'vue' +import type { CraftingRecipe } from '../types/crafting' +import { ALL_RECIPES } from '../data/recipes' + +export interface OrderItem { + recipe: CraftingRecipe + qty: number +} + +const STORAGE_KEY = 'albion-production-order' + +const recipeIndex = new Map(ALL_RECIPES.map(r => [r.outputItemId, r])) + +function load(): Map { + try { + const raw = localStorage.getItem(STORAGE_KEY) + if (raw) { + const items = JSON.parse(raw) as OrderItem[] + const result: [string, OrderItem][] = [] + for (const i of items) { + const recipe = recipeIndex.get(i.recipe.outputItemId) + if (recipe) result.push([recipe.outputItemId, { recipe, qty: i.qty }]) + } + return new Map(result) + } + } catch { /* ignore */ } + return new Map() +} + +function save(map: Map): void { + try { + localStorage.setItem(STORAGE_KEY, JSON.stringify([...map.values()])) + } catch { /* ignore */ } +} + +// ─── Singleton state ────────────────────────────────────────────────────────── + +const order = ref>(load()) + +// ─── Mutations ──────────────────────────────────────────────────────────────── + +function upsert(recipe: CraftingRecipe, qty: number): void { + if (qty <= 0) { remove(recipe.outputItemId); return } + const next = new Map(order.value) + next.set(recipe.outputItemId, { recipe, qty }) + order.value = next + save(next) +} + +function remove(outputItemId: string): void { + const next = new Map(order.value) + next.delete(outputItemId) + order.value = next + save(next) +} + +function clear(): void { + order.value = new Map() + save(order.value) +} + +// ─── Derived state ──────────────────────────────────────────────────────────── + +const orderItems = computed(() => [...order.value.values()]) +const orderCount = computed(() => order.value.size) + +function getQty(outputItemId: string): number { + return order.value.get(outputItemId)?.qty ?? 0 +} + +function inOrder(outputItemId: string): boolean { + return order.value.has(outputItemId) +} + +// ─── Public API ─────────────────────────────────────────────────────────────── + +export function useProductionOrder() { + return { orderItems, orderCount, upsert, remove, clear, getQty, inOrder } +} diff --git a/src/data/categories.json b/src/data/categories.json new file mode 100644 index 0000000..82c3342 --- /dev/null +++ b/src/data/categories.json @@ -0,0 +1,456 @@ +[ + { + "label": "Weapons", + "children": [ + { + "label": "Bow", + "children": [ + "Bow", + "Longbow", + "Warbow", + "Whispering Bow", + "Wailing Bow", + "Bow of Badon", + "Mistpiercer", + "Skystrider Bow" + ] + }, + { + "label": "Crossbow", + "children": [ + "Crossbow", + "Heavy Crossbow", + "Light Crossbow", + "Weeping Repeater", + "Boltcasters", + "Siegebow", + "Energy Shaper", + "Arclight Blasters" + ] + }, + { + "label": "Axe", + "children": [ + "Battleaxe", + "Greataxe", + "Halberd", + "Carrioncaller", + "Infernal Scythe", + "Bear Paws", + "Realmbreaker", + "Crystal Reaper" + ] + }, + { + "label": "Dagger", + "children": [ + "Dagger", + "Dagger Pair", + "Claws", + "Bloodletter", + "Demon Fang", + "Deathgivers", + "Bridled Fury", + "Twin Slayers" + ] + }, + { + "label": "Hammer", + "children": [ + "Hammer", + "Great Hammer", + "Polehammer", + "Tombhammer", + "Forge Hammers", + "Grovekeeper", + "Hand of Justice", + "Truebolt Hammer" + ] + }, + { + "label": "War Gloves", + "children": [ + "Brawler Gloves", + "Battle Bracers", + "Spiked Gauntlets", + "Ursine Maulers", + "Hellfire Hands", + "Ravenstrike Cestus", + "Fists of Avalon", + "Forcepulse Bracers" + ] + }, + { + "label": "Mace", + "children": [ + "Mace", + "Heavy Mace", + "Morning Star", + "Bedrock Mace", + "Incubus Mace", + "Camlann Mace", + "Oathkeepers", + "Dreadstorm Monarch" + ] + }, + { + "label": "Quarterstaff", + "children": [ + "Quarterstaff", + "Iron-clad Staff", + "Double Bladed Staff", + "Black Monk Stave", + "Staff of Balance", + "Grailseeker", + "Phantom Twinblade" + ] + }, + { + "label": "Spear", + "children": [ + "Spear", + "Pike", + "Glaive", + "Heron Spear", + "Spirithunter", + "Trinity Spear", + "Daybreaker", + "Rift Glaive" + ] + }, + { + "label": "Sword", + "children": [ + "Broadsword", + "Claymore", + "Dual Swords", + "Clarent Blade", + "Carving Sword", + "Galatine Pair", + "Kingmaker", + "Infinity Blade" + ] + }, + { + "label": "Arcane Staff", + "children": [ + "Arcane Staff", + "Great Arcane Staff", + "Enigmatic Staff", + "Witchwork Staff", + "Occult Staff", + "Malevolent Locus", + "Evensong", + "Astral Staff" + ] + }, + { + "label": "Cursed Staff", + "children": [ + "Cursed Staff", + "Great Cursed Staff", + "Demonic Staff", + "Lifecurse Staff", + "Cursed Skull", + "Damnation Staff", + "Shadowcaller", + "Rotcaller Staff" + ] + }, + { + "label": "Fire Staff", + "children": [ + "Fire Staff", + "Great Fire Staff", + "Infernal Staff", + "Wildfire Staff", + "Brimstone Staff", + "Blazing Staff", + "Dawnsong", + "Flamewalker Staff" + ] + }, + { + "label": "Frost Staff", + "children": [ + "Frost Staff", + "Great Frost Staff", + "Glacial Staff", + "Hoarfrost Staff", + "Icicle Staff", + "Permafrost Prism", + "Chillhowl", + "Arctic Staff" + ] + }, + { + "label": "Holy Staff", + "children": [ + "Holy Staff", + "Great Holy Staff", + "Divine Staff", + "Lifetouch Staff", + "Fallen Staff", + "Redemption Staff", + "Hallowfall", + "Exalted Staff" + ] + }, + { + "label": "Nature Staff", + "children": [ + "Nature Staff", + "Great Nature Staff", + "Wild Staff", + "Druidic Staff", + "Blight Staff", + "Rampant Staff", + "Ironroot Staff", + "Forgebark Staff" + ] + }, + { + "label": "Shapeshifter", + "children": [ + "Prowling Staff", + "Rootbound Staff", + "Primal Staff", + "Bloodmoon Staff", + "Hellspawn Staff", + "Earthrune Staff", + "Lightcaller", + "Stillgaze Staff" + ] + } + ] + }, + { + "label": "Armor", + "children": [ + { + "label": "Head", + "children": [ + { + "label": "Plate", + "children": [ + "Soldier Helmet", + "Knight Helmet", + "Guardian Helmet", + "Graveguard Helmet", + "Royal Helmet", + "Demon Helmet", + "Judicator Helmet", + "Duskweaver Helmet", + "Helmet of Valor" + ] + }, + { + "label": "Leather", + "children": [ + "Mercenary Hood", + "Hunter Hood", + "Assassin Hood", + "Stalker Hood", + "Royal Hood", + "Hellion Hood", + "Specter Hood", + "Mistwalker Hood", + "Hood of Tenacity" + ] + }, + { + "label": "Cloth", + "children": [ + "Scholar Cowl", + "Cleric Cowl", + "Mage Cowl", + "Druid Cowl", + "Royal Cowl", + "Fiend Cowl", + "Cultist Cowl", + "Feyscale Cowl", + "Cowl of Purity" + ] + } + ] + }, + { + "label": "Chest", + "children": [ + { + "label": "Plate", + "children": [ + "Soldier Armor", + "Knight Armor", + "Guardian Armor", + "Graveguard Armor", + "Royal Armor", + "Demon Armor", + "Judicator Armor", + "Duskweaver Armor", + "Armor of Valor" + ] + }, + { + "label": "Leather", + "children": [ + "Mercenary Jacket", + "Hunter Jacket", + "Assassin Jacket", + "Stalker Jacket", + "Royal Jacket", + "Hellion Jacket", + "Specter Jacket", + "Mistwalker Jacket", + "Jacket of Tenacity" + ] + }, + { + "label": "Cloth", + "children": [ + "Scholar Robe", + "Cleric Robe", + "Mage Robe", + "Druid Robe", + "Royal Robe", + "Fiend Robe", + "Cultist Robe", + "Feyscale Robe", + "Robe of Purity" + ] + } + ] + }, + { + "label": "Boots", + "children": [ + { + "label": "Plate", + "children": [ + "Soldier Boots", + "Knight Boots", + "Guardian Boots", + "Graveguard Boots", + "Royal Boots", + "Demon Boots", + "Judicator Boots", + "Duskweaver Boots", + "Boots of Valor" + ] + }, + { + "label": "Leather", + "children": [ + "Mercenary Shoes", + "Hunter Shoes", + "Assassin Shoes", + "Stalker Shoes", + "Royal Shoes", + "Hellion Shoes", + "Specter Shoes", + "Mistwalker Shoes", + "Shoes of Tenacity" + ] + }, + { + "label": "Cloth", + "children": [ + "Scholar Sandals", + "Cleric Sandals", + "Mage Sandals", + "Druid Sandals", + "Royal Sandals", + "Fiend Sandals", + "Cultist Sandals", + "Feyscale Sandals", + "Sandals of Purity" + ] + } + ] + } + ] + }, + { + "label": "Off-hand", + "children": [ + { + "label": "Mage", + "children": [] + }, + { + "label": "Hunter", + "children": [] + }, + { + "label": "Warrior", + "children": [] + } + ] + }, + { + "label": "Gathering Equipment", + "children": [ + { + "label": "Fishing", + "children": [ + "Fishing Rods", + "Garbs", + "Caps", + "Workboots", + "Backpacks" + ] + }, + { + "label": "Fiber", + "children": [ + "Sickle", + "Garbs", + "Caps", + "Workboots", + "Backpacks" + ] + }, + { + "label": "Hide", + "children": [ + "Skinning Knife", + "Garbs", + "Caps", + "Workboots", + "Backpacks" + ] + }, + { + "label": "Ore", + "children": [ + "Pickaxe", + "Garbs", + "Caps", + "Workboots", + "Backpacks" + ] + }, + { + "label": "Stone", + "children": [ + "Hammer", + "Garbs", + "Caps", + "Workboots", + "Backpacks" + ] + }, + { + "label": "Wood", + "children": [ + "Axes", + "Garbs", + "Caps", + "Workboots", + "Backpacks" + ] + }, + "Tracking Toolkit" + ] + } +] diff --git a/src/data/constants.ts b/src/data/constants.ts new file mode 100644 index 0000000..26de46f --- /dev/null +++ b/src/data/constants.ts @@ -0,0 +1,24 @@ +import type { AlbionCity, AlbionQuality } from '../types/api' +import type { Tier, ItemCategory, Enchantment } from '../types/crafting' + +export const CITIES: AlbionCity[] = [ + 'Caerleon', + 'Bridgewatch', + 'Fort Sterling', + 'Lymhurst', + 'Martlock', + 'Thetford', + 'Black Market', +] + +export const QUALITIES: { label: string; value: AlbionQuality }[] = [ + { label: 'Normal', value: 1 }, + { label: 'Good', value: 2 }, + { label: 'Outstanding', value: 3 }, + { label: 'Excellent', value: 4 }, + { label: 'Masterpiece', value: 5 }, +] + +export const TIERS: Tier[] = [4, 5, 6, 7, 8] +export const ENCHANTMENTS: Enchantment[] = [0, 1, 2, 3, 4] +export const CATEGORIES: ItemCategory[] = ['Weapons', 'Armor', 'Gathering'] diff --git a/src/data/itemTree.ts b/src/data/itemTree.ts new file mode 100644 index 0000000..ce5084d --- /dev/null +++ b/src/data/itemTree.ts @@ -0,0 +1,29 @@ +import rawCategories from './categories.json' + +export interface TreeNode { + label: string + children?: TreeNode[] +} + +type RawNode = { label: string; children?: (RawNode | string)[] } + +function normalize(raw: RawNode): TreeNode { + if (!raw.children) return { label: raw.label } + return { + label: raw.label, + children: raw.children.map(c => typeof c === 'string' ? { label: c } : normalize(c)), + } +} + +export function getLeaves(node: TreeNode): string[] { + if (!node.children) return [node.label] + const result: string[] = [] + for (const child of node.children) { + result.push(...getLeaves(child)) + } + return result +} + +export const ITEM_TREE: TreeNode[] = (rawCategories as RawNode[]).map(normalize) + +export const ALL_ITEM_NAMES: Set = new Set(ITEM_TREE.flatMap(getLeaves)) diff --git a/src/data/recipes.json b/src/data/recipes.json new file mode 100644 index 0000000..4ce8f5c --- /dev/null +++ b/src/data/recipes.json @@ -0,0 +1,2287 @@ +[ + { + "comment": "--- Bows (Planks only) -----------------------------------------------------" + }, + { + "id": "bow", + "displayName": "Bow", + "category": "Weapons", + "outputId": "T{t}_2H_BOW", + "ingredients": [ + { "itemId": "T{t}_PLANKS", "qty": 32 } + ] + }, + { + "id": "longbow", + "displayName": "Longbow", + "category": "Weapons", + "outputId": "T{t}_2H_LONGBOW", + "ingredients": [ + { "itemId": "T{t}_PLANKS", "qty": 32 } + ] + }, + { + "id": "warbow", + "displayName": "Warbow", + "category": "Weapons", + "outputId": "T{t}_2H_WARBOW", + "ingredients": [ + { "itemId": "T{t}_PLANKS", "qty": 32 } + ] + }, + { + "id": "whispering-bow", + "displayName": "Whispering Bow", + "category": "Weapons", + "outputId": "T{t}_2H_LONGBOW_UNDEAD", + "ingredients": [ + { "itemId": "T{t}_PLANKS", "qty": 32 }, + { "itemId": "T{t}_ARTEFACT_2H_LONGBOW_UNDEAD", "qty": 1, "noEnchant": true } + ] + }, + { + "id": "wailing-bow", + "displayName": "Wailing Bow", + "category": "Weapons", + "outputId": "T{t}_2H_BOW_HELL", + "ingredients": [ + { "itemId": "T{t}_PLANKS", "qty": 32 }, + { "itemId": "T{t}_ARTEFACT_2H_BOW_HELL", "qty": 1, "noEnchant": true } + ] + }, + { + "id": "bow-of-badon", + "displayName": "Bow of Badon", + "category": "Weapons", + "outputId": "T{t}_2H_BOW_KEEPER", + "ingredients": [ + { "itemId": "T{t}_PLANKS", "qty": 32 }, + { "itemId": "T{t}_ARTEFACT_2H_BOW_KEEPER", "qty": 1, "noEnchant": true } + ] + }, + { + "id": "mistpiercer", + "displayName": "Mistpiercer", + "category": "Weapons", + "outputId": "T{t}_2H_BOW_AVALON", + "ingredients": [ + { "itemId": "T{t}_PLANKS", "qty": 32 }, + { "itemId": "T{t}_ARTEFACT_2H_BOW_AVALON", "qty": 1, "noEnchant": true } + ] + }, + { + "id": "skystrider-bow", + "displayName": "Skystrider Bow", + "category": "Weapons", + "outputId": "T{t}_2H_BOW_CRYSTAL", + "ingredients": [ + { "itemId": "T{t}_PLANKS", "qty": 32 }, + { "itemId": "T{t}_ARTEFACT_2H_BOW_CRYSTAL", "qty": 1, "noEnchant": true } + ] + }, + { + "comment": "--- Crossbows (Planks + Bars) ----------------------------------------------" + }, + { + "id": "crossbow", + "displayName": "Crossbow", + "category": "Weapons", + "outputId": "T{t}_2H_CROSSBOW", + "ingredients": [ + { "itemId": "T{t}_PLANKS", "qty": 20 }, + { "itemId": "T{t}_METALBAR", "qty": 12 } + ] + }, + { + "id": "heavy-crossbow", + "displayName": "Heavy Crossbow", + "category": "Weapons", + "outputId": "T{t}_2H_CROSSBOWLARGE", + "ingredients": [ + { "itemId": "T{t}_PLANKS", "qty": 20 }, + { "itemId": "T{t}_METALBAR", "qty": 12 } + ] + }, + { + "id": "light-crossbow", + "displayName": "Light Crossbow", + "category": "Weapons", + "outputId": "T{t}_MAIN_1HCROSSBOW", + "ingredients": [ + { "itemId": "T{t}_PLANKS", "qty": 16 }, + { "itemId": "T{t}_METALBAR", "qty": 8 } + ] + }, + { + "id": "weeping-repeater", + "displayName": "Weeping Repeater", + "category": "Weapons", + "outputId": "T{t}_2H_REPEATINGCROSSBOW_UNDEAD", + "ingredients": [ + { "itemId": "T{t}_PLANKS", "qty": 20 }, + { "itemId": "T{t}_METALBAR", "qty": 12 }, + { "itemId": "T{t}_ARTEFACT_2H_REPEATINGCROSSBOW_UNDEAD", "qty": 1, "noEnchant": true } + ] + }, + { + "id": "boltcasters", + "displayName": "Boltcasters", + "category": "Weapons", + "outputId": "T{t}_2H_DUALCROSSBOW_HELL", + "ingredients": [ + { "itemId": "T{t}_PLANKS", "qty": 20 }, + { "itemId": "T{t}_METALBAR", "qty": 12 }, + { "itemId": "T{t}_ARTEFACT_2H_DUALCROSSBOW_HELL", "qty": 1, "noEnchant": true } + ] + }, + { + "id": "siegebow", + "displayName": "Siegebow", + "category": "Weapons", + "outputId": "T{t}_2H_CROSSBOWLARGE_MORGANA", + "ingredients": [ + { "itemId": "T{t}_PLANKS", "qty": 16 }, + { "itemId": "T{t}_METALBAR", "qty": 8 }, + { "itemId": "T{t}_ARTEFACT_2H_CROSSBOWLARGE_MORGANA", "qty": 1, "noEnchant": true } + ] + }, + { + "id": "energy-shaper", + "displayName": "Energy Shaper", + "category": "Weapons", + "outputId": "T{t}_2H_CROSSBOW_CANNON_AVALON", + "ingredients": [ + { "itemId": "T{t}_PLANKS", "qty": 20 }, + { "itemId": "T{t}_METALBAR", "qty": 12 }, + { "itemId": "T{t}_ARTEFACT_2H_CROSSBOW_CANNON_AVALON", "qty": 1, "noEnchant": true } + ] + }, + { + "id": "arclight-blasters", + "displayName": "Arclight Blasters", + "category": "Weapons", + "outputId": "T{t}_2H_DUALCROSSBOW_CRYSTAL", + "ingredients": [ + { "itemId": "T{t}_PLANKS", "qty": 20 }, + { "itemId": "T{t}_METALBAR", "qty": 12 }, + { "itemId": "T{t}_ARTEFACT_2H_DUALCROSSBOW_CRYSTAL", "qty": 1, "noEnchant": true } + ] + }, + { + "comment": "--- Axes (Planks + Bars) ---------------------------------------------------" + }, + { + "id": "battleaxe", + "displayName": "Battleaxe", + "category": "Weapons", + "outputId": "T{t}_MAIN_AXE", + "ingredients": [ + { "itemId": "T{t}_PLANKS", "qty": 8 }, + { "itemId": "T{t}_METALBAR", "qty": 16 } + ] + }, + { + "id": "greataxe", + "displayName": "Greataxe", + "category": "Weapons", + "outputId": "T{t}_2H_AXE", + "ingredients": [ + { "itemId": "T{t}_PLANKS", "qty": 12 }, + { "itemId": "T{t}_METALBAR", "qty": 20 } + ] + }, + { + "id": "halberd", + "displayName": "Halberd", + "category": "Weapons", + "outputId": "T{t}_2H_HALBERD", + "ingredients": [ + { "itemId": "T{t}_PLANKS", "qty": 20 }, + { "itemId": "T{t}_METALBAR", "qty": 12 } + ] + }, + { + "id": "carrioncaller", + "displayName": "Carrioncaller", + "category": "Weapons", + "outputId": "T{t}_2H_HALBERD_MORGANA", + "ingredients": [ + { "itemId": "T{t}_PLANKS", "qty": 12 }, + { "itemId": "T{t}_METALBAR", "qty": 20 }, + { "itemId": "T{t}_ARTEFACT_2H_HALBERD_MORGANA", "qty": 1, "noEnchant": true } + ] + }, + { + "id": "infernal-scythe", + "displayName": "Infernal Scythe", + "category": "Weapons", + "outputId": "T{t}_2H_SCYTHE_HELL", + "ingredients": [ + { "itemId": "T{t}_PLANKS", "qty": 12 }, + { "itemId": "T{t}_METALBAR", "qty": 20 }, + { "itemId": "T{t}_ARTEFACT_2H_SCYTHE_HELL", "qty": 1, "noEnchant": true } + ] + }, + { + "id": "bear-paws", + "displayName": "Bear Paws", + "category": "Weapons", + "outputId": "T{t}_2H_DUALAXE_KEEPER", + "ingredients": [ + { "itemId": "T{t}_PLANKS", "qty": 12 }, + { "itemId": "T{t}_METALBAR", "qty": 20 }, + { "itemId": "T{t}_ARTEFACT_2H_DUALAXE_KEEPER", "qty": 1, "noEnchant": true } + ] + }, + { + "id": "realmbreaker", + "displayName": "Realmbreaker", + "category": "Weapons", + "outputId": "T{t}_2H_AXE_AVALON", + "ingredients": [ + { "itemId": "T{t}_PLANKS", "qty": 12 }, + { "itemId": "T{t}_METALBAR", "qty": 20 }, + { "itemId": "T{t}_ARTEFACT_2H_AXE_AVALON", "qty": 1, "noEnchant": true } + ] + }, + { + "id": "crystal-reaper", + "displayName": "Crystal Reaper", + "category": "Weapons", + "outputId": "T{t}_2H_SCYTHE_CRYSTAL", + "ingredients": [ + { "itemId": "T{t}_PLANKS", "qty": 12 }, + { "itemId": "T{t}_METALBAR", "qty": 20 }, + { "itemId": "T{t}_ARTEFACT_2H_SCYTHE_CRYSTAL", "qty": 1, "noEnchant": true } + ] + }, + { + "comment": "--- Daggers (Bars + Leather) -----------------------------------------------" + }, + { + "id": "dagger", + "displayName": "Dagger", + "category": "Weapons", + "outputId": "T{t}_MAIN_DAGGER", + "ingredients": [ + { "itemId": "T{t}_METALBAR", "qty": 12 }, + { "itemId": "T{t}_LEATHER", "qty": 12 } + ] + }, + { + "id": "dagger-pair", + "displayName": "Dagger Pair", + "category": "Weapons", + "outputId": "T{t}_2H_DAGGERPAIR", + "ingredients": [ + { "itemId": "T{t}_METALBAR", "qty": 16 }, + { "itemId": "T{t}_LEATHER", "qty": 16 } + ] + }, + { + "id": "claws", + "displayName": "Claws", + "category": "Weapons", + "outputId": "T{t}_2H_CLAWPAIR", + "ingredients": [ + { "itemId": "T{t}_METALBAR", "qty": 12 }, + { "itemId": "T{t}_LEATHER", "qty": 20 } + ] + }, + { + "id": "bloodletter", + "displayName": "Bloodletter", + "category": "Weapons", + "outputId": "T{t}_MAIN_RAPIER_MORGANA", + "ingredients": [ + { "itemId": "T{t}_METALBAR", "qty": 16 }, + { "itemId": "T{t}_LEATHER", "qty": 8 }, + { "itemId": "T{t}_ARTEFACT_MAIN_RAPIER_MORGANA", "qty": 1, "noEnchant": true } + ] + }, + { + "id": "demon-fang", + "displayName": "Demon Fang", + "category": "Weapons", + "outputId": "T{t}_MAIN_DAGGER_HELL", + "ingredients": [ + { "itemId": "T{t}_METALBAR", "qty": 12 }, + { "itemId": "T{t}_LEATHER", "qty": 12 }, + { "itemId": "T{t}_ARTEFACT_MAIN_DAGGER_HELL", "qty": 1, "noEnchant": true } + ] + }, + { + "id": "deathgivers", + "displayName": "Deathgivers", + "category": "Weapons", + "outputId": "T{t}_2H_DUALSICKLE_UNDEAD", + "ingredients": [ + { "itemId": "T{t}_METALBAR", "qty": 16 }, + { "itemId": "T{t}_LEATHER", "qty": 16 }, + { "itemId": "T{t}_ARTEFACT_2H_DUALSICKLE_UNDEAD", "qty": 1, "noEnchant": true } + ] + }, + { + "id": "bridled-fury", + "displayName": "Bridled Fury", + "category": "Weapons", + "outputId": "T{t}_2H_DAGGER_KATAR_AVALON", + "ingredients": [ + { "itemId": "T{t}_METALBAR", "qty": 12 }, + { "itemId": "T{t}_LEATHER", "qty": 20 }, + { "itemId": "T{t}_ARTEFACT_2H_DAGGER_KATAR_AVALON", "qty": 1, "noEnchant": true } + ] + }, + { + "id": "twin-slayers", + "displayName": "Twin Slayers", + "category": "Weapons", + "outputId": "T{t}_2H_DAGGERPAIR_CRYSTAL", + "ingredients": [ + { "itemId": "T{t}_METALBAR", "qty": 16 }, + { "itemId": "T{t}_LEATHER", "qty": 16 }, + { "itemId": "T{t}_ARTEFACT_2H_DAGGERPAIR_CRYSTAL", "qty": 1, "noEnchant": true } + ] + }, + { + "comment": "--- Hammers (Bars only / Bars + Cloth) -------------------------------------" + }, + { + "id": "hammer", + "displayName": "Hammer", + "category": "Weapons", + "outputId": "T{t}_MAIN_HAMMER", + "ingredients": [ + { "itemId": "T{t}_METALBAR", "qty": 24 } + ] + }, + { + "id": "great-hammer", + "displayName": "Great Hammer", + "category": "Weapons", + "outputId": "T{t}_2H_HAMMER", + "ingredients": [ + { "itemId": "T{t}_METALBAR", "qty": 20 }, + { "itemId": "T{t}_CLOTH", "qty": 12 } + ] + }, + { + "id": "polehammer", + "displayName": "Polehammer", + "category": "Weapons", + "outputId": "T{t}_2H_POLEHAMMER", + "ingredients": [ + { "itemId": "T{t}_METALBAR", "qty": 20 }, + { "itemId": "T{t}_CLOTH", "qty": 12 } + ] + }, + { + "id": "tombhammer", + "displayName": "Tombhammer", + "category": "Weapons", + "outputId": "T{t}_2H_HAMMER_UNDEAD", + "ingredients": [ + { "itemId": "T{t}_METALBAR", "qty": 20 }, + { "itemId": "T{t}_CLOTH", "qty": 12 }, + { "itemId": "T{t}_ARTEFACT_2H_HAMMER_UNDEAD", "qty": 1, "noEnchant": true } + ] + }, + { + "id": "forge-hammers", + "displayName": "Forge Hammers", + "category": "Weapons", + "outputId": "T{t}_2H_DUALHAMMER_HELL", + "ingredients": [ + { "itemId": "T{t}_METALBAR", "qty": 20 }, + { "itemId": "T{t}_CLOTH", "qty": 12 }, + { "itemId": "T{t}_ARTEFACT_2H_DUALHAMMER_HELL", "qty": 1, "noEnchant": true } + ] + }, + { + "id": "grovekeeper", + "displayName": "Grovekeeper", + "category": "Weapons", + "outputId": "T{t}_2H_HAMMER_KEEPER", + "ingredients": [ + { "itemId": "T{t}_METALBAR", "qty": 20 }, + { "itemId": "T{t}_CLOTH", "qty": 12 }, + { "itemId": "T{t}_ARTEFACT_2H_HAMMER_KEEPER", "qty": 1, "noEnchant": true } + ] + }, + { + "id": "hand-of-justice", + "displayName": "Hand of Justice", + "category": "Weapons", + "outputId": "T{t}_2H_HAMMER_AVALON", + "ingredients": [ + { "itemId": "T{t}_METALBAR", "qty": 20 }, + { "itemId": "T{t}_CLOTH", "qty": 12 }, + { "itemId": "T{t}_ARTEFACT_2H_HAMMER_AVALON", "qty": 1, "noEnchant": true } + ] + }, + { + "id": "truebolt-hammer", + "displayName": "Truebolt Hammer", + "category": "Weapons", + "outputId": "T{t}_2H_HAMMER_CRYSTAL", + "ingredients": [ + { "itemId": "T{t}_METALBAR", "qty": 20 }, + { "itemId": "T{t}_CLOTH", "qty": 12 }, + { "itemId": "T{t}_ARTEFACT_2H_HAMMER_CRYSTAL", "qty": 1, "noEnchant": true } + ] + }, + { + "comment": "--- War Gloves (Bars + Leather) --------------------------------------------" + }, + { + "id": "brawler-gloves", + "displayName": "Brawler Gloves", + "category": "Weapons", + "outputId": "T{t}_2H_KNUCKLES_SET1", + "ingredients": [ + { "itemId": "T{t}_METALBAR", "qty": 12 }, + { "itemId": "T{t}_LEATHER", "qty": 20 } + ] + }, + { + "id": "battle-bracers", + "displayName": "Battle Bracers", + "category": "Weapons", + "outputId": "T{t}_2H_KNUCKLES_SET2", + "ingredients": [ + { "itemId": "T{t}_METALBAR", "qty": 12 }, + { "itemId": "T{t}_LEATHER", "qty": 20 } + ] + }, + { + "id": "spiked-gauntlets", + "displayName": "Spiked Gauntlets", + "category": "Weapons", + "outputId": "T{t}_2H_KNUCKLES_SET3", + "ingredients": [ + { "itemId": "T{t}_METALBAR", "qty": 12 }, + { "itemId": "T{t}_LEATHER", "qty": 20 } + ] + }, + { + "id": "ursine-maulers", + "displayName": "Ursine Maulers", + "category": "Weapons", + "outputId": "T{t}_2H_KNUCKLES_KEEPER", + "ingredients": [ + { "itemId": "T{t}_METALBAR", "qty": 12 }, + { "itemId": "T{t}_LEATHER", "qty": 20 }, + { "itemId": "T{t}_ARTEFACT_2H_KNUCKLES_KEEPER", "qty": 1, "noEnchant": true } + ] + }, + { + "id": "hellfire-hands", + "displayName": "Hellfire Hands", + "category": "Weapons", + "outputId": "T{t}_2H_KNUCKLES_HELL", + "ingredients": [ + { "itemId": "T{t}_METALBAR", "qty": 12 }, + { "itemId": "T{t}_LEATHER", "qty": 20 }, + { "itemId": "T{t}_ARTEFACT_2H_KNUCKLES_HELL", "qty": 1, "noEnchant": true } + ] + }, + { + "id": "ravenstrike-cestus", + "displayName": "Ravenstrike Cestus", + "category": "Weapons", + "outputId": "T{t}_2H_KNUCKLES_MORGANA", + "ingredients": [ + { "itemId": "T{t}_METALBAR", "qty": 12 }, + { "itemId": "T{t}_LEATHER", "qty": 20 }, + { "itemId": "T{t}_ARTEFACT_2H_KNUCKLES_MORGANA", "qty": 1, "noEnchant": true } + ] + }, + { + "id": "fists-of-avalon", + "displayName": "Fists of Avalon", + "category": "Weapons", + "outputId": "T{t}_2H_KNUCKLES_AVALON", + "ingredients": [ + { "itemId": "T{t}_METALBAR", "qty": 12 }, + { "itemId": "T{t}_LEATHER", "qty": 20 }, + { "itemId": "T{t}_ARTEFACT_2H_KNUCKLES_AVALON", "qty": 1, "noEnchant": true } + ] + }, + { + "id": "forcepulse-bracers", + "displayName": "Forcepulse Bracers", + "category": "Weapons", + "outputId": "T{t}_2H_KNUCKLES_CRYSTAL", + "ingredients": [ + { "itemId": "T{t}_METALBAR", "qty": 12 }, + { "itemId": "T{t}_LEATHER", "qty": 20 }, + { "itemId": "T{t}_ARTEFACT_2H_KNUCKLES_CRYSTAL", "qty": 1, "noEnchant": true } + ] + }, + { + "comment": "--- Maces (Bars + Cloth) ---------------------------------------------------" + }, + { + "id": "mace", + "displayName": "Mace", + "category": "Weapons", + "outputId": "T{t}_MAIN_MACE", + "ingredients": [ + { "itemId": "T{t}_METALBAR", "qty": 16 }, + { "itemId": "T{t}_CLOTH", "qty": 8 } + ] + }, + { + "id": "heavy-mace", + "displayName": "Heavy Mace", + "category": "Weapons", + "outputId": "T{t}_2H_MACE", + "ingredients": [ + { "itemId": "T{t}_METALBAR", "qty": 20 }, + { "itemId": "T{t}_CLOTH", "qty": 12 } + ] + }, + { + "id": "morning-star", + "displayName": "Morning Star", + "category": "Weapons", + "outputId": "T{t}_2H_MORNINGSTAR", + "ingredients": [ + { "itemId": "T{t}_METALBAR", "qty": 20 }, + { "itemId": "T{t}_CLOTH", "qty": 12 } + ] + }, + { + "id": "bedrock-mace", + "displayName": "Bedrock Mace", + "category": "Weapons", + "outputId": "T{t}_MAIN_ROCKMACE_KEEPER", + "ingredients": [ + { "itemId": "T{t}_METALBAR", "qty": 16 }, + { "itemId": "T{t}_CLOTH", "qty": 8 }, + { "itemId": "T{t}_ARTEFACT_MAIN_ROCKMACE_KEEPER", "qty": 1, "noEnchant": true } + ] + }, + { + "id": "incubus-mace", + "displayName": "Incubus Mace", + "category": "Weapons", + "outputId": "T{t}_MAIN_MACE_HELL", + "ingredients": [ + { "itemId": "T{t}_METALBAR", "qty": 16 }, + { "itemId": "T{t}_CLOTH", "qty": 8 }, + { "itemId": "T{t}_ARTEFACT_MAIN_MACE_HELL", "qty": 1, "noEnchant": true } + ] + }, + { + "id": "camlann-mace", + "displayName": "Camlann Mace", + "category": "Weapons", + "outputId": "T{t}_2H_MACE_MORGANA", + "ingredients": [ + { "itemId": "T{t}_METALBAR", "qty": 20 }, + { "itemId": "T{t}_CLOTH", "qty": 12 }, + { "itemId": "T{t}_ARTEFACT_2H_MACE_MORGANA", "qty": 1, "noEnchant": true } + ] + }, + { + "id": "oathkeepers", + "displayName": "Oathkeepers", + "category": "Weapons", + "outputId": "T{t}_2H_DUALMACE_AVALON", + "ingredients": [ + { "itemId": "T{t}_METALBAR", "qty": 20 }, + { "itemId": "T{t}_CLOTH", "qty": 12 }, + { "itemId": "T{t}_ARTEFACT_2H_DUALMACE_AVALON", "qty": 1, "noEnchant": true } + ] + }, + { + "id": "dreadstorm-monarch", + "displayName": "Dreadstorm Monarch", + "category": "Weapons", + "outputId": "T{t}_MAIN_MACE_CRYSTAL", + "ingredients": [ + { "itemId": "T{t}_METALBAR", "qty": 16 }, + { "itemId": "T{t}_CLOTH", "qty": 8 }, + { "itemId": "T{t}_ARTEFACT_MAIN_MACE_CRYSTAL", "qty": 1, "noEnchant": true } + ] + }, + { + "comment": "--- Quarterstaves (Bars + Leather) -----------------------------------------" + }, + { + "id": "quarterstaff", + "displayName": "Quarterstaff", + "category": "Weapons", + "outputId": "T{t}_2H_QUARTERSTAFF", + "ingredients": [ + { "itemId": "T{t}_METALBAR", "qty": 12 }, + { "itemId": "T{t}_LEATHER", "qty": 20 } + ] + }, + { + "id": "iron-clad-staff", + "displayName": "Iron-clad Staff", + "category": "Weapons", + "outputId": "T{t}_2H_IRONCLADEDSTAFF", + "ingredients": [ + { "itemId": "T{t}_METALBAR", "qty": 12 }, + { "itemId": "T{t}_LEATHER", "qty": 20 } + ] + }, + { + "id": "double-bladed-staff", + "displayName": "Double Bladed Staff", + "category": "Weapons", + "outputId": "T{t}_2H_DOUBLEBLADEDSTAFF", + "ingredients": [ + { "itemId": "T{t}_METALBAR", "qty": 12 }, + { "itemId": "T{t}_LEATHER", "qty": 20 } + ] + }, + { + "id": "black-monk-stave", + "displayName": "Black Monk Stave", + "category": "Weapons", + "outputId": "T{t}_2H_COMBATSTAFF_MORGANA", + "ingredients": [ + { "itemId": "T{t}_METALBAR", "qty": 12 }, + { "itemId": "T{t}_LEATHER", "qty": 20 }, + { "itemId": "T{t}_ARTEFACT_2H_COMBATSTAFF_MORGANA", "qty": 1, "noEnchant": true } + ] + }, + { + "id": "staff-of-balance", + "displayName": "Staff of Balance", + "category": "Weapons", + "outputId": "T{t}_2H_ROCKSTAFF_KEEPER", + "ingredients": [ + { "itemId": "T{t}_METALBAR", "qty": 12 }, + { "itemId": "T{t}_LEATHER", "qty": 20 }, + { "itemId": "T{t}_ARTEFACT_2H_ROCKSTAFF_KEEPER", "qty": 1, "noEnchant": true } + ] + }, + { + "id": "grailseeker", + "displayName": "Grailseeker", + "category": "Weapons", + "outputId": "T{t}_2H_QUARTERSTAFF_AVALON", + "ingredients": [ + { "itemId": "T{t}_METALBAR", "qty": 12 }, + { "itemId": "T{t}_LEATHER", "qty": 20 }, + { "itemId": "T{t}_ARTEFACT_2H_QUARTERSTAFF_AVALON", "qty": 1, "noEnchant": true } + ] + }, + { + "id": "phantom-twinblade", + "displayName": "Phantom Twinblade", + "category": "Weapons", + "outputId": "T{t}_2H_DOUBLEBLADEDSTAFF_CRYSTAL", + "ingredients": [ + { "itemId": "T{t}_METALBAR", "qty": 12 }, + { "itemId": "T{t}_LEATHER", "qty": 20 }, + { "itemId": "T{t}_ARTEFACT_2H_DOUBLEBLADEDSTAFF_CRYSTAL", "qty": 1, "noEnchant": true } + ] + }, + { + "comment": "--- Spears (Planks + Bars) -------------------------------------------------" + }, + { + "id": "spear", + "displayName": "Spear", + "category": "Weapons", + "outputId": "T{t}_MAIN_SPEAR", + "ingredients": [ + { "itemId": "T{t}_PLANKS", "qty": 16 }, + { "itemId": "T{t}_METALBAR", "qty": 8 } + ] + }, + { + "id": "pike", + "displayName": "Pike", + "category": "Weapons", + "outputId": "T{t}_2H_SPEAR", + "ingredients": [ + { "itemId": "T{t}_PLANKS", "qty": 20 }, + { "itemId": "T{t}_METALBAR", "qty": 12 } + ] + }, + { + "id": "glaive", + "displayName": "Glaive", + "category": "Weapons", + "outputId": "T{t}_2H_GLAIVE", + "ingredients": [ + { "itemId": "T{t}_PLANKS", "qty": 12 }, + { "itemId": "T{t}_METALBAR", "qty": 20 } + ] + }, + { + "id": "heron-spear", + "displayName": "Heron Spear", + "category": "Weapons", + "outputId": "T{t}_MAIN_SPEAR_KEEPER", + "ingredients": [ + { "itemId": "T{t}_PLANKS", "qty": 16 }, + { "itemId": "T{t}_METALBAR", "qty": 8 }, + { "itemId": "T{t}_ARTEFACT_MAIN_SPEAR_KEEPER", "qty": 1, "noEnchant": true } + ] + }, + { + "id": "spirithunter", + "displayName": "Spirithunter", + "category": "Weapons", + "outputId": "T{t}_2H_HARPOON_HELL", + "ingredients": [ + { "itemId": "T{t}_PLANKS", "qty": 20 }, + { "itemId": "T{t}_METALBAR", "qty": 12 }, + { "itemId": "T{t}_ARTEFACT_2H_HARPOON_HELL", "qty": 1, "noEnchant": true } + ] + }, + { + "id": "trinity-spear", + "displayName": "Trinity Spear", + "category": "Weapons", + "outputId": "T{t}_2H_TRIDENT_UNDEAD", + "ingredients": [ + { "itemId": "T{t}_PLANKS", "qty": 20 }, + { "itemId": "T{t}_METALBAR", "qty": 12 }, + { "itemId": "T{t}_ARTEFACT_2H_TRIDENT_UNDEAD", "qty": 1, "noEnchant": true } + ] + }, + { + "id": "daybreaker", + "displayName": "Daybreaker", + "category": "Weapons", + "outputId": "T{t}_MAIN_SPEAR_LANCE_AVALON", + "ingredients": [ + { "itemId": "T{t}_PLANKS", "qty": 16 }, + { "itemId": "T{t}_METALBAR", "qty": 8 }, + { "itemId": "T{t}_ARTEFACT_MAIN_SPEAR_LANCE_AVALON", "qty": 1, "noEnchant": true } + ] + }, + { + "id": "rift-glaive", + "displayName": "Rift Glaive", + "category": "Weapons", + "outputId": "T{t}_2H_GLAIVE_CRYSTAL", + "ingredients": [ + { "itemId": "T{t}_PLANKS", "qty": 12 }, + { "itemId": "T{t}_METALBAR", "qty": 20 }, + { "itemId": "T{t}_ARTEFACT_2H_GLAIVE_CRYSTAL", "qty": 1, "noEnchant": true } + ] + }, + { + "comment": "--- Swords (Bars + Leather) ------------------------------------------------" + }, + { + "id": "broadsword", + "displayName": "Broadsword", + "category": "Weapons", + "outputId": "T{t}_MAIN_SWORD", + "ingredients": [ + { "itemId": "T{t}_METALBAR", "qty": 16 }, + { "itemId": "T{t}_LEATHER", "qty": 8 } + ] + }, + { + "id": "claymore", + "displayName": "Claymore", + "category": "Weapons", + "outputId": "T{t}_2H_CLAYMORE", + "ingredients": [ + { "itemId": "T{t}_METALBAR", "qty": 20 }, + { "itemId": "T{t}_LEATHER", "qty": 12 } + ] + }, + { + "id": "dual-swords", + "displayName": "Dual Swords", + "category": "Weapons", + "outputId": "T{t}_2H_DUALSWORD", + "ingredients": [ + { "itemId": "T{t}_METALBAR", "qty": 20 }, + { "itemId": "T{t}_LEATHER", "qty": 12 } + ] + }, + { + "id": "clarent-blade", + "displayName": "Clarent Blade", + "category": "Weapons", + "outputId": "T{t}_MAIN_SCIMITAR_MORGANA", + "ingredients": [ + { "itemId": "T{t}_METALBAR", "qty": 16 }, + { "itemId": "T{t}_LEATHER", "qty": 8 }, + { "itemId": "T{t}_ARTEFACT_MAIN_SCIMITAR_MORGANA", "qty": 1, "noEnchant": true } + ] + }, + { + "id": "carving-sword", + "displayName": "Carving Sword", + "category": "Weapons", + "outputId": "T{t}_2H_CLEAVER_HELL", + "ingredients": [ + { "itemId": "T{t}_METALBAR", "qty": 20 }, + { "itemId": "T{t}_LEATHER", "qty": 12 }, + { "itemId": "T{t}_ARTEFACT_2H_CLEAVER_HELL", "qty": 1, "noEnchant": true } + ] + }, + { + "id": "galatine-pair", + "displayName": "Galatine Pair", + "category": "Weapons", + "outputId": "T{t}_2H_DUALSCIMITAR_UNDEAD", + "ingredients": [ + { "itemId": "T{t}_METALBAR", "qty": 20 }, + { "itemId": "T{t}_LEATHER", "qty": 12 }, + { "itemId": "T{t}_ARTEFACT_2H_DUALSCIMITAR_UNDEAD", "qty": 1, "noEnchant": true } + ] + }, + { + "id": "kingmaker", + "displayName": "Kingmaker", + "category": "Weapons", + "outputId": "T{t}_2H_CLAYMORE_AVALON", + "ingredients": [ + { "itemId": "T{t}_METALBAR", "qty": 20 }, + { "itemId": "T{t}_LEATHER", "qty": 12 }, + { "itemId": "T{t}_ARTEFACT_2H_CLAYMORE_AVALON", "qty": 1, "noEnchant": true } + ] + }, + { + "id": "infinity-blade", + "displayName": "Infinity Blade", + "category": "Weapons", + "outputId": "T{t}_MAIN_SWORD_CRYSTAL", + "ingredients": [ + { "itemId": "T{t}_METALBAR", "qty": 16 }, + { "itemId": "T{t}_LEATHER", "qty": 8 }, + { "itemId": "T{t}_ARTEFACT_MAIN_SWORD_CRYSTAL", "qty": 1, "noEnchant": true } + ] + }, + { + "comment": "--- Arcane Staves (Planks + Bars) ------------------------------------------" + }, + { + "id": "arcane-staff", + "displayName": "Arcane Staff", + "category": "Weapons", + "outputId": "T{t}_MAIN_ARCANESTAFF", + "ingredients": [ + { "itemId": "T{t}_PLANKS", "qty": 16 }, + { "itemId": "T{t}_METALBAR", "qty": 8 } + ] + }, + { + "id": "great-arcane-staff", + "displayName": "Great Arcane Staff", + "category": "Weapons", + "outputId": "T{t}_2H_ARCANESTAFF", + "ingredients": [ + { "itemId": "T{t}_PLANKS", "qty": 20 }, + { "itemId": "T{t}_METALBAR", "qty": 12 } + ] + }, + { + "id": "enigmatic-staff", + "displayName": "Enigmatic Staff", + "category": "Weapons", + "outputId": "T{t}_2H_ENIGMATICSTAFF", + "ingredients": [ + { "itemId": "T{t}_PLANKS", "qty": 20 }, + { "itemId": "T{t}_METALBAR", "qty": 12 } + ] + }, + { + "id": "witchwork-staff", + "displayName": "Witchwork Staff", + "category": "Weapons", + "outputId": "T{t}_MAIN_ARCANESTAFF_UNDEAD", + "ingredients": [ + { "itemId": "T{t}_PLANKS", "qty": 16 }, + { "itemId": "T{t}_METALBAR", "qty": 8 }, + { "itemId": "T{t}_ARTEFACT_MAIN_ARCANESTAFF_UNDEAD", "qty": 1, "noEnchant": true } + ] + }, + { + "id": "occult-staff", + "displayName": "Occult Staff", + "category": "Weapons", + "outputId": "T{t}_2H_ARCANESTAFF_HELL", + "ingredients": [ + { "itemId": "T{t}_PLANKS", "qty": 20 }, + { "itemId": "T{t}_METALBAR", "qty": 12 }, + { "itemId": "T{t}_ARTEFACT_2H_ARCANESTAFF_HELL", "qty": 1, "noEnchant": true } + ] + }, + { + "id": "malevolent-locus", + "displayName": "Malevolent Locus", + "category": "Weapons", + "outputId": "T{t}_2H_ENIGMATICORB_MORGANA", + "ingredients": [ + { "itemId": "T{t}_PLANKS", "qty": 20 }, + { "itemId": "T{t}_METALBAR", "qty": 12 }, + { "itemId": "T{t}_ARTEFACT_2H_ENIGMATICORB_MORGANA", "qty": 1, "noEnchant": true } + ] + }, + { + "id": "evensong", + "displayName": "Evensong", + "category": "Weapons", + "outputId": "T{t}_2H_ARCANE_RINGPAIR_AVALON", + "ingredients": [ + { "itemId": "T{t}_PLANKS", "qty": 20 }, + { "itemId": "T{t}_METALBAR", "qty": 12 }, + { "itemId": "T{t}_ARTEFACT_2H_ARCANE_RINGPAIR_AVALON", "qty": 1, "noEnchant": true } + ] + }, + { + "id": "astral-staff", + "displayName": "Astral Staff", + "category": "Weapons", + "outputId": "T{t}_2H_ARCANESTAFF_CRYSTAL", + "ingredients": [ + { "itemId": "T{t}_PLANKS", "qty": 20 }, + { "itemId": "T{t}_METALBAR", "qty": 12 }, + { "itemId": "T{t}_ARTEFACT_2H_ARCANESTAFF_CRYSTAL", "qty": 1, "noEnchant": true } + ] + }, + { + "comment": "--- Cursed Staves (Planks + Bars) ------------------------------------------" + }, + { + "id": "cursed-staff", + "displayName": "Cursed Staff", + "category": "Weapons", + "outputId": "T{t}_MAIN_CURSEDSTAFF", + "ingredients": [ + { "itemId": "T{t}_PLANKS", "qty": 16 }, + { "itemId": "T{t}_METALBAR", "qty": 8 } + ] + }, + { + "id": "great-cursed-staff", + "displayName": "Great Cursed Staff", + "category": "Weapons", + "outputId": "T{t}_2H_CURSEDSTAFF", + "ingredients": [ + { "itemId": "T{t}_PLANKS", "qty": 20 }, + { "itemId": "T{t}_METALBAR", "qty": 12 } + ] + }, + { + "id": "demonic-staff", + "displayName": "Demonic Staff", + "category": "Weapons", + "outputId": "T{t}_2H_DEMONICSTAFF", + "ingredients": [ + { "itemId": "T{t}_PLANKS", "qty": 20 }, + { "itemId": "T{t}_METALBAR", "qty": 12 } + ] + }, + { + "id": "lifecurse-staff", + "displayName": "Lifecurse Staff", + "category": "Weapons", + "outputId": "T{t}_MAIN_CURSEDSTAFF_UNDEAD", + "ingredients": [ + { "itemId": "T{t}_PLANKS", "qty": 16 }, + { "itemId": "T{t}_METALBAR", "qty": 8 }, + { "itemId": "T{t}_ARTEFACT_MAIN_CURSEDSTAFF_UNDEAD", "qty": 1, "noEnchant": true } + ] + }, + { + "id": "cursed-skull", + "displayName": "Cursed Skull", + "category": "Weapons", + "outputId": "T{t}_2H_SKULLORB_HELL", + "ingredients": [ + { "itemId": "T{t}_PLANKS", "qty": 20 }, + { "itemId": "T{t}_METALBAR", "qty": 12 }, + { "itemId": "T{t}_ARTEFACT_2H_SKULLORB_HELL", "qty": 1, "noEnchant": true } + ] + }, + { + "id": "damnation-staff", + "displayName": "Damnation Staff", + "category": "Weapons", + "outputId": "T{t}_2H_CURSEDSTAFF_MORGANA", + "ingredients": [ + { "itemId": "T{t}_PLANKS", "qty": 20 }, + { "itemId": "T{t}_METALBAR", "qty": 12 }, + { "itemId": "T{t}_ARTEFACT_2H_CURSEDSTAFF_MORGANA", "qty": 1, "noEnchant": true } + ] + }, + { + "id": "shadowcaller", + "displayName": "Shadowcaller", + "category": "Weapons", + "outputId": "T{t}_MAIN_CURSEDSTAFF_AVALON", + "ingredients": [ + { "itemId": "T{t}_PLANKS", "qty": 16 }, + { "itemId": "T{t}_METALBAR", "qty": 8 }, + { "itemId": "T{t}_ARTEFACT_MAIN_CURSEDSTAFF_AVALON", "qty": 1, "noEnchant": true } + ] + }, + { + "id": "rotcaller-staff", + "displayName": "Rotcaller Staff", + "category": "Weapons", + "outputId": "T{t}_MAIN_CURSEDSTAFF_CRYSTAL", + "ingredients": [ + { "itemId": "T{t}_PLANKS", "qty": 16 }, + { "itemId": "T{t}_METALBAR", "qty": 8 }, + { "itemId": "T{t}_ARTEFACT_MAIN_CURSEDSTAFF_CRYSTAL", "qty": 1, "noEnchant": true } + ] + }, + { + "comment": "--- Fire Staves (Planks + Bars) --------------------------------------------" + }, + { + "id": "fire-staff", + "displayName": "Fire Staff", + "category": "Weapons", + "outputId": "T{t}_MAIN_FIRESTAFF", + "ingredients": [ + { "itemId": "T{t}_PLANKS", "qty": 16 }, + { "itemId": "T{t}_METALBAR", "qty": 8 } + ] + }, + { + "id": "great-fire-staff", + "displayName": "Great Fire Staff", + "category": "Weapons", + "outputId": "T{t}_2H_FIRESTAFF", + "ingredients": [ + { "itemId": "T{t}_PLANKS", "qty": 20 }, + { "itemId": "T{t}_METALBAR", "qty": 12 } + ] + }, + { + "id": "infernal-staff", + "displayName": "Infernal Staff", + "category": "Weapons", + "outputId": "T{t}_2H_INFERNOSTAFF", + "ingredients": [ + { "itemId": "T{t}_PLANKS", "qty": 20 }, + { "itemId": "T{t}_METALBAR", "qty": 12 } + ] + }, + { + "id": "wildfire-staff", + "displayName": "Wildfire Staff", + "category": "Weapons", + "outputId": "T{t}_MAIN_FIRESTAFF_KEEPER", + "ingredients": [ + { "itemId": "T{t}_PLANKS", "qty": 16 }, + { "itemId": "T{t}_METALBAR", "qty": 8 }, + { "itemId": "T{t}_ARTEFACT_MAIN_FIRESTAFF_KEEPER", "qty": 1, "noEnchant": true } + ] + }, + { + "id": "brimstone-staff", + "displayName": "Brimstone Staff", + "category": "Weapons", + "outputId": "T{t}_2H_FIRESTAFF_HELL", + "ingredients": [ + { "itemId": "T{t}_PLANKS", "qty": 20 }, + { "itemId": "T{t}_METALBAR", "qty": 12 }, + { "itemId": "T{t}_ARTEFACT_2H_FIRESTAFF_HELL", "qty": 1, "noEnchant": true } + ] + }, + { + "id": "blazing-staff", + "displayName": "Blazing Staff", + "category": "Weapons", + "outputId": "T{t}_2H_INFERNOSTAFF_MORGANA", + "ingredients": [ + { "itemId": "T{t}_PLANKS", "qty": 20 }, + { "itemId": "T{t}_METALBAR", "qty": 12 }, + { "itemId": "T{t}_ARTEFACT_2H_INFERNOSTAFF_MORGANA", "qty": 1, "noEnchant": true } + ] + }, + { + "id": "dawnsong", + "displayName": "Dawnsong", + "category": "Weapons", + "outputId": "T{t}_2H_FIRE_RINGPAIR_AVALON", + "ingredients": [ + { "itemId": "T{t}_PLANKS", "qty": 20 }, + { "itemId": "T{t}_METALBAR", "qty": 12 }, + { "itemId": "T{t}_ARTEFACT_2H_FIRE_RINGPAIR_AVALON", "qty": 1, "noEnchant": true } + ] + }, + { + "id": "flamewalker-staff", + "displayName": "Flamewalker Staff", + "category": "Weapons", + "outputId": "T{t}_MAIN_FIRESTAFF_CRYSTAL", + "ingredients": [ + { "itemId": "T{t}_PLANKS", "qty": 16 }, + { "itemId": "T{t}_METALBAR", "qty": 8 }, + { "itemId": "T{t}_ARTEFACT_MAIN_FIRESTAFF_CRYSTAL", "qty": 1, "noEnchant": true } + ] + }, + { + "comment": "--- Frost Staves (Planks + Bars) -------------------------------------------" + }, + { + "id": "frost-staff", + "displayName": "Frost Staff", + "category": "Weapons", + "outputId": "T{t}_MAIN_FROSTSTAFF", + "ingredients": [ + { "itemId": "T{t}_PLANKS", "qty": 16 }, + { "itemId": "T{t}_METALBAR", "qty": 8 } + ] + }, + { + "id": "great-frost-staff", + "displayName": "Great Frost Staff", + "category": "Weapons", + "outputId": "T{t}_2H_FROSTSTAFF", + "ingredients": [ + { "itemId": "T{t}_PLANKS", "qty": 20 }, + { "itemId": "T{t}_METALBAR", "qty": 12 } + ] + }, + { + "id": "glacial-staff", + "displayName": "Glacial Staff", + "category": "Weapons", + "outputId": "T{t}_2H_GLACIALSTAFF", + "ingredients": [ + { "itemId": "T{t}_PLANKS", "qty": 20 }, + { "itemId": "T{t}_METALBAR", "qty": 12 } + ] + }, + { + "id": "hoarfrost-staff", + "displayName": "Hoarfrost Staff", + "category": "Weapons", + "outputId": "T{t}_MAIN_FROSTSTAFF_KEEPER", + "ingredients": [ + { "itemId": "T{t}_PLANKS", "qty": 16 }, + { "itemId": "T{t}_METALBAR", "qty": 8 }, + { "itemId": "T{t}_ARTEFACT_MAIN_FROSTSTAFF_KEEPER", "qty": 1, "noEnchant": true } + ] + }, + { + "id": "icicle-staff", + "displayName": "Icicle Staff", + "category": "Weapons", + "outputId": "T{t}_2H_ICEGAUNTLETS_HELL", + "ingredients": [ + { "itemId": "T{t}_PLANKS", "qty": 20 }, + { "itemId": "T{t}_METALBAR", "qty": 12 }, + { "itemId": "T{t}_ARTEFACT_2H_ICEGAUNTLETS_HELL", "qty": 1, "noEnchant": true } + ] + }, + { + "id": "permafrost-prism", + "displayName": "Permafrost Prism", + "category": "Weapons", + "outputId": "T{t}_2H_ICECRYSTAL_UNDEAD", + "ingredients": [ + { "itemId": "T{t}_PLANKS", "qty": 20 }, + { "itemId": "T{t}_METALBAR", "qty": 12 }, + { "itemId": "T{t}_ARTEFACT_2H_ICECRYSTAL_UNDEAD", "qty": 1, "noEnchant": true } + ] + }, + { + "id": "chillhowl", + "displayName": "Chillhowl", + "category": "Weapons", + "outputId": "T{t}_MAIN_FROSTSTAFF_AVALON", + "ingredients": [ + { "itemId": "T{t}_PLANKS", "qty": 16 }, + { "itemId": "T{t}_METALBAR", "qty": 8 }, + { "itemId": "T{t}_ARTEFACT_MAIN_FROSTSTAFF_AVALON", "qty": 1, "noEnchant": true } + ] + }, + { + "id": "arctic-staff", + "displayName": "Arctic Staff", + "category": "Weapons", + "outputId": "T{t}_2H_FROSTSTAFF_CRYSTAL", + "ingredients": [ + { "itemId": "T{t}_PLANKS", "qty": 20 }, + { "itemId": "T{t}_METALBAR", "qty": 12 }, + { "itemId": "T{t}_ARTEFACT_2H_FROSTSTAFF_CRYSTAL", "qty": 1, "noEnchant": true } + ] + }, + { + "comment": "--- Holy Staves (Planks + Cloth) -------------------------------------------" + }, + { + "id": "holy-staff", + "displayName": "Holy Staff", + "category": "Weapons", + "outputId": "T{t}_MAIN_HOLYSTAFF", + "ingredients": [ + { "itemId": "T{t}_PLANKS", "qty": 16 }, + { "itemId": "T{t}_CLOTH", "qty": 8 } + ] + }, + { + "id": "great-holy-staff", + "displayName": "Great Holy Staff", + "category": "Weapons", + "outputId": "T{t}_2H_HOLYSTAFF", + "ingredients": [ + { "itemId": "T{t}_PLANKS", "qty": 20 }, + { "itemId": "T{t}_CLOTH", "qty": 12 } + ] + }, + { + "id": "divine-staff", + "displayName": "Divine Staff", + "category": "Weapons", + "outputId": "T{t}_2H_DIVINESTAFF", + "ingredients": [ + { "itemId": "T{t}_PLANKS", "qty": 20 }, + { "itemId": "T{t}_CLOTH", "qty": 12 } + ] + }, + { + "id": "lifetouch-staff", + "displayName": "Lifetouch Staff", + "category": "Weapons", + "outputId": "T{t}_MAIN_HOLYSTAFF_MORGANA", + "ingredients": [ + { "itemId": "T{t}_PLANKS", "qty": 16 }, + { "itemId": "T{t}_CLOTH", "qty": 8 }, + { "itemId": "T{t}_ARTEFACT_MAIN_HOLYSTAFF_MORGANA", "qty": 1, "noEnchant": true } + ] + }, + { + "id": "fallen-staff", + "displayName": "Fallen Staff", + "category": "Weapons", + "outputId": "T{t}_2H_HOLYSTAFF_HELL", + "ingredients": [ + { "itemId": "T{t}_PLANKS", "qty": 20 }, + { "itemId": "T{t}_CLOTH", "qty": 12 }, + { "itemId": "T{t}_ARTEFACT_2H_HOLYSTAFF_HELL", "qty": 1, "noEnchant": true } + ] + }, + { + "id": "redemption-staff", + "displayName": "Redemption Staff", + "category": "Weapons", + "outputId": "T{t}_2H_HOLYSTAFF_UNDEAD", + "ingredients": [ + { "itemId": "T{t}_PLANKS", "qty": 20 }, + { "itemId": "T{t}_CLOTH", "qty": 12 }, + { "itemId": "T{t}_ARTEFACT_2H_HOLYSTAFF_UNDEAD", "qty": 1, "noEnchant": true } + ] + }, + { + "id": "hallowfall", + "displayName": "Hallowfall", + "category": "Weapons", + "outputId": "T{t}_MAIN_HOLYSTAFF_AVALON", + "ingredients": [ + { "itemId": "T{t}_PLANKS", "qty": 16 }, + { "itemId": "T{t}_CLOTH", "qty": 8 }, + { "itemId": "T{t}_ARTEFACT_MAIN_HOLYSTAFF_AVALON", "qty": 1, "noEnchant": true } + ] + }, + { + "id": "exalted-staff", + "displayName": "Exalted Staff", + "category": "Weapons", + "outputId": "T{t}_2H_HOLYSTAFF_CRYSTAL", + "ingredients": [ + { "itemId": "T{t}_PLANKS", "qty": 20 }, + { "itemId": "T{t}_CLOTH", "qty": 12 }, + { "itemId": "T{t}_ARTEFACT_2H_HOLYSTAFF_CRYSTAL", "qty": 1, "noEnchant": true } + ] + }, + { + "comment": "--- Nature Staves (Planks + Cloth) -----------------------------------------" + }, + { + "id": "nature-staff", + "displayName": "Nature Staff", + "category": "Weapons", + "outputId": "T{t}_MAIN_NATURESTAFF", + "ingredients": [ + { "itemId": "T{t}_PLANKS", "qty": 16 }, + { "itemId": "T{t}_CLOTH", "qty": 8 } + ] + }, + { + "id": "great-nature-staff", + "displayName": "Great Nature Staff", + "category": "Weapons", + "outputId": "T{t}_2H_NATURESTAFF", + "ingredients": [ + { "itemId": "T{t}_PLANKS", "qty": 20 }, + { "itemId": "T{t}_CLOTH", "qty": 12 } + ] + }, + { + "id": "wild-staff", + "displayName": "Wild Staff", + "category": "Weapons", + "outputId": "T{t}_2H_WILDSTAFF", + "ingredients": [ + { "itemId": "T{t}_PLANKS", "qty": 20 }, + { "itemId": "T{t}_CLOTH", "qty": 12 } + ] + }, + { + "id": "druidic-staff", + "displayName": "Druidic Staff", + "category": "Weapons", + "outputId": "T{t}_MAIN_NATURESTAFF_KEEPER", + "ingredients": [ + { "itemId": "T{t}_PLANKS", "qty": 16 }, + { "itemId": "T{t}_CLOTH", "qty": 8 }, + { "itemId": "T{t}_ARTEFACT_MAIN_NATURESTAFF_KEEPER", "qty": 1, "noEnchant": true } + ] + }, + { + "id": "blight-staff", + "displayName": "Blight Staff", + "category": "Weapons", + "outputId": "T{t}_2H_NATURESTAFF_HELL", + "ingredients": [ + { "itemId": "T{t}_PLANKS", "qty": 20 }, + { "itemId": "T{t}_CLOTH", "qty": 12 }, + { "itemId": "T{t}_ARTEFACT_2H_NATURESTAFF_HELL", "qty": 1, "noEnchant": true } + ] + }, + { + "id": "rampant-staff", + "displayName": "Rampant Staff", + "category": "Weapons", + "outputId": "T{t}_2H_NATURESTAFF_KEEPER", + "ingredients": [ + { "itemId": "T{t}_PLANKS", "qty": 20 }, + { "itemId": "T{t}_CLOTH", "qty": 12 }, + { "itemId": "T{t}_ARTEFACT_2H_NATURESTAFF_KEEPER", "qty": 1, "noEnchant": true } + ] + }, + { + "id": "ironroot-staff", + "displayName": "Ironroot Staff", + "category": "Weapons", + "outputId": "T{t}_MAIN_NATURESTAFF_AVALON", + "ingredients": [ + { "itemId": "T{t}_PLANKS", "qty": 16 }, + { "itemId": "T{t}_CLOTH", "qty": 8 }, + { "itemId": "T{t}_ARTEFACT_MAIN_NATURESTAFF_AVALON", "qty": 1, "noEnchant": true } + ] + }, + { + "id": "forgebark-staff", + "displayName": "Forgebark Staff", + "category": "Weapons", + "outputId": "T{t}_MAIN_NATURESTAFF_CRYSTAL", + "ingredients": [ + { "itemId": "T{t}_PLANKS", "qty": 16 }, + { "itemId": "T{t}_CLOTH", "qty": 8 }, + { "itemId": "T{t}_ARTEFACT_MAIN_NATURESTAFF_CRYSTAL", "qty": 1, "noEnchant": true } + ] + }, + { + "comment": "--- Shapeshifter Weapons (Planks + Leather + Alchemy) ----------------------" + }, + { + "id": "prowling-staff", + "displayName": "Prowling Staff", + "category": "Weapons", + "outputId": "T{t}_2H_SHAPESHIFTER_SET1", + "ingredients": [ + { "itemId": "T{t}_PLANKS", "qty": 20 }, + { "itemId": "T{t}_LEATHER", "qty": 12 }, + { "itemId": "T{t-1}_ALCHEMY_RARE_PANTHER", "qty": 2, "noEnchant": true } + ] + }, + { + "id": "rootbound-staff", + "displayName": "Rootbound Staff", + "category": "Weapons", + "outputId": "T{t}_2H_SHAPESHIFTER_SET2", + "ingredients": [ + { "itemId": "T{t}_PLANKS", "qty": 20 }, + { "itemId": "T{t}_LEATHER", "qty": 12 }, + { "itemId": "T{t-1}_ALCHEMY_RARE_ENT", "qty": 2, "noEnchant": true } + ] + }, + { + "id": "primal-staff", + "displayName": "Primal Staff", + "category": "Weapons", + "outputId": "T{t}_2H_SHAPESHIFTER_SET3", + "ingredients": [ + { "itemId": "T{t}_PLANKS", "qty": 20 }, + { "itemId": "T{t}_LEATHER", "qty": 12 }, + { "itemId": "T{t-1}_ALCHEMY_RARE_DIREBEAR", "qty": 2, "noEnchant": true } + ] + }, + { + "id": "bloodmoon-staff", + "displayName": "Bloodmoon Staff", + "category": "Weapons", + "outputId": "T{t}_2H_SHAPESHIFTER_MORGANA", + "ingredients": [ + { "itemId": "T{t}_PLANKS", "qty": 20 }, + { "itemId": "T{t}_LEATHER", "qty": 12 }, + { "itemId": "T{t}_ARTEFACT_2H_SHAPESHIFTER_MORGANA", "qty": 1, "noEnchant": true }, + { "itemId": "T{t-1}_ALCHEMY_RARE_WEREWOLF", "qty": 2, "noEnchant": true } + ] + }, + { + "id": "hellspawn-staff", + "displayName": "Hellspawn Staff", + "category": "Weapons", + "outputId": "T{t}_2H_SHAPESHIFTER_HELL", + "ingredients": [ + { "itemId": "T{t}_PLANKS", "qty": 20 }, + { "itemId": "T{t}_LEATHER", "qty": 12 }, + { "itemId": "T{t}_ARTEFACT_2H_SHAPESHIFTER_HELL", "qty": 1, "noEnchant": true }, + { "itemId": "T{t-1}_ALCHEMY_RARE_IMP", "qty": 2, "noEnchant": true } + ] + }, + { + "id": "earthrune-staff", + "displayName": "Earthrune Staff", + "category": "Weapons", + "outputId": "T{t}_2H_SHAPESHIFTER_KEEPER", + "ingredients": [ + { "itemId": "T{t}_PLANKS", "qty": 20 }, + { "itemId": "T{t}_LEATHER", "qty": 12 }, + { "itemId": "T{t}_ARTEFACT_2H_SHAPESHIFTER_KEEPER", "qty": 1, "noEnchant": true }, + { "itemId": "T{t-1}_ALCHEMY_RARE_ELEMENTAL", "qty": 2, "noEnchant": true } + ] + }, + { + "id": "lightcaller", + "displayName": "Lightcaller", + "category": "Weapons", + "outputId": "T{t}_2H_SHAPESHIFTER_AVALON", + "ingredients": [ + { "itemId": "T{t}_PLANKS", "qty": 20 }, + { "itemId": "T{t}_LEATHER", "qty": 12 }, + { "itemId": "T{t}_ARTEFACT_2H_SHAPESHIFTER_AVALON", "qty": 1, "noEnchant": true }, + { "itemId": "T{t-1}_ALCHEMY_RARE_EAGLE", "qty": 2, "noEnchant": true } + ] + }, + { + "id": "stillgaze-staff", + "displayName": "Stillgaze Staff", + "category": "Weapons", + "outputId": "T{t}_2H_SHAPESHIFTER_CRYSTAL", + "ingredients": [ + { "itemId": "T{t}_PLANKS", "qty": 20 }, + { "itemId": "T{t}_LEATHER", "qty": 12 }, + { "itemId": "T{t}_ARTEFACT_2H_SHAPESHIFTER_CRYSTAL", "qty": 1, "noEnchant": true } + ] + }, + { + "comment": "--- Off-hands ---------------------------------------------------------------" + }, + { + "id": "shield", + "displayName": "Shield", + "category": "Weapons", + "outputId": "T{t}_OFF_SHIELD", + "ingredients": [ + { "itemId": "T{t}_PLANKS", "qty": 4 }, + { "itemId": "T{t}_METALBAR", "qty": 4 } + ] + }, + { + "id": "torch", + "displayName": "Torch", + "category": "Weapons", + "outputId": "T{t}_OFF_TORCH", + "ingredients": [ + { "itemId": "T{t}_PLANKS", "qty": 4 }, + { "itemId": "T{t}_CLOTH", "qty": 4 } + ] + }, + { + "id": "tome-of-spells", + "displayName": "Tome of Spells", + "category": "Weapons", + "outputId": "T{t}_OFF_BOOK", + "ingredients": [ + { "itemId": "T{t}_CLOTH", "qty": 4 }, + { "itemId": "T{t}_LEATHER", "qty": 4 } + ] + }, + { + "comment": "--- Plate Armor — sets 1-3 (Bars only) ------------------------------------" + }, + { + "id": "soldier-helmet", + "displayName": "Soldier Helmet", + "category": "Armor", + "outputId": "T{t}_HEAD_PLATE_SET1", + "ingredients": [ + { "itemId": "T{t}_METALBAR", "qty": 8 } + ] + }, + { + "id": "knight-helmet", + "displayName": "Knight Helmet", + "category": "Armor", + "outputId": "T{t}_HEAD_PLATE_SET2", + "ingredients": [ + { "itemId": "T{t}_METALBAR", "qty": 8 } + ] + }, + { + "id": "guardian-helmet", + "displayName": "Guardian Helmet", + "category": "Armor", + "outputId": "T{t}_HEAD_PLATE_SET3", + "ingredients": [ + { "itemId": "T{t}_METALBAR", "qty": 8 } + ] + }, + { + "comment": "--- Plate Armor — artifact variants (Bars + artifact) ----------------------" + }, + { + "id": "graveguard-helmet", + "displayName": "Graveguard Helmet", + "category": "Armor", + "outputId": "T{t}_HEAD_PLATE_UNDEAD", + "ingredients": [ + { "itemId": "T{t}_METALBAR", "qty": 8 }, + { "itemId": "T{t}_ARTEFACT_HEAD_PLATE_UNDEAD", "qty": 1, "noEnchant": true } + ] + }, + { + "id": "demon-helmet", + "displayName": "Demon Helmet", + "category": "Armor", + "outputId": "T{t}_HEAD_PLATE_HELL", + "ingredients": [ + { "itemId": "T{t}_METALBAR", "qty": 8 }, + { "itemId": "T{t}_ARTEFACT_HEAD_PLATE_HELL", "qty": 1, "noEnchant": true } + ] + }, + { + "id": "judicator-helmet", + "displayName": "Judicator Helmet", + "category": "Armor", + "outputId": "T{t}_HEAD_PLATE_KEEPER", + "ingredients": [ + { "itemId": "T{t}_METALBAR", "qty": 8 }, + { "itemId": "T{t}_ARTEFACT_HEAD_PLATE_KEEPER", "qty": 1, "noEnchant": true } + ] + }, + { + "id": "duskweaver-helmet", + "displayName": "Duskweaver Helmet", + "category": "Armor", + "outputId": "T{t}_HEAD_PLATE_FEY", + "ingredients": [ + { "itemId": "T{t}_METALBAR", "qty": 8 }, + { "itemId": "T{t}_ARTEFACT_HEAD_PLATE_FEY", "qty": 1, "noEnchant": true } + ] + }, + { + "id": "helmet-of-valor", + "displayName": "Helmet of Valor", + "category": "Armor", + "outputId": "T{t}_HEAD_PLATE_AVALON", + "ingredients": [ + { "itemId": "T{t}_METALBAR", "qty": 8 }, + { "itemId": "T{t}_ARTEFACT_HEAD_PLATE_AVALON", "qty": 1, "noEnchant": true } + ] + }, + { + "id": "soldier-armor", + "displayName": "Soldier Armor", + "category": "Armor", + "outputId": "T{t}_ARMOR_PLATE_SET1", + "ingredients": [ + { "itemId": "T{t}_METALBAR", "qty": 16 } + ] + }, + { + "id": "knight-armor", + "displayName": "Knight Armor", + "category": "Armor", + "outputId": "T{t}_ARMOR_PLATE_SET2", + "ingredients": [ + { "itemId": "T{t}_METALBAR", "qty": 16 } + ] + }, + { + "id": "guardian-armor", + "displayName": "Guardian Armor", + "category": "Armor", + "outputId": "T{t}_ARMOR_PLATE_SET3", + "ingredients": [ + { "itemId": "T{t}_METALBAR", "qty": 16 } + ] + }, + { + "id": "graveguard-armor", + "displayName": "Graveguard Armor", + "category": "Armor", + "outputId": "T{t}_ARMOR_PLATE_UNDEAD", + "ingredients": [ + { "itemId": "T{t}_METALBAR", "qty": 16 }, + { "itemId": "T{t}_ARTEFACT_ARMOR_PLATE_UNDEAD", "qty": 1, "noEnchant": true } + ] + }, + { + "id": "demon-armor", + "displayName": "Demon Armor", + "category": "Armor", + "outputId": "T{t}_ARMOR_PLATE_HELL", + "ingredients": [ + { "itemId": "T{t}_METALBAR", "qty": 16 }, + { "itemId": "T{t}_ARTEFACT_ARMOR_PLATE_HELL", "qty": 1, "noEnchant": true } + ] + }, + { + "id": "judicator-armor", + "displayName": "Judicator Armor", + "category": "Armor", + "outputId": "T{t}_ARMOR_PLATE_KEEPER", + "ingredients": [ + { "itemId": "T{t}_METALBAR", "qty": 16 }, + { "itemId": "T{t}_ARTEFACT_ARMOR_PLATE_KEEPER", "qty": 1, "noEnchant": true } + ] + }, + { + "id": "duskweaver-armor", + "displayName": "Duskweaver Armor", + "category": "Armor", + "outputId": "T{t}_ARMOR_PLATE_FEY", + "ingredients": [ + { "itemId": "T{t}_METALBAR", "qty": 16 }, + { "itemId": "T{t}_ARTEFACT_ARMOR_PLATE_FEY", "qty": 1, "noEnchant": true } + ] + }, + { + "id": "armor-of-valor", + "displayName": "Armor of Valor", + "category": "Armor", + "outputId": "T{t}_ARMOR_PLATE_AVALON", + "ingredients": [ + { "itemId": "T{t}_METALBAR", "qty": 16 }, + { "itemId": "T{t}_ARTEFACT_ARMOR_PLATE_AVALON", "qty": 1, "noEnchant": true } + ] + }, + { + "id": "soldier-boots", + "displayName": "Soldier Boots", + "category": "Armor", + "outputId": "T{t}_SHOES_PLATE_SET1", + "ingredients": [ + { "itemId": "T{t}_METALBAR", "qty": 8 } + ] + }, + { + "id": "knight-boots", + "displayName": "Knight Boots", + "category": "Armor", + "outputId": "T{t}_SHOES_PLATE_SET2", + "ingredients": [ + { "itemId": "T{t}_METALBAR", "qty": 8 } + ] + }, + { + "id": "guardian-boots", + "displayName": "Guardian Boots", + "category": "Armor", + "outputId": "T{t}_SHOES_PLATE_SET3", + "ingredients": [ + { "itemId": "T{t}_METALBAR", "qty": 8 } + ] + }, + { + "id": "graveguard-boots", + "displayName": "Graveguard Boots", + "category": "Armor", + "outputId": "T{t}_SHOES_PLATE_UNDEAD", + "ingredients": [ + { "itemId": "T{t}_METALBAR", "qty": 8 }, + { "itemId": "T{t}_ARTEFACT_SHOES_PLATE_UNDEAD", "qty": 1, "noEnchant": true } + ] + }, + { + "id": "demon-boots", + "displayName": "Demon Boots", + "category": "Armor", + "outputId": "T{t}_SHOES_PLATE_HELL", + "ingredients": [ + { "itemId": "T{t}_METALBAR", "qty": 8 }, + { "itemId": "T{t}_ARTEFACT_SHOES_PLATE_HELL", "qty": 1, "noEnchant": true } + ] + }, + { + "id": "judicator-boots", + "displayName": "Judicator Boots", + "category": "Armor", + "outputId": "T{t}_SHOES_PLATE_KEEPER", + "ingredients": [ + { "itemId": "T{t}_METALBAR", "qty": 8 }, + { "itemId": "T{t}_ARTEFACT_SHOES_PLATE_KEEPER", "qty": 1, "noEnchant": true } + ] + }, + { + "id": "duskweaver-boots", + "displayName": "Duskweaver Boots", + "category": "Armor", + "outputId": "T{t}_SHOES_PLATE_FEY", + "ingredients": [ + { "itemId": "T{t}_METALBAR", "qty": 8 }, + { "itemId": "T{t}_ARTEFACT_SHOES_PLATE_FEY", "qty": 1, "noEnchant": true } + ] + }, + { + "id": "boots-of-valor", + "displayName": "Boots of Valor", + "category": "Armor", + "outputId": "T{t}_SHOES_PLATE_AVALON", + "ingredients": [ + { "itemId": "T{t}_METALBAR", "qty": 8 }, + { "itemId": "T{t}_ARTEFACT_SHOES_PLATE_AVALON", "qty": 1, "noEnchant": true } + ] + }, + { + "comment": "--- Leather Armor — sets 1-3 (Leather only) --------------------------------" + }, + { + "id": "mercenary-hood", + "displayName": "Mercenary Hood", + "category": "Armor", + "outputId": "T{t}_HEAD_LEATHER_SET1", + "ingredients": [ + { "itemId": "T{t}_LEATHER", "qty": 8 } + ] + }, + { + "id": "hunter-hood", + "displayName": "Hunter Hood", + "category": "Armor", + "outputId": "T{t}_HEAD_LEATHER_SET2", + "ingredients": [ + { "itemId": "T{t}_LEATHER", "qty": 8 } + ] + }, + { + "id": "assassin-hood", + "displayName": "Assassin Hood", + "category": "Armor", + "outputId": "T{t}_HEAD_LEATHER_SET3", + "ingredients": [ + { "itemId": "T{t}_LEATHER", "qty": 8 } + ] + }, + { + "comment": "--- Leather Armor — artifact variants (Leather + artifact) -----------------" + }, + { + "id": "stalker-hood", + "displayName": "Stalker Hood", + "category": "Armor", + "outputId": "T{t}_HEAD_LEATHER_UNDEAD", + "ingredients": [ + { "itemId": "T{t}_LEATHER", "qty": 8 }, + { "itemId": "T{t}_ARTEFACT_HEAD_LEATHER_UNDEAD", "qty": 1, "noEnchant": true } + ] + }, + { + "id": "hellion-hood", + "displayName": "Hellion Hood", + "category": "Armor", + "outputId": "T{t}_HEAD_LEATHER_MORGANA", + "ingredients": [ + { "itemId": "T{t}_LEATHER", "qty": 8 }, + { "itemId": "T{t}_ARTEFACT_HEAD_LEATHER_MORGANA", "qty": 1, "noEnchant": true } + ] + }, + { + "id": "specter-hood", + "displayName": "Specter Hood", + "category": "Armor", + "outputId": "T{t}_HEAD_LEATHER_HELL", + "ingredients": [ + { "itemId": "T{t}_LEATHER", "qty": 8 }, + { "itemId": "T{t}_ARTEFACT_HEAD_LEATHER_HELL", "qty": 1, "noEnchant": true } + ] + }, + { + "id": "mistwalker-hood", + "displayName": "Mistwalker Hood", + "category": "Armor", + "outputId": "T{t}_HEAD_LEATHER_FEY", + "ingredients": [ + { "itemId": "T{t}_LEATHER", "qty": 8 }, + { "itemId": "T{t}_ARTEFACT_HEAD_LEATHER_FEY", "qty": 1, "noEnchant": true } + ] + }, + { + "id": "hood-of-tenacity", + "displayName": "Hood of Tenacity", + "category": "Armor", + "outputId": "T{t}_HEAD_LEATHER_AVALON", + "ingredients": [ + { "itemId": "T{t}_LEATHER", "qty": 8 }, + { "itemId": "T{t}_ARTEFACT_HEAD_LEATHER_AVALON", "qty": 1, "noEnchant": true } + ] + }, + { + "id": "mercenary-jacket", + "displayName": "Mercenary Jacket", + "category": "Armor", + "outputId": "T{t}_ARMOR_LEATHER_SET1", + "ingredients": [ + { "itemId": "T{t}_LEATHER", "qty": 16 } + ] + }, + { + "id": "hunter-jacket", + "displayName": "Hunter Jacket", + "category": "Armor", + "outputId": "T{t}_ARMOR_LEATHER_SET2", + "ingredients": [ + { "itemId": "T{t}_LEATHER", "qty": 16 } + ] + }, + { + "id": "assassin-jacket", + "displayName": "Assassin Jacket", + "category": "Armor", + "outputId": "T{t}_ARMOR_LEATHER_SET3", + "ingredients": [ + { "itemId": "T{t}_LEATHER", "qty": 16 } + ] + }, + { + "id": "stalker-jacket", + "displayName": "Stalker Jacket", + "category": "Armor", + "outputId": "T{t}_ARMOR_LEATHER_UNDEAD", + "ingredients": [ + { "itemId": "T{t}_LEATHER", "qty": 16 }, + { "itemId": "T{t}_ARTEFACT_ARMOR_LEATHER_UNDEAD", "qty": 1, "noEnchant": true } + ] + }, + { + "id": "hellion-jacket", + "displayName": "Hellion Jacket", + "category": "Armor", + "outputId": "T{t}_ARMOR_LEATHER_MORGANA", + "ingredients": [ + { "itemId": "T{t}_LEATHER", "qty": 16 }, + { "itemId": "T{t}_ARTEFACT_ARMOR_LEATHER_MORGANA", "qty": 1, "noEnchant": true } + ] + }, + { + "id": "specter-jacket", + "displayName": "Specter Jacket", + "category": "Armor", + "outputId": "T{t}_ARMOR_LEATHER_HELL", + "ingredients": [ + { "itemId": "T{t}_LEATHER", "qty": 16 }, + { "itemId": "T{t}_ARTEFACT_ARMOR_LEATHER_HELL", "qty": 1, "noEnchant": true } + ] + }, + { + "id": "mistwalker-jacket", + "displayName": "Mistwalker Jacket", + "category": "Armor", + "outputId": "T{t}_ARMOR_LEATHER_FEY", + "ingredients": [ + { "itemId": "T{t}_LEATHER", "qty": 16 }, + { "itemId": "T{t}_ARTEFACT_ARMOR_LEATHER_FEY", "qty": 1, "noEnchant": true } + ] + }, + { + "id": "jacket-of-tenacity", + "displayName": "Jacket of Tenacity", + "category": "Armor", + "outputId": "T{t}_ARMOR_LEATHER_AVALON", + "ingredients": [ + { "itemId": "T{t}_LEATHER", "qty": 16 }, + { "itemId": "T{t}_ARTEFACT_ARMOR_LEATHER_AVALON", "qty": 1, "noEnchant": true } + ] + }, + { + "id": "mercenary-shoes", + "displayName": "Mercenary Shoes", + "category": "Armor", + "outputId": "T{t}_SHOES_LEATHER_SET1", + "ingredients": [ + { "itemId": "T{t}_LEATHER", "qty": 8 } + ] + }, + { + "id": "hunter-shoes", + "displayName": "Hunter Shoes", + "category": "Armor", + "outputId": "T{t}_SHOES_LEATHER_SET2", + "ingredients": [ + { "itemId": "T{t}_LEATHER", "qty": 8 } + ] + }, + { + "id": "assassin-shoes", + "displayName": "Assassin Shoes", + "category": "Armor", + "outputId": "T{t}_SHOES_LEATHER_SET3", + "ingredients": [ + { "itemId": "T{t}_LEATHER", "qty": 8 } + ] + }, + { + "id": "stalker-shoes", + "displayName": "Stalker Shoes", + "category": "Armor", + "outputId": "T{t}_SHOES_LEATHER_UNDEAD", + "ingredients": [ + { "itemId": "T{t}_LEATHER", "qty": 8 }, + { "itemId": "T{t}_ARTEFACT_SHOES_LEATHER_UNDEAD", "qty": 1, "noEnchant": true } + ] + }, + { + "id": "hellion-shoes", + "displayName": "Hellion Shoes", + "category": "Armor", + "outputId": "T{t}_SHOES_LEATHER_MORGANA", + "ingredients": [ + { "itemId": "T{t}_LEATHER", "qty": 8 }, + { "itemId": "T{t}_ARTEFACT_SHOES_LEATHER_MORGANA", "qty": 1, "noEnchant": true } + ] + }, + { + "id": "specter-shoes", + "displayName": "Specter Shoes", + "category": "Armor", + "outputId": "T{t}_SHOES_LEATHER_HELL", + "ingredients": [ + { "itemId": "T{t}_LEATHER", "qty": 8 }, + { "itemId": "T{t}_ARTEFACT_SHOES_LEATHER_HELL", "qty": 1, "noEnchant": true } + ] + }, + { + "id": "mistwalker-shoes", + "displayName": "Mistwalker Shoes", + "category": "Armor", + "outputId": "T{t}_SHOES_LEATHER_FEY", + "ingredients": [ + { "itemId": "T{t}_LEATHER", "qty": 8 }, + { "itemId": "T{t}_ARTEFACT_SHOES_LEATHER_FEY", "qty": 1, "noEnchant": true } + ] + }, + { + "id": "shoes-of-tenacity", + "displayName": "Shoes of Tenacity", + "category": "Armor", + "outputId": "T{t}_SHOES_LEATHER_AVALON", + "ingredients": [ + { "itemId": "T{t}_LEATHER", "qty": 8 }, + { "itemId": "T{t}_ARTEFACT_SHOES_LEATHER_AVALON", "qty": 1, "noEnchant": true } + ] + }, + { + "comment": "--- Cloth Armor — sets 1-3 (Cloth only) ------------------------------------" + }, + { + "id": "scholar-cowl", + "displayName": "Scholar Cowl", + "category": "Armor", + "outputId": "T{t}_HEAD_CLOTH_SET1", + "ingredients": [ + { "itemId": "T{t}_CLOTH", "qty": 8 } + ] + }, + { + "id": "cleric-cowl", + "displayName": "Cleric Cowl", + "category": "Armor", + "outputId": "T{t}_HEAD_CLOTH_SET2", + "ingredients": [ + { "itemId": "T{t}_CLOTH", "qty": 8 } + ] + }, + { + "id": "mage-cowl", + "displayName": "Mage Cowl", + "category": "Armor", + "outputId": "T{t}_HEAD_CLOTH_SET3", + "ingredients": [ + { "itemId": "T{t}_CLOTH", "qty": 8 } + ] + }, + { + "comment": "--- Cloth Armor — artifact variants (Cloth + artifact) ---------------------" + }, + { + "id": "druid-cowl", + "displayName": "Druid Cowl", + "category": "Armor", + "outputId": "T{t}_HEAD_CLOTH_KEEPER", + "ingredients": [ + { "itemId": "T{t}_CLOTH", "qty": 8 }, + { "itemId": "T{t}_ARTEFACT_HEAD_CLOTH_KEEPER", "qty": 1, "noEnchant": true } + ] + }, + { + "id": "fiend-cowl", + "displayName": "Fiend Cowl", + "category": "Armor", + "outputId": "T{t}_HEAD_CLOTH_HELL", + "ingredients": [ + { "itemId": "T{t}_CLOTH", "qty": 8 }, + { "itemId": "T{t}_ARTEFACT_HEAD_CLOTH_HELL", "qty": 1, "noEnchant": true } + ] + }, + { + "id": "cultist-cowl", + "displayName": "Cultist Cowl", + "category": "Armor", + "outputId": "T{t}_HEAD_CLOTH_MORGANA", + "ingredients": [ + { "itemId": "T{t}_CLOTH", "qty": 8 }, + { "itemId": "T{t}_ARTEFACT_HEAD_CLOTH_MORGANA", "qty": 1, "noEnchant": true } + ] + }, + { + "id": "feyscale-cowl", + "displayName": "Feyscale Cowl", + "category": "Armor", + "outputId": "T{t}_HEAD_CLOTH_FEY", + "ingredients": [ + { "itemId": "T{t}_CLOTH", "qty": 8 }, + { "itemId": "T{t}_ARTEFACT_HEAD_CLOTH_FEY", "qty": 1, "noEnchant": true } + ] + }, + { + "id": "cowl-of-purity", + "displayName": "Cowl of Purity", + "category": "Armor", + "outputId": "T{t}_HEAD_CLOTH_AVALON", + "ingredients": [ + { "itemId": "T{t}_CLOTH", "qty": 8 }, + { "itemId": "T{t}_ARTEFACT_HEAD_CLOTH_AVALON", "qty": 1, "noEnchant": true } + ] + }, + { + "id": "scholar-robe", + "displayName": "Scholar Robe", + "category": "Armor", + "outputId": "T{t}_ARMOR_CLOTH_SET1", + "ingredients": [ + { "itemId": "T{t}_CLOTH", "qty": 16 } + ] + }, + { + "id": "cleric-robe", + "displayName": "Cleric Robe", + "category": "Armor", + "outputId": "T{t}_ARMOR_CLOTH_SET2", + "ingredients": [ + { "itemId": "T{t}_CLOTH", "qty": 16 } + ] + }, + { + "id": "mage-robe", + "displayName": "Mage Robe", + "category": "Armor", + "outputId": "T{t}_ARMOR_CLOTH_SET3", + "ingredients": [ + { "itemId": "T{t}_CLOTH", "qty": 16 } + ] + }, + { + "id": "druid-robe", + "displayName": "Druid Robe", + "category": "Armor", + "outputId": "T{t}_ARMOR_CLOTH_KEEPER", + "ingredients": [ + { "itemId": "T{t}_CLOTH", "qty": 16 }, + { "itemId": "T{t}_ARTEFACT_ARMOR_CLOTH_KEEPER", "qty": 1, "noEnchant": true } + ] + }, + { + "id": "fiend-robe", + "displayName": "Fiend Robe", + "category": "Armor", + "outputId": "T{t}_ARMOR_CLOTH_HELL", + "ingredients": [ + { "itemId": "T{t}_CLOTH", "qty": 16 }, + { "itemId": "T{t}_ARTEFACT_ARMOR_CLOTH_HELL", "qty": 1, "noEnchant": true } + ] + }, + { + "id": "cultist-robe", + "displayName": "Cultist Robe", + "category": "Armor", + "outputId": "T{t}_ARMOR_CLOTH_MORGANA", + "ingredients": [ + { "itemId": "T{t}_CLOTH", "qty": 16 }, + { "itemId": "T{t}_ARTEFACT_ARMOR_CLOTH_MORGANA", "qty": 1, "noEnchant": true } + ] + }, + { + "id": "feyscale-robe", + "displayName": "Feyscale Robe", + "category": "Armor", + "outputId": "T{t}_ARMOR_CLOTH_FEY", + "ingredients": [ + { "itemId": "T{t}_CLOTH", "qty": 16 }, + { "itemId": "T{t}_ARTEFACT_ARMOR_CLOTH_FEY", "qty": 1, "noEnchant": true } + ] + }, + { + "id": "robe-of-purity", + "displayName": "Robe of Purity", + "category": "Armor", + "outputId": "T{t}_ARMOR_CLOTH_AVALON", + "ingredients": [ + { "itemId": "T{t}_CLOTH", "qty": 16 }, + { "itemId": "T{t}_ARTEFACT_ARMOR_CLOTH_AVALON", "qty": 1, "noEnchant": true } + ] + }, + { + "id": "scholar-sandals", + "displayName": "Scholar Sandals", + "category": "Armor", + "outputId": "T{t}_SHOES_CLOTH_SET1", + "ingredients": [ + { "itemId": "T{t}_CLOTH", "qty": 8 } + ] + }, + { + "id": "cleric-sandals", + "displayName": "Cleric Sandals", + "category": "Armor", + "outputId": "T{t}_SHOES_CLOTH_SET2", + "ingredients": [ + { "itemId": "T{t}_CLOTH", "qty": 8 } + ] + }, + { + "id": "mage-sandals", + "displayName": "Mage Sandals", + "category": "Armor", + "outputId": "T{t}_SHOES_CLOTH_SET3", + "ingredients": [ + { "itemId": "T{t}_CLOTH", "qty": 8 } + ] + }, + { + "id": "druid-sandals", + "displayName": "Druid Sandals", + "category": "Armor", + "outputId": "T{t}_SHOES_CLOTH_KEEPER", + "ingredients": [ + { "itemId": "T{t}_CLOTH", "qty": 8 }, + { "itemId": "T{t}_ARTEFACT_SHOES_CLOTH_KEEPER", "qty": 1, "noEnchant": true } + ] + }, + { + "id": "fiend-sandals", + "displayName": "Fiend Sandals", + "category": "Armor", + "outputId": "T{t}_SHOES_CLOTH_HELL", + "ingredients": [ + { "itemId": "T{t}_CLOTH", "qty": 8 }, + { "itemId": "T{t}_ARTEFACT_SHOES_CLOTH_HELL", "qty": 1, "noEnchant": true } + ] + }, + { + "id": "cultist-sandals", + "displayName": "Cultist Sandals", + "category": "Armor", + "outputId": "T{t}_SHOES_CLOTH_MORGANA", + "ingredients": [ + { "itemId": "T{t}_CLOTH", "qty": 8 }, + { "itemId": "T{t}_ARTEFACT_SHOES_CLOTH_MORGANA", "qty": 1, "noEnchant": true } + ] + }, + { + "id": "feyscale-sandals", + "displayName": "Feyscale Sandals", + "category": "Armor", + "outputId": "T{t}_SHOES_CLOTH_FEY", + "ingredients": [ + { "itemId": "T{t}_CLOTH", "qty": 8 }, + { "itemId": "T{t}_ARTEFACT_SHOES_CLOTH_FEY", "qty": 1, "noEnchant": true } + ] + }, + { + "id": "sandals-of-purity", + "displayName": "Sandals of Purity", + "category": "Armor", + "outputId": "T{t}_SHOES_CLOTH_AVALON", + "ingredients": [ + { "itemId": "T{t}_CLOTH", "qty": 8 }, + { "itemId": "T{t}_ARTEFACT_SHOES_CLOTH_AVALON", "qty": 1, "noEnchant": true } + ] + }, + { + "comment": "--- Gathering Tools (Planks + Bars) ----------------------------------------" + }, + { + "id": "pickaxe", + "displayName": "Pickaxe", + "category": "Gathering", + "enchanted": false, + "outputId": "T{t}_2H_TOOL_PICK", + "ingredients": [ + { "itemId": "T{t}_PLANKS", "qty": 6 }, + { "itemId": "T{t}_METALBAR", "qty": 2 } + ] + }, + { + "id": "sickle", + "displayName": "Sickle", + "category": "Gathering", + "enchanted": false, + "outputId": "T{t}_2H_TOOL_SICKLE", + "ingredients": [ + { "itemId": "T{t}_PLANKS", "qty": 6 }, + { "itemId": "T{t}_METALBAR", "qty": 2 } + ] + }, + { + "id": "skinning-knife", + "displayName": "Skinning Knife", + "category": "Gathering", + "enchanted": false, + "outputId": "T{t}_2H_TOOL_KNIFE", + "ingredients": [ + { "itemId": "T{t}_PLANKS", "qty": 6 }, + { "itemId": "T{t}_METALBAR", "qty": 2 } + ] + }, + { + "id": "lumberjack-axe", + "displayName": "Lumberjack Axe", + "category": "Gathering", + "enchanted": false, + "outputId": "T{t}_2H_TOOL_AXE", + "ingredients": [ + { "itemId": "T{t}_PLANKS", "qty": 6 }, + { "itemId": "T{t}_METALBAR", "qty": 2 } + ] + }, + { + "id": "stonesmasher", + "displayName": "Stonesmasher", + "category": "Gathering", + "enchanted": false, + "outputId": "T{t}_2H_TOOL_HAMMER", + "ingredients": [ + { "itemId": "T{t}_PLANKS", "qty": 6 }, + { "itemId": "T{t}_METALBAR", "qty": 2 } + ] + } +] diff --git a/src/data/recipes.ts b/src/data/recipes.ts new file mode 100644 index 0000000..835d0ef --- /dev/null +++ b/src/data/recipes.ts @@ -0,0 +1,80 @@ +import type { CraftingRecipe, Tier, Enchantment, ItemCategory, JournalType } from '../types/crafting' +import { ENCHANTMENTS } from './constants' +import rawRecipes from './recipes.json' + +// ─── Template types ─────────────────────────────────────────────────────────── + +interface IngredientTemplate { + itemId: string + qty: number | Record + noEnchant?: boolean +} + +interface RecipeTemplate { + id: string + displayName: string + category: ItemCategory + outputId: string + enchanted?: boolean + ingredients: IngredientTemplate[] +} + +const ALL_TIERS: Tier[] = [4, 5, 6, 7, 8] + +// ─── Journal type inference ──────────────────────────────────────────────────── + +function inferJournalType(outputId: string): JournalType { + if (outputId.includes('_TOOL_')) return 'toolmaker' + if (/FIRESTAFF|INFERNOSTAFF|ARCANESTAFF|ENIGMATICSTAFF|CURSEDSTAFF|DEMONICSTAFF|FROSTSTAFF|GLACIALSTAFF|HOLYSTAFF|DIVINESTAFF|NATURESTAFF|WILDSTAFF|_CLOTH_|OFF_BOOK|ENIGMATICORB|RINGPAIR|SKULLORB|ICEGAUNTLETS|ICECRYSTAL/.test(outputId)) return 'mage' + if (/BOW|_LEATHER_|MAIN_DAGGER|DAGGERPAIR|CLAWPAIR|OFF_TORCH|SHAPESHIFTER|RAPIER|DUALSICKLE|2H_DAGGER/.test(outputId)) return 'hunter' + return 'warrior' +} + +// ─── Helpers ────────────────────────────────────────────────────────────────── + +function applyTier(template: string, tier: number): string { + return template + .replace(/\{t-1\}/g, String(tier - 1)) + .replace(/\{t\}/g, String(tier)) +} + +function resolveQty(qty: number | Record, tier: number): number { + if (typeof qty === 'number') return qty + const val = qty[String(tier)] + if (val === undefined) throw new Error(`No qty defined for tier ${tier}`) + return val +} + +// ─── Build all recipes ──────────────────────────────────────────────────────── + +export const ALL_RECIPES: CraftingRecipe[] = [] + +for (const entry of rawRecipes) { + if (!('outputId' in entry)) continue // skip comment-only entries + const template = entry as unknown as RecipeTemplate + + const enchants: Enchantment[] = template.enchanted === false ? [0] : ENCHANTMENTS + + for (const tier of ALL_TIERS) { + for (const enc of enchants) { + const baseOutputId = applyTier(template.outputId, tier) + const outputItemId = enc === 0 ? baseOutputId : `${baseOutputId}@${enc}` + + const ingredients = template.ingredients.map(ing => { + const baseId = applyTier(ing.itemId, tier) + const itemId = (ing.noEnchant === true || enc === 0) ? baseId : `${baseId}_LEVEL${enc}` + return { itemId, quantity: resolveQty(ing.qty, tier) } + }) + + ALL_RECIPES.push({ + outputItemId, + displayName: `T${tier}.${enc} ${template.displayName}`, + tier, + enchantment: enc, + category: template.category, + journalType: inferJournalType(template.outputId), + ingredients, + }) + } + } +} diff --git a/src/env.d.ts b/src/env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/src/env.d.ts @@ -0,0 +1 @@ +/// diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..2425c0f --- /dev/null +++ b/src/main.ts @@ -0,0 +1,5 @@ +import { createApp } from 'vue' +import './style.css' +import App from './App.vue' + +createApp(App).mount('#app') diff --git a/src/pages/PricesPage.vue b/src/pages/PricesPage.vue new file mode 100644 index 0000000..3552ece --- /dev/null +++ b/src/pages/PricesPage.vue @@ -0,0 +1,115 @@ + + + diff --git a/src/pages/ProductionPage.vue b/src/pages/ProductionPage.vue new file mode 100644 index 0000000..68016e1 --- /dev/null +++ b/src/pages/ProductionPage.vue @@ -0,0 +1,286 @@ + + + diff --git a/src/style.css b/src/style.css new file mode 100644 index 0000000..b5c61c9 --- /dev/null +++ b/src/style.css @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; diff --git a/src/types/api.ts b/src/types/api.ts new file mode 100644 index 0000000..04dbfb0 --- /dev/null +++ b/src/types/api.ts @@ -0,0 +1,30 @@ +export interface AlbionPriceEntry { + item_id: string + city: string + quality: number + sell_price_min: number + sell_price_min_date: string + sell_price_max: number + buy_price_min: number + buy_price_max: number +} + +export type PriceCache = Map + +export interface ManualPriceEntry { + sell_price_min: number + editedAt: string // ISO date string +} + +export type ManualPriceCache = Map + +export type AlbionCity = + | 'Thetford' + | 'Caerleon' + | 'Bridgewatch' + | 'Fort Sterling' + | 'Lymhurst' + | 'Martlock' + | 'Black Market' + +export type AlbionQuality = 1 | 2 | 3 | 4 | 5 diff --git a/src/types/crafting.ts b/src/types/crafting.ts new file mode 100644 index 0000000..71b1e8b --- /dev/null +++ b/src/types/crafting.ts @@ -0,0 +1,47 @@ +export type Tier = 4 | 5 | 6 | 7 | 8 +export type Enchantment = 0 | 1 | 2 | 3 | 4 + +export type ItemCategory = 'Weapons' | 'Armor' | 'Gathering' + +export type JournalType = 'warrior' | 'mage' | 'hunter' | 'toolmaker' + +export type SortField = 'materialCost' | 'displayName' | 'tier' | 'variantType' + +export type SortDirection = 'asc' | 'desc' + +export interface SortState { + field: SortField + direction: SortDirection +} + +export interface Ingredient { + itemId: string + quantity: number +} + +export interface CraftingRecipe { + outputItemId: string + displayName: string + tier: Tier + enchantment: Enchantment + category: ItemCategory + journalType: JournalType + ingredients: Ingredient[] +} + +export interface IngredientBreakdown { + itemId: string + displayName: string + quantity: number + unitPrice: number + totalCost: number +} + +export interface ProfitResult { + recipe: CraftingRecipe + materialCost: number + effectiveMaterialCost: number + priceAgeMs: number | null // null = missing prices; ms since oldest manual entry + missingPrices: boolean + ingredientBreakdown: IngredientBreakdown[] +} diff --git a/src/types/filters.ts b/src/types/filters.ts new file mode 100644 index 0000000..df6a647 --- /dev/null +++ b/src/types/filters.ts @@ -0,0 +1,11 @@ +import type { AlbionCity } from './api' +import type { Tier, Enchantment } from './crafting' + +export interface FilterState { + city: AlbionCity + tiers: Set + selectedItemTypes: Set | null + rrr: number + nameFilter: string + enchantments: Set | null +} diff --git a/src/utils/formatting.ts b/src/utils/formatting.ts new file mode 100644 index 0000000..592ac7d --- /dev/null +++ b/src/utils/formatting.ts @@ -0,0 +1,140 @@ +const ITEM_NAME_MAP: Record = { + METALBAR: 'Metal Bar', + PLANKS: 'Planks', + LEATHER: 'Leather', + CLOTH: 'Cloth', + MEAT: 'Meat', + GRAIN: 'Grain', + VEGETABLE: 'Vegetable', + FISH: 'Fish', + HIDE: 'Hide', + PLANT: 'Plant', + FIBER: 'Fiber', + ROCK: 'Rock', + ORE: 'Ore', + ARTEFACT_2H_LONGBOW_UNDEAD: 'Ghastly Arrows', + ARTEFACT_2H_BOW_HELL: 'Demonic Arrowheads', + ARTEFACT_2H_BOW_KEEPER: 'Carved Bone', + ARTEFACT_2H_BOW_AVALON: 'Immaculately Crafted Riser', + ARTEFACT_2H_BOW_CRYSTAL: 'Windborne Crystal', + ARTEFACT_MAIN_SCIMITAR_MORGANA: 'Bloodforged Blade', + ARTEFACT_2H_CLEAVER_HELL: 'Demonic Blade', + ARTEFACT_2H_DUALSCIMITAR_UNDEAD: 'Cursed Blades', + ARTEFACT_2H_CLAYMORE_AVALON: 'Remnants of the Old King', + ARTEFACT_MAIN_SWORD_CRYSTAL: 'Infinite Crystal', + ARTEFACT_2H_HALBERD_MORGANA: 'Morgana Halberd Head', + ARTEFACT_2H_SCYTHE_HELL: 'Hellish Sicklehead', + ARTEFACT_2H_DUALAXE_KEEPER: 'Keeper Axeheads', + ARTEFACT_2H_AXE_AVALON: 'Avalonian Battle Memoir', + ARTEFACT_2H_SCYTHE_CRYSTAL: 'Edged Crystal', + ARTEFACT_2H_REPEATINGCROSSBOW_UNDEAD: 'Lost Crossbow Mechanism', +} + +export function formatItemId(itemId: string): string { + // Handle @N suffix (crafted items) + let base = itemId + let encSuffix = '' + const atMatch = base.match(/^(.+)@(\d+)$/) + if (atMatch) { + base = atMatch[1]! + encSuffix = `.${atMatch[2]}` + } + + // Handle _LEVELН suffix (enchanted ingredients) + const levelMatch = base.match(/^(T\d+_.+?)_LEVEL(\d+)$/) + if (levelMatch) { + base = levelMatch[1]! + encSuffix = `.${levelMatch[2]}` + } + + const match = base.match(/^(T\d+)_(.+)$/) + if (!match) return itemId + const tier = match[1]! + const key = match[2]! + const name = ITEM_NAME_MAP[key] ?? key + return `${tier}${encSuffix} ${name}` +} + +export function itemImageUrl(itemId: string): string { + return `https://render.albiononline.com/v1/item/${itemId}?quality=1` +} + +// Background + text for enchantment buttons/chips +export function enchantStyle(enc: number): { backgroundColor: string; color: string } { + const map: Record = { + 0: ['#6b7280', '#f3f4f6'], + 1: ['#3E8759', '#ffffff'], + 2: ['#2F828F', '#ffffff'], + 3: ['#644F8B', '#ffffff'], + 4: ['#857D47', '#ffffff'], + } + const [bg, text] = map[enc] ?? ['#6b7280', '#f3f4f6'] + return { backgroundColor: bg, color: text } +} + +// Text color only, for labels +export function enchantTextStyle(enc: number): { color: string } { + const map: Record = { + 0: '#9ca3af', + 1: '#3E8759', + 2: '#2F828F', + 3: '#644F8B', + 4: '#857D47', + } + return { color: map[enc] ?? '#9ca3af' } +} + +export function tierEnchantStyle(tier: number, enchantment: number): object { + const base = tierStyle(tier) + if (enchantment === 0) return base + return { + ...base, + outline: `2px solid ${enchantStyle(enchantment).backgroundColor}`, + outlineOffset: '2px', + } +} + +export function tierStyle(tier: number): { backgroundColor: string; color: string } { + const map: Record = { + 4: ['#355f78', '#ffffff'], + 5: ['#77221a', '#ffffff'], + 6: ['#c9712c', '#ffffff'], + 7: ['#d1b044', '#1a1a1a'], + 8: ['#d0d0d0', '#1a1a1a'], + } + const [bg, text] = map[tier] ?? ['#484047', '#ffffff'] + return { backgroundColor: bg, color: text } +} + +export function formatSilver(value: number): string { + if (value === 0) return '0' + if (Math.abs(value) >= 1_000_000) { + return (value / 1_000_000).toFixed(2) + 'M' + } + if (Math.abs(value) >= 1_000) { + return (value / 1_000).toFixed(1) + 'k' + } + return value.toLocaleString() +} + +export function formatRoi(roi: number): string { + return roi.toFixed(1) + '%' +} + +export function formatLastUpdated(date: Date | null): string { + if (!date) return 'Never' + const now = new Date() + const diffMs = now.getTime() - date.getTime() + const diffS = Math.floor(diffMs / 1000) + if (diffS < 60) return `${diffS}s ago` + const diffM = Math.floor(diffS / 60) + if (diffM < 60) return `${diffM}m ago` + const diffH = Math.floor(diffM / 60) + return `${diffH}h ago` +} + +export function formatCountdown(seconds: number): string { + const m = Math.floor(seconds / 60) + const s = seconds % 60 + return `${m}:${s.toString().padStart(2, '0')}` +} diff --git a/tailwind.config.ts b/tailwind.config.ts new file mode 100644 index 0000000..40db953 --- /dev/null +++ b/tailwind.config.ts @@ -0,0 +1,13 @@ +import type { Config } from 'tailwindcss' + +export default { + darkMode: 'class', + content: [ + './index.html', + './src/**/*.{vue,ts,tsx}', + ], + theme: { + extend: {}, + }, + plugins: [], +} satisfies Config diff --git a/tsconfig.app.json b/tsconfig.app.json new file mode 100644 index 0000000..8d16e42 --- /dev/null +++ b/tsconfig.app.json @@ -0,0 +1,16 @@ +{ + "extends": "@vue/tsconfig/tsconfig.dom.json", + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "types": ["vite/client"], + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"] +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..1ffef60 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/tsconfig.node.json b/tsconfig.node.json new file mode 100644 index 0000000..8a67f62 --- /dev/null +++ b/tsconfig.node.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "ES2023", + "lib": ["ES2023"], + "module": "ESNext", + "types": ["node"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..b53d435 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [vue()], + server: { + host: true, + }, +})