Source: app/states/play.js

'use strict';
/**
 * @module states/Play
 */
const UI = require('../ui/ui');
const Player = require('../entity/Player');
const NavMesh = require('../ai/Nav-mesh.js');
const Monster = require('../entity/Monster');
const NPC = require('../entity/NPC');
const Factory = require('../factory/Factory');
const dataStore = require('../util/data');
const Map = require('../util/Map');
const Ripple = require('../ripple/engine');
let electron = require('electron');
let window = electron.remote.getCurrentWindow();

const Sampling = require('discrete-sampling');

const _ = require('lodash');
const npcBounds = [
    [new Phaser.Point(1397, 1344), new Phaser.Point(1684, 1472)],
    [new Phaser.Point(778, 1328), new Phaser.Point(1065, 1553)],
    [new Phaser.Point(1660, 735), new Phaser.Point(1690, 1065)],
    [new Phaser.Point(1800, 2200), new Phaser.Point(3000, 2700)],
];

const monsterBounds = [
    [new Phaser.Point(3510, 2907), new Phaser.Point(3908, 3458)],
    [new Phaser.Point(3464, 688), new Phaser.Point(3868, 1274)],
];

let loadedData = null;
let timerIDs = [];

let Play = {};

Play.init = function(data) {
    if (data) {
        loadedData = data;
    };
};

Play.setLoadData = function(data) {
    loadedData = data;
};

Play.preload = function() {
     /**
     * Map creation
     */
    this.map = game.add.tilemap('map');
    this.map.addTilesetImage('outdoors', 'tileset');
    this.bgLayer = this.map.createLayer('bgLayer');
    this.bgOverlap2 = this.map.createLayer('bgOverlap2');
    this.bgOverlap = this.map.createLayer('bgOverlap');
    this.blockOverlap = this.map.createLayer('blkOverlap');
    this.blockLayer = this.map.createLayer('blkLayer');
    game.add.existing(this.blockLayer);

    this.blockLayer.resizeWorld();
    this.bgLayer.resizeWorld();
    this.game = game;
    this.navMesh = new NavMesh(this.map);

    // Input for game
    this.keyboard = game.input.keyboard;
    this.keyboard.onDownCallback = ()=> {
        switch (game.input.keyboard.event.keyCode) {
            // 27 = escape
            case 27:
                this.pauseGame();
                break;

            default:
                break;
        }
    };

    /**
     * HUD elements
     * 
     * @todo(anand): Can this be improved? May be making code slow.
     */

    this.wpn = game.add.sprite(0, 0, 'hud_weapon');
    this.wpn.width /= 2;
    this.wpn.height /= 2;
    this.wpn.x = game.camera.width - this.wpn.width;
    this.wpn.fixedToCamera = true;


    this.textStyle = {
        font: 'Press Start 2P',
        fill: '#ffff00',
        align: 'center',
        fontSize: '2em',
        stroke: 'black',
        strokeThickness: '5',
    };
    this.healthLabel = game.add.text(0, 5, 'Health', this.textStyle);
    this.healthLabel.fixedToCamera = true;
    this.repLabel = game.add.text(0, this.healthLabel.height + 10,
        'Rep', this.textStyle);
    this.repLabel.fixedToCamera = true;

    this.scoreLabel = game.add.text(0, 0, 'Score: 0', this.textStyle);
    this.scoreLabel.x = game.camera.width - (1.5 * this.scoreLabel.width);
    this.scoreLabel.y = game.camera.height - this.scoreLabel.height;
    this.scoreLabel.fixedToCamera = true;

    this.dayLabel = game.add.text(0, 0, 'Score: 0', this.textStyle);
    this.dayLabel.x = game.camera.width - (1.5 * this.dayLabel.width);
    this.dayLabel.y = game.camera.height - (2 * this.dayLabel.height);
    this.dayLabel.fixedToCamera = true;

    this.emptyHealthBar = game.add.sprite(this.healthLabel.width + 5, 0,
        'hud_emptyHealth');
    this.emptyHealthBar.fixedToCamera = true;
    this.emptyHealthBar.height = 20;
    this.fullHealthBar = game.add.sprite(this.healthLabel.width + 7, 2,
        'hud_fullHealth');
    this.fullHealthBar.fixedToCamera = true;
    this.fullHealthBar.width /= 2;
    this.fullHealthBar.height = 20;

    this.emptyRepBar = game.add.sprite(this.healthLabel.width + 5,
        this.emptyHealthBar.height + 5,
        'hud_emptyHealth');
    this.emptyRepBar.fixedToCamera = true;
    this.emptyRepBar.height = 20;
    this.fullRepBar = game.add.sprite(this.healthLabel.width + 7,
        this.emptyHealthBar.height + 7,
        'hud_fullRep');
    this.fullRepBar.fixedToCamera = true;
    this.barRealWidth = this.fullRepBar.width;
    this.fullRepBar.width /= 2;
    this.fullRepBar.height = 20;


    this.hudGroup = game.add.group();
    this.hudGroup.addMultiple([
        this.wpn,
        this.healthLabel,
        this.repLabel,
        this.scoreLabel,
        this.dayLabel,
        this.emptyHealthBar,
        this.fullHealthBar,
        this.emptyRepBar,
        this.fullRepBar,
    ]);
};

