Skip to main content
Glama
SYSTEM-INTERSECTIONS.md23.8 kB
# RPG-Lite MCP: System Intersections & Integration Planning ## Orchestration Strategy for Cross-Module Operations **Last Updated:** 2025-12-17 (Post-Condition System Implementation) **Status:** Phase 1 Complete - Mapping Future Intersections --- ## Executive Summary This document maps **critical integration points** between modules, predicting where systems will intersect and require coordination. Based on the completed condition effects system, we can now anticipate similar patterns across all modules. --- ## Core Integration Pattern: The "Effective Stats" Model ### What We Learned from Conditions The condition system established a **critical pattern** that should be replicated across all stat-modifying systems: ```typescript // ESTABLISHED PATTERN interface EffectiveStatCalculation { base: number; // Original value from character sheet effective: number; // After all modifications modified: boolean; // Was it changed? modifiers: string[]; // Human-readable list of what changed it } ``` **Why This Matters:** - ✅ LLM sees both base and modified values - ✅ Players understand what's happening to their characters - ✅ Extensible to ANY stat modification system - ✅ Works for temporary and permanent changes --- ## Module Intersection Map ### 1. CHARACTER ↔ CONDITION (✅ **IMPLEMENTED**) **Status:** COMPLETE **Integration Points:** - `getActiveConditions(characterId)` → Returns active conditions - `calculateEffectiveStats(characterId, baseStats)` → Modifies HP/speed/AC - `get_character` → Shows both base and effective stats **Data Flow:** ``` Character Storage → Condition Store → Effective Stats → Character Display (JSON) (In-Memory Map) (Runtime Calc) (ASCII Output) ``` **Key Learning:** Conditions must be **queryable outside combat** to support: - Town interactions while exhausted - Long-term curses - Persistent boons (e.g., from blessings) --- ### 2. CHARACTER ↔ EQUIPMENT (**PLANNED - HIGH PRIORITY**) **Predicted Intersection:** Equipment should use the SAME effective stats pattern **Required Integration:** ```typescript // EQUIPMENT should export (similar to conditions): export function getEquippedItems(characterId: string): EquippedItem[] export function calculateEquipmentBonuses(characterId: string, baseStats): EffectiveStatCalculation // Example output: { ac: { base: 10, effective: 18, modified: true }, modifiers: [ "Plate Armor: +18 AC", "Shield: +2 AC", "Ring of Protection: +1 AC" ] } ``` **Why This Matters:** - AC bonuses from armor/shields - Stat bonuses from magic items (Belt of Giant Strength: STR = 21) - Speed modifiers (Boots of Speed) - Attack/damage bonuses from weapons **Intersection with Conditions:** - Exhaustion Level 4 halves HP max → Equipment can't override this - Petrified → Equipment bonuses still apply to AC - **Order of Operations:** Base → Equipment → Conditions → Effective --- ### 3. CHARACTER ↔ EFFECTS (manage_effect) (**PLANNED - HIGH PRIORITY**) **Current Status:** Tool schema defined but not implemented **Predicted Intersection:** Effects = Conditions + More **Required Integration:** ```typescript // EFFECTS should be a SUPERSET of conditions interface EffectSystem { conditions: ActiveCondition[]; // D&D 5e conditions boons: CustomEffect[]; // Buffs (Bless, Haste, Bard Inspiration) curses: CustomEffect[]; // Debuffs (Hex, Bane, Slow) persistent: CustomEffect[]; // Permanent (Geas, Mark of the Beast) } // Effects should also export: export function calculateEffectiveStat sWithEffects( characterId: string, baseStats: Stats ): EffectiveStatCalculation ``` **Integration Challenge:** ``` Question: Should `getActiveConditions()` return boons/curses too? Answer: YES - all stat modifiers should be in one unified system New Export: export function getAllActiveModifiers(characterId: string): { conditions: ActiveCondition[]; boons: CustomEffect[]; curses: CustomEffect[]; equipment: EquippedItem[]; spellEffects: ActiveSpell[]; } ``` **Why This Unified Approach:** - LLM needs ONE source of truth for "what's affecting this character?" - Player confusion when effects come from 3 different systems - `get_character` should show ALL modifiers in one section --- ### 4. CHARACTER ↔ SPELL SLOTS/CONCENTRATION (**PLANNED - MEDIUM PRIORITY**) **Predicted Intersection:** Spellcasting state lives on character **Required Integration:** ```typescript // CHARACTER schema should include: interface Character { // ... existing fields ... spellSlots: { 1: { current: number, max: number }, 2: { current: number, max: number }, // ... up to 9 }; concentration: { active: boolean; spell: string; startedRound: number; duration?: number; } | null; pactMagic?: { slots: number; slotLevel: number; currentSlots: number; }; } // MAGIC module exports: export function getSpellcastingState(characterId: string) export function expendSlot(characterId: string, level: number) export function restoreSlots(characterId: string, restType: 'short' | 'long') export function breakConcentration(characterId: string, reason: string) ``` **Intersection with Conditions:** - **CRITICAL:** `incapacitated` condition should auto-break concentration - `unconscious` condition should auto-break concentration - Exhaustion Level 6 (death) should break concentration **Implementation in `manage_condition`:** ```typescript // When adding incapacitated/unconscious: if (condition === 'incapacitated' || condition === 'unconscious') { import { breakConcentration } from './magic.js'; breakConcentration(targetId, 'condition: ' + condition); } ``` --- ### 5. COMBAT (Encounter) ↔ CHARACTER (**PARTIAL - NEEDS BIDIRECTIONAL SYNC**) **Current Status:** Encounter state is separate from character persistence **Problem:** Changes to HP in combat don't persist to character JSON **Required Integration:** ```typescript // COMBAT module should: export function syncEncounterToCharacter(encounterId: string): void { const encounter = getEncounter(encounterId); for (const participant of encounter.participants) { if (participant.isCharacter) { updateCharacter({ characterId: participant.id, hp: participant.currentHp, // Conditions handled separately via manage_condition }); } } } // AUTO-SYNC POINTS: // 1. end_encounter → Sync all // 2. take_rest → Sync HP, remove conditions // 3. advance_turn → Optionally sync (performance concern) ``` **Intersection with Conditions:** - Conditions added in combat should persist outside combat - `conditionStore` Map should be serialized to disk (or SQLite) - **Current Bug:** Conditions are in-memory only, lost on restart **Fix Required:** ```typescript // In combat.ts: const CONDITION_FILE = path.join(DATA_ROOT, 'conditions.json'); function saveConditions() { const data = Object.fromEntries(conditionStore); fs.writeFileSync(CONDITION_FILE, JSON.stringify(data, null, 2)); } function loadConditions() { if (fs.existsSync(CONDITION_FILE)) { const data = JSON.parse(fs.readFileSync(CONDITION_FILE, 'utf-8')); for (const [characterId, conditions] of Object.entries(data)) { conditionStore.set(characterId, conditions as ActiveCondition[]); } } } // Call loadConditions() on server start // Call saveConditions() after every change ``` --- ### 6. COMBAT (Damage) ↔ CONDITIONS (**PLANNED - HIGH PRIORITY**) **Predicted Intersection:** Damage application should trigger condition checks **Required Integration:** ```typescript // In apply_damage: export function applyDamage(input: ApplyDamageInput): string { // ... calculate final damage ... const newHp = Math.max(0, target.hp - finalDamage); // INTERSECTION 1: Concentration check if (target.concentration?.active) { const dc = Math.max(10, Math.floor(finalDamage / 2)); checkConcentration({ characterId: targetId, damageTaken: finalDamage, modifier: calculateModifier(target.stats.con) }); } // INTERSECTION 2: Unconscious at 0 HP if (newHp === 0 && target.hp > 0) { manageCondition({ targetId, operation: 'add', condition: 'unconscious', source: 'damage: 0 HP' }); } // INTERSECTION 3: Death saves if (newHp === 0 && target.characterType === 'pc') { // Initialize death save tracking } return formatDamageResult(...); } ``` **Chain Reaction:** ``` Damage → 0 HP → Unconscious → Prone → Concentration Broken → Spell Ends → Aura Removed ``` --- ### 7. SPATIAL ↔ COMBAT ↔ AURAS (**PLANNED - COMPLEX**) **Predicted Intersection:** Movement triggers aura effects **Required Integration:** ```typescript // In move_combatant: export function moveCombatant(input: MoveCombatantInput): string { const oldPosition = target.position; const newPosition = input.to; // INTERSECTION 1: Opportunity attacks const enemiesInRange = getCreaturesInRange({ from: oldPosition, range: 5, includeEnemies: true, excludeIds: [targetId] }); if (!input.disengage && !input.forcedMovement) { // Trigger opportunity attacks } // INTERSECTION 2: Aura entry/exit const auras = getAllActiveAuras(encounterId); for (const aura of auras) { // Check if exiting aura if (isInAuraRange(oldPosition, aura) && !isInAuraRange(newPosition, aura)) { processAura({ creatureId: targetId, trigger: 'exit' }); } // Check if entering aura if (!isInAuraRange(oldPosition, aura) && isInAuraRange(newPosition, aura)) { processAura({ creatureId: targetId, trigger: 'enter' }); } } // INTERSECTION 3: Terrain effects if (hasHazardAt(newPosition)) { applyHazardDamage(targetId, newPosition); } return formatMovementResult(...); } ``` **Performance Concern:** - Each 5ft move could trigger 10+ aura checks - **Solution:** Cache aura ranges at start of turn --- ### 8. REST ↔ CHARACTER ↔ CONDITIONS ↔ SPELLS (**PLANNED - CRITICAL**) **Predicted Intersection:** Rest is a MEGA-ORCHESTRATOR **Required Integration:** ```typescript export function takeRest(input: TakeRestInput): string { const character = getCharacter(input.characterId); const results: string[] = []; if (input.restType === 'long') { // INTERSECTION 1: HP restoration if (input.restoreHp) { updateCharacter({ characterId: input.characterId, hp: character.maxHp }); results.push(`HP restored to ${character.maxHp}`); } // INTERSECTION 2: Remove exhaustion (1 level) const conditions = getActiveConditions(input.characterId); const exhaustion = conditions.find(c => c.condition === 'exhaustion'); if (exhaustion && exhaustion.exhaustionLevel! > 1) { manageCondition({ targetId: input.characterId, operation: 'remove', condition: 'exhaustion', exhaustionLevels: 1 }); results.push(`Exhaustion reduced to level ${exhaustion.exhaustionLevel! - 1}`); } // INTERSECTION 3: Restore spell slots if (input.restoreSpellSlots) { restoreAllSpellSlots(input.characterId); results.push('All spell slots restored'); } // INTERSECTION 4: Remove specified conditions if (input.clearConditions) { for (const condition of input.clearConditions) { manageCondition({ targetId: input.characterId, operation: 'remove', condition }); results.push(`Removed condition: ${condition}`); } } // INTERSECTION 5: Restore hit dice (half, rounded up) const hdToRestore = Math.ceil(character.level / 2); // ... restore hit dice ... results.push(`Restored ${hdToRestore} hit dice`); } if (input.restType === 'short') { // INTERSECTION 6: Spend hit dice for healing if (input.hitDiceToSpend) { const healing = rollHitDice(character, input.hitDiceToSpend); updateCharacter({ characterId: input.characterId, hp: Math.min(character.maxHp, character.hp + healing) }); results.push(`Healed ${healing} HP from ${input.hitDiceToSpend} hit dice`); } // INTERSECTION 7: Warlock slots (pact magic) if (character.pactMagic) { restorePactMagicSlots(input.characterId); results.push('Pact magic slots restored'); } } return createBox('REST COMPLETED', results, undefined, 'HEAVY'); } ``` **Dependencies:** ``` take_rest REQUIRES: - get_character (read state) - update_character (HP, hit dice) - manage_condition (clear conditions, reduce exhaustion) - manage_spell_slots (restore slots) - getActiveConditions (check exhaustion) ``` --- ### 9. LEVEL_UP ↔ CHARACTER ↔ SPELLS (**PLANNED - MEDIUM PRIORITY**) **Predicted Intersection:** Level-up affects multiple systems **Required Integration:** ```typescript export function levelUp(input: LevelUpInput): string { const character = getCharacter(input.characterId); const newLevel = input.targetLevel || character.level + 1; const results: string[] = []; // INTERSECTION 1: HP increase let hpGain: number; if (input.hpMethod === 'roll') { hpGain = rollDice({ expression: character.hitDie }); } else if (input.hpMethod === 'average') { hpGain = Math.ceil(parseInt(character.hitDie.split('d')[1]) / 2) + 1; } else if (input.hpMethod === 'max') { hpGain = parseInt(character.hitDie.split('d')[1]); } else { hpGain = input.manualHp!; } hpGain += calculateModifier(character.stats.con); updateCharacter({ characterId: input.characterId, level: newLevel, maxHp: character.maxHp + hpGain, hp: character.hp + hpGain, proficiencyBonus: Math.ceil(newLevel / 4) + 1 }); results.push(`Level ${newLevel}! +${hpGain} max HP`); // INTERSECTION 2: Spell slots const newSlots = calculateSpellSlotsForLevel(character.class, newLevel); // Update spell slots... results.push('Spell slots updated'); // INTERSECTION 3: New spells if (input.newSpells) { updateCharacter({ characterId: input.characterId, knownSpells: [...character.knownSpells!, ...input.newSpells] }); results.push(`Learned: ${input.newSpells.join(', ')}`); } return createBox('LEVEL UP', results, undefined, 'HEAVY'); } ``` --- ### 10. INVENTORY ↔ EQUIPMENT ↔ CHARACTER STATS (**PLANNED - HIGH PRIORITY**) **Predicted Intersection:** Items directly modify stats **Required Integration:** ```typescript // Item database schema: interface Item { id: string; name: string; type: 'weapon' | 'armor' | 'consumable' | 'wondrous'; // STAT MODIFIERS (same pattern as conditions!) mechanicalEffects?: { acBonus?: number; // Armor, Shield strBonus?: number; // Belt of Giant Strength dexBonus?: number; // Gloves of Dexterity speedModifier?: number; // Boots of Speed attackBonus?: number; // +1 Longsword damageBonus?: string; // +1d6 fire (Flame Tongue) savingThrowBonus?: number; // Ring of Protection advantageOn?: string[]; // Cloak of Elvenkind (stealth) }; // Equipment slots slot?: 'mainhand' | 'offhand' | 'armor' | 'head' | 'feet' | 'accessory'; // Consumables effect?: string; // "Heal 2d4+2 HP" usesRemaining?: number; } // INTEGRATION: export function calculateEquipmentStats(characterId: string, baseStats): EffectiveStatCalculation { const equipped = getEquippedItems(characterId); let effective = { ...baseStats }; const modifiers: string[] = []; for (const item of equipped) { if (item.mechanicalEffects) { if (item.mechanicalEffects.acBonus) { effective.ac += item.mechanicalEffects.acBonus; modifiers.push(`${item.name}: +${item.mechanicalEffects.acBonus} AC`); } if (item.mechanicalEffects.strBonus) { effective.stats.str = Math.max(effective.stats.str, item.mechanicalEffects.strBonus); modifiers.push(`${item.name}: STR = ${item.mechanicalEffects.strBonus}`); } // ... handle all effects } } return { base: baseStats, effective, modified: modifiers.length > 0, modifiers }; } ``` **Order of Operations for AC:** ``` Base (10) → + DEX modifier → + Armor (replaces base + DEX for heavy armor) → + Shield (+2) → + Magic armor bonus (+1) → + Magic item (Ring of Protection +1) → + Spell effects (Mage Armor, Shield) → + Conditions (none affect AC in 5e... YET) ``` --- ## Cross-Cutting Concerns ### A. Character Name Resolution (✅ **SOLVED**) **Problem:** Tools accept characterId (UUID) but users think in names **Solution:** All tools now accept EITHER `characterId` OR `characterName` ```typescript // Pattern used in update_character, manage_condition: let characterId = input.characterId; if (!characterId && input.characterName) { characterId = findCharacterByName(input.characterName); if (!characterId) throw new Error('Character not found'); } ``` **Remaining Work:** Extend to ALL character-referencing tools: - ✅ manage_condition - ✅ update_character - ✅ get_character - ⬜ roll_check - ⬜ take_rest - ⬜ level_up - ⬜ manage_inventory - ⬜ ALL combat tools (execute_action, apply_damage, etc.) --- ### B. Batch Operation Pattern (✅ **ESTABLISHED**) **Pattern:** Tools accept `single` OR `{ batch: single[] }` ```typescript // Established in: // - roll_dice: expression OR { batch: [...] } // - update_character: single OR { batch: [...] } // - manage_condition: single OR { batch: [...] } // TO EXTEND TO: // - execute_action (attack multiple targets) // - apply_damage (AoE spells) // - apply_healing (Mass Cure Wounds) // - manage_spell_slots (restore multiple characters) // - manage_inventory (loot distribution) ``` **Why Batch Matters:** - Fireball hits 6 enemies → 1 tool call, not 6 - DM applies exhaustion to whole party → 1 call - End of turn effects → 1 call per mechanic --- ### C. Persistence Strategy (**CRITICAL - MIXED STATUS**) **Current State:** - ✅ Characters: JSON files in AppData - ❌ Conditions: In-memory Map (lost on restart) - ❌ Encounters: Not implemented - ❌ Spell slots: Part of character JSON (good!) - ❌ Equipment: Not implemented - ❌ Session notes: Not implemented **Required Fix:** ```typescript // Option 1: Extend JSON persistence const PERSISTENCE_FILES = { characters: path.join(DATA_ROOT, 'characters/'), conditions: path.join(DATA_ROOT, 'conditions.json'), encounters: path.join(DATA_ROOT, 'encounters.json'), equipment: path.join(DATA_ROOT, 'equipment.json'), // Maps characterId → itemIds items: path.join(DATA_ROOT, 'items.json'), // Item database notes: path.join(DATA_ROOT, 'notes.json'), }; // Option 2: SQLite (as originally planned) // - Better for queries (search_notes, list_characters) // - Worse for direct file inspection // - Requires migration tooling ``` **Recommendation:** JSON for now, SQLite in Phase 2 --- ### D. Testing Intersections (**CRITICAL**) **Current Gap:** Integration tests don't exist **Required Test Suite:** ```typescript // tests/integration/condition-character.test.ts describe('Condition → Character Integration', () => { it('should show effective stats in get_character', () => { const charId = createCharacter({ name: 'Test' }); manageCondition({ targetId: charId, operation: 'add', condition: 'exhaustion', exhaustionLevels: 4 }); const result = getCharacter({ characterName: 'Test' }); expect(result.markdown).toContain('22/22'); // Halved from 44 expect(result.markdown).toContain('Exhaustion 4'); }); }); // tests/integration/damage-condition-concentration.test.ts describe('Damage → Condition → Concentration Chain', () => { it('should break concentration when unconscious', () => { // Cast concentration spell // Take damage to 0 HP // Verify unconscious added // Verify concentration broken }); }); // tests/integration/rest-multi-system.test.ts describe('Rest → Multi-System Integration', () => { it('should restore HP, slots, and remove exhaustion', () => { // Damage character, expend slots, add exhaustion // Long rest // Verify all restored }); }); ``` --- ## Recommended Implementation Order ### Phase 2: Equipment & Inventory (Next Sprint) 1. Implement `manage_inventory` (give/take/equip/unequip) 2. Create item database JSON 3. Extend `calculateEffectiveStats` to include equipment 4. Add equipment section to `get_character` output 5. Test equipment + condition interaction **Priority:** HIGH - Equipment is core to D&D gameplay ### Phase 3: Combat Orchestration 1. Implement `create_encounter` 2. Implement `execute_action` 3. Implement `apply_damage` with concentration checks 4. Implement encounter → character HP sync 5. Add condition persistence to disk **Priority:** HIGH - Combat is the main use case ### Phase 4: Magic System 1. Implement `manage_spell_slots` 2. Implement `check_concentration` / `break_concentration` 3. Implement `create_aura` 4. Integrate magic with combat actions **Priority:** MEDIUM - Spellcasting enhances combat ### Phase 5: Rest & Recovery 1. Implement `take_rest` with multi-system orchestration 2. Implement `level_up` with spell slot updates 3. Test long rest clearing conditions **Priority:** MEDIUM - Session transitions ### Phase 6: Session Management 1. Implement `add_session_note` 2. Implement `search_notes` 3. Implement `get_session_context` 4. Test note → character → condition linkage **Priority:** LOW - DM tooling, not player-facing --- ## Anti-Patterns to Avoid ### ❌ DON'T: Duplicate Stat Modification Logic ```typescript // BAD: Each system calculates AC independently function calculateAC_inCombat(char): number { return char.ac + armorBonus + conditionPenalty; } function calculateAC_inDisplay(char): number { return char.ac + equipmentBonus; } ``` **DO:** Centralize in `calculateEffectiveStats` ```typescript // GOOD: One source of truth function calculateEffectiveStats(charId, baseStats): EffectiveStats { let stats = { ...baseStats }; // Layer 1: Equipment stats = applyEquipmentBonuses(charId, stats); // Layer 2: Conditions stats = applyConditionEffects(charId, stats); // Layer 3: Effects (boons/curses) stats = applyCustomEffects(charId, stats); // Layer 4: Spells (active effects) stats = applySpellEffects(charId, stats); return stats; } ``` ### ❌ DON'T: Hard-Code Cross-Module Logic ```typescript // BAD: Conditions module knows about magic if (condition === 'incapacitated') { magic.breakConcentration(characterId); } ``` **DO:** Use event system or callbacks ```typescript // GOOD: Register hooks conditionHooks.on('conditionAdded', (charId, condition) => { if (['incapacitated', 'unconscious'].includes(condition)) { magicSystem.handleConditionChange(charId, condition); } }); ``` ### ❌ DON'T: Assume In-Memory State Persists ```typescript // BAD: Conditions lost on restart const conditionStore = new Map<string, ActiveCondition[]>(); ``` **DO:** Persist immediately ```typescript // GOOD: Save to disk after every change function setConditions(characterId: string, conditions: ActiveCondition[]) { conditionStore.set(characterId, conditions); saveConditionsToDisk(); // Persist immediately } ``` --- ## Success Metrics ✅ **Integration is successful when:** - get_character shows ALL modifiers from ALL systems - Damage correctly triggers concentration checks - Exhaustion persists across server restarts - Equipment bonuses combine correctly with conditions - Rest operations touch all relevant systems - Tests cover cross-module interactions --- _This is a living document. Update as new intersections are discovered._

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/ChatRPG'

If you have feedback or need assistance with the MCP directory API, please join our Discord server