Implement run mechanic and level progression with top-left HUD indicator

Run (B + direction):
- player_logic.c: while holding B + direction with stamina >= 10, each tile
  takes 8 frames instead of 16 (move_progress += 2, LERP >>4 unchanged) and
  costs 10 stamina per tile. DAS_REPEAT_RUN=2 chains tiles fluidly.
- Falls back silently to normal walk when stamina < 10.

Level progression:
- New global 'level' (starts at 1). title->game resets to 1; reaching the
  hatch (victory/Going Deeper) + START increments before engine_init (new
  maze); defeat + START resets to 1.
- Top-left HUD 'L<n>' via 3 sprites (OAM 23-25) from new level.png asset
  (glyphs L, 0-9), generated by scripts/generate_level.py and converted
  with png2asset -keep_duplicate_tiles for predictable tile ordering.
- VRAM base aligned to an EVEN index (8x16 hardware ignores tile LSB) and
  placed after the stamina load claim to avoid the shared BG tile block.
- update_level_display() called every frame; hides during game over/title.

Docs: README, doc/gameplay.md and the technical report updated.
This commit is contained in:
2026-06-24 11:56:19 +02:00
parent ae0706ccfb
commit 36e52d4d1a
15 changed files with 506 additions and 12 deletions
+5 -2
View File
@@ -9,14 +9,15 @@ SCRIPTS_DIR = scripts
BUILD_DIR = build BUILD_DIR = build
# C source files # 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 all: $(BUILD_DIR)/hello_iso.gb $(BUILD_DIR)/test_gameover.gb
# Generate image assets # 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_assets.py
cd $(ASSETS_DIR) && python3 ../$(SCRIPTS_DIR)/generate_enemy.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 # Convert image assets to C source files
generate_c_assets: generate_images 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)/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)/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)/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 $(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 # Main ROM target
@@ -46,4 +48,5 @@ clean:
rm -f $(SRC_DIR)/gameover.c $(SRC_DIR)/gameover.h 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)/next_level.c $(SRC_DIR)/next_level.h
rm -f $(SRC_DIR)/stamina.c $(SRC_DIR)/stamina.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 rm -f $(SRC_DIR)/title_bg.c $(SRC_DIR)/title_bg.h
+2
View File
@@ -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). - **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. - **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.
- **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. 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". - **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.
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 957 B

