Files
gh-dev-gom-claude-code-mark…/skills/scripts/src/cli/commands/animation.ts
2025-11-29 18:19:28 +08:00

603 lines
20 KiB
TypeScript

/**
* 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>', 'GameObject name or path')
.option('--state <name>', 'State name to play (Animator)')
.option('--clip <name>', 'Clip name to play (legacy Animation)')
.option('--layer <n>', 'Layer index (default: 0)', '0')
.option('--time <value>', 'Normalized start time (0-1)')
.option('--speed <value>', 'Playback speed', '1')
.option('--json', 'Output in JSON format')
.option('--timeout <ms>', '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<string, unknown> = {
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<PlayResult>(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>', 'GameObject name or path')
.option('--clip <name>', 'Specific clip to stop (legacy Animation)')
.option('--reset', 'Reset to default pose')
.option('--json', 'Output in JSON format')
.option('--timeout <ms>', '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<StopResult>(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>', 'GameObject name or path')
.option('--layer <n>', 'Layer index (default: 0)', '0')
.option('--json', 'Output in JSON format')
.option('--timeout <ms>', '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<StateResult>(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>', 'GameObject name or path')
.option('--json', 'Output in JSON format')
.option('--timeout <ms>', '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<ParametersResult>(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>', 'GameObject name or path')
.argument('<parameter>', 'Parameter name')
.option('--json', 'Output in JSON format')
.option('--timeout <ms>', '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<ParameterResult>(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>', 'GameObject name or path')
.argument('<parameter>', 'Parameter name')
.argument('<value>', 'New value (float, int, bool)')
.option('--json', 'Output in JSON format')
.option('--timeout <ms>', '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<SetParameterResult>(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>', 'GameObject name or path')
.argument('<trigger>', 'Trigger name')
.option('--json', 'Output in JSON format')
.option('--timeout <ms>', '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<TriggerResult>(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>', 'GameObject name or path')
.argument('<trigger>', 'Trigger name')
.option('--json', 'Output in JSON format')
.option('--timeout <ms>', '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<TriggerResult>(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>', 'GameObject name or path')
.argument('<state>', 'State name to transition to')
.option('--duration <seconds>', 'Transition duration', '0.25')
.option('--layer <n>', 'Layer index (default: 0)', '0')
.option('--offset <value>', 'Normalized time offset')
.option('--json', 'Output in JSON format')
.option('--timeout <ms>', '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<string, unknown> = {
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<CrossFadeResult>(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)}`);
}
}
}
});
}