Skip to main content
Glama
index-builder.ts11 kB
/** * D&D 5e Index Builder * * Builds enhanced creature index from Foundry compendiums. * This code runs in Foundry's browser context, not Node.js. * * Extracted from data-access.ts for modular system support. */ import type { IndexBuilder, DnD5eCreatureIndex } from '../types.js'; // Foundry browser globals (unavailable in Node.js TypeScript compilation) declare const ui: any; /** * D&D 5e implementation of IndexBuilder */ export class DnD5eIndexBuilder implements IndexBuilder { private moduleId: string; constructor(moduleId: string = 'foundry-mcp-bridge') { this.moduleId = moduleId; } getSystemId() { return 'dnd5e' as const; } /** * Build enhanced creature index from compendium packs */ async buildIndex(packs: any[], force = false): Promise<DnD5eCreatureIndex[]> { const startTime = Date.now(); let progressNotification: any = null; let totalErrors = 0; try { const actorPacks = packs.filter(pack => pack.metadata.type === 'Actor'); const enhancedCreatures: DnD5eCreatureIndex[] = []; // Show initial progress notification console.log(`[${this.moduleId}] Starting D&D 5e creature index build from ${actorPacks.length} packs...`); if (typeof ui !== 'undefined' && ui.notifications) { ui.notifications.info(`Starting enhanced creature index build from ${actorPacks.length} packs...`); } for (let i = 0; i < actorPacks.length; i++) { const pack = actorPacks[i]; const progressPercent = Math.round((i / actorPacks.length) * 100); // Update progress notification if (i % 3 === 0 || pack.metadata.label.toLowerCase().includes('monster')) { if (progressNotification && typeof ui !== 'undefined') { progressNotification.remove(); } if (typeof ui !== 'undefined' && ui.notifications) { progressNotification = ui.notifications.info( `Building creature index... ${progressPercent}% (${i + 1}/${actorPacks.length}) Processing: ${pack.metadata.label}` ); } } try { // Ensure pack index is loaded if (!pack.indexed) { await pack.getIndex({}); } // Process creatures in this pack const packResult = await this.extractDataFromPack(pack); enhancedCreatures.push(...packResult.creatures); totalErrors += packResult.errors; // Show milestone notifications if (i === 0 || (i + 1) % 5 === 0 || i === actorPacks.length - 1) { const totalCreaturesSoFar = enhancedCreatures.length; if (progressNotification && typeof ui !== 'undefined') { progressNotification.remove(); } if (typeof ui !== 'undefined' && ui.notifications) { progressNotification = ui.notifications.info( `Index Progress: ${i + 1}/${actorPacks.length} packs complete, ${totalCreaturesSoFar} creatures indexed` ); } } } catch (error) { console.warn(`[${this.moduleId}] Failed to process pack ${pack.metadata.label}:`, error); if (typeof ui !== 'undefined' && ui.notifications) { ui.notifications.warn(`Warning: Failed to index pack "${pack.metadata.label}" - continuing with other packs`); } } } // Clear progress notification if (progressNotification && typeof ui !== 'undefined') { progressNotification.remove(); } const buildTimeSeconds = Math.round((Date.now() - startTime) / 1000); const errorText = totalErrors > 0 ? ` (${totalErrors} extraction errors)` : ''; const successMessage = `D&D 5e creature index complete! ${enhancedCreatures.length} creatures indexed from ${actorPacks.length} packs in ${buildTimeSeconds}s${errorText}`; console.log(`[${this.moduleId}] ${successMessage}`); if (typeof ui !== 'undefined' && ui.notifications) { ui.notifications.info(successMessage); } return enhancedCreatures; } catch (error) { if (progressNotification && typeof ui !== 'undefined') { progressNotification.remove(); } const errorMessage = `Failed to build D&D 5e creature index: ${error instanceof Error ? error.message : 'Unknown error'}`; console.error(`[${this.moduleId}] ${errorMessage}`); if (typeof ui !== 'undefined' && ui.notifications) { ui.notifications.error(errorMessage); } throw error; } } /** * Extract creature data from a single compendium pack */ async extractDataFromPack(pack: any): Promise<{ creatures: DnD5eCreatureIndex[]; errors: number }> { const creatures: DnD5eCreatureIndex[] = []; let errors = 0; try { // Load all documents from pack const documents = await pack.getDocuments(); for (const doc of documents) { try { // Only process NPCs and characters if (doc.type !== 'npc' && doc.type !== 'character') { continue; } const result = this.extractCreatureData(doc, pack); if (result) { creatures.push(result.creature); errors += result.errors; } } catch (error) { console.warn(`[${this.moduleId}] Failed to extract data from ${doc.name} in ${pack.metadata.label}:`, error); errors++; } } } catch (error) { console.warn(`[${this.moduleId}] Failed to load documents from ${pack.metadata.label}:`, error); errors++; } return { creatures, errors }; } /** * Extract D&D 5e creature data from a single document */ extractCreatureData(doc: any, pack: any): { creature: DnD5eCreatureIndex; errors: number } | null { try { const system = doc.system || {}; // Extract challenge rating with comprehensive fallbacks let challengeRating = system.details?.cr ?? system.details?.cr?.value ?? system.cr?.value ?? system.cr ?? system.attributes?.cr?.value ?? system.attributes?.cr ?? system.challenge?.rating ?? system.challenge?.cr ?? 0; // Handle null values if (challengeRating === null || challengeRating === undefined) { challengeRating = 0; } // Handle fractional CR strings if (typeof challengeRating === 'string') { if (challengeRating === '1/8') challengeRating = 0.125; else if (challengeRating === '1/4') challengeRating = 0.25; else if (challengeRating === '1/2') challengeRating = 0.5; else challengeRating = parseFloat(challengeRating) || 0; } challengeRating = Number(challengeRating) || 0; // Extract creature type let creatureType = system.details?.type?.value ?? system.details?.type ?? system.type?.value ?? system.type ?? system.race?.value ?? system.race ?? system.details?.race ?? 'unknown'; if (creatureType === null || creatureType === undefined || creatureType === '') { creatureType = 'unknown'; } if (typeof creatureType !== 'string') { creatureType = String(creatureType || 'unknown'); } // Extract size let size = system.traits?.size?.value || system.traits?.size || system.size?.value || system.size || system.details?.size || 'medium'; if (typeof size !== 'string') { size = String(size || 'medium'); } // Extract hit points const hitPoints = system.attributes?.hp?.max || system.hp?.max || system.attributes?.hp?.value || system.hp?.value || system.health?.max || system.health?.value || 0; // Extract armor class const armorClass = system.attributes?.ac?.value || system.ac?.value || system.attributes?.ac || system.ac || system.armor?.value || system.armor || 10; // Extract alignment let alignment = system.details?.alignment?.value || system.details?.alignment || system.alignment?.value || system.alignment || 'unaligned'; if (typeof alignment !== 'string') { alignment = String(alignment || 'unaligned'); } // Check for spellcasting const hasSpellcasting = !!(system.spells || system.attributes?.spellcasting || (system.details?.spellLevel && system.details.spellLevel > 0) || (system.resources?.spell && system.resources.spell.max > 0) || system.spellcasting || system.traits?.spellcasting || system.details?.spellcaster); // Check for legendary actions const hasLegendaryActions = !!(system.resources?.legact || system.legendary || (system.resources?.legres && system.resources.legres.value > 0) || system.details?.legendary || system.traits?.legendary || (system.resources?.legendary && system.resources.legendary.max > 0)); // Extract character level (for PCs) const level = system.details?.level?.value || system.details?.level || system.level || undefined; // Successful extraction return { creature: { id: doc._id, name: doc.name, type: doc.type, packName: pack.metadata.id, packLabel: pack.metadata.label, img: doc.img, system: 'dnd5e', systemData: { challengeRating, creatureType: creatureType.toLowerCase(), size: size.toLowerCase(), alignment: alignment.toLowerCase(), level, hasSpellcasting, hasLegendaryActions, hitPoints, armorClass } }, errors: 0 }; } catch (error) { console.warn(`[${this.moduleId}] Failed to extract D&D 5e data from ${doc.name}:`, error); // Return basic fallback with error count return { creature: { id: doc._id, name: doc.name, type: doc.type, packName: pack.metadata.id, packLabel: pack.metadata.label, img: doc.img || '', system: 'dnd5e', systemData: { challengeRating: 0, creatureType: 'unknown', size: 'medium', hitPoints: 1, armorClass: 10, hasSpellcasting: false, hasLegendaryActions: false, alignment: 'unaligned' } }, errors: 1 }; } } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/adambdooley/foundry-vtt-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server