diff --git a/docs/STATO.md b/docs/STATO.md index eab3c2d..c661848 100644 --- a/docs/STATO.md +++ b/docs/STATO.md @@ -56,6 +56,9 @@ Online su 50037.manzolo.it tutto fino agli effetti ambientali di Codex. Aperto, coordinate logiche 640×360, gli altri ambienti restano in pixel art. Fondale di Codex conservato nel riquadro illustrato del backglass; niente pannelli sopra il Trebbio. Superfici e bagliori in cache, senza disegnare la strada nascosta sotto il flipper. Pannello a destra con progressione, timer, moltiplicatore e prossimo obiettivo. + Alleggerito (1 novembre): il mobile statico sta sullo sfondo CSS della tela (`cabinetBehind`), la tela si pulisce + e basta; estremità e giunti arrotondati solo nei livelli in cache. Arretrato grafico da 4,2 s a 0,03 s per 150 + fotogrammi (regole e misure nella memoria «effetti leggeri»). - Rampe FORTEZZA e TREBBIO: imbocchi centrali da 36 pixel, 27 utili alla pallina, salite accessibili con colpi medi, 15.000 punti. Tre passaggi sulla stessa rampa entro 20 secondi dal primo: LOCK e nuova pallina gratis. Un'altra rampa completata o il tempo esaurito interrompono la serie. Dopo il primo LOCK, primo passaggio @@ -75,7 +78,8 @@ Online su 50037.manzolo.it tutto fino agli effetti ambientali di Codex. Aperto, Comandi: mouse, due dita, tasto destro, frecce, Spazio e pulsanti; istruzioni in «Come si gioca». - Test `flipper.test.mjs`: sequenze in entrambi gli ordini e a tutti i livelli, scadenze, premi, collisioni, recupero della SUPER con la paletta, rilascio sfalsato e valvola. `npm run browser:flipper`: desktop e touch, - screenshot di tavolo, LOCK, SUPER, multiball e risultato in `/tmp/regolo-flipper`, intervallo fra frame. + screenshot di tavolo, LOCK, SUPER, multiball e risultato in `/tmp/regolo-flipper`, e la misura dell'arretrato + grafico dopo 150 fotogrammi in multiball (deve restare sotto 1,5 s). [DA ANDREA] battute, premio nell'avventura (`flipper_vinto`), record persistente per livello. - **Dedica allo zio Mario (31 ottobre)**: nei Ringraziamenti, sotto quella al Lele e più piccola: «Al mio caro zio Mario, che mi ha fatto vivere tante avventure più belle della fantasia.» diff --git a/src/engine/flipper-render.js b/src/engine/flipper-render.js index 4120822..c7c2545 100644 --- a/src/engine/flipper-render.js +++ b/src/engine/flipper-render.js @@ -44,15 +44,31 @@ function glow(g, x, y, radius, color, alpha = 1) { g.save(); g.globalAlpha *= alpha; g.globalCompositeOperation = 'screen'; g.drawImage(sprite, x - radius, y - radius, radius * 2, radius * 2); g.restore(); } const layers = new Map(); -function cached(g, key, paint, bounds = [0, 0, 640, 360]) { - if (typeof document === 'undefined') { paint(g); return; } +function layerOf(key, paint, bounds) { let layer = layers.get(key); if (!layer) { layer = Object.assign(document.createElement('canvas'), { width: bounds[2] * 2, height: bounds[3] * 2 }); const l = layer.getContext('2d'); l.scale(2, 2); l.translate(-bounds[0], -bounds[1]); paint(l); layers.set(key, layer); } - g.drawImage(layer, ...bounds); + return layer; } +function cached(g, key, paint, bounds = [0, 0, 640, 360]) { + if (typeof document === 'undefined') { paint(g); return; } + g.drawImage(layerOf(key, paint, bounds), ...bounds); +} +// Il mobile, che non cambia mai, sta sullo sfondo CSS della tela in pagina invece di essere copiato a ogni fotogramma: +// una copia a tutto schermo fra gli altri tratti costava da sola mezzo secondo ogni 150 fotogrammi (swiftshader). La +// tela si pulisce e il mobile resta sotto; si ridisegna sulla tela solo durante la scossa, che lo sposta. +const sheets = new Map(); +function cabinetBehind(g, key, paint) { + const canvas = g.canvas; + if (typeof document === 'undefined' || !(canvas instanceof HTMLCanvasElement) || !canvas.isConnected) return false; + let url = sheets.get(key); + if (!url) { url = `url(${layerOf(key, paint, [0, 0, 640, 360]).toDataURL()})`; sheets.set(key, url); } + if (canvas.dataset.cabinet !== key) { canvas.style.backgroundImage = url; canvas.style.backgroundSize = '100% 100%'; canvas.dataset.cabinet = key; } + return true; +} +export function cabinetAway(canvas) { canvas.style.backgroundImage = ''; canvas.style.backgroundSize = ''; delete canvas.dataset.cabinet; } function crown(g, x, y, size, color) { poly(g, [[x - size, y + size / 2], [x - size * 1.1, y - size / 2], [x - size / 2, y], [x, y - size], [x + size / 2, y], [x + size * 1.1, y - size / 2], [x + size, y + size / 2]], color); line(g, [[x - size, y + size * 0.8], [x + size, y + size * 0.8]], color, 1); @@ -357,10 +373,14 @@ function effects(g, c, time) { } export function drawFlipper(g, c, def, bg, glass) { const time = c.visual.time; - g.save(); g.imageSmoothingEnabled = true; g.lineCap = 'round'; g.lineJoin = 'round'; + // Niente estremità e giunti arrotondati nei tratti di ogni fotogramma: costano più di tutto il resto (vedi la + // memoria «effetti leggeri»); il mobile e i binari, in cache, li tengono. + g.save(); g.imageSmoothingEnabled = true; g.lineCap = 'butt'; g.lineJoin = 'miter'; const shake = c.fx.reduce((n, f) => f.kind === 'shake' ? Math.max(n, 1 - (time - f.t) / 0.3) : n, 0); - if (shake > 0) { g.fillStyle = INK; g.fillRect(0, 0, 640, 360); g.translate(Math.sin(time * 75) * shake * 1.3, Math.cos(time * 63) * shake); } - cached(g, `cabinet:${!!bg}:${!!glass}`, layer => cabinet(layer, c, bg, glass)); + const key = `cabinet:${!!bg}:${!!glass}`, paint = layer => cabinet(layer, c, bg, glass); + if (shake > 0) { g.fillStyle = INK; g.fillRect(0, 0, 640, 360); g.translate(Math.sin(time * 75) * shake * 1.3, Math.cos(time * 63) * shake); cached(g, key, paint); } + else if (cabinetBehind(g, key, paint)) g.clearRect(0, 0, 640, 360); + else cached(g, key, paint); for (const [i, l] of c.lanes.entries()) { if (l.lit) glow(g, l.x, l.y, 17, RIONI[i], 0.55); disc(g, l.x, l.y, 5, '#06151d', l.lit ? RIONI[i] : '#47616b'); @@ -378,7 +398,6 @@ export function drawFlipper(g, c, def, bg, glass) { const color = [TINT.sinistra, TINT.destra, TINT.super][i]; b.trail.forEach(([x, y], k) => { g.save(); g.globalAlpha = k / b.trail.length * 0.35; disc(g, x, y - 1, 1 + k / b.trail.length * 2, color); g.restore(); }); } - // Ogni rampa copre gli oggetti sotto; la sua pallina si disegna sul proprio piano. for (const b of balls) if (!b.ramp && b.releaseAt <= c.sim) ball(g, b); for (const r of c.ramps) { if (!rampOpen(c, r)) continue; diff --git a/src/main.js b/src/main.js index 1dee3ef..5d409a9 100644 --- a/src/main.js +++ b/src/main.js @@ -3,6 +3,7 @@ import { story } from './story/story.js'; import { Game, check, VERBS, VERB_LABELS, checkSave, introText, shuffledCredits, matchPlace, reelPictures, facePicture } from './engine/game.js'; import { Command } from './engine/command.js'; import { render, loadAssets, hasBackground, WIDTH, HEIGHT } from './engine/render.js'; +import { cabinetAway } from './engine/flipper-render.js'; import { readSave, writeSave, clearSave, readAudio, writeAudio, saveFile, saveFileName, readSaveFile } from './engine/storage.js'; import { Music, trackFor } from './engine/music.js'; import { Voices } from './engine/voices.js'; @@ -1195,6 +1196,7 @@ function frame(now) { if (canvas.width !== WIDTH * sceneScale) { canvas.width = WIDTH * sceneScale; canvas.height = HEIGHT * sceneScale; canvas.classList.toggle('pinball-hd', !!pinballHD); + if (!pinballHD) cabinetAway(canvas); } ctx.setTransform(sceneScale, 0, 0, sceneScale, 0, 0); render(ctx, game, { hover: debug && hoverTarget ? game.hotspot(hoverTarget) : null, spots: running && now < spotsUntil, debug: debugView() }); diff --git a/tests/flipper.browser.mjs b/tests/flipper.browser.mjs index b6a0a43..6548c31 100644 --- a/tests/flipper.browser.mjs +++ b/tests/flipper.browser.mjs @@ -130,10 +130,19 @@ try { assert.deepEqual({ balls: multi.balls, locks: multi.locks, reserve: multi.reserve }, { balls: 3, locks: 0, reserve: 5 }); await page.waitForFunction(t => game.closeup.visual.time > t + 0.7, multi.time); await page.screenshot({ path: `${out}/${name}-multiball.png` }); - const frameMs = await page.evaluate(() => new Promise(resolve => { - const stamps = []; const tick = t => { stamps.push(t); if (stamps.length === 20) resolve((t - stamps[0]) / 19); else requestAnimationFrame(tick); }; requestAnimationFrame(tick); - })); - console.log(`${name}: intervallo frame medio ${frameMs.toFixed(1)} ms`); + // L'arretrato grafico (vedi la memoria «effetti leggeri»): 150 fotogrammi disegnati di fila con tre palline e gli + // effetti vivi, poi una lettura dei pixel che aspetta la grafica. Il flipper di Codex ne lasciava 4,2 s; deve + // restare sotto il secondo e mezzo anche col rendering software di queste prove. + const backlog = await page.evaluate(async () => { + const m = await import('/src/engine/flipper.js'), r = await import('/src/engine/render.js'), c = game.closeup; + const ctx = document.querySelector('#scene').getContext('2d'); + ctx.getImageData(0, 0, 4, 4); // si aspetta prima l'arretrato di tutta la prova fin qui + for (let i = 0; i < 150; i++) { m.update(c, 1 / 60); r.render(ctx, game, { time: performance.now() }); } + const t = performance.now(); ctx.getImageData(0, 0, 4, 4); return Math.round(performance.now() - t); + }); + console.log(`${name}: arretrato grafico dopo 150 fotogrammi ${backlog} ms`); + assert.ok(backlog < 1500, `arretrato grafico ${backlog} ms`); + await page.waitForFunction(() => document.querySelector('#scene').style.backgroundImage.startsWith('url('), null, { timeout: 2000 }); // il mobile sullo sfondo CSS await page.evaluate(() => { game.update = window.pinballResume; delete window.pinballResume; delete window.pinballShot; }); // Si consumano le palline rimaste, compresa quella vinta, per arrivare al risultato reale. const finalScore = await page.evaluate(async () => { @@ -150,7 +159,7 @@ try { await page.screenshot({ path: `${out}/${name}-fine.png` }); if (mobile) await page.locator('#closeup-close').click(); else await page.keyboard.press('Enter'); // Invio chiude il risultato await page.waitForFunction(() => game.state.flags.flipper_vinto && !game.closeup); - await page.waitForFunction(() => document.querySelector('#scene').width === 640); + await page.waitForFunction(() => document.querySelector('#scene').width === 640 && !document.querySelector('#scene').style.backgroundImage); await page.close(); } assert.deepEqual(errors, []);