Compare commits

...
4 Commits
26 changed files with 173 additions and 66 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+23 -19
View File
@@ -1639,10 +1639,6 @@
"file": "andrea-3d15dd47bb.mp3",
"sec": 4.82
},
"andrea|Il Chiaru, con le ginocchia bianche di gesso e una Formula 1 blu in mano.": {
"file": "andrea-8d5911c8e7.mp3",
"sec": 4.53
},
"andrea|Il parcheggio sotto casa. Col gesso ci si disegnava la pista delle macchinine, e si giocava finché non chiamavano per cena.": {
"file": "andrea-8670e6ce99.mp3",
"sec": 7.6
@@ -1675,6 +1671,10 @@
"file": "andrea-f6c10fd559.mp3",
"sec": 6.28
},
"andrea|Il Chiaru, con le ginocchia bianche di gesso e una Formula 1 blu in mano.": {
"file": "andrea-8d5911c8e7.mp3",
"sec": 4.53
},
"andrea|Un pino a ombrello enorme. Alla ricreazione, tutti all'ombra lì sotto. Sembra ieri.": {
"file": "andrea-0a073729fd.mp3",
"sec": 7.3
@@ -2435,9 +2435,9 @@
"file": "mariano-d65fb6e871.mp3",
"sec": 3.42
},
"andrea|Si corre?": {
"file": "andrea-8052393bfd.mp3",
"sec": 0.7
"andrea|Si fa una gara alle macchinine?!?!": {
"file": "andrea-17216997e2.mp3",
"sec": 2.74
},
"andrea|Che pista hai disegnato?": {
"file": "andrea-3dd737ed1b.mp3",
@@ -2447,9 +2447,9 @@
"file": "andrea-ee03433b9c.mp3",
"sec": 1.6
},
"andrea|Dopo, Chiaru.": {
"file": "andrea-75b58c99e6.mp3",
"sec": 1.32
"andrea|Vado a fare un girino alle case rosse.": {
"file": "andrea-cb2fa74266.mp3",
"sec": 1.97
},
"chiaru|Un giro, una spinta a testa, prima te. Tutta fuori dal gesso, si torna indietro. Come la vuoi?": {
"file": "chiaru-df4b1e3a0e.mp3",
@@ -2475,9 +2475,13 @@
"file": "chiaru-a7ab8d241f.mp3",
"sec": 2.01
},
"chiaru|La pista è qui. Finché non piove.": {
"file": "chiaru-841aa1efd7.mp3",
"sec": 2.39
"chiaru|Vorrai dire un giochino!": {
"file": "chiaru-c106f9e9c4.mp3",
"sec": 1.14
},
"andrea|Ciao Mattè.": {
"file": "andrea-fc5a08bbd6.mp3",
"sec": 0.73
},
"andrea|No, aspetta.": {
"file": "andrea-04890c1d8e.mp3",
@@ -2499,9 +2503,9 @@
"file": "chiaru-a0d7dbb4c1.mp3",
"sec": 2.15
},
"andrea|Trappizzio Berti! Che fai, giochi da solo?": {
"file": "andrea-da5fb02d21.mp3",
"sec": 3.4
"andrea|Fabri! Che fai, giochi da solo?": {
"file": "andrea-44037129d8.mp3",
"sec": 2.71
},
"andrea|Come fai a prendere le biglie dove nessuno ci arriva?": {
"file": "andrea-c6670ffb48.mp3",
@@ -2515,9 +2519,9 @@
"file": "andrea-fe39c8f30c.mp3",
"sec": 1.49
},
"andrea|Ci si vede, Trappizzio Berti.": {
"file": "andrea-4089f348c4.mp3",
"sec": 2.57
"andrea|Ci si vede, Fabri.": {
"file": "andrea-d48ea04521.mp3",
"sec": 1.37
},
"bertovo|Da solo? Io gioco contro le sponde. E vinco quasi sempre.": {
"file": "bertovo-5e771069ff.mp3",
+3
View File
@@ -26,6 +26,9 @@ Online su 50037.manzolo.it tutto fino agli effetti ambientali di Codex. Aperto,
cappottè (urto forte), accompagnè (dito tenuto oltre la carica piena). Tre livelli: alla bona la guida mostra dove
ci si ferma. Test in `tests/micro.test.mjs` e nel browser (`npm run browser:micro`). Battute del Chiaru provvisorie
[DA ANDREA]; il Chiaru ha dato il consenso (29 ottobre).
Poi: si sceglie la macchinina (sei, il Chiaru tiene la blu), la forza parte da zero appena si preme e la spinta
parte quando si lascia; saluto «Vado a fare un girino alle case rosse» / «Vorrai dire un giochino!» / «Ciao Mattè»;
Andrea chiama Bertovo «Fabri». Musica in prestito dal GP (gara.mp3): [DA ANDREA] la sua, con Suno.
- **Bertovo ora si chiama «Trappizzio Berti»** in tutte le scritte (Andrea, 29 ottobre); l'id resta `bertovo`.
Fabrizio Berti e il suo libro «Memorie di sport e di paese» nei Ringraziamenti; l'idea delle cartoline in `IDEE.md`.
- **I rinquarti di Bertovo (28 ottobre, modifiche locali)**: variante del biliardo in `rinquarto.js` e
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "leggenda-del-regolo",
"version": "0.1.38",
"version": "0.1.40",
"private": true,
"type": "module",
"scripts": {
+26 -3
View File
@@ -1,4 +1,4 @@
import { HALF, CAR, canThrow, guide, done, overHeld } from './micro.js';
import { HALF, CAR, CARS, canThrow, guide, done, overHeld, carImage, choiceAt } from './micro.js';
import { text, panel } from './bocce-render.js';
const NAME = { player: 'Andrea', cpu: 'Chiaru' };
@@ -68,6 +68,12 @@ function aim(ctx, c, img) {
ctx.fillStyle = over ? '#ff6a4a' : '#e8b48f'; ctx.strokeStyle = '#6b3d26'; ctx.lineWidth = 1;
ctx.beginPath(); ctx.arc(fx, fy, 4, 0, Math.PI * 2); ctx.fill(); ctx.stroke();
ctx.fillStyle = '#f7d9c4'; ctx.fillRect(Math.round(fx + ux * 1.5) - 1, Math.round(fy + uy * 1.5) - 1, 2, 2); // l'unghia
// La forza che sale mentre si tiene premuto: una barretta sopra la macchinina.
if (c.holding) {
const x = Math.round(me.x - 15), y = Math.round(me.y - 19);
ctx.fillStyle = '#17160fd0'; ctx.fillRect(x - 1, y - 1, 32, 6);
ctx.fillStyle = over ? '#ff6a4a' : c.power > 0.75 ? '#ffd23c' : '#9be07c'; ctx.fillRect(x, y, Math.round(30 * c.power), 4);
}
}
// Dove una macchinina ha fatto il fallo: una croce di gesso rosso che sfuma, e la macchinina (ribaltata, se ha cappottato)
// che sparisce per tornare al suo posto.
@@ -83,11 +89,27 @@ function returns(ctx, c, images) {
}
}
// La scelta della macchinina: le sei in grande sul parcheggio, quella scelta cerchiata di gesso e un po' sollevata.
function choice(ctx, c, image) {
ctx.fillStyle = '#17160fc8'; ctx.fillRect(60, 56, 520, 250);
text(ctx, 'SCEGLI LA TUA MACCHININA', 320, 76, 12, '#efd49e', 'center');
CARS.forEach((car, i) => {
const [x, y] = choiceAt(i), on = i === c.choice, lift = on ? Math.sin(c.visual.time * 5) * 1.5 - 2 : 0, img = image?.(carImage(car.id, true));
if (on) { ctx.strokeStyle = 'rgba(246,246,238,0.9)'; ctx.lineWidth = 2; ctx.beginPath(); ctx.ellipse(x, y, 50, 28, 0, 0, Math.PI * 2); ctx.stroke(); }
ctx.fillStyle = 'rgba(10,12,16,0.35)'; ctx.fillRect(x - 37, y - 16, 78, 38); // l'ombra
if (img) ctx.drawImage(img, Math.round(x - img.width / 2), Math.round(y - img.height / 2 + lift));
text(ctx, car.name, x, y + 34, 8, on ? '#fff0c8' : '#a99d86', 'center');
});
const cpu = CARS[c.choice].id === 'blu' ? 'la rossa' : 'la blu e gialla';
text(ctx, `Il Chiaru prende ${cpu}. Tocca la tua, o frecce e Scegli.`, 320, 292, 8, '#d9ccba', 'center');
}
export function drawMicro(ctx, c, def, bg, image) {
ctx.save(); ctx.imageSmoothingEnabled = false;
if (bg) ctx.drawImage(bg, 0, 0, 640, 360); else { ctx.fillStyle = '#4b4d50'; ctx.fillRect(0, 0, 640, 360); }
drawChalk(ctx, c.track);
const images = { player: image?.(def.cars?.player), cpu: image?.(def.cars?.cpu) };
if (c.step === 'choose') { choice(ctx, c, image); ctx.restore(); return; }
const images = { player: image?.(carImage(c.colors.player)), cpu: image?.(carImage(c.colors.cpu)) };
if (c.step === 'rolling' && c.trail.length > 1) {
ctx.strokeStyle = 'rgba(255,248,220,0.35)'; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(...c.trail[0]);
for (const p of c.trail.slice(1)) ctx.lineTo(...p);
@@ -101,7 +123,8 @@ export function drawMicro(ctx, c, def, bg, image) {
text(ctx, 'LA PISTA COL GESSO', 8, 7, 8, '#efd49e');
const level = { bona: 'alla bona', seria: 'seria', male: 'a fassi male' }[c.level];
text(ctx, `${level} · spinte ${c.flicks.player}`, 8, 16, 7, '#d9ccba');
for (const [who, x0, color] of [['player', 168, '#e0543c'], ['cpu', 404, '#3c66d8']]) {
const tint = id => CARS.find(x => x.id === id).color;
for (const [who, x0, color] of [['player', 168, tint(c.colors.player)], ['cpu', 404, tint(c.colors.cpu)]]) {
text(ctx, NAME[who], x0, 11, 8, '#fff0c8');
ctx.fillStyle = '#3a372e'; ctx.fillRect(x0 + 52, 7, 150, 8);
ctx.fillStyle = color; ctx.fillRect(x0 + 52, 7, Math.round(150 * done(c, who)), 8);
+47 -8
View File
@@ -19,7 +19,7 @@ export const CONTROL = [
];
export const HALF = 22, CAR = { len: 26, wid: 13, r: 6 }, LAPS = 1;
// La spinta: velocità di partenza da MIN_SPEED a MAX_SPEED, e l'asfalto che frena di DECEL px/s².
export const MIN_SPEED = 60, MAX_SPEED = 400, DECEL = 420, SWING = 1.2, LONG_PRESS = 0.6, FIXED_STEP = 1 / 120;
export const MIN_SPEED = 60, MAX_SPEED = 400, DECEL = 420, SWING = 1.2, TAP = 0.14, FIXED_STEP = 1 / 120;
const REST = 0.7, THINK = 1, RESTITUTION = 0.6, OUT = HALF + CAR.r, WINDOW = 30;
// FLIP: la velocità d'urto (px/s) che ribalta la macchinina colpita; ACCOMPAGNA: quanti secondi si può restare alla
// carica piena prima che diventi un accompagnè.
@@ -144,14 +144,43 @@ function step(track, list, dt, events) {
for (const car of list) if (car.vx || car.vy) follow(track, car);
}
// Le macchinine fra cui scegliere (Andrea, 29 ottobre: «quando si parte fammi scegliere la macchinina»): il disegno di
// Codex in `assets/closeups/micro/<id>.png` (e `<id>-grande.png` per la scelta) e il colore della barra in alto.
// Il Chiaru tiene la sua blu e gialla; se la prende Andrea, lui prende la rossa.
export const CARS = [
{ id: 'rossa', name: 'Rossa e bianca', color: '#e0543c' },
{ id: 'blu', name: 'Blu e gialla', color: '#3c66d8' },
{ id: 'nera', name: 'Nera e oro', color: '#c9a23a' },
{ id: 'azzurra', name: 'Bianca e azzurra', color: '#6fb7ff' },
{ id: 'arancio', name: 'Arancione', color: '#f08a2c' },
{ id: 'viola', name: 'Viola e argento', color: '#9a5ad8' },
];
export const carImage = (id, big = false) => `assets/closeups/micro/${id}${big ? '-grande' : ''}.png`;
// Dove stanno le macchinine nella scelta: due file da tre, al centro del parcheggio.
export const choiceAt = i => [160 + (i % 3) * 160, 132 + Math.floor(i / 3) * 88];
export function pick(c, i = c.choice) {
if (c.step !== 'choose' || !CARS[i]) return false;
const mine = CARS[i].id;
c.colors = { player: mine, cpu: mine === 'blu' ? 'rossa' : 'blu' };
c.step = 'aim'; c.t = 0; c.aimLocked = false; // il clic che sceglie non deve fissare anche la mira
return true;
}
export function choose(c, i) {
if (c.step !== 'choose' || !Number.isFinite(i)) return false;
c.choice = (i + CARS.length) % CARS.length; return true;
}
const carUnder = (x, y) => CARS.findIndex((_, i) => { const [cx, cy] = choiceAt(i); return Math.abs(x - cx) < 44 && Math.abs(y - cy) < 30; });
export function newMicro(seed = Date.now() >>> 0, def = {}) {
const level = LEVELS[def.level] ? def.level : 'seria', track = TRACK;
const c = { step: 'aim', t: 0, seed, level, skill: LEVELS[level], current: 'player', power: 0.5, angle: 0,
const c = { step: 'choose', choice: 0, colors: null, t: 0, seed, level, skill: LEVELS[level], current: 'player', power: 0.5, angle: 0,
holding: false, holdTime: 0, accumulator: 0, events: [], track, laps: LAPS, flicks: { player: 0, cpu: 0 },
line: null, trail: [], returns: [], before: null, plan: null, visual: { time: 0 } };
// In griglia: Andrea all'esterno, il Chiaru all'interno, poco prima del traguardo.
c.cars = { player: newCar(track, 'player', 18, -11), cpu: newCar(track, 'cpu', 18, 11) };
c.angle = c.cars.player.heading;
// Si parte scegliendo la macchinina, se non è già data (`def.car`).
if (def.car) pick(c, CARS.findIndex(x => x.id === def.car));
return c;
}
@@ -285,7 +314,10 @@ export function aimAt(c, x, y) {
if (Math.hypot(x - car.x, y - car.y) < CAR.r) return false;
c.angle = Math.atan2(y - car.y, x - car.x); return true;
}
export const aimScreen = (c, [x, y]) => x >= 0 && x <= 640 && y >= 24 && y <= 360 && aimAt(c, x, y);
export function aimScreen(c, [x, y]) {
if (c.step === 'choose') { const i = carUnder(x, y); return i >= 0 && choose(c, i); }
return x >= 0 && x <= 640 && y >= 24 && y <= 360 && aimAt(c, x, y);
}
export function nudge(c, dir, fine = false) {
if (!canThrow(c) || c.holding || !Number.isFinite(dir)) return false;
c.angle += dir * (fine ? 0.004 : 0.03); return true;
@@ -294,8 +326,9 @@ export function setPower(c, value) {
if (!canThrow(c) || c.holding || !Number.isFinite(value)) return false;
c.power = clamp(value, 0, 1); return true;
}
export function shoot(c) { return canThrow(c) && !c.holding && strike(c, 'player', c.angle, c.power); }
export function shoot(c) { return c.step === 'choose' ? pick(c) : canThrow(c) && !c.holding && strike(c, 'player', c.angle, c.power); }
export function turn(c, dir, owner, fine = false) {
if (c.step === 'choose') { choose(c, c.choice + dir); return false; } // nella scelta frecce e pulsanti cambiano macchinina
if (!canThrow(c) || c.holding || c.turn) return false;
c.turn = { dir, owner, fine, time: 0 }; c.angle += dir * (fine ? 0.004 : 0.03);
c.aimLocked = true; return true;
@@ -311,10 +344,11 @@ export function key(c, key, down, fine = false) {
return true;
}
export function press(c, pointerId) {
if (c.step === 'choose') return pick(c);
if (!canThrow(c) || c.holding) return false;
c.turn = null; c.pointerId = pointerId; c.holdDelay = pointerId === undefined ? 0 : LONG_PRESS;
c.previousPower = c.power; c.holding = true; c.charging = false; c.holdTime = 0;
if (!c.holdDelay) c.power = 0;
// Premuto, la forza parte da zero e sale finché non si lascia (Andrea, 29 ottobre): come il dito che si carica.
c.turn = null; c.pointerId = pointerId; c.holdDelay = 0;
c.previousPower = c.power; c.holding = true; c.charging = false; c.holdTime = 0; c.power = 0;
return true;
}
export function cancel(c, pointerId) {
@@ -325,7 +359,7 @@ export function cancel(c, pointerId) {
}
export function release(c, pointerId) {
if (c.pointerId !== pointerId || !c.holding) return false;
if (c.holdTime < Math.max(0.14, c.holdDelay)) return cancel(c, pointerId); // il tocco mira soltanto
if (c.holdTime < TAP) return cancel(c, pointerId); // il tocco secco mira soltanto
const accompagne = (c.overHold ?? 0) > ACCOMPAGNA;
c.holding = c.charging = false; c.overHold = 0;
strike(c, 'player', c.angle, c.power);
@@ -346,7 +380,11 @@ export function guide(c) {
return { x: car.x + ux * length, y: car.y + uy * length, length, out };
}
// Il pulsante del pannello vale anche nella scelta della macchinina.
export const canPress = c => canThrow(c) || c.step === 'choose';
export const canAdjust = canPress;
export function button(c) {
if (c.step === 'choose') return 'Scegli';
if (c.step === 'result') return 'Finita';
if (c.step === 'rolling' || c.step === 'settle') return 'In movimento…';
return c.current === 'cpu' ? 'Spinge il Chiaru…' : 'Spingi';
@@ -354,6 +392,7 @@ export function button(c) {
// Quanta pista manca al traguardo, in percentuale, per il tabellone.
export const done = (c, who) => clamp(c.cars[who].progress / (c.laps * c.track.length), 0, 1);
export function status(c) {
if (c.step === 'choose') return `Scegli la macchinina: ${CARS[c.choice].name}. Tocca quella che vuoi, o frecce e Scegli.`;
const where = `Andrea ${Math.round(done(c, 'player') * 100)}%, Chiaru ${Math.round(done(c, 'cpu') * 100)}% del giro.`;
if (c.step === 'result') return `${where} ${c.result.winner === 'player' ? 'Hai battuto il Chiaru!' : c.result.winner === 'cpu' ? 'Ha vinto il Chiaru.' : 'Arrivati pari.'}`;
if (c.step === 'rolling' || c.step === 'settle') return `${where} ${c.line ?? ''}`.trim();
+15 -13
View File
@@ -236,10 +236,9 @@ export const story = {
// 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: {
type: 'micro',
type: 'micro', music: 'assets/audio/gara.mp3', // in prestito dal GP di Fortezza, finché non c'è la sua
picture: 'assets/closeups/micro-parcheggio.png',
names: { player: 'Andrea', cpu: 'Chiaru' },
cars: { player: 'assets/closeups/micro/rossa.png', cpu: 'assets/closeups/micro/blu.png' },
win: [{ say: 'Hai vinto te. Ma la mia aveva le ruote piene di gesso, sia chiaro.', who: 'chiaru', pose: 'shrug' }, { set: 'micro_vinto' }],
lose: [{ say: 'Visto? La blu non perde mai. Mai.', who: 'chiaru', pose: 'cheer' }],
quit: [{ say: 'Ti ritiri? Allora la vinco a tavolino.', who: 'chiaru', pose: 'laugh' }],
@@ -2781,13 +2780,6 @@ export const story = {
speakers: { chiaru: [520, 214] },
people: [{ who: 'chiaru', at: [520, 318], facing: -1, idle: 'stand', talk: ['talk', 'explain'], blink: 'blink', every: 3.7 }],
hotspots: [
{
id: 'chiaru', name: 'il Chiaru', box: [500, 240, 40, 78], at: [470, 330], face: 'right', default: 'talk',
actions: {
look: [{ say: 'Il Chiaru, con le ginocchia bianche di gesso e una Formula 1 blu in mano.' }],
talk: [{ dialogue: 'chiaru' }],
},
},
{
id: 'parcheggio', name: 'parcheggio', box: [356, 262, 280, 60], at: [450, 330], face: 'right',
actions: {
@@ -2830,6 +2822,14 @@ export const story = {
{
id: 'al_trappola', name: 'a sinistra, dal Trappola', box: [0, 236, 50, 104], at: [20, 352], exit: { scene: 'via_liguri_magelli', at: [264, 202], face: 'down' },
},
// In fondo all'elenco, così sta sopra a parcheggio e casa: il clic sul Chiaru è suo (l'ultimo della lista vince).
{
id: 'chiaru', name: 'il Chiaru', box: [498, 212, 46, 106], at: [470, 330], face: 'right', default: 'talk',
actions: {
look: [{ say: 'Il Chiaru, con le ginocchia bianche di gesso e una Formula 1 blu in mano.' }],
talk: [{ dialogue: 'chiaru' }],
},
},
],
},
// L'ingresso della scuola, giù per la strada dalla salita di Villa Adami: dal cancello alle porte a vetri.
@@ -3253,7 +3253,7 @@ export const story = {
nodes: {
start: {
options: [
{ text: 'Si corre?', next: 'livello', then: [{ say: 'Un giro, una spinta a testa, prima te. Tutta fuori dal gesso, si torna indietro. Come la vuoi?', who: 'chiaru', pose: 'explain' }] },
{ text: 'Si fa una gara alle macchinine?!?!', next: 'livello', then: [{ say: 'Un giro, una spinta a testa, prima te. Tutta fuori dal gesso, si torna indietro. Come la vuoi?', who: 'chiaru', pose: 'explain' }] },
{ text: 'Che pista hai disegnato?', then: [
{ say: 'Rettilineo, curvone, rettilineo, e in fondo la esse. Nella esse chi ha fretta esce.', who: 'chiaru', pose: 'explain' },
{ say: 'Il tombino l\'ho lasciato in pista: porta fortuna.', who: 'chiaru', pose: 'talk' },
@@ -3264,7 +3264,8 @@ export const story = {
{ say: 'Chi taglia la strada: tagliè. Chi si ribalta: cappottè. E chi esce tutto dal gesso, fuori.', who: 'chiaru', pose: 'talk' },
{ say: 'In tutti i casi, si torna dov\'eri prima.', who: 'chiaru', pose: 'shrug' },
] },
{ text: 'Dopo, Chiaru.', next: 'end', then: [{ say: 'La pista è qui. Finché non piove.', who: 'chiaru', pose: 'stand' }] },
// Il saluto (Andrea, 29 ottobre).
{ text: 'Vado a fare un girino alle case rosse.', next: 'end', then: [{ say: 'Vorrai dire un giochino!', who: 'chiaru', pose: 'laugh' }, { say: 'Ciao Mattè.' }] },
],
},
livello: {
@@ -3279,11 +3280,12 @@ export const story = {
},
// Trappizzio Berti al tavolo di mezzo: le biglie nascoste e «Oh tirami questo rinquarto!?» (Andrea, 28 ottobre).
// [DA ANDREA: le battute sono provvisorie, tranne la sua sfida]
// Andrea, parlandogli, lo chiama Fabri (29 ottobre).
bertovo: {
nodes: {
start: {
options: [
{ text: 'Trappizzio Berti! Che fai, giochi da solo?', then: [
{ text: 'Fabri! Che fai, giochi da solo?', then: [
{ say: 'Da solo? Io gioco contro le sponde. E vinco quasi sempre.', who: 'bertovo', pose: 'explain' },
] },
{ text: 'Come fai a prendere le biglie dove nessuno ci arriva?', then: [
@@ -3298,7 +3300,7 @@ export const story = {
{ text: 'Si fa una sfida a rinquarti?', next: 'sfida', then: [
{ say: 'Le biglie le metto io, nascoste per bene. Tiri te, poi tiro io. Oh tirami questo rinquarto!?', who: 'bertovo', pose: 'explain' },
] },
{ text: 'Ci si vede, Trappizzio Berti.', next: 'end', then: [{ say: 'Occhio alle sponde, fuori.', who: 'bertovo', pose: 'shrug' }] },
{ text: 'Ci si vede, Fabri.', next: 'end', then: [{ say: 'Occhio alle sponde, fuori.', who: 'bertovo', pose: 'shrug' }] },
],
},
sfida: {
+1 -1
View File
@@ -1,5 +1,5 @@
// La versione del gioco: la cambia scripts/bump.sh, insieme a package.json.
export const VERSION = '0.1.38';
export const VERSION = '0.1.40';
// Immagini, musiche e voci con ?v=<versione>: a ogni versione l'indirizzo cambia e il browser le riscarica,
// mentre fra una versione e l'altra le tiene in cache a lungo (nginx.conf).
+16 -4
View File
@@ -34,19 +34,31 @@ try {
await page.evaluate(async () => {
const { settle } = await import('/tests/helpers.mjs');
game.act('talk', 'chiaru'); settle(game);
game.choose(game.options().findIndex(o => o.text === 'Si corre?')); settle(game);
game.choose(game.options().findIndex(o => o.text === 'Si fa una gara alle macchinine?!?!')); settle(game);
game.choose(game.options().findIndex(o => o.text === 'Alla bona.')); settle(game);
});
await page.waitForSelector('#bocce-controls:not([hidden])');
await page.waitForFunction(async () => {
const { hasBackground } = await import('/src/engine/render.js');
return hasBackground('assets/closeups/micro-parcheggio.png') && hasBackground('assets/closeups/micro/rossa.png');
return hasBackground('assets/closeups/micro-parcheggio.png');
});
assert.equal(await page.locator('#bocce-throw').textContent(), 'Spingi');
// Prima si sceglie la macchinina: la viola, toccandola.
assert.equal(await page.locator('#bocce-throw').textContent(), 'Scegli');
await page.waitForFunction(async () => (await import('/src/engine/render.js')).hasBackground('assets/closeups/micro/viola-grande.png'));
if (name === 'desktop') { await page.waitForTimeout(1600); await page.screenshot({ path: `${out}/${name}-sceglie.png` }); }
{
const box = await page.locator('#scene').boundingBox(), [vx, vy] = [480, 220];
const at = { x: box.x + vx / 640 * box.width, y: box.y + vy / 360 * box.height };
if (mobile) await page.touchscreen.tap(at.x, at.y); else await page.mouse.click(at.x, at.y);
}
assert.deepEqual(await page.evaluate(() => game.closeup.colors), { player: 'viola', cpu: 'blu' });
await page.waitForTimeout(1600);
if (name === 'desktop') await page.screenshot({ path: `${out}/${name}-scelta.png` });
await page.waitForFunction(() => document.querySelector('#bocce-throw').textContent === 'Spingi');
assert.ok(await page.locator('#micro-help').isVisible(), 'il «Come si gioca» delle macchinine');
const exit = await page.locator('#closeup-close').boundingBox(), canvas = await page.locator('#scene').boundingBox();
assert.ok(exit.y >= canvas.y + canvas.height, 'l\'uscita sta fuori dalla pista');
await page.waitForTimeout(1600);
await page.waitForFunction(async () => (await import('/src/engine/render.js')).hasBackground('assets/closeups/micro/viola.png'));
const state = () => page.evaluate(() => ({ angle: game.closeup.angle, power: game.closeup.power, step: game.closeup.step, x: game.closeup.cars.player.x }));
// Un punto della pista davanti alla macchinina di Andrea, in pixel della pagina.
const ahead = await page.evaluate(() => { const c = game.closeup, i = (c.cars.player.idx + 50) % c.track.n; return c.track.pts[i]; });
+41 -17
View File
@@ -1,11 +1,16 @@
import test from 'node:test';
import { existsSync } from 'node:fs';
import assert from 'node:assert/strict';
import * as m from '../src/engine/micro.js';
import * as micro from '../src/engine/micro.js';
import { drawMicro } from '../src/engine/micro-render.js';
import { Game, MINIGAMES } from '../src/engine/game.js';
import { story } from '../src/story/story.js';
import { settle } from './helpers.mjs';
// Una gara con la macchinina già scelta (la rossa), per provare il resto.
const newMicro = (seed, def = {}) => micro.newMicro(seed, { car: 'rossa', ...def });
// Fa andare la spinta in corso (e quella del Chiaru, se tocca a lui) finché non si torna a mirare o è finita.
function run(c, limit = 20000) {
for (let i = 0; i < limit && !(m.canThrow(c) || c.step === 'result'); i++) m.update(c, 1 / 60);
@@ -44,7 +49,7 @@ test('la pista: chiusa, larga uguale, e le macchinine partono in pista poco prim
const d = Math.hypot(t.pts[i][0] - t.pts[j][0], t.pts[i][1] - t.pts[j][1]);
assert.ok(d > 2 * m.HALF + 2 * m.CAR.r, `tratti troppo vicini: ${i} e ${j} (${d.toFixed(1)} px)`);
}
const c = m.newMicro(1);
const c = newMicro(1);
for (const car of Object.values(c.cars)) {
assert.ok(!m.isOut(m.project(t, car.x, car.y).d));
assert.ok(car.progress < 0 && car.progress > -30, 'prima del traguardo');
@@ -52,10 +57,10 @@ test('la pista: chiusa, larga uguale, e le macchinine partono in pista poco prim
});
test('stesso seme: stessa partita; la spinta va lontano quanto dice la guida', () => {
const a = m.newMicro(4, { level: 'male' }), b = m.newMicro(4, { level: 'male' });
const a = newMicro(4, { level: 'male' }), b = newMicro(4, { level: 'male' });
for (const c of [a, b]) { flick(c); run(c); }
assert.deepEqual(a.cars, b.cars);
const c = m.newMicro(2, { level: 'bona' }), start = { ...c.cars.player };
const c = newMicro(2, { level: 'bona' }), start = { ...c.cars.player };
c.power = 0.6; const g = m.guide(c);
assert.ok(m.shoot(c)); run(c);
// Il Chiaru ha già spinto: la macchinina di Andrea sta dove diceva la guida (se lui non l'ha urtata).
@@ -64,7 +69,7 @@ test('stesso seme: stessa partita; la spinta va lontano quanto dice la guida', (
});
test('tutta fuori: si torna dove si era prima della spinta, e la guida lo diceva', () => {
const c = m.newMicro(3, { level: 'bona' }), start = { x: c.cars.player.x, y: c.cars.player.y };
const c = newMicro(3, { level: 'bona' }), start = { x: c.cars.player.x, y: c.cars.player.y };
c.angle = -Math.PI / 2; c.power = 1; // dritto verso l'erba in alto
assert.equal(m.guide(c).out, true);
assert.ok(m.shoot(c));
@@ -78,9 +83,22 @@ test('tutta fuori: si torna dove si era prima della spinta, e la guida lo diceva
assert.ok(c.events.some(e => e.kind === 'micro-fuori'));
});
test('si parte scegliendo la macchinina: tocco, frecce, pulsante; il Chiaru tiene la blu, o la rossa se la blu è presa', () => {
const c = micro.newMicro(1);
assert.equal(c.step, 'choose'); assert.equal(m.canThrow(c), false); assert.equal(m.button(c), 'Scegli');
assert.ok(m.aimScreen(c, m.choiceAt(4))); assert.equal(c.choice, 4);
assert.equal(m.turn(c, 1, 'x'), false); assert.equal(c.choice, 5, 'la freccia passa alla dopo');
m.turn(c, 1, 'x'); assert.equal(c.choice, 0, 'e ricomincia dalla prima');
assert.ok(m.shoot(c)); assert.deepEqual(c.colors, { player: 'rossa', cpu: 'blu' }); assert.equal(c.step, 'aim');
const d = micro.newMicro(1); m.aimScreen(d, m.choiceAt(1)); assert.ok(m.press(d, 7));
assert.deepEqual(d.colors, { player: 'blu', cpu: 'rossa' });
assert.equal(d.holding, false, 'il tocco che sceglie non carica la spinta');
for (const car of m.CARS) assert.ok(existsSync(new URL('../' + m.carImage(car.id, true), import.meta.url)) && existsSync(new URL('../' + m.carImage(car.id), import.meta.url)));
});
test('le penalità alla francese: tagliè, cappottè, accompagnè; per tutte si torna indietro', () => {
// Tagliè: dal tratto di mezzo della esse, dritti in su attraverso l'erba verso il tratto di sopra.
const t = m.newMicro(1, { level: 'bona' }), [sx, sy] = t.track.pts[Math.round(t.track.n * 0.48)];
const t = newMicro(1, { level: 'bona' }), [sx, sy] = t.track.pts[Math.round(t.track.n * 0.48)];
const p = m.project(t.track, sx, sy); Object.assign(t.cars.player, { x: sx, y: sy, idx: p.i, s: p.s, progress: p.s });
t.angle = -Math.PI / 2; t.power = 0.6; assert.ok(m.shoot(t));
for (let i = 0; i < 2000 && t.step !== 'settle'; i++) m.update(t, 1 / 60);
@@ -88,7 +106,7 @@ test('le penalità alla francese: tagliè, cappottè, accompagnè; per tutte si
assert.match(t.line, /^Tagliè! Andrea torna dov'era\./);
assert.deepEqual([t.cars.player.x, t.cars.player.y], [sx, sy]);
// Cappottè: Andrea a tutta forza contro il Chiaru fermo davanti.
const k = m.newMicro(2, { level: 'bona' }), me = k.cars.player, him = k.cars.cpu, before = { x: him.x, y: him.y };
const k = newMicro(2, { level: 'bona' }), me = k.cars.player, him = k.cars.cpu, before = { x: him.x, y: him.y };
k.angle = Math.atan2(him.y - me.y, him.x - me.x); k.power = 1; assert.ok(m.shoot(k));
for (let i = 0; i < 2000 && k.step !== 'settle'; i++) m.update(k, 1 / 60);
assert.equal(him.fault, 'cappotte');
@@ -97,7 +115,7 @@ test('le penalità alla francese: tagliè, cappottè, accompagnè; per tutte si
// Torna al suo posto; se lì c'è ora Andrea, si scosta appena quanto basta.
assert.ok(Math.hypot(him.x - before.x, him.y - before.y) <= 2 * m.CAR.r + 1e-9);
// Accompagnè: tenuto premuto ben oltre la carica piena.
const a = m.newMicro(3), start = { x: a.cars.player.x, y: a.cars.player.y };
const a = newMicro(3), start = { x: a.cars.player.x, y: a.cars.player.y };
assert.ok(m.press(a));
for (let i = 0; i < 120; i++) m.update(a, 1 / 60);
assert.ok(m.overHeld(a));
@@ -106,7 +124,7 @@ test('le penalità alla francese: tagliè, cappottè, accompagnè; per tutte si
assert.match(a.line, /^Accompagnè! Andrea torna dov'era\./);
assert.deepEqual([a.cars.player.x, a.cars.player.y], [start.x, start.y]);
// Lasciando in tempo, la spinta piena vale.
const ok = m.newMicro(3); m.press(ok);
const ok = newMicro(3); m.press(ok);
for (let i = 0; i < Math.round((m.SWING + 0.2) * 60); i++) m.update(ok, 1 / 60);
m.release(ok); assert.equal(ok.cars.player.fault, null);
});
@@ -114,7 +132,7 @@ test('le penalità alla francese: tagliè, cappottè, accompagnè; per tutte si
test('una partita intera: stesse spinte per tutti e due, un vincitore, e il Chiaru alla bona perde contro chi usa la guida', () => {
const wins = { player: 0, cpu: 0, draw: 0 };
for (const seed of [1, 2, 3]) {
const c = m.newMicro(seed, { level: 'bona' });
const c = newMicro(seed, { level: 'bona' });
for (let n = 0; n < 80 && c.step !== 'result'; n++) { if (m.canThrow(c)) guided(c); run(c); }
assert.equal(c.step, 'result');
assert.equal(c.result.flicks.player, c.result.flicks.cpu);
@@ -124,7 +142,7 @@ test('una partita intera: stesse spinte per tutti e due, un vincitore, e il Chia
assert.ok(wins.player >= 2, `alla bona Andrea, con la guida, deve vincere quasi sempre (${JSON.stringify(wins)})`);
// A fassi male il Chiaru fa il giro con meno spinte che alla bona.
const flicks = level => {
const c = m.newMicro(7, { level });
const c = newMicro(7, { level });
for (let n = 0; n < 80 && c.step !== 'result'; n++) { if (m.canThrow(c)) { c.angle = c.cars.player.heading; c.power = 0; m.shoot(c); } run(c); }
return c.flicks.cpu;
};
@@ -132,14 +150,18 @@ test('una partita intera: stesse spinte per tutti e due, un vincitore, e il Chia
});
test('i comandi: un tocco mira soltanto, tenere premuto carica, lasciare spinge; la mira si gira con le frecce', () => {
const c = m.newMicro(5);
const c = newMicro(5);
assert.ok(m.press(c, 1)); m.update(c, 0.05); assert.ok(m.release(c, 1) === true && c.step === 'aim', 'il tocco breve non spinge');
assert.ok(m.press(c, 1)); for (let i = 0; i < 60; i++) m.update(c, 1 / 60); assert.ok(c.charging);
// Premuto, la forza riparte da zero e sale; un secondo dopo è quasi piena.
assert.ok(m.press(c, 1)); assert.equal(c.power, 0);
m.update(c, 0.3); const early = c.power; assert.ok(c.charging && early > 0.2 && early < 0.3, `forza ${early}`);
for (let i = 0; i < 42; i++) m.update(c, 1 / 60);
assert.ok(c.power > early && c.power < 1);
const before = c.cars.player.x;
assert.ok(m.release(c, 1)); assert.equal(c.step, 'rolling');
m.update(c, 0.2); assert.notEqual(c.cars.player.x, before);
assert.equal(m.press(c, 2), false, 'mentre corre non si spinge');
const d = m.newMicro(5), a0 = d.angle;
const d = newMicro(5), a0 = d.angle;
m.key(d, 'ArrowRight', true); m.update(d, 0.5); m.key(d, 'ArrowRight', false);
assert.ok(d.angle > a0);
assert.ok(m.aimScreen(d, [320, 200])); assert.equal(m.aimScreen(d, [320, 10]), false, 'il tabellone non si mira');
@@ -147,7 +169,7 @@ test('i comandi: un tocco mira soltanto, tenere premuto carica, lasciare spinge;
});
test('il Chiaru pensa a pezzi: nessun fotogramma lo ferma troppo', () => {
const c = m.newMicro(6, { level: 'male' });
const c = newMicro(6, { level: 'male' });
flick(c);
let worst = 0;
for (let i = 0; i < 600 && !m.canThrow(c); i++) { const t = performance.now(); m.update(c, 1 / 60); worst = Math.max(worst, performance.now() - t); }
@@ -156,7 +178,7 @@ test('il Chiaru pensa a pezzi: nessun fotogramma lo ferma troppo', () => {
test('il disegno: gesso, macchinine, guida e risultato; lo stato non cambia', () => {
const calls = [], ctx = new Proxy({}, { get: (o, k) => k in o ? o[k] : (...args) => calls.push([k, ...args]) });
const c = m.newMicro(8, { level: 'bona' }), bg = {}, cars = { [story.closeups.micro.cars.player]: { car: 'player' } };
const c = newMicro(8, { level: 'bona' }), bg = {}, cars = { [m.carImage('rossa')]: { car: 'player' } };
const draw = () => { const before = structuredClone(c); drawMicro(ctx, c, story.closeups.micro, bg, src => cars[src]); assert.deepEqual(c, before); };
draw();
assert.ok(calls.some(([op, img]) => op === 'drawImage' && img === bg));
@@ -169,12 +191,14 @@ test('il disegno: gesso, macchinine, guida e risultato; lo stato non cambia', ()
test('il Chiaru al parcheggio di Cardetole: si chiacchiera, si sceglie il livello e si corre', () => {
assert.ok(MINIGAMES.micro);
const g = new Game(story); g.go('cardetole'); settle(g);
// Il clic su tutto il Chiaru (testa, pancia, gambe) è suo, anche dove sta sopra al parcheggio.
for (const y of [222, 260, 300, 314]) assert.equal(g.hotspotAt([520, y])?.id, 'chiaru', `clic a y=${y}`);
g.act('talk', 'chiaru'); settle(g);
assert.ok(!g.options().some(o => o.text === 'Seria.'), 'il livello non si sceglie subito');
g.choose(g.options().findIndex(o => o.text === 'Si corre?')); settle(g);
g.choose(g.options().findIndex(o => o.text === 'Si fa una gara alle macchinine?!?!')); settle(g);
g.choose(g.options().findIndex(o => o.text === 'No, aspetta.')); settle(g);
assert.equal(g.closeup, null);
g.choose(g.options().findIndex(o => o.text === 'Si corre?')); settle(g);
g.choose(g.options().findIndex(o => o.text === 'Si fa una gara alle macchinine?!?!')); settle(g);
g.choose(g.options().findIndex(o => o.text === 'A fassi male.')); settle(g);
assert.equal(g.closeup?.id, 'micro'); assert.equal(g.closeup.level, 'male');
g.closeup.step = 'result'; g.closeup.result = { winner: 'player', flicks: { player: 11, cpu: 11 } };