0f88cea912
The finale text is now 'YOUR TORCH HAS / RUN OUT, / YOU ARE TRAPPED. / JUST ANOTHER SCREAM / FROM THE DARK. / GAME OVER' — matching the tragic dark theme of the game. BGP inverted for black background with white text.
311 lines
12 KiB
C
311 lines
12 KiB
C
#include <gb/gb.h>
|
|
#include <gbdk/font.h>
|
|
#include <stdint.h>
|
|
#include <string.h>
|
|
#include <rand.h>
|
|
|
|
#include "engine.h"
|
|
#include "globals.h"
|
|
#include "maze.h"
|
|
#include "sound.h"
|
|
#include "render.h"
|
|
#include "player_logic.h"
|
|
#include "enemy_logic.h"
|
|
|
|
// Generated by png2asset
|
|
#include "tiles.h"
|
|
#include "player.h"
|
|
#include "enemy.h"
|
|
#include "gameover.h"
|
|
#include "next_level.h"
|
|
#include "stamina.h"
|
|
#include "level.h"
|
|
#include "title_bg.h"
|
|
|
|
/**
|
|
* Inizializza la schermata del titolo
|
|
*/
|
|
void title_init(void) {
|
|
// Nasconde tutti gli sprite (sia personaggio che HUD)
|
|
for (uint8_t i = 0; i < 40; i++) move_sprite(i, 0, 0);
|
|
|
|
// Carica l'immagine di sfondo del titolo
|
|
set_bkg_data(0, title_bg_TILE_COUNT, title_bg_tiles);
|
|
set_bkg_tiles(0, 0, title_bg_WIDTH / 8, title_bg_HEIGHT / 8, title_bg_map);
|
|
|
|
// Configura la palette standard B/N
|
|
BGP_REG = 0xE4;
|
|
|
|
// Centra lo scroll della telecamera in alto a sinistra (0,0)
|
|
move_bkg(0, 0);
|
|
|
|
// Inizializza l'Audio Hardware del Game Boy
|
|
NR52_REG = 0x80; // Accende il chip audio
|
|
NR50_REG = 0x77; // Volume master al massimo
|
|
NR51_REG = 0xFF; // Abilita l'output stereo su tutti i canali
|
|
|
|
// Avvia la musica del titolo riavviando il sequencer
|
|
sound_reset_music_state();
|
|
remove_VBL(play_music_tick);
|
|
add_VBL(play_music_tick);
|
|
}
|
|
|
|
/**
|
|
* Loop di aggiornamento della schermata del titolo (non fa nulla, la pressione di START è gestita in main.c)
|
|
*/
|
|
void title_update(uint8_t keys, uint8_t prev_keys) {
|
|
// Vuoto. La musica continua in background via Interrupt (VBL)
|
|
}
|
|
|
|
/**
|
|
* Inizializza l'engine per la fase di Gameplay.
|
|
* Qui viene orchestrata la "nascita" di un nuovo livello.
|
|
*/
|
|
void engine_init(void) {
|
|
SPRITES_8x16; // Imposta la modalità Sprite Alti (16 pixel invece di 8)
|
|
|
|
// Hard reset Audio
|
|
NR52_REG = 0x80;
|
|
NR50_REG = 0x77;
|
|
NR51_REG = 0xFF;
|
|
|
|
// Carica il font testuale di sistema (IBM)
|
|
font_init();
|
|
font_t ibm_font = font_load(font_ibm);
|
|
font_set(ibm_font);
|
|
|
|
// Avvia il sequencer per la musica In-Game "Eerie pulse"
|
|
sound_reset_music_state();
|
|
remove_VBL(play_music_tick);
|
|
add_VBL(play_music_tick);
|
|
|
|
// Usa il registro divisore hardware (DIV_REG) per seedare l'RNG. Garantisce labirinti diversi ogni volta.
|
|
initrand(DIV_REG);
|
|
|
|
map_size = MAP_SIZE + 2 * (level - 1); // +2/level from 7, capped at MAX_MAP_SIZE (21) -> level 8
|
|
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
|
|
generate_maze();
|
|
|
|
// Imposta le palette (DMG compatibile)
|
|
OBP0_REG = 0xE4; // Palette Normale (Nero, Grigio Scuro, Grigio Chiaro, Bianco)
|
|
OBP1_REG = 0x1B; // Palette Invertita (per il fantasma: bianco, grigio scuro, nero)
|
|
BGP_REG = 0xE4; // Palette Background
|
|
|
|
// Carica in memoria VRAM la grafica di Tiles e Sprite
|
|
set_bkg_data(0, tiles_TILE_COUNT, tiles_tiles);
|
|
set_sprite_data(0, player_TILE_COUNT, player_tiles);
|
|
set_sprite_data(player_TILE_COUNT, enemy_TILE_COUNT, enemy_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)
|
|
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;
|
|
player_ly = 1;
|
|
// Fallback di sicurezza: Cerca la prima casella libera se 1,1 è occupato (teoricamente impossibile col DFS)
|
|
if (maze[player_ly][player_lx] == 0) {
|
|
uint8_t found = 0;
|
|
for (uint8_t y = 1; y < map_size - 1; y++) {
|
|
for (uint8_t x = 1; x < map_size - 1; x++) {
|
|
if (maze[y][x] == 1) {
|
|
player_lx = x;
|
|
player_ly = y;
|
|
found = 1;
|
|
break;
|
|
}
|
|
}
|
|
if (found) break;
|
|
}
|
|
}
|
|
|
|
uint8_t enemy_min_dist = map_size / 2;
|
|
if (enemy_min_dist < 3) enemy_min_dist = 3;
|
|
// SPAWN DEI NEMICI (num_enemies fantasmi): cella calpestabile lontana >= map_size/2
|
|
// dal giocatore e >= 2 dagli altri fantasmi gia' piazzati; cooldown sfasati (i*8)
|
|
// cosi' non si muovono tutti in sincrono. Dopo 40 tentativi rilassa la distanza.
|
|
for (uint8_t i = 0; i < num_enemies; i++) {
|
|
uint8_t rx = 0, ry = 0, placed = 0;
|
|
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 (dy < 0) dy = -dy;
|
|
int8_t dist = (dx > dy) ? dx : dy;
|
|
uint8_t req = (tries < 40) ? enemy_min_dist : 2;
|
|
if (dist < (int8_t)req) continue;
|
|
uint8_t too_close = 0;
|
|
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
|
|
game_over = 0;
|
|
stamina = 100;
|
|
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)
|
|
}
|
|
|
|
/**
|
|
* 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).
|
|
* Questo metodo funge da "Direttore d'Orchestra", delegando i compiti ai moduli specializzati.
|
|
*/
|
|
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) {
|
|
game_over_timer--;
|
|
// Quando scade il timer drammatico...
|
|
if (game_over_timer == 0) {
|
|
if (game_over == 1) {
|
|
// ...Svuota lo schermo
|
|
memset(map_buffer, 0, sizeof(map_buffer));
|
|
set_bkg_tiles(0, 0, 32, 32, map_buffer);
|
|
} else if (game_over == 2) {
|
|
// Mostra la schermata "Going Deeper"
|
|
HIDE_SPRITES;
|
|
SCX_REG = 0;
|
|
SCY_REG = 0;
|
|
set_bkg_data(0, next_level_TILE_COUNT, next_level_tiles);
|
|
set_bkg_tiles(0, 0, 20, 18, next_level_map);
|
|
} else if (game_over == 3) {
|
|
// FINALE TRAGICO: livello 8 superato, ma non e' una fuga. La traccia
|
|
// si e' esaurita, sei intrappolato. Sfondo nero (BGP invertito) + testo
|
|
// chiaro col font IBM (ricaricato: il gameplay lo aveva sovrascritto).
|
|
HIDE_SPRITES;
|
|
SCX_REG = 0;
|
|
SCY_REG = 0;
|
|
BGP_REG = 0x1B; // palette invertita: sfondo nero, testo chiaro
|
|
font_init();
|
|
font_t end_font = font_load(font_ibm);
|
|
font_set(end_font);
|
|
memset(map_buffer, 0, sizeof(map_buffer)); // tile 0 = spazio
|
|
ending_puttext(3, 4, "YOUR TORCH HAS");
|
|
ending_puttext(6, 5, "RUN OUT,");
|
|
ending_puttext(2, 7, "YOU ARE TRAPPED.");
|
|
ending_puttext(0, 10, "JUST ANOTHER SCREAM");
|
|
ending_puttext(3, 11, "FROM THE DARK.");
|
|
ending_puttext(5, 14, "GAME OVER");
|
|
set_bkg_tiles(0, 0, 32, 32, map_buffer);
|
|
}
|
|
|
|
// Imposta i timer audio per far iniziare la musica finale dal modulo sound.c
|
|
// La musica verrà suonata dall'interrupt play_music_tick in background.
|
|
if (game_over == 1 || game_over == 3) {
|
|
// Start gameover sequence state (tragic beat: sconfitta o finale)
|
|
NR21_REG = 0x80;
|
|
NR22_REG = 0xF3;
|
|
NR23_REG = 0x2C;
|
|
NR24_REG = 0x84;
|
|
}
|
|
}
|
|
} else {
|
|
// 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) {
|
|
move_metasprite(gameover_metasprites[0], player_TILE_COUNT + enemy_TILE_COUNT, 8, 88, 120);
|
|
update_player_sprite();
|
|
}
|
|
|
|
// Attende la pressione di START per riavviare
|
|
if ((keys & J_START) && !(prev_keys & J_START)) {
|
|
if (game_over == 2) {
|
|
// Going Deeper -> livello successivo
|
|
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();
|
|
}
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
// --- GESTIONE DEL LOOP DI GIOCO STANDARD ---
|
|
|
|
// 1. DELEGA L'AI DEL NEMICO (se il fantasma si deve muovere, lo calcola qui)
|
|
update_enemy_logic();
|
|
|
|
// 2. DELEGA LA LOGICA DEL GIOCATORE (movimento, tasti, animazioni)
|
|
update_player_movement(keys, prev_keys);
|
|
|
|
// Note: Il rendering effettivo della mappa e della telecamera avviene già all'interno di
|
|
// update_player_movement durante l'interpolazione del passo (per fluidità sub-tile).
|
|
}
|