Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1f49486870 | ||
|
|
06f7cfbfad |
Binary file not shown.
|
After Width: | Height: | Size: 7.7 KiB |
+2
-1
@@ -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
|
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
|
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
|
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
|
- Strade attorno a casa (Andrea, 28 ottobre): la scena `area_camper` ora si chiama «Le villette del Giannini» (ci abita
|
||||||
|
|||||||
+6
-3
@@ -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
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "leggenda-del-regolo",
|
"name": "leggenda-del-regolo",
|
||||||
"version": "0.1.32",
|
"version": "0.1.33",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -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) {
|
||||||
@@ -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.
|
// 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);
|
||||||
|
|||||||
+21
-10
@@ -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,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 };
|
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));
|
||||||
@@ -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');
|
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 +168,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 +210,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 +236,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
@@ -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;
|
||||||
|
|||||||
+5
-2
@@ -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
|
// 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); `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).
|
// 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.
|
// 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 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.
|
// 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' },
|
||||||
];
|
];
|
||||||
|
|||||||
+1
-1
@@ -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.32';
|
export const VERSION = '0.1.33';
|
||||||
|
|
||||||
// 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).
|
||||||
|
|||||||
+73
-8
@@ -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; } }
|
||||||
@@ -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);
|
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);
|
||||||
|
|||||||
Reference in New Issue
Block a user