Mappa del paese per spostarsi veloci; Codex nei crediti

Il pulsante «Mappa» (o il tasto P) apre la pianta del paese a gradini: ci sono i posti già visti,
le strade sono le uscite fra loro, un clic porta lì (game.travel, mai durante un dialogo o una
battuta). Ogni scena ha la sua posizione in `map`; un posto nuovo entra con una riga. Test: posti
solo visti, strade, viaggio rifiutato in un dialogo; nel browser dal Trebbio alla Piazza.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UdeKd62opy9PKgpQXtVctF
This commit is contained in:
2026-10-23 15:59:29 +02:00
co-authored by Claude Opus 5.5
parent e743315f0f
commit ce4da39c8d
8 changed files with 123 additions and 1 deletions
+23
View File
@@ -255,6 +255,28 @@ export class Game {
else throw new Error('Effetto sconosciuto: ' + JSON.stringify(e));
}
// La mappa: i posti già visti, con la posizione sulla pianta, e le strade che li collegano (uscite fra due posti visti).
mapPlaces() {
return Object.entries(this.story.scenes)
.filter(([id, sc]) => sc.map && this.state.flags['visto:' + id])
.map(([id, sc]) => ({ id, name: sc.name, at: sc.map, here: id === this.state.scene }));
}
mapRoads() {
const seen = new Set(this.mapPlaces().map(p => p.id)), roads = new Map();
for (const id of seen)
for (const h of this.story.scenes[id].hotspots ?? [])
if (h.exit && seen.has(h.exit.scene) && h.exit.scene !== id) roads.set([id, h.exit.scene].sort().join('|'), [id, h.exit.scene].sort());
return [...roads.values()];
}
// Viaggio dalla mappa: solo verso un posto già visto, e non mentre si parla, si cammina o si sceglie in un dialogo.
travel(scene) {
const sc = this.story.scenes[scene];
if (!sc?.map || !this.state.flags['visto:' + scene] || scene === this.state.scene) return false;
if (this.busy || this.walking || this.dialogue || this.state.ended) return false;
this.go(scene, sc.entry);
return true;
}
go(scene, at, face) {
if (!this.story.scenes[scene]) throw new Error('Scena sconosciuta: ' + scene);
this.state.scene = scene;
@@ -263,6 +285,7 @@ export class Game {
this.path = [];
this.walking = false;
this.back = false;
this.state.flags['visto:' + scene] = true; // per la mappa
this.queue.unshift(...(this.scene.enter ?? []));
this.onSave(this.snapshot());
}
+29
View File
@@ -78,6 +78,31 @@ $('credits-list').replaceChildren(...(story.credits ?? []).flatMap(({ role, name
for (const b of document.querySelectorAll('.open-credits')) b.addEventListener('click', () => { $('credits').hidden = false; });
$('close-credits').addEventListener('click', () => { $('credits').hidden = true; });
// --- Mappa: i posti visti e le strade fra loro; un clic su un posto ci porta (game.travel).
function openMap() {
if (!running || game.dialogue) return;
const board = $('map-board'), svg = $('map-roads'), places = game.mapPlaces(), at = Object.fromEntries(places.map(p => [p.id, p.at]));
svg.replaceChildren(...game.mapRoads().map(([a, b]) => {
const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
[['x1', at[a][0]], ['y1', at[a][1]], ['x2', at[b][0]], ['y2', at[b][1]]].forEach(([k, v]) => line.setAttribute(k, v));
return line;
}));
board.replaceChildren(svg, ...places.map(p => {
const b = document.createElement('button');
b.type = 'button';
b.dataset.scene = p.id;
b.textContent = p.name;
b.classList.toggle('here', p.here);
b.style.left = p.at[0] + '%';
b.style.top = p.at[1] + '%';
b.addEventListener('click', () => { if (game.travel(p.id)) $('map').hidden = true; });
return b;
}));
$('map').hidden = false;
}
$('map-button').addEventListener('click', openMap);
$('close-map').addEventListener('click', () => { $('map').hidden = true; });
function showTitle() {
running = false;
const saved = storage && readSave(storage);
@@ -170,6 +195,8 @@ function updateChoices() {
window.addEventListener('keydown', e => {
if (e.target === $('volume')) return;
if (!$('credits').hidden) { if (e.key === 'Escape') $('credits').hidden = true; return; }
if (!$('map').hidden) { if (e.key === 'Escape' || e.key === 'p' || e.key === 'P') $('map').hidden = true; return; }
if ((e.key === 'p' || e.key === 'P') && running) { openMap(); return; }
if (!$('intro').hidden && [' ', 'Enter'].includes(e.key)) { e.preventDefault(); nextIntro(); return; }
if (e.key === 'm' || e.key === 'M') { music.toggle(); saveAudio(); showSound(); return; }
if (!running) return;
@@ -204,6 +231,8 @@ function updateUi() {
updateInventory();
updateChoices();
updateSpeech();
// Il pulsante della mappa c'è solo in partita, fuori dai dialoghi, e quando c'è almeno un altro posto dove andare.
$('map-button').hidden = !running || !!game.dialogue || game.mapPlaces().length < 2;
if (game.state.ended && !game.speech && !game.queue.length) {
running = false;
$('ending').hidden = false;
+14 -1
View File
@@ -4,6 +4,8 @@
export const story = {
title: 'La Leggenda del Regolo',
// La mappa (pulsante «Mappa»): ogni scena con `map: [x, y]` (percentuali della pianta) ci compare quando è
// stata visitata; le strade sono le uscite fra scene. Un posto nuovo entra con la sua riga `map`.
start: { scene: 'piazza', at: [250, 320] },
// Le righe dell'introduzione, sopra il panorama del paese, prima della prima scena.
intro: [
@@ -23,7 +25,7 @@ export const story = {
{ role: 'Musica', names: ['Andrea Manzi, con Suno'] },
{ role: 'Sfondi', names: ['dalle fotografie di San Piero di Andrea'] },
{ role: 'Voci', names: ['XTTS-v2 di Coqui'] },
{ role: 'Programmazione', names: ['Andrea Manzi, con Claude'] },
{ role: 'Programmazione', names: ['Andrea Manzi, con Claude e Codex'] },
],
characters: {
andrea: { name: 'Andrea', color: '#ffffff', sprite: 'assets/characters/andrea.json' },
@@ -59,6 +61,7 @@ export const story = {
scenes: {
piazza: {
name: 'Piazza Colonna',
map: [16, 70],
background: 'assets/scenes/piazza-colonna.png',
walk: [[84, 244], [330, 238], [345, 230], [362, 238], [462, 246], [466, 276], [544, 276], [548, 248], [588, 250], [600, 290], [640, 300], [640, 360], [0, 360], [0, 330], [84, 318]],
depth: [[236, 0.5], [360, 1.35]],
@@ -140,6 +143,7 @@ export const story = {
},
fortezza: {
name: 'Sotto le mura della Fortezza',
map: [30, 15],
background: 'assets/scenes/fortezza-esterno.png',
walk: [[110, 284], [300, 280], [330, 272], [348, 274], [400, 292], [442, 294], [640, 352], [640, 360], [0, 360], [0, 315], [60, 310], [105, 295]],
depth: [[272, 0.55], [360, 1.15]],
@@ -190,6 +194,7 @@ export const story = {
// Livello 0: la Pieve vista dalla salita, con la salita per Villa Adami alle spalle.
pieve: {
name: 'La Pieve',
map: [46, 70],
background: 'assets/scenes/pieve-dalla-salita.png',
// La rampa in primo piano e, dietro il muretto, il sagrato (ci si arriva da sinistra).
walk: [[0, 270], [80, 266], [86, 252], [100, 222], [270, 215], [300, 224], [480, 234], [560, 242], [600, 252], [610, 268], [276, 240], [276, 270], [292, 278], [640, 352], [640, 360], [0, 360]],
@@ -230,6 +235,7 @@ export const story = {
// Il cortile dell'oratorio, dietro la porta della canonica: palco, tettoia, tavolate della festa.
oratorio: {
name: 'Oratorio della Pieve',
map: [74, 70],
background: 'assets/scenes/oratorio.png',
walk: [[0, 236], [300, 226], [480, 222], [500, 232], [640, 254], [640, 360], [0, 360]],
depth: [[222, 0.5], [360, 1.15]],
@@ -279,6 +285,7 @@ export const story = {
// Via de' Medici: dalla Piazza sale verso Villa Adami (la Piazza è alle spalle, in fondo si va su).
via_de_medici: {
name: 'Via de\' Medici',
map: [14, 52],
background: 'assets/scenes/via-de-medici.png',
walk: [[0, 360], [0, 336], [160, 290], [262, 240], [288, 212], [312, 208], [340, 222], [470, 300], [545, 360]],
depth: [[208, 0.32], [360, 1.25]],
@@ -302,6 +309,7 @@ export const story = {
// L'ingresso del campeggio: dalla Piazza a diritto; poco dopo, a destra, si sale al Bosco ai Frati.
campeggio: {
name: 'Ingresso del campeggio',
map: [16, 88],
background: 'assets/scenes/campeggio.png',
walk: [[150, 360], [165, 262], [420, 266], [640, 286], [640, 360]],
depth: [[262, 0.6], [360, 1.15]],
@@ -332,6 +340,7 @@ export const story = {
// Il convento del Bosco ai Frati, nel bosco sopra il campeggio.
bosco_ai_frati: {
name: 'Bosco ai Frati',
map: [42, 88],
background: 'assets/scenes/bosco-ai-frati.png',
walk: [[0, 304], [100, 298], [480, 292], [640, 300], [640, 360], [0, 360]],
depth: [[292, 0.78], [360, 1.1]],
@@ -366,6 +375,7 @@ export const story = {
// Livello 1: Villa Adami, vista dalla salita della Fortezza guardando in discesa.
villa_adami: {
name: 'Villa Adami',
map: [34, 48],
background: 'assets/scenes/villa-adami.png',
walk: [[0, 272], [30, 246], [60, 244], [110, 256], [190, 268], [360, 296], [440, 288], [520, 276], [560, 268], [612, 262], [614, 272], [520, 288], [500, 292], [500, 360], [0, 360]],
depth: [[244, 0.5], [360, 1.2]],
@@ -392,6 +402,7 @@ export const story = {
// Tra il livello 1 e il 2: la salita di Villa Adami, con la casa del leone. Si sale alla Fortezza.
salita_villa_adami: {
name: 'Salita di Villa Adami',
map: [30, 32],
background: 'assets/scenes/salita-villa-adami.png',
walk: [[0, 318], [100, 312], [282, 318], [300, 300], [318, 262], [336, 252], [420, 266], [640, 296], [640, 360], [0, 360]],
depth: [[252, 0.5], [360, 1.1]],
@@ -424,6 +435,7 @@ export const story = {
// Il viale dei cipressi, la strada bianca tra la Fortezza e il Trebbio: in fondo c'è il castello.
viale_cipressi: {
name: 'Viale dei cipressi',
map: [55, 15],
background: 'assets/scenes/viale-cipressi.png',
walk: [[30, 360], [200, 300], [288, 252], [312, 250], [420, 300], [600, 360]],
depth: [[250, 0.16], [360, 1.2]],
@@ -446,6 +458,7 @@ export const story = {
},
trebbio: {
name: 'Castello del Trebbio',
map: [80, 15],
background: 'assets/scenes/trebbio.png',
music: 'assets/audio/ricordi.mp3',
walk: [[86, 308], [300, 306], [356, 308], [470, 312], [500, 324], [486, 360], [88, 360], [80, 330]],