pxlNav/hud/HUD.js

// Heads-Up-Display (HUD) class for PxlNav
//   Written by Kevin Edzenga, 2025
// -- -- -- -- -- -- -- -- -- -- -- --
//
// Init() is called after pxlRoom's have loaded and post processing has been applied
//   All room items and collider object triggers are loaded and ready for HUD visual display
//
// Initial pass on this class is handling events within the class
//   Will move to EventManager class once the HUD is fully functional
//     (EventManager class isn't fully integrated yet)
//


import { DEVICE_ACTION, HUD_DRAW, HUD_ELEMENT } from "../core/Enums.js";

import { ElementBase } from "./elements/elementBase.js";
import { DragRegion } from "./elements/dragRegion.js";
import { Thumbstick } from "./elements/thumbstick.js";


/**
 * Class to handle a Heads-Up Display (HUD) management for pxlNav.
 *   Examples should be ran from your `pxlRoom.build()` function --
 * @alias pxlHUD
 * @class
 * @description HUD drawing and management
 * @example
 * // Add a button to the HUD from your room
 * let buttonData = { 'style': ['my-button-style'] };
 * this.pxlHUD.addItem('myButton', HUD_ELEMENT.BUTTON, buttonData, () => {
 *   console.log('Button clicked!');
 * });
 * @example
 * // Add a draggable region to the HUD from your room
 * let dragData = { 'style': ['my-drag-style'] };
 * this.pxlHUD.addItem('myDragRegion', HUD_ELEMENT.DRAG_REGION, dragData, ( data ) => {
 *   // data = { type = pxlEnum, name='myDragRegion', value={ x: number, y: number } };
 *   console.log('Drag delta / relative offset : ', data.value );
 * });
 * @example
 * // Add a thumbstick to the HUD from your room
 * let thumbData = { 'style': ['my-thumbstick-style'] };
 * this.pxlHUD.addItem('myThumbstick', HUD_ELEMENT.THUMBSTICK, thumbData, ( data ) => {
 *   // data = { type = pxlEnum, name='myThumbstick', value={ x: number (-1 to 1), y: number (-1 to 1) } };
 *   console.log('Thumbstick value : ', data.value );
 * });
 */
export class HUD{
  /**
   * Create a new HUD instance.
   */
  constructor(){
    /**
     * @type {Object|null}
     */
    this.pxlOptions = null;
    /**
     * @type {Object|null}
     */
    this.pxlEnums = null;
    /**
     * @type {Object|null}
     */
    this.pxlGuiDraws = null;
    /**
     * @type {Object|null}
     */
    this.pxlDevice = null;

    /**
     * @type {HTMLElement|null}
     */
    this.hudParent = null;
    /**
     * @type {Object}
     */
    this.huds = {};

    /**
     * @type {Object}
     */
    this.mobileHUD = {};


  }

  // -- -- --

  /**
   * Set dependencies for the HUD.
   * @param {Object} pxlNav - The pxlNav instance containing dependencies.
   * @private
   */
  setDependencies( pxlNav ){
    this.pxlOptions = pxlNav.pxlOptions;
    this.pxlEnums = pxlNav.pxlEnums;
    this.pxlGuiDraws = pxlNav.pxlGuiDraws;
    this.pxlDevice = pxlNav.pxlDevice;
  }

  // -- -- --

  /**
   * Initialize the HUD.
   * @private
   */
  init(){

    let hudParent = document.createElement('div');
    hudParent.classList.add('pxlNav-hud-parent');
    this.pxlGuiDraws.addToContainer( hudParent );
    this.hudParent = hudParent;

    // Add mobile hud elements
    //   Thumbsticks, jump + run empty regions
    if( this.pxlOptions.mobile && !this.pxlOptions.staticCamera ){
      hudParent.classList.add('pxlNav-hud-parent-mobile');
      this.createMobileHUD();
    }
  }

  /**
   * Add an element to the HUD DOM Object.
   * @method
   * @memberof pxlHUD
   * @param {HTMLElement|Object} hudElement - The HUD element to add.
   * @example
   * //Add an created element to the managed HUD or specific parent
   * let newButton = this.pxlHUD.createButton('myButton', buttonData, ( data )=>{
   *   console.log('Button clicked!');
   * });
   * 
   * this.pxlHUD.addToHUD( newButton );
   */
  addToHUD( hudElement ){
    if( hudElement.hasOwnProperty('block') && hudElement.block !== null ){
      this.hudParent.appendChild( hudElement.block );
    }else if( hudElement?.object !== null ){
      this.hudParent.appendChild( hudElement.object );
    }else{
      this.hudParent.appendChild( hudElement );
    }
  }

