//Chase NES game by Shiru (shiru@mail.ru) 01'12
//Feel free to do anything you want with this code, consider it Public Domain

//This game is an example for my article Programming NES games in C

//include the library

#include "neslib.h"
#include <string.h>

//#link "famitone2.s"
//#link "music.s"
//#link "sounds.s"

// CHR data
//#resource "tileset.chr"
//#link "tileset.s"

//include nametables for all the screens such as title or game over

#include "title_nam.h"
#include "level_nam.h"
#include "gameover_nam.h"
#include "welldone_nam.h"

//include nametables for levels

#include "level1_nam.h"
#include "level2_nam.h"
#include "level3_nam.h"
#include "level4_nam.h"
#include "level5_nam.h"

//game uses 12:4 fixed point calculations for enemy movements

#define FP_BITS	4

//max size of the game map

#define MAP_WDT			16
#define MAP_WDT_BIT		4
#define MAP_HGT			13

//macro for calculating map offset from screen space, as
//the map size is smaller than screen to save some memory

#define MAP_ADR(x,y)	((((y)-2)<<MAP_WDT_BIT)|(x))

//size of a map tile

#define TILE_SIZE		16
#define TILE_SIZE_BIT	4

//movement directions, match to the gamepad buttons bits to
//simplify some things

#define DIR_NONE		0
#define DIR_LEFT		PAD_LEFT
#define DIR_RIGHT		PAD_RIGHT
#define DIR_UP			PAD_UP
#define DIR_DOWN		PAD_DOWN

//tile numbers for certain things in the game map
//i.e. which object corresponds to a character in the tileset

#define TILE_PLAYER		0x10
#define TILE_ENEMY1		0x11
#define TILE_ENEMY2		0x12
#define TILE_ENEMY3		0x13
#define TILE_WALL		0x40
#define TILE_EMPTY		0x44
#define TILE_ITEM		0x45

//number of levels in the game

#define LEVELS_ALL		5

//numbers for screens that are displayed by the same function as the
//level number

#define SCREEN_GAMEOVER	(LEVELS_ALL+0)
#define SCREEN_WELLDONE	(LEVELS_ALL+1)

//total number of moving characters on the screen

#define PLAYER_MAX	4

//sound effect numbers, it is easier to use meaningful defines than
//remember actual numbers of the effects

#define SFX_START		0
#define SFX_ITEM		1
#define SFX_RESPAWN1	2
#define SFX_RESPAWN2	3

//sub songs numbers in the music.ftm

#define MUSIC_LEVEL			0
#define MUSIC_GAME			1
#define MUSIC_CLEAR			2
#define MUSIC_GAME_OVER		3
#define MUSIC_WELL_DONE		4
#define MUSIC_LOSE			5

//palettes data
//all the palettes are designed in NES Screen Tool, then copy/pasted here
//with Palettes/Put C data to clipboard function
/*{pal:"nes",layout:"nes"}*/
const unsigned char palGame1[16]={ 0x0f,0x11,0x32,0x30,0x0f,0x1c,0x2c,0x3c,0x0f,0x09,0x27,0x38,0x0f,0x11,0x21,0x31 };
/*{pal:"nes",layout:"nes"}*/
const unsigned char palGame2[16]={ 0x0f,0x11,0x32,0x30,0x0f,0x11,0x21,0x31,0x0f,0x07,0x27,0x38,0x0f,0x13,0x23,0x33 };
/*{pal:"nes",layout:"nes"}*/
const unsigned char palGame3[16]={ 0x0f,0x11,0x32,0x30,0x0f,0x15,0x25,0x35,0x0f,0x05,0x27,0x38,0x0f,0x13,0x23,0x33 };
/*{pal:"nes",layout:"nes"}*/
const unsigned char palGame4[16]={ 0x0f,0x11,0x32,0x30,0x0f,0x19,0x29,0x39,0x0f,0x0b,0x27,0x38,0x0f,0x17,0x27,0x37 };
/*{pal:"nes",layout:"nes"}*/
const unsigned char palGame5[16]={ 0x0f,0x11,0x32,0x30,0x0f,0x16,0x26,0x36,0x0f,0x07,0x27,0x38,0x0f,0x18,0x28,0x38 };
/*{pal:"nes",layout:"nes"}*/
const unsigned char palGameSpr[16]={ 0x0f,0x0f,0x29,0x30,0x0f,0x0f,0x26,0x30,0x0f,0x0f,0x24,0x30,0x0f,0x0f,0x21,0x30 };
/*{pal:"nes",layout:"nes"}*/
const unsigned char palTitle[16]={ 0x0f,0x0f,0x0f,0x0f,0x0f,0x1c,0x2c,0x3c,0x0f,0x12,0x22,0x32,0x0f,0x14,0x24,0x34 };


