overte-HifiExperiments/unpublishedScripts/DomainContent/Toybox/towerDefense/playWaveGame.js
2017-02-03 10:01:08 -08:00

637 lines
20 KiB
JavaScript

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('shortbow.js?' + Date.now());
var TEMPLATES = SHORTBOW_ENTITIES.Entities;
print(utils.parseJSON);
print(utils.findSurfaceBelowPosition);
// Merge two objects into a new object. If a key name appears in both a and b,
// the value in a will be used.
function mergeObjects(a, b) {
var obj = {};
for (var key in b) {
obj[key] = b[key];
}
for (var key in a) {
obj[key] = a[key];
}
return obj;
}
function spawnTemplate(templateName, overrides) {
var template = getTemplate(templateName);
if (template === null) {
print("ERROR, unknown template name:", templateName);
return null;
}
print("Spawning: ", templateName);
var properties = mergeObjects(overrides, template);
return Entities.addEntity(properties);
}
function getTemplate(name) {
for (var i = 0; i < TEMPLATES.length; ++i) {
if (TEMPLATES[i].name == name) {
return TEMPLATES[i];
}
}
return null;
}
// Cleanup ShortBow template data
var scoreboardTemplate = getTemplate('SB.Scoreboard');
Vec3.print("Scoreboard:", scoreboardTemplate.position);
for (var i = 0; i < TEMPLATES.length; ++i) {
if (TEMPLATES[i].name !== scoreboardTemplate.name) {
var template = TEMPLATES[i];
// Fixup position
template.localPosition = Vec3.subtract(template.position, scoreboardTemplate.position);
delete template.position;
}
// Fixup model url
if (template.type == "Model") {
var urlParts = template.modelURL.split("/");
var filename = urlParts[urlParts.length - 1];
var newURL = Script.resolvePath("assets/" + filename);
print("Updated url", template.modelURL, "to", newURL);
template.modelURL = newURL;
}
}
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
}
}),
};
// The high score is stored as the text on a text entity
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, startButtonID, waveDisplayID, scoreDisplayID, livesDisplayID, highScoreDisplayID) {
this.gameState = GAME_STATES.IDLE;
this.bowPositions = bowPositions;
this.rootPosition = rootPosition;
this.spawnPositions = spawnPositions;
this.gatePosition = gatePosition;
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 = [];
}
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": -1,
"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
Script.setTimeout(function() {
Audio.playSound(WAVE_COMPLETE_SOUND, {
volume: 1.0,
position: this.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 && position.z < this.gatePosition.z) {
Entities.deleteEntity(this.enemyIDs[i]);
this.enemyIDs.splice(i, 1);
this.setLivesLeft(this.livesLeft - 1);
this.numberOfEntitiesLeftForWave--;
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;
}
Script.setTimeout(function() {
Audio.playSound(GAME_OVER_SOUND, {
volume: 1.0,
position: this.rootPosition
});
}, 1500);
//Entities.editEntity(this.livesDisplayID, { text: "GAME OVER" });
this.gameState = GAME_STATES.GAME_OVER;
print("GAME OVER");
// Update high score
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,
});
this.numberOfEntitiesLeftForWave--;
// 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;
var buttonID = null;
var waveDisplayID = null;
var scoreDisplayID = null;
var highScoreDisplayID = null;
var livesDisplayID = null;
function createLocalGame() {
rootPosition = utils.findSurfaceBelowPosition(MyAvatar.position);
rootPosition.y += 6.11;
scoreboardID = spawnTemplate("SB.Scoreboard", {
position: rootPosition
});
entityIDs.push(scoreboardID);
// Create start button
buttonID = spawnTemplate("SB.StartButton", {
parentID: scoreboardID,
script: Script.resolvePath("startGameButton.js"),
userData: JSON.stringify({
grabbableKey: {
wantsTrigger: true
},
gameChannel: COMM_CHANNEL_NAME
}),
});
entityIDs.push(buttonID);
// 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;
const roofPosition = goalPosition;
waveDisplayID = spawnTemplate("SB.DisplayWave", {
parentID: scoreboardID
});
entityIDs.push(waveDisplayID);
scoreDisplayID = spawnTemplate("SB.DisplayScore", {
parentID: scoreboardID
});
entityIDs.push(scoreDisplayID);
livesDisplayID = spawnTemplate("SB.DisplayLives", {
parentID: scoreboardID
});
entityIDs.push(livesDisplayID);
highScoreDisplayID = spawnTemplate("SB.DisplayHighScore", {
parentID: scoreboardID
});
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", {
parentID: scoreboardID
});
entityIDs.push(platformID);
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));
}
}
gameHasBeenBuilt = true;
var goalPositionFront = Vec3.sum(goalPosition, { x: 0, y: 0, z: BASES_SIZE / 2 });
return new GameManager(rootPosition, goalPositionFront, bowPositions, spawnPositions, buttonID, waveDisplayID, scoreDisplayID, livesDisplayID, highScoreDisplayID);
}
function createACGame() {
// TODO
throw("AC not implemented");
}
// Setup game
var gameManager;
if (this.EntityViewer !== undefined) {
createACGame();
} else {
createLocalGame();
}
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();
}
Script.scriptEnding.connect(cleanup);