217 lines
8.3 KiB
JavaScript
217 lines
8.3 KiB
JavaScript
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 = /<pre[^>]*>([\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();
|
|
|
|
|