const fs = require('fs'); const path = require('path'); const INPUT_HTML = path.resolve('/home/chaiman/dev/ai/stella deus/Stella Deus_ The Gate of Eternity - Fusion FAQ - PlayStation 2 - By Gundam4fun - GameFAQs.html'); const OUTPUT_JSON = path.resolve('/home/chaiman/dev/ai/stella deus/sdfh_item_data.json'); function readHtml(file) { return fs.readFileSync(file, 'utf8'); } function extractPre(html) { const preRegex = /
]*>([\s\S]*?)<\/pre>/gi;
const pieces = [];
let m;
while ((m = preRegex.exec(html)) !== null) pieces.push(m[1]);
if (pieces.length === 0) return html.replace(/<[^>]*>/g, '');
return pieces.map((s) => s.replace(/<[^>]*>/g, '')).join('\n');
}
function buildGroupTypeMap(text) {
const lines = text.split('\n');
const groupTitleRe = /^\s*(\d+\.\d+)\s*-\s*(.+?)\s*$/;
const mapTitleToType = (title) => {
const t = String(title || '').toLowerCase();
const words = t.split(/[^a-z0-9]+/).filter(Boolean);
const hasWord = (w) => words.includes(w);
const anyWord = (...ws) => ws.some((w) => hasWord(w));
if (anyWord('katana','katanas')) return 'katana';
if (anyWord('sword','swords')) return 'sword';
if (anyWord('bow','bows')) return 'bow';
if (anyWord('axe','axes')) return 'axe';
if (anyWord('spear','spears')) return 'spear';
if (anyWord('knife','knives')) return 'knife';
if (anyWord('gauntlet','gauntlets','glove','gloves','mittens')) return 'glove';
if (anyWord('shoe','shoes','boot','boots')) return 'shoe';
if (anyWord('staff','staves','rod','rods','wand','wands')) return 'staff';
if (anyWord('agryrion')) return 'agryrion';
if (anyWord('helmet','helmets')) return 'helmet';
if (anyWord('hat','hats')) return 'hat';
if (anyWord('robe','robes')) return 'robe';
if (anyWord('armor','armors','armour','mail','mails')) return 'armor';
if (anyWord('shield','shields')) return 'shield';
if (anyWord('ring','rings')) return 'ring';
if (anyWord('amulet','amulets')) return 'amulet';
if (anyWord('accessory','accessories')) return 'accessory';
if (anyWord('scroll','scrolls')) return 'scroll';
if (anyWord('class','change') || t.includes('rank up')) return 'rankup';
if (anyWord('recovery')) return 'recovery';
return undefined;
};
const groupToType = new Map();
for (const raw of lines) {
const line = raw.trimEnd();
if (/\(\s*Rank\s*\d+\s*\)/i.test(line)) continue; // skip item headers
const m = line.match(groupTitleRe);
if (!m) continue;
const group = m[1];
const title = m[2];
const type = mapTitleToType(title);
if (type && !groupToType.has(group)) groupToType.set(group, type);
}
return groupToType;
}
function computeWeaponType(section) {
// section: e.g., 1.6.8 → group 1.6
const parts = section.split('.').map((x) => parseInt(x, 10));
if (parts[0] !== 1) return undefined;
const minor = parts[1];
switch (minor) {
case 1: return 'katana';
case 2: return 'sword';
case 3: return 'bow';
case 4: return 'axe';
case 5: return 'spear';
case 6: return 'knife';
case 7: return 'glove';
case 8: return 'staff';
case 9: return 'agryrion';
default: return undefined;
}
}
function computeNonWeaponType(section) {
// Map other groups where the TOC defines a clear category
// Example given: 5.2 - Scroll → all 5.2.* are scrolls
const m = section.match(/^(\d+\.\d+)\./);
if (!m) return undefined;
const group = m[1];
if (group === '5.2') return 'scroll';
if (group === '4.2') return 'glove'; // Gauntlets
if (group === '4.3') return 'shoe'; // Shoes
if (group === '5.3') return 'rankup'; // Class change items
return undefined;
}
function parseStatsLine(line) {
const stats = {};
const inner = line.replace(/^\s*\|\s*/, '').replace(/\s*\|\s*$/, '').trim();
const parts = inner.split('/');
for (const p of parts) {
const m = p.trim().match(/([A-Z][A-Z0-9 ]+)\s+(-?\d+)/i);
if (m) stats[m[1].trim()] = parseInt(m[2], 10);
}
return Object.keys(stats).length ? stats : null;
}
function parseGuide(text) {
const lines = text.split('\n');
const items = [];
let current = null;
let inStats = false;
const headerRe = /^\s*(\d+\.\d+\.\d+)\s*-\s*(.+?)\s*\(\s*Rank\s*(\d+)\s*\)\s*(?:-\s*(.*))?$/i;
const groupTypeMap = buildGroupTypeMap(text);
// Manual additions for known groups if not present in titles
if (!groupTypeMap.has('4.1')) groupTypeMap.set('4.1', 'shield'); // 4.1 - Shields (example)
function push() {
if (!current) return;
if (!current.recipes) current.recipes = [];
if (current.description && !current.description.trim()) delete current.description;
if (current.stats && !Object.keys(current.stats).length) delete current.stats;
items.push(current);
current = null;
inStats = false;
}
for (let i = 0; i < lines.length; i += 1) {
const raw = lines[i];
const line = raw.trimEnd();
const h = line.match(headerRe);
if (h) {
push();
const section = h[1];
const name = h[2].trim();
const rank = parseInt(h[3], 10);
const desc = (h[4] || '').trim();
const groupKey = (section.match(/^(\d+\.\d+)\./) || [])[1];
// Prefer hard-coded weapon mapping for 1.X, because TOC labels are inconsistent
const isWeapon = /^1\./.test(section);
const type = (isWeapon ? computeWeaponType(section) : (computeNonWeaponType(section) || groupTypeMap.get(groupKey))) || undefined;
// Mark rank-up by description or by being in 5.3 group
const rankUp = /rank\s*up\s*item/i.test(desc) || groupKey === '5.3' || type === 'rankup';
const rankUpFor = [];
const forMatch = desc.match(/for\s*:\s*([^]+)$/i);
if (forMatch) {
for (const part of forMatch[1].split(/[\/,]/)) {
const t = part.trim();
if (t) rankUpFor.push(t);
}
}
current = { name, rank, description: desc || undefined, stats: undefined, recipes: [], type, rankUp: rankUp || undefined, rankUpFor: rankUpFor.length ? rankUpFor : undefined, section };
continue;
}
if (!current) continue;
if (/^[oO0\-=_]{5,}$/.test(line.replace(/\s+/g, ''))) { inStats = true; continue; }
if (inStats && line.includes('|')) {
const st = parseStatsLine(line);
if (st) current.stats = { ...(current.stats || {}), ...st };
continue;
}
if (inStats && line.trim() === '') { inStats = false; continue; }
if (line.includes('+') && !line.includes('=')) {
const parts = line.split('+');
if (parts.length === 2) {
const left = parts[0].trim();
const right = parts[1].trim();
if (/(\d+\.\d+\.\d+\s*-)/.test(left) || /(\d+\.\d+\.\d+\s*-)/.test(right)) continue;
const rankM1 = left.match(/\(\s*R(?:ank)?\s*(\d+)\s*\)/i);
const rankM2 = right.match(/\(\s*R(?:ank)?\s*(\d+)\s*\)/i);
if (!rankM1 || !rankM2) continue;
const name1 = left.replace(/\(\s*R(?:ank)?\s*\d+\s*\)/ig, '').trim();
const name2 = right.replace(/\(\s*R(?:ank)?\s*\d+\s*\)/ig, '').trim();
current.recipes.push({ ingredients: [ { name: name1, rank: parseInt(rankM1[1],10) }, { name: name2, rank: parseInt(rankM2[1],10) } ] });
}
}
}
push();
// Post-fix: derive types for non-weapon items with heuristics only if missing
const inferOther = (name) => {
const n = (name || '').toLowerCase();
const hasAny = (arr) => arr.some((w) => n.includes(w));
if (hasAny(['helmet','hat','turban','mask','hachimaki','hachigane'])) return 'helmet';
if (hasAny(['robe','garb','shawl'])) return 'robe';
if (hasAny(['mail','armor','armour','plate'])) return 'armor';
if (hasAny(['shield'])) return 'shield';
if (hasAny(['ring','charm','amulet','talisman','anklet','earrings'])) return 'accessory';
if (hasAny(['boots','shoes'])) return 'shoe';
if (hasAny(['potion','elixir','antidote','holy water','whistle'])) return 'recovery';
return undefined;
};
for (const it of items) {
if (!it.type) {
const t = inferOther(it.name);
if (t) it.type = t;
}
}
return items;
}
function main() {
const html = readHtml(INPUT_HTML);
const text = extractPre(html).replace(/\r\n/g, '\n');
const items = parseGuide(text);
const out = { items };
fs.writeFileSync(OUTPUT_JSON, JSON.stringify(out, null, 2), 'utf8');
console.log(`Rebuilt ${items.length} items → ${OUTPUT_JSON}`);
}
if (require.main === module) main();