Skip to main content
Glama
index.ts16.5 kB
/** * Item Preset Registry * * Aggregates all item preset sources into a unified, searchable database. * Easy to extend - just import new sources and add to SOURCES array. * * Usage: * getItemPreset('longsword') -> WeaponPreset * getItemPreset('+1 longsword') -> MagicItemPreset * searchItems({ type: 'weapon', properties: ['finesse'] }) * listItemsByType('armor') */ import type { ItemPreset, PresetSource, ItemSearchCriteria, SearchResult, ItemType, WeaponPreset, ArmorPreset, GearPreset, MagicItemPreset } from './types.js'; // Import all preset sources import { WeaponsPHB } from './weapons-phb.js'; import { ArmorPHB } from './armor-phb.js'; import { gearPhb } from './gear-phb.js'; import { magicCommonSource } from './magic-common.js'; // Re-export types for convenience export * from './types.js'; // ═══════════════════════════════════════════════════════════════════════════ // SOURCE REGISTRY // ═══════════════════════════════════════════════════════════════════════════ /** * All registered preset sources. * To add a new source, import it and add to this array. */ const SOURCES: PresetSource[] = [ WeaponsPHB, ArmorPHB, gearPhb, magicCommonSource, // Future sources: // WeaponsXGE, // MagicItemsRare, // GearTCE, ]; // ═══════════════════════════════════════════════════════════════════════════ // UNIFIED PRESET DATABASE // ═══════════════════════════════════════════════════════════════════════════ /** Unified map of all presets from all sources */ const ALL_PRESETS: Map<string, ItemPreset> = new Map(); /** Index by item type for fast filtering */ const TYPE_INDEX: Map<ItemType, Set<string>> = new Map(); /** Index by tags for semantic search */ const TAG_INDEX: Map<string, Set<string>> = new Map(); /** Track which source each preset came from */ const SOURCE_MAP: Map<string, string> = new Map(); /** * Initialize the registry by loading all sources */ function initializeRegistry(): void { if (ALL_PRESETS.size > 0) return; // Already initialized for (const source of SOURCES) { for (const [key, preset] of Object.entries(source.presets)) { const normalizedKey = normalizeKey(key); // Store preset ALL_PRESETS.set(normalizedKey, preset); SOURCE_MAP.set(normalizedKey, source.id); // Index by type const typeSet = TYPE_INDEX.get(preset.type as ItemType) || new Set(); typeSet.add(normalizedKey); TYPE_INDEX.set(preset.type as ItemType, typeSet); // Index by tags const tags = (preset as any).tags || []; for (const tag of tags) { const tagSet = TAG_INDEX.get(tag.toLowerCase()) || new Set(); tagSet.add(normalizedKey); TAG_INDEX.set(tag.toLowerCase(), tagSet); } } } } // ═══════════════════════════════════════════════════════════════════════════ // KEY NORMALIZATION // ═══════════════════════════════════════════════════════════════════════════ /** * Normalize an item name for lookup. * "Longsword" -> "longsword" * "+1 Longsword" -> "+1_longsword" * "Studded Leather" -> "studded_leather" */ export function normalizeKey(name: string): string { return name .toLowerCase() .trim() .replace(/['']/g, '') // Remove apostrophes .replace(/\s+/g, '_') // Spaces to underscores .replace(/[^a-z0-9_+\-]/g, ''); // Keep only alphanumeric, _, +, - } /** * Generate aliases for common variations */ function generateAliases(key: string): string[] { const aliases: string[] = [key]; // "studded_leather" -> "studded_leather_armor" if (!key.includes('armor') && !key.includes('weapon') && !key.includes('shield')) { aliases.push(`${key}_armor`); aliases.push(`${key}_weapon`); } // "+1_longsword" -> "plus_1_longsword", "longsword_+1" if (key.startsWith('+')) { aliases.push(key.replace('+', 'plus_')); const match = key.match(/^\+(\d+)_(.+)$/); if (match) { aliases.push(`${match[2]}_+${match[1]}`); aliases.push(`${match[2]}_plus_${match[1]}`); } } return aliases; } // ═══════════════════════════════════════════════════════════════════════════ // LOOKUP FUNCTIONS // ═══════════════════════════════════════════════════════════════════════════ /** * Get an item preset by name (case-insensitive, flexible matching) */ export function getItemPreset(name: string): ItemPreset | null { initializeRegistry(); const normalized = normalizeKey(name); // Direct lookup if (ALL_PRESETS.has(normalized)) { return ALL_PRESETS.get(normalized)!; } // Try aliases for (const alias of generateAliases(normalized)) { if (ALL_PRESETS.has(alias)) { return ALL_PRESETS.get(alias)!; } } // Fuzzy match - find closest const fuzzyMatch = findFuzzyMatch(normalized); if (fuzzyMatch) { return ALL_PRESETS.get(fuzzyMatch)!; } return null; } /** * Get a weapon preset by name */ export function getWeaponPreset(name: string): WeaponPreset | null { const preset = getItemPreset(name); return preset?.type === 'weapon' ? preset as WeaponPreset : null; } /** * Get an armor preset by name */ export function getArmorPreset(name: string): ArmorPreset | null { const preset = getItemPreset(name); return preset?.type === 'armor' ? preset as ArmorPreset : null; } /** * Get a gear preset by name */ export function getGearPreset(name: string): GearPreset | null { const preset = getItemPreset(name); return (preset?.type === 'gear' || preset?.type === 'tool' || preset?.type === 'consumable') ? preset as GearPreset : null; } /** * Get a magic item preset by name */ export function getMagicItemPreset(name: string): MagicItemPreset | null { const preset = getItemPreset(name); return preset?.type === 'magic' ? preset as MagicItemPreset : null; } /** * Check if a preset exists */ export function hasItemPreset(name: string): boolean { return getItemPreset(name) !== null; } // ═══════════════════════════════════════════════════════════════════════════ // SEARCH & FILTER FUNCTIONS // ═══════════════════════════════════════════════════════════════════════════ /** * Search items with flexible criteria */ export function searchItems(criteria: ItemSearchCriteria): SearchResult[] { initializeRegistry(); const results: SearchResult[] = []; let candidateKeys: Set<string> | null = null; // Use type index if filtering by type if (criteria.type) { const types = Array.isArray(criteria.type) ? criteria.type : [criteria.type]; candidateKeys = new Set(); for (const type of types) { const typeKeys = TYPE_INDEX.get(type); if (typeKeys) { Array.from(typeKeys).forEach(key => candidateKeys!.add(key)); } } } // Use tag index if filtering by tags if (criteria.tags && criteria.tags.length > 0) { const tagCandidates = new Set<string>(); for (const tag of criteria.tags) { const tagKeys = TAG_INDEX.get(tag.toLowerCase()); if (tagKeys) { Array.from(tagKeys).forEach(key => tagCandidates.add(key)); } } if (candidateKeys) { // Intersect with type candidates candidateKeys = new Set(Array.from(candidateKeys).filter(k => tagCandidates.has(k))); } else { candidateKeys = tagCandidates; } } // Default to all items if no index filters const keysToSearch = candidateKeys ? Array.from(candidateKeys) : Array.from(ALL_PRESETS.keys()); for (const key of keysToSearch) { const preset = ALL_PRESETS.get(key)!; const matchResult = matchesCriteria(key, preset, criteria); if (matchResult.matches) { results.push({ key, preset, score: matchResult.score, matchedFields: matchResult.matchedFields }); } } // Sort by score descending results.sort((a, b) => b.score - a.score); return results; } /** * List all items of a specific type */ export function listItemsByType(type: ItemType): ItemPreset[] { initializeRegistry(); const keys = TYPE_INDEX.get(type); if (!keys) return []; return Array.from(keys).map(key => ALL_PRESETS.get(key)!); } /** * List all items with a specific tag */ export function listItemsByTag(tag: string): ItemPreset[] { initializeRegistry(); const keys = TAG_INDEX.get(tag.toLowerCase()); if (!keys) return []; return Array.from(keys).map(key => ALL_PRESETS.get(key)!); } /** * List all available item keys */ export function listAllItemKeys(): string[] { initializeRegistry(); return Array.from(ALL_PRESETS.keys()); } /** * List all available tags */ export function listAllTags(): string[] { initializeRegistry(); return Array.from(TAG_INDEX.keys()).sort(); } /** * Get statistics about the registry */ export function getRegistryStats(): { totalItems: number; byType: Record<string, number>; sources: string[]; } { initializeRegistry(); const byType: Record<string, number> = {}; Array.from(TYPE_INDEX.entries()).forEach(([type, keys]) => { byType[type] = keys.size; }); return { totalItems: ALL_PRESETS.size, byType, sources: SOURCES.map(s => s.id) }; } // ═══════════════════════════════════════════════════════════════════════════ // HELPER FUNCTIONS // ═══════════════════════════════════════════════════════════════════════════ /** * Find the closest fuzzy match for a key */ function findFuzzyMatch(searchKey: string): string | null { let bestMatch: string | null = null; let bestScore = 0; Array.from(ALL_PRESETS.keys()).forEach(key => { const score = fuzzyScore(searchKey, key); if (score > bestScore && score > 0.6) { // Threshold bestScore = score; bestMatch = key; } }); return bestMatch; } /** * Calculate fuzzy match score between two strings (0-1) */ function fuzzyScore(search: string, target: string): number { if (search === target) return 1; if (target.includes(search)) return 0.9; if (search.includes(target)) return 0.8; // Levenshtein-like scoring const searchParts = search.split('_'); const targetParts = target.split('_'); let matchedParts = 0; for (const sp of searchParts) { if (targetParts.some(tp => tp.includes(sp) || sp.includes(tp))) { matchedParts++; } } return matchedParts / Math.max(searchParts.length, targetParts.length); } /** * Check if a preset matches search criteria */ function matchesCriteria( key: string, preset: ItemPreset, criteria: ItemSearchCriteria ): { matches: boolean; score: number; matchedFields: string[] } { const matchedFields: string[] = []; let score = 1; // Text query search if (criteria.query) { const query = criteria.query.toLowerCase(); const name = preset.name.toLowerCase(); const description = ((preset as any).description || '').toLowerCase(); if (name.includes(query)) { matchedFields.push('name'); score += 0.5; } else if (description.includes(query)) { matchedFields.push('description'); score += 0.3; } else if (key.includes(normalizeKey(query))) { matchedFields.push('key'); score += 0.2; } else { return { matches: false, score: 0, matchedFields: [] }; } } // Category filter (for weapons/armor) if (criteria.category) { const category = (preset as any).category; if (category !== criteria.category) { return { matches: false, score: 0, matchedFields: [] }; } matchedFields.push('category'); } // Rarity filter (for magic items) if (criteria.rarity) { const rarity = (preset as any).rarity; const rarities = Array.isArray(criteria.rarity) ? criteria.rarity : [criteria.rarity]; if (!rarities.includes(rarity)) { return { matches: false, score: 0, matchedFields: [] }; } matchedFields.push('rarity'); } // Value filters if (criteria.minValue !== undefined) { const value = (preset as any).value || 0; if (value < criteria.minValue) { return { matches: false, score: 0, matchedFields: [] }; } } if (criteria.maxValue !== undefined) { const value = (preset as any).value || 0; if (value > criteria.maxValue) { return { matches: false, score: 0, matchedFields: [] }; } } // Weight filters if (criteria.minWeight !== undefined) { const weight = (preset as any).weight || 0; if (weight < criteria.minWeight) { return { matches: false, score: 0, matchedFields: [] }; } } if (criteria.maxWeight !== undefined) { const weight = (preset as any).weight || 0; if (weight > criteria.maxWeight) { return { matches: false, score: 0, matchedFields: [] }; } } // Properties filter (must have ALL) if (criteria.properties && criteria.properties.length > 0) { const itemProps = (preset as any).properties || []; const hasAll = criteria.properties.every(p => itemProps.includes(p) || itemProps.includes(p.toLowerCase()) ); if (!hasAll) { return { matches: false, score: 0, matchedFields: [] }; } matchedFields.push('properties'); } // Source filter if (criteria.source) { const source = (preset as any).source; if (source !== criteria.source) { return { matches: false, score: 0, matchedFields: [] }; } matchedFields.push('source'); } // Attunement filter if (criteria.requiresAttunement !== undefined) { const attunement = (preset as any).requiresAttunement; const requiresIt = attunement === true || typeof attunement === 'string'; if (requiresIt !== criteria.requiresAttunement) { return { matches: false, score: 0, matchedFields: [] }; } matchedFields.push('attunement'); } return { matches: true, score, matchedFields }; } // Initialize on first import initializeRegistry();

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/Mnehmos/rpg-mcp'

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