Source: Frontend/game/common/monster.mjs

/* global ml */
/* eslint-disable max-len,curly,complexity,prefer-template, no-mixed-operators */
/** @module Monster */

// The maximum amount of ms we want a monster to walk for
const MAX_WALK_TIME = 1500;

import PlayerCommon from "./player.mjs";
import interpolate from "./interpolator.mjs";

const HP_PER_TYPE = {
  boss: 500,
  blue: 200,
  red: 100,
  green: 30
};

export default class MonsterCommon {

  constructor(name_in, hp_in, damage_in, floor_in, id_in, type_in) {
    this.name = name_in;
    this.hp = hp_in;
    this.damage = damage_in;
    this.floor = floor_in;
    this.id = id_in;
    this.type = type_in;
    this.hpMax = HP_PER_TYPE[type_in];

    this.targetAquired = false; // "in pursuit" boolean
    this.x = 0; // (x,y) = upper left pixel coordinate
    this.y = 0;
    this.targetx = -1; // location where monster wants to move
    this.targety = -1;
    this.alive = true;
    this.lastAttackTime = new Date().getTime();
    this.size = 1; // size multiplier

    // SPEED: 100 = regular, 50 = slow
    this.speed = 200;
    if(this.type === "blue") { // slow monsters
      this.speed = 100;
    } else if(this.type === "boss") { // very slow
      this.speed = 75;
      this.size = 2;
      this.name = "boss";
    }
  }

  /**
   * Returns true if monster can see given pixel coordinate.
   * @param x
   * @param y
   */
  canSee(x, y) {
    let x1 = -1, x2 = -1, y1 = -1, y2 = -1;
    let cornerx = this.x, cornery = this.y; // upper left
    for(let i = 0; i < 4; i++) {
      if(i === 1) {
        cornerx += MonsterCommon.SPRITE_SIZE * this.size; // upper right
      } else if(i === 2) {
        cornery += MonsterCommon.SPRITE_SIZE * this.size; // lower right
      } else if(i === 3) {
        cornerx -= MonsterCommon.SPRITE_SIZE * this.size; // lower left
      }
      if(x < cornerx) {
        x1 = x;
        x2 = cornerx;
      } else {
        x1 = cornerx;
        x2 = x;
      }
      if(y < cornery) {
        y1 = y;
        y2 = cornery;
      } else {
        y1 = cornery;
        y2 = y;
      }
      for(let j = x1; j <= x2; j += 20) { // checking every 20th pixel to improve runtime
        for(let k = y1; k <= y2; k += 20) {
          if(!this.floor.map.isOnMap(j, k, true)) {
            return false;
          }
        }
      }
    }
    return true;
  }

  /**
   * Finds distance between monster and coodinate.
   * @param x
   * @param y
   * @return {double}
   */
  findDistance(x, y) {
    let asquared = Math.pow(x - this.x, 2);
    let bsquared = Math.pow(y - this.y, 2);
    let c = Math.sqrt(asquared + bsquared);
    return c;
  }

  /**
   * Finds closest PC that can be seen and targets it.
   */
  canSeePC() {
    this.targetAquired = false;
    let minDist = -1;
    for(let player of this.floor.players) {
      if(this.canSee(player.x, player.y) && this.canSee(player.x + player.SPRITE_SIZE * player.size, player.y + player.SPRITE_SIZE * player.size) &&
         this.canSee(player.x + player.SPRITE_SIZE * player.size, player.y) && this.canSee(player.x, player.y + player.SPRITE_SIZE * player.size)) {
        this.targetAquired = true;
        if(this.findDistance(player.x, player.y) < minDist || minDist === -1) {
          this.targetx = player.x;
          this.targety = player.y;
          minDist = this.findDistance(player.x, player.y);
        }
      }
    }
    return minDist !== -1;
  }

  /**
   * Monster moves to an adjacent, unoccupied location.
   */
  wander() {
    let prev = {x: this.x, y: this.y};
    let dist;
    // the distance we want to travel
    let targetDist = this.speed * (MAX_WALK_TIME / 1000);
    let count = 1000;

    // the angle of the direction we want to travel in
    // eslint-disable-next-line
    let theta = Math.floor(Math.random() * 360) * (Math.PI / 180) - Math.PI;
    Object.assign(this, prev);

    // start moving in that direction until we reach our target or a wall
    do {
      this.x += Math.cos(theta);
      this.y += Math.sin(theta);
      // eslint-disable-next-line
      dist = Math.sqrt(Math.abs(prev.x - this.x) ** 2 + Math.abs(prev.y - this.y) ** 2);
    } while(this.spriteIsOnMap() && dist < targetDist && --count > 0);

    // Back up until we are back on the map
    do {
      this.x -= Math.cos(theta);
      this.y -= Math.sin(theta);
      // eslint-disable-next-line
      dist = Math.sqrt(Math.abs(prev.x - this.x) ** 2 + Math.abs(prev.y - this.y) ** 2);
    } while(!this.spriteIsOnMap() && dist > 0 && --count > 0);

    this.targetx = Math.floor(this.x);
    this.targety = Math.floor(this.y);
    Object.assign(this, prev);

    // Don't go anywhere if we ran for too long
    if(count === 0) {
      this.targetx = this.x;
      this.targety = this.y;
    }
  }

