From 9311e5d1d3297cb539b70d37dbbee59ec9a386a1 Mon Sep 17 00:00:00 2001 From: enne2 Date: Mon, 22 Jun 2026 19:45:04 +0200 Subject: [PATCH] Refine enemy AI pathfinding and update walkthrough documentation --- engine.c | 158 +++++++++++++++++++++++++++++++++++++++++++++++ hello_iso_gb.png | Bin 1270 -> 1325 bytes 2 files changed, 158 insertions(+) diff --git a/engine.c b/engine.c index 914cdd6..c2a9b99 100644 --- a/engine.c +++ b/engine.c @@ -33,6 +33,24 @@ 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; + +const metasprite_t enemy_metasprite[] = { + METASPR_ITEM(-8, -8, 0, S_PAL(1)), + METASPR_ITEM(0, 8, 2, S_PAL(1)), + METASPR_TERM +}; + static void generate_maze(void) { // Clear maze (all 0/walls) memset(maze, 0, sizeof(maze)); @@ -209,6 +227,7 @@ void engine_init(void) { // 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) @@ -219,12 +238,38 @@ void engine_init(void) { 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) { + BGP_REG = 0xFF; // Set screen to all black to signify Game Over + return; + } + if (is_moving) { move_progress++; @@ -264,6 +309,119 @@ void engine_update(uint8_t keys, uint8_t prev_keys) { } } + // 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 + 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; + } + + int16_t enemy_screen_x = enemy_px - scroll_x; + int16_t enemy_screen_y = enemy_py - scroll_y; + + // 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 fog of war radius (<= 2) + 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 + } + + // Collision detection / Game Over conditions + if ((player_lx == enemy_lx && player_ly == enemy_ly) || + (is_moving && enemy_is_moving && target_lx == enemy_target_lx && target_ly == enemy_target_ly) || + (is_moving && enemy_is_moving && target_lx == enemy_start_lx && target_ly == enemy_start_ly && start_lx == enemy_target_lx && start_ly == enemy_target_ly)) { + game_over = 1; + BGP_REG = 0xFF; // Screen all black + return; + } + uint8_t keys_pressed = keys & ~prev_keys; if (keys_pressed) { diff --git a/hello_iso_gb.png b/hello_iso_gb.png index b5dbb3058a5fbc7a32015b8450b77b3c1647a65b..7539b9a1ba9616e75d442904b5c788e118bf33d9 100644 GIT binary patch delta 1066 zcmV+_1l9ZY39Sl{Bti5^L_t(|obBChcAGjBM&aYGJ2X#$Ou=JvhG0)2b4!1uCrj3q zgoMB^g8lsIN1EWkt^irI7c=`dK;)151>oF~@hg$QT#>*rfBsXyfKlD=_m^MaZa3L$ z85tws0&#q~-@`lt?BJswqu+8DGy7J*h!JrLUIzg}ikpyyrfGWl_4E0Zy_S)24Y&X| zA?qPPNO29=z)!o+=QDW<4nK}MTmx3{u&&qZ?~n0aZ|W3Gi1Qc$3*zvc#}|)yafDxA zU%x*m#xzF2e}r6kVNhmfkH^Ez%r2M9%g4i!GDe867y$!w;XNLY-`LE|zQ2EVUHp3= zA0IEDAAUXmT7>w605jo*qn(d*ONBxDHVE;@=>v@8YYp%7^Kl;ciqZZBmHSxy=d57_JyVDT zSfMm3mXyK^Pw#Uhq8&I!0&GwlVlKSY-LMtr7gL{mv=9k!ftvZkkjGOyf|i0jT8ISL zVVWkpUa!gUa--c6si~o(gh+rLd?f6dp6QuKJi=oDL(c=MmOsU(U>}XvnkSr-TMRLx zAxFS?e=VNd`asOxgtSh#SmOv7hZh?$#GwMr?fBT*2cmTvrX|Fh0O!JsjTGV#0j73* zxRbXU5o+HRnHv$U5MwF8Qh4w9{@-EO0hXRT3m1RJ7vpmWtaiJl^? ze}H)I&y}{7+xA)^Zn^!^X?)M&x!ndBS{g*@z=whN{3Y1q_D2ZM4H2NL4Ka7%YcI-6 z!Ssx{TW^1)@Z3-Vy4Lx!-nX(+lfI=>-n+LyVt8)I0Nw3&vuT==2fp|88A?qJg&`=n zKRS7UYe^lLngidnSk$-G^OvY}7hn6#Y%DsOB2-C=N3M z#^&xkEm%@tt9CtJONgm|9pMb&xx)q+Dot~2F%8rbVrld|a(M0(0lL;x+#eGFr#)oq8oKRo*DsGsF`LNbv>RM0X8_|gW+lfxWLE{g6|Chc3?`S k9|1y&|E?90fI}Ak0)BP`qOM%}umAu607*qoM6N<$f-k}t=>Px# delta 1015 zcmV2oO>{32C$}%gdji&!=o#M#ep01D=E& zhX5hPJ>Uj@*?m5r$xCqfdDP(^u!4tmyx7p@ZR6wn;)A9xd;5`K z61wn`9zM>DAx=L6#&Y+8mU$AgWdw}Fi_I7!0cM1M6V0g@?Qc-I&&6M78%EHXLL|Tn zrBSh@6kd3FUmFqa#4!@!2DKsP!b?3Hw!-{k>hGQ{L;`G3GhZ0;cxp$`Qjlj0kpOpC zmc_2uYcjmtXxAb&HFTB`32-ML4dXEEfbiCw;cWmz2iVi{r#M7Q%p)EZWNtCUh=vR> zUW?~{dN>iO5Z&G&R=gWUUw zP#P6WucVa18)^Sbx(=}P&a-gwXM8ihcEVIrA*KS%g*W>Cr*s)$?!ec+^Q?42dN>hb zh+%+h;hkmwLtO=!J5YPy@)(|wo}M}SJYp(;yczc2dI~UgU}_G0{Hnngy_^KwvHvBh z@GKfp)pLpR^?-hmQ=>r5JI`9gmo)WBuwa{x*#Dz0<^O|x1kC-ApPC!#`~ALY1YA1- zJ%s3L_uu74J6qXr^*(nOper3vmwJPE?(0gAReJ0m)J(IKf-UX0dSAN@Ftl|d*46%h z%O7Dxw1$c3Eyq@6hyYz}h`AH6c2i#3CiRI5t&NB+$5dsg0NvJ`?7c5#l_q_a)_yWW z4fXyE8KAq}Zni8-@&we}L)J=7ji^stXzwvpk_UK{R2Vhk?O7tFK6z(wngHF_*GO9e zJkxqSwbKRYYMw~=OJTLnxE@dKv;l^HO4BSqF%3kB^`;2WZM|8aVLhJOQ~|oR5NBGC zr#5APu6Dlg%`xK((J(k2unLJhkZqbg2;IfJ#BO zuE(1l0Y*wM>6LafpQ1 lj(I>x@&5%wk#Ij2sXya_w=UE#Z({%e002ovPDHLkV1l<3^eX@W