- Added bestiaryRoutes.js to handle API endpoints for fetching enemies, full bestiary data, and statistics. - Implemented caching mechanism for bestiary data with a 5-minute expiration. - Created transformation functions for bestiary entries to standardize data format for the dice roller. - Added admin-only endpoint to force reload bestiary data with GM authentication. chore: Create scripts for analyzing and cleaning bestiary data - Developed analyze-bestiary-quality.js to review data quality and identify duplicates or inconsistencies. - Implemented clean-bestiary-fields.js to extract and clean specific fields from bestiary entries, reducing text duplication. - Created clean-corrupted-bestiary.js to remove corrupted entries based on defined patterns. - Generated a comprehensive database-cleanup-report.md summarizing the cleaning process and results. build: Add fast build script for streamlined deployment - Introduced fast-build.sh to facilitate quick builds and PM2 reloads without reinstalling dependencies. test: Add GM authentication logic test - Created test-gm-auth.js to verify the correctness of GM authentication logic with various test cases.
180 lines
5.4 KiB
JavaScript
180 lines
5.4 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
// Helper function to extract clean field data
|
|
function extractCleanData(text, fieldType) {
|
|
if (!text || typeof text !== 'string') return '';
|
|
|
|
// Remove the massive duplicated text by finding patterns
|
|
let cleaned = text;
|
|
|
|
// Find the actual useful data before the corruption starts
|
|
// Look for common patterns that indicate where the real data ends
|
|
const endMarkers = [
|
|
'. 361 X I I I : A d v e r s a r i e s',
|
|
'.Gear: Respirator, 3 reloads for autopistol. 361',
|
|
'Chaos "Every moment of anger',
|
|
'X I I I : A d v e r s a r i e s',
|
|
'361 X I I I',
|
|
'reality of the universe'
|
|
];
|
|
|
|
for (const marker of endMarkers) {
|
|
const index = cleaned.indexOf(marker);
|
|
if (index !== -1) {
|
|
cleaned = cleaned.substring(0, index).trim();
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Field-specific cleaning
|
|
switch (fieldType) {
|
|
case 'skills':
|
|
// Extract skills section
|
|
if (cleaned.includes('Skills: ')) {
|
|
const skillsMatch = cleaned.match(/Skills:\s*([^.]*(?:\([^)]+\)[^.]*)*)/);
|
|
if (skillsMatch) {
|
|
cleaned = skillsMatch[1].trim();
|
|
}
|
|
}
|
|
break;
|
|
|
|
case 'talents':
|
|
// Extract talents section
|
|
if (cleaned.includes('Talents: ')) {
|
|
const talentsMatch = cleaned.match(/Talents:\s*([^.]*(?:\([^)]+\)[^.]*)*)/);
|
|
if (talentsMatch) {
|
|
cleaned = talentsMatch[1].trim();
|
|
}
|
|
}
|
|
break;
|
|
|
|
case 'traits':
|
|
// Extract traits section
|
|
if (cleaned.includes('Traits: ')) {
|
|
const traitsMatch = cleaned.match(/Traits:\s*([^.]*(?:\([^)]+\)[^.]*)*)/);
|
|
if (traitsMatch) {
|
|
cleaned = traitsMatch[1].trim();
|
|
}
|
|
}
|
|
break;
|
|
|
|
case 'armour':
|
|
// Extract armour section
|
|
if (cleaned.includes('Armour: ')) {
|
|
const armourMatch = cleaned.match(/Armour:\s*([^.]*(?:\([^)]+\)[^.]*)*)/);
|
|
if (armourMatch) {
|
|
cleaned = armourMatch[1].trim();
|
|
}
|
|
}
|
|
break;
|
|
|
|
case 'weapons':
|
|
// Extract weapons section
|
|
if (cleaned.includes('Weapons: ')) {
|
|
const weaponsMatch = cleaned.match(/Weapons:\s*([^.]*(?:\([^)]+\)[^.]*)*)/);
|
|
if (weaponsMatch) {
|
|
cleaned = weaponsMatch[1].trim();
|
|
}
|
|
}
|
|
break;
|
|
|
|
case 'gear':
|
|
// Extract gear section
|
|
if (cleaned.includes('Gear: ')) {
|
|
const gearMatch = cleaned.match(/Gear:\s*([^.]*(?:\([^)]+\)[^.]*)*)/);
|
|
if (gearMatch) {
|
|
cleaned = gearMatch[1].trim();
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
|
|
// Remove any remaining corruption patterns
|
|
cleaned = cleaned.replace(/\.\s*Talents:.*$/s, '');
|
|
cleaned = cleaned.replace(/\.\s*Traits:.*$/s, '');
|
|
cleaned = cleaned.replace(/\.\s*Armour:.*$/s, '');
|
|
cleaned = cleaned.replace(/\.\s*Weapons:.*$/s, '');
|
|
cleaned = cleaned.replace(/\.\s*Gear:.*$/s, '');
|
|
|
|
// Trim and clean up extra whitespace
|
|
cleaned = cleaned.replace(/\s+/g, ' ').trim();
|
|
|
|
return cleaned;
|
|
}
|
|
|
|
async function cleanBestiaryFields() {
|
|
console.log('=== CLEANING BESTIARY FIELD DATA ===');
|
|
|
|
const bestiaryPath = path.join(__dirname, '../database/deathwatch-bestiary-extracted.json');
|
|
|
|
if (!fs.existsSync(bestiaryPath)) {
|
|
console.error('❌ Bestiary file not found:', bestiaryPath);
|
|
return;
|
|
}
|
|
|
|
// Create backup
|
|
const backupPath = `${bestiaryPath}.backup.${Date.now()}.json`;
|
|
fs.copyFileSync(bestiaryPath, backupPath);
|
|
console.log('✅ Backup created:', path.basename(backupPath));
|
|
|
|
// Load data
|
|
const data = JSON.parse(fs.readFileSync(bestiaryPath, 'utf8'));
|
|
console.log('📊 Total entries:', data.results.length);
|
|
|
|
let cleanedCount = 0;
|
|
let totalSavings = 0;
|
|
|
|
// Clean each entry
|
|
for (let i = 0; i < data.results.length; i++) {
|
|
const entry = data.results[i];
|
|
if (!entry.stats) continue;
|
|
|
|
const originalSize = JSON.stringify(entry.stats).length;
|
|
|
|
// Clean each problematic field
|
|
const fieldsToClean = ['skills', 'talents', 'traits', 'armour', 'weapons', 'gear'];
|
|
let wasChanged = false;
|
|
|
|
for (const field of fieldsToClean) {
|
|
if (entry.stats[field] && typeof entry.stats[field] === 'string' && entry.stats[field].length > 200) {
|
|
const original = entry.stats[field];
|
|
const cleaned = extractCleanData(original, field);
|
|
|
|
if (cleaned !== original) {
|
|
entry.stats[field] = cleaned;
|
|
wasChanged = true;
|
|
console.log(`🧹 Cleaned ${field} for ${entry.bestiaryName}: ${original.length} → ${cleaned.length} chars`);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (wasChanged) {
|
|
cleanedCount++;
|
|
const newSize = JSON.stringify(entry.stats).length;
|
|
totalSavings += originalSize - newSize;
|
|
}
|
|
}
|
|
|
|
// Save cleaned data
|
|
fs.writeFileSync(bestiaryPath, JSON.stringify(data, null, 2));
|
|
|
|
console.log('✅ Cleaned entries:', cleanedCount);
|
|
console.log('💾 Total size reduction:', Math.round(totalSavings / 1024), 'KB');
|
|
console.log('✅ Cleaned bestiary saved');
|
|
|
|
// Show final statistics
|
|
const finalSize = fs.statSync(bestiaryPath).size;
|
|
const originalSize = fs.statSync(backupPath).size;
|
|
console.log('📊 File size reduction:', Math.round((originalSize - finalSize) / 1024), 'KB');
|
|
console.log('📈 Compression ratio:', Math.round((1 - finalSize / originalSize) * 100) + '%');
|
|
}
|
|
|
|
if (require.main === module) {
|
|
cleanBestiaryFields().catch(console.error);
|
|
}
|
|
|
|
module.exports = { cleanBestiaryFields, extractCleanData };
|