Skip to main content
Glama
adapter.ts9.59 kB
/** * Pathfinder 2e System Adapter * * Implements SystemAdapter interface for Pathfinder 2nd Edition support. * Handles creature indexing, filtering, formatting, and data extraction. */ import type { SystemAdapter, SystemMetadata, SystemCreatureIndex, PF2eCreatureIndex } from '../types.js'; import { PF2eFiltersSchema, matchesPF2eFilters, describePF2eFilters, type PF2eFilters } from './filters.js'; /** * Pathfinder 2e system adapter */ export class PF2eAdapter implements SystemAdapter { getMetadata(): SystemMetadata { return { id: 'pf2e', name: 'pf2e', displayName: 'Pathfinder 2nd Edition', version: '1.0.0', description: 'Support for PF2e game system with Level, traits, rarity, and spellcasting entries', supportedFeatures: { creatureIndex: true, characterStats: true, spellcasting: true, powerLevel: true // Uses Level } }; } canHandle(systemId: string): boolean { return systemId.toLowerCase() === 'pf2e'; } /** * 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 PF2eIndexBuilder, not the adapter'); } getFilterSchema() { return PF2eFiltersSchema; } matchesFilters(creature: SystemCreatureIndex, filters: Record<string, any>): boolean { // Validate filters match PF2e schema const validated = PF2eFiltersSchema.safeParse(filters); if (!validated.success) { return false; } return matchesPF2eFilters(creature, validated.data as PF2eFilters); } getDataPaths(): Record<string, string | null> { return { // Pathfinder 2e specific paths level: 'system.details.level.value', creatureType: 'system.traits.value', // Array of traits size: 'system.traits.size.value', alignment: 'system.details.alignment.value', rarity: 'system.traits.rarity', traits: 'system.traits.value', // All traits as array hitPoints: 'system.attributes.hp', armorClass: 'system.attributes.ac.value', abilities: 'system.abilities', skills: 'system.skills', perception: 'system.perception', saves: 'system.saves', // PF2e doesn't have CR or legendary actions challengeRating: null, legendaryActions: null, legendaryResistances: null, spells: null // PF2e uses spellcasting entries instead }; } formatCreatureForList(creature: SystemCreatureIndex): any { const pf2eCreature = creature as PF2eCreatureIndex; const formatted: any = { id: creature.id, name: creature.name, type: creature.type, pack: { id: creature.packName, label: creature.packLabel } }; // Add PF2e specific stats if (pf2eCreature.systemData) { const stats: any = {}; if (pf2eCreature.systemData.level !== undefined) { stats.level = pf2eCreature.systemData.level; } if (pf2eCreature.systemData.traits && pf2eCreature.systemData.traits.length > 0) { stats.traits = pf2eCreature.systemData.traits; // Extract primary creature type from traits const creatureTraits = ['aberration', 'animal', 'beast', 'celestial', 'construct', 'dragon', 'elemental', 'fey', 'fiend', 'fungus', 'humanoid', 'monitor', 'ooze', 'plant', 'undead']; const primaryType = pf2eCreature.systemData.traits.find((t: string) => creatureTraits.includes(t.toLowerCase()) ); if (primaryType) stats.creatureType = primaryType; } if (pf2eCreature.systemData.rarity) { stats.rarity = pf2eCreature.systemData.rarity; } if (pf2eCreature.systemData.size) { stats.size = pf2eCreature.systemData.size; } if (pf2eCreature.systemData.alignment) { stats.alignment = pf2eCreature.systemData.alignment; } if (pf2eCreature.systemData.hitPoints) { stats.hitPoints = pf2eCreature.systemData.hitPoints; } if (pf2eCreature.systemData.armorClass) { stats.armorClass = pf2eCreature.systemData.armorClass; } if (pf2eCreature.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 pf2eCreature = creature as PF2eCreatureIndex; const formatted = this.formatCreatureForList(creature); // Add additional details if (pf2eCreature.systemData) { formatted.detailedStats = { level: pf2eCreature.systemData.level, traits: pf2eCreature.systemData.traits, size: pf2eCreature.systemData.size, alignment: pf2eCreature.systemData.alignment, rarity: pf2eCreature.systemData.rarity, hitPoints: pf2eCreature.systemData.hitPoints, armorClass: pf2eCreature.systemData.armorClass, hasSpellcasting: pf2eCreature.systemData.hasSpellcasting }; } if (creature.img) { formatted.img = creature.img; } return formatted; } describeFilters(filters: Record<string, any>): string { const validated = PF2eFiltersSchema.safeParse(filters); if (!validated.success) { return 'invalid filters'; } return describePF2eFilters(validated.data as PF2eFilters); } getPowerLevel(creature: SystemCreatureIndex): number | undefined { const pf2eCreature = creature as PF2eCreatureIndex; // PF2e: Level is the primary metric if (pf2eCreature.systemData?.level !== undefined) { return pf2eCreature.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; // Level 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 ?? abilityData.mod ?? 0, 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] = { modifier: skillData.value ?? skillData.mod ?? 0, rank: skillData.rank ?? 0, proficient: (skillData.rank ?? 0) > 0 }; } } // Perception if (system.perception) { stats.perception = { modifier: system.perception.value ?? system.perception.mod ?? 0, rank: system.perception.rank ?? 0 }; } // Saves if (system.saves) { stats.saves = {}; for (const [key, save] of Object.entries(system.saves)) { const saveData = save as any; stats.saves[key] = { modifier: saveData.value ?? saveData.mod ?? 0, rank: saveData.rank ?? 0 }; } } // Creature-specific info if (actorData.type === 'npc') { const traits = system.traits?.value || []; if (Array.isArray(traits) && traits.length > 0) { stats.traits = traits; // Extract primary creature type const creatureTraits = ['aberration', 'animal', 'beast', 'celestial', 'construct', 'dragon', 'elemental', 'fey', 'fiend', 'fungus', 'humanoid', 'monitor', 'ooze', 'plant', 'undead']; const primaryType = traits.find((t: string) => creatureTraits.includes(t.toLowerCase())); if (primaryType) { stats.creatureType = primaryType; } } const size = system.traits?.size?.value ?? system.traits?.size; if (size) { stats.size = size; } const alignment = system.details?.alignment?.value ?? system.details?.alignment; if (alignment) { stats.alignment = alignment; } const rarity = system.traits?.rarity; if (rarity) { stats.rarity = rarity; } } // Spellcasting const spellcasting = system.spellcasting || {}; const hasSpells = Object.keys(spellcasting).length > 0; if (hasSpells) { stats.spellcasting = { hasSpells: true, entries: Object.keys(spellcasting).length }; } 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