Il Boccino (Gabriele Chiesi, consenso del 31 ottobre) corre il GP di Fortezza col suo NRG bianco e rosso numero 1 (motorino e ritratto di Codex dalle sue foto) (Andrea)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GiECd9RWGMKV7n3K1mdy8T
This commit is contained in:
@@ -26,12 +26,16 @@ try {
|
||||
});
|
||||
await page.waitForSelector('#bocce-controls:not([hidden])');
|
||||
await page.waitForFunction(async () => (await import('/src/engine/render.js')).hasBackground('assets/closeups/river-flipper.png'));
|
||||
// Aspetta che finisca la dissolvenza d'ingresso: le immagini devono mostrare i colori della tavola.
|
||||
const readyAt = await page.evaluate(() => game.closeup.visual.time);
|
||||
await page.waitForFunction(t => game.closeup.visual.time > t + 1, readyAt);
|
||||
assert.equal(await page.locator('#bocce-throw').textContent(), 'Lancia');
|
||||
assert.ok(await page.locator('#flipper-help').isVisible());
|
||||
assert.equal(await page.locator('#bocce-left').textContent(), '← Flipper');
|
||||
assert.ok(!(await page.locator('#bocce-left').isDisabled()), 'i pulsanti dei flipper sono attivi anche prima del lancio');
|
||||
await page.screenshot({ path: `${out}/${name}-tavola.png` });
|
||||
const box = await page.locator('#scene').boundingBox(), P = (x, y) => [box.x + x / 640 * box.width, box.y + y / 360 * box.height];
|
||||
if (!mobile) await page.screenshot({ path: `${out}/desktop-dettaglio.png`, clip: { x: P(200, 0)[0], y: box.y, width: 248 / 640 * box.width, height: box.height } });
|
||||
const cdp = mobile ? await page.context().newCDPSession(page) : null;
|
||||
const touch = (type, points) => cdp.send('Input.dispatchTouchEvent', { type, touchPoints: points.map(([x, y]) => ({ x, y })) });
|
||||
// La molla: tenendo premuto sulla scena, col mouse e col dito.
|
||||
|
||||
+26
-9
@@ -9,6 +9,17 @@ import { existsSync } from 'node:fs';
|
||||
|
||||
const run = (c, secs) => { for (let i = 0; i < secs * 60; i++) m.update(c, 1 / 60); };
|
||||
const put = (c, ball) => Object.assign(c.ball, ball);
|
||||
// Osserva il percorso fino all'uscita, senza includere il tiro successivo sulla tavola.
|
||||
function ride(c, initial) {
|
||||
put(c, initial);
|
||||
let frames = 0, top = Infinity;
|
||||
for (let i = 0; i < 4 / m.STEP; i++) {
|
||||
m.update(c, m.STEP);
|
||||
if (c.ball?.ramp) { frames++; top = Math.min(top, c.ball.y); }
|
||||
else if (frames) return { frames, top, exit: { ...c.ball } };
|
||||
}
|
||||
assert.fail('la pallina non ha completato o abbandonato la rampa entro quattro secondi');
|
||||
}
|
||||
// Il giocatore automatico: alza il flipper quando la pallina gli scende addosso, come un umano attento.
|
||||
function autoplay(c, secs = 600) {
|
||||
for (let i = 0; i < secs * 60 && c.step === 'play'; i++) {
|
||||
@@ -161,28 +172,27 @@ test('il traguardo è acquisito appena raggiunto: anche uscendo prima, si vince;
|
||||
|
||||
test('i ponti: imboccati forte si sale, si passa sopra la tavola e si scende nella corsia di ritorno; piano si ricasca; di traverso no; tutti e due di fila, i PONTI', () => {
|
||||
const c = m.newFlipper(13, { level: 'bona' }); m.launch(c, 0.9); c.score = 0;
|
||||
put(c, { x: 247, y: 250, vx: 0, vy: -470 });
|
||||
let onRamp = 0, top = 999, exit = null; for (let i = 0; i < 150; i++) { m.update(c, 1 / 60); if (c.ball.ramp) { onRamp++; top = Math.min(top, c.ball.y); } else if (onRamp && !exit) exit = { x: c.ball.x, y: c.ball.y }; }
|
||||
const { frames: onRamp, top, exit } = ride(c, { x: 273, y: 240, vx: 0, vy: -470 });
|
||||
assert.ok(onRamp > 20, 'è salita sul ponte'); assert.ok(top < 40, 'fin sopra la tavola');
|
||||
assert.ok(exit && exit.x > 400 && exit.x < 420, `scesa nella corsia di ritorno di destra (${exit?.x.toFixed(0)})`);
|
||||
assert.equal(c.score, m.SCORE.ramp); assert.equal(c.line, 'PONTE SX'); assert.deepEqual(c.lines, ['+15000']); assert.ok(c.ramps[0].lit > 0);
|
||||
assert.ok(c.events.some(e => e.kind === 'flipper-rampa'));
|
||||
// Mentre è sul ponte i flipper rispondono lo stesso (Codex: restavano congelati).
|
||||
const f = m.newFlipper(13, { level: 'bona' }); m.launch(f, 0.9); put(f, { x: 247, y: 250, vx: 0, vy: -470 }); run(f, 0.3);
|
||||
const f = m.newFlipper(13, { level: 'bona' }); m.launch(f, 0.9); put(f, { x: 273, y: 240, vx: 0, vy: -470 }); run(f, 0.3);
|
||||
assert.ok(f.ball.ramp, 'sul ponte'); m.turn(f, -1, 'click'); run(f, 0.1);
|
||||
assert.ok(f.flippers.left.angle < 0.3, `il flipper si è alzato (${f.flippers.left.angle.toFixed(2)})`);
|
||||
// Il secondo ponte subito dopo: i PONTI, col bonus.
|
||||
put(c, { x: 393, y: 250, vx: 0, vy: -470, ramp: null }); run(c, 2.5);
|
||||
ride(c, { x: 367, y: 240, vx: 0, vy: -470, ramp: null });
|
||||
assert.equal(c.score, 2 * m.SCORE.ramp + m.SCORE.ponti, `i ponti (${c.score})`);
|
||||
assert.ok([c.line, ...c.lines].some(l => l === 'I PONTI!') && [c.line, ...c.lines].some(l => /BONUS/.test(l ?? '')));
|
||||
assert.equal(c.lastRamp, null, 'la combo è spesa');
|
||||
// Troppo piano: sale un po', non ce la fa, ricasca dall'imbocco senza punti.
|
||||
const d = m.newFlipper(14, { level: 'bona' }); m.launch(d, 0.9); d.score = 0;
|
||||
put(d, { x: 247, y: 250, vx: 0, vy: -335 }); let climbed = false; for (let i = 0; i < 300; i++) { m.update(d, 1 / 60); if (d.ball.ramp) climbed = true; }
|
||||
assert.ok(climbed, 'è salita'); assert.ok(!d.ball.ramp && d.ball.y > 236, 'ricaduta'); assert.ok(d.score < m.SCORE.ramp, 'niente punti del ponte');
|
||||
put(d, { x: 273, y: 240, vx: 0, vy: -280 }); let climbed = false; for (let i = 0; i < 300; i++) { m.update(d, 1 / 60); if (d.ball.ramp) climbed = true; }
|
||||
assert.ok(climbed, 'è salita'); assert.ok(!d.ball.ramp && d.ball.y > 226, 'ricaduta'); assert.ok(d.score < m.SCORE.ramp, 'niente punti del ponte');
|
||||
// Di traverso (Codex: sfiorando l'imbocco quasi in orizzontale saliva lo stesso): no.
|
||||
const e = m.newFlipper(15, { level: 'bona' }); m.launch(e, 0.9); e.score = 0;
|
||||
put(e, { x: 247, y: 236.1, vx: 500, vy: -30 }); run(e, 0.5);
|
||||
put(e, { x: 273, y: 226.1, vx: 500, vy: -30 }); run(e, 0.5);
|
||||
assert.ok(!e.ball.ramp && e.score < m.SCORE.ramp, 'non è salita');
|
||||
// La combo scade dopo dodici secondi, e si azzera perdendo la pallina.
|
||||
const g = m.newFlipper(16, { level: 'bona' }); m.launch(g, 0.9); g.lastRamp = { id: 'sinistra', at: g.sim }; put(g, { x: 320, y: 200, vx: 0, vy: 0 });
|
||||
@@ -199,7 +209,7 @@ test('gli effetti: ogni colpo lascia il suo segno (anello, scintille, punti che
|
||||
assert.equal(c.fx.find(f => f.kind === 'pop').text, '+1000');
|
||||
c.fx.length = 0; c.score = c.skill.goal - 100; put(c, { x: c.regolo.x, y: c.regolo.y + 12, vx: 0, vy: -200 }); run(c, 0.05);
|
||||
assert.ok(kinds().includes('burst') && kinds().includes('wash') && kinds().includes('shake'), `traguardo: ${kinds()}`);
|
||||
c.fx.length = 0; put(c, { x: 247, y: 250, vx: 0, vy: -470 }); run(c, 2.5);
|
||||
c.fx.length = 0; ride(c, { x: 273, y: 240, vx: 0, vy: -470 });
|
||||
assert.ok(kinds().includes('chase'), `ponte: ${kinds()}`);
|
||||
assert.ok(c.trail.length > 5, 'la cometa');
|
||||
const ball = c.ball; c.ball = null; run(c, m.FX_LIFE + 0.1); c.ball = ball;
|
||||
@@ -207,7 +217,7 @@ test('gli effetti: ogni colpo lascia il suo segno (anello, scintille, punti che
|
||||
// Disegnarli non cambia niente.
|
||||
const calls = [], gradient = { addColorStop() {} };
|
||||
const ctx = new Proxy({}, { get: (o, k) => k in o ? o[k] : (...args) => { calls.push([k, ...args]); return gradient; }, set: (o, k, v) => { o[k] = v; return true; } });
|
||||
put(c, { x: 247, y: 250, vx: 0, vy: -470 }); run(c, 0.4); c.fx.push({ kind: 'burst', x: 320, y: 100, t: c.visual.time, big: true }, { kind: 'shake', x: 0, y: 0, t: c.visual.time });
|
||||
put(c, { x: 273, y: 240, vx: 0, vy: -470 }); run(c, 0.4); c.fx.push({ kind: 'burst', x: 320, y: 100, t: c.visual.time, big: true }, { kind: 'shake', x: 0, y: 0, t: c.visual.time });
|
||||
const before = structuredClone(c); drawFlipper(ctx, c, story.closeups.flipper, {}); assert.deepEqual(c, before);
|
||||
assert.ok(calls.some(([op]) => op === 'translate'), 'la scossa sposta la scena');
|
||||
});
|
||||
@@ -229,6 +239,13 @@ test('il disegno del flipper non cambia lo stato; il tabellone dice l\'esito giu
|
||||
const c = m.newFlipper(11, { level: 'bona' }), bg = {};
|
||||
const draw = () => { const before = structuredClone(c); drawFlipper(ctx, c, story.closeups.flipper, bg); assert.deepEqual(c, before); };
|
||||
draw(); assert.ok(calls.some(([op, img]) => op === 'drawImage' && img === bg));
|
||||
assert.ok(calls.filter(([op]) => op === 'arc').every(([, , , r]) => r > 0), 'anche viti e inserti piccoli hanno raggi validi per Canvas');
|
||||
for (const r of c.ramps) {
|
||||
const x = (r.mouth[0][0] + r.mouth[1][0]) / 2;
|
||||
assert.ok(calls.some(([op, line, tx, ty]) => op === 'fillText' && line === 'RAMPA' && tx === x && ty > r.mouth[0][1]), 'la targhetta indica l\'imbocco dal basso');
|
||||
}
|
||||
assert.ok(calls.some(([op, line]) => op === 'fillText' && line === 'OBIETTIVO 60.000'));
|
||||
assert.ok(calls.some(([op, line]) => op === 'fillText' && line === 'MANCANO 60.000'));
|
||||
assert.ok(existsSync(new URL('../' + story.closeups.flipper.backglass, import.meta.url)), 'il fondale del tabellone esiste');
|
||||
const glass = {}; drawFlipper(ctx, c, story.closeups.flipper, bg, glass); assert.ok(calls.some(([op, img, x, y, w, h]) => op === 'drawImage' && img === glass && w === 200 && h === 360), 'il fondale sul fianco');
|
||||
m.launch(c, 0.8); run(c, 1); calls.length = 0; draw();
|
||||
|
||||
Reference in New Issue
Block a user