  // -- -- --

  /**
   * Add an item to the HUD.
   * @method
   * @memberof pxlHUD
   * @param {string} [label='default'] - The label for the HUD item.
   * @param {string} type - The type of HUD element.
   * @param {Object} data - The data for the HUD element.
   * @param {Function|null} [callbackFn=null] - The callback function for the HUD element.
   * @param {Object|null} [parentObj=null] - The parent object for the HUD element.
   * @returns {Object|null} The created HUD element.
   * @example
   * // Add a button to the HUD
   * //   Subscription passed through the `addItem()` function
   * //   Its preferer you use this function over directly creating HUD elements
   * //     As pxlHUD can manage the HUD elements and their subscriptions
   * let buttonData = { 'style': ['my-button-style'] };
   * this.pxlHUD.addItem('myButton', HUD_ELEMENT.BUTTON, buttonData, () => {
   *   console.log('Button clicked!');
   * });
   * 
   * // Add a draggable region to the HUD
   * //   Subscription called after hud element creation
   * let dragData = { 'style': ['my-drag-style'] };
   * this.pxlHUD.addItem('myDragRegion', HUD_ELEMENT.DRAG_REGION, dragData);
   *  // [...] // Later in your code
   * this.pxlHUD.subscribe('myDragRegion', ( data ) => {
   *   console.log('Drag delta / relative offset : ', data.value );
   * });
   */
  addItem( label='default', type, data, callbackFn = null, parentObj = null ){
    if( !this.huds.hasOwnProperty( label ) ){
      this.huds[ label ] = [];
    }
    let curRegion = null;

    switch( type ){
      case HUD_ELEMENT.REGION:
        curRegion = this.createRegion( label, data, callbackFn, parentObj );
        this.huds[ label ].push( curRegion );
        break;
      case HUD_ELEMENT.DRAG_REGION:
        curRegion = this.createDragRegion( label, data, callbackFn, parentObj );
        this.huds[ label ].push( curRegion );
        break;
      case HUD_ELEMENT.BUTTON:
        curRegion = this.createButton( label, data, callbackFn, parentObj );
        this.huds[ label ].push( curRegion );
        break;
      case HUD_ELEMENT.THUMBSTICK:
        curRegion = this.createThumbstick( label, data, callbackFn, parentObj );
        this.huds[ label ].push( curRegion );
        break;
      case HUD_ELEMENT.SLIDER:
        curRegion = this.createSlider( label, data, callbackFn, parentObj );
        this.huds[ label ].push( curRegion );
        break;
      case HUD_ELEMENT.IMAGE:
        curRegion = this.createImage( label, data, callbackFn, parentObj );
        this.huds[ label ].push( curRegion );
        break;
      case HUD_ELEMENT.TEXT:
        curRegion = this.createText( label, data, callbackFn, parentObj );
        this.huds[ label ].push( curRegion );
        break;
      default:
        break;
    }

    if( curRegion !== null ){

      if( parentObj !== null ){
        parentObj.appendChild( curRegion.object );
      }else{
        // Add new HUD element to the DOM
        this.addToHUD( curRegion );
      }

      // Subscribe to the HUD element if callbackFn provided
      if( callbackFn !== null ){
        curRegion.subscribe( callbackFn );
      }
    }

    return curRegion;
  }

  // -- -- --

  /**
   * Create a region element.
   * @method
   * @memberof pxlHUD
   * @param {string} label - The label for the region.
   * @param {Object} data - The data for the region.
   * @param {Function|null} [callbackFn=null] - The callback function for the region.
   * @returns {Object} The created region element.
   * @example
   * // Create a region element
   * let regionData = { 'style': ['my-region-style'] };
   * let newRegion = this.pxlHUD.createRegion('myRegion', regionData, ( data )=>{
   *   console.log( 'Region clicked!' );
   *   console.log( data );
   * });
   * this.pxlHUD.addToHUD( newRegion );
   */
  createRegion( label, data, callbackFn=null, parentObj=null ){
    let region = new ElementBase( label, data );
    region.setDependencies( { 'pxlOptions':this.pxlOptions, 'pxlEnums':this.pxlEnums, 'pxlDevice':this.pxlDevice } );
    region.build();

    if(callbackFn !== null){
      region.subscribe( callbackFn );
    }
    if( parentObj !== null ){
      parentObj.appendChild( region.object );
    }

    return region;
  }

