Skip to main content
Glama
character.repo.ts12.1 kB
import Database from 'better-sqlite3'; import { Character, CharacterSchema, NPC, NPCSchema } from '../../schema/character.js'; import { CharacterType } from '../../schema/party.js'; export class CharacterRepository { constructor(private db: Database.Database) { } create(character: Character | NPC): void { // Determine if it's an NPC or Character for validation const isNPC = 'factionId' in character || 'behavior' in character; const validChar = isNPC ? NPCSchema.parse(character) : CharacterSchema.parse(character); const stmt = this.db.prepare(` INSERT INTO characters (id, name, stats, hp, max_hp, ac, level, faction_id, behavior, character_type, character_class, race, spell_slots, pact_magic_slots, known_spells, prepared_spells, cantrips_known, max_spell_level, concentrating_on, conditions, legendary_actions, legendary_actions_remaining, legendary_resistances, legendary_resistances_remaining, has_lair_actions, resistances, vulnerabilities, immunities, current_room_id, perception_bonus, stealth_bonus, created_at, updated_at) VALUES (@id, @name, @stats, @hp, @maxHp, @ac, @level, @factionId, @behavior, @characterType, @characterClass, @race, @spellSlots, @pactMagicSlots, @knownSpells, @preparedSpells, @cantripsKnown, @maxSpellLevel, @concentratingOn, @conditions, @legendaryActions, @legendaryActionsRemaining, @legendaryResistances, @legendaryResistancesRemaining, @hasLairActions, @resistances, @vulnerabilities, @immunities, @currentRoomId, @perceptionBonus, @stealthBonus, @createdAt, @updatedAt) `); stmt.run({ id: validChar.id, name: validChar.name, stats: JSON.stringify(validChar.stats), hp: validChar.hp, maxHp: validChar.maxHp, ac: validChar.ac, level: validChar.level, factionId: (validChar as NPC).factionId || null, behavior: (validChar as NPC).behavior || null, characterType: validChar.characterType || 'pc', // CRIT-002/006: Spellcasting fields characterClass: validChar.characterClass || 'fighter', race: validChar.race || 'Human', spellSlots: validChar.spellSlots ? JSON.stringify(validChar.spellSlots) : null, pactMagicSlots: validChar.pactMagicSlots ? JSON.stringify(validChar.pactMagicSlots) : null, knownSpells: JSON.stringify(validChar.knownSpells || []), preparedSpells: JSON.stringify(validChar.preparedSpells || []), cantripsKnown: JSON.stringify(validChar.cantripsKnown || []), maxSpellLevel: validChar.maxSpellLevel || 0, concentratingOn: validChar.concentratingOn || null, conditions: JSON.stringify(validChar.conditions || []), // HIGH-007: Legendary creature fields legendaryActions: validChar.legendaryActions ?? null, legendaryActionsRemaining: validChar.legendaryActionsRemaining ?? null, legendaryResistances: validChar.legendaryResistances ?? null, legendaryResistancesRemaining: validChar.legendaryResistancesRemaining ?? null, hasLairActions: validChar.hasLairActions ? 1 : 0, resistances: JSON.stringify(validChar.resistances || []), vulnerabilities: JSON.stringify(validChar.vulnerabilities || []), immunities: JSON.stringify(validChar.immunities || []), // PHASE-1: Spatial awareness currentRoomId: validChar.currentRoomId || null, // PHASE-2: Social hearing mechanics skill bonuses perceptionBonus: validChar.perceptionBonus || 0, stealthBonus: validChar.stealthBonus || 0, createdAt: validChar.createdAt, updatedAt: validChar.updatedAt, }); } findById(id: string): Character | NPC | null { const stmt = this.db.prepare('SELECT * FROM characters WHERE id = ?'); const row = stmt.get(id) as CharacterRow | undefined; if (!row) return null; return this.rowToCharacter(row); } findAll(filters?: { characterType?: CharacterType }): (Character | NPC)[] { let query = 'SELECT * FROM characters'; const params: any[] = []; if (filters?.characterType) { query += ' WHERE character_type = ?'; params.push(filters.characterType); } const stmt = this.db.prepare(query); const rows = stmt.all(...params) as CharacterRow[]; return rows.map(row => this.rowToCharacter(row)); } findByType(characterType: CharacterType): (Character | NPC)[] { const stmt = this.db.prepare('SELECT * FROM characters WHERE character_type = ?'); const rows = stmt.all(characterType) as CharacterRow[]; return rows.map(row => this.rowToCharacter(row)); } update(id: string, updates: Partial<Character | NPC>): Character | NPC | null { const existing = this.findById(id); if (!existing) return null; const updated = { ...existing, ...updates, updatedAt: new Date().toISOString() }; // Validate const isNPC = 'factionId' in updated || 'behavior' in updated; const validChar = isNPC ? NPCSchema.parse(updated) : CharacterSchema.parse(updated); const stmt = this.db.prepare(` UPDATE characters SET name = ?, stats = ?, hp = ?, max_hp = ?, ac = ?, level = ?, faction_id = ?, behavior = ?, character_type = ?, character_class = ?, race = ?, spell_slots = ?, pact_magic_slots = ?, known_spells = ?, prepared_spells = ?, cantrips_known = ?, max_spell_level = ?, concentrating_on = ?, conditions = ?, legendary_actions = ?, legendary_actions_remaining = ?, legendary_resistances = ?, legendary_resistances_remaining = ?, has_lair_actions = ?, resistances = ?, vulnerabilities = ?, immunities = ?, current_room_id = ?, perception_bonus = ?, stealth_bonus = ?, updated_at = ? WHERE id = ? `); stmt.run( validChar.name, JSON.stringify(validChar.stats), validChar.hp, validChar.maxHp, validChar.ac, validChar.level, (validChar as NPC).factionId || null, (validChar as NPC).behavior || null, validChar.characterType || 'pc', // CRIT-002/006: Spellcasting fields validChar.characterClass || 'fighter', validChar.race || 'Human', validChar.spellSlots ? JSON.stringify(validChar.spellSlots) : null, validChar.pactMagicSlots ? JSON.stringify(validChar.pactMagicSlots) : null, JSON.stringify(validChar.knownSpells || []), JSON.stringify(validChar.preparedSpells || []), JSON.stringify(validChar.cantripsKnown || []), validChar.maxSpellLevel || 0, validChar.concentratingOn || null, JSON.stringify(validChar.conditions || []), // HIGH-007: Legendary creature fields validChar.legendaryActions ?? null, validChar.legendaryActionsRemaining ?? null, validChar.legendaryResistances ?? null, validChar.legendaryResistancesRemaining ?? null, validChar.hasLairActions ? 1 : 0, JSON.stringify(validChar.resistances || []), JSON.stringify(validChar.vulnerabilities || []), JSON.stringify(validChar.immunities || []), // PHASE-1: Spatial awareness validChar.currentRoomId || null, // PHASE-2: Social hearing mechanics skill bonuses validChar.perceptionBonus || 0, validChar.stealthBonus || 0, validChar.updatedAt, id ); return validChar; } delete(id: string): boolean { const stmt = this.db.prepare('DELETE FROM characters WHERE id = ?'); const result = stmt.run(id); return result.changes > 0; } private rowToCharacter(row: CharacterRow): Character | NPC { const base = { id: row.id, name: row.name, stats: JSON.parse(row.stats), hp: row.hp, maxHp: row.max_hp, ac: row.ac, level: row.level, characterType: (row.character_type as CharacterType) || 'pc', // CRIT-002/006: Spellcasting fields characterClass: row.character_class || 'fighter', race: row.race || 'Human', spellSlots: row.spell_slots ? JSON.parse(row.spell_slots) : undefined, pactMagicSlots: row.pact_magic_slots ? JSON.parse(row.pact_magic_slots) : undefined, knownSpells: row.known_spells ? JSON.parse(row.known_spells) : [], preparedSpells: row.prepared_spells ? JSON.parse(row.prepared_spells) : [], cantripsKnown: row.cantrips_known ? JSON.parse(row.cantrips_known) : [], maxSpellLevel: row.max_spell_level || 0, concentratingOn: row.concentrating_on || null, conditions: row.conditions ? JSON.parse(row.conditions) : [], // HIGH-007: Legendary creature fields legendaryActions: row.legendary_actions ?? undefined, legendaryActionsRemaining: row.legendary_actions_remaining ?? undefined, legendaryResistances: row.legendary_resistances ?? undefined, legendaryResistancesRemaining: row.legendary_resistances_remaining ?? undefined, hasLairActions: row.has_lair_actions === 1, resistances: row.resistances ? JSON.parse(row.resistances) : [], vulnerabilities: row.vulnerabilities ? JSON.parse(row.vulnerabilities) : [], immunities: row.immunities ? JSON.parse(row.immunities) : [], // PHASE-1: Spatial awareness currentRoomId: row.current_room_id || undefined, // PHASE-2: Social hearing mechanics skill bonuses perceptionBonus: row.perception_bonus ?? 0, stealthBonus: row.stealth_bonus ?? 0, createdAt: row.created_at, updatedAt: row.updated_at, }; if (row.faction_id || row.behavior) { return NPCSchema.parse({ ...base, factionId: row.faction_id || undefined, behavior: row.behavior || undefined, }); } return CharacterSchema.parse(base); } } interface CharacterRow { id: string; name: string; stats: string; hp: number; max_hp: number; ac: number; level: number; faction_id: string | null; behavior: string | null; character_type: string | null; // CRIT-002/006: Spellcasting columns character_class: string | null; race: string | null; spell_slots: string | null; pact_magic_slots: string | null; known_spells: string | null; prepared_spells: string | null; cantrips_known: string | null; max_spell_level: number | null; concentrating_on: string | null; conditions: string | null; // HIGH-007: Legendary creature columns legendary_actions: number | null; legendary_actions_remaining: number | null; legendary_resistances: number | null; legendary_resistances_remaining: number | null; has_lair_actions: number | null; resistances: string | null; vulnerabilities: string | null; immunities: string | null; // PHASE-1: Spatial awareness current_room_id: string | null; // PHASE-2: Social hearing mechanics skill bonuses perception_bonus: number | null; stealth_bonus: number | null; created_at: string; updated_at: string; }

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