//metasprites

const unsigned char sprPlayer[]={
	0,-1,0x49,0,
	8,-1,0x4a,0,
	0, 7,0x4b,0,
	8, 7,0x4c,0,
	128
};

const unsigned char sprEnemy1[]={
	0,-1,0x4d,1,
	8,-1,0x4e,1,
	0, 7,0x4f,1,
	8, 7,0x50,1,
	128
};

const unsigned char sprEnemy2[]={
	0,-1,0x4d,2,
	8,-1,0x4e,2,
	0, 7,0x4f,2,
	8, 7,0x50,2,
	128
};

const unsigned char sprEnemy3[]={
	0,-1,0x4d,3,
	8,-1,0x4e,3,
	0, 7,0x4f,3,
	8, 7,0x50,3,
	128
};


//list of metasprites

const unsigned char* const sprListPlayer[]={ sprPlayer,sprEnemy1,sprEnemy2,sprEnemy3 };


//list of the levels, include pointer to the packed nametable of the level,
//and pointer to the associated palette

const unsigned char* const levelList[LEVELS_ALL*2]={
level1_nam,palGame1,
level2_nam,palGame2,
level3_nam,palGame3,
level4_nam,palGame3,
level5_nam,palGame3
};


//preinitialized update list used during the gameplay

const unsigned char updateListData[]={
0x28,0x00,TILE_EMPTY,	//these four entries are used to clear
0x28,0x00,TILE_EMPTY,	//the level tile after an item is collected
0x28,0x00,TILE_EMPTY,
0x28,0x00,TILE_EMPTY,
0x20,0x4f,0x10,			//these three entires are used to display
0x20,0x50,0x10,			//number of the collected items
0x20,0x51,0x10,
NT_UPD_EOF
};


//a nametable string with the game stats, created in NES Screen Tool
//and copy/pasted here with Shift+C

const unsigned char statsStr[27]={
	0x2c,0x25,0x36,0x25,0x2c,0x1a,0x00,0x00,0x27,
	0x25,0x2d,0x33,0x1a,0x00,0x00,0x00,0x0f,0x00,
	0x00,0x00,0x00,0x2c,0x29,0x36,0x25,0x33,0x1a
};


//list of screens such as level number, game over, and well done
//contains pointers to the packed nametables

const unsigned char* screenList[3]={ level_nam,gameover_nam,welldone_nam };

//list of sub songs for corresponding screens

const unsigned char screenMusicList[3]={MUSIC_LEVEL,MUSIC_GAME_OVER,MUSIC_WELL_DONE};

//large 1-5 numbers nametable definitons, 2x3 tiles each
//numbers were drawn one next to the other in NES Screen Tool,
//then that part of nametable was copy/pasted here with Shift+C
//here they are orderded as
//   top row of 11 22 33 44 55
//middle row of 11 22 33 44 55
//bottom row of 11 22 33 44 55

const unsigned char largeNums[10*3]={
	0x7a,0x7b,0x7c,0x7d,0x7c,0x7d,0x7e,0x7f,0x80,0x81,
	0x86,0x7b,0x87,0x88,0x89,0x8a,0x8b,0x8c,0x8d,0x7d,
	0x90,0x91,0x92,0x93,0x94,0x95,0x96,0x7f,0x94,0x95
};


//array for game map, contains walls, empty spaces, and items

static unsigned char map[MAP_WDT*MAP_HGT];


//put all the subsequent global vars into zeropage, to make code faster and shorter

#pragma bss-name(push,"ZEROPAGE")
#pragma data-name(push,"ZEROPAGE")

//set of general purpose global vars that are used everywhere in the program
//this makes code faster and shorter, although not very convinent and readable

static unsigned char i,j;
static unsigned char ptr,spr;
static unsigned char px,py;
static unsigned char wait;
static unsigned int i16;
static int iy,dy;

