Compare commits

...
6 Commits
21 changed files with 228 additions and 59 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+15 -3
View File
@@ -1583,6 +1583,10 @@
"file": "andrea-ee455fdc33.mp3", "file": "andrea-ee455fdc33.mp3",
"sec": 3.67 "sec": 3.67
}, },
"andrea|Qui dietro ci stanno i camper. Ma per noi questo resta il campo del Giannini.": {
"file": "andrea-195cf2b022.mp3",
"sec": 4.86
},
"andrea|Gli scivoli dei bambini. Una volta il pallone finiva lì una volta su tre.": { "andrea|Gli scivoli dei bambini. Una volta il pallone finiva lì una volta su tre.": {
"file": "andrea-a3e36c12a1.mp3", "file": "andrea-a3e36c12a1.mp3",
"sec": 5.4 "sec": 5.4
@@ -1619,9 +1623,9 @@
"file": "andrea-3d15dd47bb.mp3", "file": "andrea-3d15dd47bb.mp3",
"sec": 4.82 "sec": 4.82
}, },
"andrea|Casa mia. Terzo piano, quello con la tapparella a metà: è la mia camera.": { "andrea|Casa mia. Secondo piano: quella con la tapparella a metà è la camera di mia sorella, e a destra quella dei miei.": {
"file": "andrea-acc7949f4c.mp3", "file": "andrea-6c9e7f684e.mp3",
"sec": 5.91 "sec": 8.72
}, },
"andrea|Se salgo, mi mettono ad apparecchiare. Prima il palio.": { "andrea|Se salgo, mi mettono ad apparecchiare. Prima il palio.": {
"file": "andrea-8945f1786d.mp3", "file": "andrea-8945f1786d.mp3",
@@ -1639,6 +1643,14 @@
"file": "andrea-018183c4dd.mp3", "file": "andrea-018183c4dd.mp3",
"sec": 5.82 "sec": 5.82
}, },
"andrea|Su questo campo ci abbiamo passato trentacinque anni, giorno, pomeriggio e sera, a giocare a calcio.": {
"file": "andrea-5550b0890f.mp3",
"sec": 7.74
},
"andrea|Le maglie a fare da pali, e i palloni tagliati che entravano nelle terrazze, ovunque.": {
"file": "andrea-f6c10fd559.mp3",
"sec": 6.28
},
"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
+9 -3
View File
@@ -256,11 +256,17 @@ I prompt fissi (stile, scene, foglio sprite) sono in `prompts/STILE.md`, escluso
`assets/closeups/gp-fortezza.png` (prompt `prompts/closeups/gp-fortezza.txt`, guida dalla planimetria `assets/closeups/gp-fortezza.png` (prompt `prompts/closeups/gp-fortezza.txt`, guida dalla planimetria
`assets/raw/fortezza-planimetria.png`, satellite in `assets/raw/fortezza-satellite.png`); il tracciato è ricalcato `assets/raw/fortezza-planimetria.png`, satellite in `assets/raw/fortezza-satellite.png`); il tracciato è ricalcato
sulla strada dipinta. Disegno `gara-render.js`, test `tests/gara.test.mjs`. I piloti sono dati in 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, `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 truccatissimo, motore 6, viola e giallo dalla sua foto, ritratto di Codex), 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) ha dato il consenso il 28 ottobre; chi ha `consenso: null` in piloti.js non corre. Mancano: quanto erano
truccati gli altri, le battute di Andrea, il rumore del motore (c'è solo 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). 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
lui; i camper stanno dietro il suo campo). Cardetole: in basso a destra «dal Giannini» (le villette), a destra il
ponticino della Carza, a sinistra il Trappola. Il campo del Giannini: a destra le villette, a sinistra il Trappola;
dal ponticino non ci si va. Via Liguri Magelli: lassù vicino al Betino si torna a casa, laggiù a destra il campo.
Da sapere: dove si gira, in fondo al ponticino, per Cardetole [DA ANDREA].
- Via Liguri Magelli (Street View di Andrea, 28 ottobre), a sinistra dal campo del Giannini: a destra la casa salmone - Via Liguri Magelli (Street View di Andrea, 28 ottobre), a sinistra dal campo del Giannini: a destra la casa salmone
con la tenda del Trappola (il Barrettino, «Vai dal Trappola»: niente interno, [DA ANDREA]), l'Arcu sotto la tenda con la tenda del Trappola (il Barrettino, «Vai dal Trappola»: niente interno, [DA ANDREA]), l'Arcu sotto la tenda
(scherzo: «ti ho visto du' minuti fa a Campomigliaio» col Betino, poi la sfida del GP), davanti la targa «Qui riposa la (scherzo: «ti ho visto du' minuti fa a Campomigliaio» col Betino, poi la sfida del GP), davanti la targa «Qui riposa la
+6 -3
View File
@@ -221,9 +221,12 @@
guida mostra la parabola fino al primo urto, Seria solo l’inizio, A fassi male niente.</p> guida mostra la parabola fino al primo urto, Seria solo l’inizio, A fassi male niente.</p>
</details> </details>
<details id="gara-help" hidden><summary>Come si gioca</summary> <details id="gara-help" hidden><summary>Come si gioca</summary>
<p>Il GP di Fortezza: tre giri attorno alle mura coi cinquantini. Tieni premuto sulla scena (o Spazio, o ↑) per il <p>Il GP di Fortezza: tre giri attorno alle mura coi cinquantini. Con un solo dito: tieni premuto sulla scena per
gas; sterza con le frecce ← →, oppure tenendo premuto e trascinando di lato col dito o col mouse, oppure tenendo il gas e trascina verso la direzione in cui vuoi andare sullo schermo. A destra vai verso destra, anche quando
premuti i pulsanti Sterza. ↓ (o il pulsante Frena, tenuto premuto) frena, anche col gas dato. Dal pannello il 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.</p>
<p>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.</p> pulsante alterna Gas e Molla. Durante la presentazione dei piloti un tocco passa al prossimo.</p>
<p>Le frecce gialle in terra sono acceleratori; le lattine rosse di miscela danno uno sprint (e ricompaiono); nelle <p>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 buche, se ci passi forte, caschi. Fuori strada l'erba frena, e nella paglia ci si ferma. Alla bona la pista mostra
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "leggenda-del-regolo", "name": "leggenda-del-regolo",
"version": "0.1.31", "version": "0.1.34",
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {
+3 -2
View File
@@ -2,8 +2,9 @@
// Senza verbo scelto vale il clic intelligente: l'azione più sensata per quel punto. // Senza verbo scelto vale il clic intelligente: l'azione più sensata per quel punto.
import { VERB_LABELS } from './game.js'; import { VERB_LABELS } from './game.js';
// «Vai a» davanti a un nome che comincia con «dal», «dalla»…: «Vai dal Trappola», non «Vai a dal Trappola». // «Vai a» davanti a un nome che dice già dove («dal Trappola», «a destra, …», «verso …», «indietro, …»): solo «Vai».
const verbLabel = (verb, name) => verb === 'walk' && /^da(l|lla|i|gli|ll')?\b/i.test(name ?? '') ? 'Vai' : VERB_LABELS[verb]; const WHERE = /^(da(l|lla|i|gli|ll')?|a|verso|indietro|su|giù|in|laggiù|lassù|dietro|oltre)(?=[\s,]|$)/i;
const verbLabel = (verb, name) => verb === 'walk' && WHERE.test(name ?? '') ? 'Vai' : VERB_LABELS[verb];
export class Command { export class Command {
constructor(game) { constructor(game) {
+24 -3
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 { text, panel } from './bocce-render.js';
import { ambientHash as hash } from './ambient.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.color; ctx.fillRect(-4, -2, 9, 4);
ctx.fillStyle = r.trim ?? '#1c1c1c'; ctx.fillRect(-4, -0.5, 9, 1); 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 = '#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. // La miscela: una fiammata dietro.
if (r.boost > 0 && Math.floor(c.age * 20) % 2) { ctx.fillStyle = '#ffb030'; ctx.fillRect(-10, -1, 3, 2); } if (r.boost > 0 && Math.floor(c.age * 20) % 2) { ctx.fillStyle = '#ffb030'; ctx.fillRect(-10, -1, 3, 2); }
ctx.restore(); 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); 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'); 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 // 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, // 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. // `sheet(id)` lo sprite di un personaggio.
@@ -139,6 +155,9 @@ function card(ctx, c, look, sheet) {
panel(ctx, x, y, w, h); panel(ctx, x, y, w, h);
text(ctx, 'GP DI FORTEZZA', x + w / 2, y + 10, 6, '#ffe28a', 'center'); text(ctx, 'GP DI FORTEZZA', x + w / 2, y + 10, 6, '#ffe28a', 'center');
figure(ctx, r, x + 46, y + 14, look, sheet); 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). // 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; const moto = r.moto && look(r.moto), t = Math.min(1, (c.t % INTRO.each) / 0.6), e = 1 - (1 - t) ** 3;
if (moto) { if (moto) {
@@ -155,7 +174,8 @@ function card(ctx, c, look, sheet) {
for (const [k, [label, value]] of [['MOTORE', r.motore], ['TESTA', r.testa]].entries()) { for (const [k, [label, value]] of [['MOTORE', r.motore], ['TESTA', r.testa]].entries()) {
const yy = y + 116 + k * 12; const yy = y + 116 + k * 12;
text(ctx, label, x + 222, yy, 5, '#c5c6b6'); text(ctx, label, x + 222, yy, 5, '#c5c6b6');
for (let i = 0; i < 5; i++) { ctx.fillStyle = i < value ? (k ? '#b9d392' : '#e0583a') : '#3a3f3c'; ctx.fillRect(x + 262 + i * 13, yy - 3, 10, 6); } // Cinque caselle; il motore truccatissimo (6) ne accende una sesta, gialla.
for (let i = 0; i < Math.max(5, value); i++) { ctx.fillStyle = i >= 5 ? '#ffe28a' : i < value ? (k ? '#b9d392' : '#e0583a') : '#3a3f3c'; ctx.fillRect(x + 262 + i * 12, yy - 3, 9, 6); }
} }
text(ctx, 'TOCCA: IL PROSSIMO', x + w / 2, y + h - 6, 5, '#8f9488', 'center'); text(ctx, 'TOCCA: IL PROSSIMO', x + w / 2, y + h - 6, 5, '#8f9488', 'center');
} }
@@ -204,6 +224,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. // 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); 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 === 'intro') { card(ctx, c, look, sheet); ctx.restore(); return; }
if (c.step === 'race' || c.step === 'start') swipe(ctx, c);
hud(ctx, c); hud(ctx, c);
if (c.step === 'race') position(ctx, c); if (c.step === 'race') position(ctx, c);
if (c.step === 'start') lights(ctx, c); if (c.step === 'start') lights(ctx, c);
+24 -12
View File
@@ -3,7 +3,7 @@
// la planimetria generale 1:500 che ha mandato Andrea (`assets/raw/fortezza-planimetria.png`), schiacciata nel // 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. // 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. // 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. // Fuori strada c'è l'erba che frena, e poi la paglia: chi ci arriva forte si ferma.
@@ -29,15 +29,17 @@ 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 }; 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. // 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 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 }; 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 clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
const wrapAngle = a => Math.atan2(Math.sin(a), Math.cos(a)); const wrapAngle = a => Math.atan2(Math.sin(a), Math.cos(a));
// I piloti sono dati (story.closeups.gara.riders, da piloti.js): `motore` da 1 (di serie) a 5 (truccato da far // I piloti sono dati (story.closeups.gara.riders, da piloti.js): `motore` da 1 (di serie) a 5 (truccato da far
// paura), `testa` da 1 (matto) a 5 (prudente). Ne vengono velocità massima e accelerazione in px/s, tenuta, e per gli // paura, 6 truccatissimo), `testa` da 1 (matto) a 5 (prudente). Ne vengono velocità massima e accelerazione in px/s, tenuta, e per gli
// altri quanto si fidano delle curve (`caution`) e quanto spesso rischiano troppo (`slip`). // altri quanto si fidano delle curve (`caution`) e quanto spesso rischiano troppo (`slip`).
export function rider({ motore = 3, testa = 3, ...r }) { export function rider({ motore = 3, testa = 3, ...r }) {
const m = clamp(motore, 1, 5), t = clamp(testa, 1, 5); // Il 6 è il truccatissimo: lo SR di Puntella (Andrea, 28 ottobre).
const m = clamp(motore, 1, 6), t = clamp(testa, 1, 5);
return { ...r, motore: m, testa: t, vmax: 74 + 6.5 * m, accel: 40 + 5 * m, grip: 1.1 - 0.03 * m, caution: 1.0025 - 0.0125 * t, slip: 0.4275 - 0.0775 * t }; return { ...r, motore: m, testa: t, vmax: 74 + 6.5 * m, accel: 40 + 5 * m, grip: 1.1 - 0.03 * m, caution: 1.0025 - 0.0125 * t, slip: 0.4275 - 0.0775 * t };
} }
// Il gruppo di prova, quando la storia non ne dà uno. // Il gruppo di prova, quando la storia non ne dà uno.
@@ -150,7 +152,7 @@ export function newGara(seed = Date.now() >>> 0, def = {}) {
if (!riders.some(r => r.id === 'andrea')) throw new Error('Nella gara manca Andrea'); 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; } 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 }, 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: {} }; 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'); export const player = c => c.riders.find(r => r.id === 'andrea');
@@ -167,7 +169,7 @@ export const setPower = () => false;
function sync(c) { function sync(c) {
c.holding = c.keys.gas.size > 0 || c.pointer !== null || c.toggle; c.holding = c.keys.gas.size > 0 || c.pointer !== null || c.toggle;
c.braking = c.keys.brake || c.brakeButton; 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. // Il primo tocco (o tasto) salta la presentazione, senza dare gas.
export function press(c, id = 'press') { export function press(c, id = 'press') {
@@ -209,12 +211,14 @@ function place(r, s, shown) {
export const presenting = c => c.step === 'intro' ? c.riders[Math.floor(c.t / INTRO.each)] ?? null : null; 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). // 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; } 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 // Lo swipe indica dove andare sullo schermo. L'origine segue il dito oltre i 60 px.
// passa senza premere, o col gas dato da tastiera o dal pannello, non sterza. export function aimScreen(c, [x, y], id = 'press') {
export function aimScreen(c, [x], id = 'press') { if (!Number.isFinite(x) || !Number.isFinite(y)) return false;
if (!Number.isFinite(x)) return false; if (c.pointer !== null && c.pointer !== id) return false;
if (c.pointer === null) c.anchor = x; if (c.pointer === null || !c.anchor) c.anchor = [x, y];
else if (c.pointer === id) c.drag = clamp((x - c.anchor) / 28, -1, 1); 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; return false;
} }
// La tastiera: ← → sterzano, ↑ Spazio e Invio sono il gas, ↓ il freno; finché si tengono giù. // La tastiera: ← → sterzano, ↑ Spazio e Invio sono il gas, ↓ il freno; finché si tengono giù.
@@ -233,7 +237,15 @@ export function key(c, name, down) {
// I pulsanti del pannello tenuti premuti: Sterza (-1, 0, 1) e Frena. // I pulsanti del pannello tenuti premuti: Sterza (-1, 0, 1) e Frena.
export function steerButton(c, dir) { c.buttons = dir; return true; } export function steerButton(c, dir) { c.buttons = dir; return true; }
export function brakeButton(c, down) { c.brakeButton = down; sync(c); 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). // 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); 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); canvas.setPointerCapture(e.pointerId);
return; 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). // (sterzo e freno hanno i loro pulsanti, che si tengono insieme al gas).
if (type === 'gara') { if (type === 'gara') {
if (press || game.skip()) return; 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.' }, 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.' }, 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). // 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.' }, 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; const miniState = () => running && game?.closeup && MINI[story.closeups[game.closeup.id].type] ? game.closeup : null;
+9 -5
View File
@@ -1,18 +1,22 @@
// La gara dei cinquantini (idea di Andrea, 28 ottobre): chi corre, in ordine di griglia (due per fila). // La gara dei cinquantini (idea di Andrea, 28 ottobre): chi corre, in ordine di griglia (due per fila).
// Ogni amico che manda la foto del suo motorino si aggiunge con una riga. `motore`: da 1 (di serie) a 5 (truccato da // 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. // far paura; 6 truccatissimo, solo lo SR di Puntella); `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 // 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). // Persone vere solo col loro consenso (`consenso`: chi l'ha dato e quando).
export const piloti = [ 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). // Puntella (Marco Villani, consenso del 28 ottobre) con lo SR truccatissimo, viola e giallo come il suo (foto di
{ id: 'puntella', name: 'Puntella', bike: 'lo SR', motore: 5, testa: 1, color: '#5a3fb5', trim: '#e8442e', moto: 'assets/closeups/gp/sr.png', consenso: null }, // [DA ANDREA: consenso di Marco Villani] // Andrea); il ritratto dalla sua foto.
{ id: 'puntella', name: 'Puntella', bike: 'lo SR truccatissimo', motore: 6, testa: 1, color: '#5b2a9e', trim: '#f2c81e', moto: 'assets/closeups/gp/sr.png', face: 'assets/closeups/gp/puntella.png', consenso: '2026-10-28' },
// Cistola (Davide Poggi) col suo CR 50 Z, blu notte e argento, dalla foto che ha mandato; il volto dal suo murale. // Cistola (Davide Poggi) col suo CR 50 Z, blu notte e argento, dalla foto che ha mandato; il volto dal suo murale.
{ id: 'cistola', name: 'Cistola', bike: 'il CR 50 Z', motore: 4, testa: 3, color: '#1d2440', trim: '#a9adb5', moto: 'assets/closeups/gp/cr50z.png', face: 'assets/closeups/gp/cistola.png', consenso: '2026-10-27' }, { id: 'cistola', name: 'Cistola', bike: 'il CR 50 Z', motore: 4, testa: 3, color: '#1d2440', trim: '#a9adb5', moto: 'assets/closeups/gp/cr50z.png', face: 'assets/closeups/gp/cistola.png', consenso: '2026-10-27' },
// Il Malaguti Fifty di Andrea: rosso con le rifiniture nere. // 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' }, { 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). // 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 notte con poche pale di remo bianche, strette e a punta (foto di Andrea, 28 ottobre).
casco: 'assets/closeups/gp/casco-tozzino.png', helmet: '#131c3c', stripe: '#f4f4f4' },
// Il Betino dell'Arcu: verde e crema, lento ma non cade mai. // 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' }, { id: 'arcu', name: 'L\'Arcu', bike: 'il Betino', motore: 1, testa: 5, color: '#2f7a3a', trim: '#e8d9a0', moto: 'assets/closeups/gp/betino.png', sprite: 'arcu' },
]; ];
+50 -13
View File
@@ -2578,10 +2578,11 @@ export const story = {
}, },
}, },
{ {
id: 'all_area_camper', name: 'a sinistra, verso l\'area camper', box: [320, 160, 110, 42], at: [442, 202], exit: { scene: 'area_camper', at: [600, 330], face: 'left' }, id: 'all_area_camper', name: 'a sinistra, verso le villette del Giannini', box: [320, 160, 110, 42], at: [442, 202], exit: { scene: 'area_camper', at: [600, 330], face: 'left' },
}, },
{ {
id: 'al_giannini', name: 'in fondo a sinistra, verso il campo del Giannini', box: [380, 165, 58, 30], at: [440, 200], exit: { scene: 'campo_giannini', at: [480, 218], face: 'left' }, // [DA ANDREA: dove si gira per Cardetole, in fondo al ponticino]
id: 'a_cardetole', name: 'in fondo a destra, verso Cardetole', box: [578, 80, 62, 90], at: [476, 204], exit: { scene: 'cardetole', at: [628, 290], face: 'left' },
}, },
{ {
id: 'alla_stazione', name: 'a destra, verso la stazione', box: [486, 160, 90, 40], at: [474, 202], exit: { scene: 'stazione', at: [20, 300], face: 'right' }, id: 'alla_stazione', name: 'a destra, verso la stazione', box: [486, 160, 90, 40], at: [474, 202], exit: { scene: 'stazione', at: [20, 300], face: 'right' },
@@ -2592,7 +2593,8 @@ export const story = {
], ],
}, },
area_camper: { area_camper: {
name: 'Area camper', // Le villette dove abita il Giannini: da qui si va al suo campo (Andrea, 28 ottobre). L'id resta area_camper.
name: 'Le villette del Giannini',
map: [70, 84], map: [70, 84],
background: 'assets/scenes/area-camper.png', background: 'assets/scenes/area-camper.png',
walk: [[0, 360], [0, 260], [200, 232], [380, 240], [450, 248], [640, 300], [640, 360]], walk: [[0, 360], [0, 260], [200, 232], [380, 240], [450, 248], [640, 300], [640, 360]],
@@ -2606,13 +2608,18 @@ export const story = {
}, },
}, },
{ {
id: 'villette', name: 'villette', box: [500, 80, 140, 120], at: [560, 280], face: 'right', id: 'villette', name: 'villette', box: [500, 80, 140, 106], at: [560, 280], face: 'right',
actions: { actions: {
look: [{ say: 'Le villette rosa coi balconi bianchi. Da qui a casa mia è un attimo.' }], look: [{ say: 'Le villette rosa coi balconi bianchi. Da qui a casa mia è un attimo.' }],
}, },
}, },
{ {
id: 'a_cardetole', name: 'verso casa, a Cardetole', box: [0, 230, 80, 130], at: [20, 300], exit: { scene: 'cardetole', at: [300, 350], face: 'up' }, // Casa è a destra, per la strada (Andrea, 28 ottobre).
id: 'a_cardetole', name: 'verso casa, a Cardetole', box: [572, 188, 68, 96], at: [618, 300], exit: { scene: 'cardetole', at: [560, 348], face: 'up' },
},
{
// Laggiù, in fondo al vialetto vicino alla siepe, il campo del Giannini (Andrea, 28 ottobre).
id: 'al_giannini', name: 'laggiù, il campo del Giannini', box: [330, 150, 96, 74], at: [380, 246], exit: { scene: 'campo_giannini', at: [480, 216], face: 'left' },
}, },
{ {
id: 'al_ponticino', name: 'indietro, il ponticino', box: [560, 300, 80, 60], at: [620, 330], exit: { scene: 'ponticino', at: [442, 202], face: 'down' }, id: 'al_ponticino', name: 'indietro, il ponticino', box: [560, 300, 80, 60], at: [620, 330], exit: { scene: 'ponticino', at: [442, 202], face: 'down' },
@@ -2620,7 +2627,8 @@ export const story = {
], ],
}, },
// Il prato del parco dei camper visto dall'altra parte: per i ragazzi, «il campo del Giannini». Ci si arriva dal // Il prato del parco dei camper visto dall'altra parte: per i ragazzi, «il campo del Giannini». Ci si arriva dal
// ponticino della Carza; niente uscita verso l'area camper, perché è già questa (Andrea, 28 ottobre). // lato delle villette dove abita il Giannini (fra gli alberi a destra) e dal Trappola; dal ponticino non si viene
// qui. L'area camper è il campo stesso: i camper stanno qui dietro (Andrea, 28 ottobre).
campo_giannini: { campo_giannini: {
name: 'Il campo del Giannini', name: 'Il campo del Giannini',
map: [82, 96], map: [82, 96],
@@ -2636,6 +2644,12 @@ export const story = {
use: [{ say: 'Senza pallone, e senza la squadra, non vale.' }], use: [{ say: 'Senza pallone, e senza la squadra, non vale.' }],
}, },
}, },
{
id: 'camper', name: 'qui dietro, i camper', box: [520, 320, 120, 40], at: [560, 340], face: 'down', default: 'look',
actions: {
look: [{ say: 'Qui dietro ci stanno i camper. Ma per noi questo resta il campo del Giannini.' }],
},
},
{ {
id: 'giochi', name: 'giochi dei bambini', box: [220, 170, 80, 40], at: [260, 216], face: 'up', id: 'giochi', name: 'giochi dei bambini', box: [220, 170, 80, 40], at: [260, 216], face: 'up',
actions: { actions: {
@@ -2643,10 +2657,11 @@ export const story = {
}, },
}, },
{ {
id: 'al_ponticino', name: 'verso il ponticino della Carza', box: [440, 150, 90, 60], at: [480, 216], exit: { scene: 'ponticino', at: [444, 204], face: 'right' }, // Fra gli alberi a destra si arriva alle villette del Giannini, in fondo al vialetto (Andrea, 28 ottobre).
id: 'all_area_camper', name: 'verso le villette del Giannini', box: [440, 150, 90, 60], at: [480, 216], exit: { scene: 'area_camper', at: [380, 246], face: 'down' },
}, },
{ {
id: 'via_liguri_magelli', name: 'Via Liguri Magelli', box: [0, 100, 70, 260], at: [30, 260], exit: { scene: 'via_liguri_magelli', at: [320, 340], face: 'up' }, id: 'via_liguri_magelli', name: 'dal Trappola, in Via Liguri Magelli', box: [0, 100, 70, 260], at: [30, 260], exit: { scene: 'via_liguri_magelli', at: [520, 306], face: 'left' },
}, },
], ],
}, },
@@ -2697,7 +2712,12 @@ export const story = {
}, },
}, },
{ {
id: 'al_giannini', name: 'verso il campo del Giannini', box: [180, 330, 280, 30], at: [320, 345], exit: { scene: 'campo_giannini', at: [60, 250], face: 'right' }, // Casa di Andrea è lassù, in cima alla salita, a destra del Betino (Andrea, 28 ottobre).
id: 'a_cardetole', name: 'lassù, verso casa a Cardetole', box: [274, 148, 40, 50], at: [264, 202], exit: { scene: 'cardetole', at: [20, 352], face: 'right' },
},
{
// Il campo del Giannini è laggiù a destra, oltre la siepe (Andrea, 28 ottobre).
id: 'al_giannini', name: 'verso il campo del Giannini', box: [430, 150, 132, 100], at: [520, 306], exit: { scene: 'campo_giannini', at: [60, 250], face: 'right' },
}, },
], ],
}, },
@@ -2713,21 +2733,38 @@ export const story = {
{ {
id: 'casa', name: 'casa di Andrea', box: [330, 20, 220, 212], at: [470, 290], face: 'up', id: 'casa', name: 'casa di Andrea', box: [330, 20, 220, 212], at: [470, 290], face: 'up',
actions: { actions: {
look: [{ say: 'Casa mia. Terzo piano, quello con la tapparella a metà: è la mia camera.' }], look: [{ say: 'Casa mia. Secondo piano: quella con la tapparella a metà è la camera di mia sorella, e a destra quella dei miei.' }],
open: [{ say: 'Se salgo, mi mettono ad apparecchiare. Prima il palio.' }], open: [{ say: 'Se salgo, mi mettono ad apparecchiare. Prima il palio.' }],
walk: [{ say: 'Se salgo, mi mettono ad apparecchiare. Prima il palio.' }], walk: [{ say: 'Se salgo, mi mettono ad apparecchiare. Prima il palio.' }],
}, },
}, },
{ {
id: 'platano', name: 'platano', box: [90, 0, 170, 216], at: [200, 306], face: 'up', id: 'platano', name: 'platano', box: [90, 0, 170, 206], at: [200, 306], face: 'up',
actions: { actions: {
look: [{ say: 'Il prato sotto casa. Qui le porte le facevamo con le maglie per terra: due maglie, un palo ciascuna.' }, { say: 'E ogni gol era «Era fuori!», «No, era dentro!».' }], look: [{ say: 'Il prato sotto casa. Qui le porte le facevamo con le maglie per terra: due maglie, un palo ciascuna.' }, { say: 'E ogni gol era «Era fuori!», «No, era dentro!».' }],
use: [{ say: 'Arrampicarmi? Alla mia età, e davanti a tutto il condominio?' }], use: [{ say: 'Arrampicarmi? Alla mia età, e davanti a tutto il condominio?' }],
}, },
}, },
// Si compare a destra dell'area camper: inquadrature invertite rispetto alle foto (Andrea, 28 ottobre, va bene così).
{ {
id: 'alla_pieve', name: 'su verso la Pieve', box: [0, 330, 640, 30], at: [300, 354], exit: { scene: 'area_camper', at: [600, 330], face: 'left' }, // Il prato in salita sotto casa: le parole sono di Andrea (28 ottobre).
id: 'prato', name: 'prato', box: [52, 208, 278, 76], at: [262, 300], face: 'up', default: 'look',
actions: {
look: [
{ say: 'Su questo campo ci abbiamo passato trentacinque anni, giorno, pomeriggio e sera, a giocare a calcio.' },
{ say: 'Le maglie a fare da pali, e i palloni tagliati che entravano nelle terrazze, ovunque.' },
],
},
},
// Le uscite di casa (Andrea, 28 ottobre): in basso a destra il Giannini (le sue villette, dalla strada delle
// villette), a destra il ponticino della Carza, a sinistra il Trappola.
{
id: 'al_giannini', name: 'dal Giannini', box: [440, 322, 200, 38], at: [560, 348], exit: { scene: 'area_camper', at: [618, 300], face: 'left' },
},
{
id: 'al_ponticino', name: 'a destra, il ponticino della Carza', box: [596, 150, 44, 110], at: [628, 290], exit: { scene: 'ponticino', at: [476, 204], face: 'down' },
},
{
id: 'al_trappola', name: 'a sinistra, dal Trappola', box: [0, 236, 50, 104], at: [20, 352], exit: { scene: 'via_liguri_magelli', at: [264, 202], face: 'down' },
}, },
], ],
}, },
+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.31'; export const VERSION = '0.1.34';
// 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).
+7
View File
@@ -16,6 +16,13 @@ test('clic intelligente: parla con le persone, guarda le cose, attraversa le usc
assert.equal(c.sentence('vecchietti'), 'Parla con vecchietti'); assert.equal(c.sentence('vecchietti'), 'Parla con vecchietti');
assert.equal(c.sentence('manifesto'), 'Guarda manifesto del Palio'); assert.equal(c.sentence('manifesto'), 'Guarda manifesto del Palio');
assert.equal(c.sentence('verso_piazzetta'), 'Vai a Via dell\'Antica Posta, verso Piazzetta Cambray Digny'); assert.equal(c.sentence('verso_piazzetta'), 'Vai a Via dell\'Antica Posta, verso Piazzetta Cambray Digny');
// Quando il nome dice già dove, niente «a» in più: «Vai dal Trappola», «Vai a destra, …».
const casa = new Game(story), cc = new Command(casa); casa.go('cardetole');
assert.equal(cc.sentence('al_giannini'), 'Vai dal Giannini');
assert.equal(cc.sentence('al_ponticino'), 'Vai a destra, il ponticino della Carza');
casa.go('via_liguri_magelli');
assert.equal(cc.sentence('a_cardetole'), 'Vai lassù, verso casa a Cardetole');
assert.equal(cc.sentence('trappola'), 'Vai dal Trappola');
assert.equal(c.clickScene(at('vecchietti')), 'act'); assert.equal(c.clickScene(at('vecchietti')), 'act');
settle(g); settle(g);
assert.equal(g.dialogue?.id, 'vecchietti'); assert.equal(g.dialogue?.id, 'vecchietti');
+77 -11
View File
@@ -1,6 +1,6 @@
import test from 'node:test'; import test from 'node:test';
import assert from 'node:assert/strict'; 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 { piloti } from '../src/story/piloti.js';
import { story } from '../src/story/story.js'; import { story } from '../src/story/story.js';
import { Game } from '../src/engine/game.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 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; }; 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é, // 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). // frenando, arriverebbe a ogni curva un po' sotto il limite. Usa i comandi del giocatore (il trascinamento).
const careful = c => { 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 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 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 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; swipeTo(c, pointAt(r.s + 14 + r.v * 0.18, lane));
// 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');
const horizon = r.v * r.v / (2 * BRAKE) + 20; const horizon = r.v * r.v / (2 * BRAKE) + 20;
let brake = false; 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; } } 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; } }
@@ -36,11 +39,12 @@ const careful = c => {
return c.holding; return c.holding;
}; };
test('i piloti sono dati: motore e testa da 1 a 5 danno velocità, accelerazione e prudenza', () => { test('i piloti sono dati: motore da 1 a 6 e testa da 1 a 5 danno velocità, accelerazione e prudenza', () => {
const slow = rider({ motore: 1, testa: 5 }), fast = rider({ motore: 5, testa: 1 }); const slow = rider({ motore: 1, testa: 5 }), fast = rider({ motore: 5, testa: 1 });
assert.ok(fast.vmax > slow.vmax && fast.accel > slow.accel && fast.grip < slow.grip); assert.ok(fast.vmax > slow.vmax && fast.accel > slow.accel && fast.grip < slow.grip);
assert.ok(fast.slip > slow.slip && fast.caution > slow.caution); assert.ok(fast.slip > slow.slip && fast.caution > slow.caution);
assert.deepEqual(rider({ motore: 9, testa: -2 }).motore, 5, 'fuori scala si ferma ai bordi'); assert.deepEqual(rider({ motore: 9, testa: -2 }).motore, 6, 'fuori scala si ferma ai bordi (6, il truccatissimo)');
assert.equal(rider({ motore: 9, testa: -2 }).testa, 1);
// Ogni pilota della storia ha nome, motorino e colore; Andrea c'è una volta sola. // Ogni pilota della storia ha nome, motorino e colore; Andrea c'è una volta sola.
for (const p of piloti) { assert.ok(p.id && p.name && p.bike && /^#[0-9a-f]{6}$/i.test(p.color), p.id); } for (const p of piloti) { assert.ok(p.id && p.name && p.bike && /^#[0-9a-f]{6}$/i.test(p.color), p.id); }
assert.equal(piloti.filter(p => p.id === 'andrea').length, 1); assert.equal(piloti.filter(p => p.id === 'andrea').length, 1);
@@ -98,15 +102,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); aimScreen(c, [300, 100], 5); aimScreen(c, [360, 100], 5);
assert.equal(steering(c), -1); assert.equal(steering(c), -1);
key(c, 'ArrowUp', false); key(c, 'ArrowLeft', false); 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, [300, 100], 9); press(c, 9);
aimScreen(c, [314, 100], 9); 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.equal(steering(c), 0.5); 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'); 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'); 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'); 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', () => { test('la presentazione: uno alla volta arrivano in griglia col motorino, poi il semaforo', () => {
const c = newGara(2, { riders: piloti }); const c = newGara(2, { riders: piloti });
assert.equal(c.step, 'intro'); assert.equal(presenting(c).id, piloti[0].id); assert.equal(c.step, 'intro'); assert.equal(presenting(c).id, piloti[0].id);
@@ -185,7 +251,7 @@ test('nella storia: dal Trappola l\'Arcu scherza sul Betino, poi la sfida; prima
assert.ok(before.some(t => t.includes('prendo la rincorsa'))); assert.ok(before.some(t => t.includes('prendo la rincorsa')));
assert.equal(g.closeup?.id, 'gara'); assert.equal(g.closeup.level, 'seria'); assert.equal(g.closeup?.id, 'gara'); assert.equal(g.closeup.level, 'seria');
assert.deepEqual(g.closeup.riders.map(r => r.id), piloti.filter(p => p.consenso !== null).map(p => p.id)); assert.deepEqual(g.closeup.riders.map(r => r.id), piloti.filter(p => p.consenso !== null).map(p => p.id));
assert.ok(!g.closeup.riders.some(r => r.id === 'puntella'), 'senza consenso non corre'); assert.ok(!g.closeup.riders.some(r => r.consenso === null), 'senza consenso non corre');
g.closeup.result = { winner: 'player', place: 1, times: {}, order: ['andrea'], offs: 0 }; g.closeup.step = 'result'; g.closeup.result = { winner: 'player', place: 1, times: {}, order: ['andrea'], offs: 0 }; g.closeup.step = 'result';
g.closeupClose(); const after = settle(g).map(s => s.text); g.closeupClose(); const after = settle(g).map(s => s.text);
assert.ok(g.state.flags.gara_vinta); assert.ok(g.state.flags.gara_vinta);