La cerbottana: dalla bottega di Pierin di Frilli in piazza lo stucco verde, poi i pallini contro il muro della piazza (fiato, respiro, tosse, bersaglio di gesso, lattine, barattolo appeso; muro di Codex) (Andrea)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GiECd9KMfjthRUnujCGSzZ
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 316 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -95,6 +95,10 @@
|
||||
"file": "andrea-a9df2e6bbb.mp3",
|
||||
"sec": 4.98
|
||||
},
|
||||
"andrea|La bottega di Pierin di Frilli, sotto la tenda rossa. Da ragazzi ci si comprava lo stucco per la cerbottana.": {
|
||||
"file": "andrea-cb15f8f919.mp3",
|
||||
"sec": 7.29
|
||||
},
|
||||
"andrea|Il Circolo 4 Maggio. Dentro, dietro il bar, la sala dei biliardi: tre tavoli e il silenzio delle grandi occasioni.": {
|
||||
"file": "andrea-f0aef83c80.mp3",
|
||||
"sec": 9.43
|
||||
@@ -2487,6 +2491,26 @@
|
||||
"file": "mariano-d65fb6e871.mp3",
|
||||
"sec": 3.42
|
||||
},
|
||||
"andrea|Una cerbottana e un po' di stucco, Pierino!": {
|
||||
"file": "andrea-eaaf19eef0.mp3",
|
||||
"sec": 2.77
|
||||
},
|
||||
"andrea|Niente, grazie: guardavo.": {
|
||||
"file": "andrea-12cbe6f9a8.mp3",
|
||||
"sec": 2.26
|
||||
},
|
||||
"andrea|Lo stucco verde di Pierino: quell'odore non te lo levi più dalle dita.": {
|
||||
"file": "andrea-2ede7e7324.mp3",
|
||||
"sec": 3.82
|
||||
},
|
||||
"andrea|Ora un muro. Quello della piazza va benissimo.": {
|
||||
"file": "andrea-9cc0991d39.mp3",
|
||||
"sec": 3.32
|
||||
},
|
||||
"andrea|No, dopo.": {
|
||||
"file": "andrea-78ad61a67a.mp3",
|
||||
"sec": 1.22
|
||||
},
|
||||
"andrea|Si fa una gara alle macchinine?!?!": {
|
||||
"file": "andrea-17216997e2.mp3",
|
||||
"sec": 2.74
|
||||
@@ -2903,6 +2927,18 @@
|
||||
"file": "bertovo-ca941bee91.mp3",
|
||||
"sec": 5.41
|
||||
},
|
||||
"andrea|Tutti sul muro, verdi in fila. Lo stucco di Pierino un tradisce mai.": {
|
||||
"file": "andrea-585cd65f93.mp3",
|
||||
"sec": 5.2
|
||||
},
|
||||
"andrea|Finito lo stucco. Domani si ripassa da Pierino.": {
|
||||
"file": "andrea-62c5d74b82.mp3",
|
||||
"sec": 3.29
|
||||
},
|
||||
"andrea|Il muro resta lì. E l'odore dello stucco, sulle dita, pure.": {
|
||||
"file": "andrea-c7dcb11e66.mp3",
|
||||
"sec": 5.52
|
||||
},
|
||||
"chiaru|Hai vinto te. Ma la mia aveva le ruote piene di gesso, sia chiaro.": {
|
||||
"file": "chiaru-91a4b23b22.mp3",
|
||||
"sec": 4.44
|
||||
|
||||
@@ -34,6 +34,7 @@ ne è un'opera derivata. Le foto scattate da Andrea non hanno problemi; quelle p
|
||||
| `via-liguri-magelli-muro.png` | Codex, ritocco di `via-liguri-magelli.png` con la Ghiacciomobile nel muro | come `via-liguri-magelli.png` | Street View | **no**: come lo sfondo da cui viene |
|
||||
| `characters/zio_mario.png` | Codex | `foto/zio-mario.png` | Andrea (29 ottobre) | sì (famiglia di Andrea) |
|
||||
| `terrazza-cardetole.png` | Codex (dalla foto, poi il terrazzo davanti) | `cardetole-finestra.jpg` | Andrea (29 ottobre) | **da decidere**: come `cardetole.png`, dice dove abita Andrea |
|
||||
| `closeups/muro-cerbottana.png` | Codex, solo testo (stile dalla piazza) | nessuna | — | sì |
|
||||
| `cardetole.png` | Codex | `cardetole.jpg` | Andrea (EXIF) | **da decidere**: è la casa vera di Andrea, pubblicarla dice dove abita |
|
||||
| `pieve-interno.png` | Codex | `pieve-interno.png` | Mariano Mirannalti (mandata ad Andrea su WhatsApp) | sì, se Mariano è d'accordo; citarlo nei crediti |
|
||||
| `panorama-intro.png` | Codex | `altre/panorama-intro.png` | da internet (screenshot con l'icona di Google Lens) | **no**, finché non si sostituisce |
|
||||
|
||||
@@ -18,6 +18,13 @@ Online su 50037.manzolo.it tutto fino agli effetti ambientali di Codex. Aperto,
|
||||
|
||||
## Dove siamo arrivati
|
||||
|
||||
- **La cerbottana (29 ottobre)**: in piazza, la bottega di Pierin di Frilli sotto la tenda rossa; si compra lo stucco
|
||||
verde e si tira al muro della piazza (`muro-cerbottana.png`, Codex). Motore `cerbottana.js`, disegno
|
||||
`cerbottana-render.js`: si mira, si tiene premuto per il fiato (giusto al 70%: poco cade sotto, troppo va sopra), il
|
||||
mirino ondeggia col respiro, troppo a fiato pieno si tossisce. Bersaglio di gesso (10/5/2), lattine sul muretto
|
||||
(3), barattolo appeso (8, seria e a fassi male); 12 pallini, servono 30/40/50 punti; i pallini restano sul muro.
|
||||
Test in `cerbottana.test.mjs` e `npm run browser:cerbottana`. [DA ANDREA] le battute, com'era la bottega, se far
|
||||
comparire Pierino (è scomparso da anni: decide Andrea).
|
||||
- **Il terrazzo di casa a Cardetole (29 ottobre)**: dalla foto di Andrea dalla finestra (il paese che sale e lassù
|
||||
la Fortezza), Codex l'ha dipinta e ci ha messo davanti il terrazzo (`terrazza-cardetole.png`). Ci si sale da
|
||||
«casa di Andrea» a Cardetole; la battuta dell'apparecchiare ora è sulla porta-finestra. Battute provvisorie
|
||||
|
||||
@@ -255,6 +255,15 @@
|
||||
il pulsante alterna Recupera e Molla. Sei lanci. La gloria: 2 pesci alla bona, 3 seria, 4 a fassi male (lo stivale
|
||||
non conta, e non pesa). Frecce ← →: distanza del lancio; Esc esce.</p>
|
||||
</details>
|
||||
<details id="cerbottana-help" hidden><summary>Come si gioca</summary>
|
||||
<p>La cerbottana, con lo stucco verde di Pierin di Frilli: dodici pallini contro il muro della piazza. Il bersaglio
|
||||
di gesso vale 10, 5 o 2 punti secondo il cerchio, ogni lattina sul muretto 3, il barattolo appeso che dondola (Seria
|
||||
e A fassi male) 8. Servono 30 punti alla bona, 40 Seria, 50 A fassi male. I pallini restano attaccati al muro.</p>
|
||||
<p>Muovi il mouse o tocca il muro per mirare: il mirino ondeggia col respiro (di più salendo di livello). Tieni
|
||||
premuto per prendere fiato e lascia per sparare: col fiato giusto il pallino va dove punti, con poco cade sotto,
|
||||
con troppo va sopra. Alla bona la barra del fiato segna il punto giusto. Se resti troppo a fiato pieno tossisci, e
|
||||
il pallino è perso. Si può anche regolare il fiato col cursore e premere Spara. Frecce per spostare il mirino.</p>
|
||||
</details>
|
||||
<details id="micro-help" hidden><summary>Come si gioca</summary>
|
||||
<p>La pista col gesso sul parcheggio di Cardetole: un giro contro il Chiaru, una spinta a testa (prima tu, poi
|
||||
lui). La tua macchinina è la rossa. Vince chi taglia per primo il traguardo a scacchi; se ci arrivate tutti e due
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"browser:biliardo": "node tests/biliardo.browser.mjs",
|
||||
"browser:micro": "node tests/micro.browser.mjs",
|
||||
"browser:gara": "node tests/gara.browser.mjs",
|
||||
"browser:cerbottana": "node tests/cerbottana.browser.mjs",
|
||||
"browser:tressette": "node tests/tressette.browser.mjs",
|
||||
"browser:scopa": "node tests/scopa.browser.mjs",
|
||||
"browser:briscola": "node tests/briscola.browser.mjs",
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
import { TARGET, CAN, SWINGER, LEDGE, PELLETS, FLIGHT, BEST, rings, reticle, swinger, canThrow, overHeld } from './cerbottana.js';
|
||||
import { text, panel } from './bocce-render.js';
|
||||
|
||||
// Il gesso sul muro: tre cerchi un po' storti (sempre gli stessi) e i numeri dei punti, in alto, ognuno nel suo anello.
|
||||
function chalkTarget(ctx, c) {
|
||||
let inner = 0;
|
||||
for (const [r, p] of rings(c)) {
|
||||
ctx.strokeStyle = 'rgba(250,248,236,0.85)'; ctx.lineWidth = 1.5; ctx.beginPath();
|
||||
for (let k = 0; k <= 48; k++) {
|
||||
const a = k / 48 * Math.PI * 2, j = 0.6 * Math.sin(k * 2.3 + r);
|
||||
const x = TARGET.x + Math.cos(a) * (r + j), y = TARGET.y + Math.sin(a) * (r + j);
|
||||
if (k) ctx.lineTo(x, y); else ctx.moveTo(x, y);
|
||||
}
|
||||
ctx.stroke();
|
||||
if (inner) text(ctx, String(p), TARGET.x, TARGET.y - (inner + r) / 2, 6, 'rgba(250,248,236,0.8)', 'center');
|
||||
inner = r;
|
||||
}
|
||||
ctx.fillStyle = 'rgba(250,248,236,0.9)'; ctx.fillRect(TARGET.x - 1, TARGET.y - 1, 2, 2);
|
||||
}
|
||||
// Una lattina: rossa con la fascia chiara, i bordi d'alluminio.
|
||||
function can(ctx, x, y, angle = 0, color = '#c8321e') {
|
||||
ctx.save(); ctx.translate(Math.round(x), Math.round(y)); ctx.rotate(angle);
|
||||
ctx.fillStyle = 'rgba(20,16,10,0.35)'; ctx.fillRect(-CAN.w / 2 + 2, -1, CAN.w, 2);
|
||||
ctx.fillStyle = color; ctx.fillRect(-CAN.w / 2, -CAN.h, CAN.w, CAN.h);
|
||||
ctx.fillStyle = '#f2e6c8'; ctx.fillRect(-CAN.w / 2, -CAN.h + 6, CAN.w, 4);
|
||||
ctx.fillStyle = '#c9cdd2'; ctx.fillRect(-CAN.w / 2, -CAN.h, CAN.w, 2); ctx.fillRect(-CAN.w / 2, -2, CAN.w, 2);
|
||||
ctx.fillStyle = 'rgba(255,255,255,0.35)'; ctx.fillRect(-CAN.w / 2 + 2, -CAN.h + 2, 1, CAN.h - 4);
|
||||
ctx.restore();
|
||||
}
|
||||
// Un pallino di stucco: verdastro, un po' schiacciato sul muro.
|
||||
function pellet(ctx, x, y, r = 2.5) {
|
||||
ctx.fillStyle = '#2b3a1c'; ctx.beginPath(); ctx.arc(x, y, r + 0.8, 0, Math.PI * 2); ctx.fill();
|
||||
ctx.fillStyle = '#8fa05a'; ctx.beginPath(); ctx.arc(x, y, r, 0, Math.PI * 2); ctx.fill();
|
||||
ctx.fillStyle = '#c3cf8e'; ctx.fillRect(Math.round(x - r / 2), Math.round(y - r / 2), 1, 1);
|
||||
}
|
||||
// La cerbottana in basso, in prospettiva: grossa vicino alla bocca, più fine in punta, girata verso il mirino, col
|
||||
// pallino di stucco in punta.
|
||||
function blowpipe(ctx, c, to) {
|
||||
const from = { x: 320, y: 376 }, a = Math.atan2(to.y - from.y, to.x - from.x), len = 64;
|
||||
const tip = { x: from.x + Math.cos(a) * len, y: from.y + Math.sin(a) * len }, nx = -Math.sin(a), ny = Math.cos(a);
|
||||
const tube = (w0, w1, color) => {
|
||||
ctx.fillStyle = color; ctx.beginPath();
|
||||
ctx.moveTo(from.x + nx * w0, from.y + ny * w0); ctx.lineTo(tip.x + nx * w1, tip.y + ny * w1);
|
||||
ctx.lineTo(tip.x - nx * w1, tip.y - ny * w1); ctx.lineTo(from.x - nx * w0, from.y - ny * w0); ctx.fill();
|
||||
};
|
||||
tube(9, 4, '#2a2018'); tube(7, 3, '#6b5236');
|
||||
ctx.fillStyle = '#8a6a44'; ctx.fillRect(Math.round(tip.x - 1), Math.round(tip.y - 1), 2, 2);
|
||||
if (canThrow(c)) pellet(ctx, tip.x, tip.y - 1, 3.5);
|
||||
}
|
||||
|
||||
export function drawCerbottana(ctx, c, def, bg) {
|
||||
ctx.save(); ctx.imageSmoothingEnabled = false;
|
||||
if (bg) ctx.drawImage(bg, 0, 0, 640, 360); else { ctx.fillStyle = '#d9a640'; ctx.fillRect(0, 0, 640, 360); }
|
||||
chalkTarget(ctx, c);
|
||||
for (const p of c.stuck) pellet(ctx, p.x, p.y);
|
||||
// Le lattine sul muretto; colpite, cadono all'indietro.
|
||||
for (const k of c.cans) { if (k.fall < 1) can(ctx, k.x, LEDGE + k.fall * 6, -k.fall * 1.4); }
|
||||
if (c.skill.swinger) {
|
||||
const s = swinger(c);
|
||||
ctx.strokeStyle = '#3a3024'; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(SWINGER.x, SWINGER.y); ctx.lineTo(s.x, s.y); ctx.stroke();
|
||||
can(ctx, s.x, s.y + SWINGER.h, s.a + c.swing.spin * Math.sin(c.visual.time * 30) * 0.6, '#2f6fb0');
|
||||
}
|
||||
// Il pallino in volo: parte grande dalla cerbottana e rimpicciolisce andando verso il muro.
|
||||
if (c.shot) {
|
||||
const k = Math.min(1, c.shot.t / FLIGHT), { from, to } = c.shot;
|
||||
pellet(ctx, from.x + (to.x - from.x) * k, from.y + (to.y - from.y) * k - Math.sin(k * Math.PI) * 18, 6 - 3.5 * k);
|
||||
}
|
||||
const r = reticle(c);
|
||||
if (canThrow(c)) {
|
||||
const over = overHeld(c);
|
||||
ctx.strokeStyle = over ? '#ff6a4a' : '#fff8dc'; ctx.lineWidth = 1;
|
||||
ctx.beginPath(); ctx.arc(r.x, r.y, 7, 0, Math.PI * 2); ctx.stroke();
|
||||
ctx.beginPath(); ctx.moveTo(r.x - 11, r.y); ctx.lineTo(r.x - 3, r.y); ctx.moveTo(r.x + 3, r.y); ctx.lineTo(r.x + 11, r.y);
|
||||
ctx.moveTo(r.x, r.y - 11); ctx.lineTo(r.x, r.y - 3); ctx.moveTo(r.x, r.y + 3); ctx.lineTo(r.x, r.y + 11); ctx.stroke();
|
||||
}
|
||||
blowpipe(ctx, c, r);
|
||||
// Il fiato: una barra in basso a sinistra (alla bona col punto giusto segnato).
|
||||
if (canThrow(c)) {
|
||||
const x = 16, y = 300, h = 50;
|
||||
ctx.fillStyle = '#17160fd0'; ctx.fillRect(x - 3, y - 3, 18, h + 6);
|
||||
ctx.fillStyle = overHeld(c) ? '#ff6a4a' : c.power > BEST + 0.08 ? '#ffd23c' : '#9be07c';
|
||||
const f = Math.round(h * c.power); ctx.fillRect(x, y + h - f, 12, f);
|
||||
if (c.skill.sweet) { ctx.fillStyle = '#fff8dc'; ctx.fillRect(x - 3, Math.round(y + h - h * BEST) - 1, 18, 2); }
|
||||
text(ctx, 'FIATO', x + 6, y - 9, 6, '#efd49e', 'center');
|
||||
}
|
||||
// In alto: punti, pallini rimasti e cosa è successo.
|
||||
ctx.fillStyle = '#17160fe8'; ctx.fillRect(0, 0, 640, 22);
|
||||
text(ctx, 'LA CERBOTTANA', 8, 7, 8, '#efd49e');
|
||||
text(ctx, { bona: 'alla bona', seria: 'seria', male: 'a fassi male' }[c.level], 8, 16, 7, '#d9ccba');
|
||||
text(ctx, `Punti ${c.points} / ${c.skill.goal}`, 260, 11, 9, c.points >= c.skill.goal ? '#b9e5b3' : '#fff0c8', 'center');
|
||||
for (let i = 0; i < PELLETS; i++) pellet(ctx, 430 + i * 15, 11, i < c.left ? 3 : 1.2);
|
||||
const line = overHeld(c) ? 'Lascia, sennò tossisci!' : c.line;
|
||||
if (line) { ctx.fillStyle = '#17160fd0'; ctx.fillRect(170, 24, 300, 14); text(ctx, line, 320, 31, 8, overHeld(c) || /corto|alto|Coff/.test(line) ? '#ffbf9f' : '#9fd0ff', 'center'); }
|
||||
if (c.step === 'result') {
|
||||
const won = c.result.winner === 'player';
|
||||
panel(ctx, 170, 140, 300, 76);
|
||||
text(ctx, won ? 'CENTRATO!' : 'LO STUCCO È FINITO', 320, 160, 12, won ? '#b9e5b3' : '#efd49e', 'center');
|
||||
text(ctx, `${c.result.points} punti, ne servivano ${c.result.goal}`, 320, 182, 8, '#efd49e', 'center');
|
||||
text(ctx, 'Torna in piazza per uscire.', 320, 200, 8, '#d9ccba', 'center');
|
||||
}
|
||||
ctx.restore();
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
// La cerbottana (idea di Andrea, 29 ottobre): da ragazzi in piazza si comprava da Pierin di Frilli lo stucco verde,
|
||||
// dall'odore inconfondibile, lo si metteva intorno alla cerbottana, si facevano i pallini e si «sparava» al muro.
|
||||
// Qui: un muro della piazza visto di fronte, un bersaglio fatto col gesso, le lattine sul muretto e (a seria e a fassi
|
||||
// male) un barattolo appeso che dondola. Si mira, si tiene premuto per prendere fiato e si lascia: con poco fiato il
|
||||
// pallino cade sotto, con troppo va sopra; il mirino ondeggia col respiro, e chi resta troppo a fiato pieno tossisce.
|
||||
// I pallini di stucco restano attaccati al muro. Unità: i pixel del primo piano 640×360.
|
||||
|
||||
export const WALL_BOTTOM = 244; // sotto comincia il muretto: lì il pallino non attacca
|
||||
export const LEDGE = 244; // dove poggiano le lattine
|
||||
export const PELLETS = 12, SWING = 1, TAP = 0.14, COUGH = 0.6, FLIGHT = 0.3, REST = 0.5, BEST = 0.7, DROP = 300;
|
||||
export const TARGET = { x: 320, y: 118, rings: [[8, 10], [18, 5], [30, 2]] };
|
||||
export const CAN = { w: 10, h: 16, points: 3 }, SWINGER = { x: 470, y: 8, length: 92, w: 12, h: 16, points: 8 };
|
||||
// I livelli: quanto ondeggia il mirino (px), quanti punti servono, se c'è il barattolo appeso, se la barra del fiato
|
||||
// mostra il punto giusto, e quanto sono grandi i cerchi del bersaglio.
|
||||
export const LEVELS = {
|
||||
bona: { sway: 3, goal: 30, swinger: false, sweet: true, rings: 1.3 },
|
||||
seria: { sway: 6, goal: 40, swinger: true, sweet: false, rings: 1 },
|
||||
male: { sway: 10, goal: 50, swinger: true, sweet: false, rings: 0.85 },
|
||||
};
|
||||
const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
|
||||
function random(c) { c.seed = (Math.imul(c.seed, 1664525) + 1013904223) >>> 0; return c.seed / 4294967296; }
|
||||
|
||||
export function newCerbottana(seed = Date.now() >>> 0, def = {}) {
|
||||
const level = LEVELS[def.level] ? def.level : 'seria';
|
||||
return { step: 'aim', t: 0, seed, level, skill: LEVELS[level], aim: { x: TARGET.x, y: TARGET.y }, power: BEST,
|
||||
holding: false, holdTime: 0, charging: false, left: PELLETS, points: 0, events: [], shot: null, stuck: [], line: null,
|
||||
cans: [200, 250, 390, 440].map(x => ({ x, down: false, fall: 0 })), swing: { hits: 0, spin: 0 }, visual: { time: 0 } };
|
||||
}
|
||||
export const canThrow = c => c.step === 'aim' && c.left > 0;
|
||||
export const canAdjust = canThrow;
|
||||
export const powerAt = time => clamp(time / SWING, 0, 1);
|
||||
export const rings = c => TARGET.rings.map(([r, p]) => [r * c.skill.rings, p]);
|
||||
|
||||
// Il respiro: il mirino gira piano intorno al punto voluto (due onde, non si ripete subito); trattenendo il fiato pieno
|
||||
// trema di più.
|
||||
export function sway(c) {
|
||||
const t = c.visual.time, a = c.skill.sway * (1 + (c.overHold ?? 0) * 2);
|
||||
return { x: a * (Math.sin(t * 1.7) + 0.5 * Math.sin(t * 3.1 + 1)), y: a * 0.8 * (Math.sin(t * 1.3 + 2) + 0.5 * Math.sin(t * 2.9)) };
|
||||
}
|
||||
export function reticle(c) { const s = sway(c); return { x: c.aim.x + s.x, y: c.aim.y + s.y }; }
|
||||
// Dove arriva il pallino: dove punta il mirino, più su o più giù secondo il fiato (BEST è il fiato giusto).
|
||||
export const landing = (c, power = c.power) => { const r = reticle(c); return { x: r.x, y: r.y + (BEST - power) * DROP }; };
|
||||
// Il barattolo appeso: un pendolo che dondola sotto la gronda (e gira su sé stesso se colpito).
|
||||
export function swinger(c) {
|
||||
const a = 0.5 * Math.sin(c.visual.time * 1.6);
|
||||
return { x: SWINGER.x + Math.sin(a) * SWINGER.length, y: SWINGER.y + Math.cos(a) * SWINGER.length, a };
|
||||
}
|
||||
|
||||
export function shoot(c) {
|
||||
if (!canThrow(c) || c.holding) return false;
|
||||
return fire(c, c.power);
|
||||
}
|
||||
function fire(c, power, cough = false) {
|
||||
const from = { x: 320, y: 380 }, to = cough ? { x: 300 + random(c) * 40, y: 330 } : landing(c, power);
|
||||
c.left--; c.power = power;
|
||||
c.shot = { from, to, t: 0, cough };
|
||||
c.step = 'flying'; c.line = null;
|
||||
c.events.push({ kind: cough ? 'cerbottana-tosse' : 'cerbottana-pff', power });
|
||||
return true;
|
||||
}
|
||||
// Arrivato: prima il barattolo appeso, poi le lattine, poi il bersaglio; sul muro il pallino resta attaccato.
|
||||
function land(c) {
|
||||
const { x, y } = c.shot.to;
|
||||
let got = 0, what = null;
|
||||
if (c.shot.cough) what = 'Coff! Il pallino è finito per terra.';
|
||||
if (!what && c.skill.swinger) {
|
||||
const s = swinger(c);
|
||||
if (Math.abs(x - s.x) <= SWINGER.w / 2 + 1 && Math.abs(y - s.y - SWINGER.h / 2) <= SWINGER.h / 2 + 1) {
|
||||
got = SWINGER.points; c.swing.hits++; c.swing.spin = 1; what = `Il barattolo appeso! ${got} punti.`;
|
||||
c.events.push({ kind: 'cerbottana-latta', power: 1 });
|
||||
}
|
||||
}
|
||||
if (!what) {
|
||||
const can = c.cans.find(k => !k.down && Math.abs(x - k.x) <= CAN.w / 2 + 1 && y >= LEDGE - CAN.h - 1 && y <= LEDGE);
|
||||
if (can) { can.down = true; got = CAN.points; what = `Lattina giù! ${got} punti.`; c.events.push({ kind: 'cerbottana-latta', power: 0.8 }); }
|
||||
}
|
||||
if (!what) {
|
||||
if (y < WALL_BOTTOM && y > 4 && x > 4 && x < 636) {
|
||||
c.stuck.push({ x, y });
|
||||
const d = Math.hypot(x - TARGET.x, y - TARGET.y), ring = rings(c).find(([r]) => d <= r);
|
||||
got = ring ? ring[1] : 0;
|
||||
what = got ? `Nel bersaglio: ${got} ${got === 1 ? 'punto' : 'punti'}.` : y < TARGET.y - 40 ? 'Troppo fiato: alto.' : 'Sul muro, fuori dal bersaglio.';
|
||||
c.events.push({ kind: 'cerbottana-muro', power: 0.6 });
|
||||
} else what = 'Poco fiato: il pallino è caduto corto.';
|
||||
}
|
||||
c.points += got; c.line = what;
|
||||
c.shot = null; c.step = 'rest'; c.t = 0;
|
||||
}
|
||||
|
||||
export function update(c, dt) {
|
||||
if (!Number.isFinite(dt) || dt <= 0) return;
|
||||
dt = Math.min(dt, 0.25); c.t += dt; c.visual.time += dt;
|
||||
for (const k of c.cans) if (k.down) k.fall = Math.min(1, k.fall + dt * 3);
|
||||
if (c.swing.spin) c.swing.spin = Math.max(0, c.swing.spin - dt);
|
||||
if (c.holding) {
|
||||
c.holdTime += dt; c.charging = c.holdTime >= TAP;
|
||||
c.power = powerAt(c.holdTime);
|
||||
c.overHold = c.power >= 1 ? (c.overHold ?? 0) + dt : 0;
|
||||
// Troppo a fiato pieno: tosse, e il pallino è perso.
|
||||
if (c.overHold > COUGH) { c.holding = c.charging = false; c.overHold = 0; fire(c, 1, true); }
|
||||
}
|
||||
if (c.step === 'flying') { c.shot.t += dt; if (c.shot.t >= FLIGHT) land(c); }
|
||||
else if (c.step === 'rest' && c.t >= REST) {
|
||||
if (c.left > 0) c.step = 'aim';
|
||||
else { c.step = 'result'; c.result = { winner: c.points >= c.skill.goal ? 'player' : 'cpu', points: c.points, goal: c.skill.goal }; }
|
||||
}
|
||||
}
|
||||
|
||||
// --- Comandi: il mirino segue il mouse o il dito; tenere premuto prende fiato, lasciare spara.
|
||||
export function aimScreen(c, [x, y]) {
|
||||
if (!canThrow(c) || c.holding || !Number.isFinite(x) || !Number.isFinite(y)) return false;
|
||||
c.aim = { x: clamp(x, 10, 630), y: clamp(y, 24, 300) }; return true;
|
||||
}
|
||||
export function nudge(c, dir) { if (!canThrow(c) || c.holding) return false; c.aim.x = clamp(c.aim.x + dir * 4, 10, 630); return true; }
|
||||
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 key(c, key, down) {
|
||||
const move = { ArrowLeft: [-4, 0], ArrowRight: [4, 0], ArrowUp: [0, -4], ArrowDown: [0, 4] }[key];
|
||||
if (!move) return false;
|
||||
if (down && canThrow(c) && !c.holding) c.aim = { x: clamp(c.aim.x + move[0], 10, 630), y: clamp(c.aim.y + move[1], 24, 300) };
|
||||
return true;
|
||||
}
|
||||
export function press(c, pointerId) {
|
||||
if (!canThrow(c) || c.holding) return false;
|
||||
c.pointerId = pointerId; c.holding = true; c.charging = false; c.holdTime = 0; c.previousPower = c.power; c.power = 0; c.overHold = 0;
|
||||
return true;
|
||||
}
|
||||
export function cancel(c, pointerId) {
|
||||
if (pointerId !== undefined && c.pointerId !== pointerId) return false;
|
||||
if (!c.holding) return false;
|
||||
c.holding = c.charging = false; c.power = c.previousPower; c.holdTime = 0; c.overHold = 0; return true;
|
||||
}
|
||||
export function release(c, pointerId) {
|
||||
if (c.pointerId !== pointerId || !c.holding) return false;
|
||||
if (c.holdTime < TAP) return cancel(c, pointerId); // il tocco secco mira soltanto
|
||||
c.holding = c.charging = false; c.overHold = 0;
|
||||
return fire(c, c.power);
|
||||
}
|
||||
// Trattenendo il fiato pieno: il disegno avvisa prima della tosse.
|
||||
export const overHeld = c => c.holding && (c.overHold ?? 0) > 0;
|
||||
|
||||
export function button(c) { return c.step === 'result' ? 'Finita' : c.step === 'aim' ? 'Spara' : 'In volo…'; }
|
||||
export function status(c) {
|
||||
const score = `Punti ${c.points} su ${c.skill.goal}, pallini ${c.left} di ${PELLETS}.`;
|
||||
if (c.step === 'result') return `${c.result.winner === 'player' ? 'Fatto! ' : 'Lo stucco è finito. '}${score}`;
|
||||
return `${score} ${c.line ?? 'Mira, tieni premuto per prendere fiato e lascia.'}`;
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import * as tressette from './tressette.js';
|
||||
import * as scopa from './scopa.js';
|
||||
import * as gara from './gara.js';
|
||||
import * as micro from './micro.js';
|
||||
import * as cerbottana from './cerbottana.js';
|
||||
|
||||
// I minigiochi in primo piano: comandi opzionali per il tiro o per la scelta di una carta.
|
||||
export const MINIGAMES = {
|
||||
@@ -22,6 +23,7 @@ export const MINIGAMES = {
|
||||
rinquarto: { create: (seed, level) => rinquarto.newRinquarto(seed, { level }), update: rinquarto.update, press: rinquarto.press, release: rinquarto.release, cancel: rinquarto.cancel, aim: rinquarto.aimScreen },
|
||||
basket: { create: (seed, level) => basket.newBasket(seed, { level }), update: basket.update, press: basket.press, release: basket.release, cancel: basket.cancel, aim: basket.aimScreen },
|
||||
pesca: { create: (seed, level) => pesca.newPesca(seed, { level }), update: pesca.update, press: pesca.press, release: pesca.release, cancel: pesca.cancel, aim: pesca.aimScreen },
|
||||
cerbottana: { create: (seed, level) => cerbottana.newCerbottana(seed, { level }), update: cerbottana.update, press: cerbottana.press, release: cerbottana.release, cancel: cerbottana.cancel, aim: cerbottana.aimScreen },
|
||||
micro: { create: (seed, level) => micro.newMicro(seed, { level }), update: micro.update, press: micro.press, release: micro.release, cancel: micro.cancel, aim: micro.aimScreen },
|
||||
gara: { create: (seed, level, deck, def) => gara.newGara(seed, { level, riders: def.riders }), update: gara.update, press: gara.press, release: gara.release, cancel: gara.cancel, aim: gara.aimScreen },
|
||||
};
|
||||
|
||||
@@ -10,6 +10,7 @@ import { drawBasket } from './basket-render.js';
|
||||
import { drawPesca } from './pesca-render.js';
|
||||
import { drawGara } from './gara-render.js';
|
||||
import { drawMicro } from './micro-render.js';
|
||||
import { drawCerbottana } from './cerbottana-render.js';
|
||||
import { drawBriscola } from './briscola-render.js';
|
||||
import { drawTressette } from './tressette-render.js';
|
||||
import { drawScopa } from './scopa-render.js';
|
||||
@@ -687,6 +688,11 @@ function drawCloseup(ctx, game, time) {
|
||||
drawBocce(ctx, c, def, backgrounds.get(def.picture), time);
|
||||
return;
|
||||
}
|
||||
if (def.type === 'cerbottana') {
|
||||
if (!backgrounds.has(def.picture)) loadBackground(def.picture);
|
||||
drawCerbottana(ctx, c, def, backgrounds.get(def.picture));
|
||||
return;
|
||||
}
|
||||
if (def.type === 'micro') {
|
||||
if (!backgrounds.has(def.picture)) loadBackground(def.picture);
|
||||
drawMicro(ctx, c, def, backgrounds.get(def.picture), src => { if (!backgrounds.has(src)) loadBackground(src); return backgrounds.get(src); });
|
||||
|
||||
@@ -136,6 +136,11 @@ export class Soundscape {
|
||||
else if (kind === 'gara-sobbalzo') this.tone(160, 90, 0.1, 0.05, 0, pan);
|
||||
else if (kind === 'gara-giro') this.tone(660, 990, 0.15, 0.05, 0, pan);
|
||||
else if (kind === 'gara-arrivo') { this.tone(660, 660, 0.12, 0.06 * strength, 0, pan); this.tone(990, 990, 0.3, 0.06 * strength, 0.14, pan); }
|
||||
// La cerbottana: il soffio, il pallino che si schiaccia sul muro, la lattina, la tosse.
|
||||
else if (kind === 'cerbottana-pff') this.noiseSound(0.12, 2200, 0.05 + strength * 0.05, { pan });
|
||||
else if (kind === 'cerbottana-muro') { this.tone(180, 90, 0.06, 0.05, 0, pan); this.noiseSound(0.05, 600, 0.04, { pan }); }
|
||||
else if (kind === 'cerbottana-latta') { this.tone(2400, 1600, 0.08, 0.05 + strength * 0.04, 0, pan); this.tone(3400, 2600, 0.05, 0.03, 0.03, pan); }
|
||||
else if (kind === 'cerbottana-tosse') { this.noiseSound(0.18, 900, 0.08, { pan }); this.tone(260, 140, 0.12, 0.06, 0.25, pan); this.tone(240, 120, 0.1, 0.05, 0.3, pan); }
|
||||
// Le macchinine: il dito che spinge, le ruote sull'asfalto, l'urto di plastica, fuori pista, il traguardo.
|
||||
else if (kind === 'micro-spinta') { this.tone(1200, 500, 0.04, 0.04 + strength * 0.04, 0, pan); this.noiseSound(0.04, 2600, 0.04, { pan }); }
|
||||
else if (kind === 'micro-strisciata') this.noiseSound(0.3 + strength * 0.7, 900, 0.03 + strength * 0.04, { pan });
|
||||
|
||||
+8
-5
@@ -12,6 +12,7 @@ import * as bocce from './engine/bocce.js';
|
||||
import * as biliardo from './engine/biliardo.js';
|
||||
import * as rinquarto from './engine/rinquarto.js';
|
||||
import * as micro from './engine/micro.js';
|
||||
import * as cerbottana from './engine/cerbottana.js';
|
||||
import * as basket from './engine/basket.js';
|
||||
import * as pesca from './engine/pesca.js';
|
||||
import * as gara from './engine/gara.js';
|
||||
@@ -510,7 +511,7 @@ canvas.addEventListener('pointerdown', e => {
|
||||
}
|
||||
if (press?.closeup) return;
|
||||
const aimed = game.closeupAim(scenePoint(e));
|
||||
if (['biliardo', 'rinquarto', 'basket', 'micro'].includes(story.closeups[game.closeup.id].type)) {
|
||||
if (['biliardo', 'rinquarto', 'basket', 'micro', 'cerbottana'].includes(story.closeups[game.closeup.id].type)) {
|
||||
if (!aimed && !game.speech) return; // dove non si può mirare (dietro Andrea) non si carica nemmeno
|
||||
if (aimed) game.closeup.aimLocked = true; // il clic fissa la mira
|
||||
}
|
||||
@@ -582,7 +583,7 @@ canvas.addEventListener('pointermove', e => {
|
||||
if (press?.gara === e.pointerId) gara.aimScreen(game.closeup, scenePoint(e), e.pointerId);
|
||||
return;
|
||||
}
|
||||
const aiming = game.closeup && ['biliardo', 'rinquarto', 'bocce', 'basket', 'pesca', 'micro'].includes(story.closeups[game.closeup.id].type);
|
||||
const aiming = game.closeup && ['biliardo', 'rinquarto', 'bocce', 'basket', 'pesca', 'micro', 'cerbottana'].includes(story.closeups[game.closeup.id].type);
|
||||
if (aiming) { if (e.pointerType === 'mouse' && !press && !game.closeup.aimLocked) game.closeupAim(scenePoint(e)); }
|
||||
else if (e.pointerType === 'mouse' || press?.closeup) game.closeupAim(scenePoint(e));
|
||||
});
|
||||
@@ -603,6 +604,7 @@ const MINI = {
|
||||
bocce: { ...bocce, nudge: (c, dir) => bocce.aim(c, c.aim.u, c.aim.w + dir * 0.3), back: 'Torna al pallaio', label: 'Bocce: tocca il campo o il dettaglio per mirare. Regola la forza e premi Lancia.' },
|
||||
biliardo: { ...biliardo, back: 'Torna al bar', label: 'Biliardo: clic o tocco fissa la mira. Frecce per affinarla, Maiusc per passi più piccoli. Regola la forza e premi Tira.' },
|
||||
rinquarto: { ...rinquarto, back: 'Torna da Trappizzio Berti', label: 'I rinquarti di Trappizzio Berti: clic o tocco fissa la mira, frecce o pulsanti Mira per girare la stecca (Maiusc per i passi fini), Tira per tirare. Dopo l’esito, Prossima.' },
|
||||
cerbottana: { ...cerbottana, back: 'Torna in piazza', label: 'La cerbottana: muovi il mouse o tocca il muro per mirare, tieni premuto per prendere fiato e lascia per sparare; oppure regola il fiato e premi Spara. Frecce per spostare il mirino.' },
|
||||
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.' },
|
||||
// La gara: niente mira né forza, il pulsante alterna gas e freno (chi non può tenere premuto).
|
||||
@@ -1013,7 +1015,7 @@ function updateUi() {
|
||||
}
|
||||
}
|
||||
if (mg && !cards) {
|
||||
setAttr($('bocce-controls'), 'aria-label', { rinquarto: 'Comandi dei rinquarti', biliardo: 'Comandi del biliardo', basket: 'Comandi del basket', pesca: 'Comandi della pesca', gara: 'Comandi della gara', micro: 'Comandi delle macchinine' }[def.type] ?? 'Comandi delle bocce');
|
||||
setAttr($('bocce-controls'), 'aria-label', { rinquarto: 'Comandi dei rinquarti', biliardo: 'Comandi del biliardo', basket: 'Comandi del basket', pesca: 'Comandi della pesca', gara: 'Comandi della gara', micro: 'Comandi delle macchinine', cerbottana: 'Comandi della cerbottana' }[def.type] ?? 'Comandi delle bocce');
|
||||
setProp($('panel'), 'hidden', true); setProp($('choices'), 'hidden', true);
|
||||
const line = mg.status(c, def.names);
|
||||
if ($('bocce-status').textContent !== line) $('bocce-status').textContent = line;
|
||||
@@ -1029,7 +1031,7 @@ function updateUi() {
|
||||
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à ' : def.type === 'cerbottana' ? 'Fiato ' : 'Forza ';
|
||||
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');
|
||||
setProp($('gara-pad'), 'hidden', def.type !== 'gara' || easy);
|
||||
@@ -1047,6 +1049,7 @@ function updateUi() {
|
||||
setProp($('biliardo-help'), 'hidden', def.type !== 'biliardo');
|
||||
setProp($('rinquarto-help'), 'hidden', def.type !== 'rinquarto');
|
||||
setProp($('micro-help'), 'hidden', def.type !== 'micro');
|
||||
setProp($('cerbottana-help'), 'hidden', def.type !== 'cerbottana');
|
||||
setProp($('basket-help'), 'hidden', def.type !== 'basket');
|
||||
setProp($('pesca-help'), 'hidden', def.type !== 'pesca');
|
||||
setProp($('gara-help'), 'hidden', def.type !== 'gara');
|
||||
@@ -1064,7 +1067,7 @@ function updateUi() {
|
||||
canvas.classList.toggle('torch', !!torch);
|
||||
canvas.classList.toggle('bocce', !!mg);
|
||||
// Biliardo e GP di Fortezza: audio, pausa e uscita fuori dal quadro (al GP la pista passa negli angoli).
|
||||
$('stage').classList.toggle('biliardo', !!c && ['biliardo', 'rinquarto', 'gara', 'micro'].includes(story.closeups[c.id].type));
|
||||
$('stage').classList.toggle('biliardo', !!c && ['biliardo', 'rinquarto', 'gara', 'micro', 'cerbottana'].includes(story.closeups[c.id].type));
|
||||
$('stage').classList.toggle('briscola', !!cards);
|
||||
setAttr(canvas, 'aria-label', torch ? 'Fessura: sposta la luce con mouse, dito o frecce. Invio cerca il prossimo dettaglio; Esc esce.' : mg ? mg.label : "La scena dell'avventura");
|
||||
// La pagina che comincia a girare fa il suo fruscio.
|
||||
|
||||
@@ -233,6 +233,17 @@ export const story = {
|
||||
quit: [{ say: 'Questo me lo tengo per la prossima.' }],
|
||||
draw: [{ say: 'Visto? Geometrie celestiali. Si riprova quando vuoi.', who: 'bertovo', pose: 'explain' }],
|
||||
},
|
||||
// La cerbottana con lo stucco verde di Pierin di Frilli, contro il muro della piazza (Andrea, 29 ottobre).
|
||||
// [DA ANDREA: le battute sono provvisorie]
|
||||
cerbottana: {
|
||||
type: 'cerbottana',
|
||||
picture: 'assets/closeups/muro-cerbottana.png',
|
||||
names: { player: 'Andrea' },
|
||||
win: [{ say: 'Tutti sul muro, verdi in fila. Lo stucco di Pierino un tradisce mai.' }, { set: 'cerbottana_vinta' }],
|
||||
lose: [{ say: 'Finito lo stucco. Domani si ripassa da Pierino.' }],
|
||||
quit: [{ say: 'Il muro resta lì. E l\'odore dello stucco, sulle dita, pure.' }],
|
||||
draw: [],
|
||||
},
|
||||
// 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: {
|
||||
@@ -505,6 +516,17 @@ export const story = {
|
||||
look: [{ say: 'La strada che porta al Fiocco di Neve. Già sento il profumo di cialda.' }],
|
||||
},
|
||||
},
|
||||
{
|
||||
// La bottega di Pierin di Frilli sotto la tenda rossa: da ragazzi ci si comprava lo stucco verde per la
|
||||
// cerbottana (Andrea, 29 ottobre). [DA ANDREA: com'era dentro, e cosa diceva Pierino]
|
||||
id: 'pierin', name: 'la bottega di Pierin di Frilli', box: [598, 176, 42, 92], at: [612, 306], face: 'right', default: 'talk',
|
||||
actions: {
|
||||
look: [{ say: 'La bottega di Pierin di Frilli, sotto la tenda rossa. Da ragazzi ci si comprava lo stucco per la cerbottana.' }],
|
||||
talk: [{ dialogue: 'pierin' }],
|
||||
open: [{ dialogue: 'pierin' }],
|
||||
use: [{ dialogue: 'pierin' }],
|
||||
},
|
||||
},
|
||||
{
|
||||
// Il Circolo 4 Maggio: il portone ad arco del palazzo arancione porta nella sala dei biliardi.
|
||||
id: 'circolo', name: 'Circolo 4 Maggio', box: [188, 190, 28, 58], at: [202, 252], face: 'up',
|
||||
@@ -3311,6 +3333,28 @@ export const story = {
|
||||
},
|
||||
},
|
||||
},
|
||||
// Da Pierin di Frilli: la cerbottana e lo stucco verde, poi il muro della piazza (Andrea, 29 ottobre).
|
||||
pierin: {
|
||||
nodes: {
|
||||
start: {
|
||||
options: [
|
||||
{ text: 'Una cerbottana e un po\' di stucco, Pierino!', next: 'livello', then: [
|
||||
{ say: 'Lo stucco verde di Pierino: quell\'odore non te lo levi più dalle dita.' },
|
||||
{ say: 'Ora un muro. Quello della piazza va benissimo.' },
|
||||
] },
|
||||
{ text: 'Niente, grazie: guardavo.', next: 'end' },
|
||||
],
|
||||
},
|
||||
livello: {
|
||||
options: [
|
||||
{ text: 'Alla bona.', next: 'end', then: [{ closeup: 'cerbottana', level: 'bona' }] },
|
||||
{ text: 'Seria.', next: 'end', then: [{ closeup: 'cerbottana', level: 'seria' }] },
|
||||
{ text: 'A fassi male.', next: 'end', then: [{ closeup: 'cerbottana', level: 'male' }] },
|
||||
{ text: 'No, dopo.', next: 'end' },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
// Il Chiaru al parcheggio di Cardetole, accanto alla pista col gesso (Andrea, 29 ottobre). [DA ANDREA: battute provvisorie]
|
||||
chiaru: {
|
||||
nodes: {
|
||||
|
||||
@@ -62,16 +62,16 @@ body { display: flex; justify-content: center; overflow-x: hidden; -webkit-tap-h
|
||||
/* 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-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 p, #biliardo-help p, #basket-help p, #pesca-help p, #gara-help p, #rinquarto-help p { max-width: 76em; line-height: 1.7; }
|
||||
#bocce-help, #biliardo-help, #basket-help, #pesca-help, #gara-help, #rinquarto-help, #micro-help, #cerbottana-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, #micro-help summary, #cerbottana-help summary { cursor: pointer; padding: 6px 0; }
|
||||
#bocce-help p, #biliardo-help p, #basket-help p, #pesca-help p, #gara-help p, #rinquarto-help p, #micro-help p, #cerbottana-help p { max-width: 76em; line-height: 1.7; }
|
||||
@media (max-width: 600px), (orientation: landscape) and (max-height: 520px) {
|
||||
#bocce-controls { padding: 8px; font-size: 10px; overflow-y: auto; min-height: 0; }
|
||||
.bocce-adjust { flex-wrap: wrap; gap: 6px; }
|
||||
.bocce-adjust label { order: -1; flex-basis: 100%; max-width: none; }
|
||||
.bocce-adjust button { flex: 1; font-size: 9px; padding: 6px; }
|
||||
#bocce-throw { flex-basis: 100%; }
|
||||
#bocce-help, #biliardo-help, #basket-help, #pesca-help, #gara-help, #rinquarto-help { font-size: 9px; }
|
||||
#bocce-help, #biliardo-help, #basket-help, #pesca-help, #gara-help, #rinquarto-help, #micro-help, #cerbottana-help { font-size: 9px; }
|
||||
}
|
||||
.effects-control { display: flex; align-items: center; justify-content: center; flex-wrap: wrap; gap: 10px; margin-bottom: 12px; font-size: clamp(9px, 1.2vw, 13px); }
|
||||
#effects-volume { width: 130px; accent-color: var(--accent); }
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
// La cerbottana nel browser vero: dalla bottega di Pierin di Frilli al muro della piazza; mira, fiato e tiro col mouse
|
||||
// e col dito. npm run browser:cerbottana — schermate in /tmp/regolo-cerbottana.
|
||||
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-cerbottana';
|
||||
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] of [['desktop', false, { width: 1280, height: 860 }], ['touch', true, { width: 844, height: 390 }]]) {
|
||||
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 () => {
|
||||
const { settle } = await import('/tests/helpers.mjs');
|
||||
settle(game); game.go('piazza'); settle(game);
|
||||
game.act('talk', 'pierin'); settle(game);
|
||||
game.choose(game.options().findIndex(o => o.text.startsWith('Una cerbottana'))); settle(game);
|
||||
game.choose(game.options().findIndex(o => o.text === 'Seria.')); settle(game);
|
||||
});
|
||||
await page.waitForSelector('#bocce-controls:not([hidden])');
|
||||
await page.waitForFunction(async () => (await import('/src/engine/render.js')).hasBackground('assets/closeups/muro-cerbottana.png'));
|
||||
assert.equal(await page.locator('#bocce-throw').textContent(), 'Spara');
|
||||
assert.ok(await page.locator('#cerbottana-help').isVisible());
|
||||
await page.waitForTimeout(1600);
|
||||
const box = await page.locator('#scene').boundingBox(), P = (x, y) => [box.x + x / 640 * box.width, box.y + y / 360 * box.height];
|
||||
if (mobile) await page.touchscreen.tap(...P(320, 118)); else { await page.mouse.move(...P(320, 118)); await page.mouse.click(...P(320, 118)); }
|
||||
assert.equal(await page.evaluate(() => game.closeup.step), 'aim', 'un tocco mira soltanto');
|
||||
await page.screenshot({ path: `${out}/${name}-mira.png` });
|
||||
if (mobile) { await page.locator('#bocce-power').fill('70'); await page.locator('#bocce-throw').tap(); }
|
||||
else { await page.mouse.down(); await page.waitForFunction(() => game.closeup.power >= 0.6); await page.mouse.up(); }
|
||||
await page.waitForFunction(() => game.closeup.left === 11 && game.closeup.step === 'aim', null, { timeout: 10000 });
|
||||
await page.screenshot({ path: `${out}/${name}-dopo.png` });
|
||||
await page.close();
|
||||
}
|
||||
assert.deepEqual(errors, []);
|
||||
console.log(`ok: schermate in ${out}`);
|
||||
} finally { await browser?.close(); server.kill(); }
|
||||
@@ -0,0 +1,82 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import * as m from '../src/engine/cerbottana.js';
|
||||
import { drawCerbottana } from '../src/engine/cerbottana-render.js';
|
||||
import { Game, MINIGAMES } from '../src/engine/game.js';
|
||||
import { story } from '../src/story/story.js';
|
||||
import { settle } from './helpers.mjs';
|
||||
|
||||
// Ferma il respiro (il mirino va dove si punta), spara col fiato dato e aspetta che il pallino arrivi.
|
||||
function shot(c, x, y, power = m.BEST) {
|
||||
c.skill = { ...c.skill, sway: 0 }; c.aim = { x, y };
|
||||
assert.ok(m.setPower(c, power)); assert.ok(m.shoot(c));
|
||||
for (let i = 0; i < 200 && c.step === 'flying'; i++) m.update(c, 1 / 60);
|
||||
return c.line;
|
||||
}
|
||||
|
||||
test('col fiato giusto il pallino va dove si punta; poco fiato cade sotto, troppo va sopra; resta attaccato al muro', () => {
|
||||
const c = m.newCerbottana(1, { level: 'seria' });
|
||||
assert.match(shot(c, m.TARGET.x, m.TARGET.y), /10 punti/);
|
||||
assert.equal(c.points, 10); assert.deepEqual(c.stuck.at(-1), { x: m.TARGET.x, y: m.TARGET.y });
|
||||
for (let i = 0; i < 60; i++) m.update(c, 1 / 60);
|
||||
assert.ok(m.landing(c, 0.5).y > m.TARGET.y + 40, 'poco fiato: sotto');
|
||||
assert.ok(m.landing(c, 0.9).y < m.TARGET.y - 40, 'troppo fiato: sopra');
|
||||
shot(c, 320, 200, 0.2);
|
||||
assert.match(c.line, /corto/); assert.equal(c.stuck.length, 1, 'il pallino corto non resta sul muro');
|
||||
});
|
||||
|
||||
test('le lattine cadono, il barattolo appeso c\'è solo a seria e a fassi male; finito lo stucco si guarda il punteggio', () => {
|
||||
const c = m.newCerbottana(2, { level: 'bona' });
|
||||
const k = c.cans[0];
|
||||
assert.match(shot(c, k.x, m.LEDGE - 8), /Lattina giù/);
|
||||
assert.ok(k.down); assert.equal(c.points, m.CAN.points);
|
||||
assert.equal(m.LEVELS.bona.swinger, false); assert.equal(m.LEVELS.seria.swinger, true);
|
||||
// Il barattolo dondola mentre il pallino vola: si mira dove sarà all'arrivo.
|
||||
const s = m.newCerbottana(3, { level: 'seria' }), p = m.swinger({ visual: { time: s.visual.time + m.FLIGHT + 1 / 120 } });
|
||||
assert.match(shot(s, p.x, p.y + m.SWINGER.h / 2), /barattolo appeso/);
|
||||
for (let i = 0; i < 60; i++) m.update(c, 1 / 60);
|
||||
while (c.left > 0) { for (let i = 0; i < 60 && !m.canThrow(c); i++) m.update(c, 1 / 60); shot(c, m.TARGET.x, m.TARGET.y); }
|
||||
for (let i = 0; i < 60; i++) m.update(c, 1 / 60);
|
||||
assert.equal(c.step, 'result'); assert.equal(c.result.winner, 'player');
|
||||
assert.equal(c.stuck.length, m.PELLETS - 1);
|
||||
});
|
||||
|
||||
test('i comandi: un tocco mira, tenere premuto prende fiato da zero, lasciare spara; troppo a fiato pieno si tossisce', () => {
|
||||
const c = m.newCerbottana(4);
|
||||
assert.ok(m.aimScreen(c, [200, 100])); assert.deepEqual(c.aim, { x: 200, y: 100 });
|
||||
assert.ok(m.press(c, 1)); m.update(c, 0.05); assert.equal(m.release(c, 1), true); assert.equal(c.step, 'aim', 'il tocco breve non spara');
|
||||
assert.ok(m.press(c, 1)); assert.equal(c.power, 0);
|
||||
for (let i = 0; i < 40; i++) m.update(c, 1 / 60);
|
||||
assert.ok(c.charging && c.power > 0.5 && c.power < 0.8);
|
||||
assert.ok(m.release(c, 1)); assert.equal(c.step, 'flying'); assert.equal(c.left, m.PELLETS - 1);
|
||||
const d = m.newCerbottana(5); m.press(d, 1);
|
||||
for (let i = 0; i < 60 * (m.SWING + m.COUGH + 0.2); i++) m.update(d, 1 / 60);
|
||||
assert.equal(d.holding, false); assert.ok(d.events.some(e => e.kind === 'cerbottana-tosse'));
|
||||
for (let i = 0; i < 60; i++) m.update(d, 1 / 60);
|
||||
assert.match(d.line, /Coff/); assert.equal(d.left, m.PELLETS - 1);
|
||||
assert.ok(m.key(d, 'ArrowUp', true)); assert.equal(d.aim.y, m.TARGET.y - 4);
|
||||
});
|
||||
|
||||
test('il disegno: muro, gesso, lattine, pallini; lo stato non cambia', () => {
|
||||
const calls = [], ctx = new Proxy({}, { get: (o, k) => k in o ? o[k] : (...args) => calls.push([k, ...args]) });
|
||||
const c = m.newCerbottana(6, { level: 'seria' }), bg = {};
|
||||
const draw = () => { const before = structuredClone(c); drawCerbottana(ctx, c, story.closeups.cerbottana, bg); assert.deepEqual(c, before); };
|
||||
draw(); assert.ok(calls.some(([op, img]) => op === 'drawImage' && img === bg));
|
||||
assert.ok(calls.some(([op, t]) => op === 'fillText' && t === 'LA CERBOTTANA'));
|
||||
c.step = 'result'; c.result = { winner: 'player', points: 44, goal: 40 }; calls.length = 0; draw();
|
||||
assert.ok(calls.some(([op, t]) => op === 'fillText' && t === 'CENTRATO!'));
|
||||
});
|
||||
|
||||
test('da Pierin di Frilli, in piazza: lo stucco, il livello e il muro', () => {
|
||||
assert.ok(MINIGAMES.cerbottana);
|
||||
const g = new Game(story); g.go('piazza'); settle(g);
|
||||
const h = g.hotspot('pierin');
|
||||
assert.equal(g.hotspotAt([h.box[0] + h.box[2] / 2, h.box[1] + h.box[3] / 2]).id, 'pierin');
|
||||
g.act('talk', 'pierin'); settle(g);
|
||||
g.choose(g.options().findIndex(o => o.text.startsWith('Una cerbottana'))); settle(g);
|
||||
g.choose(g.options().findIndex(o => o.text === 'A fassi male.')); settle(g);
|
||||
assert.equal(g.closeup?.id, 'cerbottana'); assert.equal(g.closeup.level, 'male');
|
||||
g.closeup.step = 'result'; g.closeup.result = { winner: 'player', points: 52, goal: 50 };
|
||||
g.closeupClose(); settle(g);
|
||||
assert.ok(g.state.flags.cerbottana_vinta);
|
||||
});
|
||||
Reference in New Issue
Block a user