8244790603
- Add doc/AScreamFromTheDark_report.md: deep technical report covering
architecture, features, historical workarounds and known debt.
- Rewrite README.md to reflect the current 'A Scream from the Dark'
identity (ghost enemy, stamina/jump, fog of war, going-deeper victory)
instead of the stale 'gioco carino' copy with old file paths.
- Update doc/generation.md: goal is now a random south-edge hatch,
not the max-Manhattan-distance portal.
- Remove orphaned src/victory.{c,h} and its include in render.c;
the victory sequence was replaced by the full-screen next_level image.
- Regenerate tracked png2asset outputs to match current build.
244 lines
10 KiB
C
244 lines
10 KiB
C
#include "render.h"
|
|
#include "globals.h"
|
|
#include <gb/gb.h>
|
|
#include <string.h>
|
|
#include "tiles.h"
|
|
#include "player.h"
|
|
#include "enemy.h"
|
|
#include "gameover.h"
|
|
#include "stamina.h"
|
|
#include "maze.h"
|
|
|
|
uint8_t get_tile_state(int8_t cx, int8_t cy, int8_t lx, int8_t ly) {
|
|
if (lx < 0 || lx >= MAP_SIZE || ly < 0 || ly >= MAP_SIZE) return 0;
|
|
if (maze[ly][lx] == 0) return 0;
|
|
int8_t dx = lx - cx;
|
|
int8_t dy = ly - cy;
|
|
if (dx < 0) dx = -dx;
|
|
if (dy < 0) dy = -dy;
|
|
int8_t dist = (dx > dy) ? dx : dy;
|
|
if (dist > 2) return 0;
|
|
if (dist == 2) return 2;
|
|
return 1;
|
|
}
|
|
|
|
/**
|
|
* Aggiorna l'interfaccia utente (UI) della barra della stamina.
|
|
*
|
|
* Sviluppo & Scelte Architetturali:
|
|
* 1. La UI su Game Boy può essere fatta in Background (HUD window) o tramite Sprite.
|
|
* Abbiamo scelto gli Sprite (ID da 18 a 22) perché la Window è più difficile da
|
|
* gestire insieme allo scrolling isometrico fluido.
|
|
* 2. Gli sprite sono posizionati a coordinate fisiche fisse sullo schermo
|
|
* (es. 112, 16 per l'angolo in alto a destra). Il move_bkg della telecamera NON li muove!
|
|
* 3. La barra è lunga 5 segmenti (40 pixel totali). L'algoritmo converte il valore `stamina` (0-100)
|
|
* in pixel (0-40) e seleziona il tile giusto dal set per mostrare il bordo arrotondato e
|
|
* il livello di riempimento.
|
|
*/
|
|
void update_stamina_display(void) {
|
|
if (game_over) {
|
|
// Nasconde la barra spostando gli sprite fuori schermo quando finisce la partita
|
|
for (uint8_t i = 0; i < 5; i++) {
|
|
move_sprite(18 + i, 0, 0);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Posizionamento in alto a destra (+8, +16 sono gli offset hardware del Game Boy per gli sprite)
|
|
move_sprite(18, (uint8_t)(112 + 8), 16);
|
|
move_sprite(19, (uint8_t)(120 + 8), 16);
|
|
move_sprite(20, (uint8_t)(128 + 8), 16);
|
|
move_sprite(21, (uint8_t)(136 + 8), 16);
|
|
move_sprite(22, (uint8_t)(144 + 8), 16);
|
|
|
|
// Convertiamo 0-100% in 0-40 pixels
|
|
uint16_t temp = (uint16_t)stamina * 40;
|
|
uint8_t num_pixels = temp / 100;
|
|
|
|
// Offset di base nella VRAM dove sono caricati i tile della stamina
|
|
// Caricato a partire da tiles_TILE_COUNT per non sovrascrivere la VRAM condivisa (>=128)
|
|
uint8_t base_tile = tiles_TILE_COUNT;
|
|
|
|
for (uint8_t i = 0; i < 5; i++) {
|
|
uint8_t tile_idx;
|
|
if (i == 0) {
|
|
tile_idx = (num_pixels >= 8) ? 11 : 9; // Tappo sinistro pieno (11) o vuoto (9)
|
|
} else if (i == 4) {
|
|
tile_idx = (num_pixels >= 40) ? 12 : 10; // Tappo destro pieno (12) o vuoto (10)
|
|
} else {
|
|
int8_t seg_px = (int8_t)num_pixels - (i * 8);
|
|
if (seg_px < 0) seg_px = 0;
|
|
if (seg_px > 8) seg_px = 8;
|
|
tile_idx = seg_px; // Segmenti centrali (0..8 px)
|
|
}
|
|
|
|
// Imposta il frame dell'animazione
|
|
set_sprite_tile(18 + i, base_tile + tile_idx * 2);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Disegna la porzione visibile del labirinto e applica il sistema "Fog of War".
|
|
*
|
|
* Sviluppo & Scelte Architetturali:
|
|
* 1. Proiezione Isometrica: Convertiamo le coordinate logiche della griglia X,Y
|
|
* in coordinate schermo isometriche. La formula matematica standard è:
|
|
* iso_x = (X - Y) * (TileWidth / 2)
|
|
* iso_y = (X + Y) * (TileHeight / 2)
|
|
* Questo fa ruotare la mappa di 45 gradi e la schiaccia orizzontalmente.
|
|
* 2. Distanza di Chebyshev: Usata per la nebbia. Nascondiamo tutti i blocchi che
|
|
* distano logicamente più di 2 caselle dal centro (`max(|dx|, |dy|)`).
|
|
* Questo crea un quadrato perfetto di visibilità (5x5) attorno al giocatore,
|
|
* più leggero da calcolare sul Game Boy rispetto all'Euclidea (che usa la radice quadrata).
|
|
* 3. Auto-Tiling Mask: Calcoliamo dinamicamente quali muri sono adiacenti alla cella
|
|
* per disegnare bordi e variazioni grafiche (usando bit masking `(has_tl ? 1 : 0) | ...`).
|
|
*/
|
|
void draw_map(uint8_t center_x, uint8_t center_y) {
|
|
// Svuotiamo l'intero buffer della mappa con l'indice 0 (casella nera vuota)
|
|
memset(map_buffer, 0, sizeof(map_buffer));
|
|
|
|
// Calcoliamo la "finestra" di mappa 5x5 da processare (per ottimizzazione, ignoriamo la mappa intera)
|
|
int8_t start_x = center_x - 2;
|
|
if (start_x < 0) start_x = 0;
|
|
int8_t end_x = center_x + 2;
|
|
if (end_x >= MAP_SIZE) end_x = MAP_SIZE - 1;
|
|
|
|
int8_t start_y = center_y - 2;
|
|
if (start_y < 0) start_y = 0;
|
|
int8_t end_y = center_y + 2;
|
|
if (end_y >= MAP_SIZE) end_y = MAP_SIZE - 1;
|
|
|
|
// Processiamo la mappa in DUE passate per risolvere il problema dell'overlapping isometrico.
|
|
// L'engine disegna da Nord a Sud. I tile a Sud sovrascrivono la metà inferiore dei tile a Nord.
|
|
// Se disegnassimo le scale nella prima passata, i pavimenti davanti ad esse sovrascriverebbero
|
|
// i gradini con i loro angoli trasparenti (che sono di colore piatto).
|
|
// Disegnandole per ultime, le scale sovrascriveranno i pavimenti davanti, e grazie alle maschere
|
|
// dei propri angoli inferiori, si fonderanno perfettamente con il colore del pavimento.
|
|
|
|
// Passata 1: Pavimenti normali
|
|
for (int8_t ly = start_y; ly <= end_y; ly++) {
|
|
for (int8_t lx = start_x; lx <= end_x; lx++) {
|
|
if (maze[ly][lx] == 1) {
|
|
// Distanza di Chebyshev dal giocatore
|
|
int8_t dx = lx - center_x;
|
|
int8_t dy = ly - center_y;
|
|
if (dx < 0) dx = -dx;
|
|
if (dy < 0) dy = -dy;
|
|
int8_t dist = (dx > dy) ? dx : dy;
|
|
|
|
if (dist > 2) continue;
|
|
|
|
int8_t iso_x = (lx - ly) * 2 + 12;
|
|
int8_t iso_y = (lx + ly) * 1 + 2;
|
|
uint8_t is_alt = ((lx + ly) % 2 == 0);
|
|
|
|
uint8_t state_tl = get_tile_state(center_x, center_y, lx - 1, ly);
|
|
uint8_t state_tr = get_tile_state(center_x, center_y, lx, ly - 1);
|
|
uint8_t mask = state_tl + state_tr * 3;
|
|
|
|
uint8_t v;
|
|
if (dist == 2) {
|
|
v = is_alt ? (27 + mask) : (18 + mask);
|
|
} else {
|
|
v = is_alt ? (9 + mask) : mask;
|
|
}
|
|
|
|
for (uint8_t y = 0; y < 2; y++) {
|
|
for (uint8_t x = 0; x < 4; x++) {
|
|
uint8_t target_x = (iso_x + x) & 31;
|
|
uint8_t target_y = (iso_y + y) & 31;
|
|
uint8_t src_tile = tiles_map[4 + v * 8 + y * 4 + x];
|
|
map_buffer[target_y * 32 + target_x] = src_tile;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Passata 2: Scala della Vittoria (disegnata sempre sopra)
|
|
for (int8_t ly = start_y; ly <= end_y; ly++) {
|
|
for (int8_t lx = start_x; lx <= end_x; lx++) {
|
|
if (maze[ly][lx] == 2) {
|
|
int8_t dx = lx - center_x;
|
|
int8_t dy = ly - center_y;
|
|
if (dx < 0) dx = -dx;
|
|
if (dy < 0) dy = -dy;
|
|
int8_t dist = (dx > dy) ? dx : dy;
|
|
|
|
if (dist > 2) continue;
|
|
|
|
int8_t iso_x = (lx - ly) * 2 + 12;
|
|
int8_t iso_y = (lx + ly) * 1 + 2;
|
|
uint8_t is_alt = ((lx + ly) % 2 == 0);
|
|
|
|
uint8_t state_tl = get_tile_state(center_x, center_y, lx - 1, ly);
|
|
uint8_t state_tr = get_tile_state(center_x, center_y, lx, ly - 1);
|
|
uint8_t state_bl = get_tile_state(center_x, center_y, lx, ly + 1);
|
|
uint8_t state_br = get_tile_state(center_x, center_y, lx + 1, ly);
|
|
|
|
uint16_t mask = state_tl + state_tr * 3 + state_bl * 9 + state_br * 27;
|
|
|
|
uint16_t v;
|
|
if (dist == 2) {
|
|
v = is_alt ? (36 + 243 + mask) : (36 + 162 + mask);
|
|
} else {
|
|
v = is_alt ? (36 + 81 + mask) : (36 + mask);
|
|
}
|
|
|
|
for (uint8_t y = 0; y < 2; y++) {
|
|
for (uint8_t x = 0; x < 4; x++) {
|
|
uint8_t target_x = (iso_x + x) & 31;
|
|
uint8_t target_y = (iso_y + y) & 31;
|
|
uint8_t src_tile = tiles_map[4 + v * 8 + y * 4 + x];
|
|
map_buffer[target_y * 32 + target_x] = src_tile;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
update_stamina_display();
|
|
|
|
// Ottimizzazione hardware critica: trasferire tutta la mappa (1024 bytes) via set_bkg_tiles
|
|
// causa lag e sfarfallii (VRAM access). Trasferiamo solo le righe da 2 a 17 (16 righe totali),
|
|
// che è la porzione visibile del Game Boy (160x144 px) in cui si svolge l'azione.
|
|
set_bkg_tiles(0, 2, 32, 16, &map_buffer[2 * 32]);
|
|
}
|
|
|
|
/**
|
|
* Aggiorna il registro scroll hardware della telecamera (SCX, SCY)
|
|
* calcolando la proiezione isometrica in pixel e centrandola nello schermo (160/2, 144/2).
|
|
*/
|
|
void update_camera(void) {
|
|
// Coordinate fisiche pixel calcolate allo stesso modo del motore grafico
|
|
int16_t px = (player_lx - player_ly) * 16 + 96;
|
|
int16_t py = (player_lx + player_ly) * 8 + 16;
|
|
|
|
// Centra il pixel px,py in mezzo allo schermo (-64 e -72)
|
|
scroll_x = px - 64;
|
|
scroll_y = py - 72;
|
|
|
|
// Comando hardware GBDK per scrollare il livello Background
|
|
move_bkg(scroll_x, scroll_y);
|
|
}
|
|
|
|
/**
|
|
* Aggiorna il metasprite 16x16 del giocatore.
|
|
* Applica l'animazione di camminata o l'arco parabolico per l'altezza del salto.
|
|
*/
|
|
void update_player_sprite(void) {
|
|
// Animazione di camminata (alterna tra due frame a seconda del `move_progress`)
|
|
uint8_t frame_offset = is_moving ? ((move_progress >> 2) & 1) : 0;
|
|
|
|
uint8_t y_offset = 0;
|
|
if (is_jumping) {
|
|
// Matematica dell'Arco Parabolico per il salto.
|
|
// x * (16 - x) crea una curva che sale a 16 px di apice a metà altezza (8 frame).
|
|
y_offset = (move_progress * (16 - move_progress)) >> 2;
|
|
}
|
|
|
|
// move_metasprite è fornito da png2asset e posiziona multipli sprite hardware insieme
|
|
// Il giocatore è sempre fisso al centro dello schermo (88, 88). È il mondo (sfondo) a muoversi.
|
|
move_metasprite(player_metasprites[player_dir * 2 + frame_offset], 0, 0, 88, 88 - y_offset);
|
|
}
|