#include #include #include #include #include #include "engine.h" // Generated by png2asset #include "tiles.h" #include "player.h" #include "gameover.h" #include "stamina.h" #include "title_bg.h" // Music variables static volatile uint8_t music_timer = 0; static volatile uint8_t music_step = 0; volatile uint8_t game_over = 0; volatile uint8_t game_over_timer = 0; // Game Over creepy melody variables static volatile uint8_t gameover_music_timer = 0; static volatile uint8_t gameover_music_step = 0; // Notes frequencies #define R 0 #define N_E2 458 #define N_F2 547 #define N_G2 742 #define N_A2 857 #define N_AS2 907 #define N_B2 961 #define N_C3 1046 #define N_CS3 1103 #define N_D3 1155 #define N_E3 1253 #define N_F3 1298 #define N_FS3 1340 #define N_G3 1380 #define N_GS3 1417 #define N_A3 1453 #define N_AS3 1486 #define N_B3 1517 #define N_C4 1547 #define N_CS4 1575 #define N_D4 1602 #define N_E4 1651 #define N_F4 1673 #define N_FS4 1694 #define N_G4 1714 #define N_GS4 1732 #define N_A4 1751 #define N_AS4 1767 #define N_B4 1783 #define N_C5 1798 #define N_CS5 1812 #define N_D5 1825 #define N_E5 1849 #define N_F5 1860 #define N_FS5 1871 #define N_G5 1881 #define N_GS5 1890 #define N_A5 1899 const uint16_t ch1_seq[128] = { // 1 Dm N_D3, N_A3, N_D4, N_A3, N_D3, N_A3, N_D4, N_A3, // 2 A/C# N_CS3, N_A3, N_CS4, N_A3, N_CS3, N_A3, N_CS4, N_A3, // 3 F/C N_C3, N_A3, N_C4, N_A3, N_C3, N_A3, N_C4, N_A3, // 4 G/B N_B2, N_G3, N_B3, N_G3, N_B2, N_G3, N_B3, N_G3, // 5 Bb N_AS2, N_F3, N_AS3, N_F3, N_AS2, N_F3, N_AS3, N_F3, // 6 F/A N_A2, N_F3, N_A3, N_F3, N_A2, N_F3, N_A3, N_F3, // 7 Gm N_G2, N_D3, N_G3, N_D3, N_G2, N_D3, N_G3, N_D3, // 8 A7 N_A2, N_E3, N_A3, N_CS4, N_A2, N_E3, N_A3, N_CS4, // 9 Dm (intensifies: higher arpeggio) N_D3, N_D4, N_F4, N_A4, N_D3, N_D4, N_F4, N_A4, // 10 A/C# N_CS3, N_CS4, N_E4, N_A4, N_CS3, N_CS4, N_E4, N_A4, // 11 F/C N_C3, N_C4, N_F4, N_A4, N_C3, N_C4, N_F4, N_A4, // 12 G/B N_B2, N_B3, N_D4, N_G4, N_B2, N_B3, N_D4, N_G4, // 13 Bb N_AS2, N_AS3, N_D4, N_F4, N_AS2, N_AS3, N_D4, N_F4, // 14 F/A N_A2, N_A3, N_C4, N_F4, N_A2, N_A3, N_C4, N_F4, // 15 Gm N_G2, N_G3, N_AS3, N_D4, N_G2, N_G3, N_AS3, N_D4, // 16 A7 -> Dm resolution N_A2, N_A3, N_CS4, N_E4, N_A2, R, N_D3, R }; const uint16_t ch2_seq[128] = { // 1 Dm N_F4, R, R, R, N_E4, R, R, R, // 2 A/C# N_E4, R, R, R, N_D4, R, R, R, // 3 F/C N_C4, R, R, R, N_D4, R, N_E4, R, // 4 G/B N_D4, R, R, R, N_F4, R, R, R, // 5 Bb N_D4, R, R, R, N_C4, R, R, R, // 6 F/A N_C4, R, R, R, N_AS3, R, R, R, // 7 Gm N_AS3, R, N_A3, N_AS3, N_C4, R, N_AS3, R, // 8 A7 N_A3, R, R, R, R, R, R, R, // 9 Dm N_A4, R, R, R, N_G4, R, N_F4, N_E4, // 10 A/C# N_G4, R, R, R, N_F4, R, N_E4, N_D4, // 11 F/C N_F4, R, R, R, N_E4, R, N_C4, R, // 12 G/B N_D4, R, R, R, N_G4, R, R, R, // 13 Bb N_F4, R, R, R, N_E4, R, N_D4, N_CS4, // 14 F/A N_E4, R, R, R, N_D4, R, N_C4, N_B3, // 15 Gm N_D4, R, R, R, N_C4, R, N_AS3, R, // 16 A7 -> Dm resolution N_A3, R, R, R, R, R, N_D4, R }; // Eerie pulse note frequencies (96-note sequence for normal gameplay) const uint16_t eerie_reg_vals[] = { // Pattern 1 (A minor) 1453, 1546, 1650, 1733, 1754, 1733, 1650, 1546, 1453, 1546, 1650, 1733, 1754, 1733, 1650, 1546, 1453, 1546, 1650, 1733, 1754, 1733, 1650, 1546, 1453, 1546, 1650, 1733, 1754, 1733, 1650, 1546, // Pattern 2 (D minor) 1303, 1453, 1546, 1650, 1678, 1650, 1546, 1453, 1303, 1453, 1546, 1650, 1678, 1650, 1546, 1453, 1303, 1453, 1546, 1650, 1678, 1650, 1546, 1453, 1303, 1453, 1546, 1650, 1678, 1650, 1546, 1453, // Pattern 3 (E7 tension) 1369, 1497, 1589, 1709, 1733, 1709, 1589, 1497, 1369, 1497, 1589, 1709, 1733, 1709, 1589, 1497, 1369, 1497, 1589, 1709, 1733, 1709, 1589, 1497, 1369, 1497, 1589, 1709, 1733, 1709, 1589, 1497 }; // Title Music Variables static volatile uint8_t title_music_timer = 0; static volatile uint8_t title_music_step = 0; const uint16_t title_melody[32] = { N_A2, N_E3, N_A3, N_E3, N_C4, N_A3, N_E3, N_A2, // Am N_G2, N_D3, N_G3, N_D3, N_B3, N_G3, N_D3, N_G2, // G N_F2, N_C3, N_F3, N_C3, N_A3, N_F3, N_C3, N_F2, // F N_E2, N_B2, N_E3, N_B2, N_GS3, N_E3, N_B2, N_E2 // E }; static void play_note(uint16_t reg_val) { NR21_REG = 0x80; // 50% duty cycle NR22_REG = 0x73; // volume 7, fade envelope NR23_REG = reg_val & 0xFF; NR24_REG = (reg_val >> 8) | 0x80; // trigger note } static void play_gameover_step(uint8_t step) { if (step < 128) { uint16_t n1 = ch1_seq[step]; if (n1 != R) { NR10_REG = 0x00; // No sweep NR11_REG = 0x80; // 50% duty NR12_REG = 0x73; // volume 7, fast fade for arpeggio pluck NR13_REG = n1 & 0xFF; NR14_REG = (n1 >> 8) | 0x80; } uint16_t n2 = ch2_seq[step]; if (n2 != R) { NR21_REG = 0x80; // 50% duty NR22_REG = 0xA7; // volume 10, long fade for melody sustain NR23_REG = n2 & 0xFF; NR24_REG = (n2 >> 8) | 0x80; } // Add dramatic percussion on the first beat of each chord (every 8 steps) if (step % 8 == 0) { NR41_REG = 0x01; NR42_REG = 0xB2; // Volume 11, quick fade // step < 64 gets a lower tympani, step >= 64 gets a crash if (step < 64) { NR43_REG = 0x68; // Low thud } else { NR43_REG = 0x42; // Crash } NR44_REG = 0x80; } } } static void play_music_tick(void) { if (app_state == 0) { title_music_timer++; if (title_music_timer >= 30) { // 30 frames per note title_music_timer = 0; uint16_t n = title_melody[title_music_step]; NR10_REG = 0; NR11_REG = 0x80; NR12_REG = 0x63; // soft fade NR13_REG = n & 0xFF; NR14_REG = (n >> 8) | 0x80; // Add a slow bass note every 4 steps if (title_music_step % 4 == 0) { NR21_REG = 0x80; NR22_REG = 0x77; uint16_t bn = n / 2; // Octave lower NR23_REG = bn & 0xFF; NR24_REG = (bn >> 8) | 0x80; } title_music_step++; if (title_music_step >= 32) title_music_step = 0; } return; } if (game_over) { if (game_over_timer == 0) { gameover_music_timer++; if (gameover_music_timer >= 10) { // 10 frames per note (~6 notes/sec) gameover_music_timer = 0; if (gameover_music_step < 128) { play_gameover_step(gameover_music_step); gameover_music_step++; } } } return; } music_timer++; if (music_timer >= 20) { music_timer = 0; play_note(eerie_reg_vals[music_step]); music_step++; if (music_step >= 96) { music_step = 0; } } } #define MAP_SIZE 7 uint8_t maze[MAP_SIZE][MAP_SIZE]; static uint8_t map_buffer[32 * 32]; uint8_t player_lx = 1; uint8_t player_ly = 1; uint8_t player_dir = 0; // 0=DR, 1=DL, 2=UL, 3=UR uint8_t scroll_x = 0; uint8_t scroll_y = 0; // Movement transition variables uint8_t is_moving = 0; uint8_t is_jumping = 0; uint8_t stamina = 100; uint8_t stamina_recharge_timer = 0; uint8_t move_progress = 0; int8_t start_lx, start_ly; int8_t target_lx, target_ly; int16_t start_px, start_py; int16_t target_px, target_py; // DAS variables uint8_t das_timer = 0; uint8_t das_active = 0; #define DAS_DELAY 12 #define DAS_REPEAT 6 // Enemy variables uint8_t enemy_lx = 0; uint8_t enemy_ly = 0; uint8_t enemy_is_moving = 0; uint8_t enemy_move_progress = 0; int8_t enemy_start_lx, enemy_start_ly; int8_t enemy_target_lx, enemy_target_ly; int16_t enemy_start_px, enemy_start_py; int16_t enemy_target_px, enemy_target_py; uint8_t enemy_cooldown = 0; const metasprite_t enemy_metasprite[] = { METASPR_ITEM(-8, -8, 0, S_PALETTE), METASPR_ITEM(0, 8, 2, S_PALETTE), METASPR_TERM }; static void generate_maze(void) { // Clear maze (all 0/walls) memset(maze, 0, sizeof(maze)); // Stack for backtracking (7x7 grid of odd cells has 49 cells max) uint8_t stack_x[49]; uint8_t stack_y[49]; uint8_t stack_ptr = 0; // Start at (1, 1) uint8_t cx = 1; uint8_t cy = 1; maze[cy][cx] = 1; while (1) { // Find unvisited neighbors at distance 2 // Up: (cx, cy-2), Down: (cx, cy+2), Left: (cx-2, cy), Right: (cx+2, cy) uint8_t nx[4]; uint8_t ny[4]; uint8_t count = 0; // Up if (cy >= 3 && maze[cy - 2][cx] == 0) { nx[count] = cx; ny[count] = cy - 2; count++; } // Down if (cy <= MAP_SIZE - 4 && maze[cy + 2][cx] == 0) { nx[count] = cx; ny[count] = cy + 2; count++; } // Left if (cx >= 3 && maze[cy][cx - 2] == 0) { nx[count] = cx - 2; ny[count] = cy; count++; } // Right if (cx <= MAP_SIZE - 4 && maze[cy][cx + 2] == 0) { nx[count] = cx + 2; ny[count] = cy; count++; } if (count > 0) { // Choose a random neighbor uint8_t dir = rand() % count; // Push current cell to stack stack_x[stack_ptr] = cx; stack_y[stack_ptr] = cy; stack_ptr++; // Remove wall between current cell and chosen neighbor maze[(cy + ny[dir]) / 2][(cx + nx[dir]) / 2] = 1; // Move to neighbor cx = nx[dir]; cy = ny[dir]; maze[cy][cx] = 1; } else if (stack_ptr > 0) { // Pop from stack stack_ptr--; cx = stack_x[stack_ptr]; cy = stack_y[stack_ptr]; } else { break; } } // Reopen some walls to create alternative paths/loops for (uint8_t y = 1; y < MAP_SIZE - 1; y++) { for (uint8_t x = 1; x < MAP_SIZE - 1; x++) { if (maze[y][x] == 0) { // If it connects two paths horizontally or vertically, open it with a 15% probability if ((maze[y][x - 1] == 1 && maze[y][x + 1] == 1) || (maze[y - 1][x] == 1 && maze[y + 1][x] == 1)) { if ((rand() % 100) < 15) { maze[y][x] = 1; } } } } } } static void update_stamina_display(void) { if (game_over) { // Hide the 5 stamina bar sprites when game is over for (uint8_t i = 0; i < 5; i++) { move_sprite(18 + i, 0, 0); } return; } // Position 5 sprites at top-right of the screen (fixed, completely ignores scroll) move_sprite(18, (uint8_t)(112 + 8), 16); move_sprite(19, (uint8_t)(120 + 8), 16); move_sprite(20, (uint8_t)(128 + 8), 16); move_sprite(21, (uint8_t)(136 + 8), 16); move_sprite(22, (uint8_t)(144 + 8), 16); uint16_t temp = (uint16_t)stamina * 40; uint8_t num_pixels = temp / 100; uint8_t base_tile = player_TILE_COUNT + gameover_TILE_COUNT; // 44 for (uint8_t i = 0; i < 5; i++) { uint8_t tile_idx; if (i == 0) { tile_idx = (num_pixels >= 8) ? 11 : 9; // Left Cap full (11) or empty (9) } else if (i == 4) { tile_idx = (num_pixels >= 40) ? 12 : 10; // Right Cap full (12) or empty (10) } else { int8_t seg_px = (int8_t)num_pixels - (i * 8); if (seg_px < 0) seg_px = 0; if (seg_px > 8) seg_px = 8; tile_idx = seg_px; // middle segments (0..8 px) } set_sprite_tile(18 + i, base_tile + tile_idx * 2); } } static void draw_map(uint8_t center_x, uint8_t center_y) { // Fill entire map with tile index 0 (completely black empty tile) memset(map_buffer, 0, sizeof(map_buffer)); int8_t start_x = center_x - 2; if (start_x < 0) start_x = 0; int8_t end_x = center_x + 2; if (end_x >= MAP_SIZE) end_x = MAP_SIZE - 1; int8_t start_y = center_y - 2; if (start_y < 0) start_y = 0; int8_t end_y = center_y + 2; if (end_y >= MAP_SIZE) end_y = MAP_SIZE - 1; // Draw logical map path tiles in the visible 5x5 window for (int8_t ly = start_y; ly <= end_y; ly++) { for (int8_t lx = start_x; lx <= end_x; lx++) { if (maze[ly][lx] == 1) { // Calculate Chebyshev distance to center int8_t dx = lx - center_x; int8_t dy = ly - center_y; if (dx < 0) dx = -dx; if (dy < 0) dy = -dy; int8_t dist = (dx > dy) ? dx : dy; // Hide tiles at distance > 2 (Fog of War) if (dist > 2) continue; // Centering math for 15x15 map on 32x32 background map int8_t iso_x = (lx - ly) * 2 + 12; int8_t iso_y = (lx + ly) * 1 + 2; // Alternate colors (checkerboard) uint8_t is_alt = ((lx + ly) % 2 == 0); // Calculate neighbor mask uint8_t has_tl = (lx > 0 && maze[ly][lx - 1] == 1); uint8_t has_tr = (ly > 0 && maze[ly - 1][lx] == 1); uint8_t has_bl = (ly < MAP_SIZE - 1 && maze[ly + 1][lx] == 1); uint8_t has_br = (lx < MAP_SIZE - 1 && maze[ly][lx + 1] == 1); uint8_t mask = (has_tl ? 1 : 0) | (has_tr ? 2 : 0) | (has_bl ? 4 : 0) | (has_br ? 8 : 0); // Determine variation style based on lighting distance uint8_t v; if (dist == 2) { // Darker variants (Tile 1 Dark starting at 32, Tile 2 Dark starting at 48) v = is_alt ? (48 + mask) : (32 + mask); } else { // Normal variants v = is_alt ? (16 + mask) : mask; } for (uint8_t y = 0; y < 2; y++) { for (uint8_t x = 0; x < 4; x++) { uint8_t target_x = (iso_x + x) & 31; uint8_t target_y = (iso_y + y) & 31; uint8_t src_tile = tiles_map[4 + v * 8 + y * 4 + x]; map_buffer[target_y * 32 + target_x] = src_tile; } } } } } update_stamina_display(); // Optimize: only write rows 2 to 17 (16 rows total) which contain the active isometric map, avoiding full 1024-byte copy lags set_bkg_tiles(0, 2, 32, 16, &map_buffer[2 * 32]); } static void update_camera(void) { // Center the camera on the player's logical tile coordinates int16_t px = (player_lx - player_ly) * 16 + 96; int16_t py = (player_lx + player_ly) * 8 + 16; scroll_x = px - 64; scroll_y = py - 72; move_bkg(scroll_x, scroll_y); } static void update_player_sprite(void) { uint8_t frame_offset = is_moving ? ((move_progress >> 2) & 1) : 0; uint8_t y_offset = 0; if (is_jumping) { // Parabolic arc for the jump height: peak is 16 pixels at frame 8 y_offset = (move_progress * (16 - move_progress)) >> 2; } move_metasprite(player_metasprites[player_dir * 2 + frame_offset], 0, 0, 88, 88 - y_offset); } void title_init(void) { // Hide all sprites for (uint8_t i = 0; i < 40; i++) move_sprite(i, 0, 0); // Load title background tiles set_bkg_data(0, title_bg_TILE_COUNT, title_bg_tiles); // Draw title map set_bkg_tiles(0, 0, title_bg_WIDTH / 8, title_bg_HEIGHT / 8, title_bg_map); // Set standard palette BGP_REG = 0xE4; // Center screen move_bkg(0, 0); // Initialize sound registers NR52_REG = 0x80; // Turn on sound NR50_REG = 0x77; // Max master volume NR51_REG = 0xFF; // Enable all sound channels // Start title music title_music_timer = 0; title_music_step = 0; remove_VBL(play_music_tick); add_VBL(play_music_tick); } void title_update(uint8_t keys, uint8_t prev_keys) { // Only handling music and waiting for start, which is handled in main.c and play_music_tick } void engine_init(void) { SPRITES_8x16; // Initialize sound registers NR52_REG = 0x80; // Turn on sound NR50_REG = 0x77; // Max master volume NR51_REG = 0xFF; // Enable all sound channels // Load native GBDK IBM font (loads starting at VRAM index 0x80) font_init(); font_t ibm_font = font_load(font_ibm); font_set(ibm_font); // Reset music step counters music_timer = 0; music_step = 0; gameover_music_timer = 0; gameover_music_step = 0; remove_VBL(play_music_tick); add_VBL(play_music_tick); // Seed random number generator with hardware divider register initrand(DIV_REG); // Generate maze path cells generate_maze(); // Explicitly initialize DMG palette registers OBP0_REG = 0xE4; // 11 10 01 00 (Black, Dark Gray, Light Gray, White) OBP1_REG = 0x1B; // 00 01 10 11 (Inverted palette for the ghost enemy) BGP_REG = 0xE4; // Initialize GBC palettes (safe on DMG, does nothing) set_bkg_palette(0, 8, tiles_palettes); set_sprite_palette(0, 8, player_palettes); // Load tiles set_bkg_data(0, tiles_TILE_COUNT, tiles_tiles); set_sprite_data(0, player_TILE_COUNT, player_tiles); set_sprite_data(player_TILE_COUNT, gameover_TILE_COUNT, gameover_tiles); set_sprite_data(player_TILE_COUNT + gameover_TILE_COUNT, stamina_TILE_COUNT * 2, stamina_tiles); player_lx = 1; player_ly = 1; // Safety check: if (1,1) is somehow a wall, scan the maze and start at the first path tile if (maze[player_ly][player_lx] == 0) { uint8_t found = 0; for (uint8_t y = 1; y < MAP_SIZE - 1; y++) { for (uint8_t x = 1; x < MAP_SIZE - 1; x++) { if (maze[y][x] == 1) { player_lx = x; player_ly = y; found = 1; break; } } if (found) break; } } // Spawn enemy at a random path tile sufficiently far from the player (Chebyshev distance >= 3) while (1) { uint8_t rx = rand() % MAP_SIZE; uint8_t ry = rand() % MAP_SIZE; if (maze[ry][rx] == 1) { int8_t dx = (int8_t)rx - (int8_t)player_lx; int8_t dy = (int8_t)ry - (int8_t)player_ly; if (dx < 0) dx = -dx; if (dy < 0) dy = -dy; int8_t dist = (dx > dy) ? dx : dy; if (dist >= 3) { enemy_lx = rx; enemy_ly = ry; break; } } } enemy_is_moving = 0; enemy_cooldown = 60; game_over = 0; stamina = 100; stamina_recharge_timer = 0; is_jumping = 0; draw_map(player_lx, player_ly); update_camera(); update_player_sprite(); } void engine_update(uint8_t keys, uint8_t prev_keys) { if (game_over) { if (game_over_timer > 0) { game_over_timer--; if (game_over_timer == 0) { // Clear map_buffer to 0 (completely black/empty) memset(map_buffer, 0, sizeof(map_buffer)); set_bkg_tiles(0, 0, 32, 32, map_buffer); // Initialize gameover music step variables gameover_music_timer = 45; gameover_music_step = 0; // Play a sad defeat melody / note on Channel 2 NR21_REG = 0x80; NR22_REG = 0xF3; // volume 15, fade NR23_REG = 0x2C; // low frequency NR24_REG = 0x84; // trigger note } } else { // Render the "GAME OVER" metasprite centered horizontally (88, due to sprite X-offset) and below player (120) move_metasprite(gameover_metasprites[0], player_TILE_COUNT, 8, 88, 120); // Keep the player sprite visible at center screen (88, 88) update_player_sprite(); // Render enemy sprite at its final screen coordinate relative to camera scroll int16_t enemy_px = (enemy_lx - enemy_ly) * 16 + 96; int16_t enemy_py = (enemy_lx + enemy_ly) * 8 + 16; int16_t enemy_screen_x = ((enemy_px - scroll_x) & 255) + 24; int16_t enemy_screen_y = ((enemy_py - scroll_y) & 255) + 16; int8_t edx = (int8_t)player_lx - (int8_t)enemy_lx; int8_t edy = (int8_t)player_ly - (int8_t)enemy_ly; if (edx < 0) edx = -edx; if (edy < 0) edy = -edy; uint8_t ep_dist = (edx > edy) ? edx : edy; if (ep_dist <= 2 && enemy_screen_x >= -8 && enemy_screen_x <= 168 && enemy_screen_y >= -8 && enemy_screen_y <= 152) { move_metasprite(enemy_metasprite, 0, 4, enemy_screen_x, enemy_screen_y); } else { move_metasprite(enemy_metasprite, 0, 4, 0, 0); } // Check for restart via J_START if ((keys & J_START) && !(prev_keys & J_START)) { // Hide game over metasprite by moving offscreen move_metasprite(gameover_metasprites[0], player_TILE_COUNT, 8, 0, 0); engine_init(); } } return; } // Stamina recharge: 1 point per second (every 60 frames) stamina_recharge_timer++; if (stamina_recharge_timer >= 60) { stamina_recharge_timer = 0; if (stamina < 100) { stamina++; update_stamina_display(); } } if (is_moving) { move_progress++; // Interpolate camera position int16_t px = start_px + (((target_px - start_px) * (int16_t)move_progress) >> 4); int16_t py = start_py + (((target_py - start_py) * (int16_t)move_progress) >> 4); scroll_x = px - 64; scroll_y = py - 72; move_bkg(scroll_x, scroll_y); // Update walk animation frame (alternate frame every 4 ticks) update_player_sprite(); // Update visible tiles (fog of war center) mid-way through movement if (move_progress == 8) { draw_map(target_lx, target_ly); } if (move_progress == 16) { is_moving = 0; is_jumping = 0; player_lx = target_lx; player_ly = target_ly; // Final snap to target position to avoid any rounding errors int16_t final_px = (player_lx - player_ly) * 16 + 96; int16_t final_py = (player_lx + player_ly) * 8 + 16; scroll_x = final_px - 64; scroll_y = final_py - 72; move_bkg(scroll_x, scroll_y); // Draw map at final coordinates draw_map(player_lx, player_ly); // Set player to idle stand frame update_player_sprite(); } } // Update enemy movement progress if (enemy_is_moving) { enemy_move_progress++; if (enemy_move_progress == 16) { enemy_is_moving = 0; enemy_lx = enemy_target_lx; enemy_ly = enemy_target_ly; enemy_cooldown = 60; // Delay between steps to make the enemy move slower than the player } } // Update enemy cooldown if (enemy_cooldown > 0) { enemy_cooldown--; } // Enemy AI pathfinding toward the player if (!enemy_is_moving && enemy_cooldown == 0) { // Calculate Chebyshev distance to player int8_t dx = (int8_t)player_lx - (int8_t)enemy_lx; int8_t dy = (int8_t)player_ly - (int8_t)enemy_ly; int8_t abs_dx = (dx < 0) ? -dx : dx; int8_t abs_dy = (dy < 0) ? -dy : dy; int8_t dist = (abs_dx > abs_dy) ? abs_dx : abs_dy; if (dist <= 2) { int8_t best_nx = enemy_lx; int8_t best_ny = enemy_ly; // Initialize with current squared distance so we only move if we get strictly closer int16_t min_dist_sq = (int16_t)dx * dx + (int16_t)dy * dy; // Check all 4 directions and choose the one that minimizes the squared distance to the player // Down-Right (lx + 1, ly) if (enemy_lx + 1 < MAP_SIZE && maze[enemy_ly][enemy_lx + 1] == 1) { int8_t ndx = (int8_t)player_lx - (int8_t)(enemy_lx + 1); int8_t ndy = (int8_t)player_ly - (int8_t)enemy_ly; int16_t d_sq = (int16_t)ndx * ndx + (int16_t)ndy * ndy; if (d_sq < min_dist_sq) { min_dist_sq = d_sq; best_nx = enemy_lx + 1; best_ny = enemy_ly; } } // Down-Left (lx, ly + 1) if (enemy_ly + 1 < MAP_SIZE && maze[enemy_ly + 1][enemy_lx] == 1) { int8_t ndx = (int8_t)player_lx - (int8_t)enemy_lx; int8_t ndy = (int8_t)player_ly - (int8_t)(enemy_ly + 1); int16_t d_sq = (int16_t)ndx * ndx + (int16_t)ndy * ndy; if (d_sq < min_dist_sq) { min_dist_sq = d_sq; best_nx = enemy_lx; best_ny = enemy_ly + 1; } } // Up-Left (lx - 1, ly) if (enemy_lx > 0 && maze[enemy_ly][enemy_lx - 1] == 1) { int8_t ndx = (int8_t)player_lx - (int8_t)(enemy_lx - 1); int8_t ndy = (int8_t)player_ly - (int8_t)enemy_ly; int16_t d_sq = (int16_t)ndx * ndx + (int16_t)ndy * ndy; if (d_sq < min_dist_sq) { min_dist_sq = d_sq; best_nx = enemy_lx - 1; best_ny = enemy_ly; } } // Up-Right (lx, ly - 1) if (enemy_ly > 0 && maze[enemy_ly - 1][enemy_lx] == 1) { int8_t ndx = (int8_t)player_lx - (int8_t)enemy_lx; int8_t ndy = (int8_t)player_ly - (int8_t)(enemy_ly - 1); int16_t d_sq = (int16_t)ndx * ndx + (int16_t)ndy * ndy; if (d_sq < min_dist_sq) { min_dist_sq = d_sq; best_nx = enemy_lx; best_ny = enemy_ly - 1; } } if (best_nx != (int8_t)enemy_lx || best_ny != (int8_t)enemy_ly) { enemy_is_moving = 1; enemy_move_progress = 0; enemy_start_lx = enemy_lx; enemy_start_ly = enemy_ly; enemy_target_lx = best_nx; enemy_target_ly = best_ny; enemy_start_px = (enemy_start_lx - enemy_start_ly) * 16 + 96; enemy_start_py = (enemy_start_lx + enemy_start_ly) * 8 + 16; enemy_target_px = (enemy_target_lx - enemy_target_ly) * 16 + 96; enemy_target_py = (enemy_target_lx + enemy_target_ly) * 8 + 16; } } } // Render enemy sprite relative to the scroll viewport with centering offset int16_t enemy_px, enemy_py; if (enemy_is_moving) { enemy_px = enemy_start_px + (((enemy_target_px - enemy_start_px) * (int16_t)enemy_move_progress) >> 4); enemy_py = enemy_start_py + (((enemy_target_py - enemy_start_py) * (int16_t)enemy_move_progress) >> 4); } else { enemy_px = (enemy_lx - enemy_ly) * 16 + 96; enemy_py = (enemy_lx + enemy_ly) * 8 + 16; } // Add player centering offset (+24, +16) to align with player sprite at (88, 88), wrapping coordinates to 256 int16_t enemy_screen_x = ((enemy_px - scroll_x) & 255) + 24; int16_t enemy_screen_y = ((enemy_py - scroll_y) & 255) + 16; // Determine player-enemy distance for visibility masking int8_t edx = (int8_t)player_lx - (int8_t)enemy_lx; int8_t edy = (int8_t)player_ly - (int8_t)enemy_ly; if (edx < 0) edx = -edx; if (edy < 0) edy = -edy; uint8_t ep_dist = (edx > edy) ? edx : edy; // Render only if within range (<= 2) to let player see the ghost when visible in fog of war if (ep_dist <= 2 && enemy_screen_x >= -8 && enemy_screen_x <= 168 && enemy_screen_y >= -8 && enemy_screen_y <= 152) { move_metasprite(enemy_metasprite, 0, 4, enemy_screen_x, enemy_screen_y); // Sprite index offset 4 } else { move_metasprite(enemy_metasprite, 0, 4, 0, 0); // Hide offscreen } // Get current interpolated player coordinates int16_t p_px = is_moving ? (start_px + (((target_px - start_px) * (int16_t)move_progress) >> 4)) : ((player_lx - player_ly) * 16 + 96); int16_t p_py = is_moving ? (start_py + (((target_py - start_py) * (int16_t)move_progress) >> 4)) : ((player_lx + player_ly) * 8 + 16); // Collision detection: check actual pixel distance (sprites overlap) int16_t dx_collision = p_px - enemy_px; int16_t dy_collision = p_py - enemy_py; if (dx_collision < 0) dx_collision = -dx_collision; if (dy_collision < 0) dy_collision = -dy_collision; if (dx_collision < 12 && dy_collision < 6) { game_over = 1; game_over_timer = 45; // 45 frames delay update_stamina_display(); // Hide stamina immediately // Play defeat sweep SFX on Channel 1 (slide pitch down) NR10_REG = 0x1E; // sweep register: shift down NR11_REG = 0x10; // 25% duty cycle NR12_REG = 0xF3; // initial volume 15, envelope decrease NR13_REG = 0x00; // low frequency bits NR14_REG = 0xC6; // trigger note, frequency high bits } uint8_t keys_pressed = keys & ~prev_keys; if (keys_pressed) { das_timer = DAS_DELAY; das_active = 1; } else if (keys && das_active) { if (das_timer > 0) { das_timer--; } if (das_timer == 0 && !is_moving) { das_timer = DAS_REPEAT; keys_pressed = keys; // trigger move } } else { das_active = 0; } if (is_moving) { return; // Ignore other inputs while moving } int8_t move_lx = 0; int8_t move_ly = 0; if (keys_pressed & J_RIGHT) { move_lx = 1; player_dir = 0; // DR } else if (keys_pressed & J_LEFT) { move_lx = -1; player_dir = 2; // UL } else if (keys_pressed & J_DOWN) { move_ly = 1; player_dir = 1; // DL } else if (keys_pressed & J_UP) { move_ly = -1; player_dir = 3; // UR } if (move_lx != 0 || move_ly != 0) { // If J_A is held down, attempt to jump over a wall if (keys & J_A) { int8_t land_lx = player_lx + move_lx * 2; int8_t land_ly = player_ly + move_ly * 2; // Bounds check for intermediate and landing tiles if (land_lx >= 0 && land_lx < MAP_SIZE && land_ly >= 0 && land_ly < MAP_SIZE) { // intermediate tile must be a wall (0), landing tile must be a path (1), and we need >= 60 stamina if (maze[player_ly + move_ly][player_lx + move_lx] == 0 && maze[land_ly][land_lx] == 1 && stamina >= 60) { is_moving = 1; is_jumping = 1; move_progress = 0; stamina -= 60; start_lx = player_lx; start_ly = player_ly; target_lx = land_lx; target_ly = land_ly; start_px = (start_lx - start_ly) * 16 + 96; start_py = (start_lx + start_ly) * 8 + 16; target_px = (target_lx - target_ly) * 16 + 96; target_py = (target_lx + target_ly) * 8 + 16; // Play jump SFX (sweep up on Channel 1) NR10_REG = 0x15; // sweep up NR11_REG = 0x80; // 50% duty cycle NR12_REG = 0xF3; // volume 15, fade NR13_REG = 0x00; // low frequency bits NR14_REG = 0xC3; // trigger note, frequency high bits update_stamina_display(); update_player_sprite(); return; } } // If jump checks fail, just face direction update_player_sprite(); } else { // Normal walk int8_t new_lx = player_lx + move_lx; int8_t new_ly = player_ly + move_ly; // Bounds check & wall/empty area collision check if (new_lx >= 0 && new_lx < MAP_SIZE && new_ly >= 0 && new_ly < MAP_SIZE) { if (maze[new_ly][new_lx] == 1) { is_moving = 1; move_progress = 0; start_lx = player_lx; start_ly = player_ly; target_lx = new_lx; target_ly = new_ly; start_px = (start_lx - start_ly) * 16 + 96; start_py = (start_lx + start_ly) * 8 + 16; target_px = (target_lx - target_ly) * 16 + 96; target_py = (target_lx + target_ly) * 8 + 16; update_player_sprite(); } else { update_player_sprite(); } } else { update_player_sprite(); } } } }