Skip to main content
Glama
combat.ts6.25 kB
import { DiceEngine } from './dice.js'; import { CalculationResult } from './schemas.js'; export class CombatEngine { private diceEngine: DiceEngine; constructor(seed?: string) { this.diceEngine = new DiceEngine(seed); } attackRoll(attacker: { attackBonus: number, critRange?: number }, target: { ac: number }, advantage?: boolean, disadvantage?: boolean): CalculationResult { // Roll 1d20 + bonus const critThreshold = attacker.critRange || 20; const expr = { count: 1, sides: 20, modifier: attacker.attackBonus, advantage, disadvantage }; const rollResult = this.diceEngine.roll(expr); const rolls = rollResult.metadata?.rolls as number[]; // If advantage/disadvantage, rolls has 1 element (the chosen one) or we need to look at steps. // DiceEngine implementation of advantage returns the chosen roll in rolls array? // Let's check DiceEngine.roll: // if (expr.advantage || expr.disadvantage) { ... rolls.push(...chosenSet.rolls); } // So yes, rolls[0] is the final die roll used. const d20 = rolls[0]; const total = rollResult.result as number; const isCrit = d20 >= critThreshold; const isMiss = d20 === 1; // Critical miss const hit = !isMiss && (isCrit || total >= target.ac); const steps = [ ...rollResult.steps, `Target AC: ${target.ac}`, `Natural Roll: ${d20}`, isCrit ? 'Critical Hit!' : (isMiss ? 'Critical Miss!' : (hit ? 'Hit!' : 'Miss!')) ]; return { ...rollResult, result: hit ? (isCrit ? 'crit' : 'hit') : 'miss', steps, metadata: { ...rollResult.metadata, isCrit, isMiss, hit, total } }; } damageRoll(damageExpr: string, isCrit: boolean = false): CalculationResult { // If crit, double the dice count. // We need to parse first. const baseExpr = this.diceEngine.parse(damageExpr); const expr = { ...baseExpr, count: isCrit ? baseExpr.count * 2 : baseExpr.count }; const result = this.diceEngine.roll(expr); if (isCrit) { result.steps.unshift('Critical Hit! Doubling dice count.'); } return result; } savingThrow(dc: number, modifier: number, advantage?: boolean, disadvantage?: boolean): CalculationResult { const expr = { count: 1, sides: 20, modifier, advantage, disadvantage }; const rollResult = this.diceEngine.roll(expr); const total = rollResult.result as number; const success = total >= dc; const margin = total - dc; return { ...rollResult, result: success ? 'success' : 'failure', steps: [ ...rollResult.steps, `DC: ${dc}`, success ? `Passed by ${margin}` : `Failed by ${-margin}` ], metadata: { ...rollResult.metadata, success, margin } }; } fallDamage(feet: number): CalculationResult { const diceCount = Math.min(20, Math.floor(feet / 10)); if (diceCount === 0) { return { input: `${feet} ft fall`, result: 0, steps: ['Fall distance < 10ft, no damage.'], timestamp: new Date().toISOString() }; } return this.diceEngine.roll(`${diceCount}d6`); } encounterBalance(partyLevels: number[], enemyCRs: number[]): { difficulty: string, xpBudget: number, adjustedXP: number } { // Simplified 5e logic // 1. Calculate Party XP Thresholds // This requires a table lookup. For MVP let's approximate or use a small map. // Level 1: Easy 25, Medium 50, Hard 75, Deadly 100 // ... // Let's implement a simplified version or just return the raw XP sums for now. // Task says "CR calculation, difficulty, XP budget analysis". // Let's just sum enemy XP (approx from CR) and compare to party budget. // CR to XP map const crToXp: Record<number, number> = { 0: 10, 0.125: 25, 0.25: 50, 0.5: 100, 1: 200, 2: 450, 3: 700, 4: 1100, 5: 1800 // ... extend as needed }; const getXp = (cr: number) => crToXp[cr] || cr * 200; // Fallback const totalEnemyXP = enemyCRs.reduce((sum, cr) => sum + getXp(cr), 0); // Multiplier for number of enemies let multiplier = 1; const count = enemyCRs.length; if (count === 2) multiplier = 1.5; else if (count >= 3 && count <= 6) multiplier = 2; else if (count >= 7 && count <= 10) multiplier = 2.5; else if (count >= 11) multiplier = 3; const adjustedXP = totalEnemyXP * multiplier; // Party thresholds (simplified for Level 1-5) const thresholds: Record<number, number[]> = { 1: [25, 50, 75, 100], 2: [50, 100, 150, 200], 3: [75, 150, 225, 400], 4: [125, 250, 375, 500], 5: [250, 500, 750, 1100] }; let partyThresholds = [0, 0, 0, 0]; // Easy, Med, Hard, Deadly for (const level of partyLevels) { const t = thresholds[level] || [0, 0, 0, 0]; partyThresholds[0] += t[0]; partyThresholds[1] += t[1]; partyThresholds[2] += t[2]; partyThresholds[3] += t[3]; } let difficulty = 'Trivial'; if (adjustedXP >= partyThresholds[3]) difficulty = 'Deadly'; else if (adjustedXP >= partyThresholds[2]) difficulty = 'Hard'; else if (adjustedXP >= partyThresholds[1]) difficulty = 'Medium'; else if (adjustedXP >= partyThresholds[0]) difficulty = 'Easy'; return { difficulty, xpBudget: partyThresholds[2], // Usually 'Hard' is the budget limit adjustedXP }; } }

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