Files
dwroller/scripts/upsert-missing-movement-from-pdfs.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

94 lines
4.0 KiB
JavaScript

#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
function makeBackup(bestiaryPath){
const raw = fs.readFileSync(bestiaryPath, 'utf8');
const backupsDir = path.resolve(__dirname, '..', 'database', 'backups');
if(!fs.existsSync(backupsDir)) fs.mkdirSync(backupsDir, { recursive: true });
const backupPath = path.join(backupsDir, 'deathwatch-bestiary-extracted.json.backup.pdffallback.' + Date.now() + '.json');
fs.writeFileSync(backupPath, raw, 'utf8');
return backupPath;
}
function extractFromText(text){
if(!text) return {};
const mvMatch = text.match(/Movement:\s*([0-9\s\/\-]+)\b/i);
const wdMatch = text.match(/Wounds:\s*([0-9]+)/i);
return {
movement: mvMatch ? mvMatch[1].trim().replace(/\s+/g,' ') : null,
wounds: wdMatch ? Number(wdMatch[1]) : null
};
}
function scanDatabaseFilesForName(name){
const dbDir = path.resolve(__dirname, '..', 'database');
const files = fs.readdirSync(dbDir).filter(f => f.endsWith('.json'));
const results = [];
const lower = (name || '').toLowerCase();
for(const f of files){
const p = path.join(dbDir, f);
let raw;
try{ raw = fs.readFileSync(p, 'utf8'); }catch(e){ continue; }
if(raw.toLowerCase().includes(lower)){
// try to extract
const ex = extractFromText(raw);
if(ex.movement || ex.wounds) results.push({ file: f, movement: ex.movement, wounds: ex.wounds, snippet: raw.substr(Math.max(0, raw.toLowerCase().indexOf(lower)-200), 600) });
}
}
return results;
}
function normalizeName(s){ return (s||'').replace(/[^a-z0-9\s]/gi,' ').replace(/\s+/g,' ').trim(); }
async function main(){
const bestiaryPath = path.resolve(__dirname, '..', 'database', 'deathwatch-bestiary-extracted.json');
if(!fs.existsSync(bestiaryPath)){ console.error('Bestiary not found:', bestiaryPath); process.exit(1); }
const obj = JSON.parse(fs.readFileSync(bestiaryPath, 'utf8'));
const entries = obj.results || [];
const need = entries.filter(e => !(e && e.stats && e.stats.movement && String(e.stats.movement).trim() !== ''));
console.log('Entries needing movement/wounds:', need.length);
if(need.length === 0){ console.log('Nothing to do'); return; }
const backup = makeBackup(bestiaryPath);
console.log('Backup created:', backup);
const updated = [];
for(const e of need){
const name = e.bestiaryName || (e.stats && e.stats.snippet) || '';
if(!name){ console.log('Skipping nameless entry'); continue; }
console.log('\nScanning database JSONs for', name);
// try relaxed searches: exact name, normalized name token, first two words
const candidates = [];
const direct = scanDatabaseFilesForName(name);
candidates.push(...direct);
if(candidates.length === 0){
const nrm = normalizeName(name);
if(nrm){
const tokens = nrm.split(' ').slice(0,3).join(' ');
const alt = scanDatabaseFilesForName(tokens);
candidates.push(...alt);
}
}
if(candidates.length === 0){ console.log('No PDF-derived candidates for', name); continue; }
console.log('Found candidates from files:', candidates.map(c=>c.file).join(', '));
// pick first with movement or wounds
let picked = null;
for(const c of candidates){ if(c.movement || c.wounds){ picked = c; break; } }
if(!picked) picked = candidates[0];
const before = { movement: e.stats && e.stats.movement, wounds: e.stats && e.stats.wounds };
if(!e.stats) e.stats = {};
if(picked.movement) e.stats.movement = picked.movement;
if(picked.wounds !== null && picked.wounds !== undefined) e.stats.wounds = picked.wounds;
updated.push({ name: e.bestiaryName, file: picked.file, before, after: { movement: e.stats.movement, wounds: e.stats.wounds } });
console.log('Updated', e.bestiaryName, '->', e.stats.movement, e.stats.wounds);
}
fs.writeFileSync(bestiaryPath, JSON.stringify(obj, null, 2), 'utf8');
console.log('\nDone. Updated', updated.length, 'entries.');
if(updated.length) console.log('Details:', updated);
}
main().catch(e => { console.error('Fatal', e); process.exit(99); });