Source: app/entity/NPC.js

'use strict';
/**
 * @module entity/NPC
 */
let Entity = require('./Entity');


/**
 * 
 * 
 * @param {any} x 
 * @param {any} y 
 * @param {any} key 
 * @constructor NPC
 */
function NPC(x, y, key) {
    Entity.call(this, x, y, key);

    /**
     * NPC Health.
     * 
     * Setting max HP to 100 by default.
     */
    this.maxHP = 100;
    this.HP = 100;
    this.sprintSpeed = 130;
    /**
     * Type of Entity
     */
    this.type = 'npc';

    /**
     * For all `NPC`s, I am defaulting the people they dislike
     * to monsters. This will be overridden in `Monster`.
     */
    this.dislike = ['monster'];

    /**
     * @todo(anand): Fix this hack
     * This is a major hack. We are setting all NPCs to IMMOVABLE!!
     */
    this.body.immovable = true;
}
NPC.prototype = Object.create(Entity.prototype);
NPC.prototype.constructor = NPC;

/**
 * This function tells an entity object to travel to a desired location
 * 
 * @param {number} x
 * @param {number} y
 * @param {navMesh} navMesh -navigation mesh object
 * @param {boolean} sprint if true go fast 
 * @return {boolean} return true if finished, otherwise false.
 */
NPC.prototype.gotoXY = function(x, y, navMesh, sprint = false) {
    // destination point
    const p2 = new Phaser.Point(x, y);
    // the entities location, respective to the center of its hit box
    const trueX = this.x+this.body.width /
    2 + this.body.offset.x;
    const trueY = this.y+this.body.height /
          2 + this.body.offset.y;
    const p1 = new Phaser.Point(trueX, trueY);
    // cool library magic
    const path = navMesh.findPath(p1, p2);
    /* 0. up
    *  1. right
    *  2. down
    *  3. left
    */
    if (path) {
        // check to see if the target location is reached within 5 units
        if (path.length === 2 && Math.abs(path[1].x - trueX) < 5
        && Math.abs(path[1].y - trueY) < 5) {
        if (this.state !== 'attacking') this.idleHere();
        return true;
}
    let currentTime = game.time.now;
    // limit the amount of direction changes to about 1 per 150 ms
    if (currentTime - this.directionLimiter >= 150) {
            // confusing code that ram won't understand
            Math.abs(path[1].x - trueX) >= Math.abs(path[1].y - trueY) ?
            this.moveInDirection(((path[1].x - trueX < 0)*2)+1, sprint) :
            this.moveInDirection((path[1].y - trueY > 0)*2, sprint);
            this.directionLimiter = currentTime;
    }
    } else {
        // if lost don't move
        this.idleHere();
        this.destinationX = undefined;
        this.destinationY = undefined;
    }
    return false;
};

/**
 * Allow an Entity object to wander 
 * @param {navMesh} navMesh the maps navigation mesh
 * @param {Phaser.Point} topLeft top left cornor of the bounds (x,y) default 0,0
 * @param {Phaser.Point} botRight bottom left cornor of the bounds (x,y) 
 * default map width,hieght
 */
NPC.prototype.wander = function(navMesh,
     topLeft = new Phaser.Point(0, 0),
      botRight = new Phaser.Point(game.world.width, game.world.height)) {
        if (this.state === 'dead') {
            return;
        }
    // check if the npc is still thinking about going somewhere
        if (this.idleTimer != 0) {
        this.idleTimer -= 1;
        return;
        }
    // check if the npc is en route, otherrwise find a new route
    if (!(this.destinationX && this.destinationY)) {
        this.destinationX = game.rnd.integerInRange(topLeft.x, botRight.x);
        this.destinationY = game.rnd.integerInRange(topLeft.y, botRight.y);
        if (Math.sqrt(Math.pow(this.destinationX - this.trueXY().x, 2) +
        Math.pow(this.destinationY - this.trueXY().y, 2)) > 256) {
            // reset if path is too long
            this.destinationX = undefined;
            this.destinationY = undefined;
        }
    }
    // if destination is reached, clear current destination.
    // add a random timer to wait before wandering elsewhere
    // max about 12 seconds
    if (this.gotoXY(this.destinationX, this.destinationY, navMesh)) {
        this.destinationX = undefined;
        this.destinationY = undefined;
        this.idleTimer = game.rnd.integerInRange(1, 1200);
    }
};
/**
 * Make the npc attack a target
 * @param {Entity} target target to attack
 * @param {navmesh} navMesh the navMesh of the map
 * @param {boolean=} sprint the navMesh of the map
 * @return {boolean} target no longer exists (probably dead)
 */
NPC.prototype.aggro = function(target, navMesh, sprint = false) {
    if (!target) {
        console.warn('target does not exist');
        this.idleHere();
        return true;
    }
    if (target.state === 'dead') {
        return true;
    }
    // check your location relative to target
    if (Math.abs(target.trueXY().x-this.trueXY().x)
     >= Math.abs(target.trueXY().y - this.trueXY().y)) {
         // approach from left
         if (target.trueXY().x-this.trueXY().x < 0) {
            if (this.gotoXY(target.trueXY().x+32, target.trueXY().y,
             navMesh, sprint)) {
                this.setDirection('left');
                this.attack();
            }
        } else {
            // approach from right
            if (this.gotoXY(target.trueXY().x-32, target.trueXY().y,
             navMesh, sprint)) {
                this.setDirection('right');
                this.attack();
            }
        }
    } else {
        // approach from up
        if (target.trueXY().y-this.trueXY().y < 0) {
            if (this.gotoXY(target.trueXY().x, target.trueXY().y+32,
             navMesh, sprint)) {
                this.setDirection('up');
                this.attack();
            }
        } else {
        // approach from down
            if (this.gotoXY(target.trueXY().x, target.trueXY().y-32,
             navMesh, sprint)) {
                this.setDirection('down');
                this.attack();
            }
        }
    }
};
/**
 * @param {navmesh} navMesh the navMesh of the map
 * @param {Phaser.Point} topLeft top left bounds the entity can wander in.
 * @param {Phaser.Point} botRight bottom right bounds the entity can wander in.
 * @param {Entity} player the player!
 * @param {string} behavior neutral or aggresive for now
 */
NPC.prototype.updateAI = function(navMesh, topLeft,
     botRight, player, behavior) {
    if (this.state === 'dead') return;
    this.aiStatus = behavior;
    switch (this.aiStatus) {
        case ('neutral'):
            this.wander(navMesh, topLeft, botRight);
            break;
        case ('aggressive'):
            if (Math.sqrt(Math.pow(player.trueXY().x - this.trueXY().x, 2) +
             Math.pow(player.trueXY().y - this.trueXY().y, 2)) < 192) {
                 // if in range aggro to player
                this.aggro(player, navMesh);
                this.aggroStatus = true;
            } else {
                if (this.aggroStatus) {
                    // aggro out of range, stop maybe? then continue wandering
                    this.idleHere();
                    this.aggroStatus = false;
                }
                // otherwise wander around
                this.wander(navMesh, topLeft, botRight);
            }
            break;
        default:
            console.warn('no behavior defined');
            break;
    }
};

module.exports = NPC;