Musica, vecchietti con sprite, nuove scene del paese e generatore degli sfondi

Lavoro della sessione precedente: colonna sonora (titolo, paese, ricordi) con muto e volume,
vecchietta e vecchietto sulla panchina, scene di Pieve, oratorio, Via de' Medici, Villa Adami,
salita, campeggio, Bosco ai Frati e viale dei cipressi, panorama dell'introduzione,
tools/genera.py per gli sfondi dalle foto.

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 12:59:57 +02:00
co-authored by Claude Opus 5.5
parent 5eeb634ceb
commit 33e2598718
45 changed files with 1556 additions and 78 deletions
+15 -1
View File
@@ -76,6 +76,7 @@ export class Game {
hotspots() { return (this.scene.hotspots ?? []).filter(h => check(h.when, this.state)); }
hotspot(id) { return this.hotspots().find(h => h.id === id); }
people() { return (this.scene.people ?? []).filter(p => check(p.when, this.state)); }
hotspotAt(p) {
const list = this.hotspots();
for (let i = list.length - 1; i >= 0; i--) if (inBox(list[i].box, p)) return list[i];
@@ -166,6 +167,19 @@ export class Game {
return this.back ? 'back' : 'stand';
}
// Il fotogramma di un personaggio della scena (scene.people): quando parla alterna `talk`, o tiene la posa
// chiesta dalla battuta ({ say, who, pose }); da fermo ogni `every` secondi fa `blink` per un attimo.
personPose(p) {
const idle = p.idle ?? 'sit';
if (this.speech?.who === p.who) {
if (this.speech.pose) return this.speech.pose;
const talk = p.talk ?? ['talk'];
return talk[Math.floor(this.time / 0.3) % talk.length];
}
const every = p.every ?? 4, t = (this.time + (p.offset ?? 0)) % every;
return p.blink && t > every - 0.25 ? p.blink : idle;
}
scaleAt(y) {
const d = this.scene.depth;
if (!d) return 1;
@@ -218,7 +232,7 @@ export class Game {
run(e) {
const s = this.state;
if (e.say != null) this.speech = { text: e.say, who: e.who ?? 'andrea', left: sayTime(e.say) };
if (e.say != null) this.speech = { text: e.say, who: e.who ?? 'andrea', pose: e.pose, left: sayTime(e.say) };
else if (e.walk) { this.task = e; this.startWalk(e.walk); }
// `go` prima di `face`: un cambio scena porta con sé il verso d'arrivo.
else if (e.go) this.go(e.go, e.at, e.face);
+68
View File
@@ -0,0 +1,68 @@
// Colonna sonora: un brano alla volta, in loop, con dissolvenza quando cambia.
// Quale brano suona lo decidono i dati (story.music e scene.music). Si prova a partire subito; se il browser non lo
// permette prima di un gesto del giocatore, il brano riparte al primo clic o tasto (unlock).
// Il brano del momento: titolo, fine, oppure quello della scena (o quello comune del gioco).
export function trackFor(story, { screen, scene }) {
const m = story.music ?? {};
if (screen === 'title' || screen === 'intro') return m.title ?? null;
if (screen === 'ending') return m.ending ?? m.game ?? null;
return story.scenes[scene]?.music ?? m.game ?? null;
}
const FADE = 1.2;
export class Music {
constructor({ muted = false, volume = 0.6 } = {}) {
this.muted = muted;
this.volume = volume;
this.blocked = null;
this.want = null;
this.current = null; // { src, audio }
this.old = [];
}
// Chiamata a ogni fotogramma con il brano voluto: avvia, cambia e sfuma.
update(src, dt) {
this.want = src;
if (src !== this.current?.src) {
if (this.current) this.old.push(this.current.audio);
this.current = src ? { src, audio: this.start(src) } : null;
}
// Il brano in corso sale (o scende) verso il volume scelto; quelli vecchi sfumano fino a zero.
const step = dt / FADE, target = this.muted ? 0 : this.volume;
if (this.current) {
const a = this.current.audio;
a.volume = a.volume < target ? Math.min(target, a.volume + step) : Math.max(target, a.volume - step);
}
for (const a of this.old) a.volume = Math.max(0, a.volume - step);
this.old = this.old.filter(a => a.volume > 0 || (a.pause(), false));
}
start(src) {
const audio = new Audio(src);
audio.loop = true;
audio.volume = 0;
audio.play().catch(() => { this.blocked = audio; }); // senza permesso si riprova al primo gesto
return audio;
}
unlock() {
const a = this.blocked;
this.blocked = null;
if (a && a === this.current?.audio) a.play().catch(() => {});
}
toggle() {
this.muted = !this.muted;
return this.muted;
}
setVolume(v) {
this.volume = Math.max(0, Math.min(1, v));
if (this.volume > 0) this.muted = false;
return this.volume;
}
settings() { return { volume: this.volume, muted: this.muted }; }
}
+12 -5
View File
@@ -84,10 +84,10 @@ export function loadAssets(story) {
return Promise.all([...scenes, ...people]);
}
// Il personaggio alto PERSON pixel a grandezza 1, con i piedi in (x, y); rivolto a sinistra si specchia.
// Il personaggio alto PERSON pixel a grandezza 1 (per `size` 1), con i piedi in (x, y); rivolto a sinistra si specchia.
const PERSON = 62;
function drawSprite(ctx, { atlas, img }, frame, x, y, scale, facing) {
const f = atlas.frames[frame] ?? atlas.frames.stand, k = PERSON * scale / atlas.height;
function drawSprite(ctx, { atlas, img }, frame, x, y, scale, facing, size = 1) {
const f = atlas.frames[frame] ?? atlas.frames.stand ?? Object.values(atlas.frames)[0], k = PERSON * scale * size / atlas.height;
ctx.save();
ctx.translate(Math.round(x), Math.round(y));
ctx.fillStyle = 'rgba(0,0,0,0.22)';
@@ -109,9 +109,16 @@ export function render(ctx, game, { hover } = {}) {
if (bg) ctx.drawImage(bg, 0, 0, WIDTH, HEIGHT);
for (const shape of scene.art ?? []) drawShape(ctx, shape);
for (const h of game.hotspots()) for (const shape of h.art ?? []) drawShape(ctx, shape);
// Andrea e i personaggi della scena, dal più lontano al più vicino.
const andrea = sprites.get('andrea');
if (andrea) drawSprite(ctx, andrea, game.pose(), s.x, s.y, game.scaleAt(s.y), s.facing);
else drawAndrea(ctx, s.x, s.y, game.scaleAt(s.y), s.facing, game.walking ? game.stride / 6 : null);
const drawAndreaNow = () => andrea ? drawSprite(ctx, andrea, game.pose(), s.x, s.y, game.scaleAt(s.y), s.facing)
: drawAndrea(ctx, s.x, s.y, game.scaleAt(s.y), s.facing, game.walking ? game.stride / 6 : null);
const queue = [{ y: s.y, draw: drawAndreaNow }];
for (const p of game.people()) {
const sheet = sprites.get(p.who);
if (sheet) queue.push({ y: p.at[1], draw: () => drawSprite(ctx, sheet, game.personPose(p), ...p.at, game.scaleAt(p.at[1]), p.facing ?? 1, p.size) });
}
for (const q of queue.sort((a, b) => a.y - b.y)) q.draw();
if (hover) {
ctx.strokeStyle = 'rgba(255,255,255,0.35)';
ctx.setLineDash([3, 3]);
+21
View File
@@ -27,3 +27,24 @@ export function clearSave(storage) {
return false;
}
}
// Volume e muto della musica: si ricordano anche senza una partita salvata.
const AUDIO_KEY = 'leggenda-regolo-audio';
export function readAudio(storage) {
try {
const a = JSON.parse(storage.getItem(AUDIO_KEY));
return a && typeof a.volume === 'number' ? { volume: a.volume, muted: !!a.muted } : null;
} catch {
return null;
}
}
export function writeAudio(storage, settings) {
try {
storage.setItem(AUDIO_KEY, JSON.stringify(settings));
return true;
} catch {
return false;
}
}
+43 -2
View File
@@ -3,7 +3,8 @@ import { story } from './story/story.js';
import { Game, VERBS, VERB_LABELS } from './engine/game.js';
import { Command } from './engine/command.js';
import { render, loadAssets, WIDTH, HEIGHT } from './engine/render.js';
import { readSave, writeSave, clearSave } from './engine/storage.js';
import { readSave, writeSave, clearSave, readAudio, writeAudio } from './engine/storage.js';
import { Music, trackFor } from './engine/music.js';
const $ = id => document.getElementById(id);
const canvas = $('scene'), ctx = canvas.getContext('2d');
@@ -13,6 +14,20 @@ const debug = new URLSearchParams(location.search).has('debug');
let game = null, command = null, hoverTarget = null, mouse = null, running = false;
// --- Musica: parte subito se il browser lo permette, altrimenti al primo gesto; volume e muto si ricordano.
const music = new Music((storage && readAudio(storage)) ?? {});
const saveAudio = () => storage && writeAudio(storage, music.settings());
function showSound() {
$('mute').setAttribute('aria-pressed', String(music.muted));
$('mute').setAttribute('aria-label', music.muted ? "Musica: rimetti l'audio" : "Musica: togli l'audio");
$('volume').value = Math.round(music.volume * 100);
}
for (const ev of ['pointerdown', 'keydown']) addEventListener(ev, () => music.unlock(), { capture: true });
$('mute').addEventListener('click', () => { music.toggle(); saveAudio(); showSound(); });
$('volume').addEventListener('input', e => { music.setVolume(e.target.value / 100); saveAudio(); showSound(); });
showSound();
window.music = music; // per i test nel browser
function start(state) {
game = new Game(story, { state, onSave: data => storage && writeSave(storage, data) });
command = new Command(game);
@@ -23,6 +38,27 @@ function start(state) {
window.game = game; // per i test nel browser e per curiosare dalla console
}
// --- Introduzione: le righe di story.intro una alla volta sopra il panorama; poi comincia la partita.
let introLine = -1;
function showIntro() {
if (!story.intro?.length) return start(null);
$('title').hidden = true;
$('intro').hidden = false;
introLine = -1;
nextIntro();
}
function nextIntro() {
introLine++;
if (introLine >= story.intro.length) { $('intro').hidden = true; return start(null); }
const p = $('intro-text');
p.textContent = story.intro[introLine];
p.style.animation = 'none';
void p.offsetWidth; // riavvia la comparsa
p.style.animation = '';
}
$('intro').addEventListener('click', e => { if (e.target !== $('skip-intro')) nextIntro(); });
$('skip-intro').addEventListener('click', () => { introLine = story.intro.length; nextIntro(); });
function showTitle() {
running = false;
const saved = storage && readSave(storage);
@@ -113,6 +149,9 @@ function updateChoices() {
}
window.addEventListener('keydown', e => {
if (e.target === $('volume')) 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;
if ([' ', '.', 'Enter'].includes(e.key) && game.speech) { e.preventDefault(); game.skip(); return; }
if (e.key === 'Escape') command.reset();
@@ -150,6 +189,8 @@ let last = performance.now();
function frame(now) {
const dt = Math.min(0.1, (now - last) / 1000);
last = now;
const screen = !$('title').hidden ? 'title' : !$('intro').hidden ? 'intro' : !$('ending').hidden ? 'ending' : 'game';
music.update(trackFor(story, { screen, scene: game?.state.scene }), dt);
if (game) {
if (running) game.update(dt);
render(ctx, game, { hover: debug && hoverTarget ? game.hotspot(hoverTarget) : null });
@@ -158,7 +199,7 @@ function frame(now) {
requestAnimationFrame(frame);
}
$('new-game').addEventListener('click', () => { if (storage) clearSave(storage); start(null); });
$('new-game').addEventListener('click', () => { if (storage) clearSave(storage); showIntro(); });
$('continue').addEventListener('click', () => start(readSave(storage)));
$('restart').addEventListener('click', () => { if (storage) clearSave(storage); start(null); });
+4 -1
View File
@@ -6,6 +6,9 @@ export const solution = [
['choose', 'Avete notato niente di strano stanotte?'],
['choose', 'Ci vediamo dopo.'],
['take', 'cosa_luccicante'],
['walk', 'vicolo'],
['walk', 'verso_villa_adami'],
['walk', 'su_villa_adami'],
['walk', 'salita'],
['walk', 'su_fortezza'],
['use', 'impronte', 'squama'],
];
+296 -25
View File
@@ -1,14 +1,23 @@
// Storia del prototipo: tre scene con gli sfondi veri (Piazza Colonna, Fortezza, Trebbio) per collaudare il motore.
// Storia del prototipo: le scene del paese a gradini (docs/PROGETTO.md, «La mappa») per collaudare il motore.
// Testi, personaggi ed enigmi sono provvisori e verranno sostituiti con i ricordi di Andrea (docs/PROGETTO.md).
// Le forme in `art` sono segnaposto disegnati sopra lo sfondo (vecchietti, squama, catena) in attesa degli sprite.
export const story = {
title: 'La Leggenda del Regolo',
start: { scene: 'piazza', at: [250, 320] },
// 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.',
'E stamattina, in piazza, è successo qualcosa...',
],
// Colonna sonora di Andrea (fatta con Suno): titolo, paese, ricordi (fine e Trebbio).
music: { title: 'assets/audio/titolo.mp3', game: 'assets/audio/paese.mp3', ending: 'assets/audio/ricordi.mp3' },
characters: {
andrea: { name: 'Andrea', color: '#ffffff', sprite: 'assets/characters/andrea.json' },
vecchioA: { name: 'Vecchietto col cappello', color: '#ffd27a' },
vecchioB: { name: 'Vecchietto col bastone', color: '#9fe3ff' },
vecchietta: { name: 'Vecchietta', color: '#ffd27a', sprite: 'assets/characters/vecchietta.json' },
vecchietto: { name: 'Vecchietto', color: '#9fe3ff', sprite: 'assets/characters/vecchietto.json' },
castello: { name: 'Il Castello', color: '#c8f0a0' },
},
items: {
@@ -28,12 +37,17 @@ export const story = {
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]],
entry: [345, 240],
speakers: { vecchioA: [18, 244], vecchioB: [47, 244] },
speakers: { vecchietta: [16, 250], vecchietto: [52, 252] },
// I vecchietti seduti sulla panchina: fotogrammi in assets/characters (pose e battute in game.personPose).
people: [
{ who: 'vecchietta', at: [16, 300], size: 0.75, talk: ['talk', 'explain'], blink: 'blink', every: 3.7 },
{ who: 'vecchietto', at: [52, 302], size: 0.75, talk: ['talk', 'explain'], blink: 'doze', every: 5.3, offset: 2 },
],
hotspots: [
{
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 verso il vicolo della Fortezza.' }],
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.' }],
take: [{ say: 'Se mi porto via la colonna, la piazza come la chiamano?' }],
use: [{ say: 'Arrampicarmi? Il palio lassù non c\'è comunque.' }],
},
@@ -46,6 +60,13 @@ export const story = {
talk: [{ say: 'Parlare con una porta. Neanche al Palio si arriva a tanto.' }],
},
},
{
id: 'verso_gelateria', name: 'strada verso il Fiocco di Neve', box: [570, 150, 28, 100], at: [580, 256], face: 'right', default: 'walk',
actions: {
walk: [{ say: 'Di là c\'è la gelateria del Fiocco di Neve.' }, { say: 'Con questo caldo, un bel gelatino ci starebbe proprio bene...' }, { say: 'No. Prima ritrovo il palio, poi mi premio.' }],
look: [{ say: 'La strada che porta al Fiocco di Neve. Già sento il profumo di cialda.' }],
},
},
{
id: 'manifesto', name: 'manifesto del Palio', box: [256, 196, 18, 24], at: [265, 250], face: 'up',
art: [{ rect: [256, 196, 18, 24], fill: '#f4efe4' }, { rect: [258, 199, 14, 5], fill: '#b7342e' }, { rect: [258, 207, 12, 2], fill: '#3b3b44' }, { rect: [258, 211, 10, 2], fill: '#3b3b44' }, { rect: [258, 215, 12, 2], fill: '#2f5fb3' }],
@@ -55,18 +76,14 @@ export const story = {
},
},
{
id: 'vecchietti', name: 'vecchietti', box: [2, 244, 62, 50], at: [100, 320], face: 'left', default: 'talk',
art: [
{ rect: [8, 264, 20, 26], fill: '#5d6b8a' }, { rect: [11, 250, 14, 14], fill: '#e7b58e' }, { rect: [8, 247, 20, 5], fill: '#3e3227' }, { rect: [10, 290, 7, 14], fill: '#3a3f52' }, { rect: [19, 290, 7, 14], fill: '#3a3f52' },
{ rect: [37, 264, 20, 26], fill: '#8a6a4f' }, { rect: [40, 250, 14, 14], fill: '#e7b58e' }, { rect: [40, 248, 14, 4], fill: '#d8d8d8' }, { rect: [38, 290, 7, 14], fill: '#4a4036' }, { rect: [47, 290, 7, 14], fill: '#4a4036' }, { rect: [58, 262, 3, 44], fill: '#4a3424' },
],
id: 'vecchietti', name: 'vecchietti', box: [0, 250, 72, 52], at: [100, 320], face: 'left', default: 'talk',
actions: {
look: [{ say: 'I vecchietti della panchina. Sanno tutto di tutti, soprattutto quello che non è successo.' }],
talk: [{ dialogue: 'vecchietti' }],
take: [{ say: 'Se li porto via, il paese resta senza notizie.' }],
},
give: {
squama: [{ say: 'Guardate cosa ho trovato.' }, { say: 'Bella! È una patatina?', who: 'vecchioA' }, { say: 'Ma che patatina, è il Regolo che fa la muta!', who: 'vecchioB' }, { say: 'Me la tengo io, grazie.' }],
squama: [{ say: 'Guardate cosa ho trovato.' }, { say: 'Bella! È una patatina?', who: 'vecchietta', pose: 'laugh' }, { say: 'Ma che patatina, è il Regolo che fa la muta!', who: 'vecchietto' }, { say: 'Me la tengo io, grazie.' }],
},
},
{
@@ -84,7 +101,13 @@ export const story = {
},
},
{
id: 'vicolo', name: 'vicolo verso la Fortezza', box: [330, 190, 34, 48], at: [345, 234], exit: { scene: 'fortezza', at: [175, 352], face: 'right' },
id: 'verso_campeggio', name: 'strada verso il campeggio', box: [330, 190, 34, 48], at: [345, 234], exit: { scene: 'campeggio', at: [200, 345], face: 'right' },
},
{
id: 'verso_villa_adami', name: 'Via de\' Medici, verso Villa Adami', box: [0, 196, 90, 50], at: [92, 250], exit: { scene: 'via_de_medici', at: [300, 348], face: 'up' },
},
{
id: 'verso_pieve', name: 'verso la Pieve', box: [612, 292, 28, 68], at: [628, 330], exit: { scene: 'pieve', at: [30, 320], face: 'right' },
},
],
enter: [{ if: '!intro', then: [{ set: 'intro' }, { say: 'Il palio è sparito! Il quadro che hanno dipinto i ragazzi delle medie.' }, { say: 'La festa è domani: se nessuno lo ritrova, salta tutto. Tocca a me.' }] }],
@@ -131,16 +154,264 @@ export const story = {
},
},
{
id: 'sentiero_trebbio', name: 'salita per il Trebbio', box: [0, 300, 26, 60], at: [6, 340], exit: { scene: 'trebbio', at: [110, 345], face: 'right' },
id: 'sentiero_trebbio', name: 'sentiero per il Trebbio', box: [0, 200, 90, 160], at: [20, 330], exit: { scene: 'viale_cipressi', at: [300, 350], face: 'up' },
},
{
id: 'verso_piazza', name: 'verso la Piazza', box: [120, 344, 110, 16], at: [175, 356], exit: { scene: 'piazza', at: [345, 240], face: 'left' },
id: 'verso_villa_adami', name: 'discesa verso Villa Adami', box: [180, 326, 340, 34], at: [330, 352], exit: { scene: 'salita_villa_adami', at: [330, 300], face: 'left' },
},
],
},
// Livello 0: la Pieve vista dalla salita, con la salita per Villa Adami alle spalle.
pieve: {
name: 'La Pieve',
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]],
depth: [[215, 0.42], [360, 1.15]],
entry: [30, 320],
hotspots: [
{
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.' }],
open: [{ say: 'Il prete è a benedire le carrette del Palio. Torno dopo.' }],
},
},
{
id: 'campanile', name: 'campanile', box: [272, 8, 60, 140], at: [300, 300], face: 'up',
actions: {
look: [{ say: 'Il campanile della Pieve. Da lassù si vedrebbe tutto il paese. Anche un serpente con le ali.' }],
},
},
{
id: 'statua', name: 'statua', box: [334, 172, 18, 52], at: [330, 300], face: 'up',
actions: {
look: [{ say: 'Una statua sul piedistallo. Lei il palio l\'ha visto passare di sicuro, ma non parla.' }],
talk: [{ say: 'Buongiorno.' }, { say: '...' }, { say: 'Come i vecchietti quando gli chiedi dei debiti.' }],
},
},
{
id: 'oratorio', name: 'porta dell\'oratorio', box: [452, 180, 42, 56], at: [473, 240], face: 'up', exit: { scene: 'oratorio', at: [320, 345], face: 'right' },
},
{
id: 'verso_piazza', name: 'verso la Piazza', box: [0, 262, 26, 98], at: [8, 320], exit: { scene: 'piazza', at: [610, 320], face: 'left' },
},
{
id: 'salita_villa_adami', name: 'salita verso Villa Adami', box: [440, 330, 200, 30], at: [600, 350], exit: { scene: 'villa_adami', at: [595, 268], face: 'left' },
},
],
},
// Il cortile dell'oratorio, dietro la porta della canonica: palco, tettoia, tavolate della festa.
oratorio: {
name: 'Oratorio della Pieve',
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]],
entry: [320, 345],
hotspots: [
{
id: 'palco', name: 'palco', box: [52, 149, 134, 58], at: [120, 238], face: 'up',
actions: {
look: [{ say: 'Il palco dell\'oratorio. D\'estate ci fanno le recite, la tombola e i concerti.' }, { say: 'Adesso è vuoto. Peccato, avevo un numero di giocoleria pronto.' }],
use: [{ say: 'Salire sul palco senza pubblico? Mi vergogno pure da solo.' }],
},
},
{
id: 'tettoia', name: 'tettoia', box: [333, 119, 153, 72], at: [400, 236], face: 'up',
actions: {
look: [{ say: 'La tettoia, dove si mettono le cucine quando c\'è la festa.' }],
},
},
{
id: 'tavolate', name: 'tavolate', box: [500, 128, 140, 113], at: [560, 268], face: 'right',
actions: {
look: [{ say: 'Le tavolate della festa. Fra qualche giorno qui si mangia per tre ore di fila.' }],
use: [{ say: 'Mi siedo e aspetto che arrivi la pastasciutta? Ci vorrebbe una settimana.' }],
},
},
{
id: 'cancello', name: 'cancello', box: [477, 166, 23, 52], at: [488, 236], face: 'up',
actions: {
look: [{ say: 'Il cancello di dietro.' }],
open: [{ say: 'Chiuso. Si apre solo quando c\'è la festa.' }],
},
},
{
id: 'verso_pieve', name: 'torna alla Pieve', box: [0, 334, 640, 26], at: [320, 354], exit: { scene: 'pieve', at: [473, 240], face: 'down' },
},
],
},
// 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',
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]],
entry: [300, 348],
hotspots: [
{
id: 'portone', name: 'portone ad arco', box: [478, 30, 70, 270], at: [430, 290], face: 'right',
actions: {
look: [{ say: 'Un portone antico con l\'inferriata. Chissà cosa c\'è dietro: una cantina? Un tesoro? La cantina di un tesoro?' }],
open: [{ say: 'Chiuso. Come tutte le porte interessanti di questo paese.' }],
},
},
{
id: 'su_villa_adami', name: 'su verso Villa Adami', box: [272, 186, 60, 34], at: [300, 216], exit: { scene: 'villa_adami', at: [50, 252], face: 'right' },
},
{
id: 'giu_piazza', name: 'giù verso la Piazza', box: [0, 334, 560, 26], at: [300, 354], exit: { scene: 'piazza', at: [92, 250], face: 'right' },
},
],
},
// L'ingresso del campeggio: dalla Piazza a diritto; poco dopo, a destra, si sale al Bosco ai Frati.
campeggio: {
name: 'Ingresso del campeggio',
background: 'assets/scenes/campeggio.png',
walk: [[150, 360], [165, 262], [420, 266], [640, 286], [640, 360]],
depth: [[262, 0.6], [360, 1.15]],
entry: [200, 345],
hotspots: [
{
id: 'campeggio', name: 'campeggio', box: [190, 120, 220, 140], at: [300, 280], face: 'up',
actions: {
look: [{ say: 'L\'ingresso del campeggio. A giudicare dalle bandiere, ci dorme mezzo mondo.' }],
walk: [{ say: 'Dentro non conosco nessuno. E il Regolo, che io sappia, non va in campeggio.' }],
},
},
{
id: 'bandiere', name: 'bandierine', box: [115, 52, 340, 30], at: [300, 280], face: 'up',
actions: {
look: [{ say: 'Una fila di bandierine di tutti i paesi. Il Palio della Fortezza però è roba nostra.' }],
take: [{ say: 'Senza scala ci arrivo solo con la fantasia.' }],
},
},
{
id: 'bosco_ai_frati', name: 'salita verso il Bosco ai Frati', box: [570, 250, 70, 110], at: [628, 320], exit: { scene: 'bosco_ai_frati', at: [320, 345], face: 'right' },
},
{
id: 'verso_piazza', name: 'torna in Piazza', box: [150, 290, 100, 70], at: [175, 345], exit: { scene: 'piazza', at: [345, 240], face: 'left' },
},
],
},
// Il convento del Bosco ai Frati, nel bosco sopra il campeggio.
bosco_ai_frati: {
name: 'Bosco ai Frati',
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]],
entry: [320, 345],
hotspots: [
{
id: 'convento', name: 'chiesa del convento', box: [96, 60, 386, 236], at: [320, 310], face: 'up',
actions: {
look: [{ say: 'Il convento del Bosco ai Frati, in mezzo al bosco. Qui si viene a cercare il fresco e il silenzio.' }],
open: [{ say: 'Meglio non disturbare: i frati sono gli unici in tutto il Mugello che non parlano del Palio.' }],
},
},
{
id: 'porta_convento', name: 'porta del convento', box: [526, 218, 32, 78], at: [540, 304], face: 'up',
actions: {
look: [{ say: 'Una porta ad arco col cancelletto di ferro.' }],
open: [{ say: 'Chiusa. A quest\'ora i frati pregano. O pranzano. O tutte e due.' }],
},
},
{
id: 'tiglio', name: 'tiglio', box: [0, 0, 92, 296], at: [100, 320], face: 'left',
actions: {
look: [{ say: 'Un tiglio enorme. Profuma che sembra una pasticceria.' }],
use: [{ say: 'Arrampicarmi? Ho i pantaloni buoni.' }],
},
},
{
id: 'giu_campeggio', name: 'scendi verso il campeggio', box: [0, 334, 640, 26], at: [320, 354], exit: { scene: 'campeggio', at: [620, 320], face: 'left' },
},
],
},
// Livello 1: Villa Adami, vista dalla salita della Fortezza guardando in discesa.
villa_adami: {
name: 'Villa Adami',
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]],
entry: [300, 345],
hotspots: [
{
id: 'villa', name: 'Villa Adami', box: [69, 8, 459, 247], at: [300, 300], face: 'up',
actions: {
look: [{ say: 'Villa Adami. Gialla come un tuorlo, con le persiane verdi.' }],
open: [{ say: 'Mica è casa mia. E poi suonare e scappare non si fa più dalla quinta elementare.' }],
},
},
{
id: 'verso_piazza', name: 'Via de\' Medici, verso la Piazza', box: [20, 215, 50, 40], at: [45, 250], exit: { scene: 'via_de_medici', at: [300, 216], face: 'left' },
},
{
id: 'verso_pieve', name: 'discesa verso la Pieve', box: [560, 236, 60, 38], at: [600, 266], exit: { scene: 'pieve', at: [600, 350], face: 'left' },
},
{
id: 'salita', name: 'salita verso la Fortezza', box: [100, 330, 400, 30], at: [300, 354], exit: { scene: 'salita_villa_adami', at: [340, 352], face: 'right' },
},
],
},
// 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',
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]],
entry: [340, 352],
hotspots: [
{
id: 'leone', name: 'leone di terracotta', box: [412, 66, 34, 30], at: [400, 290], face: 'up',
actions: {
look: [{ say: 'Un leone di terracotta che fa la guardia alla salita. Di serpenti, dice, non ne ha visti passare.' }],
talk: [{ say: 'Ciao leone. Visto passare un serpente con le ali?' }, { say: 'Il leone fa finta di niente. Come tutti, in questo paese.' }],
take: [{ say: 'Pesa più di me. E poi ha un\'aria permalosa.' }],
},
},
{
id: 'verso_scuola', name: 'strada verso la scuola', box: [0, 286, 120, 44], at: [40, 336], face: 'left', default: 'walk',
actions: {
walk: [{ say: 'A scuola? A maggio? Nemmeno per sogno.' }],
look: [{ say: 'Di là si va alla scuola. Ci passo già abbastanza tempo durante l\'anno.' }],
},
},
{
id: 'su_fortezza', name: 'salita verso la Fortezza', box: [168, 196, 182, 46], at: [326, 259], face: 'left', exit: { scene: 'fortezza', at: [175, 352], face: 'right' },
},
{
id: 'verso_villa_adami', name: 'discesa verso Villa Adami', box: [200, 334, 280, 26], at: [340, 356], exit: { scene: 'villa_adami', at: [300, 345], face: 'right' },
},
],
},
// Il viale dei cipressi, la strada bianca tra la Fortezza e il Trebbio: in fondo c'è il castello.
viale_cipressi: {
name: 'Viale dei cipressi',
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]],
entry: [300, 350],
hotspots: [
{
id: 'cipressi', name: 'cipressi', box: [440, 0, 200, 270], at: [400, 300], face: 'right',
actions: {
look: [{ say: 'I cipressi in fila, dritti come soldatini. Da piccolo li contavo. Non sono mai arrivato in fondo.' }],
take: [{ say: 'Un cipresso in tasca? Neanche nei sogni.' }],
},
},
{
id: 'al_trebbio', name: 'in fondo, verso il Trebbio', box: [262, 214, 76, 50], at: [300, 256], exit: { scene: 'trebbio', at: [110, 345], face: 'right' },
},
{
id: 'alla_fortezza', name: 'indietro, verso la Fortezza', box: [0, 330, 640, 30], at: [300, 352], exit: { scene: 'fortezza', at: [20, 330], face: 'right' },
},
],
},
trebbio: {
name: 'Castello del Trebbio',
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]],
depth: [[306, 0.75], [360, 1.05]],
entry: [110, 345],
@@ -169,7 +440,7 @@ export const story = {
},
},
{
id: 'verso_fortezza', name: 'discesa verso la Fortezza', box: [80, 318, 26, 42], at: [92, 345], exit: { scene: 'fortezza', at: [20, 340], face: 'right' },
id: 'verso_fortezza', name: 'viale dei cipressi, verso la Fortezza', box: [0, 250, 110, 110], at: [92, 345], exit: { scene: 'viale_cipressi', at: [300, 256], face: 'left' },
},
],
},
@@ -182,29 +453,29 @@ export const story = {
{
text: 'Avete visto chi ha preso il palio?',
then: [
{ say: 'È stato il Regolo! Quello che cadde dal cielo quando scavavano le fondamenta.', who: 'vecchioA' },
{ say: 'Macché cielo! Mangiava le bestie, e lo presero i soldati di Napoleone.', who: 'vecchioB' },
{ say: 'Napoleone? A San Piero? Ma se un c\'è mai venuto!', who: 'vecchioA' },
{ say: 'E il Regolo sì?', who: 'vecchioB' },
{ say: 'È stato il Regolo! Quello che cadde dal cielo quando scavavano le fondamenta.', who: 'vecchietta' },
{ say: 'Macché cielo! Mangiava le bestie, e lo presero i soldati di Napoleone.', who: 'vecchietto', pose: 'point' },
{ say: 'Napoleone? A San Piero? Ma se un c\'è mai venuto!', who: 'vecchietta', pose: 'point' },
{ say: 'E il Regolo sì?', who: 'vecchietto', pose: 'shrug' },
],
},
{
id: 'stanotte', once: true, text: 'Avete notato niente di strano stanotte?',
then: [
{ say: 'Io dormivo. Alla mia età si dorme anche in piedi.', who: 'vecchioB' },
{ say: 'Io stamani ho visto luccicare qualcosa sotto la panchina.', who: 'vecchioA' },
{ say: 'Ma con la mia schiena, chi ci si china?', who: 'vecchioA' },
{ say: 'Io dormivo. Alla mia età si dorme anche in piedi.', who: 'vecchietto', pose: 'doze' },
{ say: 'Io stamani ho visto luccicare qualcosa sotto la panchina.', who: 'vecchietta' },
{ say: 'Ma con la mia schiena, chi ci si china?', who: 'vecchietta', pose: 'look_down' },
{ set: 'luccichio' },
],
},
{
text: 'Come vi sembra il Palio di quest\'anno?',
then: [
{ say: 'Senza il palio? Una festa così non s\'è mai vista.', who: 'vecchioB' },
{ say: 'Nel mio, di Palio, si correva con le carriole vere. Mica come voi.', who: 'vecchioA' },
{ say: 'Senza il palio? Una festa così non s\'è mai vista.', who: 'vecchietto', pose: 'shrug' },
{ say: 'Nel mio, di Palio, si correva con le carriole vere. Mica come voi.', who: 'vecchietta' },
],
},
{ text: 'Ci vediamo dopo.', next: 'end', then: [{ say: 'Salutaci i tuoi!', who: 'vecchioA' }] },
{ text: 'Ci vediamo dopo.', next: 'end', then: [{ say: 'Salutaci i tuoi!', who: 'vecchietta' }] },
],
},
},