Permalink
Switch branches/tags
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
1335 lines (1083 sloc) 37 KB
/*
*
* SplatooD - A Splatoon Demake
*
*/
/* NESlib */
#include "neslib.h"
/* Name tables */
#include "levels/splat_title.h"
#include "levels/kelpdome.h"
#include "levels/bluefin.h"
#include "levels/saltspray.h"
#include "levels/twofish.h"
#include "levels/endgame.h"
/* Metasprite definitions */
#include "metasprites.h"
/* Music data comes from music.s */
extern const unsigned char music_music_data[];
/* 12:4 fixed-point math for player movement */
#define FP_BITS 4
/* Map dimensions */
#define MAP_WDT 16
#define MAP_WDT_BIT 4
#define MAP_HGT 15
/* Convert screen offset to map offset */
#define MAP_ADR(x,y) ((((y))<<MAP_WDT_BIT)|(x))
/* Map tiles */
#define TILE_SIZE 16
#define TILE_SIZE_BIT 4
/* Movement */
#define DIR_NONE 0
#define DIR_LEFT PAD_LEFT
#define DIR_RIGHT PAD_RIGHT
#define DIR_UP PAD_UP
#define DIR_DOWN PAD_DOWN
#define WALK_CYCLE 2
#define FRAMES_PER_STEP 10
/* Sprite indices */
#define SPR_DOWN 0
#define SPR_RIGHT 1
#define SPR_UP 2
#define SPR_LEFT 3
/* Weapons */
#define NUM_WPNS 2
#define WPN_ROLLER 0
#define WPN_CHARGER 1
/* Tuneable gameplay parameters */
#define RESPAWN_TIME 64
#define PROJECTILE_WAIT_TIME_ROLLER 16
#define PROJECTILE_WAIT_TIME_CHARGER 64
#define PROJECTILE_DISTANCE_ROLLER 4
#define PROJECTILE_DISTANCE_CHARGER 16
#define PROJECTILE_SPEED_ROLLER 8<<FP_BITS
#define PROJECTILE_SPEED_CHARGER 16<<FP_BITS
#define CHARGE_FRAMES 64
#define TIMER_START 60
#define TIMER_CYCLE 0x20
/* Tile numbers for important things */
#define TILE_PLAYERA 0x10
#define TILE_PLAYERB 0x11
#define PLAYER_MAX 2
/* Sound effects */
#define SFX_START 0
#define SFX_SPLAT 1
#define SFX_RESPAWN1 2
#define SFX_DEATH 3
/* Music */
#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 4
#define MUSIC_TITLE 5
#define MUSIC_STAGE_SELECT 6
/* Palettes */
const unsigned char palTitle[16]={ 0x0f,0x30,0x0f,0x0f,0x0f,0x30,0x0f,0x0f,0x0f,0x30,0x0f,0x0f,0x0f,0x30,0x0f,0x0f };
/* { 0x0f,0x30,0x14,0x0f,0x0f,0x30,0x29,0x0f,0x0f,0x30,0x27,0x0f,0x0f,0x30,0x0f,0x0f }; */
const unsigned char palInkable[16]={ 0x0f,0x10,0x30,0x30,0x0f,0x04,0x14,0x24,0x0f,0x19,0x29,0x3a,0x0f,0x01,0x21,0x31 };
const unsigned char palEndgame[16]={ 0x0f,0x30,0x30,0x30,0x0f,0x24,0x24,0x14,0x0f,0x29,0x29,0x3a,0x0f,0x21,0x21,0x11 };
const unsigned char palPaused[] = {0x0f,0x2d,0x2D,0x2D, 0x0f,0x0f,0x00,0x2d, 0x0f,0x0f,0x00,0x2d, 0x0f,0x00,0x30,0x30 };
const unsigned char palCharacters[8]={ 0x0f,0x04,0x30,0x0f,0x0f,0x19,0x30,0x0f };
const unsigned char palCharPaused[8] = {0x0f,0x00,0x2d,0x0f,0x0f,0x00,0x2d,0x0f};
const unsigned char palJudd[]={ 0x0f,0x26,0x27,0x30,0x0f,0x14,0x27,0x30,0x0f,0x2a,0x27,0x30,0x0f,0x26,0x27,0x30 };
/* v 1 . 0 . 5 */
const unsigned char versionStr[] = {230,232,241,231,241,236};
const unsigned char victoryMsg[] = "Victory!";
const unsigned char fourSpaces[] = " ";
const unsigned char tieMsg[] = "It's a tie!";
const unsigned char zero[]={0};
const unsigned char credits_0[] = " SplatooD";
const unsigned char credits_1[] = " a Splatoon demake";
const unsigned char credits_2[] = " Project Lead";
const unsigned char credits_3[] = " John Carmackerel";
const unsigned char credits_4[] = " Music";
const unsigned char credits_5[] = " Nobuo Squidimatsu";
const unsigned char credits_6[] = " Title & Postgame";
const unsigned char credits_7[] = " Koji Ikarashi";
const unsigned char credits_8[] = " Game Design";
const unsigned char credits_9[] = " Squideo Kojima";
const unsigned char credits_10[] = " Graphics";
const unsigned char credits_11[] = " Squid Meier";
const unsigned char credits_12[] = " Game Mechanics";
const unsigned char credits_13[] = " American McGill";
const unsigned char credits_14[] = " Splatoon (C) 2015";
const unsigned char credits_15[] = " Nintendo";
const unsigned char credits_16[] = "Thanks for playing!";
const unsigned char * credits[] = { credits_0, credits_1, credits_2, credits_3, credits_4, credits_5, credits_6, credits_7, credits_8, credits_9, credits_10, credits_11, credits_12, credits_13, credits_14, credits_15, credits_16, zero, zero, zero };
const unsigned char select_map[] = "Select Map";
const unsigned char level_kelpdome[] = "Kelp Dome";
const unsigned char level_bluefin_depot[] = "Bluefin Depot";
const unsigned char level_saltspray[] = "Saltspray Rig";
const unsigned char level_twofish[] = "Two Fish";
/* Level count */
#define LEVELS_ALL 4
/* Number of pointers in a level entry in the following list. */
#define LEVEL_ENTRY 4
/*
* Level list.
*
* Nametable, Background palette, Sprite palette, Name
*/
const unsigned char* const levelList[LEVELS_ALL*4]={
kelpdome,palInkable,palCharacters,level_kelpdome,
bluefin_depot,palInkable,palCharacters,level_bluefin_depot,
saltspray,palInkable,palCharacters,level_saltspray,
twofish,palInkable,palCharacters,level_twofish,
};
/* Weapon Select */
const unsigned char select_weapon[] = "Select Weapon";
const unsigned int projectile_speed[] = { PROJECTILE_SPEED_ROLLER, PROJECTILE_SPEED_CHARGER };
const unsigned char weapon_ranges[] = { PROJECTILE_DISTANCE_ROLLER, PROJECTILE_DISTANCE_CHARGER };
const unsigned char weapon_cooldown[] = { PROJECTILE_WAIT_TIME_ROLLER, PROJECTILE_WAIT_TIME_CHARGER };
/* This is used to ensure the updatelist starts empty. */
const unsigned char updateListData[]={
NT_UPD_EOF
};
/* (play-time) map data */
static unsigned char map[MAP_WDT*MAP_HGT];
static unsigned char palette_state[64];
/* All the globals after this end up in the zeropage. */
#pragma bssseg (push,"ZEROPAGE")
#pragma dataseg(push,"ZEROPAGE")
static unsigned char i,j;
static unsigned char ptr,spr;
static unsigned char px,py;
static unsigned char wait;
static unsigned int i16;
static unsigned int sum,div;
static unsigned char nameRow[32];
static unsigned char player_all;
static unsigned int player_x_spawn [PLAYER_MAX];
static unsigned int player_y_spawn [PLAYER_MAX];
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];
static unsigned char player_cooldown [PLAYER_MAX];
static unsigned char player_anim_cnt [PLAYER_MAX];
static unsigned char player_diag_flip[PLAYER_MAX];
static unsigned char player_wpn [PLAYER_MAX];
static unsigned char player_charge [PLAYER_MAX];
static unsigned int projectile_x [PLAYER_MAX];
static unsigned int projectile_y [PLAYER_MAX];
static unsigned char projectile_dir [PLAYER_MAX];
static int projectile_cnt [PLAYER_MAX];
static unsigned char projectile_dist [PLAYER_MAX];
static unsigned char game_level;
static unsigned char game_done;
static unsigned char game_paused;
static unsigned char frame_cnt;
static unsigned char anim_frame;
static unsigned char spr_dir;
static unsigned char bright;
static unsigned char timer;
/* PPU Update list */
static unsigned char update_list[13*3+1];
static unsigned char update_list_index = 0;
void insert_into_update_list(unsigned char addr1, unsigned char addr2, unsigned char value) {
update_list[update_list_index] = addr1;
update_list[update_list_index+1] = addr2;
update_list[update_list_index+2] = value;
update_list[update_list_index+3] = NT_UPD_EOF;
update_list_index += 3;
}
void set_tile_at(unsigned int nametable, unsigned char x, unsigned char y, unsigned char value) {
unsigned char a = (nametable + y * 32) >> 8;
unsigned char b = y * 32 + x;
insert_into_update_list(a,b,value);
}
void clear_update_list() {
update_list_index = 0;
update_list[0] = NT_UPD_EOF;
}
/**
* Set the pallete for a tile in the attribute table.
*/
void set_tile_palette(unsigned char x_idx, unsigned char y_idx, unsigned char value) {
unsigned char palette_mask;
unsigned char palette_address;
unsigned char palette_shift_val;
palette_address = (y_idx / 2) * 8 + x_idx / 2;
palette_shift_val = (y_idx % 2)*4 + (x_idx % 2)*2;
palette_mask = ~(0x03 << palette_shift_val);
palette_state[palette_address] = (palette_state[palette_address] & palette_mask) + (value << palette_shift_val);
insert_into_update_list((NAMETABLE_A >> 8) + 3, 0xc0 + palette_address, palette_state[palette_address]);
}
void reset_palette_state() {
unsigned char i;
for (i = 0; i < 64; i++) {
palette_state[i] = 0;
}
}
/* Player scores */
static unsigned int player_score[2] = { 0 };
/**
* Calculate a player's score from map data.
*/
unsigned int score_player(unsigned char player_id) {
unsigned char mask;
unsigned char intermediate;
unsigned int score = 0;
unsigned char i;
mask = player_id + 1;
mask = ((mask << 2) | mask );
mask = ((mask << 4) | mask );
for (i = 0; i < 64; i++) {
intermediate = palette_state[i] ^ (~ mask);
intermediate = 0x55 & intermediate & (intermediate >> 1);
intermediate = (intermediate >> 4) + (intermediate & 0x0f);
score += (intermediate >> 2) + (intermediate & 0x03);
}
return score;
}
/**
* Determine if a given map tile can be inked.
*/
unsigned char can_ink(unsigned char map_type) {
if (map_type == 0x2c ||
map_type == 0x2d ||
map_type == 0x3c ||
map_type == 0x3d ||
map_type == 0x2a ||
map_type == 0x2b ||
map_type == 0x3a ||
map_type == 0x3b) {
return FALSE;
}
return TRUE;
}
/* Make sure this stays a multiple of 2 */
#define TITLE_ROLL_HEIGHT 110
/**
* Used on title and credits screens to
* delay while waiting for "START" to be pressed.
*
* Allows for skippable animations.
*/
#define WAIT_WITH_SKIP(w) \
wait = w; \
while (1) { \
if (wait == 0) break; \
wait--; \
ppu_wait_frame(); \
if ((pad_trigger(0) | pad_trigger(1)) & PAD_START) { \
goto _skip_title; \
} \
}
/**
* Show title screen.
*/
void show_title(void) {
/* Ensure we have the write CHR bank set for the title, and reset scroll to 0. */
ppu_off();
bank_bg(0);
scroll(0,TITLE_ROLL_HEIGHT);
/* Load title screen into NAME A */
vram_adr(NAMETABLE_A);
vram_write(splat_title,sizeof(splat_title));
/* Clear nametable C to avoid artifacts when bouncing. */
vram_adr(NAMETABLE_C);
vram_fill(0,1024);
vram_adr(NAMETABLE_A+0x342);
vram_write(versionStr,sizeof(versionStr));
pal_bright(4);
pal_bg(palTitle);
ppu_on_bg();
WAIT_WITH_SKIP(20);
frame_cnt=0;
wait=10;
/* Scroll */
while (1) {
frame_cnt += 2;
scroll(0,TITLE_ROLL_HEIGHT-frame_cnt);
if (frame_cnt == TITLE_ROLL_HEIGHT) break;
ppu_wait_frame();
if ((pad_trigger(0) | pad_trigger(1)) & PAD_START) {
goto _skip_title;
}
}
scroll(0,0);
/* Splat splat splat */
WAIT_WITH_SKIP(20);
sfx_play(SFX_SPLAT,0);
pal_col(10,0x27);
WAIT_WITH_SKIP(20);
sfx_play(SFX_SPLAT,0);
pal_col(2,0x14);
WAIT_WITH_SKIP(20);
sfx_play(SFX_SPLAT,0);
pal_col(6,0x29);
WAIT_WITH_SKIP(20);
music_play(MUSIC_TITLE);
pal_col(3,0x30);
pal_col(15,0x30);
wait = 10;
frame_cnt = 0;
while (1) {
/* Flash start between white and gray. */
wait--;
if (!wait) {
pal_col(15,(frame_cnt&1)?0x30:0x20);
wait = 10;
frame_cnt++;
}
ppu_wait_frame();
/* Wait for input to start game. */
if ((pad_trigger(0) | pad_trigger(1)) & PAD_START) {
break;
}
}
_skip_title:
scroll(0,0);
pal_bg(palTitle);
pal_col(10,0x27);
pal_col(2,0x14);
pal_col(6,0x29);
pal_col(3,0x30);
music_stop();
sfx_play(SFX_START,0);
/* Blink rapidly. */
for (i=0;i<16;++i) {
pal_col(15, (i & 1) ? 0x0f : 0x20);
delay(4);
}
}
/* Where numbers start in the EBCDIC tileset */
#define NUM_START 0xB0
/**
* Put a number into the vram nametable when not rendering.
* Cheaper than an update table, but only use this when
* initializing VRAM and not during play.
*/
void put_num(unsigned int adr,unsigned int num,unsigned char len) {
vram_adr(adr);
if (len>2) vram_put(NUM_START+(num/100)%10);
if (len>1) vram_put(NUM_START+(num/10)%10);
vram_put(NUM_START+(num%10));
}
/**
* Print a string into VRAM.
* Use only between frames!
*/
void print_str(unsigned int adr, const unsigned char * str) {
vram_adr(adr);
while (*str) {
vram_put((*str)+0x80);
str++;
}
}
/**
* Map select screen.
*/
void show_select_map() {
/* Use bank 1 to get full characterset tiles */
bank_bg(1);
bank_spr(1);
music_play(MUSIC_STAGE_SELECT);
ppu_off();
pal_bg(palEndgame);
/* We don't use an existing nametable, but instead clear out NAMETABLE_A */
vram_adr(NAMETABLE_A);
vram_fill(0xa0,1024-64);
vram_fill(0,64);
/* "Select Map" */
print_str(NAMETABLE_A+0x0EB,select_map);
/* Map names */
for (i = 0; i < LEVELS_ALL; ++i) {
print_str(NAMETABLE_A+0x148+0x20*i,levelList[i*LEVEL_ENTRY+3]);
}
ppu_on_bg();
clear_update_list();
set_vram_update(update_list);
while (1) {
/*
* A squid represents the current selection.
*/
int tmp = NAMETABLE_A + 0x146 + 0x20 * game_level;
insert_into_update_list(tmp>>8,tmp&0xFF,'*'+0x80);
/* Loop waiting for input. */
while (1) {
ppu_wait_frame();
clear_update_list();
j = pad_trigger(0) | pad_trigger(1);
/* Start - Select map */
if (j & PAD_START) return;
/* Up - move cursor up */
if (j & PAD_UP) {
insert_into_update_list(tmp>>8,tmp&0xFF,' '+0x80);
if (game_level == 0) {
game_level = LEVELS_ALL-1;
} else {
game_level -= 1;
}
break;
}
/* Select or Down - move cursor down */
if (j & PAD_SELECT || j & PAD_DOWN) {
insert_into_update_list(tmp>>8,tmp&0xFF,' '+0x80);
game_level += 1;
if (game_level == LEVELS_ALL) game_level = 0;
break;
}
}
}
}
/*
* Two-player weapon select.
*
* This needs to be expanded to support more than two players,
* but I leave that as an exercise for later.
*/
void show_select_weapon(void) {
int player_id;
player_wpn[0] = WPN_ROLLER;
player_wpn[1] = WPN_ROLLER;
ppu_off();
pal_bg(palEndgame);
pal_spr(palCharacters);
/* We don't use an existing nametable, but instead clear out NAMETABLE_A */
vram_adr(NAMETABLE_A);
vram_fill(0xa0,1024-64);
vram_fill(0,64);
print_str(NAMETABLE_A+0x0E9,select_weapon);
oam_clear();
ppu_on_all();
anim_frame = 0;
while (1) {
next:
#define A_HEIGHT 80
#define B_HEIGHT 120
#define LEFT 100
#define RIGHT 140
if (player_wpn[0] == WPN_ROLLER) {
oam_meta_spr(LEFT, A_HEIGHT, 0<<4, SprPlayers[0][WPN_ROLLER][0][anim_frame]);
oam_meta_spr(RIGHT, A_HEIGHT, 1<<4, unselected_charger);
} else {
oam_meta_spr(LEFT, A_HEIGHT, 0<<4, unselected_roller);
oam_meta_spr(RIGHT, A_HEIGHT, 1<<4, SprPlayers[0][WPN_CHARGER][0][anim_frame]);
}
if (player_wpn[1] == WPN_ROLLER) {
oam_meta_spr(LEFT, B_HEIGHT, 2<<4, SprPlayers[1][WPN_ROLLER][0][anim_frame]);
oam_meta_spr(RIGHT, B_HEIGHT, 3<<4, unselected_charger);
} else {
oam_meta_spr(LEFT, B_HEIGHT, 2<<4, unselected_roller);
oam_meta_spr(RIGHT, B_HEIGHT, 3<<4, SprPlayers[1][WPN_CHARGER][0][anim_frame]);
}
i = 0;
anim_frame = 1 - anim_frame;
while (i < 10) {
ppu_wait_frame();
i++;
for (player_id = 0; player_id < 2; player_id++) {
j = pad_trigger(player_id);
/* Start - Select weapon */
if (j & PAD_START) return;
/* Up - move cursor up */
if (j & PAD_UP) {
if (player_wpn[player_id] == 0) {
player_wpn[player_id] = NUM_WPNS-1;
} else {
player_wpn[player_id] -= 1;
}
goto next;
}
/* Select or Down - move cursor down */
if (j & PAD_SELECT || j & PAD_LEFT || j & PAD_RIGHT) {
player_wpn[player_id] += 1;
if (player_wpn[player_id] == NUM_WPNS) player_wpn[player_id] = 0;
goto next;
}
}
}
}
}
/**
* Credits sequence.
*/
void show_credits() {
bank_bg(1);
bank_spr(1);
/* Clear the screen. */
ppu_off();
pal_bg(palEndgame);
/* Write credits. */
i = 0;
j = 0;
div = 0;
/* Clear the scrolling nametables, A and C */
vram_adr(NAMETABLE_C);
vram_fill(0xa0,1024-64);
vram_fill(0,64);
sum = NAMETABLE_C;
while (i < 17) { /* 4 6 A C */
ppu_wait_nmi();
ppu_off();
vram_adr(sum);
vram_fill(0xa0,1024-64);
vram_fill(0,64);
print_str(sum+0x126, credits[i]);
print_str(sum+0x166, credits[i+1]);
print_str(sum+0x1C6, credits[i+2]);
print_str(sum+0x206, credits[i+3]);
ppu_on_all();
do {
scroll(0,div);
/* Judd as a sprite. */
if (i == 0 && div < 0x62) {
oam_meta_spr(0x70,0x48-div,0,(char*)i16);
}
div++;
WAIT_WITH_SKIP(1);
} while (div % 240);
if (div == 480) div = 0;
if (sum == NAMETABLE_A) sum = NAMETABLE_C;
else sum = NAMETABLE_A;
WAIT_WITH_SKIP(50);
i+=4;
}
WAIT_WITH_SKIP(100);
ppu_off();
vram_adr(sum);
vram_fill(0xa0,1024-64);
ppu_on_all();
while (div % 240 < 100) {
WAIT_WITH_SKIP(1);
scroll(0,div);
div++;
}
_skip_title:
delay(20);
}
/**
* Post Game Screen
*
* Shows Judd pointing to the winner,
* inked % of map, total inked cells,
* and a gauge.
*/
void show_endgame(void) {
bank_bg(1);
bank_spr(1);
/* Set up the background (meter, "Game Over") */
ppu_off();
vram_adr(NAMETABLE_A);
vram_write(endgame,1024);
pal_bg(palEndgame);
pal_spr(palJudd);
oam_clear();
/* We're going to use the update lists to set background palettes */
clear_update_list();
/* Give players with no points a freebie to help with the math. */
if (player_score[0] == 0) player_score[0] = 1;
if (player_score[1] == 0) player_score[1] = 1;
/*
* We calculate percentages and bar gauge based on total inked
* not total available for inking. This is a bit different from
* the real deal.
*/
sum = (player_score[0] + player_score[1]);
/* Calculate percentage for player 1 */
div = player_score[0] * 100;
div /= sum;
if (div == 100) div = 99;
if (div == 0) div = 1;
put_num(NAMETABLE_A+0x1EA,div,2);
/* Player 2 must be 100% - (player 1) */
div = 100 - div;
put_num(NAMETABLE_A+0x1F4,div,2);
/* Convert Player 2's percentage to a gauge width */
div *= 9;
div /= 100;
/* Print the victory message. */
if (player_score[0] > player_score[1]) {
print_str(NAMETABLE_A+0x1C9,victoryMsg);
i16 = (int)judd_left;
} else if (player_score[0] == player_score[1]) {
/* If both players have the same score, it was a tie. */
print_str(NAMETABLE_A+0x1CB,tieMsg);
i16 = (int)judd_tie;
} else {
print_str(NAMETABLE_A+0x1CF,victoryMsg);
i16 = (int)judd_right;
}
oam_meta_spr(0x70,0x48,0,(char*)i16);
/* Print raw player scores below the gauge */
put_num(NAMETABLE_A+0x24A,player_score[0],3);
put_num(NAMETABLE_A+0x253,player_score[1],3);
/* Enable the background */
ppu_on_all();
/* Play the post-game music. */
music_play(MUSIC_WELL_DONE);
/* Set the gauge palettes. */
clear_update_list();
for (i = 4; i < 12 - div; ++i) {
/* Player 1 on the left. */
set_tile_palette(i,8,1);
}
for (i = 12 - div; i <= 11; ++i) {
/* Player 2 on the right. */
set_tile_palette(i,8,2);
}
set_vram_update(update_list);
while (1) {
/* Wait for someone to hit start. */
ppu_wait_frame();
clear_update_list();
if ((pad_trigger(0) | pad_trigger(1)) & PAD_START) break;
}
reset_palette_state();
}
/*
* Move a projectile if the is no wall
*/
void projectile_move(unsigned char id) {
unsigned char map_type = 0;
unsigned char dir = projectile_dir[id];
/*
* Reset speed; if the last direction we check is valid, we'll set speed, but we
* shouldn't let players input a valid direction that sets speed plus a higher priority
* invalid direction to let them move in the invalid direction
*/
projectile_cnt[id]=0;
projectile_dist[id]-=1;
if (projectile_dist[id] == 0) {
projectile_dir[id]=DIR_NONE;
return;
}
px=projectile_x[id]>>(TILE_SIZE_BIT+FP_BITS);
py=projectile_y[id]>>(TILE_SIZE_BIT+FP_BITS);
switch (dir) {
case DIR_NONE: return; break;
case DIR_LEFT: --px; break;
case DIR_RIGHT: ++px; break;
case DIR_UP: --py; break;
case DIR_DOWN: ++py; break;
}
if (px > MAP_WDT-1) {
projectile_dir[id]=DIR_NONE;
return;
}
if (py > MAP_HGT-1) {
projectile_dir[id]=DIR_NONE;
return;
}
map_type = map[MAP_ADR(px, py)];
if (can_ink(map_type)) {
set_tile_palette(px, py, id+1);
}
if ((map_type >= 0x22 && map_type <= 0x29) || (map_type >= 0x32 && map_type <= 0x39)) {
projectile_dir[id]=DIR_NONE;
return;
}
projectile_cnt[id]=TILE_SIZE<<FP_BITS;
}
/**
* Player movement.
*
* Player movement is tile-by-tile with an animation between
* tiles that we need to initialize.
*/
void player_move(unsigned char id,unsigned char dir) {
unsigned char map_type = 0;
if (player_cooldown[id]) {
return;
}
/* We always set direction so sprites update facing */
player_dir[id]=dir;
/* A pad direction is being held, so animate */
player_anim_cnt[id] += 1;
/*
* Reset speed; if the last direction we check is valid, we'll set speed, but we
* shouldn't let players input a valid direction that sets speed plus a higher priority
* invalid direction to let them move in the invalid direction
*/
player_cnt[id]=0;
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 (px > MAP_WDT-1) {
return;
}
if (py > MAP_HGT-1) {
return;
}
/*
* Wall tiles:
* 0x22~0x29
* 0x32~0x39
*/
map_type = map[MAP_ADR(px, py)];
if (player_wpn[id] == WPN_ROLLER && can_ink(map_type)) {
set_tile_palette(px, py, id+1);
}
if ((map_type >= 0x22 && map_type <= 0x29) || (map_type >= 0x32 && map_type <= 0x39)) {
return;
}
player_cnt[id]=TILE_SIZE<<FP_BITS;
player_diag_flip[id]=1;
sfx_play(SFX_SPLAT,0);
}
/**
* Generate a projectile from a player.
*/
void player_make_projectile(unsigned char id) {
unsigned char map_type = 0;
if (player_cooldown[id]) return;
if (player_wpn[id] == WPN_CHARGER && player_charge[id] < CHARGE_FRAMES) {
++player_charge[id];
return;
}
projectile_dist[id] = weapon_ranges[player_wpn[id]];
projectile_dir[id] = player_dir[id];
projectile_x[id] = player_x[id];
projectile_y[id] = player_y[id];
player_cooldown[id] = weapon_cooldown[player_wpn[id]];
player_charge[id] = 0;
px=projectile_x[id]>>(TILE_SIZE_BIT+FP_BITS);
py=projectile_y[id]>>(TILE_SIZE_BIT+FP_BITS);
map_type = map[MAP_ADR(px, py)];
if (can_ink(map_type)) {
set_tile_palette(px, py, id+1);
}
}
/**
* Splat!
*/
void player_die(unsigned char id) {
/* Reset to spawn. */
player_x[id] = player_x_spawn[id];
player_y[id] = player_y_spawn[id];
player_cnt[id]=0;
player_anim_cnt[id]=0;
player_diag_flip[id]=0;
player_dir[id] = DIR_NONE;
player_wait[id] = RESPAWN_TIME;
sfx_play(SFX_DEATH,0);
}
/**
* Main gameplay loop.
*/
void game_loop(void) {
unsigned char map_type;
ppu_off();
oam_clear();
bank_bg(1);
bank_spr(1);
scroll(0,0);
/* Load map */
vram_adr(NAMETABLE_A);
vram_write(levelList[game_level*LEVEL_ENTRY],1024);
vram_adr(NAMETABLE_A+0x3c0);
vram_read(palette_state, sizeof(palette_state));
/* Clear nametable C to avoid artifacts when bouncing. */
vram_adr(NAMETABLE_C);
vram_fill(0,1024);
/* Set palette */
pal_bright(4);
pal_bg(levelList[game_level*LEVEL_ENTRY+1]);
pal_spr(levelList[game_level*LEVEL_ENTRY+2]);
player_all=0;
timer = TIMER_START;
i16=NAMETABLE_A;
ptr=0;
wait=0;
vram_adr(NAMETABLE_A);
/*
* Read through the map in VRAM to find where
* players should spawn and replace their spawn
* locations with the appropriate map tile
* for the upper left corner of a spawn point.
*/
for (i = 0; i < MAP_HGT; ++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_PLAYERA:
case TILE_PLAYERB:
player_dir [player_all] = DIR_NONE;
player_x_spawn [player_all] = (j<<3)<<FP_BITS;
player_y_spawn [player_all] = (i<<4)<<FP_BITS;
player_cnt [player_all] = 0;
player_anim_cnt [player_all] = 0;
player_diag_flip [player_all] = 0;
player_wait [player_all] = 16+((spr-TILE_PLAYERA)<<4);
player_cooldown [player_all] = 0;
player_speed [player_all] = 2<<FP_BITS;
player_charge [player_all] = 0;
projectile_cnt [player_all] = 0;
projectile_dir [player_all] = DIR_NONE;
++player_all;
wait+=16;
spr=0x2a;
break;
}
map[ptr++]=spr;
vram_put(spr);
vram_put(nameRow[j+1]);
}
i16+=64;
}
/* Store spawn locations for respawn. */
for (i = 0; i < player_all; ++i) {
player_x[i] = player_x_spawn[i];
player_y[i] = player_y_spawn[i];
}
/* Clear the update list */
memcpy(update_list,updateListData,sizeof(updateListData));
set_vram_update(update_list);
/* Start rendering. */
ppu_on_all();
game_done = FALSE;
game_paused = FALSE;
bright = 0;
frame_cnt = 0;
while (!game_done) {
/* Place sprites in the OAM for players. */
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;
}
px = player_x[i]>>FP_BITS;
anim_frame = (player_anim_cnt[i] / FRAMES_PER_STEP) % WALK_CYCLE;
switch (player_dir[i]) {
case DIR_NONE:
case DIR_DOWN: spr_dir = SPR_DOWN; break;
case DIR_LEFT: spr_dir = SPR_LEFT; break;
case DIR_RIGHT: spr_dir = SPR_RIGHT; break;
case DIR_UP: spr_dir = SPR_UP; break;
}
oam_meta_spr(
px, py, spr, SprPlayers[i][player_wpn[i]][spr_dir][anim_frame],
);
spr-=16;
}
/* Wait for the next frame. */
ppu_wait_frame();
clear_update_list();
/* Tick the timer */
++frame_cnt;
if (!game_paused && (frame_cnt % TIMER_CYCLE == 0)) {
timer--;
}
/* And update it on screen. */
insert_into_update_list(0x20,0x4f,timer/10 + 0xb0);
insert_into_update_list(0x20,0x50,timer%10 + 0xb0);
/* Read both inputs for "START" in trigger mode. */
i = pad_trigger(0);
i |= pad_trigger(1);
/* Pause the game if someone hit start. */
if (i & PAD_START) {
game_paused ^= TRUE;
if (game_paused) {
/* When the game is paused, we switch to a grayscale palette */
pal_bg(palPaused);
pal_spr(palCharPaused);
} else {
/* Switch back to level palette. */
pal_bg(levelList[game_level*LEVEL_ENTRY+1]);
pal_spr(levelList[game_level*LEVEL_ENTRY+2]);
}
music_pause(game_paused);
sfx_play(SFX_SPLAT,0);
}
/* Skip processing inputs when the game is paused. */
if (game_paused) continue;
/*
* When players spawn, they flash a bit.
* We don't start the music until they are done flashing.
*/
if (wait) {
--wait;
if (!wait) {
music_play(MUSIC_GAME);
}
}
/* When the timer reaches 0, the game is over. */
if (timer == 0) {
music_play(MUSIC_CLEAR);
game_done = TRUE;
}
/*
* Process player actions.
*/
for (i = 0; i < player_all; ++i) {
/* Flash respawning players, and prevent them from providing input. */
if (player_wait[i]) {
if (player_wait[i] == 16) {
sfx_play(SFX_RESPAWN1,0);
}
--player_wait[i];
continue;
}
if (wait) continue; /* Avoid processing input during initial spawn. */
/* This player is moving, so animate their 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];
/* Finish movement. */
if (player_cnt[i] <= 0) {
if (player_cnt[i] < 0) {
player_cnt[i] = 0;
/* Align player to grid. */
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);
}
} else {
/* Continue animating movement. */
player_anim_cnt[i] += 1;
}
}
/* No current movement, accept input. */
if (!player_cnt[i]) {
/* Read pad state. */
j = pad_state(i);
if (player_cooldown[i]) {
player_cooldown[i] -= 1;
}
if (j & player_dir[i] && player_diag_flip[i]) {
/* Give priority new new direction (useful for cornering) */
j &= ~player_dir[i];
player_diag_flip[i] = 0;
player_move(i, player_dir[i]);
}
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);
if (j&PAD_A) player_make_projectile(i); else player_charge[i] = 0;
}
/* If this player has a projectile, move it as well. */
if (projectile_cnt[i]) {
if (projectile_cnt[i] <= 0) {
if (projectile_cnt[i] < 0) {
projectile_cnt[i] = 0;
/* Projectiles also lock to tiles, like players. */
projectile_x[i]=(projectile_x[i]&0xff00)+(projectile_dir[i]==DIR_LEFT?0x100:0);
projectile_y[i]=(projectile_y[i]&0xff00)+(projectile_dir[i]==DIR_UP ?0x100:0);
}
}
}
if (!projectile_cnt[i]) {
projectile_move(i);
}
if (projectile_cnt[i]) {
switch (projectile_dir[i]) {
case DIR_RIGHT: projectile_x[i]+=projectile_speed[player_wpn[i]]; break;
case DIR_LEFT: projectile_x[i]-=projectile_speed[player_wpn[i]]; break;
case DIR_DOWN: projectile_y[i]+=projectile_speed[player_wpn[i]]; break;
case DIR_UP: projectile_y[i]-=projectile_speed[player_wpn[i]]; break;
}
projectile_cnt[i]-=projectile_speed[player_wpn[i]];
}
}
/* Kill players that collide with eachother. */
for (i = 0; i < player_all; ++i) {
for (j = i + 1; j < player_all; ++j) {
if (player_x[i] == player_x[j] && player_y[i] == player_y[j]) {
// If you have a roller, kill the player you collide with
// If both players have rollers, both of them die
if (player_wpn[i] == WPN_ROLLER) player_die(j);
if (player_wpn[j] == WPN_ROLLER) player_die(i);
}
if (projectile_dir[j] != DIR_NONE &&
(player_x[i] >> (TILE_SIZE_BIT+FP_BITS)) == (projectile_x[j] >> (TILE_SIZE_BIT+FP_BITS)) &&
(player_y[i] >> (TILE_SIZE_BIT+FP_BITS)) == (projectile_y[j] >> (TILE_SIZE_BIT+FP_BITS))) {
/* Player j kills player i using a projectile */
player_die(i);
}
if (projectile_dir[i] != DIR_NONE &&
(player_x[j] >> (TILE_SIZE_BIT+FP_BITS)) == (projectile_x[i] >> (TILE_SIZE_BIT+FP_BITS)) &&
(player_y[j] >> (TILE_SIZE_BIT+FP_BITS)) == (projectile_y[i] >> (TILE_SIZE_BIT+FP_BITS))) {
/* Player i kills player j using a projectile */
player_die(j);
}
}
}
/* Kill players that fall in water. */
for ( i = 0; i < player_all; ++i) {
px = player_x[i] >> (TILE_SIZE_BIT + FP_BITS);
py = player_y[i] >> (TILE_SIZE_BIT + FP_BITS);
map_type = map[MAP_ADR(px, py)];
/* Water tiles are 0x2c, 0x2d, 0x3c, 0x3d */
if (map_type == 0x2c || map_type == 0x2d || map_type == 0x3c || map_type == 0x3d) {
player_die(i);
}
}
}
/* Game is over, calculate score. */
for (i = 0; i < player_all; ++i) {
player_score[i] = score_player(i);
}
/* Make sure the timer says 00 */
insert_into_update_list(0x20,0x50,timer%10 + 0xb0);
ppu_wait_frame();
/* Whistle */
sfx_play(SFX_START,0);
/* Stop battling! */
for (i = 0; i < 32; i += 8) {
for (j = 10; j < 13; ++j) {
clear_update_list();
set_tile_at(NAMETABLE_A,i+0,j,' '+0x80);
set_tile_at(NAMETABLE_A,i+1,j,' '+0x80);
set_tile_at(NAMETABLE_A,i+2,j,' '+0x80);
set_tile_at(NAMETABLE_A,i+3,j,' '+0x80);
set_tile_at(NAMETABLE_A,i+4,j,' '+0x80);
set_tile_at(NAMETABLE_A,i+5,j,' '+0x80);
set_tile_at(NAMETABLE_A,i+6,j,' '+0x80);
set_tile_at(NAMETABLE_A,i+7,j,' '+0x80);
ppu_wait_frame();
}
clear_update_list();
set_tile_palette(i/2+0,5,0);
set_tile_palette(i/2+1,5,0);
set_tile_palette(i/2+2,5,0);
set_tile_palette(i/2+3,5,0);
ppu_wait_frame();
}
clear_update_list();
set_tile_at(NAMETABLE_A,13,11,'G'+0x80);
set_tile_at(NAMETABLE_A,14,11,'A'+0x80);
set_tile_at(NAMETABLE_A,15,11,'M'+0x80);
set_tile_at(NAMETABLE_A,16,11,'E'+0x80);
set_tile_at(NAMETABLE_A,17,11,'!'+0x80);
set_tile_at(NAMETABLE_A,18,11,'!'+0x80);
ppu_wait_frame();
delay(100);
reset_palette_state();
}
void main(void) {
while (1) {
/* Show the main title. */
show_title();
/* Select a map. */
show_select_map();
/* Select weapons */
show_select_weapon();
/* Start the game. */
game_loop();
/* Show the final scores. */
show_endgame();
/* Play the credits. */
show_credits();
/* Loop back to the title... */
}
}