#!/usr/bin/env node /* * Import horde entries from the staged 40k RPG Tools index as compact * reviewable bestiary rows. These are derived entries, not PDF-parsed * statblocks, so they are flagged as hordes and kept intentionally small. */ const fs = require('fs'); const path = require('path'); const { bestiaryHelpers, initializationPromise } = require('../database/mariadb'); const ROOT = path.join(__dirname, '..'); const INDEX_PATH = path.join(ROOT, 'database', 'remote-extract', '40krpgtools-bestiary-index.json'); const PUBLIC_PATH = path.join(ROOT, 'public', 'deathwatch-bestiary-extracted.json'); const DB_PATH = path.join(ROOT, 'database', 'deathwatch-bestiary-extracted.json'); const HORDE_SPECS = { 'Mutant Horde': { magnitude: 20, profile: { ws: 30, bs: 20, s: 30, t: 30, ag: 30, int: 20, per: 30, wp: 30, fel: 20 }, damage: '1d10+3 I', pen: 0, armour: 3, tb: 3 }, 'PDF Guardsman Horde': { magnitude: 20, profile: { ws: 30, bs: 35, s: 30, t: 30, ag: 30, int: 30, per: 30, wp: 30, fel: 25 }, damage: '1d10+3 I', pen: 0, armour: 4, tb: 3 }, 'Rebel Horde': { magnitude: 20, profile: { ws: 30, bs: 30, s: 30, t: 30, ag: 30, int: 25, per: 30, wp: 30, fel: 25 }, damage: '1d10+3 I', pen: 0, armour: 4, tb: 3 }, 'Kroot Carnivore Horde': { magnitude: 15, profile: { ws: 40, bs: 20, s: 35, t: 30, ag: 40, int: 20, per: 35, wp: 30, fel: 10 }, damage: '1d10+4 R', pen: 0, armour: 3, tb: 3 }, 'Regressed Kroot Carnivore Horde': { magnitude: 15, profile: { ws: 40, bs: 20, s: 35, t: 30, ag: 40, int: 20, per: 35, wp: 30, fel: 10 }, damage: '1d10+4 R', pen: 0, armour: 3, tb: 3 }, 'Hromagaunt Horde': { magnitude: 20, profile: { ws: 45, bs: 20, s: 35, t: 30, ag: 55, int: 10, per: 40, wp: 30, fel: 5 }, damage: '1d10+5 R', pen: 0, armour: 3, tb: 3 }, 'Termagaunt Horde': { magnitude: 20, profile: { ws: 30, bs: 30, s: 30, t: 30, ag: 30, int: 10, per: 30, wp: 30, fel: 5 }, damage: '1d10+4 R', pen: 0, armour: 3, tb: 3 }, 'Gargoyle Horde': { magnitude: 20, profile: { ws: 30, bs: 33, s: 32, t: 30, ag: 40, int: 10, per: 40, wp: 30, fel: 0 }, damage: '1d10+5 R', pen: 3, armour: 3, tb: 3 }, }; function loadJson(file) { const raw = JSON.parse(fs.readFileSync(file, 'utf8')); const arr = Array.isArray(raw) ? raw : (raw.results || raw.entries || raw.items || []); if (!Array.isArray(arr)) throw new Error(`Unsupported bestiary JSON shape: ${file}`); return { raw, arr }; } function norm(value) { return String(value || '').toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim(); } function appendHordeEntries(arr, indexRows) { const existing = new Set(arr.map(entry => norm(entry.bestiaryName || entry.name)).filter(Boolean)); const added = []; for (const row of indexRows.filter(r => /horde/i.test(String(r.name || '')))) { const key = norm(row.name); if (existing.has(key)) continue; const spec = HORDE_SPECS[row.name] || { magnitude: 20, profile: { ws: 30, bs: 25, s: 30, t: 30, ag: 30, int: 20, per: 30, wp: 30, fel: 20 }, damage: '1d10+3 I', pen: 0, armour: 3, tb: 3, }; const stats = { horde: true, magnitude: spec.magnitude, profile: spec.profile, wounds: spec.magnitude, toughnessBonus: spec.tb, armour: spec.armour, statSource: 'index-horde', needsReview: true, sourceIndex: '40krpgtools-horde-index', threatTier: 'Horde', weapons: `Massed attacks (${spec.damage}; Pen ${spec.pen})`, }; arr.push({ bestiaryName: row.name, type: row.type, affiliation: row.affiliation, book: row.book, page: row.page, sourceUrl: row.sourceUrl, wounds: spec.magnitude, stats, horde: true, magnitude: spec.magnitude, }); existing.add(key); added.push(row.name); } return added; } async function main() { const index = JSON.parse(fs.readFileSync(INDEX_PATH, 'utf8')); const hordeRows = (index.entries || []).filter(row => /horde/i.test(String(row.name || ''))); const { raw, arr } = loadJson(PUBLIC_PATH); const added = appendHordeEntries(arr, hordeRows); let payload; if (Array.isArray(raw)) { payload = arr; } else { raw.results = arr; raw.count = arr.length; raw.generatedAt = new Date().toISOString(); payload = raw; } const serialized = JSON.stringify(payload, null, 2); for (const file of [PUBLIC_PATH, DB_PATH]) { if (fs.existsSync(file)) fs.writeFileSync(file + '.pre-horde.' + Date.now() + '.json', fs.readFileSync(file)); fs.writeFileSync(file, serialized); } await initializationPromise; await bestiaryHelpers.replaceAll(arr); console.log(`Imported ${added.length} horde entries into bestiary`); added.forEach(name => console.log(` + ${name}`)); } main() .then(() => process.exit(0)) .catch(error => { console.error(error); process.exit(1); });