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

132 lines
5.1 KiB
JavaScript

#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
const util = require('util');
async function fetchJson(url){
try{
const res = await fetch(url);
if(!res.ok) throw new Error(`${res.status} ${res.statusText}`);
return await res.json();
}catch(e){
console.error('Fetch error for', url, e.message);
return null;
}
}
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.' + Date.now() + '.json');
fs.writeFileSync(backupPath, raw, 'utf8');
return backupPath;
}
function normalizeName(s){
if(!s) return '';
return String(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 at', bestiaryPath); process.exit(1); }
let obj;
try{ obj = JSON.parse(fs.readFileSync(bestiaryPath, 'utf8')); }catch(e){ console.error('Failed to parse bestiary:', e.message); process.exit(2); }
const entries = obj.results || [];
const need = [];
for(const e of entries){
if(!e || !e.stats) continue;
const mv = e.stats.movement;
const wd = e.stats.wounds;
if(!mv || mv === null || mv === '\\n' || String(mv).trim() === '') need.push(e);
// also consider if wounds missing
else if(wd === null || wd === undefined) need.push(e);
}
console.log('Total entries:', entries.length, 'Missing movement/wounds count:', need.length);
if(need.length === 0){ console.log('Nothing to do.'); return; }
const backupPath = makeBackup(bestiaryPath);
console.log('Backup created:', backupPath);
const updated = [];
const appended = [];
for(const e of need){
const name = e.bestiaryName || (e.stats && e.stats.snippet && e.stats.snippet.split('\n')[0]) || e.apiName || '';
const q = encodeURIComponent((name || '').trim() || '');
if(!q){ console.log('Skipping entry with no name'); continue; }
const url = `http://192.168.1.144:5001/api/search?q=${q}`;
console.log('\nQuerying API for', name, '->', url);
const body = await fetchJson(url);
if(!body || !body.statblocks || body.statblocks.length === 0){
console.log('No statblocks returned for', name);
continue;
}
// try to pick best statblock: prefer matching page or exact name
const candidates = body.statblocks;
let pick = candidates[0];
// attempt better match
for(const s of candidates){
if(e.page && s.page && String(e.page) === String(s.page)){ pick = s; break; }
const bn = normalizeName(e.bestiaryName || '');
const sn = normalizeName(s.name || '');
if(bn && sn && sn.includes(bn)) { pick = s; break; }
}
const movement = pick.secondary_stats && pick.secondary_stats.movement;
const wounds = pick.secondary_stats && pick.secondary_stats.wounds;
if(!movement && (wounds === null || wounds === undefined)){
console.log('API candidate has no movement/wounds for', name);
continue;
}
// find index in entries again (safe)
const idx = entries.findIndex(r => r === e);
if(idx === -1){
// append
const newEntry = {
bestiaryName: pick.name || name,
book: pick.source || 'Unknown',
page: String(pick.page || ''),
pdf: '',
pageText: '',
stats: {
profile: null,
movement: movement || null,
wounds: (typeof wounds === 'number') ? wounds : (wounds ? Number(wounds) : null),
toughness: null,
skills: null,
talents: null,
traits: null,
armour: null,
weapons: null,
gear: null,
snippet: ''
}
};
entries.push(newEntry);
appended.push(newEntry.bestiaryName);
console.log('Appended new entry for', newEntry.bestiaryName);
}else{
const target = entries[idx];
const before = { movement: target.stats.movement, wounds: target.stats.wounds };
if(movement) target.stats.movement = movement;
if(wounds !== null && wounds !== undefined) target.stats.wounds = (typeof wounds === 'number') ? wounds : (wounds ? Number(wounds) : target.stats.wounds);
updated.push({ name: target.bestiaryName || name, before, after: { movement: target.stats.movement, wounds: target.stats.wounds } });
console.log('Updated:', target.bestiaryName, '-> movement:', target.stats.movement, 'wounds:', target.stats.wounds);
}
}
// write back
obj.results = entries;
fs.writeFileSync(bestiaryPath, JSON.stringify(obj, null, 2), 'utf8');
console.log('\nFinished. Updated:', updated.length, 'Appended:', appended.length);
if(updated.length) console.log('Updated details:', util.inspect(updated, { depth: 3 }));
if(appended.length) console.log('Appended names:', appended.join(', '));
}
main().catch(e => { console.error('Unhandled', e); process.exit(99); });