update_character
Modify D&D 5e character attributes including stats, level, equipment, and abilities to reflect gameplay changes and progression.
Instructions
Update an existing D&D 5e character with new stats, HP, level, equipment, etc.
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| characterId | No | ||
| characterName | No | ||
| name | No | ||
| race | No | ||
| class | No | ||
| level | No | ||
| background | No | ||
| characterType | No | ||
| stats | No | ||
| hp | No | ||
| healing | No | ||
| maxHp | No | ||
| ac | No | ||
| speed | No | ||
| resistances | No | ||
| immunities | No | ||
| vulnerabilities | No | ||
| conditionImmunities | No | ||
| spellcastingAbility | No | ||
| knownSpells | No | ||
| preparedSpells | No | ||
| cantrips | No | ||
| skillProficiencies | No | ||
| saveProficiencies | No | ||
| equipment | No | ||
| batch | No |
Implementation Reference
- src/modules/characters.ts:862-1036 (handler)Main execution logic for updating characters: handles single/batch updates, HP deltas/healing, stat changes, persistence to JSON files, derived stat recalculation, and formatted output.export function updateCharacter(input: UpdateCharacterInput): { success: boolean; character?: Character; markdown: string; error?: string; } { // Check if this is a batch operation if ('batch' in input && input.batch) { return updateCharacterBatch(input.batch); } // Type assertion: we know it's a single update at this point const singleInput = input as z.infer<typeof singleUpdateSchema>; const dataDir = path.join(DATA_ROOT, 'characters'); // Resolve character ID (either from direct ID or by name lookup) let characterId: string | undefined = singleInput.characterId; if (!characterId && singleInput.characterName) { const foundId = findCharacterByName(singleInput.characterName); if (!foundId) { return { success: false, markdown: '', error: `Character not found with name: ${singleInput.characterName}`, }; } characterId = foundId; } if (!characterId) { return { success: false, markdown: '', error: 'Character ID or name is required', }; } const filePath = path.join(dataDir, `${characterId}.json`); // Check if character exists if (!fs.existsSync(filePath)) { return { success: false, markdown: '', error: `Character not found: ${characterId}`, }; } // Read existing character let originalCharacter: Character; try { const fileContent = fs.readFileSync(filePath, 'utf-8'); originalCharacter = JSON.parse(fileContent); } catch (err) { const message = err instanceof Error ? err.message : String(err); return { success: false, markdown: '', error: `Failed to read character: ${message}`, }; } // Create updated character by merging changes const updatedCharacter: Character = { ...originalCharacter }; // Apply updates if (singleInput.name !== undefined) updatedCharacter.name = singleInput.name; if (singleInput.race !== undefined) updatedCharacter.race = singleInput.race; if (singleInput.class !== undefined) updatedCharacter.class = singleInput.class; if (singleInput.level !== undefined) updatedCharacter.level = singleInput.level; if (singleInput.background !== undefined) updatedCharacter.background = singleInput.background; if (singleInput.characterType !== undefined) updatedCharacter.characterType = singleInput.characterType; if (singleInput.ac !== undefined) updatedCharacter.ac = singleInput.ac; if (singleInput.speed !== undefined) updatedCharacter.speed = singleInput.speed; if (singleInput.resistances !== undefined) updatedCharacter.resistances = singleInput.resistances; if (singleInput.immunities !== undefined) updatedCharacter.immunities = singleInput.immunities; if (singleInput.vulnerabilities !== undefined) updatedCharacter.vulnerabilities = singleInput.vulnerabilities; if (singleInput.conditionImmunities !== undefined) updatedCharacter.conditionImmunities = singleInput.conditionImmunities; if (singleInput.spellcastingAbility !== undefined) updatedCharacter.spellcastingAbility = singleInput.spellcastingAbility; if (singleInput.knownSpells !== undefined) updatedCharacter.knownSpells = singleInput.knownSpells; if (singleInput.preparedSpells !== undefined) updatedCharacter.preparedSpells = singleInput.preparedSpells; if (singleInput.cantrips !== undefined) updatedCharacter.cantrips = singleInput.cantrips; if (singleInput.skillProficiencies !== undefined) updatedCharacter.skillProficiencies = singleInput.skillProficiencies; if (singleInput.saveProficiencies !== undefined) updatedCharacter.saveProficiencies = singleInput.saveProficiencies; if (singleInput.equipment !== undefined) updatedCharacter.equipment = singleInput.equipment; // Handle stats (partial update) if (singleInput.stats !== undefined) { updatedCharacter.stats = { ...originalCharacter.stats, ...singleInput.stats, }; } // Recalculate derived stats if needed const conModifier = calculateModifier(updatedCharacter.stats.con); // Recalculate proficiency bonus if level changed if (singleInput.level !== undefined) { updatedCharacter.proficiencyBonus = calculateProficiencyBonus(updatedCharacter.level); } // Recalculate maxHp if class, level, or CON changed if (singleInput.class !== undefined || singleInput.level !== undefined || singleInput.stats?.con !== undefined) { const newMaxHp = calculateMaxHP(updatedCharacter.class, updatedCharacter.level, conModifier); updatedCharacter.maxHp = newMaxHp; // If current HP is over the new max, cap it if (updatedCharacter.hp > newMaxHp) { updatedCharacter.hp = newMaxHp; } } // Apply maxHp override if provided if (singleInput.maxHp !== undefined) { updatedCharacter.maxHp = singleInput.maxHp; } // Apply healing if provided (explicit healing parameter takes precedence) if (singleInput.healing !== undefined) { const newHp = Math.min(updatedCharacter.maxHp, updatedCharacter.hp + singleInput.healing); updatedCharacter.hp = newHp; } // Apply HP update if provided (supports relative values like "+8" or "-15") else if (singleInput.hp !== undefined) { let newHp: number; if (typeof singleInput.hp === 'string') { // String delta notation (e.g., "+8" for healing, "-15" for damage) const delta = parseInt(singleInput.hp, 10); newHp = Math.max(0, Math.min(updatedCharacter.maxHp, updatedCharacter.hp + delta)); } else if (singleInput.hp < 0) { // Numeric delta notation (e.g., -12 for damage) // Negative numbers are always treated as deltas const delta = singleInput.hp; newHp = Math.max(0, Math.min(updatedCharacter.maxHp, updatedCharacter.hp + delta)); } else { // Absolute HP update (positive number or zero) newHp = singleInput.hp; // Validate HP doesn't exceed maxHp if (newHp > updatedCharacter.maxHp) { return { success: false, markdown: '', error: `HP (${newHp}) cannot exceed max HP (${updatedCharacter.maxHp})`, }; } } updatedCharacter.hp = newHp; } // Save updated character try { fs.writeFileSync(filePath, JSON.stringify(updatedCharacter, null, 2), 'utf-8'); } catch (err) { const message = err instanceof Error ? err.message : String(err); return { success: false, markdown: '', error: `Failed to save character: ${message}`, }; } // Format output showing before/after comparison const markdown = formatCharacterUpdate(originalCharacter, updatedCharacter, singleInput); return { success: true, character: updatedCharacter, markdown, }; }
- src/modules/characters.ts:101-149 (schema)Zod input schema definition supporting single character updates or batch (up to 20), with fields for all character properties including relative HP changes, equipment lists, resistances, spells, etc.const singleUpdateSchema = z.object({ characterId: z.string().min(1).optional(), characterName: z.string().min(1).optional(), name: z.string().min(1).optional(), race: z.string().optional(), class: z.string().optional(), level: z.number().min(1, 'Level must be at least 1').max(20, 'Level cannot exceed 20').optional(), background: z.string().optional(), characterType: z.enum(['pc', 'npc', 'enemy', 'neutral']).optional(), stats: z.object({ str: z.number().min(1).max(30).optional(), dex: z.number().min(1).max(30).optional(), con: z.number().min(1).max(30).optional(), int: z.number().min(1).max(30).optional(), wis: z.number().min(1).max(30).optional(), cha: z.number().min(1).max(30).optional(), }).optional(), hp: z.union([ z.string().regex(/^[+-]\d+$/, 'Relative HP must be in format "+5" or "-10"'), z.number() // Supports both absolute (positive) and delta (negative) values ]).optional(), healing: z.number().positive('Healing must be a positive number').optional(), maxHp: z.number().min(1).optional(), ac: z.number().optional(), speed: z.number().optional(), resistances: z.array(DamageTypeSchema).optional(), immunities: z.array(DamageTypeSchema).optional(), vulnerabilities: z.array(DamageTypeSchema).optional(), conditionImmunities: z.array(ConditionSchema).optional(), spellcastingAbility: AbilitySchema.optional(), knownSpells: z.array(z.string()).optional(), preparedSpells: z.array(z.string()).optional(), cantrips: z.array(z.string()).optional(), skillProficiencies: z.array(SkillSchema).optional(), saveProficiencies: z.array(AbilitySchema).optional(), equipment: z.array(z.string()).optional(), }).refine(data => data.characterId || data.characterName, { message: 'Either characterId or characterName must be provided', }); // Support both single and batch updates export const updateCharacterSchema = z.union([ singleUpdateSchema, z.object({ batch: z.array(singleUpdateSchema).min(1).max(20), }), ]); export type UpdateCharacterInput = z.infer<typeof updateCharacterSchema>;
- src/registry.ts:357-380 (registration)Tool registration entry in central registry: converts Zod schema to JSON schema, wraps handler with validation and error handling, provides description.update_character: { name: 'update_character', description: 'Update an existing D&D 5e character with new stats, HP, level, equipment, etc.', inputSchema: toJsonSchema(updateCharacterSchema), handler: async (args) => { try { const validated = updateCharacterSchema.parse(args); const result = updateCharacter(validated); if (!result.success) { return error(result.error || 'Failed to update character'); } return success(result.markdown); } 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); } }, },