- Implemented `upsert-allewis-from-5001.js` to fetch and update Allewis data from API. - Created `upsert-missing-movement-from-5001.js` to fill missing movement and wounds data from API. - Developed `upsert-missing-movement-from-pdfs.js` to extract movement and wounds from PDF text. - Added `upsert-missing-profiles-from-pdfs.js` to fill missing profiles from PDF data. - Introduced `upsert-missing-wounds-from-pdfs.js` to update wounds data from PDF sources. - Created `validate-bestiary.js` to validate the structure of the bestiary JSON. - Updated `BestiaryTab.jsx` component to display bestiary data with improved structure and search functionality. - Added tests for `BestiaryTab` to ensure proper rendering and functionality.
137 lines
4.4 KiB
JavaScript
137 lines
4.4 KiB
JavaScript
#!/usr/bin/env node
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
const DB_PATH = path.resolve(__dirname, '../database/deathwatch-bestiary-extracted.json');
|
|
const BACKUPS_DIR = path.resolve(__dirname, '../database/backups');
|
|
const PDFJS_PATH = path.resolve(__dirname, '../database/alexei-pdfjs.json');
|
|
|
|
function readJson(p) {
|
|
return JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
}
|
|
|
|
function writeJson(p, obj) {
|
|
fs.writeFileSync(p, JSON.stringify(obj, null, 2), 'utf8');
|
|
}
|
|
|
|
function ensureDir(p) {
|
|
if (!fs.existsSync(p)) fs.mkdirSync(p, { recursive: true });
|
|
}
|
|
|
|
function extractMovement(m) {
|
|
if (!m || typeof m !== 'string') return null;
|
|
const movementMatch = m.match(/(\d+\/\d+\/\d+\/\d+)/);
|
|
if (movementMatch) return movementMatch[1];
|
|
// fallback: look for patterns like 3/6/9/18 without capture groups
|
|
const alt = m.match(/(\d+\s*\/\s*\d+\s*\/\s*\d+\s*\/\s*\d+)/);
|
|
if (alt) return alt[1].replace(/\s+/g, '');
|
|
return null;
|
|
}
|
|
|
|
function extractWounds(s) {
|
|
if (s == null) return null;
|
|
if (typeof s === 'number') return s;
|
|
const str = String(s);
|
|
const m = str.match(/Wounds:\s*(\d+)/i) || str.match(/(\d{1,3})\s*$/);
|
|
if (m) return parseInt(m[1], 10);
|
|
return null;
|
|
}
|
|
|
|
function normalizeEntry(entry, pdfjsResults) {
|
|
let changed = false;
|
|
if (!entry.stats) entry.stats = {};
|
|
|
|
// Normalize movement
|
|
const movementRaw = entry.stats.movement;
|
|
const mov = extractMovement(movementRaw);
|
|
if (mov && mov !== movementRaw) {
|
|
entry.stats.movement = mov;
|
|
changed = true;
|
|
}
|
|
|
|
// Normalize wounds
|
|
const woundsRaw = entry.stats.wounds;
|
|
const wounds = extractWounds(woundsRaw || movementRaw);
|
|
if (wounds != null && entry.stats.wounds !== wounds) {
|
|
entry.stats.wounds = wounds;
|
|
changed = true;
|
|
}
|
|
|
|
// If profile missing or incomplete, try to fill from pdfjs artifacts
|
|
const needProfile = !entry.stats.profile || Object.keys(entry.stats.profile).length < 5;
|
|
const needWeapons = !entry.stats.weapons && !entry.stats.gear;
|
|
|
|
if ((needProfile || needWeapons) && pdfjsResults && entry.pdf) {
|
|
const pageNum = parseInt(entry.page, 10);
|
|
const match = pdfjsResults.find(r => {
|
|
if (!r || !r.pdf) return false;
|
|
if (String(r.pdf).trim() !== String(entry.pdf).trim()) return false;
|
|
if (r.foundPage && pageNum && r.foundPage === pageNum) return true;
|
|
if (Array.isArray(r.range) && pageNum && pageNum >= r.range[0] && pageNum <= r.range[1]) return true;
|
|
if (r.chosenPage && pageNum && r.chosenPage === pageNum) return true;
|
|
return false;
|
|
});
|
|
|
|
if (match && match.stats) {
|
|
// copy profile if missing
|
|
if (needProfile && match.stats.profile) {
|
|
entry.stats.profile = entry.stats.profile || {};
|
|
Object.assign(entry.stats.profile, match.stats.profile);
|
|
changed = true;
|
|
}
|
|
// copy movement/wounds/weapons/gear if missing
|
|
if (!entry.stats.movement && match.stats.movement) {
|
|
entry.stats.movement = match.stats.movement;
|
|
changed = true;
|
|
}
|
|
if ((!entry.stats.wounds || entry.stats.wounds === null) && match.stats.wounds) {
|
|
entry.stats.wounds = match.stats.wounds;
|
|
changed = true;
|
|
}
|
|
if (needWeapons && (match.stats.weapons || match.stats.gear)) {
|
|
entry.stats.weapons = entry.stats.weapons || match.stats.weapons || null;
|
|
entry.stats.gear = entry.stats.gear || match.stats.gear || null;
|
|
changed = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return changed;
|
|
}
|
|
|
|
function main() {
|
|
console.log('Loading bestiary:', DB_PATH);
|
|
const db = readJson(DB_PATH);
|
|
let pdfjs = null;
|
|
try {
|
|
pdfjs = readJson(PDFJS_PATH);
|
|
} catch (e) {
|
|
console.warn('Could not read pdfjs artifacts:', PDFJS_PATH);
|
|
}
|
|
|
|
ensureDir(BACKUPS_DIR);
|
|
const backupPath = path.join(BACKUPS_DIR, `deathwatch-bestiary-extracted.json.backup.${Date.now()}.json`);
|
|
fs.copyFileSync(DB_PATH, backupPath);
|
|
console.log('Backed up DB to', backupPath);
|
|
|
|
const results = db.results || [];
|
|
let modified = 0;
|
|
|
|
for (let i = 0; i < results.length; i++) {
|
|
const entry = results[i];
|
|
const changed = normalizeEntry(entry, pdfjs && pdfjs.results ? pdfjs.results : (pdfjs && pdfjs.matches ? pdfjs.matches : null));
|
|
if (changed) modified++;
|
|
}
|
|
|
|
if (modified > 0) {
|
|
db.generatedAt = new Date().toISOString();
|
|
db.count = results.length;
|
|
writeJson(DB_PATH, db);
|
|
console.log(`Updated ${modified} entries and wrote DB (count=${db.count})`);
|
|
} else {
|
|
console.log('No changes necessary');
|
|
}
|
|
}
|
|
|
|
main();
|