Macchinine: si sceglie la macchinina, la forza sale da zero tenendo premuto e la spinta parte lasciando; il saluto col Chiaru («un girino alle case rosse» / «Vorrai dire un giochino!» / «Ciao Mattè»), «Si fa una gara alle macchinine?!?!», Andrea chiama Bertovo «Fabri»; musica del GP in prestito (Andrea)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GiECd9KMfjthRUnujCGSzZ
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { HALF, CAR, canThrow, guide, done, overHeld } from './micro.js';
|
||||
import { HALF, CAR, CARS, canThrow, guide, done, overHeld, carImage, choiceAt } from './micro.js';
|
||||
import { text, panel } from './bocce-render.js';
|
||||
|
||||
const NAME = { player: 'Andrea', cpu: 'Chiaru' };
|
||||
@@ -68,6 +68,12 @@ function aim(ctx, c, img) {
|
||||
ctx.fillStyle = over ? '#ff6a4a' : '#e8b48f'; ctx.strokeStyle = '#6b3d26'; ctx.lineWidth = 1;
|
||||
ctx.beginPath(); ctx.arc(fx, fy, 4, 0, Math.PI * 2); ctx.fill(); ctx.stroke();
|
||||
ctx.fillStyle = '#f7d9c4'; ctx.fillRect(Math.round(fx + ux * 1.5) - 1, Math.round(fy + uy * 1.5) - 1, 2, 2); // l'unghia
|
||||
// La forza che sale mentre si tiene premuto: una barretta sopra la macchinina.
|
||||
if (c.holding) {
|
||||
const x = Math.round(me.x - 15), y = Math.round(me.y - 19);
|
||||
ctx.fillStyle = '#17160fd0'; ctx.fillRect(x - 1, y - 1, 32, 6);
|
||||
ctx.fillStyle = over ? '#ff6a4a' : c.power > 0.75 ? '#ffd23c' : '#9be07c'; ctx.fillRect(x, y, Math.round(30 * c.power), 4);
|
||||
}
|
||||
}
|
||||
// Dove una macchinina ha fatto il fallo: una croce di gesso rosso che sfuma, e la macchinina (ribaltata, se ha cappottato)
|
||||
// che sparisce per tornare al suo posto.
|
||||
@@ -83,11 +89,27 @@ function returns(ctx, c, images) {
|
||||
}
|
||||
}
|
||||
|
||||
// La scelta della macchinina: le sei in grande sul parcheggio, quella scelta cerchiata di gesso e un po' sollevata.
|
||||
function choice(ctx, c, image) {
|
||||
ctx.fillStyle = '#17160fc8'; ctx.fillRect(60, 56, 520, 250);
|
||||
text(ctx, 'SCEGLI LA TUA MACCHININA', 320, 76, 12, '#efd49e', 'center');
|
||||
CARS.forEach((car, i) => {
|
||||
const [x, y] = choiceAt(i), on = i === c.choice, lift = on ? Math.sin(c.visual.time * 5) * 1.5 - 2 : 0, img = image?.(carImage(car.id, true));
|
||||
if (on) { ctx.strokeStyle = 'rgba(246,246,238,0.9)'; ctx.lineWidth = 2; ctx.beginPath(); ctx.ellipse(x, y, 50, 28, 0, 0, Math.PI * 2); ctx.stroke(); }
|
||||
ctx.fillStyle = 'rgba(10,12,16,0.35)'; ctx.fillRect(x - 37, y - 16, 78, 38); // l'ombra
|
||||
if (img) ctx.drawImage(img, Math.round(x - img.width / 2), Math.round(y - img.height / 2 + lift));
|
||||
text(ctx, car.name, x, y + 34, 8, on ? '#fff0c8' : '#a99d86', 'center');
|
||||
});
|
||||
const cpu = CARS[c.choice].id === 'blu' ? 'la rossa' : 'la blu e gialla';
|
||||
text(ctx, `Il Chiaru prende ${cpu}. Tocca la tua, o frecce e Scegli.`, 320, 292, 8, '#d9ccba', 'center');
|
||||
}
|
||||
|
||||
export function drawMicro(ctx, c, def, bg, image) {
|
||||
ctx.save(); ctx.imageSmoothingEnabled = false;
|
||||
if (bg) ctx.drawImage(bg, 0, 0, 640, 360); else { ctx.fillStyle = '#4b4d50'; ctx.fillRect(0, 0, 640, 360); }
|
||||
drawChalk(ctx, c.track);
|
||||
const images = { player: image?.(def.cars?.player), cpu: image?.(def.cars?.cpu) };
|
||||
if (c.step === 'choose') { choice(ctx, c, image); ctx.restore(); return; }
|
||||
const images = { player: image?.(carImage(c.colors.player)), cpu: image?.(carImage(c.colors.cpu)) };
|
||||
if (c.step === 'rolling' && c.trail.length > 1) {
|
||||
ctx.strokeStyle = 'rgba(255,248,220,0.35)'; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(...c.trail[0]);
|
||||
for (const p of c.trail.slice(1)) ctx.lineTo(...p);
|
||||
@@ -101,7 +123,8 @@ export function drawMicro(ctx, c, def, bg, image) {
|
||||
text(ctx, 'LA PISTA COL GESSO', 8, 7, 8, '#efd49e');
|
||||
const level = { bona: 'alla bona', seria: 'seria', male: 'a fassi male' }[c.level];
|
||||
text(ctx, `${level} · spinte ${c.flicks.player}`, 8, 16, 7, '#d9ccba');
|
||||
for (const [who, x0, color] of [['player', 168, '#e0543c'], ['cpu', 404, '#3c66d8']]) {
|
||||
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)]]) {
|
||||
text(ctx, NAME[who], x0, 11, 8, '#fff0c8');
|
||||
ctx.fillStyle = '#3a372e'; ctx.fillRect(x0 + 52, 7, 150, 8);
|
||||
ctx.fillStyle = color; ctx.fillRect(x0 + 52, 7, Math.round(150 * done(c, who)), 8);
|
||||
|
||||
+47
-8
@@ -19,7 +19,7 @@ export const CONTROL = [
|
||||
];
|
||||
export const HALF = 22, CAR = { len: 26, wid: 13, r: 6 }, LAPS = 1;
|
||||
// La spinta: velocità di partenza da MIN_SPEED a MAX_SPEED, e l'asfalto che frena di DECEL px/s².
|
||||
export const MIN_SPEED = 60, MAX_SPEED = 400, DECEL = 420, SWING = 1.2, LONG_PRESS = 0.6, FIXED_STEP = 1 / 120;
|
||||
export const MIN_SPEED = 60, MAX_SPEED = 400, DECEL = 420, SWING = 1.2, TAP = 0.14, FIXED_STEP = 1 / 120;
|
||||
const REST = 0.7, THINK = 1, RESTITUTION = 0.6, OUT = HALF + CAR.r, WINDOW = 30;
|
||||
// FLIP: la velocità d'urto (px/s) che ribalta la macchinina colpita; ACCOMPAGNA: quanti secondi si può restare alla
|
||||
// carica piena prima che diventi un accompagnè.
|
||||
@@ -144,14 +144,43 @@ function step(track, list, dt, events) {
|
||||
for (const car of list) if (car.vx || car.vy) follow(track, car);
|
||||
}
|
||||
|
||||
// Le macchinine fra cui scegliere (Andrea, 29 ottobre: «quando si parte fammi scegliere la macchinina»): il disegno di
|
||||
// Codex in `assets/closeups/micro/<id>.png` (e `<id>-grande.png` per la scelta) e il colore della barra in alto.
|
||||
// Il Chiaru tiene la sua blu e gialla; se la prende Andrea, lui prende la rossa.
|
||||
export const CARS = [
|
||||
{ id: 'rossa', name: 'Rossa e bianca', color: '#e0543c' },
|
||||
{ id: 'blu', name: 'Blu e gialla', color: '#3c66d8' },
|
||||
{ id: 'nera', name: 'Nera e oro', color: '#c9a23a' },
|
||||
{ id: 'azzurra', name: 'Bianca e azzurra', color: '#6fb7ff' },
|
||||
{ id: 'arancio', name: 'Arancione', color: '#f08a2c' },
|
||||
{ id: 'viola', name: 'Viola e argento', color: '#9a5ad8' },
|
||||
];
|
||||
export const carImage = (id, big = false) => `assets/closeups/micro/${id}${big ? '-grande' : ''}.png`;
|
||||
// Dove stanno le macchinine nella scelta: due file da tre, al centro del parcheggio.
|
||||
export const choiceAt = i => [160 + (i % 3) * 160, 132 + Math.floor(i / 3) * 88];
|
||||
export function pick(c, i = c.choice) {
|
||||
if (c.step !== 'choose' || !CARS[i]) return false;
|
||||
const mine = CARS[i].id;
|
||||
c.colors = { player: mine, cpu: mine === 'blu' ? 'rossa' : 'blu' };
|
||||
c.step = 'aim'; c.t = 0; c.aimLocked = false; // il clic che sceglie non deve fissare anche la mira
|
||||
return true;
|
||||
}
|
||||
export function choose(c, i) {
|
||||
if (c.step !== 'choose' || !Number.isFinite(i)) return false;
|
||||
c.choice = (i + CARS.length) % CARS.length; return true;
|
||||
}
|
||||
const carUnder = (x, y) => CARS.findIndex((_, i) => { const [cx, cy] = choiceAt(i); return Math.abs(x - cx) < 44 && Math.abs(y - cy) < 30; });
|
||||
|
||||
export function newMicro(seed = Date.now() >>> 0, def = {}) {
|
||||
const level = LEVELS[def.level] ? def.level : 'seria', track = TRACK;
|
||||
const c = { step: 'aim', t: 0, seed, level, skill: LEVELS[level], current: 'player', power: 0.5, angle: 0,
|
||||
const c = { step: 'choose', choice: 0, colors: null, t: 0, seed, level, skill: LEVELS[level], current: 'player', power: 0.5, angle: 0,
|
||||
holding: false, holdTime: 0, accumulator: 0, events: [], track, laps: LAPS, flicks: { player: 0, cpu: 0 },
|
||||
line: null, trail: [], returns: [], before: null, plan: null, visual: { time: 0 } };
|
||||
// In griglia: Andrea all'esterno, il Chiaru all'interno, poco prima del traguardo.
|
||||
c.cars = { player: newCar(track, 'player', 18, -11), cpu: newCar(track, 'cpu', 18, 11) };
|
||||
c.angle = c.cars.player.heading;
|
||||
// Si parte scegliendo la macchinina, se non è già data (`def.car`).
|
||||
if (def.car) pick(c, CARS.findIndex(x => x.id === def.car));
|
||||
return c;
|
||||
}
|
||||
|
||||
@@ -285,7 +314,10 @@ export function aimAt(c, x, y) {
|
||||
if (Math.hypot(x - car.x, y - car.y) < CAR.r) return false;
|
||||
c.angle = Math.atan2(y - car.y, x - car.x); return true;
|
||||
}
|
||||
export const aimScreen = (c, [x, y]) => x >= 0 && x <= 640 && y >= 24 && y <= 360 && aimAt(c, x, y);
|
||||
export function aimScreen(c, [x, y]) {
|
||||
if (c.step === 'choose') { const i = carUnder(x, y); return i >= 0 && choose(c, i); }
|
||||
return x >= 0 && x <= 640 && y >= 24 && y <= 360 && aimAt(c, x, y);
|
||||
}
|
||||
export function nudge(c, dir, fine = false) {
|
||||
if (!canThrow(c) || c.holding || !Number.isFinite(dir)) return false;
|
||||
c.angle += dir * (fine ? 0.004 : 0.03); return true;
|
||||
@@ -294,8 +326,9 @@ export function setPower(c, value) {
|
||||
if (!canThrow(c) || c.holding || !Number.isFinite(value)) return false;
|
||||
c.power = clamp(value, 0, 1); return true;
|
||||
}
|
||||
export function shoot(c) { return canThrow(c) && !c.holding && strike(c, 'player', c.angle, c.power); }
|
||||
export function shoot(c) { return c.step === 'choose' ? pick(c) : canThrow(c) && !c.holding && strike(c, 'player', c.angle, c.power); }
|
||||
export function turn(c, dir, owner, fine = false) {
|
||||
if (c.step === 'choose') { choose(c, c.choice + dir); return false; } // nella scelta frecce e pulsanti cambiano macchinina
|
||||
if (!canThrow(c) || c.holding || c.turn) return false;
|
||||
c.turn = { dir, owner, fine, time: 0 }; c.angle += dir * (fine ? 0.004 : 0.03);
|
||||
c.aimLocked = true; return true;
|
||||
@@ -311,10 +344,11 @@ export function key(c, key, down, fine = false) {
|
||||
return true;
|
||||
}
|
||||
export function press(c, pointerId) {
|
||||
if (c.step === 'choose') return pick(c);
|
||||
if (!canThrow(c) || c.holding) return false;
|
||||
c.turn = null; c.pointerId = pointerId; c.holdDelay = pointerId === undefined ? 0 : LONG_PRESS;
|
||||
c.previousPower = c.power; c.holding = true; c.charging = false; c.holdTime = 0;
|
||||
if (!c.holdDelay) c.power = 0;
|
||||
// Premuto, la forza parte da zero e sale finché non si lascia (Andrea, 29 ottobre): come il dito che si carica.
|
||||
c.turn = null; c.pointerId = pointerId; c.holdDelay = 0;
|
||||
c.previousPower = c.power; c.holding = true; c.charging = false; c.holdTime = 0; c.power = 0;
|
||||
return true;
|
||||
}
|
||||
export function cancel(c, pointerId) {
|
||||
@@ -325,7 +359,7 @@ export function cancel(c, pointerId) {
|
||||
}
|
||||
export function release(c, pointerId) {
|
||||
if (c.pointerId !== pointerId || !c.holding) return false;
|
||||
if (c.holdTime < Math.max(0.14, c.holdDelay)) return cancel(c, pointerId); // il tocco mira soltanto
|
||||
if (c.holdTime < TAP) return cancel(c, pointerId); // il tocco secco mira soltanto
|
||||
const accompagne = (c.overHold ?? 0) > ACCOMPAGNA;
|
||||
c.holding = c.charging = false; c.overHold = 0;
|
||||
strike(c, 'player', c.angle, c.power);
|
||||
@@ -346,7 +380,11 @@ export function guide(c) {
|
||||
return { x: car.x + ux * length, y: car.y + uy * length, length, out };
|
||||
}
|
||||
|
||||
// Il pulsante del pannello vale anche nella scelta della macchinina.
|
||||
export const canPress = c => canThrow(c) || c.step === 'choose';
|
||||
export const canAdjust = canPress;
|
||||
export function button(c) {
|
||||
if (c.step === 'choose') return 'Scegli';
|
||||
if (c.step === 'result') return 'Finita';
|
||||
if (c.step === 'rolling' || c.step === 'settle') return 'In movimento…';
|
||||
return c.current === 'cpu' ? 'Spinge il Chiaru…' : 'Spingi';
|
||||
@@ -354,6 +392,7 @@ export function button(c) {
|
||||
// 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 function status(c) {
|
||||
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.`;
|
||||
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 === 'rolling' || c.step === 'settle') return `${where} ${c.line ?? ''}`.trim();
|
||||
|
||||
+7
-6
@@ -236,10 +236,9 @@ export const story = {
|
||||
// Le macchinine sulla pista col gesso, sul parcheggio di Cardetole, contro il Chiaru (Andrea, 28 e 29 ottobre).
|
||||
// [DA ANDREA: le battute sono provvisorie]
|
||||
micro: {
|
||||
type: 'micro',
|
||||
type: 'micro', music: 'assets/audio/gara.mp3', // in prestito dal GP di Fortezza, finché non c'è la sua
|
||||
picture: 'assets/closeups/micro-parcheggio.png',
|
||||
names: { player: 'Andrea', cpu: 'Chiaru' },
|
||||
cars: { player: 'assets/closeups/micro/rossa.png', cpu: 'assets/closeups/micro/blu.png' },
|
||||
win: [{ say: 'Hai vinto te. Ma la mia aveva le ruote piene di gesso, sia chiaro.', who: 'chiaru', pose: 'shrug' }, { set: 'micro_vinto' }],
|
||||
lose: [{ say: 'Visto? La blu non perde mai. Mai.', who: 'chiaru', pose: 'cheer' }],
|
||||
quit: [{ say: 'Ti ritiri? Allora la vinco a tavolino.', who: 'chiaru', pose: 'laugh' }],
|
||||
@@ -3254,7 +3253,7 @@ export const story = {
|
||||
nodes: {
|
||||
start: {
|
||||
options: [
|
||||
{ text: 'Si corre?', next: 'livello', then: [{ say: 'Un giro, una spinta a testa, prima te. Tutta fuori dal gesso, si torna indietro. Come la vuoi?', who: 'chiaru', pose: 'explain' }] },
|
||||
{ text: 'Si fa una gara alle macchinine?!?!', next: 'livello', then: [{ say: 'Un giro, una spinta a testa, prima te. Tutta fuori dal gesso, si torna indietro. Come la vuoi?', who: 'chiaru', pose: 'explain' }] },
|
||||
{ text: 'Che pista hai disegnato?', then: [
|
||||
{ say: 'Rettilineo, curvone, rettilineo, e in fondo la esse. Nella esse chi ha fretta esce.', who: 'chiaru', pose: 'explain' },
|
||||
{ say: 'Il tombino l\'ho lasciato in pista: porta fortuna.', who: 'chiaru', pose: 'talk' },
|
||||
@@ -3265,7 +3264,8 @@ export const story = {
|
||||
{ say: 'Chi taglia la strada: tagliè. Chi si ribalta: cappottè. E chi esce tutto dal gesso, fuori.', who: 'chiaru', pose: 'talk' },
|
||||
{ say: 'In tutti i casi, si torna dov\'eri prima.', who: 'chiaru', pose: 'shrug' },
|
||||
] },
|
||||
{ text: 'Dopo, Chiaru.', next: 'end', then: [{ say: 'La pista è qui. Finché non piove.', who: 'chiaru', pose: 'stand' }] },
|
||||
// Il saluto (Andrea, 29 ottobre).
|
||||
{ text: 'Vado a fare un girino alle case rosse.', next: 'end', then: [{ say: 'Vorrai dire un giochino!', who: 'chiaru', pose: 'laugh' }, { say: 'Ciao Mattè.' }] },
|
||||
],
|
||||
},
|
||||
livello: {
|
||||
@@ -3280,11 +3280,12 @@ export const story = {
|
||||
},
|
||||
// Trappizzio Berti al tavolo di mezzo: le biglie nascoste e «Oh tirami questo rinquarto!?» (Andrea, 28 ottobre).
|
||||
// [DA ANDREA: le battute sono provvisorie, tranne la sua sfida]
|
||||
// Andrea, parlandogli, lo chiama Fabri (29 ottobre).
|
||||
bertovo: {
|
||||
nodes: {
|
||||
start: {
|
||||
options: [
|
||||
{ text: 'Trappizzio Berti! Che fai, giochi da solo?', then: [
|
||||
{ text: 'Fabri! Che fai, giochi da solo?', then: [
|
||||
{ say: 'Da solo? Io gioco contro le sponde. E vinco quasi sempre.', who: 'bertovo', pose: 'explain' },
|
||||
] },
|
||||
{ text: 'Come fai a prendere le biglie dove nessuno ci arriva?', then: [
|
||||
@@ -3299,7 +3300,7 @@ export const story = {
|
||||
{ text: 'Si fa una sfida a rinquarti?', next: 'sfida', then: [
|
||||
{ say: 'Le biglie le metto io, nascoste per bene. Tiri te, poi tiro io. Oh tirami questo rinquarto!?', who: 'bertovo', pose: 'explain' },
|
||||
] },
|
||||
{ text: 'Ci si vede, Trappizzio Berti.', next: 'end', then: [{ say: 'Occhio alle sponde, fuori.', who: 'bertovo', pose: 'shrug' }] },
|
||||
{ text: 'Ci si vede, Fabri.', next: 'end', then: [{ say: 'Occhio alle sponde, fuori.', who: 'bertovo', pose: 'shrug' }] },
|
||||
],
|
||||
},
|
||||
sfida: {
|
||||
|
||||
Reference in New Issue
Block a user