Fetches better item data, adds spec selector

This commit is contained in:
Drew Haven 2025-08-25 10:10:35 -07:00
parent b5ae96870f
commit 181805fcab
30 changed files with 4864 additions and 592 deletions

View File

@ -7,7 +7,9 @@
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview"
"preview": "vite preview",
"fetch-items": "node scripts/fetch-item-data.js",
"fetch-items-enhanced": "node scripts/fetch-items-enhanced.js"
},
"dependencies": {
"@headlessui/react": "^2.2.7",

117
scripts/README.md Normal file
View File

@ -0,0 +1,117 @@
# WoW Item Data Fetching Scripts
This directory contains scripts to fetch World of Warcraft item data from various APIs.
## Scripts
### `fetch-item-data.js` - Basic Item Fetcher
A simple script that fetches item information from Wowhead API.
**Usage:**
```bash
# Fetch specific items by ID
npm run fetch-items 242494 242495 242481
# Fetch data for all existing items in drop files
npm run fetch-items --existing
```
**Features:**
- Fetches item name, slot, icon from Wowhead
- Supports both XML and JSON Wowhead APIs
- Rate limiting to avoid overwhelming servers
- Saves results to JSON file
### `fetch-items-enhanced.js` - Advanced Item Fetcher
Enhanced script with multiple API sources and additional features.
**Usage:**
```bash
# Basic usage
npm run fetch-items-enhanced 242494 242495 242481
# Fetch and validate existing items
npm run fetch-items-enhanced --existing --validate
# Generate TypeScript code for new dungeon
npm run fetch-items-enhanced --generate "new-dungeon" 123456 123457
# Use only specific API
npm run fetch-items-enhanced --wowhead-only 242494
npm run fetch-items-enhanced --blizzard-only 242494
```
**Features:**
- Multiple API sources (Wowhead + Blizzard)
- Data validation against existing project data
- TypeScript code generation
- Detailed reporting and analysis
- Fallback mechanisms
### `blizzard-api.js` - Blizzard API Client
Handles authentication and data fetching from Blizzard's official API.
**Setup for Blizzard API:**
1. Create an application at https://develop.battle.net/
2. Get your Client ID and Client Secret
3. Set environment variables:
```bash
export BLIZZARD_CLIENT_ID="your_client_id"
export BLIZZARD_CLIENT_SECRET="your_client_secret"
```
**Features:**
- OAuth2 token management
- Item data and media fetching
- Detailed item metadata
## Data Sources
### Wowhead API
- **Pros:** Reliable, no authentication required, good icon data
- **Cons:** Unofficial API, rate limiting needed
- **Best for:** Basic item info, icons, quick lookups
### Blizzard API
- **Pros:** Official API, detailed metadata, reliable
- **Cons:** Requires authentication setup, more complex
- **Best for:** Detailed item data, official information
## Output Files
- `fetched-items.json` - Basic fetch results
- `fetched-items-detailed-{timestamp}.json` - Enhanced fetch results with all API responses
- `{source-name}.ts` - Generated TypeScript code for new item sources
## Examples
### Fetch a few specific items
```bash
npm run fetch-items 242494 242495 242481
```
### Validate all existing project items
```bash
npm run fetch-items-enhanced --existing --validate
```
### Create a new dungeon data file
```bash
npm run fetch-items-enhanced --generate "mists-of-tirna-scithe" 178692 178693 178694
```
### Use only Wowhead (no Blizzard setup needed)
```bash
npm run fetch-items-enhanced --wowhead-only --existing
```
## Rate Limiting
Both scripts include rate limiting (1 second between requests) to be respectful to the APIs. For large batches of items, expect the process to take some time.
## Error Handling
The scripts will continue processing even if individual items fail to fetch. Check the console output and generated files for detailed results and any errors encountered.

241
scripts/blizzard-api.js Normal file
View File

@ -0,0 +1,241 @@
#!/usr/bin/env node
import https from 'https';
/**
* Blizzard API client for fetching item data
* To use this, you'll need to:
* 1. Create an app at https://develop.battle.net/
* 2. Get your client ID and client secret
* 3. Set environment variables: BLIZZARD_CLIENT_ID and BLIZZARD_CLIENT_SECRET
*/
let accessToken = null;
let tokenExpiry = null;
/**
* Gets an access token from Blizzard API
* @returns {Promise<string|null>} Access token or null if failed
*/
async function getAccessToken() {
const clientId = process.env.BLIZZARD_CLIENT_ID;
const clientSecret = process.env.BLIZZARD_CLIENT_SECRET;
if (!clientId || !clientSecret) {
console.error('Blizzard API credentials not found. Please set BLIZZARD_CLIENT_ID and BLIZZARD_CLIENT_SECRET environment variables.');
return null;
}
// Check if we have a valid token
if (accessToken && tokenExpiry && Date.now() < tokenExpiry) {
return accessToken;
}
return new Promise((resolve) => {
const auth = Buffer.from(`${clientId}:${clientSecret}`).toString('base64');
const postData = 'grant_type=client_credentials';
const options = {
hostname: 'oauth.battle.net',
port: 443,
path: '/token',
method: 'POST',
headers: {
'Authorization': `Basic ${auth}`,
'Content-Type': 'application/x-www-form-urlencoded',
'Content-Length': Buffer.byteLength(postData)
}
};
const req = https.request(options, (res) => {
let data = '';
res.on('data', (chunk) => {
data += chunk;
});
res.on('end', () => {
try {
const response = JSON.parse(data);
if (response.access_token) {
accessToken = response.access_token;
tokenExpiry = Date.now() + (response.expires_in * 1000);
resolve(accessToken);
} else {
console.error('Failed to get access token:', response);
resolve(null);
}
} catch (error) {
console.error('Error parsing token response:', error.message);
resolve(null);
}
});
});
req.on('error', (error) => {
console.error('Error getting access token:', error.message);
resolve(null);
});
req.write(postData);
req.end();
});
}
/**
* Fetches item data from Blizzard API
* @param {number} itemId - The item ID to fetch
* @param {string} region - The region (default: 'us')
* @returns {Promise<Object|null>} Item data or null if failed
*/
async function fetchFromBlizzard(itemId, region = 'us') {
const token = await getAccessToken();
if (!token) {
return null;
}
return new Promise((resolve) => {
const options = {
hostname: `${region}.api.blizzard.com`,
port: 443,
path: `/data/wow/item/${itemId}?namespace=static-${region}&locale=en_US&access_token=${token}`,
method: 'GET'
};
https.get(options, (res) => {
let data = '';
res.on('data', (chunk) => {
data += chunk;
});
res.on('end', () => {
try {
const response = JSON.parse(data);
if (response.name) {
const itemData = {
id: itemId,
name: response.name,
slot: mapSlotFromBlizzard(response.inventory_type?.type),
quality: response.quality?.name?.toLowerCase(),
icon: response.media ? `blizzard-${response.media.id}` : null,
source: 'blizzard',
blizzardData: {
itemClass: response.item_class?.name,
itemSubclass: response.item_subclass?.name,
inventoryType: response.inventory_type?.name,
bindType: response.bind_type?.name,
level: response.level,
requiredLevel: response.required_level
}
};
resolve(itemData);
} else {
console.log(`No data found for item ${itemId} on Blizzard API`);
resolve(null);
}
} catch (error) {
console.error(`Error parsing Blizzard data for item ${itemId}:`, error.message);
resolve(null);
}
});
}).on('error', (error) => {
console.error(`Error fetching from Blizzard for item ${itemId}:`, error.message);
resolve(null);
});
});
}
/**
* Maps Blizzard inventory types to our slot names
* @param {string} inventoryType - Blizzard inventory type
* @returns {string|null} Our slot name or null
*/
function mapSlotFromBlizzard(inventoryType) {
const typeMap = {
'HEAD': 'head',
'NECK': 'neck',
'SHOULDER': 'shoulders',
'BODY': 'chest',
'CHEST': 'chest',
'WAIST': 'waist',
'LEGS': 'legs',
'FEET': 'feet',
'WRIST': 'wrist',
'HAND': 'hands',
'FINGER': 'finger',
'TRINKET': 'trinket',
'WEAPON': '1h-weapon',
'SHIELD': 'off-hand',
'RANGED': '2h-weapon',
'CLOAK': 'back',
'WEAPON_2H': '2h-weapon',
'WEAPON_MAIN_HAND': '1h-weapon',
'WEAPON_OFF_HAND': 'off-hand',
'HOLDABLE': 'off-hand',
'AMMO': null,
'THROWN': '1h-weapon',
'RANGED_RIGHT': '2h-weapon',
'QUIVER': null,
'RELIC': 'trinket'
};
return typeMap[inventoryType] || null;
}
/**
* Fetches item media (icon) from Blizzard API
* @param {number} itemId - The item ID
* @param {string} region - The region (default: 'us')
* @returns {Promise<string|null>} Icon URL or null if failed
*/
async function fetchItemMedia(itemId, region = 'us') {
const token = await getAccessToken();
if (!token) {
return null;
}
return new Promise((resolve) => {
const options = {
hostname: `${region}.api.blizzard.com`,
port: 443,
path: `/data/wow/media/item/${itemId}?namespace=static-${region}&locale=en_US&access_token=${token}`,
method: 'GET'
};
https.get(options, (res) => {
let data = '';
res.on('data', (chunk) => {
data += chunk;
});
res.on('end', () => {
try {
const response = JSON.parse(data);
if (response.assets && response.assets.length > 0) {
const iconAsset = response.assets.find(asset => asset.key === 'icon');
resolve(iconAsset ? iconAsset.value : null);
} else {
resolve(null);
}
} catch (error) {
console.error(`Error parsing media data for item ${itemId}:`, error.message);
resolve(null);
}
});
}).on('error', (error) => {
console.error(`Error fetching media for item ${itemId}:`, error.message);
resolve(null);
});
});
}
export {
fetchFromBlizzard,
fetchItemMedia,
getAccessToken,
mapSlotFromBlizzard
};

313
scripts/fetch-item-data.js Normal file
View File

@ -0,0 +1,313 @@
#!/usr/bin/env node
import https from "https";
import fs from "fs/promises";
import path from "path";
/**
* Fetches item data from Wowhead API
* @param {number} itemId - The item ID to fetch
* @returns {Promise<Object|null>} Item data or null if failed
*/
async function fetchFromWowhead(itemId, source) {
return new Promise((resolve) => {
const url = `https://www.wowhead.com/item=${itemId}&xml`;
https
.get(url, (res) => {
let data = "";
res.on("data", (chunk) => {
data += chunk;
});
res.on("end", () => {
try {
// Weirdly, the JSON in the XML has more data than the XML itself or the JSON API.
const jsonString = data.match(/<json><!\[CDATA\[(.*)\]\]><\/json>/);
const jsonData = JSON.parse(`{${jsonString[1]}}`);
// Parse the XML response from Wowhead
// const nameMatch = data.match(/<name><!\[CDATA\[(.*?)\]\]><\/name>/);
const iconMatch = data.match(
/<icon displayId="(\d+)">(.*?)<\/icon>/,
);
const slotMatch = data.match(
/<inventorySlot id="(\d+)">(.*?)<\/inventorySlot>/,
);
const name = jsonData.displayName;
const itemData = {
id: itemId,
name,
icon: iconMatch ? iconMatch[2] : null,
slot: mapSlotFromWowhead(jsonData.slot),
specs: jsonData.specs || [],
source,
};
resolve(itemData);
} catch (error) {
console.error(
`Error parsing Wowhead data for item ${itemId}:`,
error.message,
);
resolve(null);
}
});
})
.on("error", (error) => {
console.error(
`Error fetching from Wowhead for item ${itemId}:`,
error.message,
);
resolve(null);
});
});
}
/**
* Fetches item data from Wowhead's JSON API (alternative method)
* @param {number} itemId - The item ID to fetch
* @returns {Promise<Object|null>} Item data or null if failed
*/
async function fetchFromWowheadJson(itemId) {
return new Promise((resolve) => {
const url = `https://nether.wowhead.com/tooltip/item/${itemId}`;
https
.get(url, (res) => {
let data = "";
res.on("data", (chunk) => {
data += chunk;
});
res.on("end", () => {
try {
const jsonData = JSON.parse(data);
if (jsonData && jsonData.name) {
const itemData = {
id: itemId,
name: jsonData.name,
icon: jsonData.icon || null,
slot: mapSlotFromWowhead(jsonData.slot || 0),
};
resolve(itemData);
} else {
resolve(null);
}
} catch (error) {
console.error(
`Error parsing Wowhead JSON data for item ${itemId}:`,
error.message,
);
resolve(null);
}
});
})
.on("error", (error) => {
console.error(
`Error fetching from Wowhead JSON for item ${itemId}:`,
error.message,
);
resolve(null);
});
});
}
/**
* Maps Wowhead slot numbers to our slot names
* @param {number} slotId - Wowhead slot ID
* @returns {string|null} Our slot name or null
*/
function mapSlotFromWowhead(slotId) {
const slotMap = {
1: "head",
2: "neck",
3: "shoulders",
5: "chest",
6: "waist",
7: "legs",
8: "feet",
9: "wrist",
10: "hands",
11: "finger",
12: "trinket",
13: "1h-weapon",
14: "off-hand",
15: "back",
16: "1h-weapon", // Main hand
17: "2h-weapon",
21: "1h-weapon", // One-handed
22: "off-hand",
23: "off-hand", // Held in off-hand
25: "1h-weapon", // Thrown
};
return slotMap[slotId] || null;
}
/**
* Fetches multiple items with rate limiting
* @param {number[]} itemIds - Array of item IDs to fetch
* @param {number} delay - Delay between requests in ms
* @returns {Promise<Object[]>} Array of item data
*/
async function fetchMultipleItems(source, itemIds, delay = 1000) {
const results = [];
console.log(`Fetching data for ${itemIds.length} items...`);
for (let i = 0; i < itemIds.length; i++) {
const itemId = itemIds[i];
console.log(`Fetching item ${itemId} (${i + 1}/${itemIds.length})`);
// Try Wowhead JSON API first, fallback to XML
let itemData = await fetchFromWowhead(itemId, source);
if (!itemData) {
itemData = await fetchFromWowheadJson(itemId, source);
}
if (itemData) {
results.push(itemData);
console.log(
`✓ Found: ${itemData.name} (${itemData.slot || "unknown slot"})`,
);
} else {
console.log(`✗ Failed to fetch data for item ${itemId}`);
}
// Rate limiting
if (i < itemIds.length - 1) {
await new Promise((resolve) => setTimeout(resolve, delay));
}
}
return results;
}
/**
* Reads item IDs from existing drop files
* @returns {Promise<number[]>} Array of item IDs
*/
async function getExistingItemIds() {
const dropsDir = path.join(process.cwd(), "src", "lib", "drops");
const itemIds = [];
try {
const files = await fs.readdir(dropsDir);
for (const file of files) {
if (file.endsWith(".ts") && file !== "index.ts") {
const filePath = path.join(dropsDir, file);
const content = await fs.readFile(filePath, "utf-8");
// Extract item IDs using regex
const idMatches = content.match(/id: (\d+) as ItemId/g);
if (idMatches) {
for (const match of idMatches) {
const id = parseInt(match.match(/id: (\d+)/)[1]);
itemIds.push(id);
}
}
}
}
} catch (error) {
console.error("Error reading existing item IDs:", error.message);
}
return [...new Set(itemIds)]; // Remove duplicates
}
/**
* Saves fetched data to a JSON file
* @param {Object[]} items - Array of item data
* @param {string} filename - Output filename
*/
async function saveToFile(items, filename = "fetched-items.json") {
try {
const outputPath = path.join(process.cwd(), filename);
await fs.writeFile(outputPath, JSON.stringify(items, null, 2));
console.log(`\nSaved ${items.length} items to ${filename}`);
} catch (error) {
console.error("Error saving file:", error.message);
}
}
/**
* Main function
*/
async function main() {
const args = process.argv.slice(2);
if (args.length === 0) {
console.log(
"Usage: npm run fetch-items [item1] [item2] ... or npm run fetch-items --existing",
);
console.log("Examples:");
console.log(" npm run fetch-items 242494 242495 242481");
console.log(
" npm run fetch-items --existing # Fetch data for all items in drop files",
);
return;
}
let itemIds = [];
let source = null;
if (args[0] === "--existing") {
console.log("Fetching data for existing items in drop files...");
itemIds = await getExistingItemIds();
console.log(`Found ${itemIds.length} existing items`);
} else {
source = args[0];
itemIds = args
.slice(1)
.map((arg) => parseInt(arg))
.filter((id) => !isNaN(id));
}
if (itemIds.length === 0) {
console.log("No valid item IDs provided");
return;
}
const items = await fetchMultipleItems(source, itemIds);
if (items.length > 0) {
await saveToFile(items);
console.log("\nSummary:");
console.log(
`Successfully fetched: ${items.length}/${itemIds.length} items`,
);
// Show slot distribution
const slotCounts = {};
items.forEach((item) => {
const slot = item.slot || "unknown";
slotCounts[slot] = (slotCounts[slot] || 0) + 1;
});
console.log("\nSlot distribution:");
Object.entries(slotCounts).forEach(([slot, count]) => {
console.log(` ${slot}: ${count}`);
});
} else {
console.log("No items were successfully fetched");
}
}
// Run the script
if (import.meta.url === `file://${process.argv[1]}`) {
main().catch(console.error);
}
export {
fetchFromWowhead,
fetchFromWowheadJson,
fetchMultipleItems,
getExistingItemIds,
};

View File

@ -0,0 +1,343 @@
#!/usr/bin/env node
import { fetchFromWowhead, fetchFromWowheadJson, getExistingItemIds } from './fetch-item-data.js';
import { fetchFromBlizzard } from './blizzard-api.js';
import fs from 'fs/promises';
import path from 'path';
/**
* Enhanced item fetcher with multiple API sources and validation
*/
/**
* Fetches item data from multiple sources with fallbacks
* @param {number} itemId - The item ID to fetch
* @returns {Promise<Object|null>} Combined item data or null if failed
*/
async function fetchItemFromAllSources(itemId) {
console.log(`Fetching item ${itemId} from multiple sources...`);
const results = {
wowhead: null,
blizzard: null,
combined: null
};
// Try Wowhead first (more reliable for icons and basic data)
try {
results.wowhead = await fetchFromWowheadJson(itemId);
if (!results.wowhead) {
results.wowhead = await fetchFromWowhead(itemId);
}
} catch (error) {
console.error(`Wowhead fetch failed for ${itemId}:`, error.message);
}
// Try Blizzard API (more detailed data but requires auth)
try {
results.blizzard = await fetchFromBlizzard(itemId);
} catch (error) {
console.error(`Blizzard fetch failed for ${itemId}:`, error.message);
}
// Combine the best data from both sources
if (results.wowhead || results.blizzard) {
results.combined = combineItemData(results.wowhead, results.blizzard, itemId);
}
return results;
}
/**
* Combines item data from multiple sources, preferring the best available data
* @param {Object|null} wowheadData - Data from Wowhead
* @param {Object|null} blizzardData - Data from Blizzard
* @param {number} itemId - The item ID
* @returns {Object} Combined item data
*/
function combineItemData(wowheadData, blizzardData, itemId) {
const combined = {
id: itemId,
name: null,
slot: null,
icon: null,
quality: null,
sources: []
};
// Prefer Wowhead for name and icon (usually more consistent)
if (wowheadData) {
combined.name = wowheadData.name;
combined.icon = wowheadData.icon;
combined.slot = wowheadData.slot;
combined.quality = wowheadData.quality;
combined.sources.push('wowhead');
}
// Use Blizzard data to fill gaps or override with better data
if (blizzardData) {
if (!combined.name) combined.name = blizzardData.name;
if (!combined.slot) combined.slot = blizzardData.slot;
if (!combined.quality) combined.quality = blizzardData.quality;
// Add Blizzard-specific data
combined.blizzardData = blizzardData.blizzardData;
combined.sources.push('blizzard');
}
return combined;
}
/**
* Validates item data against existing project data
* @param {Object[]} fetchedItems - Array of fetched item data
* @param {Object[]} existingItems - Array of existing project items
* @returns {Object} Validation report
*/
function validateItemData(fetchedItems, existingItems) {
const report = {
matches: [],
mismatches: [],
missing: [],
newItems: []
};
const existingById = {};
existingItems.forEach(item => {
existingById[item.id] = item;
});
fetchedItems.forEach(fetched => {
const existing = existingById[fetched.combined.id];
if (existing) {
const issues = [];
if (existing.name !== fetched.combined.name) {
issues.push(`name: "${existing.name}" vs "${fetched.combined.name}"`);
}
if (existing.slot !== fetched.combined.slot) {
issues.push(`slot: "${existing.slot}" vs "${fetched.combined.slot}"`);
}
if (existing.icon !== fetched.combined.icon && fetched.combined.icon) {
issues.push(`icon: "${existing.icon || 'none'}" vs "${fetched.combined.icon}"`);
}
if (issues.length > 0) {
report.mismatches.push({
id: fetched.combined.id,
issues,
existing,
fetched: fetched.combined
});
} else {
report.matches.push(fetched.combined.id);
}
} else {
report.newItems.push(fetched.combined);
}
});
// Find missing items (in project but not fetched)
const fetchedIds = new Set(fetchedItems.map(item => item.combined.id));
existingItems.forEach(item => {
if (!fetchedIds.has(item.id)) {
report.missing.push(item.id);
}
});
return report;
}
/**
* Generates TypeScript code for new items
* @param {Object[]} items - Array of item data
* @param {string} source - Source name for the items
* @returns {string} TypeScript code
*/
function generateTypeScriptCode(items, source) {
const lines = [];
lines.push(`import type { ItemId } from "../types";`);
lines.push('');
lines.push(`export const ${source.charAt(0).toUpperCase() + source.slice(1)} = [`);
items.forEach((item, index) => {
lines.push(' {');
lines.push(` id: ${item.id} as ItemId,`);
lines.push(` slot: "${item.slot}",`);
lines.push(` name: "${item.name}",`);
lines.push(` source: "${source}" as const,`);
if (item.icon) {
lines.push(` icon: "${item.icon}",`);
}
lines.push(' }' + (index < items.length - 1 ? ',' : ''));
});
lines.push('];');
return lines.join('\n');
}
/**
* Main function with enhanced features
*/
async function main() {
const args = process.argv.slice(2);
if (args.length === 0) {
console.log('Enhanced WoW Item Data Fetcher');
console.log('Usage:');
console.log(' npm run fetch-items-enhanced [options] [item1] [item2] ...');
console.log('');
console.log('Options:');
console.log(' --existing Fetch data for all items in drop files');
console.log(' --validate Compare fetched data with existing project data');
console.log(' --generate <name> Generate TypeScript code for new source');
console.log(' --blizzard-only Use only Blizzard API');
console.log(' --wowhead-only Use only Wowhead API');
console.log('');
console.log('Examples:');
console.log(' npm run fetch-items-enhanced 242494 242495 242481');
console.log(' npm run fetch-items-enhanced --existing --validate');
console.log(' npm run fetch-items-enhanced --generate "new-dungeon" 123456 123457');
return;
}
let itemIds = [];
let options = {
existing: false,
validate: false,
generate: null,
blizzardOnly: false,
wowheadOnly: false
};
// Parse arguments
for (let i = 0; i < args.length; i++) {
const arg = args[i];
if (arg === '--existing') {
options.existing = true;
} else if (arg === '--validate') {
options.validate = true;
} else if (arg === '--generate') {
options.generate = args[++i];
} else if (arg === '--blizzard-only') {
options.blizzardOnly = true;
} else if (arg === '--wowhead-only') {
options.wowheadOnly = true;
} else if (!isNaN(parseInt(arg))) {
itemIds.push(parseInt(arg));
}
}
// Get item IDs
if (options.existing) {
console.log('Fetching data for existing items in drop files...');
const existingIds = await getExistingItemIds();
itemIds = [...new Set([...itemIds, ...existingIds])];
console.log(`Found ${existingIds.length} existing items`);
}
if (itemIds.length === 0) {
console.log('No valid item IDs provided');
return;
}
console.log(`Processing ${itemIds.length} items...`);
// Fetch items with rate limiting
const results = [];
for (let i = 0; i < itemIds.length; i++) {
const itemId = itemIds[i];
console.log(`\nProcessing item ${itemId} (${i + 1}/${itemIds.length})`);
let result;
if (options.blizzardOnly) {
const blizzardData = await fetchFromBlizzard(itemId);
result = { wowhead: null, blizzard: blizzardData, combined: blizzardData };
} else if (options.wowheadOnly) {
const wowheadData = await fetchFromWowheadJson(itemId) || await fetchFromWowhead(itemId);
result = { wowhead: wowheadData, blizzard: null, combined: wowheadData };
} else {
result = await fetchItemFromAllSources(itemId);
}
if (result.combined) {
results.push(result);
console.log(`${result.combined.name} (${result.combined.slot || 'unknown slot'}) [${result.combined.sources.join(', ')}]`);
} else {
console.log(`✗ Failed to fetch data for item ${itemId}`);
}
// Rate limiting
if (i < itemIds.length - 1) {
await new Promise(resolve => setTimeout(resolve, 1000));
}
}
// Save detailed results
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
await fs.writeFile(
`fetched-items-detailed-${timestamp}.json`,
JSON.stringify(results, null, 2)
);
// Validation
if (options.validate && options.existing) {
console.log('\nValidating against existing project data...');
const existingItems = [];
// This would need to parse the existing TypeScript files
// For now, just show what we fetched
const validation = validateItemData(results, existingItems);
console.log(`\nValidation Results:`);
console.log(` Matches: ${validation.matches.length}`);
console.log(` Mismatches: ${validation.mismatches.length}`);
console.log(` New items: ${validation.newItems.length}`);
if (validation.mismatches.length > 0) {
console.log('\nMismatches found:');
validation.mismatches.forEach(mismatch => {
console.log(` Item ${mismatch.id}: ${mismatch.issues.join(', ')}`);
});
}
}
// Generate TypeScript code
if (options.generate) {
const validItems = results
.map(r => r.combined)
.filter(item => item.name && item.slot);
const tsCode = generateTypeScriptCode(validItems, options.generate);
const filename = `${options.generate}.ts`;
await fs.writeFile(filename, tsCode);
console.log(`\nGenerated TypeScript code in ${filename}`);
}
// Summary
console.log(`\nSummary:`);
console.log(`Successfully fetched: ${results.length}/${itemIds.length} items`);
const slotCounts = {};
results.forEach(result => {
const slot = result.combined.slot || 'unknown';
slotCounts[slot] = (slotCounts[slot] || 0) + 1;
});
console.log('\nSlot distribution:');
Object.entries(slotCounts).forEach(([slot, count]) => {
console.log(` ${slot}: ${count}`);
});
}
// Run the script
if (import.meta.url === `file://${process.argv[1]}`) {
main().catch(console.error);
}
export { fetchItemFromAllSources, combineItemData, validateItemData, generateTypeScriptCode };

View File

@ -6,6 +6,7 @@ import { StateContext } from "./lib/context/StateContext";
import { reducer, withLoadingAction } from "./lib/state";
import { WeaponConfig, type ItemId } from "./lib/types";
import { WeaponConfigPicker } from "./components/WeaponConfigPicker";
import { SpecSelector } from "./components/SpecSelector";
function App() {
const [state, dispatch] = useReducer(withLoadingAction(reducer), "loading");
@ -50,6 +51,7 @@ function App() {
<h1>WoW Gear Finder</h1>
<h2>Config</h2>
<WeaponConfigPicker />
<SpecSelector />
<h2>Equipment</h2>
<Equipment
state={state}

View File

@ -41,9 +41,9 @@ const Slot = ({ slot, onUnequip }: SlotProps) => {
const equipedItems = state.equipedItems
.map((item) => ({ ...item, item: ItemsById[item.id] }))
.filter((item) => item && item.item.slot === slot);
.filter((item) => item && item.item?.slot === slot);
const allItems = itemsForSlot(slot);
const allItems = itemsForSlot(state.spec)(slot);
return (
<>

View File

@ -0,0 +1,35 @@
import { useAppState } from "../lib/context/StateContext";
import { Spec } from "../lib/types";
const SpecMap = Object.fromEntries(
Object.entries(Spec).flatMap(([cls, specs]) =>
Object.entries(specs).map(([spec, specId]) => [`${cls} ${spec}`, specId]),
),
);
export const SpecSelector = () => {
const { state, dispatch } = useAppState();
const handleOnChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
const value = parseInt(event.target.value?.trim(), 10);
console.log("Selected spec ID: ", value);
if (value) {
dispatch({
action: "changeSpec",
spec: value as Spec,
});
}
};
return (
<div>
<select onChange={handleOnChange} value={state.spec}>
{Object.entries(SpecMap).map(([spec, id]) => (
<option key={id} value={id}>
{spec}
</option>
))}
</select>
</div>
);
};

276
src/lib/drops/aldani.json Normal file
View File

@ -0,0 +1,276 @@
[
{
"id": 242497,
"name": "Azhiccaran Parapodia",
"icon": "inv_devourersmallmount_light",
"slot": "trinket",
"specs": [],
"source": "Eco-Dome Aldani"
},
{
"id": 242494,
"name": "Lily of the Eternal Weave",
"icon": "inv_herb_karesh",
"slot": "trinket",
"specs": [
261,
254,
62,
1449,
102,
1447,
105,
255,
577,
63,
64,
270,
262,
1450,
65,
1451,
256,
257,
1452,
258,
264,
1444,
265,
266,
267,
1454,
1465,
1467,
1468,
1473,
268,
1456,
581,
103,
104,
253,
260,
1448,
269,
259,
1453,
263
],
"source": "Eco-Dome Aldani"
},
{
"id": 242491,
"name": "Whispers of K'aresh",
"icon": "inv_112_raidtrinkets_ring01_etherealnontechnologicalstyle_gold",
"slot": "finger",
"specs": [],
"source": "Eco-Dome Aldani"
},
{
"id": 242495,
"name": "Incorporeal Warpclaw",
"icon": "inv_misc_nightsaberclaw_mana",
"slot": "trinket",
"specs": [
103,
255,
577,
260,
1448,
581,
268,
1456,
104,
1447,
253,
261,
254,
1450,
70,
269,
259,
1453,
263,
1444,
250,
251,
252,
1455,
1451,
66,
71,
72,
1446,
73
],
"source": "Eco-Dome Aldani"
},
{
"id": 242481,
"name": "Spellstrike Warplance",
"icon": "inv_polearm_2h_outdoorethereal_c_01",
"slot": "2h-weapon",
"specs": [
255,
103,
104,
1447,
268,
269
],
"source": "Eco-Dome Aldani"
},
{
"id": 242473,
"name": "Spittle-Stained Trousers",
"icon": "inv_pants_leather_outdoorethereal_c_01",
"slot": "legs",
"specs": [
102,
1447,
105,
270,
1450,
103,
577,
581,
268,
1456,
104,
269,
259,
1453,
260,
261
],
"source": "Eco-Dome Aldani"
},
{
"id": 242482,
"name": "Reinforced Stalkerhide Vest",
"icon": "inv_chest-_leather_outdoorethereal_c_01",
"slot": "chest",
"specs": [
102,
1447,
105,
270,
1450,
103,
577,
581,
268,
1456,
104,
269,
259,
1453,
260,
261
],
"source": "Eco-Dome Aldani"
},
{
"id": 242477,
"name": "Wasteland Devotee's Wrappings",
"icon": "inv_helm_cloth_outdoorethereal_c_01",
"slot": "head",
"specs": [
62,
1449,
1452,
256,
63,
64,
265,
257,
258,
266,
267,
1454
],
"source": "Eco-Dome Aldani"
},
{
"id": 242470,
"name": "Mandibular Bonewhacker",
"icon": "inv_mace_1h_outdoorethereal_c_02",
"slot": "1h-weapon",
"specs": [
251,
72,
1451,
66,
73,
1446
],
"source": "Eco-Dome Aldani"
},
{
"id": 242468,
"name": "Al'dani Attendant's Gauze",
"icon": "inv_bracer_cloth_outdoorethereal_c_01",
"slot": "wrist",
"specs": [
62,
1449,
1452,
256,
63,
64,
265,
257,
258,
266,
267,
1454
],
"source": "Eco-Dome Aldani"
},
{
"id": 242486,
"name": "Mantle of Wounded Fate",
"icon": "inv_shoulder_leather_outdoorethereal_c_01",
"slot": "shoulders",
"specs": [
102,
1447,
105,
270,
1450,
103,
577,
581,
268,
1456,
104,
269,
259,
1453,
260,
261
],
"source": "Eco-Dome Aldani"
},
{
"id": 242490,
"name": "Ancient Oracle's Caress",
"icon": "inv_glove_cloth_outdoorethereal_c_01",
"slot": "hands",
"specs": [
62,
1449,
1452,
256,
63,
64,
265,
257,
258,
266,
267,
1454
],
"source": "Eco-Dome Aldani"
}
]

View File

@ -1,47 +0,0 @@
import type { ItemId } from "../types";
export const Aldani = [
{
id: 242494 as ItemId,
slot: "trinket",
name: "Lily of the Eternal Weave",
source: "aldani" as const,
},
{
id: 242495 as ItemId,
slot: "trinket",
name: "Incorporeal Warpclaw",
source: "aldani" as const,
icon: "inv_misc_nightsaberclaw_mana",
},
{
id: 242481 as ItemId,
slot: "2h-weapon",
name: "Spellstrike Warplance",
source: "aldani" as const,
},
{
id: 242473 as ItemId,
slot: "legs",
name: "Spittle-Stained Trousers",
source: "aldani" as const,
},
{
id: 242470 as ItemId,
slot: "1h-weapon",
name: "Mandibular Bonewhacker",
source: "aldani" as const,
},
{
id: 242486 as ItemId,
slot: "shoulders",
name: "Mantle of Wounded Fate",
source: "aldani" as const,
},
{
id: 242482 as ItemId,
slot: "chest",
name: "Reinforced Stalkerhide Vest",
source: "aldani" as const,
},
] as const;

316
src/lib/drops/ara-kara.json Normal file
View File

@ -0,0 +1,316 @@
[
{
"id": 219314,
"name": "Ara-Kara Sacbrood",
"icon": "inv_raid_mercurialegg_red",
"slot": "trinket",
"specs": [],
"source": "Ara-Kara, City of Echoes"
},
{
"id": 221159,
"name": "Harvester's Interdiction",
"icon": "inv_staff_2h_earthendungeon_c_02",
"slot": "2h-weapon",
"specs": [
255,
103,
104,
1447,
268,
269
],
"source": "Ara-Kara, City of Echoes"
},
{
"id": 219317,
"name": "Harvester's Edict",
"icon": "inv_11_0_dungeon_oldgodstatuepiece_purple",
"slot": "trinket",
"specs": [
261,
254,
62,
1449,
102,
1447,
105,
255,
577,
63,
64,
270,
262,
1450,
65,
1451,
256,
257,
1452,
258,
264,
1444,
265,
266,
267,
1454,
1465,
1467,
1468,
1473,
268,
1456,
581,
103,
104,
253,
260,
1448,
269,
259,
1453,
263
],
"source": "Ara-Kara, City of Echoes"
},
{
"id": 221150,
"name": "Arachnoid Soulcleaver",
"icon": "inv_sword_1h_earthendungeon_c_02",
"slot": "1h-weapon",
"specs": [
62,
63,
1451,
64,
270,
266,
65,
265,
267,
1454,
1465,
1467,
1468,
1473
],
"source": "Ara-Kara, City of Echoes"
},
{
"id": 221153,
"name": "Gauzewoven Legguards",
"icon": "inv_leather_earthendungeon_c_01_pant",
"slot": "legs",
"specs": [
102,
1447,
105,
270,
1450,
103,
577,
581,
268,
1456,
104,
269,
259,
1453,
260,
261
],
"source": "Ara-Kara, City of Echoes"
},
{
"id": 221157,
"name": "Unbreakable Beetlebane Bindings",
"icon": "inv_leather_earthendungeon_c_01_bracer",
"slot": "wrist",
"specs": [
102,
1447,
105,
270,
1450,
103,
577,
581,
268,
1456,
104,
269,
259,
1453,
260,
261
],
"source": "Ara-Kara, City of Echoes"
},
{
"id": 219316,
"name": "Ceaseless Swarmgland",
"icon": "inv_misc_organmass_color2",
"slot": "trinket",
"specs": [
103,
255,
577,
260,
1448,
581,
268,
1456,
104,
1447,
253,
261,
254,
1450,
70,
269,
259,
1453,
263,
1444,
250,
251,
252,
1455,
1451,
66,
71,
72,
1446,
73
],
"source": "Ara-Kara, City of Echoes"
},
{
"id": 221158,
"name": "Burrower's Cinch",
"icon": "inv_belt_cloth_earthendungeon_c_01",
"slot": "waist",
"specs": [
62,
1449,
1452,
256,
63,
64,
265,
257,
258,
266,
267,
1454
],
"source": "Ara-Kara, City of Echoes"
},
{
"id": 221164,
"name": "Archaic Venomancer's Legwraps",
"icon": "inv_pant_cloth_earthendungeon_c_01",
"slot": "legs",
"specs": [
62,
1449,
1452,
256,
63,
64,
265,
257,
258,
266,
267,
1454
],
"source": "Ara-Kara, City of Echoes"
},
{
"id": 221163,
"name": "Whispering Mask",
"icon": "inv_leather_earthendungeon_c_01_helm",
"slot": "head",
"specs": [
102,
1447,
105,
270,
1450,
103,
577,
581,
268,
1456,
104,
269,
259,
1453,
260,
261
],
"source": "Ara-Kara, City of Echoes"
},
{
"id": 221154,
"name": "Swarmcaller's Shroud",
"icon": "inv_cape_mail_earthendungeon_c_01",
"slot": "1h-weapon",
"specs": [
261,
254,
62,
1449,
102,
1447,
105,
255,
577,
63,
64,
270,
262,
1450,
65,
1451,
256,
250,
257,
1452,
73,
258,
264,
1444,
66,
265,
1446,
266,
267,
1454,
1465,
1467,
1468,
1473,
268,
1456,
581,
103,
104,
253,
260,
1448,
70,
269,
259,
1453,
263,
251,
252,
1455,
71,
72
],
"source": "Ara-Kara, City of Echoes"
}
]

View File

@ -1,52 +0,0 @@
import type { ItemId } from "../types";
export const AraKara = [
{
id: 221159 as ItemId,
slot: "2h-weapon",
name: "Harvester's Interdiction",
source: "ara-kara",
},
{
id: 219317 as ItemId,
slot: "trinket",
name: "Harvester's Edict",
source: "ara-kara",
},
{
id: 219316 as ItemId,
slot: "trinket",
name: "Ceaseless Swarmgland",
source: "ara-kara",
},
{
id: 221157 as ItemId,
slot: "wrist",
name: "Unbreakable Beetlebane Bindings",
source: "ara-kara",
},
{
id: 221153 as ItemId,
slot: "legs",
name: "Gauzewoven Legguards",
source: "ara-kara",
},
{
id: 221154 as ItemId,
slot: "back",
name: "Swarmcaller's Shroud",
source: "ara-kara",
},
{
id: 221163 as ItemId,
slot: "head",
name: "Whispering Mask",
source: "ara-kara",
},
{
id: 219315 as ItemId,
slot: "trinket",
name: "Refracting Aggression Module",
source: "ara-kara",
},
] as const;

View File

@ -0,0 +1,134 @@
[
{
"id": 219312,
"name": "Empowering Crystal of Anub'ikkaj",
"icon": "inv_arathordungeon_fragment_color5",
"slot": "trinket",
"specs": [
261, 254, 62, 1449, 102, 1447, 105, 255, 577, 63, 64, 270, 262, 1450, 65,
1451, 256, 257, 1452, 258, 264, 1444, 265, 266, 267, 1454, 1465, 1467,
1468, 1473, 268, 1456, 581, 103, 104, 253, 260, 1448, 269, 259, 1453, 263
],
"source": "dawnbreaker"
},
{
"id": 221136,
"name": "Devout Zealot's Ring",
"icon": "inv_11_0_nerubian_ring_01_color4",
"slot": "finger",
"specs": [],
"source": "dawnbreaker"
},
{
"id": 221141,
"name": "High Nerubian Signet",
"icon": "inv_11_0_nerubian_ring_02_color5",
"slot": "finger",
"specs": [],
"source": "dawnbreaker"
},
{
"id": 221135,
"name": "Fanatic's Blackened Shoulderwraps",
"icon": "inv_shoulder_cloth_earthendungeon_c_01",
"slot": "shoulders",
"specs": [62, 1449, 1452, 256, 63, 64, 265, 257, 258, 266, 267, 1454],
"source": "dawnbreaker"
},
{
"id": 219311,
"name": "Void Pactstone",
"icon": "inv_10_alchemy_alchemystone_color4",
"slot": "trinket",
"specs": [66, 250, 251, 1455, 252, 72, 1451, 70, 71, 1446, 73],
"source": "dawnbreaker"
},
{
"id": 221149,
"name": "Membranous Slippers",
"icon": "inv_boot_cloth_earthendungeon_c_01",
"slot": "feet",
"specs": [62, 1449, 1452, 256, 63, 64, 265, 257, 258, 266, 267, 1454],
"source": "dawnbreaker"
},
{
"id": 221137,
"name": "Black Shepherd's Guisarme",
"icon": "inv_polearm_2h_earthendungeon_c_02",
"slot": "2h-weapon",
"specs": [255, 103, 104, 1447, 268, 269],
"source": "dawnbreaker"
},
{
"id": 221144,
"name": "Zephyrous Sail Carver",
"icon": "inv_sword_1h_earthendungeon_c_02",
"slot": "1h-weapon",
"specs": [577, 260, 268, 581, 1450, 269],
"source": "dawnbreaker"
},
{
"id": 221142,
"name": "Scheming Assailer's Bands",
"icon": "inv_leather_earthendungeon_c_01_bracer",
"slot": "wrist",
"specs": [
102, 1447, 105, 270, 1450, 103, 577, 581, 268, 1456, 104, 269, 259, 1453,
260, 261
],
"source": "dawnbreaker"
},
{
"id": 221148,
"name": "Epaulets of the Clipped Wings",
"icon": "inv_leather_earthendungeon_c_01_shoulder",
"slot": "shoulders",
"specs": [
102, 1447, 105, 270, 1450, 103, 577, 581, 268, 1456, 104, 269, 259, 1453,
260, 261
],
"source": "dawnbreaker"
},
{
"id": 219313,
"name": "Mereldar's Toll",
"icon": "inv_arathordungeon_bell_color1",
"slot": "trinket",
"specs": [
62, 1449, 102, 1447, 105, 63, 64, 270, 262, 1450, 65, 1451, 256, 257,
1452, 258, 264, 1444, 265, 266, 267, 1454, 1465, 1467, 1468, 1473
],
"source": "dawnbreaker"
},
{
"id": 221134,
"name": "Shadow Congregant's Belt",
"icon": "inv_leather_earthendungeon_c_01_belt",
"slot": "waist",
"specs": [
102, 1447, 105, 270, 1450, 103, 577, 581, 268, 1456, 104, 269, 259, 1453,
260, 261
],
"source": "dawnbreaker"
},
{
"id": 221145,
"name": "Shipwrecker's Bludgeon",
"icon": "inv_mace_1h_earthendungeon_c_01",
"slot": "1h-weapon",
"specs": [251, 72, 1451, 66, 73, 1446],
"source": "dawnbreaker"
},
{
"id": 221132,
"name": "Overflowing Umbral Pail",
"icon": "inv_offhand_1h_earthendungeon_c_01",
"slot": "off-hand",
"specs": [
62, 1449, 102, 1447, 105, 63, 64, 270, 262, 1450, 65, 1451, 256, 257,
1452, 258, 264, 1444, 265, 266, 267, 1454, 1465, 1467, 1468, 1473
],
"source": "dawnbreaker"
}
]

View File

@ -60,7 +60,7 @@ export const Dawnbreaker = [
slot: "waist",
name: "Behemoth's Eroded Cinch",
source: "dawnbreaker" as const,
},
{
id: 212453,
slot: "trinket",

View File

@ -0,0 +1,150 @@
[
{
"id": 232541,
"name": "Improvised Seaforium Pacemaker",
"icon": "ability_blackhand_attachedslagbombs",
"slot": "trinket",
"specs": [
103, 255, 577, 260, 1448, 581, 268, 1456, 104, 1447, 253, 261, 254, 1450,
70, 269, 259, 1453, 263, 1444, 250, 251, 252, 1455, 1451, 66, 71, 72,
1446, 73
],
"source": "Operation: Floodgate"
},
{
"id": 251880,
"name": "Momma's Mega Medallion",
"icon": "inv_112_raidtrinkets_necklace02_etherealribbonorrunestyle_dark",
"slot": "neck",
"specs": [],
"source": "Operation: Floodgate"
},
{
"id": 232543,
"name": "Ringing Ritual Mud",
"icon": "inv_misc_food_legion_goomolasses_pool",
"slot": "trinket",
"specs": [],
"source": "Operation: Floodgate"
},
{
"id": 234499,
"name": "Disturbed Kelp Wraps",
"icon": "inv_leather_outdoorundermine_c_01_bracer",
"slot": "wrist",
"specs": [
102, 1447, 105, 270, 1450, 103, 577, 581, 268, 1456, 104, 269, 259, 1453,
260, 261
],
"source": "Operation: Floodgate"
},
{
"id": 232545,
"name": "Gigazap's Zap-Cap",
"icon": "inv_engineering_90_lightningbox",
"slot": "trinket",
"specs": [
62, 1449, 102, 1447, 105, 63, 64, 270, 262, 1450, 65, 1451, 256, 257,
1452, 258, 264, 1444, 265, 266, 267, 1454, 1465, 1467, 1468, 1473
],
"source": "Operation: Floodgate"
},
{
"id": 246274,
"name": "Geezle's Zapstep Boots",
"icon": "inv_leather_outdoorundermine_c_01_boot",
"slot": "feet",
"specs": [
102, 1447, 105, 270, 1450, 103, 577, 581, 268, 1456, 104, 269, 259, 1453,
260, 261
],
"source": "Operation: Floodgate"
},
{
"id": 234497,
"name": "Nonconductive Kill-o-Socks",
"icon": "inv_cloth_outdoorundermine_c_01_boot",
"slot": "feet",
"specs": [62, 1449, 1452, 256, 63, 64, 265, 257, 258, 266, 267, 1454],
"source": "Operation: Floodgate"
},
{
"id": 234494,
"name": "Gallytech Turbo-Tiller",
"icon": "inv_polearm_2h_outdoorundermine_c_01_purple",
"slot": "2h-weapon",
"specs": [255, 103, 104, 1447, 268, 269],
"source": "Operation: Floodgate"
},
{
"id": 232542,
"name": "Darkfuse Medichopper",
"icon": "inv_111_healraydrone_blackwater",
"slot": "trinket",
"specs": [
62, 1449, 102, 1447, 105, 63, 64, 270, 262, 1450, 65, 1451, 256, 257,
1452, 258, 264, 1444, 265, 266, 267, 1454, 1465, 1467, 1468, 1473
],
"source": "Operation: Floodgate"
},
{
"id": 234498,
"name": "Waterworks Filtration Mask",
"icon": "inv_leather_outdoorundermine_c_01_helm",
"slot": "head",
"specs": [
102, 1447, 105, 270, 1450, 103, 577, 581, 268, 1456, 104, 269, 259, 1453,
260, 261
],
"source": "Operation: Floodgate"
},
{
"id": 234495,
"name": "Razorchoke Slacks",
"icon": "inv_cloth_outdoorundermine_c_01_pant",
"slot": "legs",
"specs": [62, 1449, 1452, 256, 63, 64, 265, 257, 258, 266, 267, 1454],
"source": "Operation: Floodgate"
},
{
"id": 246279,
"name": "Fizzlefuse Cuffs",
"icon": "inv_cloth_outdoorundermine_c_01_bracer",
"slot": "wrist",
"specs": [62, 1449, 1452, 256, 63, 64, 265, 257, 258, 266, 267, 1454],
"source": "Operation: Floodgate"
},
{
"id": 234496,
"name": "Saboteur's Rubber Jacket",
"icon": "inv_cloth_outdoorundermine_c_01_chest",
"slot": "chest",
"specs": [62, 1449, 1452, 256, 63, 64, 265, 257, 258, 266, 267, 1454],
"source": "Operation: Floodgate"
},
{
"id": 234500,
"name": "Mechanized Junkpads",
"icon": "inv_leather_outdoorundermine_c_01_shoulder",
"slot": "shoulders",
"specs": [
102, 1447, 105, 270, 1450, 103, 577, 581, 268, 1456, 104, 269, 259, 1453,
260, 261
],
"source": "Operation: Floodgate"
},
{
"id": 234507,
"name": "Electrician's Siphoning Filter",
"icon": "inv_mail_outdoorundermine_c_01_cape",
"slot": "1h-weapon",
"specs": [
261, 254, 62, 1449, 102, 1447, 105, 255, 577, 63, 64, 270, 262, 1450, 65,
1451, 256, 250, 257, 1452, 73, 258, 264, 1444, 66, 265, 1446, 266, 267,
1454, 1465, 1467, 1468, 1473, 268, 1456, 581, 103, 104, 253, 260, 1448,
70, 269, 259, 1453, 263, 251, 252, 1455, 71, 72
],
"source": "Operation: Floodgate"
}
]

View File

@ -1,52 +0,0 @@
import type { ItemId } from "../types";
export const Floodgate = [
{
id: 232541 as ItemId,
slot: "trinket",
name: "Improvised Seaforium Pacemaker",
source: "floodgate" as const,
},
{
id: 232543 as ItemId,
slot: "trinket",
name: "Ringing Ritual Mud",
source: "floodgate" as const,
},
{
id: 234499 as ItemId,
slot: "wrist",
name: "Disturbed Kelp Wraps",
source: "floodgate" as const,
},
{
id: 246274 as ItemId,
slot: "feet",
name: "Geezle's Zapstep Boots",
source: "floodgate" as const,
},
{
id: 234494 as ItemId,
slot: "2h-weapon",
name: "Gallytech Turbo-Tiller",
source: "floodgate" as const,
},
{
id: 234498 as ItemId,
slot: "head",
name: "Waterworks Filtration Mask",
source: "floodgate" as const,
},
{
id: 234507 as ItemId,
slot: "back",
name: "Electrician's Siphoning Filter",
source: "floodgate" as const,
},
{
id: 234500 as ItemId,
slot: "shoulders",
name: "Mechanized Junkpads",
source: "floodgate" as const,
},
] as const;

View File

@ -0,0 +1,449 @@
[
{
"id": 246344,
"name": "Cursed Stone Idol",
"icon": "inv_qirajidol_onyx",
"slot": "trinket",
"specs": [
103,
255,
577,
260,
1448,
581,
268,
1456,
104,
1447,
253,
261,
254,
1450,
70,
269,
259,
1453,
263,
1444,
250,
251,
252,
1455,
1451,
66,
71,
72,
1446,
73
],
"source": "Priory of the Sacred Flame"
},
{
"id": 178824,
"name": "Signet of the False Accuser",
"icon": "inv_ring_revendrethraid_01_gold",
"slot": "finger",
"specs": [],
"source": "Priory of the Sacred Flame"
},
{
"id": 178827,
"name": "Sin Stained Pendant",
"icon": "inv_7_0raid_necklace_03a",
"slot": "neck",
"specs": [],
"source": "Priory of the Sacred Flame"
},
{
"id": 178829,
"name": "Nathrian Ferula",
"icon": "inv_staff_2h_oribosdungeon_c_01",
"slot": "2h-weapon",
"specs": [
62,
1449,
102,
1447,
105,
256,
63,
64,
270,
257,
1452,
258,
262,
264,
265,
266,
267,
1454,
1465,
1467,
1468,
1473
],
"source": "Priory of the Sacred Flame"
},
{
"id": 178832,
"name": "Gloves of Haunting Fixation",
"icon": "inv_glove_leather_oribosdungeon_c_01",
"slot": "hands",
"specs": [
102,
1447,
105,
270,
1450,
103,
577,
581,
268,
1456,
104,
269,
259,
1453,
260,
261
],
"source": "Priory of the Sacred Flame"
},
{
"id": 178826,
"name": "Sunblood Amethyst",
"icon": "inv_jewelcrafting_nightseye_01",
"slot": "trinket",
"specs": [],
"source": "Priory of the Sacred Flame"
},
{
"id": 246284,
"name": "Nathrian Reliquary",
"icon": "inv_offhand_1h_oribosdungeon_c_01",
"slot": "off-hand",
"specs": [
62,
1449,
102,
1447,
105,
63,
64,
270,
262,
1450,
65,
1451,
256,
257,
1452,
258,
264,
1444,
265,
266,
267,
1454,
1465,
1467,
1468,
1473
],
"source": "Priory of the Sacred Flame"
},
{
"id": 178822,
"name": "Cord of the Dark Word",
"icon": "inv_belt_cloth_oribosdungeon_c_01",
"slot": "waist",
"specs": [
62,
1449,
1452,
256,
63,
64,
265,
257,
258,
266,
267,
1454
],
"source": "Priory of the Sacred Flame"
},
{
"id": 246276,
"name": "Sinlight Shoulderpads",
"icon": "inv_shoulder_cloth_oribosdungeon_c_01",
"slot": "shoulders",
"specs": [
62,
1449,
1452,
256,
63,
64,
265,
257,
258,
266,
267,
1454
],
"source": "Priory of the Sacred Flame"
},
{
"id": 178819,
"name": "Skyterror's Stonehide Leggings",
"icon": "inv_pant_leather_oribosdungeon_c_01",
"slot": "legs",
"specs": [
102,
1447,
105,
270,
1450,
103,
577,
581,
268,
1456,
104,
269,
259,
1453,
260,
261
],
"source": "Priory of the Sacred Flame"
},
{
"id": 246273,
"name": "Vest of Refracted Shadows",
"icon": "inv_chest_leather_oribosdungeon_c_01",
"slot": "chest",
"specs": [
102,
1447,
105,
270,
1450,
103,
577,
581,
268,
1456,
104,
269,
259,
1453,
260,
261
],
"source": "Priory of the Sacred Flame"
},
{
"id": 178831,
"name": "Slippers of Leavened Station",
"icon": "inv_boot_cloth_oribosdungeon_c_01",
"slot": "feet",
"specs": [
62,
1449,
1452,
256,
63,
64,
265,
257,
258,
266,
267,
1454
],
"source": "Priory of the Sacred Flame"
},
{
"id": 178833,
"name": "Stonefiend Shaper's Mitts",
"icon": "inv_glove_cloth_oribosdungeon_c_01",
"slot": "hands",
"specs": [
62,
1449,
1452,
256,
63,
64,
265,
257,
258,
266,
267,
1454
],
"source": "Priory of the Sacred Flame"
},
{
"id": 178823,
"name": "Waistcord of Dark Devotion",
"icon": "inv_belt_leather_oribosdungeon_c_01",
"slot": "waist",
"specs": [
102,
1447,
105,
270,
1450,
103,
577,
581,
268,
1456,
104,
269,
259,
1453,
260,
261
],
"source": "Priory of the Sacred Flame"
},
{
"id": 178834,
"name": "Stoneguardian's Morningstar",
"icon": "inv_mace_1h_bastionquest_b_02",
"slot": "1h-weapon",
"specs": [
260,
268,
1450,
269,
263
],
"source": "Priory of the Sacred Flame"
},
{
"id": 178825,
"name": "Pulsating Stoneheart",
"icon": "inv_misc_gem_bloodstone_01",
"slot": "trinket",
"specs": [
103,
255,
577,
260,
1448,
581,
268,
1456,
104,
1447,
253,
261,
254,
1450,
70,
269,
259,
1453,
263,
1444,
250,
251,
252,
1455,
1451,
66,
71,
72,
1446,
73
],
"source": "Priory of the Sacred Flame"
},
{
"id": 178813,
"name": "Sinlight Shroud",
"icon": "inv_robe_cloth_oribosdungeon_c_01",
"slot": "chest",
"specs": [
62,
1449,
1452,
256,
63,
64,
265,
257,
258,
266,
267,
1454
],
"source": "Priory of the Sacred Flame"
},
{
"id": 178817,
"name": "Hood of Refracted Shadows",
"icon": "inv_helm_leather_oribosdungeon_c_01",
"slot": "head",
"specs": [
102,
1447,
105,
270,
1450,
103,
577,
581,
268,
1456,
104,
269,
259,
1453,
260,
261
],
"source": "Priory of the Sacred Flame"
},
{
"id": 178828,
"name": "Nathrian Tabernacle",
"icon": "inv_offhand_1h_oribosdungeon_c_01",
"slot": "off-hand",
"specs": [
62,
1449,
102,
1447,
105,
63,
64,
270,
262,
1450,
65,
1451,
256,
257,
1452,
258,
264,
1444,
265,
266,
267,
1454,
1465,
1467,
1468,
1473
],
"source": "Priory of the Sacred Flame"
}
]

View File

@ -1,52 +0,0 @@
import type { ItemId } from "../types";
export const HallsOfAtonement = [
{
id: 246344 as ItemId,
slot: "trinket",
name: "Cursed Stone Idol",
source: "halls-of-atonement" as const,
},
{
id: 178832 as ItemId,
slot: "hands",
name: "Gloves of Haunting Fixation",
source: "halls-of-atonement" as const,
},
{
id: 178819 as ItemId,
slot: "legs",
name: "Skyterror's Stonehide Leggings",
source: "halls-of-atonement" as const,
},
{
id: 246273 as ItemId,
slot: "chest",
name: "Vest of Refracted Shadows",
source: "halls-of-atonement" as const,
},
{
id: 178834 as ItemId,
slot: "1h-weapon",
name: "Stoneguardian's Morningstar",
source: "halls-of-atonement" as const,
},
{
id: 178823 as ItemId,
slot: "waist",
name: "Waistcord of Dark Devotion",
source: "halls-of-atonement" as const,
},
{
id: 178825 as ItemId,
slot: "trinket",
name: "Pulsating Stoneheart",
source: "halls-of-atonement" as const,
},
{
id: 178817 as ItemId,
slot: "head",
name: "Hood of Refracted Shadows",
source: "halls-of-atonement" as const,
},
] as const;

View File

@ -1,23 +1,24 @@
import type { Item } from "../types";
import { Aldani } from "./aldani";
import { AraKara } from "./ara-kara";
import Aldani from "./aldani.json";
import AraKara from "./ara-kara.json";
import Dawnbreaker from "./dawnbreaker.json";
import Floodgate from "./floodgate.json";
import PrioryOfTheSacredFlame from "./priory.json";
import Tazavesh from "./tazavesh.json";
import { Crafted } from "./crafted";
import { Dawnbreaker } from "./dawnbreaker";
import { Floodgate } from "./floodgate";
import { HallsOfAtonement } from "./halls-of-atonement";
import { ManaforgeOmega } from "./manaforge-omega";
import { PrioryOfTheSacredFlame } from "./priory";
import { Streets, Gambit } from "./tazavesh";
import HallsOfAtonement from "./halls-of-atonement.json";
import ManaforgeOmega from "./manaforge-omega.json";
import Tier from "./tww-s3-tier.json";
export const AllItems: Item[] = [
export const AllItems = [
...PrioryOfTheSacredFlame,
...Aldani,
...AraKara,
...Dawnbreaker,
...Floodgate,
...HallsOfAtonement,
...Streets,
...Gambit,
...Tazavesh,
...ManaforgeOmega,
...Crafted,
];
...Tier,
] as Item[];

File diff suppressed because it is too large Load Diff

View File

@ -1,209 +0,0 @@
import { Tag, type ItemId } from "../types";
export const ManaforgeOmega = [
{
id: 237673 as ItemId,
name: "Half-Mask of Fallen Storms",
slot: "head",
source: "manaforge omega",
tags: [Tag.Tier],
},
{
id: 237674 as ItemId,
name: "Grasp of Fallen Storms",
slot: "hands",
source: "manaforge omega",
tags: [Tag.Tier],
},
{
id: 237676 as ItemId,
name: "Gi of Fallen Storms",
slot: "chest",
source: "manaforge omega",
tags: [Tag.Tier],
},
{
id: 237672 as ItemId,
name: "Legwraps of Fallen Storms",
slot: "legs",
source: "manaforge omega",
tags: [Tag.Tier],
},
{
id: 237671 as ItemId,
name: "Glyphs of Fallen Storms",
slot: "shoulders",
source: "manaforge omega",
tags: [Tag.Tier],
},
{
id: 235799 as ItemId,
name: "Reshii Wraps",
slot: "back",
source: "manaforge omega",
tags: [Tag.Quest],
},
{
id: 242395,
name: "Astral Antenna",
slot: "trinket",
source: "manaforge omega",
},
{
id: 243306,
name: "Interloper's Reinforced Sandals",
slot: "feet",
source: "manaforge omega",
},
{
id: 242397,
name: "Sigil of the Cosmic Hunt",
slot: "trinket",
source: "manaforge omega",
},
{
id: 242394,
name: "Eradicating Arcanocore",
slot: "trinket",
source: "manaforge omega",
},
{
id: 237562,
name: "Time-Compressed Wristguards",
slot: "wrist",
source: "manaforge omega",
},
{
id: 237739,
name: "Obliteration Beamglaive",
slot: "2h-weapon",
source: "manaforge omega",
},
{
id: 242391,
name: "Soulbinder's Embrace",
slot: "trinket",
source: "manaforge omega",
},
{
id: 237734,
name: "Oath-Breaker's Recompense",
slot: "1h-weapon",
source: "manaforge omega",
},
{
id: 237726,
name: "Marvel of Technomancy",
slot: "2h-weapon",
source: "manaforge omega",
},
{
id: 237533,
name: "Atomic Phasebelt",
slot: "waist",
source: "manaforge omega",
},
{
id: 237738,
name: "Unbound Training Claws",
slot: "1h-weapon",
source: "manaforge omega",
},
{
id: 238027,
name: "Harvested Creephide Cord",
slot: "waist",
source: "manaforge omega",
},
{
id: 238031,
name: "Veiled Manta Vest",
slot: "chest",
source: "manaforge omega",
},
{
id: 237813,
name: "Factory-Issue Plexhammer",
slot: "1h-weapon",
source: "manaforge omega",
},
{
id: 237565,
name: "Kinetic Dunerunners",
slot: "feet",
source: "manaforge omega",
},
{
id: 237525,
name: "Irradiated Impurity Filter",
slot: "head",
source: "manaforge omega",
},
{
id: 237541,
name: "Darksorrow's Corrupted Carapace",
slot: "chest",
source: "manaforge omega",
},
{
id: 237731,
name: "Ergospheric Cudgel",
slot: "1h-weapon",
source: "manaforge omega",
},
{
id: 237552,
name: "Deathbound Shoulderpads",
slot: "shoulders",
source: "manaforge omega",
},
{
id: 237546,
name: "Bindings of Lost Essence",
slot: "wrist",
source: "manaforge omega",
},
{
id: 237540,
name: "Winged Gamma Handlers",
slot: "hands",
source: "manaforge omega",
},
{
id: 237557,
name: "Reaper's Dreadbelt",
slot: "waist",
source: "manaforge omega",
},
{
id: 237553,
name: "Laboratory Test Slippers",
slot: "feet",
source: "manaforge omega",
},
{
id: 237531,
name: "Elite Shadowguard Legwraps",
slot: "legs",
source: "manaforge omega",
},
{
id: 230026,
name: "Scrapfield 9001",
slot: "trinket",
source: "manaforge omega",
},
{
id: 228854,
name: "Bilgerat's Discarded Slacks",
slot: "legs",
source: "manaforge omega",
},
{
id: 242401,
name: "Brand of Ceaseless Ire",
slot: "trinket",
source: "manaforge omega",
icon: "inv_112_raidtrinkets_manaforgetanktrinket3",
},
] as const;

313
src/lib/drops/priory.json Normal file
View File

@ -0,0 +1,313 @@
[
{
"id": 219309,
"name": "Tome of Light's Devotion",
"icon": "inv_7xp_inscription_talenttome01",
"slot": "trinket",
"specs": [],
"source": "Priory of the Sacred Flame"
},
{
"id": 219308,
"name": "Signet of the Priory",
"icon": "inv_arathordungeon_signet_color1",
"slot": "trinket",
"specs": [
261,
254,
62,
1449,
102,
1447,
105,
255,
577,
63,
64,
270,
262,
1450,
65,
1451,
256,
250,
257,
1452,
73,
258,
264,
1444,
66,
265,
1446,
266,
267,
1454,
1465,
1467,
1468,
1473,
268,
1456,
581,
103,
104,
253,
260,
1448,
70,
269,
259,
1453,
263,
251,
252,
1455,
71,
72
],
"source": "Priory of the Sacred Flame"
},
{
"id": 252009,
"name": "Bloodstained Memento",
"icon": "inv_11_0_arathor_necklace_01_color1",
"slot": "neck",
"specs": [],
"source": "Priory of the Sacred Flame"
},
{
"id": 221200,
"name": "Radiant Necromancer's Band",
"icon": "inv_11_0_arathor_ring_01_color2",
"slot": "finger",
"specs": [],
"source": "Priory of the Sacred Flame"
},
{
"id": 221125,
"name": "Helm of the Righteous Crusade",
"icon": "inv_leather_earthendungeon_c_01_helm",
"slot": "head",
"specs": [
102,
1447,
105,
270,
1450,
103,
577,
581,
268,
1456,
104,
269,
259,
1453,
260,
261
],
"source": "Priory of the Sacred Flame"
},
{
"id": 221126,
"name": "Zealous Warden's Raiment",
"icon": "inv_chest_cloth_earthendungeon_c_01",
"slot": "chest",
"specs": [
62,
1449,
1452,
256,
63,
64,
265,
257,
258,
266,
267,
1454
],
"source": "Priory of the Sacred Flame"
},
{
"id": 221122,
"name": "Hand of Beledar",
"icon": "inv_mace_1h_earthendungeon_c_01",
"slot": "1h-weapon",
"specs": [
102,
258,
65,
105,
262,
270,
1451,
256,
257,
264,
1444,
1465,
1467,
1468,
1473
],
"source": "Priory of the Sacred Flame"
},
{
"id": 219310,
"name": "Bursting Lightshard",
"icon": "inv_arathordungeon_fragment_color4",
"slot": "trinket",
"specs": [
62,
1449,
102,
1447,
105,
63,
64,
270,
262,
1450,
65,
1451,
256,
257,
1452,
258,
264,
1444,
265,
266,
267,
1454,
1465,
1467,
1468,
1473
],
"source": "Priory of the Sacred Flame"
},
{
"id": 221120,
"name": "Stalwart Guardian's Boots",
"icon": "inv_leather_earthendungeon_c_01_boot",
"slot": "feet",
"specs": [
102,
1447,
105,
270,
1450,
103,
577,
581,
268,
1456,
104,
269,
259,
1453,
260,
261
],
"source": "Priory of the Sacred Flame"
},
{
"id": 221128,
"name": "Starforged Seraph's Mace",
"icon": "inv_mace_1h_earthendungeon_c_01",
"slot": "1h-weapon",
"specs": [
260,
268,
1450,
269,
263
],
"source": "Priory of the Sacred Flame"
},
{
"id": 221131,
"name": "Elysian Flame Crown",
"icon": "inv_helm_cloth_earthendungeon_c_01",
"slot": "head",
"specs": [
62,
1449,
1452,
256,
63,
64,
265,
257,
258,
266,
267,
1454
],
"source": "Priory of the Sacred Flame"
},
{
"id": 221121,
"name": "Honorbound Retainer's Sash",
"icon": "inv_belt_cloth_earthendungeon_c_01",
"slot": "waist",
"specs": [
62,
1449,
1452,
256,
63,
64,
265,
257,
258,
266,
267,
1454
],
"source": "Priory of the Sacred Flame"
},
{
"id": 221130,
"name": "Seraphic Wraps of the Ordained",
"icon": "inv_leather_earthendungeon_c_01_chest",
"slot": "chest",
"specs": [
102,
1447,
105,
270,
1450,
103,
577,
581,
268,
1456,
104,
269,
259,
1453,
260,
261
],
"source": "Priory of the Sacred Flame"
},
{
"id": 221116,
"name": "Glorious Defender's Poleaxe",
"icon": "inv_polearm_2h_earthendungeon_c_01",
"slot": "2h-weapon",
"specs": [
255,
103,
104,
1447,
268,
269
],
"source": "Priory of the Sacred Flame"
}
]

View File

@ -1,34 +0,0 @@
import type { ItemId } from "../types";
export const PrioryOfTheSacredFlame = [
{
id: 219309 as ItemId,
slot: "trinket",
name: "Tome of Light's Devotion",
source: "priory" as const,
},
{
id: 219308 as ItemId,
slot: "trinket",
name: "Signet of the Priory",
source: "priory" as const,
},
{
id: 252009 as ItemId,
slot: "neck",
name: "Bloodstained Memento",
source: "priory" as const,
},
{
id: 221200 as ItemId,
slot: "finger",
name: "Radiant Necromancer's Band",
source: "priory" as const,
},
{
id: 221125 as ItemId,
slot: "head",
name: "Helm of the Righteous Crusade",
source: "priory" as const,
},
] as const;

381
src/lib/drops/tazavesh.json Normal file
View File

@ -0,0 +1,381 @@
[
{
"id": 185812,
"name": "Acoustically Alluring Censer",
"icon": "inv_offhand_1h_broker_c_01",
"slot": "off-hand",
"specs": [
62, 1449, 102, 1447, 105, 63, 64, 270, 262, 1450, 65, 1451, 256, 257,
1452, 258, 264, 1444, 265, 266, 267, 1454, 1465, 1467, 1468, 1473
],
"source": "Streets of Wonder"
},
{
"id": 185801,
"name": "Anomalous Starlit Breeches",
"icon": "inv_pant_leather_oribosdungeon_c_01",
"slot": "legs",
"specs": [
102, 1447, 105, 270, 1450, 103, 577, 581, 268, 1456, 104, 269, 259, 1453,
260, 261
],
"source": "So'leah's Gambit"
},
{
"id": 185814,
"name": "Auctioneer's Counting Bracers",
"icon": "inv_bracer_cloth_oribosdungeon_c_01",
"slot": "wrist",
"specs": [62, 1449, 1452, 256, 63, 64, 265, 257, 258, 266, 267, 1454],
"source": "Streets of Wonder"
},
{
"id": 185824,
"name": "Blade of Grievous Harm",
"icon": "inv_sword_1h_broker2boss_d_01_blue",
"slot": "1h-weapon",
"specs": [251, 72, 1451, 66, 73, 1446],
"source": "Streets of Wonder"
},
{
"id": 246280,
"name": "Boots of Titanic Deconversion",
"icon": "inv_boot_leather_oribosdungeon_c_01",
"slot": "feet",
"specs": [
102, 1447, 105, 270, 1450, 103, 577, 581, 268, 1456, 104, 269, 259, 1453,
260, 261
],
"source": "So'leah's Gambit"
},
{
"id": 185817,
"name": "Bracers of Autonomous Classification",
"icon": "inv_bracer_leather_oribosdungeon_c_01",
"slot": "wrist",
"specs": [
102, 1447, 105, 270, 1450, 103, 577, 581, 268, 1456, 104, 269, 259, 1453,
260, 261
],
"source": "Streets of Wonder"
},
{
"id": 185802,
"name": "Breakbeat Shoulderguards",
"icon": "inv_shoulder_leather_oribosdungeon_c_01",
"slot": "shoulders",
"specs": [
102, 1447, 105, 270, 1450, 103, 577, 581, 268, 1456, 104, 269, 259, 1453,
260, 261
],
"source": "Streets of Wonder"
},
{
"id": 185820,
"name": "Cabochon of the Infinite Flight",
"icon": "inv_7_0raid_necklace_02c",
"slot": "neck",
"specs": [],
"source": "So'leah's Gambit"
},
{
"id": 246275,
"name": "Codebreaker's Cunning Handwraps",
"icon": "inv_glove_cloth_oribosdungeon_c_01",
"slot": "hands",
"specs": [62, 1449, 1452, 256, 63, 64, 265, 257, 258, 266, 267, 1454],
"source": "So'leah's Gambit"
},
{
"id": 185788,
"name": "Codebreaker's Cunning Sandals",
"icon": "inv_boot_cloth_oribosdungeon_c_01",
"slot": "feet",
"specs": [62, 1449, 1452, 256, 63, 64, 265, 257, 258, 266, 267, 1454],
"source": "So'leah's Gambit"
},
{
"id": 185836,
"name": "Codex of the First Technique",
"icon": "inv_misc_profession_book_enchanting",
"slot": "trinket",
"specs": [],
"source": "Streets of Wonder"
},
{
"id": 185795,
"name": "Cowl of Branching Fate",
"icon": "inv_helm_cloth_oribosdungeon_c_01",
"slot": "head",
"specs": [62, 1449, 1452, 256, 63, 64, 265, 257, 258, 266, 267, 1454],
"source": "So'leah's Gambit"
},
{
"id": 185793,
"name": "Cyphered Gloves",
"icon": "inv_glove_cloth_oribosdungeon_c_01",
"slot": "hands",
"specs": [62, 1449, 1452, 256, 63, 64, 265, 257, 258, 266, 267, 1454],
"source": "Streets of Wonder"
},
{
"id": 185781,
"name": "Drape of Titanic Dreams",
"icon": "inv_plate_oribosdungeon_c_01_cape",
"slot": "1h-weapon",
"specs": [
261, 254, 62, 1449, 102, 1447, 105, 255, 577, 63, 64, 270, 262, 1450, 65,
1451, 256, 250, 257, 1452, 73, 258, 264, 1444, 66, 265, 1446, 266, 267,
1454, 1465, 1467, 1468, 1473, 268, 1456, 581, 103, 104, 253, 260, 1448,
70, 269, 259, 1453, 263, 251, 252, 1455, 71, 72
],
"source": "So'leah's Gambit"
},
{
"id": 185843,
"name": "Duplicating Drape",
"icon": "inv_cape_leather_oribosdungeon_c_01",
"slot": "1h-weapon",
"specs": [
261, 254, 62, 1449, 102, 1447, 105, 255, 577, 63, 64, 270, 262, 1450, 65,
1451, 256, 250, 257, 1452, 73, 258, 264, 1444, 66, 265, 1446, 266, 267,
1454, 1465, 1467, 1468, 1473, 268, 1456, 581, 103, 104, 253, 260, 1448,
70, 269, 259, 1453, 263, 251, 252, 1455, 71, 72
],
"source": "Streets of Wonder"
},
{
"id": 185823,
"name": "Fatebreaker, Destroyer of Futures",
"icon": "inv_mace_1h_broker_c_01",
"slot": "1h-weapon",
"specs": [260, 268, 1450, 269, 263],
"source": "So'leah's Gambit"
},
{
"id": 185845,
"name": "First Class Healing Distributor",
"icon": "inv_7_0raid_trinket_09a",
"slot": "trinket",
"specs": [
62, 1449, 102, 1447, 105, 63, 64, 270, 262, 1450, 65, 1451, 256, 257,
1452, 258, 264, 1444, 265, 266, 267, 1454, 1465, 1467, 1468, 1473
],
"source": "Streets of Wonder"
},
{
"id": 185778,
"name": "First Fist of the So Cartel",
"icon": "inv_hand_1h_brokerassassin_c_01",
"slot": "1h-weapon",
"specs": [577, 260, 268, 581, 1456, 1450, 269, 263],
"source": "Streets of Wonder"
},
{
"id": 185804,
"name": "Harmonious Spaulders",
"icon": "inv_shoulder_cloth_oribosdungeon_c_01",
"slot": "shoulders",
"specs": [62, 1449, 1452, 256, 63, 64, 265, 257, 258, 266, 267, 1454],
"source": "Streets of Wonder"
},
{
"id": 185799,
"name": "Hyperlight Leggings",
"icon": "inv_pant_cloth_oribosdungeon_c_01",
"slot": "legs",
"specs": [62, 1449, 1452, 256, 63, 64, 265, 257, 258, 266, 267, 1454],
"source": "So'leah's Gambit"
},
{
"id": 185780,
"name": "Interrogator's Flensing Blade",
"icon": "inv_sword_1h_broker2boss_d_01_blue",
"slot": "1h-weapon",
"specs": [577, 260, 268, 581, 1450, 269],
"source": "Streets of Wonder"
},
{
"id": 185791,
"name": "Knuckle-Dusting Handwraps",
"icon": "inv_glove_leather_oribosdungeon_c_01",
"slot": "hands",
"specs": [
102, 1447, 105, 270, 1450, 103, 577, 581, 268, 1456, 104, 269, 259, 1453,
260, 261
],
"source": "Streets of Wonder"
},
{
"id": 185846,
"name": "Miniscule Mailemental in an Envelope",
"icon": "inv_misc_paperpackage01b",
"slot": "trinket",
"specs": [],
"source": "Streets of Wonder"
},
{
"id": 185842,
"name": "Ornately Engraved Amplifier",
"icon": "inv_misc_silverjadenecklace",
"slot": "neck",
"specs": [],
"source": "Streets of Wonder"
},
{
"id": 185807,
"name": "Pan-Dimensional Packing Cord",
"icon": "inv_belt_cloth_oribosdungeon_c_01",
"slot": "waist",
"specs": [62, 1449, 1452, 256, 63, 64, 265, 257, 258, 266, 267, 1454],
"source": "Streets of Wonder"
},
{
"id": 185797,
"name": "Rakishly Tipped Tricorne",
"icon": "inv_helm_leather_oribosdungeon_c_01",
"slot": "head",
"specs": [
102, 1447, 105, 270, 1450, 103, 577, 581, 268, 1456, 104, 269, 259, 1453,
260, 261
],
"source": "So'leah's Gambit"
},
{
"id": 246281,
"name": "Ring of the Panoply",
"icon": "inv_misc_60raid_ring_1b",
"slot": "finger",
"specs": [],
"source": "Streets of Wonder"
},
{
"id": 185782,
"name": "Robes of Midnight Bargains",
"icon": "inv_robe_cloth_oribosdungeon_c_01",
"slot": "chest",
"specs": [62, 1449, 1452, 256, 63, 64, 265, 257, 258, 266, 267, 1454],
"source": "Streets of Wonder"
},
{
"id": 185840,
"name": "Seal of the Panoply",
"icon": "inv_misc_60raid_ring_3b",
"slot": "finger",
"specs": [],
"source": "Streets of Wonder"
},
{
"id": 185813,
"name": "Signet of Collapsing Stars",
"icon": "inv_jewelcrafting_80_maxlvlring_blue",
"slot": "finger",
"specs": [],
"source": "So'leah's Gambit"
},
{
"id": 185786,
"name": "So'azmi's Fractal Vest",
"icon": "inv_chest_leather_oribosdungeon_c_01",
"slot": "chest",
"specs": [
102, 1447, 105, 270, 1450, 103, 577, 581, 268, 1456, 104, 269, 259, 1453,
260, 261
],
"source": "Streets of Wonder"
},
{
"id": 190958,
"name": "So'leah's Secret Technique",
"icon": "inv_60pvp_trinket1d",
"slot": "trinket",
"specs": [
261, 254, 62, 1449, 102, 1447, 105, 255, 577, 63, 64, 270, 262, 1450, 65,
1451, 256, 250, 257, 1452, 73, 258, 264, 1444, 66, 265, 1446, 266, 267,
1454, 1465, 1467, 1468, 1473, 268, 1456, 581, 103, 104, 253, 260, 1448,
70, 269, 259, 1453, 263, 251, 252, 1455, 71, 72
],
"source": "So'leah's Gambit"
},
{
"id": 185818,
"name": "So'leah's Secret Technique",
"icon": "inv_60pvp_trinket1d",
"slot": "trinket",
"specs": [
261, 254, 62, 1449, 102, 1447, 105, 255, 577, 63, 64, 270, 262, 1450, 65,
1451, 256, 250, 257, 1452, 73, 258, 264, 1444, 66, 265, 1446, 266, 267,
1454, 1465, 1467, 1468, 1473, 268, 1456, 581, 103, 104, 253, 260, 1448,
70, 269, 259, 1453, 263, 251, 252, 1455, 71, 72
],
"source": "So'leah's Gambit"
},
{
"id": 185779,
"name": "Spire of Expurgation",
"icon": "inv_staff_2h_broker_c_01",
"slot": "2h-weapon",
"specs": [255, 103, 104, 1447, 268, 269],
"source": "So'leah's Gambit"
},
{
"id": 185822,
"name": "Staff of Fractured Spacetime",
"icon": "inv_staff_2h_broker_c_01",
"slot": "2h-weapon",
"specs": [
62, 1449, 102, 1447, 105, 256, 63, 64, 270, 257, 1452, 258, 262, 264, 265,
266, 267, 1454, 1465, 1467, 1468, 1473
],
"source": "So'leah's Gambit"
},
{
"id": 190652,
"name": "Ticking Sack of Terror",
"icon": "inv_misc_bag_felclothbag",
"slot": "trinket",
"specs": [],
"source": "Streets of Wonder"
},
{
"id": 185844,
"name": "Ticking Sack of Terror",
"icon": "inv_misc_bag_felclothbag",
"slot": "trinket",
"specs": [],
"source": "Streets of Wonder"
},
{
"id": 185841,
"name": "Timetwister Tulwar",
"icon": "inv_sword_1h_broker_c_01",
"slot": "1h-weapon",
"specs": [
62, 63, 1451, 64, 270, 266, 65, 265, 267, 1454, 1465, 1467, 1468, 1473
],
"source": "So'leah's Gambit"
},
{
"id": 185790,
"name": "Treads of Titanic Deconversion",
"icon": "inv_boot_leather_oribosdungeon_c_01",
"slot": "feet",
"specs": [
102, 1447, 105, 270, 1450, 103, 577, 581, 268, 1456, 104, 269, 259, 1453,
260, 261
],
"source": "So'leah's Gambit"
},
{
"id": 185809,
"name": "Venza's Powderbelt",
"icon": "inv_belt_leather_oribosdungeon_c_01",
"slot": "waist",
"specs": [
102, 1447, 105, 270, 1450, 103, 577, 581, 268, 1456, 104, 269, 259, 1453,
260, 261
],
"source": "Streets of Wonder"
}
]

View File

@ -1,115 +0,0 @@
import type { ItemId } from "../types";
export const Gambit = [
{
id: 190958 as ItemId,
slot: "trinket",
name: "So'leah's Secret Technique",
source: "gambit" as const,
},
{
id: 185818 as ItemId,
slot: "trinket",
name: "So'leah's Secret Technique",
source: "gambit" as const,
},
{
id: 185823 as ItemId,
slot: "1h-weapon",
name: "Fatebreaker, Destroyer of Futures",
source: "gambit" as const,
},
{
id: 246280 as ItemId,
slot: "feet",
name: "Boots of Titanic Deconversion",
source: "gambit" as const,
},
{
id: 185781 as ItemId,
slot: "back",
name: "Drape of Titanic Dreams",
source: "gambit" as const,
},
{
id: 185790 as ItemId,
slot: "feet",
name: "Treads of Titanic Deconversion",
source: "gambit" as const,
},
{
id: 185797 as ItemId,
slot: "head",
name: "Rakishly Tipped Tricorne",
source: "gambit" as const,
},
{
id: 185801 as ItemId,
slot: "legs",
name: "Anomalous Starlit Breeches",
source: "gambit" as const,
},
] as const;
export const Streets = [
{
id: 185780 as ItemId,
slot: "1h-weapon",
name: "Interrogator's Flensing Blade",
source: "streets" as const,
},
{
id: 185778 as ItemId,
slot: "1h-weapon",
name: "First Fist of the So Cartel",
source: "streets" as const,
},
{
id: 185824 as ItemId,
slot: "1h-weapon",
name: "Blade of Grievous Harm",
source: "streets" as const,
},
{
id: 185809 as ItemId,
slot: "waist",
name: "Venza's Powderbelt",
source: "streets" as const,
},
{
id: 185779 as ItemId,
slot: "2h-weapon",
name: "Spire of Expurgation",
source: "streets" as const,
},
{
id: 185802 as ItemId,
slot: "shoulders",
name: "Breakbeat Shoulderguards",
source: "streets" as const,
},
{
id: 185786 as ItemId,
slot: "chest",
name: "So'azmi's Fractal Vest",
source: "streets" as const,
},
{
id: 185791 as ItemId,
slot: "hands",
name: "Knuckle-Dusting Handwraps",
source: "streets" as const,
},
{
id: 185817 as ItemId,
slot: "wrist",
name: "Bracers of Autonomous Classification",
source: "streets" as const,
},
{
id: 185843 as ItemId,
slot: "back",
name: "Duplicating Drape",
source: "streets" as const,
},
] as const;

View File

@ -0,0 +1,523 @@
[
{
"id": 237631,
"name": "Hollow Sentinel's Breastplate",
"icon": "inv_chest_plate_raiddeathknightethereal_d_01",
"slot": "chest",
"specs": [250, 251, 252, 1455],
"source": "Tier"
},
{
"id": 237629,
"name": "Hollow Sentinel's Gauntlets",
"icon": "inv_glove_plate_raiddeathknightethereal_d_01",
"slot": "hands",
"specs": [250, 251, 252, 1455],
"source": "Tier"
},
{
"id": 237628,
"name": "Hollow Sentinel's Stonemask",
"icon": "inv_helm_plate_raiddeathknightethereal_d_01",
"slot": "head",
"specs": [250, 251, 252, 1455],
"source": "Tier"
},
{
"id": 237627,
"name": "Hollow Sentinel's Stonekilt",
"icon": "inv_pant_plate_raiddeathknightethereal_d_01",
"slot": "legs",
"specs": [250, 251, 252, 1455],
"source": "Tier"
},
{
"id": 237626,
"name": "Hollow Sentinel's Perches",
"icon": "inv_shoulder_plate_raiddeathknightethereal_d_01",
"slot": "shoulders",
"specs": [250, 251, 252, 1455],
"source": "Tier"
},
{
"id": 237608,
"name": "Living Weapon's Ramparts",
"icon": "inv_shoulder_plate_raidwarriorethereal_d_01",
"slot": "shoulders",
"specs": [71, 72, 73, 1446],
"source": "Tier"
},
{
"id": 237613,
"name": "Living Weapon's Bulwark",
"icon": "inv_chest_plate_raidwarriorethereal_d_01",
"slot": "chest",
"specs": [71, 72, 73, 1446],
"source": "Tier"
},
{
"id": 237611,
"name": "Living Weapon's Crushers",
"icon": "inv_glove_plate_raidwarriorethereal_d_01",
"slot": "hands",
"specs": [71, 72, 73, 1446],
"source": "Tier"
},
{
"id": 237610,
"name": "Living Weapon's Faceshield",
"icon": "inv_helm_plate_raidwarriorethereal_d_01",
"slot": "head",
"specs": [71, 72, 73, 1446],
"source": "Tier"
},
{
"id": 237609,
"name": "Living Weapon's Legguards",
"icon": "inv_pant_plate_raidwarriorethereal_d_01",
"slot": "legs",
"specs": [71, 72, 73, 1446],
"source": "Tier"
},
{
"id": 237701,
"name": "Inquisitor's Clutches of Madness",
"icon": "inv_glove_cloth_raidwarlockethereal_d_01",
"slot": "hands",
"specs": [265, 1454, 266, 267],
"source": "Tier"
},
{
"id": 237700,
"name": "Inquisitor's Portal to Madness",
"icon": "inv_helm_cloth_raidwarlockethereal_d_01",
"slot": "head",
"specs": [265, 1454, 266, 267],
"source": "Tier"
},
{
"id": 237699,
"name": "Inquisitor's Leggings of Madness",
"icon": "inv_pant_cloth_raidwarlockethereal_d_01",
"slot": "legs",
"specs": [265, 1454, 266, 267],
"source": "Tier"
},
{
"id": 237703,
"name": "Inquisitor's Robes of Madness",
"icon": "inv_robe_cloth_raidwarlockethereal_d_01",
"slot": "chest",
"specs": [265, 1454, 266, 267],
"source": "Tier"
},
{
"id": 237698,
"name": "Inquisitor's Gaze of Madness",
"icon": "inv_shoulder_cloth_raidwarlockethereal_d_01",
"slot": "shoulders",
"specs": [265, 1454, 266, 267],
"source": "Tier"
},
{
"id": 237636,
"name": "Tassets of Channeled Fury",
"icon": "inv_pant_mail_raidshamanethereal_d_01",
"slot": "legs",
"specs": [262, 264, 1444, 263],
"source": "Tier"
},
{
"id": 237637,
"name": "Aspect of Channeled Fury",
"icon": "inv_helm_mail_raidshamanethereal_d_01",
"slot": "head",
"specs": [262, 264, 1444, 263],
"source": "Tier"
},
{
"id": 237638,
"name": "Claws of Channeled Fury",
"icon": "inv_glove_mail_raidshamanethereal_d_01",
"slot": "hands",
"specs": [262, 264, 1444, 263],
"source": "Tier"
},
{
"id": 237640,
"name": "Furs of Channeled Fury",
"icon": "inv_chest_mail_raidshamanethereal_d_01",
"slot": "chest",
"specs": [262, 264, 1444, 263],
"source": "Tier"
},
{
"id": 237635,
"name": "Fangs of Channeled Fury",
"icon": "inv_shoulder_mail_raidshamanethereal_d_01",
"slot": "shoulders",
"specs": [262, 264, 1444, 263],
"source": "Tier"
},
{
"id": 237662,
"name": "Smokemantle of the Sudden Eclipse",
"icon": "inv_shoulder_leather_raidrogueethereal_d_01",
"slot": "shoulders",
"specs": [259, 1453, 260, 261],
"source": "Tier"
},
{
"id": 237667,
"name": "Tactical Vest of the Sudden Eclipse",
"icon": "inv_chest_leather_raidrogueethereal_d_01",
"slot": "chest",
"specs": [259, 1453, 260, 261],
"source": "Tier"
},
{
"id": 237665,
"name": "Deathgrips of the Sudden Eclipse",
"icon": "inv_glove_leather_raidrogueethereal_d_01",
"slot": "hands",
"specs": [259, 1453, 260, 261],
"source": "Tier"
},
{
"id": 237664,
"name": "Hood of the Sudden Eclipse",
"icon": "inv_helm_leather_raidrogueethereal_d_01",
"slot": "head",
"specs": [259, 1453, 260, 261],
"source": "Tier"
},
{
"id": 237663,
"name": "Pants of the Sudden Eclipse",
"icon": "inv_pant_leather_raidrogueethereal_d_01",
"slot": "legs",
"specs": [259, 1453, 260, 261],
"source": "Tier"
},
{
"id": 237712,
"name": "Dying Star's Cassock",
"icon": "inv_robe_cloth_raidpriestethereal_d_01",
"slot": "chest",
"specs": [1452, 256, 257, 258],
"source": "Tier"
},
{
"id": 237708,
"name": "Dying Star's Leggings",
"icon": "inv_pant_cloth_raidpriestethereal_d_01",
"slot": "legs",
"specs": [1452, 256, 257, 258],
"source": "Tier"
},
{
"id": 237709,
"name": "Dying Star's Veil",
"icon": "inv_helm_cloth_raidpriestethereal_d_01",
"slot": "head",
"specs": [1452, 256, 257, 258],
"source": "Tier"
},
{
"id": 237710,
"name": "Dying Star's Caress",
"icon": "inv_glove_cloth_raidpriestethereal_d_01",
"slot": "hands",
"specs": [1452, 256, 257, 258],
"source": "Tier"
},
{
"id": 237707,
"name": "Dying Star's Pyrelights",
"icon": "inv_shoulder_cloth_raidpriestethereal_d_01",
"slot": "shoulders",
"specs": [1452, 256, 257, 258],
"source": "Tier"
},
{
"id": 237617,
"name": "Chargers of the Lucent Battalion",
"icon": "inv_plate_raidpaladinethereal_d_01_shoulder",
"slot": "shoulders",
"specs": [65, 1451, 66, 70],
"source": "Tier"
},
{
"id": 237622,
"name": "Cuirass of the Lucent Battalion",
"icon": "inv_plate_raidpaladinethereal_d_01_chest",
"slot": "chest",
"specs": [65, 1451, 66, 70],
"source": "Tier"
},
{
"id": 237620,
"name": "Protectors of the Lucent Battalion",
"icon": "inv_plate_raidpaladinethereal_d_01_glove",
"slot": "hands",
"specs": [65, 1451, 66, 70],
"source": "Tier"
},
{
"id": 237619,
"name": "Lightmane of the Lucent Battalion",
"icon": "inv_plate_raidpaladinethereal_d_01_helm",
"slot": "head",
"specs": [65, 1451, 66, 70],
"source": "Tier"
},
{
"id": 237618,
"name": "Cuisses of the Lucent Battalion",
"icon": "inv_plate_raidpaladinethereal_d_01_pant",
"slot": "legs",
"specs": [65, 1451, 66, 70],
"source": "Tier"
},
{
"id": 237671,
"name": "Glyphs of Fallen Storms",
"icon": "inv_shoulder_leather_raidmonkethereal_d_01",
"slot": "shoulders",
"specs": [270, 1450, 268, 269],
"source": "Tier"
},
{
"id": 237676,
"name": "Gi of Fallen Storms",
"icon": "inv_chest_leather_raidmonkethereal_d_01",
"slot": "chest",
"specs": [270, 1450, 268, 269],
"source": "Tier"
},
{
"id": 237674,
"name": "Grasp of Fallen Storms",
"icon": "inv_glove_leather_raidmonkethereal_d_01",
"slot": "hands",
"specs": [270, 1450, 268, 269],
"source": "Tier"
},
{
"id": 237673,
"name": "Half-Mask of Fallen Storms",
"icon": "inv_helm_leather_raidmonkethereal_d_01",
"slot": "head",
"specs": [270, 1450, 268, 269],
"source": "Tier"
},
{
"id": 237672,
"name": "Legwraps of Fallen Storms",
"icon": "inv_pant_leather_raidmonkethereal_d_01",
"slot": "legs",
"specs": [270, 1450, 268, 269],
"source": "Tier"
},
{
"id": 237716,
"name": "Augur's Ephemeral Orbs of Power",
"icon": "inv_shoulder_cloth_raidmageethereal_d_01",
"slot": "shoulders",
"specs": [1449, 62, 63, 64],
"source": "Tier"
},
{
"id": 237721,
"name": "Augur's Ephemeral Habiliments",
"icon": "inv_robe_cloth_raidmageethereal_d_01",
"slot": "chest",
"specs": [1449, 62, 63, 64],
"source": "Tier"
},
{
"id": 237719,
"name": "Augur's Ephemeral Mitts",
"icon": "inv_glove_cloth_raidmageethereal_d_01",
"slot": "hands",
"specs": [1449, 62, 63, 64],
"source": "Tier"
},
{
"id": 237718,
"name": "Augur's Ephemeral Wide-Brim",
"icon": "inv_helm_cloth_raidmageethereal_d_01",
"slot": "head",
"specs": [1449, 62, 63, 64],
"source": "Tier"
},
{
"id": 237717,
"name": "Augur's Ephemeral Trousers",
"icon": "inv_pant_cloth_raidmageethereal_d_01",
"slot": "legs",
"specs": [1449, 62, 63, 64],
"source": "Tier"
},
{
"id": 237649,
"name": "Midnight Herald's Hauberk",
"icon": "inv_chest_mail_raidhunterethereal_d_01",
"slot": "chest",
"specs": [253, 1448, 254, 255],
"source": "Tier"
},
{
"id": 237647,
"name": "Midnight Herald's Gloves",
"icon": "inv_glove_mail_raidhunterethereal_d_01",
"slot": "hands",
"specs": [253, 1448, 254, 255],
"source": "Tier"
},
{
"id": 237646,
"name": "Midnight Herald's Cowl",
"icon": "inv_helm_mail_raidhunterethereal_d_01",
"slot": "head",
"specs": [253, 1448, 254, 255],
"source": "Tier"
},
{
"id": 237645,
"name": "Midnight Herald's Petticoat",
"icon": "inv_pant_mail_raidhunterethereal_d_01",
"slot": "legs",
"specs": [253, 1448, 254, 255],
"source": "Tier"
},
{
"id": 237644,
"name": "Midnight Herald's Shadowguards",
"icon": "inv_shoulder_mail_raidhunterethereal_d_01",
"slot": "shoulders",
"specs": [253, 1448, 254, 255],
"source": "Tier"
},
{
"id": 237654,
"name": "Spellweaver's Immaculate Runeslacks",
"icon": "inv_pant_mail_raidevokerethereal_d_01",
"slot": "legs",
"specs": [1473, 1465, 1467, 1468],
"source": "Tier"
},
{
"id": 237655,
"name": "Spellweaver's Immaculate Focus",
"icon": "inv_helm_mail_raidevokerethereal_d_01",
"slot": "head",
"specs": [1473, 1465, 1467, 1468],
"source": "Tier"
},
{
"id": 237656,
"name": "Spellweaver's Immaculate Scaleguards",
"icon": "inv_glove_mail_raidevokerethereal_d_01",
"slot": "hands",
"specs": [1473, 1465, 1467, 1468],
"source": "Tier"
},
{
"id": 237658,
"name": "Spellweaver's Immaculate Crestward",
"icon": "inv_chest_mail_raidevokerethereal_d_01",
"slot": "chest",
"specs": [1473, 1465, 1467, 1468],
"source": "Tier"
},
{
"id": 237653,
"name": "Spellweaver's Immaculate Pauldrons",
"icon": "inv_shoulder_mail_raidevokerethereal_d_01",
"slot": "shoulders",
"specs": [1473, 1465, 1467, 1468],
"source": "Tier"
},
{
"id": 237685,
"name": "Vest of the Mother Eagle",
"icon": "inv_chest_leather_raiddruidethereal_d_01",
"slot": "chest",
"specs": [102, 1447, 105, 103, 104],
"source": "Tier"
},
{
"id": 237683,
"name": "Wings of the Mother Eagle",
"icon": "inv_glove_leather_raiddruidethereal_d_01",
"slot": "hands",
"specs": [102, 1447, 105, 103, 104],
"source": "Tier"
},
{
"id": 237682,
"name": "Skymane of the Mother Eagle",
"icon": "inv_helm_leather_raiddruidethereal_d_01",
"slot": "head",
"specs": [102, 1447, 105, 103, 104],
"source": "Tier"
},
{
"id": 237681,
"name": "Breeches of the Mother Eagle",
"icon": "inv_pant_leather_raiddruidethereal_d_01",
"slot": "legs",
"specs": [102, 1447, 105, 103, 104],
"source": "Tier"
},
{
"id": 237680,
"name": "Ritual Pauldrons of the Mother Eagle",
"icon": "inv_shoulder_leather_raiddruidethereal_d_01",
"slot": "shoulders",
"specs": [102, 1447, 105, 103, 104],
"source": "Tier"
},
{
"id": 237690,
"name": "Charhound's Vicious Hidecoat",
"icon": "inv_pant_leather_raiddemonhunterethereal_d_01",
"slot": "legs",
"specs": [577, 581, 1456],
"source": "Tier"
},
{
"id": 237692,
"name": "Charhound's Vicious Felclaws",
"icon": "inv_glove_leather_raiddemonhunterethereal_d_01",
"slot": "hands",
"specs": [577, 581, 1456],
"source": "Tier"
},
{
"id": 237691,
"name": "Charhound's Vicious Scalp",
"icon": "inv_helm_leather_raiddemonhunterethereal_d_01",
"slot": "head",
"specs": [577, 581, 1456],
"source": "Tier"
},
{
"id": 237694,
"name": "Charhound's Vicious Bindings",
"icon": "inv_chest_leather_raiddemonhunterethereal_d_01",
"slot": "chest",
"specs": [577, 581, 1456],
"source": "Tier"
},
{
"id": 237689,
"name": "Charhound's Vicious Hornguards",
"icon": "inv_shoulder_leather_raiddemonhunterethereal_d_01",
"slot": "shoulders",
"specs": [577, 581, 1456],
"source": "Tier"
}
]

View File

@ -2,6 +2,7 @@ import _ from "lodash";
import { AllItems } from "./drops";
import type { State } from "./state";
import {
Spec,
UpgradeType,
WeaponConfig,
type Item,
@ -19,12 +20,20 @@ export const ItemsById: Record<string, Item> = AllItems.reduce(
{} as Record<ItemId, Item>,
);
const itemHasSpec =
(spec: Spec) =>
(item: Item): boolean =>
item.specs && (item.specs.length === 0 || item.specs?.includes(spec));
export const ItemsBySlot: Record<Slot, Item[]> = _.groupBy(
AllItems,
(item: Item) => item.slot,
) as Record<Slot, Item[]>;
export const itemsForSlot = (slot: Slot): Item[] => ItemsBySlot[slot] ?? [];
export const itemsForSlot =
(spec: Spec) =>
(slot: Slot): Item[] =>
ItemsBySlot[slot]?.filter(itemHasSpec(spec)) ?? [];
function qualityToNumber(quality: Quality): number {
switch (quality) {
@ -70,14 +79,16 @@ export const getUpgrades = ({
equipedItems,
bisList,
weaponConfig,
spec,
}: State): Upgrade[] => {
return Object.entries(ItemsBySlot).flatMap(([slot, items]) => {
return Object.entries(ItemsBySlot).flatMap(([slot, allItems]) => {
const items = allItems.filter(itemHasSpec(spec)); // Assuming spec 1 for simplicity
const equipedItemsHydrated = equipedItems.flatMap(({ id, quality }) => ({
item: ItemsById[id],
quality,
}));
const equipedInSlot = equipedItemsHydrated.filter(
(item) => item.item.slot === slot,
(item) => item.item?.slot === slot,
);
const itemsForSlot = itemsPerSlot(slot as Slot, weaponConfig);

View File

@ -1,16 +1,18 @@
import type { EquipedItem, Item, ItemId, Quality } from "./types";
import { WeaponConfig } from "./types";
import { Spec, WeaponConfig } from "./types";
export type State = {
equipedItems: EquipedItem[];
bisList: ItemId[];
weaponConfig: WeaponConfig;
spec: Spec;
};
export const emptyState: State = {
equipedItems: [],
bisList: [],
weaponConfig: WeaponConfig.TwoHander, // Default weapon config
spec: Spec.Monk.Brewmaster,
};
export type Action =
@ -32,6 +34,10 @@ export type Action =
itemId: ItemId;
isBis: boolean;
}
| {
action: "changeSpec";
spec: Spec;
}
| {
action: "changeWeaponConfig";
weaponConfig: WeaponConfig;
@ -76,6 +82,11 @@ export const reducer = (state: State, action: Action): State => {
...(action.isBis ? [action.itemId] : []),
],
};
case "changeSpec":
return {
...state,
spec: action.spec,
};
case "changeWeaponConfig":
return {
...state,

View File

@ -30,17 +30,7 @@ export type Quality = (typeof Quality)[keyof typeof Quality];
export type ItemId = number & { __type: "ItemId" };
export type Source =
| "aldani"
| "ara-kara"
| "dawnbreaker"
| "floodgate"
| "gambit"
| "halls-of-atonement"
| "priory"
| "streets"
| "manaforge omega"
| "crafted";
export type Source = string & { __type: "Source" };
export type Item = {
id: ItemId;
@ -49,6 +39,7 @@ export type Item = {
source: Source;
icon?: string; // Wowhead icon name (not ID)
tags?: Tag[];
specs: number[];
};
export type EquipedItem = {
@ -82,3 +73,69 @@ export const Tag = {
};
export type Tag = (typeof Tag)[keyof typeof Tag];
export const Spec = {
Mage: {
Arcane: 62,
Fire: 63,
Frost: 64,
},
Paladin: {
Holy: 65,
Protection: 66,
Retribution: 70,
},
Warrior: {
Arms: 71,
Fury: 72,
Protection: 73,
},
Druid: {
Feral: 103,
Balance: 102,
Guardian: 104,
Restoration: 105,
},
DeathKnight: {
Blood: 250,
Frost: 251,
Unholy: 252,
},
Hunter: {
Marksmanship: 253,
Survival: 254,
BeastMastery: 255,
},
Priest: {
Discipline: 256,
Holy: 257,
Shadow: 258,
},
Rogue: {
Assassination: 259,
Subtlety: 260,
Outlaw: 261,
},
Shaman: {
Elemental: 262,
Enhancement: 263,
Restoration: 264,
},
Warlock: {
Affliction: 265,
Demonology: 266,
Destruction: 267,
},
Monk: {
Brewmaster: 268,
Windwalker: 269,
Mistweaver: 270,
},
DemonHunter: {
Havoc: 577,
Vengeance: 581,
},
};
export type Spec =
(typeof Spec)[keyof typeof Spec][keyof (typeof Spec)[keyof typeof Spec]];