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
+15
View File
@@ -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();
+3
View File
@@ -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;
+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)
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;
+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 "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 {
+26 -4
View File
@@ -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;
+49
View File
@@ -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".
*
+9
View File
@@ -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);