Il GP di Fortezza: gara dei cinquantini attorno alle mura (pista di Codex dalla planimetria, sterzo, acceleratori, miscela, buche, presentazione dei piloti coi motorini dalle foto); Via Liguri Magelli col Trappola, la targa della Ghiacciomobile e il Betino in cima alla salita; l'Arcu lancia la sfida dal Trappola e ha la voce nuova (Andrea)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GiECdCPptNRwbVzjzHbohi
This commit is contained in:
@@ -0,0 +1,215 @@
|
||||
import { TRACK, CURVES, PICKUPS, BRAKE, LAPS, COUNTDOWN, ROAD, VERGE, CAN, pointAt, standings, player, lapOf, presenting, placeOf, INTRO } from './gara.js';
|
||||
import { text, panel } from './bocce-render.js';
|
||||
import { ambientHash as hash } from './ambient.js';
|
||||
|
||||
// Il GP di Fortezza visto dall'alto. Col quadro di Codex (def.picture) la strada e la Fortezza sono già dipinte; senza,
|
||||
// si disegnano qui dai dati del tracciato. Sopra, sempre: la guida, gli acceleratori, le buche, le lattine, i motorini.
|
||||
function stroke(ctx, from, to, width, color, lane = 0) {
|
||||
ctx.strokeStyle = color; ctx.lineWidth = width; ctx.lineCap = 'round';
|
||||
ctx.beginPath();
|
||||
for (let s = from; s <= to; s += 2) { const p = pointAt(s, lane); s === from ? ctx.moveTo(p.x, p.y) : ctx.lineTo(p.x, p.y); }
|
||||
ctx.stroke();
|
||||
}
|
||||
function loop(ctx, width, color) {
|
||||
ctx.strokeStyle = color; ctx.lineWidth = width; ctx.lineJoin = 'round';
|
||||
ctx.beginPath();
|
||||
TRACK.pts.forEach(([x, y], i) => i ? ctx.lineTo(x, y) : ctx.moveTo(x, y));
|
||||
ctx.closePath(); ctx.stroke();
|
||||
}
|
||||
function ground(ctx) {
|
||||
ctx.fillStyle = '#6f8f3e'; ctx.fillRect(0, 0, 640, 360);
|
||||
for (let i = 0; i < 260; i++) {
|
||||
ctx.fillStyle = hash(i * 13 + 1) > 0.5 ? '#7c9b47' : '#638236';
|
||||
ctx.fillRect(Math.floor(hash(i * 13 + 2) * 640), Math.floor(hash(i * 13 + 3) * 360), 2 + Math.floor(hash(i * 13 + 4) * 6), 2);
|
||||
}
|
||||
// Dentro il giro, le mura della Fortezza e il prato dei bastioni.
|
||||
ctx.beginPath();
|
||||
TRACK.pts.forEach(([x, y], i) => i ? ctx.lineTo(x, y) : ctx.moveTo(x, y));
|
||||
ctx.closePath();
|
||||
ctx.fillStyle = '#8a7a5a'; ctx.fill();
|
||||
loop(ctx, VERGE * 2 + 8, '#6f8f3e');
|
||||
loop(ctx, VERGE * 2, '#7c9b47');
|
||||
loop(ctx, ROAD * 2 + 2, '#7a6446');
|
||||
loop(ctx, ROAD * 2, '#b59b72');
|
||||
for (let i = 0; i < TRACK.pts.length; i += 3) {
|
||||
const [x, y] = TRACK.pts[i], o = (hash(i) - 0.5) * ROAD * 1.6;
|
||||
ctx.fillStyle = hash(i + 7) > 0.5 ? '#a88d64' : '#c2a87e'; ctx.fillRect(Math.round(x + o * 0.5), Math.round(y + o * 0.5), 2, 1);
|
||||
}
|
||||
// La paglia fuori dalle curve.
|
||||
for (const k of CURVES) for (let s = k.start; s <= k.end; s += 8) {
|
||||
const p = pointAt(s, -(VERGE + 3) * k.side);
|
||||
ctx.save(); ctx.translate(p.x, p.y); ctx.rotate(p.heading);
|
||||
ctx.fillStyle = '#d9b75a'; ctx.fillRect(-3.5, -2.5, 7, 5); ctx.fillStyle = '#b8953f'; ctx.fillRect(-3.5, -0.5, 7, 1);
|
||||
ctx.restore();
|
||||
}
|
||||
const p = pointAt(0);
|
||||
ctx.save(); ctx.translate(p.x, p.y); ctx.rotate(p.heading);
|
||||
for (let i = -6; i < 6; i++) for (let j = 0; j < 2; j++) { ctx.fillStyle = (i + j) % 2 ? '#111' : '#f4f4f4'; ctx.fillRect(j * 2 - 2, i * 2, 2, 2); }
|
||||
ctx.restore();
|
||||
}
|
||||
function guide(ctx, c) {
|
||||
const v = player(c).vmax;
|
||||
// Alla bona, in rosso dove si frena per arrivare giusti in curva; Seria, un cono all'inizio della frenata.
|
||||
for (const k of CURVES) {
|
||||
const from = k.apex - (v * v - k.speed * k.speed * 0.7) / (2 * BRAKE);
|
||||
if (c.skill.guide === 'zone') stroke(ctx, from, k.apex, 4, 'rgba(214,64,47,0.6)');
|
||||
if (c.skill.guide === 'coni') {
|
||||
const p = pointAt(from, (ROAD + 3) * k.side);
|
||||
ctx.fillStyle = '#f07a1a'; ctx.beginPath(); ctx.moveTo(p.x, p.y - 5); ctx.lineTo(p.x + 3, p.y + 2); ctx.lineTo(p.x - 3, p.y + 2); ctx.fill();
|
||||
ctx.fillStyle = '#fff'; ctx.fillRect(p.x - 1.5, p.y - 2, 3, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
function things(ctx, c) {
|
||||
for (const p of PICKUPS) {
|
||||
ctx.save(); ctx.translate(Math.round(p.x), Math.round(p.y)); ctx.rotate(p.heading);
|
||||
if (p.kind === 'boost') {
|
||||
// L'acceleratore: tre frecce gialle che lampeggiano in fila.
|
||||
for (let k = 0; k < 3; k++) {
|
||||
ctx.fillStyle = Math.floor(c.age * 6 - k) % 3 === 0 ? '#fff27a' : '#e0b020';
|
||||
ctx.beginPath(); ctx.moveTo(-6 + k * 5, -5); ctx.lineTo(-2 + k * 5, 0); ctx.lineTo(-6 + k * 5, 5); ctx.lineTo(-4 + k * 5, 0); ctx.fill();
|
||||
}
|
||||
} else if (p.kind === 'buca') {
|
||||
ctx.fillStyle = '#5a4630'; ctx.beginPath(); ctx.ellipse(0, 0, 6, 4.5, 0, 0, Math.PI * 2); ctx.fill();
|
||||
ctx.fillStyle = '#2e2418'; ctx.beginPath(); ctx.ellipse(0.5, 0.5, 4.5, 3, 0, 0, Math.PI * 2); ctx.fill();
|
||||
} else if ((c.taken[p.id] ?? -Infinity) <= c.t - CAN.respawn) {
|
||||
// La lattina della miscela, che saltella.
|
||||
const y = Math.round(Math.sin(c.age * 5 + p.id) * 1);
|
||||
ctx.rotate(-p.heading);
|
||||
ctx.fillStyle = '#f4e3c2'; ctx.fillRect(-3, -4 + y, 6, 8);
|
||||
ctx.fillStyle = '#c8321e'; ctx.fillRect(-2, -3 + y, 4, 6);
|
||||
ctx.fillStyle = '#f0c23a'; ctx.fillRect(-2, -1 + y, 4, 1); ctx.fillRect(-1, -5 + y, 2, 1);
|
||||
}
|
||||
ctx.restore();
|
||||
}
|
||||
}
|
||||
function moped(ctx, r, c) {
|
||||
const out = r.out > 0;
|
||||
ctx.save(); ctx.translate(Math.round(r.x), Math.round(r.y)); ctx.rotate(r.a + (out ? Math.sin(c.age * 22) * 0.6 : 0));
|
||||
ctx.fillStyle = 'rgba(0,0,0,0.3)'; ctx.fillRect(-6, -1, 13, 4);
|
||||
ctx.fillStyle = '#151515'; ctx.fillRect(-7, -1, 3, 2); ctx.fillRect(5, -1, 3, 2); // le ruote
|
||||
ctx.fillStyle = '#f4e3c2'; ctx.fillRect(-5, -3, 11, 6); // il bordo chiaro: anche i motorini scuri si vedono
|
||||
ctx.fillStyle = r.color; ctx.fillRect(-4, -2, 9, 4);
|
||||
ctx.fillStyle = r.trim ?? '#1c1c1c'; ctx.fillRect(-4, -0.5, 9, 1);
|
||||
ctx.fillStyle = '#2a2a2a'; ctx.fillRect(4, -3, 1, 6); // il manubrio
|
||||
ctx.fillStyle = r.id === 'andrea' ? '#f4e3c2' : '#2a2a2a'; ctx.beginPath(); ctx.arc(-1, 0, 2, 0, Math.PI * 2); ctx.fill(); // il casco
|
||||
// La miscela: una fiammata dietro.
|
||||
if (r.boost > 0 && Math.floor(c.age * 20) % 2) { ctx.fillStyle = '#ffb030'; ctx.fillRect(-10, -1, 3, 2); }
|
||||
ctx.restore();
|
||||
if (r.id === 'andrea') { ctx.strokeStyle = '#f4e3c2'; ctx.lineWidth = 1; ctx.beginPath(); ctx.arc(r.x, r.y, 10, 0, Math.PI * 2); ctx.stroke(); }
|
||||
}
|
||||
// In basso a sinistra, lontano dalla pausa e dal pulsante per uscire: il giro, la classifica e il tachimetro.
|
||||
function hud(ctx, c) {
|
||||
const me = player(c), order = standings(c), h = 34 + order.length * 11, y = 354 - h;
|
||||
panel(ctx, 4, y, 116, h);
|
||||
text(ctx, `GIRO ${lapOf(me)}/${LAPS}`, 10, y + 9, 7);
|
||||
order.forEach((r, i) => {
|
||||
ctx.fillStyle = '#f4e3c2'; ctx.fillRect(9, y + 18 + i * 11, 8, 8);
|
||||
ctx.fillStyle = r.color; ctx.fillRect(10, y + 19 + i * 11, 6, 6);
|
||||
text(ctx, `${i + 1}. ${r.name}`, 21, y + 23 + i * 11, 6, r.id === 'andrea' ? '#ffe28a' : '#f4e3c2');
|
||||
});
|
||||
const k = Math.min(1, me.v / me.vmax), bar = y + h - 11;
|
||||
ctx.fillStyle = '#2a2f2c'; ctx.fillRect(10, bar, 50, 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');
|
||||
}
|
||||
// La presentazione: il cartellino di chi arriva. A sinistra il personaggio (lo sprite del gioco, o il ritratto), a
|
||||
// destra il suo motorino dipinto, sotto quanto va (motore) e quanto rischia (testa). `look(src)` dà l'immagine caricata,
|
||||
// `sheet(id)` lo sprite di un personaggio.
|
||||
function figure(ctx, r, x, y, look, sheet) {
|
||||
const s = r.sprite && sheet(r.sprite);
|
||||
if (s) {
|
||||
const f = s.atlas.frames.stand ?? Object.values(s.atlas.frames)[0], k = Math.min(1, 84 / f.h);
|
||||
ctx.drawImage(s.img, f.x, f.y, f.w, f.h, Math.round(x - f.w * k / 2), Math.round(y + 88 - f.h * k), Math.round(f.w * k), Math.round(f.h * k));
|
||||
return;
|
||||
}
|
||||
const img = r.face && look(r.face);
|
||||
ctx.fillStyle = '#f4e3c2'; ctx.fillRect(x - 31, y + 13, 62, 62);
|
||||
if (img) { ctx.drawImage(img, x - 30, y + 14, 60, 60); return; }
|
||||
// Nessun ritratto (ancora): la sagoma.
|
||||
ctx.fillStyle = '#2a2f2c'; ctx.fillRect(x - 30, y + 14, 60, 60);
|
||||
ctx.fillStyle = '#4a504c'; ctx.beginPath(); ctx.arc(x, y + 36, 11, 0, Math.PI * 2); ctx.fill();
|
||||
ctx.fillRect(x - 18, y + 52, 36, 22);
|
||||
text(ctx, '?', x, y + 37, 10, '#8f9488', 'center');
|
||||
}
|
||||
function card(ctx, c, look, sheet) {
|
||||
const r = presenting(c);
|
||||
if (!r) { panel(ctx, 220, 130, 200, 40); text(ctx, 'GP DI FORTEZZA', 320, 142, 7, '#ffe28a', 'center'); text(ctx, 'TUTTI IN GRIGLIA!', 320, 158, 7, '#f4e3c2', 'center'); return; }
|
||||
const x = 150, y = 92, w = 340, h = 150;
|
||||
panel(ctx, x, y, w, h);
|
||||
text(ctx, 'GP DI FORTEZZA', x + w / 2, y + 10, 6, '#ffe28a', 'center');
|
||||
figure(ctx, r, x + 46, y + 14, look, sheet);
|
||||
// Il motorino, entra da sinistra e si ferma (come in pista).
|
||||
const moto = r.moto && look(r.moto), t = Math.min(1, (c.t % INTRO.each) / 0.6), e = 1 - (1 - t) ** 3;
|
||||
if (moto) {
|
||||
const k = Math.min(150 / moto.width, 80 / moto.height), mw = moto.width * k, mh = moto.height * k;
|
||||
ctx.save(); ctx.beginPath(); ctx.rect(x + 92, y + 18, w - 100, 92); ctx.clip();
|
||||
ctx.drawImage(moto, Math.round(x + 92 + (w - 100 - mw) / 2 - (1 - e) * 120), Math.round(y + 104 - mh), Math.round(mw), Math.round(mh));
|
||||
ctx.restore();
|
||||
} else {
|
||||
ctx.fillStyle = r.color; ctx.fillRect(x + 160, y + 70, 100, 22);
|
||||
ctx.fillStyle = r.trim ?? '#1c1c1c'; ctx.fillRect(x + 160, y + 80, 100, 3);
|
||||
}
|
||||
text(ctx, r.name, x + 100, y + 118, 8, r.id === 'andrea' ? '#ffe28a' : '#f4e3c2');
|
||||
text(ctx, r.bike, x + 100, y + 130, 6, '#c5c6b6');
|
||||
for (const [k, [label, value]] of [['MOTORE', r.motore], ['TESTA', r.testa]].entries()) {
|
||||
const yy = y + 116 + k * 12;
|
||||
text(ctx, label, x + 222, yy, 5, '#c5c6b6');
|
||||
for (let i = 0; i < 5; i++) { ctx.fillStyle = i < value ? (k ? '#b9d392' : '#e0583a') : '#3a3f3c'; ctx.fillRect(x + 262 + i * 13, yy - 3, 10, 6); }
|
||||
}
|
||||
text(ctx, 'TOCCA: IL PROSSIMO', x + w / 2, y + h - 6, 5, '#8f9488', 'center');
|
||||
}
|
||||
// La posizione in corsa, grande in alto a sinistra, e un cartellino sopra il Fifty.
|
||||
function position(ctx, c) {
|
||||
const me = player(c), place = placeOf(c), n = c.riders.length, first = place === 1;
|
||||
panel(ctx, 4, 4, 70, 40);
|
||||
text(ctx, 'POSIZIONE', 39, 11, 5, '#c5c6b6', 'center');
|
||||
text(ctx, `${place}°`, 11, 29, 14, first ? '#ffe28a' : '#f4e3c2');
|
||||
text(ctx, `su ${n}`, 68, 33, 6, '#c5c6b6', 'right');
|
||||
if (me.shown === false) return;
|
||||
const x = Math.round(me.x), y = Math.round(me.y) - 17;
|
||||
ctx.fillStyle = 'rgba(22,25,23,0.85)'; ctx.fillRect(x - 8, y - 5, 16, 10);
|
||||
ctx.fillStyle = first ? '#ffe28a' : '#f4e3c2'; ctx.fillRect(x - 1, y + 5, 2, 2);
|
||||
text(ctx, `${place}°`, x, y + 0.5, 6, first ? '#ffe28a' : '#f4e3c2', 'center');
|
||||
}
|
||||
function lights(ctx, c) {
|
||||
const lit = Math.min(3, Math.floor(c.t) + 1);
|
||||
panel(ctx, 270, 140, 100, 50);
|
||||
text(ctx, 'GP DI FORTEZZA', 320, 150, 6, '#ffe28a', 'center');
|
||||
for (let i = 0; i < 3; i++) {
|
||||
ctx.fillStyle = i < lit ? '#e0402a' : '#3a2a28';
|
||||
ctx.beginPath(); ctx.arc(300 + i * 20, 170, 7, 0, Math.PI * 2); ctx.fill();
|
||||
}
|
||||
if (c.t >= COUNTDOWN - 0.01) text(ctx, 'VIA!', 320, 200, 10, '#b9d392', 'center');
|
||||
}
|
||||
function result(ctx, c) {
|
||||
const order = c.result.order.map(id => c.riders.find(r => r.id === id));
|
||||
panel(ctx, 200, 96, 240, 44 + order.length * 14);
|
||||
text(ctx, 'GP DI FORTEZZA', 320, 108, 6, '#c5c6b6', 'center');
|
||||
text(ctx, c.result.place === 1 ? 'PRIMO!' : `${c.result.place}° POSTO`, 320, 122, 10, c.result.place === 1 ? '#ffe28a' : '#f4e3c2', 'center');
|
||||
order.forEach((r, i) => {
|
||||
ctx.fillStyle = '#f4e3c2'; ctx.fillRect(213, 137 + i * 14, 10, 10);
|
||||
ctx.fillStyle = r.color; ctx.fillRect(214, 138 + i * 14, 8, 8);
|
||||
text(ctx, `${i + 1}. ${r.name}`, 228, 142 + i * 14, 7, r.id === 'andrea' ? '#ffe28a' : '#f4e3c2');
|
||||
text(ctx, `${c.result.times[r.id].toFixed(1)}s`, 428, 142 + i * 14, 7, '#c5c6b6', 'right');
|
||||
});
|
||||
}
|
||||
|
||||
export function drawGara(ctx, c, def, bg, look = () => null, sheet = () => null) {
|
||||
ctx.save();
|
||||
if (bg) ctx.drawImage(bg, 0, 0, 640, 360);
|
||||
else ground(ctx);
|
||||
guide(ctx, c);
|
||||
things(ctx, c);
|
||||
// Prima chi è dietro, così chi è davanti resta sopra; Andrea sempre sopra a tutti.
|
||||
for (const r of [...standings(c)].reverse().sort((a, b) => (a.id === 'andrea') - (b.id === 'andrea'))) if (r.shown !== false) moped(ctx, r, c);
|
||||
if (c.step === 'intro') { card(ctx, c, look, sheet); ctx.restore(); return; }
|
||||
hud(ctx, c);
|
||||
if (c.step === 'race') position(ctx, c);
|
||||
if (c.step === 'start') lights(ctx, 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.boost > 0 && me.out <= 0) text(ctx, 'MISCELA!', 320, 180, 8, '#ffb030', 'center');
|
||||
if (c.step === 'result') result(ctx, c);
|
||||
ctx.restore();
|
||||
}
|
||||
Reference in New Issue
Block a user