www.gusucode.com > 线圈大师微信单独游戏包源码程序 > 线圈大师/Coil-master/Coil-master/js/coil.js

    /**
 * Encapsulates the main logic of the Coil game.
 * Available under MIT license.
 * 
 * @author Hakim El Hattab (http://hakim.se)
 */
var Coil = (function(){
	
	// Target framerate 
	var FRAMERATE = 60;
		
	// Default dimensions of the world
	var DEFAULT_WIDTH = 900,
		DEFAULT_HEIGHT = 510;
	
	// Flags if the game should output debug information
	var DEBUG = URLUtil.queryValue('debug') == '1';

	var TOUCH_INPUT = navigator.userAgent.match( /(iPhone|iPad|iPod|Android)/i );
	
	// The number of enemies that may exist at the same time,
	// this scales depending on difficulty
	var ENEMY_COUNT = 2;
		ENEMY_SIZE = 10;
	
	// The height of the header / status bar
	var HEADER_HEIGHT = 30;
	
	var MENU_FADE_IN_DURATION = 600,
		MENU_FADE_OUT_DURATION = 600;
	
	var ENEMY_TYPE_NORMAL = 1,
		ENEMY_TYPE_BOMB = 2,
		ENEMY_TYPE_NORMAL_MOVER = 3,
		ENEMY_TYPE_BOMB_MOVER = 4;
	
	var ENEMY_MOVER_START_FRAME = FRAMERATE * 2;
	
	// Game states applied to the body so that elements can be 
	// toggled as needed in CSS
	var STATE_WELCOME = 'start',
		STATE_PLAYING = 'playing',
		STATE_LOSER = 'loser';
		STATE_WINNER = 'winner';
	
	// Scoring defaults (these may scale depending on difficulty)
	var SCORE_PER_ENEMY = 30,
		SCORE_PER_TICK = 0.01;
	
	var ENERGY_PER_ENEMY_DEATH = -30,
		ENERGY_PER_ENEMY_ENCLOSED = 1,
		ENERGY_PER_BOMB_ENCLOSED = -30;
	
	// The maximum score multiplier that may be reached
	var MULTIPLIER_LIMIT = 4;
	
	// The maximum number of simultaneous effects to run
	var NUMBER_OF_EFFECTS = 10;
	
	// The world dimensions
	var world = { 
		width: DEFAULT_WIDTH, 
		height: DEFAULT_HEIGHT 
	};
		
	// Mouse input tracking
	var mouse = {
		// The current position
		x: 0,
		y: 0,
		
		// The position previous to the current
		previousX: 0,
		previousY: 0,
		
		// The velocity, based on the difference between
		// the current and next positions
		velocityX: 0,
		velocityY: 0,
		
		// Flags if the mouse is currently pressed down
		down: false
	};
	
	var sprites = {
		bomb: null,
		enemy: null,
		enemyDyingA: null,
		enemyDyingB: null
	}
	
	var canvas,
		context,
		
		// WebGL canvas and context
		canvas3d,
		context3d,
		
		dirtyRegions = [],
		
		effectsEnabled = false,
		effectsShaderProgram,
		effectsVertices,
		effectsBuffer,
		effectsTexture,
		effectsTime = 0,
		
		// DOM elements
		container,
		menu,
		startButton,
		scorePanel,
		lagWarning,
		
		// The stack index for the current effect
		effectIndex = 0,
		
		// Game state
		playing = false,
		score = 0,
		duration = 0,
		difficulty = 1,
		multiplier = new Multiplier( 0.2, MULTIPLIER_LIMIT ),	
		
		// Scoring meta
		frameScore = 0,
		frameCount = 0,
		
		// Time tracking
		timeStart = Date.now(),
		timeLastFrame = Date.now(),
		timeLastSecond = Date.now(),
		timeGameStart = Date.now(),
		
		// Time values used to track performance on every frame
		timeDelta = 0,
		timeFactor = 0,
		
		// Performance (FPS) tracking
		fps = 0,
		fpsMin = 1000,
		fpsMax = 0,
		framesThisSecond = 0,
		
		// Game elements
		notifications = [],
		intersections = [],
		particles = [],
		enemies = [],
		effects = [],
		player;
	
	/**
	 * 
	 */
	function initialize() {
		// Run selectors and cache element references
		container = $( '#game' );
		menu = $( '#menu');
		canvas = document.querySelector( '#world' );
		canvas3d = document.querySelector( '#effects' );
		scorePanel = document.querySelector( '#score' );
		startButton = document.querySelector( '#start-button' );
		lagWarning = document.querySelector( '#lag-warning' );
		lagWarningAction = lagWarning.querySelector( 'a' );
		
		try {
			context3d = canvas3d.getContext("webgl") || canvas3d.getContext("experimental-webgl");
	    } catch(e) {}
		
		// Is WebGL supported?
	    if( !!context3d ) {
			activate3dEffects();
	    }
		
		if ( canvas && canvas.getContext ) {
			context = canvas.getContext('2d');
			
			// Bind event listeners
			startButton.addEventListener('click', onStartButtonClick, false);
			lagWarningAction.addEventListener('click', onLagWarningButtonClick, false);
			document.addEventListener('mousedown', onDocumentMouseDownHandler, false);
			document.addEventListener('mousemove', onDocumentMouseMoveHandler, false);
			document.addEventListener('mouseup', onDocumentMouseUpHandler, false);
			canvas.addEventListener('touchstart', onCanvasTouchStartHandler, false);
			canvas.addEventListener('touchmove', onCanvasTouchMoveHandler, false);
			canvas.addEventListener('touchend', onCanvasTouchEndHandler, false);
			window.addEventListener('resize', onWindowResizeHandler, false);
			
			// Force an initial layout
			onWindowResizeHandler();
			
			createSprites();
			createEffects();
			
			// Now that everything is laid out we can show the canvas & UI
			container.fadeIn( MENU_FADE_IN_DURATION );
			menu.hide().delay( MENU_FADE_IN_DURATION ).fadeIn( MENU_FADE_IN_DURATION );
			
			// Update the game state
			document.body.setAttribute( 'class', STATE_WELCOME );
			
			reset();
			update();
		}
		else {
			alert( 'Doesn\'t seem like your browser supports the HTML5 canvas element :(' );
		}
	   
	}
	
	function activate3dEffects() {
		context3d.clearColor(0.0, 0.0, 0.0, 0.0);
	    
		// Compile our shader program
		var vertexShader = $( '#vertexShader' ).text();
		var fragmentShader = $( '#fragmentShader' ).text();
		effectsShaderProgram = WebGLUtil.createShaderProgram( context3d, vertexShader, fragmentShader );
		
		// Define the plane vertices
		effectsVertices = new Float32Array([ -1.0, -1.0,   1.0, -1.0,    -1.0,  1.0,     1.0, -1.0,    1.0,  1.0,    -1.0,  1.0]);
		
		// Buffer our vertices
		effectsBuffer = context3d.createBuffer();
		context3d.bindBuffer( context3d.ARRAY_BUFFER, effectsBuffer );
		context3d.bufferData( context3d.ARRAY_BUFFER, effectsVertices, context3d.STATIC_DRAW );
		
		// Load the shader texture
		effectsTexture = WebGLUtil.loadTexture( context3d, 'images/texture.png', $.proxy( function() {
			
			// Bind the shader texture
			WebGLUtil.bindTexture( context3d, effectsTexture );
			
			// If the shader was linked successfully, we're all set to
			// render 3d effects
			if (context3d.getProgramParameter(effectsShaderProgram, context3d.LINK_STATUS)) {
				effectsEnabled = true;
				
				context3d.viewport( 0, 0, 1024, 1024 );
				context3d.useProgram( effectsShaderProgram );
				
				var t0 = context3d.getUniformLocation( effectsShaderProgram, "texture" );
				context3d.uniform1i( t0, 0 ); 
				context3d.activeTexture( context3d.TEXTURE0 ); 
				context3d.bindTexture( context3d.TEXTURE_2D, effectsTexture );
				
				canvas3d.style.display = 'block';
				
				// Forces the 3D canvas to resize
				onWindowResizeHandler();
			}
			else {
				effectsEnabled = false;
			}
		}, this ) );
		
	}
	
	function disable3dEffects() {
		lagWarning.style.display = 'none';
		
		effectsEnabled = false;
		effects = [];
		effectIndex = 0;
		
		canvas3d.style.display = 'none';
	}
	
	function showLagWarning() {
		if (effectsEnabled) {
			lagWarning.style.display = 'block';
		}
	}
	
	function createSprites() {
		var canvasWidth = 64,
			canvasHeight = 64,
			cvs,
			ctx;
		
		// Enemy Sprite
		cvs = document.createElement( 'canvas' );
		cvs.setAttribute( 'width', canvasWidth );
		cvs.setAttribute( 'height', canvasHeight );
		ctx = cvs.getContext('2d');
		ctx.beginPath();
		ctx.arc( canvasWidth * 0.5, canvasHeight * 0.5, ENEMY_SIZE, 0, Math.PI*2, true );
		ctx.lineWidth = 2;
		ctx.fillStyle = 'rgba(0,200,220, 0.9)';
		ctx.strokeStyle = 'rgba(255,255,255,0.4)'
		ctx.shadowColor = 'rgba(0,240,255,0.9)';
		ctx.shadowOffsetX = 0;
		ctx.shadowOffsetY = 0;
		ctx.shadowBlur = 20;
		ctx.stroke();
		ctx.fill();
		
		sprites.enemy = cvs;
		
		
		// Enemy Dying (state A) Sprite
		cvs = document.createElement( 'canvas' );
		cvs.setAttribute( 'width', canvasWidth );
		cvs.setAttribute( 'height', canvasHeight );
		ctx = cvs.getContext('2d');
		ctx.beginPath();
		ctx.arc( canvasWidth * 0.5, canvasHeight * 0.5, ENEMY_SIZE * 1.4, 0, Math.PI*2, true );
		ctx.lineWidth = 2;
		ctx.fillStyle = 'rgba(190,220,90, 0.9)';
		ctx.strokeStyle = 'rgba(255,255,255,0.4)'
		ctx.shadowColor = 'rgba(220,240,150,0.9)';
		ctx.shadowOffsetX = 0;
		ctx.shadowOffsetY = 0;
		ctx.shadowBlur = 20;
		ctx.stroke();
		ctx.fill();
		
		sprites.enemyDyingA = cvs;
		
		
		// Enemy Dying (state B) Sprite
		cvs = document.createElement( 'canvas' );
		cvs.setAttribute( 'width', canvasWidth );
		cvs.setAttribute( 'height', canvasHeight );
		ctx = cvs.getContext('2d');
		ctx.beginPath();
		ctx.arc( canvasWidth * 0.5, canvasHeight * 0.5, ENEMY_SIZE * 1.4, 0, Math.PI*2, true );
		ctx.lineWidth = 2;
		ctx.fillStyle = 'rgba(190,220,90, 0.9)';
		ctx.strokeStyle = 'rgba(255,255,255,0.4)'
		ctx.shadowColor = 'rgba(220,240,150,0.9)';
		ctx.shadowOffsetX = 0;
		ctx.shadowOffsetY = 0;
		ctx.shadowBlur = 10;
		ctx.stroke();
		ctx.fill();
		
		sprites.enemyDyingB = cvs;
		
		
		// Bomb Sprite
		cvs = document.createElement( 'canvas' );
		cvs.setAttribute( 'width', canvasWidth );
		cvs.setAttribute( 'height', canvasHeight );
		ctx = cvs.getContext('2d');
		ctx.beginPath();
		ctx.arc( canvasWidth * 0.5, canvasHeight * 0.5, ENEMY_SIZE, 0, Math.PI*2, true );
		ctx.lineWidth = 2;
		ctx.fillStyle = 'rgba(220,50,50, 0.9)';
		ctx.strokeStyle = 'rgba(255,255,255,0.4)'
		ctx.shadowColor = "rgba(255,100,100,0.9)";
		ctx.shadowOffsetX = 0;
		ctx.shadowOffsetY = 0;
		ctx.shadowBlur = 10;
		ctx.stroke();
		ctx.fill();
		
		sprites.bomb = cvs;
	}
	
	function createEffects() {
		while( effects.length < NUMBER_OF_EFFECTS ) {
			effects.push( new Effect( 0, 0, 0 ) );
		}
	}
	
	function start() {
		reset();
		
		timeStart = Date.now();
		timeLastFrame = timeStart;
		
		playing = true;
		
		menu.fadeOut( MENU_FADE_OUT_DURATION, function() {
			// Remove the header after the menu has appeared since
			// it will no longer be used
			$( 'h1', this ).remove();
		} );
		
		// Update the game state
		document.body.setAttribute( 'class', STATE_PLAYING );
		
	}
	
	function stop() {
		scorePanel.style.display = 'block';
		scorePanel.querySelector( 'p' ).innerHTML = Math.floor( score );
		
		playing = false;
		menu.fadeIn( MENU_FADE_IN_DURATION );
	}
	
	function reset() {
		player = new Player();
		player.x = mouse.x;
		player.y = mouse.y;
		
		notifications = [];
		intersections = [];
		particles = [];
		enemies = [];
		effects = [];
		
		score = 0;
		duration = 0;
		playing = false;
		difficulty = 1;
		effectIndex = 0;
		
		createEffects();
		
		multiplier.reset();
		
		frameCount = 0;
		frameScore = 0;
		
		timeStart = 0;
		timeLastFrame = 0;
	}
	
	function emitParticles( color, x, y, speed, quantity ) {
		while( quantity-- ) {
			particles.push( new Particle( x, y, speed, color ) );
		}
	}
	
	function emitEffect( x, y ) {
		if (effectsEnabled) {
			effectIndex++;
			
			if (effectIndex >= NUMBER_OF_EFFECTS) {
				effectIndex = 0;
			}
			
			effects[effectIndex].x = x;
			effects[effectIndex].y = y;
			effects[effectIndex].time = 0;
			effects[effectIndex].alive = true;
		}
	}
	
	function notify( text, x, y, scale, rgb ) {
		notifications.push( new Notification( text, x, y, scale, rgb ) );
	}
	
	function invalidate( x, y, width, height ) {
		dirtyRegions.push( {
			x: x,
			y: y,
			width: width,
			height: height
		} );
	}
	
	function adjustScore( offset ) {
		var multipliedOffset = 0;
		
		if( playing ) {
			multipliedOffset = offset * multiplier.major;
			
			// Adjust the score, but scale the adjustment by a factor
			// of the framerate. This is done to avoid giving people
			// with low FPS an advantage.
			score += multipliedOffset * ( fps / FRAMERATE );
		}
		
		return multipliedOffset;
	}
	
	function update() {
		
		clear();
		
		// There are quite the few updates and renders that only need
		// to be carried out while the game is active
		if (playing) {
			context.save();
			context.globalCompositeOperation = 'lighter';
			
			updateMeta();
			updatePlayer();
			updateParticles();
			
			findIntersections();
			solveIntersections();
			
			renderPlayer();
			
			updateEnemies();
			renderEnemies();
			renderParticles();
			
			context.restore();
			
			renderNotifications();
		}
		
		if( effectsEnabled ) {
			updateEffects();
			
			if (frameCount % 2 == 0) {
				renderEffects();
			}
		}
		
		// After the user has started his first game, this will never
		// go back to being 0
		if( score !== 0 ) {
			renderHeader();
		}
		
		if( DEBUG ) {
			debug();
		}
		
		requestAnimFrame( update );
	}
	
	function clear() {
		var i = dirtyRegions.length;
		
		while( i-- ) {
			var r = dirtyRegions[i];
			context.clearRect( Math.floor( r.x ), Math.floor( r.y ), Math.ceil( r.width ), Math.ceil( r.height ) );
		}
		
		dirtyRegions = [];
	}
	
	function debug() {
		var i = dirtyRegions.length;
		
		while( i-- ) {
			var r = dirtyRegions[i];
			context.fillStyle = 'rgba(0,255,0,0.2)';
			context.fillRect( Math.floor( r.x ), Math.floor( r.y ), Math.ceil( r.width ), Math.ceil( r.height ) );
		}
	}
	
	function findIntersections() {
		var i = player.trail.length;
		
		var candidates = [];
		
		while( i-- ) {
			var j = player.trail.length;
			
			var p1 = player.trail[i];
			var p2 = player.trail[i+1];
			
			while( j-- ) {
				
				if ( Math.abs(i-j) > 1 ) {
					var p3 = player.trail[j];
					var p4 = player.trail[j + 1];
					
					if (p1 && p2 && p3 && p4) {
						var intersection = findLineIntersection(p1, p2, p3, p4);
						if ( intersection ) {
							candidates.push( [ Math.min(i,j), Math.max(i,j), intersection ] );
						}
					}
				}
				
			}
		}
		
		intersections = [];
		
		// Remove duplicates
		while( candidates.length ) {
			var i = intersections.length;
			
			var candidate = candidates.pop();
			
			while( i-- ) {
				if( candidate && intersections[i] && candidate[0] === intersections[i][0] && candidate[1] === intersections[i][1] ) {
					candidate = null;
				}
			}
			
			if( candidate ) {
				intersections.push(candidate);
			}
		}
	}
	
	function solveIntersections() {
		
		while( intersections.length ) {
			var ix = intersections.pop();
			
			// Begin the trail path
			context.beginPath();
			
			var points = player.trail.slice( ix[0], ix[1] );
			points[0] = ix[2];
			points.push( ix[2] );
			
			var bounds = new Region();
			
			for( var i = 0, len = points.length; i < len; i++ ) {
				var p1 = points[i];
				var p2 = points[i+1];
				
				if( i === 0 ) {
					// This is the first loop, so we need to start by moving into position
					context.moveTo( p1.x, p1.y );
				}
				else if( p1 && p2 ) {
					// Draw a curve between the current and next trail point
					context.quadraticCurveTo( p1.x, p1.y, p1.x + ( p2.x - p1.x ) / 2, p1.y + ( p2.y - p1.y ) / 2 );
				}
				
				bounds.inflate( p1.x, p1.y );
			}
			
			var center = bounds.center();
			
			// Solid fill, faster
			// context.fillStyle = 'rgba(0,255,255,0.2)';
			// context.closePath();
			
			// Gradient fill, prettier
			var gradient = context.createRadialGradient( center.x, center.y, 0, center.x, center.y, bounds.size() );
			gradient.addColorStop(1,'rgba(0, 255, 255, 0.0)');
			gradient.addColorStop(0,'rgba(0, 255, 255, 0.2)');
			context.fillStyle = gradient;
			context.closePath();
			
			context.fill();
			
		}
		
		// Only check for collisions every third frame to reduce lag
		if ( frameCount % 2 == 1 ) {
			
			var bmp = context.getImageData(0, 0, world.width, world.height);
			var bmpw = bmp.width;
			var pixels = bmp.data;
			
			var casualties = [];
			
			var i = enemies.length;
			
			while (i--) {
				var enemy = enemies[i];
				
				var ex = Math.round( enemy.x );
				var ey = Math.round( enemy.y );
				
				var indices = [	
					((ey * bmpw) + Math.round(ex - ENEMY_SIZE)) * 4, 
					((ey * bmpw) + Math.round(ex + ENEMY_SIZE)) * 4, 
					((Math.round(ey - ENEMY_SIZE) * bmpw) + ex) * 4, 
					((Math.round(ey + ENEMY_SIZE) * bmpw) + ex) * 4
				];
				
				var j = indices.length;
				
				while (j--) {
					var index = indices[j];
					
					if (pixels[index + 1] === 255 && pixels[index + 2] === 255) {
					
						if (enemy.type === ENEMY_TYPE_BOMB || enemy.type === ENEMY_TYPE_BOMB_MOVER) {
							handleBombInClosure(enemy);
						}
						else {
							handleEnemyInClosure(enemy);
							
							casualties.push(enemy);
						}
						
						enemies.splice(i, 1);
						
						break;
					}
				}
			}
			
			// If more than one enemy was killed, show the multiplier
			if (casualties.length > 1) {
				// Increase the score exponential depending on the number of
				// casualties
				var scoreChange = adjustScore(casualties.length * SCORE_PER_ENEMY);
				
				notify(scoreChange, player.x, player.y - 10, casualties.length / 1.5, [250, 250, 100]);
			}
			
		}
	}
	
	function updateMeta() {
		// Fetch the current time for this frame
		var timeThisFrame = Date.now();
		
		// Increase the frame count
		framesThisSecond ++;
		
		// Check if a second has passed since the last time we updated the FPS
		if( timeThisFrame > timeLastSecond + 1000 ) {
			// Establish the current, minimum and maximum FPS
			fps = Math.min( Math.round( ( framesThisSecond * 1000 ) / ( timeThisFrame - timeLastSecond ) ), FRAMERATE );
			fpsMin = Math.min( fpsMin, fps );
			fpsMax = Math.max( fpsMax, fps );
			
			timeLastSecond = timeThisFrame;
			framesThisSecond = 0;
		}
		
		timeDelta = timeThisFrame - timeLastFrame;
		timeFactor = timeDelta / ( 1000 / FRAMERATE );
		
		// Increment the difficulty by a factor of the time
		// passed since the last rendered frame to ensure that
		// difficulty progresses at the same speed no matter what
		// FPS the game runs at
		difficulty += 0.002 * Math.max( timeFactor, 1 );
		adjustScore( 1 );
		
		frameCount ++;
		frameScore ++;
		
		duration = timeThisFrame - timeStart;
		
		timeLastFrame = timeThisFrame;
		
		if( frameCount > FRAMERATE * 6 && Math.round( ( fpsMin + fpsMax + fps ) / 3 ) < 30 ) {
			showLagWarning();
		}
	}
	
	function updatePlayer() {
		
		// Interpolate towards the mouse, results in smooth
		// movement
		player.interpolate( mouse.x, mouse.y, 0.4 );
		
		// Add points to the trail, if needed
		while( player.trail.length < player.length ) {
			player.trail.push( new Point( player.x, player.y ) );
		}
		
		// Remove the oldest point in the trail
		player.trail.shift();
		
		// No energy – no game
		if( player.energy === 0 ) {
			stop();
		}
		
	}
	
	function updateEnemies() {
		
		var enemy; 
		var padding = 60;
		
		var i = enemies.length;
		
		var numberOfBombs = 0;
		var numberOfMovers = 0;
		
		while (i--) {
			if( enemies[i].type === ENEMY_TYPE_BOMB ) {
				numberOfBombs++;
			}
		}
		
		var canAddBombs = numberOfBombs / enemies.length < 0.4;
		var canAddMovers = numberOfMovers / enemies.length < 0.3 && frameCount > ENEMY_MOVER_START_FRAME;
		
		i = Math.floor( ENEMY_COUNT + difficulty ) - enemies.length;
		
		while( i-- && Math.random() > 0.85 ) {
			
			var type = ENEMY_TYPE_NORMAL;

			if( canAddBombs ) {
				type = Math.random() > 0.5 ? ENEMY_TYPE_NORMAL : ENEMY_TYPE_BOMB;
			}
			
			enemy = new Enemy();
			enemy.x = padding + Math.round( Math.random() * ( world.width - padding - padding ) );
			enemy.y = padding + Math.round( Math.random() * ( world.height - padding - padding ) );
			enemy.type = type;
			
			enemies.push(enemy);
		}
		
		i = enemies.length;
		
		while( i-- ) {
			enemy = enemies[i];
			
			enemy.time = Math.min( enemy.time + ( 0.2 * timeFactor ), 100 );
			enemy.scale += ( ( enemy.scaleTarget - enemy.scale ) + 0.01 ) * 0.3;
			enemy.alpha += ( enemy.alphaTarget - enemy.alpha ) * 0.1;
			
			if( enemy.type === ENEMY_TYPE_BOMB_MOVER ||enemy.type === ENEMY_TYPE_NORMAL_MOVER ) {
				enemy.x += enemy.velocity.x;
				enemy.y += enemy.velocity.y;
				
				if( enemy.x < 0 || enemy.x > world.width - ENEMY_SIZE ) {
					enemy.velocity.x = -enemy.velocity.x;
				}
				else if( enemy.y < 0 || enemy.y > world.height - ENEMY_SIZE ) {
					enemy.velocity.y = -enemy.velocity.y;
				}
			}
			
			// If this enemy is alive but has reached the end of its life span
			if( enemy.alive && enemy.time === 100 ) {
				
				// Fade out bombs
				if ( enemy.type === ENEMY_TYPE_BOMB || enemy.type === ENEMY_TYPE_BOMB_MOVER ) {
					handleBombDeath( enemy );
				}
				else {
					handleEnemyDeath( enemy );
					enemies.splice(i,1);
				}
				
				enemy.alive = false;
				
			}
			
			// Remove any faded out bombs
			if( enemy.alive === false && enemy.alphaTarget === 0 && enemy.alpha < 0.05 ) {
				enemies.splice(i,1);
			}
			
		}
		
	}
	
	function updateParticles() {
		
		var i = particles.length;
		
		while( i-- ) {
			var particle = particles[i];
			
			particle.x += particle.velocity.x;
			particle.y += particle.velocity.y;
			
			particle.velocity.x *= 0.98;
			particle.velocity.y *= 0.98;
			
			if ( particle.fading === true ) {
				particle.alpha *= 0.92;
			}
			else if( Math.random() > 0.92 ) {
				particle.fading = true;
			}
			
			if( particle.alpha < 0.05 ) {
				particles.splice(i,1);
			}
		}
		
	}
	
	function updateEffects() {
		var i = effects.length;
		
		while( i-- ) {
			var effect = effects[i];
			
			if (effect.alive) {
				effect.time = Math.min( ( effect.time + 0.01 ) * ( 1 + ( 1 - effect.time ) ), 1 );
			}
			else {
				effect.time = Math.max( ( effect.time - 0.01 ) * 0.99, 0 );
			}
			
			if( effect.time === 1 ) {
				effect.alive = false;
			}
		}
	}
	
	function renderPlayer() {
		// Begin the trail path
		context.beginPath();
		
		var bounds = new Region();
		var i = player.trail.length;
		
		// Draw a curve through the tail
		for( var i = 0, len = player.trail.length; i < len; i++ ) {
			var p1 = player.trail[i];
			var p2 = player.trail[i+1];
			
			if( i === 0 ) {
				// This is the first loop, so we need to start by moving into position
				context.moveTo( p1.x + ( p2.x - p1.x ) / 2, p1.y + ( p2.y - p1.y ) / 2 );
			}
			else if( p2 ) {
				// Draw a curve between the current and next trail point
				context.quadraticCurveTo( p1.x, p1.y, p1.x + ( p2.x - p1.x ) / 2, p1.y + ( p2.y - p1.y ) / 2 );
			}
			
			bounds.inflate( p1.x, p1.y );
		}
		
		// Draw the trail stroke
		context.strokeStyle = '#648d93';
		context.lineWidth = 2;
		context.stroke();
		
		bounds.expand( 4, 4 );
		
		var boundsRect = bounds.toRectangle();
		
		invalidate( boundsRect.x, boundsRect.y, boundsRect.width, boundsRect.height );
	}
	
	function renderEnemies() {
		
		var i = enemies.length;
		
		while (i--) {
			var enemy = enemies[i];
			
			var sprite = null;
			
			// The if statements here determine which sprite that
			// will be used to represent this entity
			if (enemy.type === ENEMY_TYPE_BOMB || enemy.type === ENEMY_TYPE_BOMB_MOVER) {
				sprite = sprites.bomb;
			}
			else {
				sprite = sprites.enemy;
				
				// Are we in the dying phase?
				if (enemy.time > 65) {
					sprite = sprites.enemyDyingA;
					
					if (Math.round(enemy.time) % 2 == 0) {
						sprite = sprites.enemyDyingB;
					}
				}
			}
			
			context.save();
			context.globalAlpha = enemy.alpha;
			
			context.translate( Math.round( enemy.x ), Math.round( enemy.y ) );
			context.scale( enemy.scale, enemy.scale );
			context.drawImage( sprite, -Math.round( sprite.width/2 ), -Math.round( sprite.height/2 ) );
			
			context.restore();
			
			var sw = ( sprite.width * enemy.scale ) + 4;
			var sh = ( sprite.height * enemy.scale ) + 4;
			
			invalidate( enemy.x-(sw/2), enemy.y-(sw/2), sw, sh );
		}
	}
	
	function renderParticles() {
		
		var i = particles.length;
		
		while( i-- ) {
			var particle = particles[i];
			
			context.save();
			context.globalAlpha = particle.alpha;
			context.fillStyle = particle.color;
			context.fillRect( particle.x, particle.y, 1, 1 );
			context.restore();
			
			invalidate( particle.x - 2, particle.y - 2, 4, 4 );
		}
		
	}
	
	function renderNotifications() {
		var i = notifications.length;
		
		// Go through and draw all notification texts
		while( i-- ) {
			var p = notifications[i];
			
			// Make the text float upwards
			p.y -= 0.4;
			
			var r = 14 * p.scale;
			
			// Draw the notification
			context.save();
			context.font = 'bold ' + Math.round(12 * p.scale) + "px Arial";
			
			context.beginPath();
			context.fillStyle = 'rgba(0,0,0,'+(0.7 * p.alpha)+')';
			context.arc( p.x, p.y, r, 0, Math.PI*2, true );
			context.fill();
			
			context.fillStyle = "rgba( "+p.rgb[0]+", "+p.rgb[1]+", "+p.rgb[2]+", " + p.alpha + " )";
			context.fillText( p.text, p.x - ( context.measureText( p.text ).width * 0.5 ), p.y + (4 * p.scale) );
			context.restore();
			
			// Fade out
			p.alpha *= 1 - ( 0.08 * (1-((p.alpha-0.08)/1)) );
			
			// If the notifaction is faded out, remove it
			if( p.alpha < 0.05 ) {
				notifications.splice( i, 1 );
			}
			
			r += 2;
			
			invalidate( p.x - r, p.y - r, r*2, r*2 );
		}
	}
	
	function renderEffects() {
		effectsTime += 0.01;
		
		var l1 = context3d.getAttribLocation( effectsShaderProgram, "position" );
	    var l2 = context3d.getUniformLocation( effectsShaderProgram, "time" );
	    var l3 = context3d.getUniformLocation( effectsShaderProgram, "resolution" );
	    var l4 = context3d.getUniformLocation( effectsShaderProgram, "mouse" );
	    
		context3d.bindBuffer( context3d.ARRAY_BUFFER, effectsBuffer );
		
		context3d.uniform1f( l2, effectsTime );
	    context3d.uniform2f( l3, world.width, world.height );
	    context3d.uniform2f( l4, mouse.x, mouse.y );
		
		var i = NUMBER_OF_EFFECTS;
		
		while( i-- ) {
			var effect = effects[i];
			
			var pointer = context3d.getUniformLocation( effectsShaderProgram, "e" + i );
			context3d.uniform3f( pointer, effect.x, effect.y, effect.time );
		}
		
		context3d.vertexAttribPointer( l1, 2, context3d.FLOAT, false, 0, 0 );
	    context3d.enableVertexAttribArray( l1 );
		
		context3d.drawArrays(context3d.TRIANGLES, 0, 6);
    	context3d.disableVertexAttribArray(l1);
	}
	
	function renderHeader() {
		
		var padding = 10,
			energyBarHeight = 4,
			energyBarWidth = 100,
			ENERGY_LABEL = 'ENERGY:',
			MULTIPLIER_LABEL = 'MULTIPLIER:',
			TIME_LABEL = 'TIME:',
			SCORE_LABEL = 'SCORE:';
		
		player.animatedEnergy += ( player.energy - player.animatedEnergy ) * 0.2;
		
		context.fillStyle = 'rgba(0,0,0,0.5)';
		context.fillRect( 0, 0, world.width, HEADER_HEIGHT );
		
		context.save();
		context.translate( padding, padding );
		
			// Energy label
			context.font = "10px Arial";
			context.fillStyle = "#ffffff";
			context.fillText( ENERGY_LABEL, 0, 8 );
			context.translate( 56, 0 );
			
			// Energy bar 
			context.save();
			context.fillStyle = 'rgba(40,40,40,0.8)';
			context.fillRect( 0, 2, energyBarWidth, energyBarHeight );
			context.shadowOffsetX = 0;
			context.shadowOffsetY = 0;
			context.shadowBlur = 14;
			context.shadowColor = "rgba(0,240,255,0.9)";
			context.fillStyle = 'rgba(0,200,220, 0.8)';
			context.fillRect( 0, 2, ( player.animatedEnergy / 100 ) * energyBarWidth, energyBarHeight );
			context.restore();
			
			context.translate( 122, 0 );
			
			// Multiplier label
			context.font = "10px Arial";
			context.fillStyle = "#ffffff";
			context.fillText( MULTIPLIER_LABEL, 0, 8 );
			context.translate( 73, 0 );
			
			// Multiplier
			var i = MULTIPLIER_LIMIT - 1;
			
			while( i-- ) {
				context.save();
				context.beginPath();
				
				var x = 6 + ( i / MULTIPLIER_LIMIT ) * 80;
				var y = 5;
				var s = 6;
				
				context.fillStyle = 'rgba(40,40,40,0.8)';
				context.arc( x, y, s, 0, Math.PI*2, true );
				context.fill();
				
				if( i < multiplier.major ) {
					context.beginPath();
					context.shadowOffsetX = 0;
					context.shadowOffsetY = 0;
					context.shadowBlur = 14;
					context.shadowColor = "rgba(0,240,255,0.9)";
					context.fillStyle = 'rgba(0,200,220,0.8)';
					
					if (i < multiplier.major - 1) {
						// We're drawing a major (entirely filled) step
						context.arc( x, y, s, 0, Math.PI*2, true );
					}
					else {
						// We're drawing a minor (partly filled) step
						context.fillStyle = 'rgba(0,200,220,' + (0.8 * multiplier.minor) + ')';
						context.arc( x, y, s * multiplier.minor, 0, Math.PI*2, false );
					}
					
					context.fill();
				}
				
				context.restore();
			}
			
			context.translate( 73, 0 );
			
			// Time label
			context.font = "10px Arial";
			context.fillStyle = "#ffffff";
			context.fillText( TIME_LABEL, 0, 8 );
			
			// Time
			context.font = "bold 10px Arial";
			context.fillStyle = 'rgba(0,200,220, 0.8)';
			context.fillText( Math.round( duration / 1000 ) + 's', 35, 8 );
			
			context.translate( 65, 0 );
			
			// Score label
			context.font = "10px Arial";
			context.fillStyle = "#ffffff";
			context.fillText( SCORE_LABEL, 0, 8 );
			
			// Score
			context.font = "bold 10px Arial";
			context.fillStyle = 'rgba(0,200,220, 0.8)';
			context.fillText( Math.floor(score), 47, 8 );
			
		context.restore();
		
		invalidate( 0, 0, world.width, HEADER_HEIGHT + 5 );
	}
	
	/**
	 * Invoked when an enemy dies of age.
	 */
	function handleEnemyDeath( entity ) {
		player.adjustEnergy( ENERGY_PER_ENEMY_DEATH );
		multiplier.reset();
		
		emitParticles( '#eeeeee', entity.x, entity.y, 3, 15 );
		
		notify( ENERGY_PER_ENEMY_DEATH+'♥', entity.x, entity.y, 1.2, [230,90,90] );
		
		emitEffect( entity.x, entity.y );
	}
	
	/**
	 * Invoked when a bomb dies of age.
	 */
	function handleBombDeath( entity ) {
		entity.alphaTarget = 0;
		entity.scaleTarget = 0.01;
	}
	
	/**
	 * Invoked when an enemy has been enclosed.
	 */
	function handleEnemyInClosure( entity ) {
		player.adjustEnergy( ENERGY_PER_ENEMY_ENCLOSED );
		
		var mb = multiplier.major;
		multiplier.increase();
		
		// If the multiplier increased by one major point,
		// highlight this to the user
		if( multiplier.major > mb ) {
			notify( 'X' + multiplier.major, world.width/2, world.height/2, multiplier.major, [60,250,130] );
			emitEffect( world.width/2, world.height/2 );
		}
		
		emitParticles( '#eeeeee', entity.x, entity.y, 3, 6 );
		
		var scoreChange = adjustScore( SCORE_PER_ENEMY );
		
		notify( '' + Math.floor( scoreChange ), entity.x, entity.y );
		
		emitEffect( entity.x, entity.y );
	}
	
	/**
	 * Invoked when a bomb has been enclosed.
	 */
	function handleBombInClosure( entity ) {
		player.adjustEnergy( ENERGY_PER_BOMB_ENCLOSED );
		multiplier.reset();
		
		notify( ENERGY_PER_BOMB_ENCLOSED+'♥', entity.x, entity.y, 1.2, [230,90,90] );
		
		emitEffect( entity.x, entity.y );
	}
	
	function findLineIntersection( p1, p2, p3, p4 ) {
		var s1 = {
			x: p2.x - p1.x,
			y: p2.y - p1.y
		}
		
		var s2 = {
			x: p4.x - p3.x,
			y: p4.y - p3.y
		}
		
		var s = (-s1.y * (p1.x - p3.x) + s1.x * (p1.y - p3.y)) / (-s2.x * s1.y + s1.x * s2.y);
    	var t = ( s2.x * (p1.y - p3.y) - s2.y * (p1.x - p3.x)) / (-s2.x * s1.y + s1.x * s2.y);
		
		if (s >= 0 && s <= 1 && t >= 0 && t <= 1) {
			return {
				x: p1.x + ( t * s1.x ),
				y: p1.y + ( t * s1.y )
			};
		}
		
		return null;
	}
	
	function onStartButtonClick(event){
		start();
		event.preventDefault();
	}
	
	function onLagWarningButtonClick(event){
		disable3dEffects();
		event.preventDefault();
	}
	
	function onDocumentMouseDownHandler(event){
		mouse.down = true;
	}
	
	function onDocumentMouseMoveHandler(event){
		mouse.previousX = mouse.x;
		mouse.previousY = mouse.y;
		
		mouse.x = event.clientX - (window.innerWidth - world.width) * 0.5;
		mouse.y = event.clientY - (window.innerHeight - world.height) * 0.5;
		
		mouse.velocityX = Math.abs( mouse.x - mouse.previousX ) / world.width;
		mouse.velocityY = Math.abs( mouse.y - mouse.previousY ) / world.height;
	}
	
	function onDocumentMouseUpHandler(event) {
		mouse.down = false;
	}
	
	function onCanvasTouchStartHandler(event) {
		if(event.touches.length == 1) {
			event.preventDefault();
			
			mouse.x = event.touches[0].pageX - (window.innerWidth - world.width) * 0.5;
			mouse.y = event.touches[0].pageY - (window.innerHeight - world.height) * 0.5;
			
			mouse.down = true;
		}
	}
	
	function onCanvasTouchMoveHandler(event) {
		if(event.touches.length == 1) {
			event.preventDefault();

			mouse.x = event.touches[0].pageX - (window.innerWidth - world.width) * 0.5;
			mouse.y = event.touches[0].pageY - (window.innerHeight - world.height) * 0.5 - 20;
		}
	}
	
	function onCanvasTouchEndHandler(event) {
		mouse.down = false;
	}
	
	function onWindowResizeHandler() {
		// Update the game size
		world.width = TOUCH_INPUT ? window.innerWidth : DEFAULT_WIDTH;
		world.height = TOUCH_INPUT ? window.innerHeight : DEFAULT_HEIGHT;
		
		// Resize the container
		container.width( world.width );
		container.height( world.height );
		
		// Resize the canvas
		canvas.width = world.width;
		canvas.height = world.height;
		
		// Determine the x/y position of the canvas
		var cx = Math.max( (window.innerWidth - world.width) * 0.5, 1 );
		var cy = Math.max( (window.innerHeight - world.height) * 0.5, 1 );
		
		// Update the position of the canvas
		container.css( {
			left: cx,
			top: cy
		} );
		
		// Center the menu
		menu.css( {
			left: ( world.width - menu.width() ) / 2,
			top: ( world.height - menu.height() ) / 2
		} );
		
		// Update the WebGL canvas if it exists
		if( effectsEnabled ) {
			// Resize the canvas
			canvas3d.width = world.width;
			canvas3d.height = world.height;
			
			// Resize the GL viewport
			context3d.viewportWidth = world.width;
			context3d.viewportHeight = world.height;
		}
	}
	
	initialize();
	
})();


