diff --git a/presets/gb/chase.c b/presets/gb/chase.c new file mode 100644 index 00000000..29267de9 --- /dev/null +++ b/presets/gb/chase.c @@ -0,0 +1,1430 @@ +/* + * 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 from Coleco (NES FamiTone transcription). + * 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. + */ + +//#link "gb/sfr.sgb" +//#link "gb/crt0.sgb" +//#resource "gb/global.sgb" +#include +#include +#include "gb/types.h" +#include "gb/hardware.h" +#include "gb/gb.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; +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 }, +}; + +/* 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}*/ + 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:8,h:8,bpp:1,count:12,brev:1,np:2,pofs:1,sl:2}*/ + 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); + } +} + +byte next_music_byte(void) { + if (!music_ptr) return 0xff; + return *music_ptr++; +} + +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_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_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 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); + /* 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(); + } +} diff --git a/presets/gb/gb/bcd.h b/presets/gb/gb/bcd.h index 4f3f0049..76f503cf 100644 --- a/presets/gb/gb/bcd.h +++ b/presets/gb/gb/bcd.h @@ -1,7 +1,7 @@ #ifndef __BCD_H_INCLUDE #define __BCD_H_INCLUDE -#include +#include "types.h" #include /** @file bcd.h diff --git a/presets/gb/gb/cgb.h b/presets/gb/gb/cgb.h index 7cbded42..f2ec280e 100644 --- a/presets/gb/gb/cgb.h +++ b/presets/gb/gb/cgb.h @@ -17,7 +17,7 @@ #ifndef _CGB_H #define _CGB_H -#include +#include "types.h" #include /** Macro to create a CGB palette color entry out of 5-bit color components. diff --git a/presets/gb/gb/drawing.h b/presets/gb/gb/drawing.h index bc53d042..e4129b93 100644 --- a/presets/gb/gb/drawing.h +++ b/presets/gb/gb/drawing.h @@ -30,7 +30,7 @@ #ifndef __DRAWING_H #define __DRAWING_H -#include +#include "types.h" #include /** Size of the screen in pixels */ @@ -56,7 +56,7 @@ #define SIGNED 1 #define UNSIGNED 0 -#include +#include "types.h" /** Print the string 'str' with no interpretation @see gotogxy() diff --git a/presets/gb/gb/emu_debug.h b/presets/gb/gb/emu_debug.h index b74bec1e..51ab5c54 100644 --- a/presets/gb/gb/emu_debug.h +++ b/presets/gb/gb/emu_debug.h @@ -12,7 +12,7 @@ #ifndef __EMU_DEBUG_INCLUDE #define __EMU_DEBUG_INCLUDE -#include +#include "types.h" /** Macro to display a message in the emulator debug message window diff --git a/presets/gb/gb/gbdecompress.h b/presets/gb/gb/gbdecompress.h index 79a6c985..d645bd50 100644 --- a/presets/gb/gb/gbdecompress.h +++ b/presets/gb/gb/gbdecompress.h @@ -8,7 +8,7 @@ #ifndef __GBDECOMPRESS_H_INCLUDE #define __GBDECOMPRESS_H_INCLUDE -#include +#include "types.h" #include /** gb-decompress data from sour into dest diff --git a/presets/gb/gb/isr.h b/presets/gb/gb/isr.h index 59b2165f..01f321a7 100644 --- a/presets/gb/gb/isr.h +++ b/presets/gb/gb/isr.h @@ -10,7 +10,7 @@ #define _ISR_H_INCLUDE_ #include -#include +#include "types.h" // #define VECTOR_VBL 0x40 // you can not define raw vector for VBlank interrupt #define VECTOR_STAT 0x48 /**< Address for the STAT interrupt vector */ diff --git a/presets/gb/gb/metasprites.h b/presets/gb/gb/metasprites.h index 5bb27974..af9d077e 100644 --- a/presets/gb/gb/metasprites.h +++ b/presets/gb/gb/metasprites.h @@ -68,7 +68,7 @@ #define _METASPRITES_H_INCLUDE #include -#include +#include "types.h" #include /** Metasprite sub-item structure diff --git a/presets/gb/gb/sgb.h b/presets/gb/gb/sgb.h index ab37a04f..8cd17c4f 100644 --- a/presets/gb/gb/sgb.h +++ b/presets/gb/gb/sgb.h @@ -6,7 +6,7 @@ #ifndef _SGB_H #define _SGB_H -#include +#include "types.h" #include #define SGB_PAL_01 0x00U /**< SGB Command: Set SGB Palettes 0 & 1 */ diff --git a/presets/gb/hello.sgb b/presets/gb/hello.sgb index 78a605f3..2ccb6d43 100644 --- a/presets/gb/hello.sgb +++ b/presets/gb/hello.sgb @@ -1,5 +1,5 @@ -.include "gingerbread.sgb" +.include "gb/gingerbread.sgb" ; Set the size of the ROM file here. 0 means 32 kB, 1 means 64 kB, 2 means 128 kB and so on. ROM_SIZE = 1 diff --git a/presets/gb/pakupaku.c b/presets/gb/pakupaku.c new file mode 100644 index 00000000..a359cdaf --- /dev/null +++ b/presets/gb/pakupaku.c @@ -0,0 +1,631 @@ +/* + * Paku Paku for Game Boy — port of presets/pacman/pakupaku.c + * + * One-lane chase inspired by crisp-game-lib "PAKU PAKU": + * A / Start = reverse Left / Right = face that way + * Collect dots; power pellet makes the ghost edible. + * Movement/AI match the Pac-Man hardware port; graphics from pacman.5e/5f + * (regenerate with: python3 scripts/gen_gb_pakupaku_gfx.py). + */ + +//#link "gb/sfr.sgb" +//#link "gb/crt0.sgb" +//#resource "gb/global.sgb" +#include +#include "gb/types.h" +#include "gb/hardware.h" +#include "gb/gb.h" + +typedef uint8_t byte; +typedef uint16_t word; +typedef int8_t sbyte; + +#define FIELD_MAX 152 /* arcade 208 scaled to GB width */ +#define PLAYER_SPD 2 +#define NUM_DOTS 18 +#define HIT_DIST 12 + +#define ROW_ABOVE 8 +#define DOT_TILE_Y 9 +#define ROW_BELOW 10 +#define CORRIDOR_Y 68 + +/* BG tiles from pacman.5e (after font 0..34) */ +#define BKG_BASE 40 +#define T_WALL_ABOVE (BKG_BASE + 0) +#define T_WALL_BELOW (BKG_BASE + 1) +#define T_DOT (BKG_BASE + 2) +#define T_POWER (BKG_BASE + 3) + +/* + * Arcade pacman.5f sprites → GB 16×16 (two 8×16 OAM each). + * Each frame is 4 tiles at SPR_BASE: L-top, L-bot, R-top, R-bot. + * Regenerate: python3 scripts/gen_gb_pakupaku_gfx.py + */ +#define SPR_BASE 48 +#define SPR_FRAME(i) ((byte)(SPR_BASE + (i) * 4)) +#define SP_OPEN SPR_FRAME(0) +#define SP_MID SPR_FRAME(1) +#define SP_CLOSED SPR_FRAME(2) +#define SP_DEATH0 SPR_FRAME(3) /* 11 frames: 3..13 */ +#define SP_DEATH_N 11 +#define SP_GHOST0 SPR_FRAME(14) +#define SP_GHOST1 SPR_FRAME(15) +#define SP_SCARED0 SPR_FRAME(16) +#define SP_SCARED1 SPR_FRAME(17) +#define SP_EYES SPR_FRAME(18) +#define SPR_TILE_COUNT 76 + +typedef struct { + byte tx; + byte active; + byte is_power; +} Dot; + +word rnd = 0xCACE; +word score; +byte multiplier; +byte player_x; +sbyte player_vx; +byte enemy_x; +sbyte enemy_eye_vx; +word power_ticks; +word anim_ticks; +byte btn_prev; +byte game_over; +byte waka; +Dot dots[NUM_DOTS]; + +byte joy_left, joy_right, joy_fire, joy_start; +byte sfx_timer; +byte sfx_kind; /* 0=off 1/2=waka 3=eat 4=death 6=power */ +byte death_phase; + +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 'E' */ 0x7E,0x60,0x60,0x7C,0x60,0x60,0x7E,0x00, + /*14 'F' */ 0x7E,0x60,0x60,0x7C,0x60,0x60,0x60,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 'K' */ 0x66,0x6C,0x78,0x70,0x78,0x6C,0x66,0x00, + /*19 'L' */ 0x60,0x60,0x60,0x60,0x60,0x60,0x7E,0x00, + /*20 'M' */ 0x63,0x77,0x7F,0x6B,0x63,0x63,0x63,0x00, + /*21 'N' */ 0x66,0x76,0x7E,0x7E,0x6E,0x66,0x66,0x00, + /*22 'O' */ 0x3C,0x66,0x66,0x66,0x66,0x66,0x3C,0x00, + /*23 'P' */ 0x7C,0x66,0x66,0x7C,0x60,0x60,0x60,0x00, + /*24 'R' */ 0x7C,0x66,0x66,0x7C,0x6C,0x66,0x66,0x00, + /*25 'S' */ 0x3C,0x66,0x60,0x3C,0x06,0x66,0x3C,0x00, + /*26 'T' */ 0x7E,0x18,0x18,0x18,0x18,0x18,0x18,0x00, + /*27 'U' */ 0x66,0x66,0x66,0x66,0x66,0x66,0x3C,0x00, + /*28 'V' */ 0x66,0x66,0x66,0x66,0x66,0x3C,0x18,0x00, + /*29 'W' */ 0x63,0x63,0x63,0x6B,0x7F,0x77,0x63,0x00, + /*30 'X' */ 0x66,0x66,0x3C,0x18,0x3C,0x66,0x66,0x00, + /*31 'D' */ 0x78,0x6C,0x66,0x66,0x66,0x6C,0x78,0x00, + /*32 '-' */ 0x00,0x00,0x00,0x7E,0x00,0x00,0x00,0x00, + /*33 '!' */ 0x18,0x18,0x18,0x18,0x18,0x00,0x18,0x00, + /*34 '/' */ 0x06,0x06,0x0C,0x18,0x30,0x60,0x60,0x00, +}; + +const uint8_t bkg_tiles[] = { +/*{w:8,h:8,bpp:1,count:4,brev:1,np:2,pofs:1,sl:2}*/ + 0xff,0xff,0x00,0xff,0x00,0xff,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0x00,0xff,0x00,0xff,0xff,0xff, + 0x00,0x00,0x00,0x00,0x00,0x00,0x18,0x00,0x18,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x3c,0x00,0x7e,0x00,0xff,0x00,0xff,0x00,0xff,0x00,0xff,0x00,0x7e,0x00,0x3c,0x00, +}; + +const uint8_t sprite_tiles[] = { +/*{w:8,h:8,bpp:1,count:76,brev:1,np:2,pofs:1,sl:2}*/ + 0x00,0x00,0x07,0x07,0x1f,0x1f,0x3f,0x3f,0x3f,0x3f,0x7e,0x7e,0x7c,0x7c,0x78,0x78, + 0x7c,0x7c,0x7e,0x7e,0x3f,0x3f,0x3f,0x3f,0x1f,0x1f,0x07,0x07,0x00,0x00,0x00,0x00, + 0x00,0x00,0xc0,0xc0,0xc0,0xc0,0x80,0x80,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x80,0x80,0xc0,0xc0,0xc0,0xc0,0x00,0x00,0x00,0x00, + 0x00,0x00,0x07,0x07,0x1f,0x1f,0x3f,0x3f,0x3f,0x3f,0x7f,0x7f,0x7f,0x7f,0x78,0x78, + 0x7f,0x7f,0x7f,0x7f,0x3f,0x3f,0x3f,0x3f,0x1f,0x1f,0x07,0x07,0x00,0x00,0x00,0x00, + 0x00,0x00,0xc0,0xc0,0xf0,0xf0,0xf8,0xf8,0xf8,0xf8,0xe0,0xe0,0x00,0x00,0x00,0x00, + 0x00,0x00,0xe0,0xe0,0xf8,0xf8,0xf8,0xf8,0xf0,0xf0,0xc0,0xc0,0x00,0x00,0x00,0x00, + 0x00,0x00,0x07,0x07,0x1f,0x1f,0x3f,0x3f,0x3f,0x3f,0x7f,0x7f,0x7f,0x7f,0x7f,0x7f, + 0x7f,0x7f,0x7f,0x7f,0x3f,0x3f,0x3f,0x3f,0x1f,0x1f,0x07,0x07,0x00,0x00,0x00,0x00, + 0x00,0x00,0xc0,0xc0,0xf0,0xf0,0xf8,0xf8,0xf8,0xf8,0xfc,0xfc,0xfc,0xfc,0xfc,0xfc, + 0xfc,0xfc,0xfc,0xfc,0xf8,0xf8,0xf8,0xf8,0xf0,0xf0,0xc0,0xc0,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x60,0x60,0x70,0x70,0x78,0x78,0x7c,0x7c,0x7e,0x7e, + 0x3f,0x3f,0x3f,0x3f,0x1f,0x1f,0x07,0x07,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x0c,0x0c,0x1c,0x1c,0x3c,0x3c,0x7c,0x7c,0xfc,0xfc, + 0xf8,0xf8,0xf8,0xf8,0xf0,0xf0,0xc0,0xc0,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x40,0x40,0xf0,0xf0,0xf8,0xf8,0xfe,0xfe, + 0x7f,0x7f,0x7f,0x7f,0x3f,0x3f,0x0e,0x0e,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x04,0x04,0x1e,0x1e,0x3e,0x3e,0xfe,0xfe, + 0xfc,0xfc,0xfc,0xfc,0xf8,0xf8,0xe0,0xe0,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xe0,0xe0,0xfc,0xfc, + 0xff,0xff,0x7f,0x7f,0x3f,0x3f,0x0e,0x0e,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x0e,0x0e,0x7e,0x7e, + 0xfe,0xfe,0xfc,0xfc,0xf8,0xf8,0xe0,0xe0,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xf0,0xf0, + 0xff,0xff,0xff,0xff,0x7f,0x7f,0x1e,0x1e,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x1e,0x1e, + 0xfe,0xfe,0xfe,0xfe,0xfc,0xfc,0xf0,0xf0,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x0f,0x0f,0xff,0xff,0xff,0xff,0x7f,0x7f,0x1e,0x1e,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0xe0,0xe0,0xfe,0xfe,0xfe,0xfe,0xfc,0xfc,0xf0,0xf0,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x03,0x03,0x0f,0x0f,0x7f,0x7f,0xff,0xff,0x7e,0x7e,0x1c,0x1c,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x80,0x80,0xe0,0xe0,0xfc,0xfc,0xfe,0xfe,0xfc,0xfc,0x70,0x70,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x01,0x01,0x03,0x03,0x0f,0x0f,0x1f,0x1f,0x7f,0x7f,0x7e,0x7e,0x3c,0x3c,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x80,0x80,0xe0,0xe0,0xf0,0xf0,0xfc,0xfc,0xfc,0xfc,0x78,0x78,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x01,0x01,0x03,0x03,0x07,0x07,0x07,0x07,0x0f,0x0f,0x1f,0x1f,0x0e,0x0e,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x80,0x80,0xc0,0xc0,0xc0,0xc0,0xe0,0xe0,0xf0,0xf0,0xe0,0xe0,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x01,0x01,0x01,0x01,0x03,0x03,0x03,0x03,0x03,0x03,0x07,0x07,0x02,0x02,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x80,0x80,0x80,0x80,0x80,0x80,0xc0,0xc0,0x80,0x80,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x04,0x04,0x12,0x12, + 0x08,0x08,0x00,0x00,0x30,0x30,0x00,0x00,0x08,0x08,0x10,0x10,0x02,0x02,0x04,0x04, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x40,0x40,0x80,0x80, + 0x10,0x10,0x20,0x20,0x00,0x00,0x18,0x18,0x00,0x00,0x20,0x20,0x90,0x90,0x40,0x40, + 0x00,0x00,0x03,0x03,0x0f,0x0f,0x1f,0x1f,0x3f,0x39,0x3f,0x30,0x3c,0x33,0x7c,0x73, + 0x7f,0x79,0x7f,0x7f,0x7f,0x7f,0x7f,0x7f,0x7f,0x7f,0x6e,0x6e,0x46,0x46,0x00,0x00, + 0x00,0x00,0xc0,0xc0,0xf0,0xf0,0xf8,0xf8,0xfc,0xe4,0xfc,0xc0,0xf0,0xcc,0xf2,0xce, + 0xfe,0xe6,0xfe,0xfe,0xfe,0xfe,0xfe,0xfe,0xfe,0xfe,0x76,0x76,0x62,0x62,0x00,0x00, + 0x00,0x00,0x03,0x03,0x0f,0x0f,0x1f,0x1f,0x3f,0x39,0x3f,0x30,0x3c,0x33,0x7c,0x73, + 0x7f,0x79,0x7f,0x7f,0x7f,0x7f,0x7f,0x7f,0x7f,0x7f,0x7b,0x7b,0x31,0x31,0x00,0x00, + 0x00,0x00,0xc0,0xc0,0xf0,0xf0,0xf8,0xf8,0xfc,0xe4,0xfc,0xc0,0xf0,0xcc,0xf2,0xce, + 0xfe,0xe6,0xfe,0xfe,0xfe,0xfe,0xfe,0xfe,0xfe,0xfe,0xde,0xde,0x8c,0x8c,0x00,0x00, + 0x00,0x00,0x03,0x03,0x0f,0x0f,0x1f,0x1f,0x3f,0x3f,0x3f,0x3f,0x3f,0x39,0x7f,0x79, + 0x7f,0x7f,0x7f,0x7f,0x7f,0x66,0x7f,0x59,0x7f,0x7f,0x6e,0x6e,0x46,0x46,0x00,0x00, + 0x00,0x00,0xc0,0xc0,0xf0,0xf0,0xf8,0xf8,0xfc,0xfc,0xfc,0xfc,0xfc,0x9c,0xfe,0x9e, + 0xfe,0xfe,0xfe,0xfe,0xfe,0x66,0xfe,0x9a,0xfe,0xfe,0x76,0x76,0x62,0x62,0x00,0x00, + 0x00,0x00,0x03,0x03,0x0f,0x0f,0x1f,0x1f,0x3f,0x3f,0x3f,0x3f,0x3f,0x39,0x7f,0x79, + 0x7f,0x7f,0x7f,0x7f,0x7f,0x66,0x7f,0x59,0x7f,0x7f,0x7b,0x7b,0x31,0x31,0x00,0x00, + 0x00,0x00,0xc0,0xc0,0xf0,0xf0,0xf8,0xf8,0xfc,0xfc,0xfc,0xfc,0xfc,0x9c,0xfe,0x9e, + 0xfe,0xfe,0xfe,0xfe,0xfe,0x66,0xfe,0x9a,0xfe,0xfe,0xde,0xde,0x8c,0x8c,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x06,0x00,0x0f,0x00,0x0c,0x00,0x0c,0x00, + 0x06,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x18,0x00,0x3c,0x00,0x30,0x00,0x30,0x00, + 0x18,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, +}; + +byte rand8(void) { + rnd = rnd * 17 + 53; + return (byte)(rnd >> 8); +} + +byte abs_diff(byte a, byte b) { + return (a > b) ? (byte)(a - b) : (byte)(b - a); +} + +byte glyph(char c) { + if (c == ' ') return 0; + if (c >= '0' && c <= '9') return (byte)(1 + (c - '0')); + switch (c) { + case 'A': return 11; case 'C': return 12; case 'E': return 13; + case 'F': return 14; case 'G': return 15; case 'H': return 16; + case 'I': return 17; case 'K': return 18; case 'L': return 19; + case 'M': return 20; case 'N': return 21; case 'O': return 22; + case 'P': return 23; case 'R': return 24; case 'S': return 25; + case 'T': return 26; case 'U': return 27; case 'V': return 28; + case 'W': return 29; case 'X': return 30; case 'D': return 31; + case '-': return 32; case '!': return 33; case '/': return 34; + 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 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_all_sprites(void) { + byte i; + for (i = 0; i < 40; i++) move_sprite(i, 0, 0); +} + +void read_controls(void) { + byte j = joypad(); + joy_left = (j & J_LEFT) != 0; + joy_right = (j & J_RIGHT) != 0; + joy_fire = (j & J_A) != 0; + joy_start = (j & J_START) != 0; +} + +void psg_init(void) { + NR52_REG = 0x80; + NR50_REG = 0x77; + NR51_REG = 0xFF; + NR10_REG = 0x00; +} + +void sfx_off(void) { + sfx_timer = 0; + sfx_kind = 0; + NR12_REG = 0x00; + NR14_REG = 0x80; + NR22_REG = 0x00; + NR24_REG = 0x80; +} + +void beep(word per, byte vol, byte len) { + /* ch1 one-shot */ + NR10_REG = 0x00; + NR11_REG = 0x80; + NR12_REG = (byte)((vol << 4) | 0x01); + NR13_REG = (byte)(per & 0xff); + NR14_REG = (byte)(0x80 | ((per >> 8) & 0x07)); + sfx_timer = len; +} + +void play_sfx(byte id) { + sfx_kind = id; + if (id == 4) death_phase = 0; + switch (id) { + case 1: beep(0x6C0, 10, 3); break; /* waka hi */ + case 2: beep(0x650, 9, 3); break; /* waka lo */ + case 3: beep(0x700, 12, 12); break; /* eat ghost */ + case 4: beep(0x480, 12, 40); break; /* death */ + case 6: beep(0x720, 11, 8); break; /* power */ + default: sfx_off(); break; + } +} + +void tick_sfx(void) { + if (!sfx_timer) return; + --sfx_timer; + if (!sfx_timer && sfx_kind == 4) { + death_phase++; + if (death_phase < 4) { + beep((word)(0x480 - death_phase * 0x40), 10, 10); + sfx_kind = 4; + } else { + death_phase = 0; + sfx_kind = 0; + } + } +} + +void wait_vblank(void) { + wait_vbl_done(); + tick_sfx(); +} + +void wait_frames(byte n) { + while (n--) wait_vblank(); +} + +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_hud(void) { + put_str(0, 0, "PAKU PAKU"); + put_str(0, 1, "SCORE"); + put_digit(6, 1, (byte)((score / 1000) % 10)); + put_digit(7, 1, (byte)((score / 100) % 10)); + put_digit(8, 1, (byte)((score / 10) % 10)); + put_digit(9, 1, (byte)(score % 10)); + put_str(12, 1, "X"); + put_digit(13, 1, multiplier > 9 ? 9 : multiplier); + put_str(0, 17, "A=TURN"); +} + +void draw_corridor(void) { + byte x; + for (x = 0; x < 20; x++) { + set_bkg_tile_xy(x, ROW_ABOVE, T_WALL_ABOVE); + set_bkg_tile_xy(x, DOT_TILE_Y, 0); + set_bkg_tile_xy(x, ROW_BELOW, T_WALL_BELOW); + } +} + +void add_dots(void) { + byte i, pi; + pi = (player_x > 80) ? (byte)(1 + (rand8() % 7)) : (byte)(10 + (rand8() % 7)); + for (i = 0; i < NUM_DOTS; i++) { + dots[i].tx = (byte)(i + 1); + dots[i].active = 1; + dots[i].is_power = (i == pi); + } + if (multiplier < 99) multiplier++; +} + +void draw_dots(void) { + byte i; + for (i = 0; i < NUM_DOTS; i++) { + if (!dots[i].active) { + set_bkg_tile_xy(dots[i].tx, DOT_TILE_Y, 0); + continue; + } + if (dots[i].is_power) { + if ((anim_ticks >> 3) & 1) + set_bkg_tile_xy(dots[i].tx, DOT_TILE_Y, T_POWER); + else + set_bkg_tile_xy(dots[i].tx, DOT_TILE_Y, 0); + } else { + set_bkg_tile_xy(dots[i].tx, DOT_TILE_Y, T_DOT); + } + } +} + +void collect_dots(void) { + byte i, ptx; + ptx = (byte)((player_x + 8) >> 3); + if (ptx < 1) ptx = 1; + if (ptx > NUM_DOTS) ptx = NUM_DOTS; + for (i = 0; i < NUM_DOTS; i++) { + if (!dots[i].active) continue; + if (dots[i].tx != ptx) continue; + dots[i].active = 0; + set_bkg_tile_xy(dots[i].tx, DOT_TILE_Y, 0); + if (dots[i].is_power) { + play_sfx(6); + if (!enemy_eye_vx) power_ticks = 200; + } else { + waka ^= 1; + play_sfx(waka ? 1 : 2); + } + score += multiplier; + } +} + +byte dots_left(void) { + byte i, n = 0; + for (i = 0; i < NUM_DOTS; i++) + if (dots[i].active) n++; + return n; +} + +byte pac_frame(void) { + byte ai = (byte)(anim_ticks & 3); + if (ai == 0) return SP_OPEN; + if (ai == 1) return SP_MID; + if (ai == 2) return SP_CLOSED; + return SP_MID; +} + +void put_actor16(byte i0, byte frame, byte flip, byte prop, byte x, byte y) { + /* Two 8×16 sprites = one 16×16 arcade sprite; swap halves when flipped. */ + byte i1 = (byte)(i0 + 1); + byte p = prop; + byte oam0, oam1; + if (flip) p |= S_FLIPX; + set_sprite_tile(i0, frame); + set_sprite_tile(i1, (byte)(frame + 2)); + set_sprite_prop(i0, p); + set_sprite_prop(i1, p); + if (flip) { + oam0 = (byte)(x + 16); + oam1 = (byte)(x + 8); + } else { + oam0 = (byte)(x + 8); + oam1 = (byte)(x + 16); + } + /* Hide if past the right edge (OAM X wraps). */ + if (x > 152) { + move_sprite(i0, 0, 0); + move_sprite(i1, 0, 0); + return; + } + move_sprite(i0, oam0, (byte)(y + 16)); + move_sprite(i1, oam1, (byte)(y + 16)); +} + +void draw_actors(void) { + byte ghost, gprop, pflip, gflip; + sbyte evx; + + pflip = (player_vx < 0) ? 1 : 0; + put_actor16(0, pac_frame(), pflip, 0, player_x, CORRIDOR_Y); + + if (enemy_eye_vx) { + ghost = SP_EYES; + gflip = (enemy_eye_vx < 0) ? 1 : 0; + gprop = 0; + } else if (power_ticks) { + ghost = ((anim_ticks >> 1) & 1) ? SP_SCARED1 : SP_SCARED0; + gflip = 0; + gprop = 0; + /* blink near end of power */ + if (power_ticks < 60 && (anim_ticks & 0x08)) { + move_sprite(2, 0, 0); + move_sprite(3, 0, 0); + return; + } + } else { + ghost = ((anim_ticks >> 1) & 1) ? SP_GHOST1 : SP_GHOST0; + evx = (player_x > enemy_x) ? 1 : -1; + gflip = (evx < 0) ? 1 : 0; + gprop = 0; + } + put_actor16(2, ghost, gflip, gprop, enemy_x, CORRIDOR_Y); +} + +void title_screen(void) { + byte x; + clrscr(); + hide_all_sprites(); + move_bkg(0, 0); + /* corridor stripe behind the demo sprites */ + for (x = 0; x < 20; x++) { + set_bkg_tile_xy(x, 6, T_WALL_ABOVE); + set_bkg_tile_xy(x, 7, 0); + set_bkg_tile_xy(x, 8, T_WALL_BELOW); + } + put_str(5, 2, "PAKU PAKU"); + put_actor16(0, SP_OPEN, 0, 0, 52, 48); + put_actor16(2, SP_GHOST0, 0, 0, 92, 48); + put_str(4, 11, "A TO TURN"); + put_str(2, 13, "EAT DOTS EAT GHOST"); + put_str(4, 16, "PRESS START"); + wait_for_button(); + sfx_off(); +} + +void show_game_over(void) { + byte frame_i, t, frame; + hide_all_sprites(); + put_actor16(0, SP_OPEN, 0, 0, player_x, CORRIDOR_Y); + play_sfx(4); + /* Arcade: 11 frames × 8 vblanks */ + for (frame_i = 0; frame_i < SP_DEATH_N; frame_i++) { + frame = (byte)(SP_DEATH0 + (byte)(frame_i * 4)); + for (t = 0; t < 8; t++) { + put_actor16(0, frame, 0, 0, player_x, CORRIDOR_Y); + wait_vblank(); + read_controls(); + if (joy_fire || joy_start) goto death_done; + } + } +death_done: + hide_all_sprites(); + put_str(5, 7, "GAME OVER"); + put_str(4, 10, "PRESS START"); + wait_for_button(); + sfx_off(); +} + +void reset_round(void) { + player_x = 48; + player_vx = 1; + enemy_x = 128; + enemy_eye_vx = 0; + power_ticks = 0; + anim_ticks = 0; + btn_prev = 1; + game_over = 0; + waka = 0; + sfx_off(); + draw_corridor(); + add_dots(); +} + +void game_loop(void) { + byte btn; + sbyte evx; + byte espd; + + clrscr(); + hide_all_sprites(); + move_bkg(0, 0); + draw_corridor(); + draw_hud(); + reset_round(); + + while (!game_over) { + wait_vblank(); + anim_ticks++; + read_controls(); + + btn = (joy_fire || joy_start) ? 1 : 0; + if (btn && !btn_prev) player_vx = (sbyte)-player_vx; + btn_prev = btn; + if (joy_left) player_vx = -1; + if (joy_right) player_vx = 1; + + /* Same wrap as pacman pakupaku.c */ + player_x = (byte)(player_x + player_vx * PLAYER_SPD); + if (player_x < 4) player_x = FIELD_MAX; + else if (player_x > FIELD_MAX) player_x = 4; + + collect_dots(); + if (!dots_left()) add_dots(); + + /* Ghost AI — identical rules to presets/pacman/pakupaku.c */ + if (enemy_eye_vx) { + evx = enemy_eye_vx; + espd = 3; + } else if (power_ticks) { + evx = (player_x > enemy_x) ? (sbyte)-1 : 1; + espd = 1; + } else { + evx = (player_x > enemy_x) ? 1 : -1; + espd = 2; + } + if (evx > 0) { + if (enemy_x < FIELD_MAX - 2) enemy_x = (byte)(enemy_x + espd); + else if (enemy_eye_vx) enemy_eye_vx = 0; + } else { + if (enemy_x > 6) enemy_x = (byte)(enemy_x - espd); + else if (enemy_eye_vx) enemy_eye_vx = 0; + } + + if (!enemy_eye_vx && abs_diff(player_x, enemy_x) < HIT_DIST) { + if (power_ticks) { + play_sfx(3); + score += (word)(10 * (multiplier ? multiplier : 1)); + enemy_eye_vx = (player_x > 80) ? (sbyte)-1 : 1; + power_ticks = 0; + if (multiplier < 99) multiplier++; + } else { + game_over = 1; + } + } + + if (power_ticks) power_ticks--; + + draw_dots(); + draw_hud(); + draw_actors(); + } +} + +void setup_graphics(void) { + DISPLAY_OFF; + set_bkg_1bpp_data(0, 35, font_1bpp); + set_bkg_data(BKG_BASE, 4, bkg_tiles); + set_sprite_data(SPR_BASE, SPR_TILE_COUNT, sprite_tiles); + SPRITES_8x16; + BGP_REG = 0xE4; + OBP0_REG = 0xE4; + OBP1_REG = 0xE4; + psg_init(); + HIDE_WIN; + SHOW_BKG; + SHOW_SPRITES; + DISPLAY_ON; +} + +void main(void) { + setup_graphics(); + score = 0; + multiplier = 0; + while (1) { + title_screen(); + score = 0; + multiplier = 0; + game_loop(); + show_game_over(); + } +} diff --git a/src/common/gbpalette.ts b/src/common/gbpalette.ts new file mode 100644 index 00000000..88fa3814 --- /dev/null +++ b/src/common/gbpalette.ts @@ -0,0 +1,43 @@ +/** + * Shared Game Boy DMG (olive LCD) palette. + * + * Shade 0 = lightest (LCD pixel off / "white"), shade 3 = darkest (pixel on). + * Values are packed for Canvas ImageData as Uint32 little-endian RGBA (= 0xAABBGGRR). + */ + +export function rgbToPixel(r: number, g: number, b: number): number { + return 0xFF000000 | ((b & 0xff) << 16) | ((g & 0xff) << 8) | (r & 0xff); +} + +/** Four DMG shades, lightest → darkest (matches BGP/OBP shade indices). */ +export const DMG_PALETTE: number[] = [ + rgbToPixel(0xd0, 0xd8, 0x84), // Lightest (warm mint) + rgbToPixel(0x87, 0xa8, 0x4c), // Light mid + rgbToPixel(0x52, 0x6b, 0x34), // Dark mid + rgbToPixel(0x2d, 0x31, 0x22), // Darkest (deep olive) +]; + +/** RGB888 triples matching DMG_PALETTE (for boytacean remap / tooling). */ +export const DMG_PALETTE_RGB: [number, number, number][] = [ + [0xd0, 0xd8, 0x84], + [0x87, 0xa8, 0x4c], + [0x52, 0x6b, 0x34], + [0x2d, 0x31, 0x22], +]; + +/** Asset-editor / PREDEF_PALETTES entries (RGB without alpha; callers OR 0xff000000). */ +export const DMG_PALETTE_RGB24: number[] = [ + 0xd0d884, + 0x87a84c, + 0x526b34, + 0x2d3122, +]; + +/** Map a greyscale / near-grey RGB888 pixel to a DMG shade (0=light … 3=dark). */ +export function dmgShadeFromRgb(r: number, g: number, b: number): number { + var y = (r + g + b) / 3; + if (y >= 192) return 0; + if (y >= 128) return 1; + if (y >= 64) return 2; + return 3; +} diff --git a/src/ide/pixeleditor.ts b/src/ide/pixeleditor.ts index 2e98f828..6e21c71b 100644 --- a/src/ide/pixeleditor.ts +++ b/src/ide/pixeleditor.ts @@ -14,7 +14,7 @@ export interface EditorContext { } export type SelectablePalette = { - node: PixNode + node: PixNode | null name: string palette: Uint32Array } @@ -224,7 +224,11 @@ export function validateAssetData(datastr: string, fmt): string | null { maxOffset = Math.max(maxOffset, offset); } } - var required = maxOffset + (nplanes > 1 ? (nplanes - 1) * pofs : 0) + 1 + skip; + // Planar formats (e.g. NES pofs>=wpimg) store extra planes past each image block. + // Interleaved formats (GB/SMS pofs < wpimg) already include plane bytes in maxOffset. + var planeExtent = (nplanes > 1) ? (nplanes - 1) * pofs : 0; + if (planeExtent < wpimg) planeExtent = 0; + var required = maxOffset + planeExtent + 1 + skip; if (words.length != required) { return `Expected ${required} value(s), found ${words.length}`; @@ -341,6 +345,10 @@ export function convertPaletteFormat(palbytes: UintArray, palfmt: PixelEditorPal // TODO: illegal colors? const PREDEF_PALETTES = { + // LCD polarity: index 0 = lightest (pixel off), 3 = darkest (pixel on) + 'gb': [ + 0xd0d884, 0x87a84c, 0x526b34, 0x2d3122, + ], 'nes': [ 0x525252, 0xB40000, 0xA00000, 0xB1003D, 0x740069, 0x00005B, 0x00005F, 0x001840, 0x002F10, 0x084A08, 0x006700, 0x124200, 0x6D2800, 0x000000, 0x000000, 0x000000, 0xC4D5E7, 0xFF4000, 0xDC0E22, 0xFF476B, 0xD7009F, 0x680AD7, 0x0019BC, 0x0054B1, 0x006A5B, 0x008C03, 0x00AB00, 0x2C8800, 0xA47200, 0x000000, 0x000000, 0x000000, diff --git a/src/ide/views/asseteditor.ts b/src/ide/views/asseteditor.ts index 2c94c83d..b32d0a98 100644 --- a/src/ide/views/asseteditor.ts +++ b/src/ide/views/asseteditor.ts @@ -1,6 +1,7 @@ import { newDiv, ProjectView } from "./baseviews"; import { platform_id, current_project, projectWindows } from "../ui"; +import { DMG_PALETTE } from "../../common/gbpalette"; import { FileData } from "../../common/workertypes"; import { hex, safeident, rgb2bgr } from "../../common/util"; import * as pixed from "../pixeleditor"; @@ -80,6 +81,18 @@ export class AssetEditorView implements ProjectView, pixed.EditorContext { node = node.right; } }); + // Game Boy: default to LCD green scale (0=light … 3=dark) when no project palette + if (result.length == 0 && platform_id && platform_id.startsWith('gb')) { + if (matchlen == 4) { + result.push({ node: null, name: "DMG Green", palette: new Uint32Array(DMG_PALETTE) }); + } else if (matchlen == 2) { + result.push({ + node: null, + name: "DMG Green", + palette: new Uint32Array([DMG_PALETTE[0], DMG_PALETTE[3]]), + }); + } + } return result; } diff --git a/src/machine/gb.ts b/src/machine/gb.ts index 230055b1..7ade561c 100644 --- a/src/machine/gb.ts +++ b/src/machine/gb.ts @@ -2,16 +2,9 @@ import { SM83, SM83State } from "../common/cpu/SM83"; import { BasicScanlineMachine, Bus } from "../common/devices"; import { newAddressDecoder, padBytes, Keys, makeKeycodeMap, newKeyboardHandler, EmuHalt } from "../common/emu"; +import { DMG_PALETTE } from "../common/gbpalette"; import { hex } from "../common/util"; -// Game Boy DMG palette: 4 shades of green (darkest to lightest) -const DMG_PALETTE: number[] = [ - 0xFFd0d884, // Lightest (Warm Mint) - 0xFF87a84c, // Light Mid - 0xFF526b34, // Dark Mid - 0xFF2d3122, // Darkest (Deep Olive) -]; - // Convert GBC 15-bit RGB (xBBBBBGGGGGRRRRR) to 32-bit ARGB function cgbColorToARGB(lo: number, hi: number): number { var r5 = lo & 0x1F; diff --git a/src/platform/gb.ts b/src/platform/gb.ts index b94f9657..1e06cf5b 100644 --- a/src/platform/gb.ts +++ b/src/platform/gb.ts @@ -7,6 +7,8 @@ const GB_PRESETS: Preset[] = [ { id: 'hello.c', name: 'Hello World (C)' }, { id: 'testdrawing.c', name: 'Drawing Routines (C)' }, { id: 'testphys.c', name: 'Sprite Test (C)' }, + { id: 'chase.c', name: "Shiru's Chase Game" }, + { id: 'pakupaku.c', name: 'Paku Paku' }, { id: 'hello.sgb', name: 'Hello World (ASM)' }, { id: 'main.wiz', name: 'Snake Game (Wiz)' }, ];