'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;