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:
@@ -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.
|
- **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.
|
- **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.
|
- **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".
|
- **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).
|
- **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.
|
- **Schermate a tutto schermo**: copertina 2-bit nativa per il titolo, immagine "Going Deeper" per la vittoria, metasprite "GAME OVER" per la sconfitta.
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ Camera centrata: `scroll_x = px - 64`, `scroll_y = py - 72` (160×144 display
|
|||||||
|
|
||||||
### 3.2 Generazione del labirinto (`maze.c`)
|
### 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.
|
- **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 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.
|
- **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).
|
- 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
|
### 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`)
|
### 3.7 AI dei fantasmi (`enemy_logic.c`) — multi-entity
|
||||||
- **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).
|
- **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).
|
||||||
- **Cooldown**: dopo ogni passo (16 frame di LERP) rimane congelato 60 frame (1 s). Il giocatore è più veloce → il salto evasivo è il salvavita.
|
- **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.
|
||||||
- **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.
|
- **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.
|
||||||
- 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).
|
- **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`)
|
### 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:
|
Sequencer agganciato al **VBL interrupt** (`add_VBL(play_music_tick)`), ~60 Hz, non blocca il rendering. Usa i 4 canali APU:
|
||||||
|
|||||||
+11
-3
@@ -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.
|
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 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.
|
L'indicatore `L<n>` è 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.
|
||||||
|
|||||||
+128
-149
@@ -1,6 +1,6 @@
|
|||||||
#include "enemy_logic.h"
|
#include "enemy_logic.h"
|
||||||
#include "globals.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>
|
#include <gb/gb.h>
|
||||||
|
|
||||||
// Tile graphics required for metasprites
|
// Tile graphics required for metasprites
|
||||||
@@ -8,163 +8,142 @@
|
|||||||
#include "enemy.h"
|
#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:
|
* Sviluppo & Scelte Architetturali:
|
||||||
* 1. Cooldown System: Il fantasma fa "passi" esatti sulla griglia proprio come il giocatore.
|
* 1. Multi-entity: fino a MAX_ENEMIES (8) fantasmi coesistenti. Lo stato di ciascuno
|
||||||
* Tuttavia, se si muovesse a ogni frame, sarebbe impossibile scappare.
|
* e' in array indicizzati (enemy_lx[i], enemy_is_moving[i], ...). Il numero attivo
|
||||||
* Abbiamo implementato un timer di `enemy_cooldown` (60 frame = 1 secondo) di pausa
|
* e' num_enemies (= livello, capped). Ogni fantasma usa 2 slot OAM (sprite 8x16)
|
||||||
* tra un passo e l'altro. Il giocatore cammina/salta più velocemente, creando una
|
* a partire da OAM 2 + i*2 (il player usa 0-1).
|
||||||
* tensione in cui devi pianificare i tuoi balzi per seminarlo.
|
* 2. Cooldown: dopo ogni passo (16 frame di LERP) il fantasma aspetta `enemy_step_cooldown`
|
||||||
* 2. Pathfinding (Ricerca del Percorso): Invece di usare A* (A-Star) che consumerebbe
|
* frame prima del prossimo (scalato col livello: piu' breve = piu' veloce). I cooldown
|
||||||
* troppa RAM e CPU per un Game Boy a 8-bit, il fantasma usa una logica "Greedy".
|
* iniziali sono sfasati (i*8) cosi' non si muovono in sincrono.
|
||||||
* Calcola la distanza al quadrato verso il giocatore per ciascuna delle 4 direzioni
|
* 3. Pathfinding Greedy (non A*): per ogni fantasma, tra le 4 celle adiacenti calpestabili
|
||||||
* valide (muri permettendo). Sceglie la direzione che *minimizza* questa distanza.
|
* sceglie quella che minimizza la distanza al quadrato verso il giocatore (no sqrt, no
|
||||||
* Non essendo A*, può incastrarsi in vicoli ciechi a forma di "U", che è esattamente
|
* heap: sostenibile su 4 MHz anche con 8 fantasmi). Difetto voluto: si incastra nei
|
||||||
* una debolezza voluta per permettere al giocatore di seminarlo usando il level design.
|
* vicoli a U -> dinamica di gioco per seminarli.
|
||||||
* 3. Collisione Pixel-Perfect: Anche se logica e pathfinding sono su griglia (Grid-Based),
|
* 4. Attivazione: il fantasma insegue solo se entro il raggio di nebbia (Chebyshev <=
|
||||||
* la Morte scatta solo se i *pixel* fisici a schermo dei due sprite si sovrappongono.
|
* 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) {
|
void update_enemy_logic(void) {
|
||||||
// 1. Aggiorna l'interpolazione del movimento visivo del fantasma
|
// Posizione pixel del giocatore (interpolata se in movimento), usata per la collisione.
|
||||||
if (enemy_is_moving) {
|
int16_t p_px = is_moving ? (start_px + (((target_px - start_px) * (int16_t)move_progress) >> 4))
|
||||||
enemy_move_progress++;
|
: ((player_lx - player_ly) * 16 + 96);
|
||||||
if (enemy_move_progress == 16) {
|
int16_t p_py = is_moving ? (start_py + (((target_py - start_py) * (int16_t)move_progress) >> 4))
|
||||||
// Movimento completato
|
: ((player_lx + player_ly) * 8 + 16);
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Decrementa il timer di riposo se non si sta muovendo
|
for (uint8_t i = 0; i < num_enemies; i++) {
|
||||||
if (enemy_cooldown > 0) {
|
// 1. Aggiorna l'interpolazione del movimento visivo
|
||||||
enemy_cooldown--;
|
if (enemy_is_moving[i]) {
|
||||||
}
|
enemy_move_progress[i]++;
|
||||||
|
if (enemy_move_progress[i] == 16) {
|
||||||
// 3. AI PATHFINDING: Calcola il prossimo passo se è pronto a muoversi
|
enemy_is_moving[i] = 0;
|
||||||
if (!enemy_is_moving && enemy_cooldown == 0) {
|
enemy_lx[i] = enemy_target_lx[i];
|
||||||
// Calcola Distanza di Chebyshev logica dal giocatore per sapere se siamo "vicini"
|
enemy_ly[i] = enemy_target_ly[i];
|
||||||
int8_t dx = (int8_t)player_lx - (int8_t)enemy_lx;
|
// Pausa prima del prossimo passo (scalata col livello).
|
||||||
int8_t dy = (int8_t)player_ly - (int8_t)enemy_ly;
|
enemy_cooldown[i] = enemy_step_cooldown;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// 4. RENDERING DEL NEMICO (Telecamera relativa)
|
// 2. Decrementa il timer di riposo
|
||||||
// Recuperiamo i pixel assoluti
|
if (enemy_cooldown[i] > 0) {
|
||||||
int16_t enemy_px, enemy_py;
|
enemy_cooldown[i]--;
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5. COLLISIONE FATALE (Player Hitbox check)
|
// 3. AI PATHFINDING (greedy) se pronto a muoversi
|
||||||
// Per calcolare esattamente il game over, usiamo la posizione *Pixel* di entrambi.
|
if (!enemy_is_moving[i] && enemy_cooldown[i] == 0) {
|
||||||
int16_t p_px = is_moving ? (start_px + (((target_px - start_px) * (int16_t)move_progress) >> 4)) : ((player_lx - player_ly) * 16 + 96);
|
int8_t dx = (int8_t)player_lx - (int8_t)enemy_lx[i];
|
||||||
int16_t p_py = is_moving ? (start_py + (((target_py - start_py) * (int16_t)move_progress) >> 4)) : ((player_lx + player_ly) * 8 + 16);
|
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;
|
// Insegue solo se entro il raggio di nebbia.
|
||||||
int16_t dy_collision = p_py - enemy_py;
|
if (dist <= (int8_t)fog_radius) {
|
||||||
if (dx_collision < 0) dx_collision = -dx_collision;
|
int8_t best_nx = (int8_t)enemy_lx[i];
|
||||||
if (dy_collision < 0) dy_collision = -dy_collision;
|
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 (enemy_lx[i] + 1 < map_size && maze[enemy_ly[i]][enemy_lx[i] + 1] != 0) {
|
||||||
if (dx_collision < 12 && dy_collision < 6) {
|
int8_t ndx = (int8_t)player_lx - (int8_t)(enemy_lx[i] + 1);
|
||||||
game_over = 1; // 1 = Defeat
|
int16_t d_sq = (int16_t)ndx * ndx + (int16_t)dy * dy;
|
||||||
game_over_timer = 45; // Fermo immagine drammatico per 45 frame (~0.7s) prima del menu nero
|
if (d_sq < min_dist_sq) { min_dist_sq = d_sq; best_nx = enemy_lx[i] + 1; best_ny = enemy_ly[i]; }
|
||||||
update_stamina_display(); // Aggiornando con game_over=1 l'HUD scompare istantaneamente
|
}
|
||||||
|
if (enemy_ly[i] + 1 < map_size && maze[enemy_ly[i] + 1][enemy_lx[i]] != 0) {
|
||||||
// Esegue il Sound Effect della cattura: Suono profondo a caduta (Slide Down) sul Canale 1
|
int8_t ndy = (int8_t)player_ly - (int8_t)(enemy_ly[i] + 1);
|
||||||
NR10_REG = 0x1E; // sweep register: shift down
|
int16_t d_sq = (int16_t)dx * dx + (int16_t)ndy * ndy;
|
||||||
NR11_REG = 0x10; // 25% duty cycle
|
if (d_sq < min_dist_sq) { min_dist_sq = d_sq; best_nx = enemy_lx[i]; best_ny = enemy_ly[i] + 1; }
|
||||||
NR12_REG = 0xF3; // volume alto, calo rapido (envelope decrease)
|
}
|
||||||
NR13_REG = 0x00; // frequenza bassa
|
if (enemy_lx[i] > 0 && maze[enemy_ly[i]][enemy_lx[i] - 1] != 0) {
|
||||||
NR14_REG = 0xC6; // trigger
|
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.
|
// Usa il registro divisore hardware (DIV_REG) per seedare l'RNG. Garantisce labirinti diversi ogni volta.
|
||||||
initrand(DIV_REG);
|
initrand(DIV_REG);
|
||||||
|
|
||||||
// Dimensione del labirinto crescente col livello: +2 tile per livello a partire da 7,
|
map_size = MAP_SIZE + 2 * (level - 1); // +2/level from 7, capped at MAX_MAP_SIZE (21) -> level 8
|
||||||
// capped a MAX_MAP_SIZE (17). Sempre dispari (per il pattern stanza/muro del DFS).
|
|
||||||
map_size = MAP_SIZE + 2 * (level - 1);
|
|
||||||
if (map_size > MAX_MAP_SIZE) map_size = MAX_MAP_SIZE;
|
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
|
// DELEGA LA GENERAZIONE: Chiede al modulo maze.c di creare l'array della mappa
|
||||||
generate_maze();
|
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;
|
uint8_t enemy_min_dist = map_size / 2;
|
||||||
if (enemy_min_dist < 3) enemy_min_dist = 3;
|
if (enemy_min_dist < 3) enemy_min_dist = 3;
|
||||||
while (1) {
|
// SPAWN DEI NEMICI (num_enemies fantasmi): cella calpestabile lontana >= map_size/2
|
||||||
uint8_t rx = rand() % map_size;
|
// dal giocatore e >= 2 dagli altri fantasmi gia' piazzati; cooldown sfasati (i*8)
|
||||||
uint8_t ry = rand() % map_size;
|
// cosi' non si muovono tutti in sincrono. Dopo 40 tentativi rilassa la distanza.
|
||||||
if (maze[ry][rx] == 1) {
|
for (uint8_t i = 0; i < num_enemies; i++) {
|
||||||
int8_t dx = (int8_t)rx - (int8_t)player_lx;
|
uint8_t rx = 0, ry = 0, placed = 0;
|
||||||
int8_t dy = (int8_t)ry - (int8_t)player_ly;
|
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 (dx < 0) dx = -dx;
|
||||||
if (dy < 0) dy = -dy;
|
if (dy < 0) dy = -dy;
|
||||||
int8_t dist = (dx > dy) ? dx : dy;
|
int8_t dist = (dx > dy) ? dx : dy;
|
||||||
if (dist >= (int8_t)enemy_min_dist) {
|
uint8_t req = (tries < 40) ? enemy_min_dist : 2;
|
||||||
enemy_lx = rx;
|
if (dist < (int8_t)req) continue;
|
||||||
enemy_ly = ry;
|
uint8_t too_close = 0;
|
||||||
break;
|
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
|
// Reset Variabili di Gameplay
|
||||||
enemy_is_moving = 0;
|
|
||||||
enemy_cooldown = 60;
|
|
||||||
game_over = 0;
|
game_over = 0;
|
||||||
stamina = 100;
|
stamina = 100;
|
||||||
stamina_recharge_timer = 0;
|
stamina_recharge_timer = 0;
|
||||||
@@ -164,6 +190,21 @@ void engine_init(void) {
|
|||||||
update_level_display(); // Mostra l'indicatore livello (top-left)
|
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).
|
* 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.
|
* 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) {
|
} else if (game_over == 2) {
|
||||||
// Mostra la schermata "Going Deeper"
|
// Mostra la schermata "Going Deeper"
|
||||||
HIDE_SPRITES;
|
HIDE_SPRITES;
|
||||||
// Reset scroll so the full screen image is aligned
|
|
||||||
SCX_REG = 0;
|
SCX_REG = 0;
|
||||||
SCY_REG = 0;
|
SCY_REG = 0;
|
||||||
set_bkg_data(0, next_level_TILE_COUNT, next_level_tiles);
|
set_bkg_data(0, next_level_TILE_COUNT, next_level_tiles);
|
||||||
set_bkg_tiles(0, 0, 20, 18, next_level_map);
|
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
|
// 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 {
|
} 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) {
|
if (game_over == 1) {
|
||||||
move_metasprite(gameover_metasprites[0], player_TILE_COUNT + enemy_TILE_COUNT, 8, 88, 120);
|
move_metasprite(gameover_metasprites[0], player_TILE_COUNT + enemy_TILE_COUNT, 8, 88, 120);
|
||||||
}
|
update_player_sprite();
|
||||||
|
|
||||||
// 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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Attende la pressione di START per riavviare
|
// Attende la pressione di START per riavviare
|
||||||
if ((keys & J_START) && !(prev_keys & J_START)) {
|
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) {
|
if (game_over == 2) {
|
||||||
|
// Going Deeper -> livello successivo
|
||||||
level++;
|
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;
|
return;
|
||||||
|
|||||||
+14
-9
@@ -8,6 +8,8 @@ uint8_t map_size = MAP_SIZE;
|
|||||||
uint8_t maze[MAX_MAP_SIZE][MAX_MAP_SIZE];
|
uint8_t maze[MAX_MAP_SIZE][MAX_MAP_SIZE];
|
||||||
uint8_t map_buffer[32 * 32];
|
uint8_t map_buffer[32 * 32];
|
||||||
|
|
||||||
|
uint8_t fog_radius = 2;
|
||||||
|
|
||||||
uint8_t player_lx = 1;
|
uint8_t player_lx = 1;
|
||||||
uint8_t player_ly = 1;
|
uint8_t player_ly = 1;
|
||||||
uint8_t player_dir = 0;
|
uint8_t player_dir = 0;
|
||||||
@@ -28,15 +30,18 @@ int16_t target_px, target_py;
|
|||||||
|
|
||||||
uint8_t stamina = 100;
|
uint8_t stamina = 100;
|
||||||
uint8_t stamina_recharge_timer = 0;
|
uint8_t stamina_recharge_timer = 0;
|
||||||
|
uint8_t stamina_recharge_rate = 60;
|
||||||
|
|
||||||
uint8_t level = 1;
|
uint8_t level = 1;
|
||||||
|
|
||||||
uint8_t enemy_lx = 0;
|
uint8_t num_enemies = 1;
|
||||||
uint8_t enemy_ly = 0;
|
uint8_t enemy_step_cooldown = 60;
|
||||||
uint8_t enemy_is_moving = 0;
|
uint8_t enemy_lx[MAX_ENEMIES] = {0};
|
||||||
uint8_t enemy_move_progress = 0;
|
uint8_t enemy_ly[MAX_ENEMIES] = {0};
|
||||||
int8_t enemy_start_lx, enemy_start_ly;
|
uint8_t enemy_is_moving[MAX_ENEMIES] = {0};
|
||||||
int8_t enemy_target_lx, enemy_target_ly;
|
uint8_t enemy_move_progress[MAX_ENEMIES] = {0};
|
||||||
int16_t enemy_start_px, enemy_start_py;
|
int8_t enemy_start_lx[MAX_ENEMIES], enemy_start_ly[MAX_ENEMIES];
|
||||||
int16_t enemy_target_px, enemy_target_py;
|
int8_t enemy_target_lx[MAX_ENEMIES], enemy_target_ly[MAX_ENEMIES];
|
||||||
uint8_t enemy_cooldown = 0;
|
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
|
* GLOBAL GAME STATE
|
||||||
* ==========================================
|
* ==========================================
|
||||||
* This file contains all the global variables that are shared across different
|
* Shared variables across modules (rendering, player, enemy, audio).
|
||||||
* logical modules (e.g., rendering, player logic, enemy logic, audio).
|
* Centralized here to avoid circular dependencies in C.
|
||||||
* We place them here to avoid circular dependencies between modules.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// --- General Application State ---
|
// --- General Application State ---
|
||||||
// app_state = 0 means Title Screen, 1 means Gameplay
|
// app_state = 0 means Title Screen, 1 means Gameplay
|
||||||
extern uint8_t app_state;
|
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;
|
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;
|
extern volatile uint8_t game_over_timer;
|
||||||
|
|
||||||
// --- Map Data ---
|
// --- Map Data ---
|
||||||
#define MAP_SIZE 7 // initial maze size (level 1); must be odd
|
#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)
|
// 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];
|
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 ---
|
// --- Camera & Rendering ---
|
||||||
// Map buffer used to draw the isometric tiles into the Game Boy Background map
|
|
||||||
extern uint8_t map_buffer[32 * 32];
|
extern uint8_t map_buffer[32 * 32];
|
||||||
// Camera scroll coordinates to keep the player centered
|
|
||||||
extern uint8_t scroll_x;
|
extern uint8_t scroll_x;
|
||||||
extern int8_t scroll_y;
|
extern int8_t scroll_y;
|
||||||
|
|
||||||
extern uint8_t stairs_lx;
|
extern uint8_t stairs_lx;
|
||||||
extern uint8_t stairs_ly;
|
extern uint8_t stairs_ly;
|
||||||
|
|
||||||
// --- Mappa e Entita ---
|
// --- Player ---
|
||||||
extern uint8_t player_lx;
|
extern uint8_t player_lx;
|
||||||
extern uint8_t player_ly;
|
extern uint8_t player_ly;
|
||||||
extern uint8_t player_dir; // 0=DR, 1=DL, 2=UL, 3=UR
|
extern uint8_t player_dir; // 0=DR, 1=DL, 2=UL, 3=UR
|
||||||
|
|
||||||
// --- Movement & Physics State ---
|
// --- Movement & Physics State ---
|
||||||
// Shared variables for interpolated movement (walking/jumping)
|
|
||||||
extern uint8_t is_moving;
|
extern uint8_t is_moving;
|
||||||
extern uint8_t is_jumping;
|
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 is_running; // B+direction: 8-frame step, 10 stamina/tile
|
||||||
extern uint8_t move_progress; // 0 to 16, tracks the sub-tile animation progress
|
extern uint8_t move_progress; // 0 to 16
|
||||||
extern int8_t start_lx, start_ly;
|
extern int8_t start_lx, start_ly;
|
||||||
extern int8_t target_lx, target_ly;
|
extern int8_t target_lx, target_ly;
|
||||||
extern int16_t start_px, start_py;
|
extern int16_t start_px, start_py;
|
||||||
extern int16_t target_px, target_py;
|
extern int16_t target_px, target_py;
|
||||||
|
|
||||||
// --- Stamina System ---
|
// --- Stamina System ---
|
||||||
// Stamina powers the jump/run mechanic. 100 = full. Recharges over time.
|
|
||||||
extern uint8_t stamina;
|
extern uint8_t stamina;
|
||||||
extern uint8_t stamina_recharge_timer;
|
extern uint8_t stamina_recharge_timer;
|
||||||
|
extern uint8_t stamina_recharge_rate; // frames per +1 stamina (grows with level)
|
||||||
|
|
||||||
// --- Level Progression ---
|
// --- Level Progression ---
|
||||||
// Current level (starts at 1, increments each time the hatch is reached).
|
extern uint8_t level; // starts at 1; game completes after clearing level 8
|
||||||
extern uint8_t level;
|
|
||||||
|
|
||||||
// --- Enemy State ---
|
// --- Enemy System (multi-entity: up to MAX_ENEMIES ghosts) ---
|
||||||
extern uint8_t enemy_lx;
|
#define MAX_ENEMIES 8
|
||||||
extern uint8_t enemy_ly;
|
extern uint8_t num_enemies; // current ghost count (= level, capped at MAX_ENEMIES)
|
||||||
extern uint8_t enemy_is_moving;
|
extern uint8_t enemy_step_cooldown; // base pause (frames) between ghost steps; shrinks with level
|
||||||
extern uint8_t enemy_move_progress;
|
extern uint8_t enemy_lx[MAX_ENEMIES];
|
||||||
extern int8_t enemy_start_lx, enemy_start_ly;
|
extern uint8_t enemy_ly[MAX_ENEMIES];
|
||||||
extern int8_t enemy_target_lx, enemy_target_ly;
|
extern uint8_t enemy_is_moving[MAX_ENEMIES];
|
||||||
extern int16_t enemy_start_px, enemy_start_py;
|
extern uint8_t enemy_move_progress[MAX_ENEMIES];
|
||||||
extern int16_t enemy_target_px, enemy_target_py;
|
extern int8_t enemy_start_lx[MAX_ENEMIES], enemy_start_ly[MAX_ENEMIES];
|
||||||
extern uint8_t enemy_cooldown;
|
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
|
#endif
|
||||||
+5
-3
@@ -33,9 +33,10 @@ static uint8_t das_active = 0;
|
|||||||
*/
|
*/
|
||||||
void update_player_movement(uint8_t keys, uint8_t prev_keys) {
|
void update_player_movement(uint8_t keys, uint8_t prev_keys) {
|
||||||
// --- Gestione della Stamina ---
|
// --- 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++;
|
stamina_recharge_timer++;
|
||||||
if (stamina_recharge_timer >= 60) {
|
if (stamina_recharge_timer >= stamina_recharge_rate) {
|
||||||
stamina_recharge_timer = 0;
|
stamina_recharge_timer = 0;
|
||||||
if (stamina < 100) {
|
if (stamina < 100) {
|
||||||
stamina++;
|
stamina++;
|
||||||
@@ -87,7 +88,8 @@ void update_player_movement(uint8_t keys, uint8_t prev_keys) {
|
|||||||
|
|
||||||
// Controllo Casella Vittoria (ID 2)
|
// Controllo Casella Vittoria (ID 2)
|
||||||
if (maze[player_ly][player_lx] == 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
|
game_over_timer = 30; // Piccolo ritardo prima che la scena cambi
|
||||||
update_stamina_display();
|
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 (dx < 0) dx = -dx;
|
||||||
if (dy < 0) dy = -dy;
|
if (dy < 0) dy = -dy;
|
||||||
int8_t dist = (dx > dy) ? dx : dy;
|
int8_t dist = (dx > dy) ? dx : dy;
|
||||||
if (dist > 2) return 0;
|
if (dist > (int8_t)fog_radius) return 0;
|
||||||
if (dist == 2) return 2;
|
if (dist == (int8_t)fog_radius) return 2;
|
||||||
return 1;
|
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)
|
// Svuotiamo l'intero buffer della mappa con l'indice 0 (casella nera vuota)
|
||||||
memset(map_buffer, 0, sizeof(map_buffer));
|
memset(map_buffer, 0, sizeof(map_buffer));
|
||||||
|
|
||||||
// Calcoliamo la "finestra" di mappa 5x5 da processare (per ottimizzazione, ignoriamo la mappa intera)
|
// Calcoliamo la "finestra" di mappa (2r+1 x 2r+1) da processare (fog_radius)
|
||||||
int8_t start_x = center_x - 2;
|
int8_t start_x = center_x - fog_radius;
|
||||||
if (start_x < 0) start_x = 0;
|
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;
|
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;
|
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;
|
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.
|
// 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;
|
if (dy < 0) dy = -dy;
|
||||||
int8_t dist = (dx > dy) ? dx : 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_x = (lx - ly) * 2 + 12;
|
||||||
int8_t iso_y = (lx + ly) * 1 + 2;
|
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 mask = state_tl + state_tr * 3;
|
||||||
|
|
||||||
uint8_t v;
|
uint8_t v;
|
||||||
if (dist == 2) {
|
if (dist == (int8_t)fog_radius) {
|
||||||
v = is_alt ? (27 + mask) : (18 + mask);
|
v = is_alt ? (27 + mask) : (18 + mask);
|
||||||
} else {
|
} else {
|
||||||
v = is_alt ? (9 + mask) : mask;
|
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;
|
if (dy < 0) dy = -dy;
|
||||||
int8_t dist = (dx > dy) ? dx : 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_x = (lx - ly) * 2 + 12;
|
||||||
int8_t iso_y = (lx + ly) * 1 + 2;
|
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 mask = state_tl + state_tr * 3 + state_bl * 9 + state_br * 27;
|
||||||
|
|
||||||
uint16_t v;
|
uint16_t v;
|
||||||
if (dist == 2) {
|
if (dist == (int8_t)fog_radius) {
|
||||||
v = is_alt ? (36 + 243 + mask) : (36 + 162 + mask);
|
v = is_alt ? (36 + 243 + mask) : (36 + 162 + mask);
|
||||||
} else {
|
} else {
|
||||||
v = is_alt ? (36 + 81 + mask) : (36 + mask);
|
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();
|
update_stamina_display();
|
||||||
|
|
||||||
// Trasferiamo l'intera mappa 32x32 (1024 byte) al Background hardware.
|
// Flush dinamico a 16 righe (512 byte) centrato sulla iso_y del centro di disegno,
|
||||||
// Con labirinti grandi (map_size > 7) la finestra fog-of-war 5x5, proiettata in
|
// con gestione del wrap della mappa 32x32. 16 righe = prestazioni del progetto
|
||||||
// coordinate isometriche assolute, puo' cadere in righe OLTRE il vecchio range
|
// originale; centrando su center_iso_y la nebbia ricade sempre nelle righe flussate
|
||||||
// 2-17 (a causa del wrapping & 31), quindi non basta piu' flussare solo 16 righe.
|
// anche nei labirinti grandi, dove le iso_y assolute wrappano fuori dal vecchio
|
||||||
// draw_map e' chiamato solo ai passi del movimento (non ogni frame), quindi il
|
// range fisso 2-17.
|
||||||
// costo del flush completo e' sostenibile.
|
int16_t center_iso_y = (int16_t)center_x + (int16_t)center_y + 2;
|
||||||
set_bkg_tiles(0, 0, 32, 32, map_buffer);
|
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++;
|
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++;
|
victory_music_timer++;
|
||||||
if (victory_music_timer >= 15) { // 15 frames per note = 4 notes/sec
|
if (victory_music_timer >= 15) { // 15 frames per note = 4 notes/sec
|
||||||
victory_music_timer = 0;
|
victory_music_timer = 0;
|
||||||
|
|||||||
Reference in New Issue
Block a user