  /**
   * Sets the position closer to the target position.
   * @param {number} deltaTime The number of ms since the last move
   */
  move(deltaTime) {
    let prevy = this.y;
    let prevx = this.x;
    if(this.alive) {
      interpolate(this, deltaTime, this.targetx, this.targety);
    }
    // Disable collision detection on the client
    if(typeof window === "undefined") {
      let collisionMonster = this.collisionEntities(this.floor.monsters, MonsterCommon.SPRITE_SIZE);
      let collisionPlayer = this.collisionEntities(this.floor.players, PlayerCommon.SPRITE_SIZE);
      if(collisionMonster !== -1 || collisionPlayer !== -1) {
        this.targetx = prevx;
        this.targety = prevy;
        this.x = prevx;
        this.y = prevy;
        this.collision = true;
        let currentTime = new Date().getTime();
        if(collisionPlayer !== -1 && currentTime - this.lastAttackTime >= 187) { // attacks max evey 0.75 seconds
          this.lastAttackTime = currentTime;
          this.attack(collisionPlayer);
        }
      } else {
        this.collision = false;
      }
      if(!this.spriteIsOnMap()) {
        this.x = prevx;
        this.y = prevy;
        this.targetx = prevx;
        this.targety = prevy;
      }
    }
  }

  /**
   * Moves monster.
   * If PC has been seen, move strategically towards last seen location.
   * Else (if PC not seen yet or last seen PC location has been explored) the monster wanders.
   */
  figureOutWhereToGo() {
    this.canSeePC();
    if(this.targetAquired) {
      ml.logger.debug(`Monster ${this.id} targeting player at (${this.targetx}, ${this.targety})`, ml.tags.monster);
    }
    if(this.alive) {
      if(!this.targetAquired && !this.collision) {
        if(this.targetx === -1 || this.targety === -1) {
          this.targetx = this.x;
          this.targety = this.y;
        }
        if(Math.abs(this.x - this.targetx) < 2 && Math.abs(this.y - this.targety) < 2) {
          this.wander();
        }
        ml.logger.debug(`Monster ${this.id} wandering to (${this.targetx}, ${this.targety})`, ml.tags.monster);
      }
    }
  }

  /**
   * Player attacks Monster
   * @param {*} hp health points that the monster's health decrements by
   */
  beAttacked(hp) {
    this.hp -= hp;
    if(this.hp <= 0) {
      this.die();
    }
    ml.logger.verbose(`Monster ${this.id} was attacked with ${hp} damage (hp: ${this.hp})`, ml.tags.monster);
  }

  /**
   * Monster attacks PC
   * @param {*} playerID id for player that monster is attacking
   */
  attack(playerID) {
    this.floor.players[playerID].beAttacked(this.damage);
  }

  /**
   * Places monster in a random "room" with no other monsters.
   */
  placeInRandomRoom(calls = 0) {
    let numRooms = this.floor.map.rooms.length;
    this.initialRoom = Math.floor(Math.random() * numRooms);
    let randomDiffX = Math.floor(Math.random() * this.floor.map.rooms[this.initialRoom].width);
    this.x = this.floor.map.rooms[this.initialRoom].x + randomDiffX;
    let randomDiffY = Math.floor(Math.random() * this.floor.map.rooms[this.initialRoom].height);
    this.y = this.floor.map.rooms[this.initialRoom].y + randomDiffY;
    if(!this.spriteIsOnMap() || this.collisionEntities(this.floor.monsters, MonsterCommon.SPRITE_SIZE) !== -1 ||
        this.collisionEntities(this.floor.players || [], MonsterCommon.SPRITE_SIZE) !== -1)
      this.placeInRandomRoom(calls + 1);
    if(this.type === "boss" && this.floor.map.rooms[this.initialRoom].height === this.floor.map.rooms[this.initialRoom].width)
      this.placeInRandomRoom(calls + 1);
  }

  /**
   * Check to see if whole sprite is on the map.
   * @returns {boolean}
   */
  spriteIsOnMap() {
    return this.floor.map.isOnMap(this.x, this.y, true) && this.floor.map.isOnMap(this.x + MonsterCommon.SPRITE_SIZE * this.size, this.y, true)
    && this.floor.map.isOnMap(this.x, this.y + MonsterCommon.SPRITE_SIZE * this.size, true) && this.floor.map.isOnMap(this.x + MonsterCommon.SPRITE_SIZE * this.size, this.y + MonsterCommon.SPRITE_SIZE * this.size, true);
  }

  /**
   * Checks to see if there's a monster colliding with this monster.
   * Compares corners of each sprite to do so.
   * @returns {boolean}
   */
  collisionEntities(entities, spriteSize) {
    let x = -1;
    let y = -1;
    for(let entity of entities) {
      if(this !== entity) {
        for(let j = 0; j < 4; j++) { // four corners to check for each sprite
          if(j === 0) { // upper left corner
            x = entity.x;
            y = entity.y;
          } else if(j === 1) { // upper right corner
            x = entity.x + spriteSize * entity.size;
            y = entity.y;
          } else if(j === 2) { // lower right corner
            x = entity.x + spriteSize * entity.size;
            y = entity.y + spriteSize * entity.size;
          } else if(j === 3) { // lower left corner
            x = entity.x;
            y = entity.y + spriteSize * entity.size;
          }
          if(x >= this.x && x <= this.x + spriteSize * entity.size) { // within x bounds
            if(y >= this.y && y <= this.y + spriteSize * entity.size) { // and within y bounds
              let index = entities.indexOf(entity);
              ml.logger.debug(`Monster ${this.id} at (${this.x}, ${this.y}) collided with entity ${index} at (${entity.x}, ${entity.y})`, ml.tags.monster);
              return index;
            }
          }
        }
      }
    }
    return -1; // indicate no collision
  }

  /**
   * Manually set coordinates for a single
   */
  setCoodinates(x, y) {
    this.x = x;
    this.y = y;
  }
}

MonsterCommon.SPRITE_SIZE = 48;