L'Ingorgo Sonoro: dal manifesto in piazza, ingrandito e toccato, «Quanto darei per essere all'Ingorgo stasera?!?» e l'effetto speciale; il centro di notte pieno zeppo di gente (sfondi di Codex), le console di Andrea (Suno) che si mescolano camminando, Andrea più grande alla festa, uscendo si torna a maggio. Baldoria al campo sportivo col bandierone della Sampdoria (Codex) (Andrea)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GiECd9Quo9nEdXyjXYF8ma
This commit is contained in:
+43
-3
@@ -13,6 +13,7 @@ import * as gara from './gara.js';
|
||||
import * as micro from './micro.js';
|
||||
import * as cerbottana from './cerbottana.js';
|
||||
import { newCorsa, updateCorsa } from './corsa.js';
|
||||
import { newIncanto, updateIncanto } from './incanto.js';
|
||||
import * as condor from './condor.js';
|
||||
import * as campana from './campana.js';
|
||||
import * as poker from './poker.js';
|
||||
@@ -148,6 +149,9 @@ export function restoreState(story, data) {
|
||||
return { scene: data.scene, x, y, facing: data.facing === -1 ? -1 : 1, inventory, flags: pick(data.flags), used: pick(data.used), ended: data.ended === true };
|
||||
}
|
||||
|
||||
// Il manifesto ingrandito si apre con uno zoom: un tocco prima che sia aperto non conta.
|
||||
export const POSTER_READY = 0.35;
|
||||
|
||||
export class Game {
|
||||
// voiceTime(who, text): secondi della voce registrata per la battuta, se c'è (il fumetto non finisce prima).
|
||||
constructor(story, { state, onSave, voiceTime } = {}) {
|
||||
@@ -295,6 +299,13 @@ export class Game {
|
||||
closeupTap() {
|
||||
const c = this.closeup;
|
||||
if (!c || this.speech) return false;
|
||||
// Un manifesto ingrandito (type 'poster'): toccarlo lo chiude e fa partire `tap` (il desiderio dell'Ingorgo).
|
||||
if (this.story.closeups[c.id].type === 'poster') {
|
||||
if (c.t < POSTER_READY) return false;
|
||||
this.closeup = null;
|
||||
this.queue.unshift(...(this.story.closeups[c.id].tap ?? []));
|
||||
return true;
|
||||
}
|
||||
if (this.story.closeups[c.id].type) return false; // torcia e minigiochi non si sfogliano
|
||||
if (c.step === 'cover') Object.assign(c, { step: 'title', t: 0 });
|
||||
else if (c.step === 'title') Object.assign(c, { step: 'turn', t: 0 });
|
||||
@@ -329,6 +340,12 @@ export class Game {
|
||||
}
|
||||
// Il viaggio si chiude da solo alla fine (o uscendo prima): si arriva comunque.
|
||||
if (def?.type === 'viaggio' || def?.type === 'corsa') { this.queue.unshift(...def.complete); return true; }
|
||||
// L'incantesimo, chiuso prima del lampo, fa comunque il suo cambio.
|
||||
if (def?.type === 'incanto') {
|
||||
if (!c.swapped) for (const e of def.swap ?? []) this.run(e);
|
||||
this.queue.unshift(...(def.complete ?? []));
|
||||
return true;
|
||||
}
|
||||
// I minigiochi: a partita finita (risultato deciso, anche se il conteggio si sta ancora mostrando) la battuta
|
||||
// di chi ha vinto; uscendo prima, quella di chi lascia.
|
||||
if (MINIGAMES[def?.type]) {
|
||||
@@ -430,15 +447,23 @@ export class Game {
|
||||
const talk = p.talk ?? ['talk'];
|
||||
return talk[Math.floor(this.time / 0.3) % talk.length];
|
||||
}
|
||||
// `flourish`: ogni tanto un gesto lungo a più fotogrammi (Baldoria che sventola il bandierone).
|
||||
const f = p.flourish;
|
||||
if (f) {
|
||||
const ft = (this.time + (p.offset ?? 0)) % f.every;
|
||||
if (ft < f.for) return f.poses[Math.floor(ft / (f.step ?? 0.35)) % f.poses.length];
|
||||
}
|
||||
const every = p.every ?? 4, t = (this.time + (p.offset ?? 0)) % every;
|
||||
return p.blink && t > every - 0.25 ? p.blink : idle;
|
||||
}
|
||||
|
||||
// La grandezza a quella altezza della scena; una fase può ingrandire tutti (`scale`: alla notte dell'Ingorgo la folla
|
||||
// dipinta è grande, e Andrea ci si perdeva in mezzo).
|
||||
scaleAt(y) {
|
||||
const d = this.scene.depth;
|
||||
if (!d) return 1;
|
||||
const d = this.scene.depth, k = this.phase()?.scale ?? 1;
|
||||
if (!d) return k;
|
||||
const [[y0, s0], [y1, s1]] = d, t = Math.max(0, Math.min(1, (y - y0) / (y1 - y0)));
|
||||
return s0 + (s1 - s0) * t;
|
||||
return (s0 + (s1 - s0) * t) * k;
|
||||
}
|
||||
|
||||
speakerPoint(who) {
|
||||
@@ -490,6 +515,12 @@ export class Game {
|
||||
if (this.story.closeups[c.id].type === 'torch') { this.updateTorch(dt); return; }
|
||||
if (this.story.closeups[c.id].type === 'viaggio') { this.updateTrip(dt); return; }
|
||||
if (this.story.closeups[c.id].type === 'corsa') { if (updateCorsa(c, this.story.closeups[c.id], dt)) this.closeupClose(); return; }
|
||||
if (this.story.closeups[c.id].type === 'incanto') {
|
||||
const now = updateIncanto(c, dt);
|
||||
if (now === 'swap') for (const e of this.story.closeups[c.id].swap ?? []) this.run(e);
|
||||
if (now === 'end') this.closeupClose();
|
||||
return;
|
||||
}
|
||||
const m = this.minigame();
|
||||
if (m) {
|
||||
m.update(c, dt);
|
||||
@@ -533,6 +564,8 @@ export class Game {
|
||||
this.closeup = type === 'torch' ? { id: e.closeup, step: 'search', t: 0, age: 0, found: 0, dwell: 0, beam: [320, 180] }
|
||||
: type === 'viaggio' ? { id: e.closeup, t: 0, age: 0, events: [], typed: 0 }
|
||||
: type === 'corsa' ? newCorsa(e.closeup)
|
||||
: type === 'incanto' ? newIncanto(e.closeup)
|
||||
: type === 'poster' ? { id: e.closeup, step: 'poster', t: 0, age: 0 }
|
||||
// `level`: «Alla bona», «Seria» o «A fassi male», scelto nel dialogo con l'avversario.
|
||||
// `decks`: il mazzo da usare quando il suo flag è acceso (briscola: le napoletane al posto delle toscane).
|
||||
: MINIGAMES[type] ? { id: e.closeup, ...MINIGAMES[type].create(Math.floor(Math.random() * 4294967296), e.level,
|
||||
@@ -586,6 +619,13 @@ export class Game {
|
||||
this.back = false;
|
||||
this.state.flags['visto:' + scene] = true; // per la mappa
|
||||
this.queue.unshift(...(this.scene.enter ?? []));
|
||||
// Le zone (story.zones): un flag che vale solo in certi posti, come la festa dell'Ingorgo. Uscendo (a piedi o
|
||||
// con la mappa) il flag si spegne e partono le battute del risveglio.
|
||||
for (const z of this.story.zones ?? []) {
|
||||
if (!this.state.flags[z.flag] || z.scenes.includes(scene)) continue;
|
||||
delete this.state.flags[z.flag];
|
||||
this.queue.unshift(...(z.leave ?? []));
|
||||
}
|
||||
this.dirty = true; // si salva quando l'ingresso nella scena è finito
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
// L'incantesimo (story.closeups, type 'incanto'; Andrea, 30 ottobre: «Quanto darei per essere all'Ingorgo stasera?!?
|
||||
// Effetto speciale e compare»). Sopra la scena di adesso, con Andrea dentro: i fasci delle luci da palco che girano
|
||||
// sempre più svelti, i colpi di cassa che fanno pulsare l'aria, il lampo bianco; nel bianco cambia tutto (`swap`, di
|
||||
// solito il flag della festa: lo sfondo diventa quello della notte), poi il bianco sfuma e piovono i coriandoli.
|
||||
// Unità: i pixel della scena 640×360.
|
||||
|
||||
// I tempi, in secondi: i fasci, il lampo che sale, il cambio (a bianco pieno), il bianco che sfuma, la fine.
|
||||
export const T = { beams: 0.2, flash: 1.9, swap: 2.2, clear: 3.0, end: 3.8 };
|
||||
const COLORS = ['255,60,200', '60,220,255', '120,255,90', '255,190,40', '170,90,255'];
|
||||
const clamp01 = k => Math.max(0, Math.min(1, k));
|
||||
// Un numero a caso ma sempre lo stesso per lo stesso indice: i coriandoli non tremano da un fotogramma all'altro.
|
||||
const hash = n => { const x = Math.sin(n * 127.1 + 311.7) * 43758.5453; return x - Math.floor(x); };
|
||||
|
||||
export function newIncanto(id) { return { id, t: 0, age: 0, events: [], beats: 0, swapped: false }; }
|
||||
|
||||
// Il tempo che passa e i suoni (un colpo di cassa a ogni battito, sempre più fitti; il lampo). Torna 'swap' nel
|
||||
// momento del cambio, 'end' alla fine.
|
||||
export function updateIncanto(c, dt) {
|
||||
c.t += dt;
|
||||
const beats = Math.floor(beatAt(Math.min(c.t, T.flash)));
|
||||
for (; c.beats < beats; c.beats++) c.events.push({ kind: 'incanto-cassa', power: 0.5 + 0.5 * c.beats / 8 });
|
||||
if (!c.boom && c.t >= T.flash) { c.boom = true; c.events.push({ kind: 'incanto-lampo', power: 1 }); }
|
||||
if (!c.swapped && c.t >= T.swap) { c.swapped = true; return 'swap'; }
|
||||
return c.t >= T.end ? 'end' : null;
|
||||
}
|
||||
// Quanti battiti sono passati: accelerano, da uno ogni mezzo secondo a uno ogni decimo.
|
||||
export const beatAt = t => t <= 0 ? 0 : 2 * t + 1.6 * t * t;
|
||||
|
||||
// Quanto è bianco lo schermo: sale fino al cambio, resta pieno un attimo, poi sfuma.
|
||||
export function whiteness(t) {
|
||||
if (t < T.flash) return 0;
|
||||
if (t < T.swap) return clamp01((t - T.flash) / (T.swap - T.flash));
|
||||
return clamp01(1 - (t - T.swap) / (T.clear - T.swap));
|
||||
}
|
||||
|
||||
export function drawIncanto(ctx, c) {
|
||||
const t = c.t, before = t < T.swap;
|
||||
ctx.save();
|
||||
// Il colpo di cassa: un velo scuro che si apre e si chiude, l'aria che pulsa.
|
||||
if (before) {
|
||||
const b = beatAt(t), pulse = 1 - (b - Math.floor(b));
|
||||
ctx.fillStyle = `rgba(10,0,30,${0.25 * clamp01(t / 0.6) * pulse})`;
|
||||
ctx.fillRect(0, 0, 640, 360);
|
||||
}
|
||||
// I fasci: partono da sotto, ai lati, e spazzano il cielo; più passa il tempo, più sono e più girano svelti.
|
||||
const strength = before ? clamp01((t - T.beams) / 0.5) : clamp01(1 - (t - T.swap) / 0.6);
|
||||
if (strength > 0) {
|
||||
ctx.globalCompositeOperation = 'lighter';
|
||||
const n = 2 + Math.min(6, Math.floor(t * 3));
|
||||
for (let i = 0; i < n; i++) {
|
||||
const from = [i % 2 ? 560 : 80, 372], speed = 0.9 + i * 0.35 + t * 0.9;
|
||||
const a = -Math.PI / 2 + Math.sin(t * speed + i * 1.7) * 0.9, len = 520, half = 0.07;
|
||||
ctx.fillStyle = `rgba(${COLORS[i % COLORS.length]},${0.22 * strength})`;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(...from);
|
||||
ctx.lineTo(from[0] + Math.cos(a - half) * len, from[1] + Math.sin(a - half) * len);
|
||||
ctx.lineTo(from[0] + Math.cos(a + half) * len, from[1] + Math.sin(a + half) * len);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
}
|
||||
// Le scintille che salgono da terra, a quadratini.
|
||||
for (let i = 0; i < 40; i++) {
|
||||
const life = (t * (0.5 + hash(i)) + hash(i + 50)) % 1, x = 20 + hash(i + 100) * 600, y = 360 - life * 300;
|
||||
ctx.fillStyle = `rgba(${COLORS[i % COLORS.length]},${0.9 * strength * (1 - life)})`;
|
||||
ctx.fillRect(Math.round(x), Math.round(y), 2, 2);
|
||||
}
|
||||
ctx.globalCompositeOperation = 'source-over';
|
||||
}
|
||||
// Il lampo.
|
||||
const white = whiteness(t);
|
||||
if (white > 0) { ctx.fillStyle = `rgba(255,255,250,${white})`; ctx.fillRect(0, 0, 640, 360); }
|
||||
// Dopo il cambio, i coriandoli che cadono sulla festa.
|
||||
if (!before) {
|
||||
const k = (t - T.swap) / (T.end - T.swap);
|
||||
for (let i = 0; i < 70; i++) {
|
||||
const x = (hash(i) * 640 + Math.sin(t * 3 + i) * 8) % 640, y = -10 + (hash(i + 7) * 120 + k * 420 * (0.6 + hash(i + 3) * 0.6));
|
||||
if (y > 360) continue;
|
||||
ctx.fillStyle = `rgba(${COLORS[i % COLORS.length]},${clamp01(1.3 - k)})`;
|
||||
ctx.fillRect(Math.round(x), Math.round(y), 3, 2);
|
||||
}
|
||||
}
|
||||
ctx.restore();
|
||||
}
|
||||
+44
-21
@@ -9,22 +9,39 @@ import { versioned } from '../version.js';
|
||||
// Quale brano: quello della scena se ce l'ha; altrimenti, all'aperto, quello della fase del giorno (la sera ha il suo);
|
||||
// altrimenti quello generale del paese.
|
||||
// Un primo piano con la sua musica (i tavoli da carte, col valzer) la porta con sé finché resta aperto.
|
||||
export function trackFor(story, { screen, scene, phase, closeup }) {
|
||||
// Una scena con le console accese (`stages`, ognuna col suo `when`) non ha un brano solo ma una miscela: ogni console
|
||||
// suona il suo, più forte quanto più Andrea (`at`) le è vicino (mix). Le console possono stare anche fuori dal quadro:
|
||||
// è la musica della piazza accanto, che arriva da lontano.
|
||||
// `ok(cond)` dice se una condizione della storia vale adesso (game.js, check).
|
||||
export function trackFor(story, { screen, scene, phase, closeup, at, ok = () => false }) {
|
||||
const m = story.music ?? {};
|
||||
if (screen === 'splash') return null;
|
||||
if (screen === 'title' || screen === 'intro') return m.title ?? null;
|
||||
if (screen === 'ending') return m.ending ?? m.game ?? null;
|
||||
if (closeup && story.closeups?.[closeup]?.music) return story.closeups[closeup].music;
|
||||
const sc = story.scenes[scene];
|
||||
const stages = (sc?.stages ?? []).filter(st => ok(st.when));
|
||||
if (stages.length) return mix(stages, at ?? sc.entry ?? [320, 300]);
|
||||
return sc?.music ?? (sc?.indoor ? null : phase?.music) ?? m.game ?? null;
|
||||
}
|
||||
|
||||
// Il volume di ogni console: pieno fino a `near` pixel, poi cala fino a `far` (non sotto un filo, `floor`: la festa
|
||||
// si sente sempre). La più forte per prima.
|
||||
export function mix(stages, [x, y]) {
|
||||
return stages.map(st => {
|
||||
const d = Math.hypot(x - st.at[0], (y - st.at[1]) * 1.5), near = st.near ?? 90, far = st.far ?? 520;
|
||||
const k = Math.max(0, Math.min(1, (d - near) / (far - near)));
|
||||
return { src: st.music, gain: Math.max(st.floor ?? 0.08, (1 - k) ** 1.6) };
|
||||
}).sort((a, b) => b.gain - a.gain);
|
||||
}
|
||||
|
||||
const FADE = 1.2;
|
||||
|
||||
// Dove il volume non si può cambiare (iOS), almeno il muto e lo zero funzionano: `muted` lì si rispetta.
|
||||
function setLevel(a) {
|
||||
// Dove il volume non si può cambiare (iOS), almeno il muto e lo zero funzionano: `muted` lì si rispetta. Lì, nella
|
||||
// miscela delle console, suona solo la più forte (`alone`): le altre a volume pieno coprirebbero tutto.
|
||||
function setLevel(a, alone = false) {
|
||||
a.volume = a.level;
|
||||
a.muted = a.level === 0;
|
||||
a.muted = a.level === 0 || (alone && a.level < 1 && Math.abs(a.volume - a.level) > 0.01);
|
||||
}
|
||||
|
||||
export class Music {
|
||||
@@ -33,40 +50,46 @@ export class Music {
|
||||
this.volume = volume;
|
||||
this.blocked = null;
|
||||
this.want = null;
|
||||
this.current = null; // { src, audio }
|
||||
this.old = [];
|
||||
this.tracks = new Map(); // src -> { src, audio, gain }: quelli voluti e quelli che sfumano
|
||||
this.wanted = []; // i src voluti adesso, il più forte per primo
|
||||
this.duck = 1; // 1 = pieno; meno di 1 mentre parla qualcuno
|
||||
this.hold = false; // in pausa: il brano sfuma e si ferma, poi riprende da dov'era
|
||||
this.away = false; // pagina nascosta (sul telefono si è passati a un'altra app): tutto fermo subito
|
||||
}
|
||||
|
||||
// Il brano principale (il più forte) e quelli che stanno sfumando.
|
||||
get current() { return this.tracks.get(this.wanted[0]) ?? null; }
|
||||
get old() { return [...this.tracks.values()].filter(t => !this.wanted.includes(t.src)).map(t => t.audio); }
|
||||
|
||||
// Nascosta la pagina il browser non chiama più update (niente fotogrammi): i brani si fermano qui, di colpo.
|
||||
// Tornati, update li fa ripartire da dov'erano (se non c'è la pausa).
|
||||
setAway(away) {
|
||||
this.away = away;
|
||||
if (away) for (const a of [this.current?.audio, ...this.old]) a?.pause();
|
||||
if (away) for (const t of this.tracks.values()) t.audio.pause();
|
||||
}
|
||||
|
||||
// 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;
|
||||
// Chiamata a ogni fotogramma con il brano voluto (o la miscela delle console, [{ src, gain }]): avvia, cambia e sfuma.
|
||||
update(want, dt) {
|
||||
this.want = want;
|
||||
const list = want == null ? [] : typeof want === 'string' ? [{ src: want, gain: 1 }] : want;
|
||||
this.wanted = list.map(w => w.src);
|
||||
for (const w of list) {
|
||||
const t = this.tracks.get(w.src) ?? { src: w.src, audio: this.start(w.src) };
|
||||
t.gain = w.gain;
|
||||
this.tracks.set(w.src, t);
|
||||
}
|
||||
// Il brano in corso sale (o scende) verso il volume scelto; quelli vecchi sfumano fino a zero, e lì si fermano.
|
||||
// Ogni brano voluto sale (o scende) verso il suo volume; quelli lasciati sfumano fino a zero, e lì si fermano.
|
||||
// Il livello si tiene in `level`, non si rilegge da audio.volume: su iPhone e iPad quello è di sola lettura e
|
||||
// resta sempre 1, e un brano vecchio non arrivava mai a zero (in chiesa l'organo suonava insieme al paese).
|
||||
const step = dt / FADE, target = this.muted || this.hold ? 0 : this.volume * this.duck;
|
||||
if (this.current) {
|
||||
const a = this.current.audio;
|
||||
const step = dt / FADE, full = this.muted || this.hold ? 0 : this.volume * this.duck;
|
||||
for (const t of [...this.tracks.values()]) {
|
||||
const a = t.audio, on = this.wanted.includes(t.src), target = on ? full * t.gain : 0;
|
||||
a.level = a.level < target ? Math.min(target, a.level + step) : Math.max(target, a.level - step);
|
||||
setLevel(a);
|
||||
setLevel(a, list.length > 1 && t.src !== this.wanted[0]);
|
||||
if (!on) { if (a.level === 0) { a.pause(); this.tracks.delete(t.src); } continue; }
|
||||
if (this.hold && a.level === 0 && !a.paused) a.pause();
|
||||
else if (!this.hold && !this.away && a.paused && a !== this.blocked) a.play().catch(() => { this.blocked = a; });
|
||||
}
|
||||
for (const a of this.old) { a.level = Math.max(0, a.level - step); setLevel(a); }
|
||||
this.old = this.old.filter(a => a.level > 0 || (a.pause(), false));
|
||||
}
|
||||
|
||||
start(src) {
|
||||
@@ -81,7 +104,7 @@ export class Music {
|
||||
unlock() {
|
||||
const a = this.blocked;
|
||||
this.blocked = null;
|
||||
if (a && a === this.current?.audio) a.play().catch(() => {});
|
||||
if (a && [...this.tracks.values()].some(t => t.audio === a && this.wanted.includes(t.src))) a.play().catch(() => {});
|
||||
}
|
||||
|
||||
toggle() {
|
||||
|
||||
+28
-2
@@ -1,6 +1,6 @@
|
||||
// Disegno della scena su un canvas 640×360, ingrandito dal CSS senza sfocatura.
|
||||
// Le forme provvisorie (`art`) verranno sostituite da sfondi e sprite in pixel art.
|
||||
import { check, PAGE_TURN } from './game.js';
|
||||
import { check, PAGE_TURN, POSTER_READY } from './game.js';
|
||||
import { ambientEffects, ambientHash } from './ambient.js';
|
||||
import { drawTorch } from './torch.js';
|
||||
import { drawBocce } from './bocce-render.js';
|
||||
@@ -16,6 +16,7 @@ import { drawCondor } from './condor-render.js';
|
||||
import { drawCampana } from './campana-render.js';
|
||||
import { drawPoker } from './poker-render.js';
|
||||
import { drawViaggio } from './viaggio.js';
|
||||
import { drawIncanto } from './incanto.js';
|
||||
import { drawBriscola } from './briscola-render.js';
|
||||
import { drawTressette } from './tressette-render.js';
|
||||
import { drawScopa } from './scopa-render.js';
|
||||
@@ -131,13 +132,15 @@ export async function loadAssets(story, first = story.start.scene, { together =
|
||||
}
|
||||
|
||||
// Gli sfondi in ordine di distanza dalla scena di partenza, seguendo le uscite; in fondo quelle non raggiungibili.
|
||||
// Prima tutti gli sfondi veri, poi le varianti (`backdrops`, come la notte dell'Ingorgo): quando servono prima, la
|
||||
// scena se le carica da sé.
|
||||
export function backgroundOrder(story, first) {
|
||||
const seen = new Set([first]), queue = [first];
|
||||
for (let i = 0; i < queue.length; i++)
|
||||
for (const h of story.scenes[queue[i]]?.hotspots ?? [])
|
||||
if (h.exit && story.scenes[h.exit.scene] && !seen.has(h.exit.scene)) { seen.add(h.exit.scene); queue.push(h.exit.scene); }
|
||||
for (const id of Object.keys(story.scenes)) if (!seen.has(id)) queue.push(id);
|
||||
return [...new Set(queue.flatMap(id => [story.scenes[id].background, ...(story.scenes[id].backdrops ?? []).map(b => b.src)]).filter(Boolean))];
|
||||
return [...new Set([...queue.map(id => story.scenes[id].background), ...queue.flatMap(id => (story.scenes[id].backdrops ?? []).map(b => b.src))].filter(Boolean))];
|
||||
}
|
||||
|
||||
// Il personaggio alto PERSON pixel a grandezza 1 (per `size` 1), con i piedi in (x, y); rivolto a sinistra si specchia.
|
||||
@@ -698,6 +701,29 @@ function drawCloseup(ctx, game, time) {
|
||||
drawCorsa(ctx, c, def, { bg: backgrounds.get(bg), frames, tint: game.phase()?.tint });
|
||||
return;
|
||||
}
|
||||
// L'incantesimo va sopra la scena (con Andrea dentro), già disegnata.
|
||||
if (def.type === 'incanto') { drawIncanto(ctx, c); return; }
|
||||
// Il manifesto ingrandito: la scena si scurisce e lui viene avanti, poi l'invito a toccarlo.
|
||||
if (def.type === 'poster') {
|
||||
if (!backgrounds.has(def.picture)) loadBackground(def.picture);
|
||||
const img = backgrounds.get(def.picture), k = Math.min(1, c.t / POSTER_READY), ease = 1 - (1 - k) ** 3;
|
||||
ctx.fillStyle = `rgba(8, 4, 16, ${0.75 * ease})`;
|
||||
ctx.fillRect(0, 0, WIDTH, HEIGHT);
|
||||
if (img) {
|
||||
const fit = Math.min(600 / img.width, 300 / img.height) * (0.55 + 0.45 * ease), w = img.width * fit, h = img.height * fit;
|
||||
ctx.globalAlpha = ease;
|
||||
ctx.drawImage(img, Math.round((WIDTH - w) / 2), Math.round((HEIGHT - 26 - h) / 2), Math.round(w), Math.round(h));
|
||||
ctx.globalAlpha = 1;
|
||||
}
|
||||
if (k >= 1 && Math.floor(time / 600) % 2 === 0) {
|
||||
ctx.font = '8px "Press Start 2P", monospace';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillStyle = '#ffe9b0';
|
||||
ctx.fillText(def.hint ?? 'Tocca il manifesto', WIDTH / 2, HEIGHT - 12);
|
||||
ctx.textAlign = 'left';
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (def.type === 'torch') {
|
||||
if (!backgrounds.has(def.picture)) loadBackground(def.picture);
|
||||
drawTorch(ctx, c, def, backgrounds.get(def.picture));
|
||||
|
||||
@@ -148,6 +148,9 @@ export class Soundscape {
|
||||
// La macchina del tempo: il tuono del fulmine e lo sfrigolio delle sgommate di fuoco.
|
||||
else if (kind === 'corsa-tuono') { this.noiseSound(0.9, 400, 0.16, { pan }); this.tone(90, 40, 0.8, 0.1, 0, pan); }
|
||||
else if (kind === 'corsa-fuoco') this.noiseSound(1.6, 2400, 0.05, { pan });
|
||||
// L'incantesimo dell'Ingorgo: i colpi di cassa che accelerano, poi il lampo con lo spazzolone del piatto.
|
||||
else if (kind === 'incanto-cassa') this.tone(150, 45, 0.22, 0.12 + strength * 0.1, 0, pan);
|
||||
else if (kind === 'incanto-lampo') { this.noiseSound(1.4, 5200, 0.12, { pan }); this.tone(60, 35, 1.2, 0.16, 0, pan); this.tone(880, 1760, 0.6, 0.05, 0.05, pan); }
|
||||
// La campana: il sassolino che vola e batte sull'asfalto, il salto, il piede giù.
|
||||
else if (kind === 'campana-lancio') this.noiseSound(0.1, 1500, 0.025, { pan });
|
||||
else if (kind === 'campana-sasso') { this.tone(1900, 1100, 0.04, 0.05 * strength + 0.02, 0, pan); this.tone(1500, 900, 0.03, 0.03, 0.06, pan); this.noiseSound(0.04, 3000, 0.03, { pan }); }
|
||||
|
||||
+3
-2
@@ -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, introText, shuffledCredits, matchPlace, reelPictures, facePicture } from './engine/game.js';
|
||||
import { Game, check, VERBS, VERB_LABELS, checkSave, introText, shuffledCredits, matchPlace, reelPictures, facePicture } 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';
|
||||
@@ -1127,7 +1127,8 @@ function frame(now) {
|
||||
// Mentre si parla la musica si abbassa; quando compaiono gli occhi del Regolo quasi si spegne, per la sorpresa.
|
||||
const searching = game?.closeup?.id === 'fessura';
|
||||
music.duck = searching ? game.closeup.found >= 2 ? 0.05 : 0.25 : game?.state.flags.occhi_regolo ? 0.12 : voices.playing ? 0.45 : 1;
|
||||
music.update(trackFor(story, { screen, scene: game?.state.scene, phase: game?.phase(), closeup: game?.closeup?.id }), dt);
|
||||
music.update(trackFor(story, { screen, scene: game?.state.scene, phase: game?.phase(), closeup: game?.closeup?.id,
|
||||
at: game && [game.state.x, game.state.y], ok: cond => !!game && check(cond, game.state) }), dt);
|
||||
// Un minuto senza toccare niente, a gioco fermo: pausa da sola.
|
||||
if (running && now - idleSince > IDLE_PAUSE && !game.speech && !game.walking && !game.queue.length) openPause();
|
||||
ambience.configure({ active: screen === 'game' && running && !document.hidden && $('credits').hidden, muted: music.muted, duck: voices.playing ? 0.4 : 1 });
|
||||
|
||||
+125
-5
@@ -72,6 +72,21 @@ export const story = {
|
||||
{ id: 'pomeriggio' },
|
||||
{ id: 'tramonto', when: 'indizio_regolo', tint: 'rgba(255, 110, 40, 0.16)' },
|
||||
{ id: 'sera', when: 'casetta_aperta', tint: 'rgba(24, 28, 90, 0.42)', music: 'assets/audio/sera.mp3' },
|
||||
// La notte dell'Ingorgo Sonoro (Andrea, 30 ottobre): gli sfondi sono già di notte, il velo serve solo ad Andrea.
|
||||
// La musica la fanno le console delle scene (`stages`).
|
||||
{ id: 'ingorgo', when: 'ingorgo', tint: 'rgba(40, 20, 90, 0.16)', scale: 1.6 },
|
||||
],
|
||||
// Le zone: un flag che vale solo in certi posti. L'Ingorgo Sonoro (Andrea, 30 ottobre: «era la festa del paese
|
||||
// fatta una delle prime sere di luglio, da piazza fino a Cambray Digny, compresa piazza del Comune») si accende
|
||||
// col manifesto in piazza e si spegne uscendo dal centro. [DA ANDREA: le battute sono provvisorie]
|
||||
zones: [
|
||||
{
|
||||
flag: 'ingorgo', scenes: ['piazza', 'via_antica_posta', 'piazzetta_comune', 'antica_posta_alta', 'piazzetta'],
|
||||
leave: [
|
||||
{ say: 'Mi stropiccio gli occhi. La musica non c\'è più: è di nuovo maggio.' },
|
||||
{ say: 'E il palio è sempre sparito. Però che serata.' },
|
||||
],
|
||||
},
|
||||
],
|
||||
// Il taccuino (pulsante «Taccuino», tasto N): le tappe del capitolo in ordine. Una tappa è fatta quando la sua
|
||||
// condizione `done` è vera; la prima non fatta è l'obiettivo di adesso. Solo testo: non si sente a voce.
|
||||
@@ -312,6 +327,27 @@ export const story = {
|
||||
draw: [],
|
||||
quit: [{ say: 'Il gesso lo lascio lì. Ci giocherà qualche bambino, che è più in allenamento di me.' }],
|
||||
},
|
||||
// Il manifesto dell'Ingorgo, ingrandito (Andrea, 30 ottobre: «se mi avvicino compaia ingrandito, e poi parta
|
||||
// l'Ingorgo se ci clicco»). [DA ANDREA: il manifesto vero; questo è di Codex, provvisorio]
|
||||
manifesto_ingorgo: {
|
||||
type: 'poster',
|
||||
picture: 'assets/closeups/manifesto-ingorgo.png',
|
||||
hint: 'Tocca il manifesto',
|
||||
tap: [
|
||||
{ say: 'Il manifesto dell\'Ingorgo Sonoro dell\'anno scorso. Da piazza fino a Cambray Digny, una console in ogni angolo.' },
|
||||
{ say: 'Quanto darei per essere all\'Ingorgo stasera?!?' },
|
||||
{ closeup: 'ingorgo' },
|
||||
],
|
||||
},
|
||||
// L'incantesimo dell'Ingorgo Sonoro, dal manifesto in piazza: luci, cassa, lampo, ed è la notte della festa.
|
||||
ingorgo: {
|
||||
type: 'incanto',
|
||||
swap: [{ set: 'ingorgo' }],
|
||||
complete: [
|
||||
{ say: 'Ma... è l\'Ingorgo! Le console, la gente, le spine della birra!' },
|
||||
{ say: 'Il palio può aspettare una sera. O no?' },
|
||||
],
|
||||
},
|
||||
// Le macchinine sulla pista col gesso, sul parcheggio di Cardetole, contro il Chiaru (Andrea, 28 e 29 ottobre).
|
||||
// [DA ANDREA: le battute sono provvisorie]
|
||||
micro: {
|
||||
@@ -402,6 +438,8 @@ export const story = {
|
||||
// Andrea Tagliaferri, il Taglia (consenso suo, 29 ottobre): sprite di Codex dalla sua foto.
|
||||
taglia: { name: 'Il Taglia', color: '#ffe36a', sprite: 'assets/characters/taglia.json' },
|
||||
barondeduke: { name: 'Il BaronDeDuke', color: '#e0c090', sprite: 'assets/characters/barondeduke.json' },
|
||||
// Baldoria (Yuri Baldi, nei Ringraziamenti), tifoso della Sampdoria col bandierone (Andrea, 30 ottobre; sprite di Codex).
|
||||
baldoria: { name: 'Baldoria', color: '#7fb2ff', sprite: 'assets/characters/baldoria.json' },
|
||||
mariano: { name: 'Mariano', color: '#ffb0a3', sprite: 'assets/characters/mariano.json' },
|
||||
// Non parla: è una cosa, disegnata come un personaggio per stare davanti o dietro ad Andrea.
|
||||
fra_peppino: { name: 'Fra Peppino', color: '#e8c89a', sprite: 'assets/characters/fra-peppino.json' },
|
||||
@@ -543,15 +581,22 @@ export const story = {
|
||||
name: 'Piazza Colonna',
|
||||
map: [32, 45],
|
||||
background: 'assets/scenes/piazza-colonna.png',
|
||||
effects: [{ type: 'clouds', area: [160, 0, 440, 80], count: 3, big: true }, { type: 'windows', phase: 'sera', rects: [[16, 37, 8, 18], [48, 42, 8, 18], [50, 110, 8, 16], [97, 77, 8, 12], [122, 80, 8, 12], [170, 90, 8, 12], [204, 103, 8, 12], [239, 111, 8, 12], [268, 117, 7, 14], [287, 125, 6, 14], [97, 130, 9, 14], [127, 133, 9, 14], [170, 142, 9, 14], [204, 150, 8, 12], [239, 155, 8, 12], [268, 160, 7, 14], [287, 163, 6, 14], [386, 163, 14, 16], [424, 160, 14, 16], [472, 157, 14, 16], [524, 155, 14, 16], [390, 212, 14, 16], [526, 208, 14, 16]] }, { type: 'birds', phase: ['pomeriggio', 'tramonto'], area: [150, 20, 460, 80] }],
|
||||
backdrops: [{ when: 'ingorgo', src: 'assets/scenes/ingorgo-piazza.png' }], // Codex, la notte dell'Ingorgo (30 ottobre)
|
||||
// Le console dell'Ingorgo Sonoro (music.js, mix). [DA ANDREA: quale genere suonava in quale piazza]
|
||||
// La console grande davanti al palazzo; da Via dell'Antica Posta, a sinistra, arriva il latino del Comune.
|
||||
stages: [
|
||||
{ when: 'ingorgo', music: 'assets/audio/ingorgo-house.mp3', at: [195, 235] },
|
||||
{ when: 'ingorgo', music: 'assets/audio/ingorgo-latino.mp3', at: [-160, 250], far: 600 },
|
||||
],
|
||||
effects: [{ type: 'clouds', area: [160, 0, 440, 80], count: 3, big: true, phase: ['pomeriggio', 'tramonto', 'sera'] }, { type: 'windows', phase: 'sera', rects: [[16, 37, 8, 18], [48, 42, 8, 18], [50, 110, 8, 16], [97, 77, 8, 12], [122, 80, 8, 12], [170, 90, 8, 12], [204, 103, 8, 12], [239, 111, 8, 12], [268, 117, 7, 14], [287, 125, 6, 14], [97, 130, 9, 14], [127, 133, 9, 14], [170, 142, 9, 14], [204, 150, 8, 12], [239, 155, 8, 12], [268, 160, 7, 14], [287, 163, 6, 14], [386, 163, 14, 16], [424, 160, 14, 16], [472, 157, 14, 16], [524, 155, 14, 16], [390, 212, 14, 16], [526, 208, 14, 16]] }, { type: 'birds', phase: ['pomeriggio', 'tramonto'], area: [150, 20, 460, 80] }],
|
||||
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: { vecchietta: [24, 250], vecchietto: [56, 244] },
|
||||
// I vecchietti seduti sulla panchina: fotogrammi in assets/characters (pose e battute in game.personPose).
|
||||
people: [
|
||||
{ who: 'vecchietta', at: [24, 317], size: 0.92, talk: ['talk', 'explain'], blink: 'blink', every: 3.7 },
|
||||
{ who: 'vecchietto', at: [56, 304], size: 0.88, talk: ['talk', 'explain'], blink: 'doze', every: 5.3, offset: 2 },
|
||||
{ who: 'vecchietta', at: [24, 317], when: '!ingorgo', size: 0.92, talk: ['talk', 'explain'], blink: 'blink', every: 3.7 },
|
||||
{ who: 'vecchietto', at: [56, 304], when: '!ingorgo', size: 0.88, talk: ['talk', 'explain'], blink: 'doze', every: 5.3, offset: 2 },
|
||||
],
|
||||
hotspots: [
|
||||
{
|
||||
@@ -614,7 +659,7 @@ export const story = {
|
||||
exit: { scene: 'circolo_4_maggio', at: [590, 202], face: 'left' },
|
||||
},
|
||||
{
|
||||
id: 'manifesto', name: 'manifesto del Palio', box: [256, 196, 18, 24], at: [265, 250], face: 'up',
|
||||
id: 'manifesto', name: 'manifesto del Palio', box: [256, 196, 18, 24], at: [265, 250], face: 'up', when: '!ingorgo',
|
||||
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' }],
|
||||
actions: {
|
||||
look: [
|
||||
@@ -625,7 +670,20 @@ export const story = {
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'vecchietti', name: 'vecchietti', box: [0, 250, 72, 52], at: [100, 320], face: 'left', default: 'talk',
|
||||
// Il manifesto dell'Ingorgo Sonoro dell'estate prima, mezzo strappato: guardandolo, Andrea esprime il
|
||||
// desiderio, e la festa arriva (Andrea, 30 ottobre). [DA ANDREA: le battute sono provvisorie]
|
||||
id: 'manifesto_ingorgo', name: 'vecchio manifesto dell\'Ingorgo Sonoro', box: [226, 198, 16, 22], at: [234, 250], face: 'up', when: '!ingorgo',
|
||||
// Come quello vero (assets/raw/riferimenti/manifesto-ingorgo-2014.png): blu, il cartello bianco e rosso col DJ.
|
||||
art: [{ rect: [226, 198, 16, 22], fill: '#1c2050' }, { rect: [233, 201, 2, 2], fill: '#d8262e' }, { rect: [231, 203, 6, 2], fill: '#d8262e' },
|
||||
{ rect: [229, 205, 10, 4], fill: '#d8262e' }, { rect: [232, 205, 4, 3], fill: '#f4f1e8' }, { rect: [228, 211, 12, 2], fill: '#f4f1e8' },
|
||||
{ rect: [229, 215, 7, 2], fill: '#ffd23a' }, { rect: [238, 198, 4, 4], fill: '#cdbb9a' }],
|
||||
actions: {
|
||||
look: [{ closeup: 'manifesto_ingorgo' }],
|
||||
take: [{ say: 'Lo lascio dov\'è: è l\'unica cosa che ricorda luglio, qui, a maggio.' }],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'vecchietti', name: 'vecchietti', box: [0, 250, 72, 52], at: [100, 320], face: 'left', default: 'talk', when: '!ingorgo',
|
||||
actions: {
|
||||
look: [{ say: 'I vecchietti della panchina. Sanno tutto di tutti, soprattutto quello che non è successo.' }],
|
||||
talk: [
|
||||
@@ -1563,6 +1621,13 @@ export const story = {
|
||||
name: 'Piazzetta Cambray Digny',
|
||||
map: [40, 38],
|
||||
background: 'assets/scenes/piazzetta-cambray-digny.png',
|
||||
backdrops: [{ when: 'ingorgo', src: 'assets/scenes/ingorgo-piazzetta.png' }], // Codex, la notte dell'Ingorgo (30 ottobre)
|
||||
// Le console dell'Ingorgo Sonoro (music.js, mix). [DA ANDREA: quale genere suonava in quale piazza]
|
||||
// La tecno davanti alla casetta bianca; l'underground arriva da giù, da Via dell'Antica Posta.
|
||||
stages: [
|
||||
{ when: 'ingorgo', music: 'assets/audio/ingorgo-techno.mp3', at: [495, 245] },
|
||||
{ when: 'ingorgo', music: 'assets/audio/ingorgo-underground.mp3', at: [700, 300], far: 650 },
|
||||
],
|
||||
walk: [[0, 360], [0, 288], [300, 284], [470, 276], [495, 268], [540, 268], [600, 300], [640, 330], [640, 360]],
|
||||
depth: [[268, 0.55], [360, 1.15]],
|
||||
entry: [320, 330],
|
||||
@@ -1600,6 +1665,14 @@ export const story = {
|
||||
name: 'Via dell\'Antica Posta',
|
||||
map: [35, 41],
|
||||
background: 'assets/scenes/via-antica-posta.png',
|
||||
backdrops: [{ when: 'ingorgo', src: 'assets/scenes/ingorgo-antica-posta.png' }], // Codex, la notte dell'Ingorgo (30 ottobre)
|
||||
// Le console dell'Ingorgo Sonoro (music.js, mix). [DA ANDREA: quale genere suonava in quale piazza]
|
||||
// Il bar della birra sui gradini; il latino dal Comune a sinistra, la house giù dalla piazza, l'underground su in cima.
|
||||
stages: [
|
||||
{ when: 'ingorgo', music: 'assets/audio/ingorgo-latino.mp3', at: [-110, 250] },
|
||||
{ when: 'ingorgo', music: 'assets/audio/ingorgo-house.mp3', at: [320, 560] },
|
||||
{ when: 'ingorgo', music: 'assets/audio/ingorgo-underground.mp3', at: [384, 40], far: 600 },
|
||||
],
|
||||
walk: [[0, 360], [0, 300], [120, 290], [220, 252], [300, 240], [372, 226], [396, 226], [470, 252], [630, 300], [640, 360]],
|
||||
depth: [[226, 0.35], [360, 1.15]],
|
||||
entry: [320, 345],
|
||||
@@ -1639,6 +1712,13 @@ export const story = {
|
||||
name: 'Piazzetta del Comune',
|
||||
map: [32, 39],
|
||||
background: 'assets/scenes/piazzetta-comune.png',
|
||||
backdrops: [{ when: 'ingorgo', src: 'assets/scenes/ingorgo-piazzetta-comune.png' }], // Codex, la notte dell'Ingorgo (30 ottobre)
|
||||
// Le console dell'Ingorgo Sonoro (music.js, mix). [DA ANDREA: quale genere suonava in quale piazza]
|
||||
// La console del latino davanti al Comune; la house arriva da giù, dalla piazza.
|
||||
stages: [
|
||||
{ when: 'ingorgo', music: 'assets/audio/ingorgo-latino.mp3', at: [290, 235] },
|
||||
{ when: 'ingorgo', music: 'assets/audio/ingorgo-house.mp3', at: [260, 640], far: 700 },
|
||||
],
|
||||
walk: [[0, 360], [0, 318], [130, 292], [470, 288], [540, 300], [520, 360]],
|
||||
depth: [[288, 0.55], [360, 1.15]],
|
||||
entry: [260, 350],
|
||||
@@ -1686,6 +1766,14 @@ export const story = {
|
||||
name: 'In cima a Via dell\'Antica Posta',
|
||||
map: [38, 39],
|
||||
background: 'assets/scenes/via-antica-posta-alta.png',
|
||||
backdrops: [{ when: 'ingorgo', src: 'assets/scenes/ingorgo-antica-posta-alta.png' }], // Codex, la notte dell'Ingorgo (30 ottobre)
|
||||
// Le console dell'Ingorgo Sonoro (music.js, mix). [DA ANDREA: quale genere suonava in quale piazza]
|
||||
// L'underground sotto il muro della villa; su, in fondo, la tecno di Cambray Digny; giù, lontano, la house.
|
||||
stages: [
|
||||
{ when: 'ingorgo', music: 'assets/audio/ingorgo-underground.mp3', at: [565, 235] },
|
||||
{ when: 'ingorgo', music: 'assets/audio/ingorgo-techno.mp3', at: [270, 120], far: 600 },
|
||||
{ when: 'ingorgo', music: 'assets/audio/ingorgo-house.mp3', at: [240, 640], far: 750 },
|
||||
],
|
||||
walk: [[0, 360], [0, 322], [190, 226], [283, 222], [470, 360]],
|
||||
depth: [[222, 0.35], [360, 1.15]],
|
||||
entry: [240, 350],
|
||||
@@ -1998,7 +2086,19 @@ export const story = {
|
||||
walk: [[0, 298], [640, 298], [640, 360], [0, 360]],
|
||||
depth: [[298, 1.0], [360, 1.2]],
|
||||
entry: [40, 330],
|
||||
// Baldoria sotto la tribuna col bandierone della Sampdoria: ogni tanto lo sventola (Andrea, 30 ottobre).
|
||||
speakers: { baldoria: [490, 196] },
|
||||
people: [{ who: 'baldoria', at: [490, 318], facing: -1, idle: 'stand', talk: ['talk', 'explain'], blink: 'blink', every: 4.3,
|
||||
flourish: { poses: ['wave', 'wave2'], every: 9, for: 2.1, step: 0.35 } }],
|
||||
hotspots: [
|
||||
{
|
||||
id: 'baldoria', name: 'Baldoria', box: [462, 200, 56, 124], at: [430, 326], face: 'right', default: 'talk',
|
||||
actions: {
|
||||
look: [{ say: 'Baldoria, con la maglia blucerchiata e un bandierone della Sampdoria grande come lui. Qui a San Piero.' }],
|
||||
talk: [{ dialogue: 'baldoria' }],
|
||||
take: [{ say: 'Il bandierone? Prima dovrei passare sul suo cadavere. E non è un modo di dire.' }],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'tribuna', name: 'tribuna', box: [254, 172, 116, 40], at: [310, 320], face: 'up',
|
||||
actions: {
|
||||
@@ -3974,6 +4074,26 @@ export const story = {
|
||||
},
|
||||
},
|
||||
// Il Taglia al campone delle poste, con la Condor (Andrea, 29 ottobre). [DA ANDREA: battute provvisorie]
|
||||
// Baldoria al campo sportivo. [DA ANDREA: le battute sono provvisorie]
|
||||
baldoria: {
|
||||
nodes: {
|
||||
start: {
|
||||
options: [
|
||||
{ text: 'Baldoria! Ma che ci fai col bandierone al campo?', then: [
|
||||
{ say: 'Mi alleno. Il bandierone va sventolato tutti i giorni, sennò si arrugginisce il braccio.', who: 'baldoria', pose: 'explain' },
|
||||
{ say: 'Forza Doria!', who: 'baldoria', pose: 'wave' },
|
||||
] },
|
||||
{ text: 'Guarda che qui la Sampdoria non ci gioca.', then: [
|
||||
{ say: 'La Samp gioca dovunque ci sia un blucerchiato. E oggi ci sono io.', who: 'baldoria', pose: 'cheer' },
|
||||
] },
|
||||
{ text: 'Hai visto passare il palio, per caso?', then: [
|
||||
{ say: 'L\'unico palio che conosco è blu, bianco, rosso e nero. E ce l\'ho in mano.', who: 'baldoria', pose: 'laugh' },
|
||||
] },
|
||||
{ text: 'Ciao, Baldoria.', next: 'end' },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
taglia: {
|
||||
nodes: {
|
||||
start: {
|
||||
|
||||
Reference in New Issue
Block a user