diff --git a/Makefile b/Makefile index 8170e71..4097696 100644 --- a/Makefile +++ b/Makefile @@ -9,14 +9,15 @@ SCRIPTS_DIR = scripts BUILD_DIR = build # C source files -SRCS = $(SRC_DIR)/main.c $(SRC_DIR)/engine.c $(SRC_DIR)/globals.c $(SRC_DIR)/maze.c $(SRC_DIR)/sound.c $(SRC_DIR)/render.c $(SRC_DIR)/player_logic.c $(SRC_DIR)/enemy_logic.c $(SRC_DIR)/tiles.c $(SRC_DIR)/player.c $(SRC_DIR)/enemy.c $(SRC_DIR)/gameover.c $(SRC_DIR)/next_level.c $(SRC_DIR)/stamina.c $(SRC_DIR)/title_bg.c +SRCS = $(SRC_DIR)/main.c $(SRC_DIR)/engine.c $(SRC_DIR)/globals.c $(SRC_DIR)/maze.c $(SRC_DIR)/sound.c $(SRC_DIR)/render.c $(SRC_DIR)/player_logic.c $(SRC_DIR)/enemy_logic.c $(SRC_DIR)/tiles.c $(SRC_DIR)/player.c $(SRC_DIR)/enemy.c $(SRC_DIR)/gameover.c $(SRC_DIR)/next_level.c $(SRC_DIR)/stamina.c $(SRC_DIR)/level.c $(SRC_DIR)/title_bg.c all: $(BUILD_DIR)/hello_iso.gb $(BUILD_DIR)/test_gameover.gb # Generate image assets -generate_images: $(SCRIPTS_DIR)/generate_assets.py $(SCRIPTS_DIR)/generate_enemy.py +generate_images: $(SCRIPTS_DIR)/generate_assets.py $(SCRIPTS_DIR)/generate_enemy.py $(SCRIPTS_DIR)/generate_level.py cd $(ASSETS_DIR) && python3 ../$(SCRIPTS_DIR)/generate_assets.py cd $(ASSETS_DIR) && python3 ../$(SCRIPTS_DIR)/generate_enemy.py + cd $(ASSETS_DIR) && python3 ../$(SCRIPTS_DIR)/generate_level.py # Convert image assets to C source files generate_c_assets: generate_images @@ -26,6 +27,7 @@ generate_c_assets: generate_images $(PNG2ASSET) $(ASSETS_DIR)/gameover.png -c $(SRC_DIR)/gameover.c -bpp 2 -noflip -keep_palette_order $(PNG2ASSET) $(ASSETS_DIR)/next_level.png -c $(SRC_DIR)/next_level.c -map -bpp 2 -noflip -max_palettes 1 $(PNG2ASSET) $(ASSETS_DIR)/stamina.png -c $(SRC_DIR)/stamina.c -bpp 2 -noflip -keep_palette_order + $(PNG2ASSET) $(ASSETS_DIR)/level.png -c $(SRC_DIR)/level.c -sw 8 -sh 16 -bpp 2 -noflip -keep_palette_order -keep_duplicate_tiles $(PNG2ASSET) $(ASSETS_DIR)/title_bg.png -c $(SRC_DIR)/title_bg.c -map -bpp 2 -noflip -keep_palette_order -max_palettes 1 # Main ROM target @@ -46,4 +48,5 @@ clean: rm -f $(SRC_DIR)/gameover.c $(SRC_DIR)/gameover.h rm -f $(SRC_DIR)/next_level.c $(SRC_DIR)/next_level.h rm -f $(SRC_DIR)/stamina.c $(SRC_DIR)/stamina.h + rm -f $(SRC_DIR)/level.c $(SRC_DIR)/level.h rm -f $(SRC_DIR)/title_bg.c $(SRC_DIR)/title_bg.h diff --git a/README.md b/README.md index 7a470aa..738d19a 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,8 @@ Un survival-horror procedurale in prospettiva isometrica per Game Boy (DMG/CGB), - **Movimento interpolato (LERP)**: spostamenti fluidi del personaggio e della telecamera su 16 tick a punto fisso (no float). - **Delayed Auto-Shift (DAS)**: controlli alla Tetris — delay iniziale di 12 frame e ripetizione ogni 6 frame per il movimento continuo tenendo premuto il D-Pad. - **Salto evasivo con Stamina**: A+direzione scavalca il blocco adiacente atterrando 2 tile più in là (la cella intermedia deve essere un muro). Costa 60 punti stamina; la barra si ricarica di 1 punto al secondo. +- **Corsa con Stamina**: B+direzione fa correre il protagonista: il passo dura 8 frame invece di 16 e consuma 10 stamina per tile, con DAS più rapido per incatenare i tile fluidamente. Se la stamina scende sotto 10 ripiega automaticamente sulla camminata normale. +- **Progressione livelli**: si parte dal livello 1; raggiungere la botola fa sprofondare nel livello successivo (livello +1). L'indicatore `L` in alto a sinistra (sprite HUD, come la barra stamina in alto a destra) mostra il livello corrente e si aggiorna ad ogni livello superato. La sconfitta riporta al livello 1. - **AI del fantasma**: pathfinding greedy con distanza al quadrato (niente sqrt, niente A*), cooldown di 1 secondo tra i passi, hitbox pixel-perfect (12×6 px) per una morte "giusta". - **Audio procedurale**: colonna sonora sintetizzata manipolando direttamente i registri APU via VBL interrupt (nessun campione). - **Schermate a tutto schermo**: copertina 2-bit nativa per il titolo, immagine "Going Deeper" per la vittoria, metasprite "GAME OVER" per la sconfitta. diff --git a/assets/level.png b/assets/level.png new file mode 100644 index 0000000..a098a07 Binary files /dev/null and b/assets/level.png differ diff --git a/doc/AScreamFromTheDark_report.md b/doc/AScreamFromTheDark_report.md index 8ace14f..be44272 100644 --- a/doc/AScreamFromTheDark_report.md +++ b/doc/AScreamFromTheDark_report.md @@ -61,11 +61,17 @@ Camera centrata: `scroll_x = px - 64`, `scroll_y = py - 72` (160×144 display - **Fase 3 — botola**: sceglie una cella calpestabile **a sufficiente distanza** dalla partenza `(1,1)` (distanza di Chebyshev ≥ `MIN_GOAL_DIST` = 3) tra **tutte** le celle del labirinto, non più vincolata al bordo sud. Fallback estremo sulla cella calpestabile più lontana in assoluto da `(1,1)` se nessuna cella fosse a sufficienza distante (teoricamente impossibile in un perfect maze 7x7). ### 3.3 Movimento & DAS (`player_logic.c`) -- **State machine rigida**: durante `is_moving` (16 frame di LERP) ogni input è ignorato → movimento strettamente grid-based stile Zelda/Pokémon. -- **LERP** a punto fisso: `px = start_px + ((target_px - start_px) * move_progress) >> 4` (divisione per 16 via shift, no float). -- **DAS (Delayed Auto Shift)** alla Tetris: `DAS_DELAY=12` frame di attesa iniziale, poi `DAS_REPEAT=6` frame tra ripetizioni. `keys_pressed = keys & ~prev_keys` rileva il fronte di salita. -- **Salto evasivo**: A+direzione → atterraggio 2 tile più in là. Condizioni: cella intermedia DEVE essere muro (`maze==0`), cella di arrivo pavimento/vittoria, stamina ≥ 60. Consuma 60 stamina. L'arco parabolico è solo visivo: `y_offset = (move_progress * (16 - move_progress)) >> 2` (apice 16px a frame 8). La logica resta 2D. -- **Stamina**: ricarica 1 punto/s (60 frame). Barra UI a 5 segmenti sprite (ID 18–22) in alto a destra, conversione `stamina*40/100` → pixel. +- **State machine rigida**: durante `is_moving` (16 frame di LERP, o 8 in corsa) ogni input è ignorato → movimento strettamente grid-based stile Zelda/Pokémon. +- **LERP** a punto fisso: `px = start_px + ((target_px - start_px) * move_progress) >> 4`. In corsa `move_progress` incrementa di 2/frame → il passo dura 8 frame invece di 16. +- **DAS (Delayed Auto Shift)** alla Tetris: `DAS_DELAY=12` frame di attesa iniziale, poi `DAS_REPEAT=6` frame tra ripetizioni (camminata); `DAS_REPEAT_RUN=2` mentre si corre per incatenare i tile. +- **Salto evasivo**: A+direzione → atterraggio 2 tile più in là. Condizioni: cella intermedia DEVE essere muro, cella di arrivo pavimento/vittoria, stamina ≥ 60. Consuma 60 stamina. Arco parabolico solo visivo: `y_offset = (move_progress * (16 - move_progress)) >> 2`. +- **Corsa**: B+direzione (stamina ≥ 10) → passo di 8 frame, costo 10 stamina/tile, DAS rapido. Se stamina < 10 ripiega su camminata normale senza costo. +- **Stamina**: ricarica 1 punto/s. Barra UI a 5 sprite (ID 18–22) in alto a destra. + +### 3.3b Progressione livelli (`globals.c` + `engine.c` + `render.c`) +- Variabile globale `level` (parte da 1). Il titolo→gioco la azzera a 1 (`main.c`); raggiungere la botola (vittoria, `game_over==2`) e premere START fa `level++` prima di `engine_init()` (nuovo labirinto); la sconfitta (`game_over==1`) + START fa `level = 1`. +- Indicatore HUD `L` in alto a sinistra via 3 sprite (OAM ID 23–25), disegnati dall'asset `level.png` (11 glifi 8x16: 'L', '0'–'9') generato da `scripts/generate_level.py` e convertito con `png2asset -keep_duplicate_tiles` (senza deduplica, così il glifo di ordine i occupa i tile 2i/2i+1 in ordine prevedibile). +- **VRAM**: i glifi sono caricati a `LEVEL_SPRITE_BASE = allineamento-pari-di (tiles_TILE_COUNT + stamina_TILE_COUNT*2)` (~204) — base PARI perché in modalità 8x16 l'hardware ignora il LSB dell'indice tile; e disgiunta dal blocco tile del background e dalla stamina (stesso workaround del commit 93deb35). `update_level_display()` viene chiamata ogni frame in `engine_update` e si nasconde da sola quando `game_over` è attivo. ### 3.4 Fog of War (`render.c` + `enemy_logic.c`) Distanza di **Chebyshev** `max(|dx|,|dy|)` invece di euclidea (no sqrt, no lookup): quadrato di visibilità 5×5. diff --git a/doc/gameplay.md b/doc/gameplay.md index c4b77c1..10a886e 100644 --- a/doc/gameplay.md +++ b/doc/gameplay.md @@ -27,3 +27,16 @@ Condizioni per il salto: La stamina si ricarica di 1 punto ogni secondo. Eseguire un salto costa 60 punti, disabilitando ulteriori balzi per un minuto intero. Questo lo rende una manovra evasiva salva-vita estrema e non uno strumento da spam abusivo. Visivamente, l'altezza del salto non altera la logica (sempre 2D Grid), ma solo il rendering in `update_player_sprite()`, in cui sottraiamo al posizionamento verticale la formula parabolica `x * (16 - x) / 4`. + +## La Corsa e il Costo in Stamina + +Tenendo premuto il tasto `B` assieme a una direzione direzionale, il giocatore si mette a correre: +la transizione tra tile dura **8 frame** invece di 16 (l'incremento di `move_progress` raddoppia, mentre la formula LERP a punto fisso `>> 4` resta invariata e raggiunge il target in metà tempo). Ogni tile corso consuma **10 punti Stamina**. +Il DAS diventa più rapido (`DAS_REPEAT_RUN = 2`) così da incatenare i tile fluidamente quando si tiene premuto B+direzione. +Se la Stamina scende sotto 10, B+direzione **ripiega silenziosamente su camminata normale** (16 frame, 0 costo); la corsa si riattiva da sola appena la stamina torna ≥ 10. La ricarica resta di 1 punto al secondo, quindi la corsa è uno strumento a scatto: da pieno (100) si possono percorrere fino a 10 tile, dopodiché serve tempo per ricaricarla. + +## Progressione dei Livelli + +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. La sconfitta (cattura da parte del fantasma) riporta invece al **livello 1**. + +L'indicatore `L` è mostrato in **alto a sinistra** tramite tre sprite hardware (OAM ID 23–25), disegnati dall'asset `level.png` (11 glifi 8x16: 'L', '0'–'9'). Le decine vengono mostrate solo dal livello 10 in poi (sotto, lo sprite decine è spostato fuori schermo). Come la barra stamina (in alto a destra), l'indicatore è a coordinate-schermo fisse e indipendente dallo scroll isometrico, e viene nascosto durante il game over e sul titolo. La base VRAM dei glifi è allineata a un indice **pari** perché in modalità sprite 8x16 l'hardware ignora il bit meno significativo dell'indice tile. diff --git a/scripts/generate_level.py b/scripts/generate_level.py new file mode 100644 index 0000000..0a14d82 --- /dev/null +++ b/scripts/generate_level.py @@ -0,0 +1,149 @@ +from PIL import Image + +# Standard Game Boy palette (same as generate_assets.py) +# 0: White/transparent-for-sprites, 1: Light Gray, 2: Dark Gray, 3: Black +PALETTE = [ + 224, 248, 207, # 0 + 134, 192, 108, # 1 + 48, 104, 80, # 2 + 7, 24, 33 # 3 +] + [0] * (256 * 3 - 12) + +# 5x7 font, '1' = light pixel (on), drawn with light gray (1) on transparent (0). +GLYPHS = { + ' ': [ + "00000", + "00000", + "00000", + "00000", + "00000", + "00000", + "00000", + ], + 'L': [ + "10000", + "10000", + "10000", + "10000", + "10000", + "10000", + "11111", + ], + '0': [ + "01110", + "10001", + "10011", + "10101", + "11001", + "10001", + "01110", + ], + '1': [ + "00100", + "01100", + "00100", + "00100", + "00100", + "00100", + "01110", + ], + '2': [ + "01110", + "10001", + "00001", + "00010", + "00100", + "01000", + "11111", + ], + '3': [ + "11110", + "00001", + "00001", + "01110", + "00001", + "00001", + "11110", + ], + '4': [ + "00010", + "00110", + "01010", + "10010", + "11111", + "00010", + "00010", + ], + '5': [ + "11111", + "10000", + "11110", + "00001", + "00001", + "10001", + "01110", + ], + '6': [ + "00110", + "01000", + "10000", + "11110", + "10001", + "10001", + "01110", + ], + '7': [ + "11111", + "00001", + "00010", + "00100", + "01000", + "10000", + "10000", + ], + '8': [ + "01110", + "10001", + "10001", + "01110", + "10001", + "10001", + "01110", + ], + '9': [ + "01110", + "10001", + "10001", + "01111", + "00001", + "00010", + "01100", + ], +} + +# Glyph order in the sprite sheet (top -> bottom). 11 glyphs, each 8x16 = 2 tiles. +# Index i (0-based) occupies tiles (2i, 2i+1) in level_tiles thanks to -keep_duplicate_tiles. +# 0: 'L', 1..10: '0'..'9'. (No blank glyph: hidden digits are moved offscreen instead.) +ORDER = ['L', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'] + +def main(): + # 8x16 per glyph, stacked vertically -> 8 wide x (16*11)=176 tall + img = Image.new("P", (8, 176), 0) # transparent background + img.putpalette(PALETTE) + + for gi, name in enumerate(ORDER): + glyph = GLYPHS[name] + y_base = gi * 16 + # Place the 5x7 glyph in the top rows (0..6), columns 1..5. + for row in range(7): + line = glyph[row] + for col in range(5): + if line[col] == '1': + img.putpixel((1 + col, y_base + row), 1) # light gray + # Rows 7..15 stay transparent (0) + + img.save("level.png") + print("Generated level.png (11 glyphs: L, 0-9)") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/src/engine.c b/src/engine.c index 0527720..fc7c573 100644 --- a/src/engine.c +++ b/src/engine.c @@ -19,6 +19,7 @@ #include "gameover.h" #include "next_level.h" #include "stamina.h" +#include "level.h" #include "title_bg.h" /** @@ -96,6 +97,8 @@ void engine_init(void) { set_sprite_data(player_TILE_COUNT + enemy_TILE_COUNT, gameover_TILE_COUNT, gameover_tiles); // Load stamina_tiles AFTER the background tiles to prevent VRAM overlap in the shared block (indices > 127) set_sprite_data(tiles_TILE_COUNT, stamina_TILE_COUNT * 2, stamina_tiles); + // Load the level HUD glyph tiles right after stamina (blank, 'L', '0'..'9'). + set_sprite_data(LEVEL_SPRITE_BASE, level_TILE_COUNT, level_tiles); // SPAWN DEL GIOCATORE player_lx = 1; @@ -143,11 +146,13 @@ void engine_init(void) { stamina_recharge_timer = 0; is_jumping = 0; is_moving = 0; + is_running = 0; // Richiama render.c per forzare un disegno iniziale della scena draw_map(player_lx, player_ly); update_camera(); update_player_sprite(); + update_level_display(); // Mostra l'indicatore livello (top-left) } /** @@ -156,6 +161,10 @@ void engine_init(void) { */ void engine_update(uint8_t keys, uint8_t prev_keys) { + // Indicatore livello: aggiornato ogni frame. Si nasconde da solo quando + // `game_over` e' attivo (sconfitta/vittoria) grazie al check interno. + update_level_display(); + // --- GESTIONE STATO DI FINE GIOCO (Sconfitta o Vittoria) --- if (game_over) { if (game_over_timer > 0) { @@ -217,6 +226,12 @@ void engine_update(uint8_t keys, uint8_t prev_keys) { // 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 -> nuova partita dal livello 1. + if (game_over == 2) { + level++; + } else { + level = 1; + } move_metasprite(gameover_metasprites[0], player_TILE_COUNT + enemy_TILE_COUNT, 8, 0, 0); SHOW_SPRITES; engine_init(); diff --git a/src/globals.c b/src/globals.c index bfd405f..ebb4a2b 100644 --- a/src/globals.c +++ b/src/globals.c @@ -18,6 +18,7 @@ uint8_t stairs_ly = 0; uint8_t is_moving = 0; uint8_t is_jumping = 0; +uint8_t is_running = 0; uint8_t move_progress = 0; int8_t start_lx, start_ly; int8_t target_lx, target_ly; @@ -27,6 +28,8 @@ int16_t target_px, target_py; uint8_t stamina = 100; uint8_t stamina_recharge_timer = 0; +uint8_t level = 1; + uint8_t enemy_lx = 0; uint8_t enemy_ly = 0; uint8_t enemy_is_moving = 0; diff --git a/src/globals.h b/src/globals.h index 5ae6328..47d137f 100644 --- a/src/globals.h +++ b/src/globals.h @@ -47,6 +47,7 @@ extern uint8_t player_dir; // 0=DR, 1=DL, 2=UL, 3=UR // 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 int8_t start_lx, start_ly; extern int8_t target_lx, target_ly; @@ -54,10 +55,14 @@ extern int16_t start_px, start_py; extern int16_t target_px, target_py; // --- Stamina System --- -// Stamina powers the jump mechanic. 100 = full. Recharges over time. +// Stamina powers the jump/run mechanic. 100 = full. Recharges over time. extern uint8_t stamina; extern uint8_t stamina_recharge_timer; +// --- Level Progression --- +// Current level (starts at 1, increments each time the hatch is reached). +extern uint8_t level; + // --- Enemy State --- extern uint8_t enemy_lx; extern uint8_t enemy_ly; diff --git a/src/level.c b/src/level.c new file mode 100644 index 0000000..dba4b45 --- /dev/null +++ b/src/level.c @@ -0,0 +1,187 @@ +//AUTOGENERATED FILE FROM png2asset + +#include +#include +#include + +BANKREF(level) + +const palette_color_t level_palettes[32] = { + RGB8(224,248,207), RGB8(134,192,108), RGB8( 48,104, 80), RGB8( 7, 24, 33) + , + RGB8( 0, 0, 0), RGB8( 0, 0, 0), RGB8( 0, 0, 0), RGB8( 0, 0, 0) + , + RGB8( 0, 0, 0), RGB8( 0, 0, 0), RGB8( 0, 0, 0), RGB8( 0, 0, 0) + , + RGB8( 0, 0, 0), RGB8( 0, 0, 0), RGB8( 0, 0, 0), RGB8( 0, 0, 0) + , + RGB8( 0, 0, 0), RGB8( 0, 0, 0), RGB8( 0, 0, 0), RGB8( 0, 0, 0) + , + RGB8( 0, 0, 0), RGB8( 0, 0, 0), RGB8( 0, 0, 0), RGB8( 0, 0, 0) + , + RGB8( 0, 0, 0), RGB8( 0, 0, 0), RGB8( 0, 0, 0), RGB8( 0, 0, 0) + , + RGB8( 0, 0, 0), RGB8( 0, 0, 0), RGB8( 0, 0, 0), RGB8( 0, 0, 0) + +}; + +const uint8_t level_tiles[352] = { + 0x40,0x00,0x40,0x00, + 0x40,0x00,0x40,0x00, + 0x40,0x00,0x40,0x00, + 0x7c,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00, + +0x38,0x00,0x44,0x00, + 0x4c,0x00,0x54,0x00, + 0x64,0x00,0x44,0x00, + 0x38,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00, + +0x10,0x00,0x30,0x00, + 0x10,0x00,0x10,0x00, + 0x10,0x00,0x10,0x00, + 0x38,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00, + +0x38,0x00,0x44,0x00, + 0x04,0x00,0x08,0x00, + 0x10,0x00,0x20,0x00, + 0x7c,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00, + +0x78,0x00,0x04,0x00, + 0x04,0x00,0x38,0x00, + 0x04,0x00,0x04,0x00, + 0x78,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00, + +0x08,0x00,0x18,0x00, + 0x28,0x00,0x48,0x00, + 0x7c,0x00,0x08,0x00, + 0x08,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00, + +0x7c,0x00,0x40,0x00, + 0x78,0x00,0x04,0x00, + 0x04,0x00,0x44,0x00, + 0x38,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00, + +0x18,0x00,0x20,0x00, + 0x40,0x00,0x78,0x00, + 0x44,0x00,0x44,0x00, + 0x38,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00, + +0x7c,0x00,0x04,0x00, + 0x08,0x00,0x10,0x00, + 0x20,0x00,0x40,0x00, + 0x40,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00, + +0x38,0x00,0x44,0x00, + 0x44,0x00,0x38,0x00, + 0x44,0x00,0x44,0x00, + 0x38,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00, + +0x38,0x00,0x44,0x00, + 0x44,0x00,0x3c,0x00, + 0x04,0x00,0x08,0x00, + 0x30,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00 + +}; + +const metasprite_t level_metasprite0[] = { + METASPR_ITEM(-8, -4, 0, S_PAL(0)), + METASPR_TERM +}; + +const metasprite_t level_metasprite1[] = { + METASPR_ITEM(-8, -4, 2, S_PAL(0)), + METASPR_TERM +}; + +const metasprite_t level_metasprite2[] = { + METASPR_ITEM(-8, -4, 4, S_PAL(0)), + METASPR_TERM +}; + +const metasprite_t level_metasprite3[] = { + METASPR_ITEM(-8, -4, 6, S_PAL(0)), + METASPR_TERM +}; + +const metasprite_t level_metasprite4[] = { + METASPR_ITEM(-8, -4, 8, S_PAL(0)), + METASPR_TERM +}; + +const metasprite_t level_metasprite5[] = { + METASPR_ITEM(-8, -4, 10, S_PAL(0)), + METASPR_TERM +}; + +const metasprite_t level_metasprite6[] = { + METASPR_ITEM(-8, -4, 12, S_PAL(0)), + METASPR_TERM +}; + +const metasprite_t level_metasprite7[] = { + METASPR_ITEM(-8, -4, 14, S_PAL(0)), + METASPR_TERM +}; + +const metasprite_t level_metasprite8[] = { + METASPR_ITEM(-8, -4, 16, S_PAL(0)), + METASPR_TERM +}; + +const metasprite_t level_metasprite9[] = { + METASPR_ITEM(-8, -4, 18, S_PAL(0)), + METASPR_TERM +}; + +const metasprite_t level_metasprite10[] = { + METASPR_ITEM(-8, -4, 20, S_PAL(0)), + METASPR_TERM +}; + +const metasprite_t* const level_metasprites[11] = { + level_metasprite0, level_metasprite1, level_metasprite2, level_metasprite3, level_metasprite4, level_metasprite5, level_metasprite6, level_metasprite7, level_metasprite8, level_metasprite9, level_metasprite10 +}; diff --git a/src/level.h b/src/level.h new file mode 100644 index 0000000..daab6bb --- /dev/null +++ b/src/level.h @@ -0,0 +1,29 @@ +//AUTOGENERATED FILE FROM png2asset +#ifndef METASPRITE_level_H +#define METASPRITE_level_H + +#include +#include +#include + +#define level_TILE_ORIGIN 0 +#define level_TILE_W 8 +#define level_TILE_H 16 +#define level_WIDTH 8 +#define level_HEIGHT 16 +#define level_TILE_COUNT 22 +#define level_PALETTE_COUNT 8 +#define level_COLORS_PER_PALETTE 4 +#define level_TOTAL_COLORS 32 +#define level_PIVOT_X 4 +#define level_PIVOT_Y 8 +#define level_PIVOT_W 8 +#define level_PIVOT_H 16 +extern const metasprite_t* const level_metasprites[11]; + +BANKREF_EXTERN(level) + +extern const palette_color_t level_palettes[32]; +extern const uint8_t level_tiles[352]; + +#endif diff --git a/src/main.c b/src/main.c index ef5e0eb..179cf18 100644 --- a/src/main.c +++ b/src/main.c @@ -1,5 +1,6 @@ #include #include "engine.h" +#include "globals.h" uint8_t app_state = 0; // 0 = title, 1 = game @@ -20,6 +21,7 @@ void main(void) { title_update(keys, prev_keys); if ((keys & J_START) && !(prev_keys & J_START)) { app_state = 1; + level = 1; // Nuova partita dal livello 1 engine_init(); } } else { diff --git a/src/player_logic.c b/src/player_logic.c index 6db6481..0ffa7d9 100644 --- a/src/player_logic.c +++ b/src/player_logic.c @@ -9,6 +9,9 @@ static uint8_t das_timer = 0; static uint8_t das_active = 0; #define DAS_DELAY 12 #define DAS_REPEAT 6 +#define DAS_REPEAT_RUN 2 // DAS piu' rapido mentre si corre (il passo di corsa dura 8 frame) +#define RUN_STAMINA_COST 10 +#define RUN_MIN_STAMINA 10 /** * Gestisce la logica di movimento del giocatore, i salti e le collisioni. @@ -23,6 +26,10 @@ static uint8_t das_active = 0; * 3. Meccanica del Salto: Premendo A+Direzione, il giocatore può saltare *esattamente* 2 celle * più in là. Il codice verifica se la cella intermedia (+1) è un ostacolo (0) e se * la cella di atterraggio (+2) è calpestabile. Consuma 60 di stamina per bilanciare il vantaggio tattico. + * 4. Meccanica della Corsa: Premendo B+Direzione (con stamina >= 10) il giocatore si mette a correre: + * il passo dura 8 frame invece di 16 (move_progress incrementato di 2) e consuma 10 stamina per tile. + * Il DAS diventa piu' rapido (DAS_REPEAT_RUN) per incatenare i tile fluidamente. Se la stamina + * non basta, ripiega silenziosamente su camminata normale senza consumo. */ void update_player_movement(uint8_t keys, uint8_t prev_keys) { // --- Gestione della Stamina --- @@ -39,7 +46,9 @@ void update_player_movement(uint8_t keys, uint8_t prev_keys) { // --- State Machine: Transizione Animazione --- // Se siamo nel mezzo di uno spostamento tra tile, calcoliamo i pixel interpolati if (is_moving) { - move_progress++; + // Corsa: il passo dura 8 frame (incremento 2) invece di 16 (incremento 1). + // La formula LERP resta a punto fisso (>>4 == /16): move_progress raggiunge 16 in meta' tempo. + move_progress += is_running ? 2 : 1; // Interpolazione lineare (LERP) da start a target int16_t px = start_px + (((target_px - start_px) * (int16_t)move_progress) >> 4); // >> 4 equivale a / 16 @@ -62,6 +71,7 @@ void update_player_movement(uint8_t keys, uint8_t prev_keys) { if (move_progress == 16) { is_moving = 0; is_jumping = 0; + is_running = 0; player_lx = target_lx; player_ly = target_ly; @@ -91,9 +101,11 @@ void update_player_movement(uint8_t keys, uint8_t prev_keys) { // --- Lettura Controlli (Polling) con DAS --- uint8_t keys_pressed = keys & ~prev_keys; // Tasti premuti ESATTAMENTE in questo frame + // La corsa (B tenuto + stamina) usa un DAS piu' corto per incatenare i tile fluidamente. + uint8_t wants_run = (keys & J_B) && (stamina >= RUN_MIN_STAMINA); if (keys_pressed) { - das_timer = DAS_DELAY; + das_timer = wants_run ? DAS_REPEAT_RUN : DAS_DELAY; das_active = 1; } else if (keys && das_active) { // Se si tiene premuto un tasto... @@ -102,7 +114,7 @@ void update_player_movement(uint8_t keys, uint8_t prev_keys) { } // Se il timer arriva a 0, emuliamo una pressione ripetuta if (das_timer == 0 && !is_moving) { - das_timer = DAS_REPEAT; + das_timer = wants_run ? DAS_REPEAT_RUN : DAS_REPEAT; keys_pressed = keys; } } else { @@ -168,7 +180,7 @@ void update_player_movement(uint8_t keys, uint8_t prev_keys) { // Se le condizioni di salto falliscono, aggiorna semplicemente il frame per far girare il personaggio update_player_sprite(); } - // 2. Movimento standard (Camminata) + // 2. Movimento standard (Camminata o Corsa) else { int8_t new_lx = player_lx + move_lx; int8_t new_ly = player_ly + move_ly; @@ -178,6 +190,16 @@ void update_player_movement(uint8_t keys, uint8_t prev_keys) { if (maze[new_ly][new_lx] == 1 || maze[new_ly][new_lx] == 2) { is_moving = 1; move_progress = 0; + // Corsa: B + direzione con stamina sufficiente. Il passo dura 8 frame + // (vedi l'incremento di move_progress sopra) e costa 10 stamina per tile. + // Se la stamina non basta, ripiega silenziosamente su camminata normale. + if ((keys & J_B) && stamina >= RUN_MIN_STAMINA) { + is_running = 1; + stamina -= RUN_STAMINA_COST; // 10 stamina per tile (guardia sopra: stamina >= 10) + update_stamina_display(); + } else { + is_running = 0; + } start_lx = player_lx; start_ly = player_ly; target_lx = new_lx; diff --git a/src/render.c b/src/render.c index 9fb551e..903fc1d 100644 --- a/src/render.c +++ b/src/render.c @@ -7,6 +7,7 @@ #include "enemy.h" #include "gameover.h" #include "stamina.h" +#include "level.h" #include "maze.h" uint8_t get_tile_state(int8_t cx, int8_t cy, int8_t lx, int8_t ly) { @@ -77,6 +78,54 @@ void update_stamina_display(void) { } } +/** + * Aggiorna l'indicatore del livello (HUD in alto a sinistra). + * + * Sviluppo & Scelte Architetturali: + * 1. Come la barra stamina, usiamo sprite hardware (ID 23..25) perche' il background + * e' scrollato isometricamente e non e' adatto a un HUD fisso. Gli sprite sono + * coordinate-schermo fisse, indipendenti dallo scroll. + * 2. L'asset `level.png` contiene 12 glifi 8x16 (blank, 'L', '0'..'9'), caricati in + * VRAM sprite subito dopo i tile della stamina (base = tiles_TILE_COUNT + + * stamina_TILE_COUNT) per evitare sovrapposizioni con i tile del background + * (stesso workaround del commit 93deb35 per la VRAM condivisa). + * 3. Si mostrano 3 sprite: 'L', decine (blank se livello < 10), unita'. + * Viene nascosto durante il game over (sconfitta/vittoria) e sul titolo. + */ +#define LEVEL_HUD_SPRITE 23 // primo OAM ID del livello (dopo stamina 18..22) + +void update_level_display(void) { + // Nascondi durante il game over (sconfitta o vittoria/Going Deeper). + if (game_over) { + for (uint8_t i = 0; i < 3; i++) { + move_sprite(LEVEL_HUD_SPRITE + i, 0, 0); + } + return; + } + + uint8_t base = LEVEL_SPRITE_BASE; + uint8_t tens = (level / 10) % 10; + uint8_t ones = level % 10; + + // Mappatura glifi in level.png (11 glifi, ognuno 8x16 = 2 tile, in ordine grazie + // a -keep_duplicate_tiles). Indice di ordine i -> tile (base + 2*i): + // 0 = 'L', 1..10 = '0'..'9'. Quindi la cifra d -> indice (1+d) -> tile base + (1+d)*2. + set_sprite_tile(LEVEL_HUD_SPRITE + 0, base + 0 * 2); // 'L' (ordine 0) + set_sprite_tile(LEVEL_HUD_SPRITE + 2, base + (1 + ones) * 2); // unita' (cifra ones) + + // Posizionamento in alto a sinistra (offset hardware +8 x, +16 y). + move_sprite(LEVEL_HUD_SPRITE + 0, 8, 16); // 'L' a screen x=0 + move_sprite(LEVEL_HUD_SPRITE + 2, 24, 16); // unita' a screen x=16 + + // Decine: mostrate solo se livello >= 10 (altrimenti sprite nascosto offscreen). + if (level >= 10) { + set_sprite_tile(LEVEL_HUD_SPRITE + 1, base + (1 + tens) * 2); // decine (cifra tens) + move_sprite(LEVEL_HUD_SPRITE + 1, 16, 16); // screen x=8 + } else { + move_sprite(LEVEL_HUD_SPRITE + 1, 0, 0); // nascondi decine + } +} + /** * Disegna la porzione visibile del labirinto e applica il sistema "Fog of War". * diff --git a/src/render.h b/src/render.h index 633f7ce..e88b3b8 100644 --- a/src/render.h +++ b/src/render.h @@ -20,6 +20,15 @@ void update_camera(void); // Update stamina bar graphics void update_stamina_display(void); +// Update the level indicator (top-left HUD) +void update_level_display(void); + +// Base VRAM sprite tile index for the level HUD glyphs. Placed after the full stamina +// load claim and aligned UP to an EVEN tile index: in 8x16 sprite mode the hardware +// ignores the tile-index LSB, so the base must be even for base + 2*i to land on the +// glyph's top tile. Disjoint from BG tile block and stamina (same rationale as 93deb35). +#define LEVEL_SPRITE_BASE ((uint8_t)(((tiles_TILE_COUNT + (uint8_t)(stamina_TILE_COUNT * 2)) + 1) & (uint8_t)0xFEu)) + // Update the player sprite (animation frames, jumping height offset) void update_player_sprite(void);