Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6a5a80d7af | ||
|
|
88f0439f79 | ||
|
|
c244424af9 | ||
|
|
c26d888263 | ||
|
|
f8dbd01358 | ||
|
|
e3d5d8b0d3 | ||
|
|
1b7511f6ca |
@@ -18,6 +18,13 @@ Online su 50037.manzolo.it tutto fino agli effetti ambientali di Codex. Aperto,
|
|||||||
|
|
||||||
## Dove siamo arrivati
|
## Dove siamo arrivati
|
||||||
|
|
||||||
|
- **GP di Fortezza, guida semplice (29 ottobre)**: alla bona e Seria si tiene premuto per il gas e lasciando si
|
||||||
|
frena; lo sterzo segue la strada da solo (scansa le buche, sorpassa, prende la miscela), ma oltre la velocità che la
|
||||||
|
curva regge il motorino scivola fuori verso la paglia (`SIMPLE` in `gara.js`). Ginocchio in terra resta con lo
|
||||||
|
sterzo vero. Il pannello nasconde Sterza, Frena, swipe e sensibilità. Test in `gara.test.mjs` e
|
||||||
|
`npm run browser:gara`. Seria più difficile (parere di Codex, simulato): sopra il 110% del limite in curva si esce
|
||||||
|
di sicuro (stun 1,2 s), la curva pulita dà una spinta, rivali a 0,81; CURVA! (preavviso) e LASCIA! sul motorino.
|
||||||
|
In simulazione: chi frena giusto 1° in 46 s, tutto gas 4°-5° in 55-59 s.
|
||||||
- **La pista col gesso (29 ottobre, modifiche locali)**: le Micro Machines sul parcheggio di Cardetole contro il
|
- **La pista col gesso (29 ottobre, modifiche locali)**: le Micro Machines sul parcheggio di Cardetole contro il
|
||||||
Chiaru (Matteo Chiarusi, sprite di Codex dalla foto di Andrea, accanto alla pista). Motore in `micro.js`, disegno in
|
Chiaru (Matteo Chiarusi, sprite di Codex dalla foto di Andrea, accanto alla pista). Motore in `micro.js`, disegno in
|
||||||
`micro-render.js`: pista disegnata a gesso dal codice sopra l'asfalto di Codex (`micro-parcheggio.png`), un giro,
|
`micro-render.js`: pista disegnata a gesso dal codice sopra l'asfalto di Codex (`micro-parcheggio.png`), un giro,
|
||||||
|
|||||||
+7
-1
@@ -225,7 +225,13 @@
|
|||||||
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. Con un solo dito: tieni premuto sulla scena per
|
<p>Il GP di Fortezza: tre giri attorno alle mura coi cinquantini. <b>Alla bona e Seria</b> la guida è semplice:
|
||||||
|
tieni premuto sulla scena (o Spazio, o il pulsante Gas) per accelerare e lascia per frenare; lo sterzo segue la
|
||||||
|
strada da solo, scansa le buche e sorpassa. Ma se entri in curva troppo forte scivoli fuori, e nella paglia ti
|
||||||
|
fermi: molla prima delle curve. Sopra il motorino «CURVA!» avvisa che ne arriva una, «LASCIA!» dice quando
|
||||||
|
mollare. A Seria chi entra in curva troppo forte va fuori di sicuro, e chi la prende giusta fa la «curva pulita»
|
||||||
|
ed esce con una spinta in più.</p>
|
||||||
|
<p><b>Ginocchio in terra</b> si guida davvero. Con un solo dito: tieni premuto sulla scena per
|
||||||
il gas e trascina verso la direzione in cui vuoi andare sullo schermo. A destra vai verso destra, anche quando
|
il gas e trascina verso la direzione in cui vuoi andare sullo schermo. A destra vai verso destra, anche quando
|
||||||
il motorino scende. I piccoli movimenti non sterzano; il cerchietto segue gli swipe lunghi. Alza il dito per
|
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>
|
mollare il gas e raddrizzare lo sterzo. Funziona anche col mouse premuto.</p>
|
||||||
|
|||||||
+2
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "leggenda-del-regolo",
|
"name": "leggenda-del-regolo",
|
||||||
"version": "0.1.40",
|
"version": "0.1.43",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -8,6 +8,7 @@
|
|||||||
"browser": "node tests/browser.mjs",
|
"browser": "node tests/browser.mjs",
|
||||||
"browser:biliardo": "node tests/biliardo.browser.mjs",
|
"browser:biliardo": "node tests/biliardo.browser.mjs",
|
||||||
"browser:micro": "node tests/micro.browser.mjs",
|
"browser:micro": "node tests/micro.browser.mjs",
|
||||||
|
"browser:gara": "node tests/gara.browser.mjs",
|
||||||
"browser:tressette": "node tests/tressette.browser.mjs",
|
"browser:tressette": "node tests/tressette.browser.mjs",
|
||||||
"browser:scopa": "node tests/scopa.browser.mjs",
|
"browser:scopa": "node tests/scopa.browser.mjs",
|
||||||
"browser:briscola": "node tests/briscola.browser.mjs",
|
"browser:briscola": "node tests/briscola.browser.mjs",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { TRACK, CURVES, PICKUPS, BRAKE, LAPS, COUNTDOWN, ROAD, VERGE, CAN, SWIPE, 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, mustLift, PREAVVISO } 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';
|
||||||
|
|
||||||
@@ -216,6 +216,12 @@ function result(ctx, c) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Una scritta sopra il motorino di Andrea, su un fondo scuro perché si legga anche sullo sterrato e sul telefono.
|
||||||
|
function tag(ctx, line, r, color) {
|
||||||
|
const x = Math.round(r.x), y = Math.round(r.y) - 20, w = line.length * 9 + 10;
|
||||||
|
ctx.fillStyle = 'rgba(23,22,15,0.85)'; ctx.fillRect(x - w / 2, y - 7, w, 14);
|
||||||
|
text(ctx, line, x, y, 10, color, 'center');
|
||||||
|
}
|
||||||
export function drawGara(ctx, c, def, bg, look = () => null, sheet = () => null) {
|
export function drawGara(ctx, c, def, bg, look = () => null, sheet = () => null) {
|
||||||
ctx.save();
|
ctx.save();
|
||||||
if (bg) ctx.drawImage(bg, 0, 0, 640, 360);
|
if (bg) ctx.drawImage(bg, 0, 0, 640, 360);
|
||||||
@@ -232,6 +238,10 @@ export function drawGara(ctx, c, def, bg, look = () => null, sheet = () => null)
|
|||||||
const me = player(c);
|
const me = player(c);
|
||||||
if (c.step === 'race' && me.out > 0) text(ctx, me.lastHole !== undefined ? 'NELLA BUCA!' : 'NELLA PAGLIA!', 320, 180, 10, '#ffe28a', 'center');
|
if (c.step === 'race' && me.out > 0) text(ctx, me.lastHole !== undefined ? 'NELLA BUCA!' : 'NELLA PAGLIA!', 320, 180, 10, '#ffe28a', 'center');
|
||||||
if (c.step === 'race' && me.boost > 0 && me.out <= 0) text(ctx, 'MISCELA!', 320, 180, 8, '#ffb030', 'center');
|
if (c.step === 'race' && me.boost > 0 && me.out <= 0) text(ctx, 'MISCELA!', 320, 180, 8, '#ffb030', 'center');
|
||||||
|
// La guida semplice: LASCIA! sopra il motorino quando serve frenare, e la curva pulita.
|
||||||
|
if (c.step === 'race' && c.holding && mustLift(c)) { if (Math.floor(c.age * 8) % 2) tag(ctx, 'LASCIA!', me, '#ff6a4a'); }
|
||||||
|
else if (c.step === 'race' && c.holding && mustLift(c, PREAVVISO)) tag(ctx, 'CURVA!', me, '#ffe28a');
|
||||||
|
if (c.step === 'race' && c.clean !== undefined && c.t - c.clean < 1.2) text(ctx, 'CURVA PULITA!', 320, 180, 9, '#b9e5b3', 'center');
|
||||||
if (c.step === 'result') result(ctx, c);
|
if (c.step === 'result') result(ctx, c);
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
}
|
}
|
||||||
|
|||||||
+93
-12
@@ -50,12 +50,20 @@ export const RIDERS = [
|
|||||||
{ id: 'andrea', name: 'Andrea', bike: 'il Fifty', motore: 3, testa: 3, color: '#d8402f', trim: '#1c1c1c' },
|
{ id: 'andrea', name: 'Andrea', bike: 'il Fifty', motore: 3, testa: 3, color: '#d8402f', trim: '#1c1c1c' },
|
||||||
{ id: 'arcu', name: 'L\'Arcu', bike: 'il Betino', motore: 1, testa: 5, color: '#2f7a3a', trim: '#e8d9a0' },
|
{ id: 'arcu', name: 'L\'Arcu', bike: 'il Betino', motore: 1, testa: 5, color: '#2f7a3a', trim: '#e8d9a0' },
|
||||||
];
|
];
|
||||||
// I livelli: quanto vanno gli altri, quanto si resta fermi nella paglia, e la guida disegnata in pista.
|
// I livelli: quanto vanno gli altri, quanto si resta fermi nella paglia, e la guida disegnata in pista. `simple`: la
|
||||||
|
// guida semplice (Andrea, 29 ottobre: dal telefono lo sterzo era ingiocabile): si tiene premuto per il gas e lasciando
|
||||||
|
// si frena, lo sterzo segue la strada da solo, e chi entra in curva troppo forte scivola fuori. Ginocchio in terra
|
||||||
|
// tiene lo sterzo vero.
|
||||||
export const LEVELS = {
|
export const LEVELS = {
|
||||||
bona: { rivals: 0.74, stun: 0.4, guide: 'zone' },
|
bona: { rivals: 0.74, stun: 0.4, guide: 'zone', simple: true },
|
||||||
seria: { rivals: 0.84, stun: 0.9, guide: 'coni' },
|
// Seria (Andrea, 29 ottobre: «chi non rallenta in curva deve andare fuori»; valori proposti da Codex e simulati):
|
||||||
|
// si scivola di più, sopra il limite in curva si esce per forza, e la curva pulita dà una spinta.
|
||||||
|
seria: { rivals: 0.81, stun: 1.2, guide: 'coni', simple: true,
|
||||||
|
tune: { slide: 12, clean: { min: 0.85, slack: 2, time: 0.6, accel: 1.2 }, force: { over: 1.1, hold: 0.18, apex: 1.15, time: 0.3, guard: 0.6 } } },
|
||||||
male: { rivals: 0.93, stun: 1.4, guide: null },
|
male: { rivals: 0.93, stun: 1.4, guide: null },
|
||||||
};
|
};
|
||||||
|
// `def.simple` la forza (i test della guida vera la spengono anche alla bona).
|
||||||
|
export const simple = c => !!c.simple;
|
||||||
|
|
||||||
function catmull(p0, p1, p2, p3, t) {
|
function catmull(p0, p1, p2, p3, t) {
|
||||||
const t2 = t * t, t3 = t2 * t;
|
const t2 = t * t, t3 = t2 * t;
|
||||||
@@ -154,7 +162,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, simple: def.simple ?? !!skill.simple, tune: { ...SIMPLE, ...skill.tune }, 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, finger: null, sens: SWIPE.sens,
|
pointer: null, toggle: false, brakeButton: false, drag: 0, buttons: 0, anchor: null, finger: null, sens: SWIPE.sens,
|
||||||
power: SWIPE.sens, accumulator: 0, events: [], result: null, order: riders.map(r => r.id), taken: {} };
|
power: SWIPE.sens, accumulator: 0, events: [], result: null, order: riders.map(r => r.id), taken: {} };
|
||||||
}
|
}
|
||||||
@@ -258,12 +266,75 @@ export function steering(c) {
|
|||||||
// 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);
|
||||||
|
|
||||||
|
// Lo sterzo che segue la strada: si guarda un punto più avanti sulla propria corsia e ci si gira (gli avversari, e
|
||||||
|
// Andrea nella guida semplice). Andando troppo forte in curva lo sterzo non basta e si scivola fuori.
|
||||||
|
const follow = r => { const aim = pointAt(r.s + 14 + r.v * 0.18, r.lane * 0.6); return clamp(wrapAngle(Math.atan2(aim.y - r.y, aim.x - r.x) - r.a) * 2.6, -1, 1); };
|
||||||
|
// La guida semplice di Andrea: lo sterzo scansa le buche, sorpassa e va sulle lattine di miscela; ma oltre la velocità
|
||||||
|
// che la curva regge (la più stretta nei SIMPLE.window pixel davanti) il motorino scivola verso l'esterno, tanto più
|
||||||
|
// quanto si esagera (SIMPLE.slide): a tutto gas, nelle curve vere si finisce nella paglia.
|
||||||
|
// Ogni livello può stringere (LEVELS.*.tune). `clean`: il premio della curva pulita, presa senza mai passare il
|
||||||
|
// limite (`slack` px/s di tolleranza) e senza frenare troppo (almeno `min` del limite): per `time` secondi in uscita
|
||||||
|
// si accelera di più (`accel`), senza superare la velocità massima. `force`: l'uscita sicura, sopra `over` volte il
|
||||||
|
// limite per `hold` secondi (o `apex` volte nel punto più stretto) si va nella paglia in `time` secondi, una volta per
|
||||||
|
// curva; rientrati, `guard` secondi di tregua.
|
||||||
|
export const SIMPLE = { grip: 1, slide: 8, window: 50, clean: null, force: null };
|
||||||
|
export function simpleLimit(r, s = r.s, tune = SIMPLE) {
|
||||||
|
let low = Infinity;
|
||||||
|
for (let d = -10; d <= tune.window; d += STEP) low = Math.min(low, limitAt(s + d));
|
||||||
|
return low * r.grip * tune.grip;
|
||||||
|
}
|
||||||
|
// La curva in corso: dentro si tiene il peggio dell'eccesso e la velocità più bassa (per il premio), e si guarda se
|
||||||
|
// si è andati troppo forte abbastanza a lungo (per l'uscita sicura).
|
||||||
|
function judgeCurve(c, r, dt) {
|
||||||
|
const tune = c.tune, s = ((r.s % TRACK.length) + TRACK.length) % TRACK.length;
|
||||||
|
const k = CURVES.find(k => s >= k.start - 20 && s <= k.end + 10);
|
||||||
|
if (k) {
|
||||||
|
const w = r.curve?.k === k ? r.curve : (r.curve = { k, excess: 0, slow: Infinity, over: 0, fault: false });
|
||||||
|
w.excess = Math.max(w.excess, r.excess); w.slow = Math.min(w.slow, r.v);
|
||||||
|
const f = tune.force, limit = limitAt(k.apex) * r.grip * tune.grip;
|
||||||
|
if (f && !w.fault && !(r.guard > 0) && r.out === 0) {
|
||||||
|
w.over = r.v > limit * f.over ? w.over + dt : 0;
|
||||||
|
if (w.over >= f.hold || (Math.abs(s - k.apex) < STEP * 2 && r.v > limit * f.apex)) { w.fault = true; r.forced = f.time; }
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const w = r.curve; r.curve = null;
|
||||||
|
if (!w || !tune.clean || w.fault) return;
|
||||||
|
const limit = limitAt(w.k.apex) * r.grip * tune.grip;
|
||||||
|
if (w.excess <= tune.clean.slack && w.slow >= limit * tune.clean.min && r.out === 0) {
|
||||||
|
r.push = tune.clean.time; r.cleans = (r.cleans ?? 0) + 1;
|
||||||
|
c.events.push({ kind: 'gara-giro', who: r.id, power: 1 }); c.clean = c.t;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Andrea, nella guida semplice, deve lasciare entro `lead` secondi per arrivare alle curve davanti sotto il limite.
|
||||||
|
// Sopra il motorino: CURVA! in giallo con PREAVVISO secondi d'anticipo, poi LASCIA! in rosso quando è il momento (sul
|
||||||
|
// telefono non serve l'istante preciso, e lasciare troppo presto, che qui vuol dire frenare, non conviene).
|
||||||
|
export const PREAVVISO = 0.7, LIFT = 0;
|
||||||
|
export function mustLift(c, lead = LIFT) {
|
||||||
|
const r = player(c);
|
||||||
|
if (!simple(c) || c.step !== 'race' || r.out > 0) return false;
|
||||||
|
const horizon = r.v * r.v / (2 * BRAKE) + r.v * lead + 10;
|
||||||
|
for (let d = 0; d <= horizon; d += 4) {
|
||||||
|
const a = simpleLimit(r, r.s + d, c.tune), room = Math.max(0, d - r.v * lead);
|
||||||
|
if (r.v > a && r.v * r.v > a * a + 2 * BRAKE * room) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
function autopilot(c, r) {
|
||||||
|
const next = PICKUPS.find(p => p.kind !== 'boost' && ahead(r.s, p.s) < 30 + r.v * 0.6);
|
||||||
|
// Chi sta davanti più lento si passa dall'altra parte; le buche si scansano, la miscela si prende.
|
||||||
|
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 && o.v < r.v + 8);
|
||||||
|
if (next?.kind === 'buca') r.lane = next.lane > 0 ? -5 : 5;
|
||||||
|
else if (slow) r.lane = slow.lat > 0 ? -6 : 6;
|
||||||
|
else if (next?.kind === 'miscela') r.lane = next.lane;
|
||||||
|
r.excess = Math.max(0, r.v - simpleLimit(r, r.s, c.tune));
|
||||||
|
return follow(r);
|
||||||
|
}
|
||||||
// Gli avversari: guardano un punto più avanti sulla loro corsia e ci sterzano; danno gas finché, frenando da qui,
|
// Gli avversari: guardano un punto più avanti sulla loro corsia e ci sterzano; danno gas finché, frenando da qui,
|
||||||
// arriverebbero a ogni curva davanti sotto il limite (un po' meno, quanto si fidano). Chi ha poca testa ogni tanto
|
// arriverebbero a ogni curva davanti sotto il limite (un po' meno, quanto si fidano). Chi ha poca testa ogni tanto
|
||||||
// si fida troppo.
|
// si fida troppo.
|
||||||
function drive(c, r) {
|
function drive(c, r) {
|
||||||
const aim = pointAt(r.s + 14 + r.v * 0.18, r.lane * 0.6);
|
r.steer = follow(r);
|
||||||
r.steer = clamp(wrapAngle(Math.atan2(aim.y - r.y, aim.x - r.x) - r.a) * 2.6, -1, 1);
|
|
||||||
const curve = CURVES.find(k => { const s = ((r.s % TRACK.length) + TRACK.length) % TRACK.length; return s >= k.start - 60 && s < k.start - 57; });
|
const curve = CURVES.find(k => { const s = ((r.s % TRACK.length) + TRACK.length) % TRACK.length; return s >= k.start - 60 && s < k.start - 57; });
|
||||||
if (curve && r.lastCurve !== curve) { r.lastCurve = curve; r.risk = random(c) < r.slip ? 1.15 : 1; }
|
if (curve && r.lastCurve !== curve) { r.lastCurve = curve; r.risk = random(c) < r.slip ? 1.15 : 1; }
|
||||||
const horizon = (r.v * r.v) / (2 * BRAKE) + 20;
|
const horizon = (r.v * r.v) / (2 * BRAKE) + 20;
|
||||||
@@ -283,22 +354,28 @@ function drive(c, r) {
|
|||||||
}
|
}
|
||||||
function stepRider(c, r, dt) {
|
function stepRider(c, r, dt) {
|
||||||
if (r.finished !== null) { r.gas = false; r.brake = false; drive(c, r); r.gas = false; }
|
if (r.finished !== null) { r.gas = false; r.brake = false; drive(c, r); r.gas = false; }
|
||||||
else if (r.out > 0) { r.out = Math.max(0, r.out - dt); r.v = 0; r.u = 0; return; }
|
else if (r.out > 0) { r.out = Math.max(0, r.out - dt); r.v = 0; r.u = 0; if (!r.out && c.tune.force) r.guard = c.tune.force.guard; return; }
|
||||||
|
else if (r.id === 'andrea' && simple(c)) { r.gas = c.holding; r.brake = !c.holding; r.steer = autopilot(c, r); judgeCurve(c, r, dt); }
|
||||||
else if (r.id === 'andrea') { r.gas = c.holding; r.brake = c.braking; r.steer = steering(c); }
|
else if (r.id === 'andrea') { r.gas = c.holding; r.brake = c.braking; r.steer = steering(c); }
|
||||||
else drive(c, r);
|
else drive(c, r);
|
||||||
if (r.finished !== null) r.gas = false;
|
if (r.finished !== null) r.gas = false;
|
||||||
const grass = Math.abs(r.lat) > ROAD, top = r.vmax * (r.boost > 0 ? BOOST.speed : 1) * (grass ? 0.5 : 1);
|
const grass = Math.abs(r.lat) > ROAD, top = r.vmax * (r.boost > 0 ? BOOST.speed : 1) * (grass ? 0.5 : 1);
|
||||||
// Il freno vince sul gas: chi li tiene tutti e due frena.
|
// Il freno vince sul gas: chi li tiene tutti e due frena.
|
||||||
if (r.brake) r.v -= BRAKE * dt;
|
if (r.brake) { r.v -= BRAKE * dt; r.push = 0; }
|
||||||
else if (r.gas) r.v += r.accel * dt * (1 - 0.4 * r.v / top) * (r.boost > 0 ? 1.6 : 1);
|
else if (r.gas) r.v += r.accel * dt * (1 - 0.4 * r.v / top) * (r.boost > 0 ? 1.6 : 1) * (r.push > 0 ? c.tune.clean.accel : 1);
|
||||||
else r.v -= COAST * dt;
|
else r.v -= COAST * dt;
|
||||||
if (r.v > top) r.v = Math.max(top, r.v - 120 * dt);
|
if (r.v > top) r.v = Math.max(top, r.v - 120 * dt);
|
||||||
r.v = Math.max(0, r.v);
|
r.v = Math.max(0, r.v);
|
||||||
r.boost = Math.max(0, r.boost - dt);
|
r.boost = Math.max(0, r.boost - dt);
|
||||||
|
if (r.push) r.push = Math.max(0, r.push - dt);
|
||||||
|
if (r.guard) r.guard = Math.max(0, r.guard - dt);
|
||||||
// Lo sterzo: da fermi non si gira; in curva una parte della velocità resta per traverso e si scivola fuori.
|
// Lo sterzo: da fermi non si gira; in curva una parte della velocità resta per traverso e si scivola fuori.
|
||||||
const turn = r.steer * TURN * Math.min(1, r.v / 22) * dt;
|
const turn = r.steer * TURN * Math.min(1, r.v / 22) * dt;
|
||||||
r.a = wrapAngle(r.a + turn);
|
r.a = wrapAngle(r.a + turn);
|
||||||
r.u -= r.v * Math.sin(turn) * SLIDE * (r.v / r.vmax) / r.grip;
|
r.u -= r.v * Math.sin(turn) * SLIDE * (r.v / r.vmax) / r.grip;
|
||||||
|
if (r.excess) r.u -= TRACK.side[r.idx] * r.excess * c.tune.slide * dt; // la guida semplice: troppo forte, si allarga
|
||||||
|
// L'uscita sicura: verso l'esterno della curva, e lo sterzo non la salva.
|
||||||
|
if (r.forced > 0) { r.forced = Math.max(0, r.forced - dt); r.u = -(r.curve?.k.side ?? TRACK.side[r.idx]) * 90; r.v = Math.max(r.v, 30); }
|
||||||
r.u *= Math.exp(-GRIP * r.grip * dt);
|
r.u *= Math.exp(-GRIP * r.grip * dt);
|
||||||
r.x += (Math.cos(r.a) * r.v - Math.sin(r.a) * r.u) * dt;
|
r.x += (Math.cos(r.a) * r.v - Math.sin(r.a) * r.u) * dt;
|
||||||
r.y += (Math.sin(r.a) * r.v + Math.cos(r.a) * r.u) * dt;
|
r.y += (Math.sin(r.a) * r.v + Math.cos(r.a) * r.u) * dt;
|
||||||
@@ -307,7 +384,8 @@ function stepRider(c, r, dt) {
|
|||||||
if (Math.abs(r.lat) > VERGE) {
|
if (Math.abs(r.lat) > VERGE) {
|
||||||
const h = TRACK.heading[r.idx], p = TRACK.pts[r.idx], side = Math.sign(r.lat), back = side * (VERGE - 1);
|
const h = TRACK.heading[r.idx], p = TRACK.pts[r.idx], side = Math.sign(r.lat), back = side * (VERGE - 1);
|
||||||
r.x = p[0] - Math.sin(h) * back; r.y = p[1] + Math.cos(h) * back; r.lat = back; r.u = 0;
|
r.x = p[0] - Math.sin(h) * back; r.y = p[1] + Math.cos(h) * back; r.lat = back; r.u = 0;
|
||||||
if (r.v > 40 && r.finished === null) {
|
if ((r.v > 40 || r.forced > 0) && r.finished === null) {
|
||||||
|
r.forced = 0;
|
||||||
r.out = c.skill.stun * (r.id === 'andrea' ? 1 : 0.7); r.offs++; r.risk = 1;
|
r.out = c.skill.stun * (r.id === 'andrea' ? 1 : 0.7); r.offs++; r.risk = 1;
|
||||||
c.events.push({ kind: 'gara-fuori', who: r.id, power: r.id === 'andrea' ? 1 : 0.4 });
|
c.events.push({ kind: 'gara-fuori', who: r.id, power: r.id === 'andrea' ? 1 : 0.4 });
|
||||||
}
|
}
|
||||||
@@ -406,7 +484,7 @@ export function status(c) {
|
|||||||
const r = presenting(c);
|
const r = presenting(c);
|
||||||
return r ? `${r.name} con ${r.bike}. (Un tocco: il prossimo.)` : 'Tutti in griglia!';
|
return r ? `${r.name} con ${r.bike}. (Un tocco: il prossimo.)` : 'Tutti in griglia!';
|
||||||
}
|
}
|
||||||
if (c.step === 'start') return c.t < COUNTDOWN - 0.01 ? `GP di Fortezza: si parte fra ${Math.ceil(COUNTDOWN - c.t)}… Tieni premuto per il gas, sterza con ← →.` : 'Via!';
|
if (c.step === 'start') return c.t < COUNTDOWN - 0.01 ? `GP di Fortezza: si parte fra ${Math.ceil(COUNTDOWN - c.t)}… Tieni premuto per il gas, ${simple(c) ? 'lascia per frenare' : 'sterza con ← →'}.` : 'Via!';
|
||||||
if (c.step === 'result') {
|
if (c.step === 'result') {
|
||||||
const who = c.riders.find(r => r.id === c.result.order[0]).name;
|
const who = c.riders.find(r => r.id === c.result.order[0]).name;
|
||||||
return c.result.place === 1 ? `Primo al GP di Fortezza! ${LAPS} giri in ${c.result.times.andrea.toFixed(1)} secondi.` : `${c.result.place}° posto. Ha vinto ${who}.`;
|
return c.result.place === 1 ? `Primo al GP di Fortezza! ${LAPS} giri in ${c.result.times.andrea.toFixed(1)} secondi.` : `${c.result.place}° posto. Ha vinto ${who}.`;
|
||||||
@@ -414,6 +492,9 @@ export function status(c) {
|
|||||||
const me = player(c), place = placeOf(c);
|
const me = player(c), place = placeOf(c);
|
||||||
if (me.out > 0) return me.lastHole !== undefined ? 'Nella buca! Ci si rialza…' : 'Nella paglia! Si riparte…';
|
if (me.out > 0) return me.lastHole !== undefined ? 'Nella buca! Ci si rialza…' : 'Nella paglia! Si riparte…';
|
||||||
if (me.boost > 0) return `Giro ${lapOf(me)} di ${LAPS}, ${place}° posto. Miscela!`;
|
if (me.boost > 0) return `Giro ${lapOf(me)} di ${LAPS}, ${place}° posto. Miscela!`;
|
||||||
|
if (simple(c)) return `Giro ${lapOf(me)} di ${LAPS}, ${place}° posto. ${c.clean !== undefined && c.t - c.clean < 1.5 ? 'Curva pulita!' : mustLift(c) && c.holding ? 'Lascia!' : mustLift(c, PREAVVISO) && c.holding ? 'Curva!' : c.holding ? 'Gas!' : 'Freno.'}`;
|
||||||
return `Giro ${lapOf(me)} di ${LAPS}, ${place}° posto. ${c.braking ? 'Freno!' : c.holding ? 'Gas!' : 'Senza gas.'}`;
|
return `Giro ${lapOf(me)} di ${LAPS}, ${place}° posto. ${c.braking ? 'Freno!' : c.holding ? 'Gas!' : 'Senza gas.'}`;
|
||||||
}
|
}
|
||||||
export const button = c => c.step === 'intro' ? 'Avanti' : c.step === 'result' ? 'Finita' : c.holding ? 'Molla' : 'Gas';
|
// Nella guida semplice il pulsante si tiene premuto: la scritta resta Gas (su iPhone un pulsante che cambia sotto il dito
|
||||||
|
// perde il tocco).
|
||||||
|
export const button = c => c.step === 'intro' ? 'Avanti' : c.step === 'result' ? 'Finita' : simple(c) ? 'Gas' : c.holding ? 'Molla' : 'Gas';
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { HALF, CAR, CARS, canThrow, guide, done, overHeld, carImage, choiceAt } from './micro.js';
|
import { HALF, CAR, CARS, canThrow, guide, done, overHeld, carImage, choiceAt, why, rank } from './micro.js';
|
||||||
import { text, panel } from './bocce-render.js';
|
import { text, panel } from './bocce-render.js';
|
||||||
|
|
||||||
const NAME = { player: 'Andrea', cpu: 'Chiaru' };
|
const NAME = { player: 'Andrea', cpu: 'Chiaru' };
|
||||||
@@ -124,21 +124,27 @@ export function drawMicro(ctx, c, def, bg, image) {
|
|||||||
const level = { bona: 'alla bona', seria: 'seria', male: 'a fassi male' }[c.level];
|
const level = { bona: 'alla bona', seria: 'seria', male: 'a fassi male' }[c.level];
|
||||||
text(ctx, `${level} · spinte ${c.flicks.player}`, 8, 16, 7, '#d9ccba');
|
text(ctx, `${level} · spinte ${c.flicks.player}`, 8, 16, 7, '#d9ccba');
|
||||||
const tint = id => CARS.find(x => x.id === id).color;
|
const tint = id => CARS.find(x => x.id === id).color;
|
||||||
for (const [who, x0, color] of [['player', 168, tint(c.colors.player)], ['cpu', 404, tint(c.colors.cpu)]]) {
|
for (const [who, x0, color] of [['player', 200, tint(c.colors.player)], ['cpu', 430, tint(c.colors.cpu)]]) {
|
||||||
|
// Primo o secondo, mentre si va (Andrea, 29 ottobre).
|
||||||
|
const place = rank(c, who);
|
||||||
|
text(ctx, `${place}°`, x0 - 4, 11, 9, place === 1 ? '#ffe28a' : '#a99d86', 'right');
|
||||||
text(ctx, NAME[who], x0, 11, 8, '#fff0c8');
|
text(ctx, NAME[who], x0, 11, 8, '#fff0c8');
|
||||||
ctx.fillStyle = '#3a372e'; ctx.fillRect(x0 + 52, 7, 150, 8);
|
ctx.fillStyle = '#3a372e'; ctx.fillRect(x0 + 52, 7, 140, 8);
|
||||||
ctx.fillStyle = color; ctx.fillRect(x0 + 52, 7, Math.round(150 * done(c, who)), 8);
|
ctx.fillStyle = color; ctx.fillRect(x0 + 52, 7, Math.round(140 * done(c, who)), 8);
|
||||||
ctx.strokeStyle = '#897b57'; ctx.lineWidth = 1; ctx.strokeRect(x0 + 52.5, 7.5, 149, 7);
|
ctx.strokeStyle = '#897b57'; ctx.lineWidth = 1; ctx.strokeRect(x0 + 52.5, 7.5, 139, 7);
|
||||||
}
|
}
|
||||||
const turn = c.step === 'result' ? null : c.current === 'cpu' ? 'Spinge il Chiaru…' : c.step === 'aim' ? 'Tocca a te: mira e spingi' : null;
|
const turn = c.step === 'result' ? null : c.current === 'cpu' ? 'Spinge il Chiaru…' : c.step === 'aim' ? 'Tocca a te: mira e spingi' : null;
|
||||||
const line = overHeld(c) ? 'Lascia! Sennò è accompagnè' : c.line ?? turn;
|
const line = overHeld(c) ? 'Lascia! Sennò è accompagnè' : c.line ?? turn;
|
||||||
if (line) { ctx.fillStyle = '#17160fd0'; ctx.fillRect(170, 24, 300, 14); text(ctx, line, 320, 31, 8, /^(Fuori|Tagliè|Cappottè|Accompagnè)!/.test(c.line ?? '') ? '#ffbf9f' : '#9fd0ff', 'center'); }
|
if (line) { ctx.fillStyle = '#17160fd0'; ctx.fillRect(170, 24, 300, 14); text(ctx, line, 320, 31, 8, /^(Fuori|Tagliè|Cappottè|Accompagnè)!/.test(c.line ?? '') ? '#ffbf9f' : '#9fd0ff', 'center'); }
|
||||||
if (c.step === 'result') {
|
if (c.step === 'result') {
|
||||||
const w = c.result.winner, title = w === 'player' ? 'HAI BATTUTO IL CHIARU!' : w === 'cpu' ? 'HA VINTO IL CHIARU' : 'ARRIVATI PARI';
|
const w = c.result.winner, title = w === 'player' ? 'HAI BATTUTO IL CHIARU!' : w === 'cpu' ? 'HA VINTO IL CHIARU' : 'ARRIVATI PARI';
|
||||||
panel(ctx, 170, 140, 300, 72);
|
// Il perché, in due righe: chi è arrivato a quale spinta, e di quanto è andato più in là.
|
||||||
text(ctx, title, 320, 160, 12, w === 'player' ? '#b9e5b3' : '#efd49e', 'center');
|
const [first, second] = why(c);
|
||||||
text(ctx, `Un giro, ${c.result.flicks.player} spinte a testa`, 320, 182, 8, '#efd49e', 'center');
|
panel(ctx, 130, 132, 380, 92);
|
||||||
text(ctx, 'Torna dal Chiaru per uscire.', 320, 198, 8, '#d9ccba', 'center');
|
text(ctx, title, 320, 150, 12, w === 'player' ? '#b9e5b3' : '#efd49e', 'center');
|
||||||
|
text(ctx, first, 320, 172, 8, '#efd49e', 'center');
|
||||||
|
text(ctx, second, 320, 186, 8, '#efd49e', 'center');
|
||||||
|
text(ctx, 'Torna dal Chiaru per uscire.', 320, 208, 8, '#d9ccba', 'center');
|
||||||
}
|
}
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
}
|
}
|
||||||
|
|||||||
+17
-3
@@ -265,7 +265,7 @@ function settle(c) {
|
|||||||
const end = c.laps * c.track.length, over = { player: c.cars.player.progress - end, cpu: c.cars.cpu.progress - end };
|
const end = c.laps * c.track.length, over = { player: c.cars.player.progress - end, cpu: c.cars.cpu.progress - end };
|
||||||
if (c.shooter === 'cpu' && (over.player >= 0 || over.cpu >= 0)) {
|
if (c.shooter === 'cpu' && (over.player >= 0 || over.cpu >= 0)) {
|
||||||
const winner = over.player >= 0 && over.cpu >= 0 ? (Math.abs(over.player - over.cpu) < 1 ? 'draw' : over.player > over.cpu ? 'player' : 'cpu') : over.player >= 0 ? 'player' : 'cpu';
|
const winner = over.player >= 0 && over.cpu >= 0 ? (Math.abs(over.player - over.cpu) < 1 ? 'draw' : over.player > over.cpu ? 'player' : 'cpu') : over.player >= 0 ? 'player' : 'cpu';
|
||||||
c.step = 'result'; c.result = { winner, flicks: { ...c.flicks } };
|
c.step = 'result'; c.result = { winner, flicks: { ...c.flicks }, over };
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Chi ha già tagliato il traguardo aspetta; spinge l'altro.
|
// Chi ha già tagliato il traguardo aspetta; spinge l'altro.
|
||||||
@@ -389,12 +389,26 @@ export function button(c) {
|
|||||||
if (c.step === 'rolling' || c.step === 'settle') return 'In movimento…';
|
if (c.step === 'rolling' || c.step === 'settle') return 'In movimento…';
|
||||||
return c.current === 'cpu' ? 'Spinge il Chiaru…' : 'Spingi';
|
return c.current === 'cpu' ? 'Spinge il Chiaru…' : 'Spingi';
|
||||||
}
|
}
|
||||||
|
// Il perché del risultato, da scrivere (Andrea, 29 ottobre: «ho tagliato prima io, ma ha vinto il Chiaru»). Le
|
||||||
|
// macchinine vere sono lunghe 4 centimetri e mezzo: un pixel è quasi due millimetri.
|
||||||
|
export const CM = 4.5 / CAR.len;
|
||||||
|
export function why(c) {
|
||||||
|
const { winner, flicks, over } = c.result, n = flicks.player;
|
||||||
|
if (over.player >= 0 && over.cpu >= 0) {
|
||||||
|
const cm = Math.max(1, Math.round(Math.abs(over.player - over.cpu) * CM));
|
||||||
|
if (winner === 'draw') return [`Tutti e due al traguardo alla spinta ${n},`, 'pari al millimetro.'];
|
||||||
|
return [`Tutti e due al traguardo alla spinta ${n}:`, `vince chi va più in là, ${winner === 'player' ? 'Andrea' : 'il Chiaru'} di ${cm} ${cm === 1 ? 'centimetro' : 'centimetri'}.`];
|
||||||
|
}
|
||||||
|
return winner === 'player' ? [`Andrea al traguardo alla spinta ${n};`, 'al Chiaru non è bastata la sua.'] : [`Il Chiaru al traguardo alla spinta ${n};`, 'Andrea non c\'è arrivato.'];
|
||||||
|
}
|
||||||
|
// Chi è davanti adesso (1 o 2), per strada fatta.
|
||||||
|
export const rank = (c, who) => c.cars[who].progress >= c.cars[OTHER[who]].progress ? 1 : 2;
|
||||||
// Quanta pista manca al traguardo, in percentuale, per il tabellone.
|
// Quanta pista manca al traguardo, in percentuale, per il tabellone.
|
||||||
export const done = (c, who) => clamp(c.cars[who].progress / (c.laps * c.track.length), 0, 1);
|
export const done = (c, who) => clamp(c.cars[who].progress / (c.laps * c.track.length), 0, 1);
|
||||||
export function status(c) {
|
export function status(c) {
|
||||||
if (c.step === 'choose') return `Scegli la macchinina: ${CARS[c.choice].name}. Tocca quella che vuoi, o frecce e Scegli.`;
|
if (c.step === 'choose') return `Scegli la macchinina: ${CARS[c.choice].name}. Tocca quella che vuoi, o frecce e Scegli.`;
|
||||||
const where = `Andrea ${Math.round(done(c, 'player') * 100)}%, Chiaru ${Math.round(done(c, 'cpu') * 100)}% del giro.`;
|
const where = `Andrea ${rank(c, 'player')}° (${Math.round(done(c, 'player') * 100)}%), Chiaru ${rank(c, 'cpu')}° (${Math.round(done(c, 'cpu') * 100)}%).`;
|
||||||
if (c.step === 'result') return `${where} ${c.result.winner === 'player' ? 'Hai battuto il Chiaru!' : c.result.winner === 'cpu' ? 'Ha vinto il Chiaru.' : 'Arrivati pari.'}`;
|
if (c.step === 'result') return `${c.result.winner === 'player' ? 'Hai battuto il Chiaru!' : c.result.winner === 'cpu' ? 'Ha vinto il Chiaru.' : 'Arrivati pari.'} ${why(c).join(' ')}`;
|
||||||
if (c.step === 'rolling' || c.step === 'settle') return `${where} ${c.line ?? ''}`.trim();
|
if (c.step === 'rolling' || c.step === 'settle') return `${where} ${c.line ?? ''}`.trim();
|
||||||
if (c.current === 'cpu') return `${where} Spinge il Chiaru…`;
|
if (c.current === 'cpu') return `${where} Spinge il Chiaru…`;
|
||||||
return `${where} ${c.line ? c.line + ' ' : ''}Tocca a te: mira e spingi.`;
|
return `${where} ${c.line ? c.line + ' ' : ''}Tocca a te: mira e spingi.`;
|
||||||
|
|||||||
+24
-7
@@ -606,7 +606,7 @@ const MINI = {
|
|||||||
micro: { ...micro, back: 'Torna dal Chiaru', label: 'La pista col gesso: muovi il mouse o tocca il parcheggio per mirare, tieni premuto per caricare la spinta e lascia; oppure regola la forza e premi Spingi. Frecce o pulsanti Mira per girare.' },
|
micro: { ...micro, back: 'Torna dal Chiaru', label: 'La pista col gesso: muovi il mouse o tocca il parcheggio per mirare, tieni premuto per caricare la spinta e lascia; oppure regola la forza e premi Spingi. Frecce o pulsanti Mira per girare.' },
|
||||||
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 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.' },
|
gara: { ...gara, toggle: true, back: 'Torna dal Trappola', label: 'GP di Fortezza. Alla bona e Seria: tieni premuto sulla scena (o Spazio) per il gas e lascia per frenare, lo sterzo va da solo; in curva troppo forte si esce. Ginocchio in terra: 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;
|
||||||
@@ -634,7 +634,17 @@ for (const ev of ['pointerup', 'pointercancel', 'lostpointercapture']) $('gara-p
|
|||||||
padPointer = null; $('gara-pad').classList.remove('active');
|
padPointer = null; $('gara-pad').classList.remove('active');
|
||||||
});
|
});
|
||||||
$('gara-pad').addEventListener('contextmenu', e => e.preventDefault());
|
$('gara-pad').addEventListener('contextmenu', e => e.preventDefault());
|
||||||
$('bocce-throw').addEventListener('click', () => { const c = miniState(); if (c && !game.speech) mini().shoot?.(c); });
|
// GP, guida semplice: il pulsante Gas si tiene premuto (giù gas, su freno) invece di accendersi e spegnersi al clic;
|
||||||
|
// sul telefono, alla bona, il clic arrivava solo alzando il dito (Andrea, 29 ottobre).
|
||||||
|
const holdGas = c => !!c && story.closeups[c.id].type === 'gara' && gara.simple(c) && c.step !== 'intro' && c.step !== 'result';
|
||||||
|
$('bocce-throw').addEventListener('click', () => { const c = miniState(); if (c && !game.speech && !holdGas(c)) mini().shoot?.(c); });
|
||||||
|
$('bocce-throw').addEventListener('pointerdown', e => {
|
||||||
|
const c = miniState(); if (!holdGas(c) || game.speech) return;
|
||||||
|
e.preventDefault();
|
||||||
|
if (gara.press(c, 'gas-button')) try { $('bocce-throw').setPointerCapture(e.pointerId); } catch { /* un tocco sintetico: va bene lo stesso */ }
|
||||||
|
});
|
||||||
|
for (const ev of ['pointerup', 'pointercancel', 'lostpointercapture']) $('bocce-throw').addEventListener(ev, () => { const c = miniState(); if (holdGas(c)) gara.release(c, 'gas-button'); });
|
||||||
|
$('bocce-throw').addEventListener('contextmenu', e => { if (holdGas(miniState())) e.preventDefault(); });
|
||||||
// Le carte da gioco: dove si tocca la carta sul tavolo, gioco per gioco.
|
// Le carte da gioco: dove si tocca la carta sul tavolo, gioco per gioco.
|
||||||
const CARD_AT = { briscola: cardAt, tressette: tressetteAt, scopa: scopaAt };
|
const CARD_AT = { briscola: cardAt, tressette: tressetteAt, scopa: scopaAt };
|
||||||
for (let i = 0; i < 3; i++) {
|
for (let i = 0; i < 3; i++) {
|
||||||
@@ -935,7 +945,10 @@ function updateUi() {
|
|||||||
updateSpeech();
|
updateSpeech();
|
||||||
const mg = miniState() && mini(), def = mg && story.closeups[c.id];
|
const mg = miniState() && mini(), def = mg && story.closeups[c.id];
|
||||||
const cards = ['briscola', 'tressette', 'scopa'].includes(def?.type);
|
const cards = ['briscola', 'tressette', 'scopa'].includes(def?.type);
|
||||||
setProp($('bocce-controls'), 'hidden', !mg || cards);
|
// La presentazione dei piloti del GP a tutto schermo: i comandi arrivano col semaforo (Andrea, 29 ottobre).
|
||||||
|
const presenta = !!mg && story.closeups[c.id].type === 'gara' && c.step === 'intro';
|
||||||
|
$('app').classList.toggle('presenta', presenta);
|
||||||
|
setProp($('bocce-controls'), 'hidden', !mg || cards || presenta);
|
||||||
setProp($('bocce-controls'), 'inert', !running);
|
setProp($('bocce-controls'), 'inert', !running);
|
||||||
setProp($('briscola-controls'), 'hidden', def?.type !== 'briscola');
|
setProp($('briscola-controls'), 'hidden', def?.type !== 'briscola');
|
||||||
setProp($('briscola-controls'), 'inert', !running);
|
setProp($('briscola-controls'), 'inert', !running);
|
||||||
@@ -1011,19 +1024,23 @@ function updateUi() {
|
|||||||
// Il GP di Fortezza: niente forza, e i due pulsanti di mira diventano lo sterzo (si tengono premuti).
|
// Il GP di Fortezza: niente forza, e i due pulsanti di mira diventano lo sterzo (si tengono premuti).
|
||||||
// Nel GP di Fortezza il cursore della forza diventa la sensibilità dello sterzo col dito (ricordata).
|
// Nel GP di Fortezza il cursore della forza diventa la sensibilità dello sterzo col dito (ricordata).
|
||||||
const powerLabel = $('bocce-power').closest('label');
|
const powerLabel = $('bocce-power').closest('label');
|
||||||
// Ai rinquarti la forza è sempre piena: il cursore non serve.
|
// Ai rinquarti la forza è sempre piena: il cursore non serve. Nella guida semplice del GP (alla bona e seria) si
|
||||||
setProp(powerLabel, 'hidden', def.type === 'rinquarto');
|
// usa solo il gas: niente sensibilità, sterzo, freno né riquadro dello swipe.
|
||||||
|
const easy = def.type === 'gara' && gara.simple(c);
|
||||||
|
setProp(powerLabel, 'hidden', def.type === 'rinquarto' || easy);
|
||||||
|
for (const id of ['bocce-left', 'bocce-right']) setProp($(id), 'hidden', easy);
|
||||||
const powerName = def.type === 'gara' ? 'Sensibilità ' : 'Forza ';
|
const powerName = def.type === 'gara' ? 'Sensibilità ' : 'Forza ';
|
||||||
if (powerLabel.firstChild.nodeValue !== powerName) powerLabel.firstChild.nodeValue = powerName;
|
if (powerLabel.firstChild.nodeValue !== powerName) powerLabel.firstChild.nodeValue = powerName;
|
||||||
setAttr($('bocce-power'), 'aria-label', def.type === 'gara' ? 'Sensibilità dello sterzo col dito' : 'Forza del lancio');
|
setAttr($('bocce-power'), 'aria-label', def.type === 'gara' ? 'Sensibilità dello sterzo col dito' : 'Forza del lancio');
|
||||||
setProp($('gara-pad'), 'hidden', def.type !== 'gara');
|
setProp($('gara-pad'), 'hidden', def.type !== 'gara' || easy);
|
||||||
if (def.type === 'gara' && c.sensLoaded === undefined) {
|
if (def.type === 'gara' && c.sensLoaded === undefined) {
|
||||||
c.sensLoaded = true;
|
c.sensLoaded = true;
|
||||||
try { const v = localStorage.getItem(GARA_SENS); if (v !== null) gara.setPower(c, Number(v) / 100); } catch { /* si resta al valore di partenza */ }
|
try { const v = localStorage.getItem(GARA_SENS); if (v !== null) gara.setPower(c, Number(v) / 100); } catch { /* si resta al valore di partenza */ }
|
||||||
}
|
}
|
||||||
setText($('bocce-left'), def.type === 'gara' ? '← Sterza' : '← Mira');
|
setText($('bocce-left'), def.type === 'gara' ? '← Sterza' : '← Mira');
|
||||||
setText($('bocce-right'), def.type === 'gara' ? 'Sterza →' : 'Mira →');
|
setText($('bocce-right'), def.type === 'gara' ? 'Sterza →' : 'Mira →');
|
||||||
setProp($('gara-brake'), 'hidden', def.type !== 'gara');
|
setProp($('gara-brake'), 'hidden', def.type !== 'gara' || easy);
|
||||||
|
$('bocce-throw').classList.toggle('hold', easy);
|
||||||
setProp($('gara-brake'), 'disabled', !(mg.canAdjust?.(c) ?? false) || !!game.speech);
|
setProp($('gara-brake'), 'disabled', !(mg.canAdjust?.(c) ?? false) || !!game.speech);
|
||||||
setText($('bocce-throw'), mg.button?.(c) ?? (c.step === 'result' ? 'Finita' : c.step === 'rolling' ? 'In movimento…' : c.current === 'cpu' ? `Tira ${def.names.cpu}…` : def.type === 'bocce' ? 'Lancia' : 'Tira'));
|
setText($('bocce-throw'), mg.button?.(c) ?? (c.step === 'result' ? 'Finita' : c.step === 'rolling' ? 'In movimento…' : c.current === 'cpu' ? `Tira ${def.names.cpu}…` : def.type === 'bocce' ? 'Lancia' : 'Tira'));
|
||||||
setProp($('bocce-help'), 'hidden', def.type !== 'bocce');
|
setProp($('bocce-help'), 'hidden', def.type !== 'bocce');
|
||||||
|
|||||||
+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.40';
|
export const VERSION = '0.1.43';
|
||||||
|
|
||||||
// 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).
|
||||||
|
|||||||
@@ -56,7 +56,11 @@ body { display: flex; justify-content: center; overflow-x: hidden; -webkit-tap-h
|
|||||||
text-align: center; color: #c5c6b6; border: 2px dashed #897b57; border-radius: 10px; background: #1d2320; }
|
text-align: center; color: #c5c6b6; border: 2px dashed #897b57; border-radius: 10px; background: #1d2320; }
|
||||||
#gara-pad.active { border-style: solid; color: #f3dcac; }
|
#gara-pad.active { border-style: solid; color: #f3dcac; }
|
||||||
#stage, #bocce-controls, #gara-pad { -webkit-touch-callout: none; -webkit-user-select: none; user-select: none; }
|
#stage, #bocce-controls, #gara-pad { -webkit-touch-callout: none; -webkit-user-select: none; user-select: none; }
|
||||||
#bocce-throw { background: #ead09a; color: #202b25; min-width: 100px; }
|
#bocce-throw { background: #ead09a; color: #202b25; min-width: 100px; touch-action: none; }
|
||||||
|
/* La presentazione dei piloti del GP: la scena da sola, i comandi arrivano col semaforo. */
|
||||||
|
#app.presenta #side { display: none; }
|
||||||
|
/* La guida semplice del GP: il Gas si tiene premuto col pollice, più alto. */
|
||||||
|
#bocce-throw.hold { min-height: 72px; font-size: 14px; }
|
||||||
.bocce-adjust button:disabled { opacity: .4; cursor: default; }
|
.bocce-adjust button:disabled { opacity: .4; cursor: default; }
|
||||||
#bocce-help, #biliardo-help, #basket-help, #pesca-help, #gara-help, #rinquarto-help { margin-top: 9px; color: #c5c6b6; font-size: 10px; }
|
#bocce-help, #biliardo-help, #basket-help, #pesca-help, #gara-help, #rinquarto-help { margin-top: 9px; color: #c5c6b6; font-size: 10px; }
|
||||||
#bocce-help summary, #biliardo-help summary, #basket-help summary, #pesca-help summary, #gara-help summary, #rinquarto-help summary { cursor: pointer; padding: 6px 0; }
|
#bocce-help summary, #biliardo-help summary, #basket-help summary, #pesca-help summary, #gara-help summary, #rinquarto-help summary { cursor: pointer; padding: 6px 0; }
|
||||||
@@ -318,6 +322,8 @@ body.full #sound { position: fixed; z-index: 13; top: max(8px, env(safe-area-ins
|
|||||||
#app { width: 100vw; flex-direction: row; }
|
#app { width: 100vw; flex-direction: row; }
|
||||||
#stage { flex: none; width: min(calc(100dvh * 16 / 9), 68vw); align-self: flex-start; }
|
#stage { flex: none; width: min(calc(100dvh * 16 / 9), 68vw); align-self: flex-start; }
|
||||||
#stage.biliardo { width: min(calc((100dvh - 90px) * 16 / 9), 68vw); }
|
#stage.biliardo { width: min(calc((100dvh - 90px) * 16 / 9), 68vw); }
|
||||||
|
#app.presenta { justify-content: center; }
|
||||||
|
#app.presenta #stage.biliardo { width: min(calc((100dvh - 44px) * 16 / 9), 100vw); }
|
||||||
#stage.briscola { width: min(calc((100dvh - 90px) * 16 / 9), 68vw); }
|
#stage.briscola { width: min(calc((100dvh - 90px) * 16 / 9), 68vw); }
|
||||||
#briscola-controls, #tressette-controls, #scopa-controls { overflow-y: auto; min-height: 0; padding: 8px; font-size: 9px; }
|
#briscola-controls, #tressette-controls, #scopa-controls { overflow-y: auto; min-height: 0; padding: 8px; font-size: 9px; }
|
||||||
/* Nella colonna stretta accanto al tavolo: carte e segni su due colonne, parole che vanno a capo. */
|
/* Nella colonna stretta accanto al tavolo: carte e segni su due colonne, parole che vanno a capo. */
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
// Il GP di Fortezza nel browser vero: alla bona e seria la guida semplice (solo gas, lasciando si frena), a ginocchio
|
||||||
|
// in terra lo sterzo vero. npm run browser:gara — schermate in /tmp/regolo-gara.
|
||||||
|
import { chromium } from 'playwright';
|
||||||
|
import { mkdirSync } from 'node:fs';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { staticServer } from './helpers.mjs';
|
||||||
|
|
||||||
|
const out = process.env.OUT ?? '/tmp/regolo-gara';
|
||||||
|
mkdirSync(out, { recursive: true });
|
||||||
|
const server = await staticServer();
|
||||||
|
let browser;
|
||||||
|
const errors = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
browser = await chromium.launch({ args: ['--autoplay-policy=no-user-gesture-required', '--use-gl=angle', '--use-angle=swiftshader', '--enable-unsafe-swiftshader'] });
|
||||||
|
for (const [name, mobile, viewport, level] of [
|
||||||
|
['touch', true, { width: 844, height: 390 }, 'seria'],
|
||||||
|
['portrait', true, { width: 390, height: 844 }, 'bona'],
|
||||||
|
['desktop', false, { width: 1280, height: 860 }, 'male'],
|
||||||
|
]) {
|
||||||
|
const page = await browser.newPage({ viewport, hasTouch: mobile, isMobile: mobile });
|
||||||
|
page.on('pageerror', e => errors.push(e.message));
|
||||||
|
await page.goto(`http://127.0.0.1:${server.port}/`);
|
||||||
|
await page.click('#splash');
|
||||||
|
await page.click('#new-game');
|
||||||
|
await page.click('#rione [data-rione="campomigliaio"]');
|
||||||
|
await page.click('#skip-intro');
|
||||||
|
await page.evaluate(async level => {
|
||||||
|
const { settle } = await import('/tests/helpers.mjs');
|
||||||
|
settle(game); game.run({ closeup: 'gara', level }); settle(game);
|
||||||
|
}, level);
|
||||||
|
// La presentazione dei piloti a tutto schermo: niente comandi finché non c'è il semaforo.
|
||||||
|
await page.waitForFunction(() => game.closeup?.step === 'intro');
|
||||||
|
await page.waitForTimeout(300);
|
||||||
|
assert.equal(await page.locator('#bocce-controls').isVisible(), false, `${name}: comandi nascosti nella presentazione`);
|
||||||
|
assert.equal(await page.locator('#side').isVisible(), false, `${name}: la scena da sola`);
|
||||||
|
await page.screenshot({ path: `${out}/${name}-presentazione.png` });
|
||||||
|
// Salta la presentazione dei piloti e aspetta il via.
|
||||||
|
await page.evaluate(async () => { const g = await import('/src/engine/gara.js'); while (game.closeup.step === 'intro') g.press(game.closeup); });
|
||||||
|
await page.waitForFunction(() => game.closeup.step === 'race', null, { timeout: 15000 });
|
||||||
|
await page.waitForSelector('#bocce-controls:not([hidden])');
|
||||||
|
const easy = level !== 'male';
|
||||||
|
for (const id of ['#bocce-left', '#bocce-right', '#gara-brake', '#gara-pad'])
|
||||||
|
assert.equal(await page.locator(id).isVisible(), !easy, `${name}: ${id} ${easy ? 'nascosto' : 'visibile'}`);
|
||||||
|
assert.equal(await page.locator('#bocce-power').isVisible(), !easy, 'la sensibilità solo con lo sterzo vero');
|
||||||
|
// Nella guida semplice il pulsante Gas si tiene premuto (giù gas, su freno) e la scritta non cambia; con lo
|
||||||
|
// sterzo vero un tocco accende il gas e un altro lo spegne.
|
||||||
|
const gas = page.locator('#bocce-throw');
|
||||||
|
if (easy) await gas.dispatchEvent('pointerdown', { pointerId: 5, pointerType: mobile ? 'touch' : 'mouse', isPrimary: true });
|
||||||
|
else await gas.click();
|
||||||
|
await page.waitForFunction(() => game.closeup.riders.find(r => r.id === 'andrea').v > 40, null, { timeout: 15000 });
|
||||||
|
assert.equal(await gas.textContent(), easy ? 'Gas' : 'Molla');
|
||||||
|
await page.screenshot({ path: `${out}/${name}-${level}.png` });
|
||||||
|
if (easy) await gas.dispatchEvent('pointerup', { pointerId: 5, pointerType: mobile ? 'touch' : 'mouse', isPrimary: true });
|
||||||
|
else await gas.click();
|
||||||
|
const v = await page.evaluate(() => game.closeup.riders.find(r => r.id === 'andrea').v);
|
||||||
|
await page.waitForTimeout(300);
|
||||||
|
const after = await page.evaluate(() => game.closeup.riders.find(r => r.id === 'andrea').v);
|
||||||
|
assert.ok(after < v, `${name}: senza gas rallenta (${v.toFixed(0)} → ${after.toFixed(0)})`);
|
||||||
|
await page.close();
|
||||||
|
}
|
||||||
|
assert.deepEqual(errors, []);
|
||||||
|
console.log(`ok: schermate in ${out}`);
|
||||||
|
} finally {
|
||||||
|
await browser?.close();
|
||||||
|
server.kill();
|
||||||
|
}
|
||||||
+67
-6
@@ -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 { SWIPE, setPower, 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 { SIMPLE, simple, simpleLimit, mustLift, PREAVVISO, SWIPE, setPower, 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';
|
||||||
@@ -58,7 +58,7 @@ test('i piloti sono dati: motore da 1 a 6 e testa da 1 a 5 danno velocità, acce
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('il semaforo, poi il gas: tenendo premuto si va, lasciando si rallenta, il freno vince sul gas', () => {
|
test('il semaforo, poi il gas: tenendo premuto si va, lasciando si rallenta, il freno vince sul gas', () => {
|
||||||
const c = newGara(3, { level: 'seria', riders: RIDERS });
|
const c = newGara(3, { level: 'seria', riders: RIDERS, simple: false });
|
||||||
for (const r of RIDERS) assert.ok(press(c, 7), r.id);
|
for (const r of RIDERS) assert.ok(press(c, 7), r.id);
|
||||||
assert.equal(c.step, 'start', 'un tocco per pilota e la presentazione è finita');
|
assert.equal(c.step, 'start', 'un tocco per pilota e la presentazione è finita');
|
||||||
assert.equal(c.holding, false, 'senza dare gas');
|
assert.equal(c.holding, false, 'senza dare gas');
|
||||||
@@ -161,7 +161,7 @@ test('l’origine segue gli swipe lunghi e si può correggere; rilascio e annull
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('alla bona si completano tre giri usando solo un dito, senza tasti o pulsanti', () => {
|
test('alla bona si completano tre giri usando solo un dito, senza tasti o pulsanti', () => {
|
||||||
const c = race(newGara(5, { level: 'bona', riders: piloti.filter(p => p.consenso !== null) }));
|
const c = race(newGara(5, { level: 'bona', riders: piloti.filter(p => p.consenso !== null), simple: false }));
|
||||||
aimScreen(c, [320, 180], 'bot'); press(c, 'bot');
|
aimScreen(c, [320, 180], 'bot'); press(c, 'bot');
|
||||||
for (let t = 0; t < 180 && c.step === 'race'; t += 1 / 60) {
|
for (let t = 0; t < 180 && c.step === 'race'; t += 1 / 60) {
|
||||||
swipeTo(c, pointAt(player(c).s + 50));
|
swipeTo(c, pointAt(player(c).s + 50));
|
||||||
@@ -198,7 +198,7 @@ test('la presentazione: uno alla volta arrivano in griglia col motorino, poi il
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('sempre a gas e dritto si finisce nella paglia; con giudizio alla bona si vince', () => {
|
test('sempre a gas e dritto si finisce nella paglia; con giudizio alla bona si vince', () => {
|
||||||
const wild = newGara(5, { level: 'bona', riders: piloti.filter(p => p.consenso !== null) });
|
const wild = newGara(5, { level: 'bona', riders: piloti.filter(p => p.consenso !== null), simple: false });
|
||||||
skip(wild);
|
skip(wild);
|
||||||
run(wild, COUNTDOWN + 60, () => true);
|
run(wild, COUNTDOWN + 60, () => true);
|
||||||
assert.ok(wild.heard.has('gara-fuori:andrea'));
|
assert.ok(wild.heard.has('gara-fuori:andrea'));
|
||||||
@@ -206,7 +206,7 @@ test('sempre a gas e dritto si finisce nella paglia; con giudizio alla bona si v
|
|||||||
assert.equal(wild.step, 'race', 'dritto non si arriva');
|
assert.equal(wild.step, 'race', 'dritto non si arriva');
|
||||||
const heard = new Set();
|
const heard = new Set();
|
||||||
for (const seed of [1, 2, 3, 4]) {
|
for (const seed of [1, 2, 3, 4]) {
|
||||||
const c = newGara(seed, { level: 'bona', riders: piloti.filter(p => p.consenso !== null) });
|
const c = newGara(seed, { level: 'bona', riders: piloti.filter(p => p.consenso !== null), simple: false });
|
||||||
skip(c); setPower(c, 1); // a sensibilità piena: il dito risponde subito
|
skip(c); setPower(c, 1); // a sensibilità piena: il dito risponde subito
|
||||||
for (const k of run(c, COUNTDOWN + 120, careful)) heard.add(k);
|
for (const k of run(c, COUNTDOWN + 120, careful)) heard.add(k);
|
||||||
assert.equal(c.step, 'result'); assert.equal(c.result.winner, 'player', `seme ${seed}`);
|
assert.equal(c.step, 'result'); assert.equal(c.result.winner, 'player', `seme ${seed}`);
|
||||||
@@ -219,7 +219,7 @@ test('sempre a gas e dritto si finisce nella paglia; con giudizio alla bona si v
|
|||||||
|
|
||||||
test('alla sensibilità di partenza (bassa) lo stesso pilota non finisce mai nella paglia', () => {
|
test('alla sensibilità di partenza (bassa) lo stesso pilota non finisce mai nella paglia', () => {
|
||||||
for (const seed of [1, 2, 3]) {
|
for (const seed of [1, 2, 3]) {
|
||||||
const c = newGara(seed, { level: 'bona', riders: piloti.filter(p => p.consenso !== null) });
|
const c = newGara(seed, { level: 'bona', riders: piloti.filter(p => p.consenso !== null), simple: false });
|
||||||
skip(c); assert.equal(c.sens, SWIPE.sens);
|
skip(c); assert.equal(c.sens, SWIPE.sens);
|
||||||
run(c, COUNTDOWN + 120, careful);
|
run(c, COUNTDOWN + 120, careful);
|
||||||
assert.equal(c.step, 'result'); assert.equal(c.result.offs, 0, `seme ${seed}`);
|
assert.equal(c.step, 'result'); assert.equal(c.result.offs, 0, `seme ${seed}`);
|
||||||
@@ -270,3 +270,64 @@ test('nella storia: dal Trappola l\'Arcu scherza sul Betino, poi la sfida; prima
|
|||||||
// Il Betino, lassù: solo Andrea ne parla.
|
// Il Betino, lassù: solo Andrea ne parla.
|
||||||
g.act('use', 'betino'); assert.ok(settle(g).every(s => s.who === 'andrea'));
|
g.act('use', 'betino'); assert.ok(settle(g).every(s => s.who === 'andrea'));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// La guida semplice (Andrea, 29 ottobre: dal telefono lo sterzo era ingiocabile): alla bona e seria si tiene premuto
|
||||||
|
// per il gas e lasciando si frena, lo sterzo va da solo; troppo forte in curva si scivola fuori. Ginocchio in terra no.
|
||||||
|
test('guida semplice: alla bona e seria sì, a ginocchio in terra no; lasciando si frena e lo sterzo va da solo', () => {
|
||||||
|
const riders = piloti.filter(p => p.consenso !== null);
|
||||||
|
assert.equal(simple(newGara(1, { level: 'bona', riders })), true);
|
||||||
|
assert.equal(simple(newGara(1, { level: 'seria', riders })), true);
|
||||||
|
assert.equal(simple(newGara(1, { level: 'male', riders })), false);
|
||||||
|
const c = race(newGara(1, { level: 'bona', riders }));
|
||||||
|
run(c, 2, () => true);
|
||||||
|
const v = player(c).v; assert.ok(v > 40);
|
||||||
|
run(c, 0.3, () => false);
|
||||||
|
assert.ok(player(c).v < v - BRAKE * 0.3 + 5, 'lasciando frena, non solo rallenta');
|
||||||
|
assert.match(status(c), /Freno\./);
|
||||||
|
// Le frecce non sterzano: lo sterzo segue la strada.
|
||||||
|
key(c, 'ArrowLeft', true); run(c, 2, () => true); key(c, 'ArrowLeft', false);
|
||||||
|
assert.ok(Math.abs(player(c).lat) < 9, 'resta in strada');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('guida semplice: sempre a tutto gas si finisce nella paglia; mollando prima delle curve no, e alla bona si vince', () => {
|
||||||
|
const riders = piloti.filter(p => p.consenso !== null);
|
||||||
|
// Molla quando, frenando da qui, arriverebbe a una curva sopra il limite della guida semplice.
|
||||||
|
const wise = c => { const r = player(c), h = r.v * r.v / (2 * BRAKE) + 10; for (let d = 0; d <= h; d += 4) { const a = simpleLimit(r, r.s + d); if (r.v * r.v > a * a + 2 * BRAKE * d) return false; } return true; };
|
||||||
|
for (const seed of [1, 2]) {
|
||||||
|
const wild = race(newGara(seed, { level: 'bona', riders }));
|
||||||
|
run(wild, 120, () => true);
|
||||||
|
assert.ok(wild.result.offs > 0, 'a tutto gas si esce');
|
||||||
|
const c = race(newGara(seed, { level: 'bona', riders }));
|
||||||
|
run(c, 120, wise);
|
||||||
|
assert.equal(c.result.offs, 0); assert.equal(c.result.falls, 0, 'le buche si scansano');
|
||||||
|
assert.equal(c.result.winner, 'player', `seme ${seed}`);
|
||||||
|
}
|
||||||
|
assert.ok(SIMPLE.slide > 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Seria semplice: chi non rallenta in curva va fuori e perde, chi frena giusto fa le curve pulite e vince; LASCIA! dice quando', () => {
|
||||||
|
const riders = piloti.filter(p => p.consenso !== null);
|
||||||
|
const wise = c => { const r = player(c), h = r.v * r.v / (2 * BRAKE) + 10; for (let d = 0; d <= h; d += 4) { const a = simpleLimit(r, r.s + d, c.tune); if (r.v * r.v > a * a + 2 * BRAKE * d) return false; } return true; };
|
||||||
|
for (const seed of [1, 2]) {
|
||||||
|
const wild = race(newGara(seed, { level: 'seria', riders }));
|
||||||
|
run(wild, 120, () => true);
|
||||||
|
const good = race(newGara(seed, { level: 'seria', riders }));
|
||||||
|
run(good, 120, wise);
|
||||||
|
assert.ok(wild.result.offs >= 4, `a tutto gas fuori in quasi ogni curva (${wild.result.offs})`);
|
||||||
|
assert.ok(wild.result.place >= 3);
|
||||||
|
assert.equal(good.result.offs, 0); assert.equal(good.result.winner, 'player', `seme ${seed}`);
|
||||||
|
assert.ok(player(good).cleans >= 5, 'le curve pulite');
|
||||||
|
assert.ok(wild.result.times.andrea - good.result.times.andrea > 6, 'tutto gas costa più di 6 secondi');
|
||||||
|
}
|
||||||
|
// Chi segue LASCIA! guida come chi frena giusto; CURVA! arriva prima.
|
||||||
|
const c = race(newGara(1, { level: 'seria', riders }));
|
||||||
|
let early = 0, said = false;
|
||||||
|
for (let t = 0; t < 20; t += 1 / 60) {
|
||||||
|
c.holding = !mustLift(c);
|
||||||
|
if (mustLift(c, PREAVVISO) && !mustLift(c)) early++;
|
||||||
|
update(c, 1 / 60);
|
||||||
|
if (c.holding && mustLift(c)) said ||= /Lascia!/.test(status(c));
|
||||||
|
}
|
||||||
|
assert.ok(early > 20, `il preavviso CURVA! viene prima di LASCIA! (${early} fotogrammi)`);
|
||||||
|
assert.ok(said, 'il pannello dice Lascia!');
|
||||||
|
});
|
||||||
|
|||||||
+16
-2
@@ -96,6 +96,19 @@ test('si parte scegliendo la macchinina: tocco, frecce, pulsante; il Chiaru tien
|
|||||||
for (const car of m.CARS) assert.ok(existsSync(new URL('../' + m.carImage(car.id, true), import.meta.url)) && existsSync(new URL('../' + m.carImage(car.id), import.meta.url)));
|
for (const car of m.CARS) assert.ok(existsSync(new URL('../' + m.carImage(car.id, true), import.meta.url)) && existsSync(new URL('../' + m.carImage(car.id), import.meta.url)));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('primo e secondo mentre si va; nel risultato il perché', () => {
|
||||||
|
const c = newMicro(1);
|
||||||
|
c.cars.player.progress = 100; c.cars.cpu.progress = 80;
|
||||||
|
assert.equal(m.rank(c, 'player'), 1); assert.equal(m.rank(c, 'cpu'), 2);
|
||||||
|
assert.match(m.status(c), /Andrea 1°.*Chiaru 2°/);
|
||||||
|
c.result = { winner: 'cpu', flicks: { player: 20, cpu: 20 }, over: { player: 5, cpu: 22 } };
|
||||||
|
assert.deepEqual(m.why(c), ['Tutti e due al traguardo alla spinta 20:', 'vince chi va più in là, il Chiaru di 3 centimetri.']);
|
||||||
|
c.result = { winner: 'player', flicks: { player: 9, cpu: 9 }, over: { player: 5, cpu: -40 } };
|
||||||
|
assert.deepEqual(m.why(c), ['Andrea al traguardo alla spinta 9;', 'al Chiaru non è bastata la sua.']);
|
||||||
|
c.result = { winner: 'draw', flicks: { player: 9, cpu: 9 }, over: { player: 5, cpu: 5.4 } };
|
||||||
|
assert.match(m.why(c).join(' '), /pari/);
|
||||||
|
});
|
||||||
|
|
||||||
test('le penalità alla francese: tagliè, cappottè, accompagnè; per tutte si torna indietro', () => {
|
test('le penalità alla francese: tagliè, cappottè, accompagnè; per tutte si torna indietro', () => {
|
||||||
// Tagliè: dal tratto di mezzo della esse, dritti in su attraverso l'erba verso il tratto di sopra.
|
// Tagliè: dal tratto di mezzo della esse, dritti in su attraverso l'erba verso il tratto di sopra.
|
||||||
const t = newMicro(1, { level: 'bona' }), [sx, sy] = t.track.pts[Math.round(t.track.n * 0.48)];
|
const t = newMicro(1, { level: 'bona' }), [sx, sy] = t.track.pts[Math.round(t.track.n * 0.48)];
|
||||||
@@ -184,7 +197,8 @@ test('il disegno: gesso, macchinine, guida e risultato; lo stato non cambia', ()
|
|||||||
assert.ok(calls.some(([op, img]) => op === 'drawImage' && img === bg));
|
assert.ok(calls.some(([op, img]) => op === 'drawImage' && img === bg));
|
||||||
assert.ok(calls.filter(([op, img]) => op === 'drawImage' && img?.car === 'player').length >= 2, 'la macchinina e il suo fantasma');
|
assert.ok(calls.filter(([op, img]) => op === 'drawImage' && img?.car === 'player').length >= 2, 'la macchinina e il suo fantasma');
|
||||||
assert.ok(calls.some(([op, t]) => op === 'fillText' && t === 'Chiaru'));
|
assert.ok(calls.some(([op, t]) => op === 'fillText' && t === 'Chiaru'));
|
||||||
c.step = 'result'; c.result = { winner: 'cpu', flicks: { player: 12, cpu: 12 } }; calls.length = 0; draw();
|
c.step = 'result'; c.result = { winner: 'cpu', flicks: { player: 12, cpu: 12 }, over: { player: 3, cpu: 20 } }; calls.length = 0; draw();
|
||||||
|
assert.ok(calls.some(([op, t]) => op === 'fillText' && t === 'vince chi va più in là, il Chiaru di 3 centimetri.'));
|
||||||
assert.ok(calls.some(([op, t]) => op === 'fillText' && t === 'HA VINTO IL CHIARU'));
|
assert.ok(calls.some(([op, t]) => op === 'fillText' && t === 'HA VINTO IL CHIARU'));
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -201,7 +215,7 @@ test('il Chiaru al parcheggio di Cardetole: si chiacchiera, si sceglie il livell
|
|||||||
g.choose(g.options().findIndex(o => o.text === 'Si fa una gara alle macchinine?!?!')); settle(g);
|
g.choose(g.options().findIndex(o => o.text === 'Si fa una gara alle macchinine?!?!')); settle(g);
|
||||||
g.choose(g.options().findIndex(o => o.text === 'A fassi male.')); settle(g);
|
g.choose(g.options().findIndex(o => o.text === 'A fassi male.')); settle(g);
|
||||||
assert.equal(g.closeup?.id, 'micro'); assert.equal(g.closeup.level, 'male');
|
assert.equal(g.closeup?.id, 'micro'); assert.equal(g.closeup.level, 'male');
|
||||||
g.closeup.step = 'result'; g.closeup.result = { winner: 'player', flicks: { player: 11, cpu: 11 } };
|
g.closeup.step = 'result'; g.closeup.result = { winner: 'player', flicks: { player: 11, cpu: 11 }, over: { player: 4, cpu: -30 } };
|
||||||
g.closeupClose(); settle(g);
|
g.closeupClose(); settle(g);
|
||||||
assert.ok(g.state.flags.micro_vinto);
|
assert.ok(g.state.flags.micro_vinto);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user