import { z } from 'zod';
import { FoundryClient } from '../foundry-client.js';
import { Logger } from '../logger.js';
import { ErrorHandler } from '../utils/error-handler.js';
export interface ActorCreationToolsOptions {
foundryClient: FoundryClient;
logger: Logger;
}
export class ActorCreationTools {
private foundryClient: FoundryClient;
private logger: Logger;
private errorHandler: ErrorHandler;
constructor({ foundryClient, logger }: ActorCreationToolsOptions) {
this.foundryClient = foundryClient;
this.logger = logger.child({ component: 'ActorCreationTools' });
this.errorHandler = new ErrorHandler(this.logger);
}
/**
* Tool definitions for actor creation operations
*/
getToolDefinitions() {
return [
{
name: 'create-actor-from-compendium',
description: 'Create one or more actors from a specific compendium entry with custom names. Use search-compendium first to find the exact creature you want, then use this tool with the packId and itemId from the search results.',
inputSchema: {
type: 'object',
properties: {
packId: {
type: 'string',
description: 'ID of the compendium pack containing the creature (e.g., "dnd5e.monsters")',
},
itemId: {
type: 'string',
description: 'ID of the specific creature entry within the pack (get this from search-compendium results)',
},
names: {
type: 'array',
items: { type: 'string' },
description: 'Custom names for the created actors (e.g., ["Flameheart", "Sneak", "Peek"])',
minItems: 1,
},
quantity: {
type: 'number',
description: 'Number of actors to create (default: based on names array length)',
minimum: 1,
maximum: 10,
},
addToScene: {
type: 'boolean',
description: 'Whether to add created actors to the current scene as tokens',
default: false,
},
placement: {
type: 'object',
description: 'Token placement options (only used when addToScene is true)',
properties: {
type: {
type: 'string',
enum: ['random', 'grid', 'center', 'coordinates'],
description: 'Placement strategy',
default: 'grid',
},
coordinates: {
type: 'array',
items: {
type: 'object',
properties: {
x: { type: 'number', description: 'X coordinate in pixels' },
y: { type: 'number', description: 'Y coordinate in pixels' },
},
required: ['x', 'y'],
},
description: 'Specific coordinates for each token (required when type is "coordinates")',
},
},
required: ['type'],
},
},
required: ['packId', 'itemId', 'names'],
},
},
{
name: 'get-compendium-entry-full',
description: 'Retrieve complete stat block data including items, spells, and abilities for actor creation',
inputSchema: {
type: 'object',
properties: {
packId: {
type: 'string',
description: 'Compendium pack identifier',
},
entryId: {
type: 'string',
description: 'Entry identifier within the pack',
},
},
required: ['packId', 'entryId'],
},
},
];
}
/**
* Handle actor creation from specific compendium entry
*/
async handleCreateActorFromCompendium(args: any): Promise<any> {
const schema = z.object({
packId: z.string().min(1, 'Pack ID cannot be empty'),
itemId: z.string().min(1, 'Item ID cannot be empty'),
names: z.array(z.string().min(1)).min(1, 'At least one name is required'),
quantity: z.number().min(1).max(10).optional(),
addToScene: z.boolean().default(false),
placement: z.object({
type: z.enum(['random', 'grid', 'center', 'coordinates']).default('grid'),
coordinates: z.array(z.object({
x: z.number(),
y: z.number(),
})).optional(),
}).optional(),
});
const { packId, itemId, names, quantity, addToScene, placement } = schema.parse(args);
const finalQuantity = quantity || names.length;
this.logger.info('Creating actors from specific compendium entry', {
packId,
itemId,
names,
quantity: finalQuantity,
addToScene,
});
try {
// Ensure we have enough names for the quantity
const customNames = [...names];
while (customNames.length < finalQuantity) {
const baseName = names[0] || 'Unnamed';
customNames.push(`${baseName} ${customNames.length + 1}`);
}
// Create the actors via Foundry module using exact pack/item IDs
const result = await this.foundryClient.query('foundry-mcp-bridge.createActorFromCompendium', {
packId,
itemId,
customNames: customNames.slice(0, finalQuantity),
quantity: finalQuantity,
addToScene,
placement: placement ? {
type: placement.type,
coordinates: placement.coordinates,
} : undefined,
});
this.logger.info('Actor creation completed', {
totalCreated: result.totalCreated,
totalRequested: result.totalRequested,
tokensPlaced: result.tokensPlaced || 0,
hasErrors: !!result.errors,
});
// Format response for Claude
return this.formatSimpleActorCreationResponse(result, packId, itemId, customNames.slice(0, finalQuantity));
} catch (error) {
this.errorHandler.handleToolError(error, 'create-actor-from-compendium', 'actor creation');
}
}
/**
* Handle getting full compendium entry data
*/
async handleGetCompendiumEntryFull(args: any): Promise<any> {
const schema = z.object({
packId: z.string().min(1, 'Pack ID cannot be empty'),
entryId: z.string().min(1, 'Entry ID cannot be empty'),
});
const { packId, entryId } = schema.parse(args);
this.logger.info('Getting full compendium entry', { packId, entryId });
try {
const fullEntry = await this.foundryClient.query('foundry-mcp-bridge.getCompendiumDocumentFull', {
packId,
documentId: entryId,
});
this.logger.debug('Successfully retrieved full compendium entry', {
packId,
entryId,
name: fullEntry.name,
hasItems: !!fullEntry.items?.length,
hasEffects: !!fullEntry.effects?.length,
});
return this.formatCompendiumEntryResponse(fullEntry);
} catch (error) {
this.errorHandler.handleToolError(error, 'get-compendium-entry-full', 'compendium retrieval');
}
}
/**
* Format compendium entry response
*/
private formatCompendiumEntryResponse(entry: any): any {
const itemsInfo = entry.items?.length > 0
? `\n📦 Items: ${entry.items.map((item: any) => item.name).join(', ')}`
: '';
const effectsInfo = entry.effects?.length > 0
? `\n✨ Effects: ${entry.effects.map((effect: any) => effect.name).join(', ')}`
: '';
return {
name: entry.name,
type: entry.type,
pack: entry.packLabel,
system: entry.system,
fullData: entry.fullData,
items: entry.items || [],
effects: entry.effects || [],
summary: `📊 **${entry.name}** (${entry.type} from ${entry.packLabel})${itemsInfo}${effectsInfo}`,
};
}
/**
* Format simplified actor creation response
*/
private formatSimpleActorCreationResponse(result: any, packId: string, itemId: string, customNames: string[]): any {
const summary = `✅ Created ${result.totalCreated} of ${result.totalRequested} requested actors`;
const details = result.actors.map((actor: any) =>
`• **${actor.name}** (from ${packId})`
).join('\n');
const sceneInfo = result.tokensPlaced > 0
? `\n🎯 Added ${result.tokensPlaced} tokens to the current scene`
: '';
const errorInfo = result.errors?.length > 0
? `\n⚠️ Issues: ${result.errors.join(', ')}`
: '';
return {
summary,
success: result.success,
details: {
actors: result.actors,
sourceEntry: {
packId,
itemId,
},
tokensPlaced: result.tokensPlaced || 0,
errors: result.errors,
},
message: summary + '\n\n' + details + sceneInfo + errorInfo,
};
}
}