Files
ascreamfromthedark-gb/doc/AScreamFromTheDark_report.md
T
enne2 0e2e383ad7 Rimuovi spoiler del finale dalla documentazione
Il testo del finale tragico ('YOUR TORCH HAS RUN OUT...') e' uno
spoiler che va scoperto giocando. Sostituito con 'messaggio tragico
(spoiler-free, da scoprire giocando)' in gameplay.md e nel report
tecnico. Le info tecniche (game_over=3, 192 note, musica dedicata,
BGP invertito) restano, ma il contenuto narrativo non viene rivelato.
2026-06-24 20:43:38 +02:00

156 lines
9.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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, messaggio tragico (spoiler-free) + 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.