fix1
This commit is contained in:
48
scripts/build-shop-db.js
Normal file
48
scripts/build-shop-db.js
Normal 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
331
scripts/clean-armoury.js
Executable file → Normal 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|weapon’s|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
86
scripts/fetch-armoury.js
Normal 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
194
scripts/parse-armor.js
Normal 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
128
scripts/parse-weapons.js
Normal 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 };
|
||||
Reference in New Issue
Block a user