Skip to main content
Glama
aura.test.ts30.7 kB
/** * Tests for Aura System * Covers aura creation, radius checks, target filtering, effect application, and integration */ import { describe, it, expect, beforeEach } from 'vitest'; import { getDb } from '../../src/storage/index.js'; import { AuraRepository } from '../../src/storage/repos/aura.repo.js'; import { CharacterRepository } from '../../src/storage/repos/character.repo.js'; import { ConcentrationRepository } from '../../src/storage/repos/concentration.repo.js'; import { createAura, endAura, endAurasByOwner, getActiveAuras, isInAuraRange, calculateDistance, shouldAffectTarget, checkAuraEffectsForTarget, checkAuraDuration, expireOldAuras, applyAuraEffect, } from '../../src/engine/magic/aura.js'; import { startConcentration } from '../../src/engine/magic/concentration.js'; import { AuraState, AuraEffect, CreateAuraRequest } from '../../src/schema/aura.js'; import { Token, Position } from '../../src/schema/encounter.js'; import { Character } from '../../src/schema/character.js'; describe('Aura System', () => { let db: any; let auraRepo: AuraRepository; let characterRepo: CharacterRepository; let concentrationRepo: ConcentrationRepository; beforeEach(() => { db = getDb(':memory:'); auraRepo = new AuraRepository(db); characterRepo = new CharacterRepository(db); concentrationRepo = new ConcentrationRepository(db); // Clean up any existing auras from previous tests const existingAuras = auraRepo.findAll(); for (const aura of existingAuras) { auraRepo.delete(aura.id); } }); describe('Distance Calculations', () => { it('should calculate distance between positions in feet', () => { const pos1: Position = { x: 0, y: 0 }; const pos2: Position = { x: 3, y: 4 }; // 5 squares away const distance = calculateDistance(pos1, pos2); expect(distance).toBe(25); // 5 squares * 5 feet = 25 feet }); it('should calculate distance for adjacent squares', () => { const pos1: Position = { x: 0, y: 0 }; const pos2: Position = { x: 1, y: 0 }; // 1 square away const distance = calculateDistance(pos1, pos2); expect(distance).toBe(5); // 1 square * 5 feet = 5 feet }); it('should return 0 for same position', () => { const pos1: Position = { x: 5, y: 5 }; const pos2: Position = { x: 5, y: 5 }; const distance = calculateDistance(pos1, pos2); expect(distance).toBe(0); }); }); describe('Aura Range Checks', () => { it('should correctly identify positions within aura radius', () => { const center: Position = { x: 10, y: 10 }; const target: Position = { x: 12, y: 10 }; // 2 squares = 10 feet away const radius = 15; // 15 feet const inRange = isInAuraRange(center, target, radius); expect(inRange).toBe(true); }); it('should correctly identify positions outside aura radius', () => { const center: Position = { x: 10, y: 10 }; const target: Position = { x: 14, y: 10 }; // 4 squares = 20 feet away const radius = 15; // 15 feet const inRange = isInAuraRange(center, target, radius); expect(inRange).toBe(false); }); it('should include positions exactly at radius edge', () => { const center: Position = { x: 0, y: 0 }; const target: Position = { x: 3, y: 0 }; // 3 squares = 15 feet away const radius = 15; // 15 feet const inRange = isInAuraRange(center, target, radius); expect(inRange).toBe(true); }); }); describe('Aura Creation and Tracking', () => { it('should create a new aura successfully', () => { // Create a character first const character: Character = { id: 'cleric-1', name: 'Cleric', stats: { str: 10, dex: 10, con: 14, int: 10, wis: 16, cha: 10 }, hp: 30, maxHp: 30, ac: 16, level: 5, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), }; characterRepo.create(character); const request: CreateAuraRequest = { ownerId: 'cleric-1', spellName: 'Spirit Guardians', spellLevel: 3, radius: 15, affectsAllies: false, affectsEnemies: true, affectsSelf: false, effects: [ { trigger: 'start_of_turn', type: 'damage', dice: '3d8', damageType: 'radiant', saveType: 'wisdom', saveDC: 14, }, ], currentRound: 1, maxDuration: 100, requiresConcentration: true, }; const aura = createAura(request, auraRepo); expect(aura.id).toBeDefined(); expect(aura.ownerId).toBe('cleric-1'); expect(aura.spellName).toBe('Spirit Guardians'); expect(aura.radius).toBe(15); expect(aura.affectsEnemies).toBe(true); expect(aura.requiresConcentration).toBe(true); }); it('should retrieve active auras', () => { const character: Character = { id: 'paladin-1', name: 'Paladin', stats: { str: 16, dex: 10, con: 14, int: 10, wis: 12, cha: 14 }, hp: 40, maxHp: 40, ac: 18, level: 6, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), }; characterRepo.create(character); const request: CreateAuraRequest = { ownerId: 'paladin-1', spellName: 'Aura of Protection', spellLevel: 0, radius: 10, affectsAllies: true, affectsEnemies: false, affectsSelf: true, effects: [ { trigger: 'start_of_turn', type: 'buff', bonusAmount: 2, bonusType: 'saves', description: '+2 to all saving throws', }, ], currentRound: 1, requiresConcentration: false, }; createAura(request, auraRepo); const activeAuras = getActiveAuras(auraRepo); expect(activeAuras).toHaveLength(1); expect(activeAuras[0].spellName).toBe('Aura of Protection'); }); it('should end an aura by ID', () => { const character: Character = { id: 'wizard-1', name: 'Wizard', stats: { str: 8, dex: 14, con: 12, int: 18, wis: 12, cha: 10 }, hp: 25, maxHp: 25, ac: 12, level: 5, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), }; characterRepo.create(character); const request: CreateAuraRequest = { ownerId: 'wizard-1', spellName: 'Cloudkill', spellLevel: 5, radius: 20, affectsAllies: false, affectsEnemies: true, affectsSelf: false, effects: [ { trigger: 'start_of_turn', type: 'damage', dice: '5d8', damageType: 'poison', saveType: 'constitution', saveDC: 15, }, ], currentRound: 1, maxDuration: 100, requiresConcentration: true, }; const aura = createAura(request, auraRepo); expect(getActiveAuras(auraRepo)).toHaveLength(1); const removed = endAura(aura.id, auraRepo); expect(removed).toBe(true); expect(getActiveAuras(auraRepo)).toHaveLength(0); }); it('should end all auras owned by a character', () => { const character: Character = { id: 'druid-1', name: 'Druid', stats: { str: 10, dex: 12, con: 14, int: 12, wis: 16, cha: 10 }, hp: 35, maxHp: 35, ac: 14, level: 7, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), }; characterRepo.create(character); // Create multiple auras for (let i = 0; i < 3; i++) { const request: CreateAuraRequest = { ownerId: 'druid-1', spellName: `Aura ${i}`, spellLevel: 1, radius: 10, affectsAllies: true, affectsEnemies: false, affectsSelf: false, effects: [ { trigger: 'start_of_turn', type: 'healing', dice: '1d6', }, ], currentRound: 1, requiresConcentration: false, }; createAura(request, auraRepo); } expect(getActiveAuras(auraRepo)).toHaveLength(3); const removed = endAurasByOwner('druid-1', auraRepo); expect(removed).toBe(3); expect(getActiveAuras(auraRepo)).toHaveLength(0); }); }); describe('Target Filtering', () => { it('should affect enemies when affectsEnemies is true', () => { const aura: AuraState = { id: 'aura-1', ownerId: 'owner-1', spellName: 'Test Aura', spellLevel: 1, radius: 10, affectsAllies: false, affectsEnemies: true, affectsSelf: false, effects: [], startedAt: 1, requiresConcentration: false, }; const enemy: Token = { id: 'enemy-1', name: 'Goblin', hp: 10, maxHp: 10, ac: 12, initiative: 10, isEnemy: true, }; const ownerIsAlly = false; // Enemy of the owner const shouldAffect = shouldAffectTarget(aura, enemy, ownerIsAlly); expect(shouldAffect).toBe(true); }); it('should not affect allies when affectsAllies is false', () => { const aura: AuraState = { id: 'aura-1', ownerId: 'owner-1', spellName: 'Test Aura', spellLevel: 1, radius: 10, affectsAllies: false, affectsEnemies: true, affectsSelf: false, effects: [], startedAt: 1, requiresConcentration: false, }; const ally: Token = { id: 'ally-1', name: 'Fighter', hp: 40, maxHp: 40, ac: 18, initiative: 15, isEnemy: false, }; const ownerIsAlly = true; // Ally of the owner const shouldAffect = shouldAffectTarget(aura, ally, ownerIsAlly); expect(shouldAffect).toBe(false); }); it('should affect self when affectsSelf is true', () => { const aura: AuraState = { id: 'aura-1', ownerId: 'owner-1', spellName: 'Test Aura', spellLevel: 1, radius: 10, affectsAllies: true, affectsEnemies: false, affectsSelf: true, effects: [], startedAt: 1, requiresConcentration: false, }; const self: Token = { id: 'owner-1', name: 'Paladin', hp: 40, maxHp: 40, ac: 18, initiative: 12, isEnemy: false, }; const ownerIsAlly = true; const shouldAffect = shouldAffectTarget(aura, self, ownerIsAlly); expect(shouldAffect).toBe(true); }); it('should not affect self when affectsSelf is false', () => { const aura: AuraState = { id: 'aura-1', ownerId: 'owner-1', spellName: 'Test Aura', spellLevel: 1, radius: 10, affectsAllies: true, affectsEnemies: true, affectsSelf: false, effects: [], startedAt: 1, requiresConcentration: false, }; const self: Token = { id: 'owner-1', name: 'Cleric', hp: 30, maxHp: 30, ac: 16, initiative: 10, isEnemy: false, }; const ownerIsAlly = true; const shouldAffect = shouldAffectTarget(aura, self, ownerIsAlly); expect(shouldAffect).toBe(false); }); }); describe('Effect Application', () => { it('should apply damage effect with saving throw', () => { const aura: AuraState = { id: 'aura-1', ownerId: 'cleric-1', spellName: 'Spirit Guardians', spellLevel: 3, radius: 15, affectsAllies: false, affectsEnemies: true, affectsSelf: false, effects: [], startedAt: 1, requiresConcentration: true, }; const effect: AuraEffect = { trigger: 'start_of_turn', type: 'damage', dice: '3d8', damageType: 'radiant', saveType: 'wisdom', saveDC: 14, }; const target: Token = { id: 'goblin-1', name: 'Goblin', hp: 20, maxHp: 20, ac: 12, initiative: 10, isEnemy: true, abilityScores: { strength: 8, dexterity: 14, constitution: 10, intelligence: 10, wisdom: 8, // -1 modifier charisma: 8, }, }; const result = applyAuraEffect(aura, effect, target, 'start_of_turn'); expect(result.auraId).toBe('aura-1'); expect(result.targetId).toBe('goblin-1'); expect(result.effectType).toBe('damage'); expect(result.saveRoll).toBeDefined(); expect(result.saveDC).toBe(14); // If save failed, damage should be dealt if (!result.succeeded) { expect(result.damageDealt).toBeGreaterThan(0); expect(result.damageType).toBe('radiant'); } }); it('should apply healing effect without save', () => { const aura: AuraState = { id: 'aura-1', ownerId: 'paladin-1', spellName: 'Aura of Vitality', spellLevel: 3, radius: 10, affectsAllies: true, affectsEnemies: false, affectsSelf: true, effects: [], startedAt: 1, requiresConcentration: true, }; const effect: AuraEffect = { trigger: 'start_of_turn', type: 'healing', dice: '2d6', }; const target: Token = { id: 'fighter-1', name: 'Fighter', hp: 25, maxHp: 40, ac: 18, initiative: 15, isEnemy: false, }; const result = applyAuraEffect(aura, effect, target, 'start_of_turn'); expect(result.succeeded).toBe(true); // No save required expect(result.healingDone).toBeGreaterThan(0); expect(result.healingDone).toBeLessThanOrEqual(12); // Max 2d6 }); it('should apply buff effect', () => { const aura: AuraState = { id: 'aura-1', ownerId: 'paladin-1', spellName: 'Aura of Protection', spellLevel: 0, radius: 10, affectsAllies: true, affectsEnemies: false, affectsSelf: true, effects: [], startedAt: 1, requiresConcentration: false, }; const effect: AuraEffect = { trigger: 'start_of_turn', type: 'buff', bonusAmount: 2, bonusType: 'saves', description: '+2 to all saving throws', }; const target: Token = { id: 'cleric-1', name: 'Cleric', hp: 30, maxHp: 30, ac: 16, initiative: 12, isEnemy: false, }; const result = applyAuraEffect(aura, effect, target, 'start_of_turn'); expect(result.succeeded).toBe(true); expect(result.description).toBe('+2 to all saving throws'); }); }); describe('Duration Tracking', () => { it('should detect when aura has exceeded duration', () => { const aura: AuraState = { id: 'aura-1', ownerId: 'wizard-1', spellName: 'Cloudkill', spellLevel: 5, radius: 20, affectsAllies: false, affectsEnemies: true, affectsSelf: false, effects: [], startedAt: 1, maxDuration: 10, requiresConcentration: true, }; const currentRound = 11; const isExpired = checkAuraDuration(aura, currentRound); expect(isExpired).toBe(true); }); it('should not expire aura within duration', () => { const aura: AuraState = { id: 'aura-1', ownerId: 'cleric-1', spellName: 'Spirit Guardians', spellLevel: 3, radius: 15, affectsAllies: false, affectsEnemies: true, affectsSelf: false, effects: [], startedAt: 1, maxDuration: 100, requiresConcentration: true, }; const currentRound = 50; const isExpired = checkAuraDuration(aura, currentRound); expect(isExpired).toBe(false); }); it('should never expire auras without duration limit', () => { const aura: AuraState = { id: 'aura-1', ownerId: 'paladin-1', spellName: 'Aura of Protection', spellLevel: 0, radius: 10, affectsAllies: true, affectsEnemies: false, affectsSelf: true, effects: [], startedAt: 1, maxDuration: undefined, requiresConcentration: false, }; const currentRound = 9999; const isExpired = checkAuraDuration(aura, currentRound); expect(isExpired).toBe(false); }); it('should clean up expired auras', () => { const character: Character = { id: 'sorcerer-1', name: 'Sorcerer', stats: { str: 8, dex: 14, con: 12, int: 12, wis: 10, cha: 18 }, hp: 28, maxHp: 28, ac: 12, level: 6, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), }; characterRepo.create(character); // Create aura with short duration const request: CreateAuraRequest = { ownerId: 'sorcerer-1', spellName: 'Short Duration Aura', spellLevel: 2, radius: 10, affectsAllies: false, affectsEnemies: true, affectsSelf: false, effects: [ { trigger: 'start_of_turn', type: 'damage', dice: '2d6', damageType: 'fire', }, ], currentRound: 1, maxDuration: 5, requiresConcentration: false, }; createAura(request, auraRepo); expect(getActiveAuras(auraRepo)).toHaveLength(1); // Advance to round 6 (duration expired) const expiredIds = expireOldAuras(6, auraRepo); expect(expiredIds).toHaveLength(1); expect(getActiveAuras(auraRepo)).toHaveLength(0); }); }); describe('Concentration Integration', () => { it('should start concentration when creating concentration aura', () => { const character: Character = { id: 'cleric-2', name: 'Cleric', stats: { str: 10, dex: 10, con: 14, int: 10, wis: 16, cha: 10 }, hp: 30, maxHp: 30, ac: 16, level: 5, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), }; characterRepo.create(character); const request: CreateAuraRequest = { ownerId: 'cleric-2', spellName: 'Spirit Guardians', spellLevel: 3, radius: 15, affectsAllies: false, affectsEnemies: true, affectsSelf: false, effects: [ { trigger: 'start_of_turn', type: 'damage', dice: '3d8', damageType: 'radiant', saveType: 'wisdom', saveDC: 14, }, ], currentRound: 1, maxDuration: 100, requiresConcentration: true, }; // Manually start concentration (normally done by createAura tool handler) startConcentration( request.ownerId, request.spellName, request.spellLevel, request.currentRound, request.maxDuration, undefined, concentrationRepo, characterRepo ); const concentration = concentrationRepo.findByCharacterId('cleric-2'); expect(concentration).toBeDefined(); expect(concentration?.activeSpell).toBe('Spirit Guardians'); }); }); describe('Integration Tests', () => { it('should handle Spirit Guardians spell correctly', () => { const character: Character = { id: 'cleric-3', name: 'Brother Marcus', stats: { str: 10, dex: 10, con: 14, int: 10, wis: 16, cha: 10 }, hp: 30, maxHp: 30, ac: 16, level: 5, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), }; characterRepo.create(character); const request: CreateAuraRequest = { ownerId: 'cleric-3', spellName: 'Spirit Guardians', spellLevel: 3, radius: 15, affectsAllies: false, affectsEnemies: true, affectsSelf: false, effects: [ { trigger: 'start_of_turn', type: 'damage', dice: '3d8', damageType: 'radiant', saveType: 'wisdom', saveDC: 14, }, ], currentRound: 1, maxDuration: 100, requiresConcentration: true, }; const aura = createAura(request, auraRepo); expect(aura.spellName).toBe('Spirit Guardians'); expect(aura.radius).toBe(15); expect(aura.affectsEnemies).toBe(true); expect(aura.affectsAllies).toBe(false); expect(aura.requiresConcentration).toBe(true); expect(aura.effects).toHaveLength(1); expect(aura.effects[0].trigger).toBe('start_of_turn'); expect(aura.effects[0].type).toBe('damage'); }); it('should handle Aura of Protection (Paladin feature) correctly', () => { const character: Character = { id: 'paladin-2', name: 'Sir Gideon', stats: { str: 16, dex: 10, con: 14, int: 10, wis: 12, cha: 14 }, hp: 40, maxHp: 40, ac: 18, level: 6, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), }; characterRepo.create(character); const request: CreateAuraRequest = { ownerId: 'paladin-2', spellName: 'Aura of Protection', spellLevel: 0, radius: 10, affectsAllies: true, affectsEnemies: false, affectsSelf: true, effects: [ { trigger: 'start_of_turn', type: 'buff', bonusAmount: 2, bonusType: 'saves', description: '+2 to all saving throws', }, ], currentRound: 1, requiresConcentration: false, }; const aura = createAura(request, auraRepo); expect(aura.spellName).toBe('Aura of Protection'); expect(aura.radius).toBe(10); expect(aura.affectsAllies).toBe(true); expect(aura.affectsSelf).toBe(true); expect(aura.affectsEnemies).toBe(false); expect(aura.requiresConcentration).toBe(false); expect(aura.maxDuration).toBeUndefined(); // Permanent }); it('should process multiple aura effects on a target', () => { const character1: Character = { id: 'char-1', name: 'Character 1', stats: { str: 10, dex: 10, con: 10, int: 10, wis: 10, cha: 10 }, hp: 30, maxHp: 30, ac: 15, level: 5, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), }; characterRepo.create(character1); // Create two different auras const request1: CreateAuraRequest = { ownerId: 'char-1', spellName: 'Aura 1', spellLevel: 1, radius: 10, affectsAllies: false, affectsEnemies: true, affectsSelf: false, effects: [ { trigger: 'start_of_turn', type: 'damage', dice: '1d6', damageType: 'fire', }, ], currentRound: 1, requiresConcentration: false, }; const request2: CreateAuraRequest = { ownerId: 'char-1', spellName: 'Aura 2', spellLevel: 1, radius: 10, affectsAllies: false, affectsEnemies: true, affectsSelf: false, effects: [ { trigger: 'start_of_turn', type: 'damage', dice: '1d4', damageType: 'cold', }, ], currentRound: 1, requiresConcentration: false, }; createAura(request1, auraRepo); createAura(request2, auraRepo); const tokens: Token[] = [ { id: 'char-1', name: 'Character 1', hp: 30, maxHp: 30, ac: 15, initiative: 15, isEnemy: false, position: { x: 0, y: 0 }, }, { id: 'enemy-1', name: 'Enemy', hp: 20, maxHp: 20, ac: 12, initiative: 10, isEnemy: true, position: { x: 1, y: 0 }, // 5 feet away }, ]; const results = checkAuraEffectsForTarget(tokens, 'enemy-1', 'start_of_turn', auraRepo); // Should have 2 effects (one from each aura) expect(results).toHaveLength(2); }); }); });

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