/* Simple JavaScript Inheritance * By John Resig http://ejohn.org/ * MIT Licensed. */ // Inspired by base2 and Prototype (function(){ var initializing = false, fnTest = /xyz/.test(function(){xyz;}) ? /\b_super\b/ : /.*/; // The base Class implementation (does nothing) this.Class = function(){}; // Create a new Class that inherits from this class Class.extend = function(prop) { var _super = this.prototype; // Instantiate a base class (but only create the instance, // don't run the init constructor) initializing = true; var prototype = new this(); initializing = false; // Copy the properties over onto the new prototype for (var name in prop) { // Check if we're overwriting an existing function prototype[name] = typeof prop[name] == "function" && typeof _super[name] == "function" && fnTest.test(prop[name]) ? (function(name, fn){ return function() { var tmp = this._super; // Add a new ._super() method that is the same method // but on the super-class this._super = _super[name]; // The method only need to be bound temporarily, so we // remove it when we're done executing var ret = fn.apply(this, arguments); this._super = tmp; return ret; }; })(name, prop[name]) : prop[name]; } // The dummy class constructor function Class() { // All construction is actually done in the init method if ( !initializing && this.init ) this.init.apply(this, arguments); } // Populate our constructed prototype object Class.prototype = prototype; // Enforce the constructor to be what we expect Class.prototype.constructor = Class; // And make this class extendable Class.extend = arguments.callee; return Class; }; })(); // ################################################################### // shims // // ################################################################### (function() { var requestAnimationFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame || window.msRequestAnimationFrame; window.requestAnimationFrame = requestAnimationFrame; })(); (function() { if (!window.performance.now) { window.performance.now = (!Date.now) ? function() { return new Date().getTime(); } : function() { return Date.now(); } } })(); // ################################################################### // Constants // // ################################################################### var IS_CHROME = /Chrome/.test(navigator.userAgent) && /Google Inc/.test(navigator.vendor); var CANVAS_WIDTH = 640; var CANVAS_HEIGHT = 640; var SPRITE_SHEET_SRC = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAAEACAYAAAADRnAGAAACGUlEQVR42u3aSQ7CMBAEQIsn8P+/hiviAAK8zFIt5QbELiTHmfEYE3L9mZE9AAAAqAVwBQ8AAAD6THY5CgAAAKbfbPX3AQAAYBEEAADAuZrC6UUyfMEEAIBiAN8OePXnAQAAsLcmmKFPAQAAgHMbm+gbr3Sdo/LtcAAAANR6GywPAgBAM4D2JXAAABoBzBjA7AmlOx8AAEAzAOcDAADovTc4vQim6wUCABAYQG8QAADd4dPd2fRVYQAAANQG0B4HAABAawDnAwAA6AXgfAAAALpA2uMAAABwPgAAgPoAM9Ci/R4AAAD2dmqcEQIAIC/AiQGuAAYAAECcRS/a/cJXkUf2AAAAoBaA3iAAALrD+gIAAADY9baX/nwAAADNADwFAADo9YK0e5FMX/UFACA5QPSNEAAAAHKtCekmDAAAAADvBljtfgAAAGgMMGOrunvCy2uCAAAACFU6BwAAwF6AGQPa/XsAAADYB+B8AAAAtU+ItD4OAwAAAFVhAACaA0T7B44/BQAAANALwGMQAAAAADYO8If2+P31AgAAQN0SWbhFDwCAZlXgaO1xAAAA1FngnA8AACAeQPSNEAAAAM4CnC64AAAA4GzN4N9NSfgKEAAAAACszO26X8/X6BYAAAD0Anid8KcLAAAAAAAAAJBnwNEvAAAA9Jns1ygAAAAAAAAAAAAAAAAAAABAQ4COCENERERERERERBrnAa1sJuUVr3rsAAAAAElFTkSuQmCC'; var LEFT_KEY = 37; var RIGHT_KEY = 39; var SHOOT_KEY = 88; var TEXT_BLINK_FREQ = 500; var PLAYER_CLIP_RECT = { x: 0, y: 204, w: 62, h: 32 }; var ALIEN_BOTTOM_ROW = [ { x: 0, y: 0, w: 51, h: 34 }, { x: 0, y: 102, w: 51, h: 34 }]; var ALIEN_MIDDLE_ROW = [ { x: 0, y: 137, w: 50, h: 33 }, { x: 0, y: 170, w: 50, h: 34 }]; var ALIEN_TOP_ROW = [ { x: 0, y: 68, w: 50, h: 32 }, { x: 0, y: 34, w: 50, h: 32 }]; var ALIEN_X_MARGIN = 40; var ALIEN_SQUAD_WIDTH = 11 * ALIEN_X_MARGIN; // ################################################################### // Utility functions & classes // // ################################################################### function getRandomArbitrary(min, max) { return Math.random() * (max - min) + min; } function getRandomInt(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min; } function clamp(num, min, max) { return Math.min(Math.max(num, min), max); } function valueInRange(value, min, max) { return (value <= max) && (value >= min); } function checkRectCollision(A, B) { var xOverlap = valueInRange(A.x, B.x, B.x + B.w) || valueInRange(B.x, A.x, A.x + A.w); var yOverlap = valueInRange(A.y, B.y, B.y + B.h) || valueInRange(B.y, A.y, A.y + A.h); return xOverlap && yOverlap; } var Point2D = Class.extend({ init: function(x, y) { this.x = (typeof x === 'undefined') ? 0 : x; this.y = (typeof y === 'undefined') ? 0 : y; }, set: function(x, y) { this.x = x; this.y = y; } }); var Rect = Class.extend({ init: function(x, y, w, h) { this.x = (typeof x === 'undefined') ? 0 : x; this.y = (typeof y === 'undefined') ? 0 : y; this.w = (typeof w === 'undefined') ? 0 : w; this.h = (typeof h === 'undefined') ? 0 : h; }, set: function(x, y, w, h) { this.x = x; this.y = y; this.w = w; this.h = h; } }); // ################################################################### // Globals // // ################################################################### var canvas = null; var ctx = null; var spriteSheetImg = null; var bulletImg = null; var keyStates = null; var prevKeyStates = null; var lastTime = 0; var player = null; var aliens = []; var particleManager = null; var updateAlienLogic = false; var alienDirection = -1; var alienYDown = 0; var alienCount = 0; var wave = 1; var hasGameStarted = false; // ################################################################### // Entities // // ################################################################### var BaseSprite = Class.extend({ init: function(img, x, y) { this.img = img; this.position = new Point2D(x, y); this.scale = new Point2D(1, 1); this.bounds = new Rect(x, y, this.img.width, this.img.height); this.doLogic = true; }, update: function(dt) { }, _updateBounds: function() { this.bounds.set(this.position.x, this.position.y, ~~(0.5 + this.img.width * this.scale.x), ~~(0.5 + this.img.height * this.scale.y)); }, _drawImage: function() { ctx.drawImage(this.img, this.position.x, this.position.y); }, draw: function(resized) { this._updateBounds(); this._drawImage(); } }); var SheetSprite = BaseSprite.extend({ init: function(sheetImg, clipRect, x, y) { this._super(sheetImg, x, y); this.clipRect = clipRect; this.bounds.set(x, y, this.clipRect.w, this.clipRect.h); }, update: function(dt) {}, _updateBounds: function() { var w = ~~(0.5 + this.clipRect.w * this.scale.x); var h = ~~(0.5 + this.clipRect.h * this.scale.y); this.bounds.set(this.position.x - w/2, this.position.y - h/2, w, h); }, _drawImage: function() { ctx.save(); ctx.transform(this.scale.x, 0, 0, this.scale.y, this.position.x, this.position.y); ctx.drawImage(this.img, this.clipRect.x, this.clipRect.y, this.clipRect.w, this.clipRect.h, ~~(0.5 + -this.clipRect.w*0.5), ~~(0.5 + -this.clipRect.h*0.5), this.clipRect.w, this.clipRect.h); ctx.restore(); }, draw: function(resized) { this._super(resized); } }); var Player = SheetSprite.extend({ init: function() { this._super(spriteSheetImg, PLAYER_CLIP_RECT, CANVAS_WIDTH/2, CANVAS_HEIGHT - 70); this.scale.set(0.85, 0.85); this.lives = 3; this.xVel = 0; this.bullets = []; this.bulletDelayAccumulator = 0; this.score = 0; }, reset: function() { this.lives = 3; this.score = 0; this.position.set(CANVAS_WIDTH/2, CANVAS_HEIGHT - 70); }, shoot: function() { var bullet = new Bullet(this.position.x, this.position.y - this.bounds.h / 2, 1, 1000); this.bullets.push(bullet); }, handleInput: function() { if (isKeyDown(LEFT_KEY)) { this.xVel = -175; } else if (isKeyDown(RIGHT_KEY)) { this.xVel = 175; } else this.xVel = 0; if (wasKeyPressed(SHOOT_KEY)) { if (this.bulletDelayAccumulator > 0.5) { this.shoot(); this.bulletDelayAccumulator = 0; } } }, updateBullets: function(dt) { for (var i = this.bullets.length - 1; i >= 0; i--) { var bullet = this.bullets[i]; if (bullet.alive) { bullet.update(dt); } else { this.bullets.splice(i, 1); bullet = null; } } }, update: function(dt) { // update time passed between shots this.bulletDelayAccumulator += dt; // apply x vel this.position.x += this.xVel * dt; // cap player position in screen bounds this.position.x = clamp(this.position.x, this.bounds.w/2, CANVAS_WIDTH - this.bounds.w/2); this.updateBullets(dt); }, draw: function(resized) { this._super(resized); // draw bullets for (var i = 0, len = this.bullets.length; i < len; i++) { var bullet = this.bullets[i]; if (bullet.alive) { bullet.draw(resized); } } } }); var Bullet = BaseSprite.extend({ init: function(x, y, direction, speed) { this._super(bulletImg, x, y); this.direction = direction; this.speed = speed; this.alive = true; }, update: function(dt) { this.position.y -= (this.speed * this.direction) * dt; if (this.position.y < 0) { this.alive = false; } }, draw: function(resized) { this._super(resized); } }); var Enemy = SheetSprite.extend({ init: function(clipRects, x, y) { this._super(spriteSheetImg, clipRects[0], x, y); this.clipRects = clipRects; this.scale.set(0.5, 0.5); this.alive = true; this.onFirstState = true; this.stepDelay = 1; // try 2 secs to start with... this.stepAccumulator = 0; this.doShoot - false; this.bullet = null; }, toggleFrame: function() { this.onFirstState = !this.onFirstState; this.clipRect = (this.onFirstState) ? this.clipRects[0] : this.clipRects[1]; }, shoot: function() { this.bullet = new Bullet(this.position.x, this.position.y + this.bounds.w/2, -1, 500); }, update: function(dt) { this.stepAccumulator += dt; if (this.stepAccumulator >= this.stepDelay) { if (this.position.x < this.bounds.w/2 + 20 && alienDirection < 0) { updateAlienLogic = true; } if (alienDirection === 1 && this.position.x > CANVAS_WIDTH - this.bounds.w/2 - 20) { updateAlienLogic = true; } if (this.position.y > CANVAS_WIDTH - 50) { reset(); } var fireTest = Math.floor(Math.random() * (this.stepDelay + 1)); if (getRandomArbitrary(0, 1000) <= 5 * (this.stepDelay + 1)) { this.doShoot = true; } this.position.x += 10 * alienDirection; this.toggleFrame(); this.stepAccumulator = 0; } this.position.y += alienYDown; if (this.bullet !== null && this.bullet.alive) { this.bullet.update(dt); } else { this.bullet = null; } }, draw: function(resized) { this._super(resized); if (this.bullet !== null && this.bullet.alive) { this.bullet.draw(resized); } } }); var ParticleExplosion = Class.extend({ init: function() { this.particlePool = []; this.particles = []; }, draw: function() { for (var i = this.particles.length - 1; i >= 0; i--) { var particle = this.particles[i]; particle.moves++; particle.x += particle.xunits; particle.y += particle.yunits + (particle.gravity * particle.moves); particle.life--; if (particle.life <= 0 ) { if (this.particlePool.length < 100) { this.particlePool.push(this.particles.splice(i,1)); } else { this.particles.splice(i,1); } } else { ctx.globalAlpha = (particle.life)/(particle.maxLife); ctx.fillStyle = particle.color; ctx.fillRect(particle.x, particle.y, particle.width, particle.height); ctx.globalAlpha = 1; } } }, createExplosion: function(x, y, color, number, width, height, spd, grav, lif) { for (var i =0;i < number;i++) { var angle = Math.floor(Math.random()*360); var speed = Math.floor(Math.random()*spd/2) + spd; var life = Math.floor(Math.random()*lif)+lif/2; var radians = angle * Math.PI/ 180; var xunits = Math.cos(radians) * speed; var yunits = Math.sin(radians) * speed; if (this.particlePool.length > 0) { var tempParticle = this.particlePool.pop(); tempParticle.x = x; tempParticle.y = y; tempParticle.xunits = xunits; tempParticle.yunits = yunits; tempParticle.life = life; tempParticle.color = color; tempParticle.width = width; tempParticle.height = height; tempParticle.gravity = grav; tempParticle.moves = 0; tempParticle.alpha = 1; tempParticle.maxLife = life; this.particles.push(tempParticle); } else { this.particles.push({x:x,y:y,xunits:xunits,yunits:yunits,life:life,color:color,width:width,height:height,gravity:grav,moves:0,alpha:1, maxLife:life}); } } } }); // ################################################################### // Initialization functions // // ################################################################### function initCanvas() { // create our canvas and context canvas = document.getElementById('game-canvas'); ctx = canvas.getContext('2d'); // turn off image smoothing setImageSmoothing(false); // create our main sprite sheet img spriteSheetImg = new Image(); spriteSheetImg.src = SPRITE_SHEET_SRC; preDrawImages(); // add event listeners and initially resize window.addEventListener('resize', resize); document.addEventListener('keydown', onKeyDown); document.addEventListener('keyup', onKeyUp); } function preDrawImages() { var canvas = drawIntoCanvas(2, 8, function(ctx) { ctx.fillStyle = 'white'; ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height); }); bulletImg = new Image(); bulletImg.src = canvas.toDataURL(); } function setImageSmoothing(value) { this.ctx['imageSmoothingEnabled'] = value; this.ctx['mozImageSmoothingEnabled'] = value; this.ctx['oImageSmoothingEnabled'] = value; this.ctx['webkitImageSmoothingEnabled'] = value; this.ctx['msImageSmoothingEnabled'] = value; } function initGame() { dirtyRects = []; aliens = []; player = new Player(); particleManager = new ParticleExplosion(); setupAlienFormation(); drawBottomHud(); } function setupAlienFormation() { alienCount = 0; for (var i = 0, len = 5 * 11; i < len; i++) { var gridX = (i % 11); var gridY = Math.floor(i / 11); var clipRects; switch (gridY) { case 0: case 1: clipRects = ALIEN_BOTTOM_ROW; break; case 2: case 3: clipRects = ALIEN_MIDDLE_ROW; break; case 4: clipRects = ALIEN_TOP_ROW; break; } aliens.push(new Enemy(clipRects, (CANVAS_WIDTH/2 - ALIEN_SQUAD_WIDTH/2) + ALIEN_X_MARGIN/2 + gridX * ALIEN_X_MARGIN, CANVAS_HEIGHT/3.25 - gridY * 40)); alienCount++; } } function reset() { aliens = []; setupAlienFormation(); player.reset(); } function init() { initCanvas(); keyStates = []; prevKeyStates = []; resize(); } // ################################################################### // Helpful input functions // // ################################################################### function isKeyDown(key) { return keyStates[key]; } function wasKeyPressed(key) { return !prevKeyStates[key] && keyStates[key]; } // ################################################################### // Drawing & Update functions // // ################################################################### function updateAliens(dt) { if (updateAlienLogic) { updateAlienLogic = false; alienDirection = -alienDirection; alienYDown = 25; } for (var i = aliens.length - 1; i >= 0; i--) { var alien = aliens[i]; if (!alien.alive) { aliens.splice(i, 1); alien = null; alienCount--; if (alienCount < 1) { wave++; setupAlienFormation(); } return; } alien.stepDelay = ((alienCount * 20) - (wave * 10)) / 1000; if (alien.stepDelay <= 0.05) { alien.stepDelay = 0.05; } alien.update(dt); if (alien.doShoot) { alien.doShoot = false; alien.shoot(); } } alienYDown = 0; } function resolveBulletEnemyCollisions() { var bullets = player.bullets; for (var i = 0, len = bullets.length; i < len; i++) { var bullet = bullets[i]; for (var j = 0, alen = aliens.length; j < alen; j++) { var alien = aliens[j]; if (checkRectCollision(bullet.bounds, alien.bounds)) { alien.alive = bullet.alive = false; particleManager.createExplosion(alien.position.x, alien.position.y, 'white', 70, 5,5,3,.15,50); player.score += 25; } } } } function resolveBulletPlayerCollisions() { for (var i = 0, len = aliens.length; i < len; i++) { var alien = aliens[i]; if (alien.bullet !== null && checkRectCollision(alien.bullet.bounds, player.bounds)) { if (player.lives === 0) { hasGameStarted = false; } else { alien.bullet.alive = false; particleManager.createExplosion(player.position.x, player.position.y, 'green', 100, 8,8,6,0.001,40); player.position.set(CANVAS_WIDTH/2, CANVAS_HEIGHT - 70); player.lives--; break; } } } } function resolveCollisions() { resolveBulletEnemyCollisions(); resolveBulletPlayerCollisions(); } function updateGame(dt) { player.handleInput(); prevKeyStates = keyStates.slice(); player.update(dt); updateAliens(dt); resolveCollisions(); } function drawIntoCanvas(width, height, drawFunc) { var canvas = document.createElement('canvas'); canvas.width = width; canvas.height = height; var ctx = canvas.getContext('2d'); drawFunc(ctx); return canvas; } function fillText(text, x, y, color, fontSize) { if (typeof color !== 'undefined') ctx.fillStyle = color; if (typeof fontSize !== 'undefined') ctx.font = fontSize + 'px Play'; ctx.fillText(text, x, y); } function fillCenteredText(text, x, y, color, fontSize) { var metrics = ctx.measureText(text); fillText(text, x - metrics.width/2, y, color, fontSize); } function fillBlinkingText(text, x, y, blinkFreq, color, fontSize) { if (~~(0.5 + Date.now() / blinkFreq) % 2) { fillCenteredText(text, x, y, color, fontSize); } } function drawBottomHud() { ctx.fillStyle = '#02ff12'; ctx.fillRect(0, CANVAS_HEIGHT - 30, CANVAS_WIDTH, 2); fillText(player.lives + ' x ', 10, CANVAS_HEIGHT - 7.5, 'white', 20); ctx.drawImage(spriteSheetImg, player.clipRect.x, player.clipRect.y, player.clipRect.w, player.clipRect.h, 45, CANVAS_HEIGHT - 23, player.clipRect.w * 0.5, player.clipRect.h * 0.5); fillText('CREDITI: ', CANVAS_WIDTH - 115, CANVAS_HEIGHT - 7.5); fillCenteredText('PUNTEGGIO: ' + player.score, CANVAS_WIDTH/2, 20); fillBlinkingText('00', CANVAS_WIDTH - 25, CANVAS_HEIGHT - 7.5, TEXT_BLINK_FREQ); } function drawAliens(resized) { for (var i = 0; i < aliens.length; i++) { var alien = aliens[i]; alien.draw(resized); } } function drawGame(resized) { player.draw(resized); drawAliens(resized); particleManager.draw(); drawBottomHud(); } function drawStartScreen() { fillCenteredText("Space Invaders", CANVAS_WIDTH/2, CANVAS_HEIGHT/2.75, '#FFFFFF', 36); fillBlinkingText("Invio per giocare, x per sparare!", CANVAS_WIDTH/2, CANVAS_HEIGHT/2, 500, '#FFFFFF', 36); } function animate() { var now = window.performance.now(); var dt = now - lastTime; if (dt > 100) dt = 100; if (wasKeyPressed(13) && !hasGameStarted) { initGame(); hasGameStarted = true; } if (hasGameStarted) { updateGame(dt / 1000); } ctx.fillStyle = 'black'; ctx.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT); if (hasGameStarted) { drawGame(false); } else { drawStartScreen(); } lastTime = now; requestAnimationFrame(animate); } // ################################################################### // Event Listener functions // // ################################################################### function resize() { var w = window.innerWidth; var h = window.innerHeight; // calculate the scale factor to keep a correct aspect ratio var scaleFactor = Math.min(w / CANVAS_WIDTH, h / CANVAS_HEIGHT); if (IS_CHROME) { canvas.width = CANVAS_WIDTH * scaleFactor; canvas.height = CANVAS_HEIGHT * scaleFactor; setImageSmoothing(false); ctx.transform(scaleFactor, 0, 0, scaleFactor, 0, 0); } else { // resize the canvas css properties canvas.style.width = CANVAS_WIDTH * scaleFactor + 'px'; canvas.style.height = CANVAS_HEIGHT * scaleFactor + 'px'; } } function onKeyDown(e) { e.preventDefault(); keyStates[e.keyCode] = true; } function onKeyUp(e) { e.preventDefault(); keyStates[e.keyCode] = false; } // ################################################################### // Start game! // // ################################################################### window.onload = function() { init(); animate(); };