e5c37c7c86
Riscritti tutti i file doc/ e README.md per riflettere lo stato attuale del gioco (8 livelli, multi-nemico, corsa, finale tragico, musica dedicata, fog scalabile, flush dinamico, claimed.png death screen, ecc.): - index.md: nuovo indice con riferimenti ai 7 capitoli aggiornati. - architecture.md: tabella moduli con righe, variabili chiave globals, pipeline asset, vincoli hardware (ROM/WRAM/VRAM/OAM). - graphics.md: proiezione iso, fog scalabile (fog_radius), auto-tiling multi-pass, flush dinamico 16-righe con wrap, HUD stamina + livello. - generation.md: DFS con stack WRAM, dimensioni crescenti 7->21, botola a distanza scalabile, spawn multi-nemico. - gameplay.md: DAS, camminata, corsa, salto, stamina scalabile, progressione 8 livelli, schermate (death/going deeper/finale). - ai.md: multi-entity (8 fantasmi), greedy, cooldown scalabile, hitbox, culling. - audio.md: 4 canali APU, 5 tracce (title 112/3canali, gameplay 96, gameover 128, finale 192 loop, going deeper 96), SFX. - AScreamFromTheDark_report.md: riscritto completamente (~200 righe), tutte le sezioni aggiornate, workaround storici, debiti tecnici. - README.md: intro, caratteristiche, soundtrack, architettura, test.
156 lines
9.7 KiB
Markdown
156 lines
9.7 KiB
Markdown
# A Scream from the Dark — Report Tecnico Approfondito
|
||
|
||
> Survival-horror procedurale isometrico per Game Boy (DMG/CGB), scritto in C con **GBDK-2020** (SDCC).
|
||
> Repository: `gameboy-hello-iso` · ROM: `build/hello_iso.gb` (32 KB).
|
||
> ~6500 righe C/H + ~2000 righe Python (pipeline asset).
|
||
|
||
---
|
||
|
||
## 1. Identità del gioco
|
||
|
||
"A Scream from the Dark" è un survival-horror procedurale in prospettiva isometrica 2.5D. Sei imprigionato in un labirinto generato casualmente, illuminato solo da un ristretto quadrato di visibilità (Fog of War). Dei **fantasmi** si nascondono nel buio e ti braccano. L'unica via è una **botola** posta lontano dalla partenza: raggiungerla significa sprofondare più giù (*Going Deeper*) e affrontare un livello più grande, con più nemici e meno visibilità. Dopo **8 livelli** il gioco finisce con un **finale tragico**.
|
||
|
||
### Loop di gioco
|
||
1. **Title screen** — sfondo 2-bit + tema musicale tragico (112 note, 3 canali, ~56 sec in loop).
|
||
2. **Gameplay** — esplorazione a griglia con camminata, **corsa** (B+direzione), **salto evasivo** (A+direzione), stamina, AI greedy multi-nemico, hitbox pixel-perfect. Indicatore `L<n>` in alto a sinistra.
|
||
3. **Game Over (Sconfitta)** — 45 frame di fermo immagine → schermata `claimed.png` + metasprite "GAME OVER" + musica tragica (128 note). START → ricomincia dallo stesso livello.
|
||
4. **Going Deeper (Vittoria, livelli 1-7)** — schermata testuale "GOING DEEPER / LEVEL N" + melodia misteriosa (96 step). START → livello successivo (più grande, più difficile).
|
||
5. **Finale (Livello 8)** — `game_over = 3`: sfondo nero, "YOUR TORCH HAS / RUN OUT, / YOU ARE TRAPPED. / JUST ANOTHER SCREAM / FROM THE DARK. / GAME OVER" + musica dedicata (192 note, lamento discendente in Re minore, loop). START → titolo.
|
||
|
||
---
|
||
|
||
## 2. Architettura del codice
|
||
|
||
### 2.1 Moduli
|
||
| File | Ruolo |
|
||
|------|-------|
|
||
| `main.c` | Entry point, loop VBL, `app_state` (0=title, 1=game). |
|
||
| `engine.c` | Orchestrazione: `title_init/update`, `engine_init`, `engine_update`, game-over branches (1/2/3). |
|
||
| `globals.c/h` | Stato globale: mappa `[21][21]`, `map_size`, `fog_radius`, `level`, `num_enemies`, enemy arrays[8], stamina, ecc. |
|
||
| `maze.c` | DFS + loop + botola. Array statici in WRAM. |
|
||
| `player_logic.c` | DAS, camminata, corsa, salto, stamina, rilevamento botola (game_over 2/3). |
|
||
| `enemy_logic.c` | AI greedy multi-entity, cooldown scalabile, rendering, hitbox. |
|
||
| `render.c` | Iso, fog scalabile, auto-tiling multi-pass, flush dinamico 16-righe, HUD stamina + livello. |
|
||
| `sound.c` | Sequencer VBL: title (112), gameplay (96), gameover (128), finale (192 loop). |
|
||
| Asset `.c` | `tiles`, `player`, `enemy`, `gameover`, `stamina`, `level`, `claimed`, `title_bg` (png2asset). |
|
||
|
||
### 2.2 Pipeline asset
|
||
Script Python (`generate_assets.py`, `generate_enemy.py`, `generate_level.py`) → PNG → `png2asset` → `src/*.c` → `lcc` (SDCC) → ROM.
|
||
|
||
---
|
||
|
||
## 3. Funzionalità chiave
|
||
|
||
### 3.1 Proiezione isometrica
|
||
```
|
||
iso_x = (lx - ly) * 2 + 12 // tile 32x16
|
||
iso_y = (lx + ly) * 1 + 2 // altezza 8px
|
||
px = (lx - ly) * 16 + 96 // pixel fisici
|
||
py = (lx + ly) * 8 + 16
|
||
```
|
||
Camera: `scroll_x = px - 64`, `scroll_y = py - 72`.
|
||
|
||
### 3.2 Generazione labirinto (`maze.c`)
|
||
- **DFS iterativo** con stack in WRAM (statico, sized per `MAX_MAP_SIZE=21`). Celle dispari = stanze, pari = muri.
|
||
- **Loop**: riapertura 15% dei muri → percorsi alternativi.
|
||
- **Botola**: cella a Chebyshev ≥ `map_size/2` da (1,1), scelta casuale.
|
||
- **Dimensioni crescenti**: `map_size = 7 + 2*(level-1)`, capped 21 (livello 8). Sempre dispari.
|
||
|
||
### 3.3 Movimento & DAS (`player_logic.c`)
|
||
- **State machine**: durante `is_moving` (16 frame, 8 in corsa) input ignorato.
|
||
- **LERP** a punto fisso `>>4`. Corsa: `move_progress += 2`.
|
||
- **DAS**: delay 12, repeat 6 (walk) / 2 (run).
|
||
- **Salto**: A+dir → 2 tile, cella intermedia deve essere muro, stamina ≥ 60. Arco parabolico visivo.
|
||
- **Corsa**: B+dir, stamina ≥ 10, 8 frame/tile, 10 stamina/tile. Fallback a walk se stamina < 10.
|
||
- **Stamina**: ricarica 1 pt ogni `stamina_recharge_rate` (60..144 frame, scala col livello).
|
||
|
||
### 3.4 Fog of War (`render.c` + `enemy_logic.c`)
|
||
Chebyshev `max(|dx|,|dy|)` con raggio `fog_radius`:
|
||
- L1-6: `fog_radius = 2` (5×5).
|
||
- L7-8: `fog_radius = 1` (3×3).
|
||
Celle a `dist == fog_radius` → tile scuro (penombra). Il nemico si attiva solo se entro `fog_radius`.
|
||
|
||
### 3.5 Auto-tiling & multi-pass
|
||
Bitmasking 4-vicini (16 varianti × 2 stili). Pass 1: pavimenti. Pass 2: botola ( Painter's algorithm). Flush **dinamico 16 righe** centrato su `center_iso_y` con wrap (2 chiamate `set_bkg_tiles` se necessario). Necessario per labirinti grandi dove la finestra fog wrappa fuori dal range fisso 2-17.
|
||
|
||
### 3.6 AI Fantasmi (`enemy_logic.c`)
|
||
- **Multi-entity**: fino a 8 fantasmi (`num_enemies = level`). Stato in array. OAM `2+i*2`.
|
||
- **Greedy**: minimizza `dx²+dy²` tra le 4 celle adiacenti. No A* (troppo pesante su 4 MHz con 8 nemici).
|
||
- **Cooldown scalabile**: `enemy_step_cooldown = 60 - 7*(level-1)` (floor 10). Sfasamento iniziale `i*8`.
|
||
- **Hitbox pixel-perfect**: `|dx|<12 && |dy|<6`.
|
||
- **Culling**: renderizzato solo se entro `fog_radius` e on-screen.
|
||
|
||
### 3.7 Audio (`sound.c`)
|
||
Sequencer via VBL interrupt, 4 canali APU:
|
||
- **Title**: 112 note, 3 canali (CH1 melodia + CH2 basso indipendente + CH4 rintocchi), ~56 sec, loop. Progressione Dm-Bb-F-C-A7-Gm-A7 con C# (7ª armonica) e discesa cromatica. Envelope haunting.
|
||
- **Gameplay**: 96 note eerie pulse, 20 frame/nota.
|
||
- **Gameover**: 128 note polifoniche + noise (thud/crash), 10 frame/nota.
|
||
- **Finale**: 192 note (24 accordi), lamento discendente Dm→abisso (C2), 14 frame/nota, **loop infinito**. CH1 melodia sommessa + CH2 basso profondo + CH4 toll (mid/crash/deep).
|
||
- **Going Deeper**: 96 note misteriose, 15 frame/nota.
|
||
- **SFX**: salto (sweep up CH1), cattura (sweep down CH1).
|
||
|
||
### 3.8 HUD
|
||
- **Stamina** (alto dx): 5 sprite OAM 18-22. Base VRAM `tiles_TILE_COUNT` (≥128, workaround overlap).
|
||
- **Livello** (alto sx): 3 sprite OAM 23-25, asset `level.png` (glifi L, 0-9). Base VRAM allineata a indice **pari** (8x16 hardware ignora LSB). `-keep_duplicate_tiles` per ordine prevedibile. Nascosto durante game over.
|
||
|
||
### 3.9 Progressione livelli
|
||
- **8 livelli** con difficoltà crescente:
|
||
- Maze: 7×7 → 21×21 (+2/livello).
|
||
- Nemici: 1 → 8 (+1/livello).
|
||
- Cooldown fantasma: 60 → 11 frame.
|
||
- Ricarica stamina: 60 → 144 frame/pt.
|
||
- Nebbia: 5×5 → 3×3 (dal L7).
|
||
- **Sconfitta**: ricomincia dallo stesso livello (non azzera).
|
||
- **Livello 8 superato**: `game_over = 3` (finale tragico) invece di 2.
|
||
|
||
### 3.10 Schermate
|
||
- **Title**: `title_bg.png` (160×144, 2-bit) + musica.
|
||
- **Death**: `claimed.png` (160×144, 2-bit) + metasprite "GAME OVER".
|
||
- **Going Deeper**: testo font IBM "GOING DEEPER / LEVEL N" (ex immagine, sostituita per risparmiare ROM).
|
||
- **Finale**: font IBM su sfondo nero (BGP 0x1B invertito), scritta diretta in `map_buffer` (tile = ASCII-32, no printf).
|
||
|
||
---
|
||
|
||
## 4. Workaround e fix storici (dalla git history)
|
||
|
||
| Commit | Problema | Workaround |
|
||
|--------|----------|------------|
|
||
| `93deb35` | VRAM overlap stamina/BG | Load stamina a `tiles_TILE_COUNT` (≥128). |
|
||
| `6d94992` | 2-frame lag DAS | Sync DAS con state machine passo. |
|
||
| `7d85aec` | Perfect maze intrappolava | Riapertura 15% muri → loop. |
|
||
| `b47455c` | Offset rendering nemico | Wrap `&255` + hitbox pixel-perfect 12×6. |
|
||
| `f14e264` | Scale bloccavano fantasma | Ridisegnate come botola (tile 2 calpestabile). |
|
||
| `cb19677` | Glitch next_level + scroll | Reset SCX/SCY + quantizzazione soglie fisse. |
|
||
| (livelli) | ROM >32KB con 3 immagini full-screen | Sostituito next_level image con testo font; claimed image.resize per fit. |
|
||
| (HUD livello) | Base VRAM dispari in 8x16 | Allineamento a pari `(raw+1) & 0xFE`. |
|
||
| (flush) | 32-row flush stallsava 5 frame | Flush dinamico 16-righe centrato + wrap. |
|
||
| (EVELYN) | Warning SDCC 110 | Easter-egg Frank Zappa, benigno. |
|
||
|
||
---
|
||
|
||
## 5. Debiti tecnici e osservazioni
|
||
|
||
1. **`test_movement.py`**: cerca `hello_iso.noi` nella cwd ma il Makefile genera solo `test_gameover.noi` in `build/`. Script stale.
|
||
2. **Warning 110 SDCC** ("EVELYN the modified DOG"): benigno, Easter-egg di Sandeep Dutta (omaggio a Frank Zappa, canzone "Evelyn, a Modified Dog"). Indica che l'ottimizzatore ha rielaborato il flusso di controllo.
|
||
3. **`victory.c/h` orfani**: rimossi (sostituiti da `next_level`/finale).
|
||
4. **Flush 16-righe**: ~2-3 frame di stall per `draw_map` (preesistente, accettabile). draw_map chiamato solo ai passi del movimento.
|
||
5. **Sprite budget**: ~27 sprite su 40. Per-scanline (10) rispettato (HUD alto, player centro, nemici sparsi nel fog).
|
||
|
||
---
|
||
|
||
## 6. Test & verifica
|
||
|
||
Pipeline headless (PyBoy + OpenCV):
|
||
- `test_pyboy.py` / `take_screenshot.py`: screenshot della ROM.
|
||
- `test_movement.py`: legge maze in WRAM e simula input (indirizzi stale).
|
||
- `opencv_analyze_tiles.py`: rileva disallineamenti isometrici.
|
||
- `test_gameover.gb`: ROM standalone per schermata game over.
|
||
- `test_finale.gb`: ROM standalone che va subito al finale (musica + testo) per test rapidi.
|
||
|
||
Verificato via PyBoy: dimensioni 7→21, nemici 1→8, cooldown 60→11, recharge 60→144, fog 2→1 (L7), hatch L1→game_over=2 / L8→game_over=3, finale renderizza + START→title→L1, death screen claimed.png, title music 3 canali attivi.
|
||
|
||
---
|
||
|
||
## 7. Conclusione
|
||
|
||
"A Scream from the Dark" è un survival-horror completo per Game Boy: 8 livelli di difficoltà crescente, multi-nemico, corsa e salto con stamina, fog of war scalabile, audio polifonico procedurale (4 tracce + SFX), finale tragico con musica dedicata — tutto in 32 KB di ROM e ~3 KB di WRAM. Il codice è modulare, commentato in italiano, e rappresenta un caso di studio su come tradurre feature "moderne" in matematica a punto fisso e gestione diretta dei registri hardware del Game Boy. |