/** * Animation command * * Animation control commands for Animator and legacy Animation */ import { Command } from 'commander'; import * as logger from '@/utils/logger'; import * as config from '@/utils/config'; import { createUnityClient } from '@/unity/client'; import { COMMANDS } from '@/constants'; import { outputJson } from '@/utils/output-formatter'; /** * Register Animation command */ export function registerAnimationCommand(program: Command): void { const animCmd = program .command('animation') .alias('anim') .description('Animation control commands'); // Play animation animCmd .command('play') .description('Play animation on a GameObject') .argument('', 'GameObject name or path') .option('--state ', 'State name to play (Animator)') .option('--clip ', 'Clip name to play (legacy Animation)') .option('--layer ', 'Layer index (default: 0)', '0') .option('--time ', 'Normalized start time (0-1)') .option('--speed ', 'Playback speed', '1') .option('--json', 'Output in JSON format') .option('--timeout ', 'WebSocket connection timeout in milliseconds', '30000') .action(async (gameObject, options) => { let client = null; try { const projectRoot = config.getProjectRoot(); const port = program.opts().port || config.getUnityPort(projectRoot); if (!port) { logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.'); process.exit(1); } client = createUnityClient(port); await client.connect(); interface PlayResult { success: boolean; gameObject: string; type: string; stateName?: string; clipName?: string; message: string; } const params: Record = { gameObject, layer: parseInt(options.layer, 10), speed: parseFloat(options.speed), }; if (options.state) params.stateName = options.state; if (options.clip) params.clipName = options.clip; if (options.time) params.normalizedTime = parseFloat(options.time); const result = await client.sendRequest(COMMANDS.ANIMATION_PLAY, params); if (options.json) { outputJson(result); } else { logger.info(`✓ ${result.message}`); logger.info(` Type: ${result.type}`); if (result.stateName) logger.info(` State: ${result.stateName}`); if (result.clipName) logger.info(` Clip: ${result.clipName}`); } } catch (error) { logger.error('Failed to play animation', error); process.exit(1); } finally { if (client) { try { client.disconnect(); } catch (disconnectError) { logger.debug(`Error during disconnect: ${disconnectError instanceof Error ? disconnectError.message : String(disconnectError)}`); } } } }); // Stop animation animCmd .command('stop') .description('Stop animation on a GameObject') .argument('', 'GameObject name or path') .option('--clip ', 'Specific clip to stop (legacy Animation)') .option('--reset', 'Reset to default pose') .option('--json', 'Output in JSON format') .option('--timeout ', 'WebSocket connection timeout in milliseconds', '30000') .action(async (gameObject, options) => { let client = null; try { const projectRoot = config.getProjectRoot(); const port = program.opts().port || config.getUnityPort(projectRoot); if (!port) { logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.'); process.exit(1); } client = createUnityClient(port); await client.connect(); interface StopResult { success: boolean; gameObject: string; type: string; message: string; } const result = await client.sendRequest(COMMANDS.ANIMATION_STOP, { gameObject, clipName: options.clip, resetToDefault: options.reset || false, }); if (options.json) { outputJson(result); } else { logger.info(`✓ ${result.message}`); logger.info(` Type: ${result.type}`); } } catch (error) { logger.error('Failed to stop animation', error); process.exit(1); } finally { if (client) { try { client.disconnect(); } catch (disconnectError) { logger.debug(`Error during disconnect: ${disconnectError instanceof Error ? disconnectError.message : String(disconnectError)}`); } } } }); // Get animation state animCmd .command('state') .alias('get-state') .description('Get current animation state') .argument('', 'GameObject name or path') .option('--layer ', 'Layer index (default: 0)', '0') .option('--json', 'Output in JSON format') .option('--timeout ', 'WebSocket connection timeout in milliseconds', '30000') .action(async (gameObject, options) => { let client = null; try { const projectRoot = config.getProjectRoot(); const port = program.opts().port || config.getUnityPort(projectRoot); if (!port) { logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.'); process.exit(1); } client = createUnityClient(port); await client.connect(); interface AnimatorStateResult { success: boolean; gameObject: string; type: string; enabled: boolean; speed: number; layer: number; currentState: { clipName: string; normalizedTime: number; length: number; speed: number; isLooping: boolean; }; isInTransition: boolean; layerCount: number; parameterCount: number; } interface AnimationStateResult { success: boolean; gameObject: string; type: string; isPlaying: boolean; clipCount: number; clips: Array<{ name: string; length: number; normalizedTime: number; speed: number; weight: number; enabled: boolean; wrapMode: string; }>; } type StateResult = AnimatorStateResult | AnimationStateResult; const result = await client.sendRequest(COMMANDS.ANIMATION_GET_STATE, { gameObject, layer: parseInt(options.layer, 10), }); if (options.json) { outputJson(result); } else { logger.info(`✓ Animation state for ${result.gameObject}:`); logger.info(` Type: ${result.type}`); if (result.type === 'Animator') { const r = result as AnimatorStateResult; logger.info(` Enabled: ${r.enabled}`); logger.info(` Speed: ${r.speed}`); logger.info(` Layer: ${r.layer}/${r.layerCount}`); logger.info(` In Transition: ${r.isInTransition}`); logger.info(` Parameters: ${r.parameterCount}`); if (r.currentState) { logger.info(` Current State:`); logger.info(` Clip: ${r.currentState.clipName}`); logger.info(` Time: ${(r.currentState.normalizedTime * 100).toFixed(1)}%`); logger.info(` Length: ${r.currentState.length.toFixed(2)}s`); logger.info(` Looping: ${r.currentState.isLooping}`); } } else { const r = result as AnimationStateResult; logger.info(` Playing: ${r.isPlaying}`); logger.info(` Clips: ${r.clipCount}`); for (const clip of r.clips) { logger.info(` ● ${clip.name} (${clip.length.toFixed(2)}s, ${clip.wrapMode})`); } } } } catch (error) { logger.error('Failed to get animation state', error); process.exit(1); } finally { if (client) { try { client.disconnect(); } catch (disconnectError) { logger.debug(`Error during disconnect: ${disconnectError instanceof Error ? disconnectError.message : String(disconnectError)}`); } } } }); // Get all parameters animCmd .command('params') .alias('get-params') .description('Get all Animator parameters') .argument('', 'GameObject name or path') .option('--json', 'Output in JSON format') .option('--timeout ', 'WebSocket connection timeout in milliseconds', '30000') .action(async (gameObject, options) => { let client = null; try { const projectRoot = config.getProjectRoot(); const port = program.opts().port || config.getUnityPort(projectRoot); if (!port) { logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.'); process.exit(1); } client = createUnityClient(port); await client.connect(); interface ParametersResult { success: boolean; gameObject: string; count: number; parameters: Array<{ name: string; type: string; value: unknown; }>; } const result = await client.sendRequest(COMMANDS.ANIMATION_GET_PARAMETERS, { gameObject, }); if (options.json) { outputJson(result); } else { logger.info(`✓ ${result.count} parameter(s) on ${result.gameObject}:`); for (const p of result.parameters) { const valueStr = p.value !== null ? `= ${p.value}` : ''; logger.info(` ● ${p.name} (${p.type}) ${valueStr}`); } } } catch (error) { logger.error('Failed to get parameters', error); process.exit(1); } finally { if (client) { try { client.disconnect(); } catch (disconnectError) { logger.debug(`Error during disconnect: ${disconnectError instanceof Error ? disconnectError.message : String(disconnectError)}`); } } } }); // Get single parameter animCmd .command('get-param') .description('Get an Animator parameter value') .argument('', 'GameObject name or path') .argument('', 'Parameter name') .option('--json', 'Output in JSON format') .option('--timeout ', 'WebSocket connection timeout in milliseconds', '30000') .action(async (gameObject, parameter, options) => { let client = null; try { const projectRoot = config.getProjectRoot(); const port = program.opts().port || config.getUnityPort(projectRoot); if (!port) { logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.'); process.exit(1); } client = createUnityClient(port); await client.connect(); interface ParameterResult { success: boolean; gameObject: string; parameterName: string; parameterType: string; value: unknown; } const result = await client.sendRequest(COMMANDS.ANIMATION_GET_PARAMETER, { gameObject, parameterName: parameter, }); if (options.json) { outputJson(result); } else { logger.info(`✓ ${result.parameterName} (${result.parameterType}): ${result.value}`); } } catch (error) { logger.error('Failed to get parameter', error); process.exit(1); } finally { if (client) { try { client.disconnect(); } catch (disconnectError) { logger.debug(`Error during disconnect: ${disconnectError instanceof Error ? disconnectError.message : String(disconnectError)}`); } } } }); // Set parameter animCmd .command('set-param') .description('Set an Animator parameter value') .argument('', 'GameObject name or path') .argument('', 'Parameter name') .argument('', 'New value (float, int, bool)') .option('--json', 'Output in JSON format') .option('--timeout ', 'WebSocket connection timeout in milliseconds', '30000') .action(async (gameObject, parameter, value, options) => { let client = null; try { const projectRoot = config.getProjectRoot(); const port = program.opts().port || config.getUnityPort(projectRoot); if (!port) { logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.'); process.exit(1); } client = createUnityClient(port); await client.connect(); interface SetParameterResult { success: boolean; gameObject: string; parameterName: string; parameterType: string; value: unknown; } // Parse value based on type let parsedValue: unknown = value; if (value === 'true') parsedValue = true; else if (value === 'false') parsedValue = false; else if (!isNaN(parseFloat(value))) parsedValue = parseFloat(value); const result = await client.sendRequest(COMMANDS.ANIMATION_SET_PARAMETER, { gameObject, parameterName: parameter, value: parsedValue, }); if (options.json) { outputJson(result); } else { logger.info(`✓ Set ${result.parameterName} = ${result.value}`); } } catch (error) { logger.error('Failed to set parameter', error); process.exit(1); } finally { if (client) { try { client.disconnect(); } catch (disconnectError) { logger.debug(`Error during disconnect: ${disconnectError instanceof Error ? disconnectError.message : String(disconnectError)}`); } } } }); // Set trigger animCmd .command('trigger') .alias('set-trigger') .description('Set an Animator trigger') .argument('', 'GameObject name or path') .argument('', 'Trigger name') .option('--json', 'Output in JSON format') .option('--timeout ', 'WebSocket connection timeout in milliseconds', '30000') .action(async (gameObject, trigger, options) => { let client = null; try { const projectRoot = config.getProjectRoot(); const port = program.opts().port || config.getUnityPort(projectRoot); if (!port) { logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.'); process.exit(1); } client = createUnityClient(port); await client.connect(); interface TriggerResult { success: boolean; gameObject: string; triggerName: string; message: string; } const result = await client.sendRequest(COMMANDS.ANIMATION_SET_TRIGGER, { gameObject, triggerName: trigger, }); if (options.json) { outputJson(result); } else { logger.info(`✓ Trigger '${result.triggerName}' set`); } } catch (error) { logger.error('Failed to set trigger', error); process.exit(1); } finally { if (client) { try { client.disconnect(); } catch (disconnectError) { logger.debug(`Error during disconnect: ${disconnectError instanceof Error ? disconnectError.message : String(disconnectError)}`); } } } }); // Reset trigger animCmd .command('reset-trigger') .description('Reset an Animator trigger') .argument('', 'GameObject name or path') .argument('', 'Trigger name') .option('--json', 'Output in JSON format') .option('--timeout ', 'WebSocket connection timeout in milliseconds', '30000') .action(async (gameObject, trigger, options) => { let client = null; try { const projectRoot = config.getProjectRoot(); const port = program.opts().port || config.getUnityPort(projectRoot); if (!port) { logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.'); process.exit(1); } client = createUnityClient(port); await client.connect(); interface TriggerResult { success: boolean; gameObject: string; triggerName: string; message: string; } const result = await client.sendRequest(COMMANDS.ANIMATION_RESET_TRIGGER, { gameObject, triggerName: trigger, }); if (options.json) { outputJson(result); } else { logger.info(`✓ Trigger '${result.triggerName}' reset`); } } catch (error) { logger.error('Failed to reset trigger', error); process.exit(1); } finally { if (client) { try { client.disconnect(); } catch (disconnectError) { logger.debug(`Error during disconnect: ${disconnectError instanceof Error ? disconnectError.message : String(disconnectError)}`); } } } }); // CrossFade animCmd .command('crossfade') .description('CrossFade to a state') .argument('', 'GameObject name or path') .argument('', 'State name to transition to') .option('--duration ', 'Transition duration', '0.25') .option('--layer ', 'Layer index (default: 0)', '0') .option('--offset ', 'Normalized time offset') .option('--json', 'Output in JSON format') .option('--timeout ', 'WebSocket connection timeout in milliseconds', '30000') .action(async (gameObject, state, options) => { let client = null; try { const projectRoot = config.getProjectRoot(); const port = program.opts().port || config.getUnityPort(projectRoot); if (!port) { logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.'); process.exit(1); } client = createUnityClient(port); await client.connect(); interface CrossFadeResult { success: boolean; gameObject: string; stateName: string; transitionDuration: number; message: string; } const params: Record = { gameObject, stateName: state, transitionDuration: parseFloat(options.duration), layer: parseInt(options.layer, 10), }; if (options.offset) params.normalizedTimeOffset = parseFloat(options.offset); const result = await client.sendRequest(COMMANDS.ANIMATION_CROSSFADE, params); if (options.json) { outputJson(result); } else { logger.info(`✓ CrossFade to '${result.stateName}' (${result.transitionDuration}s)`); } } catch (error) { logger.error('Failed to crossfade', error); process.exit(1); } finally { if (client) { try { client.disconnect(); } catch (disconnectError) { logger.debug(`Error during disconnect: ${disconnectError instanceof Error ? disconnectError.message : String(disconnectError)}`); } } } }); }