|
@@ -0,0 +1,714 @@
|
|
|
+/**
|
|
|
+ * 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: <span>' + Math.round( score ) + '</span>';
|
|
|
+ scoreText += ' Time: <span>' + Math.round( ( ( new Date().getTime() - time ) / 1000 ) * 100 ) / 100 + 's</span>';
|
|
|
+ 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: <span>' + Math.round( score ) + '</span>';
|
|
|
+ scoreText += ' Time: <span>' + Math.round( ( ( new Date().getTime() - time ) / 1000 ) * 100 ) / 100 + 's</span>';
|
|
|
+ scoreText += ' <p class="fps">FPS: <span>' + Math.round( fps ) + ' ('+Math.round(Math.max(Math.min(fps/FRAMERATE, FRAMERATE), 0)*100)+'%)</span></p>';
|
|
|
+ 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();
|