- Implemented `validatePlayer` and `normalizeTabInfo` functions in `database/validate.js` for player data validation and normalization. - Added error handling for required fields and enforced data types and constraints. - Introduced standardization for characteristics and skills. feat: Create script for extracting rules from PDF files - Developed `extract-rules.js` to extract text from PDF rulebooks and categorize rules. - Integrated `pdftotext` for PDF processing and created a searchable rules database. - Implemented rule categorization and indexing for efficient searching. feat: Implement RulesTab component for rule searching - Created `RulesTab.jsx` for searching and displaying rules with filtering options. - Added recent searches and quick reference buttons for user convenience. - Integrated API calls for fetching rules and displaying results dynamically. feat: Add test script for Rules API - Created `test-rules.js` to test the functionality of the Rules API endpoints. - Included tests for search and stats endpoints to ensure proper response handling.
160 lines
6.1 KiB
JavaScript
160 lines
6.1 KiB
JavaScript
const fs = require('fs');
|
|
const path = require('path');
|
|
const { playerHelpers } = require('../sqlite-db');
|
|
|
|
const armouryPath = path.join(__dirname, '..', '..', 'public', 'deathwatch-armoury.json');
|
|
const armoury = JSON.parse(fs.readFileSync(armouryPath, 'utf8'));
|
|
|
|
function findItemByName(name) {
|
|
const needle = String(name).toLowerCase();
|
|
for (const cat of Object.keys(armoury.items)) {
|
|
const arr = armoury.items[cat];
|
|
if (Array.isArray(arr)) {
|
|
let found = arr.find(i => String(i.name).toLowerCase() === needle);
|
|
if (found) return found;
|
|
found = arr.find(i => String(i.name).toLowerCase().includes(needle));
|
|
if (found) return found;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
(function main(){
|
|
const name = 'anders';
|
|
const player = playerHelpers.getByName(name);
|
|
if (!player) return console.error('Player not found', name);
|
|
|
|
const backupsDir = path.join(__dirname, '..', 'backups');
|
|
if (!fs.existsSync(backupsDir)) fs.mkdirSync(backupsDir, { recursive: true });
|
|
const before = path.join(backupsDir, `anders.before.${Date.now()}.json`);
|
|
fs.writeFileSync(before, JSON.stringify(player, null, 2));
|
|
console.log('Backup written:', before);
|
|
|
|
// Character exact from screenshot
|
|
const charName = 'Brother Sepheran';
|
|
const characteristics = {
|
|
ws: 47,
|
|
bs: 48,
|
|
s: 41,
|
|
t: 43,
|
|
ag: 42,
|
|
int: 50,
|
|
per: 43,
|
|
wp: 48,
|
|
fel: 38
|
|
};
|
|
|
|
// Skills to mark trained based on sheet
|
|
const trained = [
|
|
'Awareness (Per)',
|
|
'Common Lore (Int)',
|
|
'Dodge (Ag)',
|
|
'Intimidate (S)',
|
|
'Logic (Int)',
|
|
'Scholastic Lore (Int)',
|
|
// space marine defaults
|
|
'Guns (BS)', 'Melee (WS)'
|
|
];
|
|
|
|
const canonical = [
|
|
'Acrobatics (Ag)', 'Awareness (Per)', 'Charm (Fel)', 'Climb (S)', 'Command (Fel)',
|
|
'Common Lore (Int)', 'Deathwatch', 'Imperium', 'War', 'Dodge (Ag)',
|
|
'Forbidden Lore (Int)', 'Xenos', 'Intimidate (S)', 'Literacy (Int)', 'Medicae (Int)',
|
|
'Navigation (Int)', 'Pilot (Ag)', 'Scholastic Lore (Int)', 'Codex Astartes',
|
|
'Scrutiny (Per)', 'Search (Per)', 'Silent Move (Ag)', 'Speak Language (Int)',
|
|
'High Gothic', 'Low Gothic', 'Survival (Int)', 'Tactics (Int)', 'Tracking (Int)',
|
|
'Tech-Use (Int)'
|
|
];
|
|
|
|
const existingSkills = player.tabInfo && player.tabInfo.skills ? player.tabInfo.skills : [];
|
|
const skillMap = {};
|
|
for (const s of canonical) skillMap[s] = { trained: false, plus10: false, plus20: false };
|
|
for (const s of trained) { skillMap[s] = skillMap[s] || { trained: false, plus10:false, plus20:false }; skillMap[s].trained = true; }
|
|
if (Array.isArray(existingSkills)) {
|
|
for (const s of existingSkills) { if (!skillMap[s]) skillMap[s] = { trained:true, plus10:false, plus20:false }; else skillMap[s].trained = true; }
|
|
} else if (typeof existingSkills === 'object') {
|
|
for (const [k,v] of Object.entries(existingSkills)) { skillMap[k] = Object.assign(skillMap[k]||{trained:false,plus10:false,plus20:false}, v); }
|
|
}
|
|
|
|
// Weapons & gear
|
|
const weaponNames = ['Heavy Bolter', 'Astartes Bolt Pistol', 'Bolt Pistol'];
|
|
const weapons = [];
|
|
for (const wn of weaponNames) {
|
|
const item = findItemByName(wn);
|
|
if (item) {
|
|
const stats = item.stats || {};
|
|
const dmg = stats.damage || '';
|
|
const rangeMatch = dmg.match(/Range\s*([^;]+)/i);
|
|
const penMatch = dmg.match(/Pen\s*(\d+)/i);
|
|
const clipMatch = dmg.match(/Clip\s*(\d+)/i);
|
|
const rldMatch = dmg.match(/Reload\s*([^;]+)/i);
|
|
weapons.push({
|
|
name: item.name,
|
|
class: stats.class || '',
|
|
damage: dmg,
|
|
type: item.category || '',
|
|
pen: penMatch ? penMatch[1] : '',
|
|
range: rangeMatch ? rangeMatch[1].trim() : '',
|
|
rof: '',
|
|
clip: clipMatch ? clipMatch[1] : '',
|
|
rld: rldMatch ? rldMatch[1].trim() : '',
|
|
special: stats.source || ''
|
|
});
|
|
}
|
|
}
|
|
|
|
const frag = findItemByName('Frag Grenade') || { name: 'Frag Grenade' };
|
|
const krak = findItemByName('Krak Grenade') || { name: 'Krak Grenade' };
|
|
const inventory = [ { name: frag.name, count: 3 }, { name: krak.name, count: 3 } ];
|
|
const gearEntries = [ { name: frag.name, qty:3 }, { name: krak.name, qty:3 } ];
|
|
const knife = findItemByName('Combat Knife') || findItemByName('Combat Knife') || { name: 'Combat Knife' };
|
|
gearEntries.push({ name: knife.name, qty:1 });
|
|
|
|
const armourItem = findItemByName('Mark VII Power Armour') || findItemByName('Astartes Power Armour');
|
|
const armour = {};
|
|
if (armourItem && armourItem.stats && armourItem.stats.protection) {
|
|
armour.body = armourItem.name;
|
|
armour.ap = armourItem.stats.protection.body || 8;
|
|
armour.head = armourItem.stats.protection.head || 0;
|
|
armour.ra = armourItem.stats.protection.arms || 8;
|
|
armour.la = armour.ra;
|
|
armour.rl = armourItem.stats.protection.legs || 8;
|
|
armour.ll = armour.rl;
|
|
} else {
|
|
armour.body = armourItem ? armourItem.name : 'Mark VII Power Armour';
|
|
armour.ap = 8; armour.head = 0; armour.ra = 8; armour.la = 8; armour.rl = 8; armour.ll = 8;
|
|
}
|
|
gearEntries.push({ name: armour.body, qty: 1 });
|
|
|
|
const talents = ['Unrelenting Devastation', 'Deathwatch Training'];
|
|
const fate = { total: 2, current: 2 };
|
|
const wounds = { total: 23, current: 23, fatigue: 0 };
|
|
const movement = { half:5, full:10, charge:15, halfAction:5, fullAction:10, run:30 };
|
|
const renown = 'Rank 1';
|
|
const powerArmour = true;
|
|
|
|
const newTab = Object.assign({}, player.tabInfo || {}, {
|
|
charName,
|
|
characteristics,
|
|
skills: skillMap,
|
|
weapons,
|
|
inventory,
|
|
gear: (player.tabInfo && player.tabInfo.gear && player.tabInfo.gear.length) ? player.tabInfo.gear.concat(gearEntries) : gearEntries,
|
|
armour,
|
|
talents,
|
|
fate,
|
|
wounds,
|
|
movement,
|
|
renown,
|
|
powerArmour
|
|
});
|
|
|
|
const ok = playerHelpers.update(name, { name, rollerInfo: player.rollerInfo || {}, shopInfo: player.shopInfo || {}, tabInfo: newTab, pw: player.pw || '', pwHash: player.pwHash || '' });
|
|
if (!ok) return console.error('Failed to update player');
|
|
const after = playerHelpers.getByName(name);
|
|
const afterPath = path.join(backupsDir, `anders.after.${Date.now()}.json`);
|
|
fs.writeFileSync(afterPath, JSON.stringify(after, null, 2));
|
|
console.log('Updated player', name);
|
|
console.log(JSON.stringify(after, null, 2));
|
|
})();
|