This commit is contained in:
2025-08-15 16:23:34 +02:00
parent 4c48f04c4e
commit cd10ab0718
23 changed files with 6434 additions and 992 deletions

48
scripts/build-shop-db.js Normal file
View File

@@ -0,0 +1,48 @@
const { categorizedWeapons } = require('./parse-weapons');
const { categorizedArmor } = require('./parse-armor');
// Combine all shop items into a single database
const buildShopDatabase = () => {
const shopItems = {
// Weapons
rangedWeapons: categorizedWeapons.rangedWeapons,
meleeWeapons: categorizedWeapons.meleeWeapons,
grenades: categorizedWeapons.grenades,
otherWeapons: categorizedWeapons.other,
// Armor
powerArmor: categorizedArmor.powerArmor,
carapaceArmor: categorizedArmor.carapaceArmor,
primitiveArmor: categorizedArmor.primitiveArmor,
naturalArmor: categorizedArmor.naturalArmor,
xenosArmor: categorizedArmor.xenosArmor,
shields: categorizedArmor.shields,
otherArmor: categorizedArmor.otherArmor
};
// Add metadata
const shopDatabase = {
version: "2.0.0",
lastUpdated: new Date().toISOString(),
categories: Object.keys(shopItems),
items: shopItems
};
return shopDatabase;
};
// Write the database
if (require.main === module) {
const fs = require('fs');
const path = require('path');
// First ensure our source data is up to date
require('./parse-weapons');
require('./parse-armor');
// Then build and write the combined database
const output = path.resolve('public/deathwatch-armoury.json');
const database = buildShopDatabase();
fs.writeFileSync(output, JSON.stringify(database, null, 2), 'utf8');
console.log('Wrote combined shop database to:', output);
}

331
scripts/clean-armoury.js Executable file → Normal file
View File