  /**
   * Create a draggable region element.
   * @method
   * @memberof pxlHUD
   * @param {string} label - The label for the draggable region.
   * @param {Object} data - The data for the draggable region.
   * @param {Function|null} [callbackFn=null] - The callback function for the draggable region.
   * @returns {Object} The created draggable region element.
   * @example
   * // Create a draggable region element
   * let dragData = { 'style': ['my-drag-style'] };
   * let newDragRegion = this.pxlHUD.createDragRegion('myDragRegion', dragData, ( data )=>{
   *  console.log( 'Drag delta / relative offset {X,Y} :' );
   *  console.log( data.value );
   * });
   * this.pxlHUD.addToHUD( newDragRegion );
   */
  createDragRegion( label, data, callbackFn=null, parentObj=null ){
    let dragRegion = new DragRegion( label, data );
    dragRegion.setDependencies( { 'pxlOptions':this.pxlOptions, 'pxlEnums':this.pxlEnums, 'pxlDevice':this.pxlDevice } );
    dragRegion.build();

    if(callbackFn !== null){
      dragRegion.subscribe( callbackFn );
    }
    if( parentObj !== null ){
      parentObj.appendChild( dragRegion.object );
    }

    return dragRegion;
  }

  /**
   * Create a button element.
   * @method
   * @memberof pxlHUD
   * @param {string} label - The label for the button.
   * @param {Object} data - The data for the button.
   * @param {Function|null} [callbackFn=null] - The callback function for the button.
   * @returns {Object} The created button element.
   * @example
   * // Create a button element
   * let buttonData = { 'style': ['my-button-style'] };
   * let newButton = this.pxlHUD.createButton('myButton', buttonData, ( data )=>{
   *   console.log( 'Button clicked!' );
   * });
   * this.pxlHUD.addToHUD( newButton );
   */
  createButton( label, data, callbackFn=null, parentObj=null ){
    let button = new ElementBase( label, data );
    button.setDependencies( { 'pxlOptions':this.pxlOptions, 'pxlEnums':this.pxlEnums, 'pxlDevice':this.pxlDevice } );
    button.build();

    if(callbackFn !== null){
      button.subscribe( callbackFn );
    }
    if( parentObj !== null ){
      parentObj.appendChild( button.object );
    }

    return button;
  }

  /**
   * Create a thumbstick element.
   * @method
   * @memberof pxlHUD
   * @param {string} label - The label for the thumbstick.
   * @param {Object} data - The data for the thumbstick.
   * @param {Function|null} [callbackFn=null] - The callback function for the thumbstick.
   * @returns {Object} The created thumbstick element.
   * @example
   * // Create a thumbstick element
   * let thumbstickData = { 'style': ['my-thumbstick-style'] };
   * let newThumbstick = this.pxlHUD.createThumbstick('myThumbstick', thumbstickData, ( data )=>{
   *   console.log( 'Thumbstick value changed!' );
   *   console.log( data );
   * });
   * this.pxlHUD.addToHUD( newThumbstick );
   */
  createThumbstick( label, data, callbackFn=null ){

    let thumbstickObj = new Thumbstick( label, data );
    thumbstickObj.setDependencies( { 'pxlOptions':this.pxlOptions, 'pxlEnums':this.pxlEnums, 'pxlDevice':this.pxlDevice } );
    thumbstickObj.build();

    // Connect thumbstick interactions to listeners
    if( callbackFn !== null ){
      thumbstickObj.subscribe( callbackFn );
    }

    return thumbstickObj;
  }

