Source: states/StateHandler.js

/**
 * @fileoverview Base state handler class and utilities
 * @module StateHandler
 */

import { Emotions, Config } from '../constants.js'

/**
 * Abstract base class for state handlers
 * @abstract
 * @class
 */
export class StateHandler {
  /**
   * Create a state handler
   * @param {string} stateName - The name of the state
   * @param {OwenAnimationContext} context - The animation context
   */
  constructor (stateName, context) {
    /**
     * The name of this state
     * @type {string}
     */
    this.stateName = stateName

    /**
     * The animation context
     * @type {OwenAnimationContext}
     */
    this.context = context

    /**
     * Currently playing animation clip
     * @type {AnimationClip|null}
     */
    this.currentClip = null

    /**
     * Nested state information
     * @type {Object|null}
     */
    this.nestedState = null
  }

  /**
   * Enter this state
   * @abstract
   * @param {string|null} [_fromState=null] - The previous state (unused in base class)
   * @param {string} [_emotion=Emotions.NEUTRAL] - The emotion to enter with (unused in base class)
   * @returns {Promise<void>}
   * @throws {Error} Must be implemented by subclasses
   */
  async enter (_fromState = null, _emotion = Emotions.NEUTRAL) {
    throw new Error('enter method must be implemented by subclasses')
  }

  /**
   * Exit this state
   * @abstract
   * @param {string|null} [_toState=null] - The next state (unused in base class)
   * @param {string} [_emotion=Emotions.NEUTRAL] - The emotion to exit with (unused in base class)
   * @returns {Promise<void>}
   * @throws {Error} Must be implemented by subclasses
   */
  async exit (_toState = null, _emotion = Emotions.NEUTRAL) {
    throw new Error('exit method must be implemented by subclasses')
  }

  /**
   * Update this state (called every frame)
   * @param {number} _deltaTime - Time elapsed since last update (ms, unused in base class)
   * @returns {void}
   */
  update (_deltaTime) {
    // Override in subclasses if needed
  }

  /**
   * Handle a user message while in this state
   * @param {string} _message - The user message (unused in base class)
   * @returns {Promise<void>}
   */
  async handleMessage (_message) {
    // Override in subclasses if needed
  }

  /**
   * Get available transitions from this state
   * @returns {string[]} Array of state names that can be transitioned to
   */
  getAvailableTransitions () {
    return []
  }

  /**
   * Wait for an animation clip to finish playing
   * @protected
   * @param {AnimationClip} clip - The animation clip to wait for
   * @returns {Promise<void>} Promise that resolves when the clip finishes
   */
  async waitForClipEnd (clip) {
    return new Promise((resolve) => {
      const checkFinished = () => {
        if (!clip.isPlaying()) {
          resolve()
        } else {
          requestAnimationFrame(checkFinished)
        }
      }
      checkFinished()
    })
  }

  /**
   * Stop the currently playing clip
   * @protected
   * @param {number} [fadeOutDuration] - Fade out duration
   * @returns {Promise<void>}
   */
  async stopCurrentClip (fadeOutDuration = Config.DEFAULT_FADE_OUT) {
    if (this.currentClip) {
      await this.currentClip.stop(fadeOutDuration)
      this.currentClip = null
    }
  }
}