Skip to main content
Glama
adapter.ts8.7 kB
/** * D&D 5e System Adapter * * Implements SystemAdapter interface for D&D 5th Edition support. * Handles creature indexing, filtering, formatting, and data extraction. */ import type { SystemAdapter, SystemMetadata, SystemCreatureIndex, DnD5eCreatureIndex } from '../types.js'; import { DnD5eFiltersSchema, matchesDnD5eFilters, describeDnD5eFilters, type DnD5eFilters } from './filters.js'; /** * D&D 5e system adapter */ export class DnD5eAdapter implements SystemAdapter { getMetadata(): SystemMetadata { return { id: 'dnd5e', name: 'dnd5e', displayName: 'Dungeons & Dragons 5th Edition', version: '1.0.0', description: 'Support for D&D 5e game system with Challenge Rating, creature types, and legendary actions', supportedFeatures: { creatureIndex: true, characterStats: true, spellcasting: true, powerLevel: true // Uses Challenge Rating } }; } canHandle(systemId: string): boolean { return systemId.toLowerCase() === 'dnd5e'; } /** * Extract creature data from Foundry document for indexing * This is called by the index builder in Foundry's browser context */ extractCreatureData(doc: any, pack: any): { creature: SystemCreatureIndex; errors: number } | null { // Implementation is in index-builder.ts since it runs in browser // This method is here for type compliance but delegates to IndexBuilder throw new Error('extractCreatureData should be called from DnD5eIndexBuilder, not the adapter'); } getFilterSchema() { return DnD5eFiltersSchema; } matchesFilters(creature: SystemCreatureIndex, filters: Record<string, any>): boolean { // Validate filters match D&D 5e schema const validated = DnD5eFiltersSchema.safeParse(filters); if (!validated.success) { return false; } return matchesDnD5eFilters(creature, validated.data as DnD5eFilters); } getDataPaths(): Record<string, string | null> { return { // D&D 5e specific paths challengeRating: 'system.details.cr', creatureType: 'system.details.type.value', size: 'system.traits.size', alignment: 'system.details.alignment', level: 'system.details.level.value', // For NPCs/characters hitPoints: 'system.attributes.hp', armorClass: 'system.attributes.ac.value', abilities: 'system.abilities', skills: 'system.skills', spells: 'system.spells', legendaryActions: 'system.resources.legact', legendaryResistances: 'system.resources.legres', // PF2e-specific paths don't exist in D&D 5e perception: null, saves: null, rarity: null }; } formatCreatureForList(creature: SystemCreatureIndex): any { const dnd5eCreature = creature as DnD5eCreatureIndex; const formatted: any = { id: creature.id, name: creature.name, type: creature.type, pack: { id: creature.packName, label: creature.packLabel } }; // Add D&D 5e specific stats if (dnd5eCreature.systemData) { const stats: any = {}; if (dnd5eCreature.systemData.challengeRating !== undefined) { stats.challengeRating = dnd5eCreature.systemData.challengeRating; } if (dnd5eCreature.systemData.creatureType) { stats.creatureType = dnd5eCreature.systemData.creatureType; } if (dnd5eCreature.systemData.size) { stats.size = dnd5eCreature.systemData.size; } if (dnd5eCreature.systemData.alignment) { stats.alignment = dnd5eCreature.systemData.alignment; } if (dnd5eCreature.systemData.hitPoints) { stats.hitPoints = dnd5eCreature.systemData.hitPoints; } if (dnd5eCreature.systemData.armorClass) { stats.armorClass = dnd5eCreature.systemData.armorClass; } if (dnd5eCreature.systemData.hasLegendaryActions) { stats.hasLegendaryActions = true; } if (dnd5eCreature.systemData.hasSpellcasting) { stats.spellcaster = true; } if (Object.keys(stats).length > 0) { formatted.stats = stats; } } if (creature.img) { formatted.hasImage = true; } return formatted; } formatCreatureForDetails(creature: SystemCreatureIndex): any { const dnd5eCreature = creature as DnD5eCreatureIndex; const formatted = this.formatCreatureForList(creature); // Add additional details if (dnd5eCreature.systemData) { formatted.detailedStats = { challengeRating: dnd5eCreature.systemData.challengeRating, creatureType: dnd5eCreature.systemData.creatureType, size: dnd5eCreature.systemData.size, alignment: dnd5eCreature.systemData.alignment, level: dnd5eCreature.systemData.level, hitPoints: dnd5eCreature.systemData.hitPoints, armorClass: dnd5eCreature.systemData.armorClass, hasSpellcasting: dnd5eCreature.systemData.hasSpellcasting, hasLegendaryActions: dnd5eCreature.systemData.hasLegendaryActions }; } if (creature.img) { formatted.img = creature.img; } return formatted; } describeFilters(filters: Record<string, any>): string { const validated = DnD5eFiltersSchema.safeParse(filters); if (!validated.success) { return 'invalid filters'; } return describeDnD5eFilters(validated.data as DnD5eFilters); } getPowerLevel(creature: SystemCreatureIndex): number | undefined { const dnd5eCreature = creature as DnD5eCreatureIndex; // D&D 5e: Try CR first, then character level if (dnd5eCreature.systemData?.challengeRating !== undefined) { return dnd5eCreature.systemData.challengeRating; } if (dnd5eCreature.systemData?.level !== undefined) { return dnd5eCreature.systemData.level; } return undefined; } /** * Extract character statistics from actor data */ extractCharacterStats(actorData: any): any { const system = actorData.system || {}; const stats: any = {}; // Basic info stats.name = actorData.name; stats.type = actorData.type; // Challenge Rating or Level const cr = system.details?.cr ?? system.details?.cr?.value ?? system.cr; if (cr !== undefined && cr !== null) { stats.challengeRating = Number(cr); } const level = system.details?.level?.value ?? system.details?.level ?? system.level; if (level !== undefined && level !== null) { stats.level = Number(level); } // Hit Points const hp = system.attributes?.hp; if (hp) { stats.hitPoints = { current: hp.value ?? 0, max: hp.max ?? 0, temp: hp.temp ?? 0 }; } // Armor Class const ac = system.attributes?.ac?.value ?? system.attributes?.ac; if (ac !== undefined) { stats.armorClass = ac; } // Abilities (STR, DEX, CON, INT, WIS, CHA) if (system.abilities) { stats.abilities = {}; for (const [key, ability] of Object.entries(system.abilities)) { const abilityData = ability as any; stats.abilities[key] = { value: abilityData.value ?? 10, modifier: abilityData.mod ?? 0 }; } } // Skills if (system.skills) { stats.skills = {}; for (const [key, skill] of Object.entries(system.skills)) { const skillData = skill as any; stats.skills[key] = { value: skillData.value ?? 0, modifier: skillData.total ?? skillData.mod ?? 0, proficient: skillData.proficient ?? 0 }; } } // Creature-specific info if (actorData.type === 'npc') { const creatureType = system.details?.type?.value ?? system.details?.type; if (creatureType) { stats.creatureType = creatureType; } const size = system.traits?.size?.value ?? system.traits?.size ?? system.size; if (size) { stats.size = size; } const alignment = system.details?.alignment?.value ?? system.details?.alignment; if (alignment) { stats.alignment = alignment; } // Legendary actions const legact = system.resources?.legact; if (legact) { stats.legendaryActions = { available: legact.value ?? 0, max: legact.max ?? 0 }; } } // Spellcasting const hasSpells = !!(system.spells || system.attributes?.spellcasting || (system.details?.spellLevel && system.details.spellLevel > 0)); if (hasSpells) { stats.spellcasting = { hasSpells: true, spellLevel: system.details?.spellLevel ?? 0 }; } return stats; } }

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