A Campomigliaio, accanto all'Arcu, il Betino (motorino verde e crema con la scritta sul serbatoio) e la Ghiacciomobile (la Corsa nera): sprite di Codex dalle foto di riferimento di Andrea, battute di Andrea (guardandoli, e parlando alla macchina che non risponde), voci; «Ghiacciomòbile» nella pronuncia
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A1dxvtsZx8NE9fwjXUNDBr
This commit is contained in:
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"image": "betino.png",
|
||||||
|
"height": 104,
|
||||||
|
"frames": {
|
||||||
|
"idle": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"w": 161,
|
||||||
|
"h": 104,
|
||||||
|
"ax": 80.5,
|
||||||
|
"ay": 104
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 32 KiB |
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"image": "ghiacciomobile.png",
|
||||||
|
"height": 144,
|
||||||
|
"frames": {
|
||||||
|
"idle": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"w": 324,
|
||||||
|
"h": 144,
|
||||||
|
"ax": 162,
|
||||||
|
"ay": 144
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 59 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -943,6 +943,22 @@
|
|||||||
"file": "andrea-00f2e70a4d.mp3",
|
"file": "andrea-00f2e70a4d.mp3",
|
||||||
"sec": 2.95
|
"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.": {
|
"andrea|Il circolino: il Circolo SMS di Campomigliaio. Un'istituzione, qui i volontari hanno sempre fatto miracoli.": {
|
||||||
"file": "andrea-90bbad0de0.mp3",
|
"file": "andrea-90bbad0de0.mp3",
|
||||||
"sec": 9.59
|
"sec": 9.59
|
||||||
@@ -1523,6 +1539,22 @@
|
|||||||
"file": "andrea-60cdae2e35.mp3",
|
"file": "andrea-60cdae2e35.mp3",
|
||||||
"sec": 5.1
|
"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?": {
|
"andrea|Una briscola?": {
|
||||||
"file": "andrea-d2f285f610.mp3",
|
"file": "andrea-d2f285f610.mp3",
|
||||||
"sec": 1.04
|
"sec": 1.04
|
||||||
|
|||||||
+36
-1
@@ -230,6 +230,8 @@ export const story = {
|
|||||||
// Non parla: è una cosa, disegnata come un personaggio per stare davanti o dietro ad Andrea.
|
// 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' },
|
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' },
|
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: {
|
items: {
|
||||||
squama: {
|
squama: {
|
||||||
@@ -1535,8 +1537,31 @@ export const story = {
|
|||||||
entry: [320, 330],
|
entry: [320, 330],
|
||||||
// L'Arcu davanti al circolino, accanto alla fontanella (sprite di Codex, 25 ottobre).
|
// L'Arcu davanti al circolino, accanto alla fontanella (sprite di Codex, 25 ottobre).
|
||||||
speakers: { arcu: [418, 214] },
|
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: [
|
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',
|
id: 'circolo', name: 'il circolino', box: [100, 0, 380, 205], at: [300, 290], face: 'up',
|
||||||
actions: {
|
actions: {
|
||||||
@@ -2168,6 +2193,16 @@ export const story = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
dialogues: {
|
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: {
|
giocatore_carte: {
|
||||||
nodes: {
|
nodes: {
|
||||||
start: { options: [
|
start: { options: [
|
||||||
|
|||||||
@@ -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 [id, scene] of Object.entries(story.scenes))
|
||||||
for (const p of scene.people ?? []) {
|
for (const p of scene.people ?? []) {
|
||||||
assert.ok(story.characters[p.who]?.sprite, `${id}: ${p.who} senza foglio`);
|
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);
|
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}`);
|
for (const pose of [p.idle ?? 'sit', p.blink, ...(p.talk ?? ['talk'])].filter(Boolean)) assert.ok(f[pose], `${p.who}: manca ${pose}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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:
|
||||||
|
'<style>body{margin:0}canvas{display:block;width:1280px;height:720px;image-rendering:pixelated}</style><canvas width="640" height="360"></canvas>' }));
|
||||||
|
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();
|
||||||
|
}
|
||||||
@@ -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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -5,5 +5,6 @@
|
|||||||
"padel": "pàdel",
|
"padel": "pàdel",
|
||||||
"l'A. Bettini": "l'Abbettini",
|
"l'A. Bettini": "l'Abbettini",
|
||||||
"da i' Trappola, ai' Barrettino": "dai Trappola, ai Barrettino",
|
"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"
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user