Impact

This is the source code for the Drop Demo Game.

You can download a complete package with all images, sounds, the .html file and the Impact Game Engine on your personal download page, after you've purchased a license.

An interesting aspect of the game is the LCD look that is generated from the source images when the game is loaded. E.g. the sprite for the bouncing ball is just 4x4 px in size.

ig.module(
	'drop'
)
.requires(
	'impact.game',
	'impact.entity',
	'impact.collision-map',
	'impact.background-map',
	'impact.font'
)
.defines(function(){

	
	
// The Backdrop image for the game, subclassed from ig.Image
// because it needs to be drawn in it's natural, unscaled size, 
FullsizeBackdrop = ig.Image.extend({
	resize: function(){ /* Do Nothing */ }, 
	draw: function() {
		if( !this.loaded ) { return; }
		ig.system.context.drawImage( this.data, 0, 0 );
	}
});



// The Collectable Coin Entity
EntityCoin = ig.Entity.extend({
	size: {x:6, y:6},
	offset: {x:-1, y:-1},
	animSheet: new ig.AnimationSheet( 'media/coin.png', 4, 4 ),
	type: ig.Entity.TYPE.B,
	
	sound: new ig.Sound('media/coin.ogg'),
	
	init: function( x, y, settings ) {
		this.addAnim( 'idle', 0.1, [0,1] );		
		this.parent( x, y, settings );
	},
	
	update: function() {
		this.parent();
		if( this.pos.y - ig.game.screen.y < -32 ) {
			this.kill();
		}
	},
	
	pickup: function() {
		ig.game.score += 500;
		this.sound.play();
		this.kill();
	}
});



// The Bouncing Player Ball thing
EntityPlayer = ig.Entity.extend({
	size: {x:4, y:4},
	checkAgainst: ig.Entity.TYPE.B,
	
	animSheet: new ig.AnimationSheet( 'media/player.png', 4, 4 ),
	
	maxVel: {x: 50, y: 300},
	friction: {x: 600, y:0},
	speed: 300,
	bounciness: 0.5,
	sound: new ig.Sound('media/bounce.ogg'),
	
	init: function( x, y, settings ) {
		this.addAnim( 'idle', 0.1, [0] );		
		this.parent( x, y, settings );
	},
	
	update: function() {
		// User Input
		if( ig.input.state('left') ) {
			this.accel.x = -this.speed;
		}
		else if( ig.input.state('right') ) {
			this.accel.x = this.speed;
		}
		else {
			this.accel.x = 0;
		}
		
		this.parent();
	},
	
	handleMovementTrace: function( res ) {
		if( res.collision.y && this.vel.y > 32 ) {
			this.sound.play();
		}
		this.parent(res);
	},
	
	check: function( other ) {
		// The 'other' entity must be a coin, because we
		// only have two entity types (coins and the player)
		other.pickup();
	}
});



// A Custom Loader for the game, that, after all images have been
// loaded, goes through them and "pixifies" them to create the LCD
// effect.
DropLoader = ig.Loader.extend({
	end: function() {
		for( i in ig.Image.cache ) {
			var img = ig.Image.cache[i];
			if( !(img instanceof FullsizeBackdrop) ) {
				this.pixify( img, ig.system.scale );
			}
		}
		this.parent();
	},
	
	
	// This essentially deletes the last row and collumn of pixels for
	// each upscaled pixel.
	pixify: function( img, s ) {
		var ctx = img.data.getContext('2d');
		var px = ctx.getImageData(0, 0, img.data.width, img.data.height);
		
		for( var y = 0; y < img.data.height; y++ ) {
			for( var x = 0; x < img.data.width; x++ ) {
				var index = (y * img.data.width + x) * 4;
				var alpha = (x % s == 0 || y % s == 0) ? 0 : 0.9;
				px.data[index + 3] = px.data[index + 3] * alpha;
			}
		}
		ctx.putImageData( px, 0, 0 );
	}
});



// The actual Game Source
DropGame = ig.Game.extend({
	clearColor: '#c7e300',
	gravity: 240,
	player: null,
		
	map: [],
	score: 0,
	speed: 1,
	
	tiles: new ig.Image( 'media/tiles.png' ),
	backdrop: new FullsizeBackdrop( 'media/backdrop.png' ),
	font: new ig.Font( 'media/04b03.font.png' ),
	gameOverSound: new ig.Sound( 'media/gameover.ogg' ),
	
	init: function() {			
		ig.system.smoothPositioning = false;
		
		ig.input.bind(ig.KEY.LEFT_ARROW, 'left');
		ig.input.bind(ig.KEY.RIGHT_ARROW, 'right');
		ig.input.bind(ig.KEY.ENTER, 'ok');
		
		// The first part of the map is always the same
		this.map = [
			[0,0,0,0,0,0,0,0],
			[0,0,0,0,0,0,0,0],
			[0,0,0,0,0,0,0,0],
			[0,0,0,0,0,0,0,0],
			[0,0,0,1,1,0,0,0],
			[0,0,0,0,0,0,0,0],
			[0,0,0,0,0,0,0,0],
			[0,0,0,0,0,0,0,0],
		];
		
		// Now randomly generate the remaining rows
		for( var y = 8; y < 18; y++ ) {	
			this.map[y] = this.getRow();
		}
		
		// The map is used as CollisionMap AND BackgroundMap
		this.collisionMap = new ig.CollisionMap( 8, this.map );
		var bgmap = new ig.BackgroundMap( 8, this.map, this.tiles );

		// Add the bgmap to the Game's array of BackgroundMaps
		// so it will be automatically drawn by .draw()
		this.backgroundMaps.push( bgmap );
		
		this.player = this.spawnEntity( EntityPlayer, 30, 16 );
	},
	
	
	getRow: function() {
		// Randomly generate a row of blocks for the map. This is a naive
		// approach, that sometimes leaves the player hanging with no 
		// block to jump to. It's random after all.
		var row = [];
		for( var x = 0; x < 8; x++ ) {
			row[x] = Math.random() > 0.93 ? 1 : 0;
		}
		return row;
	},
	
	
	placeCoin: function() {
		// Randomly find a free spot for the coin, max 12 tries
		for( var i = 0; i < 12; i++ ) {
			var tile = (Math.random() * 8).ceil();
			if(
				this.map[this.map.length-1][tile] &&
				!this.map[this.map.length-2][tile]
			) {
				var y = (this.map.length-1) * 8;
				var x = tile * 8 + 1;
				this.spawnEntity( EntityCoin, x, y );
				return;
			}
		}
	},
	
	
	update: function() {
		if( ig.input.pressed('ok') ) {
			ig.system.setGame( DropGame );
		}
			
		if( this.gameOver ) {
			return;
		}
		
		this.speed += ig.system.tick * (10/this.speed);
		this.screen.y += ig.system.tick * this.speed;
		this.score += ig.system.tick * this.speed;
		
		// Do we need a new row?
		if( this.screen.y > 40 ) {
			
			// Move screen and entities one tile up
			this.screen.y -= 8;
			for( var i =0; i < this.entities.length; i++ ) {
				this.entities[i].pos.y -= 8;
			}
			
			// Delete first row, insert new
			this.map.shift();
			this.map.push(this.getRow());
			
			// Place coin?
			if( Math.random() > 0.5 ) {
				this.placeCoin();
			}
		}
		this.parent();
		
		// check for gameover
		var pp = this.player.pos.y - this.screen.y;
		if( pp > ig.system.height + 8 || pp < -32 ) {
			this.gameOver = true;
			this.gameOverSound.play();
			
		}
	},
	
	
	draw: function() {
		this.backdrop.draw();
		
		if( this.gameOver ) {
			this.font.draw( 'Game Over!', 32, 32, ig.Font.ALIGN.CENTER );
			this.font.draw( 'Press Enter', 32, 48, ig.Font.ALIGN.CENTER );
			this.font.draw( 'to Restart', 32, 56, ig.Font.ALIGN.CENTER );
		}
		else {
			for( var i = 0; i < this.backgroundMaps.length; i++ ) {
				this.backgroundMaps[i].draw();
			}
			
			for( var i = 0; i < this.entities.length; i++ ) {
				this.entities[i].draw();
			}
		}
		
		var s = this.score.floor().toString();
		this.font.draw( s, 62, 2, ig.Font.ALIGN.RIGHT );
	}
});

// Start the game - 30fps, 64x96 pixels, scaled up 5x
ig.main('#canvas', DropGame, 30, 64, 96, 5, DropLoader );

});