mirror of
https://github.com/sehugg/8bitworkshop.git
synced 2026-10-25 15:05:25 +00:00
1466 lines
40 KiB
C
1466 lines
40 KiB
C
/*
|
||
* Chase for Game Boy — port of Shiru's NES Chase (presets/nes/chase).
|
||
*
|
||
* Level maps match the NES nametables exactly (cropped to content).
|
||
* Camera scrolls only when a level exceeds the 160×136 playfield.
|
||
* Title bounce matches NES title_screen() physics.
|
||
* Music/SFX transcribed from the NES FamiTone data (presets/nes/chase/music.s).
|
||
* Graphics from presets/nes/chase/tileset.chr
|
||
* (regenerate with: python3 scripts/gen_chase_nes_gfx.py).
|
||
*
|
||
* Controls: D-pad move, Start = pause, A = start / continue.
|
||
*/
|
||
|
||
#include <stdint.h>
|
||
#include <string.h>
|
||
#include "gb/types.h"
|
||
#include "gb/hardware.h"
|
||
#include "gb/gb.h"
|
||
#include "gb/cgb.h"
|
||
|
||
typedef uint8_t byte;
|
||
typedef uint16_t word;
|
||
|
||
#define MAP_W_MAX 16
|
||
#define MAP_H_MAX 13
|
||
#define LEVELS_FIXED 5 /* hand-authored NES layouts */
|
||
#define ACTORS_MAX 4
|
||
|
||
#define TILE_PX 16
|
||
#define FP_BITS 4
|
||
#define TILE_TO_POS(t) ((word)(t) << (4 + FP_BITS))
|
||
#define POS_TO_TILE(p) ((byte)((p) >> (4 + FP_BITS)))
|
||
#define POS_SNAP_MASK 0xff00
|
||
|
||
/* Visible playfield above bottom HUD (window at WY=136) */
|
||
#define VIEW_W 160
|
||
#define VIEW_H 136
|
||
#define VIEW_TILES_W 20
|
||
#define VIEW_TILES_H 17
|
||
#define HUD_WY 136
|
||
|
||
#define T_FLOOR 0
|
||
#define T_WALL 1
|
||
#define T_ITEM 2
|
||
#define T_VOID 3 /* outside irregular NES silhouette — blank, solid */
|
||
|
||
#define DIR_NONE 0
|
||
#define DIR_LEFT 1
|
||
#define DIR_RIGHT 2
|
||
#define DIR_UP 4
|
||
#define DIR_DOWN 8
|
||
|
||
#define BKG_BASE 32
|
||
#define TILE_WTL (BKG_BASE + 0)
|
||
#define TILE_WTR (BKG_BASE + 1)
|
||
#define TILE_WBL (BKG_BASE + 2)
|
||
#define TILE_WBR (BKG_BASE + 3)
|
||
#define TILE_FLOOR (BKG_BASE + 4)
|
||
#define TILE_GTL (BKG_BASE + 5)
|
||
#define TILE_GTR (BKG_BASE + 6)
|
||
#define TILE_GBL (BKG_BASE + 7)
|
||
#define TILE_GBR (BKG_BASE + 8)
|
||
#define TILE_GTL2 (BKG_BASE + 9)
|
||
#define TILE_GTR2 (BKG_BASE + 10)
|
||
#define TILE_GBL2 (BKG_BASE + 11)
|
||
#define TILE_GBR2 (BKG_BASE + 12)
|
||
|
||
#define SPR_TILE_PL 16
|
||
#define SPR_TILE_PL2 20
|
||
#define SPR_TILE_EN 24
|
||
|
||
#define ENV_MAX 13
|
||
#define ENV_SUSTAIN 7
|
||
#define MUSIC_VOICES 2
|
||
|
||
typedef struct {
|
||
word x, y;
|
||
word cnt;
|
||
word speed;
|
||
byte dir;
|
||
byte wait;
|
||
byte kind;
|
||
} Actor;
|
||
|
||
byte joy_left, joy_right, joy_up, joy_down, joy_fire, joy_start;
|
||
byte fire_prev, start_prev;
|
||
byte start_level; /* chosen on title screen (0-based) */
|
||
|
||
byte map[MAP_W_MAX * MAP_H_MAX];
|
||
Actor actors[ACTORS_MAX];
|
||
byte actor_n;
|
||
byte game_level;
|
||
byte game_lives;
|
||
byte items_count;
|
||
byte items_collected;
|
||
byte game_clear;
|
||
byte game_done;
|
||
byte game_paused;
|
||
byte spawn_wait;
|
||
word rnd = 0xCACE;
|
||
byte frame_cnt;
|
||
byte cam_x, cam_y;
|
||
byte cam_max_x, cam_max_y;
|
||
byte map_w, map_h; /* current level size (cells) */
|
||
byte map_tx0, map_ty0; /* BG tile origin — inset when level fits */
|
||
byte map_ox, map_oy; /* pixel origin (= map_t*8) */
|
||
|
||
byte voice_vol[3];
|
||
byte voice_note[3];
|
||
word voice_per[3];
|
||
byte cur_duration;
|
||
byte music_speed;
|
||
byte music_release;
|
||
byte music_enable;
|
||
byte music_paused;
|
||
const uint8_t* music_ptr;
|
||
const uint8_t* music_loop;
|
||
const uint8_t* music_rts = 0;
|
||
byte music_runlen = 0;
|
||
byte chord[4];
|
||
byte chord_n;
|
||
const uint8_t* sfx_ptr;
|
||
byte sfx_timer;
|
||
byte sfx_vol;
|
||
byte sfx_busy; /* ch1 claimed by SFX */
|
||
byte hw_note[3]; /* last note pushed to hardware (avoid retrigger clicks) */
|
||
|
||
/*
|
||
* Level maps — exact NES Chase layouts (from levelN_nam.h metatiles).
|
||
* # wall * gem P player 1/2/3 enemies (space) void outside shape
|
||
* After these five, generate_level() builds endless mazes (no dead ends,
|
||
* corner pillars so diagonals stay walls, orthogonal exits only).
|
||
*/
|
||
typedef struct {
|
||
byte w, h;
|
||
const char* const* rows;
|
||
} Level;
|
||
|
||
const char* const level_1[] = {
|
||
"########",
|
||
"#P*****#",
|
||
"#*####*#",
|
||
"#******#",
|
||
"#*####*#",
|
||
"#*****1#",
|
||
"########",
|
||
};
|
||
|
||
const char* const level_2[] = {
|
||
"##########",
|
||
"#P***#**1#",
|
||
"#*##*#*#*#",
|
||
"#********#",
|
||
"###*#*#*##",
|
||
"#********#",
|
||
"#*#*#*##*#",
|
||
"#2*******#",
|
||
"##########",
|
||
};
|
||
|
||
const char* const level_3[] = {
|
||
" ########## ",
|
||
" #P***#**1# ",
|
||
"###*##*#*#*###",
|
||
"#********#***#",
|
||
"#*#*#*##*#*#*#",
|
||
"#***#********#",
|
||
"###*#*#*##*###",
|
||
" #***#***2# ",
|
||
" ########## ",
|
||
};
|
||
|
||
const char* const level_4[] = {
|
||
" ###### ",
|
||
" #P***#######",
|
||
" #*##*#****1#",
|
||
"###*##*#*#*#*#",
|
||
"#**********#*#",
|
||
"#*#*#*##*#*#*#",
|
||
"#*#**********#",
|
||
"#*#*#*#*##*###",
|
||
"#2****#*##*# ",
|
||
"#######***3# ",
|
||
" ###### ",
|
||
};
|
||
|
||
const char* const level_5[] = {
|
||
" ############ ",
|
||
" #P********1# ",
|
||
"###*##*##*##*###",
|
||
"#**************#",
|
||
"##*#*###*#*#*#*#",
|
||
"#*****#********#",
|
||
"#*###*#*#*###*##",
|
||
"#*******#******#",
|
||
"##*#*#*###*#*#*#",
|
||
"#**************#",
|
||
"###*##*##*##*###",
|
||
" #2********3# ",
|
||
" ############ ",
|
||
};
|
||
|
||
const Level levels[LEVELS_FIXED] = {
|
||
{ 8, 7, level_1 },
|
||
{ 10, 9, level_2 },
|
||
{ 14, 9, level_3 },
|
||
{ 14, 11, level_4 },
|
||
{ 16, 13, level_5 },
|
||
};
|
||
|
||
/*{pal:555,n:16,name:"Background"}*/
|
||
palette_color_t bkg_palettes[1][4] = {
|
||
{ 0x2062, 0x318E, 0x6A28, 0x7714 },
|
||
};
|
||
/*{pal:555,n:16,name:"Sprite"}*/
|
||
palette_color_t fg_palettes[1][4] = {
|
||
{ 0x73DB, 0x4BDC, 0x2B31, 0x14A3 },
|
||
};
|
||
|
||
/* Filled by generate_level() for endless mazes after the fixed set */
|
||
char gen_row[MAP_H_MAX][MAP_W_MAX + 1];
|
||
byte gen_w, gen_h;
|
||
/* Compact CHASE wordmark (wall tiles), fits GB 20-tile width */
|
||
const uint8_t title_logo[6][20] = {
|
||
{0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
|
||
{0,35,34,0,34,0,34,0,0,35,0,0,0,35,34,0,34,35,34,0},
|
||
{32,0,0,0,32,0,32,0,32,0,32,0,32,0,0,0,32,0,0,0},
|
||
{34,0,0,0,34,35,34,0,34,35,34,0,0,35,0,0,34,35,0,0},
|
||
{32,0,0,0,32,0,32,0,32,0,32,0,0,0,32,0,32,0,0,0},
|
||
{0,35,34,0,34,0,34,0,34,0,34,0,34,35,0,0,34,35,34,0},
|
||
};
|
||
|
||
|
||
/* GB pulse periods; index 0x28 ~= C4 (matches Coleco/SFX note indices) */
|
||
const word gb_note_per[64] = {
|
||
0x000,0x000,0x000,0x000,0x000,0x000,0x000,0x000,
|
||
0x000,0x000,0x000,0x000,0x000,0x000,0x000,0x000,
|
||
0x02c,0x09d,0x107,0x16b,0x1c9,0x223,0x277,0x2c7,
|
||
0x312,0x358,0x39b,0x3da,0x416,0x44e,0x483,0x4b5,
|
||
0x4e5,0x511,0x53b,0x563,0x589,0x5ac,0x5ce,0x5ed,
|
||
0x60b,0x627,0x642,0x65b,0x672,0x689,0x69e,0x6b2,
|
||
0x6c4,0x6d6,0x6e7,0x6f7,0x706,0x714,0x721,0x72d,
|
||
0x739,0x744,0x74f,0x759,0x762,0x76b,0x773,0x77b,
|
||
};
|
||
|
||
const uint8_t mus_level[] = {
|
||
0x1c,0x28,0x82, 0x1c,0x28,0x82, 0x1e,0x2a,0x8c,
|
||
0xff
|
||
};
|
||
|
||
const uint8_t mus_game[] = {
|
||
0x17,0x23,0x81,0x00,0x00,0x83,0x17,0x23,0x81,0x00,0x00,0x81,0x16,0x22,0x82,
|
||
0x1c,0x28,0x81,0x00,0x00,0x87,0x15,0x21,0x81,0x00,0x00,0x83,0x15,0x21,0x82,
|
||
0x14,0x20,0x82,0x1c,0x28,0x81,0x00,0x00,0x87,0x17,0x23,0x81,0x00,0x00,0x83,
|
||
0x17,0x23,0x81,0x00,0x00,0x81,0x16,0x22,0x82,0x1c,0x28,0x81,0x00,0x00,0x83,
|
||
0x16,0x22,0x81,0x00,0x00,0x83,0x15,0x21,0x81,0x00,0x00,0x83,0x15,0x21,0x82,
|
||
0x1c,0x28,0x81,0x14,0x20,0x83,0x00,0x00,0x86,0x17,0x23,0x81,0x00,0x00,0x83,
|
||
0x17,0x23,0x81,0x00,0x00,0x81,0x16,0x22,0x82,0x1c,0x28,0x81,0x00,0x00,0x83,
|
||
0x16,0x22,0x81,0x00,0x00,0x83,0x15,0x21,0x81,0x00,0x00,0x83,0x15,0x21,0x82,
|
||
0x14,0x20,0x82,0x1c,0x28,0x81,0x00,0x00,0x87,0x12,0x1e,0x81,0x00,0x00,0x83,
|
||
0x12,0x1e,0x81,0x00,0x00,0x81,0x14,0x20,0x82,0x1c,0x28,0x81,0x00,0x00,0x83,
|
||
0x15,0x21,0x82,0x1c,0x28,0x81,0x17,0x23,0x85,0x00,0x00,0x8c,
|
||
0xff
|
||
};
|
||
|
||
const uint8_t mus_clear[] = {
|
||
0x21,0x2d,0x81,0x00,0x00,0x81,0x21,0x2d,0x81,0x00,0x00,0x81,0x21,0x2d,0x81,
|
||
0x00,0x00,0x83,0x21,0x2d,0x81,0x00,0x00,0x81,0x21,0x2d,0x81,0x00,0x00,0x81,
|
||
0x23,0x2f,0x89,0x00,0x00,0x8b,
|
||
0xff
|
||
};
|
||
|
||
const uint8_t mus_lose[] = {
|
||
0x1c,0x28,0x88, 0x19,0x25,0x84, 0x17,0x23,0x94,
|
||
0xff
|
||
};
|
||
|
||
const uint8_t mus_gameover[] = {
|
||
0x15,0x21,0x81,0x15,0x21,0x83,0x14,0x20,0x81,0x14,0x20,0x83,0x13,0x1f,0x81,
|
||
0x13,0x1f,0x83,0x12,0x1e,0x82,0x10,0x1c,0x86,0x1c,0x28,0x8c,
|
||
0xff
|
||
};
|
||
|
||
const uint8_t mus_welldone[] = {
|
||
0x17,0x23,0x88,0x23,0x2f,0x86,0x23,0x2f,0x86,0x23,0x2f,0x88,0x17,0x23,0x84,
|
||
0x15,0x21,0x88,0x21,0x2d,0x86,0x21,0x2d,0x86,0x21,0x2d,0x88,0x15,0x21,0x84,
|
||
0x17,0x23,0x88,0x23,0x2f,0x86,0x23,0x2f,0x86,0x23,0x2f,0x88,0x17,0x23,0x84,
|
||
0x10,0x1c,0x88,0x1c,0x28,0x86,0x1e,0x2a,0x86,0x1e,0x2a,0x8c,
|
||
0xff
|
||
};
|
||
|
||
const uint8_t sfx_start[] = {
|
||
/* C4 F4 G4 C5 F5 G5 ×3, volume steps like NES $7f/$74/$71 */
|
||
0x28,15,4, 0x2d,14,4, 0x2f,14,4, 0x34,13,4, 0x39,12,4, 0x3b,12,4,
|
||
0x28,12,4, 0x2d,11,4, 0x2f,11,4, 0x34,10,4, 0x39,10,4, 0x3b,9,4,
|
||
0x28,9,4, 0x2d,8,4, 0x2f,8,4, 0x34,7,4, 0x39,7,4, 0x3b,6,4,
|
||
0xff
|
||
};
|
||
|
||
const uint8_t sfx_item[] = {
|
||
/* short high blip: C4→F4→E4→G4 then softer repeat */
|
||
0x28,12,1, 0x2d,11,1, 0x2c,10,1, 0x2f,9,1,
|
||
0x28,8,1, 0x2d,7,1, 0x2c,6,1, 0x2f,5,1,
|
||
0xff
|
||
};
|
||
|
||
const uint8_t sfx_respawn_p[] = {
|
||
/* NES pulse arpeggio — bright rising chirp */
|
||
0x3c,10,1, 0x3e,11,1, 0x3f,12,1, 0x3c,13,1, 0x3e,14,1,
|
||
0x3f,14,1, 0x3c,13,1, 0x3e,12,1, 0x3f,11,1,
|
||
0x3c,9,1, 0x3e,8,1, 0x3f,7,1, 0x3c,6,1, 0x3e,5,1,
|
||
0x3f,4,1, 0x3c,3,1, 0x3e,2,1, 0x3f,2,1,
|
||
0xff
|
||
};
|
||
|
||
const uint8_t sfx_respawn_e[] = {
|
||
/* slightly lower / darker than player respawn */
|
||
0x38,9,1, 0x3a,10,1, 0x3c,11,1, 0x38,12,1, 0x3a,13,1,
|
||
0x3c,13,1, 0x38,12,1, 0x3a,11,1, 0x3c,10,1,
|
||
0x38,8,1, 0x3a,7,1, 0x3c,6,1, 0x38,5,1, 0x3a,4,1,
|
||
0x3c,3,1, 0x38,3,1, 0x3a,2,1, 0x3c,2,1,
|
||
0xff
|
||
};
|
||
|
||
const uint8_t font_1bpp[] = {
|
||
/* 0 space */ 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
|
||
/* 1 '0' */ 0x3C,0x66,0x6E,0x76,0x66,0x66,0x3C,0x00,
|
||
/* 2 '1' */ 0x18,0x38,0x18,0x18,0x18,0x18,0x7E,0x00,
|
||
/* 3 '2' */ 0x3C,0x66,0x06,0x0C,0x18,0x30,0x7E,0x00,
|
||
/* 4 '3' */ 0x3C,0x66,0x06,0x1C,0x06,0x66,0x3C,0x00,
|
||
/* 5 '4' */ 0x0C,0x1C,0x3C,0x6C,0x7E,0x0C,0x0C,0x00,
|
||
/* 6 '5' */ 0x7E,0x60,0x7C,0x06,0x06,0x66,0x3C,0x00,
|
||
/* 7 '6' */ 0x1C,0x30,0x60,0x7C,0x66,0x66,0x3C,0x00,
|
||
/* 8 '7' */ 0x7E,0x06,0x0C,0x18,0x30,0x30,0x30,0x00,
|
||
/* 9 '8' */ 0x3C,0x66,0x66,0x3C,0x66,0x66,0x3C,0x00,
|
||
/*10 '9' */ 0x3C,0x66,0x66,0x3E,0x06,0x0C,0x38,0x00,
|
||
/*11 'A' */ 0x3C,0x66,0x66,0x7E,0x66,0x66,0x66,0x00,
|
||
/*12 'C' */ 0x3C,0x66,0x60,0x60,0x60,0x66,0x3C,0x00,
|
||
/*13 'D' */ 0x78,0x6C,0x66,0x66,0x66,0x6C,0x78,0x00,
|
||
/*14 'E' */ 0x7E,0x60,0x60,0x7C,0x60,0x60,0x7E,0x00,
|
||
/*15 'G' */ 0x3C,0x66,0x60,0x6E,0x66,0x66,0x3C,0x00,
|
||
/*16 'H' */ 0x66,0x66,0x66,0x7E,0x66,0x66,0x66,0x00,
|
||
/*17 'I' */ 0x3C,0x18,0x18,0x18,0x18,0x18,0x3C,0x00,
|
||
/*18 'L' */ 0x60,0x60,0x60,0x60,0x60,0x60,0x7E,0x00,
|
||
/*19 'M' */ 0x63,0x77,0x7F,0x6B,0x63,0x63,0x63,0x00,
|
||
/*20 'N' */ 0x66,0x76,0x7E,0x7E,0x6E,0x66,0x66,0x00,
|
||
/*21 'O' */ 0x3C,0x66,0x66,0x66,0x66,0x66,0x3C,0x00,
|
||
/*22 'P' */ 0x7C,0x66,0x66,0x7C,0x60,0x60,0x60,0x00,
|
||
/*23 'R' */ 0x7C,0x66,0x66,0x7C,0x6C,0x66,0x66,0x00,
|
||
/*24 'S' */ 0x3C,0x66,0x60,0x3C,0x06,0x66,0x3C,0x00,
|
||
/*25 'T' */ 0x7E,0x18,0x18,0x18,0x18,0x18,0x18,0x00,
|
||
/*26 'U' */ 0x66,0x66,0x66,0x66,0x66,0x66,0x3C,0x00,
|
||
/*27 'V' */ 0x66,0x66,0x66,0x66,0x66,0x3C,0x18,0x00,
|
||
/*28 'W' */ 0x63,0x63,0x63,0x6B,0x7F,0x77,0x63,0x00,
|
||
/*29 'Y' */ 0x66,0x66,0x66,0x3C,0x18,0x18,0x18,0x00,
|
||
/*30 '/' */ 0x06,0x06,0x0C,0x18,0x30,0x60,0x60,0x00,
|
||
/*31 ':' */ 0x00,0x18,0x18,0x00,0x18,0x18,0x00,0x00,
|
||
};
|
||
|
||
const uint8_t bkg_tiles[] = {
|
||
/* From presets/nes/chase/tileset.chr (walls/floor/gems);
|
||
* BG pens 1:1 — NES bright → GB dark ink under BGP 0xE4. */
|
||
/*{w:8,h:8,bpp:1,count:13,brev:1,np:2,pofs:1,sl:2,palname:"Background"}*/
|
||
0xff,0x00,0xff,0x7f,0xff,0x7f,0xe0,0x7f,
|
||
0xef,0x7f,0xe8,0x7f,0xe8,0x7f,0xe8,0x7f,
|
||
0xfe,0x00,0xfc,0xfe,0xfa,0xfc,0x06,0xf8,
|
||
0xf6,0xf8,0x16,0xe8,0x16,0xe8,0x16,0xe8,
|
||
0xe8,0x7f,0xe8,0x7f,0xe8,0x7f,0xef,0x78,
|
||
0xe0,0x7f,0xdf,0x60,0xbf,0x40,0x00,0x00,
|
||
0x16,0xe8,0x16,0xe8,0x16,0xe8,0xf6,0x08,
|
||
0x06,0xf8,0xfe,0x00,0xfe,0x00,0x00,0x00,
|
||
0xea,0x00,0xe5,0x00,0xea,0x00,0x05,0x00,
|
||
0xae,0x00,0x5e,0x00,0xae,0x00,0x50,0x00,
|
||
0xea,0x00,0xe5,0x00,0xea,0x00,0x05,0x00,
|
||
0xac,0x00,0x58,0x03,0xa3,0x07,0x53,0x07,
|
||
0xea,0x00,0xe5,0x00,0xea,0x00,0x05,0x00,
|
||
0x2e,0x00,0x1e,0xc0,0x8e,0xe0,0x40,0xa0,
|
||
0xe2,0x07,0xe1,0x06,0xe0,0x03,0x00,0x00,
|
||
0xae,0x00,0x5e,0x00,0xae,0x00,0x50,0x00,
|
||
0x8a,0x20,0x05,0x20,0x0a,0xc0,0x05,0x00,
|
||
0xae,0x00,0x5e,0x00,0xae,0x00,0x50,0x00,
|
||
0xea,0x00,0xe5,0x00,0xea,0x00,0x04,0x00,
|
||
0xa8,0x03,0x53,0x07,0xa3,0x07,0x52,0x07,
|
||
0xea,0x00,0xe5,0x00,0xea,0x00,0x05,0x00,
|
||
0x0e,0xc0,0x8e,0xe0,0x4e,0xa0,0x80,0x20,
|
||
0xe1,0x06,0xe0,0x03,0xe8,0x00,0x05,0x00,
|
||
0xae,0x00,0x5e,0x00,0xae,0x00,0x50,0x00,
|
||
0x0a,0x20,0x05,0xc0,0x2a,0x00,0x05,0x00,
|
||
0xae,0x00,0x5e,0x00,0xae,0x00,0x50,0x00,
|
||
};
|
||
|
||
const uint8_t sprite_tiles[] = {
|
||
/* From NES metasprites; 8x16 L/R pairs (player, player2, enemy);
|
||
* pen0 clear; pen1 black→3, pen2 body→2, pen3 white→1. */
|
||
/*{w:16,h:16,bpp:1,count:3,brev:1,np:2,pofs:1,sl:2,wpimg:64,remap:[5,1,2,3,4,0,6,7,8,9,10,11,12],palname:"Sprite"}*/
|
||
0x00,0x00,0x3f,0x3f,0x60,0x7f,0x5f,0x60,
|
||
0x5f,0x6e,0x5f,0x72,0x5f,0x76,0x5f,0x76,
|
||
0x5f,0x7e,0x5f,0x60,0x5f,0x60,0x47,0x7f,
|
||
0x67,0x7c,0x7f,0x7e,0x3f,0x3f,0x00,0x00,
|
||
0x00,0x00,0xfc,0xfc,0x06,0xfe,0xf2,0x0e,
|
||
0xf2,0x7e,0xfa,0x4e,0xfa,0x6e,0xfa,0x6e,
|
||
0xfa,0x7e,0xf2,0x0e,0xf2,0x0e,0xe2,0xfe,
|
||
0xe6,0x3e,0xfe,0x7e,0xfc,0xfc,0x00,0x00,
|
||
0x3f,0x3f,0x60,0x7f,0x5f,0x60,0x5f,0x6e,
|
||
0x5f,0x72,0x5f,0x76,0x5f,0x76,0x5f,0x7e,
|
||
0x5f,0x60,0x5f,0x60,0x47,0x7f,0x67,0x7c,
|
||
0x7f,0x7e,0x3f,0x3f,0x00,0x00,0x00,0x00,
|
||
0xfc,0xfc,0x06,0xfe,0xf2,0x0e,0xf2,0x7e,
|
||
0xfa,0x4e,0xfa,0x6e,0xfa,0x6e,0xfa,0x7e,
|
||
0xf2,0x0e,0xf2,0x0e,0xe2,0xfe,0xe6,0x3e,
|
||
0xfe,0x7e,0xfc,0xfc,0x00,0x00,0x00,0x00,
|
||
0x00,0x00,0x7f,0x7f,0x40,0x7f,0x5f,0x60,
|
||
0x7f,0x7f,0x5f,0x7f,0x5e,0x77,0x5f,0x76,
|
||
0x5f,0x72,0x5f,0x6e,0x5f,0x60,0x4b,0x77,
|
||
0x67,0x7e,0x3f,0x3c,0x1f,0x1f,0x00,0x00,
|
||
0x00,0x00,0xfe,0xfe,0x02,0xfe,0xf2,0x0e,
|
||
0xfe,0xfe,0xfa,0xfe,0x7a,0xee,0xfa,0x6e,
|
||
0xfa,0x4e,0xf2,0x7e,0xf2,0x0e,0xd2,0xee,
|
||
0xe6,0x7e,0xfc,0x3c,0xf8,0xf8,0x00,0x00,
|
||
};
|
||
|
||
|
||
byte rand8(void) {
|
||
rnd = rnd * 17 + 53;
|
||
return (byte)(rnd >> 8);
|
||
}
|
||
|
||
void read_controls(void) {
|
||
byte j = joypad();
|
||
joy_left = (j & J_LEFT) != 0;
|
||
joy_right = (j & J_RIGHT) != 0;
|
||
joy_up = (j & J_UP) != 0;
|
||
joy_down = (j & J_DOWN) != 0;
|
||
joy_fire = (j & J_A) != 0;
|
||
joy_start = (j & J_START) != 0;
|
||
}
|
||
|
||
void psg_init(void) {
|
||
NR52_REG = 0x80;
|
||
NR51_REG = 0xFF;
|
||
NR50_REG = 0x77;
|
||
NR10_REG = 0x00;
|
||
}
|
||
|
||
void gb_ch_off(byte ch) {
|
||
if (ch == 0) { NR12_REG = 0x00; NR14_REG = 0x80; hw_note[0] = 0; }
|
||
else { NR22_REG = 0x00; NR24_REG = 0x80; hw_note[1] = 0; }
|
||
}
|
||
|
||
void gb_ch_on(byte ch, byte note, word per, byte vol) {
|
||
byte lo = (byte)(per & 0xFF);
|
||
byte hi = (byte)((per >> 8) & 0x07);
|
||
byte v = vol > 15 ? 15 : vol;
|
||
byte retrig = (hw_note[ch] != note);
|
||
hw_note[ch] = note;
|
||
if (ch == 0) {
|
||
NR10_REG = 0x00;
|
||
NR11_REG = 0x80; /* 50% duty */
|
||
if (retrig) NR12_REG = (byte)((v << 4) | 0x00);
|
||
NR13_REG = lo;
|
||
NR14_REG = (byte)((retrig ? 0x80 : 0x00) | hi);
|
||
} else {
|
||
NR21_REG = 0x80;
|
||
if (retrig) NR22_REG = (byte)((v << 4) | 0x00);
|
||
NR23_REG = lo;
|
||
NR24_REG = (byte)((retrig ? 0x80 : 0x00) | hi);
|
||
}
|
||
}
|
||
|
||
void set_voice(byte ch, byte note, byte vol) {
|
||
if (note < 0x10) {
|
||
voice_note[ch] = 0;
|
||
voice_vol[ch] = 0;
|
||
return;
|
||
}
|
||
voice_per[ch] = gb_note_per[note & 63];
|
||
voice_note[ch] = note;
|
||
voice_vol[ch] = vol;
|
||
}
|
||
|
||
void mute_voice(byte ch) {
|
||
voice_note[ch] = 0;
|
||
voice_vol[ch] = 0;
|
||
}
|
||
|
||
void sn_update_hw(void) {
|
||
/* bass = ch2 (NR2), lead = ch1 (NR1) unless SFX owns ch1 */
|
||
if (voice_vol[0] && voice_note[0])
|
||
gb_ch_on(1, voice_note[0], voice_per[0], voice_vol[0]);
|
||
else
|
||
gb_ch_off(1);
|
||
|
||
if (sfx_busy) {
|
||
if (sfx_vol)
|
||
gb_ch_on(0, voice_note[2], voice_per[2], sfx_vol);
|
||
else
|
||
gb_ch_off(0);
|
||
} else if (voice_vol[1] && voice_note[1]) {
|
||
gb_ch_on(0, voice_note[1], voice_per[1], voice_vol[1]);
|
||
} else {
|
||
gb_ch_off(0);
|
||
}
|
||
}
|
||
|
||
// decode next music byte, handling compression
|
||
byte next_music_byte(void) {
|
||
byte ch;
|
||
if (!music_ptr) return 0xff;
|
||
ch = *music_ptr++;
|
||
if (music_runlen) {
|
||
// we are in a backreference, count down
|
||
if (--music_runlen == 0) {
|
||
music_ptr = music_rts;
|
||
music_rts = 0;
|
||
}
|
||
} else if (ch == 0xfe) {
|
||
// back-reference: 0xfe <offset> <length>
|
||
byte offset = *music_ptr++;
|
||
music_runlen = *music_ptr++;
|
||
music_rts = music_ptr;
|
||
music_ptr -= offset + 3;
|
||
return next_music_byte();
|
||
}
|
||
return ch;
|
||
}
|
||
|
||
void sfx_stop(void) {
|
||
sfx_ptr = 0;
|
||
sfx_timer = 0;
|
||
sfx_vol = 0;
|
||
sfx_busy = 0;
|
||
mute_voice(2);
|
||
hw_note[0] = 0; /* allow lead to retrigger after SFX */
|
||
}
|
||
|
||
void sfx_play(const uint8_t* seq) {
|
||
sfx_ptr = seq;
|
||
sfx_timer = 0;
|
||
sfx_vol = 0;
|
||
sfx_busy = 1;
|
||
hw_note[0] = 0; /* force retrigger on first SFX note */
|
||
}
|
||
|
||
void sfx_update(void) {
|
||
byte note, frames;
|
||
if (!sfx_ptr) return;
|
||
if (sfx_timer) {
|
||
sfx_timer--;
|
||
if (sfx_timer) return;
|
||
}
|
||
note = *sfx_ptr++;
|
||
if (note == 0xff) {
|
||
sfx_stop();
|
||
return;
|
||
}
|
||
sfx_vol = *sfx_ptr++;
|
||
frames = *sfx_ptr++;
|
||
if (!frames) frames = 1;
|
||
sfx_timer = frames;
|
||
if (note & 0x80) {
|
||
mute_voice(2);
|
||
sfx_vol = 0;
|
||
} else {
|
||
set_voice(2, note & 63, sfx_vol);
|
||
}
|
||
}
|
||
|
||
void music_stop(void) {
|
||
byte i;
|
||
music_enable = 0;
|
||
music_ptr = 0;
|
||
music_rts = 0;
|
||
music_runlen = 0;
|
||
music_loop = 0;
|
||
chord_n = 0;
|
||
music_release = 0;
|
||
for (i = 0; i < MUSIC_VOICES; i++) mute_voice(i);
|
||
hw_note[0] = hw_note[1] = hw_note[2] = 0;
|
||
}
|
||
|
||
void music_play(const uint8_t* music, byte loop) {
|
||
byte i;
|
||
music_enable = 0;
|
||
music_paused = 0;
|
||
music_ptr = music;
|
||
music_rts = 0;
|
||
music_runlen = 0;
|
||
music_loop = loop ? music : 0;
|
||
cur_duration = 0;
|
||
chord_n = 0;
|
||
music_release = 0;
|
||
if (music == mus_game || music == mus_welldone) music_speed = 4;
|
||
else if (music == mus_gameover) music_speed = 6;
|
||
else music_speed = 3;
|
||
for (i = 0; i < MUSIC_VOICES; i++) mute_voice(i);
|
||
music_enable = 1;
|
||
}
|
||
|
||
void flush_chord(void) {
|
||
byte bass = (chord_n >= 1) ? chord[0] : 0;
|
||
byte lead = (chord_n >= 2) ? chord[1] : 0;
|
||
chord_n = 0;
|
||
if (bass) { set_voice(0, bass & 63, ENV_MAX); music_release = 0; }
|
||
else if (voice_note[0]) music_release = 1;
|
||
else mute_voice(0);
|
||
if (lead) { set_voice(1, lead & 63, ENV_MAX); music_release = 0; }
|
||
else if (voice_note[1]) music_release = 1;
|
||
else mute_voice(1);
|
||
}
|
||
|
||
void music_seq(void) {
|
||
byte ch, floor, note;
|
||
if (!music_enable || music_paused) return;
|
||
floor = music_release ? 0 : ENV_SUSTAIN;
|
||
for (ch = 0; ch < MUSIC_VOICES; ch++) {
|
||
if (voice_vol[ch] > floor) {
|
||
voice_vol[ch] -= music_release ? 2 : 1;
|
||
if (voice_vol[ch] < floor || (music_release && voice_vol[ch] > 200))
|
||
voice_vol[ch] = floor;
|
||
} else if (music_release && voice_vol[ch] == 0 && voice_note[ch]) {
|
||
mute_voice(ch);
|
||
}
|
||
}
|
||
if (!music_ptr) {
|
||
if (music_loop) music_ptr = music_loop;
|
||
else return;
|
||
}
|
||
if (cur_duration) { cur_duration--; return; }
|
||
while (1) {
|
||
note = next_music_byte();
|
||
if (note == 0xff) {
|
||
music_ptr = music_loop;
|
||
if (!music_ptr) { music_enable = 0; return; }
|
||
continue;
|
||
}
|
||
if (note & 0x80) {
|
||
flush_chord();
|
||
cur_duration = (note & 63) * music_speed;
|
||
if (!cur_duration) cur_duration = music_speed;
|
||
cur_duration--;
|
||
return;
|
||
}
|
||
if (chord_n < 4) chord[chord_n++] = note;
|
||
}
|
||
}
|
||
|
||
void music_update(void) {
|
||
sfx_update();
|
||
music_seq();
|
||
sn_update_hw();
|
||
}
|
||
|
||
void wait_vblank(void) {
|
||
wait_vbl_done();
|
||
music_update();
|
||
}
|
||
|
||
void wait_frames(byte n) {
|
||
while (n--) wait_vblank();
|
||
}
|
||
|
||
byte map_at(byte x, byte y) {
|
||
if (x >= map_w || y >= map_h) return T_WALL;
|
||
return map[y * MAP_W_MAX + x];
|
||
}
|
||
|
||
void map_set(byte x, byte y, byte t) {
|
||
map[y * MAP_W_MAX + x] = t;
|
||
}
|
||
|
||
byte can_enter(byte tx, byte ty) {
|
||
byte t = map_at(tx, ty);
|
||
return t == T_FLOOR || t == T_ITEM;
|
||
}
|
||
|
||
byte glyph(char c) {
|
||
if (c == ' ') return 0;
|
||
if (c >= '0' && c <= '9') return (byte)(1 + (c - '0'));
|
||
if (c == '/') return 30;
|
||
if (c == ':') return 31;
|
||
switch (c) {
|
||
case 'A': return 11; case 'C': return 12; case 'D': return 13;
|
||
case 'E': return 14; case 'G': return 15; case 'H': return 16;
|
||
case 'I': return 17; case 'L': return 18; case 'M': return 19;
|
||
case 'N': return 20; case 'O': return 21; case 'P': return 22;
|
||
case 'R': return 23; case 'S': return 24; case 'T': return 25;
|
||
case 'U': return 26; case 'V': return 27; case 'W': return 28;
|
||
case 'Y': return 29;
|
||
default: return 0;
|
||
}
|
||
}
|
||
|
||
void put_str(byte x, byte y, const char* s) {
|
||
while (*s) set_bkg_tile_xy(x++, y, glyph(*s++));
|
||
}
|
||
|
||
void put_digit(byte x, byte y, byte d) {
|
||
set_bkg_tile_xy(x, y, (byte)(1 + (d % 10)));
|
||
}
|
||
|
||
void put_str_win(byte x, byte y, const char* s) {
|
||
while (*s) set_win_tile_xy(x++, y, glyph(*s++));
|
||
}
|
||
|
||
void put_digit_win(byte x, byte y, byte d) {
|
||
set_win_tile_xy(x, y, (byte)(1 + (d % 10)));
|
||
}
|
||
|
||
void clrscr(void) {
|
||
byte x, y;
|
||
for (y = 0; y < 32; y++)
|
||
for (x = 0; x < 32; x++)
|
||
set_bkg_tile_xy(x, y, 0);
|
||
}
|
||
|
||
void hide_actor_sprites(byte i) {
|
||
byte s = (byte)(i << 1);
|
||
move_sprite(s, 0, 0);
|
||
move_sprite((byte)(s + 1), 0, 0);
|
||
}
|
||
|
||
void hide_all_sprites(void) {
|
||
byte i;
|
||
for (i = 0; i < 40; i++) move_sprite(i, 0, 0);
|
||
}
|
||
|
||
void update_camera(void) {
|
||
word px = actors[0].x >> FP_BITS;
|
||
word py = actors[0].y >> FP_BITS;
|
||
word tx, ty;
|
||
/* Free axes follow the player; locked axes keep load_level placement. */
|
||
if (cam_max_x == 0) {
|
||
tx = cam_x;
|
||
} else {
|
||
if (px > (VIEW_W / 2)) tx = px - (VIEW_W / 2); else tx = 0;
|
||
if (tx > cam_max_x) tx = cam_max_x;
|
||
}
|
||
if (cam_max_y == 0) {
|
||
ty = cam_y;
|
||
} else {
|
||
if (py > (VIEW_H / 2)) ty = py - (VIEW_H / 2); else ty = 0;
|
||
if (ty > cam_max_y) ty = cam_max_y;
|
||
}
|
||
cam_x = (byte)tx;
|
||
cam_y = (byte)ty;
|
||
move_bkg(cam_x, cam_y);
|
||
}
|
||
|
||
void draw_actors(void) {
|
||
byte i;
|
||
for (i = 0; i < actor_n; i++) {
|
||
Actor* a = &actors[i];
|
||
int16_t sx, sy;
|
||
byte base, prop, s;
|
||
s = (byte)(i << 1);
|
||
if (a->wait) {
|
||
if (a->wait >= 16 || (a->wait & 2)) { hide_actor_sprites(i); continue; }
|
||
}
|
||
sx = (int16_t)(a->x >> FP_BITS) + (int16_t)map_ox - (int16_t)cam_x;
|
||
sy = (int16_t)(a->y >> FP_BITS) + (int16_t)map_oy - (int16_t)cam_y;
|
||
/* hide if fully outside playfield */
|
||
/* hide if off playfield or X would wrap through OAM (byte) cast */
|
||
if (sx < -8 || sx >= VIEW_W || sy < -16 || sy >= VIEW_H) {
|
||
hide_actor_sprites(i);
|
||
continue;
|
||
}
|
||
if (a->kind == 0)
|
||
base = (frame_cnt & 8) ? SPR_TILE_PL2 : SPR_TILE_PL;
|
||
else
|
||
base = SPR_TILE_EN;
|
||
prop = (a->kind >= 2) ? S_PALETTE : 0;
|
||
set_sprite_tile(s, base);
|
||
set_sprite_tile((byte)(s + 1), (byte)(base + 2));
|
||
set_sprite_prop(s, prop);
|
||
set_sprite_prop((byte)(s + 1), prop);
|
||
move_sprite(s, (byte)(sx + 8), (byte)(sy + 16));
|
||
move_sprite((byte)(s + 1), (byte)(sx + 16), (byte)(sy + 16));
|
||
}
|
||
}
|
||
|
||
void draw_cell(byte x, byte y) {
|
||
byte t = map_at(x, y);
|
||
byte sx = (byte)(map_tx0 + (x << 1));
|
||
byte sy = (byte)(map_ty0 + (y << 1));
|
||
byte spark = (frame_cnt & 16) != 0;
|
||
if (t == T_VOID) {
|
||
set_bkg_tile_xy(sx, sy, 0);
|
||
set_bkg_tile_xy(sx + 1, sy, 0);
|
||
set_bkg_tile_xy(sx, sy + 1, 0);
|
||
set_bkg_tile_xy(sx + 1, sy + 1, 0);
|
||
} else if (t == T_WALL) {
|
||
set_bkg_tile_xy(sx, sy, TILE_WTL);
|
||
set_bkg_tile_xy(sx + 1, sy, TILE_WTR);
|
||
set_bkg_tile_xy(sx, sy + 1, TILE_WBL);
|
||
set_bkg_tile_xy(sx + 1, sy + 1, TILE_WBR);
|
||
} else if (t == T_ITEM) {
|
||
if (spark) {
|
||
set_bkg_tile_xy(sx, sy, TILE_GTL2);
|
||
set_bkg_tile_xy(sx + 1, sy, TILE_GTR2);
|
||
set_bkg_tile_xy(sx, sy + 1, TILE_GBL2);
|
||
set_bkg_tile_xy(sx + 1, sy + 1, TILE_GBR2);
|
||
} else {
|
||
set_bkg_tile_xy(sx, sy, TILE_GTL);
|
||
set_bkg_tile_xy(sx + 1, sy, TILE_GTR);
|
||
set_bkg_tile_xy(sx, sy + 1, TILE_GBL);
|
||
set_bkg_tile_xy(sx + 1, sy + 1, TILE_GBR);
|
||
}
|
||
} else {
|
||
set_bkg_tile_xy(sx, sy, TILE_FLOOR);
|
||
set_bkg_tile_xy(sx + 1, sy, TILE_FLOOR);
|
||
set_bkg_tile_xy(sx, sy + 1, TILE_FLOOR);
|
||
set_bkg_tile_xy(sx + 1, sy + 1, TILE_FLOOR);
|
||
}
|
||
}
|
||
|
||
void set_color_palettes(byte i) {
|
||
set_bkg_palette(0, 1, bkg_palettes[i]);
|
||
set_sprite_palette(0, 1, fg_palettes[i]);
|
||
}
|
||
|
||
void draw_hud(void) {
|
||
byte lv = (byte)(game_level + 1);
|
||
put_str_win(0, 0, "L");
|
||
put_digit_win(1, 0, lv / 10);
|
||
put_digit_win(2, 0, lv % 10);
|
||
put_str_win(4, 0, "G");
|
||
put_digit_win(5, 0, items_collected / 10);
|
||
put_digit_win(6, 0, items_collected % 10);
|
||
put_str_win(7, 0, "/");
|
||
put_digit_win(8, 0, items_count / 10);
|
||
put_digit_win(9, 0, items_count % 10);
|
||
put_str_win(11, 0, "H");
|
||
put_digit_win(12, 0, game_lives > 0 ? (byte)(game_lives - 1) : 0);
|
||
if (game_paused) put_str_win(14, 0, "PAUSE");
|
||
else put_str_win(14, 0, " ");
|
||
}
|
||
|
||
void show_menu_screen(void) {
|
||
HIDE_WIN;
|
||
hide_all_sprites();
|
||
move_bkg(0, 0);
|
||
cam_x = cam_y = 0;
|
||
clrscr();
|
||
}
|
||
|
||
void actor_try_dir(byte id, byte dir) {
|
||
Actor* a = &actors[id];
|
||
byte tx = POS_TO_TILE(a->x);
|
||
byte ty = POS_TO_TILE(a->y);
|
||
if (dir == DIR_LEFT) tx--;
|
||
else if (dir == DIR_RIGHT) tx++;
|
||
else if (dir == DIR_UP) ty--;
|
||
else if (dir == DIR_DOWN) ty++;
|
||
else return;
|
||
if (!can_enter(tx, ty)) return;
|
||
a->dir = dir;
|
||
a->cnt = (word)TILE_PX << FP_BITS;
|
||
}
|
||
|
||
void try_collect(byte id) {
|
||
byte tx, ty;
|
||
Actor* a = &actors[id];
|
||
if (id != 0 || a->wait) return;
|
||
tx = POS_TO_TILE(a->x);
|
||
ty = POS_TO_TILE(a->y);
|
||
if (map_at(tx, ty) != T_ITEM) return;
|
||
map_set(tx, ty, T_FLOOR);
|
||
draw_cell(tx, ty);
|
||
items_collected++;
|
||
sfx_play(sfx_item);
|
||
draw_hud();
|
||
}
|
||
|
||
byte gen_is_open(char c) {
|
||
return c == '*' || c == 'P' || c == '1' || c == '2' || c == '3';
|
||
}
|
||
|
||
/* Corner pillars (even,even) stay walls — diagonals of every floor stay solid. */
|
||
byte gen_is_pillar(byte x, byte y) {
|
||
return !(x & 1) && !(y & 1);
|
||
}
|
||
|
||
byte gen_ortho_degree(byte x, byte y) {
|
||
byte n = 0;
|
||
if (gen_is_open(gen_row[y - 1][x])) n++;
|
||
if (gen_is_open(gen_row[y + 1][x])) n++;
|
||
if (gen_is_open(gen_row[y][x - 1])) n++;
|
||
if (gen_is_open(gen_row[y][x + 1])) n++;
|
||
return n;
|
||
}
|
||
|
||
/* May open (x,y) only if it is not a pillar and does not form a 2×2 floor block. */
|
||
byte gen_can_open(byte x, byte y) {
|
||
if (x < 1 || y < 1 || x >= gen_w - 1 || y >= gen_h - 1) return 0;
|
||
if (gen_is_pillar(x, y)) return 0;
|
||
if (gen_is_open(gen_row[y][x])) return 0;
|
||
/* Four possible 2×2 blocks that would include this cell */
|
||
if (gen_is_open(gen_row[y - 1][x - 1]) && gen_is_open(gen_row[y - 1][x]) &&
|
||
gen_is_open(gen_row[y][x - 1]))
|
||
return 0;
|
||
if (gen_is_open(gen_row[y - 1][x]) && gen_is_open(gen_row[y - 1][x + 1]) &&
|
||
gen_is_open(gen_row[y][x + 1]))
|
||
return 0;
|
||
if (gen_is_open(gen_row[y][x - 1]) && gen_is_open(gen_row[y + 1][x - 1]) &&
|
||
gen_is_open(gen_row[y + 1][x]))
|
||
return 0;
|
||
if (gen_is_open(gen_row[y][x + 1]) && gen_is_open(gen_row[y + 1][x]) &&
|
||
gen_is_open(gen_row[y + 1][x + 1]))
|
||
return 0;
|
||
return 1;
|
||
}
|
||
|
||
byte gen_try_open(byte x, byte y) {
|
||
if (!gen_can_open(x, y)) return 0;
|
||
gen_row[y][x] = '*';
|
||
return 1;
|
||
}
|
||
|
||
/* Keep every even,even cell as a wall (border + interior pillars). */
|
||
void gen_force_pillars(void) {
|
||
byte x, y;
|
||
for (y = 0; y < gen_h; y++) {
|
||
for (x = 0; x < gen_w; x++) {
|
||
if (gen_is_pillar(x, y)) gen_row[y][x] = '#';
|
||
}
|
||
}
|
||
}
|
||
|
||
/*
|
||
* Remove cul-de-sacs: prefer opening an orthogonal wall (never a pillar / 2×2),
|
||
* otherwise fill the dead-end floor as wall. Exits are U/D/L/R only.
|
||
*/
|
||
void braid_dead_ends(void) {
|
||
byte x, y, n, guard, changed;
|
||
|
||
for (guard = 0; guard < 64; guard++) {
|
||
changed = 0;
|
||
for (y = 1; y < gen_h - 1; y++) {
|
||
for (x = 1; x < gen_w - 1; x++) {
|
||
if (!gen_is_open(gen_row[y][x])) continue;
|
||
n = gen_ortho_degree(x, y);
|
||
if (n >= 2) continue;
|
||
/* Prefer opening opposite the single exit. */
|
||
if (n == 1) {
|
||
if (gen_is_open(gen_row[y - 1][x]) && gen_try_open(x, (byte)(y + 1))) {
|
||
changed = 1;
|
||
continue;
|
||
}
|
||
if (gen_is_open(gen_row[y + 1][x]) && gen_try_open(x, (byte)(y - 1))) {
|
||
changed = 1;
|
||
continue;
|
||
}
|
||
if (gen_is_open(gen_row[y][x - 1]) && gen_try_open((byte)(x + 1), y)) {
|
||
changed = 1;
|
||
continue;
|
||
}
|
||
if (gen_is_open(gen_row[y][x + 1]) && gen_try_open((byte)(x - 1), y)) {
|
||
changed = 1;
|
||
continue;
|
||
}
|
||
}
|
||
if (gen_try_open(x, (byte)(y - 1)) || gen_try_open(x, (byte)(y + 1)) ||
|
||
gen_try_open((byte)(x - 1), y) || gen_try_open((byte)(x + 1), y)) {
|
||
changed = 1;
|
||
continue;
|
||
}
|
||
/* Cannot add a legal exit — shrink the cul-de-sac away. */
|
||
gen_row[y][x] = '#';
|
||
changed = 1;
|
||
}
|
||
}
|
||
if (!changed) break;
|
||
}
|
||
}
|
||
|
||
/*
|
||
* Binary-tree on odd cells (corner pillars stay walls), extra orthogonal
|
||
* openings, then braid. No dead ends; no 2×2 floors; exits are U/D/L/R only.
|
||
*/
|
||
void generate_level(byte diff) {
|
||
byte x, y, i, n_en, placed;
|
||
byte w, h;
|
||
|
||
/* Size grows with progress; stay odd for the carve grid. */
|
||
w = (byte)(9 + (diff % 4) * 2); /* 9,11,13,15 */
|
||
if (w > MAP_W_MAX) w = MAP_W_MAX;
|
||
if (!(w & 1)) w--;
|
||
h = (byte)(9 + ((diff / 2) % 3) * 2); /* 9,11,13 */
|
||
if (h > MAP_H_MAX) h = MAP_H_MAX;
|
||
if (!(h & 1)) h--;
|
||
gen_w = w;
|
||
gen_h = h;
|
||
|
||
for (y = 0; y < h; y++) {
|
||
for (x = 0; x < w; x++) gen_row[y][x] = '#';
|
||
gen_row[y][w] = 0;
|
||
}
|
||
|
||
for (y = 1; y < h - 1; y += 2) {
|
||
for (x = 1; x < w - 1; x += 2) {
|
||
gen_row[y][x] = '*';
|
||
if (x + 2 <= w - 2 && y + 2 <= h - 2) {
|
||
if (rand8() & 1) gen_row[y][x + 1] = '*';
|
||
else gen_row[y + 1][x] = '*';
|
||
} else if (x + 2 <= w - 2) {
|
||
gen_row[y][x + 1] = '*';
|
||
} else if (y + 2 <= h - 2) {
|
||
gen_row[y + 1][x] = '*';
|
||
}
|
||
}
|
||
}
|
||
|
||
/* Extra orthogonal openings between floors (never pillars / 2×2). */
|
||
for (i = 0; i < (byte)(10 + diff * 2); i++) {
|
||
x = (byte)(1 + (rand8() % (w - 2)));
|
||
y = (byte)(1 + (rand8() % (h - 2)));
|
||
if (!gen_can_open(x, y)) continue;
|
||
if (gen_is_open(gen_row[y - 1][x]) || gen_is_open(gen_row[y + 1][x]) ||
|
||
gen_is_open(gen_row[y][x - 1]) || gen_is_open(gen_row[y][x + 1]))
|
||
gen_row[y][x] = '*';
|
||
}
|
||
|
||
braid_dead_ends();
|
||
gen_force_pillars();
|
||
braid_dead_ends(); /* pillars may recreate a cul-de-sac — clean again */
|
||
|
||
/* Player near top-left floor */
|
||
gen_row[1][1] = 'P';
|
||
|
||
/* 1–3 enemies toward the bottom / right */
|
||
n_en = 1;
|
||
if (diff >= 2) n_en = 2;
|
||
if (diff >= 5) n_en = 3;
|
||
placed = 0;
|
||
y = (byte)(h - 2);
|
||
while (1) {
|
||
x = (byte)(w - 2);
|
||
while (1) {
|
||
if (gen_row[y][x] == '*' && !(x <= 2 && y <= 2)) {
|
||
gen_row[y][x] = (char)('1' + placed);
|
||
placed++;
|
||
if (placed >= n_en) break;
|
||
}
|
||
if (x <= 1) break;
|
||
x--;
|
||
}
|
||
if (placed >= n_en || y <= 1) break;
|
||
y--;
|
||
}
|
||
/* Fallback if we somehow placed none (h/w odd ⇒ bottom-right interior is a room). */
|
||
if (!placed) gen_row[h - 2][w - 2] = '1';
|
||
}
|
||
|
||
void load_level_cells(const char* const* rows, byte w, byte h) {
|
||
byte x, y;
|
||
const char* row;
|
||
map_w = w;
|
||
map_h = h;
|
||
actor_n = 0;
|
||
items_count = 0;
|
||
items_collected = 0;
|
||
for (y = 0; y < h; y++) {
|
||
row = rows[y];
|
||
for (x = 0; x < w; x++) {
|
||
char c = row[x];
|
||
byte t = T_FLOOR;
|
||
if (c == ' ') t = T_VOID;
|
||
else if (c == '#') t = T_WALL;
|
||
else if (c == '*') { t = T_ITEM; items_count++; }
|
||
else if (c == 'P' || c == '1' || c == '2' || c == '3') {
|
||
Actor* a = &actors[actor_n];
|
||
a->x = TILE_TO_POS(x);
|
||
a->y = TILE_TO_POS(y);
|
||
a->cnt = 0;
|
||
a->dir = DIR_NONE;
|
||
if (c == 'P') { a->kind = 0; a->speed = 32; a->wait = 16; }
|
||
else {
|
||
a->kind = (byte)(c - '0');
|
||
a->speed = (word)(10 + ((a->kind - 1) << 1) + (game_level >> 2));
|
||
if (a->speed > 20) a->speed = 20;
|
||
a->wait = (byte)(16 + (a->kind << 4));
|
||
}
|
||
actor_n++;
|
||
t = T_FLOOR;
|
||
}
|
||
map[y * MAP_W_MAX + x] = t;
|
||
}
|
||
}
|
||
}
|
||
|
||
void load_level(byte li) {
|
||
byte x, y, i;
|
||
word map_px, map_py;
|
||
const char* rowptrs[MAP_H_MAX];
|
||
|
||
if (li < LEVELS_FIXED) {
|
||
load_level_cells(levels[li].rows, levels[li].w, levels[li].h);
|
||
} else {
|
||
generate_level((byte)(li - LEVELS_FIXED));
|
||
for (y = 0; y < gen_h; y++) rowptrs[y] = gen_row[y];
|
||
load_level_cells(rowptrs, gen_w, gen_h);
|
||
}
|
||
|
||
/*
|
||
* Place maze in the BG map. Prefer a 1-cell (2-tile) inset from the top
|
||
* when leftover room allows; keep tile origin even so 16×16 metas align.
|
||
* Outside the maze stays blank — do not invent border walls.
|
||
*/
|
||
{
|
||
byte tw = (byte)(map_w << 1);
|
||
byte th = (byte)(map_h << 1);
|
||
byte left;
|
||
if (tw < VIEW_TILES_W) {
|
||
left = (byte)(VIEW_TILES_W - tw);
|
||
map_tx0 = (byte)((left >> 1) & ~1);
|
||
} else {
|
||
map_tx0 = 0;
|
||
}
|
||
if (th < VIEW_TILES_H) {
|
||
left = (byte)(VIEW_TILES_H - th);
|
||
if (left >= 4)
|
||
map_ty0 = 2;
|
||
else
|
||
map_ty0 = (byte)(left & ~1);
|
||
} else {
|
||
map_ty0 = 0;
|
||
}
|
||
map_ox = (byte)(map_tx0 << 3);
|
||
map_oy = (byte)(map_ty0 << 3);
|
||
}
|
||
clrscr();
|
||
for (y = 0; y < map_h; y++)
|
||
for (x = 0; x < map_w; x++)
|
||
draw_cell(x, y);
|
||
for (i = actor_n; i < ACTORS_MAX; i++) hide_actor_sprites(i);
|
||
spawn_wait = (byte)(actor_n << 4);
|
||
map_px = (word)map_w * TILE_PX;
|
||
map_py = (word)map_h * TILE_PX;
|
||
cam_max_x = (map_px > VIEW_W) ? (byte)(map_px - VIEW_W) : 0;
|
||
cam_max_y = (map_py > VIEW_H) ? (byte)(map_py - VIEW_H) : 0;
|
||
cam_x = 0;
|
||
cam_y = 0;
|
||
move_bkg(cam_x, cam_y);
|
||
move_win(7, HUD_WY);
|
||
SHOW_WIN;
|
||
}
|
||
|
||
byte hit_player(void) {
|
||
byte i;
|
||
word px = actors[0].x >> FP_BITS;
|
||
word py = actors[0].y >> FP_BITS;
|
||
if (actors[0].wait) return 0;
|
||
for (i = 1; i < actor_n; i++) {
|
||
word ex, ey;
|
||
if (actors[i].wait) continue;
|
||
ex = actors[i].x >> FP_BITS;
|
||
ey = actors[i].y >> FP_BITS;
|
||
if (!((px + 4) >= (ex + 12) || (ex + 4) >= (px + 12) ||
|
||
(py + 4) >= (ey + 12) || (ey + 4) >= (py + 12)))
|
||
return 1;
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
void animate_gems(void) {
|
||
byte x, y;
|
||
if ((frame_cnt & 15) != 0) return;
|
||
for (y = 0; y < map_h; y++)
|
||
for (x = 0; x < map_w; x++)
|
||
if (map_at(x, y) == T_ITEM) draw_cell(x, y);
|
||
}
|
||
|
||
void enemy_ai(byte id) {
|
||
Actor* a = &actors[id];
|
||
byte tx = POS_TO_TILE(a->x);
|
||
byte ty = POS_TO_TILE(a->y);
|
||
byte dirs[4];
|
||
byte n = 0;
|
||
byte prev = a->dir;
|
||
byte pick;
|
||
if (prev != DIR_RIGHT && can_enter((byte)(tx - 1), ty)) dirs[n++] = DIR_LEFT;
|
||
if (prev != DIR_LEFT && can_enter((byte)(tx + 1), ty)) dirs[n++] = DIR_RIGHT;
|
||
if (prev != DIR_DOWN && can_enter(tx, (byte)(ty - 1))) dirs[n++] = DIR_UP;
|
||
if (prev != DIR_UP && can_enter(tx, (byte)(ty + 1))) dirs[n++] = DIR_DOWN;
|
||
if (!n) return;
|
||
pick = dirs[rand8() % n];
|
||
actor_try_dir(id, pick);
|
||
if (n > 1) {
|
||
Actor* p = &actors[0];
|
||
if (prev != DIR_DOWN && p->y < a->y) actor_try_dir(id, DIR_UP);
|
||
if (prev != DIR_UP && p->y > a->y) actor_try_dir(id, DIR_DOWN);
|
||
if (prev != DIR_RIGHT && p->x < a->x) actor_try_dir(id, DIR_LEFT);
|
||
if (prev != DIR_LEFT && p->x > a->x) actor_try_dir(id, DIR_RIGHT);
|
||
}
|
||
}
|
||
|
||
void player_controls(void) {
|
||
byte j = 0;
|
||
Actor* a = &actors[0];
|
||
if (joy_left) j |= DIR_LEFT;
|
||
if (joy_right) j |= DIR_RIGHT;
|
||
if (joy_up) j |= DIR_UP;
|
||
if (joy_down) j |= DIR_DOWN;
|
||
if (j & a->dir) {
|
||
j = (byte)(j & ~a->dir);
|
||
actor_try_dir(0, a->dir);
|
||
}
|
||
if (j & DIR_LEFT) actor_try_dir(0, DIR_LEFT);
|
||
if (j & DIR_RIGHT) actor_try_dir(0, DIR_RIGHT);
|
||
if (j & DIR_UP) actor_try_dir(0, DIR_UP);
|
||
if (j & DIR_DOWN) actor_try_dir(0, DIR_DOWN);
|
||
}
|
||
|
||
void advance_actor(byte id) {
|
||
Actor* a = &actors[id];
|
||
word step;
|
||
if (!a->cnt) return;
|
||
step = a->speed;
|
||
if (step > a->cnt) step = a->cnt;
|
||
if (a->dir == DIR_LEFT) a->x -= step;
|
||
else if (a->dir == DIR_RIGHT) a->x += step;
|
||
else if (a->dir == DIR_UP) a->y -= step;
|
||
else if (a->dir == DIR_DOWN) a->y += step;
|
||
a->cnt -= step;
|
||
if (!a->cnt) {
|
||
a->x &= POS_SNAP_MASK;
|
||
a->y &= POS_SNAP_MASK;
|
||
try_collect(id);
|
||
}
|
||
}
|
||
|
||
void wait_for_button(void) {
|
||
read_controls();
|
||
while (!joy_fire && !joy_start) { wait_vblank(); read_controls(); }
|
||
while (joy_fire || joy_start) { wait_vblank(); read_controls(); }
|
||
}
|
||
|
||
void draw_title_logo(byte y0) {
|
||
byte y, x;
|
||
for (y = 0; y < 6; y++)
|
||
for (x = 0; x < 20; x++)
|
||
set_bkg_tile_xy(x, (byte)(y0 + y), title_logo[y][x]);
|
||
}
|
||
|
||
void title_screen(void) {
|
||
/*
|
||
* NES starts at scroll Y=240 over a blank second nametable, then falls to 0.
|
||
* GB has one 256px BG map that wraps, so Y=240 still shows the title.
|
||
* Mirror the effect: keep the title in the top 112px, leave 112..255 blank,
|
||
* and start at SCY=112 (a full screen of blank) before falling to 0.
|
||
*/
|
||
int16_t iy, dy;
|
||
byte wait, i;
|
||
byte left_prev, right_prev;
|
||
byte lv;
|
||
show_menu_screen();
|
||
HIDE_BKG;
|
||
draw_title_logo(2);
|
||
put_str(2, 8, "COLLECT ALL GEMS");
|
||
put_str(3, 9, "AVOID ENEMIES");
|
||
put_str(3, 13, "PD SHIRU 2012");
|
||
iy = (int16_t)112 << FP_BITS;
|
||
dy = (int16_t)(-8) << FP_BITS;
|
||
move_bkg(0, (byte)(iy >> FP_BITS));
|
||
SHOW_BKG;
|
||
wait_frames(20);
|
||
wait = 160;
|
||
frame_cnt = 0;
|
||
read_controls();
|
||
while (1) {
|
||
wait_vblank();
|
||
move_bkg(0, (byte)(iy >> FP_BITS));
|
||
read_controls();
|
||
if (joy_start || joy_fire) break;
|
||
iy += dy;
|
||
if (iy < 0) {
|
||
iy = 0;
|
||
dy = (int16_t)((-dy) >> 1);
|
||
}
|
||
if (dy > ((int16_t)(-8) << FP_BITS)) dy -= 2;
|
||
if (wait) {
|
||
--wait;
|
||
} else {
|
||
if (frame_cnt & 32) put_str(4, 11, "PRESS START");
|
||
else put_str(4, 11, " ");
|
||
++frame_cnt;
|
||
}
|
||
}
|
||
move_bkg(0, 0);
|
||
put_str(4, 11, "PRESS START");
|
||
sfx_play(sfx_start);
|
||
for (i = 0; i < 16; ++i) {
|
||
if (i & 1) put_str(4, 11, " ");
|
||
else put_str(4, 11, "PRESS START");
|
||
wait_frames(4);
|
||
}
|
||
while (joy_fire || joy_start) { wait_vblank(); read_controls(); }
|
||
|
||
/* Level select — L/R change start, Start/A to play */
|
||
put_str(4, 11, " ");
|
||
put_str(3, 11, "LEVEL");
|
||
put_str(1, 14, "L R TO PICK");
|
||
put_str(4, 15, "START");
|
||
left_prev = right_prev = 1;
|
||
while (1) {
|
||
lv = (byte)(start_level + 1);
|
||
put_digit(9, 11, lv / 10);
|
||
put_digit(10, 11, lv % 10);
|
||
wait_vblank();
|
||
read_controls();
|
||
if (joy_left && !left_prev) {
|
||
if (start_level) start_level--;
|
||
}
|
||
if (joy_right && !right_prev) {
|
||
if (start_level < 29) start_level++; /* L30 = deep into procedural */
|
||
}
|
||
left_prev = joy_left;
|
||
right_prev = joy_right;
|
||
if (joy_start || joy_fire) break;
|
||
}
|
||
while (joy_fire || joy_start) { wait_vblank(); read_controls(); }
|
||
}
|
||
|
||
void show_level_banner(void) {
|
||
byte lv = (byte)(game_level + 1);
|
||
show_menu_screen();
|
||
put_str(5, 8, "LEVEL");
|
||
put_digit(11, 8, lv / 10);
|
||
put_digit(12, 8, lv % 10);
|
||
music_play(mus_level, 0);
|
||
wait_frames(50);
|
||
music_stop();
|
||
sfx_stop();
|
||
}
|
||
|
||
void show_game_over(void) {
|
||
show_menu_screen();
|
||
put_str(5, 8, "GAME OVER");
|
||
music_play(mus_gameover, 0);
|
||
wait_for_button();
|
||
music_stop();
|
||
}
|
||
|
||
void show_well_done(void) {
|
||
show_menu_screen();
|
||
put_str(5, 7, "WELL DONE");
|
||
put_str(1, 10, "ALL GEMS COLLECTED");
|
||
music_play(mus_welldone, 0);
|
||
wait_for_button();
|
||
music_stop();
|
||
}
|
||
|
||
void game_loop(void) {
|
||
byte i;
|
||
hide_all_sprites();
|
||
load_level(game_level);
|
||
draw_hud();
|
||
update_camera();
|
||
game_done = 0;
|
||
game_clear = 0;
|
||
game_paused = 0;
|
||
music_paused = 0;
|
||
frame_cnt = 0;
|
||
start_prev = 1;
|
||
music_stop();
|
||
BGP_REG = 0xE4;
|
||
while (!game_done) {
|
||
wait_vblank();
|
||
frame_cnt++;
|
||
read_controls();
|
||
if (joy_start && !start_prev) {
|
||
game_paused = !game_paused;
|
||
music_paused = game_paused;
|
||
BGP_REG = game_paused ? 0xF9 : 0xE4;
|
||
draw_hud();
|
||
}
|
||
start_prev = joy_start;
|
||
if (game_paused) { draw_actors(); continue; }
|
||
animate_gems();
|
||
|
||
if (items_collected >= items_count && !game_clear) {
|
||
game_clear = 1;
|
||
game_done = 1;
|
||
sfx_stop();
|
||
music_play(mus_clear, 0);
|
||
}
|
||
|
||
if (spawn_wait) {
|
||
--spawn_wait;
|
||
if (!spawn_wait && !music_enable)
|
||
music_play(mus_game, 1);
|
||
}
|
||
|
||
for (i = 0; i < actor_n; i++) {
|
||
if (actors[i].wait) {
|
||
if (actors[i].wait == 16)
|
||
sfx_play(i ? sfx_respawn_e : sfx_respawn_p);
|
||
actors[i].wait--;
|
||
continue;
|
||
}
|
||
if (spawn_wait) continue;
|
||
advance_actor(i);
|
||
if (!actors[i].cnt) {
|
||
if (i == 0) player_controls();
|
||
else enemy_ai(i);
|
||
}
|
||
}
|
||
update_camera();
|
||
draw_actors();
|
||
if (!game_clear && hit_player()) {
|
||
sfx_stop();
|
||
music_play(mus_lose, 0);
|
||
game_done = 1;
|
||
wait_frames(100);
|
||
}
|
||
}
|
||
if (game_clear) wait_frames(100);
|
||
music_stop();
|
||
sfx_stop();
|
||
hide_all_sprites();
|
||
HIDE_WIN;
|
||
BGP_REG = 0xE4;
|
||
}
|
||
|
||
void setup_graphics(void) {
|
||
byte x;
|
||
DISPLAY_OFF;
|
||
set_bkg_1bpp_data(0, 32, font_1bpp);
|
||
set_bkg_data(BKG_BASE, 13, bkg_tiles);
|
||
set_sprite_data(16, 12, sprite_tiles);
|
||
set_color_palettes(0);
|
||
/* same font tiles for window HUD */
|
||
for (x = 0; x < 20; x++) set_win_tile_xy(x, 0, 0);
|
||
SPRITES_8x16;
|
||
BGP_REG = 0xE4;
|
||
OBP0_REG = 0xE4;
|
||
OBP1_REG = 0xD2; /* slightly different for enemy kinds 2+ */
|
||
psg_init();
|
||
move_win(7, HUD_WY);
|
||
HIDE_WIN;
|
||
SHOW_BKG;
|
||
SHOW_SPRITES;
|
||
DISPLAY_ON;
|
||
}
|
||
|
||
void main(void) {
|
||
setup_graphics();
|
||
while (1) {
|
||
title_screen();
|
||
game_level = start_level;
|
||
game_lives = 4;
|
||
rnd ^= (word)frame_cnt << 8; /* fresh maze seed each run */
|
||
/* Levels 1–5 fixed; then endless procedural until lives run out. */
|
||
while (game_lives) {
|
||
show_level_banner();
|
||
game_loop();
|
||
if (game_clear) {
|
||
if (game_level != 255) game_level++;
|
||
} else {
|
||
game_lives--;
|
||
}
|
||
}
|
||
show_game_over();
|
||
}
|
||
}
|