diff --git a/assets/scenes/via-liguri-magelli-muro.png b/assets/scenes/via-liguri-magelli-muro.png new file mode 100644 index 0000000..105bd78 Binary files /dev/null and b/assets/scenes/via-liguri-magelli-muro.png differ diff --git a/assets/voices/andrea-7be29434e0.mp3 b/assets/voices/andrea-7be29434e0.mp3 deleted file mode 100644 index ba95726..0000000 Binary files a/assets/voices/andrea-7be29434e0.mp3 and /dev/null differ diff --git a/assets/voices/andrea-a193a43f3e.mp3 b/assets/voices/andrea-a193a43f3e.mp3 new file mode 100644 index 0000000..5e65346 Binary files /dev/null and b/assets/voices/andrea-a193a43f3e.mp3 differ diff --git a/assets/voices/andrea-decbc1c807.mp3 b/assets/voices/andrea-decbc1c807.mp3 new file mode 100644 index 0000000..bc4ad10 Binary files /dev/null and b/assets/voices/andrea-decbc1c807.mp3 differ diff --git a/assets/voices/andrea-def44809bc.mp3 b/assets/voices/andrea-def44809bc.mp3 deleted file mode 100644 index d429cbf..0000000 Binary files a/assets/voices/andrea-def44809bc.mp3 and /dev/null differ diff --git a/assets/voices/index.json b/assets/voices/index.json index 31bb10e..6e8085f 100644 --- a/assets/voices/index.json +++ b/assets/voices/index.json @@ -1187,14 +1187,6 @@ "file": "andrea-00f2e70a4d.mp3", "sec": 2.95 }, - "andrea|La Ghiacciomobile nera. Hai visto cose che noi poveri umani non potremmo neppur lontanamente immaginare.": { - "file": "andrea-7be29434e0.mp3", - "sec": 7.25 - }, - "andrea|Con questo sole, di ghiaccio c'è rimasto giusto il nome. Vo a piedi.": { - "file": "andrea-def44809bc.mp3", - "sec": 5.08 - }, "andrea|Il circolino: il Circolo SMS di Campomigliaio. Un'istituzione, qui i volontari hanno sempre fatto miracoli.": { "file": "andrea-90bbad0de0.mp3", "sec": 9.59 @@ -1627,6 +1619,14 @@ "file": "andrea-7c17aa384c.mp3", "sec": 3.2 }, + "andrea|La Ghiacciomobile nera, mezza dentro il muro. Hai visto cose che noi poveri umani non potremmo neppur lontanamente immaginare.": { + "file": "andrea-decbc1c807.mp3", + "sec": 9.49 + }, + "andrea|Per tirarla fuori di lì ci vuole altro che le mie braccia.": { + "file": "andrea-a193a43f3e.mp3", + "sec": 3.78 + }, "andrea|Nessuno ha mai capito davvero il tuo immenso valore.": { "file": "andrea-3e85200509.mp3", "sec": 2.93 diff --git a/docs/IMMAGINI.md b/docs/IMMAGINI.md index d62ccee..4cf4bbb 100644 --- a/docs/IMMAGINI.md +++ b/docs/IMMAGINI.md @@ -31,6 +31,7 @@ ne è un'opera derivata. Le foto scattate da Andrea non hanno problemi; quelle p | `provinciale-pignone.png`, `piazzetta-cambray-digny.png`, `via-cipriani.png`, `via-massorondinaio.png`, `mozzete.png`, `ponticino-mozzete.png`, `coldaia.png`, `falegnameria-coldaia.png`, `gabbiano.png` | Codex | screenshot di Google Street View | Google | **no**: da rifare con foto di Andrea | | `closeups/micro-parcheggio.png`, `closeups/micro/*.png` (macchinine) | Codex, solo testo | nessuna | — | sì | | `characters/chiaru.png` | Codex | `foto/chiaru.jpg` | Andrea (29 ottobre) | sì (il Chiaru consenziente) | +| `via-liguri-magelli-muro.png` | Codex, ritocco di `via-liguri-magelli.png` con la Ghiacciomobile nel muro | come `via-liguri-magelli.png` | Street View | **no**: come lo sfondo da cui viene | | `cardetole.png` | Codex | `cardetole.jpg` | Andrea (EXIF) | **da decidere**: è la casa vera di Andrea, pubblicarla dice dove abita | | `pieve-interno.png` | Codex | `pieve-interno.png` | Mariano Mirannalti (mandata ad Andrea su WhatsApp) | sì, se Mariano è d'accordo; citarlo nei crediti | | `panorama-intro.png` | Codex | `altre/panorama-intro.png` | da internet (screenshot con l'icona di Google Lens) | **no**, finché non si sostituisce | diff --git a/docs/STATO.md b/docs/STATO.md index 79f0d5d..6d33782 100644 --- a/docs/STATO.md +++ b/docs/STATO.md @@ -18,6 +18,11 @@ Online su 50037.manzolo.it tutto fino agli effetti ambientali di Codex. Aperto, ## Dove siamo arrivati +- **La Ghiacciomobile nel muro (29 ottobre)**: non è più al circolino di Campomigliaio. In Via Liguri Magelli, prima + del tramonto (`!indizio_regolo`), è schiantata dentro il muro giallo coi mattoni rotti e senza targa + (`via-liguri-magelli-muro.png`, ritocco di Codex); dal tramonto il muro è rifatto e c'è la targa. Il motore ha le + varianti di sfondo per scena (`backdrops`, con `when`; `game.backdrop()`). [DA ANDREA] lo zio Mario che guarda + come tirarla fuori, e la battuta dello schianto. - **GP di Fortezza, guida semplice (29 ottobre)**: alla bona e Seria si tiene premuto per il gas e lasciando si frena; lo sterzo segue la strada da solo (scansa le buche, sorpassa, prende la miscela), ma oltre la velocità che la curva regge il motorino scivola fuori verso la paglia (`SIMPLE` in `gara.js`). Ginocchio in terra resta con lo diff --git a/src/engine/game.js b/src/engine/game.js index 51b4b29..271b72f 100644 --- a/src/engine/game.js +++ b/src/engine/game.js @@ -162,6 +162,9 @@ export class Game { } get scene() { return this.story.scenes[this.state.scene]; } + // Lo sfondo di adesso: la prima variante della scena (`backdrops`, ognuna con la sua condizione `when`) che vale, + // sennò il suo `background`. Così un posto cambia con la storia (la Ghiacciomobile nel muro, poi la targa). + backdrop(scene = this.scene) { return scene.backdrops?.find(b => check(b.when, this.state))?.src ?? scene.background; } // Occupato: c'è qualcosa in corso che il giocatore non può interrompere. La camminata verso un punto // o verso un oggetto si può sempre cambiare con un altro clic: la coda che la seguiva viene scartata. get busy() { return !!this.speech || !!this.closeup || this.reach > 0 || (this.task ? !this.task.interruptible : this.queue.length > 0); } diff --git a/src/engine/render.js b/src/engine/render.js index 6276639..6ab6e9c 100644 --- a/src/engine/render.js +++ b/src/engine/render.js @@ -131,7 +131,7 @@ export function backgroundOrder(story, first) { 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.map(id => story.scenes[id].background).filter(Boolean))]; + return [...new Set(queue.flatMap(id => [story.scenes[id].background, ...(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. @@ -153,8 +153,8 @@ function drawSprite(ctx, { atlas, img }, frame, x, y, scale, facing, size = 1) { } export function render(ctx, game, { hover, spots, debug, time = performance.now() } = {}) { - const scene = game.scene, s = game.state, bg = backgrounds.get(scene.background); - if (!bg && scene.background) loadBackground(scene.background); + const scene = game.scene, s = game.state, backdrop = game.backdrop(), bg = backgrounds.get(backdrop); + if (!bg && backdrop) loadBackground(backdrop); ctx.imageSmoothingEnabled = false; ctx.clearRect(0, 0, WIDTH, HEIGHT); if (bg) ctx.drawImage(bg, 0, 0, WIDTH, HEIGHT); @@ -395,7 +395,7 @@ export const EFFECTS = { // Alberi al vento (i cipressi): il pezzo di sfondo si piega tutto insieme dalla base, di frazioni di pixel e con // due onde lente sovrapposte, così il movimento è morbido e non a scatti. sway(ctx, e, time, game) { - const bg = backgrounds.get(game.scene.background); + const bg = backgrounds.get(game.backdrop()); if (!bg) return; const k = bg.width / WIDTH, amount = e.amount ?? 3.2; for (const [i, [x, y, w, h]] of e.rects.entries()) { @@ -412,7 +412,7 @@ export const EFFECTS = { // ognuna a una sua altezza e velocità, uscita da un lato rientra dall'altro. Si vedono solo dove lo sfondo è // cielo (skyMask): passano dietro alberi, torri e tetti. Le due grandi solo coi cieli larghi (`big`). clouds(ctx, e, time, game) { - const bg = backgrounds.get(game.scene.background), mask = bg && skyMask(bg); + const bg = backgrounds.get(game.backdrop()), mask = bg && skyMask(bg); if (!mask) return; const layer = scratch(), lc = layer.getContext('2d'), [ax, ay, aw, ah] = e.area; const pool = e.big ? CLOUDS : CLOUDS.slice(2); @@ -452,7 +452,7 @@ export const EFFECTS = { // Una bandiera dipinta nello sfondo che sventola: a strisce verticali che salgono e scendono, ferme dalla parte // dell'asta (`pole`: 'left' o 'right') e sempre più mosse verso il lembo libero. cloth(ctx, e, time, game) { - const bg = backgrounds.get(game.scene.background); + const bg = backgrounds.get(game.backdrop()); if (!bg) return; const k = bg.width / WIDTH; for (const [i, [x, y, w, h]] of e.rects.entries()) { diff --git a/src/main.js b/src/main.js index 1a50fdb..14a3798 100644 --- a/src/main.js +++ b/src/main.js @@ -1099,7 +1099,7 @@ function frame(now) { ambience.update(soundscapeFor(game?.state.scene, game?.closeup?.id), game?.phase()?.id, dt); // A gioco fermo (titolo, pausa, fine) la scena non cambia: un ultimo disegno e poi niente, per la batteria del // telefono. Si ridisegna solo se nel frattempo arriva lo sfondo che mancava. - const still = game && !running && `${game.state.scene}|${hasBackground(game.scene.background)}`; + const still = game && !running && `${game.state.scene}|${hasBackground(game.backdrop())}`; if (game && (!still || still !== drawnStill)) { if (running) game.update(dt); render(ctx, game, { hover: debug && hoverTarget ? game.hotspot(hoverTarget) : null, spots: running && now < spotsUntil, debug: debugView() }); diff --git a/src/story/story.js b/src/story/story.js index d666608..4141dc5 100644 --- a/src/story/story.js +++ b/src/story/story.js @@ -2116,23 +2116,11 @@ export const story = { entry: [320, 330], // L'Arcu davanti al circolino, accanto alla fontanella (sprite di Codex, 25 ottobre). speakers: { arcu: [418, 214] }, + // La Ghiacciomobile non è più qui: sta nel muro di Via Liguri Magelli (Andrea, 29 ottobre). people: [ { who: 'arcu', at: [418, 292], facing: -1, idle: 'stand', talk: ['talk', 'explain'], blink: 'blink', every: 4.2 }, - // Oggetti statici: ordinati per profondità come la macchina del tempo, senza voce. - // Con la depth della scena misura circa 160 px; resta sopra il bordo di walk. (Il Betino dell'Arcu sta in - // cima alla salita di Via Liguri Magelli, Andrea 28 ottobre.) - { who: 'ghiacciomobile', at: [176, 286], size: 0.986, idle: 'idle', talk: [] }, ], hotspots: [ - { - id: 'ghiacciomobile', name: 'la Ghiacciomobile', box: [96, 215, 160, 71], at: [240, 308], face: 'up', default: 'look', - // La Corsa nera degli amici (sprite di Codex, 26 ottobre). Parlandole, Andrea ricorda; lei non risponde. - actions: { - look: [{ say: 'La Ghiacciomobile nera. Hai visto cose che noi poveri umani non potremmo neppur lontanamente immaginare.' }], - talk: [{ dialogue: 'ghiacciomobile' }], - use: [{ say: 'Con questo sole, di ghiaccio c\'è rimasto giusto il nome. Vo a piedi.' }], - }, - }, { id: 'circolo', name: 'il circolino', box: [100, 0, 380, 205], at: [300, 290], face: 'up', actions: { @@ -2710,6 +2698,9 @@ export const story = { name: 'Via Liguri Magelli', map: [76, 97], background: 'assets/scenes/via-liguri-magelli.png', + // All'inizio la Ghiacciomobile è dentro il muro giallo, coi mattoni rotti (Codex, 29 ottobre); dal tramonto + // (le impronte del Regolo) il muro è rifatto e c'è la sua targa (Andrea, 29 ottobre). + backdrops: [{ when: '!indizio_regolo', src: 'assets/scenes/via-liguri-magelli-muro.png' }], // La strada gira intorno all'Audi nera parcheggiata davanti al Trappola. walk: [[0, 360], [0, 320], [140, 262], [226, 198], [272, 198], [300, 242], [370, 246], [400, 252], [580, 318], [640, 330], [640, 360]], depth: [[198, 0.3], [360, 1.1]], @@ -2745,11 +2736,21 @@ export const story = { }, }, { - id: 'targa', name: 'targa della Ghiacciomobile', box: [570, 130, 62, 78], at: [565, 336], face: 'right', default: 'look', + id: 'targa', name: 'targa della Ghiacciomobile', box: [570, 130, 62, 78], at: [565, 336], face: 'right', default: 'look', when: 'indizio_regolo', actions: { look: [{ say: '«Qui riposa la Ghiacciomobile, che si è schiantata lì.»' }, { say: 'C\'è anche la fotina. Pareva sempre nuova.' }], }, }, + { + // La Ghiacciomobile nel muro (prima del tramonto). Le battute erano del circolino (Andrea, 26 ottobre); + // [DA ANDREA] quella dello schianto. Parlandole, Andrea ricorda; lei non risponde. + id: 'ghiacciomobile', name: 'la Ghiacciomobile', box: [462, 192, 140, 92], at: [440, 300], face: 'right', default: 'look', when: '!indizio_regolo', + actions: { + look: [{ say: 'La Ghiacciomobile nera, mezza dentro il muro. Hai visto cose che noi poveri umani non potremmo neppur lontanamente immaginare.' }], + talk: [{ dialogue: 'ghiacciomobile' }], + use: [{ say: 'Per tirarla fuori di lì ci vuole altro che le mie braccia.' }], + }, + }, { // La prima battuta è di Andrea (dal circolino, 26 ottobre). id: 'betino', name: 'il Betino dell\'Arcu', box: [232, 172, 40, 24], at: [250, 206], face: 'up', default: 'look', @@ -2764,7 +2765,7 @@ export const story = { }, { // Il campo del Giannini è laggiù a destra, oltre la siepe (Andrea, 28 ottobre). - id: 'al_giannini', name: 'verso il campo del Giannini', box: [430, 150, 132, 100], at: [520, 306], exit: { scene: 'campo_giannini', at: [60, 250], face: 'right' }, + id: 'al_giannini', name: 'verso il campo del Giannini', box: [404, 150, 58, 100], at: [520, 306], exit: { scene: 'campo_giannini', at: [60, 250], face: 'right' }, }, ], }, diff --git a/tests/story.test.mjs b/tests/story.test.mjs index 5aeb400..b939b91 100644 --- a/tests/story.test.mjs +++ b/tests/story.test.mjs @@ -437,7 +437,7 @@ test('interrompere la catena del ghiacciolo non perde la torcia né la chiavetta test('gli sfondi si caricano dalla scena di partenza verso le più lontane, tutti e una volta sola', () => { const order = backgroundOrder(story, story.start.scene); - const all = new Set(Object.values(story.scenes).map(s => s.background).filter(Boolean)); + const all = new Set(Object.values(story.scenes).flatMap(s => [s.background, ...(s.backdrops ?? []).map(b => b.src)]).filter(Boolean)); assert.equal(order.length, all.size); assert.deepEqual(new Set(order), all); assert.equal(order[0], story.scenes[story.start.scene].background, 'prima la piazza'); diff --git a/tests/veicoli.test.mjs b/tests/veicoli.test.mjs index feb6883..9d784a8 100644 --- a/tests/veicoli.test.mjs +++ b/tests/veicoli.test.mjs @@ -6,9 +6,10 @@ import { Game } from '../src/engine/game.js'; import { inside } from '../src/engine/geometry.js'; import { settle } from './helpers.mjs'; -// La Ghiacciomobile al circolino è uno sprite; il Betino in cima alla salita di Via Liguri Magelli è dipinto nel quadro. -const SPRITES = [['campomigliaio_circolo', 'ghiacciomobile', [150, 170]]]; -const VEHICLES = [...SPRITES, ['via_liguri_magelli', 'betino']]; +// I veicoli sono dipinti nel quadro: il Betino in cima alla salita di Via Liguri Magelli e, prima del tramonto, la +// Ghiacciomobile dentro il muro giallo (Andrea, 29 ottobre). Nessuno è più uno sprite. +const SPRITES = []; +const VEHICLES = [['via_liguri_magelli', 'betino'], ['via_liguri_magelli', 'ghiacciomobile']]; test('i veicoli restano fuori dal percorso e lasciano liberi gli altri hotspot', () => { for (const [scene, id, [lo, hi]] of SPRITES) { const g = new Game(story); @@ -62,3 +63,15 @@ test('guarda e usa i veicoli: parla solo Andrea e non cambia il progresso', () = } } }); + +test('Via Liguri Magelli: prima la Ghiacciomobile nel muro (senza targa), dal tramonto il muro rifatto con la targa', () => { + const g = new Game(story); + g.go('via_liguri_magelli'); settle(g); + assert.equal(g.backdrop(), 'assets/scenes/via-liguri-magelli-muro.png'); + assert.ok(g.hotspot('ghiacciomobile') && !g.hotspot('targa')); + assert.ok(!story.scenes.campomigliaio_circolo.hotspots.some(h => h.id === 'ghiacciomobile'), 'al circolino non c\'è più'); + g.state.flags.indizio_regolo = true; + assert.equal(g.backdrop(), 'assets/scenes/via-liguri-magelli.png'); + assert.ok(g.hotspot('targa') && !g.hotspot('ghiacciomobile')); + assert.ok(existsSync(new URL('../assets/scenes/via-liguri-magelli-muro.png', import.meta.url))); +});