initial commit

This commit is contained in:
2026-03-22 05:17:01 +00:00
parent 9a41fa4ef2
commit ef6e94958a
155 changed files with 43839 additions and 0 deletions

283
scripts/extract-data.mjs Normal file
View File

@@ -0,0 +1,283 @@
/**
* Extracts dungeon data from index.original.html and emits TypeScript data files.
* Run with: node scripts/extract-data.mjs
*/
import { readFileSync, writeFileSync, mkdirSync } from 'fs';
import { JSDOM } from 'jsdom';
const html = readFileSync('index.original.html', 'utf-8');
const dom = new JSDOM(html);
const doc = dom.window.document;
// ── Sidebar: extract dungeon list ──
const tabs = doc.querySelectorAll('.dungeon-tab');
const dungeonList = [];
for (const tab of tabs) {
const img = tab.querySelector('img');
const href = tab.getAttribute('href');
const isDisabled = tab.classList.contains('disabled');
const name = tab.textContent.trim();
const icon = img ? `/assets/${img.getAttribute('src').split('/').pop()}` : '';
const id = href ? href.replace('#', '') : name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, '');
dungeonList.push({ id, name, icon, disabled: isDisabled || undefined });
}
// ── Parse each dungeon page ──
const dungeonPages = doc.querySelectorAll('.dungeon-page');
const dungeonDataMap = {};
for (const page of dungeonPages) {
const id = page.getAttribute('id');
// Header
const header = page.querySelector('.dungeon-header');
const headerImg = header?.querySelector('img');
const headerText = header?.querySelector('.dungeon-header-text');
const name = headerText?.querySelector('h1')?.textContent?.trim() || id;
const descP = headerText?.querySelector('p');
const descriptionHtml = descP ? descP.innerHTML.trim() : '';
const headerImage = headerImg ? `/assets/${headerImg.getAttribute('src').split('/').pop()}` : '';
// Find icon from dungeonList
const listItem = dungeonList.find(d => d.id === id);
const icon = listItem?.icon || '';
// Parse sections in order
const sections = [];
const children = page.children;
for (let i = 0; i < children.length; i++) {
const child = children[i];
if (child.classList.contains('dungeon-header')) continue;
if (child.classList.contains('section-divider')) {
sections.push({ type: 'divider' });
continue;
}
if (child.classList.contains('trash-section')) {
sections.push({ type: 'trash', data: parseTrashSection(child) });
continue;
}
if (child.classList.contains('boss-section')) {
sections.push({ type: 'boss', data: parseBossSection(child) });
continue;
}
}
dungeonDataMap[id] = { id, name, descriptionHtml, headerImage, icon, sections };
}
function parseTrashSection(el) {
const headerEl = el.querySelector('.trash-header');
const header = headerEl?.textContent?.trim() || '';
const content = [];
const children = el.children;
for (let i = 0; i < children.length; i++) {
const child = children[i];
if (child.classList.contains('trash-header')) continue;
if (child.classList.contains('quick-ref')) {
content.push({ type: 'quickRef', data: { html: child.innerHTML.trim() } });
continue;
}
if (child.classList.contains('mob-card')) {
content.push({ type: 'mob', data: parseMobCard(child) });
continue;
}
}
return { header, content };
}
function parseMobCard(el) {
const nameEl = el.querySelector('.mob-name');
const name = nameEl?.textContent?.trim() || '';
const nameHtml = nameEl?.innerHTML?.trim() || '';
const tankRelevant = el.classList.contains('tank-relevant');
const abilities = [];
const lis = el.querySelectorAll('.mob-abilities li');
for (const li of lis) {
abilities.push(li.innerHTML.trim());
}
return { name, nameHtml, tankRelevant, abilitiesHtml: abilities };
}
function parseBossSection(el) {
const headerEl = el.querySelector('.boss-header');
const img = headerEl?.querySelector('img');
const textEl = headerEl?.querySelector('.boss-header-text');
const bossNumber = textEl?.querySelector('.boss-number')?.textContent?.trim() || '';
const bossName = textEl?.querySelector('h2')?.textContent?.trim() || '';
const subtitle = textEl?.querySelector('.boss-subtitle')?.textContent?.trim() || '';
const image = img ? `/assets/${img.getAttribute('src').split('/').pop()}` : '';
const abilities = [];
const cards = el.querySelectorAll('.ability-card');
for (const card of cards) {
abilities.push(parseAbilityCard(card));
}
return { bossNumber, name: bossName, subtitle, image, abilities };
}
function parseAbilityCard(el) {
const nameEl = el.querySelector('.ability-name');
const link = nameEl?.querySelector('a');
const roleTag = nameEl?.querySelector('.role-tag');
const name = link?.textContent?.trim() || nameEl?.childNodes[0]?.textContent?.trim() || '';
const wowheadUrl = link?.getAttribute('href') || undefined;
const roleText = roleTag?.textContent?.trim()?.toLowerCase() || 'everyone';
const role = ['tank', 'healer', 'dps', 'everyone'].includes(roleText) ? roleText : 'everyone';
let importance = null;
if (el.classList.contains('tank-important')) importance = 'tank-important';
else if (el.classList.contains('healer-important')) importance = 'healer-important';
else if (el.classList.contains('everyone-important')) importance = 'everyone-important';
const descEl = el.querySelector('.ability-desc');
const descriptionHtml = descEl?.innerHTML?.trim() || '';
return { name, wowheadUrl, role, importance, descriptionHtml };
}
// ── Emit TypeScript files ──
mkdirSync('src/data', { recursive: true });
function escapeForTemplate(str) {
return str.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\$\{/g, '\\${');
}
function emitDungeonFile(dungeon) {
const slug = dungeon.id;
const varName = slug.replace(/-./g, m => m[1].toUpperCase());
let ts = `import type { DungeonData } from "@/types/dungeon";\n\n`;
ts += `export const ${varName}: DungeonData = {\n`;
ts += ` id: ${JSON.stringify(dungeon.id)},\n`;
ts += ` name: ${JSON.stringify(dungeon.name)},\n`;
ts += ` descriptionHtml: \`${escapeForTemplate(dungeon.descriptionHtml)}\`,\n`;
ts += ` headerImage: ${JSON.stringify(dungeon.headerImage)},\n`;
ts += ` icon: ${JSON.stringify(dungeon.icon)},\n`;
ts += ` sections: [\n`;
for (const section of dungeon.sections) {
if (section.type === 'divider') {
ts += ` { type: "divider" },\n`;
} else if (section.type === 'trash') {
ts += emitTrashSection(section.data);
} else if (section.type === 'boss') {
ts += emitBossSection(section.data);
}
}
ts += ` ],\n`;
ts += `};\n`;
writeFileSync(`src/data/${slug}.ts`, ts);
console.log(` Wrote src/data/${slug}.ts`);
return { slug, varName };
}
function emitTrashSection(data) {
let ts = ` {\n type: "trash",\n data: {\n`;
ts += ` header: ${JSON.stringify(data.header)},\n`;
ts += ` content: [\n`;
for (const item of data.content) {
if (item.type === 'quickRef') {
ts += ` { type: "quickRef", data: { html: \`${escapeForTemplate(item.data.html)}\` } },\n`;
} else if (item.type === 'mob') {
ts += emitMobCard(item.data);
}
}
ts += ` ],\n`;
ts += ` },\n },\n`;
return ts;
}
function emitMobCard(data) {
let ts = ` {\n type: "mob",\n data: {\n`;
ts += ` name: ${JSON.stringify(data.name)},\n`;
ts += ` nameHtml: \`${escapeForTemplate(data.nameHtml)}\`,\n`;
ts += ` tankRelevant: ${data.tankRelevant},\n`;
ts += ` abilitiesHtml: [\n`;
for (const ab of data.abilitiesHtml) {
ts += ` \`${escapeForTemplate(ab)}\`,\n`;
}
ts += ` ],\n`;
ts += ` },\n },\n`;
return ts;
}
function emitBossSection(data) {
let ts = ` {\n type: "boss",\n data: {\n`;
ts += ` bossNumber: ${JSON.stringify(data.bossNumber)},\n`;
ts += ` name: ${JSON.stringify(data.name)},\n`;
ts += ` subtitle: ${JSON.stringify(data.subtitle)},\n`;
ts += ` image: ${JSON.stringify(data.image)},\n`;
ts += ` abilities: [\n`;
for (const ab of data.abilities) {
ts += ` {\n`;
ts += ` name: ${JSON.stringify(ab.name)},\n`;
if (ab.wowheadUrl) {
ts += ` wowheadUrl: ${JSON.stringify(ab.wowheadUrl)},\n`;
}
ts += ` role: ${JSON.stringify(ab.role)},\n`;
ts += ` importance: ${JSON.stringify(ab.importance)},\n`;
ts += ` descriptionHtml: \`${escapeForTemplate(ab.descriptionHtml)}\`,\n`;
ts += ` },\n`;
}
ts += ` ],\n`;
ts += ` },\n },\n`;
return ts;
}
// Emit individual dungeon files
console.log('Extracting dungeon data...');
const exports = [];
for (const [id, dungeon] of Object.entries(dungeonDataMap)) {
exports.push(emitDungeonFile(dungeon));
}
// Emit index.ts
let indexTs = `import type { DungeonData, DungeonListItem } from "@/types/dungeon";\n`;
for (const { slug, varName } of exports) {
indexTs += `import { ${varName} } from "./${slug}";\n`;
}
indexTs += `\nexport const dungeonList: DungeonListItem[] = [\n`;
for (const item of dungeonList) {
indexTs += ` { id: ${JSON.stringify(item.id)}, name: ${JSON.stringify(item.name)}, icon: ${JSON.stringify(item.icon)}`;
if (item.disabled) indexTs += `, disabled: true`;
indexTs += ` },\n`;
}
indexTs += `];\n`;
indexTs += `\nexport const dungeonDataMap: Record<string, DungeonData> = {\n`;
for (const { slug, varName } of exports) {
indexTs += ` "${slug}": ${varName},\n`;
}
indexTs += `};\n`;
writeFileSync('src/data/index.ts', indexTs);
console.log(' Wrote src/data/index.ts');
console.log(`\nDone! Extracted ${exports.length} dungeons.`);
// Summary
for (const [id, dungeon] of Object.entries(dungeonDataMap)) {
const bosses = dungeon.sections.filter(s => s.type === 'boss').length;
const trash = dungeon.sections.filter(s => s.type === 'trash').length;
console.log(` ${dungeon.name}: ${bosses} bosses, ${trash} trash sections`);
}