- 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.
145 lines
6.0 KiB
JavaScript
145 lines
6.0 KiB
JavaScript
// Simple validation and normalization utilities for player records
|
|
function isObject(v) { return v && typeof v === 'object' && !Array.isArray(v); }
|
|
|
|
function normalizeName(name) {
|
|
if (!name) return '';
|
|
return String(name).trim().replace(/\s+/g, ' ');
|
|
}
|
|
|
|
function normalizeTabInfo(tabInfo) {
|
|
const out = Object.assign({}, isObject(tabInfo) ? tabInfo : {});
|
|
// Flatten nested tabInfo.tabInfo
|
|
if (isObject(out.tabInfo)) {
|
|
Object.assign(out, out.tabInfo);
|
|
delete out.tabInfo;
|
|
}
|
|
|
|
// numeric fields
|
|
['rp','xp','xpSpent','wounds','movement'].forEach(k=>{
|
|
if (out[k] !== undefined) {
|
|
const n = Number(out[k]);
|
|
out[k] = Number.isFinite(n) ? n : 0;
|
|
} else {
|
|
out[k] = 0;
|
|
}
|
|
});
|
|
|
|
// Ensure standard arrays/objects exist
|
|
if (!Array.isArray(out.skills)) out.skills = [];
|
|
if (!Array.isArray(out.inventory)) out.inventory = [];
|
|
if (!isObject(out.characteristics)) out.characteristics = {};
|
|
|
|
// Ensure standard Deathwatch Space Marine characteristic keys exist (lowercase keys
|
|
// matching the frontend PlayerTab expectations). Do not overwrite existing values.
|
|
const standardChars = ['ws','bs','s','t','ag','int','per','wp','fel'];
|
|
// Accept existing upper/lower keys but store lowercase
|
|
const charsIn = Object.assign({}, out.characteristics);
|
|
out.characteristics = {};
|
|
for (const k of standardChars) {
|
|
// prefer existing lowercase, then uppercase variants
|
|
if (charsIn[k] !== undefined) out.characteristics[k] = Number(charsIn[k]) || 0;
|
|
else if (charsIn[k.toUpperCase()] !== undefined) out.characteristics[k] = Number(charsIn[k.toUpperCase()]) || 0;
|
|
else out.characteristics[k] = 0;
|
|
}
|
|
|
|
// Standard Deathwatch core skills to add if missing (preserve existing extras)
|
|
const standardSkills = [
|
|
'Acrobatics (Ag)', 'Awareness (Per)', 'Barter (Fel)', 'Blather (Fel)',
|
|
'Carouse (Fel)', 'Charm (Fel)', 'Chem-Use (Int)', 'Ciphers (Int)',
|
|
'Chapter Runes', 'Climb (S)', 'Command (Fel)', 'Common Lore (Int)',
|
|
'Adeptus Astartes', 'Deathwatch', 'Imperium', 'War', 'Concealment (Ag)',
|
|
'Contortionist (Ag)', 'Deceive (Fel)', 'Demolition (Int)', 'Disguise (Fel)',
|
|
'Dodge (Ag)', 'Drive (Ag)', 'Evaluate', 'Forbidden Lore (Int)', 'Xenos',
|
|
'Gamble (Int)', 'Inquiry (Fel)', 'Interrogation (WP)', 'Intimidate (S)',
|
|
'Invocation (WP)', 'Lip Reading (Per)', 'Literacy (Int)', 'Logic (Int)',
|
|
'Medicae (Int)', 'Navigation (Int)', 'Surface', 'Performer (Fel)',
|
|
'Pilot (Ag)', 'Psyniscience (Per)', 'Scholastic Lore (Int)', 'Codex Astartes',
|
|
'Scrutiny (Per)', 'Search (Per)', 'Secret Tongue (Int)', 'Security (Ag)',
|
|
'Shadowing (Ag)', 'Silent Move (Ag)', 'Sleight of Hand (Ag)', 'Speak Language (Int)',
|
|
'Survival (Int)', 'Swim (S)', 'Tactics (Int)', 'Tech-Use (Int)', 'Tracking (Int)',
|
|
'Trade (Int)', 'Wrangling (Int)'
|
|
];
|
|
// Add any missing standard skills (case-sensitive check)
|
|
for (const s of standardSkills) {
|
|
if (!out.skills.includes(s)) out.skills.push(s);
|
|
}
|
|
|
|
// renown normalization - allowed values: None, Respected, Distinguished, Renowned
|
|
const allowedRenown = ['None','Respected','Distinguished','Renowned'];
|
|
if (out.renown === '' || out.renown === null || out.renown === undefined) out.renown = 'None';
|
|
if (typeof out.renown === 'string') {
|
|
// normalize casing
|
|
const cand = out.renown.trim();
|
|
const found = allowedRenown.find(a => a.toLowerCase() === cand.toLowerCase());
|
|
out.renown = found || 'None';
|
|
} else {
|
|
out.renown = 'None';
|
|
}
|
|
|
|
// Clamp rp/xp ranges to reasonable bounds
|
|
out.rp = Math.max(0, Math.min(1000000, Number(out.rp || 0)));
|
|
out.xp = Math.max(0, Math.min(1000000, Number(out.xp || 0)));
|
|
out.xpSpent = Math.max(0, Math.min(out.xp, Number(out.xpSpent || 0)));
|
|
|
|
return out;
|
|
}
|
|
|
|
function validatePlayer(player) {
|
|
const errors = [];
|
|
const normalized = Object.assign({}, player);
|
|
|
|
// name
|
|
normalized.name = normalizeName(player.name);
|
|
if (!normalized.name) errors.push('name: required');
|
|
if (normalized.name.length > 100) errors.push('name: too long');
|
|
|
|
// roller/shop/tab must be objects
|
|
normalized.rollerInfo = isObject(player.rollerInfo) ? player.rollerInfo : {};
|
|
normalized.shopInfo = isObject(player.shopInfo) ? player.shopInfo : {};
|
|
|
|
// If caller provided flat tab-related fields at top-level (common from frontend),
|
|
// move them into tabInfo before normalization so defaults and validation apply.
|
|
const flatFields = [
|
|
'playerName','charName','gear','chapter','demeanour','speciality','rank','powerArmour',
|
|
'description','pastEvent','personalDemeanour','characteristics','skills','weapons','armour',
|
|
'talents','psychic','wounds','insanity','movement','fate','corruption','renown','xp','xpSpent',
|
|
'notes','rp','inventory'
|
|
];
|
|
|
|
const incomingTab = isObject(player.tabInfo) ? Object.assign({}, player.tabInfo) : {};
|
|
for (const k of flatFields) {
|
|
if (Object.prototype.hasOwnProperty.call(player, k) && incomingTab[k] === undefined) {
|
|
incomingTab[k] = player[k];
|
|
}
|
|
}
|
|
|
|
normalized.tabInfo = normalizeTabInfo(incomingTab);
|
|
|
|
// Validate characteristics structure
|
|
if (isObject(normalized.tabInfo.characteristics)) {
|
|
const chars = normalized.tabInfo.characteristics;
|
|
Object.keys(chars).forEach(k => {
|
|
const n = Number(chars[k]);
|
|
if (!Number.isFinite(n) || n < 0 || n > 100) {
|
|
errors.push(`characteristics.${k}: must be number 0-100`);
|
|
chars[k] = Math.max(0, Math.min(100, Number.isFinite(n) ? n : 0));
|
|
}
|
|
});
|
|
} else {
|
|
normalized.tabInfo.characteristics = {};
|
|
}
|
|
|
|
// Truncate very long free-text fields
|
|
if (typeof normalized.tabInfo.description === 'string' && normalized.tabInfo.description.length > 2000) {
|
|
normalized.tabInfo.description = normalized.tabInfo.description.substring(0,2000);
|
|
errors.push('description: truncated to 2000 chars');
|
|
}
|
|
|
|
// passwords: prefer pwHash; if plain pw present flag it (do not accept plaintext pw)
|
|
if (player.pw && player.pw.length > 0) errors.push('pw: plain password present; provide pwHash instead');
|
|
|
|
return { valid: errors.length === 0, errors, normalized };
|
|
}
|
|
|
|
module.exports = { validatePlayer, normalizeTabInfo };
|