  /**
   * Create a slider element.
   * @method
   * @memberof pxlHUD
   * @param {string} label - The label for the slider.
   * @param {Object} data - The data for the slider.
   * @param {Function|null} [callbackFn=null] - The callback function for the slider.
   * @returns {Object} The created slider element.
   * @example
   * // Create a slider element
   * let sliderData = { 'min': 0, 'max': 100, 'value': 50, 'style': ['my-slider-style'] };
   * let newSlider = this.pxlHUD.createSlider('mySlider', sliderData, ( data )=>{
   *   console.log( 'Slider value changed!' );
   *   console.log( data );
   * });
   * this.pxlHUD.addToHUD( newSlider );
   */
  createSlider( label, data, callbackFn=null, parentObj=null ){
    let slider = new ElementBase( label, data );
    slider.setDependencies( { 'pxlOptions':this.pxlOptions, 'pxlEnums':this.pxlEnums, 'pxlDevice':this.pxlDevice } );
    slider.build();

    if(callbackFn !== null){
      slider.subscribe( callbackFn );
    }
    if( parentObj !== null ){
      parentObj.appendChild( slider.object );
    }

    return slider;
  }

  /**
   * Create an image element.
   * @method
   * @memberof pxlHUD
   * @param {string} label - The label for the image.
   * @param {Object} data - The data for the image.
   * @param {Function|null} [callbackFn=null] - The callback function for the image.
   * @returns {Object} The created image element.
   * @example
   * // Create an image element
   * let imageData = { 'src': 'path/to/image.png', 'style': ['my-image-style'] };
   * let newImage = this.pxlHUD.createImage('myImage', imageData, ( data )=>{
   *   console.log( 'Image clicked!' );
   * });
   * this.pxlHUD.addToHUD( newImage );
   */
  createImage( label, data, callbackFn=null, parentObj=null ){
    let image = new ElementBase( label, data );
    image.setDependencies( { 'pxlOptions':this.pxlOptions, 'pxlEnums':this.pxlEnums, 'pxlDevice':this.pxlDevice } );
    image.build();

    if(callbackFn !== null){
      image.subscribe( callbackFn );
    }
    if( parentObj !== null ){
      parentObj.appendChild( image.object );
    }

    return image;
  }

  /**
   * Create a text element.
   * @method
   * @memberof pxlHUD
   * @param {string} label - The label for the text.
   * @param {Object} data - The data for the text.
   * @param {Function|null} [callbackFn=null] - The callback function for the text.
   * @returns {Object} The created text element.
   * @example
   * // Create a text element
   * let textData = { 'text': 'Hello, world!', 'style': ['my-text-style'] };
   * let newText = this.pxlHUD.createText('myText', textData, ( data )=>{
   *   console.log( 'Text clicked!' );
   * });
   * this.pxlHUD.addToHUD( newText );
   */
  createText( label, data, callbackFn=null, parentObj=null ){
    let text = new ElementBase( label, data );
    text.setDependencies( { 'pxlOptions':this.pxlOptions, 'pxlEnums':this.pxlEnums, 'pxlDevice':this.pxlDevice } );
    text.build();

    if(callbackFn !== null){
      text.subscribe( callbackFn );
    }
    if( parentObj !== null ){
      parentObj.appendChild( text.object );
    }

    return text;
  }

  // -- -- --

