execute_action
Execute combat actions like attack or dash during RPG encounters to resolve character turns and advance gameplay.
Instructions
Execute a combat action in an encounter (attack, dash, disengage, dodge, etc.). Phase 1 supports attack and dash actions.
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| encounterId | Yes | ||
| actorId | No | ||
| actorName | No | ||
| actionType | Yes | ||
| actionCost | No | action | |
| targetId | No | ||
| targetName | No | ||
| weaponType | No | ||
| damageExpression | No | ||
| damageType | No | ||
| moveTo | No | ||
| advantage | No | ||
| disadvantage | No | ||
| manualAttackRoll | No | ||
| manualDamageRoll | No | ||
| shoveDirection | No |
Implementation Reference
- src/registry.ts:467-485 (registration)Registration of the execute_action tool in the central registry. Specifies name, description, converts executeActionSchema to JSON schema for MCP, and provides a wrapper handler that validates input with Zod and calls the executeAction function from combat module.execute_action: { name: 'execute_action', description: 'Execute a combat action in an encounter (attack, dash, disengage, dodge, etc.). Phase 1 supports attack and dash actions.', inputSchema: toJsonSchema(executeActionSchema), handler: async (args) => { try { const validated = executeActionSchema.parse(args); const result = executeAction(validated); return success(result); } catch (err) { if (err instanceof z.ZodError) { const messages = err.errors.map(e => `${e.path.join('.')}: ${e.message}`).join(', '); return error(`Validation failed: ${messages}`); } const message = err instanceof Error ? err.message : String(err); return error(message); } }, },
- src/modules/combat.ts:1143-1178 (schema)Zod input schema for the execute_action tool, defining parameters like encounterId, actor, actionType (attack, dash, etc.), target, damage, movement, advantage/disadvantage, and manual rolls.export const executeActionSchema = z.object({ encounterId: z.string(), // Actor (either/or) actorId: z.string().optional(), actorName: z.string().optional(), // Action selection actionType: ActionTypeSchema, // Action economy actionCost: ActionCostSchema.default('action'), // Target (for attack) targetId: z.string().optional(), targetName: z.string().optional(), // Attack options weaponType: WeaponTypeSchema.optional(), damageExpression: z.string().optional(), damageType: DamageTypeSchema.optional(), // Movement options moveTo: PositionSchema.optional(), // Advantage/disadvantage advantage: z.boolean().optional(), disadvantage: z.boolean().optional(), // Manual rolls (pre-rolled dice) manualAttackRoll: z.number().optional(), manualDamageRoll: z.number().optional(), // Phase 2: Shove direction (away or prone) shoveDirection: z.enum(['away', 'prone']).optional(), });
- src/modules/combat.ts:1224-1258 (handler)Core handler function for execute_action tool. Retrieves encounter state, locates actor by ID or name, then dispatches to specific action handlers (attack, dash, disengage, dodge, grapple, shove) based on actionType.export function executeAction(input: ExecuteActionInput): string { // Get encounter const encounter = encounterStore.get(input.encounterId); if (!encounter) { throw new Error(`Encounter not found: ${input.encounterId}`); } // Find actor let actor = input.actorId ? encounter.participants.find(p => p.id === input.actorId) : encounter.participants.find(p => p.name.toLowerCase() === input.actorName?.toLowerCase()); if (!actor) { throw new Error(`Actor not found: ${input.actorId || input.actorName}`); } // Dispatch by action type switch (input.actionType) { case 'attack': return handleAttackAction(encounter, actor, input); case 'dash': return handleDashAction(encounter, actor, input); case 'disengage': return handleDisengageAction(encounter, actor, input); case 'dodge': return handleDodgeAction(encounter, actor, input); case 'grapple': return handleGrappleAction(encounter, actor, input); case 'shove': return handleShoveAction(encounter, actor, input); default: // Other actions return a placeholder return formatActionNotImplemented(actor.name, input.actionType); } }
- src/modules/combat.ts:1264-1387 (helper)Primary helper for attack actions: handles target selection, attack rolls (with advantage/disadvantage/dodge penalties), damage rolling/parsing, critical hits, HP deduction, and optional movement (with distance checks). Called by main executeAction.function handleAttackAction( encounter: EncounterState, actor: Participant & { initiative: number; surprised?: boolean }, input: ExecuteActionInput ): string { // Find target let target = input.targetId ? encounter.participants.find(p => p.id === input.targetId) : encounter.participants.find(p => p.name.toLowerCase() === input.targetName?.toLowerCase()); if (!target) { throw new Error(`Target not found: ${input.targetId || input.targetName || 'No target specified'}`); } // Check if target is dodging - applies disadvantage const targetTracker = getTurnTracker(input.encounterId, target.id); const targetIsDodging = targetTracker.isDodging; const effectiveDisadvantage = input.disadvantage || targetIsDodging; // Roll attack (d20) let attackRoll: number; let isNat20 = false; let isNat1 = false; let rollDescription: string; if (input.manualAttackRoll !== undefined) { attackRoll = input.manualAttackRoll; isNat20 = attackRoll === 20; isNat1 = attackRoll === 1; rollDescription = `${attackRoll} (manual)`; } else if (input.advantage || effectiveDisadvantage) { // Roll 2d20 const roll1 = Math.floor(Math.random() * 20) + 1; const roll2 = Math.floor(Math.random() * 20) + 1; if (input.advantage && !effectiveDisadvantage) { attackRoll = Math.max(roll1, roll2); rollDescription = `${attackRoll} (${roll1}, ${roll2} - advantage)`; } else if (effectiveDisadvantage && !input.advantage) { attackRoll = Math.min(roll1, roll2); const reason = targetIsDodging ? 'target dodging' : 'disadvantage'; rollDescription = `${attackRoll} (${roll1}, ${roll2} - ${reason})`; } else { // Both advantage and disadvantage = straight roll attackRoll = Math.floor(Math.random() * 20) + 1; rollDescription = `${attackRoll} (adv/disadv cancel)`; } isNat20 = attackRoll === 20; isNat1 = attackRoll === 1; } else { attackRoll = Math.floor(Math.random() * 20) + 1; isNat20 = attackRoll === 20; isNat1 = attackRoll === 1; rollDescription = `${attackRoll}`; } // Calculate hit (natural 20 always hits, natural 1 always misses) const isHit = isNat20 || (!isNat1 && attackRoll >= target.ac); const isCritical = isNat20; // Calculate damage if hit let damage = 0; let damageRolls: number[] = []; let damageDescription = ''; if (isHit && input.damageExpression) { if (input.manualDamageRoll !== undefined) { damage = input.manualDamageRoll; damageDescription = `${damage} (manual)`; } else { // Parse and roll damage const diceResult = parseDice(input.damageExpression); damage = diceResult.total; damageRolls = diceResult.rolls; // Double dice on critical hit (roll again and add) if (isCritical) { const critBonus = parseDice(input.damageExpression.replace(/[+-]\d+$/, '')); // Remove modifier for crit damage += critBonus.total; damageRolls = [...damageRolls, ...critBonus.rolls]; damageDescription = `${damage} (${damageRolls.join(', ')} - CRITICAL!)`; } else { damageDescription = `${damage} (${damageRolls.join(', ')})`; } } } // Apply damage to target let oldHp = target.hp; if (isHit && damage > 0) { target.hp = Math.max(0, target.hp - damage); } // Handle movement if specified let movementInfo = ''; if (input.moveTo) { const moveResult = handleMoveInternal(encounter, actor, input.moveTo); movementInfo = moveResult; } // Format output return formatAttackResult( actor.name, target.name, attackRoll, rollDescription, target.ac, isHit, isCritical, isNat1, damage, damageDescription, input.damageType || 'slashing', oldHp, target.hp, target.maxHp, input.actionCost || 'action', input.advantage, input.disadvantage, movementInfo ); }