From b60651487ee767e0477ed1c89164c45c5a64e0dd Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Wed, 25 Jan 2017 15:47:29 -0800 Subject: [PATCH] Update shortbow to use Entity Server Script --- .../Toybox/towerDefense/enemyEntity.js | 4 +- .../Toybox/towerDefense/playWaveGame.js | 603 ++---------------- .../towerDefense/shortbowGameManager.js | 518 +++++++++++++++ .../towerDefense/shortbowServerEntity.js | 58 ++ .../Toybox/towerDefense/startGameButton.js | 2 + 5 files changed, 637 insertions(+), 548 deletions(-) create mode 100644 unpublishedScripts/DomainContent/Toybox/towerDefense/shortbowGameManager.js create mode 100644 unpublishedScripts/DomainContent/Toybox/towerDefense/shortbowServerEntity.js diff --git a/unpublishedScripts/DomainContent/Toybox/towerDefense/enemyEntity.js b/unpublishedScripts/DomainContent/Toybox/towerDefense/enemyEntity.js index 1182860fd0..4174f91ff3 100644 --- a/unpublishedScripts/DomainContent/Toybox/towerDefense/enemyEntity.js +++ b/unpublishedScripts/DomainContent/Toybox/towerDefense/enemyEntity.js @@ -26,6 +26,7 @@ this.entityIDsThatHaveCollidedWithMe.push(entityB); var colliderName = Entities.getEntityProperties(entityB, 'name').name; + print("Hit: ", entityB); // If the other entity's name includes 'projectile' and we haven't hit it before, // continue on. @@ -34,9 +35,10 @@ Messages.sendMessage(this.gameChannel, JSON.stringify({ type: "enemy-killed", entityID: this.entityID, + position: Entities.getEntityProperties(this.entityID, 'position').position })); Entities.deleteEntity(this.entityID); - } else if (colliderName.indexOf("goal") > -1) { + } else if (colliderName.indexOf("GateCollider") > -1) { Messages.sendMessage(this.gameChannel, JSON.stringify({ type: "enemy-escaped", entityID: this.entityID, diff --git a/unpublishedScripts/DomainContent/Toybox/towerDefense/playWaveGame.js b/unpublishedScripts/DomainContent/Toybox/towerDefense/playWaveGame.js index 76dfb9fba6..9294550e22 100644 --- a/unpublishedScripts/DomainContent/Toybox/towerDefense/playWaveGame.js +++ b/unpublishedScripts/DomainContent/Toybox/towerDefense/playWaveGame.js @@ -1,20 +1,16 @@ print("============= Script Starting ============="); -var BEGIN_BUILDING_SOUND = SoundCache.getSound(Script.resolvePath("assets/sounds/gameOn.wav")); -var GAME_OVER_SOUND = SoundCache.getSound(Script.resolvePath("assets/sounds/gameOver.wav")); -var WAVE_COMPLETE_SOUND = SoundCache.getSound(Script.resolvePath("assets/sounds/waveComplete.wav")); -var EXPLOSION_SOUND = SoundCache.getSound(Script.resolvePath("assets/sounds/explosion.wav")); -var TARGET_HIT_SOUND = SoundCache.getSound(Script.resolvePath("assets/sounds/targetHit.wav")); -var ESCAPE_SOUND = SoundCache.getSound(Script.resolvePath("assets/sounds/escape.wav")); - -Script.include('utils.js'); +Script.include('utils.js?' + Date.now()); Script.include('shortbow.js?' + Date.now()); -var TEMPLATES = SHORTBOW_ENTITIES.Entities; -print(utils.parseJSON); -print(utils.findSurfaceBelowPosition); +Script.include('shortbowGameManager.js?' + Date.now()); +TEMPLATES = SHORTBOW_ENTITIES.Entities; // Merge two objects into a new object. If a key name appears in both a and b, // the value in a will be used. +// +// @param {object} a +// @param {object} b +// @returns {object} The new object function mergeObjects(a, b) { var obj = {}; for (var key in b) { @@ -26,6 +22,15 @@ function mergeObjects(a, b) { return obj; } +// Spawn an entity from a template. +// +// The overrides can be used to override or add properties in the template. For instance, +// it's common to override the `position` property so that you can set the position +// of the entity to be spawned. +// +// @param {string} templateName The name of the template to spawn +// @param {object} overrides An object containing properties that will override +// any properties set in the template. function spawnTemplate(templateName, overrides) { var template = getTemplate(templateName); if (template === null) { @@ -37,6 +42,11 @@ function spawnTemplate(templateName, overrides) { return Entities.addEntity(properties); } +// TEMPLATES contains a dictionary of different named entity templates. An entity +// template is just a list of properties. +// +// @param name Name of the template to get +// @return {object} The matching template, or null if not found function getTemplate(name) { for (var i = 0; i < TEMPLATES.length; ++i) { if (TEMPLATES[i].name == name) { @@ -46,7 +56,7 @@ function getTemplate(name) { return null; } -// Cleanup ShortBow template data +// Cleanup Shortbow template data var scoreboardTemplate = getTemplate('SB.Scoreboard'); Vec3.print("Scoreboard:", scoreboardTemplate.position); for (var i = 0; i < TEMPLATES.length; ++i) { @@ -68,475 +78,8 @@ for (var i = 0; i < TEMPLATES.length; ++i) { } } - -var GAME_STATES = { - IDLE: 0, - PLAYING: 1, - BETWEEN_WAVES: 2, - GAME_OVER: 3, -}; - -var COMM_CHANNEL_NAME = 'wavegame'; var entityIDs = []; -var baseEnemyProperties = { - name: "WG.Enemy", - type: "Box", - registrationPoint: { x: 0.5, y: 0, z: 0.5 }, - dimensions: { x: 0.7, y: 0.7, z: 0.7 }, - velocity: { - x: 0, - y: 0, - z: -3 - }, - dynamic: true, - gravity: { - x: 0, - y: -10, - z: 0, - }, - restitution: 0, - friction: 0, - damping: 0, - linearDamping: 0, - lifetime: 100, - script: Script.resolvePath('enemyEntity.js'), - userData: JSON.stringify({ - gameChannel: COMM_CHANNEL_NAME, - }) -} -var baseEnemyProperties = { - damping: 0, - linearDamping: 0, - angularDamping: 0, - "acceleration": { - "x": 0, - "y": -9, - "z": 0 - }, - "angularVelocity": { - "x": -0.058330666273832321, - "y": -0.77943277359008789, - "z": -2.1163818836212158 - }, - "clientOnly": 0, - "collisionsWillMove": 1, - "dimensions": { - "x": 0.63503998517990112, - "y": 0.63503998517990112, - "z": 0.63503998517990112 - }, - "dynamic": 1, - "gravity": { - "x": 0, - "y": -15, - "z": 0 - }, - "id": "{ed8f7339-8bbd-4750-968e-c3ceb9d64721}", - "modelURL": "http://hifi-content.s3.amazonaws.com/Examples%20Content/production/marblecollection/Amber.fbx?2", - "name": "SB.Enemy", - "owningAvatarID": "{00000000-0000-0000-0000-000000000000}", - "queryAACube": { - "scale": 1.0999215841293335, - "x": -0.54996079206466675, - "y": -0.54996079206466675, - "z": -0.54996079206466675 - }, - //"restitution": 0.99000000953674316, - "rotation": { - "w": 0.52459806203842163, - "x": 0.3808099627494812, - "y": -0.16060420870780945, - "z": 0.74430292844772339 - }, - "shapeType": "sphere", - "type": "Model", - velocity: { - x: 0, - y: 0, - z: -3 - }, - script: Script.resolvePath('enemyEntity.js'), - userData: JSON.stringify({ - gameChannel: COMM_CHANNEL_NAME, - "grabbableKey": { - "grabbable": false - } - }), -}; - -// Encode a set of key-value pairs into a param string. Does NOT do any URL escaping. -function encodeURLParams(params) { - var paramPairs = []; - for (var key in params) { - paramPairs.push(key + "=" + params[key]); - } - return paramPairs.join("&"); -} - -function sendAndUpdateHighScore(highScoreDisplayID, entityID, score, wave, numPlayers) { - const URL = 'https://script.google.com/macros/s/AKfycbwbjCm9mGd1d5BzfAHmVT_XKmWyUYRkjCEqDOKm1368oM8nqWni/exec'; - print("Sending high score"); - - const paramString = encodeURLParams({ - entityID: entityID, - score: score, - wave: wave, - numPlayers: numPlayers - }); - - var req = new XMLHttpRequest(); - req.onreadystatechange = function() { - print("ready state: ", req.readyState, req.status, req.readyState === req.DONE, req.response); - if (req.readyState === req.DONE && req.status === 200) { - print("Got response for high score: ", req.response); - var response = JSON.parse(req.responseText); - if (response.highScore !== undefined) { - Entities.editEntity(highScoreDisplayID, { - text: response.highScore - }); - } - } - }; - req.open('GET', URL + "?" + paramString); - req.timeout = 10000; - req.send(); -} - -// The method of checking the local entity for the high score is currently disabled. -// As of 1/9/2017 we don't have support for getting nearby entity data in server entity scripts, -// so until then we have to rely on a remote source to store and retrieve that information. -function getHighScoreFromDisplay(entityID) { - var highScore = parseInt(Entities.getEntityProperties(entityID, 'text').text); - print("High score is: ", entityID, highScore); - if (highScore === NaN) { - return -1; - } - return highScore; -} -function setHighScoreOnDisplay(entityID, highScore) { - print("Setting high score to: ", entityID, highScore); - Entities.editEntity(entityID, { - text: highScore - }); -} - - -function GameManager(rootPosition, gatePosition, bowPositions, spawnPositions, rootEntityID, startButtonID, waveDisplayID, scoreDisplayID, livesDisplayID, highScoreDisplayID) { - this.gameState = GAME_STATES.IDLE; - - this.bowPositions = bowPositions; - this.rootPosition = rootPosition; - this.spawnPositions = spawnPositions; - this.gatePosition = gatePosition; - this.rootEntityID = rootEntityID; - this.startButtonID = startButtonID; - this.waveDisplayID = waveDisplayID; - this.scoreDisplayID = scoreDisplayID; - this.livesDisplayID = livesDisplayID; - this.highScoreDisplayID = highScoreDisplayID; - - // Gameplay state - this.waveNumber = 0; - this.livesLeft = 5; - this.score = 0; - this.nextWaveTimer = null; - this.spawnEnemyTimers = []; - this.enemyIDs = []; - this.entityIDs = []; - this.bowIDs = []; - - sendAndUpdateHighScore(this.highScoreDisplayID, this.rootEntityID, this.score + 10, this.waveNumber, 1); -} -GameManager.prototype = { - cleanup: function() { - for (var i = 0; i < this.entityIDs.length; i++) { - Entities.deleteEntity(this.entityIDs[i]); - } - this.entityIDs = []; - for (var i = this.bowIDs.length - 1; i >= 0; i--) { - Entities.deleteEntity(this.bowIDs[i]); - } - this.bowIDs = []; - for (var i = 0; i < this.enemyIDs.length; i++) { - Entities.deleteEntity(this.enemyIDs[i]); - } - this.enemyIDs = []; - }, - startGame: function() { - if (this.gameState !== GAME_STATES.IDLE) { - print("playWaveGameManager.js | Error, trying to start game when not in idle state"); - return; - } - - print("Game started!!"); - - Entities.editEntity(this.startButtonID, { visible: false }); - - - // Spawn bows - for (var i = 0; i < this.bowPositions.length; ++i) { - const bowPosition = this.bowPositions[i]; - Vec3.print("Creating bow: ", bowPosition); - this.bowIDs.push(Entities.addEntity({ - position: bowPosition, - "collisionsWillMove": 1, - "compoundShapeURL": Script.resolvePath("bow/bow_collision_hull.obj"), - "created": "2016-09-01T23:57:55Z", - "dimensions": { - "x": 0.039999999105930328, - "y": 1.2999999523162842, - "z": 0.20000000298023224 - }, - "dynamic": 1, - "gravity": { - "x": 0, - "y": -9.8, - "z": 0 - }, - "modelURL": Script.resolvePath("bow/bow-deadly.fbx"), - "name": "WG.Hifi-Bow", - "rotation": { - "w": 0.9718012809753418, - "x": 0.15440607070922852, - "y": -0.10469216108322144, - "z": -0.14418250322341919 - }, - "script": Script.resolvePath("bow/bow.js"), - "shapeType": "compound", - "type": "Model", - "userData": "{\"grabbableKey\":{\"grabbable\":true},\"wearable\":{\"joints\":{\"RightHand\":[{\"x\":0.0813,\"y\":0.0452,\"z\":0.0095},{\"x\":-0.3946,\"y\":-0.6604,\"z\":0.4748,\"w\":-0.4275}],\"LeftHand\":[{\"x\":-0.0881,\"y\":0.0259,\"z\":0.0159},{\"x\":0.4427,\"y\":-0.6519,\"z\":0.4592,\"w\":0.4099}]}}}" - })); - } - - // Initialize game state - this.waveNumber = 0; - this.setScore(0); - this.setLivesLeft(6); - - this.nextWaveTimer = Script.setTimeout(this.startNextWave.bind(this), 100); - this.spawnEnemyTimers = []; - this.checkEnemyPositionsTimer = null; - this.enemyIDs = []; - - // SpawnQueue is a list of enemies left to spawn. Each entry looks like: - // - // { spawnAt: 1000, position: { x: 0, y: 0, z: 0 } } - // - // where spawnAt is the number of millseconds after the start of the wave - // to spawn the enemy. The list is sorted by spawnAt, ascending. - this.spawnQueue = []; - - this.gameState = GAME_STATES.PLAYING; - - Audio.playSound(BEGIN_BUILDING_SOUND, { - volume: 1.0, - position: this.rootPosition - }); - - }, - startNextWave: function() { - print("Starting next wave"); - this.gameState = GAME_STATES.PLAYING; - this.waveNumber++; - this.enemyIDs = []; - this.spawnQueue = []; - this.spawnStartTime = Date.now(); - - Entities.editEntity(this.waveDisplayID, { - text: this.waveNumber - }); - - var numberOfEnemiesLeftToSpawn = this.waveNumber * 2; - var delayBetweenSpawns = 2000 / Math.max(1, Math.log(this.waveNumber)); - var currentDelay = 2000; - - print("Number of enemies:", numberOfEnemiesLeftToSpawn); - this.checkEnemyPositionsTimer = Script.setInterval(this.checkForEscapedEnemies.bind(this), 100); - - for (var i = 0; i < numberOfEnemiesLeftToSpawn; ++i) { - print("Adding enemy"); - var idx = Math.floor(Math.random() * this.spawnPositions.length); - this.spawnQueue.push({ spawnAt: currentDelay, position: this.spawnPositions[idx] }); - currentDelay += delayBetweenSpawns; - } - - print("Starting wave", this.waveNumber); - - }, - checkWaveComplete: function() { - if (this.gameState !== GAME_STATES.PLAYING) { - return; - } - - if (this.spawnQueue.length <= 0 && this.enemyIDs.length === 0) { - this.gameState = GAME_STATES.BETWEEN_WAVES; - Script.setTimeout(this.startNextWave.bind(this), 5000); - - Script.clearInterval(this.checkEnemyPositionsTimer); - this.checkEnemyPositionsTimer = null; - - // Play after 1.5s to let other sounds finish playing - var self = this; - Script.setTimeout(function() { - Audio.playSound(WAVE_COMPLETE_SOUND, { - volume: 1.0, - position: self.rootPosition - }); - }, 1500); - } - }, - setLivesLeft: function(lives) { - lives = Math.max(0, lives); - this.livesLeft = lives; - Entities.editEntity(this.livesDisplayID, { - text: this.livesLeft - }); - }, - setScore: function(score) { - this.score = score; - Entities.editEntity(this.scoreDisplayID, { - text: utils.formatNumberWithCommas(this.score) - }); - }, - checkSpawnQueue: function() { - var waveElapsedTime = Date.now() - this.spawnStartTime; - while (this.spawnQueue.length > 0 && waveElapsedTime > this.spawnQueue[0].spawnAt) { - baseEnemyProperties.position = this.spawnQueue[0].position; - var entityID = Entities.addEntity(baseEnemyProperties); - this.enemyIDs.push(entityID); - this.spawnQueue.splice(0, 1); - Script.setTimeout(function() { - var velocity = Entities.getEntityProperties(entityID, 'velocity').velocity; - velocity.y += 5; - Entities.editEntity(entityID, { velocity: velocity }); - - }, 500 + Math.random() * 4000); - } - //print("Spawn queue size: ", this.spawnQueue.length, "Elapsed time: ", waveElapsedTime, "Number of enemies:", this.enemyIDs.length); - }, - checkForEscapedEnemies: function() { - // Move this somewhere else? - this.checkSpawnQueue(); - - var enemiesEscaped = false; - for (var i = this.enemyIDs.length - 1; i >= 0; --i) { - var position = Entities.getEntityProperties(this.enemyIDs[i], 'position').position; - if (position === undefined) { - // If the enemy can no longer be found, assume it was hit - this.enemyIDs.splice(i, 1); - Audio.playSound(TARGET_HIT_SOUND, { - volume: 1.0, - position: this.rootPosition, - }); - this.setScore(this.score + 100); - enemiesEscaped = true; - } else if (position.z < this.gatePosition.z) { - Entities.deleteEntity(this.enemyIDs[i]); - this.enemyIDs.splice(i, 1); - this.setLivesLeft(this.livesLeft - 1); - Audio.playSound(ESCAPE_SOUND, { - volume: 1.0, - position: this.rootPosition - }); - enemiesEscaped = true; - } - } - //print("LIVES LEFT: ", this.livesLeft, this.numberOfEntitiesLeftForWave); - if (this.livesLeft <= 0) { - this.endGame(); - } else if (enemiesEscaped) { - this.checkWaveComplete(); - } - }, - endGame: function() { - if (this.gameState !== GAME_STATES.PLAYING) { - return; - } - - var self = this; - Script.setTimeout(function() { - Audio.playSound(GAME_OVER_SOUND, { - volume: 1.0, - position: self.rootPosition - }); - }, 1500); - - //Entities.editEntity(this.livesDisplayID, { text: "GAME OVER" }); - - this.gameState = GAME_STATES.GAME_OVER; - print("GAME OVER"); - - - // Update high score - sendAndUpdateHighScore(this.highScoreDisplayID, this.rootEntityID, this.score, this.waveNumber, 1); - - //var highScore = getHighScoreFromDisplay(this.highScoreDisplayID); - //if (this.score > highScore) { - // setHighScoreOnDisplay(this.highScoreDisplayID, this.score); - //} else { - // print("Score not higher", this.score, highScore); - //} - - - // Cleanup - Script.clearTimeout(this.nextWaveTimer); - this.nextWaveTimer = null; - for (var i = 0; i < this.spawnEnemyTimers.length; ++i) { - Script.clearTimeout(this.spawnEnemyTimers[i]); - } - this.spawnEnemyTimers = []; - - Script.clearInterval(this.checkEnemyPositionsTimer); - this.checkEnemyPositionsTimer = null; - - - for (var i = this.bowIDs.length - 1; i >= 0; i--) { - var id = this.bowIDs[i]; - print("Checking bow: ", id); - var userData = utils.parseJSON(Entities.getEntityProperties(id, 'userData').userData); - var bowIsHeld = userData.grabKey !== undefined && userData.grabKey !== undefined && userData.grabKey.refCount > 0 - print("Held: ", bowIsHeld); - if (!bowIsHeld) { - Entities.deleteEntity(id); - this.bowIDs.splice(i, 1); - } - } - - Script.setTimeout(function() { - Entities.editEntity(this.startButtonID, { visible: true }); - this.gameState = GAME_STATES.IDLE; - }.bind(this), 3000); - - for (var i = 0; i < this.enemyIDs.length; i++) { - Entities.deleteEntity(this.enemyIDs[i]); - } - this.enemyIDs = []; - }, - onEnemyKilled: function(entityID, position) { - if (this.gameState !== GAME_STATES.PLAYING) { - return; - } - - var idx = this.enemyIDs.indexOf(entityID); - if (idx >= 0) { - this.enemyIDs.splice(idx, 1); - Audio.playSound(TARGET_HIT_SOUND, { - volume: 1.0, - //position: position, - position: this.rootPosition, - }); - - // Update score - this.setScore(this.score + 100); - print("SCORE: ", this.score); - - this.checkWaveComplete(); - } - }, -}; - -// TODO: Eventually these will need to be found at runtime by the AC var rootPosition = null; var goalPosition = null; var scoreboardID = null; @@ -545,6 +88,7 @@ var waveDisplayID = null; var scoreDisplayID = null; var highScoreDisplayID = null; var livesDisplayID = null; +var platformID = null; function createLocalGame() { rootPosition = utils.findSurfaceBelowPosition(MyAvatar.position); rootPosition.y += 6.11; @@ -562,7 +106,7 @@ function createLocalGame() { grabbableKey: { wantsTrigger: true }, - gameChannel: COMM_CHANNEL_NAME + gameChannel: 'shortbow-' + scoreboardID }), }); entityIDs.push(buttonID); @@ -574,7 +118,6 @@ function createLocalGame() { const ROOF_HEIGHT = 0.2; goalPosition.y += BASES_HEIGHT - ROOF_HEIGHT; - const roofPosition = goalPosition; waveDisplayID = spawnTemplate("SB.DisplayWave", { parentID: scoreboardID @@ -596,90 +139,56 @@ function createLocalGame() { }); entityIDs.push(highScoreDisplayID); - - -} - -var gameHasBeenBuilt = false; -function buildGame() { - const BASES_SIZE = 15; - - // TODO: Generate these when a button is pressed - var platformID = spawnTemplate("SB.Platform", { + platformID = spawnTemplate("SB.Platform", { parentID: scoreboardID }); entityIDs.push(platformID); - var bowPositions = []; - var spawnPositions = []; + spawnTemplate("SB.GateCollider", { + parentID: scoreboardID, + visible: false + }); + entityIDs.push(platformID); + + Entities.editEntity(scoreboardID, { + userData: JSON.stringify({ + platformID: platformID, + buttonID: buttonID, + waveDisplayID: waveDisplayID, + scoreDisplayID: scoreDisplayID, + livesDisplayID: livesDisplayID, + highScoreDisplayID: highScoreDisplayID, + }), + serverScripts: Script.resolvePath('shortbowServerEntity.js') + }); + + bowPositions = []; + spawnPositions = []; for (var i = 0; i < TEMPLATES.length; ++i) { var template = TEMPLATES[i]; if (template.name === "SB.BowSpawn") { bowPositions.push(Vec3.sum(rootPosition, template.localPosition)); - Vec3.print("PUshing bow position", Vec3.sum(rootPosition, template.localPosition)); + Vec3.print("Pushing bow position", Vec3.sum(rootPosition, template.localPosition)); } else if (template.name === "SB.EnemySpawn") { spawnPositions.push(Vec3.sum(rootPosition, template.localPosition)); - Vec3.print("PUshing spawnposition", Vec3.sum(rootPosition, template.localPosition)); + Vec3.print("Pushing spawnposition", Vec3.sum(rootPosition, template.localPosition)); } } - gameHasBeenBuilt = true; - - var goalPositionFront = Vec3.sum(goalPosition, { x: 0, y: 0, z: BASES_SIZE / 2 }); - return new GameManager(rootPosition, goalPositionFront, bowPositions, spawnPositions, platformID, buttonID, waveDisplayID, scoreDisplayID, livesDisplayID, highScoreDisplayID); + const BASES_SIZE = 15; + goalPositionFront = Vec3.sum(goalPosition, { x: 0, y: 0, z: BASES_SIZE / 2 }); } -function createACGame() { - // TODO - throw("AC not implemented"); -} - - -// Setup game -var gameManager; -if (this.EntityViewer !== undefined) { - createACGame(); -} else { +if (Script.isClientScript()) { createLocalGame(); -} + //var gameManager = new ShortbowGameManager(rootPosition, goalPositionFront, bowPositions, spawnPositions, scoreboardID, buttonID, waveDisplayID, scoreDisplayID, livesDisplayID, highScoreDisplayID); -Messages.subscribe(COMM_CHANNEL_NAME); -Messages.messageReceived.connect(function(channel, messageJSON, senderID) { - print("playWaveGame.js | Recieved: " + messageJSON + " from " + senderID); - if (channel === COMM_CHANNEL_NAME) { - var message = utils.parseJSON(messageJSON); - if (message === undefined) { - print("playWaveGame.js | Received non-json message"); - return; - } - switch (message.type) { - case 'build-game': - break; - case 'start-game': - if (gameHasBeenBuilt) { - gameManager.startGame(); - } else { - gameManager = buildGame(); - } - break; - case 'enemy-killed': - gameManager.onEnemyKilled(message.entityID, message.position); - break; - case 'enemy-escaped': - gameManager.onEnemyEscaped(message.entityID); - break; - default: - print("playWaveGame.js | Ignoring unknown message type: ", message.type); - break; + function cleanup() { + for (var i = 0; i < entityIDs.length; ++i) { + Entities.deleteEntity(entityIDs[i]); } + gameManager.cleanup(); } -}); -function cleanup() { - for (var i = 0; i < entityIDs.length; ++i) { - Entities.deleteEntity(entityIDs[i]); - } - gameManager.cleanup(); + Script.scriptEnding.connect(cleanup); } - -Script.scriptEnding.connect(cleanup); diff --git a/unpublishedScripts/DomainContent/Toybox/towerDefense/shortbowGameManager.js b/unpublishedScripts/DomainContent/Toybox/towerDefense/shortbowGameManager.js new file mode 100644 index 0000000000..f9d9ef2e12 --- /dev/null +++ b/unpublishedScripts/DomainContent/Toybox/towerDefense/shortbowGameManager.js @@ -0,0 +1,518 @@ +Script.include('utils.js'); + +// Load the sounds that we will be using in the game so they are ready to be +// used when we need them. +var BEGIN_BUILDING_SOUND = SoundCache.getSound(Script.resolvePath("assets/sounds/gameOn.wav")); +var GAME_OVER_SOUND = SoundCache.getSound(Script.resolvePath("assets/sounds/gameOver.wav")); +var WAVE_COMPLETE_SOUND = SoundCache.getSound(Script.resolvePath("assets/sounds/waveComplete.wav")); +var EXPLOSION_SOUND = SoundCache.getSound(Script.resolvePath("assets/sounds/explosion.wav")); +var TARGET_HIT_SOUND = SoundCache.getSound(Script.resolvePath("assets/sounds/targetHit.wav")); +var ESCAPE_SOUND = SoundCache.getSound(Script.resolvePath("assets/sounds/escape.wav")); + +// +--------+ +-----------+ +-----------------+ +// | | | <------+ | +// | IDLE +------> PLAYING | | BETWEEN_WAVES | +// | | | +------> | +// +----^---+ +-----+-----+ +-----------------+ +// | | +// | | +// | +------v------+ +// | | | +// +---------+ GAME_OVER | +// | | +// +-------------+ +var GAME_STATES = { + IDLE: 0, + PLAYING: 1, + BETWEEN_WAVES: 2, + GAME_OVER: 3, +}; + +// Encode a set of key-value pairs into a param string. Does NOT do any URL escaping. +function encodeURLParams(params) { + var paramPairs = []; + for (var key in params) { + paramPairs.push(key + "=" + params[key]); + } + return paramPairs.join("&"); +} + +function sendAndUpdateHighScore(highScoreDisplayID, entityID, score, wave, numPlayers) { + const URL = 'https://script.google.com/macros/s/AKfycbwbjCm9mGd1d5BzfAHmVT_XKmWyUYRkjCEqDOKm1368oM8nqWni/exec'; + print("Sending high score"); + + const paramString = encodeURLParams({ + entityID: entityID, + score: score, + wave: wave, + numPlayers: numPlayers + }); + + var req = new XMLHttpRequest(); + req.onreadystatechange = function() { + print("ready state: ", req.readyState, req.status, req.readyState === req.DONE, req.response); + if (req.readyState === req.DONE && req.status === 200) { + print("Got response for high score: ", req.response); + var response = JSON.parse(req.responseText); + if (response.highScore !== undefined) { + Entities.editEntity(highScoreDisplayID, { + text: response.highScore + }); + } + } + }; + req.open('GET', URL + "?" + paramString); + req.timeout = 10000; + req.send(); +} + +// The method of checking the local entity for the high score is currently disabled. +// As of 1/9/2017 we don't have support for getting nearby entity data in server entity scripts, +// so until then we have to rely on a remote source to store and retrieve that information. +function getHighScoreFromDisplay(entityID) { + var highScore = parseInt(Entities.getEntityProperties(entityID, 'text').text); + print("High score is: ", entityID, highScore); + if (highScore === NaN) { + return -1; + } + return highScore; +} +function setHighScoreOnDisplay(entityID, highScore) { + print("Setting high score to: ", entityID, highScore); + Entities.editEntity(entityID, { + text: highScore + }); +} + +var baseEnemyProperties = { + name: "WG.Enemy", + damping: 0, + linearDamping: 0, + angularDamping: 0, + "acceleration": { + "x": 0, + "y": -9, + "z": 0 + }, + "angularVelocity": { + "x": -0.058330666273832321, + "y": -0.77943277359008789, + "z": -2.1163818836212158 + }, + "clientOnly": 0, + "collisionsWillMove": 1, + "dimensions": { + "x": 0.63503998517990112, + "y": 0.63503998517990112, + "z": 0.63503998517990112 + }, + "dynamic": 1, + "gravity": { + "x": 0, + "y": -15, + "z": 0 + }, + "id": "{ed8f7339-8bbd-4750-968e-c3ceb9d64721}", + "modelURL": "http://hifi-content.s3.amazonaws.com/Examples%20Content/production/marblecollection/Amber.fbx?2", + "name": "SB.Enemy", + "owningAvatarID": "{00000000-0000-0000-0000-000000000000}", + "queryAACube": { + "scale": 1.0999215841293335, + "x": -0.54996079206466675, + "y": -0.54996079206466675, + "z": -0.54996079206466675 + }, + //"restitution": 0.99000000953674316, + "rotation": { + "w": 0.52459806203842163, + "x": 0.3808099627494812, + "y": -0.16060420870780945, + "z": 0.74430292844772339 + }, + "shapeType": "sphere", + "type": "Model", + velocity: { + x: 0, + y: 0, + z: -30 + }, + script: Script.resolvePath('enemyEntity.js'), +}; + + +ShortbowGameManager = function(rootPosition, gatePosition, bowPositions, spawnPositions, rootEntityID, startButtonID, waveDisplayID, scoreDisplayID, livesDisplayID, highScoreDisplayID) { + this.gameState = GAME_STATES.IDLE; + + this.bowPositions = bowPositions; + this.rootPosition = rootPosition; + this.spawnPositions = spawnPositions; + this.gatePosition = gatePosition; + this.rootEntityID = rootEntityID; + this.startButtonID = startButtonID; + this.waveDisplayID = waveDisplayID; + this.scoreDisplayID = scoreDisplayID; + this.livesDisplayID = livesDisplayID; + this.highScoreDisplayID = highScoreDisplayID; + print(waveDisplayID, scoreDisplayID, livesDisplayID); + + // Gameplay state + this.waveNumber = 0; + this.livesLeft = 5; + this.score = 0; + this.nextWaveTimer = null; + this.spawnEnemyTimers = []; + this.enemyIDs = []; + this.entityIDs = []; + this.bowIDs = []; + + this.commChannelName = "shortbow-" + this.rootEntityID; + print("Listening on: ", this.commChannelName); + + Messages.subscribe(this.commChannelName); + Messages.messageReceived.connect(this, this.onReceivedMessage); + + this.reset(); + sendAndUpdateHighScore(this.highScoreDisplayID, this.rootEntityID, this.score, this.waveNumber, 1); +} +ShortbowGameManager.prototype = { + reset: function() { + Entities.editEntity(this.startButtonID, { + visible: true + }); + }, + cleanup: function() { + Messages.unsubscribe(this.commChannelName); + Messages.messageReceived.disconnect(this, this.onReceivedMessage); + + for (var i = 0; i < this.entityIDs.length; i++) { + Entities.deleteEntity(this.entityIDs[i]); + } + this.entityIDs = []; + for (var i = this.bowIDs.length - 1; i >= 0; i--) { + Entities.deleteEntity(this.bowIDs[i]); + } + this.bowIDs = []; + for (var i = 0; i < this.enemyIDs.length; i++) { + Entities.deleteEntity(this.enemyIDs[i]); + } + this.enemyIDs = []; + }, + onReceivedMessage: function(channel, messageJSON, senderID) { + print("playWaveGame.js | Recieved: " + messageJSON + " from " + senderID, channel, this.commChannelName); + + if (channel === this.commChannelName) { + var message = utils.parseJSON(messageJSON); + if (message === undefined) { + print("playWaveGame.js | Received non-json message"); + return; + } + switch (message.type) { + case 'start-game': + this.startGame(); + break; + case 'enemy-killed': + this.onEnemyKilled(message.entityID, message.position); + break; + case 'enemy-escaped': + this.onEnemyEscaped(message.entityID); + break; + default: + print("playWaveGame.js | Ignoring unknown message type: ", message.type); + break; + } + } + }, + startGame: function() { + if (this.gameState !== GAME_STATES.IDLE) { + print("playWaveGameManager.js | Error, trying to start game when not in idle state"); + return; + } + + print("Game started!!"); + + Entities.editEntity(this.startButtonID, { visible: false }); + + + // Spawn bows + for (var i = 0; i < this.bowPositions.length; ++i) { + const bowPosition = this.bowPositions[i]; + Vec3.print("Creating bow: ", bowPosition); + this.bowIDs.push(Entities.addEntity({ + position: bowPosition, + "collisionsWillMove": 1, + "compoundShapeURL": Script.resolvePath("bow/bow_collision_hull.obj"), + "created": "2016-09-01T23:57:55Z", + "dimensions": { + "x": 0.039999999105930328, + "y": 1.2999999523162842, + "z": 0.20000000298023224 + }, + "dynamic": 1, + "gravity": { + "x": 0, + "y": -9.8, + "z": 0 + }, + "modelURL": Script.resolvePath("bow/bow-deadly.fbx"), + "name": "WG.Hifi-Bow", + "rotation": { + "w": 0.9718012809753418, + "x": 0.15440607070922852, + "y": -0.10469216108322144, + "z": -0.14418250322341919 + }, + "script": Script.resolvePath("bow/bow.js"), + "shapeType": "compound", + "type": "Model", + "userData": "{\"grabbableKey\":{\"grabbable\":true},\"wearable\":{\"joints\":{\"RightHand\":[{\"x\":0.0813,\"y\":0.0452,\"z\":0.0095},{\"x\":-0.3946,\"y\":-0.6604,\"z\":0.4748,\"w\":-0.4275}],\"LeftHand\":[{\"x\":-0.0881,\"y\":0.0259,\"z\":0.0159},{\"x\":0.4427,\"y\":-0.6519,\"z\":0.4592,\"w\":0.4099}]}}}" + })); + } + + // Initialize game state + this.waveNumber = 0; + this.setScore(0); + this.setLivesLeft(60); + + this.nextWaveTimer = Script.setTimeout(this.startNextWave.bind(this), 100); + this.spawnEnemyTimers = []; + this.checkEnemyPositionsTimer = null; + this.enemyIDs = []; + + // SpawnQueue is a list of enemies left to spawn. Each entry looks like: + // + // { spawnAt: 1000, position: { x: 0, y: 0, z: 0 } } + // + // where spawnAt is the number of millseconds after the start of the wave + // to spawn the enemy. The list is sorted by spawnAt, ascending. + this.spawnQueue = []; + + this.gameState = GAME_STATES.PLAYING; + + Audio.playSound(BEGIN_BUILDING_SOUND, { + volume: 1.0, + position: this.rootPosition + }); + + }, + startNextWave: function() { + print("Starting next wave"); + this.gameState = GAME_STATES.PLAYING; + this.waveNumber++; + this.enemyIDs = []; + this.spawnQueue = []; + this.spawnStartTime = Date.now(); + + print("Editing wave number 500", this.waveDisplayID, this.waveNumber); + Entities.editEntity('{1b630f6a-a6e4-4bca-be5e-d8d2709bb278}', { + text: '500',//this.waveNumber + }); + + var numberOfEnemiesLeftToSpawn = this.waveNumber * 2; + var delayBetweenSpawns = 2000 / Math.max(1, Math.log(this.waveNumber)); + var currentDelay = 2000; + + print("Number of enemies:", numberOfEnemiesLeftToSpawn); + this.checkEnemyPositionsTimer = Script.setInterval(this.checkForEscapedEnemies.bind(this), 100); + + for (var i = 0; i < numberOfEnemiesLeftToSpawn; ++i) { + print("Adding enemy"); + var idx = Math.floor(Math.random() * this.spawnPositions.length); + this.spawnQueue.push({ spawnAt: currentDelay, position: this.spawnPositions[idx] }); + currentDelay += delayBetweenSpawns; + } + + print("Starting wave", this.waveNumber); + + }, + checkWaveComplete: function() { + if (this.gameState !== GAME_STATES.PLAYING) { + return; + } + + if (this.spawnQueue.length <= 0 && this.enemyIDs.length === 0) { + this.gameState = GAME_STATES.BETWEEN_WAVES; + Script.setTimeout(this.startNextWave.bind(this), 5000); + + Script.clearInterval(this.checkEnemyPositionsTimer); + this.checkEnemyPositionsTimer = null; + + // Play after 1.5s to let other sounds finish playing + var self = this; + Script.setTimeout(function() { + Audio.playSound(WAVE_COMPLETE_SOUND, { + volume: 1.0, + position: self.rootPosition + }); + }, 1500); + } + }, + setLivesLeft: function(lives) { + lives = Math.max(0, lives); + this.livesLeft = lives; + Entities.editEntity(this.livesDisplayID, { + text: this.livesLeft + }); + }, + setScore: function(score) { + this.score = score; + Entities.editEntity(this.scoreDisplayID, { + text: utils.formatNumberWithCommas(this.score) + }); + }, + checkSpawnQueue: function() { + var waveElapsedTime = Date.now() - this.spawnStartTime; + while (this.spawnQueue.length > 0 && waveElapsedTime > this.spawnQueue[0].spawnAt) { + baseEnemyProperties.position = this.spawnQueue[0].position; + + baseEnemyProperties.userData = JSON.stringify({ + gameChannel: this.commChannelName, + grabbableKey: { + grabbable: false + } + }); + + var entityID = Entities.addEntity(baseEnemyProperties); + this.enemyIDs.push(entityID); + this.spawnQueue.splice(0, 1); + Script.setTimeout(function() { + var velocity = Entities.getEntityProperties(entityID, 'velocity').velocity; + velocity.y += 5; + Entities.editEntity(entityID, { velocity: velocity }); + + }, 500 + Math.random() * 4000); + } + //print("Spawn queue size: ", this.spawnQueue.length, "Elapsed time: ", waveElapsedTime, "Number of enemies:", this.enemyIDs.length); + }, + checkForEscapedEnemies: function() { + // Move this somewhere else? + this.checkSpawnQueue(); + + return; + + var enemiesEscaped = false; + for (var i = this.enemyIDs.length - 1; i >= 0; --i) { + var position = Entities.getEntityProperties(this.enemyIDs[i], 'position').position; + if (position === undefined) { + // If the enemy can no longer be found, assume it was hit + //this.enemyIDs.splice(i, 1); + //Audio.playSound(TARGET_HIT_SOUND, { + // volume: 1.0, + // position: this.rootPosition, + //}); + //this.setScore(this.score + 100); + //enemiesEscaped = true; + } else if (position.z < this.gatePosition.z) { + Entities.deleteEntity(this.enemyIDs[i]); + this.enemyIDs.splice(i, 1); + this.setLivesLeft(this.livesLeft - 1); + Audio.playSound(ESCAPE_SOUND, { + volume: 1.0, + position: this.rootPosition + }); + enemiesEscaped = true; + } + } + //print("LIVES LEFT: ", this.livesLeft, this.numberOfEntitiesLeftForWave); + if (this.livesLeft <= 0) { + this.endGame(); + } else if (enemiesEscaped) { + this.checkWaveComplete(); + } + }, + endGame: function() { + if (this.gameState !== GAME_STATES.PLAYING) { + return; + } + + var self = this; + Script.setTimeout(function() { + Audio.playSound(GAME_OVER_SOUND, { + volume: 1.0, + position: self.rootPosition + }); + }, 1500); + + //Entities.editEntity(this.livesDisplayID, { text: "GAME OVER" }); + this.gameState = GAME_STATES.GAME_OVER; + print("GAME OVER"); + + // Update high score + sendAndUpdateHighScore(this.highScoreDisplayID, this.rootEntityID, this.score, this.waveNumber, 1); + + // Cleanup + Script.clearTimeout(this.nextWaveTimer); + this.nextWaveTimer = null; + for (var i = 0; i < this.spawnEnemyTimers.length; ++i) { + Script.clearTimeout(this.spawnEnemyTimers[i]); + } + this.spawnEnemyTimers = []; + + Script.clearInterval(this.checkEnemyPositionsTimer); + this.checkEnemyPositionsTimer = null; + + + for (var i = this.bowIDs.length - 1; i >= 0; i--) { + var id = this.bowIDs[i]; + print("Checking bow: ", id); + var userData = utils.parseJSON(Entities.getEntityProperties(id, 'userData').userData); + var bowIsHeld = userData.grabKey !== undefined && userData.grabKey !== undefined && userData.grabKey.refCount > 0 + print("Held: ", bowIsHeld); + if (!bowIsHeld) { + Entities.deleteEntity(id); + this.bowIDs.splice(i, 1); + } + } + + Script.setTimeout(function() { + Entities.editEntity(this.startButtonID, { visible: true }); + this.gameState = GAME_STATES.IDLE; + }.bind(this), 3000); + + for (var i = 0; i < this.enemyIDs.length; i++) { + Entities.deleteEntity(this.enemyIDs[i]); + } + this.enemyIDs = []; + }, + onEnemyKilled: function(entityID, position) { + if (this.gameState !== GAME_STATES.PLAYING) { + return; + } + + var idx = this.enemyIDs.indexOf(entityID); + if (idx >= 0) { + this.enemyIDs.splice(idx, 1); + Audio.playSound(TARGET_HIT_SOUND, { + volume: 1.0, + //position: position, + position: this.rootPosition, + }); + + // Update score + this.setScore(this.score + 100); + print("SCORE: ", this.score); + + this.checkWaveComplete(); + } + }, + onEnemyEscaped: function(entityID, position) { + var enemiesEscaped = false; + for (var i = this.enemyIDs.length - 1; i >= 0; --i) { + if (this.enemyIDs[i] == entityID) { + Entities.deleteEntity(this.enemyIDs[i]); + this.enemyIDs.splice(i, 1); + this.setLivesLeft(this.livesLeft - 1); + Audio.playSound(ESCAPE_SOUND, { + volume: 1.0, + position: this.rootPosition + }); + enemiesEscaped = true; + } + } + //print("LIVES LEFT: ", this.livesLeft, this.numberOfEntitiesLeftForWave); + if (this.livesLeft <= 0) { + this.endGame(); + } else if (enemiesEscaped) { + this.checkWaveComplete(); + } + }, +}; diff --git a/unpublishedScripts/DomainContent/Toybox/towerDefense/shortbowServerEntity.js b/unpublishedScripts/DomainContent/Toybox/towerDefense/shortbowServerEntity.js new file mode 100644 index 0000000000..7f700705df --- /dev/null +++ b/unpublishedScripts/DomainContent/Toybox/towerDefense/shortbowServerEntity.js @@ -0,0 +1,58 @@ +(function() { + Script.include('utils.js?' + Date.now()); + Script.include('playWaveGame.js?' + Date.now()); + Script.include('shortbowGameManager.js?' + Date.now()); + + this.entityID = null; + var gameManager = null; + this.preload = function(entityID) { + this.entityID = entityID; + + var props = Entities.getEntityProperties(entityID, ['position', 'userData']); + var data = utils.parseJSON(props.userData); + if (data === undefined) { + print("Error parsing shortbow entity userData, returning"); + return; + } + + var rootPosition = props.position; + + // Generate goal that the enemies try to get to + goalPosition = Vec3.sum(rootPosition, { x: 0, y: -10, z: -20 }); + const BASES_HEIGHT = 16; + const ROOF_HEIGHT = 0.2; + + goalPosition.y += BASES_HEIGHT - ROOF_HEIGHT; + + var platformID = data.platformID; + var buttonID = data.buttonID; + var waveDisplayID = data.waveDisplayID; + var livesDisplayID = data.livesDisplayID; + var scoreDisplayID = data.scoreDisplayID; + var highScoreDisplayID = data.highScoreDisplayID; + + const BASES_SIZE = 15; + var goalPositionFront = Vec3.sum(goalPosition, { x: 0, y: 0, z: BASES_SIZE / 2 }); + + var bowPositions = []; + var spawnPositions = []; + for (var i = 0; i < TEMPLATES.length; ++i) { + var template = TEMPLATES[i]; + if (template.name === "SB.BowSpawn") { + bowPositions.push(Vec3.sum(rootPosition, template.localPosition)); + Vec3.print("Pushing bow position", Vec3.sum(rootPosition, template.localPosition)); + } else if (template.name === "SB.EnemySpawn") { + spawnPositions.push(Vec3.sum(rootPosition, template.localPosition)); + Vec3.print("Pushing spawnposition", Vec3.sum(rootPosition, template.localPosition)); + } + } + + gameManager = new ShortbowGameManager(rootPosition, goalPositionFront, bowPositions, spawnPositions, this.entityID, buttonID, waveDisplayID, scoreDisplayID, livesDisplayID, highScoreDisplayID); + }; + this.unload = function() { + if (gameManager) { + gameManager.cleanup(); + gameManager = null; + } + }; +}); diff --git a/unpublishedScripts/DomainContent/Toybox/towerDefense/startGameButton.js b/unpublishedScripts/DomainContent/Toybox/towerDefense/startGameButton.js index 321a6108b2..331df15c72 100644 --- a/unpublishedScripts/DomainContent/Toybox/towerDefense/startGameButton.js +++ b/unpublishedScripts/DomainContent/Toybox/towerDefense/startGameButton.js @@ -5,7 +5,9 @@ }; StartButton.prototype = { preload: function(entityID) { + print("Preloading start button"); this.entityID = entityID; + this.commChannel = "shortbow-" + Entities.getEntityProperties(entityID, 'parentID').parentID; Script.addEventHandler(entityID, "collisionWithEntity", this.onCollide.bind(this)); }, signalAC: function() {