Si sceglie il rione all'inizio: Piazza, Pieve, Cardetole o Campomigliaio

Dopo «Nuova partita» la domanda «Per chi corri quest'anno?» con i quattro rioni nei loro colori; la scelta resta
nella partita (rione:<id>) e cambia la riga dell'introduzione e le battute sul rione di Andrea. Voci nuove.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ApqNBvNbNCwXqR2RLo8QJh
This commit is contained in:
2026-10-24 15:04:15 +02:00
co-authored by Claude Opus 5.5
parent ee6c03a04d
commit 94a5479645
20 changed files with 149 additions and 18 deletions
+3
View File
@@ -35,6 +35,9 @@ const SMALL_AREA = 24 * 24;
// La miniatura di uno sfondo per la mappa (tools/miniature.py): assets/scenes/x.png -> assets/scenes/mini/x.png.
export const miniature = src => src?.replace(/([^/]+)$/, 'mini/$1') ?? null;
// Una riga dell'introduzione: un testo, oppure { rione: { id: testo } } con la riga del rione scelto.
export const introText = (line, rione) => typeof line === 'string' ? line : line.rione[rione] ?? Object.values(line.rione)[0];
export const sayTime = text => Math.max(1.6, 0.9 + text.length * 0.055);
export function newState(story) {
+1 -1
View File
@@ -14,7 +14,7 @@ export function voiceLines(story) {
if (typeof value.say === 'string') add(value.who ?? 'andrea', value.say);
Object.values(value).forEach(visit);
};
for (const line of story.intro ?? []) add('andrea', line);
for (const line of story.intro ?? []) for (const text of typeof line === 'string' ? [line] : Object.values(line.rione)) add('andrea', text);
visit(story.scenes);
visit(story.items);
visit(story.dialogues);
+35 -10
View File
@@ -1,6 +1,6 @@
// Avvio nel browser: collega motore, disegno e interfaccia HTML (verbi, inventario, dialoghi, fumetti).
import { story } from './story/story.js';
import { Game, VERBS, VERB_LABELS, checkSave } from './engine/game.js';
import { Game, VERBS, VERB_LABELS, checkSave, introText } from './engine/game.js';
import { Command } from './engine/command.js';
import { render, loadAssets, hasBackground, WIDTH, HEIGHT } from './engine/render.js';
import { readSave, writeSave, clearSave, readAudio, writeAudio, saveFile, saveFileName, readSaveFile } from './engine/storage.js';
@@ -45,9 +45,11 @@ window.voices = voices;
let voiced = null;
const say = (who, text) => voices.play(who, text, music.muted);
function start(state) {
// Una partita salvata (state) o nuova; la nuova col rione scelto, che resta nei flag (`rione:<id>`).
function start(state, rione = null) {
game = new Game(story, { state, onSave: data => storage && writeSave(storage, data), voiceTime: (who, text) => voices.seconds(who, text) });
command = new Command(game);
if (!state && rione) game.state.flags['rione:' + rione] = true;
if (!state) game.go(story.start.scene, story.start.at);
$('title').hidden = true;
$('ending').hidden = true;
@@ -56,9 +58,28 @@ function start(state) {
}
// --- Introduzione: le righe di story.intro una alla volta sopra il panorama; poi comincia la partita.
// --- Il rione: dopo «Nuova partita», per chi corre Andrea (story.rioni); poi l'introduzione con la sua riga.
let rione = null;
$('rione-buttons').replaceChildren(...Object.entries(story.rioni ?? {}).map(([id, r]) => {
const b = document.createElement('button');
b.type = 'button';
b.dataset.rione = id;
b.style.setProperty('--rione', r.color);
b.innerHTML = '<span class="rione-name"></span><span class="rione-colors"></span>';
b.querySelector('.rione-name').textContent = r.name;
b.querySelector('.rione-colors').textContent = r.colors;
b.addEventListener('click', () => { rione = id; $('rione').hidden = true; showIntro(); });
return b;
}));
function chooseRione() {
if (!story.rioni) return showIntro();
$('title').hidden = true;
$('rione').hidden = false;
}
let introLine = -1;
function showIntro() {
if (!story.intro?.length) return start(null);
if (!story.intro?.length) return start(null, rione);
$('title').hidden = true;
$('intro').hidden = false;
introLine = -1;
@@ -66,10 +87,10 @@ function showIntro() {
}
function nextIntro() {
introLine++;
if (introLine >= story.intro.length) { voices.stop(); $('intro').hidden = true; return start(null); }
const p = $('intro-text');
p.textContent = story.intro[introLine];
say('andrea', story.intro[introLine]);
if (introLine >= story.intro.length) { voices.stop(); $('intro').hidden = true; return start(null, rione); }
const p = $('intro-text'), text = introText(story.intro[introLine], rione);
p.textContent = text;
say('andrea', text);
p.style.animation = 'none';
void p.offsetWidth; // riavvia la comparsa
p.style.animation = '';
@@ -419,7 +440,7 @@ function frame(now) {
// sotto zero, il browser lancerebbe un errore e il ciclo dei fotogrammi si fermerebbe (gioco congelato).
const dt = Math.max(0, Math.min(0.1, (now - last) / 1000));
last = now;
const screen = !$('title').hidden ? 'title' : !$('splash').hidden ? 'splash' : !$('intro').hidden ? 'intro' : !$('ending').hidden ? 'ending' : 'game';
const screen = !$('title').hidden ? 'title' : !$('splash').hidden ? 'splash' : !$('intro').hidden || !$('rione').hidden ? 'intro' : !$('ending').hidden ? 'ending' : 'game';
document.body.classList.toggle('full', screen === 'title' || screen === 'splash' || screen === 'intro' || !$('credits').hidden);
music.duck = voices.playing ? 0.45 : 1;
music.update(trackFor(story, { screen, scene: game?.state.scene }), dt);
@@ -437,9 +458,13 @@ function frame(now) {
requestAnimationFrame(frame);
}
$('new-game').addEventListener('click', () => { if (storage) clearSave(storage); showIntro(); });
$('new-game').addEventListener('click', () => { if (storage) clearSave(storage); chooseRione(); });
$('continue').addEventListener('click', () => start(readSave(storage)));
$('restart').addEventListener('click', () => { if (storage) clearSave(storage); start(null); });
// Ricomincia: stessa scelta di rione della partita appena finita.
$('restart').addEventListener('click', () => {
if (storage) clearSave(storage);
start(null, Object.keys(game.state.flags).find(f => f.startsWith('rione:'))?.slice(6) ?? rione);
});
// Dietro il titolo si vede già la piazza.
// --- Cartello d'apertura «Manzolo (Poca) Art Presents»: 5 secondi (un clic o un tasto lo salta), poi il titolo.
+23 -5
View File
@@ -36,11 +36,25 @@ export const story = {
// 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] },
// I quattro rioni del Palio: dopo «Nuova partita» si sceglie per chi corre Andrea, e la partita lo ricorda col
// flag `rione:<id>`. Le battute che parlano del rione di Andrea lo controllano con `if`.
rioni: {
piazza: { name: 'Piazza', colors: 'i rossi', color: '#d0453a' },
pieve: { name: 'Pieve', colors: 'i bianchi', color: '#f2eee2' },
cardetole: { name: 'Cardetole', colors: 'i verdi', color: '#3fa34d' },
campomigliaio: { name: 'Campomigliaio', colors: 'i blu', color: '#2f6fd0' },
},
// Le righe dell'introduzione, sopra il panorama del paese, prima della prima scena.
intro: [
'San Piero a Sieve, nel Mugello. Fine maggio 2007.',
'In paese si prepara il Palio della Fortezza: i quattro rioni si sfidano, e chi vince si porta a casa il palio, il quadro dipinto dai ragazzi delle medie.',
'Io corro per Campomigliaio, i blu.',
// La riga del rione: quella del rione scelto dopo «Nuova partita» (story.rioni).
{ rione: {
piazza: 'Io corro per la Piazza, i rossi.',
pieve: 'Io corro per la Pieve, i bianchi.',
cardetole: 'Io corro per Cardetole, i verdi: gioco in casa.',
campomigliaio: 'Io corro per Campomigliaio, i blu.',
} },
'E stamattina, in piazza, è successo qualcosa...',
],
// Colonna sonora di Andrea (fatta con Suno): titolo, paese, ricordi (fine e Trebbio).
@@ -219,7 +233,7 @@ export const story = {
{
id: 'colonna', name: 'colonna', box: [492, 134, 36, 140], at: [505, 290], face: 'up',
actions: {
look: [{ say: 'La colonna, il centro del paese. Da qui passa tutto: le notizie, il Palio, i pettegolezzi.' }, { say: 'Ai piedi c\'è una strana scia luccicante che va a sinistra, verso la salita di Villa Adami.' }],
look: [{ say: 'La colonna, il centro del paese. Da qui passa tutto: le notizie, il Palio, i pettegolezzi.' }, { if: 'rione:piazza', then: [{ say: 'E qui è casa del mio rione: noi rossi.' }] }, { say: 'Ai piedi c\'è una strana scia luccicante che va a sinistra, verso la salita di Villa Adami.' }],
take: [{ say: 'Se mi porto via la colonna, la piazza come la chiamano?' }],
use: [{ say: 'Arrampicarmi? Il palio lassù non c\'è comunque.' }],
},
@@ -408,7 +422,7 @@ export const story = {
{
id: 'chiesa', name: 'Pieve di San Pietro', box: [108, 78, 166, 132], at: [200, 290], face: 'up',
actions: {
look: [{ say: 'La Pieve di San Pietro. Il rione Pieve, quelli in bianco.' }],
look: [{ if: 'rione:pieve', then: [{ say: 'La Pieve di San Pietro. Il mio rione: noi bianchi.' }], else: [{ say: 'La Pieve di San Pietro. Il rione Pieve, quelli in bianco.' }] }],
open: [{ say: 'Il prete è a benedire le carrette del Palio. Torno dopo.' }],
},
},
@@ -462,7 +476,7 @@ export const story = {
{
id: 'verso_cardetole', name: 'strada per Cardetole', box: [0, 204, 82, 54], at: [95, 250], face: 'left', exit: { scene: 'carza', at: [300, 350], face: 'up' },
actions: {
look: [{ say: 'La strada che scende a Cardetole, il rione dei verdi.' }],
look: [{ if: 'rione:cardetole', then: [{ say: 'La strada che scende a Cardetole: casa mia, e il mio rione, i verdi.' }], else: [{ say: 'La strada che scende a Cardetole, il rione dei verdi.' }] }],
},
},
{
@@ -1018,7 +1032,11 @@ export const story = {
{
id: 'la_luna', name: 'a sinistra, verso La Luna e Campomigliaio', box: [0, 216, 44, 56], at: [12, 248], face: 'left', default: 'walk',
actions: {
walk: [{ say: 'Più avanti c\'è La Luna, e poi Campomigliaio, il mio rione: i blu.' }, { say: 'Ci torno quando ho ritrovato il palio. A mani vuote non mi ci faccio vedere.' }],
walk: [{
if: 'rione:campomigliaio',
then: [{ say: 'Più avanti c\'è La Luna, e poi Campomigliaio, il mio rione: i blu.' }, { say: 'Ci torno quando ho ritrovato il palio. A mani vuote non mi ci faccio vedere.' }],
else: [{ say: 'Più avanti c\'è La Luna, e poi Campomigliaio, il rione dei blu.' }, { say: 'Prima il palio. Le gite fuori porta dopo.' }],
}],
look: [{ say: 'La Provinciale va avanti verso La Luna e Campomigliaio.' }],
},
},