/**
 * pauses the game
 */
Play.pauseGame = function() {
    game.paused ? game.paused = false : game.paused = true;
    if (game.paused) {
        // reveal pause menu
        for (let i = 0; i < this.pauseMenu.length; i++) {
            this.pauseMenu[i].reveal();
            this.pauseBg.visible = true;
            this.controlText.visible = true;
        }
    } else {
        // hide the menu
        for (let i = 0; i < this.pauseMenu.length; i++) {
            this.pauseMenu[i].hide();
            this.pauseBg.visible = false;
            this.controlText.visible = false;
        }
    }
};

Play.create = function() {
    // this.player.bringToTop();
    /**
     * Check if we should load game.
     */
    if (loadedData) {
        this.loadBoard(loadedData);
    } else {
        this.populateBoard();
    }
    /**
     * Center camera on player
     */
    this.game.camera.follow(this.player);

    this.map.setCollisionBetween(1, 10000, true, this.blockLayer);
    this.map.setCollisionBetween(1, 10000, true, this.blockOverlap);


    /**
     * Day night cycle
     */
    this.light = game.add.graphics();
    this.light.beginFill(0x18007A);
    this.light.alpha = 0;
    this.light.drawRect(0, 0, game.camera.width, game.camera.height);
    this.light.fixedToCamera = true;
    this.light.endFill();
    this.dayTime = true;

    /**
     * Pause menu set up
     */
    this.pauseMenu = [];
    // pause background
    this.pauseBg = game.add.graphics();
    this.pauseBg.beginFill(0x0);
    this.pauseBg.alpha = .2;
    this.pauseBg.visible = false;
    this.pauseBg.drawRect(0, 0, game.camera.width, game.camera.height);
    this.pauseBg.fixedToCamera = true;
    // controls
    this.controlText = game.add.text(game.camera.width/2, 600, 'Up:    W   Left:   A\nDown:  S   Right:  D\nMelee: M   Sprint: Shift');
    this.controlText.font = 'Press Start 2P';
    this.controlText.fill = '#ff5100';
    this.controlText.stroke = '#0';
    this.controlText.strokeThickness = 5;
    this.controlText.fontSize = '3em';
    this.controlText.anchor.setTo(.5, .5);
    this.controlText.align = 'left';
    this.controlText.fixedToCamera = true;
    this.controlText.visible = false;
    // add a save button
    this.pauseMenu.push(new UI.MenuButton(game.camera.width/2,
         200, '  Save  ', null, ()=>{
            console.log('Manually saving');
            this.pauseMenu[0].text.text = '  Save ' +
             String.fromCodePoint(0x1F60A);
            setTimeout(()=> {
                this.pauseMenu[0].text.text = '  Save  ';
            }, 750);
            dataStore.manualSaveState();
         }, '4.5em' ));
    // add a settings button
    this.pauseMenu.push(new UI.MenuButton(game.camera.width/2,
         300, window.isFullScreen() ? 'Windowed' : 'Fullscreen',
          null, ()=>{
            console.log('fulscreen toggled');
            game.paused = false;
            window.setResizable(true);
            window.setFullScreenable(true);
            if (window.isFullScreen()) {
                window.setFullScreen(false);
                this.pauseMenu[1].text.text = 'Fullscreen';
            } else {
                window.setFullScreen(true);
                this.pauseMenu[1].text.text = 'Windowed';
            }
            window.setResizable(false);
            window.setFullScreenable(false);
            game.paused = true;
         }, '4.5em' ));
    // add a menu button
    this.pauseMenu.push(new UI.MenuButton(game.camera.width/2,
        400, 'Main Menu', null, ()=>{
           game.input.keyboard.onDownCallback = null;
           game.state.start('Menu');
           game.paused = false;
        }, '4.5em' ));

    // hide the pause menu
    for (let i = 0; i < this.pauseMenu.length; i++) {
        this.pauseMenu[i].text.fill = '#00bbff';
        this.pauseMenu[i].text.stroke = '#0';
        this.pauseMenu[i].text.strokeThickness = 5;
        this.pauseMenu[i].hide();
    }

    /**
     * Setting datastore callback interval
     * 
     * Start autosaving 10 seconds after game starts
     */
    let i = setInterval(() => {
        dataStore.autosaveEntity(this.player);
        this.monsterGroup.forEachAlive(dataStore.autosaveEntity);
        this.npcGroup.forEachAlive(dataStore.autosaveEntity);
    }, 1000);
    timerIDs.push(i);

    game.world.bringToTop(this.hudGroup);

    this.rippleGossip = new Ripple();
    i = setInterval(() => {
         /**
         * Trigger a few conversations
         */
        /**
         * Build the datastructure keeping track of Entities
         * 
         * Period: 1.5 sec
         * 
         * What I did here is call the things immediately and then
         */
        this.generateMap();
        let totalEntities =
            this.monsterGroup.total +
            this.npcGroup.total;
        Map.discreteSamples(Math.floor(totalEntities/3)).forEach(function(p, i) {
            this.rippleGossip.triggerGossip(p);
        }, this);
    }, 1000);
};

