GP di Fortezza: guida a un dito col telefono (premuto è gas, lo swipe indica dove andare sullo schermo; Codex) e il casco di Tozzino alla Damon Hill (Andrea)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GiECdCPptNRwbVzjzHbohi
This commit is contained in:
2026-10-28 21:26:53 +01:00
co-authored by Claude Opus 5.5
parent e1b92278a5
commit 06f7cfbfad
8 changed files with 131 additions and 28 deletions
+22 -2
View File
@@ -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);
+21 -10
View File
@@ -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);
+2 -2
View File
@@ -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;
+5 -2
View File
@@ -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' },
];