/**
 * Base class for all game entities.
 */
function Entity( x, y ) {
	this.alive = false;
}
Entity.prototype = new Point();

/**
 * Player entity.
 */
function Player() {
	this.trail = [];
	this.size = 8;
	this.length = 45;
	this.energy = 100;
	this.animatedEnergy = 0;
	
	this.adjustEnergy = function( offset ) {
		this.energy = Math.min( Math.max( this.energy + offset, 0 ), 100 );
	}
}
Player.prototype = new Entity();

/**
 * Player entity.
 */
function Enemy() {
	this.scale = 0.01;
	this.scaleTarget = 1;
	
	this.alpha = 0;
	this.alphaTarget = 1;
	
	this.time = 0;
	this.type = 1;
	
	this.velocity = { x: 0, y: 0 };
	
	this.alive = true;
}
Enemy.prototype = new Entity();

/**
 * Particle entity.
 */
function Particle( x, y, speed, color ) {
	this.x = x;
	this.y = y;
	
	this.velocity = {
		x: -speed+(Math.random()*speed*2),
		y: -speed+(Math.random()*speed*2)
	};
	
	this.color = color;
	this.alpha = 1;
	this.fading = false;
}
Particle.prototype = new Entity();

/**
 * Notification entity used for score, health and 
 * multiplier changes.
 */
function Notification( text, x, y, scale, rgb ) {
	this.text = text || '';
	this.x = x || 0;
	this.y = y || 0;
	this.scale = scale || 1;
	this.rgb = rgb || [255,255,255];
	this.alpha = 1;
}
Notification.prototype = new Entity();

/**
 * Represents an effect space in the game field, 
 * renders as an explosion wave in the WebGL background.
 */
function Effect( time, x, y ) {
	this.x = x || 0;
	this.y = y || 0;
	this.time = time || 0;
	this.alive = false;
}

/**
 * Used to keep track of and update the score 
 * multiplier.
 */
function Multiplier( step, max ) {
	this.major = 1;
	this.minor = 0;
	
	this.max = max;
	this.step = step;
	
	this.reset = function() {
		this.major = 1;
		this.minor = 0;
	}
	
	this.increase = function() {
		this.minor += this.step;
		
		// Do we need to increment the major value?
		while( this.minor >= 1 ) {
			if (this.major < this.max) {
				this.major++;
			}
			
			this.minor = 1 - this.minor;
		}
	}
}