502 lines
18 KiB
C
502 lines
18 KiB
C
#include <gb/gb.h>
|
|
#include <stdint.h>
|
|
#include <string.h>
|
|
#include <rand.h>
|
|
#include "engine.h"
|
|
|
|
// Generated by png2asset
|
|
#include "tiles.h"
|
|
#include "player.h"
|
|
|
|
#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 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;
|
|
uint8_t game_over = 0;
|
|
uint8_t game_over_timer = 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 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;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
set_bkg_tiles(0, 0, 32, 32, map_buffer);
|
|
}
|
|
|
|
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;
|
|
move_metasprite(player_metasprites[player_dir * 2 + frame_offset], 0, 0, 88, 88);
|
|
}
|
|
|
|
void engine_init(void) {
|
|
SPRITES_8x16;
|
|
|
|
// 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);
|
|
|
|
// Spawn enemy at a random path tile sufficiently far from the player
|
|
while (1) {
|
|
uint8_t rx = rand() % MAP_SIZE;
|
|
uint8_t ry = rand() % MAP_SIZE;
|
|
if (maze[ry][rx] == 1) {
|
|
// Distance (Manhattan) to player start (1, 1)
|
|
int8_t dx = rx - 1;
|
|
int8_t dy = ry - 1;
|
|
if (dx < 0) dx = -dx;
|
|
if (dy < 0) dy = -dy;
|
|
if ((dx + dy) >= 5) {
|
|
enemy_lx = rx;
|
|
enemy_ly = ry;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
enemy_is_moving = 0;
|
|
enemy_cooldown = 30;
|
|
game_over = 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) {
|
|
BGP_REG = 0xFF; // Set screen to all black to signify Game Over
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
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;
|
|
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 = 24; // 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 <= 4) {
|
|
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)
|
|
int16_t enemy_screen_x = enemy_px - scroll_x + 24;
|
|
int16_t enemy_screen_y = enemy_py - scroll_y + 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 (<= 3) to let player see the ghost coming from the edge of fog of war
|
|
if (ep_dist <= 3 && 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 = 30; // 30 frames delay (0.5 seconds) to allow player to see the overlap
|
|
}
|
|
|
|
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) {
|
|
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 {
|
|
// If collision blocks, just update direction look frame
|
|
update_player_sprite();
|
|
}
|
|
} else {
|
|
// Out of bounds: update direction look frame
|
|
update_player_sprite();
|
|
}
|
|
}
|
|
}
|