Skip to main content
Glama
aura.ts9.93 kB
/** * Aura System - Manages area-effect auras centered on characters * Handles aura creation, position-based effect triggers, and movement integration */ import { randomUUID } from 'crypto'; import { AuraState, AuraEffect, AuraEffectResult, CreateAuraRequest, AuraTrigger } from '../../schema/aura.js'; import { Position, Token } from '../../schema/encounter.js'; import { AuraRepository } from '../../storage/repos/aura.repo.js'; /** * Calculate distance in feet between two positions * Assumes each grid square is 5 feet */ export function calculateDistance(pos1: Position, pos2: Position): number { const dx = pos1.x - pos2.x; const dy = pos1.y - pos2.y; const squares = Math.sqrt(dx * dx + dy * dy); return Math.round(squares * 5); // Convert squares to feet } /** * Check if a position is within an aura's radius * @param auraCenter - Position of the aura's owner * @param targetPosition - Position to check * @param radius - Aura radius in feet */ export function isInAuraRange( auraCenter: Position, targetPosition: Position, radius: number ): boolean { const distance = calculateDistance(auraCenter, targetPosition); return distance <= radius; } /** * Create a new aura */ export function createAura( request: CreateAuraRequest, auraRepo: AuraRepository ): AuraState { const aura: AuraState = { id: randomUUID(), ownerId: request.ownerId, spellName: request.spellName, spellLevel: request.spellLevel, radius: request.radius, affectsAllies: request.affectsAllies, affectsEnemies: request.affectsEnemies, affectsSelf: request.affectsSelf, effects: request.effects, startedAt: request.currentRound, maxDuration: request.maxDuration, requiresConcentration: request.requiresConcentration, }; auraRepo.create(aura); return aura; } /** * End an aura by ID */ export function endAura(auraId: string, auraRepo: AuraRepository): boolean { return auraRepo.delete(auraId); } /** * End all auras owned by a character */ export function endAurasByOwner(ownerId: string, auraRepo: AuraRepository): number { return auraRepo.deleteByOwnerId(ownerId); } /** * Get all active auras */ export function getActiveAuras(auraRepo: AuraRepository): AuraState[] { return auraRepo.findAll(); } /** * Get auras affecting a specific position * @param tokens - All tokens in the encounter (to find aura owner positions) * @param targetPosition - Position to check * @param auraRepo - Aura repository */ export function getAurasAtPosition( tokens: Token[], targetPosition: Position, auraRepo: AuraRepository ): AuraState[] { const allAuras = auraRepo.findAll(); const aurasAtPosition: AuraState[] = []; for (const aura of allAuras) { // Find the aura owner's position const owner = tokens.find(t => t.id === aura.ownerId); if (!owner || !owner.position) { continue; } // Check if target position is within aura radius if (isInAuraRange(owner.position, targetPosition, aura.radius)) { aurasAtPosition.push(aura); } } return aurasAtPosition; } /** * Check if a target should be affected by an aura * @param aura - The aura to check * @param target - The target token * @param ownerIsAlly - Whether the aura owner is an ally of the target */ export function shouldAffectTarget( aura: AuraState, target: Token, ownerIsAlly: boolean ): boolean { // Check if target is the aura owner if (target.id === aura.ownerId) { return aura.affectsSelf; } // Check ally/enemy filters if (ownerIsAlly && !aura.affectsAllies) { return false; } if (!ownerIsAlly && !aura.affectsEnemies) { return false; } return true; } /** * Roll a saving throw * @param abilityModifier - The ability modifier for the save * @returns Object with roll and total */ export function rollSave(abilityModifier: number): { roll: number; total: number } { const roll = Math.floor(Math.random() * 20) + 1; const total = roll + abilityModifier; return { roll, total }; } /** * Roll damage or healing dice * @param dice - Dice notation (e.g., "3d8") * @returns Total result */ export function rollDice(dice: string): number { const match = dice.match(/^(\d+)d(\d+)(?:\+(\d+))?$/); if (!match) { throw new Error(`Invalid dice notation: ${dice}`); } const [, numDiceStr, diceSizeStr, bonusStr] = match; const numDice = parseInt(numDiceStr, 10); const diceSize = parseInt(diceSizeStr, 10); const bonus = bonusStr ? parseInt(bonusStr, 10) : 0; let total = bonus; for (let i = 0; i < numDice; i++) { total += Math.floor(Math.random() * diceSize) + 1; } return total; } /** * Apply a single aura effect to a target * @param aura - The aura * @param effect - The specific effect to apply * @param target - The target token * @returns Result of the effect application */ export function applyAuraEffect( aura: AuraState, effect: AuraEffect, target: Token, trigger: AuraTrigger ): AuraEffectResult { const result: AuraEffectResult = { auraId: aura.id, auraName: aura.spellName, targetId: target.id, trigger, effectType: effect.type, succeeded: false, }; // Handle effects that require saving throws if (effect.saveType && effect.saveDC) { const abilityModifier = getAbilityModifier(target, effect.saveType); const saveResult = rollSave(abilityModifier); result.saveRoll = saveResult.roll; result.saveDC = effect.saveDC; result.saveTotal = saveResult.total; result.succeeded = saveResult.total >= effect.saveDC; // If save succeeded, effect doesn't apply (for most damaging effects) if (result.succeeded && (effect.type === 'damage' || effect.type === 'condition')) { return result; } } else { // No save required - effect automatically succeeds result.succeeded = true; } // Apply the effect based on type switch (effect.type) { case 'damage': if (effect.dice) { result.damageDealt = rollDice(effect.dice); result.damageType = effect.damageType; } break; case 'healing': if (effect.dice) { result.healingDone = rollDice(effect.dice); } break; case 'condition': if (effect.conditions) { result.conditionsApplied = effect.conditions; } break; case 'buff': case 'debuff': case 'custom': result.description = effect.description; break; } return result; } /** * Get ability modifier for a token */ function getAbilityModifier(token: Token, ability: string): number { if (!token.abilityScores) { return 0; } const abilityMap: Record<string, keyof typeof token.abilityScores> = { strength: 'strength', dexterity: 'dexterity', constitution: 'constitution', intelligence: 'intelligence', wisdom: 'wisdom', charisma: 'charisma', }; const abilityKey = abilityMap[ability.toLowerCase()]; if (!abilityKey) { return 0; } const score = token.abilityScores[abilityKey]; return Math.floor((score - 10) / 2); } /** * Check aura effects for a specific target and trigger * @param tokens - All tokens in the encounter * @param targetId - ID of the target to check * @param trigger - The trigger type (enter, exit, start_of_turn, etc.) * @param auraRepo - Aura repository * @returns Array of effect results */ export function checkAuraEffectsForTarget( tokens: Token[], targetId: string, trigger: AuraTrigger, auraRepo: AuraRepository ): AuraEffectResult[] { const target = tokens.find(t => t.id === targetId); if (!target || !target.position) { return []; } const aurasAtPosition = getAurasAtPosition(tokens, target.position, auraRepo); const results: AuraEffectResult[] = []; for (const aura of aurasAtPosition) { // Find aura owner to determine ally status const owner = tokens.find(t => t.id === aura.ownerId); if (!owner) continue; // Determine if target is ally (same isEnemy status as owner) const ownerIsAlly = target.isEnemy === owner.isEnemy; // Check if target should be affected if (!shouldAffectTarget(aura, target, ownerIsAlly)) { continue; } // Apply each effect that matches the trigger for (const effect of aura.effects) { if (effect.trigger === trigger) { const result = applyAuraEffect(aura, effect, target, trigger); results.push(result); } } } return results; } /** * Check if aura has exceeded its duration */ export function checkAuraDuration( aura: AuraState, currentRound: number ): boolean { if (!aura.maxDuration) { return false; // No duration limit } const roundsElapsed = currentRound - aura.startedAt; return roundsElapsed >= aura.maxDuration; } /** * Clean up expired auras * @param currentRound - Current combat round * @param auraRepo - Aura repository * @returns Array of expired aura IDs */ export function expireOldAuras( currentRound: number, auraRepo: AuraRepository ): string[] { const allAuras = auraRepo.findAll(); const expiredIds: string[] = []; for (const aura of allAuras) { if (checkAuraDuration(aura, currentRound)) { auraRepo.delete(aura.id); expiredIds.push(aura.id); } } return expiredIds; }

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