Anatomia di un gioco (che non sembra un gioco didattico)
Mille righe di JavaScript vaniglia. Un file HTML. Si apre nel browser, ti mette in un dungeon, e ti uccide. Niente React, niente Unity, niente build system. La prima reazione è: un gioco. La seconda, se hai passato anni in aule a cercare il modo di spiegare cose complicate senza annoiare nessuno, è: un microscopio.
Dentro queste righe ci sono almeno quattro corsi compressi — algoritmi, generazione procedurale, teoria delle decisioni, architettura software — e nessuno si presenta come tale. Si presentano come nemici che ti inseguono, oro che luccica, scale che portano al piano successivo. Lo studente impara qualcosa che non sa di star imparando, che è esattamente la condizione in cui l’apprendimento funziona. Lo diceva Rodari in un contesto diverso ma con la stessa intuizione: il gioco non è il contrario dell’impegno, è la sua forma più efficiente.
In passato la domanda ricorrente non era mai “come funziona l’algoritmo” ma “a cosa serve”. A* serve a far muovere il nemico che ti insegue. Serve a suggerirti il percorso quando premi H. Serve a generare una mappa diversa ogni volta che muori. Adesso il docente capisce A*. Non perché glielo hai spiegato — perché l’ha visto fallire quando il nemico si incastra in un corridoio, e funzionare quando tre nemici si distribuiscono nello spazio senza accalcarsi.
Ma la versione attuale del gioco ha una scelta che non è solo tecnica — è il nodo attorno a cui si costruisce l’intera progressione didattica.
L’hint costa un punto vita.
Un punto. Sembra niente. Ma cambia tutto. Nella versione originale l’hint era gratuito: premevi H e il gioco ti mostrava il percorso ottimale verso l’oro, le scale, la pozione. Un oracolo senza prezzo. Il problema è che un oracolo gratuito trasforma il labirinto in qualcosa di automatico e quindi noioso. Se sai sempre dove andare, la nebbia di guerra diventa decorazione. L’incertezza, che è il motore emotivo del gioco, evapora.
Ora premere H costa un punto vita e allerta i nemici nel raggio di quattro celle. Li mette in stato di allerta: ti inseguono da più lontano, si attivano anche fuori dalla tua visuale. La conoscenza ha un prezzo, e il prezzo è duplice: perdi salute e guadagni attenzione indesiderata. Se hai 4 HP e un bruto a tre celle di distanza, chiedere aiuto al sistema potrebbe essere l’ultima cosa che fai.
Questo è insegnabile. Non come meccanica di gioco — come modello epistemologico. Ogni sistema di informazione ha un costo: computazionale, energetico, politico. Google ti dà risposte gratis? No: ti dà risposte in cambio di dati. Un consulente ti dà competenza in cambio di denaro. Un informatore ti dà intelligence in cambio di protezione. L’hint che costa un punto vita è la versione più onesta di questo scambio: sai esattamente cosa paghi e cosa ottieni. In aula, bastano cinque minuti per passare da “il gioco mi toglie vita quando chiedo aiuto” a “ogni forma di conoscenza ha un costo nascosto o esplicito”. Non è forzatura — è la struttura.
E qui entra la seconda novità che cambia la natura del dungeon.
I nemici rubano. Non è un modo di dire. Se un nemico nel suo turno cammina su una cella con oro, l’oro sparisce. Se cammina su un’arma, la prende — e il suo danno aumenta permanentemente. Se trova una pozione, si cura a piena vita. Il dungeon non è più un ambiente: è un’entità competitiva. L’oro che vedi lampeggiare a tre stanze di distanza potrebbe non esserci quando arrivi. Il messaggio “Nemici rubano oro!” che compare nell’HUD produce, ogni volta, una scarica di urgenza che nessun tutorial sulla competizione per risorse potrebbe replicare.
Didatticamente, questa meccanica apre un territorio enorme. Si può parlare di scarsità dinamica — le risorse non sono solo limitate, sono contese. Si può parlare di incentivi alla velocità — il giocatore prudente che esplora ogni angolo perde più dell’esploratore rapido che rischia. Si può parlare di sistemi a somma zero: ciò che prende il nemico non lo prendi tu, e viceversa. Concetti da teoria dei giochi che in un manuale richiedono diagrammi di payoff e definizioni formali. Qui richiedono morire tre volte perché un ‘rapido’ ti ha soffiato l’arma.
I ‘rapidi’, appunto. Ecco il terzo strato.
Tre tipi di nemici. Non è una trovata — è un laboratorio di design parametrico. Il base (E) si muove orizzontale o verticale una volta per turno. Il rapido (F) si muove in diagonale, due volte per turno, con euristica Chebyshev anziché Manhattan — il che significa che in spazi aperti ti accerchia, taglia angoli, arriva da dove non te lo aspetti. Il bruto (B) si muove ogni due turni, colpisce forte, pesa tre nella cost map (gli altri lo evitano), ed è quasi impossibile da mancare. Stesso algoritmo A*, tre comportamenti emergenti. Se vuoi insegnare cosa significa parametrizzare un sistema, non trovi esempio migliore. Non dici “il polimorfismo è quando classi diverse implementano la stessa interfaccia” — mostri che lo stesso A* produce un assassino diagonale, un carro armato cieco e un fante prevedibile, a seconda di quattro numeri: {hpMul, dmgMul, speed, diag}.
Il rapido che si muove in diagonale è anche un caso di studio sulle euristiche. Il gioco usa Manhattan ($|x_1 – x_2| + |y_1 – y_2|$) per i nemici orizontal-vertical e Chebyshev ($\max(|x_1 – x_2|, |y_1 – y_2|)$) per i diagonali. La differenza è visibile: il rapido non zigzaga nei corridoi, scivola negli spazi aperti. Due righe di codice, un cambiamento qualitativo nel comportamento percepito. Si mostra in aula e si chiede: cosa cambia? E la risposta arriva dai giocatori, non dal docente.
OK. Rischio di perdermi nei nemici. Torniamo alla mappa.
Le mappe ora sono generate con Binary Space Partitioning — BSP. L’algoritmo originale piazzava stanze rettangolari a caso evitando sovrapposizioni, poi le collegava con corridoi a L. Funzionava, ma produceva geometrie prevedibili: rettangoli regolari, corridoi ortogonali, stanze che non avevano rapporto strutturale tra loro. Il BSP divide lo spazio ricorsivamente in due metà, alterna tagli orizzontali e verticali, poi piazza una stanza in ogni foglia dell’albero e collega le foglie sorelle. Il risultato sono stanze di dimensioni variabili, corridoi che seguono la gerarchia spaziale, e una sensazione — non quantificabile ma percepibile — di luogo costruito, non di griglia randomizzata.
Per il combattimento, il feedback chiedeva qualcosa oltre il puro stat-check. Il gioco ora ha un sistema di imboscata: se attacchi un nemico che ha due o più muri adiacenti (è in un corridoio, in un angolo, contro un vicolo cieco), il danno aumenta del 50%. “Imboscata!” appare nell’HUD con un suono distintivo. Il giocatore che attira i nemici nei corridoi stretti e poi li colpisce quando sono incastrati non sta solo “giocando bene” — sta applicando posizionamento tattico, che è il concetto base della strategia militare da quando esistono i corridoi stretti, cioè da sempre. Il rapido in diagonale è difficile da imboscare — arriva da troppe direzioni. Il bruto è facile — è grosso, lento, finisce sempre negli angoli. Tre nemici, tre risposte tattiche diverse.
E poi c’è la legacy. Quando il tuo eroe muore, il gioco salva il suo nome — Aldo, Bice, Corrado, nomi italiani scelti dal PRNG — il piano raggiunto, le uccisioni, il punteggio. La partita successiva, sul piano dove è caduto, compare un ‘?’. Camminci sopra e leggi: “Qui cadde Aldo. Le pietre ricordano.” La schermata di morte mostra la lista degli ultimi cinque eroi con i loro numeri. Non è un salvataggio. Non è un checkpoint. È un deposito stratigrafico. La morte nel gioco originale era silenzio totale: muori, ricomincia, zero tracce. Ora la morte è geologica — si deposita. Ogni partita aggiunge uno strato. Il dungeon si riempie di ombre che non sono gameplay ma sono storia.
Forse un ‘?’ su una griglia 50×18 non merita tutta questa enfasi. Ma il punto regge: il sistema legacy trasforma il permadeath da meccanica punitiva a meccanica narrativa, e questo in aula si spiega in cinque minuti e produce una discussione di un’ora.
La struttura didattica che funziona, basata su tutto questo, ha cinque sessioni con un arco che va dal visibile al costruibile.
Prima sessione: si gioca. Proiettore, tastiera, si muore. Con docenti funziona meglio che con studenti — esperienza diretta, non teoria: i docenti hanno più paura di sbagliare. Dopo venti minuti si raccolgono osservazioni. I nemici vi seguono? Quelli arancioni sono diversi? Perché quelli rossi grossi si muovono ogni tanto? L’oro che avete visto c’è ancora? Cosa succede quando premete H? Non stai insegnando. Stai raccogliendo domande che diventeranno lezioni.
Seconda sessione: si apre il codice. Tre pezzi. ETYPES — tre oggetti, stesse chiavi, valori diversi. La nebbia di guerra e i suoi tre stati. L’A* con euristica variabile. Si mostra che lo stesso algoritmo produce comportamenti diversi cambiando parametri. Si mostra che la nebbia è un modello di certezza, non un effetto grafico. Si mostra che la differenza tra Manhattan e Chebyshev è la differenza tra un nemico prevedibile e uno che ti circonda. Un filologo che lavora su un manoscritto lacunoso conosce questa asimmetria tra visibile e ignoto per mestiere: sa dove sono le lacune, legge cosa è leggibile, inferisce il resto. L’interpolazione del copista è l’allucinazione del modello linguistico con ottocento anni di anticipo.
Terza sessione: teoria delle decisioni. Il punteggio, le strategie implicite, l’hint che costa vita, i nemici che rubano. Si gioca due partite con lo stesso seed — possibile grazie al PRNG deterministico — prendendo decisioni diverse, e si confrontano i risultati. Si discute: quando conviene chiedere aiuto? Quando il costo dell’informazione supera il beneficio? Come cambia la strategia se l’ambiente è competitivo (nemici che rubano) anziché passivo?
Quarta sessione: si modifica il codice. Si aggiunge un nemico, si cambia la BSP, si introduce un oggetto. Il gioco è un file senza dipendenze. Un adolescente con un editor di testo può farlo. In un’epoca in cui persino le calcolatrici online chiedono i cookie, un file HTML autocontenuto è un atto di resistenza piccolo ma preciso.
Quinta sessione: si scrive il lore. Il gioco ha frammenti atmosferici — “Le pareti trasudano umidità”, “I muri qui sono più antichi” — ma non ha storia. Chi ha costruito il dungeon? Perché? Cosa c’è in fondo? Lo scrivono gli studenti. A quel punto non stai più insegnando anatomia di un gioco. Stai insegnando che i giochi sono testi. Il che, per un filologo prestato alla tecnologia, è semplicemente tornare a casa per una strada che non ti aspettavi.
Il gioco è in italiano. Non tradotto — pensato. “Piano”, non “Floor”. “Rapido” e “Bruto”, non “Scout” e “Tank”. “Imboscata!”, non “Ambush!”. I frammenti di lore suonano come devono suonare in italiano. Sembra niente. Ma in un’aula italiana, la differenza tra uno strumento che parla la tua lingua e uno che la traduce è la differenza tra un arnese e un corpo estraneo.
Riferimenti:
- Amit Patel, Red Blob Games — “Introduction to A*” e “Heuristics” (redblobgames.com/pathfinding/). Euristiche Manhattan vs Chebyshev, visualizzazioni interattive.
- Bob Nystrom, Game Programming Patterns (gameprogrammingpatterns.com). Architettura software per giochi senza engine.
- Rogue Basin, “Basic BSP Dungeon Generation” (roguebasin.com). Binary Space Partitioning per mappe procedurali.
- Josh Ge, Cogmind devblog (gridsagegames.com). Generazione procedurale come strumento narrativo, AI multi-agente.
- Berlin Interpretation (2008) — definizione formale del genere roguelike, riferimento per permadeath e generazione procedurale.
- Gianni Rodari, Grammatica della fantasia (1973). Il gioco come struttura cognitiva.
- INDIRE, “Monitor sulla scuola digitale” (indire.it). Dati longitudinali sull’adozione tecnologica nelle scuole italiane 2011-2018.
<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>☠ DUNGEON v3</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Share+Tech+Mono&display=swap');
:root {
--bg: #050a05;
--green: #39ff5a;
--mid: #1a7a2a;
--dark: #0d3d14;
--dim: #0a1a0a;
--gold: #f0c040;
--red: #ff3333;
--blue: #44aaff;
--purple:#cc66ff;
--orange:#ff8833;
}
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body {
background: var(--bg);
color: var(--green);
font-family: 'Share Tech Mono', monospace;
min-height: 100dvh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
overflow: hidden;
}
body::before {
content:''; position:fixed; inset:0; pointer-events:none; z-index:10;
background: repeating-linear-gradient(0deg,
transparent, transparent 2px,
rgba(0,0,0,.15) 2px, rgba(0,0,0,.15) 4px);
}
body::after {
content:''; position:fixed; inset:0; pointer-events:none; z-index:11;
background: radial-gradient(ellipse at center, transparent 50%, rgba(0,0,0,.75) 100%);
}
#shell {
display: flex; flex-direction: column;
border: 1px solid var(--dark);
box-shadow: 0 0 30px #39ff5a22, inset 0 0 60px #00000088;
max-width: 100vw;
transition: transform 0.05s;
}
#hud {
display: flex; align-items: center; gap: .7rem;
padding: .3rem .6rem;
background: var(--dim); border-bottom: 1px solid var(--dark);
font-size: clamp(.6rem, 1.8vw, .75rem);
flex-wrap: wrap; white-space: nowrap;
transition: background 0.15s;
}
#hud.flash-red { background: #3a0a0a; }
#hud.flash-gold { background: #2a2000; }
#hud.flash-green { background: #0a2a0a; }
#hud.flash-purple{ background: #1a0a2a; }
#hud span { color: var(--mid); }
#hud b { color: var(--green); text-shadow: 0 0 6px #39ff5a88; }
.hp-val { color: var(--red)!important; text-shadow:0 0 8px #ff333888!important; }
.gd-val { color: var(--gold)!important; text-shadow:0 0 8px #f0c04088!important; }
.kill-val { color: var(--red)!important; text-shadow:0 0 8px #ff333888!important; }
.atk-val { color: var(--purple)!important; text-shadow:0 0 8px #cc66ff88!important; }
#h-msg { margin-left:auto; font-size:.75em; min-width:10ch; text-align:right; }
#hp-bar-wrap {
width:60px; height:6px;
background:#1a0a0a; border:1px solid #331111;
border-radius:2px; overflow:hidden;
}
#hp-bar {
height:100%; background:var(--red);
transition: width 0.3s, background 0.3s;
box-shadow:0 0 4px #ff333388;
}
#lore-bar {
padding: .15rem .6rem;
background: #080808; border-bottom: 1px solid #1a1a1a;
font-size: clamp(.5rem, 1.4vw, .6rem);
color: #556655;
font-style: italic;
min-height: 1.4em;
transition: color 1s;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
canvas { display:block; cursor:crosshair; }
#bar {
display:flex; padding:.25rem .6rem;
background:var(--dim); border-top:1px solid var(--dark);
font-size:clamp(.55rem,1.6vw,.65rem);
color:var(--mid); gap:.5rem; flex-wrap:wrap; align-items:center;
}
#bar kbd {
background:var(--dark); color:var(--green);
border:1px solid var(--mid); padding:0 .2em; border-radius:2px;
font-family:inherit; font-size:.9em;
}
#hint-btn {
margin-left:auto;
background:none; border:1px solid var(--gold); color:var(--gold);
font-family:inherit; font-size:clamp(.55rem,1.6vw,.65rem);
padding:.1em .6em; cursor:pointer; border-radius:3px;
}
#hint-btn:hover { background:#f0c04022; }
#dpad {
display:none;
grid-template-areas: ". u ." "l h r" ". d .";
grid-template-columns: repeat(3,1fr);
gap:4px; padding:.4rem;
background:var(--dim); border-top:1px solid var(--dark);
touch-action:none;
}
#dpad button {
background:var(--dark); border:1px solid var(--mid);
color:var(--green); font-family:inherit;
font-size:1.2rem; padding:.5rem;
cursor:pointer; border-radius:4px;
-webkit-tap-highlight-color:transparent;
user-select:none;
}
#dpad button:active { background:#0d3d14; }
#dpad .btn-hint { color:var(--gold); border-color:var(--gold); font-size:.75rem; }
@media (pointer:coarse) { #dpad { display:grid; } }
#overlay {
position:fixed; inset:0; z-index:200;
display:flex; flex-direction:column;
align-items:center; justify-content:center;
background:rgba(0,0,0,.92); gap:.8rem; text-align:center; padding:1rem;
}
#overlay.hidden { display:none; }
#overlay h1 {
font-size:clamp(1.8rem,8vw,3.5rem);
letter-spacing:.2em; animation:flicker 3s infinite;
}
#overlay .sub { color:var(--mid); font-size:.75rem; letter-spacing:.1em; }
#overlay .stat { color:var(--gold); font-size:.8rem; line-height:1.6; white-space:pre-line; }
#overlay .legacy { color:#556655; font-size:.65rem; line-height:1.5; white-space:pre-line; margin-top:.3rem; }
#overlay .tip { color:var(--mid); font-size:.65rem; max-width:34ch; line-height:1.4; }
#overlay button {
padding:.5em 2.5em; background:none;
border:1px solid var(--green); color:var(--green);
font-family:inherit; font-size:.9rem; letter-spacing:.1em;
cursor:pointer; text-shadow:0 0 6px #39ff5a88;
box-shadow:0 0 10px #39ff5a44;
}
#overlay button:hover { background:var(--dark); }
@keyframes flicker {
0%,94%,100%{opacity:1} 95%,97%{opacity:.15} 96%,98%{opacity:.7}
}
</style>
</head>
<body>
<div id="overlay">
<h1 id="ov-title" style="color:var(--green)">☠ DUNGEON</h1>
<div id="ov-sub" class="sub"></div>
<div id="ov-stat" class="stat"></div>
<div id="ov-legacy" class="legacy"></div>
<div id="ov-tip" class="tip"></div>
<button id="ov-btn">INIZIA</button>
</div>
<div id="shell">
<div id="hud">
<span>P<b id="h-lvl">1</b></span>
<span>HP <b class="hp-val" id="h-hp">30</b></span>
<div id="hp-bar-wrap"><div id="hp-bar"></div></div>
<span>ORO <b class="gd-val" id="h-gold">0</b></span>
<span>ATK <b class="atk-val" id="h-atk">10</b></span>
<span>☠<b class="kill-val" id="h-kills">0</b></span>
<div id="h-msg"></div>
</div>
<div id="lore-bar"></div>
<canvas id="cv"></canvas>
<div id="bar">
<span><kbd>↑</kbd><kbd>←</kbd><kbd>↓</kbd><kbd>→</kbd> muovi</span>
<span><kbd>H</kbd> hint <span style="color:#ff3333">(-1HP)</span></span>
<span><kbd>X</kbd> esci</span>
<span style="color:#f0c040">$ oro</span>
<span style="color:#ff3333">E F B nemici</span>
<span style="color:#44aaff">> scale</span>
<span style="color:#39ff5a">+ vita</span>
<span style="color:#cc66ff">† arma</span>
<button id="hint-btn">💡 HINT (-1HP)</button>
</div>
<div id="dpad">
<button style="grid-area:u" data-k="w">▲</button>
<button style="grid-area:l" data-k="a">◀</button>
<button style="grid-area:r" data-k="d">▶</button>
<button style="grid-area:d" data-k="s">▼</button>
<button style="grid-area:h" class="btn-hint" id="btn-hint">💡</button>
</div>
</div>
<script>
/* ═══════════════════════════════════════════════════════════════════════════ */
/* CONSTANTS */
/* ═══════════════════════════════════════════════════════════════════════════ */
var MW=50,MH=18,HP_MAX=30,VISION=6,HINT_ALERT_RADIUS=4;
var WALL='#',FLOOR='.',GOLD='$',STAIR='>',POTION='+',WEAPON='\u2020';
var ETYPES={
E:{ch:'E',col:'#ff3333',glow:'#ff333399',hpMul:1,dmgMul:1,speed:1,diag:false,miss:0.15,name:'nemico'},
F:{ch:'F',col:'#ff8833',glow:'#ff883399',hpMul:0.5,dmgMul:0.6,speed:2,diag:true,miss:0.25,name:'rapido'},
B:{ch:'B',col:'#ff2222',glow:'#ff222299',hpMul:2.5,dmgMul:1.8,speed:0.5,diag:false,miss:0.08,name:'bruto'}
};
var COLORS={};
COLORS['#']='#2a8040';COLORS['.']='#0d3d14';COLORS['$']='#f0c040';
COLORS['E']='#ff3333';COLORS['F']='#ff8833';COLORS['B']='#ff2222';
COLORS['>']='#44aaff';COLORS['@']='#39ff5a';
COLORS['+']='#39ff5a';COLORS['\u2020']='#cc66ff';COLORS['?']='#334433';
var GLOW={};
GLOW['$']='#f0c04099';GLOW['E']='#ff333399';GLOW['F']='#ff883399';
GLOW['B']='#ff222299';GLOW['>']='#44aaff99';
GLOW['@']='#39ff5a99';GLOW['+']='#39ff5a99';GLOW['\u2020']='#cc66ff99';
/* ═══════════════════════════════════════════════════════════════════════════ */
/* LORE */
/* ═══════════════════════════════════════════════════════════════════════════ */
var LORE_ENTER=[
"Le pareti trasudano umidità. Qualcosa si muove, più in basso.",
"L'eco dei tuoi passi torna deformato. Non sei solo.",
"L'aria cambia. Più fredda. Più vecchia.",
"Graffi sui muri. Qualcuno è passato prima di te.",
"Un odore ferroso. Sangue? Ruggine? Non importa.",
"Le ombre si allungano dove non dovrebbero.",
"Il silenzio qui ha una consistenza diversa.",
"Scendi ancora. Le pietre ricordano chi non è risalito.",
"Il buio non è vuoto. È pieno di cose che aspettano.",
"Ogni piano ha la sua voce. Questa mormora.",
"I muri qui sono più antichi. Costruiti da chi, e perché?",
"La polvere si muove senza vento.",
"Qualcuno ha inciso una data nel muro. Non la riconosci.",
"Il pavimento è consumato. Migliaia di passi prima dei tuoi.",
"Senti qualcosa raccogliere monete. Non sei tu."
];
var LORE_DEEP=[
"A questa profondità, la luce è un ricordo.",
"Nessuno costruisce un dungeon senza motivo. Il motivo è sotto.",
"I nemici qui hanno occhi diversi. Abituati al buio.",
"Le stanze sono più grandi. Servivano a contenere qualcosa.",
"Senti il peso dei piani sopra di te.",
"I rapidi qui si muovono in diagonale. Accerchiano.",
"I bruti custodiscono qualcosa. Non si muovono per caso."
];
var LORE_DEATH=[
"Il dungeon si richiude. Un altro nome da dimenticare.",
"Le ombre reclamano ciò che era loro.",
"Il silenzio torna. Era solo in prestito.",
"Non sei il primo. Non sarai l'ultimo."
];
var LORE_STEAL=["I nemici rubano ciò che non difendi.","Qualcuno è più veloce di te.","L'oro non aspetta."];
/* ═══════════════════════════════════════════════════════════════════════════ */
/* LEGACY */
/* ═══════════════════════════════════════════════════════════════════════════ */
var HERO_NAMES=["Aldo","Bice","Corrado","Daria","Enzo","Fulvia","Gino",
"Irma","Livio","Marta","Nino","Olga","Piero","Rita","Silvio","Tina",
"Ugo","Vera","Zeno","Alma","Bruno","Clara","Dino","Elsa","Fosco",
"Giada","Luca","Noemi","Oscar","Renata"];
var legacy=[],heroName="";
function loadLegacy(){try{var s=localStorage.getItem('dg_leg3');if(s)legacy=JSON.parse(s);}catch(e){legacy=[];}}
function saveLegacy(h){legacy.push(h);if(legacy.length>10)legacy=legacy.slice(-10);try{localStorage.setItem('dg_leg3',JSON.stringify(legacy));}catch(e){}}
function getGhosts(f){var g=[];for(var i=0;i<legacy.length;i++)if(legacy[i].floor===f)g.push(legacy[i]);return g;}
function getLegacyText(){
if(!legacy.length)return'';
var l=['\u2014 Eroi caduti \u2014'];
var s=Math.max(0,legacy.length-5);
for(var i=s;i<legacy.length;i++){var h=legacy[i];l.push(h.name+': piano '+h.floor+', '+h.kills+'\u2620, '+h.score+'pt');}
return l.join('\n');
}
/* ═══════════════════════════════════════════════════════════════════════════ */
/* CANVAS */
/* ═══════════════════════════════════════════════════════════════════════════ */
var cv=document.getElementById('cv'),ctx=cv.getContext('2d'),CW,CH;
function resize(){
var hudH=document.getElementById('hud').offsetHeight+document.getElementById('lore-bar').offsetHeight+document.getElementById('bar').offsetHeight+4;
var dpadH=window.matchMedia('(pointer:coarse)').matches?140:0;
var availW=window.innerWidth-4,availH=window.innerHeight-hudH-dpadH-8;
var byW=Math.floor(availW/MW),byH=Math.floor(availH/MH);
CW=Math.max(7,Math.min(byW,Math.round(byH/1.55)));
CH=Math.max(11,Math.round(CW*1.55));
cv.width=CW*MW;cv.height=CH*MH;
cv.style.width=cv.width+'px';cv.style.height=cv.height+'px';
ctx.textBaseline='top';setFont();
if(map)fullDraw();
}
function setFont(){ctx.font=Math.floor(CH*0.88)+"px 'Share Tech Mono',monospace";}
/* ═══════════════════════════════════════════════════════════════════════════ */
/* AUDIO */
/* ═══════════════════════════════════════════════════════════════════════════ */
var audioCtx=null,audioOk=false;
function initAudio(){if(!audioCtx)try{audioCtx=new(window.AudioContext||window.webkitAudioContext)();audioOk=true;}catch(e){audioOk=false;}}
function beep(f,d,v,t){if(!audioCtx)return;try{var o=audioCtx.createOscillator(),g=audioCtx.createGain();o.type=t||'square';o.frequency.value=f;g.gain.value=v||.08;g.gain.exponentialRampToValueAtTime(.001,audioCtx.currentTime+d);o.connect(g);g.connect(audioCtx.destination);o.start();o.stop(audioCtx.currentTime+d);}catch(e){}}
function sndStep(){beep(200,.04,.03);}
function sndGold(){beep(880,.08,.06);beep(1100,.12,.05);}
function sndHit(){beep(120,.15,.1,'sawtooth');}
function sndKill(){beep(300,.06,.07);beep(500,.1,.06);}
function sndPotion(){beep(660,.1,.06);beep(880,.15,.05);}
function sndWeapon(){beep(440,.08,.07);beep(660,.08,.06);beep(880,.12,.05);}
function sndStair(){beep(330,.1,.06);beep(440,.1,.05);beep(660,.15,.05);}
function sndDeath(){beep(200,.2,.1,'sawtooth');beep(100,.4,.08,'sawtooth');}
function sndMiss(){beep(150,.1,.06,'sawtooth');}
function sndBrute(){beep(80,.2,.12,'sawtooth');beep(60,.3,.1,'sawtooth');}
function sndAmbush(){beep(500,.06,.08);beep(700,.08,.06);beep(900,.1,.05);}
function sndSteal(){beep(400,.1,.06,'sawtooth');beep(300,.15,.05,'sawtooth');}
function sndHint(){beep(660,.05,.04);beep(440,.08,.03);}
function hudFlash(c){var h=document.getElementById('hud');h.classList.add(c);setTimeout(function(){h.classList.remove(c);},200);}
/* ═══════════════════════════════════════════════════════════════════════════ */
/* PRNG */
/* ═══════════════════════════════════════════════════════════════════════════ */
function mkRng(seed){
var s=(seed|0)||1;
return{
next:function(){s^=s<<13;s^=s>>17;s^=s<<5;return(s>>>0)/4294967296;},
int:function(a,b){return a+Math.floor(this.next()*(b-a+1));},
shuffle:function(arr){for(var i=arr.length-1;i>0;i--){var j=this.int(0,i);var t=arr[i];arr[i]=arr[j];arr[j]=t;}return arr;},
pick:function(arr){return arr[this.int(0,arr.length-1)];}
};
}
/* ═══════════════════════════════════════════════════════════════════════════ */
/* A* — supports diagonal for fast enemies */
/* ═══════════════════════════════════════════════════════════════════════════ */
function MinHeap(){this.d=[];}
MinHeap.prototype.push=function(n){this.d.push(n);this._u(this.d.length-1);};
MinHeap.prototype.pop=function(){var t=this.d[0];var l=this.d.pop();if(this.d.length>0){this.d[0]=l;this._dn(0);}return t;};
MinHeap.prototype._u=function(i){while(i>0){var p=(i-1)>>1;if(this._lt(i,p)){var t=this.d[i];this.d[i]=this.d[p];this.d[p]=t;i=p;}else break;}};
MinHeap.prototype._dn=function(i){var n=this.d.length;while(true){var b=i,l=2*i+1,r=2*i+2;if(l<n&&this._lt(l,b))b=l;if(r<n&&this._lt(r,b))b=r;if(b===i)break;var t=this.d[i];this.d[i]=this.d[b];this.d[b]=t;i=b;}};
MinHeap.prototype._lt=function(a,b){var da=this.d[a],db=this.d[b];return da.f<db.f||(da.f===db.f&&da.g>db.g);};
var NB4=[[0,-1],[0,1],[-1,0],[1,0]];
var NB8=[[0,-1],[0,1],[-1,0],[1,0],[-1,-1],[1,-1],[-1,1],[1,1]];
function astar(sx,sy,tx,ty,costMap,allowDiag){
if(sx===tx&&sy===ty)return[];
var nb=allowDiag?NB8:NB4;
var gs=[],cf=[];
for(var y=0;y<MH;y++){gs[y]=new Float32Array(MW);cf[y]=[];for(var x=0;x<MW;x++){gs[y][x]=999999;cf[y][x]=null;}}
gs[sy][sx]=0;
var op=new MinHeap();
// Chebyshev for diagonal, Manhattan for cardinal
var hfn=allowDiag?function(x,y){return Math.max(Math.abs(tx-x),Math.abs(ty-y));}
:function(x,y){return Math.abs(tx-x)+Math.abs(ty-y);};
op.push({x:sx,y:sy,g:0,f:hfn(sx,sy)});
while(op.d.length>0){
var cur=op.pop();var cx=cur.x,cy=cur.y;
if(cx===tx&&cy===ty){var path=[];var rx=tx,ry=ty;while(rx!==sx||ry!==sy){path.push([rx,ry]);var p=cf[ry][rx];rx=p[0];ry=p[1];}path.reverse();return path;}
if(cur.g>gs[cy][cx])continue;
for(var ni=0;ni<nb.length;ni++){
var nx=cx+nb[ni][0],ny=cy+nb[ni][1];
if(nx<0||nx>=MW||ny<0||ny>=MH||map[ny][nx]===WALL)continue;
// Diagonal: require both adjacent cardinal cells open (no corner-cutting through walls)
if(ni>=4){if(map[cy][nx]===WALL||map[ny][cx]===WALL)continue;}
var mc=ni>=4?1.41:1;
if(costMap)mc+=costMap[ny][nx];
var ng=gs[cy][cx]+mc;
if(ng<gs[ny][nx]){gs[ny][nx]=ng;cf[ny][nx]=[cx,cy];op.push({x:nx,y:ny,g:ng,f:ng+hfn(nx,ny)});}
}
}
return null;
}
function astarNearest(sx,sy,targets){
if(!targets.length)return null;
var gs=[],cf=[];
for(var y=0;y<MH;y++){gs[y]=new Float32Array(MW);cf[y]=[];for(var x=0;x<MW;x++){gs[y][x]=999999;cf[y][x]=null;}}
var isT=[];for(var y2=0;y2<MH;y2++)isT[y2]=new Uint8Array(MW);
var tInfo={};
for(var ti=0;ti<targets.length;ti++){var tg=targets[ti];isT[tg.y][tg.x]=1;tInfo[tg.y*MW+tg.x]=tg;}
function heur(x,y){var best=999999;for(var i=0;i<targets.length;i++){var d=Math.abs(targets[i].x-x)+Math.abs(targets[i].y-y);if(d<best)best=d;}return best;}
gs[sy][sx]=0;
var op=new MinHeap();op.push({x:sx,y:sy,g:0,f:heur(sx,sy)});
while(op.d.length>0){
var cur=op.pop();var cx=cur.x,cy=cur.y;
if(isT[cy][cx]){var path=[];var rx=cx,ry=cy;while(rx!==sx||ry!==sy){path.push([rx,ry]);var p=cf[ry][rx];rx=p[0];ry=p[1];}path.reverse();return{path:path,target:[cx,cy],targetChar:tInfo[cy*MW+cx].ch};}
if(cur.g>gs[cy][cx])continue;
for(var ni=0;ni<4;ni++){
var nx=cx+NB4[ni][0],ny=cy+NB4[ni][1];
if(nx<0||nx>=MW||ny<0||ny>=MH||map[ny][nx]===WALL)continue;
var ng=gs[cy][cx]+1;
if(ng<gs[ny][nx]){gs[ny][nx]=ng;cf[ny][nx]=[cx,cy];op.push({x:nx,y:ny,g:ng,f:ng+heur(nx,ny)});}
}
}
return null;
}
function buildEnemyCostMap(){
var cm=[];for(var y=0;y<MH;y++)cm[y]=new Float32Array(MW);
for(var ei=0;ei<enemies.length;ei++){var e=enemies[ei];if(e.hp<=0)continue;
var w=e.type==='B'?3:2;
for(var dy=-1;dy<=1;dy++)for(var dx=-1;dx<=1;dx++){var nx=e.x+dx,ny=e.y+dy;if(nx>=0&&nx<MW&&ny>=0&&ny<MH)cm[ny][nx]+=w;}}
return cm;
}
/* ═══════════════════════════════════════════════════════════════════════════ */
/* BSP MAP GENERATION */
/* ═══════════════════════════════════════════════════════════════════════════ */
function BSPNode(x,y,w,h){this.x=x;this.y=y;this.w=w;this.h=h;this.left=null;this.right=null;this.room=null;}
function bspSplit(node,rng,depth){
if(depth>5)return;
var MIN_SIZE=6;
var canH=node.h>=MIN_SIZE*2+1;
var canV=node.w>=MIN_SIZE*2+1;
if(!canH&&!canV)return;
var splitH;
if(canH&&canV)splitH=rng.next()>0.5;
else splitH=canH;
if(splitH){
var split=rng.int(MIN_SIZE,node.h-MIN_SIZE);
node.left=new BSPNode(node.x,node.y,node.w,split);
node.right=new BSPNode(node.x,node.y+split,node.w,node.h-split);
}else{
var split=rng.int(MIN_SIZE,node.w-MIN_SIZE);
node.left=new BSPNode(node.x,node.y,split,node.h);
node.right=new BSPNode(node.x+split,node.y,node.w-split,node.h);
}
bspSplit(node.left,rng,depth+1);
bspSplit(node.right,rng,depth+1);
}
function bspRooms(node,rng,m){
if(!node.left&&!node.right){
// Leaf — carve room with padding
var rw=rng.int(Math.max(3,node.w-4),node.w-2);
var rh=rng.int(Math.max(3,node.h-3),node.h-2);
var rx=node.x+rng.int(1,node.w-rw-1);
var ry=node.y+rng.int(1,node.h-rh-1);
// Clamp to map bounds
if(rx<1)rx=1;if(ry<1)ry=1;
if(rx+rw>=MW-1)rw=MW-2-rx;if(ry+rh>=MH-1)rh=MH-2-ry;
node.room={x:rx,y:ry,w:rw,h:rh};
for(var cy=ry;cy<ry+rh;cy++)for(var cx=rx;cx<rx+rw;cx++){
if(cy>0&&cy<MH-1&&cx>0&&cx<MW-1)m[cy][cx]=FLOOR;
}
return node.room;
}
var rl=node.left?bspRooms(node.left,rng,m):null;
var rr=node.right?bspRooms(node.right,rng,m):null;
// Connect the two child rooms
if(rl&&rr){
var ax=Math.floor(rl.x+rl.w/2),ay=Math.floor(rl.y+rl.h/2);
var bx=Math.floor(rr.x+rr.w/2),by=Math.floor(rr.y+rr.h/2);
// L-corridor
if(rng.next()>0.5){
for(var x=Math.min(ax,bx);x<=Math.max(ax,bx);x++)if(m[ay]&&x>0&&x<MW-1)m[ay][x]=FLOOR;
for(var y=Math.min(ay,by);y<=Math.max(ay,by);y++)if(m[y]&&bx>0&&bx<MW-1)m[y][bx]=FLOOR;
}else{
for(var y=Math.min(ay,by);y<=Math.max(ay,by);y++)if(m[y]&&ax>0&&ax<MW-1)m[y][ax]=FLOOR;
for(var x=Math.min(ax,bx);x<=Math.max(ax,bx);x++)if(m[by]&&x>0&&x<MW-1)m[by][x]=FLOOR;
}
}
return rl||rr;
}
function bspCollectRooms(node,rooms){
if(node.room)rooms.push(node.room);
if(node.left)bspCollectRooms(node.left,rooms);
if(node.right)bspCollectRooms(node.right,rooms);
}
function genMap(seed,floor){
var rng=mkRng(seed);var m=[];
for(var r=0;r<MH;r++){m[r]=[];for(var c=0;c<MW;c++)m[r][c]=WALL;}
// BSP generation
var root=new BSPNode(1,1,MW-2,MH-2);
bspSplit(root,rng,0);
bspRooms(root,rng,m);
// Collect rooms for item placement
var rooms=[];bspCollectRooms(root,rooms);
if(!rooms.length){
// Fallback: carve a big room
rooms.push({x:3,y:3,w:MW-6,h:MH-6});
for(var cy=3;cy<MH-3;cy++)for(var cx=3;cx<MW-3;cx++)m[cy][cx]=FLOOR;
}
// Extra random corridor for loops
if(rooms.length>2){
var ra=rng.pick(rooms),rb=rng.pick(rooms);
if(ra!==rb){
var ax=Math.floor(ra.x+ra.w/2),ay=Math.floor(ra.y+ra.h/2);
var bx=Math.floor(rb.x+rb.w/2),by=Math.floor(rb.y+rb.h/2);
for(var y=Math.min(ay,by);y<=Math.max(ay,by);y++)if(m[y])m[y][ax]=FLOOR;
for(var x=Math.min(ax,bx);x<=Math.max(ax,bx);x++)if(m[by])m[by][x]=FLOOR;
}
}
/* ─── Scaled items ─── */
var nGold=Math.max(2,8-Math.floor(floor*0.6));
var nEnemy=Math.min(15,4+Math.floor(floor*1.0));
var nPotion=Math.max(1,3-Math.floor(floor/3));
var nWeapon=(floor>=9&&floor%2===0)?1:(floor%3===0)?1:0;
var pool=[];
for(var y=1;y<MH-1;y++)for(var x=1;x<MW-1;x++){
if(m[y][x]===FLOOR&&!(x===rooms[0].x+1&&y===rooms[0].y+1))pool.push([x,y]);
}
rng.shuffle(pool);
var idx=0;
for(var i=0;i<nGold&&idx<pool.length;i++,idx++)m[pool[idx][1]][pool[idx][0]]=GOLD;
m._enemyTypes=[];
for(var i=0;i<nEnemy&&idx<pool.length;i++,idx++){
var etype='E';
if(floor>=3){var r=rng.next();if(floor>=6&&r<0.2)etype='B';else if(r<0.35)etype='F';}
m[pool[idx][1]][pool[idx][0]]=etype;
m._enemyTypes.push({x:pool[idx][0],y:pool[idx][1],type:etype});
}
for(var i=0;i<nPotion&&idx<pool.length;i++,idx++)m[pool[idx][1]][pool[idx][0]]=POTION;
for(var i=0;i<nWeapon&&idx<pool.length;i++,idx++)m[pool[idx][1]][pool[idx][0]]=WEAPON;
if(idx<pool.length)m[pool[idx][1]][pool[idx][0]]=STAIR;
// Ghosts
var ghosts=getGhosts(floor);
for(var gi=0;gi<ghosts.length&&idx+1<pool.length;gi++){
idx++;m[pool[idx][1]][pool[idx][0]]='?';
m._ghostName=ghosts[gi].name;
}
m._spawnX=rooms[0].x+1;m._spawnY=rooms[0].y+1;
return m;
}
/* ═══════════════════════════════════════════════════════════════════════════ */
/* ENEMY SYSTEM — typed, with item pickup */
/* ═══════════════════════════════════════════════════════════════════════════ */
var enemies=[];
function spawnEnemies(){
enemies=[];
if(!map._enemyTypes)return;
for(var i=0;i<map._enemyTypes.length;i++){
var et=map._enemyTypes[i];
var info=ETYPES[et.type]||ETYPES.E;
var ehp=Math.max(1,Math.round((2+lvl*0.7)*info.hpMul));
enemies.push({x:et.x,y:et.y,hp:ehp,maxHp:ehp,type:et.type,turnParity:0,dmgBonus:0,alerted:false});
}
}
function findEnemy(x,y){
for(var i=0;i<enemies.length;i++)if(enemies[i].x===x&&enemies[i].y===y&&enemies[i].hp>0)return enemies[i];
return null;
}
/* Flanking: count walls around target enemy */
function getFlankBonus(ex,ey){
var walls=0;
for(var ni=0;ni<4;ni++){
var nx=ex+NB4[ni][0],ny=ey+NB4[ni][1];
if(nx<0||nx>=MW||ny<0||ny>=MH||map[ny][nx]===WALL)walls++;
}
return walls>=2?1.5:1.0; // 50% bonus if cornered in corridor
}
function enemyTurn(){
var costMap=buildEnemyCostMap();
globalTurnCount++;
// Track stolen items for single message
var stolenGold=0,stolenWeapon=false,stolenPotion=false;
for(var i=enemies.length-1;i>=0;i--){
var e=enemies[i];if(e.hp<=0)continue;
var info=ETYPES[e.type]||ETYPES.E;
// Speed: brutes every other turn
if(info.speed<1&&globalTurnCount%2!==0)continue;
var moves=info.speed>=2?2:1;
for(var mi=0;mi<moves;mi++){
var dist=Math.abs(e.x-px)+Math.abs(e.y-py);
// Alerted enemies pursue from further away
var pursueRange=e.alerted?VISION+6:VISION+2;
if(dist>pursueRange)continue;
if(dist<=1){
var baseDmg=3+Math.floor(lvl*0.6);
var dmg=Math.max(1,Math.round(baseDmg*info.dmgMul))+e.dmgBonus;
hp-=dmg;
var label=info.name.charAt(0).toUpperCase()+info.name.slice(1);
showMsg(label+' attacca! -'+dmg+' HP','#ff3333');
if(e.type==='B')sndBrute();else sndHit();
screenShake();hudFlash('flash-red');updateHUD();
if(hp<=0){alive=false;sndDeath();showOverlay(false);return;}
break;
}
var path=astar(e.x,e.y,px,py,costMap,info.diag);
if(path&&path.length>0){
var nx=path[0][0],ny=path[0][1];
if(nx===px&&ny===py)continue;
var blocked=false;
for(var j=0;j<enemies.length;j++){if(j!==i&&enemies[j].hp>0&&enemies[j].x===nx&&enemies[j].y===ny){blocked=true;break;}}
if(blocked)continue;
var cellAt=map[ny][nx];
// ENEMY ITEM PICKUP
if(cellAt===GOLD){
stolenGold+=8+Math.floor(lvl*2.5);
map[e.y][e.x]=FLOOR;e.x=nx;e.y=ny;map[e.y][e.x]=e.type;
} else if(cellAt===WEAPON){
e.dmgBonus+=3;stolenWeapon=true;
map[e.y][e.x]=FLOOR;e.x=nx;e.y=ny;map[e.y][e.x]=e.type;
} else if(cellAt===POTION){
e.hp=e.maxHp;stolenPotion=true;
map[e.y][e.x]=FLOOR;e.x=nx;e.y=ny;map[e.y][e.x]=e.type;
} else if(cellAt===FLOOR||cellAt==='?'){
map[e.y][e.x]=FLOOR;e.x=nx;e.y=ny;map[e.y][e.x]=e.type;
}
}
}
// Decay alert after 1 move
if(e.alerted)e.alerted=false;
}
// Show steal messages
if(stolenGold>0){showMsg('Nemici rubano oro! -'+stolenGold,'#ff8833');sndSteal();}
else if(stolenWeapon){showMsg('Un nemico ha preso un\'arma!','#cc66ff');sndSteal();}
else if(stolenPotion){showMsg('Un nemico si cura!','#39ff5a');sndSteal();}
}
function alertNearbyEnemies(){
for(var i=0;i<enemies.length;i++){
var e=enemies[i];if(e.hp<=0)continue;
var dist=Math.abs(e.x-px)+Math.abs(e.y-py);
if(dist<=HINT_ALERT_RADIUS)e.alerted=true;
}
}
/* ═══════════════════════════════════════════════════════════════════════════ */
/* FOG */
/* ═══════════════════════════════════════════════════════════════════════════ */
var visible,explored;
function resetFog(){visible=[];explored=[];for(var y=0;y<MH;y++){visible[y]=new Uint8Array(MW);explored[y]=new Uint8Array(MW);}}
function updateFog(){
for(var y=0;y<MH;y++)for(var x=0;x<MW;x++)visible[y][x]=0;
var q=[[px,py]];visible[py][px]=1;explored[py][px]=1;
var head=0;
while(head<q.length){
var cx=q[head][0],cy=q[head][1];head++;
var dist=Math.abs(cx-px)+Math.abs(cy-py);
if(dist>=VISION)continue;
for(var ni=0;ni<8;ni++){
var nx=cx+NB8[ni][0],ny=cy+NB8[ni][1];
if(nx<0||nx>=MW||ny<0||ny>=MH||visible[ny][nx])continue;
visible[ny][nx]=1;explored[ny][nx]=1;
if(map[ny][nx]!==WALL)q.push([nx,ny]);
}
}
}
/* ═══════════════════════════════════════════════════════════════════════════ */
/* HINT — costs 1 HP, alerts enemies, learns preferences */
/* ═══════════════════════════════════════════════════════════════════════════ */
var hintCells=[],hintFade=null,hintAnim=null,hintMode=0;
var hintHistory=[],lastHintTarget=null;
function getHintWeights(){
var w={};w[GOLD]=1;w[STAIR]=1;w[POTION]=1;w[WEAPON]=1;
for(var i=0;i<hintHistory.length;i++){var h=hintHistory[i];if(h.followed)w[h.type]=(w[h.type]||1)+0.5;}
return w;
}
function checkHintFollowed(){
if(!lastHintTarget)return;
var dist=Math.abs(px-lastHintTarget.x)+Math.abs(py-lastHintTarget.y);
var followed=(dist<lastHintTarget.startDist-1);
hintHistory.push({type:lastHintTarget.ch,followed:followed});
if(hintHistory.length>20)hintHistory=hintHistory.slice(-20);
lastHintTarget=null;
}
function doHint(){
if(!alive||!map)return;
clearHint();checkHintFollowed();
// COST: 1 HP
if(hp<=1){showMsg('Troppo debole per concentrarsi!','#ff3333');return;}
hp-=1;
sndHint();hudFlash('flash-purple');
updateHUD();
// ALERT nearby enemies
alertNearbyEnemies();
var objectives=[];
for(var y=0;y<MH;y++)for(var x=0;x<MW;x++){
var ch=map[y][x];
if(ch===STAIR||ch===GOLD||ch===POTION||ch===WEAPON)objectives.push({x:x,y:y,ch:ch});
}
if(!objectives.length){showMsg('Nessun obiettivo!','#ff3333');return;}
var prio;
if(hp/hpMax<0.35){prio=[POTION,STAIR,GOLD,WEAPON];}
else{
var weights=getHintWeights();
var types=[GOLD,STAIR,POTION,WEAPON];
types.sort(function(a,b){return(weights[b]||1)-(weights[a]||1);});
var rotated=[];
for(var ti=0;ti<types.length;ti++)rotated.push(types[(ti+hintMode)%types.length]);
prio=rotated;hintMode++;
}
var result=null;
for(var pi=0;pi<prio.length;pi++){
var targets=[];
for(var oi=0;oi<objectives.length;oi++)if(objectives[oi].ch===prio[pi])targets.push(objectives[oi]);
if(!targets.length)continue;
result=astarNearest(px,py,targets);if(result)break;
}
if(!result)result=astarNearest(px,py,objectives);
if(!result){showMsg('Nessun percorso!','#ff3333');return;}
hintCells=result.path;
lastHintTarget={x:result.target[0],y:result.target[1],ch:result.targetChar,
startDist:Math.abs(px-result.target[0])+Math.abs(py-result.target[1])};
var names={};names[GOLD]='ORO';names[STAIR]='SCALE';names[POTION]='POZIONE';names[WEAPON]='ARMA';
showMsg('\u2192 '+(names[result.targetChar]||'?')+' ('+result.path.length+') \u26a0nemici allertati','#f0c040');
var revealIdx=0,BATCH=3;setFont();
function revealStep(){
for(var b=0;b<BATCH&&revealIdx<hintCells.length;b++,revealIdx++){
var hx=hintCells[revealIdx][0],hy=hintCells[revealIdx][1];
drawHintDot(hx,hy,revealIdx,hintCells.length,revealIdx===hintCells.length-1);
}
if(revealIdx<hintCells.length)hintAnim=requestAnimationFrame(revealStep);
}
hintAnim=requestAnimationFrame(revealStep);
hintFade=setTimeout(clearHint,4500);
}
function drawHintDot(x,y,idx,total,isEnd){
var px2=x*CW,py2=y*CH;
var t=total>1?idx/(total-1):0;
var r=Math.round(240-t*40),g=Math.round(192-t*60),b=Math.round(64+t*130);
var col='rgb('+r+','+g+','+b+')';
ctx.fillStyle=isEnd?'#1a2a3a':'#2a1a00';
ctx.fillRect(px2,py2,CW,CH);
ctx.shadowColor=col;ctx.shadowBlur=isEnd?12:6;
ctx.fillStyle=col;
ctx.fillText(isEnd?'\u25C9':'\u00B7',px2+1,py2+1);
ctx.shadowBlur=0;ctx.shadowColor='transparent';
}
function clearHint(){
if(hintFade){clearTimeout(hintFade);hintFade=null;}
if(hintAnim){cancelAnimationFrame(hintAnim);hintAnim=null;}
if(!map)return;setFont();
for(var i=0;i<hintCells.length;i++){var hx=hintCells[i][0],hy=hintCells[i][1];drawCellFog(hx,hy);}
hintCells=[];
}
/* ═══════════════════════════════════════════════════════════════════════════ */
/* RENDER */
/* ═══════════════════════════════════════════════════════════════════════════ */
function drawCellFog(x,y){
var px2=x*CW,py2=y*CH;
var isPlayer=(x===px&&y===py);
var ch=isPlayer?'@':map[y][x];
if(visible[y][x]){
ctx.fillStyle='#050a05';ctx.fillRect(px2,py2,CW,CH);
var color=COLORS[ch]||'#39ff5a';
// Armed enemies glow purple
if((ch==='E'||ch==='F'||ch==='B')){
var eObj=findEnemy(x,y);
if(eObj&&eObj.dmgBonus>0){color='#dd44ff';}
}
ctx.fillStyle=color;
if(GLOW[ch]){ctx.shadowColor=GLOW[ch];ctx.shadowBlur=(ch==='@')?10:(ch==='B')?12:6;}
else if(ch===WALL){ctx.shadowColor='#2a804066';ctx.shadowBlur=4;}
else if(ch==='?'){ctx.shadowColor='#33443366';ctx.shadowBlur=3;}
else{ctx.shadowColor='transparent';ctx.shadowBlur=0;}
ctx.fillText(ch,px2+1,py2+1);
ctx.shadowBlur=0;ctx.shadowColor='transparent';
} else if(explored[y][x]){
ctx.fillStyle='#081008';ctx.fillRect(px2,py2,CW,CH);
if(ch===WALL){ctx.fillStyle='#1a6030';ctx.fillText('#',px2+1,py2+1);}
else if(ch===STAIR){ctx.fillStyle='#2255aa';ctx.fillText('>',px2+1,py2+1);}
else if(ch==='?'){ctx.fillStyle='#223322';ctx.fillText('?',px2+1,py2+1);}
else if(ch===FLOOR){ctx.fillStyle='#0c2a10';ctx.fillText('.',px2+1,py2+1);}
} else {
if(map[y][x]===WALL){ctx.fillStyle='#050a05';ctx.fillRect(px2,py2,CW,CH);ctx.fillStyle='#104020';ctx.fillText('#',px2+1,py2+1);}
else{ctx.fillStyle='#030603';ctx.fillRect(px2,py2,CW,CH);}
}
}
function fullDraw(){
updateFog();ctx.shadowBlur=0;ctx.shadowColor='transparent';
ctx.fillStyle='#030603';ctx.fillRect(0,0,cv.width,cv.height);
setFont();
for(var y=0;y<MH;y++)for(var x=0;x<MW;x++)drawCellFog(x,y);
}
function screenShake(){
var sh=document.getElementById('shell');
sh.style.transform='translate('+(Math.random()*6-3)+'px,'+(Math.random()*6-3)+'px)';
setTimeout(function(){sh.style.transform='none';},80);
}
/* ═══════════════════════════════════════════════════════════════════════════ */
/* LORE BAR */
/* ═══════════════════════════════════════════════════════════════════════════ */
function showLore(t){var el=document.getElementById('lore-bar');el.textContent=t;el.style.color='#889988';setTimeout(function(){el.style.color='#445544';},3000);}
function getLoreForFloor(f,rng){return f>=8?rng.pick(LORE_DEEP):rng.pick(LORE_ENTER);}
/* ═══════════════════════════════════════════════════════════════════════════ */
/* HUD */
/* ═══════════════════════════════════════════════════════════════════════════ */
var msgTimer;
function updateHUD(){
document.getElementById('h-lvl').textContent=lvl;
document.getElementById('h-hp').textContent=Math.max(0,hp)+'/'+hpMax;
document.getElementById('h-gold').textContent=gold;
document.getElementById('h-atk').textContent=atkPow;
document.getElementById('h-kills').textContent=kills;
var pct=Math.max(0,hp/hpMax*100);
var bar=document.getElementById('hp-bar');
bar.style.width=pct+'%';
bar.style.background=pct>50?'#39ff5a':pct>25?'#f0c040':'#ff3333';
}
function showMsg(t,c){
var el=document.getElementById('h-msg');el.textContent=t;el.style.color=c||'#f0c040';
clearTimeout(msgTimer);msgTimer=setTimeout(function(){el.textContent='';},3000);
}
/* ═══════════════════════════════════════════════════════════════════════════ */
/* STATE */
/* ═══════════════════════════════════════════════════════════════════════════ */
var map,px,py,hp,hpMax,gold,kills,atkPow,lvl,alive,runSeed,turns,globalTurnCount;
function startGame(){
initAudio();loadLegacy();
hpMax=HP_MAX;hp=hpMax;gold=0;kills=0;atkPow=10;lvl=1;alive=true;turns=0;
globalTurnCount=0;hintMode=0;hintHistory=[];lastHintTarget=null;
runSeed=Math.floor(Math.random()*1000000);
heroName=mkRng(runSeed).pick(HERO_NAMES);
loadLevel();hideOverlay();
showLore(heroName+' scende nel dungeon.');
}
function loadLevel(){
hintCells=[];if(hintFade)clearTimeout(hintFade);if(hintAnim)cancelAnimationFrame(hintAnim);
resetFog();
map=genMap(runSeed+lvl*997+13,lvl);
px=map._spawnX||2;py=map._spawnY||2;spawnEnemies();fullDraw();updateHUD();
if(lvl>1){var lrng=mkRng(runSeed+lvl*31);showLore('Piano '+lvl+'. '+getLoreForFloor(lvl,lrng));}
}
/* ═══════════════════════════════════════════════════════════════════════════ */
/* GAME LOGIC */
/* ═══════════════════════════════════════════════════════════════════════════ */
var DIRS={w:[0,-1],a:[-1,0],s:[0,1],d:[1,0]};
function step(k){
if(!alive)return;
if(k==='x'){alive=false;showOverlay(false);return;}
if(k==='h'){doHint();return;}
if(!DIRS[k])return;
clearHint();checkHintFollowed();
var dx=DIRS[k][0],dy=DIRS[k][1];
var nx=px+dx,ny=py+dy;
if(nx<0||nx>=MW||ny<0||ny>=MH||map[ny][nx]===WALL)return;
var cell=map[ny][nx];
turns++;
/* ─── COMBAT with flanking ─── */
if(cell==='E'||cell==='F'||cell==='B'){
var eObj=findEnemy(nx,ny);
if(eObj){
var info=ETYPES[eObj.type]||ETYPES.E;
if(Math.random()<info.miss){
sndMiss();showMsg('Mancato!','#ff3333');
} else {
var flank=getFlankBonus(nx,ny);
var dmg=Math.round(atkPow*flank);
var ambush=flank>1;
eObj.hp-=dmg;
if(eObj.hp<=0){
map[ny][nx]=FLOOR;kills++;
var reward=Math.round((5+lvl*1.5)*(eObj.type==='B'?2.5:eObj.type==='F'?0.8:1));
gold+=reward;sndKill();
if(ambush)sndAmbush();
hudFlash('flash-gold');
showMsg((ambush?'Imboscata! ':'')+' \u2694 '+info.name+' +'+reward+' oro','#ff3333');
} else {
sndHit();
showMsg((ambush?'Imboscata! ':'')+info.name+' HP:'+eObj.hp+'/'+eObj.maxHp,'#ff3333');
}
}
} else {
map[ny][nx]=FLOOR;kills++;gold+=5;sndKill();showMsg('\u2694 +5 oro','#ff3333');
}
screenShake();hudFlash('flash-red');
enemyTurn();updateHUD();fullDraw();
if(hp<=0){alive=false;sndDeath();showOverlay(false);}
return;
}
/* ─── MOVEMENT ─── */
px=nx;py=ny;sndStep();
if(cell===GOLD){var amt=8+Math.floor(lvl*2.5);gold+=amt;map[ny][nx]=FLOOR;showMsg('Oro! +'+amt,'#f0c040');sndGold();hudFlash('flash-gold');}
if(cell===POTION){var heal=12+Math.floor(lvl*1.5);hp=Math.min(hp+heal,hpMax);map[ny][nx]=FLOOR;showMsg('Pozione! +'+heal+' HP','#39ff5a');sndPotion();hudFlash('flash-green');}
if(cell===WEAPON){var bonus=3+Math.floor(lvl/2);atkPow+=bonus;map[ny][nx]=FLOOR;showMsg('\u2020 ATK +'+bonus+'! Ora '+atkPow,'#cc66ff');sndWeapon();hudFlash('flash-purple');}
if(cell==='?'){map[ny][nx]=FLOOR;showMsg('Ombra di '+(map._ghostName||'ignoto')+'...','#556655');showLore('Qui cadde '+(map._ghostName||'qualcuno')+'. Le pietre ricordano.');}
if(cell===STAIR){
lvl++;hp=Math.min(hp+3,hpMax);
if(lvl%3===0){hpMax+=5;hp=Math.min(hp+5,hpMax);}
showMsg('Piano '+lvl+'!','#44aaff');sndStair();loadLevel();return;
}
enemyTurn();updateHUD();fullDraw();
if(hp<=0){alive=false;sndDeath();showOverlay(false);}
}
/* ═══════════════════════════════════════════════════════════════════════════ */
/* INPUT */
/* ═══════════════════════════════════════════════════════════════════════════ */
var KEY_MAP={'w':'w','a':'a','s':'s','d':'d','arrowup':'w','arrowleft':'a','arrowdown':'s','arrowright':'d','h':'h','x':'x',' ':'h'};
document.addEventListener('keydown',function(e){var k=KEY_MAP[e.key.toLowerCase()];if(k){e.preventDefault();step(k);}});
document.getElementById('hint-btn').addEventListener('click',function(e){e.preventDefault();doHint();});
function mobBind(id,fn){document.getElementById(id).addEventListener('pointerdown',function(e){e.preventDefault();fn();});}
mobBind('btn-hint',doHint);
var dpadBtns=document.querySelectorAll('#dpad button[data-k]');
for(var bi=0;bi<dpadBtns.length;bi++){(function(btn){btn.addEventListener('pointerdown',function(e){e.preventDefault();step(btn.getAttribute('data-k'));});})(dpadBtns[bi]);}
/* ═══════════════════════════════════════════════════════════════════════════ */
/* OVERLAY */
/* ═══════════════════════════════════════════════════════════════════════════ */
var TIPS=[
'Cammina su un nemico per attaccarlo',
'I nemici usano A* per inseguirti',
'I rapidi (F) si muovono in diagonale!',
'I bruti (B) colpiscono forte ma sono lenti',
'H: hint A* — costa 1 HP e allerta i nemici vicini',
'Ogni 3 piani il tuo HP massimo aumenta',
'L\'hint impara le tue preferenze nel tempo',
'Attacca nemici in corridoio: bonus imboscata +50%',
'I muri sono sempre visibili, i nemici no',
'I nemici possono rubare oro e armi!',
'I ? sono ombre di eroi caduti prima di te',
'Mappe generate con BSP: ogni partita è diversa'
];
function showOverlay(intro){
document.getElementById('overlay').classList.remove('hidden');
var legEl=document.getElementById('ov-legacy');
if(intro){
document.getElementById('ov-title').textContent='\u2620 DUNGEON';
document.getElementById('ov-title').style.color='var(--green)';
document.getElementById('ov-sub').textContent='A* \u00B7 BSP \u00B7 FOG OF WAR \u00B7 PERMADEATH';
document.getElementById('ov-stat').textContent='';
legEl.textContent=getLegacyText();
document.getElementById('ov-tip').textContent=TIPS[Math.floor(Math.random()*TIPS.length)];
document.getElementById('ov-btn').textContent='INIZIA';
} else {
var dead=hp<=0;
document.getElementById('ov-title').textContent=dead?'\u2620 SEI MORTO':'RITIRATO';
document.getElementById('ov-title').style.color=dead?'var(--red)':'var(--mid)';
document.getElementById('ov-sub').textContent=heroName+' \u00B7 Piano '+lvl+' \u00B7 '+turns+' turni';
var score=gold+kills*15+lvl*50;
document.getElementById('ov-stat').textContent='Oro: '+gold+' \u00B7 Uccisioni: '+kills+' \u00B7 Piani: '+lvl+'\nPUNTEGGIO: '+score;
if(dead){
saveLegacy({name:heroName,floor:lvl,kills:kills,gold:gold,score:score});
legEl.textContent=LORE_DEATH[Math.floor(Math.random()*LORE_DEATH.length)]+'\n\n'+getLegacyText();
}else{legEl.textContent=getLegacyText();}
document.getElementById('ov-tip').textContent=dead?TIPS[Math.floor(Math.random()*TIPS.length)]:'Sei sopravvissuto!';
document.getElementById('ov-btn').textContent='RIPROVA';
}
}
function hideOverlay(){document.getElementById('overlay').classList.add('hidden');}
document.getElementById('ov-btn').addEventListener('click',startGame);
/* ═══════════════════════════════════════════════════════════════════════════ */
loadLegacy();
document.fonts.ready.then(resize);
window.addEventListener('resize',resize);
resize();showOverlay(true);
</script>
</body>
</html>

Leave a comment