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:
2026-06-24 13:04:39 +02:00
parent 5e8709ade0
commit 4968d2e885
10 changed files with 317 additions and 264 deletions
+1 -1
View File
@@ -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<n>` 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<n>` 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.
+9 -7
View File
@@ -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:
+11 -3
View File
@@ -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<n>` è mostrato in **alto a sinistra** tramite tre sprite hardware (OAM ID 2325), 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.
+128 -149
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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;