Files
dwroller/scripts/structure-enemies.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

117 lines
4.3 KiB
JavaScript

#!/usr/bin/env node
// Convert enemies-clean.json into structured enemy records with best-effort parsing
const fs = require('fs');
const path = require('path');
const IN_FILE = path.join(__dirname, '..', 'database', 'enemies-clean.json');
const OUT_FILE = path.join(__dirname, '..', 'database', 'deathwatch-enemies.json');
const OUT_SAMPLE = path.join(__dirname, '..', 'database', 'deathwatch-enemies-sample.json');
function parseArmourByLoc(snippet) {
const lower = snippet.toLowerCase();
const armour = {};
// Try common labels
const patterns = {
head: /head(?:[:\s]+)([0-9]{1,3})/i,
body: /body(?:[:\s]+)([0-9]{1,3})/i,
ra: /r(?:ight)?\s*arm(?:[:\s]+)([0-9]{1,3})/i,
la: /l(?:eft)?\s*arm(?:[:\s]+)([0-9]{1,3})/i,
rl: /r(?:ight)?\s*leg(?:[:\s]+)([0-9]{1,3})/i,
ll: /l(?:eft)?\s*leg(?:[:\s]+)([0-9]{1,3})/i,
ra2: /ra[:\s]*([0-9]{1,3})/i,
la2: /la[:\s]*([0-9]{1,3})/i,
rl2: /rl[:\s]*([0-9]{1,3})/i,
ll2: /ll[:\s]*([0-9]{1,3})/i,
ap: /ap[:\s]*([0-9]{1,3})/i
};
for (const k of ['head','body','ra','la','rl','ll']) {
const p = patterns[k] || patterns[k+'2'];
if (p) {
const m = snippet.match(p);
if (m) armour[k] = Number(m[1]);
}
}
// legacy format like "armour: body 10; head 0; ra 8; la 8; rl 8; ll 8"
const armorLine = snippet.match(/armou?r[^\n]{0,120}/i);
if (armorLine) {
const line = armorLine[0];
// find numbers after known keys
const kvs = line.match(/(body|head|ra|la|rl|ll)[:\s]*([0-9]{1,3})/gi);
if (kvs) {
for (const kv of kvs) {
const m = kv.match(/(body|head|ra|la|rl|ll)[:\s]*([0-9]{1,3})/i);
if (m) armour[m[1].toLowerCase()] = Number(m[2]);
}
}
// fallback: single number in armour line may be body AP
const single = line.match(/(\d{1,3})/);
if (single && !armour.body) armour.body = Number(single[1]);
}
// If we found any keys, return them, otherwise null
return Object.keys(armour).length ? armour : null;
}
function parsePage(snippets, stats) {
if (stats && stats.page) return stats.page;
for (const s of snippets) {
const m = s.match(/page\s*no\.\s*([0-9]{1,4})/i) || s.match(/p(?:age)?\.?\s*([0-9]{1,4})/i);
if (m) return Number(m[1]);
}
return null;
}
function isPromising(entry) {
const s = entry.stats || {};
if (s.wounds || s.toughness || s.ap || s.page) return true;
const snippetText = (entry.snippets||[]).join('\n');
if (/\barmou?r\b|\bwounds?\b|\bTB\b|\btoughness\b|\bAP\b/i.test(snippetText)) return true;
// frequency-based: if seen many times
if (entry.count && entry.count >= 2) return true;
return false;
}
function chooseSource(sources) {
if (!sources || sources.length === 0) return null;
// Prefer Deathwatch core and supplements in order
const preferred = ['Deathwatch - Core Rulebook','Deathwatch - Core Rulebook.pdf','Deathwatch - Rising Tempest','Deathwatch - Rites of Battle','Deathwatch - The Emperor Protects','Deathwatch - The Emperors Chosen'];
for (const p of preferred) {
for (const s of sources) if (s.toLowerCase().includes(p.toLowerCase())) return s;
}
return sources[0];
}
function main() {
if (!fs.existsSync(IN_FILE)) { console.error('Missing input', IN_FILE); process.exit(1); }
const raw = JSON.parse(fs.readFileSync(IN_FILE, 'utf8'));
const structured = [];
for (const e of raw) {
if (!isPromising(e)) continue;
const name = e.name || '';
const stats = e.stats || {};
const snippets = e.snippets || [];
const armourByLoc = parseArmourByLoc(snippets.join('\n')) || null;
const page = parsePage(snippets, stats);
const rec = {
name: name,
wounds: stats.wounds || null,
toughness: stats.toughness || null,
armour: stats.ap || (armourByLoc && armourByLoc.body) || null,
armourByLoc: armourByLoc,
source: chooseSource(e.sources || []),
page: page,
snippets: snippets.slice(0,3),
count: e.count || 0
};
structured.push(rec);
}
// sort by count desc
structured.sort((a,b) => (b.count||0) - (a.count||0));
fs.writeFileSync(OUT_FILE, JSON.stringify(structured, null, 2));
fs.writeFileSync(OUT_SAMPLE, JSON.stringify(structured.slice(0,200), null, 2));
console.log('Wrote', OUT_FILE, 'with', structured.length, 'structured promising entries');
}
if (require.main === module) main();