Compare commits

...
2 Commits
26 changed files with 163 additions and 59 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", "file": "andrea-3d15dd47bb.mp3",
"sec": 4.82 "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.": { "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", "file": "andrea-8670e6ce99.mp3",
"sec": 7.6 "sec": 7.6
@@ -1675,6 +1671,10 @@
"file": "andrea-f6c10fd559.mp3", "file": "andrea-f6c10fd559.mp3",
"sec": 6.28 "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.": { "andrea|Un pino a ombrello enorme. Alla ricreazione, tutti all'ombra lì sotto. Sembra ieri.": {
"file": "andrea-0a073729fd.mp3", "file": "andrea-0a073729fd.mp3",
"sec": 7.3 "sec": 7.3
@@ -2435,9 +2435,9 @@
"file": "mariano-d65fb6e871.mp3", "file": "mariano-d65fb6e871.mp3",
"sec": 3.42 "sec": 3.42
}, },
"andrea|Si corre?": { "andrea|Si fa una gara alle macchinine?!?!": {
"file": "andrea-8052393bfd.mp3", "file": "andrea-17216997e2.mp3",
"sec": 0.7 "sec": 2.74
}, },
"andrea|Che pista hai disegnato?": { "andrea|Che pista hai disegnato?": {
"file": "andrea-3dd737ed1b.mp3", "file": "andrea-3dd737ed1b.mp3",
@@ -2447,9 +2447,9 @@
"file": "andrea-ee03433b9c.mp3", "file": "andrea-ee03433b9c.mp3",
"sec": 1.6 "sec": 1.6
}, },
"andrea|Dopo, Chiaru.": { "andrea|Vado a fare un girino alle case rosse.": {
"file": "andrea-75b58c99e6.mp3", "file": "andrea-cb2fa74266.mp3",
"sec": 1.32 "sec": 1.97
}, },
"chiaru|Un giro, una spinta a testa, prima te. Tutta fuori dal gesso, si torna indietro. Come la vuoi?": { "chiaru|Un giro, una spinta a testa, prima te. Tutta fuori dal gesso, si torna indietro. Come la vuoi?": {
"file": "chiaru-df4b1e3a0e.mp3", "file": "chiaru-df4b1e3a0e.mp3",
@@ -2475,9 +2475,13 @@
"file": "chiaru-a7ab8d241f.mp3", "file": "chiaru-a7ab8d241f.mp3",
"sec": 2.01 "sec": 2.01
}, },
"chiaru|La pista è qui. Finché non piove.": { "chiaru|Vorrai dire un giochino!": {
"file": "chiaru-841aa1efd7.mp3", "file": "chiaru-c106f9e9c4.mp3",
"sec": 2.39 "sec": 1.14
},
"andrea|Ciao Mattè.": {
"file": "andrea-fc5a08bbd6.mp3",
"sec": 0.73
}, },
"andrea|No, aspetta.": { "andrea|No, aspetta.": {
"file": "andrea-04890c1d8e.mp3", "file": "andrea-04890c1d8e.mp3",
@@ -2499,9 +2503,9 @@
"file": "chiaru-a0d7dbb4c1.mp3", "file": "chiaru-a0d7dbb4c1.mp3",
"sec": 2.15 "sec": 2.15
}, },
"andrea|Trappizzio Berti! Che fai, giochi da solo?": { "andrea|Fabri! Che fai, giochi da solo?": {
"file": "andrea-da5fb02d21.mp3", "file": "andrea-44037129d8.mp3",
"sec": 3.4 "sec": 2.71
}, },
"andrea|Come fai a prendere le biglie dove nessuno ci arriva?": { "andrea|Come fai a prendere le biglie dove nessuno ci arriva?": {
"file": "andrea-c6670ffb48.mp3", "file": "andrea-c6670ffb48.mp3",
@@ -2515,9 +2519,9 @@
"file": "andrea-fe39c8f30c.mp3", "file": "andrea-fe39c8f30c.mp3",
"sec": 1.49 "sec": 1.49
}, },
"andrea|Ci si vede, Trappizzio Berti.": { "andrea|Ci si vede, Fabri.": {
"file": "andrea-4089f348c4.mp3", "file": "andrea-d48ea04521.mp3",
"sec": 2.57 "sec": 1.37
}, },
"bertovo|Da solo? Io gioco contro le sponde. E vinco quasi sempre.": { "bertovo|Da solo? Io gioco contro le sponde. E vinco quasi sempre.": {
"file": "bertovo-5e771069ff.mp3", "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 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 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). [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`. - **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`. 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 - **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", "name": "leggenda-del-regolo",
"version": "0.1.39", "version": "0.1.40",
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "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'; import { text, panel } from './bocce-render.js';
const NAME = { player: 'Andrea', cpu: 'Chiaru' }; 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.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.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 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) // 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. // 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) { export function drawMicro(ctx, c, def, bg, image) {
ctx.save(); ctx.imageSmoothingEnabled = false; ctx.save(); ctx.imageSmoothingEnabled = false;
if (bg) ctx.drawImage(bg, 0, 0, 640, 360); else { ctx.fillStyle = '#4b4d50'; ctx.fillRect(0, 0, 640, 360); } if (bg) ctx.drawImage(bg, 0, 0, 640, 360); else { ctx.fillStyle = '#4b4d50'; ctx.fillRect(0, 0, 640, 360); }
drawChalk(ctx, c.track); 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) { 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]); 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); 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'); text(ctx, 'LA PISTA COL GESSO', 8, 7, 8, '#efd49e');
const level = { bona: 'alla bona', seria: 'seria', male: 'a fassi male' }[c.level]; const level = { bona: 'alla bona', seria: 'seria', male: 'a fassi male' }[c.level];
text(ctx, `${level} · spinte ${c.flicks.player}`, 8, 16, 7, '#d9ccba'); 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'); text(ctx, NAME[who], x0, 11, 8, '#fff0c8');
ctx.fillStyle = '#3a372e'; ctx.fillRect(x0 + 52, 7, 150, 8); 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); 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; 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². // 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; 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 // 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è. // 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); 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 = {}) { export function newMicro(seed = Date.now() >>> 0, def = {}) {
const level = LEVELS[def.level] ? def.level : 'seria', track = TRACK; 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 }, 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 } }; line: null, trail: [], returns: [], before: null, plan: null, visual: { time: 0 } };
// In griglia: Andrea all'esterno, il Chiaru all'interno, poco prima del traguardo. // 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.cars = { player: newCar(track, 'player', 18, -11), cpu: newCar(track, 'cpu', 18, 11) };
c.angle = c.cars.player.heading; 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; 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; 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; 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) { export function nudge(c, dir, fine = false) {
if (!canThrow(c) || c.holding || !Number.isFinite(dir)) return false; if (!canThrow(c) || c.holding || !Number.isFinite(dir)) return false;
c.angle += dir * (fine ? 0.004 : 0.03); return true; 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; if (!canThrow(c) || c.holding || !Number.isFinite(value)) return false;
c.power = clamp(value, 0, 1); return true; 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) { 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; 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.turn = { dir, owner, fine, time: 0 }; c.angle += dir * (fine ? 0.004 : 0.03);
c.aimLocked = true; return true; c.aimLocked = true; return true;
@@ -311,10 +344,11 @@ export function key(c, key, down, fine = false) {
return true; return true;
} }
export function press(c, pointerId) { export function press(c, pointerId) {
if (c.step === 'choose') return pick(c);
if (!canThrow(c) || c.holding) return false; if (!canThrow(c) || c.holding) return false;
c.turn = null; c.pointerId = pointerId; c.holdDelay = pointerId === undefined ? 0 : LONG_PRESS; // Premuto, la forza parte da zero e sale finché non si lascia (Andrea, 29 ottobre): come il dito che si carica.
c.previousPower = c.power; c.holding = true; c.charging = false; c.holdTime = 0; c.turn = null; c.pointerId = pointerId; c.holdDelay = 0;
if (!c.holdDelay) c.power = 0; c.previousPower = c.power; c.holding = true; c.charging = false; c.holdTime = 0; c.power = 0;
return true; return true;
} }
export function cancel(c, pointerId) { export function cancel(c, pointerId) {
@@ -325,7 +359,7 @@ export function cancel(c, pointerId) {
} }
export function release(c, pointerId) { export function release(c, pointerId) {
if (c.pointerId !== pointerId || !c.holding) return false; 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; const accompagne = (c.overHold ?? 0) > ACCOMPAGNA;
c.holding = c.charging = false; c.overHold = 0; c.holding = c.charging = false; c.overHold = 0;
strike(c, 'player', c.angle, c.power); 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 }; 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) { export function button(c) {
if (c.step === 'choose') return 'Scegli';
if (c.step === 'result') return 'Finita'; if (c.step === 'result') return 'Finita';
if (c.step === 'rolling' || c.step === 'settle') return 'In movimento…'; if (c.step === 'rolling' || c.step === 'settle') return 'In movimento…';
return c.current === 'cpu' ? 'Spinge il Chiaru…' : 'Spingi'; 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. // 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 const done = (c, who) => clamp(c.cars[who].progress / (c.laps * c.track.length), 0, 1);
export function status(c) { 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.`; 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 === '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(); if (c.step === 'rolling' || c.step === 'settle') return `${where} ${c.line ?? ''}`.trim();
+7 -6
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). // Le macchinine sulla pista col gesso, sul parcheggio di Cardetole, contro il Chiaru (Andrea, 28 e 29 ottobre).
// [DA ANDREA: le battute sono provvisorie] // [DA ANDREA: le battute sono provvisorie]
micro: { 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', picture: 'assets/closeups/micro-parcheggio.png',
names: { player: 'Andrea', cpu: 'Chiaru' }, 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' }], 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' }], 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' }], quit: [{ say: 'Ti ritiri? Allora la vinco a tavolino.', who: 'chiaru', pose: 'laugh' }],
@@ -3254,7 +3253,7 @@ export const story = {
nodes: { nodes: {
start: { start: {
options: [ 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: [ { 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: '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' }, { say: 'Il tombino l\'ho lasciato in pista: porta fortuna.', who: 'chiaru', pose: 'talk' },
@@ -3265,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: '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' }, { 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: { livello: {
@@ -3280,11 +3280,12 @@ export const story = {
}, },
// Trappizzio Berti al tavolo di mezzo: le biglie nascoste e «Oh tirami questo rinquarto!?» (Andrea, 28 ottobre). // 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] // [DA ANDREA: le battute sono provvisorie, tranne la sua sfida]
// Andrea, parlandogli, lo chiama Fabri (29 ottobre).
bertovo: { bertovo: {
nodes: { nodes: {
start: { start: {
options: [ 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' }, { 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: [ { text: 'Come fai a prendere le biglie dove nessuno ci arriva?', then: [
@@ -3299,7 +3300,7 @@ export const story = {
{ text: 'Si fa una sfida a rinquarti?', next: 'sfida', then: [ { 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' }, { 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: { sfida: {
+1 -1
View File
@@ -1,5 +1,5 @@
// La versione del gioco: la cambia scripts/bump.sh, insieme a package.json. // La versione del gioco: la cambia scripts/bump.sh, insieme a package.json.
export const VERSION = '0.1.39'; export const VERSION = '0.1.40';
// Immagini, musiche e voci con ?v=<versione>: a ogni versione l'indirizzo cambia e il browser le riscarica, // 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). // 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 () => { await page.evaluate(async () => {
const { settle } = await import('/tests/helpers.mjs'); const { settle } = await import('/tests/helpers.mjs');
game.act('talk', 'chiaru'); settle(game); 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); game.choose(game.options().findIndex(o => o.text === 'Alla bona.')); settle(game);
}); });
await page.waitForSelector('#bocce-controls:not([hidden])'); await page.waitForSelector('#bocce-controls:not([hidden])');
await page.waitForFunction(async () => { await page.waitForFunction(async () => {
const { hasBackground } = await import('/src/engine/render.js'); 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'); 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(); 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'); 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 })); 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. // 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]; }); const ahead = await page.evaluate(() => { const c = game.closeup, i = (c.cars.player.idx + 50) % c.track.n; return c.track.pts[i]; });
+39 -17
View File
@@ -1,11 +1,16 @@
import test from 'node:test'; import test from 'node:test';
import { existsSync } from 'node:fs';
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import * as m from '../src/engine/micro.js'; 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 { drawMicro } from '../src/engine/micro-render.js';
import { Game, MINIGAMES } from '../src/engine/game.js'; import { Game, MINIGAMES } from '../src/engine/game.js';
import { story } from '../src/story/story.js'; import { story } from '../src/story/story.js';
import { settle } from './helpers.mjs'; 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. // 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) { function run(c, limit = 20000) {
for (let i = 0; i < limit && !(m.canThrow(c) || c.step === 'result'); i++) m.update(c, 1 / 60); 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]); 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)`); 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)) { for (const car of Object.values(c.cars)) {
assert.ok(!m.isOut(m.project(t, car.x, car.y).d)); assert.ok(!m.isOut(m.project(t, car.x, car.y).d));
assert.ok(car.progress < 0 && car.progress > -30, 'prima del traguardo'); 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', () => { 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); } for (const c of [a, b]) { flick(c); run(c); }
assert.deepEqual(a.cars, b.cars); 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); c.power = 0.6; const g = m.guide(c);
assert.ok(m.shoot(c)); run(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). // 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', () => { 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 c.angle = -Math.PI / 2; c.power = 1; // dritto verso l'erba in alto
assert.equal(m.guide(c).out, true); assert.equal(m.guide(c).out, true);
assert.ok(m.shoot(c)); 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')); 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', () => { 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. // 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 }); 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)); 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); 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.match(t.line, /^Tagliè! Andrea torna dov'era\./);
assert.deepEqual([t.cars.player.x, t.cars.player.y], [sx, sy]); assert.deepEqual([t.cars.player.x, t.cars.player.y], [sx, sy]);
// Cappottè: Andrea a tutta forza contro il Chiaru fermo davanti. // 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)); 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); for (let i = 0; i < 2000 && k.step !== 'settle'; i++) m.update(k, 1 / 60);
assert.equal(him.fault, 'cappotte'); 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. // 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); 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. // 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)); assert.ok(m.press(a));
for (let i = 0; i < 120; i++) m.update(a, 1 / 60); for (let i = 0; i < 120; i++) m.update(a, 1 / 60);
assert.ok(m.overHeld(a)); 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.match(a.line, /^Accompagnè! Andrea torna dov'era\./);
assert.deepEqual([a.cars.player.x, a.cars.player.y], [start.x, start.y]); assert.deepEqual([a.cars.player.x, a.cars.player.y], [start.x, start.y]);
// Lasciando in tempo, la spinta piena vale. // 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); 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); 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', () => { 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 }; const wins = { player: 0, cpu: 0, draw: 0 };
for (const seed of [1, 2, 3]) { 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); } 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.step, 'result');
assert.equal(c.result.flicks.player, c.result.flicks.cpu); 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)})`); 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. // A fassi male il Chiaru fa il giro con meno spinte che alla bona.
const flicks = level => { 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); } 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; 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', () => { 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)); 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; const before = c.cars.player.x;
assert.ok(m.release(c, 1)); assert.equal(c.step, 'rolling'); assert.ok(m.release(c, 1)); assert.equal(c.step, 'rolling');
m.update(c, 0.2); assert.notEqual(c.cars.player.x, before); m.update(c, 0.2); assert.notEqual(c.cars.player.x, before);
assert.equal(m.press(c, 2), false, 'mentre corre non si spinge'); 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); m.key(d, 'ArrowRight', true); m.update(d, 0.5); m.key(d, 'ArrowRight', false);
assert.ok(d.angle > a0); 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'); 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', () => { 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); flick(c);
let worst = 0; 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); } 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', () => { 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 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); }; const draw = () => { const before = structuredClone(c); drawMicro(ctx, c, story.closeups.micro, bg, src => cars[src]); assert.deepEqual(c, before); };
draw(); draw();
assert.ok(calls.some(([op, img]) => op === 'drawImage' && img === bg)); assert.ok(calls.some(([op, img]) => op === 'drawImage' && img === bg));
@@ -173,10 +195,10 @@ test('il Chiaru al parcheggio di Cardetole: si chiacchiera, si sceglie il livell
for (const y of [222, 260, 300, 314]) assert.equal(g.hotspotAt([520, y])?.id, 'chiaru', `clic a y=${y}`); 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); g.act('talk', 'chiaru'); settle(g);
assert.ok(!g.options().some(o => o.text === 'Seria.'), 'il livello non si sceglie subito'); 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); g.choose(g.options().findIndex(o => o.text === 'No, aspetta.')); settle(g);
assert.equal(g.closeup, null); 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); 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'); 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 } }; g.closeup.step = 'result'; g.closeup.result = { winner: 'player', flicks: { player: 11, cpu: 11 } };