8-level progression with scaling difficulty and a finale
Maze size grows +2/level from 7x7 to 21x21 (cap MAX_MAP_SIZE=21, reached at level 8). Additional difficulty axes scale with level: - +1 ghost per level (up to MAX_ENEMIES=8); enemy state moved to arrays, enemy_logic.c rewritten as a multi-entity loop (own AI, render at OAM 2+i*2, staggered cooldowns). - Ghost cooldown shrinks: 60 -> ~11 frames between steps. - Stamina recharge slows: 60 -> 144 frames per point. - Fog tightens to 3x3 (fog_radius=1) from level 7. Fog rendering uses a dynamic 16-row centered flush (with wrap) instead of the fixed rows 2-17 / full 32x32, so the fog stays correct on big mazes without the multi-frame stall of the full flush. Game ends after level 8: reaching the hatch at level 8 sets game_over=3 (finale) instead of 2. Finale screen reloads the IBM font and writes 'YOU ESCAPED / THE DARKNESS / LEVEL 8 CLEARED / PRESS START' into the BG map; START returns to the title (new game starts at level 1). Verified via PyBoy: sizes 7..21, fog 2->1 at L7, enemies 1..8 (distinct, on floor, can catch the player), cooldown/recharge scaling, L1 hatch -> game_over=2, L8 hatch -> game_over=3, finale renders + START->title->L1.
This commit is contained in:
+128
-149
@@ -1,6 +1,6 @@
|
||||
#include "enemy_logic.h"
|
||||
#include "globals.h"
|
||||
#include "render.h" // For update_stamina_display
|
||||
#include "render.h" // For update_stamina_display (called on fatal collision)
|
||||
#include <gb/gb.h>
|
||||
|
||||
// Tile graphics required for metasprites
|
||||
@@ -8,163 +8,142 @@
|
||||
#include "enemy.h"
|
||||
|
||||
/**
|
||||
* Gestisce l'intelligenza artificiale (AI) e il rendering del nemico (il fantasma).
|
||||
* Gestisce l'intelligenza artificiale e il rendering dei fantasmi (nemici).
|
||||
*
|
||||
* Sviluppo & Scelte Architetturali:
|
||||
* 1. Cooldown System: Il fantasma fa "passi" esatti sulla griglia proprio come il giocatore.
|
||||
* Tuttavia, se si muovesse a ogni frame, sarebbe impossibile scappare.
|
||||
* Abbiamo implementato un timer di `enemy_cooldown` (60 frame = 1 secondo) di pausa
|
||||
* tra un passo e l'altro. Il giocatore cammina/salta più velocemente, creando una
|
||||
* tensione in cui devi pianificare i tuoi balzi per seminarlo.
|
||||
* 2. Pathfinding (Ricerca del Percorso): Invece di usare A* (A-Star) che consumerebbe
|
||||
* troppa RAM e CPU per un Game Boy a 8-bit, il fantasma usa una logica "Greedy".
|
||||
* Calcola la distanza al quadrato verso il giocatore per ciascuna delle 4 direzioni
|
||||
* valide (muri permettendo). Sceglie la direzione che *minimizza* questa distanza.
|
||||
* Non essendo A*, può incastrarsi in vicoli ciechi a forma di "U", che è esattamente
|
||||
* una debolezza voluta per permettere al giocatore di seminarlo usando il level design.
|
||||
* 3. Collisione Pixel-Perfect: Anche se logica e pathfinding sono su griglia (Grid-Based),
|
||||
* la Morte scatta solo se i *pixel* fisici a schermo dei due sprite si sovrappongono.
|
||||
* 1. Multi-entity: fino a MAX_ENEMIES (8) fantasmi coesistenti. Lo stato di ciascuno
|
||||
* e' in array indicizzati (enemy_lx[i], enemy_is_moving[i], ...). Il numero attivo
|
||||
* e' num_enemies (= livello, capped). Ogni fantasma usa 2 slot OAM (sprite 8x16)
|
||||
* a partire da OAM 2 + i*2 (il player usa 0-1).
|
||||
* 2. Cooldown: dopo ogni passo (16 frame di LERP) il fantasma aspetta `enemy_step_cooldown`
|
||||
* frame prima del prossimo (scalato col livello: piu' breve = piu' veloce). I cooldown
|
||||
* iniziali sono sfasati (i*8) cosi' non si muovono in sincrono.
|
||||
* 3. Pathfinding Greedy (non A*): per ogni fantasma, tra le 4 celle adiacenti calpestabili
|
||||
* sceglie quella che minimizza la distanza al quadrato verso il giocatore (no sqrt, no
|
||||
* heap: sostenibile su 4 MHz anche con 8 fantasmi). Difetto voluto: si incastra nei
|
||||
* vicoli a U -> dinamica di gioco per seminarli.
|
||||
* 4. Attivazione: il fantasma insegue solo se entro il raggio di nebbia (Chebyshev <=
|
||||
* fog_radius), coerente col fog of war.
|
||||
* 5. Collisione Pixel-Perfect: la morte scatta se i pixel fisici di un qualunque fantasma
|
||||
* si sovrappongono a quelli del giocatore (|dx|<12, |dy|<6).
|
||||
*/
|
||||
void update_enemy_logic(void) {
|
||||
// 1. Aggiorna l'interpolazione del movimento visivo del fantasma
|
||||
if (enemy_is_moving) {
|
||||
enemy_move_progress++;
|
||||
if (enemy_move_progress == 16) {
|
||||
// Movimento completato
|
||||
enemy_is_moving = 0;
|
||||
enemy_lx = enemy_target_lx;
|
||||
enemy_ly = enemy_target_ly;
|
||||
// Imposta una pausa di 1 secondo prima del prossimo passo (Bilanciamento difficoltà)
|
||||
enemy_cooldown = 60;
|
||||
}
|
||||
}
|
||||
// Posizione pixel del giocatore (interpolata se in movimento), usata per la collisione.
|
||||
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);
|
||||
|
||||
// 2. Decrementa il timer di riposo se non si sta muovendo
|
||||
if (enemy_cooldown > 0) {
|
||||
enemy_cooldown--;
|
||||
}
|
||||
|
||||
// 3. AI PATHFINDING: Calcola il prossimo passo se è pronto a muoversi
|
||||
if (!enemy_is_moving && enemy_cooldown == 0) {
|
||||
// Calcola Distanza di Chebyshev logica dal giocatore per sapere se siamo "vicini"
|
||||
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;
|
||||
|
||||
// L'AI "si sveglia" e inizia a inseguirti solo se è entro 2 celle di distanza
|
||||
// (cioè è entrato nel tuo cono visivo di nebbia)
|
||||
if (dist <= 2) {
|
||||
int8_t best_nx = enemy_lx;
|
||||
int8_t best_ny = enemy_ly;
|
||||
|
||||
// Inizializza con la distanza Euclidea al QUADRATO (x^2 + y^2) corrente.
|
||||
// Non usiamo la radice quadrata (sqrt) perché è un calcolo pesantissimo su Game Boy.
|
||||
// Il quadrato della distanza conserva le proporzioni perfette per il confronto (min_dist).
|
||||
int16_t min_dist_sq = (int16_t)dx * dx + (int16_t)dy * dy;
|
||||
|
||||
// Controllo 1: Direzione Down-Right (+1 X)
|
||||
if (enemy_lx + 1 < map_size && maze[enemy_ly][enemy_lx + 1] != 0) {
|
||||
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; }
|
||||
}
|
||||
// Controllo 2: Direzione Down-Left (+1 Y)
|
||||
if (enemy_ly + 1 < map_size && maze[enemy_ly + 1][enemy_lx] != 0) {
|
||||
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; }
|
||||
}
|
||||
// Controllo 3: Direzione Up-Left (-1 X)
|
||||
if (enemy_lx > 0 && maze[enemy_ly][enemy_lx - 1] != 0) {
|
||||
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; }
|
||||
}
|
||||
// Controllo 4: Direzione Up-Right (-1 Y)
|
||||
if (enemy_ly > 0 && maze[enemy_ly - 1][enemy_lx] != 0) {
|
||||
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; }
|
||||
}
|
||||
|
||||
// Se abbiamo trovato un percorso migliore (Strictly closer)
|
||||
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;
|
||||
|
||||
// Preparazione coordinate pixel hardware per l'interpolazione fluida
|
||||
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;
|
||||
for (uint8_t i = 0; i < num_enemies; i++) {
|
||||
// 1. Aggiorna l'interpolazione del movimento visivo
|
||||
if (enemy_is_moving[i]) {
|
||||
enemy_move_progress[i]++;
|
||||
if (enemy_move_progress[i] == 16) {
|
||||
enemy_is_moving[i] = 0;
|
||||
enemy_lx[i] = enemy_target_lx[i];
|
||||
enemy_ly[i] = enemy_target_ly[i];
|
||||
// Pausa prima del prossimo passo (scalata col livello).
|
||||
enemy_cooldown[i] = enemy_step_cooldown;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4. RENDERING DEL NEMICO (Telecamera relativa)
|
||||
// Recuperiamo i pixel assoluti
|
||||
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;
|
||||
}
|
||||
|
||||
// Il motore renderizza lo sfondo scrollando, ma gli SPRITE devono essere disegnati a mano.
|
||||
// Togliamo il valore di scroll della telecamera (scroll_x, scroll_y) dalla posizione assoluta.
|
||||
// +24 e +16 sono offset per l'allineamento hardware OAM del Game Boy per le griglie 16x16.
|
||||
int16_t enemy_screen_x = ((enemy_px - scroll_x) & 255) + 24;
|
||||
int16_t enemy_screen_y = ((enemy_py - scroll_y) & 255) + 16;
|
||||
|
||||
// Mostriamo lo sprite solo se è all'interno della griglia logica visibile (distanza <= 2)
|
||||
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;
|
||||
|
||||
// Il limite x=168 e y=152 assicura di nascondere gli sprite appena fuori dai bordi fisici
|
||||
if (ep_dist <= 2 && enemy_screen_x >= -8 && enemy_screen_x <= 168 && enemy_screen_y >= -8 && enemy_screen_y <= 152) {
|
||||
// Disegna il metasprite del Fantasma usando gli offset di memoria generati in enemy.h
|
||||
move_metasprite(enemy_metasprites[0], player_TILE_COUNT, 4, enemy_screen_x, enemy_screen_y);
|
||||
} else {
|
||||
// Nasconde lo sprite spostandolo a coordinate (0,0) offscreen
|
||||
move_metasprite(enemy_metasprites[0], player_TILE_COUNT, 4, 0, 0);
|
||||
}
|
||||
// 2. Decrementa il timer di riposo
|
||||
if (enemy_cooldown[i] > 0) {
|
||||
enemy_cooldown[i]--;
|
||||
}
|
||||
|
||||
// 5. COLLISIONE FATALE (Player Hitbox check)
|
||||
// Per calcolare esattamente il game over, usiamo la posizione *Pixel* di entrambi.
|
||||
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);
|
||||
// 3. AI PATHFINDING (greedy) se pronto a muoversi
|
||||
if (!enemy_is_moving[i] && enemy_cooldown[i] == 0) {
|
||||
int8_t dx = (int8_t)player_lx - (int8_t)enemy_lx[i];
|
||||
int8_t dy = (int8_t)player_ly - (int8_t)enemy_ly[i];
|
||||
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;
|
||||
|
||||
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;
|
||||
// Insegue solo se entro il raggio di nebbia.
|
||||
if (dist <= (int8_t)fog_radius) {
|
||||
int8_t best_nx = (int8_t)enemy_lx[i];
|
||||
int8_t best_ny = (int8_t)enemy_ly[i];
|
||||
int16_t min_dist_sq = (int16_t)dx * dx + (int16_t)dy * dy;
|
||||
|
||||
// Se i pixel centrali sono vicini (X < 12 e Y < 6), scatena il GAME OVER
|
||||
if (dx_collision < 12 && dy_collision < 6) {
|
||||
game_over = 1; // 1 = Defeat
|
||||
game_over_timer = 45; // Fermo immagine drammatico per 45 frame (~0.7s) prima del menu nero
|
||||
update_stamina_display(); // Aggiornando con game_over=1 l'HUD scompare istantaneamente
|
||||
|
||||
// Esegue il Sound Effect della cattura: Suono profondo a caduta (Slide Down) sul Canale 1
|
||||
NR10_REG = 0x1E; // sweep register: shift down
|
||||
NR11_REG = 0x10; // 25% duty cycle
|
||||
NR12_REG = 0xF3; // volume alto, calo rapido (envelope decrease)
|
||||
NR13_REG = 0x00; // frequenza bassa
|
||||
NR14_REG = 0xC6; // trigger
|
||||
if (enemy_lx[i] + 1 < map_size && maze[enemy_ly[i]][enemy_lx[i] + 1] != 0) {
|
||||
int8_t ndx = (int8_t)player_lx - (int8_t)(enemy_lx[i] + 1);
|
||||
int16_t d_sq = (int16_t)ndx * ndx + (int16_t)dy * dy;
|
||||
if (d_sq < min_dist_sq) { min_dist_sq = d_sq; best_nx = enemy_lx[i] + 1; best_ny = enemy_ly[i]; }
|
||||
}
|
||||
if (enemy_ly[i] + 1 < map_size && maze[enemy_ly[i] + 1][enemy_lx[i]] != 0) {
|
||||
int8_t ndy = (int8_t)player_ly - (int8_t)(enemy_ly[i] + 1);
|
||||
int16_t d_sq = (int16_t)dx * dx + (int16_t)ndy * ndy;
|
||||
if (d_sq < min_dist_sq) { min_dist_sq = d_sq; best_nx = enemy_lx[i]; best_ny = enemy_ly[i] + 1; }
|
||||
}
|
||||
if (enemy_lx[i] > 0 && maze[enemy_ly[i]][enemy_lx[i] - 1] != 0) {
|
||||
int8_t ndx = (int8_t)player_lx - (int8_t)(enemy_lx[i] - 1);
|
||||
int16_t d_sq = (int16_t)ndx * ndx + (int16_t)dy * dy;
|
||||
if (d_sq < min_dist_sq) { min_dist_sq = d_sq; best_nx = enemy_lx[i] - 1; best_ny = enemy_ly[i]; }
|
||||
}
|
||||
if (enemy_ly[i] > 0 && maze[enemy_ly[i] - 1][enemy_lx[i]] != 0) {
|
||||
int8_t ndy = (int8_t)player_ly - (int8_t)(enemy_ly[i] - 1);
|
||||
int16_t d_sq = (int16_t)dx * dx + (int16_t)ndy * ndy;
|
||||
if (d_sq < min_dist_sq) { min_dist_sq = d_sq; best_nx = enemy_lx[i]; best_ny = enemy_ly[i] - 1; }
|
||||
}
|
||||
|
||||
if (best_nx != (int8_t)enemy_lx[i] || best_ny != (int8_t)enemy_ly[i]) {
|
||||
enemy_is_moving[i] = 1;
|
||||
enemy_move_progress[i] = 0;
|
||||
enemy_start_lx[i] = (int8_t)enemy_lx[i];
|
||||
enemy_start_ly[i] = (int8_t)enemy_ly[i];
|
||||
enemy_target_lx[i] = best_nx;
|
||||
enemy_target_ly[i] = best_ny;
|
||||
enemy_start_px[i] = (enemy_start_lx[i] - enemy_start_ly[i]) * 16 + 96;
|
||||
enemy_start_py[i] = (enemy_start_lx[i] + enemy_start_ly[i]) * 8 + 16;
|
||||
enemy_target_px[i] = (enemy_target_lx[i] - enemy_target_ly[i]) * 16 + 96;
|
||||
enemy_target_py[i] = (enemy_target_lx[i] + enemy_target_ly[i]) * 8 + 16;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4. RENDERING (telecamera relativa)
|
||||
int16_t enemy_px, enemy_py;
|
||||
if (enemy_is_moving[i]) {
|
||||
enemy_px = enemy_start_px[i] + (((enemy_target_px[i] - enemy_start_px[i]) * (int16_t)enemy_move_progress[i]) >> 4);
|
||||
enemy_py = enemy_start_py[i] + (((enemy_target_py[i] - enemy_start_py[i]) * (int16_t)enemy_move_progress[i]) >> 4);
|
||||
} else {
|
||||
enemy_px = (enemy_lx[i] - enemy_ly[i]) * 16 + 96;
|
||||
enemy_py = (enemy_lx[i] + enemy_ly[i]) * 8 + 16;
|
||||
}
|
||||
|
||||
int16_t enemy_screen_x = ((enemy_px - scroll_x) & 255) + 24;
|
||||
int16_t enemy_screen_y = ((enemy_py - scroll_y) & 255) + 16;
|
||||
|
||||
// Visibilita': solo se entro il raggio di nebbia e on-screen.
|
||||
int8_t edx = (int8_t)player_lx - (int8_t)enemy_lx[i];
|
||||
int8_t edy = (int8_t)player_ly - (int8_t)enemy_ly[i];
|
||||
if (edx < 0) edx = -edx;
|
||||
if (edy < 0) edy = -edy;
|
||||
uint8_t ep_dist = (edx > edy) ? edx : edy;
|
||||
|
||||
if (ep_dist <= fog_radius && enemy_screen_x >= -8 && enemy_screen_x <= 168 && enemy_screen_y >= -8 && enemy_screen_y <= 152) {
|
||||
move_metasprite(enemy_metasprites[0], player_TILE_COUNT, 2 + i * 2, enemy_screen_x, enemy_screen_y);
|
||||
} else {
|
||||
move_metasprite(enemy_metasprites[0], player_TILE_COUNT, 2 + i * 2, 0, 0);
|
||||
}
|
||||
|
||||
// 5. COLLISIONE FATALE (pixel-perfect) contro questo fantasma
|
||||
int16_t dxc = p_px - enemy_px;
|
||||
int16_t dyc = p_py - enemy_py;
|
||||
if (dxc < 0) dxc = -dxc;
|
||||
if (dyc < 0) dyc = -dyc;
|
||||
if (dxc < 12 && dyc < 6) {
|
||||
game_over = 1;
|
||||
game_over_timer = 45;
|
||||
update_stamina_display(); // nasconde l'HUD (game_over attivo)
|
||||
// Sound effect della cattura (slide down sul canale 1)
|
||||
NR10_REG = 0x1E;
|
||||
NR11_REG = 0x10;
|
||||
NR12_REG = 0xF3;
|
||||
NR13_REG = 0x00;
|
||||
NR14_REG = 0xC6;
|
||||
return; // un fantasma ti ha preso: basta per questo frame
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+96
-47
@@ -82,11 +82,16 @@ void engine_init(void) {
|
||||
// Usa il registro divisore hardware (DIV_REG) per seedare l'RNG. Garantisce labirinti diversi ogni volta.
|
||||
initrand(DIV_REG);
|
||||
|
||||
// Dimensione del labirinto crescente col livello: +2 tile per livello a partire da 7,
|
||||
// capped a MAX_MAP_SIZE (17). Sempre dispari (per il pattern stanza/muro del DFS).
|
||||
map_size = MAP_SIZE + 2 * (level - 1);
|
||||
map_size = MAP_SIZE + 2 * (level - 1); // +2/level from 7, capped at MAX_MAP_SIZE (21) -> level 8
|
||||
if (map_size > MAX_MAP_SIZE) map_size = MAX_MAP_SIZE;
|
||||
|
||||
// --- Assi di difficolta' progressivi (scalano col livello) ---
|
||||
fog_radius = (level >= 7) ? 1 : 2; // nebbia 5x5 -> 3x3 dal livello 7
|
||||
stamina_recharge_rate = 60 + (level - 1) * 12; // ricarica stamina piu' lenta
|
||||
enemy_step_cooldown = 60 - (level - 1) * 7; // fantasma piu' veloce (cooldown piu' corto)
|
||||
if (enemy_step_cooldown < 10) enemy_step_cooldown = 10;
|
||||
num_enemies = (level <= MAX_ENEMIES) ? level : MAX_ENEMIES; // +1 fantasma per livello
|
||||
|
||||
// DELEGA LA GENERAZIONE: Chiede al modulo maze.c di creare l'array della mappa
|
||||
generate_maze();
|
||||
|
||||
@@ -124,32 +129,53 @@ void engine_init(void) {
|
||||
}
|
||||
}
|
||||
|
||||
// SPAWN DEL NEMICO
|
||||
// Cerca randomicamente un punto lontano almeno map_size/2 celle dal giocatore
|
||||
// (scala con la dimensione del labirinto: in una mappa grande il fantasma parte
|
||||
// lontano e non si attiva finché non entra nel cono visivo 5x5 del fog of war).
|
||||
uint8_t enemy_min_dist = map_size / 2;
|
||||
if (enemy_min_dist < 3) enemy_min_dist = 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;
|
||||
// SPAWN DEI NEMICI (num_enemies fantasmi): cella calpestabile lontana >= map_size/2
|
||||
// dal giocatore e >= 2 dagli altri fantasmi gia' piazzati; cooldown sfasati (i*8)
|
||||
// cosi' non si muovono tutti in sincrono. Dopo 40 tentativi rilassa la distanza.
|
||||
for (uint8_t i = 0; i < num_enemies; i++) {
|
||||
uint8_t rx = 0, ry = 0, placed = 0;
|
||||
for (uint8_t tries = 0; tries < 80 && !placed; tries++) {
|
||||
uint8_t candx = rand() % map_size;
|
||||
uint8_t candy = rand() % map_size;
|
||||
if (maze[candy][candx] != 1) continue;
|
||||
int8_t dx = (int8_t)candx - (int8_t)player_lx;
|
||||
int8_t dy = (int8_t)candy - (int8_t)player_ly;
|
||||
if (dx < 0) dx = -dx;
|
||||
if (dy < 0) dy = -dy;
|
||||
int8_t dist = (dx > dy) ? dx : dy;
|
||||
if (dist >= (int8_t)enemy_min_dist) {
|
||||
enemy_lx = rx;
|
||||
enemy_ly = ry;
|
||||
break;
|
||||
uint8_t req = (tries < 40) ? enemy_min_dist : 2;
|
||||
if (dist < (int8_t)req) continue;
|
||||
uint8_t too_close = 0;
|
||||
for (uint8_t j = 0; j < i; j++) {
|
||||
int8_t ex = (int8_t)candx - (int8_t)enemy_lx[j];
|
||||
int8_t ey = (int8_t)candy - (int8_t)enemy_ly[j];
|
||||
if (ex < 0) ex = -ex;
|
||||
if (ey < 0) ey = -ey;
|
||||
if (((ex > ey) ? ex : ey) < 2) { too_close = 1; break; }
|
||||
}
|
||||
if (too_close) continue;
|
||||
rx = candx; ry = candy; placed = 1;
|
||||
}
|
||||
if (!placed) {
|
||||
for (uint8_t y = 1; y < map_size - 1 && !placed; y++)
|
||||
for (uint8_t x = 1; x < map_size - 1 && !placed; x++)
|
||||
if (maze[y][x] == 1 && !(x == player_lx && y == player_ly)) { rx = x; ry = y; placed = 1; }
|
||||
}
|
||||
enemy_lx[i] = rx;
|
||||
enemy_ly[i] = ry;
|
||||
enemy_is_moving[i] = 0;
|
||||
enemy_move_progress[i] = 0;
|
||||
enemy_cooldown[i] = (uint8_t)(enemy_step_cooldown + i * 8);
|
||||
}
|
||||
// Nasconde gli sprite dei nemici eccedenti (es. quando num_enemies cala tornando al titolo).
|
||||
for (uint8_t i = num_enemies; i < MAX_ENEMIES; i++) {
|
||||
enemy_is_moving[i] = 0;
|
||||
move_metasprite(enemy_metasprites[0], player_TILE_COUNT, 2 + i * 2, 0, 0);
|
||||
}
|
||||
|
||||
// Reset Variabili di Gameplay
|
||||
enemy_is_moving = 0;
|
||||
enemy_cooldown = 60;
|
||||
game_over = 0;
|
||||
stamina = 100;
|
||||
stamina_recharge_timer = 0;
|
||||
@@ -164,6 +190,21 @@ void engine_init(void) {
|
||||
update_level_display(); // Mostra l'indicatore livello (top-left)
|
||||
}
|
||||
|
||||
/**
|
||||
* Scrive una stringa nel map_buffer (copria RAM della BG map 32x32) usando gli indici
|
||||
* tile del font IBM (tile = ASCII - 32, il font parte dallo spazio). Usato per la
|
||||
* schermata finale senza dipendere da printf/console.
|
||||
*/
|
||||
static void ending_puttext(uint8_t col, uint8_t row, const char *s) {
|
||||
uint8_t i = 0;
|
||||
while (s[i]) {
|
||||
if (col + i < 32) {
|
||||
map_buffer[(uint16_t)row * 32 + col + i] = (uint8_t)(s[i] - ' ');
|
||||
}
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loop Principale di Aggiornamento del Gioco (chiamato ad ogni Frame ~60 FPS).
|
||||
* Questo metodo funge da "Direttore d'Orchestra", delegando i compiti ai moduli specializzati.
|
||||
@@ -187,11 +228,27 @@ void engine_update(uint8_t keys, uint8_t prev_keys) {
|
||||
} else if (game_over == 2) {
|
||||
// Mostra la schermata "Going Deeper"
|
||||
HIDE_SPRITES;
|
||||
// Reset scroll so the full screen image is aligned
|
||||
SCX_REG = 0;
|
||||
SCY_REG = 0;
|
||||
set_bkg_data(0, next_level_TILE_COUNT, next_level_tiles);
|
||||
set_bkg_tiles(0, 0, 20, 18, next_level_map);
|
||||
} else if (game_over == 3) {
|
||||
// FINALE: livello 8 superato, il gioco finisce. Schermata di chiusura
|
||||
// con testo ricavato dal font IBM (ricaricato perche' il gameplay lo
|
||||
// aveva sovrascritto coi tile del labirinto). Scriviamo direttamente
|
||||
// nel map_buffer (tile = ASCII - 32) senza dipendere da printf.
|
||||
HIDE_SPRITES;
|
||||
SCX_REG = 0;
|
||||
SCY_REG = 0;
|
||||
font_init();
|
||||
font_t end_font = font_load(font_ibm);
|
||||
font_set(end_font);
|
||||
memset(map_buffer, 0, sizeof(map_buffer)); // tile 0 = spazio (sfondo)
|
||||
ending_puttext(4, 6, "YOU ESCAPED");
|
||||
ending_puttext(3, 8, "THE DARKNESS");
|
||||
ending_puttext(2, 11, "LEVEL 8 CLEARED");
|
||||
ending_puttext(3, 14, "PRESS START");
|
||||
set_bkg_tiles(0, 0, 32, 32, map_buffer);
|
||||
}
|
||||
|
||||
// Imposta i timer audio per far iniziare la musica finale dal modulo sound.c
|
||||
@@ -207,42 +264,34 @@ void engine_update(uint8_t keys, uint8_t prev_keys) {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Dopo il timer, disegna i metasprite di GAME OVER fissi in mezzo allo schermo
|
||||
// Dopo il timer: nasconde tutti i fantasmi (niente AI durante il game over)
|
||||
for (uint8_t i = 0; i < MAX_ENEMIES; i++) {
|
||||
move_metasprite(enemy_metasprites[0], player_TILE_COUNT, 2 + i * 2, 0, 0);
|
||||
}
|
||||
// Sconfitta: metasprite GAME OVER + giocatore al centro.
|
||||
if (game_over == 1) {
|
||||
move_metasprite(gameover_metasprites[0], player_TILE_COUNT + enemy_TILE_COUNT, 8, 88, 120);
|
||||
}
|
||||
|
||||
// Il giocatore rimane visibile al centro, il nemico scompare se vinto o appare se sconfitto.
|
||||
update_player_sprite();
|
||||
|
||||
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 (game_over == 1 && ep_dist <= 2 && enemy_screen_x >= -8 && enemy_screen_x <= 168 && enemy_screen_y >= -8 && enemy_screen_y <= 152) {
|
||||
move_metasprite(enemy_metasprites[0], player_TILE_COUNT, 4, enemy_screen_x, enemy_screen_y);
|
||||
} else {
|
||||
move_metasprite(enemy_metasprites[0], player_TILE_COUNT, 4, 0, 0);
|
||||
update_player_sprite();
|
||||
}
|
||||
|
||||
// Attende la pressione di START per riavviare
|
||||
if ((keys & J_START) && !(prev_keys & J_START)) {
|
||||
// Nasconde la grafica testuale e ricarica tutto l'engine.
|
||||
// Vittoria (Going Deeper) -> livello successivo.
|
||||
// Sconfitta -> si ricomincia dallo stesso livello raggiunto (non si azzera).
|
||||
if (game_over == 2) {
|
||||
// Going Deeper -> livello successivo
|
||||
level++;
|
||||
move_metasprite(gameover_metasprites[0], player_TILE_COUNT + enemy_TILE_COUNT, 8, 0, 0);
|
||||
SHOW_SPRITES;
|
||||
engine_init();
|
||||
} else if (game_over == 3) {
|
||||
// Finale: torna al titolo (la prossima partita ripartira' dal livello 1)
|
||||
app_state = 0;
|
||||
title_init();
|
||||
} else {
|
||||
// Sconfitta: ricomincia dallo stesso livello raggiunto (non si azzera)
|
||||
move_metasprite(gameover_metasprites[0], player_TILE_COUNT + enemy_TILE_COUNT, 8, 0, 0);
|
||||
SHOW_SPRITES;
|
||||
engine_init();
|
||||
}
|
||||
move_metasprite(gameover_metasprites[0], player_TILE_COUNT + enemy_TILE_COUNT, 8, 0, 0);
|
||||
SHOW_SPRITES;
|
||||
engine_init();
|
||||
}
|
||||
}
|
||||
return;
|
||||
|
||||
+14
-9
@@ -8,6 +8,8 @@ uint8_t map_size = MAP_SIZE;
|
||||
uint8_t maze[MAX_MAP_SIZE][MAX_MAP_SIZE];
|
||||
uint8_t map_buffer[32 * 32];
|
||||
|
||||
uint8_t fog_radius = 2;
|
||||
|
||||
uint8_t player_lx = 1;
|
||||
uint8_t player_ly = 1;
|
||||
uint8_t player_dir = 0;
|
||||
@@ -28,15 +30,18 @@ int16_t target_px, target_py;
|
||||
|
||||
uint8_t stamina = 100;
|
||||
uint8_t stamina_recharge_timer = 0;
|
||||
uint8_t stamina_recharge_rate = 60;
|
||||
|
||||
uint8_t level = 1;
|
||||
|
||||
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 num_enemies = 1;
|
||||
uint8_t enemy_step_cooldown = 60;
|
||||
uint8_t enemy_lx[MAX_ENEMIES] = {0};
|
||||
uint8_t enemy_ly[MAX_ENEMIES] = {0};
|
||||
uint8_t enemy_is_moving[MAX_ENEMIES] = {0};
|
||||
uint8_t enemy_move_progress[MAX_ENEMIES] = {0};
|
||||
int8_t enemy_start_lx[MAX_ENEMIES], enemy_start_ly[MAX_ENEMIES];
|
||||
int8_t enemy_target_lx[MAX_ENEMIES], enemy_target_ly[MAX_ENEMIES];
|
||||
int16_t enemy_start_px[MAX_ENEMIES], enemy_start_py[MAX_ENEMIES];
|
||||
int16_t enemy_target_px[MAX_ENEMIES], enemy_target_py[MAX_ENEMIES];
|
||||
uint8_t enemy_cooldown[MAX_ENEMIES] = {0};
|
||||
+27
-26
@@ -7,71 +7,72 @@
|
||||
* ==========================================
|
||||
* GLOBAL GAME STATE
|
||||
* ==========================================
|
||||
* This file contains all the global variables that are shared across different
|
||||
* logical modules (e.g., rendering, player logic, enemy logic, audio).
|
||||
* We place them here to avoid circular dependencies between modules.
|
||||
* Shared variables across modules (rendering, player, enemy, audio).
|
||||
* Centralized here to avoid circular dependencies in C.
|
||||
*/
|
||||
|
||||
// --- General Application State ---
|
||||
// app_state = 0 means Title Screen, 1 means Gameplay
|
||||
extern uint8_t app_state;
|
||||
|
||||
// game_over = 0 means Playing, 1 means Defeat (Ghost caught you), 2 means Victory (Reached portal)
|
||||
// game_over: 0 = Playing, 1 = Defeat (caught), 2 = Victory/Going Deeper (next level),
|
||||
// 3 = Finale (cleared level 8 -> game complete)
|
||||
extern volatile uint8_t game_over;
|
||||
// Timer used to delay actions after game over (e.g. before showing text or playing music)
|
||||
extern volatile uint8_t game_over_timer;
|
||||
|
||||
// --- Map Data ---
|
||||
#define MAP_SIZE 7 // initial maze size (level 1); must be odd
|
||||
#define MAX_MAP_SIZE 17 // hard cap for maze growth (odd); used as the array bound
|
||||
#define MAX_MAP_SIZE 21 // hard cap for maze growth (odd); array bound. Reached at level 8.
|
||||
// The generated maze: 0 = Wall, 1 = Floor, 2 = Victory Tile (hatch)
|
||||
extern uint8_t map_size; // current maze side length (grows with level, MAP_SIZE..MAX_MAP_SIZE)
|
||||
extern uint8_t map_size; // current maze side length (MAP_SIZE + 2*(level-1), capped)
|
||||
extern uint8_t maze[MAX_MAP_SIZE][MAX_MAP_SIZE];
|
||||
|
||||
// --- Fog of War ---
|
||||
extern uint8_t fog_radius; // Chebyshev visibility radius (2 normally, 1 from level 7)
|
||||
|
||||
// --- Camera & Rendering ---
|
||||
// Map buffer used to draw the isometric tiles into the Game Boy Background map
|
||||
extern uint8_t map_buffer[32 * 32];
|
||||
// Camera scroll coordinates to keep the player centered
|
||||
extern uint8_t scroll_x;
|
||||
extern int8_t scroll_y;
|
||||
|
||||
extern uint8_t stairs_lx;
|
||||
extern uint8_t stairs_ly;
|
||||
|
||||
// --- Mappa e Entita ---
|
||||
// --- Player ---
|
||||
extern uint8_t player_lx;
|
||||
extern uint8_t player_ly;
|
||||
extern uint8_t player_dir; // 0=DR, 1=DL, 2=UL, 3=UR
|
||||
|
||||
// --- Movement & Physics State ---
|
||||
// Shared variables for interpolated movement (walking/jumping)
|
||||
extern uint8_t is_moving;
|
||||
extern uint8_t is_jumping;
|
||||
extern uint8_t is_running; // B+direction: step in 8 frames instead of 16, costs 10 stamina/tile
|
||||
extern uint8_t move_progress; // 0 to 16, tracks the sub-tile animation progress
|
||||
extern uint8_t is_running; // B+direction: 8-frame step, 10 stamina/tile
|
||||
extern uint8_t move_progress; // 0 to 16
|
||||
extern int8_t start_lx, start_ly;
|
||||
extern int8_t target_lx, target_ly;
|
||||
extern int16_t start_px, start_py;
|
||||
extern int16_t target_px, target_py;
|
||||
|
||||
// --- Stamina System ---
|
||||
// Stamina powers the jump/run mechanic. 100 = full. Recharges over time.
|
||||
extern uint8_t stamina;
|
||||
extern uint8_t stamina_recharge_timer;
|
||||
extern uint8_t stamina_recharge_rate; // frames per +1 stamina (grows with level)
|
||||
|
||||
// --- Level Progression ---
|
||||
// Current level (starts at 1, increments each time the hatch is reached).
|
||||
extern uint8_t level;
|
||||
extern uint8_t level; // starts at 1; game completes after clearing level 8
|
||||
|
||||
// --- Enemy State ---
|
||||
extern uint8_t enemy_lx;
|
||||
extern uint8_t enemy_ly;
|
||||
extern uint8_t enemy_is_moving;
|
||||
extern uint8_t enemy_move_progress;
|
||||
extern int8_t enemy_start_lx, enemy_start_ly;
|
||||
extern int8_t enemy_target_lx, enemy_target_ly;
|
||||
extern int16_t enemy_start_px, enemy_start_py;
|
||||
extern int16_t enemy_target_px, enemy_target_py;
|
||||
extern uint8_t enemy_cooldown;
|
||||
// --- Enemy System (multi-entity: up to MAX_ENEMIES ghosts) ---
|
||||
#define MAX_ENEMIES 8
|
||||
extern uint8_t num_enemies; // current ghost count (= level, capped at MAX_ENEMIES)
|
||||
extern uint8_t enemy_step_cooldown; // base pause (frames) between ghost steps; shrinks with level
|
||||
extern uint8_t enemy_lx[MAX_ENEMIES];
|
||||
extern uint8_t enemy_ly[MAX_ENEMIES];
|
||||
extern uint8_t enemy_is_moving[MAX_ENEMIES];
|
||||
extern uint8_t enemy_move_progress[MAX_ENEMIES];
|
||||
extern int8_t enemy_start_lx[MAX_ENEMIES], enemy_start_ly[MAX_ENEMIES];
|
||||
extern int8_t enemy_target_lx[MAX_ENEMIES], enemy_target_ly[MAX_ENEMIES];
|
||||
extern int16_t enemy_start_px[MAX_ENEMIES], enemy_start_py[MAX_ENEMIES];
|
||||
extern int16_t enemy_target_px[MAX_ENEMIES], enemy_target_py[MAX_ENEMIES];
|
||||
extern uint8_t enemy_cooldown[MAX_ENEMIES];
|
||||
|
||||
#endif
|
||||
+5
-3
@@ -33,9 +33,10 @@ static uint8_t das_active = 0;
|
||||
*/
|
||||
void update_player_movement(uint8_t keys, uint8_t prev_keys) {
|
||||
// --- Gestione della Stamina ---
|
||||
// Ricarica la stamina di 1 punto al secondo (60 frames su Game Boy)
|
||||
// Ricarica la stamina di 1 punto ogni `stamina_recharge_rate` frame (1/s al livello 1,
|
||||
// piu' lento ai livelli alti). `stamina_recharge_rate` e' impostato in engine_init.
|
||||
stamina_recharge_timer++;
|
||||
if (stamina_recharge_timer >= 60) {
|
||||
if (stamina_recharge_timer >= stamina_recharge_rate) {
|
||||
stamina_recharge_timer = 0;
|
||||
if (stamina < 100) {
|
||||
stamina++;
|
||||
@@ -87,7 +88,8 @@ void update_player_movement(uint8_t keys, uint8_t prev_keys) {
|
||||
|
||||
// Controllo Casella Vittoria (ID 2)
|
||||
if (maze[player_ly][player_lx] == 2) {
|
||||
game_over = 2; // Stato 2 = Vittoria
|
||||
// Livello 8 completato -> finale (gioco finito); altrimenti Going Deeper (lvl successivo).
|
||||
game_over = (level >= 8) ? 3 : 2;
|
||||
game_over_timer = 30; // Piccolo ritardo prima che la scena cambi
|
||||
update_stamina_display();
|
||||
|
||||
|
||||
+25
-18
@@ -18,8 +18,8 @@ uint8_t get_tile_state(int8_t cx, int8_t cy, int8_t lx, int8_t ly) {
|
||||
if (dx < 0) dx = -dx;
|
||||
if (dy < 0) dy = -dy;
|
||||
int8_t dist = (dx > dy) ? dx : dy;
|
||||
if (dist > 2) return 0;
|
||||
if (dist == 2) return 2;
|
||||
if (dist > (int8_t)fog_radius) return 0;
|
||||
if (dist == (int8_t)fog_radius) return 2;
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -146,15 +146,15 @@ void draw_map(uint8_t center_x, uint8_t center_y) {
|
||||
// Svuotiamo l'intero buffer della mappa con l'indice 0 (casella nera vuota)
|
||||
memset(map_buffer, 0, sizeof(map_buffer));
|
||||
|
||||
// Calcoliamo la "finestra" di mappa 5x5 da processare (per ottimizzazione, ignoriamo la mappa intera)
|
||||
int8_t start_x = center_x - 2;
|
||||
// Calcoliamo la "finestra" di mappa (2r+1 x 2r+1) da processare (fog_radius)
|
||||
int8_t start_x = center_x - fog_radius;
|
||||
if (start_x < 0) start_x = 0;
|
||||
int8_t end_x = center_x + 2;
|
||||
int8_t end_x = center_x + fog_radius;
|
||||
if ((uint8_t)end_x >= map_size) end_x = map_size - 1;
|
||||
|
||||
int8_t start_y = center_y - 2;
|
||||
int8_t start_y = center_y - fog_radius;
|
||||
if (start_y < 0) start_y = 0;
|
||||
int8_t end_y = center_y + 2;
|
||||
int8_t end_y = center_y + fog_radius;
|
||||
if ((uint8_t)end_y >= map_size) end_y = map_size - 1;
|
||||
|
||||
// Processiamo la mappa in DUE passate per risolvere il problema dell'overlapping isometrico.
|
||||
@@ -175,7 +175,7 @@ void draw_map(uint8_t center_x, uint8_t center_y) {
|
||||
if (dy < 0) dy = -dy;
|
||||
int8_t dist = (dx > dy) ? dx : dy;
|
||||
|
||||
if (dist > 2) continue;
|
||||
if (dist > (int8_t)fog_radius) continue;
|
||||
|
||||
int8_t iso_x = (lx - ly) * 2 + 12;
|
||||
int8_t iso_y = (lx + ly) * 1 + 2;
|
||||
@@ -186,7 +186,7 @@ void draw_map(uint8_t center_x, uint8_t center_y) {
|
||||
uint8_t mask = state_tl + state_tr * 3;
|
||||
|
||||
uint8_t v;
|
||||
if (dist == 2) {
|
||||
if (dist == (int8_t)fog_radius) {
|
||||
v = is_alt ? (27 + mask) : (18 + mask);
|
||||
} else {
|
||||
v = is_alt ? (9 + mask) : mask;
|
||||
@@ -214,7 +214,7 @@ void draw_map(uint8_t center_x, uint8_t center_y) {
|
||||
if (dy < 0) dy = -dy;
|
||||
int8_t dist = (dx > dy) ? dx : dy;
|
||||
|
||||
if (dist > 2) continue;
|
||||
if (dist > (int8_t)fog_radius) continue;
|
||||
|
||||
int8_t iso_x = (lx - ly) * 2 + 12;
|
||||
int8_t iso_y = (lx + ly) * 1 + 2;
|
||||
@@ -228,7 +228,7 @@ void draw_map(uint8_t center_x, uint8_t center_y) {
|
||||
uint16_t mask = state_tl + state_tr * 3 + state_bl * 9 + state_br * 27;
|
||||
|
||||
uint16_t v;
|
||||
if (dist == 2) {
|
||||
if (dist == (int8_t)fog_radius) {
|
||||
v = is_alt ? (36 + 243 + mask) : (36 + 162 + mask);
|
||||
} else {
|
||||
v = is_alt ? (36 + 81 + mask) : (36 + mask);
|
||||
@@ -248,13 +248,20 @@ void draw_map(uint8_t center_x, uint8_t center_y) {
|
||||
|
||||
update_stamina_display();
|
||||
|
||||
// Trasferiamo l'intera mappa 32x32 (1024 byte) al Background hardware.
|
||||
// Con labirinti grandi (map_size > 7) la finestra fog-of-war 5x5, proiettata in
|
||||
// coordinate isometriche assolute, puo' cadere in righe OLTRE il vecchio range
|
||||
// 2-17 (a causa del wrapping & 31), quindi non basta piu' flussare solo 16 righe.
|
||||
// draw_map e' chiamato solo ai passi del movimento (non ogni frame), quindi il
|
||||
// costo del flush completo e' sostenibile.
|
||||
set_bkg_tiles(0, 0, 32, 32, map_buffer);
|
||||
// Flush dinamico a 16 righe (512 byte) centrato sulla iso_y del centro di disegno,
|
||||
// con gestione del wrap della mappa 32x32. 16 righe = prestazioni del progetto
|
||||
// originale; centrando su center_iso_y la nebbia ricade sempre nelle righe flussate
|
||||
// anche nei labirinti grandi, dove le iso_y assolute wrappano fuori dal vecchio
|
||||
// range fisso 2-17.
|
||||
int16_t center_iso_y = (int16_t)center_x + (int16_t)center_y + 2;
|
||||
uint8_t start = (uint8_t)((center_iso_y - 8) & 31);
|
||||
if (start + 16 <= 32) {
|
||||
set_bkg_tiles(0, start, 32, 16, &map_buffer[(uint16_t)start * 32]);
|
||||
} else {
|
||||
uint8_t first = 32 - start;
|
||||
set_bkg_tiles(0, start, 32, first, &map_buffer[(uint16_t)start * 32]);
|
||||
set_bkg_tiles(0, 0, 32, 16 - first, map_buffer);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+1
-1
@@ -305,7 +305,7 @@ void play_music_tick(void) {
|
||||
gameover_music_step++;
|
||||
}
|
||||
}
|
||||
} else if (game_over == 2) { // Next Level (Going Deeper)
|
||||
} else if (game_over == 2 || game_over == 3) { // Next Level (Going Deeper) o Finale
|
||||
victory_music_timer++;
|
||||
if (victory_music_timer >= 15) { // 15 frames per note = 4 notes/sec
|
||||
victory_music_timer = 0;
|
||||
|
||||
Reference in New Issue
Block a user