//this array is used to determine movement directions for enemies

static unsigned char dir[4];

//this array is used to convert nametable into game map, row by row

static unsigned char nameRow[32];

//number of moving characters on current level

static unsigned char player_all;

//character variables

static unsigned int  player_x    [PLAYER_MAX];
static unsigned int  player_y    [PLAYER_MAX];
static unsigned char player_dir  [PLAYER_MAX];
static int           player_cnt  [PLAYER_MAX];
static unsigned int  player_speed[PLAYER_MAX];
static unsigned char player_wait [PLAYER_MAX];

//number of items on current level, total and collected

static unsigned char items_count;
static unsigned char items_collected;

//game state variables

static unsigned char game_level;
static unsigned char game_lives;

//game state flags, they are 0 or 1

static unsigned char game_done;
static unsigned char game_paused;
static unsigned char game_clear;

//system vars used everywhere as well

static unsigned char frame_cnt;
static unsigned char bright;

//update list

static unsigned char update_list[7*3+1];



//smoothly fade current bright to the given value
//in case when to=0 it also stops the music,
//turns the display off, reset vram update and scroll

void pal_fade_to(unsigned to)
{
	if(!to) music_stop();

	while(bright!=to)
	{
		delay(4);
		if(bright<to) ++bright; else --bright;
		pal_bright(bright);
	}

	if(!bright)
	{
		ppu_off();
		set_vram_update(NULL);
		scroll(0,0);
	}
}



//show title screen

void title_screen(void)
{
	scroll(-8,240);//title is aligned to the color attributes, so shift it a bit to the right

	vram_adr(NAMETABLE_A);
	vram_unrle(title_nam);

	vram_adr(NAMETABLE_C);//clear second nametable, as it is visible in the jumping effect
	vram_fill(0,1024);

	pal_bg(palTitle);
	pal_bright(4);
	ppu_on_bg();
	delay(20);//delay just to make it look better

	iy=240<<FP_BITS;
	dy=-8<<FP_BITS;
	frame_cnt=0;
	wait=160;
	bright=4;

	while(1)
	{
		ppu_wait_frame();

		scroll(-8,iy>>FP_BITS);

		if(pad_trigger(0)&PAD_START) break;

		iy+=dy;

		if(iy<0)
		{
			iy=0;
			dy=-dy>>1;
		}

		if(dy>(-8<<FP_BITS)) dy-=2;

		if(wait)
		{
			--wait;
		}
		else
		{
			pal_col(2,(frame_cnt&32)?0x0f:0x20);//blinking press start text
			++frame_cnt;
		}
	}

	scroll(-8,0);//if start is pressed, show the title at whole
	sfx_play(SFX_START,0);

	for(i=0;i<16;++i)//and blink the text faster
	{
		pal_col(2,i&1?0x0f:0x20);
		delay(4);
	}

	pal_fade_to(0);
}



//show level intro, game over, or well done screen

void show_screen(unsigned char num)
{
	scroll(-4,0); //all the screens are misaligneg horizontally by half of a tile

	if(num<LEVELS_ALL) spr=0; else spr=num-LEVELS_ALL+1;//get offset in the screens list

	vram_adr(NAMETABLE_A);
	vram_unrle(screenList[spr]);

	if(!spr)//if it is the level screen, print large number
	{
		j=num<<1;
		i16=0x2194;//position of the number in the nametable

		for(i=0;i<3;++i)
		{
			vram_adr(i16);
			vram_put(largeNums[j]);
			vram_put(largeNums[j+1]);
			j+=10;
			i16+=32;
		}
	}

	i16=(num==SCREEN_GAMEOVER)?0x1525:0x1121;//two colors for flashing text in LSB and MSB

	frame_cnt=0;

	pal_col(2,i16&0xff);//this palette entry is used for flashing text
	pal_col(3,0x30);
	pal_col(6,0x30);
	ppu_on_bg();

	pal_fade_to(4);
	music_play(screenMusicList[spr]);

	if(!spr)//if it is the level screen, just wait one second
	{
		delay(50);
	}
	else//otherwise wait for Start button and display flashing text
	{
		while(1)
		{
			ppu_wait_frame();

			pal_col(2,frame_cnt&2?i16&0xff:i16>>8);

			if(pad_trigger(0)&PAD_START) break;

			++frame_cnt;
		}
	}

	pal_fade_to(0);
}



