diff --git a/assets/closeups/gp/casco-tozzino.png b/assets/closeups/gp/casco-tozzino.png new file mode 100644 index 0000000..8aef00b Binary files /dev/null and b/assets/closeups/gp/casco-tozzino.png differ diff --git a/docs/STATO.md b/docs/STATO.md index f4387d1..6de61d5 100644 --- a/docs/STATO.md +++ b/docs/STATO.md @@ -258,7 +258,8 @@ I prompt fissi (stile, scene, foglio sprite) sono in `prompts/STILE.md`, escluso sulla strada dipinta. Disegno `gara-render.js`, test `tests/gara.test.mjs`. I piloti sono dati in `src/story/piloti.js`: una riga per amico, `motore` e `testa` da 1 a 5, colori dalle foto. Corrono Puntella (SR, colori Aprilia da GP provvisori), Cistola (CR 50 Z), Andrea (Malaguti Fifty rosso e nero), Tozzino (Booster blu - scuro, di serie), l'Arcu (Betino). **Puntella (Marco Villani) non corre finché manca il suo consenso** (`consenso: null` in piloti.js: quando arriva, la data). Mancano: quanto erano + scuro, di serie, casco alla Damon Hill di Codex: `prompts/personaggi/casco-tozzino.txt`), l'Arcu (Betino). Col telefono + si guida con un dito: premuto è gas, lo swipe indica dove andare sullo schermo (Codex, 28 ottobre). **Puntella (Marco Villani) non corre finché manca il suo consenso** (`consenso: null` in piloti.js: quando arriva, la data). Mancano: quanto erano truccati gli altri, le battute di Andrea, il rumore del motore (c'è solo quello dell'arrivo in griglia); i confini della paglia non si vedono sul quadro (revisione di Codex, punto 9). - Strade attorno a casa (Andrea, 28 ottobre): la scena `area_camper` ora si chiama «Le villette del Giannini» (ci abita diff --git a/index.html b/index.html index bc7ba00..ccdfa97 100644 --- a/index.html +++ b/index.html @@ -221,9 +221,12 @@ guida mostra la parabola fino al primo urto, Seria solo l’inizio, A fassi male niente.
Il GP di Fortezza: tre giri attorno alle mura coi cinquantini. Tieni premuto sulla scena (o Spazio, o ↑) per il - gas; sterza con le frecce ← →, oppure tenendo premuto e trascinando di lato col dito o col mouse, oppure tenendo - premuti i pulsanti Sterza. ↓ (o il pulsante Frena, tenuto premuto) frena, anche col gas dato. Dal pannello il +
Il GP di Fortezza: tre giri attorno alle mura coi cinquantini. Con un solo dito: tieni premuto sulla scena per + il gas e trascina verso la direzione in cui vuoi andare sullo schermo. A destra vai verso destra, anche quando + il motorino scende. I piccoli movimenti non sterzano; il cerchietto segue gli swipe lunghi. Alza il dito per + mollare il gas e raddrizzare lo sterzo. Funziona anche col mouse premuto.
+Con la tastiera: Spazio, Invio o ↑ per il gas, ← → per sterzare; oppure tieni premuti i pulsanti Sterza. + ↓ (o il pulsante Frena, tenuto premuto) frena, anche col gas dato. Dal pannello il pulsante alterna Gas e Molla. Durante la presentazione dei piloti un tocco passa al prossimo.
Le frecce gialle in terra sono acceleratori; le lattine rosse di miscela danno uno sprint (e ricompaiono); nelle buche, se ci passi forte, caschi. Fuori strada l'erba frena, e nella paglia ci si ferma. Alla bona la pista mostra diff --git a/src/engine/gara-render.js b/src/engine/gara-render.js index 1ec895f..4aa8c1e 100644 --- a/src/engine/gara-render.js +++ b/src/engine/gara-render.js @@ -1,4 +1,4 @@ -import { TRACK, CURVES, PICKUPS, BRAKE, LAPS, COUNTDOWN, ROAD, VERGE, CAN, pointAt, standings, player, lapOf, presenting, placeOf, INTRO } from './gara.js'; +import { TRACK, CURVES, PICKUPS, BRAKE, LAPS, COUNTDOWN, ROAD, VERGE, CAN, SWIPE, pointAt, standings, player, lapOf, presenting, placeOf, INTRO } from './gara.js'; import { text, panel } from './bocce-render.js'; import { ambientHash as hash } from './ambient.js'; @@ -92,7 +92,8 @@ function moped(ctx, r, c) { ctx.fillStyle = r.color; ctx.fillRect(-4, -2, 9, 4); ctx.fillStyle = r.trim ?? '#1c1c1c'; ctx.fillRect(-4, -0.5, 9, 1); ctx.fillStyle = '#2a2a2a'; ctx.fillRect(4, -3, 1, 6); // il manubrio - ctx.fillStyle = r.id === 'andrea' ? '#f4e3c2' : '#2a2a2a'; ctx.beginPath(); ctx.arc(-1, 0, 2, 0, Math.PI * 2); ctx.fill(); // il casco + ctx.fillStyle = r.helmet ?? (r.id === 'andrea' ? '#f4e3c2' : '#2a2a2a'); ctx.beginPath(); ctx.arc(-1, 0, 2, 0, Math.PI * 2); ctx.fill(); // il casco + if (r.stripe) { ctx.fillStyle = r.stripe; ctx.fillRect(-2, -0.5, 2, 1); } // La miscela: una fiammata dietro. if (r.boost > 0 && Math.floor(c.age * 20) % 2) { ctx.fillStyle = '#ffb030'; ctx.fillRect(-10, -1, 3, 2); } ctx.restore(); @@ -113,6 +114,21 @@ function hud(ctx, c) { ctx.fillStyle = me.boost > 0 ? '#ffb030' : k > 0.9 ? '#e0583a' : '#b9d392'; ctx.fillRect(10, bar, Math.round(50 * k), 5); text(ctx, `${Math.round(me.v * 0.5)} km/h`, 116, bar + 3, 6, '#f4e3c2', 'right'); } +// L'origine mobile dello swipe e la direzione scelta col dito (o col mouse premuto). +function swipe(ctx, c) { + if (c.pointer === null || !c.anchor || !c.finger) return; + const [x, y] = c.anchor, [fx, fy] = c.finger, dx = fx - x, dy = fy - y, length = Math.hypot(dx, dy); + ctx.save(); + ctx.strokeStyle = 'rgba(244,227,194,0.55)'; ctx.lineWidth = 1.5; ctx.lineCap = 'round'; + ctx.beginPath(); ctx.arc(x, y, 4, 0, Math.PI * 2); ctx.stroke(); + if (length >= SWIPE.dead) { + const ux = dx / length, uy = dy / length; + ctx.beginPath(); ctx.moveTo(x + ux * 6, y + uy * 6); ctx.lineTo(fx, fy); + ctx.moveTo(fx - ux * 6 - uy * 3, fy - uy * 6 + ux * 3); ctx.lineTo(fx, fy); + ctx.lineTo(fx - ux * 6 + uy * 3, fy - uy * 6 - ux * 3); ctx.stroke(); + } + ctx.restore(); +} // La presentazione: il cartellino di chi arriva. A sinistra il personaggio (lo sprite del gioco, o il ritratto), a // destra il suo motorino dipinto, sotto quanto va (motore) e quanto rischia (testa). `look(src)` dà l'immagine caricata, // `sheet(id)` lo sprite di un personaggio. @@ -139,6 +155,9 @@ function card(ctx, c, look, sheet) { panel(ctx, x, y, w, h); text(ctx, 'GP DI FORTEZZA', x + w / 2, y + 10, 6, '#ffe28a', 'center'); figure(ctx, r, x + 46, y + 14, look, sheet); + // Il casco di chi ce l'ha disegnato, accanto al personaggio. + const casco = r.casco && look(r.casco); + if (casco) { const k = 30 / Math.max(casco.width, casco.height); ctx.drawImage(casco, Math.round(x + 66), Math.round(y + 20), Math.round(casco.width * k), Math.round(casco.height * k)); } // Il motorino, entra da sinistra e si ferma (come in pista). const moto = r.moto && look(r.moto), t = Math.min(1, (c.t % INTRO.each) / 0.6), e = 1 - (1 - t) ** 3; if (moto) { @@ -204,6 +223,7 @@ export function drawGara(ctx, c, def, bg, look = () => null, sheet = () => null) // Prima chi è dietro, così chi è davanti resta sopra; Andrea sempre sopra a tutti. for (const r of [...standings(c)].reverse().sort((a, b) => (a.id === 'andrea') - (b.id === 'andrea'))) if (r.shown !== false) moped(ctx, r, c); if (c.step === 'intro') { card(ctx, c, look, sheet); ctx.restore(); return; } + if (c.step === 'race' || c.step === 'start') swipe(ctx, c); hud(ctx, c); if (c.step === 'race') position(ctx, c); if (c.step === 'start') lights(ctx, c); diff --git a/src/engine/gara.js b/src/engine/gara.js index 97562bf..cdd959a 100644 --- a/src/engine/gara.js +++ b/src/engine/gara.js @@ -3,7 +3,7 @@ // la planimetria generale 1:500 che ha mandato Andrea (`assets/raw/fortezza-planimetria.png`), schiacciata nel // formato del gioco (640x360): x = 90 + (x0 - 215)·0,7, y = 20 + (y0 - 42)·0,49. // -// Si tiene premuto per il gas (mouse, dito, Spazio o ↑), si sterza con ← → (o trascinando di lato col dito, o coi +// Si tiene premuto per il gas (mouse, dito, Spazio o ↑), si sterza con ← → (o indicando la direzione con uno swipe, o coi // pulsanti), ↓ frena. Sulla pista: acceleratori, buche (andandoci forte si casca) e lattine di miscela da raccogliere. // Fuori strada c'è l'erba che frena, e poi la paglia: chi ci arriva forte si ferma. @@ -29,6 +29,7 @@ export const LAPS = 3, STEP = 2, ROAD = 9, VERGE = 15, COUNTDOWN = 3, FIXED_STEP export const INTRO = { each: 4, ride: 1.8, from: 110, after: 1 }; // Sterzo (rad/s a velocità piena di sterzo), freno, rallentamento senza gas, e quanto si scivola di lato in curva. export const TURN = 3.4, BRAKE = 150, COAST = 45, GRIP = 9, SLIDE = 0.5; +export const SWIPE = { dead: 10, reach: 60, gain: 2.6 }; export const BOOST = { speed: 1.35, time: 1.2 }, CAN = { time: 1.6, respawn: 6 }, HOLE = { fast: 45, out: 1.1 }; const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v)); const wrapAngle = a => Math.atan2(Math.sin(a), Math.cos(a)); @@ -150,7 +151,7 @@ export function newGara(seed = Date.now() >>> 0, def = {}) { if (!riders.some(r => r.id === 'andrea')) throw new Error('Nella gara manca Andrea'); for (const r of riders) { r.grid = r.s; r.shown = false; } return { step: 'intro', t: 0, age: 0, seed, level, skill, riders, holding: false, braking: false, keys: { left: false, right: false, gas: new Set(), brake: false }, - pointer: null, toggle: false, brakeButton: false, drag: 0, buttons: 0, anchor: null, + pointer: null, toggle: false, brakeButton: false, drag: 0, buttons: 0, anchor: null, finger: null, power: 0, accumulator: 0, events: [], result: null, order: riders.map(r => r.id), taken: {} }; } export const player = c => c.riders.find(r => r.id === 'andrea'); @@ -167,7 +168,7 @@ export const setPower = () => false; function sync(c) { c.holding = c.keys.gas.size > 0 || c.pointer !== null || c.toggle; c.braking = c.keys.brake || c.brakeButton; - if (c.pointer === null) c.drag = 0; + if (c.pointer === null) { c.drag = 0; c.anchor = null; c.finger = null; } } // Il primo tocco (o tasto) salta la presentazione, senza dare gas. export function press(c, id = 'press') { @@ -209,12 +210,14 @@ function place(r, s, shown) { export const presenting = c => c.step === 'intro' ? c.riders[Math.floor(c.t / INTRO.each)] ?? null : null; // Il pulsante Gas del pannello: un tocco lo accende, un altro lo spegne (per chi non può tenere premuto). export function shoot(c) { if (!canThrow(c)) return false; if (c.step === 'intro') return nextIntro(c); c.toggle = !c.toggle; sync(c); return true; } -// Il dito o il mouse premuto sulla scena: quanto ci si sposta di lato da dove si è premuto è lo sterzo. Il mouse che -// passa senza premere, o col gas dato da tastiera o dal pannello, non sterza. -export function aimScreen(c, [x], id = 'press') { - if (!Number.isFinite(x)) return false; - if (c.pointer === null) c.anchor = x; - else if (c.pointer === id) c.drag = clamp((x - c.anchor) / 28, -1, 1); +// Lo swipe indica dove andare sullo schermo. L'origine segue il dito oltre i 60 px. +export function aimScreen(c, [x, y], id = 'press') { + if (!Number.isFinite(x) || !Number.isFinite(y)) return false; + if (c.pointer !== null && c.pointer !== id) return false; + if (c.pointer === null || !c.anchor) c.anchor = [x, y]; + c.finger = [x, y]; + const dx = x - c.anchor[0], dy = y - c.anchor[1], distance = Math.hypot(dx, dy); + if (distance > SWIPE.reach) c.anchor = [x - dx * SWIPE.reach / distance, y - dy * SWIPE.reach / distance]; return false; } // La tastiera: ← → sterzano, ↑ Spazio e Invio sono il gas, ↓ il freno; finché si tengono giù. @@ -233,7 +236,15 @@ export function key(c, name, down) { // I pulsanti del pannello tenuti premuti: Sterza (-1, 0, 1) e Frena. export function steerButton(c, dir) { c.buttons = dir; return true; } export function brakeButton(c, down) { c.brakeButton = down; sync(c); return true; } -export const steering = c => clamp((c.keys.right ? 1 : 0) - (c.keys.left ? 1 : 0) + c.buttons + c.drag, -1, 1); +export function steering(c) { + c.drag = 0; + if (c.pointer !== null && c.anchor && c.finger) { + const dx = c.finger[0] - c.anchor[0], dy = c.finger[1] - c.anchor[1]; + // Si ricalcola anche col dito fermo: il motorino intanto cambia direzione. + if (Math.hypot(dx, dy) >= SWIPE.dead) c.drag = clamp(wrapAngle(Math.atan2(dy, dx) - player(c).a) * SWIPE.gain, -1, 1); + } + return clamp((c.keys.right ? 1 : 0) - (c.keys.left ? 1 : 0) + c.buttons + c.drag, -1, 1); +} // La distanza davanti, sul giro, da s a un punto della pista (sempre fra 0 e la lunghezza del giro). export const ahead = (from, to) => ((((to - from) % TRACK.length) + TRACK.length) % TRACK.length); diff --git a/src/main.js b/src/main.js index 5969df3..4c6f08b 100644 --- a/src/main.js +++ b/src/main.js @@ -498,7 +498,7 @@ canvas.addEventListener('pointerdown', e => { canvas.setPointerCapture(e.pointerId); return; } - // Il GP di Fortezza: un dito solo sulla scena dà gas e sterza trascinando; gli altri tocchi non contano + // Il GP di Fortezza: un dito solo dà gas e indica la direzione sullo schermo; gli altri tocchi non contano // (sterzo e freno hanno i loro pulsanti, che si tengono insieme al gas). if (type === 'gara') { if (press || game.skip()) return; @@ -584,7 +584,7 @@ const MINI = { biliardo: { ...biliardo, back: 'Torna al bar', label: 'Biliardo: clic o tocco fissa la mira. Frecce per affinarla, Maiusc per passi più piccoli. Regola la forza e premi Tira.' }, basket: { ...basket, back: 'Torna in strada', label: 'Fai due tiri: muovi il mouse o tocca per mirare, tieni premuto per caricare la forza, lascia per tirare.' }, // La gara: niente mira né forza, il pulsante alterna gas e freno (chi non può tenere premuto). - gara: { ...gara, toggle: true, back: 'Torna dal Trappola', label: 'GP di Fortezza: tieni premuto (mouse, dito, Spazio o ↑) per il gas, sterza con ← → o trascinando di lato; ↓ frena.' }, + gara: { ...gara, toggle: true, back: 'Torna dal Trappola', label: 'GP di Fortezza: tieni premuto sulla scena per il gas e trascina verso la direzione in cui vuoi andare sullo schermo; alza il dito per mollare. Anche col mouse. Oppure Spazio, Invio o ↑ per il gas, ← → per sterzare, ↓ per frenare; disponibili anche i pulsanti Gas, Sterza e Frena.' }, pesca: { ...pesca, back: 'Torna in Fortezza', label: 'La pesca: tieni premuto per lanciare, premi quando il galleggiante va sotto, tieni premuto per recuperare.' }, }; const miniState = () => running && game?.closeup && MINI[story.closeups[game.closeup.id].type] ? game.closeup : null; diff --git a/src/story/piloti.js b/src/story/piloti.js index 0580301..80c2dc4 100644 --- a/src/story/piloti.js +++ b/src/story/piloti.js @@ -2,7 +2,8 @@ // Ogni amico che manda la foto del suo motorino si aggiunge con una riga. `motore`: da 1 (di serie) a 5 (truccato da // far paura); `testa`: da 1 (matto, in curva rischia) a 5 (prudente). `color` e `trim`: i colori del motorino in pista. // Per la presentazione: `moto`, il disegno del motorino (Codex, dalla foto); `sprite`, il personaggio del gioco (un -// nome di story.characters), oppure `face`, un ritratto quadrato; senza nessuno dei due, una sagoma. +// nome di story.characters), oppure `face`, un ritratto quadrato; senza nessuno dei due, una sagoma. `casco`: il disegno +// del casco per il cartellino, `helmet` e `stripe` i suoi colori visti dall'alto in pista. // Persone vere solo col loro consenso (`consenso`: chi l'ha dato e quando). export const piloti = [ // Lo SR di Puntella era il più truccato di tutti; per ora i colori dell'Aprilia SR 50 da GP (foto di Andrea). @@ -12,7 +13,9 @@ export const piloti = [ // Il Malaguti Fifty di Andrea: rosso con le rifiniture nere. { id: 'andrea', name: 'Andrea', bike: 'il Fifty', motore: 3, testa: 3, color: '#d8402f', trim: '#1c1c1c', moto: 'assets/closeups/gp/fifty.png', sprite: 'andrea' }, // Tozzino (Francesco Tozzi) col suo Booster blu scuro, di serie, dalle foto che ha mandato (l'adesivo sul manubrio compreso). - { id: 'tozzino', name: 'Tozzino', bike: 'il Booster', motore: 1, testa: 3, color: '#1a2a52', trim: '#111318', moto: 'assets/closeups/gp/booster.png', sprite: 'tozzino' }, + { id: 'tozzino', name: 'Tozzino', bike: 'il Booster', motore: 1, testa: 3, color: '#1a2a52', trim: '#111318', moto: 'assets/closeups/gp/booster.png', sprite: 'tozzino', + // Il casco alla Damon Hill: blu scuro con le righe bianche dei remi (Andrea, 28 ottobre). + casco: 'assets/closeups/gp/casco-tozzino.png', helmet: '#1b2a5c', stripe: '#f4f4f4' }, // Il Betino dell'Arcu: verde e crema, lento ma non cade mai. { id: 'arcu', name: 'L\'Arcu', bike: 'il Betino', motore: 1, testa: 5, color: '#2f7a3a', trim: '#e8d9a0', moto: 'assets/closeups/gp/betino.png', sprite: 'arcu' }, ]; diff --git a/tests/gara.test.mjs b/tests/gara.test.mjs index ee38bee..765af3d 100644 --- a/tests/gara.test.mjs +++ b/tests/gara.test.mjs @@ -1,6 +1,6 @@ import test from 'node:test'; import assert from 'node:assert/strict'; -import { newGara, update, press, presenting, INTRO, release, shoot, key, steerButton, aimScreen, steering, status, button, standings, player, rider, limitAt, pointAt, TRACK, CURVES, PICKUPS, BRAKE, VERGE, LAPS, COUNTDOWN, RIDERS } from '../src/engine/gara.js'; +import { newGara, update, press, presenting, INTRO, release, cancel, shoot, key, steerButton, brakeButton, aimScreen, steering, status, button, standings, player, rider, limitAt, pointAt, TRACK, CURVES, PICKUPS, BRAKE, VERGE, LAPS, COUNTDOWN, RIDERS } from '../src/engine/gara.js'; import { piloti } from '../src/story/piloti.js'; import { story } from '../src/story/story.js'; import { Game } from '../src/engine/game.js'; @@ -18,6 +18,12 @@ const run = (c, seconds, drive, dt = 1 / 60) => { }; const skip = c => { while (c.step === 'intro') press(c); return c; }; const race = c => { skip(c); assert.equal(c.step, 'start'); run(c, COUNTDOWN + 0.01); assert.equal(c.step, 'race'); return c; }; +// Un dito al centro della scena indica la direzione del punto davanti al motorino. +const swipeTo = (c, target) => { + const r = player(c), dx = target.x - r.x, dy = target.y - r.y, length = Math.hypot(dx, dy); + if (c.pointer === null) { aimScreen(c, [320, 180], 'bot'); press(c, 'bot'); } + aimScreen(c, [c.anchor[0] + dx / length * 50, c.anchor[1] + dy / length * 50], 'bot'); +}; // Il pilota di giudizio: guarda avanti e ci sterza (scansando buche e chi gli sta davanti), e dà gas finché, // frenando, arriverebbe a ogni curva un po' sotto il limite. Usa i comandi del giocatore (il trascinamento). const careful = c => { @@ -25,10 +31,7 @@ const careful = c => { const next = PICKUPS.find(p => p.kind !== 'boost' && p.s - here > -8 && p.s - here < 60); const slow = c.riders.find(o => o !== r && o.s - r.s > 0 && o.s - r.s < 26 && Math.abs(o.lat - r.lat) < 7); const lane = next?.kind === 'buca' ? (next.lane > 0 ? -6 : 6) : slow ? (slow.lat > 0 ? -6 : 6) : next ? next.lane : 0; - const ahead = pointAt(r.s + 14 + r.v * 0.18, lane), want = Math.atan2(ahead.y - r.y, ahead.x - r.x) - r.a; - // Come un dito premuto che trascina: lo zero dove si è premuto, di lato quanto serve. - if (c.pointer === null) { aimScreen(c, [320, 180], 'bot'); press(c, 'bot'); } - aimScreen(c, [320 + 28 * Math.max(-1, Math.min(1, Math.atan2(Math.sin(want), Math.cos(want)) * 2.6)), 180], 'bot'); + swipeTo(c, pointAt(r.s + 14 + r.v * 0.18, lane)); const horizon = r.v * r.v / (2 * BRAKE) + 20; let brake = false; for (let d = 0; d <= horizon; d += 4) { const a = limitAt(r.s + d) * 0.85; if (r.v * r.v > a * a + 2 * BRAKE * d) { brake = true; break; } } @@ -98,15 +101,77 @@ test('lo sterzo: frecce, pulsanti e il dito che trascina; il mouse che passa sen aimScreen(c, [300, 100], 5); aimScreen(c, [360, 100], 5); assert.equal(steering(c), -1); key(c, 'ArrowUp', false); key(c, 'ArrowLeft', false); - // Il dito premuto: il punto dove preme è lo zero, di lato si sterza; un altro puntatore non sposta niente. + // Il dito indica una direzione; un altro puntatore non sposta niente. + player(c).a = -0.5 / 2.6; aimScreen(c, [300, 100], 9); press(c, 9); - aimScreen(c, [314, 100], 9); assert.equal(steering(c), 0.5); - aimScreen(c, [250, 100], 4); assert.equal(steering(c), 0.5); + aimScreen(c, [314, 100], 9); assert.ok(Math.abs(steering(c) - 0.5) < 1e-10); + aimScreen(c, [250, 100], 4); assert.ok(Math.abs(steering(c) - 0.5) < 1e-10); release(c, 9); assert.equal(steering(c), 0, 'lasciando il dito lo sterzo torna dritto'); const a = player(c).a; key(c, 'ArrowUp', true); key(c, 'ArrowRight', true); run(c, 0.4); assert.notEqual(player(c).a, a, 'sterzando si gira'); assert.equal(key(c, 'x', true), false, 'gli altri tasti non sono suoi'); }); +test('lo swipe segue lo schermo: scendendo, destra sterza negativo; avanti e piccoli movimenti vanno dritto', () => { + const c = race(newGara(3)), r = player(c); + r.a = Math.PI / 2; + aimScreen(c, [320, 180], 9); press(c, 9); + aimScreen(c, [370, 180], 9); + assert.equal(steering(c), -1, 'scendendo, la destra dello schermo è a sinistra del motorino'); + r.a = 0; + assert.ok(Math.abs(steering(c)) < 1e-10, 'col dito fermo lo sterzo si annulla quando il motorino è allineato'); + r.a = Math.PI / 2; + aimScreen(c, [320, 230], 9); assert.ok(Math.abs(steering(c)) < 1e-10, 'swipe in avanti'); + aimScreen(c, [323, 184], 9); assert.equal(steering(c), 0, '5 px non fanno tremare lo sterzo'); + aimScreen(c, [330, 230], 9); + assert.ok(Math.abs(steering(c) + Math.atan2(10, 50) * 2.6) < 1e-10, 'sterzo proporzionale all’angolo'); + r.a = -Math.PI + 0.1; + aimScreen(c, [270, 180], 9); + assert.ok(Math.abs(steering(c) + 0.26) < 1e-10, 'si gira dalla parte più breve anche passando da π a -π'); + release(c, 9); + assert.equal(c.holding, false); assert.equal(steering(c), 0); + assert.equal(c.anchor, null); assert.equal(c.finger, null); +}); + +test('l’origine segue gli swipe lunghi e si può correggere; rilascio e annullamento puliscono il dito', () => { + const c = race(newGara(3)); + player(c).a = 0; + aimScreen(c, [100, 100], 9); press(c, 9); + for (const x of [200, 300, 400]) { + aimScreen(c, [x, 100], 9); + assert.deepEqual(c.anchor, [x - 60, 100]); + assert.ok(Math.abs(steering(c)) < 1e-10); + } + aimScreen(c, [400, 160], 9); + assert.ok(Math.abs(Math.hypot(c.finger[0] - c.anchor[0], c.finger[1] - c.anchor[1]) - 60) < 1e-10); + assert.equal(steering(c), 1, 'si corregge verso il basso senza arrivare al bordo'); + key(c, 'ArrowLeft', true); shoot(c); brakeButton(c, true); + release(c, 9); + assert.equal(steering(c), -1, 'lasciare il dito conserva lo sterzo della tastiera'); + assert.ok(c.holding && c.braking, 'Gas e Frena del pannello restano attivi'); + key(c, 'ArrowDown', true); brakeButton(c, false); + assert.ok(c.braking, 'il freno della tastiera resta attivo'); + aimScreen(c, [200, 200], 10); press(c, 10); + assert.equal(c.drag, 0, 'il nuovo tocco non eredita lo swipe'); + cancel(c); + assert.equal(c.holding, false); assert.equal(c.braking, false); assert.equal(steering(c), 0); + assert.equal(c.anchor, null); assert.equal(c.finger, null); +}); + +test('alla bona si completano tre giri usando solo un dito, senza tasti o pulsanti', () => { + const c = race(newGara(5, { level: 'bona', riders: piloti })); + aimScreen(c, [320, 180], 'bot'); press(c, 'bot'); + for (let t = 0; t < 180 && c.step === 'race'; t += 1 / 60) { + swipeTo(c, pointAt(player(c).s + 50)); + assert.ok(c.holding && c.pointer === 'bot'); + assert.equal(c.keys.gas.size, 0); assert.equal(c.toggle, false); + assert.equal(c.keys.left || c.keys.right || c.braking || !!c.buttons, false); + update(c, 1 / 60); + } + assert.equal(c.step, 'result'); + assert.equal(player(c).laps, LAPS); + assert.ok(player(c).finished > 0); +}); + test('la presentazione: uno alla volta arrivano in griglia col motorino, poi il semaforo', () => { const c = newGara(2, { riders: piloti }); assert.equal(c.step, 'intro'); assert.equal(presenting(c).id, piloti[0].id);