  /**
   * Create mobile HUD elements.
   * @private
   */
  createMobileHUD(){
    let curId = '';
    let curElement = '';
    let curData = {};

    let mobileHudParent = document.createElement('div');
    mobileHudParent.classList.add('pxlNav-hudMobile-parent');
    mobileHudParent.classList.add();

    this.hudParent.appendChild( mobileHudParent );

    let noneState = this.pxlEnums.HUD_ACTION.NONE;
    let hoverState = this.pxlEnums.HUD_ACTION.HOVER;
    let activeState = this.pxlEnums.HUD_ACTION.ACTIVE;

    // Virtual thumbsticks with dragable inner peg
    curId = 'thumbstick_left';
    curData = {
       'style': [ 'pxlNav-hudMobile-joystick-left' ],
       'objectStyles': [
          {
            'object': 'block',
          },
          {
            'object': 'parent',
          },
          {
            'object': 'inner',
          },
        ]
      };
      curData['objectStyles'][0][noneState] = ['pxlNav-hudElement-thumbstick-block-default'];
      curData['objectStyles'][0][hoverState] = ['pxlNav-hudElement-thumbstick-block-hover'];
      curData['objectStyles'][0][activeState] = ['pxlNav-hudElement-thumbstick-block-active'];

      curData['objectStyles'][1][noneState] = ['pxlNav-hudElement-thumbstick-default'];
      curData['objectStyles'][1][hoverState] = ['pxlNav-hudElement-thumbstick-hover'];
      curData['objectStyles'][1][activeState] = ['pxlNav-hudElement-thumbstick-active', 'pxlNav-hudElement-thumbstick-left'];

      curData['objectStyles'][2][noneState] = ['pxlNav-hudElement-thumbstick-inner-default'];
      curData['objectStyles'][2][hoverState] = ['pxlNav-hudElement-thumbstick-inner-hover'];
      curData['objectStyles'][2][activeState] = ['pxlNav-hudElement-thumbstick-inner-active'];

    
    curElement = this.addItem( curId, HUD_ELEMENT.THUMBSTICK, Object.assign({},curData) );

    // Connect movement joystick to device actions
    curElement.subscribe(( deltas )=>{
      this.mobileDeligate( DEVICE_ACTION.MOVE, deltas );
    });

    this.mobileHUD[ curId ] = curElement;

    // -- -- --

    curId =  'thumbstick_right';
    
    curData = Object.assign({},curData);
    curData['style'] = [ 'pxlNav-hudMobile-joystick-right' ];
    curData['objectStyles'][1] = { 'object': 'parent' };
    curData['objectStyles'][1][noneState] = ['pxlNav-hudElement-thumbstick-default'];
    curData['objectStyles'][1][hoverState] = ['pxlNav-hudElement-thumbstick-hover'];
    curData['objectStyles'][1][activeState] = ['pxlNav-hudElement-thumbstick-active', 'pxlNav-hudElement-thumbstick-right'];

    curElement = this.addItem( curId, HUD_ELEMENT.THUMBSTICK, curData );

    // Connect look joystick to device actions
    curElement.subscribe(( deltas )=>{
      this.mobileDeligate( DEVICE_ACTION.LOOK, deltas );
    });

    this.mobileHUD[ curId ] = curElement;

    // -- -- --


    // Empty tap reagions for mobile jump & run
    curId = 'mobile_jump';
    curData = { 'style': [ 'pxlNav-hudMobile-region-jump' ] };
    curElement = this.addItem( curId, HUD_ELEMENT.REGION, curData, ( e )=>{
      this.mobileDeligate( DEVICE_ACTION.JUMP, {}, e );
    } );

    this.mobileHUD[ 'jump' ] = curElement;

    // -- -- --

    curId =  'mobile_ACTION';
    curData = { 'style': [ 'pxlNav-hudMobile-region-action' ] };
    curElement = this.addItem( curId, HUD_ELEMENT.REGION, curData );

    // Connect item region to device actions
    curElement.subscribe(( e )=>{
      this.mobileDeligate( DEVICE_ACTION.ACTION, e );
    });

    this.mobileHUD[ 'item' ] = curElement;



  }

  // -- -- --

  /**
   * Handle mobile HUD interactions to the device.
   * @param {pxlEnum} eventType - The type of event.
   * @param {Object} data - The data for the event.
   * @private
   */
  mobileDeligate( eventType, data={}, status=true ){
    this.pxlDevice.deviceAction( eventType, data, status );
  }


  // -- -- --

  /**
   * Subscribe to a HUD element's events.
   * @method
   * @memberof pxlHUD
   * @param {string} label - The label of the HUD element.
   * @param {Function} callbackFn - The callback function to subscribe.
   * @example
   * // Subscribe to a HUD element
   * let newButton = this.pxlHUD.addItem( 'myButton', HUD_ELEMENT.BUTTON );
   * this.pxlHUD.subscribe( 'myButton', ( data )=>{
   *   console.log( 'Button clicked!' );
   * });
   */
  subscribe( label, callbackFn ){
    if( this.huds.hasOwnProperty( label ) ){
      if( !this.huds[ label ].hasOwnProperty('callbacks') ){
        this.huds[ label ].callbacks = [];
      }
      this.huds[ label ].callbacks.push( callbackFn );
    }
  }

  /**
   * Emit an event for a HUD element.
   * @method
   * @memberof pxlHUD
   * @param {string} label - The label of the HUD element.
   * @param {Object} data - The data for the event.
   */
  emit( label, data ){
    if( this.huds.hasOwnProperty( label ) && this.huds[ label ].hasOwnProperty('callbacks') ){
      this.huds[ label ].callbacks.forEach( (fn)=>{
        fn( data );
      });
    }
  }

}