@@ -1,96 +1,243 @@
/*
Clean and normalize armoury JSON produced from PDF extraction.
Usage:
node scripts/clean-armoury.js public/deathwatch-armoury.json [output.json]
*/
const rawWeaponData = `
Angelus Bolter Basic - Bolt Range 50m; S/3/-; 1d10+9 X; Pen 6; Clip 36; Reload 2 Full; Tearing 25 Respected Deathwatch First Founding 97
Argrax Clawed Hooves Natural - Primitive 1d10 I; Pen 0; 0 Any Deathwatch The Emperor Protects 46
Armour of the Remorseless Crusader (Forearm Sword) One-Handed Melee - Primitive 1d10+8 R; Pen 6; +10 to hit; Sanctified 70 Hero Deathwatch Deathwatch Core Rulebook 167
Artificer Omnissian Axe One-Handed Melee - Power 1d10+9 E; Pen 7; Power Field, Unbalanced 25 Distinguished Deathwatch First Founding 99
Astartes Anti-Plant Grenade Thrown - Grenade 3d10 E; Pen 0; Blast (3) 18 Any Deathwatch Living Errata v1.1 136
Astartes Armoursbane Missile Launcher Heavy - Launch Range 200m; S/-/-; 0d0 S; Pen 0; Clip 1; Reload 2 Full; Two handed; 15 Any Deathwatch Living Errata v1.1 137
Astartes Assault Cannon Mounted - Solid Projectile Range 150m; -/-/10; 2d10+12 I; Pen 6; Clip 200; Reload 3 Full; Tearing, Devastating (1) 30 Famed Deathwatch Living Errata v1.1 149
Astartes Assault Shotgun Basic - Solid Projectile Range 30m; S/3/5; 1d10+10 I; Pen 4; Clip 20; Reload 1 Full; Two handed; Reliable, Scatter 7 Any Deathwatch Living Errata v1.1 136
Astartes Blind Grenade Thrown - Grenade 0d0 S; Pen 0; Smoke (10) 10 Any Deathwatch Living Errata v1.1 150
Astartes Bolt Pistol Pistol - Bolt Range 30m; S/2/-; 1d10+9 X; Pen 4; Clip 14; Reload 1 Full; Tearing 5 Any Deathwatch Living Errata v1.1 146
Astartes Bolter-Godwyn Basic - Bolt Range 100m; S/3/-; 1d10+9 X; Pen 4; Clip 28; Reload 1 Full; Two handed; Tearing 5 Any Deathwatch Living Errata v1.1 146
Astartes Boltgun-Stalker Basic - Bolt Range 200m; S/-/-; 1d10+9 X; Pen 4; Clip 24; Reload 1 Full; Two handed; Accurate, Tearing 15 Respected Deathwatch Living Errata v1.1 146
Astartes Chainfist One-Handed Melee - Power 2d10+"2*SB" E; Pen 10; Power Field, Tearing, Unwieldy 40 Famed Deathwatch Living Errata v1.1 154
Astartes Chainsword One-Handed Melee - Chain 1d10+3 R; Pen 3; Balanced, Tearing 5 Any Deathwatch Living Errata v1.1 153
Astartes Combat Knife One-Handed Melee - Primitive 1d10 R; Pen 2; 3 Any Deathwatch Living Errata v1.1 155
Astartes Combat Shield One-Handed Melee - Primitive 1d5+1 I; Pen 0; Balanced 20 Distinguished Deathwatch Living Errata v1.1 166
Astartes Combi-Weapon Basic - Bolt Range 100m; S/4/-; 1d10+9 X; Pen 4; Clip 28; Reload 1 Full; Two handed; Tearing 15 Respected Deathwatch Living Errata v1.1 146
Astartes Conversion Beamer Heavy - Exotic Range 100m; S/-/-; 0d0 E; Pen 0; Clip 4; Reload 2 Full; Two handed; 40 Hero Deathwatch Living Errata v1.1 152
Astartes Cyclone Missile Launcher Mounted - Launch Range 300m; S/2/-; 0d0 S; Pen 0; Clip 12; Reload 3 Full; 25 Famed Deathwatch Living Errata v1.1 152
Astartes Digital Flamer Pistol - Exotic Range 4m; S/-/-; 1d10+9 E; Pen 4; Clip 1; Reload 1d5 Hours; Flame, Spray 35 Famed Deathwatch Living Errata v1.1 153
Astartes Digital Laser Pistol - Exotic Range 4m; S/-/-; 1d10+4 E; Pen 7; Clip 1; Reload 1d5 Hours; Reliable 35 Famed Deathwatch Living Errata v1.1 153
Astartes Digital Melta Pistol - Exotic Range 4m; S/-/-; 2d10+12 E; Pen 10; Clip 1; Reload 1d5 Hours; 35 Famed Deathwatch Living Errata v1.1 153
Astartes Executioner Axe Two-Handed Melee - Power 1d10+13 E; Pen 8; Two handed; Power Field, Unwieldy, Felling (x1) 25 Famed Deathwatch Living Errata v1.1 139
Astartes Flamer Basic - Flame Range 20m; S/-/-; 1d10+9 E; Pen 4; Clip 6; Reload 2 Full; Two handed; Flame, Spray 10 Any Deathwatch Living Errata v1.1 148
Astartes Force Staff One-Handed Melee - Primitive 1d10+"1+PR+SB" S; Pen "PR"; Balanced 25 Respected Deathwatch Living Errata v1.1 155
Astartes Force Sword One-Handed Melee - Primitive 1d10+"2+PR+SB" S; Pen "2+PR"; Balanced 25 Respected Deathwatch Living Errata v1.1 156
Astartes Frag Grenade Thrown - Grenade 2d10+2 X; Pen 0; Blast (4) 1 Any Deathwatch Living Errata v1.1 150
Astartes Hand Flamer Pistol - Flame Range 10m; S/-/-; 1d10+7 E; Pen 4; Clip 4; Reload 2 Full; Flame, Spray 10 Any Deathwatch Living Errata v1.1 149
Astartes Heavy Flamer Heavy - Flame Range 30m; S/-/-; 1d10+12 E; Pen 6; Clip 10; Reload 2 Full; Two handed; Flame, Spray 15 Any Deathwatch Living Errata v1.1 149
Astartes Heavy-Bolter Heavy - Bolt Range 150m; -/-/6; 1d10+12 X; Pen 5; Clip 60; Reload 1 Full; Two handed; Tearing 20 Any Deathwatch Living Errata v1.1 147
Astartes Incendiary Grenade Thrown - Grenade 1d10+4 E; Pen 0; Blast (3) 15 Any Deathwatch Living Errata v1.1 150
Astartes Infernus Pistol Pistol - Melta Range 10m; S/-/-; 2d10+12 E; Pen 12; Clip 4; Reload 1 Full; Melta 35 Famed Deathwatch Living Errata v1.1 148
Astartes Krak Grenade Thrown - Grenade 3d10+4 X; Pen 6; 1 Any Deathwatch Living Errata v1.1 150
Astartes Lascannon Heavy - Las Range 300m; S/-/-; 5d10+10 E; Pen 10; Clip 6; Reload 2 Full; Two handed; Proven (3) 30 Respected Deathwatch Living Errata v1.1 149
Astartes Lightning Claw One-Handed Melee - Power 1d10+6 E; Pen 8; Power Field, Proven (4) 30 Distinguished Deathwatch Living Errata v1.1 154
Astartes Meltagun-Vulkan Basic - Melta Range 30m; S/-/-; 2d10+14 E; Pen 12; Clip 6; Reload 2 Full; Two handed; Melta 20 Respected Deathwatch Living Errata v1.1 148
Astartes Missile Launcher-Soundstrike Heavy - Launch Range 250m; S/-/-; 0d0 S; Pen 0; Clip 8; Reload 1 Full; Two handed; 10 Any Deathwatch Living Errata v1.1 152
Astartes Multi-Melta - Maxima Heavy - Melta Range 60m; S/-/-; 2d10+16 E; Pen 12; Clip 12; Reload 2 Full; Two handed; Blast (1), Melta 35 Respected Deathwatch Living Errata v1.1 148
Astartes Nova Grenade Thrown - Grenade 1d10 E; Pen 0; Blast (3) 15 Any Deathwatch Living Errata v1.1 151
Astartes Photon Flash Grenade Thrown - Grenade 0d0 S; Pen 0; 5 Any Deathwatch Living Errata v1.1 151
Astartes Plasma Cannon Heavy - Plasma Range 150m; S/-/-; 2d10+12 E; Pen 10; Clip 16; Reload 5 Full; Two handed; Blast (3), Volatile, Maximal 30 Distinguished Deathwatch Living Errata v1.1 147
Astartes Plasma Grenade Thrown - Grenade 1d10+12 E; Pen 8; Blast (3) 20 Distinguished Deathwatch Living Errata v1.1 151
Astartes Plasma Gun -Ragefire Basic - Plasma Range 100m; S/2/-; 1d10+12 E; Pen 10; Clip 24; Reload 4 Full; Volatile, Maximal 20 Respected Deathwatch Living Errata v1.1 148
Astartes Plasma Pistol Pistol - Plasma Range 30m; S/2/-; 1d10+10 E; Pen 8; Clip 12; Reload 3 Full; Volatile, Maximal 25 Respected Deathwatch Living Errata v1.1 148
Astartes Power Axe One-Handed Melee - Power 1d10+8 E; Pen 7; Power Field, Unbalanced 20 Respected Deathwatch Living Errata v1.1 155
Astartes Power Claymore Two-Handed Melee - Power 1d10+11 E; Pen 8; Two handed; Power Field, Unwieldy, Devastating (1) 15 Distinguished Deathwatch Living Errata v1.1 139
Astartes Power Falchion One-Handed Melee - Power 1d10+4 E; Pen 6; Two handed; Power Field, Razor Sharp 25 Distinguished Deathwatch Living Errata v1.1 139
Astartes Power Fist One-Handed Melee - Power 2d10+"2*SB" E; Pen 9; Power Field, Unwieldy 30 Distinguished Deathwatch Living Errata v1.1 155
Astartes Power Spear Two-Handed Melee - Power 1d10+6 E; Pen 7; Two handed; Power Field 15 Distinguished Deathwatch Living Errata v1.1 139
Astartes Power Sword One-Handed Melee - Power 1d10+6 E; Pen 6; Balanced, Power Field 20 Respected Deathwatch Living Errata v1.1 155
Astartes Servo-Arm Mounted - Primitive 2d10+14 I; Pen 10; 30 Any Deathwatch Living Errata v1.1 177
`.trim();
const fs = require('fs')
const path = require('path')
function loadJson(p){ return JSON.parse(fs.readFileSync(p,'utf8')) }
function saveJson(p, obj){ fs.mkdirSync(path.dirname(p), { recursive: true }); fs.writeFileSync(p, JSON.stringify(obj,null,2),'utf8') }
function titleCaseWord(w){
if (!w) return w
if (w.toUpperCase()==='X' || w.toUpperCase()==='E') return w.toUpperCase()
if (/^\d/.test(w)) return w
if (w.length<=2) return w[0] ? (w[0].toUpperCase()+w.slice(1).toLowerCase()) : w
return w[0].toUpperCase()+w.slice(1).toLowerCase()
}
function titleCaseName(s){
return String(s||'')
.split(/([\s\-()/]+)/)
.map(part => /[\s\-()/]+/.test(part) ? part : titleCaseWord(part))
.join('')
.replace(/\bOf\b|\bAnd\b|\bThe\b/g, m=>m.toLowerCase())
}
function cleanName(raw, desc){
let n = String(raw||'').trim()
const d = String(desc||'')
// Common OCR noise
n = n.replace(/^rtes\b/i,'Astartes ')
n = n.replace(/WeApons/gi,'Weapons')
// If desc contains a canonical prefix use it
const m1 = d.match(/\b(Astartes [A-Za-z][A-Za-z\-\s()]+?)(?:\s+(?:Basic|Pistol|Heavy|Mounted)\b)/)
if (m1) n = m1[1]
// Strip trailing stat blocks starting at class tokens
n = n.replace(/\s+(Basic|Pistol|Heavy|Mounted)\b[\s\S]*$/,'')
// Remove leading filler
n = n.replace(/^[-–—•\s"'`]+/,'')
n = n.replace(/^(a|an|the)\s+/i,'')
// Compact whitespace
n = n.replace(/\s{2,}/g,' ')
// Title case
n = titleCaseName(n)
return n.trim()
}
const keepWord = /(Astartes|Bolter|Boltgun|Bolt Pistol|Storm Bolter|Plasma|Melta|Flamer|Lascannon|Shotgun|Autogun|Autopistol|Lasgun|Laspistol|Assault Cannon|Sniper Rifle|Combi|Cyclone|Missile|Launcher|Grenade|Power Sword|Power Axe|Power Fist|Thunder Hammer|Lightning Claw|Chainsword|Chainfist|Shield|Armor|Armour)/i
const looksBad = /\b(although|because|while|most|this|these|those|provides|requires|usually|common|mode|effects|counts|burst|power|pack|fuel|rounds|weapon is|weapons|weapons can|weapons work)\b/i
function isLikelyItem(name, cat){
if (!name) return false
if (name.length < 3 || name.length > 60) return false
if (/^[a-z]/.test(name)) return false
if (looksBad.test(name)) return false
if (/(WeApons|Weapon)/i.test(name) && !/Power|Force|Storm|Shield|Astartes/i.test(name)) return false
if (/(Grenade|Missile)/i.test(name)) return true
if (/(Launcher)/i.test(name)) return true
if (keepWord.test(name)) return true
// Armor pieces
if (/(Shield|Armour|Armor|Helmet|Helm)/i.test(name)) return true
return false
}
function normalizeEntry(e){
const name = cleanName(e.name, e.desc)
let req = Number(e.req||0)
if (!Number.isFinite(req) || req<0) req = 0
const renown = /Respected|Distinguished|Famed|Hero/.test(e.renown||'') ? e.renown : 'None'
const category = e.category || 'Gear'
return { ...e, name, req, renown, category }
}
function main(){
const input = process.argv[2] || path.resolve('public/deathwatch-armoury.json')
const output = process.argv[3] || input
const src = loadJson(input)
const cleaned = []
const seen = new Set()
for (const e of src){
const n = normalizeEntry(e)
if (!isLikelyItem(n.name, n.category)) continue
const key = n.name.toLowerCase()
if (seen.has(key)) continue
seen.add(key)
cleaned.push(n)
/** Parse raw weapon data */
const parseWeaponData = (data) => {
// Split into lines and filter out empty ones
const lines = data.split('\n').filter(line => line.trim());
const weapons = [];
// Skip header
for (let i = 1; i < lines.length; i++) {
const cols = lines[i].split('\t').map(col => col.trim());
if (cols.length >= 8) {
weapons.push({
name: cols[0],
class: cols[1],
damage: cols[2],
cost: parseInt(cols[3]) || 0,
availability: cols[4],
gameSetting: cols[5],
book: cols[6],
pageNo: cols[7]
});
}
}
// Sort by category then name
cleaned.sort((a,b)=> String(a.category).localeCompare(String(b.category)) || a.name.localeCompare(b.name))
saveJson(output, cleaned)
console.log('Cleaned items:', cleaned.length, 'from', src.length, '->', output)
return weapons;
};
/** Core Deathwatch equipment types */
const weaponData = `
Name Class Damage Cost Availability Game Setting Book Page No.
${rawWeaponData}
`.trim();
const weapons = parseWeaponData(weaponData);
// Use Map to deduplicate items, preferring Living Errata versions
const uniqueWeapons = new Map();
weapons.forEach(weapon => {
const key = weapon.name;
const existing = uniqueWeapons.get(key);
// Prefer Living Errata over Core Rulebook
if (!existing || weapon.book.includes('Living Errata')) {
uniqueWeapons.set(key, weapon);
}
});
const mapAvailabilityToRenown = (availability) => {
const map = {
'Any': 'None',
'Common': 'None',
'Scarce': 'None',
'Rare': 'Respected',
'Very Rare': 'Distinguished',
'Extremely Rare': 'Hero',
'Near Unique': 'Hero',
'Unique': 'Legend',
'Distinguished': 'Distinguished',
'Famed': 'Hero',
'Respected': 'Respected',
'Hero': 'Hero'
};
return map[availability] || 'Distinguished';
};
const mapClassToCategory = (itemClass) => {
if (itemClass.toLowerCase().includes('melee')) return 'Melee Weapon';
if (itemClass.toLowerCase().includes('pistol')) return 'Ranged Weapon';
if (itemClass.toLowerCase().includes('basic') ||
itemClass.toLowerCase().includes('heavy') ||
itemClass.toLowerCase().includes('mounted')) return 'Ranged Weapon';
if (itemClass.toLowerCase().includes('thrown')) return 'Grenade';
return 'Other';
};
// Group items by category
const generateShopItems = () => {
const items = Array.from(uniqueWeapons.values()).map(w => ({
name: w.name,
req: w.cost,
renown: mapAvailabilityToRenown(w.availability),
category: mapClassToCategory(w.class),
stats: {
damage: w.damage,
class: w.class,
source: `${w.book} p${w.pageNo}`
}
}));
return {
armor: [
{ name: "Astartes Power Armour", req: 40, renown: "None", category: "Armor" },
{ name: "Astartes Mk6 Corvus Power Armour", req: 45, renown: "Distinguished", category: "Armor" },
// Convert the raw weapons into categorized shop items
const generateShopItems = () => {
const items = Array.from(uniqueWeapons.values()).map(w => ({
name: w.name,
req: w.cost,
renown: mapAvailabilityToRenown(w.availability),
category: mapClassToCategory(w.class),
stats: {
damage: w.damage,
class: w.class,
source: `${w.book} p${w.pageNo}`
}
}));
// Group items by category
const categorized = {
armor: [
{ name: "Astartes Power Armour", req: 40, renown: "None", category: "Armor" },
{ name: "Astartes Mk6 Corvus Power Armour", req: 45, renown: "Distinguished", category: "Armor" },
{ name: "Astartes Mk7 Aquila Power Armour", req: 45, renown: "Distinguished", category: "Armor" },
{ name: "Astartes Mk8 Errant Power Armour", req: 50, renown: "Distinguished", category: "Armor" },
{ name: "Armour of the Remorseless Crusader", req: 70, renown: "Hero", category: "Armor" },
{ name: "Astartes Scout Armour", req: 30, renown: "None", category: "Armor" },
{ name: "Astartes Terminator Armour", req: 100, renown: "Hero", category: "Armor" },
{ name: "Astartes Artificer Armour", req: 60, renown: "Distinguished", category: "Armor" },
{ name: "Astartes Storm Shield", req: 35, renown: "Distinguished", category: "Armor" },
{ name: "Astartes Combat Shield", req: 20, renown: "Distinguished", category: "Armor" },
{ name: "Astartes Iron Halo", req: 45, renown: "Hero", category: "Armor" },
{ name: "Astartes Sacred Shield", req: 40, renown: "Hero", category: "Armor" }
],
rangedWeapons: [
{ name: "Astartes Bolt Pistol", req: 15, renown: "None", category: "Ranged Weapon" },
{ name: "Astartes Boltgun", req: 20, renown: "None", category: "Ranged Weapon" },
{ name: "Astartes Heavy Bolter", req: 30, renown: "None", category: "Ranged Weapon" },
{ name: "Astartes Storm Bolter", req: 25, renown: "Distinguished", category: "Ranged Weapon" },
{ name: "Angelus Bolter", req: 25, renown: "Respected", category: "Ranged Weapon" },
{ name: "Stalker Pattern Boltgun", req: 30, renown: "Distinguished", category: "Ranged Weapon" },
{ name: "Hellfire Bolter", req: 35, renown: "Distinguished", category: "Ranged Weapon" },
{ name: "Plasma Pistol", req: 30, renown: "Distinguished", category: "Ranged Weapon" },
{ name: "Plasma Gun", req: 35, renown: "Distinguished", category: "Ranged Weapon" },
{ name: "Plasma Cannon", req: 45, renown: "Hero", category: "Ranged Weapon" },
{ name: "Meltagun", req: 35, renown: "Distinguished", category: "Ranged Weapon" },
{ name: "Multi-Melta", req: 45, renown: "Hero", category: "Ranged Weapon" },
{ name: "Flamer", req: 25, renown: "None", category: "Ranged Weapon" },
{ name: "Heavy Flamer", req: 35, renown: "Distinguished", category: "Ranged Weapon" },
{ name: "Lascannon", req: 40, renown: "Distinguished", category: "Ranged Weapon" },
{ name: "Missile Launcher", req: 40, renown: "Distinguished", category: "Ranged Weapon" },
{ name: "Vengeance Launcher", req: 45, renown: "Hero", category: "Ranged Weapon" },
{ name: "Astartes Grenade Launcher", req: 30, renown: "Distinguished", category: "Ranged Weapon" }
],
meleeWeapons: [
{ name: "Chainsword", req: 15, renown: "None", category: "Melee Weapon" },
{ name: "Eviscerator", req: 35, renown: "Distinguished", category: "Melee Weapon" },
{ name: "Power Sword", req: 25, renown: "Distinguished", category: "Melee Weapon" },
{ name: "Power Axe", req: 25, renown: "Distinguished", category: "Melee Weapon" },
{ name: "Power Maul", req: 25, renown: "Distinguished", category: "Melee Weapon" },
{ name: "Power Lance", req: 30, renown: "Distinguished", category: "Melee Weapon" },
{ name: "Power Fist", req: 35, renown: "Distinguished", category: "Melee Weapon" },
{ name: "Lightning Claw", req: 35, renown: "Distinguished", category: "Melee Weapon" },
{ name: "Thunder Hammer", req: 40, renown: "Hero", category: "Melee Weapon" },
{ name: "Force Sword", req: 35, renown: "Distinguished", category: "Melee Weapon" },
{ name: "Force Axe", req: 35, renown: "Distinguished", category: "Melee Weapon" },
{ name: "Force Halberd", req: 35, renown: "Distinguished", category: "Melee Weapon" },
{ name: "Force Staff", req: 35, renown: "Distinguished", category: "Melee Weapon" },
{ name: "Combat Knife", req: 10, renown: "None", category: "Melee Weapon" }
],
ammunition: [
{ name: "Bolt Pistol Magazine", req: 2, renown: "None", category: "Ammunition" },
{ name: "Boltgun Magazine", req: 2, renown: "None", category: "Ammunition" },
{ name: "Storm Bolter Magazine", req: 3, renown: "None", category: "Ammunition" },
{ name: "Heavy Bolter Belt", req: 5, renown: "None", category: "Ammunition" },
{ name: "Hellfire Rounds", req: 8, renown: "Distinguished", category: "Ammunition" },
{ name: "Metal Storm Rounds", req: 8, renown: "Distinguished", category: "Ammunition" },
{ name: "Dragonfire Rounds", req: 8, renown: "Distinguished", category: "Ammunition" },
{ name: "Vengeance Rounds", req: 8, renown: "Distinguished", category: "Ammunition" },
{ name: "Kraken Penetrator Rounds", req: 8, renown: "Distinguished", category: "Ammunition" },
{ name: "Plasma Charge Pack", req: 5, renown: "None", category: "Ammunition" },
{ name: "Melta Charge", req: 5, renown: "None", category: "Ammunition" },
{ name: "Promethium Tank", req: 3, renown: "None", category: "Ammunition" },
{ name: "Missile Pack", req: 5, renown: "None", category: "Ammunition" },
{ name: "Krak Missiles", req: 8, renown: "None", category: "Ammunition" },
{ name: "Frag Missiles", req: 6, renown: "None", category: "Ammunition" },
{ name: "Vengeance Launcher Shells", req: 6, renown: "Distinguished", category: "Ammunition" }
]
};
const path = require('path');
const fs = require('fs');
if (require.main === module) {
const output = path.resolve('public/deathwatch-armoury.json');
fs.writeFileSync(output, JSON.stringify(coreItems, null, 2), 'utf8');
console.log('Wrote core items to:', output);
}
if (require.main === module) main()
module.exports = coreItems;

86
scripts/fetch-armoury.js Normal file
View File

@@ -0,0 +1,86 @@
const axios = require('axios');
const cheerio = require('cheerio');
async function fetchWeaponData() {
try {
const response = await axios.get('http://www.40krpgtools.com/armoury/weapons/');
const html = response.data;
const $ = cheerio.load(html);
const weapons = [];
// Select the table and iterate through rows
$('table tr').each((i, row) => {
if (i === 0) return; // Skip header row
const cols = $(row).find('td');
if (cols.length < 8) return; // Skip incomplete rows
weapons.push({
name: $(cols[0]).text().trim(),
class: $(cols[1]).text().trim(),
damage: $(cols[2]).text().trim(),
cost: parseInt($(cols[3]).text().trim()) || 0,
availability: $(cols[4]).text().trim(),
gameSetting: $(cols[5]).text().trim(),
book: $(cols[6]).text().trim(),
pageNo: $(cols[7]).text().trim()
});
});
// Filter for Deathwatch items only
const deathwatchItems = weapons.filter(w =>
w.gameSetting === 'Deathwatch' &&
w.name.startsWith('Astartes')
);
// Convert to our shop format
const shopItems = deathwatchItems.map(w => ({
name: w.name,
req: w.cost,
renown: mapAvailabilityToRenown(w.availability),
category: mapClassToCategory(w.class),
stats: {
damage: w.damage,
class: w.class,
source: `${w.book} p${w.pageNo}`
}
}));
return shopItems;
} catch (error) {
console.error('Error fetching weapon data:', error);
return [];
}
}
function mapAvailabilityToRenown(availability) {
const map = {
'Any': 'None',
'Common': 'None',
'Scarce': 'None',
'Rare': 'Respected',
'Very Rare': 'Distinguished',
'Extremely Rare': 'Hero',
'Near Unique': 'Hero',
'Unique': 'Legend'
};
return map[availability] || 'Distinguished';
}
function mapClassToCategory(itemClass) {
if (itemClass.includes('Melee')) return 'Melee Weapon';
if (itemClass.includes('Pistol')) return 'Ranged Weapon';
if (itemClass.includes('Basic') || itemClass.includes('Heavy')) return 'Ranged Weapon';
return 'Other';
}
if (require.main === module) {
fetchWeaponData()
.then(items => {
console.log(JSON.stringify(items, null, 2));
})
.catch(console.error);
}
module.exports = { fetchWeaponData };

194
scripts/parse-armor.js Normal file
View File

@@ -0,0 +1,194 @@
const armorData = `
Name Type Protection Cost Availability Game Setting Book Page No.
Armour of the Remorseless Crusader Power 12 all 70 Any Deathwatch Deathwatch Core Rulebook 167
Astartes Artificer Armour Power 12 all 60 Hero Deathwatch Deathwatch Core Rulebook 163
Astartes Mk 1 Thunder Power Armour Power 0 / 6 / 8 / 4 0 Any Deathwatch Rites of Battle 151
Astartes Mk 2 Crusade Power Armour Power 0 / 7 / 9 / 7 0 Any Deathwatch Rites of Battle 151
Astartes Mk 3 Iron Power Armour Power 0 / 9 / 10 / 9 0 Any Deathwatch Rites of Battle 151
Astartes Mk 4 Maximus Power Armour Power 0 / 7 / 9 / 7 0 Any Deathwatch Rites of Battle 151
Astartes Mk 5 Heresy Power Armour Power 0 / 8 / 9 / 8 0 Any Deathwatch Rites of Battle 151
Astartes Mk 6 Corvus Power Armour Power 0 / 8 / 9 / 8 0 Any Deathwatch Rites of Battle 151
Astartes Mk 7 Aquilla Power Armour Power 0 / 8 / 10 / 8 0 Any Deathwatch Rites of Battle 151
Astartes Mk 8 Errant Power Armour Power 0 / 8 / 11 / 8 0 Any Deathwatch Rites of Battle 151
Astartes Mk1 Power Armour Helm Power 6 / 0 / 0 / 0 0 Any Deathwatch Rites of Battle 151
Astartes Mk2 Power Armour Helm Power 7 / 0 / 0 / 0 0 Any Deathwatch Rites of Battle 151
Astartes Mk3 Power Armour Helm Power 9 / 0 / 0 / 0 0 Any Deathwatch Rites of Battle 151
Astartes Mk4 Power Armour Helm Power 7 / 0 / 0 / 0 0 Any Deathwatch Rites of Battle 151
Astartes Mk5 Power Armour Helm Power 8 / 0 / 0 / 0 0 Any Deathwatch Rites of Battle 151
Astartes Mk6 Power Armour Helm Power 8 / 0 / 0 / 0 0 Any Deathwatch Rites of Battle 151
Astartes Mk7 Power Armour Helm Power 8 / 0 / 0 / 0 0 Any Deathwatch Rites of Battle 151
Astartes Mk8 Power Armour Helm Power 8 / 0 / 0 / 0 0 Any Deathwatch Rites of Battle 151
Astartes Power Armour Power 0 / 8 / 10 / 8 0 Any Deathwatch Deathwatch Core Rulebook 160
Astartes Power Armour Helm Power 8 / 0 / 0 / 0 0 Any Deathwatch Deathwatch Core Rulebook 160
Astartes Scout Armour Carapace 0 / 6 / 6 / 0 0 Any Deathwatch Deathwatch Core Rulebook 163
Astartes Storm Shield - Protective Shield 0 / 0LA 4RA / 4 / 0 35 Distinguished Deathwatch Deathwatch Core Rulebook 166
Astartes Tactical Dreadnaught-Terminator Armour Power 0 / 14 / 14 / 14 100 Famed Deathwatch Deathwatch Core Rulebook 164
Astartes Tactical Dreadnaught-Terminator Armour Helm Power 14 / 0 / 0 / 0 100 Famed Deathwatch Deathwatch Core Rulebook 164
Astartes Terminator Armour Power 14 all 100 Famed Deathwatch Deathwatch Core Rulebook 164
Auran Golden Saurian Scale Armour Primitive 6 all 0 Any Deathwatch The Emperor Protects 48
Auran Saurian Scale Armour Primitive 4 all 0 Any Deathwatch The Emperor Protects 46
Chaos Armour Plating Other 6 all 0 Any Deathwatch Mark of the Xenos 91
Chaos Bones of Subersion Other 9 all 0 Any Deathwatch Mark of the Xenos 86
Chaos Flak Robes and Brazen Carapace Carapace 5 / 5 / 6 / 4 0 Any Deathwatch Deathwatch Core Rulebook 364
Chaos Slinnar Obsidian Shell Other 8 all 0 Any Deathwatch Mark of the Xenos 92
Chaos Warded Adamantine Shell Other 9 all 0 Any Deathwatch Mark of the Xenos 95
CM Power Armour Power 8 / 8 / 10 / 8 0 Any Deathwatch Mark of the Xenos 114
Daemon Iron Hide Other 13 all 0 Any Deathwatch Mark of the Xenos 101
Deathwatch Scout Armor Carapace 0 / 6 / 6 / 6 0 Any Deathwatch Rites of Battle 142
Diagnostor Helmet Power 8 / 0 / 0 / 0 15 Any Deathwatch Deathwatch Core Rulebook 165
Dmn Armour of Chaos Power 12 all 0 Any Deathwatch Deathwatch Core Rulebook 362
Dmn Brazen Chaos Armour Other 12 all 0 Any Deathwatch The Emperor Protects 136
Fenris Pattern Wolf Helm Power 8 / 0 / 0 / 0 0 Respected Deathwatch Rites of Battle 143
Nec Metallic Exoskeleton Necron 8 all 0 Any Deathwatch The Emperor Protects 94
Nec Metallic Shell Necron 10 all 0 Any Deathwatch The Emperor Protects 94
Obliterator Warped Mechnical Flesh Other 12 all 0 Any Deathwatch Mark of the Xenos 120
Ork Eavy Armour Ork 6 / 4 / 6 / 4 0 Any Deathwatch Mark of the Xenos 57
Ork Flak Armour Ork 0 / 0 / 2 / 0 0 Any Deathwatch Mark of the Xenos 57
Ork Mega Armour Ork 6 / 10 / 14 / 10 0 Any Deathwatch Mark of the Xenos 57
Rad Corrupted Mechnical Flesh Other 7 all 0 Any Deathwatch The Emperor Protects 91
Scales Primitive 2 all 0 Any Deathwatch The Emperor Protects 50
Scaly Hide Primitive 4 all 0 Any Deathwatch The Emperor Protects 46
Skull Helm Power 9 / 0 / 0 / 0 0 Any Deathwatch Rites of Battle 143
Tau Fire Warrior Armour Carapace 6 all 0 Any Deathwatch Deathwatch Core Rulebook 368
Tau Gun Drone Armour Natural 5 all 0 Any Deathwatch Deathwatch Core Rulebook 367
Tau Pathfinder Armour Carapace 7 / 0 / 7 / 0 0 Any Deathwatch Mark of the Xenos 25
Tau XV-8 Crisis Battlesuit Power 9 all 0 Any Deathwatch Mark of the Xenos 9
Tau XV-88 Broadside Battlesuit Power 12 all 0 Any Deathwatch Mark of the Xenos 6
Tau XV15 Stealth Suit (Limbs) Power 0 / 7 / 0 / 7 0 Any Deathwatch Deathwatch Core Rulebook 367
Tau XV15 Stealth Suit (Top) Power 8 / 0 / 8 / 0 0 Any Deathwatch Deathwatch Core Rulebook 367
Tau XV8 Crisis Suit Power 9 all 0 Any Deathwatch Deathwatch Core Rulebook 366
Tyr Bonded Exoskeleton Natural 10 all 0 Any Deathwatch Deathwatch Core Rulebook 370
Tyr Chitinous Carapace Natural 3 all 0 Any Deathwatch Deathwatch Core Rulebook 371
Tyr Exoskeleton Natural 2 all 0 Any Deathwatch Mark of the Xenos 43
Tyr Hardened Carapace Natural 6 all 0 Any Deathwatch Mark of the Xenos 40
Tyr Light Reinforced Chitin Natural 4 all 0 Any Deathwatch Mark of the Xenos 39
Tyr Medium Reinforced Chitin Natural 6 all 0 Any Deathwatch Mark of the Xenos 37
Tyr Reinforced Chitin Natural 8 all 0 Any Deathwatch Deathwatch Core Rulebook 371
Tyr Thickened Scales Natural 10 all 0 Any Deathwatch Mark of the Xenos 46
Vespid Chitin Natural 3 all 0 Any Deathwatch Mark of the Xenos 26
Xenos Hardened Hide Natural 6 all 0 Any Deathwatch Mark of the Xenos 66
Xenos Scaly Hide Natural 3 all 0 Any Deathwatch Mark of the Xenos 63
Xenos Slimey Hide Natural 2 all 0 Any Deathwatch Mark of the Xenos 68
Xenos Thick Scales Natural 5 all 0 Any Deathwatch Mark of the Xenos 65`;
// Parse armor data into structured objects
const parseArmor = (data) => {
const lines = data.split('\n').filter(line => line.trim());
return lines.map(line => {
const cols = line.split('\t').map(col => col.trim());
if (cols.length >= 8) {
return {
name: cols[0],
type: cols[1],
protection: cols[2],
cost: parseInt(cols[3]) || 0,
availability: cols[4],
gameSetting: cols[5],
book: cols[6],
pageNo: cols[7]
};
}
return null;
}).filter(a => a !== null);
};
// Map armor types to categories
const typeToCategory = (armorType) => {
const lowerType = armorType.toLowerCase();
if (lowerType.includes('power')) return 'Power Armor';
if (lowerType.includes('carapace')) return 'Carapace Armor';
if (lowerType.includes('primitive')) return 'Primitive Armor';
if (lowerType.includes('natural')) return 'Natural Armor';
if (lowerType.includes('shield')) return 'Shields';
if (lowerType === 'necron') return 'Xenos Armor';
if (lowerType === 'ork') return 'Xenos Armor';
return 'Other Armor';
};
// Parse protection values into a structured format
const parseProtection = (protection) => {
if (protection.includes('all')) {
const value = parseInt(protection.split(' ')[0]);
return {
head: value,
arms: value,
body: value,
legs: value
};
}
const parts = protection.split('/').map(p => p.trim());
return {
head: parseInt(parts[0]) || 0,
arms: parseInt(parts[1]) || 0,
body: parseInt(parts[2]) || 0,
legs: parseInt(parts[3]) || 0
};
};
// Map availability levels to renown levels (using the same mapping as weapons)
const availabilityToRenown = {
'Any': 'None',
'Common': 'None',
'Scarce': 'None',
'Rare': 'Respected',
'Very Rare': 'Distinguished',
'Extremely Rare': 'Hero',
'Near Unique': 'Hero',
'Unique': 'Legend',
'Distinguished': 'Distinguished',
'Famed': 'Hero',
'Respected': 'Respected',
'Hero': 'Hero'
};
// Process armor into shop format
const processArmor = () => {
const armor = parseArmor(armorData);
// Use a Map to deduplicate items (keeping only the Living Errata version when available)
const uniqueArmor = new Map();
armor.forEach(item => {
const key = item.name;
const existing = uniqueArmor.get(key);
// Prefer Living Errata over Core Rulebook
if (!existing || item.book.includes('Living Errata')) {
uniqueArmor.set(key, {
name: item.name,
req: parseInt(item.cost),
renown: availabilityToRenown[item.availability] || 'Distinguished',
category: typeToCategory(item.type),
stats: {
type: item.type,
protection: parseProtection(item.protection),
source: `${item.book} p${item.pageNo}`
}
});
}
});
return Array.from(uniqueArmor.values());
};
// Process armor and organize by category
const armor = processArmor();
const categorizedArmor = {
powerArmor: armor.filter(item => item.category === 'Power Armor'),
carapaceArmor: armor.filter(item => item.category === 'Carapace Armor'),
primitiveArmor: armor.filter(item => item.category === 'Primitive Armor'),
naturalArmor: armor.filter(item => item.category === 'Natural Armor'),
xenosArmor: armor.filter(item => item.category === 'Xenos Armor'),
shields: armor.filter(item => item.category === 'Shields'),
otherArmor: armor.filter(item => item.category === 'Other Armor')
};
if (require.main === module) {
const fs = require('fs');
const path = require('path');
const output = path.resolve('public/deathwatch-armor.json');
fs.writeFileSync(output, JSON.stringify(categorizedArmor, null, 2), 'utf8');
console.log('Wrote armor data to:', output);
}
module.exports = { categorizedArmor, processArmor };

128
scripts/parse-weapons.js Normal file
View File

@@ -0,0 +1,128 @@
const weaponsData = `
Angelus Bolter Basic - Bolt Range 50m; S/3/-; 1d10+9 X; Pen 6; Clip 36; Reload 2 Full; Tearing 25 Respected Deathwatch First Founding 97
Argrax Clawed Hooves Natural - Primitive 1d10 I; Pen 0; 0 Any Deathwatch The Emperor Protects 46
Armour of the Remorseless Crusader (Forearm Sword) One-Handed Melee - Primitive 1d10+8 R; Pen 6; +10 to hit; Sanctified 70 Hero Deathwatch Deathwatch Core Rulebook 167
Artificer Omnissian Axe One-Handed Melee - Power 1d10+9 E; Pen 7; Power Field, Unbalanced 25 Distinguished Deathwatch First Founding 99
Astartes Anti-Plant Grenade Thrown - Grenade 3d10 E; Pen 0; Blast (3) 18 Any Deathwatch Living Errata v1.1 136
Astartes Armoursbane Missile Launcher Heavy - Launch Range 200m; S/-/-; 0d0 S; Pen 0; Clip 1; Reload 2 Full; Two handed; 15 Any Deathwatch Living Errata v1.1 137
Astartes Assault Cannon Mounted - Solid Projectile Range 150m; -/-/10; 2d10+12 I; Pen 6; Clip 200; Reload 3 Full; Tearing, Devastating (1) 30 Famed Deathwatch Living Errata v1.1 149
Astartes Assault Shotgun Basic - Solid Projectile Range 30m; S/3/5; 1d10+10 I; Pen 4; Clip 20; Reload 1 Full; Two handed; Reliable, Scatter 7 Any Deathwatch Living Errata v1.1 136
Astartes Blind Grenade Thrown - Grenade 0d0 S; Pen 0; Smoke (10) 10 Any Deathwatch Living Errata v1.1 150
Astartes Bolt Pistol Pistol - Bolt Range 30m; S/2/-; 1d10+9 X; Pen 4; Clip 14; Reload 1 Full; Tearing 5 Any Deathwatch Living Errata v1.1 146
Astartes Bolter-Godwyn Basic - Bolt Range 100m; S/3/-; 1d10+9 X; Pen 4; Clip 28; Reload 1 Full; Two handed; Tearing 5 Any Deathwatch Living Errata v1.1 146
Astartes Boltgun-Stalker Basic - Bolt Range 200m; S/-/-; 1d10+9 X; Pen 4; Clip 24; Reload 1 Full; Two handed; Accurate, Tearing 15 Respected Deathwatch Living Errata v1.1 146
Astartes Chainfist One-Handed Melee - Power 2d10+"2*SB" E; Pen 10; Power Field, Tearing, Unwieldy 40 Famed Deathwatch Living Errata v1.1 154
Astartes Chainsword One-Handed Melee - Chain 1d10+3 R; Pen 3; Balanced, Tearing 5 Any Deathwatch Living Errata v1.1 153
Astartes Combat Knife One-Handed Melee - Primitive 1d10 R; Pen 2; 3 Any Deathwatch Living Errata v1.1 155
Astartes Combat Shield One-Handed Melee - Primitive 1d5+1 I; Pen 0; Balanced 20 Distinguished Deathwatch Living Errata v1.1 166
Astartes Combi-Weapon Basic - Bolt Range 100m; S/4/-; 1d10+9 X; Pen 4; Clip 28; Reload 1 Full; Two handed; Tearing 15 Respected Deathwatch Living Errata v1.1 146
Astartes Conversion Beamer Heavy - Exotic Range 100m; S/-/-; 0d0 E; Pen 0; Clip 4; Reload 2 Full; Two handed; 40 Hero Deathwatch Living Errata v1.1 152
Astartes Cyclone Missile Launcher Mounted - Launch Range 300m; S/2/-; 0d0 S; Pen 0; Clip 12; Reload 3 Full; 25 Famed Deathwatch Living Errata v1.1 152
Astartes Digital Flamer Pistol - Exotic Range 4m; S/-/-; 1d10+9 E; Pen 4; Clip 1; Reload 1d5 Hours; Flame, Spray 35 Famed Deathwatch Living Errata v1.1 153
Astartes Chainfist One-Handed Melee - Power 2d10+"2*SB" E; Pen 10; Power Field, Tearing, Unwieldy 40 Famed Deathwatch Living Errata v1.1 154
Astartes Chainfist One-Handed Melee - Power 2d10+"2*SB" E; Pen 10; Power Field, Tearing 40 Famed Deathwatch Deathwatch Core Rulebook 154
Astartes Chainsword One-Handed Melee - Chain 1d10+3 R; Pen 4; Balanced, Tearing 5 Any Deathwatch Deathwatch Core Rulebook 153
Astartes Chainsword One-Handed Melee - Chain 1d10+3 R; Pen 3; Balanced, Tearing 5 Any Deathwatch Living Errata v1.1 153
Astartes Combat Knife One-Handed Melee - Primitive 1d10 R; Pen 2; 3 Any Deathwatch Living Errata v1.1 155
Astartes Combat Knife One-Handed Melee - Primitive 1d10+2 R; Pen 2; 3 Any Deathwatch Deathwatch Core Rulebook 155
Astartes Combat Shield One-Handed Melee - Primitive 1d5+1 I; Pen 0; Balanced 20 Distinguished Deathwatch Living Errata v1.1 166
Astartes Combat Shield One-Handed Melee - Primitive 1d5+1 I; Pen 0; Balanced 20 Distinguished Deathwatch Deathwatch Core Rulebook 166
Astartes Combi-Weapon Basic - Bolt Range 100m; S/2/4; 2d10+5 X; Pen 5; Clip 28; Reload 1 Full; Two handed; Tearing 15 Respected Deathwatch Deathwatch Core Rulebook 146
`;
// Parse weapon data into structured objects
const parseWeapons = (data) => {
const lines = data.split('\n').filter(line => line.trim());
return lines.map(line => {
const cols = line.split('\t').map(col => col.trim());
if (cols.length >= 8) {
return {
name: cols[0],
class: cols[1],
damage: cols[2],
cost: parseInt(cols[3]) || 0,
availability: cols[4],
gameSetting: cols[5],
book: cols[6],
pageNo: cols[7]
};
}
return null;
}).filter(w => w !== null);
};
// Map availability levels to renown levels
const availabilityToRenown = {
'Any': 'None',
'Common': 'None',
'Scarce': 'None',
'Rare': 'Respected',
'Very Rare': 'Distinguished',
'Extremely Rare': 'Hero',
'Near Unique': 'Hero',
'Unique': 'Legend',
'Distinguished': 'Distinguished',
'Famed': 'Hero',
'Respected': 'Respected',
'Hero': 'Hero'
};
// Map weapon classes to shop categories
const classToCategory = (itemClass) => {
const lowerClass = itemClass.toLowerCase();
if (lowerClass.includes('melee')) return 'Melee Weapon';
if (lowerClass.includes('pistol')) return 'Ranged Weapon';
if (lowerClass.includes('basic') ||
lowerClass.includes('heavy') ||
lowerClass.includes('mounted')) return 'Ranged Weapon';
if (lowerClass.includes('thrown')) return 'Grenade';
return 'Other';
};
// Process weapons into shop format
const processWeapons = () => {
const weapons = parseWeapons(weaponsData);
// Use a Map to deduplicate items (keeping only the Living Errata version when available)
const uniqueWeapons = new Map();
weapons.forEach(weapon => {
const key = weapon.name;
const existing = uniqueWeapons.get(key);
// Prefer Living Errata over Core Rulebook
if (!existing || weapon.book.includes('Living Errata')) {
uniqueWeapons.set(key, {
name: weapon.name,
req: parseInt(weapon.cost),
renown: availabilityToRenown[weapon.availability] || 'Distinguished',
category: classToCategory(weapon.class),
stats: {
damage: weapon.damage,
class: weapon.class,
source: `${weapon.book} p${weapon.pageNo}`
}
});
}
});
return Array.from(uniqueWeapons.values());
};
// Process weapons and organize by category
const weapons = processWeapons();
const categorizedWeapons = {
rangedWeapons: weapons.filter(item => item.category === 'Ranged Weapon'),
meleeWeapons: weapons.filter(item => item.category === 'Melee Weapon'),
grenades: weapons.filter(item => item.category === 'Grenade'),
other: weapons.filter(item => item.category === 'Other')
};
if (require.main === module) {
const fs = require('fs');
const path = require('path');
const output = path.resolve('public/deathwatch-weapons.json');
fs.writeFileSync(output, JSON.stringify(categorizedWeapons, null, 2), 'utf8');
console.log('Wrote weapon data to:', output);
}
module.exports = { categorizedWeapons, processWeapons };