Source: Backend/game/player.mjs

/* global ml */
/* jshint node: true */
import PlayerCommon, {BASE_STATS} from '../../Frontend/game/common/player';
import PlayerModel from '../models/player';
import LobbyModel from '../models/lobby';
import UserModel from '../models/user';

/** @module backend/game/player */

const HP_BOOST_RATE = 1500;
const HP_BOOST_PERC = 0.05; // out of 1

/**
 * The player class.
 */
export default class Player extends PlayerCommon {
  constructor(...args) {
    super(...args);
    this._lastHpBoost = Date.now();
  }

  /**
   * Move the player to the spawn point
   */
  respawn() {
    do {
      let spawn = this.floor.map.getSpawnPoint();
      this.x = this._confirmedX = spawn.x;
      this.y = this._confirmedY = spawn.y;
    } while(!this.spriteIsOnMap());
  }

  /**
   * Private. Returns an object where the username of the player is the
   * key and the value is the player object.
   * @param {Floor} floor
   */
  static async getPlayers(floor) {
    let lobbyId = floor.id.slice(0, floor.id.indexOf('-'));
    let lobbies = await LobbyModel.findAll({
      where: {
        lobbyId: lobbyId
      }
    });
    let players = [];
    for(var lobby of lobbies) {
      let player = await PlayerModel.findOne({
        where: {
          id: lobby.player
        }
      });
      players.push(player);
    }
    let users = {};
    for(var player of players) {
      let user = await UserModel.findOne({
        where: {
          id: player.username
        }
      });
      users[user.username] = player;
    }
    return users;
  }

  /**
   * Loads all players
   * @param {Floor} floor - The floor to load the player for
   */
  static async load(floor) { // eslint-disable-line complexity
    /* Load every player in this lobby that is inGame */
    let players = await Player.getPlayers(floor);
    floor.players = [];
    for(var username of Object.keys(players)) {
      if(players[username].inGame) {
        if(players[username].hp) {
          let playerToPush = new Player(username, players[username].hp, players[username].spriteName, floor);
          if(players[username].x && players[username].y) {
            playerToPush.setCoordinates(players[username].x, players[username].y);
            playerToPush._confirmedX = players[username].x;
            playerToPush._confirmedY = players[username].y;
          } else {
            playerToPush.respawn();
          }
          floor.players.push(playerToPush);
        }
      }
    }
  }

  /**
   * Updates player records for all players in this lobby
   * @param {Floor} floor - The floor to save
   */
  static async saveAll(floor) {
    if(floor.players) {
      let players = await Player.getPlayers(floor);
      for(var username of Object.keys(players)) {
        let newValues = floor.players.find((playerClass) => {
          return playerClass.name === username;
        });

        if(!newValues) {
          // player died do something about that here
          await players[username].update({
            hp: 0
          });
          continue;
        }

        await players[username].update({
          spriteName: newValues.spriteName,
          x: newValues.x,
          y: newValues.y,
          hp: newValues.hp
        });
      }
    }
  }

  /**
   * Serialize player model for client
   */
  toJSON() {
    return {
      username: this.name,
      hp: this.hp,
      spriteName: this.spriteName,
      alive: this.alive,
      _confirmedId: this._confirmedId,
      _lastFrame: this._lastFrame,
      _confirmedX: this._confirmedX,
      _confirmedY: this._confirmedY,
      attackAngle: this.attackAngle,
      range: this.range,
      wearing: this.wearing,
      hpMax: this.hpMax,
      damage: this.damage,
      speed: this.speed,
      defence: this.defence
    };
  }

  /**
   * Player dies
   */
  die() {
    ml.logger.info(`Player ${this.name} died`, ml.tags.player);
    this.alive = false;
    /* Remove the player from the player array */
    let player = this.floor.players.indexOf(this);
    this.floor.players.splice(player, 1);
  }

  /**
   * Boost hp after you stop moving
   */
  move(...args) {
    super.move(...args);

    // Restore a player's in safe rooms
    let rect = this.floor.map.getRect(this.x, this.y) || {};
    if(Date.now() - this._lastHpBoost >= HP_BOOST_RATE && rect.noMonsters) {
      this._lastHpBoost = Date.now();
      this.hp = Math.min(this.hp + (this.hpMax * HP_BOOST_PERC), this.hpMax);
    }

    // Don't boost their health the second they enter a room
    if(!rect.noMonsters) {
      this._lastHpBoost = Date.now();
    }
  }

  /**
   * Removes items that have been worn past their due date
   */
  removeOldItems() {
    for(let wornItem of Object.values(this.wearing)) {
      try {
        if(Date.now() - wornItem.timeWorn > wornItem.maxWearTime) {
          wornItem.holder = false;
          this.wearing[wornItem.category] = null;
          ml.logger.verbose(`Player ${this.name} removed ${wornItem.spriteName}`, ml.tags.player);
          this.updateStats();
          this._reportStats();
        }
      } catch(error) {
        // Passing in case an item doesn't have a maxWearTime
      }
    }
  }

  /**
   * Player picks up nearby items that are not currently being worn
   */
  _pickupNearbyItems() {
    for(let item of this.floor.items) {
      if(
        item.getPosition() &&
        this._withinRadius(this.getPosition(), item.getPosition(), 12) &&
        !this.wearing[item.category]
      ) {
        item.pickup(this.name);
        ml.logger.verbose(`Player ${this.name} picked up a(n) ${item.spriteName}`, ml.tags.player);
        this.wieldItem(item);
      }
    }
  }

  /**
   * Updates the player's stats based on what is being worn.
   */
  updateStats() {
    this._setStatsToBase();
    for(let item of Object.values(this.wearing)) {
      if(item) {
        this.speed += item.movementSpeed;
        this.damage += item.damage;
        this.defence += item.defence;
        this.range += item.range;
      }
    }
  }

  wieldItem(item) {
    if(item.category === "key") {
      this.hasKey = true;
    }
    this.wearing[item.category] = item;
    this.updateStats();
    this._reportStats();
  }

  _setStatsToBase() {
    this.speed = BASE_STATS.speed;
    this.damage = BASE_STATS.damage;
    this.defence = BASE_STATS.defence;
    this.range = BASE_STATS.range;
  }

  _reportStats() {
    ml.logger.verbose(`${this.name}'s stats are now SP: ${this.speed}, DMG: ${this.damage}, DEF: ${this.defence}, RNG: ${this.range}`, ml.tags.player);
  }

  /**
   * Returns true if obj is within the desired radius of the center circle.
   *
   * @param {object} center - Coords to use that acts as the center of the circle
   * @param {object} obj - Object to compare to center's coord
   * @param {int} radius - the desired radius of the circle to check
   */
  _withinRadius(center, obj, radius) {
    let centerCoords = { x: Math.round(center.x), y: Math.round(center.y) };
    let objCoords = { x: Math.round(obj.x), y: Math.round(obj.y) };
    let hyp = Math.pow(objCoords.x - centerCoords.x, 2) + Math.pow(objCoords.y - centerCoords.y, 2);
    return hyp <= Math.pow(radius, 2);
  }
}