/** * @fileoverview Main animation context controller * @module core */ import { States, Emotions, Config } from '../constants.js'; /** * Main controller for the Owen animation system * Manages state transitions, animation playback, and user interactions * @class */ export class OwenAnimationContext { /** * Create an Owen animation context * @param {THREE.Object3D} model - The 3D character model * @param {THREE.AnimationMixer} mixer - The Three.js animation mixer * @param {AnimationClipFactory} animationClipFactory - Factory for creating clips * @param {StateFactory} stateFactory - Factory for creating state handlers */ constructor(model, mixer, animationClipFactory, stateFactory) { /** * The 3D character model * @type {THREE.Object3D} */ this.model = model; /** * The Three.js animation mixer * @type {THREE.AnimationMixer} */ this.mixer = mixer; /** * Factory for creating animation clips * @type {AnimationClipFactory} */ this.animationClipFactory = animationClipFactory; /** * Factory for creating state handlers * @type {StateFactory} */ this.stateFactory = stateFactory; /** * Map of animation clips by name * @type {Map} */ this.clips = new Map(); /** * Map of state handlers by name * @type {Map} */ this.states = new Map(); /** * Current active state * @type {string} */ this.currentState = States.WAIT; /** * Current active state handler * @type {StateHandler|null} */ this.currentStateHandler = null; /** * Timer for inactivity detection * @type {number} */ this.inactivityTimer = 0; /** * Inactivity timeout in milliseconds * @type {number} */ this.inactivityTimeout = Config.INACTIVITY_TIMEOUT; /** * Whether the system is initialized * @type {boolean} */ this.initialized = false; } /** * Initialize the animation system * @returns {Promise} */ async initialize() { if (this.initialized) return; // Create animation clips from model this.clips = await this.animationClipFactory.createClipsFromModel(this.model); // Create actions for all clips for (const [ , clip ] of this.clips) { clip.createAction(this.mixer); } // Initialize state handlers this.initializeStates(); // Start in wait state await this.transitionTo(States.WAIT); this.initialized = true; console.log('Owen Animation System initialized'); } /** * Initialize all state handlers * @private * @returns {void} */ initializeStates() { const stateNames = this.stateFactory.getAvailableStates(); for (const stateName of stateNames) { const handler = this.stateFactory.createStateHandler(stateName, this); this.states.set(stateName, handler); } } /** * Transition to a new state * @param {string} newStateName - The name of the state to transition to * @param {string} [emotion=Emotions.NEUTRAL] - The emotion for the transition * @returns {Promise} * @throws {Error} If state is not found or transition is invalid */ async transitionTo(newStateName, emotion = Emotions.NEUTRAL) { if (!this.states.has(newStateName)) { throw new Error(`State '${newStateName}' not found`); } const oldState = this.currentState; const newStateHandler = this.states.get(newStateName); console.log(`Transitioning from ${oldState} to ${newStateName}`); // Exit current state if (this.currentStateHandler) { await this.currentStateHandler.exit(newStateName, emotion); } // Enter new state this.currentState = newStateName; this.currentStateHandler = newStateHandler; await this.currentStateHandler.enter(oldState, emotion); // Reset inactivity timer this.resetActivityTimer(); } /** * Handle a user message * @param {string} message - The user message * @returns {Promise} */ async handleUserMessage(message) { console.log(`Handling user message: "${message}"`); this.onUserActivity(); // If sleeping, wake up first if (this.currentState === States.SLEEP) { await this.transitionTo(States.REACT); } // Let current state handle the message if (this.currentStateHandler) { await this.currentStateHandler.handleMessage(message); } // Transition to appropriate next state based on current state if (this.currentState === States.WAIT) { await this.transitionTo(States.REACT); } else if (this.currentState === States.REACT) { await this.transitionTo(States.TYPE); } } /** * Called when user activity is detected * @returns {void} */ onUserActivity() { this.resetActivityTimer(); // Wake up if sleeping if (this.currentState === States.SLEEP) { this.transitionTo(States.WAIT); } } /** * Reset the inactivity timer * @private * @returns {void} */ resetActivityTimer() { this.inactivityTimer = 0; } /** * Handle inactivity timeout * @private * @returns {Promise} */ async handleInactivity() { console.log('Inactivity detected, transitioning to sleep'); await this.transitionTo(States.SLEEP); } /** * Update the animation system (call every frame) * @param {number} deltaTime - Time elapsed since last update (ms) * @returns {void} */ update(deltaTime) { if (!this.initialized) return; // Update Three.js mixer this.mixer.update(deltaTime / 1000); // Convert to seconds // Update current state if (this.currentStateHandler) { this.currentStateHandler.update(deltaTime); } // Update inactivity timer this.inactivityTimer += deltaTime; if (this.inactivityTimer > this.inactivityTimeout && this.currentState !== States.SLEEP) { this.handleInactivity(); } } /** * Get an animation clip by name * @param {string} name - The animation clip name * @returns {AnimationClip|undefined} The animation clip or undefined if not found */ getClip(name) { return this.clips.get(name); } /** * Get animation clips matching a pattern * @param {string} pattern - Pattern to match (supports * wildcards) * @returns {AnimationClip[]} Array of matching clips */ getClipsByPattern(pattern) { const regex = new RegExp(pattern.replace(/\*/g, '.*')); const matches = []; for (const [ name, clip ] of this.clips) { if (regex.test(name)) { matches.push(clip); } } return matches; } /** * Get the current state name * @returns {string} The current state name */ getCurrentState() { return this.currentState; } /** * Get the current state handler * @returns {StateHandler|null} The current state handler */ getCurrentStateHandler() { return this.currentStateHandler; } /** * Get available transitions from current state * @returns {string[]} Array of available state transitions */ getAvailableTransitions() { if (this.currentStateHandler) { return this.currentStateHandler.getAvailableTransitions(); } return []; } /** * Get all available animation clip names * @returns {string[]} Array of clip names */ getAvailableClips() { return Array.from(this.clips.keys()); } /** * Get all available state names * @returns {string[]} Array of state names */ getAvailableStates() { return Array.from(this.states.keys()); } /** * Dispose of the animation system and clean up resources * @returns {void} */ dispose() { // Stop all animations for (const [ , clip ] of this.clips) { if (clip.action) { clip.action.stop(); } } // Clear caches this.clips.clear(); this.states.clear(); this.animationClipFactory.clearCache(); this.initialized = false; console.log('Owen Animation System disposed'); } }