Play.update = function() {
    if (this.player.state === 'dead') {
        game.score = this.player.score;
        game.dayCount = this.player.daysSurvived;
        setTimeout(() => {
            game.state.start('Game Over');
        }, 2000);
    }
    while (this.fullHealthBar.width < 146) this.fullHealthBar.width += 1;
    this.scoreLabel.text = 'Score: ' + this.player.score;
    this.dayLabel.text = 'Day ' + this.player.daysSurvived;
    /**
     * Debug Stuff
     */
    // game.debug.body(this.player);

    // day / night cycle
    if (this.dayTime) {
        this.light.alpha += .0001;
    } else {
        this.light.alpha -= .0007;
    }
    if (this.light.alpha <= 0 && this.dayTime === false) {
        this.dayTime = true;
        this.player.daysSurvived++;
        this.light.alpha = 0;
    }
    if (this.light.alpha >= .5) {
        this.dayTime = false;
    }

    /**
     * Deal with collision of entities
     */
    game.physics.arcade.collide(this.entitiesGroup, this.blockLayer);
    game.physics.arcade.collide(this.entitiesGroup, this.blockOverlap);
    game.physics.arcade.collide(this.entitiesGroup, this.entitiesGroup,
        entityCollision, null, this);


    /**
     * NPC Code
     * 
     * Threshold distance to attack is 8 tiles.
     * => 4 tiles on either side
     * => Distance to player = 128
     * => 128^2 = 16384
     */
    let tL = new Phaser.Point(772, 448);
    let bR = new Phaser.Point(3426, 2893);
    let tL2 = new Phaser.Point(3151, 568);
    let bR2 = new Phaser.Point(4452, 3565);

    this.npcGroup.forEachAlive((e) => {
        /**
         * NOTE(anand):
         * 
         * At this point, the NPC can either attack the player
         * or run away if they dont like the player
         * or do nothing otherwise.
         * 
         * What I will do is this.
         * 
         * If Reputation is below 0 (it will always be >= -1):
         * Generate a random number between -1 and 0. 
         * - If the number lies between -1 and the reputation
         *   - avoid the player
         * - Else
         *   - attck the player
         * Else (Rep >= 0)
         * - wander
         */
        let attitude = 'neutral';
        if (e.reputation < 0) {
            let decision = -Math.random();
            if (decision > e.reputation) {
                attitude = 'aggressive';
            }
        }
        e.updateAI(this.navMesh, tL, bR, this.player, attitude);
    });
    this.monsterGroup.forEachAlive((e) => {
        /**
         * NOTE(anand):
         * 
         * For monster, I will attack regardless,
         * but I will sprint if I realllllly don't
         * like the player (less than -0.8?)
         */
        let attitude = 'aggressive';
        if (e.reputation < -0.8) {
            // Really aggro
            e.slowSprint = e.sprintSpeed;
            e.sprintSpeed = 2 * e.slowSprint;
        }
        e.updateAI(this.navMesh, tL2, bR2, this.player, attitude);
    });

    /**
     * PLAYER CODE
     */
    if (this.player.state === 'dead') return;
    // Displays the hitbox for the Player
    // this.game.debug.body(this.player);
    // game.debug.body(this.player.collideBox);
    // game.debug.bodyInfo(this.player.collideBox, 32, 32);

    // SHIFT for running
    let sprint = false;
    if (this.keyboard.isDown(Phaser.Keyboard.SHIFT)) {
        sprint = true;
    }

    // Attack
    if ((this.keyboard.isDown(Phaser.Keyboard.M)) &&
        (this.player.state !== 'attacking')) {
        this.player.attack();
    } else {
        /**
         * attacking == false 
         * iff we are on the last frame. ie. the whole animation has played.
         */
        // 
        let temp = this.player.frame - 161;
        if ((temp % 13 === 0)) {
            if (!(this.keyboard.isDown(Phaser.Keyboard.M))) {
                this.player.state = 'idling';
            }
        }
    }

    // Moving the player, but only if you aren't attacking.

    if (this.keyboard.isDown(Phaser.Keyboard.W)) {
        this.player.moveInDirection('up', sprint);
    } else if (this.keyboard.isDown(Phaser.Keyboard.S)) {
        this.player.moveInDirection('down', sprint);
    } else if (this.keyboard.isDown(Phaser.Keyboard.A)) {
        this.player.moveInDirection('left', sprint);
    } else if (this.keyboard.isDown(Phaser.Keyboard.D)) {
        this.player.moveInDirection('right', sprint);
    } else if (this.player.state !== 'attacking') {
        this.player.idleHere();
    }

    /**
     * Deciding which character to render on top of the other.
     * 
     * @todo(anand): Only do this check for the nearest 4 neighbors.
     */
    let nearest4 = Map.nearest(this.player);
    nearest4.forEach((entity) => {
        // console.log(JSON.stringify([entity[0].trueXY(), entity[1]]));
        if ((this.player.y + this.player.height) >
         (entity[0].y + entity[0].height)) {
            game.world.bringToTop(this.player);
            // console.log('player on top');
        } else {
            // console.log('entity on top');
            game.world.bringToTop(entity[0]);
        }
    });

    let totalEntities =
        this.monsterGroup.total +
        this.npcGroup.total;
    let repNum = 0;
    let repSum = 0;
    Map.nearest(this.player, totalEntities, game.camera.width / 2)
        .forEach((point) => {
            /**
             * Get the average reputation of all the entities withing
             * the screen.
             */
            if (point[0].alive) {
                repSum += point[0].reputation;
                repNum += 1;
            }
        });
    let avgRep = (isNaN(repSum / repNum)) ? 0 : repSum / repNum;
    // console.log('Average Reputation: ' + avgRep);
    this.fullRepBar.width = (this.barRealWidth / 2) * (1 + (avgRep));
    if (this.fullRepBar.width < this.barRealWidth / 2) {
        this.fullRepBar.tint = 0x800000;
    } else if (this.fullRepBar.width > this.barRealWidth / 2) {
        this.fullRepBar.tint = 0x66ff33;
    } else {
        this.fullRepBar.tint = 0x999999;
    }
};