+11 -5
View File
@@ -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). - **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`) ### 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. - **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` (divisione per 16 via shift, no float). - **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. `keys_pressed = keys & ~prev_keys` rileva il fronte di salita. - **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 (`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. - **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`.
- **Stamina**: ricarica 1 punto/s (60 frame). Barra UI a 5 segmenti sprite (ID 1822) in alto a destra, conversione `stamina*40/100` → pixel. - **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 1822) 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<n>` in alto a sinistra via 3 sprite (OAM ID 2325), 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`) ### 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. Distanza di **Chebyshev** `max(|dx|,|dy|)` invece di euclidea (no sqrt, no lookup): quadrato di visibilità 5×5.
+13
View File
@@ -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. 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`. 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<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.
+149
View File
@@ -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()
+15
View File
@@ -19,6 +19,7 @@
#include "gameover.h" #include "gameover.h"
#include "next_level.h" #include "next_level.h"
#include "stamina.h" #include "stamina.h"
#include "level.h"
#include "title_bg.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); 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) // 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); 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 // SPAWN DEL GIOCATORE
player_lx = 1; player_lx = 1;
@@ -143,11 +146,13 @@ void engine_init(void) {
stamina_recharge_timer = 0; stamina_recharge_timer = 0;
is_jumping = 0; is_jumping = 0;
is_moving = 0; is_moving = 0;
is_running = 0;
// Richiama render.c per forzare un disegno iniziale della scena // Richiama render.c per forzare un disegno iniziale della scena
draw_map(player_lx, player_ly); draw_map(player_lx, player_ly);
update_camera(); update_camera();
update_player_sprite(); 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) { 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) --- // --- GESTIONE STATO DI FINE GIOCO (Sconfitta o Vittoria) ---
if (game_over) { if (game_over) {
if (game_over_timer > 0) { 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 // 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 // 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); move_metasprite(gameover_metasprites[0], player_TILE_COUNT + enemy_TILE_COUNT, 8, 0, 0);
SHOW_SPRITES; SHOW_SPRITES;
engine_init(); engine_init();
+3
View File
@@ -18,6 +18,7 @@ uint8_t stairs_ly = 0;
uint8_t is_moving = 0; uint8_t is_moving = 0;
uint8_t is_jumping = 0; uint8_t is_jumping = 0;
uint8_t is_running = 0;
uint8_t move_progress = 0; uint8_t move_progress = 0;
int8_t start_lx, start_ly; int8_t start_lx, start_ly;
int8_t target_lx, target_ly; int8_t target_lx, target_ly;
@@ -27,6 +28,8 @@ 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 level = 1;
uint8_t enemy_lx = 0; uint8_t enemy_lx = 0;
uint8_t enemy_ly = 0; uint8_t enemy_ly = 0;
uint8_t enemy_is_moving = 0; uint8_t enemy_is_moving = 0;
+6 -1
View File
@@ -47,6 +47,7 @@ extern uint8_t player_dir; // 0=DR, 1=DL, 2=UL, 3=UR
// Shared variables for interpolated movement (walking/jumping) // 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 move_progress; // 0 to 16, tracks the sub-tile animation progress extern uint8_t move_progress; // 0 to 16, tracks the sub-tile animation progress
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;
@@ -54,10 +55,14 @@ 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 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;
extern uint8_t stamina_recharge_timer; 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 --- // --- Enemy State ---
extern uint8_t enemy_lx; extern uint8_t enemy_lx;
extern uint8_t enemy_ly; extern uint8_t enemy_ly;
+187
View File
@@ -0,0 +1,187 @@
//AUTOGENERATED FILE FROM png2asset
#include <stdint.h>
#include <gbdk/platform.h>
#include <gbdk/metasprites.h>
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
};
+29
View File
@@ -0,0 +1,29 @@
//AUTOGENERATED FILE FROM png2asset
#ifndef METASPRITE_level_H
#define METASPRITE_level_H
#include <stdint.h>
#include <gbdk/platform.h>
#include <gbdk/metasprites.h>
#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
+2
View File
@@ -1,5 +1,6 @@
#include <gb/gb.h> #include <gb/gb.h>
#include "engine.h" #include "engine.h"
#include "globals.h"
uint8_t app_state = 0; // 0 = title, 1 = game uint8_t app_state = 0; // 0 = title, 1 = game
@@ -20,6 +21,7 @@ void main(void) {
title_update(keys, prev_keys); title_update(keys, prev_keys);
if ((keys & J_START) && !(prev_keys & J_START)) { if ((keys & J_START) && !(prev_keys & J_START)) {
app_state = 1; app_state = 1;
level = 1; // Nuova partita dal livello 1
engine_init(); engine_init();
} }
} else { } else {
+26 -4
View File
@@ -9,6 +9,9 @@ static uint8_t das_timer = 0;
static uint8_t das_active = 0; static uint8_t das_active = 0;
#define DAS_DELAY 12 #define DAS_DELAY 12
#define DAS_REPEAT 6 #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. * 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 * 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 * 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. * 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) { void update_player_movement(uint8_t keys, uint8_t prev_keys) {
// --- Gestione della Stamina --- // --- Gestione della Stamina ---
@@ -39,7 +46,9 @@ void update_player_movement(uint8_t keys, uint8_t prev_keys) {
// --- State Machine: Transizione Animazione --- // --- State Machine: Transizione Animazione ---
// Se siamo nel mezzo di uno spostamento tra tile, calcoliamo i pixel interpolati // Se siamo nel mezzo di uno spostamento tra tile, calcoliamo i pixel interpolati
if (is_moving) { 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 // 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 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) { if (move_progress == 16) {
is_moving = 0; is_moving = 0;
is_jumping = 0; is_jumping = 0;
is_running = 0;
player_lx = target_lx; player_lx = target_lx;
player_ly = target_ly; player_ly = target_ly;
@@ -91,9 +101,11 @@ void update_player_movement(uint8_t keys, uint8_t prev_keys) {
// --- Lettura Controlli (Polling) con DAS --- // --- Lettura Controlli (Polling) con DAS ---
uint8_t keys_pressed = keys & ~prev_keys; // Tasti premuti ESATTAMENTE in questo frame 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) { if (keys_pressed) {
das_timer = DAS_DELAY; das_timer = wants_run ? DAS_REPEAT_RUN : DAS_DELAY;
das_active = 1; das_active = 1;
} else if (keys && das_active) { } else if (keys && das_active) {
// Se si tiene premuto un tasto... // 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 // Se il timer arriva a 0, emuliamo una pressione ripetuta
if (das_timer == 0 && !is_moving) { if (das_timer == 0 && !is_moving) {
das_timer = DAS_REPEAT; das_timer = wants_run ? DAS_REPEAT_RUN : DAS_REPEAT;
keys_pressed = keys; keys_pressed = keys;
} }
} else { } 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 // Se le condizioni di salto falliscono, aggiorna semplicemente il frame per far girare il personaggio
update_player_sprite(); update_player_sprite();
} }
// 2. Movimento standard (Camminata) // 2. Movimento standard (Camminata o Corsa)
else { else {
int8_t new_lx = player_lx + move_lx; int8_t new_lx = player_lx + move_lx;
int8_t new_ly = player_ly + move_ly; 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) { if (maze[new_ly][new_lx] == 1 || maze[new_ly][new_lx] == 2) {
is_moving = 1; is_moving = 1;
move_progress = 0; 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_lx = player_lx;
start_ly = player_ly; start_ly = player_ly;
target_lx = new_lx; target_lx = new_lx;
+49
View File
@@ -7,6 +7,7 @@
#include "enemy.h" #include "enemy.h"
#include "gameover.h" #include "gameover.h"
#include "stamina.h" #include "stamina.h"
#include "level.h"
#include "maze.h" #include "maze.h"
uint8_t get_tile_state(int8_t cx, int8_t cy, int8_t lx, int8_t ly) { 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". * Disegna la porzione visibile del labirinto e applica il sistema "Fog of War".
* *
+9
View File
@@ -20,6 +20,15 @@ void update_camera(void);
// Update stamina bar graphics // Update stamina bar graphics
void update_stamina_display(void); 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) // Update the player sprite (animation frames, jumping height offset)
void update_player_sprite(void); void update_player_sprite(void);