//set up a move in the specified direction if there is no wall

void player_move(unsigned char id,unsigned char dir)
{
	px=player_x[id]>>(TILE_SIZE_BIT+FP_BITS);
	py=player_y[id]>>(TILE_SIZE_BIT+FP_BITS);

	switch(dir)
	{
	case DIR_LEFT:  --px; break;
	case DIR_RIGHT: ++px; break;
	case DIR_UP:    --py; break;
	case DIR_DOWN:  ++py; break;
	}

	if(map[MAP_ADR(px,py)]==TILE_WALL) return;

	player_cnt[id]=TILE_SIZE<<FP_BITS;
	player_dir[id]=dir;
}



//print a 1-3 digit decimal number into VRAM

void put_num(unsigned int adr,unsigned int num,unsigned char len)
{
	vram_adr(adr);

	if(len>2) vram_put(0x10+num/100);
	if(len>1) vram_put(0x10+num/10%10);
	vram_put(0x10+num%10);
}



//the main gameplay code

void game_loop(void)
{
	oam_clear();

	i=game_level<<1;

	vram_adr(NAMETABLE_A);
	vram_unrle(levelList[i]);					//unpack level nametable

	vram_adr(NAMETABLE_A+0x0042);
	vram_write((unsigned char*)statsStr,27); 	//add game stats string

	pal_bg(levelList[i+1]); 						//set up background palette
	pal_spr(palGameSpr); 							//set up sprites palette

	player_all=0;
	items_count=0;
	items_collected=0;

	//this loop reads the level nametable back from VRAM, row by row,
	//constructs game map, removes spawn points from the nametable,
	//and writes back to the VRAM

	i16=NAMETABLE_A+0x0080;
	ptr=0;
	wait=0;

	for(i=2;i<MAP_HGT+2;++i)
	{
		vram_adr(i16);
		vram_read(nameRow,32);
		vram_adr(i16);

		for(j=0;j<MAP_WDT<<1;j+=2)
		{
			spr=nameRow[j];

			switch(spr)
			{
			case TILE_PLAYER://player
			case TILE_ENEMY1://enemies
			case TILE_ENEMY2:
			case TILE_ENEMY3:
				player_dir  [player_all]=DIR_NONE;
				player_x    [player_all]=(j<<3)<<FP_BITS;
				player_y    [player_all]=(i<<4)<<FP_BITS;
				player_cnt  [player_all]=0;
				player_wait [player_all]=16+((spr-TILE_PLAYER)<<4);
				player_speed[player_all]=(spr==TILE_PLAYER)?2<<FP_BITS:10+((spr-TILE_ENEMY1)<<1);
				++player_all;
				wait+=16;
				spr=TILE_EMPTY;
				break;

			case TILE_ITEM:
				++items_count;
				break;
			}

			map[ptr++]=spr;

			vram_put(spr);
			vram_put(nameRow[j+1]);
		}

		i16+=64;
	}

	//setup update list

	memcpy(update_list,updateListData,sizeof(updateListData));

	set_vram_update(update_list);

	//put constant game stats numbers, that aren't updated during level

	put_num(NAMETABLE_A+0x0048,game_level+1,1);
	put_num(NAMETABLE_A+0x0053,items_count,3);
	put_num(NAMETABLE_A+0x005d,game_lives-1,1);

	//enable display

	ppu_on_all();

	game_done=FALSE;
	game_paused=FALSE;
	game_clear=FALSE;

	bright=0;
	frame_cnt=0;

	while(!game_done)
	{
		//construct OAM from object parameters

		spr=(player_all-1)<<4;

		for(i=0;i<player_all;++i)
		{
			py=player_y[i]>>FP_BITS;

			if(player_wait[i])
			{
				if(player_wait[i]>=16||player_wait[i]&2) py=240;
			}

			oam_meta_spr(player_x[i]>>FP_BITS,py,spr,sprListPlayer[i]);
			spr-=16;
		}

		//wait for next frame
		//it is here and not at beginning of the loop because you need
		//to update OAM for the very first frame, and you also need to do that
		//right after object parameters were changed, so either OAM update should
		//be in a function that called before the loop and at the end of the loop,
		//or wait for NMI should be placed there
		//otherwise you would have situation update-wait-display, i.e.
		//one frame delay between action and display of its result

		ppu_wait_frame();

		++frame_cnt;

		//slowly fade virtual brightness to needed value,
		//which is max for gameplay or half for pause

		if(!(frame_cnt&3))
		{
			if(!game_paused&&bright<4) ++bright;
			if( game_paused&&bright>2) --bright;

			pal_bright(bright);
		}

		//poll the gamepad in the trigger mode

		i=pad_trigger(0);

		//it start was released and then pressed, toggle pause mode

		if(i&PAD_START)
		{
			game_paused^=TRUE;
			music_pause(game_paused);
		}

		//don't process anything in pause mode, just display latest game state

		if(game_paused) continue;

		//CHR bank switching animation with different speed for background and sprites

		bank_bg((frame_cnt>>4)&1);
		bank_spr((frame_cnt>>3)&1);

		//a counter that does not allow objects to move while spawn animation plays

		if(wait)
		{
			--wait;

			if(!wait) music_play(MUSIC_GAME);//start the music when all the objects spawned
		}

		//check for level completion condition

		if(items_collected==items_count)
		{
			music_play(MUSIC_CLEAR);
			game_done=TRUE;
			game_clear=TRUE;
		}

		//process all the objects
		//player and enemies are the same type of object in this game,
		//to make code simpler and shorter, but generally they need to be
		//different kind of objects

		for(i=0;i<player_all;++i)
		{
			//per-object spawn animation counter, it counts fron N to 16 to 0
			//needed because objects spawn in sequence, not all at once

			if(player_wait[i])
			{
				if(player_wait[i]==16) sfx_play(i?SFX_RESPAWN2:SFX_RESPAWN1,i);
				--player_wait[i];
				continue;
			}

			if(wait) continue; //don't process object movements if spawn animation is running

			//check collision of an enemy object with player object
			//NOT logic is used here, check http://gendev.spritesmind.net/page-collide.html

			if(i)
			{
				if(!((player_x[i]+(4 <<FP_BITS))>=(player_x[0]+(12<<FP_BITS))||
				     (player_x[i]+(12<<FP_BITS))< (player_x[0]+(4 <<FP_BITS))||
					 (player_y[i]+(4 <<FP_BITS))>=(player_y[0]+(12<<FP_BITS))||
					 (player_y[i]+(12<<FP_BITS))< (player_y[0]+(4 <<FP_BITS))))
				{
					//if an enemy touch the player, quit the game loop

					if(!game_clear)
					{
						music_play(MUSIC_LOSE);
						game_done=TRUE;
						break;
					}
				}
			}

			//if movement counter is not zero, process the movement

			if(player_cnt[i])
			{
				switch(player_dir[i])
				{
				case DIR_RIGHT: player_x[i]+=player_speed[i]; break;
				case DIR_LEFT:  player_x[i]-=player_speed[i]; break;
				case DIR_DOWN:  player_y[i]+=player_speed[i]; break;
				case DIR_UP:    player_y[i]-=player_speed[i]; break;
				}

				player_cnt[i]-=player_speed[i];

				//if move from one tile to another is over, realign the object to tile grid
				//it is needed because when it moves with non-integer speed, it could
				//overrun the destination tile a little bit, and thus can't take a turn properly

				if(player_cnt[i]<=0)
				{
					if(player_cnt[i]<0) //overrun
					{
						player_cnt[i]=0;

						//0xff is a coordinate mask that leaves only integer tile offeset
						//it is 8:4:4 here, where 8 is integer tile coordinate,
						//first 4 is offset in the tile, which is 16 pixels wide,
						//and second 4 is fixed point resolution

						player_x[i]=(player_x[i]&0xff00)+(player_dir[i]==DIR_LEFT?0x100:0);
						player_y[i]=(player_y[i]&0xff00)+(player_dir[i]==DIR_UP  ?0x100:0);
					}

					//it is is the player object, check if there is an item in the new tile
					if(!i)
					{
						i16=MAP_ADR((player_x[i]>>(TILE_SIZE_BIT+FP_BITS)),
						            (player_y[i]>>(TILE_SIZE_BIT+FP_BITS)));

						if(map[i16]==TILE_ITEM)
						{
							map[i16]=TILE_EMPTY; //mark as collected in the game map

							sfx_play(SFX_ITEM,2);
							++items_collected;

							//get address of the tile in the nametable

							i16=NAMETABLE_A+0x0080+(((player_y[i]>>(TILE_SIZE_BIT+FP_BITS))-2)<<6)|
							                        ((player_x[i]>>(TILE_SIZE_BIT+FP_BITS))<<1);

							//replace it with empty tile through the update list

							update_list[0]=i16>>8;
							update_list[1]=i16&255;
							update_list[3]=update_list[0];
							update_list[4]=update_list[1]+1;
							i16+=32;
							update_list[6]=i16>>8;
							update_list[7]=i16&255;
							update_list[9]=update_list[6];
							update_list[10]=update_list[7]+1;

							//update number of collected items in the game stats

							update_list[14]=0x10+items_collected/100;
							update_list[17]=0x10+items_collected/10%10;
							update_list[20]=0x10+items_collected%10;
						}
					}
				}
			}

			if(!player_cnt[i]) //movement to the next tile is done, set up new movement
			{
				if(!i) //this is the player, process controls
				{
					//get gamepad state, it was previously polled with pad_trigger

					j=pad_state(0);

					//this is a tricky part to make controls more predictable
					//when you press two directions at once, sliding by a wall
					//to take turn into a passage on the side
					//this piece of code gives current direction lower priority
					//through testing it first
					//bits in player_dir var are matching to the buttons bits

					if(j&player_dir[0])
					{
						j&=~player_dir[0]; //remove the direction from further check
						player_move(i,player_dir[0]); //change the direction
					}

					//now continue control processing as usual

					if(j&PAD_LEFT)  player_move(i,DIR_LEFT);
					if(j&PAD_RIGHT) player_move(i,DIR_RIGHT);
					if(j&PAD_UP)    player_move(i,DIR_UP);
					if(j&PAD_DOWN)  player_move(i,DIR_DOWN);
				}
				else //this is an enemy, run AI
				{
					//the AI is very simple
					//first we create list of all directions that are possible to take
					//excluding the direction that is opposite to previous one

					i16=MAP_ADR((player_x[i]>>8),(player_y[i]>>8));
					ptr=player_dir[i];
					j=0;

					if(ptr!=DIR_RIGHT&&map[i16-1]!=TILE_WALL) dir[j++]=DIR_LEFT;
					if(ptr!=DIR_LEFT &&map[i16+1]!=TILE_WALL) dir[j++]=DIR_RIGHT;
					if(ptr!=DIR_DOWN &&map[i16-MAP_WDT]!=TILE_WALL) dir[j++]=DIR_UP;
					if(ptr!=DIR_UP   &&map[i16+MAP_WDT]!=TILE_WALL) dir[j++]=DIR_DOWN;

					//randomly select a possible direction

					player_move(i,dir[rand8()%j]);

					//if there was more than one possible direction,
					//i.e. it is a branch and not a corridor,
					//attempt to move towards the player

					if(j>1)
					{
						if(ptr!=DIR_DOWN &&player_y[0]<player_y[i]) player_move(i,DIR_UP);
						if(ptr!=DIR_UP   &&player_y[0]>player_y[i]) player_move(i,DIR_DOWN);
						if(ptr!=DIR_RIGHT&&player_x[0]<player_x[i]) player_move(i,DIR_LEFT);
						if(ptr!=DIR_LEFT &&player_x[0]>player_x[i]) player_move(i,DIR_RIGHT);
					}
				}
			}
		}
	}

	delay(100);
	pal_fade_to(0);
}



//this is where the program starts
extern const void sound_data[];
extern const void music_data[];

void main(void)
{
  	famitone_init(&music_data);
	sfx_init(&sound_data);
	nmi_set_callback(famitone_update);
  
	while(1)//infinite loop, title-gameplay
	{
		title_screen();

		game_level=0;
		game_lives=4;

		while(game_lives&&game_level<LEVELS_ALL)//loop for gameplay
		{
			show_screen(game_level);

			game_loop();

			if(game_clear) ++game_level; else --game_lives;
		}

		show_screen(!game_lives?SCREEN_GAMEOVER:SCREEN_WELLDONE);//show game results
	}
}