/**
 * Handle collision between two `Entities`
 * 
 * This needs to be run in the context of Play state
 * 
 * @param {any} entity1 
 * @param {any} entity2 
 */
function entityCollision(entity1, entity2) {
    // entity2 seems to be the Player, and entity1 is the Enemy
    if (entity1.frame === 272) {
        entity1.kill();
        return;
    }
    if (entity2.frame === 272) {
        entity2.kill();
        return;
    }
    /**
     * @todo(anand): Handle code to get injured
     */
    if (game.physics.arcade.collide(entity1, this.blockLayer) ||
        game.physics.arcade.collide(entity1, this.blockOverlap) ||
        game.physics.arcade.collide(entity2, this.blockLayer) ||
        game.physics.arcade.collide(entity2, this.blockOverlap)) {
        return;
    }

    /**
     * @todo(anand): I think this needs to be made general to all Entities
     * 
     * We shouldn't be assuming that entity 2 is always going to be Player
     * also, other entities can attack too
     */
    /**
     * The type of person who died
     */
    let dead = null;
    let perp = null;
    let action = '';
    if (entity2.state == 'attacking') {
        entity2.attack();
        if (entity1.state !== 'dead') {
            entity1.die();
            entity1.body.enable = false;
        }
        dead = entity1;
        perp = entity2;
        action = 'kill';
    }
    if (entity1.state === 'attacking') {
        entity1.attack();
        if (entity2.state !== 'dead') {
            entity2.die();
            entity2.body.enable = false;
        }
        perp = entity1;
        dead = entity2;
        action = 'kill';
    }
    /**
     * @todo(anand): Need to implement Game Over
     */
    if (dead && perp && action) {
        if (perp.type === 'player') {
            switch (dead.type) {
                case 'npc':
                    console.log('Killed an NPC :(');
                    break;
                case 'monster':
                    this.player.score++;
                    break;
            }
        }
        
        // let nearest = Map.nearest(this.player, 3, 256);
        // nearest.forEach(function(p, i) {
        //     if (p[0].state !== 'dead') {
        //         let witness =p[0];
        //         this.rippleGossip.createRumor(
        //             witness,
        //             dead,
        //             perp,
        //             action);
        //     }
        // }, this);
        let nearest = Map.nearest(this.player, 3, 256);
        let numWitnesses = Math.floor(Math.random() * nearest.length);
        let witnesses = Sampling.sample_from_array(nearest, numWitnesses, false);
        
        if (!witnesses) return;
        witnesses.forEach(function(p, i) {
            if (p[0].state !== 'dead') {
                let witness =p[0];
                this.rippleGossip.createRumor(
                    witness,
                    dead,
                    perp,
                    action);
            }
        }, this);
    }
}

