/** * Core * MIT licensed * * Copyright (C) 2018 Hakim El Hattab, http://hakim.se */ var Core = new function(){ // Flags if we are running mobile mode var isMobile = !!navigator.userAgent.toLowerCase().match( /ipod|ipad|iphone|android/gi ); var DEFAULT_WIDTH = 1000, DEFAULT_HEIGHT = 650, BORDER_WIDTH = 6, FRAMERATE = 60; // Types of organisms var ORGANISM_ENEMY = 'enemy', ORGANISM_ENERGY = 'energy'; // The world dimensions var world = { width: isMobile ? window.innerWidth : DEFAULT_WIDTH, height: isMobile ? window.innerHeight : DEFAULT_HEIGHT }; var canvas, context; var canvasBackground, contextBackground; // UI DOM elements var status; var panels; var message; var title; var startButton; // Game elements var organisms = []; var particles = []; var player; // Mouse properties var mouseX = (window.innerWidth + world.width) * 0.5; var mouseY = (window.innerHeight + world.height) * 0.5; var mouseIsDown = false; var spaceIsDown = false; // Game properties and scoring var playing = false; var score = 0; var time = 0; var duration = 0; var difficulty = 1; var lastspawn = 0; // Game statistics var fc = 0; // Frame count var fs = 0; // Frame score var cs = 0; // Collision score // The world's velocity var velocity = { x: -1.3, y: 1 }; // Performance (FPS) tracking var fps = 0; var fpsMin = 1000; var fpsMax = 0; var timeLastSecond = new Date().getTime(); var frames = 0; this.init = function(){ canvas = document.getElementById('world'); canvasBackground = document.getElementById('background'); panels = document.getElementById('panels'); status = document.getElementById('status'); message = document.getElementById('message'); title = document.getElementById('title'); startButton = document.getElementById('startButton'); if (canvas && canvas.getContext) { context = canvas.getContext('2d'); contextBackground = canvasBackground.getContext('2d'); // Register event listeners document.addEventListener('mousemove', documentMouseMoveHandler, false); document.addEventListener('mousedown', documentMouseDownHandler, false); document.addEventListener('mouseup', documentMouseUpHandler, false); canvas.addEventListener('touchstart', documentTouchStartHandler, false); document.addEventListener('touchmove', documentTouchMoveHandler, false); document.addEventListener('touchend', documentTouchEndHandler, false); window.addEventListener('resize', windowResizeHandler, false); startButton.addEventListener('click', startButtonClickHandler, false); document.addEventListener('keydown', documentKeyDownHandler, false); document.addEventListener('keyup', documentKeyUpHandler, false); // Define our player player = new Player(); // Force an initial resize to make sure the UI is sized correctly windowResizeHandler(); // If we are running on mobile, certain elements need to be configured differently if( isMobile ) { status.style.width = world.width + 'px'; canvas.style.border = 'none'; } animate(); } }; function renderBackground() { var gradient = contextBackground.createRadialGradient( world.width * 0.5, world.height * 0.5, 0, world.width * 0.5, world.height * 0.5, 500 ); gradient.addColorStop(0,'rgba(0, 70, 70, 1)'); gradient.addColorStop(1,'rgba(0, 8, 14, 1)'); contextBackground.fillStyle = gradient; contextBackground.fillRect( 0, 0, world.width, world.height ); } /** * Handles click on the start button in the UI. */ function startButtonClickHandler(event){ if( playing == false ) { playing = true; // Reset game properties organisms = []; score = 0; difficulty = 1; // Reset game tracking properties fc = 0; fs = 0; ms = 0; cs = 0; // Reset the player data player.energy = 30; // Hide the game UI panels.style.display = 'none'; status.style.display = 'block'; time = new Date().getTime(); } } /** * Stops the currently ongoing game and shows the * resulting data in the UI. */ function gameOver() { playing = false; // Determine the duration of the game duration = new Date().getTime() - time; // Show the UI panels.style.display = 'block'; // Ensure that the score is an integer score = Math.round(score); // Write the users score to the UI title.innerHTML = 'game over! (' + score + ' points)'; // Update the status bar with the final score and time scoreText = 'score: ' + Math.round( score ) + ''; scoreText += ' time: ' + Math.round( ( ( new Date().getTime() - time ) / 1000 ) * 100 ) / 100 + 's'; status.innerHTML = scoreText; } function documentKeyDownHandler(event) { switch( event.keyCode ) { case 32: if( !spaceIsDown && player.energy > 15 ) { player.energy -= 4; } spaceIsDown = true; event.preventDefault(); break; } } function documentKeyUpHandler(event) { switch( event.keyCode ) { case 32: spaceIsDown = false; event.preventDefault(); break; } } /** * Event handler for document.onmousemove. */ function documentMouseMoveHandler(event){ mouseX = event.clientX - (window.innerWidth - world.width) * 0.5 - BORDER_WIDTH; mouseY = event.clientY - (window.innerHeight - world.height) * 0.5 - BORDER_WIDTH; } /** * Event handler for document.onmousedown. */ function documentMouseDownHandler(event){ mouseIsDown = true; } /** * Event handler for document.onmouseup. */ function documentMouseUpHandler(event) { mouseIsDown = false; } /** * Event handler for document.ontouchstart. */ function documentTouchStartHandler(event) { if(event.touches.length == 1) { event.preventDefault(); mouseX = event.touches[0].pageX - (window.innerWidth - world.width) * 0.5; mouseY = event.touches[0].pageY - (window.innerHeight - world.height) * 0.5; mouseIsDown = true; } } /** * Event handler for document.ontouchmove. */ function documentTouchMoveHandler(event) { if(event.touches.length == 1) { event.preventDefault(); mouseX = event.touches[0].pageX - (window.innerWidth - world.width) * 0.5 - 60; mouseY = event.touches[0].pageY - (window.innerHeight - world.height) * 0.5 - 30; } } /** * Event handler for document.ontouchend. */ function documentTouchEndHandler(event) { mouseIsDown = false; } /** * Event handler for window.onresize. */ function windowResizeHandler() { // Update the game size world.width = isMobile ? window.innerWidth : DEFAULT_WIDTH; world.height = isMobile ? window.innerHeight : DEFAULT_HEIGHT; // Center the player player.position.x = world.width * 0.5; player.position.y = world.height * 0.5; // Resize the canvas canvas.width = world.width; canvas.height = world.height; canvasBackground.width = world.width; canvasBackground.height = world.height; // Determine the x/y position of the canvas var cvx = (window.innerWidth - world.width) * 0.5; var cvy = (window.innerHeight - world.height) * 0.5; // Position the canvas canvas.style.position = 'absolute'; canvas.style.left = cvx + 'px'; canvas.style.top = cvy + 'px'; canvasBackground.style.position = 'absolute'; canvasBackground.style.left = cvx + BORDER_WIDTH + 'px'; canvasBackground.style.top = cvy + BORDER_WIDTH + 'px'; if( isMobile ) { panels.style.left = '0px'; panels.style.top = '0px'; panels.style.width = '100%'; panels.style.height = '100%'; status.style.left = '0px'; status.style.top = '0px'; } else { panels.style.left = cvx + BORDER_WIDTH + 'px'; panels.style.top = cvy + BORDER_WIDTH + 'px'; panels.style.width = world.width + 'px'; panels.style.height = world.height + 'px'; status.style.left = cvx + BORDER_WIDTH + 'px'; status.style.top = cvy + BORDER_WIDTH + 'px'; } renderBackground(); } /** * Emits a random number of particles from a set point. * * @param position The point where particles will be * emitted from * @param spread The pixel spread of the emittal */ function emitParticles( position, direction, spread, seed ) { var q = seed + ( Math.random() * seed ); while( --q >= 0 ) { var p = new Point(); p.position.x = position.x + ( Math.sin(q) * spread ); p.position.y = position.y + ( Math.cos(q) * spread ); p.velocity = { x: direction.x + ( -1 + Math.random() * 2 ), y: direction.y + ( - 1 + Math.random() * 2 ) }; p.alpha = 1; particles.push( p ); } } /** * Called on every frame to update the game properties * and render the current state to the canvas. */ function animate() { // Fetch the current time for this frame var frameTime = new Date().getTime(); // Increase the frame count frames ++; // Check if a second has passed since the last time we updated the FPS if( frameTime > timeLastSecond + 1000 ) { // Establish the current, minimum and maximum FPS fps = Math.min( Math.round( ( frames * 1000 ) / ( frameTime - timeLastSecond ) ), FRAMERATE ); fpsMin = Math.min( fpsMin, fps ); fpsMax = Math.max( fpsMax, fps ); timeLastSecond = frameTime; frames = 0; } // A factor by which the score will be scaled, depending on current FPS var scoreFactor = 0.01 + ( Math.max( Math.min( fps, FRAMERATE ), 0 ) / FRAMERATE * 0.99 ); // Scales down the factor by itself scoreFactor = scoreFactor * scoreFactor; // Clear the canvas of all old pixel data context.clearRect(0,0,canvas.width,canvas.height); var i, ilen, j, jlen; // Only update game properties and draw the player if a game is active if( playing ) { // Increment the difficulty slightly difficulty += 0.0015; // Increment the score depending on difficulty score += (0.4 * difficulty) * scoreFactor; // Increase the game frame count stat fc ++; // Increase the score count stats fs += (0.4 * difficulty) * scoreFactor; //player.angle = Math.atan2( mouseY - player.position.y, mouseX - player.position.x ); var targetAngle = Math.atan2( mouseY - player.position.y, mouseX - player.position.x ); if( Math.abs( targetAngle - player.angle ) > Math.PI ) { player.angle = targetAngle; } player.angle += ( targetAngle - player.angle ) * 0.2; player.energyRadiusTarget = ( player.energy / 100 ) * ( player.radius * 0.8 ); player.energyRadius += ( player.energyRadiusTarget - player.energyRadius ) * 0.2; player.shield = { x: player.position.x + Math.cos( player.angle ) * player.radius, y: player.position.y + Math.sin( player.angle ) * player.radius }; // Shield context.beginPath(); context.strokeStyle = '#648d93'; context.lineWidth = 3; context.arc( player.position.x, player.position.y, player.radius, player.angle + 1.6, player.angle - 1.6, true ); context.stroke(); // Core context.beginPath(); context.fillStyle = "#249d93"; context.strokeStyle = "#3be2d4"; context.lineWidth = 1.5; player.updateCore(); var loopedNodes = player.coreNodes.concat(); loopedNodes.push( player.coreNodes[0] ); for( var i = 0; i < loopedNodes.length; i++ ) { p = loopedNodes[i]; p2 = loopedNodes[i+1]; p.position.x += ( (player.position.x + p.normal.x + p.offset.x) - p.position.x ) * 0.2; p.position.y += ( (player.position.y + p.normal.y + p.offset.y) - p.position.y ) * 0.2; if( i == 0 ) { // This is the first loop, so we need to start by moving into position context.moveTo( p.position.x, p.position.y ); } else if( p2 ) { // Draw a curve between the current and next trail point context.quadraticCurveTo( p.position.x, p.position.y, p.position.x + ( p2.position.x - p.position.x ) / 2, p.position.y + ( p2.position.y - p.position.y ) / 2 ); } } context.closePath(); context.fill(); context.stroke(); } if( spaceIsDown && player.energy > 10 ) { player.energy -= 0.1; context.beginPath(); context.strokeStyle = '#648d93'; context.lineWidth = 1; context.fillStyle = 'rgba( 0, 100, 100, ' + ( player.energy / 100 ) * 0.9 + ' )'; context.arc( player.position.x, player.position.y, player.radius, 0, Math.PI*2, true ); context.fill(); context.stroke(); // play sound for the shield CoreAudio.playShield(); } var enemyCount = 0; var energyCount = 0; // Go through each enemy and draw it + update its properties for( i = 0; i < organisms.length; i++ ) { p = organisms[i]; p.position.x += p.velocity.x; p.position.y += p.velocity.y; p.alpha += ( 1 - p.alpha ) * 0.1; if( p.type == ORGANISM_ENEMY ) context.fillStyle = 'rgba( 255, 0, 0, ' + p.alpha + ' )'; if( p.type == ORGANISM_ENERGY ) context.fillStyle = 'rgba( 0, 235, 190, ' + p.alpha + ' )'; context.beginPath(); context.arc(p.position.x, p.position.y, p.size/2, 0, Math.PI*2, true); context.fill(); var angle = Math.atan2( p.position.y - player.position.y, p.position.x - player.position.x ); if (playing) { var dist = Math.abs( angle - player.angle ); if( dist > Math.PI ) { dist = ( Math.PI * 2 ) - dist; } if ( dist < 1.6 ) { if (p.distanceTo(player.position) > player.radius - 5 && p.distanceTo(player.position) < player.radius + 5) { p.dead = true; // play sound CoreAudio.organismDead(); } } if (spaceIsDown && p.distanceTo(player.position) < player.radius && player.energy > 11) { p.dead = true; score += 4; } if (p.distanceTo(player.position) < player.energyRadius + (p.size * 0.5)) { if (p.type == ORGANISM_ENEMY) { player.energy -= 6; // play sound CoreAudio.energyDown(); } if (p.type == ORGANISM_ENERGY) { player.energy += 8; score += 30; // play sound CoreAudio.energyUp(); } player.energy = Math.max(Math.min(player.energy, 100), 0); p.dead = true; } } // If the enemy is outside of the game bounds, destroy it if( p.position.x < -p.size || p.position.x > world.width + p.size || p.position.y < -p.size || p.position.y > world.height + p.size ) { p.dead = true; } // If the enemy is dead, remove it if( p.dead ) { emitParticles( p.position, { x: (p.position.x - player.position.x) * 0.02, y: (p.position.y - player.position.y) * 0.02 }, 5, 5 ); organisms.splice( i, 1 ); i --; } else { if( p.type == ORGANISM_ENEMY ) enemyCount ++; if( p.type == ORGANISM_ENERGY ) energyCount ++; } } // If there are less enemies than intended for this difficulty, add another one if( enemyCount < 1 * difficulty && new Date().getTime() - lastspawn > 100 ) { organisms.push( giveLife( new Enemy() ) ); lastspawn = new Date().getTime(); } // if( energyCount < 1 && Math.random() > 0.996 ) { organisms.push( giveLife( new Energy() ) ); } // Go through and draw all particle effects for( i = 0; i < particles.length; i++ ) { p = particles[i]; // Apply velocity to the particle p.position.x += p.velocity.x; p.position.y += p.velocity.y; // Fade out p.alpha -= 0.02; // Draw the particle context.fillStyle = 'rgba(255,255,255,'+Math.max(p.alpha,0)+')'; context.fillRect( p.position.x, p.position.y, 1, 1 ); // If the particle is faded out to less than zero, remove it if( p.alpha <= 0 ) { particles.splice( i, 1 ); } } // If the game is active, update the game status bar with score, duration and FPS if( playing ) { scoreText = 'score: ' + Math.round( score ) + ''; scoreText += ' time: ' + Math.round( ( ( new Date().getTime() - time ) / 1000 ) * 100 ) / 100 + 's'; scoreText += '

fps: ' + Math.round( fps ) + ' ('+Math.round(Math.max(Math.min(fps/FRAMERATE, FRAMERATE), 0)*100)+'%)

'; status.innerHTML = scoreText; if( player.energy === 0 ) { emitParticles( player.position, { x: 0, y: 0 }, 10, 40 ); gameOver(); // play sound CoreAudio.playGameOver(); } } requestAnimFrame( animate ); } /** * */ function giveLife( organism ) { var side = Math.round( Math.random() * 3 ); switch( side ) { case 0: organism.position.x = 10; organism.position.y = world.height * Math.random(); break; case 1: organism.position.x = world.width * Math.random(); organism.position.y = 10; break; case 2: organism.position.x = world.width - 10; organism.position.y = world.height * Math.random(); break; case 3: organism.position.x = world.width * Math.random(); organism.position.y = world.height - 10; break; } if( playing ) CoreAudio.playSynth( organism.position.x / world.width ); organism.speed = Math.min( Math.max( Math.random(), 0.6 ), 0.75 ); organism.velocity.x = ( player.position.x - organism.position.x ) * 0.006 * organism.speed; organism.velocity.y = ( player.position.y - organism.position.y ) * 0.006 * organism.speed; if( organism.type == 'enemy' ) { organism.velocity.x *= (1+(Math.random()*0.1)); organism.velocity.y *= (1+(Math.random()*0.1)); } organism.alpha = 0; return organism; } }; function Point( x, y ) { this.position = { x: x, y: y }; } Point.prototype.distanceTo = function(p) { var dx = p.x-this.position.x; var dy = p.y-this.position.y; return Math.sqrt(dx*dx + dy*dy); }; Point.prototype.clonePosition = function() { return { x: this.position.x, y: this.position.y }; }; function Player() { this.position = { x: 0, y: 0 }; this.length = 15; this.energy = 30; this.energyRadius = 0; this.energyRadiusTarget = 0; this.radius = 60; this.angle = 0; this.coreQuality = 16; this.coreNodes = []; } Player.prototype = new Point(); Player.prototype.updateCore = function() { var i, j, n; if( this.coreNodes.length == 0 ) { var i, n; for (i = 0; i < this.coreQuality; i++) { n = { position: { x: this.position.x, y: this.position.y }, normal: { x: 0, y: 0 }, normalTarget: { x: 0, y: 0 }, offset: { x: 0, y: 0 } }; this.coreNodes.push( n ); } } for (i = 0; i < this.coreQuality; i++) { var n = this.coreNodes[i]; var angle = ( i / this.coreQuality ) * Math.PI * 2; n.normal.x = Math.cos( angle ) * this.energyRadius; n.normal.y = Math.sin( angle ) * this.energyRadius; n.offset.x = Math.random() * 5; n.offset.y = Math.random() * 5; } }; function Enemy() { this.position = { x: 0, y: 0 }; this.velocity = { x: 0, y: 0 }; this.size = 6 + ( Math.random() * 4 ); this.speed = 1; this.type = 'enemy'; } Enemy.prototype = new Point(); function Energy() { this.position = { x: 0, y: 0 }; this.velocity = { x: 0, y: 0 }; this.size = 10 + ( Math.random() * 6 ); this.speed = 1; this.type = 'energy'; } Energy.prototype = new Point(); // shim with setTimeout fallback from http://paulirish.com/2011/requestanimationframe-for-smart-animating/ window.requestAnimFrame = (function(){ return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.oRequestAnimationFrame || window.msRequestAnimationFrame || function(/* function */ callback, /* DOMElement */ element){ window.setTimeout(callback, 1000 / 60); }; })(); Core.init();