Skip to main content
Glama
combat-tools.ts.backup12.7 kB
import { z } from 'zod'; import { CombatEngine, CombatParticipant } from '../engine/combat/engine.js'; import { PubSub } from '../engine/pubsub.js'; import { getCombatManager } from './state/combat-manager.js'; import { getDb } from '../storage/index.js'; import { EncounterRepository } from '../storage/repos/encounter.repo.js'; import { SessionContext } from './types.js'; // Global combat state (in-memory for MVP) let pubsub: PubSub | null = null; export function setCombatPubSub(instance: PubSub) { pubsub = instance; } // Tool definitions export const CombatTools = { CREATE_ENCOUNTER: { name: 'create_encounter', description: `Create a new combat encounter with the specified participants. Example: { "seed": "battle-1", "participants": [ { "id": "hero-1", "name": "Valeros", "initiativeBonus": 2, "hp": 20, "maxHp": 20 }, { "id": "goblin-1", "name": "Goblin", "initiativeBonus": 1, "hp": 7, "maxHp": 7 } ] }`, inputSchema: z.object({ seed: z.string().describe('Seed for deterministic combat resolution'), participants: z.array(z.object({ id: z.string(), name: z.string(), initiativeBonus: z.number().int(), hp: z.number().int().positive(), maxHp: z.number().int().positive(), conditions: z.array(z.any()).default([]) })).min(1) }) }, GET_ENCOUNTER_STATE: { name: 'get_encounter_state', description: 'Get the current state of the active combat encounter.', inputSchema: z.object({ encounterId: z.string().describe('The ID of the encounter') }) }, EXECUTE_COMBAT_ACTION: { name: 'execute_combat_action', description: `Execute a combat action (attack, heal, etc.). Examples: { "action": "attack", "actorId": "hero-1", "targetId": "goblin-1", "attackBonus": 5, "dc": 12, "damage": 6 } { "action": "heal", "actorId": "cleric-1", "targetId": "hero-1", "amount": 8 }`, inputSchema: z.object({ encounterId: z.string().describe('The ID of the encounter'), action: z.enum(['attack', 'heal']), actorId: z.string(), targetId: z.string(), attackBonus: z.number().int().optional(), dc: z.number().int().optional(), damage: z.number().int().optional(), amount: z.number().int().optional() }) }, ADVANCE_TURN: { name: 'advance_turn', description: 'Advance to the next combatant\'s turn.', inputSchema: z.object({ encounterId: z.string().describe('The ID of the encounter') }) }, END_ENCOUNTER: { name: 'end_encounter', description: 'End the current combat encounter.', inputSchema: z.object({ encounterId: z.string().describe('The ID of the encounter') }) }, LOAD_ENCOUNTER: { name: 'load_encounter', description: 'Load a combat encounter from the database.', inputSchema: z.object({ encounterId: z.string().describe('The ID of the encounter to load') }) } } as const; // Tool handlers export async function handleCreateEncounter(args: unknown, ctx: SessionContext) { // No need to check for existing encounter globally anymore const parsed = CombatTools.CREATE_ENCOUNTER.inputSchema.parse(args); // Create combat engine const engine = new CombatEngine(parsed.seed, pubsub || undefined); // Convert participants to proper format const participants: CombatParticipant[] = parsed.participants.map(p => ({ ...p, conditions: [] })); // Start encounter const state = engine.startEncounter(participants); // Generate encounter ID const encounterId = `encounter-${parsed.seed}-${Date.now()}`; // Store with session namespace getCombatManager().create(`${ctx.sessionId}:${encounterId}`, engine); // Persist initial state const db = getDb(process.env.NODE_ENV === 'test' ? ':memory:' : 'rpg.db'); const repo = new EncounterRepository(db); // Create the encounter record first repo.create({ id: encounterId, // regionId is optional tokens: state.participants.map(p => ({ id: p.id, name: p.name, initiativeBonus: p.initiativeBonus, hp: p.hp, maxHp: p.maxHp, conditions: p.conditions, abilityScores: p.abilityScores })), round: state.round, activeTokenId: state.turnOrder[state.currentTurnIndex], status: 'active', createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() }); const currentParticipant = engine.getCurrentParticipant(); return { content: [ { type: 'text' as const, text: JSON.stringify({ encounterId, message: 'Combat encounter started', turnOrder: state.turnOrder, round: state.round, currentTurn: currentParticipant?.name || null }, null, 2) } ] }; } export async function handleGetEncounterState(args: unknown, ctx: SessionContext) { const parsed = CombatTools.GET_ENCOUNTER_STATE.inputSchema.parse(args); let engine = getCombatManager().get(`${ctx.sessionId}:${parsed.encounterId}`); // Auto-load from database if not in memory if (!engine) { const db = getDb(process.env.NODE_ENV === 'test' ? ':memory:' : 'rpg.db'); const repo = new EncounterRepository(db); const state = repo.loadState(parsed.encounterId); if (!state) { throw new Error(`Encounter ${parsed.encounterId} not found.`); } // Create engine and load state engine = new CombatEngine(parsed.encounterId, pubsub || undefined); engine.loadState(state); getCombatManager().create(`${ctx.sessionId}:${parsed.encounterId}`, engine); } const state = engine.getState(); if (!state) { throw new Error('No active encounter'); } const currentParticipant = engine.getCurrentParticipant(); return { content: [ { type: 'text' as const, text: JSON.stringify({ encounterId: parsed.encounterId, round: state.round, currentTurn: { participantId: currentParticipant?.id, participantName: currentParticipant?.name }, participants: state.participants.map(p => ({ id: p.id, name: p.name, hp: p.hp, maxHp: p.maxHp, conditions: p.conditions })), turnOrder: state.turnOrder }, null, 2) } ] }; } export async function handleExecuteCombatAction(args: unknown, ctx: SessionContext) { const parsed = CombatTools.EXECUTE_COMBAT_ACTION.inputSchema.parse(args); let engine = getCombatManager().get(`${ctx.sessionId}:${parsed.encounterId}`); // Auto-load from database if not in memory if (!engine) { const db = getDb(process.env.NODE_ENV === 'test' ? ':memory:' : 'rpg.db'); const repo = new EncounterRepository(db); const state = repo.loadState(parsed.encounterId); if (!state) { throw new Error(`Encounter ${parsed.encounterId} not found.`); } engine = new CombatEngine(parsed.encounterId, pubsub || undefined); engine.loadState(state); getCombatManager().create(`${ctx.sessionId}:${parsed.encounterId}`, engine); } let result: any = { action: parsed.action, actorId: parsed.actorId, targetId: parsed.targetId }; if (parsed.action === 'attack') { if (parsed.attackBonus === undefined || parsed.dc === undefined) { throw new Error('Attack action requires attackBonus and dc'); } // Make attack check const degree = engine.makeCheck(parsed.attackBonus, parsed.dc); const success = degree === 'success' || degree === 'critical-success'; result.success = success; result.degree = degree; if (success && parsed.damage) { engine.applyDamage(parsed.targetId, parsed.damage); result.damageDealt = parsed.damage; } else { result.damageDealt = 0; } } else if (parsed.action === 'heal') { if (parsed.amount === undefined) { throw new Error('Heal action requires amount'); } engine.heal(parsed.targetId, parsed.amount); result.amountHealed = parsed.amount; } // Save state const state = engine.getState(); if (state) { const db = getDb(process.env.NODE_ENV === 'test' ? ':memory:' : 'rpg.db'); const repo = new EncounterRepository(db); repo.saveState(parsed.encounterId, state); } return { content: [ { type: 'text' as const, text: JSON.stringify(result, null, 2) } ] }; } export async function handleAdvanceTurn(args: unknown, ctx: SessionContext) { const parsed = CombatTools.ADVANCE_TURN.inputSchema.parse(args); let engine = getCombatManager().get(`${ctx.sessionId}:${parsed.encounterId}`); // Auto-load from database if not in memory if (!engine) { const db = getDb(process.env.NODE_ENV === 'test' ? ':memory:' : 'rpg.db'); const repo = new EncounterRepository(db); const state = repo.loadState(parsed.encounterId); if (!state) { throw new Error(`Encounter ${parsed.encounterId} not found.`); } engine = new CombatEngine(parsed.encounterId, pubsub || undefined); engine.loadState(state); getCombatManager().create(`${ctx.sessionId}:${parsed.encounterId}`, engine); } const previousParticipant = engine.getCurrentParticipant(); const newParticipant = engine.nextTurnWithConditions(); const state = engine.getState(); // Save state if (state) { const db = getDb(process.env.NODE_ENV === 'test' ? ':memory:' : 'rpg.db'); const repo = new EncounterRepository(db); repo.saveState(parsed.encounterId, state); } return { content: [ { type: 'text' as const, text: JSON.stringify({ previousTurn: previousParticipant?.name || null, currentTurn: newParticipant?.name || null, round: state?.round || 0 }, null, 2) } ] }; } export async function handleEndEncounter(args: unknown, ctx: SessionContext) { const parsed = CombatTools.END_ENCOUNTER.inputSchema.parse(args); const success = getCombatManager().delete(`${ctx.sessionId}:${parsed.encounterId}`); if (!success) { throw new Error(`Encounter ${parsed.encounterId} not found.`); } return { content: [ { type: 'text' as const, text: JSON.stringify({ message: 'Encounter ended', encounterId: parsed.encounterId }, null, 2) } ] }; } export async function handleLoadEncounter(args: unknown, ctx: SessionContext) { const parsed = CombatTools.LOAD_ENCOUNTER.inputSchema.parse(args); const db = getDb(process.env.NODE_ENV === 'test' ? ':memory:' : 'rpg.db'); const repo = new EncounterRepository(db); const state = repo.loadState(parsed.encounterId); if (!state) { throw new Error(`Encounter ${parsed.encounterId} not found in database.`); } // Create engine and load state const engine = new CombatEngine(parsed.encounterId, pubsub || undefined); engine.loadState(state); getCombatManager().create(`${ctx.sessionId}:${parsed.encounterId}`, engine); return { content: [{ type: 'text' as const, text: JSON.stringify({ message: 'Encounter loaded', encounterId: parsed.encounterId, round: state.round }, null, 2) }] }; } // Helper for tests export function clearCombatState() { // No-op or clear manager }

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