Play.populateBoard = function() {
    /**
     * Generate a factory and a few monsters
     */
    this.monsterGroup = game.add.group();
    this.monsterFactory = new Factory(Monster, this.monsterGroup,
        monsterBounds, 30);
    for (let i = 0; i < 30; i++) {
        /**
         * Generate a random location withing 3/4ths of the map
         */
        this.monsterFactory.next(null, null, 'enemy');
    }

    /**
     * Generate a factory and a few NPCs
     */
    this.npcGroup = game.add.group();
    this.npcFactory = new Factory(NPC, this.npcGroup, npcBounds, 40);
    for (let i = 0; i < 40; i++) {
        /**
         * Generate a random location withing 3/4ths of the map
         */
        this.npcFactory.next(null, null, 'woman');
    }

    /**
     * Create the Player, setting location and naming as 'player'.
     * Giving him Physics and allowing collision with the world boundaries.
     */
    this.player = new Player(1971,
        504,
        'player');

    /**
     * Add all Entities to the same group.
     */
    this.entitiesGroup = game.add.group();
    this.entitiesGroup.addMultiple([
        this.player,
        this.npcGroup,
        this.monsterGroup,
    ]);
};

Play.loadBoard = function(data) {
    let playerData = data.player;
    let monstersData = data.monsters;
    let npcData = data.npc;

    /**
     * Generate a factory and a few monsters
     */
    this.monsterGroup = game.add.group();
    this.monsterFactory = new Factory(Monster, this.monsterGroup,
        monsterBounds, Object.keys(monstersData).length);
    let i = 0;
    for (let id in monstersData) {
        if (Object.prototype.hasOwnProperty.call(monstersData, id)) {
            i = i + 1;
            // console.debug('Monster #' + i);
            let e = monstersData[id];
            let E = this.monsterFactory.next(e.x, e.y, e.key);
            E.deserialize(e);
        }
    }

    /**
     * Generate a factory and a few NPCs
     */
    i = 0;
    this.npcGroup = game.add.group();
    this.npcFactory = new Factory(NPC, this.npcGroup, npcBounds,
        Object.keys(npcData).length);
    for (let id in npcData) {
        if (Object.prototype.hasOwnProperty.call(npcData, id)) {
            i = i + 1;
            // console.debug('NPC #' + i);
            let e = npcData[id];
            let E = this.npcFactory.next(e.x, e.y, e.key);
            E.deserialize(e);
        }
    }

    /**
     * Create the Player, setting location and naming as 'player'.
     * Giving him Physics and allowing collision with the world boundaries.
     */
    this.player = new Player(playerData.x, playerData.y, playerData.key);
    this.player.deserialize(playerData);

    /**
     * Add all Entities to the same group.
     */
    this.entitiesGroup = game.add.group();
    this.entitiesGroup.addMultiple([
        this.player,
        this.npcGroup,
        this.monsterGroup,
    ]);
};


