diff --git a/assets/characters/betino.json b/assets/characters/betino.json new file mode 100644 index 0000000..7614d34 --- /dev/null +++ b/assets/characters/betino.json @@ -0,0 +1,14 @@ +{ + "image": "betino.png", + "height": 104, + "frames": { + "idle": { + "x": 0, + "y": 0, + "w": 161, + "h": 104, + "ax": 80.5, + "ay": 104 + } + } +} diff --git a/assets/characters/betino.png b/assets/characters/betino.png new file mode 100644 index 0000000..c3b2eda Binary files /dev/null and b/assets/characters/betino.png differ diff --git a/assets/characters/ghiacciomobile.json b/assets/characters/ghiacciomobile.json new file mode 100644 index 0000000..aae96fe --- /dev/null +++ b/assets/characters/ghiacciomobile.json @@ -0,0 +1,14 @@ +{ + "image": "ghiacciomobile.png", + "height": 144, + "frames": { + "idle": { + "x": 0, + "y": 0, + "w": 324, + "h": 144, + "ax": 162, + "ay": 144 + } + } +} diff --git a/assets/characters/ghiacciomobile.png b/assets/characters/ghiacciomobile.png new file mode 100644 index 0000000..d04decc Binary files /dev/null and b/assets/characters/ghiacciomobile.png differ diff --git a/assets/voices/andrea-2acf574bfa.mp3 b/assets/voices/andrea-2acf574bfa.mp3 new file mode 100644 index 0000000..29f4085 Binary files /dev/null and b/assets/voices/andrea-2acf574bfa.mp3 differ diff --git a/assets/voices/andrea-34aad1ffe3.mp3 b/assets/voices/andrea-34aad1ffe3.mp3 new file mode 100644 index 0000000..efb5c62 Binary files /dev/null and b/assets/voices/andrea-34aad1ffe3.mp3 differ diff --git a/assets/voices/andrea-3e85200509.mp3 b/assets/voices/andrea-3e85200509.mp3 new file mode 100644 index 0000000..fc206de Binary files /dev/null and b/assets/voices/andrea-3e85200509.mp3 differ diff --git a/assets/voices/andrea-57375fe2af.mp3 b/assets/voices/andrea-57375fe2af.mp3 new file mode 100644 index 0000000..a3a4cb7 Binary files /dev/null and b/assets/voices/andrea-57375fe2af.mp3 differ diff --git a/assets/voices/andrea-6c6e2fd461.mp3 b/assets/voices/andrea-6c6e2fd461.mp3 new file mode 100644 index 0000000..d1424b2 Binary files /dev/null and b/assets/voices/andrea-6c6e2fd461.mp3 differ diff --git a/assets/voices/andrea-7be29434e0.mp3 b/assets/voices/andrea-7be29434e0.mp3 new file mode 100644 index 0000000..ba95726 Binary files /dev/null and b/assets/voices/andrea-7be29434e0.mp3 differ diff --git a/assets/voices/andrea-c79d2d1c81.mp3 b/assets/voices/andrea-c79d2d1c81.mp3 new file mode 100644 index 0000000..e14a500 Binary files /dev/null and b/assets/voices/andrea-c79d2d1c81.mp3 differ diff --git a/assets/voices/andrea-def44809bc.mp3 b/assets/voices/andrea-def44809bc.mp3 new file mode 100644 index 0000000..d429cbf Binary files /dev/null and b/assets/voices/andrea-def44809bc.mp3 differ diff --git a/assets/voices/index.json b/assets/voices/index.json index 728fd24..3300bfc 100644 --- a/assets/voices/index.json +++ b/assets/voices/index.json @@ -943,6 +943,22 @@ "file": "andrea-00f2e70a4d.mp3", "sec": 2.95 }, + "andrea|Nessuno ha mai capito davvero il tuo immenso valore.": { + "file": "andrea-3e85200509.mp3", + "sec": 2.93 + }, + "andrea|O via, lascialo riposare. A pedalare oggi ci penso io.": { + "file": "andrea-34aad1ffe3.mp3", + "sec": 4.81 + }, + "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 @@ -1523,6 +1539,22 @@ "file": "andrea-60cdae2e35.mp3", "sec": 5.1 }, + "andrea|Mi ricordi come avete fatto quella volta a tornare illesi dopo aver lasciato una fiancata a Campolungo?": { + "file": "andrea-2acf574bfa.mp3", + "sec": 5.36 + }, + "andrea|Ho sentito dire dal Trappola che pensavi di poter attraversare i muri.": { + "file": "andrea-57375fe2af.mp3", + "sec": 3.45 + }, + "andrea|Riposa, Ghiacciomobile.": { + "file": "andrea-c79d2d1c81.mp3", + "sec": 2.19 + }, + "andrea|Ho sentito dire dal Trappola che pensavi di poter attraversare i muri. Ti stimo tanto.": { + "file": "andrea-6c6e2fd461.mp3", + "sec": 5.69 + }, "andrea|Una briscola?": { "file": "andrea-d2f285f610.mp3", "sec": 1.04 diff --git a/src/story/story.js b/src/story/story.js index aa3fb5f..bfd660c 100644 --- a/src/story/story.js +++ b/src/story/story.js @@ -230,6 +230,8 @@ export const story = { // 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' }, macchina: { name: 'La macchina del tempo', color: '#c8d8e8', sprite: 'assets/characters/macchina-tempo.json' }, + betino: { name: 'il Betino', sprite: 'assets/characters/betino.json' }, + ghiacciomobile: { name: 'la Ghiacciomobile', sprite: 'assets/characters/ghiacciomobile.json' }, }, items: { squama: { @@ -1535,8 +1537,31 @@ export const story = { entry: [320, 330], // L'Arcu davanti al circolino, accanto alla fontanella (sprite di Codex, 25 ottobre). speakers: { arcu: [418, 214] }, - people: [{ who: 'arcu', at: [418, 292], facing: -1, idle: 'stand', talk: ['talk', 'explain'], blink: 'blink', every: 4.2 }], + 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 misurano circa 78 e 160 px; restano sopra il bordo di walk. + { who: 'betino', at: [480, 263], size: 0.767, idle: 'idle', talk: [] }, + { who: 'ghiacciomobile', at: [176, 286], size: 0.986, idle: 'idle', talk: [] }, + ], hotspots: [ + { + id: 'betino', name: 'il Betino', box: [441, 212, 78, 51], at: [480, 282], face: 'up', default: 'look', + // Il motorino di Andrea (sprite di Codex, 26 ottobre); la battuta è sua. + actions: { + look: [{ say: 'Nessuno ha mai capito davvero il tuo immenso valore.' }], + use: [{ say: 'O via, lascialo riposare. A pedalare oggi ci penso io.' }], + }, + }, + { + 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: { @@ -2168,6 +2193,16 @@ export const story = { }, }, dialogues: { + // Andrea parla alla Ghiacciomobile (battute di Andrea, 26 ottobre): la macchina resta zitta, e va bene così. + ghiacciomobile: { + nodes: { + start: { options: [ + { text: 'Mi ricordi come avete fatto quella volta a tornare illesi dopo aver lasciato una fiancata a Campolungo?', then: [{ say: 'Mi ricordi come avete fatto quella volta a tornare illesi dopo aver lasciato una fiancata a Campolungo?' }, { say: '...' }] }, + { text: 'Ho sentito dire dal Trappola che pensavi di poter attraversare i muri.', then: [{ say: 'Ho sentito dire dal Trappola che pensavi di poter attraversare i muri. Ti stimo tanto.' }, { say: '...' }] }, + { text: 'Riposa, Ghiacciomobile.', next: 'end' }, + ] }, + }, + }, giocatore_carte: { nodes: { start: { options: [ diff --git a/tests/story.test.mjs b/tests/story.test.mjs index e56999d..e0271ad 100644 --- a/tests/story.test.mjs +++ b/tests/story.test.mjs @@ -182,7 +182,7 @@ test('i personaggi in scena hanno il foglio e tutte le pose che usano', () => { for (const [id, scene] of Object.entries(story.scenes)) for (const p of scene.people ?? []) { assert.ok(story.characters[p.who]?.sprite, `${id}: ${p.who} senza foglio`); - assert.ok(scene.speakers?.[p.who], `${id}: manca il punto del fumetto di ${p.who}`); + if ((p.talk ?? ['talk']).length) assert.ok(scene.speakers?.[p.who], `${id}: manca il punto del fumetto di ${p.who}`); const f = frames(p.who); for (const pose of [p.idle ?? 'sit', p.blink, ...(p.talk ?? ['talk'])].filter(Boolean)) assert.ok(f[pose], `${p.who}: manca ${pose}`); } diff --git a/tests/veicoli.browser.mjs b/tests/veicoli.browser.mjs new file mode 100644 index 0000000..48c1f7d --- /dev/null +++ b/tests/veicoli.browser.mjs @@ -0,0 +1,39 @@ +// Verifica visiva mirata: node tests/veicoli.browser.mjs +// Usa lo stesso Chromium/SwiftShader di browser.mjs, senza titolo e audio. +import { chromium } from 'playwright'; +import { mkdirSync } from 'node:fs'; +import assert from 'node:assert/strict'; +import { staticServer } from './helpers.mjs'; + +const out = process.env.OUT ?? '/tmp/regolo-shots'; +mkdirSync(out, { recursive: true }); +const server = await staticServer(); +let browser; +try { + browser = await chromium.launch({ args: ['--use-gl=angle', '--use-angle=swiftshader', '--enable-unsafe-swiftshader'] }); + const page = await browser.newPage({ viewport: { width: 1280, height: 720 } }); + const errors = []; + page.on('pageerror', e => errors.push(e.message)); + page.on('response', r => { if (r.status() >= 400) errors.push(`${r.status()} ${r.url()}`); }); + await page.route('**/__veicoli', route => route.fulfill({ contentType: 'text/html', body: + '' })); + await page.goto(`http://127.0.0.1:${server.port}/__veicoli`); + await page.evaluate(async () => { + const [{ story }, { Game }, renderer] = await Promise.all([ + import('/src/story/story.js'), import('/src/engine/game.js'), import('/src/engine/render.js'), + ]); + const loaded = await renderer.loadAssets(story, 'campomigliaio_circolo'); + if (loaded.some(ok => !ok)) throw new Error('Uno sfondo/personaggio non è arrivato'); + const game = new Game(story); + game.go('campomigliaio_circolo'); + game.time = 8; + const ctx = document.querySelector('canvas').getContext('2d'); + renderer.render(ctx, game, { time: 8000 }); + }); + assert.deepEqual(errors, []); + await page.screenshot({ path: `${out}/campomigliaio-veicoli.png` }); + console.log(`Schermata: ${out}/campomigliaio-veicoli.png`); +} finally { + await browser?.close(); + server.kill(); +} diff --git a/tests/veicoli.test.mjs b/tests/veicoli.test.mjs new file mode 100644 index 0000000..5a12bc0 --- /dev/null +++ b/tests/veicoli.test.mjs @@ -0,0 +1,57 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { readFileSync, existsSync } from 'node:fs'; +import { story } from '../src/story/story.js'; +import { Game } from '../src/engine/game.js'; +import { inside } from '../src/engine/geometry.js'; +import { settle } from './helpers.mjs'; + +test('i veicoli del circolino restano fuori dal percorso e lasciano liberi gli altri hotspot', () => { + const g = new Game(story); + g.go('campomigliaio_circolo'); + settle(g); + const vehicles = ['betino', 'ghiacciomobile']; + for (const id of vehicles) { + const person = g.people().find(p => p.who === id), h = g.hotspot(id); + const url = new URL('../' + story.characters[id].sprite, import.meta.url); + const atlas = JSON.parse(readFileSync(url, 'utf8')), frame = atlas.frames.idle; + assert.ok(existsSync(new URL(atlas.image, url))); + assert.deepEqual(Object.keys(atlas.frames), ['idle']); + const k = 62 * g.scaleAt(person.at[1]) * person.size / atlas.height; + const width = frame.w * k; + assert.ok(id === 'betino' ? width >= 70 && width <= 80 : width >= 150 && width <= 170); + // Tutto l'ingombro (non soltanto l'ancora) deve restare fuori dall'area calpestabile. + for (let x = h.box[0]; x <= h.box[0] + h.box[2]; x++) + for (let y = h.box[1]; y <= h.box[1] + h.box[3]; y++) + assert.ok(!inside(g.scene.walk, [x, y]), `${id}: invade walk in ${x},${y}`); + for (const other of g.hotspots().filter(o => !vehicles.includes(o.id))) { + const [x, y, w, hgt] = h.box, [ox, oy, ow, oh] = other.box; + assert.ok(x + w <= ox || ox + ow <= x || y + hgt <= oy || oy + oh <= y, + `${id}: copre ${other.id}`); + } + for (const time of [0, 3.9, 4.1, 20]) { + g.time = time; + assert.equal(g.personPose(person), 'idle'); + } + } +}); + +test('guarda e usa i veicoli: parla solo Andrea e non cambia il progresso', () => { + const g = new Game(story); + g.go('campomigliaio_circolo'); + settle(g); + const flags = { ...g.state.flags }, inventory = [...g.state.inventory]; + for (const id of ['betino', 'ghiacciomobile']) { + const h = g.hotspot(id); + assert.equal(g.hotspotAt([h.box[0] + h.box[2] / 2, h.box[1] + h.box[3] / 2]).id, id); + for (const verb of ['look', 'use']) { + assert.ok(g.act(verb, id)); + const said = settle(g); + assert.ok(said.length && said.every(s => s.who === 'andrea')); + assert.deepEqual([g.state.x, g.state.y], h.at); + assert.deepEqual(g.state.flags, flags); + assert.deepEqual(g.state.inventory, inventory); + assert.equal(g.state.scene, 'campomigliaio_circolo'); + } + } +}); diff --git a/tools/pronuncia.json b/tools/pronuncia.json index 2f526fd..1b9a588 100644 --- a/tools/pronuncia.json +++ b/tools/pronuncia.json @@ -5,5 +5,6 @@ "padel": "pàdel", "l'A. Bettini": "l'Abbettini", "da i' Trappola, ai' Barrettino": "dai Trappola, ai Barrettino", - "S'ha andà a fallo": "Si va a farlo" + "S'ha andà a fallo": "Si va a farlo", + "Ghiacciomobile": "Ghiacciomòbile" }