diff --git a/README.md b/README.md index 672c830..e995815 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ Un survival-horror procedurale in prospettiva isometrica per Game Boy (DMG/CGB), - **Delayed Auto-Shift (DAS)**: controlli alla Tetris — delay iniziale di 12 frame e ripetizione ogni 6 frame per il movimento continuo tenendo premuto il D-Pad. - **Salto evasivo con Stamina**: A+direzione scavalca il blocco adiacente atterrando 2 tile più in là (la cella intermedia deve essere un muro). Costa 60 punti stamina; la barra si ricarica di 1 punto al secondo. - **Corsa con Stamina**: B+direzione fa correre il protagonista: il passo dura 8 frame invece di 16 e consuma 10 stamina per tile, con DAS più rapido per incatenare i tile fluidamente. Se la stamina scende sotto 10 ripiega automaticamente sulla camminata normale. -- **Progressione livelli**: si parte dal livello 1; raggiungere la botola fa sprofondare nel livello successivo (livello +1). L'indicatore `L` in alto a sinistra (sprite HUD, come la barra stamina in alto a destra) mostra il livello corrente e si aggiorna ad ogni livello superato. In caso di sconfitta si ricomincia dall'**ultimo livello raggiunto** (il contatore non si azzera). **La dimensione del labirinto cresce col livello**: +2 tile per lato ad ogni livello (7→9→11→13→15→17), capped a 17x17, così la difficoltà aumenta progressivamente. +- **Progressione livelli (8 livelli, poi finale)**: si parte dal livello 1; raggiungere la botola fa sprofondare nel livello successivo. L'indicatore `L` in alto a sinistra mostra il livello corrente. In caso di sconfitta si ricomincia dall'ultimo livello raggiunto. **La dimensione del labirinto cresce di 2 tile per lato ad ogni livello** (7→9→11→13→15→17→19→21x21 al livello 8). **Difficoltà progressiva**: +1 fantasma per livello (fino a 8), fantasma più veloce (cooldown più corto), stamina che si ricarica più lentamente, e nebbia più stretta (5x5→3x3) dal livello 7. **Superato il livello 8 il gioco finisce** con una schermata finale. - **AI del fantasma**: pathfinding greedy con distanza al quadrato (niente sqrt, niente A*), cooldown di 1 secondo tra i passi, hitbox pixel-perfect (12×6 px) per una morte "giusta". - **Audio procedurale**: colonna sonora sintetizzata manipolando direttamente i registri APU via VBL interrupt (nessun campione). - **Schermate a tutto schermo**: copertina 2-bit nativa per il titolo, immagine "Going Deeper" per la vittoria, metasprite "GAME OVER" per la sconfitta. diff --git a/doc/AScreamFromTheDark_report.md b/doc/AScreamFromTheDark_report.md index bfaf4cb..5d84a26 100644 --- a/doc/AScreamFromTheDark_report.md +++ b/doc/AScreamFromTheDark_report.md @@ -57,7 +57,7 @@ Camera centrata: `scroll_x = px - 64`, `scroll_y = py - 72` (160×144 display ### 3.2 Generazione del labirinto (`maze.c`) - **DFS iterativo con stack in WRAM** (`stack_x/y` statici, sized `(MAX_MAP_SIZE/2)^2`): evita l'overflow dello stack hardware del LR35902. Celle dispari = stanze, celle pari = muri divisori; scava il muro di mezzo con la media aritmetica delle coordinate. -- **Dimensione crescente col livello**: `map_size = MAP_SIZE + 2*(level-1)`, capped a `MAX_MAP_SIZE`=17 (sempre dispari). L'array `maze` è allocato `[MAX_MAP_SIZE][MAX_MAP_SIZE]` (289 byte) e i moduli usano `map_size` come bound runtime. +- **Dimensione crescente col livello**: `map_size = MAP_SIZE + 2*(level-1)`, capped a `MAX_MAP_SIZE`=21 (raggiunto al livello 8, sempre dispari). L'array `maze` è allocato `[MAX_MAP_SIZE][MAX_MAP_SIZE]` (441 byte) e i moduli usano `map_size` come bound runtime. - **Fase 2 — loop**: riapre muri residui che collegano due corridoi opposti con probabilità 15%. Vitale per il gameplay d'inseguimento. - **Fase 3 — botola**: sceglie una cella calpestabile a distanza di Chebyshev ≥ `map_size/2` dalla partenza `(1,1)` (soglia scalata: 3 per 7x7, 8 per 17x17), tra **tutte** le celle del labirinto. Fallback sulla cella più lontana in assoluto. @@ -88,13 +88,15 @@ Distanza di **Chebyshev** `max(|dx|,|dy|)` invece di euclidea (no sqrt, no looku - Questo evita l'uso di sprite hardware per la botola (che causerebbero flickering per il limite di 10 sprite/scanline) e contiene il consumo di VRAM (limite 256 tile). ### 3.6 Ottimizzazione rendering -`memset` azzera `map_buffer` (32x32) ad ogni `draw_map`, poi disegna solo la finestra fog 5x5 (~25 tile). Trasferisce l'intera mappa 32x32 al background hardware (`set_bkg_tiles(0,0,32,32,map_buffer)`): necessario perché con labirinti grandi (`map_size` > 7) la finestra fog, proiettata in coordinate iso assolute, può cadere fuori dal range di righe che la vecchia ottimizzazione (righe 2-17) flussava, a causa del wrapping `& 31`. `draw_map` è chiamato solo ai passi del movimento (frame 8 e completamento, non ogni frame), quindi il flush completo è sostenibile. Aggiornamento a `move_progress==8` (metà passo) per fluidità del fog of war. `map_buffer` usato con wrap `& 31` per gestire lo scroll modulare. +`memset` azzera `map_buffer` (32x32) ad ogni `draw_map`, poi disegna solo la finestra fog (2r+1)^2. Flush **dinamico a 16 righe** (512 byte) centrato sulla iso_y del centro di disegno, con gestione del wrap della mappa 32x32 via due `set_bkg_tiles` quando la finestra attraversa il bordo. Necessario perché con labirinti grandi la nebbia, in coordinate iso assolute, wrappa fuori dal vecchio range fisso 2-17; 16 righe mantengono le prestazioni originali. Aggiornamento a `move_progress==8` (metà passo) per fluidità del fog of war. -### 3.7 AI del fantasma (`enemy_logic.c`) -- **Greedy invece di A***: confronta la distanza al quadrato `dx²+dy²` delle 4 celle adiacenti calpestabili e sceglie la minima. No sqrt (pesante su 4 MHz), no heap/nodi (frame drops inammissibili). Difetto voluto: si incastra in vicoli a U → diventa una dinamica di gameplay (seminarlo col level design). -- **Cooldown**: dopo ogni passo (16 frame di LERP) rimane congelato 60 frame (1 s). Il giocatore è più veloce → il salto evasivo è il salvavita. -- **Hitbox pixel-perfect**: la morte non scatta per coincidenza di cella logica, ma per sovrapposizione dei pixel fisici con tolleranza `|dx|<12 && |dy|<6`. Più "giusta" agli occhi del giocatore. -- Culling off-screen: `enemy_screen_x` e `_y` con wrap `& 255` + soglie `-8..168` / `-8..152`; se fuori range o `ep_dist > 2` lo sprite viene spostato a `(0,0)` (offscreen). +### 3.7 AI dei fantasmi (`enemy_logic.c`) — multi-entity +- **Fino a 8 fantasmi** (`num_enemies = level`, capped `MAX_ENEMIES=8`): stato in array indicizzati (`enemy_lx[i]`, `enemy_is_moving[i]`, ...). Ogni fantasma usa 2 slot OAM a partire da `2 + i*2` (il player usa 0-1). +- **Greedy invece di A***: per ciascun fantasma, tra le 4 celle adiacenti calpestabili sceglie quella che minimizza la distanza al quadrato verso il giocatore (no sqrt, no heap). Difetto voluto: si incastra nei vicoli a U. +- **Cooldown scalabile**: dopo ogni passo (16 frame LERP) il fantasma aspetta `enemy_step_cooldown` frame (60 al L1, ~11 al L8). Cooldown iniziali sfasati (i*8) per non sincronizzarli. +- **Attivazione**: insegue solo se entro il raggio di nebbia (Chebyshev ≤ `fog_radius`). +- **Hitbox pixel-perfect**: la morte scatta se i pixel di un qualunque fantasma si sovrappongono a quelli del giocatore (|dx|<12, |dy|<6); al primo catch il loop si ferma (`return`). +- **Culling**: `enemy_screen_x/y` con wrap `& 255` + soglie fisiche; fuori range o oltre la nebbia lo sprite va offscreen. ### 3.8 Audio (`sound.c`) Sequencer agganciato al **VBL interrupt** (`add_VBL(play_music_tick)`), ~60 Hz, non blocca il rendering. Usa i 4 canali APU: diff --git a/doc/gameplay.md b/doc/gameplay.md index ca69afd..f8a5d5d 100644 --- a/doc/gameplay.md +++ b/doc/gameplay.md @@ -39,10 +39,18 @@ Se la Stamina scende sotto 10, B+direzione **ripiega silenziosamente su camminat Il gioco parte dal **livello 1**. Raggiungere la botola (casella traguardo) significa "sprofondare più giù" (*Going Deeper*): alla pressione di START il livello viene **incrementato** e viene generato un nuovo labirinto. In caso di sconfitta (cattura da parte del fantasma) si **ricomincia dallo stesso livello raggiunto**: il contatore non si azzera, solo il passaggio per il titolo (nuova partita) riparte da 1. -## Dimensione crescente del labirinto +## Dimensione crescente del labirinto e difficoltà progressiva -Ad ogni livello la **dimensione del labirinto cresce di 2 tile per lato**: si parte da 7x7 (livello 1) e si arriva a 9x9, 11x11, 13x13, 15x15, fino al cap di **17x17**. La dimensione corrente è la variabile globale `map_size` (l'array `maze` è allocato con bound `MAX_MAP_SIZE` = 17 in modo da contenere qualunque dimensione). Il DFS in `maze.c` genera un perfect maze su `map_size`x`map_size` (le celle dispari sono le stanze, per questo `map_size` è sempre dispari), poi lo rompe con i loop al 15% e posiziona la botola a distanza di Chebyshev ≥ `map_size/2` dalla partenza. Anche lo spawn del fantasma scala: parte a distanza ≥ `map_size/2`, così nelle mappe grandi non si attiva subito (il suo cono visivo resta comunque 5x5, fissato dal fog of war). +Ad ogni livello la **dimensione del labirinto cresce di 2 tile per lato**: 7x7 (livello 1) fino a **21x21** (livello 8). La dimensione corrente è la variabile globale `map_size` (l'array `maze` è allocato con bound `MAX_MAP_SIZE` = 21). Il DFS genera un perfect maze su `map_size`x`map_size` (le celle dispari sono le stanze, per questo `map_size` è sempre dispari), poi lo rompe con i loop al 15% e posiziona la botola a distanza di Chebyshev ≥ `map_size/2` dalla partenza. -Poiché con labirinti grandi la finestra fog-of-war 5x5, proiettata in coordinate isometriche assolute, può cadere fuori dal vecchio range di righe flussato (2-17) a causa del wrapping della mappa 32x32, `draw_map` ora flussa l'intera mappa 32x32 al hardware background. `draw_map` è chiamato solo ai passi del movimento (non ogni frame), quindi il costo è sostenibile. +Oltre alla dimensione, ci sono altri **assi di difficoltà** che scalano col livello: +- **Numero di fantasmi**: `num_enemies = level` (capped a `MAX_ENEMIES` = 8). Ogni fantasma ha stato indipendente (array) e AI greedy propria; cooldown iniziali sfasati per non sincronizzarli. +- **Fantasma più veloce**: `enemy_step_cooldown = 60 - 7*(level-1)` (floor 10) — pausa più breve tra i passi ai livelli alti. +- **Stamina più lenta**: `stamina_recharge_rate = 60 + 12*(level-1)` — la barra si ricarica più lentamente. +- **Nebbia più stretta**: `fog_radius = 1` (3x3) dal livello 7 (invece di 2 / 5x5). + +**Il gioco finisce dopo il livello 8**: superare la botola al livello 8 setta `game_over = 3` (finale) invece di 2 (Going Deeper). La schermata finale usa il font IBM ricaricato per stampare "YOU ESCAPED / THE DARKNESS / LEVEL 8 CLEARED / PRESS START"; START torna al titolo (nuova partita dal livello 1). + +Poiché con labirinti grandi la finestra fog-of-war, proiettata in coordinate isometriche assolute, può cadere fuori dal vecchio range di righe flussato (2-17) a causa del wrapping della mappa 32x32, `draw_map` ora usa un flush **dinamico a 16 righe** centrato sulla iso_y del centro di disegno (con gestione del wrap via due `set_bkg_tiles`). Questo mantiene le prestazioni del progetto originale (16 righe = 512 byte) coprendo la nebbia in qualunque posizione. L'indicatore `L` è mostrato in **alto a sinistra** tramite tre sprite hardware (OAM ID 23–25), disegnati dall'asset `level.png` (11 glifi 8x16: 'L', '0'–'9'). Le decine vengono mostrate solo dal livello 10 in poi (sotto, lo sprite decine è spostato fuori schermo). Come la barra stamina (in alto a destra), l'indicatore è a coordinate-schermo fisse e indipendente dallo scroll isometrico, e viene nascosto durante il game over e sul titolo. La base VRAM dei glifi è allineata a un indice **pari** perché in modalità sprite 8x16 l'hardware ignora il bit meno significativo dell'indice tile. diff --git a/src/enemy_logic.c b/src/enemy_logic.c index f6d9343..cf29808 100644 --- a/src/enemy_logic.c +++ b/src/enemy_logic.c @@ -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 // 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 + } } -} +} \ No newline at end of file diff --git a/src/engine.c b/src/engine.c index b751917..6f4dd73 100644 --- a/src/engine.c +++ b/src/engine.c @@ -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; diff --git a/src/globals.c b/src/globals.c index 26da86b..2c7d1d8 100644 --- a/src/globals.c +++ b/src/globals.c @@ -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}; \ No newline at end of file diff --git a/src/globals.h b/src/globals.h index 3f07020..563a079 100644 --- a/src/globals.h +++ b/src/globals.h @@ -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 \ No newline at end of file diff --git a/src/player_logic.c b/src/player_logic.c index c781354..ce3078a 100644 --- a/src/player_logic.c +++ b/src/player_logic.c @@ -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(); diff --git a/src/render.c b/src/render.c index c8f9d10..d919dfc 100644 --- a/src/render.c +++ b/src/render.c @@ -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); + } } /** diff --git a/src/sound.c b/src/sound.c index ea3d85d..5030d27 100644 --- a/src/sound.c +++ b/src/sound.c @@ -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;