Play.generateMap = function() {
    // setTimeout(() => {
    let entities = [];
    // entities.push(this.player);
    // I see no point in adding the player
    this.monsterGroup.forEachAlive(function(monster) {
        entities.push(monster);
    });
    this.npcGroup.forEachAlive(function(npc) {
        entities.push(npc);
    });
    Map.create(entities);
    // }, 1500);
};

Play.autosaveData = function() {
    setTimeout(() => {
        dataStore.autosaveEntity(this.player);
        this.monsterGroup.forEachAlive(dataStore.autosaveEntity);
        this.npcGroup.forEachAlive(dataStore.autosaveEntity);
    }, 1000);
};

Play.manualSaveData = function() {
    const self = this;
    dataStore.manualSaveEntity(sel.player);
    self.monsterGroup.forEachAlive(dataStore.manualSaveEntity);
    self.npcGroup.forEachAlive(dataStore.manualSaveEntity);
};

/**
 * This will return the distance to the player squared.
 * 
 * Square root calculation is not trivial.
 * 
 * @param {Entity} entity 
 * @return {number}
 */
Play.getPlayerDistance2 = function(entity) {
    let player = this.player.trueXY();
    let e = entity.trueXY();
    return Math.pow(player.x - e.x, 2) + Math.pow(player.y - e.y, 2);
};

Play.shutdown = function() {
    if (this.rippleGossip) {
        this.rippleGossip.kill();
    }
    timerIDs.forEach((id) => {
        clearInterval(id);
    });
};

Phaser.Tilemap.prototype.setCollisionBetween = function(start, stop,
    collides, layer, recalculate) {
       if (collides === undefined) {
collides = true;
}
       if (layer === undefined) {
layer = this.currentLayer;
}
       if (recalculate === undefined) {
recalculate = true;
}

       layer = this.getLayer(layer);

       for (let index = start; index <= stop; index++) {
           if (collides) {
               this.collideIndexes.push(index);
           } else {
               let i = this.collideIndexes.indexOf(index);

               if (i > -1) {
                   this.collideIndexes.splice(i, 1);
               }
           }
       }

       for (let y = 0; y < this.layers[layer].height; y++) {
           for (let x = 0; x < this.layers[layer].width; x++) {
               let tile = this.layers[layer].data[y][x];

               if (tile && tile.index >= start && tile.index <= stop) {
                   if (collides) {
                       tile.setCollision(true, true, true, true);
                   } else {
                       tile.resetCollision();
                   }

                   tile.faceTop = collides;
                   tile.faceBottom = collides;
                   tile.faceLeft = collides;
                   tile.faceRight = collides;
               }
           }
       }

       if (recalculate) {
           //  Now re-calculate interesting faces
           this.calculateFaces(layer);
       }

       return layer;
   };

module.exports = Play;