Files
dwroller/scripts/normalize-and-merge-bestiary.js
Alex caf8a03553 feat: Add scripts for upserting and validating bestiary data
- 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.
2025-08-17 16:21:00 +02:00

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();