diff --git a/README.md b/README.md index a4e471bc8d..df8159fde2 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,4 @@ + # Classic Arcade Game Clone Project ## Table of Contents @@ -18,3 +19,4 @@ For detailed instructions on how to get started, check out this [guide](https:// ## Contributing This repository is the starter code for _all_ Udacity students. Therefore, we most likely will not accept pull requests. + diff --git a/css/style.css b/css/style.css deleted file mode 100644 index e28c912414..0000000000 --- a/css/style.css +++ /dev/null @@ -1,3 +0,0 @@ -body { - text-align: center; -} diff --git a/images/enemy-bug.png b/images/enemy-bug.png old mode 100755 new mode 100644 index 191587b4fa..ac7c828852 Binary files a/images/enemy-bug.png and b/images/enemy-bug.png differ diff --git a/index.html b/index.html index c9a3b8e775..ee402a539b 100644 --- a/index.html +++ b/index.html @@ -2,12 +2,38 @@ - Effective JavaScript: Frogger - + + Ladybugger + + - - - - + +
+
+
+ +
+
+ + + + + diff --git a/js/app.js b/js/app.js index dad7367547..f1e654ea4a 100644 --- a/js/app.js +++ b/js/app.js @@ -1,46 +1,251 @@ -// Enemies our player must avoid -var Enemy = function() { - // Variables applied to each of our instances go here, - // we've provided one for you to get started +'use strict'; + +// GLOBAL VARIABLES +var CANVAS_WIDTH = 505; +var CANVAS_HEIGHT = 606; +var STARTING_ENEMIES = 4; +var DEFAULT_CHARACTER = 2; +var LEVEL_UP_DELAY = 1000; +var LEVEL_UP_MESSAGE = 'Level Up!'; +var GAME_OVER_MESSAGE = 'Game Over'; +// Declaring player here globally as init from Intro.handleInput method +var player, allEnemies; + +// Character select screen +var Intro = function() { + this.characters = [ + 'images/char-cat-girl.png', + 'images/char-horn-girl.png', + 'images/char-boy.png', + 'images/char-pink-girl.png', + 'images/char-princess-girl.png' + ]; + this.selector = 'images/Selector.png'; + this.selected = DEFAULT_CHARACTER; + this.ready = false; +}; + +// Draw 5 characters and selector image +Intro.prototype.render = function() { + ctx.clearRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT); + + ctx.font = '36px monospace'; + ctx.textAlign = 'center'; + ctx.globalAlpha = 0.5; + ctx.drawImage(Resources.get('images/enemy-bug.png'), 292, -5); + ctx.globalAlpha = 1; + ctx.fillText('Ladybugger', CANVAS_WIDTH / 2, 110); + ctx.font = '18px monospace'; + ctx.fillText('[<] select your hero [>]', CANVAS_WIDTH / 2, 300); + ctx.font = '14px monospace'; + ctx.fillText('hit [space] to start', CANVAS_WIDTH / 2, 550); + ctx.textAlign = 'left'; + + this.characters.forEach(function(char,i) { + ctx.drawImage(Resources.get(char), 101 * i, 300); + }); + ctx.drawImage(Resources.get(this.selector), 101 * this.selected, 360); +}; + +// Manage various dialogues and score displays for game +var Status = function() { + this.level = 1; + this.gameOver = false; + this.levelUp = false; +}; + +Status.prototype.scoreBoard = function() { + ctx.font = 'normal 24px monospace'; + // Clear transparent background - text doesn't like it, blurs a lot + ctx.clearRect(0, 0, CANVAS_WIDTH, 50); + ctx.fillText('Level: ' + this.level, 25, 40); + ctx.fillText('Enemies: ' + allEnemies.length, 200, 40); +}; + +Status.prototype.message = function(message,again) { + ctx.fillStyle = 'black'; + ctx.font = '36px monospace'; + ctx.textAlign = 'center'; + ctx.globalAlpha = 0.7; + ctx.fillRect(101, 200, 303, 150); + ctx.globalAlpha = 1; + ctx.fillStyle = 'white'; + ctx.fillText(message, CANVAS_WIDTH / 2, 275); + if(again) { + ctx.font = '16px monospace'; + ctx.fillText('press [space] to play again', CANVAS_WIDTH / 2, 325); + } + ctx.textAlign = 'left'; + ctx.fillStyle = 'black'; +}; + +Status.prototype.nextLevel = function() { + this.message(LEVEL_UP_MESSAGE); + setTimeout(function() { + this.levelUp = false; + // Add an enemy because we're sadistic :D + allEnemies.push(new Enemy()); + // Reposition player + player.init(202.5, 300); + // Start the game loop again + main(); + // Bind this to setTimeout otherwise this.levelUp becomes window scope + }.bind(this),LEVEL_UP_DELAY); +}; + + +Status.prototype.render = function() { + this.scoreBoard(); + + if(this.gameOver) { + this.message(GAME_OVER_MESSAGE,true); + } + // Display the level up dialogue between levels + if(this.levelUp) { + this.nextLevel(); + } +}; - // The image/sprite for our enemies, this uses - // a helper we've provided to easily load images +// Trying my hand at inheritance +var Entity = function() { + this.x = 0; + this.y = 0; +}; + +Entity.prototype.render = function() { + ctx.drawImage(Resources.get(this.sprite), this.x, this.y); +}; + +// Enemies our player must avoid +function Enemy() { this.sprite = 'images/enemy-bug.png'; + this.init(); +} + +Enemy.prototype = Object.create(Entity.prototype); +Enemy.prototype.constructor = Enemy; + +// Initialize enemy location and speed +Enemy.prototype.init = function() { + this.x = -100; + this.y = 51 + (83 * Math.floor(Math.random() * (4 - 1))); + this.speed = 50 * (Math.floor(Math.random() * (6 - 1)) + 1); }; -// Update the enemy's position, required method for game -// Parameter: dt, a time delta between ticks Enemy.prototype.update = function(dt) { - // You should multiply any movement by the dt parameter - // which will ensure the game runs at the same speed for - // all computers. + this.x += this.speed * dt; + // If enemy left screen re-spawn + if (this.x > CANVAS_WIDTH) { + this.init(); + } }; -// Draw the enemy on the screen, required method for game -Enemy.prototype.render = function() { - ctx.drawImage(Resources.get(this.sprite), this.x, this.y); +// There goes my (inherited) hero... +function Player(character) { + this.sprite = character; + this.init(202.5, 300); +} + +Player.prototype = Object.create(Entity.prototype); +Player.prototype.constructor = Player; + +Player.prototype.init = function(x,y) { + this.x = x; + this.y = y; +}; + +Player.prototype.update = function() { + // When player reaches water level up + if(this.y === -32 && gameStatus.levelUp === false) { + // Flag to display level up message + gameStatus.levelUp = true; + gameStatus.level++; + } +}; + +Player.prototype.move = function(x,y) { + // Keep player in bounds + if((this.x+x >= 0 && this.y+y >= -32) && (this.x+x <= 404.5 && this.y+y <= 383)) { + this.x += x; + this.y += y; + } }; -// Now write your own player class -// This class requires an update(), render() and -// a handleInput() method. +// This probably should be renamed to be more of a game.handleInput method... +Player.prototype.handleInput = function(key) { + if (key === 'left') { + this.move(-101, 0); + } + if (key === 'right') { + this.move(101, 0); + } + if (key === 'up') { + this.move(0, -83); + } + if (key === 'down') { + this.move(0, 83); + } +}; +// This restarts game after game over screen +Status.prototype.handleInput = function(key) { + if (key === 'space') { + this.gameOver = false; + initEnemies(STARTING_ENEMIES); + this.level = 1; + player.init(202.5,300); + main(); + } +}; -// Now instantiate your objects. -// Place all enemy objects in an array called allEnemies -// Place the player object in a variable called player +Intro.prototype.handleInput = function(key) { + // Our character select screen + if (key === 'left' && this.selected > 0) { + this.selected--; + } + if (key === 'right' && this.selected <4) { + this.selected++; + } + if (key === 'space') { + this.ready = true; + // Initialize player here once character selected + player = new Player(this.characters[this.selected]); + startGame(); + } + // Render on keyup rather than frames for intro + this.render(); +}; +function initEnemies(num) { + allEnemies = []; + for (var k = 0; k < num; k++) { + allEnemies.push(new Enemy()); + } +} +// Initialize our objects +var intro = new Intro(); +initEnemies(STARTING_ENEMIES); +var gameStatus = new Status(); -// This listens for key presses and sends the keys to your -// Player.handleInput() method. You don't need to modify this. + +// Keyup listener document.addEventListener('keyup', function(e) { var allowedKeys = { 37: 'left', 38: 'up', 39: 'right', - 40: 'down' + 40: 'down', + 32: 'space' }; - player.handleInput(allowedKeys[e.keyCode]); -}); + // Decide which input handler to call + if (!intro.ready) { + intro.handleInput(allowedKeys[e.keyCode]); + } else if (gameStatus.gameOver) { + gameStatus.handleInput(allowedKeys[e.keyCode]); + } else { + player.handleInput(allowedKeys[e.keyCode]); + } + +}); \ No newline at end of file diff --git a/js/engine.js b/js/engine.js index 7fb7b09149..73b70b24a3 100644 --- a/js/engine.js +++ b/js/engine.js @@ -1,3 +1,4 @@ + /* Engine.js * This file provides the game loop functionality (update entities and render), * draws the initial game board on the screen, and then calls the update and @@ -13,6 +14,7 @@ * writing app.js a little simpler to work with. */ + var Engine = (function(global) { /* Predefine the variables we'll be using within this scope, * create the canvas element, grab the 2D context for that canvas @@ -24,64 +26,45 @@ var Engine = (function(global) { ctx = canvas.getContext('2d'), lastTime; - canvas.width = 505; - canvas.height = 606; - doc.body.appendChild(canvas); + canvas.width = CANVAS_WIDTH; + canvas.height = CANVAS_HEIGHT; + doc.getElementById('gameContainer').appendChild(canvas); + + // Main main() available globally for use in app.js + global.main = function() { - /* This function serves as the kickoff point for the game loop itself - * and handles properly calling the update and render methods. - */ - function main() { - /* Get our time delta information which is required if your game - * requires smooth animation. Because everyone's computer processes - * instructions at different speeds we need a constant value that - * would be the same for everyone (regardless of how fast their - * computer is) - hurray time! - */ var now = Date.now(), dt = (now - lastTime) / 1000.0; - /* Call our update/render functions, pass along the time delta to - * our update function since it may be used for smooth animation. - */ update(dt); render(); - /* Set our lastTime variable which is used to determine the time delta - * for the next time this function is called. - */ lastTime = now; - /* Use the browser's requestAnimationFrame function to call this - * function again as soon as the browser is able to draw another frame. - */ - win.requestAnimationFrame(main); - } + // Check game isn't in a paused status before requesting new frame + if(!gameStatus.gameOver && !gameStatus.levelUp) { + win.requestAnimationFrame(main); + } + }; - /* This function does some initial setup that should only occur once, - * particularly setting the lastTime variable that is required for the - * game loop. - */ + // Let's go... refactored slightly function init() { - reset(); + intro.render(); + } + + // Starts game (duh) made global to access from app.js once character selected + global.startGame = function() { lastTime = Date.now(); main(); - } + }; - /* This function is called by main (our game loop) and itself calls all - * of the functions which may need to update entity's data. Based on how - * you implement your collision detection (when two entities occupy the - * same space, for instance when your character should die), you may find - * the need to add an additional function call here. For now, we've left - * it commented out - you may or may not want to implement this - * functionality this way (you could just implement collision detection - * on the entities themselves within your app.js file). - */ + // Update function called from main() function update(dt) { updateEntities(dt); - // checkCollisions(); + checkCollisions(); } + /* This is called by the update function and loops through all of the * objects within your allEnemies array as defined in app.js and calls * their update() methods. It will then call the update function for your @@ -89,6 +72,7 @@ var Engine = (function(global) { * the data/properties related to the object. Do your drawing in your * render methods. */ + function updateEntities(dt) { allEnemies.forEach(function(enemy) { enemy.update(dt); @@ -96,16 +80,22 @@ var Engine = (function(global) { player.update(); } - /* This function initially draws the "game level", it will then call - * the renderEntities function. Remember, this function is called every - * game tick (or loop of the game engine) because that's how games work - - * they are flipbooks creating the illusion of animation but in reality - * they are just drawing the entire screen over and over. - */ + // See if player has been eaten by a ladybug (?!) + function checkCollisions() { + allEnemies.forEach(function(enemy) { + if(enemy.y === player.y) { + if(enemy.x+60 > player.x && enemy.x-60 < player.x) { + // gameOver flag generates dialogue box in app.js + gameStatus.gameOver = true; + } + } + }); + } + + // Do all the drawing (or rendering, I suppose) function render() { - /* This array holds the relative URL to the image used - * for that particular row of the game level. - */ + + // Our background images, ordered as displayed var rowImages = [ 'images/water-block.png', // Top row is water 'images/stone-block.png', // Row 1 of 3 of stone @@ -118,6 +108,7 @@ var Engine = (function(global) { numCols = 5, row, col; + // Before drawing, clear existing canvas ctx.clearRect(0,0,canvas.width,canvas.height); @@ -125,19 +116,14 @@ var Engine = (function(global) { * and, using the rowImages array, draw the correct image for that * portion of the "grid" */ + for (row = 0; row < numRows; row++) { for (col = 0; col < numCols; col++) { - /* The drawImage function of the canvas' context element - * requires 3 parameters: the image to draw, the x coordinate - * to start drawing and the y coordinate to start drawing. - * We're using our Resources helpers to refer to our images - * so that we get the benefits of caching these images, since - * we're using them over and over. - */ ctx.drawImage(Resources.get(rowImages[row]), col * 101, row * 83); } } + renderEntities(); } @@ -145,41 +131,41 @@ var Engine = (function(global) { * tick. Its purpose is to then call the render functions you have defined * on your enemy and player entities within app.js */ + function renderEntities() { - /* Loop through all of the objects within the allEnemies array and call - * the render function you have defined. - */ + + // Draw enemies in current location allEnemies.forEach(function(enemy) { enemy.render(); }); + // Draw player in current location player.render(); - } - /* This function does nothing but it could have been a good place to - * handle game reset states - maybe a new game menu or a game over screen - * those sorts of things. It's only called once by the init() method. - */ - function reset() { - // noop + // Draw scoreboard and display game messages if necessary + gameStatus.render(); } - /* Go ahead and load all of the images we know we're going to need to - * draw our game level. Then set init as the callback method, so that when - * all of these images are properly loaded our game will start. - */ + // Load our images Resources.load([ 'images/stone-block.png', 'images/water-block.png', 'images/grass-block.png', 'images/enemy-bug.png', - 'images/char-boy.png' + 'images/char-cat-girl.png', + 'images/char-horn-girl.png', + 'images/char-boy.png', + 'images/char-pink-girl.png', + 'images/char-princess-girl.png', + 'images/Selector.png' ]); Resources.onReady(init); + /* Assign the canvas' context object to the global variable (the window * object when run in a browser) so that developers can use it more easily * from within their app.js files. */ + global.ctx = ctx; })(this);