Permalink
Cannot retrieve contributors at this time
3418 lines (2961 sloc)
72.6 KB
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // Lizard | |
| // Copyright Brad Smith 2018 | |
| // http://lizardnes.com | |
| #include <cstdlib> | |
| #include <cstring> | |
| #include "lizard_game.h" | |
| #include "lizard_ppu.h" | |
| #include "lizard_audio.h" | |
| #include "lizard_lizard.h" | |
| #include "lizard_dogs.h" | |
| #include "lizard_text.h" | |
| #include "lizard_version.h" | |
| #include "assets/export/data.h" | |
| #include "assets/export/data_music.h" | |
| #include "enums.h" | |
| // in lizard_main.cpp | |
| extern bool system_pal(); | |
| extern void system_pal_set(bool on); | |
| extern bool system_easy(); | |
| extern void system_easy_set(bool on); | |
| extern bool system_music(); | |
| extern void system_music_set(bool on); | |
| extern bool system_resume(); | |
| extern void resume_save(); | |
| extern bool system_playback(); | |
| // for log testing | |
| //#define DEBUG_MODE(x) NES_DEBUG("%s(%02X)\n",(x),zp.gamepad); | |
| #define DEBUG_MODE(x) {} | |
| namespace Game | |
| { | |
| // | |
| // NES data | |
| // | |
| struct NES_ZP zp; | |
| struct NES_STACK stack; | |
| struct NES_RAM h; | |
| struct Lizard lizard; | |
| struct Resume resume; | |
| // | |
| // Special variables, not part of NES implementation | |
| // | |
| static bool option_mode; | |
| static unsigned int game_mode; | |
| bool debug = false; | |
| bool debug_dogs = false; | |
| int debug_oam_mark = 0; // oam tiles rendered | |
| int debug_oam_loss = 0; // oam tiles discarded (overflowed limit of 64) | |
| int ntsc_phase = 0; // randomized PPU phase for NTSC colour artifact simulation | |
| bool pending_resume_save = false; // autosave during room transitions to hide load hitch | |
| // | |
| // Misc | |
| // | |
| extern const uint8 hex_to_ascii[16]; // in lizard_dogs.cpp | |
| const uint8 TIP_POINT[TIP_MAX] = { 13,17,19,23,29,31 }; | |
| uint8 prng() | |
| { | |
| // NOTE: a seed of 0 will loop to itself | |
| NES_ASSERT(zp.seed != 0, "prng in deadlock!"); | |
| if (zp.seed & 0x8000) zp.seed = (zp.seed << 1) ^ 0x00D7; | |
| else zp.seed = (zp.seed << 1); | |
| return zp.seed & 0xFF; | |
| } | |
| uint8 prng(int count) | |
| { | |
| NES_ASSERT(count > 0,"prng with count of 0"); | |
| uint8 result = 0; | |
| while (count) | |
| { | |
| result = prng(); | |
| --count; | |
| } | |
| return result; | |
| } | |
| bool coin_read(uint8 c) | |
| { | |
| NES_ASSERT(c < 128,"Coin out of range!"); | |
| uint8 pos = c >> 3; | |
| uint8 bit = c & 7; | |
| return (h.coin[pos] & (1<<bit))!=0; | |
| } | |
| void coin_take(uint8 c) | |
| { | |
| NES_ASSERT(c < 128,"Coin out of range!"); | |
| uint8 pos = c >> 3; | |
| uint8 bit = c & 7; | |
| h.coin[pos] |= (1<<bit); | |
| NES_ASSERT(coin_read(c),"coin_take() failed"); | |
| zp.coin_saved = 0; | |
| } | |
| void coin_return(uint8 c) | |
| { | |
| NES_ASSERT(c < 128,"Coin out of range!"); | |
| uint8 pos = c >> 3; | |
| uint8 bit = c & 7; | |
| h.coin[pos] &= ~uint8(1<<bit); | |
| NES_ASSERT(!coin_read(c),"coin_read() failed"); | |
| zp.coin_saved = 0; | |
| } | |
| uint8 coin_count() | |
| { | |
| uint8 count = 0; | |
| for (int i=0; i<128; ++i) | |
| { | |
| if (coin_read(i)) ++count; | |
| } | |
| return count; | |
| } | |
| bool flag_read(uint8 f) | |
| { | |
| NES_ASSERT(f < 128,"Flag out of range!"); | |
| uint8 pos = f >> 3; | |
| uint8 bit = f & 7; | |
| return (h.flag[pos] & (1<<bit))!=0; | |
| } | |
| void flag_set(uint8 f) | |
| { | |
| NES_ASSERT(f < 128,"Flag out of range!"); | |
| uint8 pos = f >> 3; | |
| uint8 bit = f & 7; | |
| h.flag[pos] |= (1<<bit); | |
| NES_ASSERT(flag_read(f),"flag_set() failed"); | |
| zp.coin_saved = 0; | |
| } | |
| void flag_clear(uint8 f) | |
| { | |
| NES_ASSERT(f < 128,"Flag out of range!"); | |
| uint8 pos = f >> 3; | |
| uint8 bit = f & 7; | |
| h.flag[pos] &= ~uint8(1<<bit); | |
| NES_ASSERT(!flag_read(f),"flag_clear() failed"); | |
| zp.coin_saved = 0; | |
| } | |
| void decimal_clear() | |
| { | |
| for (int i=0; i<5; ++i) h.decimal[i] = 0; | |
| } | |
| void decimal_add(uint8 x) | |
| { | |
| while (x > 0) | |
| { | |
| int digit = 0; | |
| if (x >= 100) | |
| { | |
| digit = 2; | |
| x -= 99; | |
| } | |
| else if (x >= 10) | |
| { | |
| digit = 1; | |
| x -= 9; | |
| } | |
| while (digit < 5) | |
| { | |
| ++h.decimal[digit]; | |
| if (h.decimal[digit] >= 10) | |
| { | |
| h.decimal[digit] = 0; | |
| } | |
| else | |
| { | |
| break; | |
| } | |
| ++digit; | |
| } | |
| if (digit >= 5) | |
| { | |
| for (int i=0; i<5; ++i) h.decimal[i] = 9; // maxed out at 99999 | |
| return; | |
| } | |
| --x; | |
| } | |
| } | |
| void decimal_add32(uint32 x) | |
| { | |
| while (x >= 256) | |
| { | |
| decimal_add(255); | |
| decimal_add(1); | |
| x -= 256; | |
| } | |
| NES_ASSERT(x < 256, "decimal_add32 failure"); | |
| decimal_add(uint8(x)); | |
| } | |
| void decimal_print() | |
| { | |
| uint8 d = 5; | |
| do | |
| { | |
| --d; | |
| if (h.decimal[d] != 0) goto finish; | |
| PPU::write(0x70); | |
| } while (d > 1); | |
| do | |
| { | |
| --d; | |
| finish: | |
| PPU::write(0x60 + h.decimal[d]); | |
| } while (d > 0); | |
| } | |
| void metric_print() | |
| { | |
| uint8 d = 5; | |
| do | |
| { | |
| --d; | |
| if (h.decimal[d] != 0) goto finish; | |
| PPU::write(0xAB); | |
| } while (d > 1); | |
| do | |
| { | |
| --d; | |
| finish: | |
| PPU::write(0xA0 + h.decimal[d]); | |
| } while (d > 0); | |
| } | |
| void dogs_cycle() | |
| { | |
| const int DOG_ADD[8] = { 1, 5, 11, 15, 7, 3, 13, 9 }; // relative primes that will cycle all dogs | |
| // properties: 1 vs 15 order flips every frame | |
| // 0 is always at 0 | |
| // 8 is always at 8 | |
| zp.dog_add_select = (zp.dog_add_select + 1) & 7; | |
| zp.dog_add = DOG_ADD[zp.dog_add_select]; | |
| } | |
| void nmi_update_at(uint8 x, uint8 y) | |
| { | |
| zp.nmi_load = 0x2000 + (32 * y) + x; | |
| } | |
| void ppu_nmi_update_row() | |
| { | |
| PPU::latch(zp.nmi_load); | |
| ppu_nmi_write_row(); | |
| } | |
| void ppu_nmi_write_row() | |
| { | |
| for (int i=0; i<32; ++i) | |
| { | |
| PPU::write(stack.nmi_update[i]); | |
| } | |
| } | |
| void ppu_nmi_update_double() | |
| { | |
| PPU::latch(zp.nmi_load); | |
| for (int i=0; i<64; ++i) PPU::write(stack.nmi_update[i]); | |
| } | |
| // | |
| // Meta sprites | |
| // | |
| void sprite_0_init() | |
| { | |
| h.oam[0] = 0xFF; // Y = offscreen | |
| h.oam[1] = 0x2F; // tile | |
| h.oam[2] = 0x20; // beneath background | |
| //h.oam[2] = 0; // for testing | |
| h.oam[3] = 247; // X position | |
| } | |
| void sprite_begin() | |
| { | |
| zp.oam_pos = 4; | |
| debug_oam_mark = 0; | |
| debug_oam_loss = 0; | |
| } | |
| // forward declarations | |
| static void sprite_add(uint8 x, uint8 y, const uint8* meta, uint8 count); | |
| static void sprite_add_flipped(uint8 x, uint8 y, const uint8* meta, uint8 count); | |
| static void sprite_add_edge(uint8 x, uint8 y, const uint8* meta, uint8 count); | |
| static void sprite_add_flipped_edge(uint8 x, uint8 y, const uint8* meta, uint8 count); | |
| void sprite_prepare(uint8 x) | |
| { | |
| zp.sprite_center = (x ^ (x >> 1)) & 0xC0; | |
| } | |
| void sprite0_add(uint8 x, uint8 y, uint8 sprite) | |
| { | |
| NES_ASSERT(sprite < DATA_sprite0_COUNT,"Sprite out of range!"); | |
| uint8 count = data_sprite0_tile_count[sprite]; | |
| zp.att_or = data_sprite0_vpal[sprite] ? (zp.dog_now & 1) : 0; | |
| NES_ASSERT(count > 0, "Empty sprite not allowed!"); | |
| const uint8* meta = data_sprite0[sprite]; | |
| sprite_add(x,y,meta,count); | |
| } | |
| void sprite1_add(uint8 x, uint8 y, uint8 sprite) | |
| { | |
| NES_ASSERT(sprite < DATA_sprite1_COUNT,"Sprite out of range!"); | |
| uint8 count = data_sprite1_tile_count[sprite]; | |
| zp.att_or = data_sprite1_vpal[sprite] ? (zp.dog_now & 1) : 0; | |
| NES_ASSERT(count > 0, "Empty sprite not allowed!"); | |
| const uint8* meta = data_sprite1[sprite]; | |
| sprite_add(x,y,meta,count); | |
| } | |
| void sprite2_add(uint8 x, uint8 y, uint8 sprite) | |
| { | |
| NES_ASSERT(sprite < DATA_sprite2_COUNT,"Sprite out of range!"); | |
| uint8 count = data_sprite2_tile_count[sprite]; | |
| zp.att_or = data_sprite2_vpal[sprite] ? (zp.dog_now & 1) : 0; | |
| NES_ASSERT(count > 0, "Empty sprite not allowed!"); | |
| const uint8* meta = data_sprite2[sprite]; | |
| sprite_add(x,y,meta,count); | |
| } | |
| void sprite0_add_flipped(uint8 x, uint8 y, uint8 sprite) | |
| { | |
| NES_ASSERT(sprite < DATA_sprite0_COUNT,"Sprite out of range!"); | |
| uint8 count = data_sprite0_tile_count[sprite]; | |
| zp.att_or = data_sprite0_vpal[sprite] ? (zp.dog_now & 1) : 0; | |
| NES_ASSERT(count > 0, "Empty sprite not allowed!"); | |
| const uint8* meta = data_sprite0[sprite]; | |
| sprite_add_flipped(x,y,meta,count); | |
| } | |
| void sprite1_add_flipped(uint8 x, uint8 y, uint8 sprite) | |
| { | |
| NES_ASSERT(sprite < DATA_sprite1_COUNT,"Sprite out of range!"); | |
| uint8 count = data_sprite1_tile_count[sprite]; | |
| zp.att_or = data_sprite1_vpal[sprite] ? (zp.dog_now & 1) : 0; | |
| NES_ASSERT(count > 0, "Empty sprite not allowed!"); | |
| const uint8* meta = data_sprite1[sprite]; | |
| sprite_add_flipped(x,y,meta,count); | |
| } | |
| void sprite2_add_flipped(uint8 x, uint8 y, uint8 sprite) | |
| { | |
| NES_ASSERT(sprite < DATA_sprite2_COUNT,"Sprite out of range!"); | |
| uint8 count = data_sprite2_tile_count[sprite]; | |
| zp.att_or = data_sprite2_vpal[sprite] ? (zp.dog_now & 1) : 0; | |
| NES_ASSERT(count > 0, "Empty sprite not allowed!"); | |
| const uint8* meta = data_sprite2[sprite]; | |
| sprite_add_flipped(x,y,meta,count); | |
| } | |
| static void sprite_add(uint8 x, uint8 y, const uint8* meta, uint8 count) | |
| { | |
| NES_ASSERT(meta, "NULL sprite!"); | |
| if (!(zp.sprite_center & 0x40)) | |
| { | |
| sprite_add_edge(x,y,meta,count); | |
| return; | |
| } | |
| while (count) | |
| { | |
| if (zp.oam_pos == 0) { debug_oam_loss += count; return; } | |
| uint8 yt = meta[0] + y; | |
| if (yt < 239) | |
| { | |
| h.oam[zp.oam_pos+0] = yt; | |
| h.oam[zp.oam_pos+1] = meta[1]; | |
| h.oam[zp.oam_pos+2] = meta[2] | zp.att_or; | |
| h.oam[zp.oam_pos+3] = meta[3] + x; | |
| zp.oam_pos += 4; | |
| } | |
| meta += 4; | |
| --count; | |
| } | |
| } | |
| static void sprite_add_edge(uint8 x, uint8 y, const uint8* meta, uint8 count) | |
| { | |
| NES_ASSERT(meta, "NULL sprite!"); | |
| while (count) | |
| { | |
| if (zp.oam_pos == 0) { debug_oam_loss += count; return; } | |
| uint8 yt = meta[0] + y; | |
| if (yt < 239) | |
| { | |
| h.oam[zp.oam_pos+0] = yt; | |
| h.oam[zp.oam_pos+1] = meta[1]; | |
| h.oam[zp.oam_pos+2] = meta[2] | zp.att_or; | |
| h.oam[zp.oam_pos+3] = meta[3] + x; | |
| if (0 != ((h.oam[zp.oam_pos+3] ^ zp.sprite_center) & 0x80)) // if x bit 7 does not match, prevent tile | |
| { | |
| h.oam[zp.oam_pos+0] = 255; | |
| } | |
| else | |
| { | |
| zp.oam_pos += 4; | |
| } | |
| } | |
| meta += 4; | |
| --count; | |
| } | |
| } | |
| void sprite_add_flipped(uint8 x, uint8 y, const uint8* meta, uint8 count) | |
| { | |
| NES_ASSERT(meta, "NULL sprite!"); | |
| if (!(zp.sprite_center & 0x40)) | |
| { | |
| sprite_add_flipped_edge(x,y,meta,count); | |
| return; | |
| } | |
| while (count) | |
| { | |
| if (zp.oam_pos == 0) { debug_oam_loss += count; return; } | |
| uint8 yt = meta[0] + y; | |
| if (yt < 239) | |
| { | |
| h.oam[zp.oam_pos+0] = yt; | |
| h.oam[zp.oam_pos+1] = meta[1]; | |
| h.oam[zp.oam_pos+2] = (meta[2] ^ 0x40) | zp.att_or; | |
| h.oam[zp.oam_pos+3] = (x-8) - meta[3]; | |
| zp.oam_pos += 4; | |
| } | |
| meta += 4; | |
| --count; | |
| } | |
| } | |
| static void sprite_add_flipped_edge(uint8 x, uint8 y, const uint8* meta, uint8 count) | |
| { | |
| NES_ASSERT(meta, "NULL sprite!"); | |
| while (count) | |
| { | |
| if (zp.oam_pos == 0) { debug_oam_loss += count; return; } | |
| uint8 yt = meta[0] + y; | |
| if (yt < 239) | |
| { | |
| h.oam[zp.oam_pos+0] = yt; | |
| h.oam[zp.oam_pos+1] = meta[1]; | |
| h.oam[zp.oam_pos+2] = (meta[2] ^ 0x40) | zp.att_or; | |
| h.oam[zp.oam_pos+3] = (x-8) - meta[3]; | |
| if (0 != ((h.oam[zp.oam_pos+3] ^ zp.sprite_center) & 0x80)) // if x bit 7 does not match, prevent tile | |
| { | |
| h.oam[zp.oam_pos+0] = 255; | |
| } | |
| else | |
| { | |
| zp.oam_pos += 4; | |
| } | |
| } | |
| meta += 4; | |
| --count; | |
| } | |
| } | |
| void sprite_tile_add(uint8 x, uint8 y, uint8 tile, uint8 att) | |
| { | |
| if (zp.oam_pos == 0) { debug_oam_loss += 1; return; } | |
| h.oam[zp.oam_pos+0] = y; | |
| h.oam[zp.oam_pos+1] = tile; | |
| h.oam[zp.oam_pos+2] = att; | |
| h.oam[zp.oam_pos+3] = x; | |
| zp.oam_pos += 4; | |
| } | |
| void sprite_tile_add_clip(uint8 x, uint8 y, uint8 tile, uint8 att) | |
| { | |
| if (x >= 64 && x < 192 && (zp.sprite_center & 0x40)) | |
| { | |
| sprite_tile_add(x,y,tile,att); | |
| return; | |
| } | |
| if (((x ^ zp.sprite_center) & 0x80) != 0) return; | |
| sprite_tile_add(x,y,tile,att); | |
| } | |
| void sprite_finish() | |
| { | |
| debug_oam_mark = zp.oam_pos / 4; | |
| if (debug_oam_mark == 0) debug_oam_mark = 64; | |
| while (zp.oam_pos != 0) | |
| { | |
| h.oam[zp.oam_pos] = 0xFF; | |
| ++zp.oam_pos; | |
| } | |
| } | |
| // copies 0-31 bytes of nmi_update to 32-63 | |
| void nmi_update_shift() | |
| { | |
| for (uint8 x=0; x<32; ++x) | |
| { | |
| stack.nmi_update[x+32] = stack.nmi_update[x]; | |
| } | |
| } | |
| void nmi_double_fill(uint8 f) | |
| { | |
| for (int i=0; i<64; ++i) | |
| { | |
| stack.nmi_update[i] = f; | |
| } | |
| } | |
| void nmi_double_scroll() | |
| { | |
| uint16 scroll = zp.scroll_x; | |
| if (h.dog_type[RIVER_SLOT] == DOG_RIVER) | |
| { | |
| scroll = ((h.dog_data[3][RIVER_SLOT] & 1) << 8) + h.dog_data[2][RIVER_SLOT]; | |
| } | |
| uint8 sp = scroll >> 3; | |
| uint8 pos = sp; | |
| if (sp < 32) // <32 shift is right to left | |
| { | |
| pos += 31; | |
| for (int i=31; i>=0; --i) | |
| { | |
| stack.nmi_update[pos] = stack.nmi_update[i]; | |
| pos = pos - 1; | |
| } | |
| pos = sp + 32; | |
| } | |
| else // >=32 shift is left to right | |
| { | |
| for (int i=0; i<32; ++i) | |
| { | |
| stack.nmi_update[pos] = stack.nmi_update[i]; | |
| pos = (pos+1) & 63; | |
| } | |
| } | |
| for (int i=0; i<32; ++i) | |
| { | |
| stack.nmi_update[pos] = 0x70; | |
| pos = (pos+1) & 63; | |
| } | |
| } | |
| // | |
| // Game modes | |
| // | |
| void set_game_mode(uint8 mode) | |
| { | |
| zp.mode_temp = 0; | |
| game_mode = mode; | |
| } | |
| uint8 get_game_mode() | |
| { | |
| return game_mode; | |
| } | |
| // forward | |
| void tick_time(); | |
| void tick_mode_title(); | |
| void tick_mode_start(); | |
| void tick_mode_play(); | |
| void tick_mode_river(); | |
| void tick_mode_fade_out(); | |
| void tick_mode_load(); | |
| void tick_mode_fade_in(); | |
| void tick_mode_talk(); | |
| void tick_mode_pause_draw(uint8 kill_line); | |
| void tick_mode_pause_in(); | |
| void tick_mode_pause(); | |
| void tick_mode_pause_out(); | |
| void tick_mode_hold(); | |
| void load_mode_ending(); | |
| void tick_mode_ending(); | |
| void tick_mode_book(); | |
| void tick_mode_soundtrack(); | |
| void tick_mode_crash(); | |
| void tick_time() | |
| { | |
| uint8 fps = system_pal() ? 50 : 60; | |
| ++h.metric_time_f; | |
| if (h.metric_time_f >= fps) | |
| { | |
| h.metric_time_f = 0; | |
| ++h.metric_time_s; | |
| if (h.metric_time_s >= 60) | |
| { | |
| h.metric_time_s = 0; | |
| ++h.metric_time_m; | |
| if (h.metric_time_m >= 60) | |
| { | |
| h.metric_time_m = 0; | |
| ++h.metric_time_h; | |
| if (h.metric_time_h > 99) | |
| { | |
| h.metric_time_h = 99; | |
| h.metric_time_m = 59; | |
| h.metric_time_s = 59; | |
| h.metric_time_f = fps - 1; | |
| } | |
| } | |
| } | |
| } | |
| } | |
| static bool mode_title_initial = true; | |
| static bool mode_title_resume = false; | |
| static bool mode_title_settings = false; | |
| static int mode_title_select_play = 0; | |
| static int mode_title_select_setting = 0; | |
| static int mode_title_setting_music = 0; | |
| static int mode_title_gamepad_last = 0; | |
| bool resume_apply(); // forward declaration | |
| static void mode_title_arrows(int position, bool invert) | |
| { | |
| NES_ASSERT(position < 2, "mode_title_select_setting invalid!"); | |
| int apos = position * 32; | |
| uint8 arrow0 = text_convert('\xAB'); | |
| uint8 arrow1 = text_convert('\xAC'); | |
| stack.nmi_update[apos+ 9] = invert ? arrow1 : arrow0; | |
| stack.nmi_update[apos+22] = invert ? arrow0 : arrow1; | |
| } | |
| void tick_mode_title() | |
| { | |
| DEBUG_MODE("title"); | |
| const uint8 TOILET_CODE = (PAD_SELECT | PAD_B); | |
| if (zp.mode_temp == 0) | |
| { | |
| mode_title_settings = false; | |
| mode_title_select_play = 0; | |
| mode_title_select_setting = 0; | |
| mode_title_setting_music = system_music() ? 1 : 0; | |
| mode_title_resume = (resume.valid & system_resume()) & (!system_playback()); | |
| mode_title_gamepad_last = zp.gamepad; | |
| zp.mode_temp = 1; // 1 = redraw mode | |
| } | |
| if (zp.mode_temp == 1) | |
| { | |
| // redraw | |
| if (!mode_title_settings) | |
| { | |
| // don't clear BETA/DEMO text for first display | |
| if (!mode_title_initial) | |
| { | |
| text_load(TEXT_BLANK); | |
| nmi_update_shift(); | |
| nmi_update_at(0,11); | |
| } | |
| ppu_nmi_update_double(); | |
| if (!mode_title_resume) | |
| { | |
| // NES cartridge style PUSH_START | |
| text_load(TEXT_BLANK); | |
| nmi_update_shift(); | |
| uint8 text_start = ((zp.gamepad&TOILET_CODE)==TOILET_CODE) ? TEXT_PUSH_SHART : TEXT_PUSH_START; | |
| text_load(text_start); | |
| nmi_update_at(0,13); | |
| ppu_nmi_update_double(); | |
| } | |
| else | |
| { | |
| // a RESUME option if you've played before | |
| text_load_meta(TEXT_META_NEW_GAME); | |
| nmi_update_shift(); | |
| text_load_meta(TEXT_META_RESUME); | |
| mode_title_arrows(mode_title_select_play,true); | |
| nmi_update_at(0,13); | |
| ppu_nmi_update_double(); | |
| } | |
| } | |
| else | |
| { | |
| // settings page | |
| text_load(TEXT_BLANK); | |
| nmi_update_shift(); | |
| text_load(TEXT_SETTINGS); | |
| nmi_update_at(0,11); | |
| ppu_nmi_update_double(); | |
| NES_ASSERT(mode_title_setting_music < 3, "mode_title_setting_music invalid"); | |
| const Game::TextEnum LINE_MUSIC[3] = { TEXT_MUSIC_OFF, TEXT_MUSIC_ON, TEXT_MUSIC_SOUNDTRACK }; | |
| uint8 line_music = LINE_MUSIC[mode_title_setting_music]; | |
| uint8 line_difficulty = system_easy() ? TEXT_DIFFICULTY_EASY : TEXT_DIFFICULTY_NORMAL; | |
| text_load(line_music); | |
| nmi_update_shift(); | |
| text_load(line_difficulty); | |
| nmi_update_at(0,13); | |
| mode_title_arrows(mode_title_select_setting,false); | |
| ppu_nmi_update_double(); | |
| } | |
| zp.mode_temp = 2; | |
| } | |
| uint8 gamepad_new = (zp.gamepad ^ mode_title_gamepad_last) & zp.gamepad; | |
| mode_title_gamepad_last = zp.gamepad; | |
| prng(4); // tick random number generator | |
| if (gamepad_new & PAD_SELECT) | |
| { | |
| mode_title_settings = !mode_title_settings; | |
| mode_title_initial = false; | |
| zp.mode_temp = 1; | |
| goto render; | |
| } | |
| if (mode_title_settings) | |
| { | |
| // settings page | |
| if (gamepad_new & (PAD_U | PAD_D)) | |
| { | |
| mode_title_select_setting ^= 1; | |
| zp.mode_temp = 1; | |
| } | |
| else if (gamepad_new & (PAD_L | PAD_R | PAD_A)) | |
| { | |
| if (mode_title_select_setting == 0) | |
| { | |
| system_easy_set(!system_easy()); | |
| zp.mode_temp = 1; | |
| } | |
| else if (mode_title_select_setting == 1) | |
| { | |
| mode_title_setting_music += (gamepad_new & (PAD_R | PAD_A)) ? 1 : -1; | |
| if (mode_title_setting_music < 0) mode_title_setting_music = 2; | |
| if (mode_title_setting_music > 2) mode_title_setting_music = 0; | |
| system_music_set(mode_title_setting_music > 0); | |
| zp.mode_temp = 1; | |
| } | |
| } | |
| else if (gamepad_new & (PAD_START | PAD_B)) | |
| { | |
| if (mode_title_setting_music == 2) | |
| { | |
| goto start_soundtrack; | |
| } | |
| else | |
| { | |
| mode_title_settings = false; | |
| zp.mode_temp = 1; | |
| } | |
| } | |
| } | |
| else if (mode_title_resume) | |
| { | |
| if (gamepad_new & (PAD_U | PAD_D)) | |
| { | |
| mode_title_select_play ^= 1; | |
| zp.mode_temp = 1; | |
| } | |
| else if (gamepad_new & PAD_START) | |
| { | |
| if (mode_title_select_play == 0) | |
| { | |
| goto resume_game; | |
| } | |
| else | |
| { | |
| goto start_game; | |
| } | |
| } | |
| } | |
| else | |
| { | |
| if (gamepad_new & PAD_START) | |
| { | |
| goto start_game; | |
| } | |
| } | |
| render: | |
| PPU::scroll_x(zp.scroll_x); | |
| PPU::overlay_y(240,false); | |
| PPU::overlay_scroll(256,0); | |
| zp.nmi_ready = NMI_READY; | |
| return; | |
| start_game: | |
| resume.valid = false; | |
| if (zp.current_room == DATA_room_start) | |
| { | |
| // remove PC only "PUSH ESCAPE FOR OPTIONS" text | |
| PPU::latch_at(0,27); | |
| room_load_partial(5); for (int i=32; i<64; ++i) PPU::write(stack.nmi_update[i]); | |
| room_load_partial(6); for (int i=32; i<64; ++i) PPU::write(stack.nmi_update[i]); | |
| room_load_partial(7); for (int i=32; i<64; ++i) PPU::write(stack.nmi_update[i]); | |
| // remove "PUSH START" text | |
| for (int i=0; i<64; ++i) stack.nmi_update[i] = 0x80; | |
| nmi_update_at(0,13); | |
| zp.nmi_ready = NMI_DOUBLE; | |
| } | |
| zp.easy = system_easy() ? 0xFF : 0x00; | |
| enable_music(system_music()); | |
| // randomly select human palette/hair for this play | |
| h.human0_hair = prng(8); | |
| h.human1_hair = prng(8); | |
| redo_human0_pal: | |
| zp.human0_pal = prng(2) & 3; | |
| if (zp.human0_pal == 0) goto redo_human0_pal; | |
| --zp.human0_pal; | |
| redo_human1_pal: | |
| h.human1_pal = prng(2) & 3; | |
| if (h.human1_pal == 0) goto redo_human1_pal; | |
| --h.human1_pal; | |
| // all dead human1 set | |
| h.human1_set[0] = 3; | |
| h.human1_set[1] = 3; | |
| h.human1_set[2] = 3; | |
| h.human1_set[3] = 3; | |
| h.human1_set[4] = 3; | |
| h.human1_set[5] = 3; | |
| // for TAS sync, hold LEFT+RIGHT to fix PRNG | |
| if ((zp.gamepad & (PAD_L | PAD_R)) == (PAD_L | PAD_R)) | |
| { | |
| zp.seed = 0x0101; | |
| zp.password[0] = 0; | |
| zp.password[1] = 0; | |
| zp.password[2] = 0; | |
| zp.password[3] = 0; | |
| zp.password[4] = 0; | |
| zp.nmi_count = 0; | |
| } | |
| set_game_mode(GAME_START); | |
| PPU::scroll_x(zp.scroll_x); | |
| PPU::overlay_y(240,false); | |
| PPU::overlay_scroll(256,0); | |
| //zp.nmi_ready = NMI_DOUBLE; | |
| return; | |
| resume_game: | |
| if (resume_apply()) | |
| { | |
| for (int i=0; i<8; ++i) zp.chr_cache[i] = 0xFF; // invalidate cache in case of eyesight change | |
| zp.dog_add = 1; // required for dogs to appear on first active frame | |
| set_game_mode(GAME_FADE_OUT); | |
| } | |
| else | |
| { | |
| // shouldn't happen | |
| // failsafe if it's an invalid state | |
| resume.valid = false; | |
| init(); | |
| } | |
| return; | |
| start_soundtrack: | |
| enable_music(true); | |
| set_game_mode(GAME_SOUNDTRACK); | |
| tick_mode_soundtrack(); | |
| return; | |
| } | |
| void tick_mode_start() | |
| { | |
| DEBUG_MODE("start"); | |
| uint8 fade = 0x00; | |
| if (zp.mode_temp == 0) | |
| { | |
| sprite_begin(); | |
| lizard_draw(); | |
| dogs_draw(); | |
| sprite_finish(); | |
| for (unsigned int i=0; i<32; ++i) h.shadow_palette[i] = h.palette[i]; | |
| fade = 0x40; | |
| } | |
| else if (zp.mode_temp == 2) fade = 0x30; | |
| else if (zp.mode_temp == 4) fade = 0x20; | |
| else if (zp.mode_temp == 6) fade = 0x10; | |
| else if (zp.mode_temp == 8) fade = 0x00; | |
| if (0 == (zp.mode_temp & 0x1)) | |
| { | |
| for (unsigned int i=16; i<24; ++i) | |
| { | |
| uint8 p = h.shadow_palette[i] - fade; | |
| if (p >= 0x40) p = 0x0F; | |
| h.palette[i] = p; | |
| } | |
| } | |
| if (zp.mode_temp >= 8) | |
| { | |
| set_game_mode(GAME_PLAY); | |
| } | |
| else | |
| ++zp.mode_temp; | |
| PPU::scroll_x(zp.scroll_x); | |
| PPU::overlay_y(240,false); | |
| PPU::overlay_scroll(256,0); | |
| zp.nmi_ready = NMI_READY; | |
| } | |
| void tick_easy() | |
| { | |
| // not yet initialized | |
| if (zp.easy == 0xFF) return; | |
| // update if changed in options | |
| if (system_easy() && zp.easy == 0) | |
| { | |
| zp.easy = 1; | |
| } | |
| else if (!system_easy() && zp.easy != 0) | |
| { | |
| zp.easy = 0; | |
| } | |
| if (zp.easy == 0) return; | |
| const uint8 EASY_FRAMES[2] = { | |
| 4, // NTSC skips 1/3 frames | |
| 6, // PAL skips 1/5 frames | |
| }; | |
| ++zp.easy; | |
| if (zp.easy >= EASY_FRAMES[system_pal() ? 1 : 0]) | |
| { | |
| zp.easy = 1; | |
| } | |
| } | |
| void tick_mode_play() | |
| { | |
| DEBUG_MODE("play"); | |
| if (zp.room_change != 0) | |
| { | |
| set_game_mode(GAME_FADE_OUT); | |
| tick_mode_fade_out(); | |
| return; | |
| } | |
| if (zp.easy == 1) goto skip_frame; | |
| if (zp.game_pause != 0) | |
| { | |
| set_game_mode(GAME_PAUSE_IN); | |
| tick_mode_pause_in(); | |
| return; | |
| } | |
| if (zp.climb_assist_time > 0) | |
| { | |
| --zp.climb_assist_time; | |
| zp.gamepad |= zp.climb_assist; | |
| } | |
| else | |
| { | |
| zp.climb_assist = 0; | |
| } | |
| lizard_tick(); | |
| if (zp.game_pause != 0) // if lizard requests pause, immediately do it | |
| { | |
| set_game_mode(GAME_PAUSE_IN); | |
| tick_mode_pause_in(); | |
| return; | |
| } | |
| dogs_tick(); | |
| skip_frame: | |
| sprite_begin(); | |
| lizard_draw(); | |
| dogs_draw(); | |
| sprite_finish(); | |
| if (h.dog_type[0] == DOG_FROB) | |
| { | |
| zp.scroll_x = h.dog_data[FROB_SCREEN][0] << 8; | |
| } | |
| tick_time(); | |
| tick_easy(); | |
| PPU::scroll_x(zp.scroll_x); | |
| PPU::overlay_y(240,false); | |
| PPU::overlay_scroll(256,0); | |
| zp.nmi_ready = zp.nmi_next; | |
| zp.nmi_next = NMI_READY; | |
| } | |
| void river_draw() | |
| { | |
| h.wave_draw_control &= 0x7F; | |
| if (h.dog_data[RIVER_OVERLAP][RIVER_SLOT] == 0) | |
| { | |
| lizard_draw(); | |
| dogs_draw(); | |
| } | |
| else | |
| { | |
| dogs_draw(); | |
| lizard_draw(); | |
| } | |
| // splash | |
| uint8 splash = h.dog_data[RIVER_SPLASH_TIME][RIVER_SLOT]; | |
| uint8 flip = h.dog_data[RIVER_SPLASH_FLIP][RIVER_SLOT]; | |
| if (splash < 16) | |
| { | |
| uint8 attrib = 0x05 | flip;; | |
| uint8 tile = 0x38 | (splash >> 3); | |
| sprite_tile_add(zp.smoke_x & 0xFF,zp.smoke_y,tile,attrib); | |
| } | |
| // draw the shadow | |
| if (lizard.dead != 0) return; | |
| uint8 dx = uint8(lizard_px); | |
| if (lizard_pz > 0) | |
| { | |
| sprite_tile_add(dx-7,lizard_py-3,0xC6,0x02); | |
| sprite_tile_add(dx-3,lizard_py-3,0xC6,0x02); | |
| } | |
| } | |
| void setup_river() | |
| { | |
| // sprite 0 | |
| h.oam[0] = 95 - 6; | |
| // clear 6 lines from the top of the sprite | |
| PPU::latch(0x12F0); | |
| for (int i=0; i<6; ++i) PPU::write(0); | |
| zp.chr_cache[4] = 0xFF; // invalidate cache to restore in the next room | |
| } | |
| void tick_mode_river() | |
| { | |
| DEBUG_MODE("river"); | |
| if (zp.room_change != 0) | |
| { | |
| set_game_mode(GAME_FADE_OUT); | |
| tick_mode_fade_out(); | |
| return; | |
| } | |
| if (zp.easy == 1) goto skip_river_frame; | |
| if (zp.game_pause != 0) | |
| { | |
| set_game_mode(GAME_PAUSE_IN); | |
| tick_mode_pause_in(); | |
| return; | |
| } | |
| lizard_tick(); | |
| if (zp.game_pause != 0) // if lizard requests pause, immediately do it | |
| { | |
| set_game_mode(GAME_PAUSE_IN); | |
| tick_mode_pause_in(); | |
| return; | |
| } | |
| h.dog_data[RIVER_OVERLAP][RIVER_SLOT] = 0; | |
| dogs_tick(); | |
| skip_river_frame: | |
| sprite_begin(); | |
| river_draw(); | |
| sprite_finish(); | |
| zp.scroll_x = | |
| h.dog_data[RIVER_SCROLL_A0][RIVER_SLOT] | | |
| (h.dog_data[RIVER_SCROLL_A1][RIVER_SLOT] << 8); | |
| PPU::scroll_x(zp.scroll_x); | |
| PPU::overlay_y(240,false); | |
| PPU::overlay_scroll(256,0); | |
| PPU::split_x( | |
| h.dog_data[RIVER_SCROLL_B0][RIVER_SLOT] | | |
| (h.dog_data[RIVER_SCROLL_B1][RIVER_SLOT] << 8), | |
| 96); | |
| tick_time(); | |
| tick_easy(); | |
| zp.nmi_ready = zp.nmi_next; | |
| zp.nmi_next = NMI_READY; | |
| } | |
| void tick_mode_fade_out() | |
| { | |
| DEBUG_MODE("fade_out"); | |
| uint8 fade = 0x00; | |
| if (zp.mode_temp == 0) | |
| { | |
| for (unsigned int i=0; i<32; ++i) h.shadow_palette[i] = h.palette[i]; | |
| fade = 0x10; | |
| } | |
| else if (zp.mode_temp == 2) fade = 0x20; | |
| else if (zp.mode_temp == 4) fade = 0x30; | |
| else if (zp.mode_temp >= 6) fade = 0x40; | |
| if (0 == (zp.mode_temp & 0x1)) | |
| { | |
| for (unsigned int i=0; i<32; ++i) | |
| { | |
| uint8 p = h.shadow_palette[i] - fade; | |
| if (p >= 0x40) p = 0x0F; | |
| h.palette[i] = p; | |
| } | |
| } | |
| if (zp.mode_temp >= 8) | |
| { | |
| set_game_mode(GAME_LOAD); | |
| } | |
| else | |
| ++zp.mode_temp; | |
| // just retain scroll from previous mode | |
| //PPU::scroll_x(0); | |
| //PPU::overlay_y(240,false); | |
| //PPU::overlay_scroll(256,0); | |
| zp.nmi_ready = NMI_READY; | |
| } | |
| extern const uint8 HAIR_6E[128]; | |
| const uint8 HAIR_6E[128] = | |
| { | |
| 0x00,0x00,0x00,0x00,0x00,0x24,0x18,0x20,0x00,0x00,0x18,0x3C,0x3C,0x18,0x00,0x1C, | |
| 0x3A,0x7F,0x6F,0x43,0x43,0x25,0x1A,0x20,0x3A,0x7F,0x7F,0x7F,0x7F,0x19,0x02,0x1C, | |
| 0x3C,0x7E,0x7E,0x42,0x42,0x24,0x18,0x20,0x3C,0x7E,0x7E,0x7E,0x7E,0x18,0x00,0x1C, | |
| 0x18,0x3C,0x66,0x62,0x42,0x66,0x7E,0x62,0x18,0x3C,0x7E,0x7E,0x7E,0x5A,0x66,0x5E, | |
| 0x1E,0x3C,0x24,0x00,0x00,0x24,0x18,0x20,0x1E,0x3C,0x3C,0x3C,0x3C,0x18,0x00,0x1C, | |
| 0x18,0x3C,0x76,0x62,0x42,0x24,0x18,0x20,0x18,0x3C,0x7E,0x7E,0x7E,0x18,0x00,0x1C, | |
| 0x00,0x18,0x24,0x00,0x00,0x24,0x18,0x20,0x00,0x18,0x3C,0x3C,0x3C,0x18,0x00,0x1C, | |
| 0x14,0x7A,0x24,0x42,0x00,0x24,0x18,0x20,0x14,0x7A,0x3C,0x7E,0x3C,0x18,0x00,0x1C, | |
| }; | |
| const uint8 HAIR_69[128] = | |
| { | |
| 0x00,0x00,0x00,0x00,0x00,0x24,0x1C,0x30,0x00,0x00,0x18,0x3C,0x3C,0x18,0x02,0x0F, | |
| 0x3A,0x7F,0x5F,0x47,0x43,0x25,0x1C,0x30,0x3A,0x7F,0x7F,0x7F,0x7F,0x19,0x02,0x0F, | |
| 0x1C,0x3E,0x7F,0x43,0x02,0x24,0x1C,0x30,0x1C,0x3E,0x7F,0x7F,0x3E,0x18,0x02,0x0F, | |
| 0x18,0x3C,0x2E,0x02,0x02,0x27,0x3D,0x30,0x18,0x3C,0x3E,0x3E,0x3E,0x1B,0x23,0x0F, | |
| 0x1C,0x38,0x2C,0x04,0x00,0x24,0x1C,0x30,0x1C,0x38,0x3C,0x3C,0x3C,0x18,0x02,0x0F, | |
| 0x18,0x3C,0x76,0x46,0x02,0x24,0x1C,0x30,0x18,0x3C,0x7E,0x7E,0x3E,0x18,0x02,0x0F, | |
| 0x00,0x18,0x2C,0x04,0x00,0x24,0x1C,0x30,0x00,0x18,0x3C,0x3C,0x3C,0x18,0x02,0x0F, | |
| 0x14,0x7A,0x2C,0x46,0x00,0x24,0x1C,0x30,0x14,0x7A,0x3C,0x7E,0x3C,0x18,0x02,0x0F, | |
| }; | |
| void hair_override() | |
| { | |
| // apply hairstyle tiles | |
| if (zp.chr_cache[5] == DATA_chr_lizard_dismount && !flag_read(FLAG_EYESIGHT)) | |
| { | |
| uint8 hair = (h.human0_hair >> 5) & 7; | |
| const uint8* tile; | |
| tile = HAIR_6E + (hair << 4); | |
| PPU::latch(0x16E0); | |
| for (int i=0; i<16; ++i) PPU::write(tile[i]); | |
| tile = HAIR_69 + (hair << 4); | |
| PPU::latch(0x1690); | |
| for (int i=0; i<16; ++i) PPU::write(tile[i]); | |
| } | |
| } | |
| void tick_mode_load() | |
| { | |
| DEBUG_MODE("load"); | |
| // TIP NOT IN DEMO | |
| zp.current_lizard = zp.next_lizard; | |
| room_load(); | |
| hair_override(); | |
| // flip lizard if walked through door | |
| if (zp.room_change == 2) | |
| { | |
| lizard.face ^= 1; | |
| } | |
| zp.room_change = 0; | |
| if (zp.ending) | |
| { | |
| load_mode_ending(); | |
| } | |
| else | |
| { | |
| sprite_begin(); | |
| if (h.dog_type[HOLD_SLOT] != DOG_HOLD_SCREEN) | |
| { | |
| lizard_draw(); | |
| } | |
| dogs_draw(); | |
| sprite_finish(); | |
| } | |
| set_game_mode(GAME_FADE_IN); | |
| } | |
| void tick_mode_fade_in() | |
| { | |
| DEBUG_MODE("fade_in"); | |
| uint8 fade = 0x00; | |
| if (zp.mode_temp == 0) | |
| { | |
| for (unsigned int i=0; i<32; ++i) h.shadow_palette[i] = h.palette[i]; | |
| fade = 0x40; | |
| } | |
| else if (zp.mode_temp == 2) fade = 0x30; | |
| else if (zp.mode_temp == 4) fade = 0x20; | |
| else if (zp.mode_temp == 6) fade = 0x10; | |
| else if (zp.mode_temp == 8) fade = 0x00; | |
| if (0 == (zp.mode_temp & 0x1)) | |
| { | |
| for (unsigned int i=0; i<32; ++i) | |
| { | |
| uint8 p = h.shadow_palette[i] - fade; | |
| if (p >= 0x40) p = 0x0F; | |
| h.palette[i] = p; | |
| } | |
| } | |
| if (zp.mode_temp >= 8) | |
| { | |
| if (zp.ending) | |
| set_game_mode(GAME_ENDING); | |
| else if (h.dog_type[RIVER_SLOT] == DOG_RIVER) | |
| set_game_mode(GAME_RIVER); | |
| else if (h.dog_type[HOLD_SLOT] == DOG_HOLD_SCREEN) | |
| set_game_mode(GAME_HOLD); | |
| else | |
| set_game_mode(GAME_PLAY); | |
| } | |
| else | |
| ++zp.mode_temp; | |
| PPU::scroll_x(zp.scroll_x); | |
| PPU::overlay_y(240,false); | |
| PPU::overlay_scroll(256,0); | |
| zp.nmi_ready = NMI_READY; | |
| } | |
| void pause_rain() | |
| { | |
| // simulate use of prng in pause_rain on the NES (updated in audio thread instead here) | |
| if (h.text_select == 2) return; // player_paused on NES but should be equivalent (player_paused belongs to audio thread here) | |
| if (h.dog_type[3] != DOG_RAIN_BOSS) return; | |
| prng(); | |
| } | |
| void tick_mode_talk() | |
| { | |
| DEBUG_MODE("talk"); | |
| if (zp.mode_temp == 0) | |
| { | |
| text_start(zp.game_message); | |
| } | |
| bool go_to_pause = false; | |
| if (zp.mode_temp < (64*3)) | |
| { | |
| if ((zp.mode_temp & 63) == 0) | |
| { | |
| if (h.text_more != 0) | |
| { | |
| text_continue(); | |
| for (int x=0; x<32; ++x) stack.scratch[x] = stack.nmi_update[x]; | |
| } | |
| else | |
| { | |
| zp.mode_temp = 192; | |
| go_to_pause = true; | |
| goto finish; | |
| } | |
| } | |
| int line = (zp.mode_temp >> 6) & 3; | |
| nmi_update_at(0,24+line); | |
| for (int x=0; x<32; ++x) stack.nmi_update[x] = stack.scratch[x]; | |
| if (zp.current_lizard != LIZARD_OF_KNOWLEDGE && | |
| lizard.dismount != 3) // prevent confound at final ending | |
| { | |
| text_confound(zp.mode_temp); | |
| } | |
| const char space = text_convert(' '); | |
| int s = (zp.mode_temp & 63) >> 1; | |
| if ((zp.mode_temp & 1) == 0 && | |
| stack.nmi_update[s] != space) | |
| { | |
| play_sound(SOUND_TALK); | |
| } | |
| for (int i=s+1; i < 32; ++i) | |
| { | |
| stack.nmi_update[i] = space; | |
| } | |
| ++zp.mode_temp; | |
| } | |
| else | |
| { | |
| finish: | |
| nmi_update_at(0,27); | |
| text_load(TEXT_UNPAUSE); | |
| go_to_pause = true; | |
| } | |
| nmi_double_scroll(); | |
| tick_mode_pause_draw(22*8); | |
| PPU::scroll_x(zp.scroll_x); | |
| uint16 overlay_scroll = zp.scroll_x; | |
| if (h.dog_type[RIVER_SLOT] == DOG_RIVER) | |
| { | |
| overlay_scroll = ((h.dog_data[3][RIVER_SLOT] & 1) << 8) + h.dog_data[2][RIVER_SLOT]; | |
| } | |
| PPU::overlay_scroll(overlay_scroll,22*8); | |
| zp.nmi_ready = NMI_WIDE; | |
| if (go_to_pause) | |
| { | |
| set_game_mode(GAME_PAUSE); | |
| } | |
| } | |
| // pause stuff | |
| void tick_mode_pause_draw(uint8 kill_line) | |
| { | |
| dogs_cycle(); // cycle draw order manually (dogs_tick is skipped) | |
| sprite_begin(); // initialize sprites for rendering | |
| // 8 hidden $2F sprites that steal sprite priority at the top of the pause overlay | |
| for (int i=0; i<8; ++i) | |
| { | |
| sprite_tile_add(252,kill_line-1,0x2F,0x21); | |
| } | |
| PPU::overlay_y(kill_line,false); | |
| PPU::overlay_scroll_y(kill_line); | |
| if (h.dog_type[RIVER_SLOT] == DOG_RIVER) | |
| { | |
| river_draw(); | |
| } | |
| else | |
| { | |
| lizard_draw(); | |
| dogs_draw(); | |
| } | |
| sprite_finish(); | |
| // kill sprites at or below the given line | |
| for (int i=1; i<64; ++i) | |
| { | |
| unsigned char & oamy = h.oam[(i*4)+0]; | |
| if (oamy >= kill_line) | |
| { | |
| oamy = 255; | |
| } | |
| } | |
| pause_rain(); | |
| } | |
| void tick_mode_pause_black_row(uint8 row, bool mid) | |
| { | |
| uint8 r = row + 22; | |
| uint8 k = (r * 8); | |
| if (mid) k = 22*8; | |
| tick_mode_pause_draw(k); | |
| nmi_double_fill(0x70); | |
| zp.nmi_load = (r << 5) + 0x2000; | |
| zp.nmi_ready = NMI_WIDE; | |
| } | |
| void tick_mode_pause_restore_row(uint8 row) | |
| { | |
| uint8 r = row + 22; | |
| uint8 k = ((r+1) * 8); | |
| tick_mode_pause_draw(k); | |
| //if (h.dog_type[0] != DOG_FROB) | |
| { | |
| room_load_partial(row); | |
| // clear valves on unpause in volcano boss | |
| if (h.dog_type[3] == DOG_RACCOON_LAUNCHER && row < 6) | |
| { | |
| uint8 i = 15 - (row / 2); | |
| NES_ASSERT(h.dog_type[i] == DOG_RACCOON_VALVE,"expected raccoon_valve in slots 13-15"); | |
| if (h.dog_data[RACCOON_VALVE_LOCK][i] > 0) | |
| { | |
| uint8 x = ((h_dog_x[i] >> 3) + 31) & 63; | |
| stack.nmi_update[x+0] = 0x80; | |
| stack.nmi_update[x+1] = 0x80; | |
| } | |
| } | |
| } | |
| //else | |
| { | |
| // NOT IN DEMO | |
| } | |
| zp.nmi_load = (r << 5) + 0x2400; | |
| zp.nmi_ready = NMI_WIDE; | |
| } | |
| void tick_mode_pause_in() | |
| { | |
| DEBUG_MODE("pause_in"); | |
| // text_select = 0 talk, text characters display gradually with a sound | |
| // text_select = 1 message, text is displayed immediately without sound | |
| // text_select = 2 just pause (music paused, show password) | |
| // game_message = text or message enum to use | |
| switch (zp.mode_temp) | |
| { | |
| // frame 0-7: black out 8 bottom rows (bottom to top) | |
| case 0: | |
| if (h.text_select == 2) // pause | |
| { | |
| pause_audio(); | |
| } | |
| // fall through | |
| case 1: | |
| case 2: | |
| case 3: | |
| case 4: | |
| case 5: | |
| case 6: | |
| case 7: | |
| tick_mode_pause_black_row(7 - zp.mode_temp, false); | |
| break; | |
| // frame 8: black out attributes | |
| case 8: | |
| for (int i=0; i<32; ++i) | |
| { | |
| stack.nmi_update[i ] = h.att_mirror[i+32]; | |
| stack.nmi_update[i+32] = h.att_mirror[i+96]; | |
| } | |
| for (int i=0; i<8; ++i) | |
| { | |
| // 23E0/27E0 - no change | |
| //stack.nmi_update[i+ 0+ 8] &= 0xFF; | |
| //stack.nmi_update[i+32+ 8] &= 0xFF; | |
| // 23E8/27E8 - black bottom half | |
| stack.nmi_update[i+ 0+ 8] &= 0x0F; | |
| stack.nmi_update[i+32+ 8] &= 0x0F; | |
| // 23F0/27F0 - black all | |
| stack.nmi_update[i+ 0+16] = 0; | |
| stack.nmi_update[i+32+16] = 0; | |
| // 23F8/27F8 - black all | |
| stack.nmi_update[i+ 0+24] = 0; | |
| stack.nmi_update[i+32+24] = 0; | |
| } | |
| tick_mode_pause_draw(22*8); | |
| zp.nmi_load = 0x23E0; | |
| zp.nmi_ready = NMI_WIDE; | |
| break; | |
| // frame 9: solid line at top | |
| case 9: | |
| nmi_double_fill(0x6F); | |
| tick_mode_pause_draw(22*8); | |
| nmi_update_at(0,22); | |
| zp.nmi_ready = NMI_WIDE; | |
| break; | |
| case 10: | |
| if (h.text_select == 2) // pause | |
| { | |
| text_load(TEXT_MY_LIZARD); | |
| } | |
| else if (h.text_select == 1) // message | |
| { | |
| text_load(zp.game_message); | |
| } | |
| else // talk | |
| { | |
| text_load(TEXT_BLANK); | |
| } | |
| nmi_double_scroll(); | |
| tick_mode_pause_draw(22*8); | |
| nmi_update_at(0,22+1); | |
| zp.nmi_ready = NMI_WIDE; | |
| break; | |
| case 11: | |
| if (h.text_select == 2) // pause | |
| { | |
| CT_ASSERT(TEXT_LIZARD_KNOWLEDGE == TEXT_LIZARD_KNOWLEDGE + LIZARD_OF_KNOWLEDGE, "Lizard name text out of order."); | |
| CT_ASSERT(TEXT_LIZARD_BOUNCE == TEXT_LIZARD_KNOWLEDGE + LIZARD_OF_BOUNCE , "Lizard name text out of order."); | |
| CT_ASSERT(TEXT_LIZARD_SWIM == TEXT_LIZARD_KNOWLEDGE + LIZARD_OF_SWIM , "Lizard name text out of order."); | |
| CT_ASSERT(TEXT_LIZARD_HEAT == TEXT_LIZARD_KNOWLEDGE + LIZARD_OF_HEAT , "Lizard name text out of order."); | |
| CT_ASSERT(TEXT_LIZARD_SURF == TEXT_LIZARD_KNOWLEDGE + LIZARD_OF_SURF , "Lizard name text out of order."); | |
| CT_ASSERT(TEXT_LIZARD_PUSH == TEXT_LIZARD_KNOWLEDGE + LIZARD_OF_PUSH , "Lizard name text out of order."); | |
| CT_ASSERT(TEXT_LIZARD_STONE == TEXT_LIZARD_KNOWLEDGE + LIZARD_OF_STONE , "Lizard name text out of order."); | |
| CT_ASSERT(TEXT_LIZARD_COFFEE == TEXT_LIZARD_KNOWLEDGE + LIZARD_OF_COFFEE , "Lizard name text out of order."); | |
| CT_ASSERT(TEXT_LIZARD_LOUNGE == TEXT_LIZARD_KNOWLEDGE + LIZARD_OF_LOUNGE , "Lizard name text out of order."); | |
| CT_ASSERT(TEXT_LIZARD_DEATH == TEXT_LIZARD_KNOWLEDGE + LIZARD_OF_DEATH , "Lizard name text out of order."); | |
| CT_ASSERT(TEXT_LIZARD_BEYOND == TEXT_LIZARD_KNOWLEDGE + LIZARD_OF_BEYOND , "Lizard name text out of order."); | |
| text_load(TEXT_LIZARD_KNOWLEDGE + zp.current_lizard); | |
| } | |
| else if (h.text_select == 1) // message | |
| { | |
| text_continue(); | |
| } | |
| else // talk | |
| { | |
| text_load(TEXT_BLANK); | |
| } | |
| nmi_double_scroll(); | |
| tick_mode_pause_draw(22*8); | |
| nmi_update_at(0,22+2); | |
| zp.nmi_ready = NMI_WIDE; | |
| break; | |
| case 12: | |
| if (h.text_select == 1) // message | |
| { | |
| text_continue(); | |
| } | |
| else // pause/talk | |
| { | |
| text_load(TEXT_BLANK); | |
| } | |
| nmi_double_scroll(); | |
| tick_mode_pause_draw(22*8); | |
| nmi_update_at(0,22+3); | |
| zp.nmi_ready = NMI_WIDE; | |
| break; | |
| case 13: | |
| if (h.text_select == 2) // pause | |
| { | |
| text_load(TEXT_PASSWORD); | |
| for (int i=0;i<sizeof(zp.password);++i) stack.nmi_update[13+i] += zp.password[i]; | |
| } | |
| else if (h.text_select == 1) // message | |
| { | |
| text_continue(); | |
| } | |
| else // talk | |
| { | |
| text_load(TEXT_BLANK); | |
| } | |
| nmi_double_scroll(); | |
| tick_mode_pause_draw(22*8); | |
| nmi_update_at(0,22+4); | |
| zp.nmi_ready = NMI_WIDE; | |
| break; | |
| case 14: | |
| if (h.text_select == 0) // talk | |
| { | |
| text_load(TEXT_BLANK); | |
| } | |
| else // pause/message | |
| { | |
| text_load(TEXT_UNPAUSE); | |
| } | |
| nmi_double_scroll(); | |
| tick_mode_pause_draw(22*8); | |
| nmi_update_at(0,22+5); | |
| zp.nmi_ready = NMI_WIDE; | |
| break; | |
| default: | |
| NES_ASSERT(false,"Unknown PAUSE_IN state!"); | |
| break; | |
| } | |
| uint16 scroll = zp.scroll_x; | |
| if (h.dog_type[RIVER_SLOT] == DOG_RIVER) | |
| { | |
| scroll = ((h.dog_data[3][RIVER_SLOT] & 1) << 8) + h.dog_data[2][RIVER_SLOT]; | |
| } | |
| PPU::overlay_scroll_x(scroll); | |
| //PPU::overlay_scroll_y set by tick_mode_pause_draw | |
| //PPU::overlay_y set by tick_mode_pause_draw | |
| ++zp.mode_temp; | |
| if (zp.mode_temp >= 15) | |
| { | |
| if (h.text_select == 0) // talk | |
| { | |
| set_game_mode(GAME_TALK); | |
| } | |
| else // pause/message | |
| { | |
| set_game_mode(GAME_PAUSE); | |
| } | |
| } | |
| } | |
| uint8 lizard_cheat_code[9] = | |
| { | |
| 0, PAD_R, | |
| 0, PAD_U, | |
| 0, PAD_D, | |
| 0, PAD_D, PAD_D|PAD_SELECT | |
| }; | |
| uint8 lizard_diagnostic_code[9] = | |
| { | |
| 0, PAD_D, | |
| 0, PAD_U, | |
| 0, PAD_L, | |
| 0, PAD_L, PAD_L|PAD_SELECT | |
| }; | |
| void tick_mode_pause() | |
| { | |
| DEBUG_MODE("pause"); | |
| if (zp.mode_temp == 0) | |
| { | |
| zp.game_pause = 0; | |
| zp.m = 0; // cheat code index | |
| zp.n = 0; // diagnostic code index | |
| if (!(zp.gamepad & PAD_START)) zp.mode_temp = 1; // wait for start release | |
| } | |
| tick_mode_pause_draw(22*8); | |
| if (zp.mode_temp > 0) | |
| { | |
| if (zp.gamepad != lizard_cheat_code[zp.m]) // look for next step in | |
| { | |
| ++zp.m; | |
| if (zp.gamepad != lizard_cheat_code[zp.m]) | |
| { | |
| zp.m = 0; | |
| } | |
| else if (zp.m >= 8) | |
| { | |
| h.metric_cheater = 1; | |
| zp.current_lizard += 1; | |
| if (zp.current_lizard >= (debug ? LIZARD_OF_COUNT : LIZARD_OF_COFFEE)) zp.current_lizard = 0; | |
| zp.next_lizard = zp.current_lizard; | |
| lizard.power = 0; | |
| play_sound(SOUND_SWITCH); | |
| for (int d=0; d<16; ++d) | |
| { | |
| if (h.dog_type[d] == DOG_SAVE_STONE) | |
| { | |
| h.dog_data[SAVE_STONE_ON][d] = 0; | |
| } | |
| } | |
| set_game_mode(GAME_PAUSE_OUT); | |
| } | |
| } | |
| if (zp.gamepad != lizard_diagnostic_code[zp.n] && zp.current_room != DATA_room_diagnostic) | |
| { | |
| ++zp.n; | |
| if (zp.gamepad != lizard_diagnostic_code[zp.n]) | |
| { | |
| zp.n = 0; | |
| } | |
| else if (zp.n >= 8) | |
| { | |
| h.metric_cheater = 1; | |
| zp.room_change = 2; | |
| zp.current_door = 0; | |
| h.diagnostic_room = zp.current_room; | |
| zp.current_room = DATA_room_diagnostic; | |
| if (h.text_select == 2) // paused | |
| { | |
| unpause_audio(); | |
| } | |
| set_game_mode(GAME_FADE_OUT); | |
| } | |
| } | |
| if (zp.gamepad & PAD_START) // start button finishes pause | |
| { | |
| set_game_mode(GAME_PAUSE_OUT); | |
| } | |
| } | |
| uint16 scroll = zp.scroll_x; | |
| if (h.dog_type[RIVER_SLOT] == DOG_RIVER) | |
| { | |
| scroll = ((h.dog_data[3][RIVER_SLOT] & 1) << 8) + h.dog_data[2][RIVER_SLOT]; | |
| } | |
| PPU::overlay_scroll_x(scroll); | |
| //PPU::overlay_scroll_y set by tick_mode_pause_draw | |
| //PPU::overlay_y set by tick_mode_pause_draw | |
| zp.nmi_ready = NMI_READY; | |
| } | |
| void tick_mode_pause_out() | |
| { | |
| DEBUG_MODE("pause_out"); | |
| switch (zp.mode_temp) | |
| { | |
| // frame 0-5: black out rows with text | |
| case 0: | |
| case 1: | |
| case 2: | |
| case 3: | |
| case 4: | |
| tick_mode_pause_black_row(5 - zp.mode_temp, true); | |
| break; | |
| case 5: | |
| tick_mode_pause_black_row(0, false); // can't use priority sprites anymore | |
| break; | |
| // frame 6: restore attributes | |
| case 6: | |
| for (int i=0; i<32; ++i) | |
| { | |
| stack.nmi_update[i ] = h.att_mirror[i+32]; | |
| stack.nmi_update[i+32] = h.att_mirror[i+96]; | |
| } | |
| tick_mode_pause_draw(22*8); | |
| zp.nmi_load = 0x23E0; | |
| zp.nmi_ready = NMI_WIDE; | |
| break; | |
| // frame 7-14: restore tiles | |
| case 7: | |
| case 8: | |
| case 9: | |
| case 10: | |
| case 11: | |
| case 12: | |
| case 13: | |
| case 14: | |
| tick_mode_pause_restore_row(zp.mode_temp - 7); | |
| break; | |
| default: | |
| NES_ASSERT(false,"Unknown PAUSE_OUT state!"); | |
| break; | |
| } | |
| uint16 scroll = zp.scroll_x; | |
| if (h.dog_type[RIVER_SLOT] == DOG_RIVER) | |
| { | |
| scroll = ((h.dog_data[3][RIVER_SLOT] & 1) << 8) + h.dog_data[2][RIVER_SLOT]; | |
| } | |
| PPU::overlay_scroll_x(scroll); | |
| //PPU::overlay_scroll_y set by tick_mode_pause_draw | |
| //PPU::overlay_y set by tick_mode_pause_draw | |
| ++zp.mode_temp; | |
| if (zp.mode_temp >= 15) | |
| { | |
| if (h.text_select == 2) // pause | |
| { | |
| unpause_audio(); | |
| } | |
| h.text_select = 0; | |
| zp.game_message = 0; | |
| if (h.end_book != 0) | |
| { | |
| set_game_mode(GAME_BOOK); | |
| } | |
| else if (h.dog_type[RIVER_SLOT] == DOG_RIVER) | |
| { | |
| set_game_mode(GAME_RIVER); | |
| } | |
| else | |
| { | |
| set_game_mode(GAME_PLAY); | |
| } | |
| } | |
| } | |
| uint8 mode_hold_frame = 0; | |
| uint8 mode_hold_seconds = 0; | |
| uint8 mode_hold_ready = 0; | |
| void tick_mode_hold() | |
| { | |
| DEBUG_MODE("hold"); | |
| NES_ASSERT(h.dog_type[HOLD_SLOT] == DOG_HOLD_SCREEN, "GAME_MODE_HOLD without hold_screen?"); | |
| if (zp.mode_temp == 0) | |
| { | |
| mode_hold_frame = 0; | |
| mode_hold_seconds = 0; | |
| mode_hold_ready = 0; | |
| zp.mode_temp = 1; | |
| } | |
| if (zp.gamepad == 0) | |
| { | |
| mode_hold_ready = 1; | |
| } | |
| if (mode_hold_ready && | |
| (zp.gamepad & (PAD_A | PAD_B | PAD_SELECT | PAD_START)) && | |
| h.dog_param[HOLD_SLOT] < 255 && | |
| (h.dog_type[TIP_SLOT] != DOG_TIP || zp.gamepad & PAD_START) // tip only responds to START | |
| ) | |
| { | |
| zp.room_change = 2; | |
| zp.current_door = 1; | |
| zp.current_room = h_door_link[zp.current_door]; | |
| if (h.dog_y[HOLD_SLOT] == 255) | |
| { | |
| zp.room_change = 1; | |
| zp.current_door = h.tip_return_door; | |
| zp.current_room = h.tip_return_room; | |
| } | |
| set_game_mode(GAME_FADE_OUT); | |
| } | |
| else if (h.dog_param[HOLD_SLOT]) | |
| { | |
| ++mode_hold_frame; | |
| if (mode_hold_frame >= 60 || (system_pal() && mode_hold_frame >= 50)) | |
| { | |
| mode_hold_frame = 0; | |
| ++mode_hold_seconds; | |
| if (mode_hold_seconds >= h.dog_param[HOLD_SLOT] && h.dog_param[HOLD_SLOT] < 255) | |
| { | |
| zp.room_change = 2; | |
| zp.current_door = 1; | |
| zp.current_room = h_door_link[zp.current_door]; | |
| if (h.dog_y[HOLD_SLOT] == 255) | |
| { | |
| zp.room_change = 1; | |
| zp.current_door = h.tip_return_door; | |
| zp.current_room = h.tip_return_room; | |
| } | |
| set_game_mode(GAME_FADE_OUT); | |
| } | |
| } | |
| } | |
| if (zp.easy != 1) | |
| { | |
| dogs_tick(); | |
| } | |
| sprite_begin(); | |
| dogs_draw(); | |
| sprite_finish(); | |
| if (h.dog_param[HOLD_SLOT] == 0) | |
| { | |
| tick_time(); | |
| } | |
| tick_easy(); | |
| PPU::scroll_x(zp.scroll_x); | |
| PPU::overlay_y(240,false); | |
| PPU::overlay_scroll(256,0); | |
| zp.nmi_ready = zp.nmi_next; | |
| zp.nmi_next = NMI_READY; | |
| } | |
| uint8 mode_ending_scroll_sub = 0; | |
| uint8 mode_ending_scroll = 0; | |
| uint8 mode_ending_row = 0; | |
| void load_mode_ending() | |
| { | |
| // setup message | |
| mode_ending_row = 0; | |
| mode_ending_scroll = 0; | |
| mode_ending_scroll_sub = 0; | |
| text_start(TEXT_CREDITS); | |
| PPU::meta_cls(); | |
| PPU::latch(0x2400); | |
| for (int i=0; i<16; ++i) | |
| { | |
| text_continue(); | |
| ppu_nmi_write_row(); | |
| } | |
| mode_ending_row = 16; | |
| // setup sprite s | |
| sprite_0_init(); | |
| h.oam[0] = 120-4; | |
| sprite_begin(); | |
| dogs_draw(); | |
| sprite_finish(); | |
| // setup palettes | |
| palette_load(4,DATA_palette_lizard0+zp.current_lizard); | |
| palette_load(5,DATA_palette_human0+zp.human0_pal); | |
| // leave 6 as in data | |
| palette_load(7,DATA_palette_human0+h.human1_pal); | |
| } | |
| void tick_mode_ending() | |
| { | |
| DEBUG_MODE("ending"); | |
| // turn off ending mode | |
| zp.ending = 0; | |
| zp.nmi_ready = NMI_READY; | |
| if (h.text_more) | |
| { | |
| ++mode_ending_scroll_sub; | |
| if (mode_ending_scroll_sub >= 4) | |
| { | |
| mode_ending_scroll_sub = 0; | |
| if (0 == (mode_ending_scroll & 7)) | |
| { | |
| text_continue(); | |
| zp.nmi_load = 0x2400 + (32 * mode_ending_row); | |
| zp.nmi_ready = NMI_ROW; | |
| ++mode_ending_row; | |
| if (mode_ending_row >= 30) mode_ending_row = 0; | |
| } | |
| ++mode_ending_scroll; | |
| if (mode_ending_scroll >= 240) mode_ending_scroll = 0; | |
| } | |
| } | |
| PPU::scroll_x(zp.scroll_x); | |
| PPU::overlay_y(120,false); | |
| PPU::overlay_scroll(256,mode_ending_scroll); | |
| if (zp.gamepad == 0) | |
| { | |
| zp.mode_temp = 1; // wait for input release | |
| } | |
| else if ((zp.gamepad == PAD_START) && (zp.mode_temp != 0)) | |
| { | |
| h.human1_hair = prng(8); | |
| redo_human1_pal: | |
| h.human1_pal = prng(2) & 3; | |
| if (h.human1_pal == 0) goto redo_human1_pal; | |
| --h.human1_pal; | |
| zp.current_lizard = LIZARD_OF_KNOWLEDGE; | |
| zp.next_lizard = LIZARD_OF_KNOWLEDGE; | |
| h.last_lizard = 0xFF; | |
| zp.current_room = DATA_room_start_again; | |
| zp.current_door = 0; | |
| lizard.face = 0; | |
| lizard.power = 0; | |
| zp.room_change = 1; | |
| set_game_mode(GAME_FADE_OUT); | |
| } | |
| } | |
| void tick_mode_book() | |
| { | |
| DEBUG_MODE("book"); | |
| NES_ASSERT(h.dog_type[BOOK_SLOT] == DOG_BOOK, "book mode without book in BOOK_SLOT?"); | |
| if (zp.easy == 1) goto skip_frame; | |
| // 1. flip book to verso if needed (fades sky to black) | |
| // 2. flip verso page to blank bage, begin flipping | |
| lizard.power = 0; | |
| if (h.dog_param[BOOK_SLOT] == 0) // recto | |
| { | |
| h.dog_param[BOOK_SLOT] = 2; // turn to verso | |
| } | |
| else if (h.dog_param[BOOK_SLOT] == 1) // verso | |
| { | |
| h.dog_param[BOOK_SLOT] = 4; // turn to blank | |
| play_sound(SOUND_TWINKLE); | |
| } | |
| else if (h.dog_param[BOOK_SLOT] == 5) // flipping | |
| { | |
| ++zp.mode_temp; | |
| if (zp.mode_temp == 0) | |
| { | |
| // begin the ending after ~20 seconds | |
| ++h.end_book; | |
| if (h.end_book >= 5) | |
| { | |
| zp.room_change = 1; | |
| zp.current_door = 1; | |
| //zp.current_room = DATA_room_end_chain0; // NOT IN DEMO | |
| zp.current_room = DATA_room_start; | |
| set_game_mode(GAME_FADE_OUT); | |
| tick_mode_fade_out(); | |
| } | |
| } | |
| } | |
| dogs_tick(); | |
| skip_frame: | |
| sprite_begin(); | |
| const uint8 p = h.dog_data[BOOK_HUMAN][BOOK_SLOT]; | |
| if (p < 64 && (prng() & 63) >= p) // random fade lizard | |
| { | |
| lizard_draw(); | |
| } | |
| dogs_draw(); | |
| sprite_finish(); | |
| // does not tick time | |
| tick_easy(); | |
| PPU::scroll_x(zp.scroll_x); | |
| PPU::overlay_y(240,false); | |
| PPU::overlay_scroll(256,0); | |
| zp.nmi_ready = zp.nmi_next; | |
| zp.nmi_next = NMI_READY; | |
| } | |
| uint8 mode_soundtrack_track = 0; | |
| void tick_mode_soundtrack() | |
| { | |
| DEBUG_MODE("soundtrack"); | |
| if (zp.mode_temp == 0) | |
| { | |
| // wipe palette, prevents 1 frame of bad colour | |
| PPU::latch(0x3F00); | |
| for (int i=0; i<32; ++i) PPU::write(0x0F); | |
| zp.current_room = DATA_room_soundtrack; | |
| room_load(); | |
| zp.mode_temp = 1; | |
| mode_soundtrack_track = 0; | |
| } | |
| if (zp.gamepad == 0) // wait for release of input | |
| { | |
| zp.mode_temp = 2; | |
| } | |
| else if (zp.mode_temp == 2) // if input was released | |
| { | |
| zp.mode_temp = 1; // wait for release after this | |
| if (zp.gamepad & PAD_SELECT) | |
| { | |
| init(); | |
| return; | |
| } | |
| else if (zp.gamepad & PAD_U) | |
| { | |
| if (mode_soundtrack_track >= 1) | |
| { | |
| --mode_soundtrack_track; | |
| play_music(mode_soundtrack_track); | |
| } | |
| } | |
| else if (zp.gamepad & PAD_D) | |
| { | |
| if (mode_soundtrack_track < 18) | |
| { | |
| ++mode_soundtrack_track; | |
| play_music(mode_soundtrack_track); | |
| } | |
| } | |
| } | |
| sprite_begin(); | |
| sprite_prepare(88); | |
| sprite2_add(88,64+(mode_soundtrack_track*8),DATA_sprite2_lizard_stand); | |
| sprite_tile_add(95,55+(mode_soundtrack_track*8),0x67,0x02); | |
| sprite_finish(); | |
| PPU::scroll_x(zp.scroll_x); | |
| PPU::overlay_y(240,false); | |
| PPU::overlay_scroll(256,0); | |
| zp.nmi_ready = NMI_READY; | |
| } | |
| void crash_text(uint8 x, uint8 y, uint8 t) | |
| { | |
| text_load(t); | |
| PPU::latch_at(x,y); | |
| ppu_nmi_write_row(); | |
| } | |
| void crash_fake_hex(uint8 x, uint8 y, const char* s) | |
| { | |
| PPU::latch_at(x,y); | |
| while (*s) | |
| { | |
| uint8 c = text_convert(*s); | |
| PPU::write(c); | |
| ++s; | |
| } | |
| } | |
| void tick_mode_crash() | |
| { | |
| DEBUG_MODE("crash"); | |
| if (zp.mode_temp == 0) | |
| { | |
| // phony BSOD (this is just an imitation of the functional NES version) | |
| flag_clear(FLAG_EYESIGHT); | |
| zp.chr_cache[1] = 0xFF; // force load of font | |
| chr_load(1,DATA_chr_font); | |
| PPU::latch(0x3F00); | |
| for (int i=0; i<8; ++i) | |
| { | |
| PPU::write(0x01); // dark blue | |
| PPU::write(0x21); // light blue | |
| PPU::write(0x31); // lightest blue | |
| PPU::write(0x30); // white | |
| } | |
| PPU::latch_at(0,6); | |
| for (int y=18;y>0;--y) | |
| for (int x=32;x>0;--x) | |
| PPU::write(0x70); | |
| // these numbers to matched the NES Lizard crash from diagnostics screen at some point | |
| crash_text(11, 7,TEXT_CRASH_CRASH); | |
| crash_text( 8, 9,TEXT_CRASH_A); crash_fake_hex(11, 9,"FE"); | |
| crash_text( 8,10,TEXT_CRASH_X); crash_fake_hex(11,10,"18"); | |
| crash_text( 8,11,TEXT_CRASH_Y); crash_fake_hex(11,11,"02"); | |
| crash_text( 8,12,TEXT_CRASH_S); crash_fake_hex(11,12,"F0"); | |
| crash_text( 8,13,TEXT_CRASH_P); crash_fake_hex(11,13,"37"); | |
| crash_text( 8,14,TEXT_CRASH_PC); crash_fake_hex(12,14,"BD41"); | |
| crash_text( 8,15,TEXT_CRASH_BANK); crash_fake_hex(14,15,"0D"); | |
| crash_fake_hex( 8,17,"6D990E03FF02BD9D"); | |
| crash_fake_hex( 8,18,"0FA4D644DA0000"); | |
| crash_text( 6,22,TEXT_CRASH_STOP); | |
| PPU::scroll_x(0); | |
| PPU::overlay_y(0,false); // hide sprites with overlay | |
| PPU::overlay_scroll(0,0); | |
| zp.nmi_ready = NMI_NONE; | |
| } | |
| if (zp.gamepad == 0) // wait for release of input | |
| { | |
| zp.mode_temp = 2; | |
| } | |
| else if (zp.mode_temp == 2) // if input was released | |
| { | |
| if (zp.gamepad == PAD_START) | |
| { | |
| // reset game | |
| init(); | |
| } | |
| } | |
| } | |
| // | |
| // Public functions | |
| // | |
| Array16 h_dog_x [16]; | |
| Array16 h_blocker_x0 [ 4]; | |
| Array16 h_blocker_x1 [ 4]; | |
| Array16 h_door_x [ 8]; | |
| Array16 h_door_link [ 8]; | |
| int array16_init() | |
| { | |
| Array16::generate(h_dog_x, h.dog_x, h.dog_xh, 16); | |
| Array16::generate(h_blocker_x0, h.blocker_x0, h.blocker_xh0, 4); | |
| Array16::generate(h_blocker_x1, h.blocker_x1, h.blocker_xh1, 4); | |
| Array16::generate(h_door_x, h.door_x, h.door_xh, 8); | |
| Array16::generate(h_door_link, h.door_link, h.door_linkh, 8); | |
| return 1; | |
| } | |
| static int array16_init_run = array16_init(); // forces initialization at startup | |
| void init() | |
| { | |
| // these are skipped by init | |
| uint16 hold_seed = zp.seed; | |
| // ZP | |
| memset(&zp,0,sizeof(zp)); | |
| memset(&lizard,0,sizeof(lizard)); // on ZP | |
| // Stack | |
| memset(&stack,0,sizeof(stack)); | |
| // RAM | |
| memset(&h,0,sizeof(h)); | |
| // Special | |
| set_option_mode(false); | |
| set_game_mode(GAME_TITLE); | |
| text_init(); | |
| for (int i=0; i<8; ++i) zp.chr_cache[i] = 0xFF; | |
| uint16 start_room = DATA_room_start; | |
| uint8 start_door = 0; | |
| uint8 start_lizard = LIZARD_OF_START; | |
| NES_ASSERT(start_lizard < LIZARD_OF_COUNT,"LIZARD_OF_START out of range!"); | |
| zp.current_room = start_room; | |
| zp.current_door = start_door; | |
| zp.current_lizard = start_lizard; | |
| zp.next_lizard = start_lizard; | |
| zp.easy = 0xFF; // overwritten when PUSH START | |
| zp.nmi_next = NMI_READY; | |
| h.last_lizard = 0xFF; | |
| h.last_lizard_save = 0xFF; | |
| h.tip_return_room = DATA_room_start; | |
| // seed it retained over reset | |
| zp.seed = hold_seed; | |
| if (zp.seed == 0) zp.seed = 1; | |
| // fill password from resume | |
| for (int i=0; i<5; ++i) zp.password[i] = resume.password[i];; | |
| uint16 nr; | |
| bool valid_password = password_read(&nr,NULL); | |
| if (!valid_password || !checkpoint(nr)) // invalid password data | |
| { | |
| password_build(); | |
| resume.valid = false; // invalid password = invalid resume | |
| } | |
| ntsc_phase = rand() % 6; // randomize colour artifact simulation in TV mode | |
| pending_resume_save = false; | |
| // wipe OAM | |
| memset(h.oam,0xFF,sizeof(h.oam)); | |
| sprite_0_init(); | |
| // wipe nametables | |
| PPU::latch(0x2000); | |
| char space = text_convert(' '); | |
| for (int i=0; i<(1024-64); ++i) PPU::write(space); | |
| for (int i=0; i<64; ++i) PPU::write(0x00); | |
| for (int i=0; i<(1024-64); ++i) PPU::write(space); | |
| for (int i=0; i<64; ++i) PPU::write(0x00); | |
| // wipe palette | |
| PPU::latch(0x3F00); | |
| for (int i=0; i<32; ++i) PPU::write(0x0F); | |
| zp.current_room = DATA_room_start; | |
| play_music(MUSIC_SILENT); | |
| room_load(); | |
| text_load(TEXT_VERSION); | |
| #if BETA | |
| stack.nmi_update[18] = hex_to_ascii[VERSION_MAJOR ]; | |
| stack.nmi_update[20] = hex_to_ascii[VERSION_MINOR ]; | |
| stack.nmi_update[22] = hex_to_ascii[VERSION_BETA ]; | |
| stack.nmi_update[24] = hex_to_ascii[VERSION_REVISED]; | |
| #endif | |
| PPU::latch_at(0,11); | |
| ppu_nmi_write_row(); | |
| text_load(TEXT_PUSH_START); | |
| PPU::latch_at(0,13); | |
| ppu_nmi_write_row(); | |
| static bool first_run = true; | |
| if (first_run) | |
| { | |
| // original 3-line version | |
| //PPU::meta_text(32+0,27,text_get(TEXT_BLANK),0); | |
| //PPU::meta_text(32+0,28,text_get(TEXT_META_OPTIONS),0); | |
| //PPU::meta_text(32+0,29,text_get(TEXT_BLANK),0); | |
| // more compact 1-line options text | |
| PPU::meta_text(32+0,29,text_get(TEXT_META_OPTIONS),0); | |
| // if they've managed to reset the game, they already pressed escape | |
| // so maybe there's no need to show this again? | |
| first_run = false; | |
| } | |
| zp.nmi_ready = NMI_READY; | |
| } | |
| void tick(uint8 input) | |
| { | |
| PPU::debug_cls(); | |
| if (option_mode) | |
| { | |
| PPU::overlay_y(0,false); | |
| PPU::split_x(0,240); | |
| return; | |
| } | |
| zp.gamepad = input; | |
| switch (game_mode) | |
| { | |
| case GAME_TITLE: tick_mode_title(); break; | |
| case GAME_START: tick_mode_start(); break; | |
| case GAME_PLAY: tick_mode_play(); break; | |
| case GAME_RIVER: tick_mode_river(); break; | |
| case GAME_FADE_OUT: tick_mode_fade_out(); break; | |
| case GAME_LOAD: tick_mode_load(); break; | |
| case GAME_FADE_IN: tick_mode_fade_in(); break; | |
| case GAME_TALK: tick_mode_talk(); break; | |
| case GAME_PAUSE_IN: tick_mode_pause_in(); break; | |
| case GAME_PAUSE: tick_mode_pause(); break; | |
| case GAME_PAUSE_OUT: tick_mode_pause_out(); break; | |
| case GAME_HOLD: tick_mode_hold(); break; | |
| case GAME_ENDING: tick_mode_ending(); break; | |
| case GAME_BOOK: tick_mode_book(); break; | |
| case GAME_SOUNDTRACK: tick_mode_soundtrack(); break; | |
| case GAME_CRASH: tick_mode_crash(); break; | |
| default: | |
| NES_ASSERT(false,"Invalid game mode!"); | |
| init(); | |
| break; | |
| } | |
| if (debug_dogs) | |
| { | |
| PPU::debug_text("OAM: %2d/%2d",debug_oam_mark,debug_oam_mark+debug_oam_loss); | |
| } | |
| ++zp.nmi_count; | |
| if (zp.nmi_ready != NMI_NONE) | |
| { | |
| // update palettes | |
| { | |
| uint8* p = h.palette; | |
| PPU::latch(0x3F00); | |
| for (int i=32; i>0; --i) | |
| { | |
| PPU::write(*p); | |
| ++p; | |
| } | |
| } | |
| // update nametable if requested | |
| if (zp.nmi_ready == NMI_ROW) | |
| { | |
| PPU::latch(zp.nmi_load); | |
| for (int i=0; i<32; ++i) PPU::write(stack.nmi_update[i]); | |
| } | |
| else if (zp.nmi_ready == NMI_DOUBLE) | |
| { | |
| PPU::latch(zp.nmi_load); | |
| for (int i=0; i<64; ++i) PPU::write(stack.nmi_update[i]); | |
| } | |
| else if (zp.nmi_ready == NMI_WIDE) | |
| { | |
| PPU::latch(zp.nmi_load); | |
| for (int i=0; i<32; ++i) PPU::write(stack.nmi_update[i]); | |
| PPU::latch(zp.nmi_load ^ 0x0400); | |
| for (int i=0; i<32; ++i) PPU::write(stack.nmi_update[i+32]); | |
| } | |
| else if (zp.nmi_ready == NMI_STREAM) | |
| { | |
| uint8 sp = 1; | |
| uint8 count = stack.nmi_update[0]; | |
| NES_ASSERT(count <= 21, "NMI_STREAM update too long."); | |
| if (count > 21) count = 21; // failsafe | |
| for (; count>0; --count) | |
| { | |
| uint16 addr = (stack.nmi_update[sp+0] << 8) | | |
| stack.nmi_update[sp+1]; | |
| PPU::latch(addr); | |
| PPU::write( stack.nmi_update[sp+2]); | |
| sp += 3; | |
| } | |
| } | |
| else if (zp.nmi_ready == NMI_OFF) | |
| { | |
| // turns of rendering on NES | |
| // needed to elminate write bus conflict while writing lots of data to the PPU | |
| } | |
| // upload sprites | |
| PPU::oam_dma(h.oam); | |
| zp.nmi_ready = NMI_NONE; | |
| } | |
| } | |
| void set_option_mode(bool o) | |
| { | |
| option_mode = o; | |
| } | |
| bool get_option_mode() | |
| { | |
| return option_mode; | |
| } | |
| bool get_suspended() | |
| { | |
| return option_mode || (game_mode == GAME_PAUSE) || (game_mode == GAME_TITLE); | |
| } | |
| void set_debug(bool d) | |
| { | |
| debug = d; | |
| } | |
| void set_debug_dogs(bool d) | |
| { | |
| debug_dogs = d; | |
| } | |
| void resume_point(bool dead) | |
| { | |
| // prevents starting a new game but dying before saving from producing a new restore point | |
| if (!dead) | |
| { | |
| resume.valid = true; | |
| } | |
| // system state | |
| resume.pal = system_pal() ? 1 : 0; | |
| resume.easy = system_easy() ? 1 : 0; | |
| // ZP state | |
| resume.continued = zp.continued; | |
| resume.seed = zp.seed; | |
| for (int i=0; i<5; ++i) resume.password[i] = zp.password[i]; | |
| resume.human0_pal = zp.human0_pal; | |
| // RAM state | |
| for (int i=0; i<16; ++i) resume.coin[i] = h.coin[i]; | |
| for (int i=0; i<16; ++i) resume.flag[i] = h.flag[i]; | |
| resume.piggy_bank = h.piggy_bank; | |
| resume.last_lizard = h.last_lizard; | |
| resume.human1_pal = h.human1_pal; | |
| resume.human0_hair = h.human0_hair; | |
| resume.human1_hair = h.human1_hair; | |
| resume.moose_text = h.moose_text; | |
| resume.moose_text_inc = h.moose_text_inc; | |
| resume.heep_text = h.heep_text; | |
| for (int i=0; i<6; ++i) resume.human1_set[i] = h.human1_set[i]; | |
| for (int i=0; i<6; ++i) resume.human1_het[i] = h.human1_het[i]; | |
| resume.metric_time_h = h.metric_time_h; | |
| resume.metric_time_m = h.metric_time_m; | |
| resume.metric_time_s = h.metric_time_s; | |
| resume.metric_time_f = h.metric_time_f; | |
| resume.metric_bones = h.metric_bones; | |
| resume.metric_jumps = h.metric_jumps; | |
| resume.metric_continue = h.metric_continue; | |
| resume.metric_cheater = h.metric_cheater; | |
| resume.frogs_fractioned = h.frogs_fractioned; | |
| resume.tip_index = h.tip_index; | |
| resume.tip_counter = h.tip_counter; | |
| // save to disk at next room transition | |
| pending_resume_save = true; | |
| } | |
| bool resume_apply() | |
| { | |
| // system state | |
| bool pal = (resume.pal != 0); | |
| bool easy = (resume.easy != 0); | |
| // use settings for PAL/Easy, don't load them | |
| //system_pal_set(pal); | |
| //system_easy_set(easy); | |
| zp.easy = system_easy() ? 1 : 0; | |
| if (resume.metric_cheater != 0) | |
| { | |
| } | |
| // ZP state | |
| zp.continued = resume.continued; | |
| zp.seed = resume.seed; | |
| for (int i=0; i<5; ++i) zp.password[i] = resume.password[i]; | |
| zp.human0_pal = resume.human0_pal; | |
| // RAM state | |
| for (int i=0; i<16; ++i) h.coin[i] = resume.coin[i]; | |
| for (int i=0; i<16; ++i) h.flag[i] = resume.flag[i]; | |
| h.piggy_bank = resume.piggy_bank; | |
| h.last_lizard = resume.last_lizard; | |
| h.human1_pal = resume.human1_pal; | |
| h.human0_hair = resume.human0_hair; | |
| h.human1_hair = resume.human1_hair; | |
| h.moose_text = resume.moose_text; | |
| h.moose_text_inc = resume.moose_text_inc; | |
| h.heep_text = resume.heep_text; | |
| for (int i=0; i<6; ++i) h.human1_set[i] = resume.human1_set[i]; | |
| for (int i=0; i<6; ++i) h.human1_het[i] = resume.human1_het[i]; | |
| h.metric_time_h = resume.metric_time_h; | |
| h.metric_time_m = resume.metric_time_m; | |
| h.metric_time_s = resume.metric_time_s; | |
| h.metric_time_f = resume.metric_time_f; | |
| h.metric_bones = resume.metric_bones; | |
| h.metric_jumps = resume.metric_jumps; | |
| h.metric_continue = resume.metric_continue; | |
| h.metric_cheater = resume.metric_cheater; | |
| h.frogs_fractioned = resume.frogs_fractioned; | |
| h.tip_index = resume.tip_index; | |
| h.tip_counter = resume.tip_counter; | |
| // convert time if FPS/Easy has changed | |
| int src_fps = resume.pal ? 50 : 60; | |
| if (resume.easy != 0) src_fps = 40; | |
| int dest_fps = system_pal() ? 50 : 60; | |
| if (system_easy()) dest_fps = 40; | |
| convert_time(src_fps, dest_fps); | |
| // apply and prepare for room change | |
| uint16 nr; | |
| uint8 nl; | |
| if (password_read(&nr,&nl)) | |
| { | |
| if (checkpoint(nr)) | |
| { | |
| zp.current_room = nr; | |
| zp.current_lizard = nl; | |
| zp.next_lizard = nl; | |
| for (int i=0; i<16; ++i) h.coin_save[i] = h.coin[i]; | |
| for (int i=0; i<16; ++i) h.flag_save[i] = h.flag[i]; | |
| h.piggy_bank_save = h.piggy_bank; | |
| h.last_lizard_save = h.last_lizard; | |
| zp.coin_saved = 1; | |
| zp.room_change = 2; | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| void convert_time(int src_fps, int dest_fps) | |
| { | |
| NES_ASSERT(src_fps >= 40 && dest_fps >=40, "invalid FPS in time conversion?"); | |
| NES_ASSERT(src_fps <= 60 && dest_fps <=60, "invalid FPS in time conversion?"); | |
| if (src_fps == dest_fps) return; | |
| uint32 total_frames = | |
| h.metric_time_h * (60 * 60 * src_fps) + | |
| h.metric_time_m * (60 * src_fps) + | |
| h.metric_time_s * src_fps + | |
| h.metric_time_f; | |
| h.metric_time_f = total_frames % dest_fps; | |
| h.metric_time_s = (total_frames / dest_fps) % 60; | |
| h.metric_time_m = (total_frames / (dest_fps * 60)) % 60; | |
| h.metric_time_h = (total_frames / (dest_fps * 60 * 60)); | |
| if (h.metric_time_h > 99) | |
| { | |
| h.metric_time_h = 99; | |
| h.metric_time_m = 59; | |
| h.metric_time_s = 59; | |
| h.metric_time_f = dest_fps-1; | |
| } | |
| else | |
| { | |
| NES_ASSERT(total_frames == ( | |
| h.metric_time_h * (60 * 60 * dest_fps) + | |
| h.metric_time_m * (60 * dest_fps) + | |
| h.metric_time_s * dest_fps + | |
| h.metric_time_f), "time conversion broken!"); | |
| } | |
| } | |
| void palette_load(unsigned int slot, unsigned int select) | |
| { | |
| slot = slot & 7; | |
| if (select > DATA_palette_COUNT) select = 0; | |
| const uint8* pal = data_palette[select]; | |
| memcpy(h.palette + (slot<<2), pal, 4); | |
| } | |
| void chr_load(unsigned int slot, unsigned int select) | |
| { | |
| if (select >= 254) return; // skip | |
| slot = slot & 7; | |
| if (select > DATA_chr_COUNT) select = 0; | |
| if (zp.chr_cache[slot] == select) return; // skip | |
| zp.chr_cache[slot] = select; | |
| PPU::latch(slot << 10); | |
| if ( | |
| !flag_read(FLAG_EYESIGHT) || | |
| select == DATA_chr_font || | |
| select == DATA_chr_metrics || | |
| (select == DATA_chr_dog_b2 && // special case for 2 rooms that stick characters into dog_b2 | |
| (zp.current_room == DATA_room_info || | |
| zp.current_room == DATA_room_ice_F_single_moose | |
| )) | |
| ) | |
| { | |
| const uint8* chr = data_chr[select]; | |
| for (int i=1024; i>0; --i) | |
| { | |
| PPU::write(*chr); | |
| ++chr; | |
| } | |
| // apply translation glyphs | |
| if (select == DATA_chr_font) | |
| { | |
| const uint8* glyphs = text_get_glyphs(); | |
| int count = 0; | |
| while (*glyphs != 0) | |
| { | |
| if (count >= 45) | |
| { | |
| NES_ASSERT(false,"text_glyphs missing terminating 0!"); | |
| return; | |
| } | |
| uint8 tile = *glyphs; | |
| NES_ASSERT(tile >= 0xC0, "glyph tile out of range."); | |
| tile = (tile & 0x3F) | 0x40; | |
| PPU::latch(tile << 4); | |
| for (int i=1; i<9; ++i) PPU::write(glyphs[i]); | |
| for (int i=1; i<9; ++i) PPU::write(glyphs[i]); | |
| glyphs += 9; | |
| ++count; | |
| } | |
| } | |
| } | |
| else | |
| { | |
| const uint8* chr = data_mini + (select*64); | |
| for (int i=64; i>0; --i) | |
| { | |
| // each byte contains 4 pixels of data, corresponding to a 2x2 tile (x2 bitplanes) | |
| // | |
| // Packed: ABCDEFGH | |
| // | |
| // Unpacked: | |
| // | |
| // AE <- bitplane 0 | |
| // BF | |
| // | |
| // CG <- bitplane 1 | |
| // DH | |
| uint8 c = *chr; | |
| for (int j=4; j>0; --j) | |
| { | |
| // select 1 of 4 bit pairs and duplicate bit across nibble | |
| uint8 cs = c & 0x88; | |
| cs |= cs >> 1; | |
| cs |= cs >> 1; | |
| cs |= cs >> 1; | |
| // write 4 rows of the same pixels to CHR tile | |
| PPU::write(cs); | |
| PPU::write(cs); | |
| PPU::write(cs); | |
| PPU::write(cs); | |
| c <<= 1; | |
| } | |
| ++chr; | |
| } | |
| // fix sprite 0 tile $2F (don't let it be corrupted by eyesight) | |
| if (select == DATA_chr_lizard) | |
| { | |
| PPU::latch(0x12F0); | |
| for (int i=0;i<8;++i) | |
| { | |
| PPU::write(0x03); | |
| } | |
| } | |
| } | |
| } | |
| static void collide_set_row(int nmt, uint8 y, const uint8* row) | |
| { | |
| uint8 pos = y * 8; | |
| for (int i=0; i<32; i += 4) | |
| { | |
| uint8 c = 0; | |
| if (nmt == 0) | |
| { | |
| if (row[i+0] & 0x80) c |= 0x01; | |
| if (row[i+1] & 0x80) c |= 0x02; | |
| if (row[i+2] & 0x80) c |= 0x04; | |
| if (row[i+3] & 0x80) c |= 0x08; | |
| } | |
| else | |
| { | |
| if (row[i+0] & 0x80) c |= 0x80; | |
| if (row[i+1] & 0x80) c |= 0x40; | |
| if (row[i+2] & 0x80) c |= 0x20; | |
| if (row[i+3] & 0x80) c |= 0x10; | |
| } | |
| h.collision[pos] |= c; | |
| ++pos; | |
| } | |
| } | |
| // remember the last room for partial restore after pause | |
| const unsigned int UNPACKED_SIZE = 2048 + (5 * 32); | |
| static uint8 unpacked[UNPACKED_SIZE]; | |
| void room_load() | |
| { | |
| if (pending_resume_save) | |
| { | |
| if (system_resume()) | |
| { | |
| resume_save(); | |
| } | |
| pending_resume_save = false; | |
| } | |
| unsigned int select = zp.current_room; | |
| if (select > DATA_room_COUNT) select = 0; | |
| const uint8* packed = data_room[select]; | |
| unsigned int packed_pos = 0; | |
| unsigned int unpacked_pos = 0; | |
| while (packed_pos < data_room_packed_size[select] && unpacked_pos < UNPACKED_SIZE) | |
| { | |
| uint8 v = packed[packed_pos]; ++packed_pos; | |
| if (v != 255) | |
| { | |
| unpacked[unpacked_pos] = v; ++unpacked_pos; | |
| } | |
| else // RLE | |
| { | |
| if ((packed_pos + 2) > data_room_packed_size[select]) break; // this is an error, unpacked length does not match | |
| uint8 run = packed[packed_pos]; ++packed_pos; | |
| v = packed[packed_pos]; ++packed_pos; | |
| NES_ASSERT(run > 0, "Room data RLE run of 0, probable corrupt data."); | |
| while (run > 0 && unpacked_pos < UNPACKED_SIZE) | |
| { | |
| unpacked[unpacked_pos] = v; ++unpacked_pos; | |
| --run; | |
| } | |
| } | |
| } | |
| NES_ASSERT(packed_pos == data_room_packed_size[select],"Room packing size mismatch."); | |
| NES_ASSERT(unpacked_pos == UNPACKED_SIZE,"Room unpacking size mismatch."); | |
| // to assist decompression | |
| #define DEBUG_DECOMPRESS 0 | |
| #if DEBUG_DECOMPRESS | |
| NES_DEBUG("Debug Room Unpack: %04X\n",select); | |
| NES_DEBUG("Offset: %02X%02X\n", unpacked[1], unpacked[2]); | |
| for (int pos=2;pos<UNPACKED_SIZE;pos+=32) | |
| { | |
| NES_DEBUG("Row %02X: ",pos/32); | |
| for (int p=0;p<32;++p) | |
| { | |
| NES_DEBUG("%02X ",unpacked[pos+p]); | |
| } | |
| NES_DEBUG("\n"); | |
| } | |
| #endif | |
| unsigned int pos = 0; | |
| const uint8* nmt_block_0 = unpacked + pos; | |
| pos += (64 * 8); | |
| const uint8* room_palette = unpacked + pos + 0; | |
| const uint8* room_chr = unpacked + pos + 8; | |
| const uint8 room_water = unpacked[ pos + 16]; | |
| const uint8 room_music = unpacked[ pos + 17]; | |
| const uint8 room_scrolling = unpacked[ pos + 18]; | |
| // 13 bytes of padding = unpacked pos + 19; | |
| pos += 32; | |
| const uint8* room_door_x0 = unpacked + pos + 0; | |
| const uint8* room_door_x1 = unpacked + pos + 8; | |
| const uint8* room_door_link0 = unpacked + pos + 16; | |
| const uint8* room_door_link1 = unpacked + pos + 24; | |
| pos += 32; | |
| const uint8* room_door_y = unpacked + pos + 0; | |
| // 8 bytes of padding = unpacked + pos + 8; | |
| const uint8* room_dog_type = unpacked + pos + 16; | |
| pos += 32; | |
| const uint8* room_dog_x0 = unpacked + pos + 0; | |
| const uint8* room_dog_x1 = unpacked + pos + 16; | |
| pos += 32; | |
| const uint8* room_dog_y = unpacked + pos + 0; | |
| const uint8* room_dog_param = unpacked + pos + 16; | |
| pos += 32; | |
| const uint8* nmt_block_1 = unpacked + pos; | |
| pos += (32 * 22 * 2); | |
| // empty collision, solid rows 30/31 | |
| memset(h.collision+0x00,0x00,0xF0); | |
| memset(h.collision+0xF0,0xFF,0x10); | |
| // block 0 | |
| // bottom 8 rows of nametable interleaved | |
| pos = 0; | |
| for (int j=0;j<8;++j) | |
| { | |
| PPU::latch(0x22C0 + (j * 32)); | |
| collide_set_row(0, j+22, nmt_block_0 + pos); | |
| for (int i=0; i<32; ++i) { PPU::write(nmt_block_0[pos]); ++pos;} | |
| PPU::latch(0x26C0 + (j * 32)); | |
| collide_set_row(1, j+22, nmt_block_0 + pos); | |
| for (int i=0; i<32; ++i) { PPU::write(nmt_block_0[pos]); ++pos;} | |
| } | |
| // block 1 | |
| // left nametable, top 22 rows | |
| pos = 0; | |
| PPU::latch(0x2000); | |
| for (int y=0; y<22; ++y) | |
| { | |
| collide_set_row(0, y, nmt_block_1 + pos); | |
| for (int x=0; x<32; ++x) | |
| { | |
| PPU::write(nmt_block_1[pos]); | |
| ++pos; | |
| } | |
| } | |
| // right nametable, top 22 rows | |
| PPU::latch(0x2400); | |
| for (int y=0; y<22; ++y) | |
| { | |
| collide_set_row(1, y, nmt_block_1 + pos); | |
| for (int x=0; x<32; ++x) | |
| { | |
| PPU::write(nmt_block_1[pos]); | |
| ++pos; | |
| } | |
| } | |
| // attributes | |
| uint8 att_pos = 0; | |
| PPU::latch(0x23C0); | |
| for (int i=0; i<64; ++i) | |
| { | |
| PPU::write(nmt_block_1[pos]); | |
| h.att_mirror[att_pos] = nmt_block_1[pos]; | |
| ++att_pos; | |
| ++pos; | |
| } | |
| PPU::latch(0x27C0); | |
| for (int i=0; i<64; ++i) | |
| { | |
| PPU::write(nmt_block_1[pos]); | |
| h.att_mirror[att_pos] = nmt_block_1[pos]; | |
| ++att_pos; | |
| ++pos; | |
| } | |
| sprite_0_init(); | |
| play_music(room_music); | |
| // reset scroll parameters | |
| PPU::scroll_x(zp.scroll_x); | |
| PPU::overlay_y(240,false); | |
| PPU::overlay_scroll(256,0); | |
| // load other GPU data | |
| for (int i=0;i<8;++i) palette_load(i,room_palette[i]); | |
| for (int i=0;i<8;++i) chr_load(i,room_chr[i]); | |
| zp.water = room_water; | |
| zp.room_scrolling = room_scrolling; | |
| // doors | |
| for (int d=0; d<8; ++d) | |
| { | |
| h_door_x[d] = (room_door_x0[d] << 8) | room_door_x1[d]; | |
| h.door_y[d] = room_door_y[d]; | |
| h_door_link[d] = (room_door_link0[d] << 8 ) | room_door_link1[d]; | |
| } | |
| lizard_init(h_door_x[zp.current_door],h.door_y[zp.current_door]); | |
| // dogs | |
| // setup all dog params/types before init (some try to correlate thing in init) | |
| for (int d=0; d<16; ++d) | |
| { | |
| h_dog_x[d] = (room_dog_x0[d] << 8) | room_dog_x1[d]; | |
| h.dog_y[d] = room_dog_y[d]; | |
| h.dog_param[d] = room_dog_param[d]; | |
| h.dog_type[d] = room_dog_type[d]; | |
| for (int i=0; i<14; ++i) h.dog_data[i][d] = 0; | |
| } | |
| // clear blockers before dog_init (some dogs will use the blockers) | |
| for (int i=0; i<4; ++i) | |
| { | |
| empty_blocker(i); | |
| } | |
| zp.nmi_next = NMI_READY; // cancel any pending NMI updates | |
| h.boss_talk = 0; // less code to do it here than with every boss | |
| h.text_select = 0; | |
| play_bg_noise(0,0); | |
| // initialize dogs | |
| for (int d=0; d<16; ++d) | |
| { | |
| dog_init(d); | |
| } | |
| lizard_fall_test(); // dogs could set up blockers, so fall test is after | |
| if (zp.seed == 0) | |
| { | |
| // should never happen, but a fallback just in case to keep the PRNG alive | |
| zp.seed = zp.nmi_count | 0x0100; | |
| } | |
| if (h.dog_type[RIVER_SLOT] == DOG_RIVER) | |
| { | |
| setup_river(); | |
| } | |
| // disable split, only mode_river/ending will re-enable it | |
| PPU::split_x(0,240); | |
| } | |
| void room_load_partial(uint8 row) | |
| { | |
| NES_ASSERT(row < 8,"room_load_partial row out of range!"); | |
| unsigned int offset = 0; | |
| offset += row * 64; | |
| for (int i=0; i<32; ++i) | |
| { | |
| stack.nmi_update[i+32] = unpacked[offset+i+ 0]; | |
| stack.nmi_update[i+ 0] = unpacked[offset+i+32]; | |
| } | |
| } | |
| // | |
| // Collision | |
| // | |
| void collide_set_tile(uint8 x, uint8 y) | |
| { | |
| NES_ASSERT(y < 32, "collide_set_tile off bottom of screen!"); | |
| unsigned int cidx = (y << 3) | ((x & 31) >> 2); | |
| if (x < 32) | |
| { | |
| uint8 bit = 1 << (x & 3); | |
| h.collision[cidx & 255] |= bit; | |
| } | |
| else if (x < 64) | |
| { | |
| uint8 bit = 0x80 >> (x & 3); | |
| h.collision[cidx & 255] |= bit; | |
| } | |
| } | |
| void collide_clear_tile(uint8 x, uint8 y) | |
| { | |
| NES_ASSERT(y < 32, "collide_clear_tile off bottom of screen!"); | |
| unsigned int cidx = (y << 3) | ((x & 31) >> 2); | |
| if (x < 32) | |
| { | |
| uint8 bit = 1 << (x & 3); | |
| h.collision[cidx & 255] &= (bit ^ 0xFF); | |
| } | |
| else if (x < 64) | |
| { | |
| uint8 bit = 0x80 >> (x & 3); | |
| h.collision[cidx & 255] &= (bit ^ 0xFF); | |
| } | |
| } | |
| bool collide_tile(uint16 x, uint8 y) | |
| { | |
| uint8 tx = (x & 255) >> 3; | |
| uint8 ty = y >> 3; | |
| unsigned int cidx = (ty << 3) | (tx >> 2); | |
| bool r = true; | |
| if (x & 0x100) | |
| { | |
| switch (tx & 3) | |
| { | |
| case 0: r = 0 != (h.collision[cidx&255] & 0x80); break; | |
| case 1: r = 0 != (h.collision[cidx&255] & 0x40); break; | |
| case 2: r = 0 != (h.collision[cidx&255] & 0x20); break; | |
| case 3: r = 0 != (h.collision[cidx&255] & 0x10); break; | |
| default: break; | |
| } | |
| } | |
| else | |
| { | |
| switch (tx & 3) | |
| { | |
| case 0: r = 0 != (h.collision[cidx&255] & 0x01); break; | |
| case 1: r = 0 != (h.collision[cidx&255] & 0x02); break; | |
| case 2: r = 0 != (h.collision[cidx&255] & 0x04); break; | |
| case 3: r = 0 != (h.collision[cidx&255] & 0x08); break; | |
| default: break; | |
| } | |
| } | |
| //NES_DEBUG("collide_tile(%d, %d) = %d\n",x,y,r); | |
| return r; | |
| } | |
| bool collide_blocker(uint8 i, uint16 x, uint8 y) | |
| { | |
| bool r = false; | |
| if ( x >= h_blocker_x0[i] && | |
| x <= h_blocker_x1[i] && | |
| y >= h.blocker_y0[i] && | |
| y <= h.blocker_y1[i] ) | |
| { | |
| r = true; | |
| } | |
| //NES_DEBUG("collide_blocker(%d, %d, %d) = %d\n",i,x,y,r); | |
| return r; | |
| } | |
| bool collide_all(uint16 x, uint8 y) | |
| { | |
| if (collide_tile(x,y)) return true; | |
| if (collide_blocker(0,x,y)) return true; | |
| if (collide_blocker(1,x,y)) return true; | |
| if (collide_blocker(2,x,y)) return true; | |
| if (collide_blocker(3,x,y)) return true; | |
| return false; | |
| } | |
| uint8 collide_tile_left(uint16 x, uint8 y) | |
| { | |
| uint8 shift = 0; | |
| if (collide_tile(x,y)) | |
| { | |
| uint16 new_x = (x & ~0x7) + 8; | |
| shift += (new_x - x); | |
| x = new_x; | |
| } | |
| return shift; | |
| } | |
| uint8 collide_tile_right(uint16 x, uint8 y) | |
| { | |
| uint8 shift = 0; | |
| if (collide_tile(x,y)) | |
| { | |
| uint16 new_x = (x & ~0x7) - 1; | |
| shift += (x - new_x); | |
| x = new_x; | |
| } | |
| return shift; | |
| } | |
| uint8 collide_tile_up(uint16 x, uint8 y) | |
| { | |
| uint8 shift = 0; | |
| if (collide_tile(x,y)) | |
| { | |
| uint8 new_y = (y & ~0x7) + 8; | |
| shift += (new_y - y); | |
| y = new_y; | |
| } | |
| return shift; | |
| } | |
| uint8 collide_tile_down(uint16 x, uint8 y) | |
| { | |
| uint8 shift = 0; | |
| if (collide_tile(x,y)) | |
| { | |
| uint8 new_y = (y & ~0x7) - 1; | |
| shift += (y - new_y); | |
| y = new_y; | |
| } | |
| return shift; | |
| } | |
| uint8 collide_all_left(uint16 x, uint8 y) | |
| { | |
| uint8 shift = 0; | |
| for (int i=0; i<4; ++i) | |
| { | |
| if (collide_blocker(i,x,y)) | |
| { | |
| uint16 new_x = h_blocker_x1[i] + 1; | |
| shift += (new_x - x); | |
| x = new_x; | |
| } | |
| } | |
| if (collide_tile(x,y)) | |
| { | |
| uint16 new_x = (x & ~0x7) + 8; | |
| shift += (new_x - x); | |
| x = new_x; | |
| } | |
| return shift; | |
| } | |
| uint8 collide_all_right(uint16 x, uint8 y) | |
| { | |
| uint8 shift = 0; | |
| for (int i=0; i<4; ++i) | |
| { | |
| if (collide_blocker(i,x,y)) | |
| { | |
| uint16 new_x = h_blocker_x0[i] - 1; | |
| shift += (x - new_x); | |
| x = new_x; | |
| } | |
| } | |
| if (collide_tile(x,y)) | |
| { | |
| uint16 new_x = (x & ~0x7) - 1; | |
| shift += (x - new_x); | |
| x = new_x; | |
| } | |
| return shift; | |
| } | |
| uint8 collide_all_up(uint16 x, uint8 y) | |
| { | |
| uint8 shift = 0; | |
| for (int i=0; i<4; ++i) | |
| { | |
| if (collide_blocker(i,x,y)) | |
| { | |
| uint8 new_y = h.blocker_y1[i] + 1; | |
| shift += (new_y - y); | |
| y = new_y; | |
| } | |
| } | |
| if (collide_tile(x,y)) | |
| { | |
| uint8 new_y = (y & ~0x7) + 8; | |
| shift += (new_y - y); | |
| y = new_y; | |
| } | |
| return shift; | |
| } | |
| uint8 collide_all_down(uint16 x, uint8 y) | |
| { | |
| uint8 shift = 0; | |
| for (int i=0; i<4; ++i) | |
| { | |
| if (collide_blocker(i,x,y)) | |
| { | |
| uint8 new_y = h.blocker_y0[i] - 1; | |
| shift += (y - new_y); | |
| y = new_y; | |
| } | |
| } | |
| if (collide_tile(x,y)) | |
| { | |
| uint8 new_y = (y & ~0x7) - 1; | |
| shift += (y - new_y); | |
| y = new_y; | |
| } | |
| return shift; | |
| } | |
| // | |
| // Password stuff | |
| // | |
| void password_scramble() | |
| { | |
| zp.password[0] ^= 0x2; | |
| zp.password[1] ^= 0x1; | |
| zp.password[3] ^= 0x6; | |
| zp.password[4] ^= 0x5; | |
| } | |
| void password_build() | |
| { | |
| // password is combination of: | |
| // current_room : 9 bits | |
| // current_lizard : 4 bits | |
| // parity : 2 bits | |
| // packed as 5 3-bit values | |
| // bit 0 bit 1 bit 2 | |
| zp.password[4] = ((zp.current_room )&1) | ((zp.current_room>>3)&2) | ((zp.current_lizard<<2)&4) ; | |
| zp.password[3] = ((zp.current_room>>1)&1) | ((zp.current_room>>4)&2) | ((zp.current_lizard<<1)&4) ; | |
| zp.password[1] = ((zp.current_room>>2)&1) | ((zp.current_room>>5)&2) | ((zp.current_lizard )&4) ; | |
| zp.password[0] = ((zp.current_room>>3)&1) | ((zp.current_room>>6)&2) | ((zp.current_lizard>>1)&4) ; | |
| zp.password[2] = ((zp.current_room>>8)&1) ; | |
| zp.password[2] |= (zp.password[4] ^ zp.password[3] ^ zp.password[1] ^ zp.password[0]) & 0x6; // parity | |
| password_scramble(); // scramble | |
| #ifdef _DEBUG | |
| { | |
| uint16 rr; | |
| uint8 rl; | |
| NES_ASSERT(password_read(&rr,&rl),"Built password is invalid."); | |
| NES_ASSERT(rr==zp.current_room && rl==zp.current_lizard,"Built password is corrupt."); | |
| } | |
| #endif | |
| } | |
| bool password_read(uint16* read_room, uint8* read_lizard) | |
| { | |
| password_scramble(); // unscramble | |
| // check parity | |
| uint8 parity = (zp.password[4] ^ zp.password[3] ^ zp.password[1] ^ zp.password[0]) & 0x6; | |
| if ((zp.password[2] & 0x6) != parity) | |
| { | |
| password_scramble(); // rescramble | |
| return false; | |
| } | |
| uint16 rr; | |
| uint8 rl; | |
| rr = ((zp.password[4]&1) ) | | |
| ((zp.password[3]&1)<<1) | | |
| ((zp.password[1]&1)<<2) | | |
| ((zp.password[0]&1)<<3) | | |
| ((zp.password[4]&2)<<3) | | |
| ((zp.password[3]&2)<<4) | | |
| ((zp.password[1]&2)<<5) | | |
| ((zp.password[0]&2)<<6) | | |
| ((zp.password[2]&1)<<8) ; | |
| rl = ((zp.password[4]&4)>>2) | | |
| ((zp.password[3]&4)>>1) | | |
| ((zp.password[1]&4) ) | | |
| ((zp.password[0]&4)<<1) ; | |
| password_scramble(); // rescramble | |
| // check ranges | |
| if (rr >= DATA_room_COUNT) return false; // invalid room | |
| if (rl >= LIZARD_OF_COUNT) return false; // invalid lizard | |
| if (read_room) *read_room = rr; | |
| if (read_lizard) *read_lizard = rl; | |
| return true; | |
| } | |
| bool checkpoint(uint16 room) | |
| { | |
| for (int c=0; c < DATA_checkpoints_COUNT; ++c) | |
| { | |
| if (room == data_checkpoints[c]) | |
| { | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| } // namespace Game | |
| // C++ string helpers | |
| char* string_cpy(char* dest, const char* src, unsigned int len) | |
| { | |
| dest[len-1] = 0; | |
| return ::strncpy(dest,src,len-1); | |
| } | |
| char* string_cat(char* dest, const char* src, unsigned int len) | |
| { | |
| dest[len-1] = 0; | |
| return ::strncat(dest,src,len-1); | |
| } | |
| char char_lower(char x) | |
| { | |
| return (x >= 'A' && x <= 'Z') ? | |
| x - ('A' - 'a') : | |
| x; | |
| } | |
| bool string_less(const char* a, const char* b) | |
| { | |
| char ca = char_lower(*a); | |
| char cb = char_lower(*b); | |
| while (ca == cb) | |
| { | |
| if (ca == 0) return false; | |
| ++a; | |
| ++b; | |
| ca = char_lower(*a); | |
| cb = char_lower(*b); | |
| } | |
| return ca < cb; | |
| } | |
| unsigned int string_len(const char* src) | |
| { | |
| return ::strlen(src); | |
| } | |
| int string_icmp(const char* s1, const char* s2) | |
| { | |
| #ifdef __GNUC__ | |
| return strcasecmp(s1,s2); | |
| #else | |
| return stricmp(s1,s2); | |
| #endif | |
| } | |
| // end of file |