Source

browser/lib/Ballistics/Ballistics.js

import { AudioFX } from '../Audio/AudioFX';
import { Bullet } from './Bullet';
import { Grenade } from './Grenade';
import { weapons } from './mappings';

/**
 * Ballistics handler.
 * @class
 * @category Game Ballistics
 */
export class Ballistics
{
  /**
   * Create new Ballistics handler
   */
  constructor () {

    /**
     * weapon - the equipped weapon
     * @type {Object}
     */
    this.weapon = null;

    /**
     * trigger - determines whether or not the weapon is triggered
     * @type {Object}
     */
    this.trigger = true;

    /**
     * frames - counter to control the update frequency for weapon actions
     * @type {number}
     */
    this.frames = 0;

    /**
     * projectiles - array of tracked projectiles currently on canvas
     * @type {array}
     */
    this.projectiles = [];

    /**
     * indexesToDelete - indexes of various projectile entities to remove
     * @type {array}
     */
    this.indexesToDelete = [];


    /**
     * Determine whether the weapons HUD has been initialized
     * @type {boolean}
     */
    this.displayInitialized = false;
  }

  /**
   * Render ballistics projectiles.
   * 
   * This is called every frame/repaint to render projectiles. Note that
   * projectiles are an animated entities, therefore their x,y coordinates
   * will change on update.
   * 
   * @returns {void}
   */
  render () {
    this.initializeWeaponsHUD();
    for (let i = 0; i < this.projectiles.length; i++) {
      this.projectiles[i].render(this.weapon.projectile.color);
    }
  }

  /**
   * Update game ballistics data, handle weapon triggering and projectile cleanup.
   * 
   * Checks the weapon state, sets the weapon stats, handles tiggered weapon actions
   * and audio. This is called every frame/repaint.
   * 
   * @param {Game} game - the managed game instance
   * 
   * @returns {void}
   */
  update (game) {
    this.weapon = weapons[game.selectedWeaponIndex];

    if (this.weapon && this.trigger && ! game.player.dead) {
      document.querySelector('#out-of-ammo').style.display = 'none';

      if (game.mouse.pressed) {
        this.handleFire(game.context, game.player);
      } else {
        AudioFX.stop('weapon');
      }
    } else {
      this.frames++;
      if (this.frames >= 60) {
        this.frames = 0;
        this.trigger = true;
      }
    }

    this.cleanupProjectiles(game.walls);
  }

  /**
   * Handle weapon fire.
   * 
   * @param {CanvasRenderingContext2D} context - the canvas rendering context
   * @param {Player} player - the player entity
   * 
   * @returns {void}
   */
  handleFire (context, player) {
    if (this.shouldReloadWeaponAmmoClip()) {
      return;
    }

    --this.weapon.clip.current;

    AudioFX.weapon({
      equippedWeapon: this.weapon
    }, 'fire', 1.5);

    this.registerProjectiles(context, player);
    this.trigger = this.weapon.trigger;

    this.setEquippedWeaponDisplayInformation();
  }

  /**
   * Initialize the weapons HUD.
   * 
   * @returns {void}
   */
  initializeWeaponsHUD () {
    if (! this.displayInitialized) {
      this.setEquippedWeaponDisplayInformation();
    }

    this.displayInitialized = true;
  }

  /**
   * Set equippred weapon display information e.g. ammo, magazines.
   * 
   * @returns {void}
   */
  setEquippedWeaponDisplayInformation (weaponsIndex = null) {
    const weapon = weaponsIndex !== null
      ? weapons[weaponsIndex]
      : this.weapon;

    if (! weapon) {
      return;
    }

    const { name, clip, magazines } = weapon;

    document.querySelector('#equipped-weapon').innerHTML = name;
    document.querySelector('#ammo-remaining').innerHTML = clip.current;
    document.querySelector('#ammo-capacity').innerHTML = clip.capacity;
    document.querySelector('#magazines-remaining').innerHTML = magazines.current;
    document.querySelector('#magazines-total').innerHTML = magazines.capacity;
  }

  /**
   * Determine if weapon needs to be reloaded and handle accordingly.
   * 
   * @returns {boolean}
   */
  shouldReloadWeaponAmmoClip () {
    if (this.weapon.clip.current <= 0) {
      if (this.weapon.magazines.current > 0) {
        --this.weapon.magazines.current;
        this.refillWeaponAmmoClip();
      } else {
        this.weapon.clip.current = 0;
        document.querySelector('#out-of-ammo').style.display = 'inline';

        return true;
      }
    }

    return false;
  }

  /**
   * Refill the curren ammo clip or replenish magazines.
   * 
   * @returns {void}
   */
  refillWeaponAmmoClip () {
    if (this.weapon.clip.current === this.weapon.clip.capacity) {
      if (this.weapon.magazines.current < this.weapon.magazines.capacity) {
        ++this.weapon.magazines.current;
      }
    } else {
      this.weapon.clip.current = this.weapon.clip.capacity;
    }

    this.setEquippedWeaponDisplayInformation();
  }

  /**
   * Register bullet projectiles for rendering and collision.
   * 
   * @param {CanvasRenderingContext2D} context - the canvas rendering context
   * @param {Player} player - the player entity
   * 
   * @returns {void}
   */
  registerProjectiles (context, player) {
    const { spread, delay } = this.weapon.projectile;
    for (let i = spread.min; i <= spread.max; i++) {

      const projectile = (this.weapon.type === 'throwable' && delay)
        ? new Grenade(context, player, i)
        : new Bullet(context, player, i);

      this.projectiles.push(projectile);
    }
  }

  /**
   * Clean up handled projectiles to stop rendering and updates.
   * 
   * @param {Wall[]} walls - the rendered walls
   * 
   * @returns {void}
   */
  cleanupProjectiles (walls) {
    this.indexesToDelete = [];
    for (let i = 0; i < this.projectiles.length; i++) {
      this.projectiles[i].update(walls, this.weapon.projectile.dropoff);
      if (this.projectiles[i].markToDelete) {
        this.indexesToDelete.push(i);
      }
    }

    for (let i = 0; i < this.indexesToDelete.length; i++) {
      this.projectiles.splice(this.indexesToDelete[i], 1);
    }
  }
}