Il flipper del River: la tavola di Codex con San Piero dipinto (Fortezza, Pieve, torre, la Sieve fra i flipper, il Regolo con la corona, le bandiere dei rioni) e sopra le parti meccaniche del gioco: molla, arco, corsie dei rioni (tutte e quattro: PALIO), funghi, bersagli a caduta, Regolo, sponde elastiche, due flipper; fisica a passo fisso, tre palline, tre livelli (Andrea)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GiECd9RWGMKV7n3K1mdy8T
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 208 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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -691,9 +691,13 @@
|
|||||||
"file": "andrea-b830f3f8ca.mp3",
|
"file": "andrea-b830f3f8ca.mp3",
|
||||||
"sec": 6.87
|
"sec": 6.87
|
||||||
},
|
},
|
||||||
"andrea|Un ghiacciolo ci starebbe. Ma prima il palio, poi i ghiaccioli.": {
|
"andrea|Un ghiacciolo ci starebbe. Ma prima... il flipper.": {
|
||||||
"file": "andrea-2669d0fea1.mp3",
|
"file": "andrea-737d8c7f64.mp3",
|
||||||
"sec": 5.23
|
"sec": 4.56
|
||||||
|
},
|
||||||
|
"andrea|Dentro, in fondo, fra le figurine: il flipper. Con San Piero dipinto sulla tavola.": {
|
||||||
|
"file": "andrea-61a0d27bbb.mp3",
|
||||||
|
"sec": 6.67
|
||||||
},
|
},
|
||||||
"andrea|La farmacia, con la croce verde. Mia nonna ci veniva per le pasticche e per le chiacchiere. Più per le chiacchiere.": {
|
"andrea|La farmacia, con la croce verde. Mia nonna ci veniva per le pasticche e per le chiacchiere. Più per le chiacchiere.": {
|
||||||
"file": "andrea-1447e3515d.mp3",
|
"file": "andrea-1447e3515d.mp3",
|
||||||
@@ -2967,6 +2971,30 @@
|
|||||||
"file": "arcu-26a5fb0d0c.mp3",
|
"file": "arcu-26a5fb0d0c.mp3",
|
||||||
"sec": 3.17
|
"sec": 3.17
|
||||||
},
|
},
|
||||||
|
"andrea|Una partita alla bona, tanto per sentire la molla.": {
|
||||||
|
"file": "andrea-ad00ae1145.mp3",
|
||||||
|
"sec": 3.38
|
||||||
|
},
|
||||||
|
"andrea|Una partita seria.": {
|
||||||
|
"file": "andrea-3479aca550.mp3",
|
||||||
|
"sec": 1.16
|
||||||
|
},
|
||||||
|
"andrea|Un'altra volta.": {
|
||||||
|
"file": "andrea-1b517ab1ef.mp3",
|
||||||
|
"sec": 0.9
|
||||||
|
},
|
||||||
|
"andrea|Seimila punti e sono contento.": {
|
||||||
|
"file": "andrea-b6d9563c93.mp3",
|
||||||
|
"sec": 2.02
|
||||||
|
},
|
||||||
|
"andrea|Dodicimila. Come quando ci lasciavo le cinquecento lire.": {
|
||||||
|
"file": "andrea-01a2eb256d.mp3",
|
||||||
|
"sec": 4.12
|
||||||
|
},
|
||||||
|
"andrea|Venticinquemila. Il record del River, o quasi.": {
|
||||||
|
"file": "andrea-e455fd8483.mp3",
|
||||||
|
"sec": 4.2
|
||||||
|
},
|
||||||
"andrea|Due tiri alla bona, tanto per scaldarmi.": {
|
"andrea|Due tiri alla bona, tanto per scaldarmi.": {
|
||||||
"file": "andrea-d564365fc2.mp3",
|
"file": "andrea-d564365fc2.mp3",
|
||||||
"sec": 2.53
|
"sec": 2.53
|
||||||
@@ -3683,6 +3711,18 @@
|
|||||||
"file": "andrea-356a1afe8d.mp3",
|
"file": "andrea-356a1afe8d.mp3",
|
||||||
"sec": 5.91
|
"sec": 5.91
|
||||||
},
|
},
|
||||||
|
"andrea|Traguardo! Se c'era la classifica, oggi ci mettevo le mie iniziali.": {
|
||||||
|
"file": "andrea-6fd4fcd345.mp3",
|
||||||
|
"sec": 4.64
|
||||||
|
},
|
||||||
|
"andrea|Tre palline, e tutte nella Sieve. Il flipper del River non perdona.": {
|
||||||
|
"file": "andrea-5c306f1589.mp3",
|
||||||
|
"sec": 5.57
|
||||||
|
},
|
||||||
|
"andrea|Lascio la pallina lì. Tanto il River non chiude.": {
|
||||||
|
"file": "andrea-dcbb849b12.mp3",
|
||||||
|
"sec": 4.02
|
||||||
|
},
|
||||||
"andrea|Bruciate coi fiocchi! Mezza Smarronata le mangia grazie a me.": {
|
"andrea|Bruciate coi fiocchi! Mezza Smarronata le mangia grazie a me.": {
|
||||||
"file": "andrea-145f20bff9.mp3",
|
"file": "andrea-145f20bff9.mp3",
|
||||||
"sec": 5.11
|
"sec": 5.11
|
||||||
|
|||||||
+1
-1
@@ -151,7 +151,7 @@ si segna qui con la data e il punto di `src/story/story.js` dove sta; le persone
|
|||||||
Al bar e al circolo:
|
Al bar e al circolo:
|
||||||
- *Calcio balilla al River*: due stecche sole (difesa e attacco), vista dall'alto come il biliardo, che ha già
|
- *Calcio balilla al River*: due stecche sole (difesa e attacco), vista dall'alto come il biliardo, che ha già
|
||||||
la fisica delle sponde. Il gioco da bar più sampierino che c'è; il River «coi flipper e le figurine» lo cita già.
|
la fisica delle sponde. Il gioco da bar più sampierino che c'è; il River «coi flipper e le figurine» lo cita già.
|
||||||
- *Flipper del River*: più lavoro, ma la fisica delle biglie c'è già.
|
- *Flipper del River*: fatto il 31 ottobre (vedi STATO.md).
|
||||||
- *Freccette al Circolo*: la mira vaga e si ferma al clic, tre freccette, si chiude il 301. Poco codice.
|
- *Freccette al Circolo*: la mira vaga e si ferma al clic, tre freccette, si chiude il 301. Poco codice.
|
||||||
- *Morra*: niente fisica, tutto dialogo e tempismo; molto toscana, le grida dei numeri in voce XTTS.
|
- *Morra*: niente fisica, tutto dialogo e tempismo; molto toscana, le grida dei numeri in voce XTTS.
|
||||||
Sport:
|
Sport:
|
||||||
|
|||||||
@@ -50,6 +50,15 @@ Online su 50037.manzolo.it tutto fino agli effetti ambientali di Codex. Aperto,
|
|||||||
fuoco; quando son cotte, «Rovescia nel sacco». Contano le buone su 18 (alla bona 10, seria 15, a fassi male 16);
|
fuoco; quando son cotte, «Rovescia nel sacco». Contano le buone su 18 (alla bona 10, seria 15, a fassi male 16);
|
||||||
vincendo, il flag `bruciate_fatte`. Test in `bruciate.test.mjs`, prova `npm run browser:bruciate`.
|
vincendo, il flag `bruciate_fatte`. Test in `bruciate.test.mjs`, prova `npm run browser:bruciate`.
|
||||||
[DA ANDREA] le battute dei volontari; un premio (un cartoccio da portare via?).
|
[DA ANDREA] le battute dei volontari; un premio (un cartoccio da portare via?).
|
||||||
|
- **Il flipper del River (31 ottobre)**: «Usa» o «Apri» il River in Via Provinciale, si sceglie il livello e si gioca
|
||||||
|
(`src/engine/flipper.js`, disegno `flipper-render.js`, tavola di Codex `assets/closeups/river-flipper.png` con San
|
||||||
|
Piero dipinto: Fortezza, Pieve, torre, la Sieve che scende fra i flipper, il ponte, il Regolo con la corona, le
|
||||||
|
bandiere dei rioni). Fisica a passo fisso: molla, arco, quattro corsie dei rioni (tutte e quattro = PALIO), tre
|
||||||
|
funghi, bersagli a caduta della Pieve e della torre, il Regolo, sponde elastiche, due flipper, uscite laterali,
|
||||||
|
salvataggio della pallina una volta sola. Comandi: molla tenendo premuto (o cursore + Lancia, o Spazio), flipper
|
||||||
|
con le due metà della scena, i pulsanti «← Flipper»/«Flipper →» tenuti premuti o le frecce. Tre palline; traguardo
|
||||||
|
6000/12000/25000. Test `flipper.test.mjs` (col giocatore automatico), prova `npm run browser:flipper`.
|
||||||
|
[DA ANDREA] le battute; cosa si vince (`flipper_vinto` per ora non serve a niente).
|
||||||
- **Dedica allo zio Mario (31 ottobre)**: nei Ringraziamenti, sotto quella al Lele e più piccola: «Al mio caro zio
|
- **Dedica allo zio Mario (31 ottobre)**: nei Ringraziamenti, sotto quella al Lele e più piccola: «Al mio caro zio
|
||||||
Mario, che mi ha fatto vivere tante avventure più belle della fantasia.»
|
Mario, che mi ha fatto vivere tante avventure più belle della fantasia.»
|
||||||
- **Il Taglia (31 ottobre)**: tolte le battute sulla sua altezza (Baldoria: non gli fa piacere).
|
- **Il Taglia (31 ottobre)**: tolte le battute sulla sua altezza (Baldoria: non gli fa piacere).
|
||||||
|
|||||||
+12
@@ -303,6 +303,18 @@
|
|||||||
quelle di sopra restano crude; troppo forte volano fuori, nel fuoco. COTTURA: quando è nel verde, Rovescia nel
|
quelle di sopra restano crude; troppo forte volano fuori, nel fuoco. COTTURA: quando è nel verde, Rovescia nel
|
||||||
sacco (o Invio). Contano le bruciate buone: alla bona ne bastano 10 su 18, seria 15, a fassi male 16.</p>
|
sacco (o Invio). Contano le bruciate buone: alla bona ne bastano 10 su 18, seria 15, a fassi male 16.</p>
|
||||||
</details>
|
</details>
|
||||||
|
<details id="flipper-help" hidden><summary>Come si gioca</summary>
|
||||||
|
<p>Il flipper del River, con San Piero dipinto sulla tavola. La pallina parte dalla corsia di destra: tieni premuto
|
||||||
|
per caricare la molla e lascia per lanciare (o regola la forza e premi Lancia, o Spazio). Fa il giro dell'arco e
|
||||||
|
scende per una delle quattro corsie dei rioni: accendile tutte e quattro per il PALIO (3000 punti). I tre funghi
|
||||||
|
sono i bastioni della Fortezza (100 a botta); i bersagli bianchi a sinistra sono la Pieve e quelli gialli a
|
||||||
|
destra la torre dell'orologio (500 l'uno, 2000 la fila intera); il bersaglio verde con la corona è il Regolo
|
||||||
|
(1000).</p>
|
||||||
|
<p>I flipper: tieni premuto sulla metà sinistra o destra della scena, o i pulsanti «← Flipper» e «Flipper →», o le
|
||||||
|
frecce sinistra e destra. Tre palline; la prima volta, appena lanciata, se la perdi subito torna (alla bona per
|
||||||
|
8 secondi, seria per 3, a fassi male mai). Si vince arrivando al traguardo: alla bona 6000 punti, seria 12000,
|
||||||
|
a fassi male 25000.</p>
|
||||||
|
</details>
|
||||||
<details id="cerbottana-help" hidden><summary>Come si gioca</summary>
|
<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
|
<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
|
di gesso vale 10, 5 o 2 punti secondo il cerchio, ogni lattina sul muretto 3, il barattolo appeso che dondola (Seria
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
"browser:condor": "node tests/condor.browser.mjs",
|
"browser:condor": "node tests/condor.browser.mjs",
|
||||||
"browser:campana": "node tests/campana.browser.mjs",
|
"browser:campana": "node tests/campana.browser.mjs",
|
||||||
"browser:bruciate": "node tests/bruciate.browser.mjs",
|
"browser:bruciate": "node tests/bruciate.browser.mjs",
|
||||||
|
"browser:flipper": "node tests/flipper.browser.mjs",
|
||||||
"browser:ingorgo": "node tests/ingorgo.browser.mjs",
|
"browser:ingorgo": "node tests/ingorgo.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",
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
// Il flipper del River in primo piano (flipper.js): la tavola dipinta da Codex con San Piero, e sopra le parti
|
||||||
|
// meccaniche disegnate dal gioco, nelle stesse coordinate della fisica: sponde cromate, corsie dei rioni con le luci,
|
||||||
|
// funghi, bersagli a caduta, il Regolo, sponde elastiche, flipper, molla e pallina. A sinistra, sul fianco nero del
|
||||||
|
// mobile, il quadro dei punti. Niente filtri né passate a tutto schermo (vedi risveglio.js).
|
||||||
|
import { FIELD, LANE, R, FLIP, PIVOT, SCORE, waiting, canThrow } from './flipper.js';
|
||||||
|
import { text, panel } from './bocce-render.js';
|
||||||
|
|
||||||
|
const COLORS = { piazza: '#d0453a', pieve: '#f2eee2', cardetole: '#3fa34d', campomigliaio: '#2f6fd0' };
|
||||||
|
const rail = (ctx, [ax, ay], [bx, by], dark = '#2a2622', light = '#c9c3b6') => {
|
||||||
|
ctx.lineCap = 'round';
|
||||||
|
ctx.strokeStyle = dark; ctx.lineWidth = 4; ctx.beginPath(); ctx.moveTo(ax, ay); ctx.lineTo(bx, by); ctx.stroke();
|
||||||
|
ctx.strokeStyle = light; ctx.lineWidth = 1.5; ctx.beginPath(); ctx.moveTo(ax, ay); ctx.lineTo(bx, by); ctx.stroke();
|
||||||
|
};
|
||||||
|
function disc(ctx, x, y, r, fill, rim) {
|
||||||
|
ctx.fillStyle = rim; ctx.beginPath(); ctx.arc(x, y, r, 0, Math.PI * 2); ctx.fill();
|
||||||
|
ctx.fillStyle = fill; ctx.beginPath(); ctx.arc(x, y, r - 2, 0, Math.PI * 2); ctx.fill();
|
||||||
|
}
|
||||||
|
function bumper(ctx, b, time) {
|
||||||
|
const k = b.hit > 0 ? 1 + b.hit * 1.2 : 1, r = b.r * k;
|
||||||
|
ctx.fillStyle = 'rgba(0,0,0,0.25)'; ctx.beginPath(); ctx.arc(b.x + 2, b.y + 3, r, 0, Math.PI * 2); ctx.fill();
|
||||||
|
disc(ctx, b.x, b.y, r, b.hit > 0 ? '#ffe9a8' : '#d9463a', '#5a1d16');
|
||||||
|
disc(ctx, b.x, b.y, r * 0.55, b.hit > 0 ? '#fff6d8' : '#f0a24a', '#8a3a1e');
|
||||||
|
ctx.fillStyle = 'rgba(255,255,255,0.55)'; ctx.fillRect(Math.round(b.x - r * 0.4), Math.round(b.y - r * 0.6), 3, 2);
|
||||||
|
void time;
|
||||||
|
}
|
||||||
|
function flipper(ctx, c, side) {
|
||||||
|
const f = c.flippers[side], [px, py] = PIVOT[side], tx = px + Math.cos(f.angle) * FLIP.len, ty = py + Math.sin(f.angle) * FLIP.len;
|
||||||
|
ctx.lineCap = 'round';
|
||||||
|
ctx.strokeStyle = 'rgba(0,0,0,0.3)'; ctx.lineWidth = FLIP.r * 2 + 2; ctx.beginPath(); ctx.moveTo(px + 1, py + 3); ctx.lineTo(tx + 1, ty + 3); ctx.stroke();
|
||||||
|
ctx.strokeStyle = '#3a2a1a'; ctx.lineWidth = FLIP.r * 2 + 2; ctx.beginPath(); ctx.moveTo(px, py); ctx.lineTo(tx, ty); ctx.stroke();
|
||||||
|
ctx.strokeStyle = f.held.length ? '#ffd45a' : '#e8b640'; ctx.lineWidth = FLIP.r * 2 - 1; ctx.beginPath(); ctx.moveTo(px, py); ctx.lineTo(tx, ty); ctx.stroke();
|
||||||
|
ctx.strokeStyle = '#fff1b8'; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(px, py - 1.5); ctx.lineTo(tx, ty - 1.5); ctx.stroke();
|
||||||
|
disc(ctx, px, py, 4, '#8a7a6a', '#2a2622');
|
||||||
|
}
|
||||||
|
function plunger(ctx, c) {
|
||||||
|
const x = (LANE.x0 + LANE.x1) / 2, top = 326, pull = c.holding ? c.power * 14 : canThrow(c) ? c.power * 6 : 0;
|
||||||
|
ctx.fillStyle = '#4a4440'; ctx.fillRect(Math.round(x) - 3, top + pull, 6, 336 - top - pull + 10);
|
||||||
|
ctx.fillStyle = '#9a9088';
|
||||||
|
for (let y = top + pull + 2; y < 346; y += 3) ctx.fillRect(Math.round(x) - 4, Math.round(y), 8, 1);
|
||||||
|
ctx.fillStyle = '#d9463a'; ctx.fillRect(Math.round(x) - 5, Math.round(top + pull) - 3, 10, 4);
|
||||||
|
}
|
||||||
|
function ball(ctx, b) {
|
||||||
|
if (!b) return;
|
||||||
|
ctx.fillStyle = 'rgba(0,0,0,0.35)'; ctx.beginPath(); ctx.arc(b.x + 2, b.y + 3, R, 0, Math.PI * 2); ctx.fill();
|
||||||
|
const g = ctx.createRadialGradient(b.x - 1.5, b.y - 1.5, 0.5, b.x, b.y, R);
|
||||||
|
g.addColorStop(0, '#ffffff'); g.addColorStop(0.4, '#cfd3da'); g.addColorStop(1, '#5a6068');
|
||||||
|
ctx.fillStyle = g; ctx.beginPath(); ctx.arc(b.x, b.y, R, 0, Math.PI * 2); ctx.fill();
|
||||||
|
}
|
||||||
|
// Il quadro dei punti sul fianco del mobile: punti, palline, i rioni accesi, il traguardo, l'ultimo avviso.
|
||||||
|
function board(ctx, c, def) {
|
||||||
|
panel(ctx, 22, 60, 160, 150);
|
||||||
|
text(ctx, 'FLIPPER DEL RIVER', 102, 76, 7, '#efd49e', 'center');
|
||||||
|
text(ctx, String(c.step === 'result' ? c.result.score : c.score).padStart(6, '0'), 102, 100, 14, '#fff4c8', 'center');
|
||||||
|
text(ctx, 'PUNTI', 102, 116, 5, '#a9a08a', 'center');
|
||||||
|
text(ctx, `PALLINE ${c.step === 'result' ? 0 : c.balls}`, 34, 136, 6, '#d7ccb4');
|
||||||
|
text(ctx, `TRAGUARDO ${c.skill.goal}`, 34, 150, 6, '#d7ccb4');
|
||||||
|
text(ctx, 'RIONI', 34, 168, 6, '#d7ccb4');
|
||||||
|
c.lanes.forEach((l, i) => disc(ctx, 80 + i * 20, 168, 6, l.lit ? COLORS[l.rione] : '#3a3630', l.lit ? '#fff4c8' : '#5a5650'));
|
||||||
|
text(ctx, (def.names?.player ?? 'Andrea').toUpperCase(), 34, 190, 6, '#efd49e');
|
||||||
|
if (c.line) text(ctx, c.line.toUpperCase(), 102, 203, 5, '#ffe08c', 'center');
|
||||||
|
}
|
||||||
|
function result(ctx, c) {
|
||||||
|
panel(ctx, 20, 230, 164, 56);
|
||||||
|
text(ctx, c.result.winner === 'player' ? 'TRAGUARDO FATTO!' : 'FINE PARTITA', 102, 246, 7, '#efd49e', 'center');
|
||||||
|
text(ctx, `${c.result.score} su ${c.result.goal}`, 102, 266, 7, c.result.winner === 'player' ? '#9fd27a' : '#e0b07a', 'center');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function drawFlipper(ctx, c, def, bg) {
|
||||||
|
const time = c.visual.time;
|
||||||
|
ctx.save();
|
||||||
|
if (bg) ctx.drawImage(bg, 0, 0, 640, 360);
|
||||||
|
else { ctx.fillStyle = '#17110c'; ctx.fillRect(0, 0, 640, 360); ctx.fillStyle = '#e9d9b4'; ctx.fillRect(FIELD.x0, FIELD.y0, FIELD.x1 - FIELD.x0, FIELD.y1 - FIELD.y0); }
|
||||||
|
// Le luci delle corsie dei rioni, sotto le bandiere.
|
||||||
|
for (const l of c.lanes) disc(ctx, l.x, l.y + 18, 4, l.lit ? COLORS[l.rione] : 'rgba(40,36,30,0.6)', l.lit ? '#fff4c8' : 'rgba(0,0,0,0.4)');
|
||||||
|
// Le sponde: tutte le pareti della fisica, cromate; le elastiche con la gomma rossa.
|
||||||
|
for (const w of c.walls) {
|
||||||
|
if (w.gate) { ctx.strokeStyle = '#8a8078'; ctx.lineWidth = 1; ctx.setLineDash([2, 2]); ctx.beginPath(); ctx.moveTo(...w.a); ctx.lineTo(...w.b); ctx.stroke(); ctx.setLineDash([]); continue; }
|
||||||
|
if (w.sling) { rail(ctx, w.a, w.b, '#5a1d16', '#e8584a'); continue; }
|
||||||
|
rail(ctx, w.a, w.b);
|
||||||
|
}
|
||||||
|
// I bersagli a caduta: in piedi colorati, caduti un segno a terra.
|
||||||
|
for (const t of c.targets) {
|
||||||
|
const [x, y0] = t.a, y1 = t.b[1];
|
||||||
|
if (t.down) { ctx.fillStyle = 'rgba(0,0,0,0.35)'; ctx.fillRect(x - 2, y0, 4, y1 - y0); continue; }
|
||||||
|
ctx.fillStyle = '#2a2622'; ctx.fillRect(x - 3, y0 - 1, 6, y1 - y0 + 2);
|
||||||
|
ctx.fillStyle = t.bank === 'pieve' ? '#f2eee2' : '#e8b640'; ctx.fillRect(x - 2, y0, 4, y1 - y0);
|
||||||
|
}
|
||||||
|
for (const b of c.bumpers) bumper(ctx, b, time);
|
||||||
|
// Il Regolo: il bersaglio verde con la corona, che brilla quando è colpito.
|
||||||
|
disc(ctx, c.regolo.x, c.regolo.y, c.regolo.r + (c.regolo.hit > 0 ? 2 : 0), c.regolo.hit > 0 ? '#c9ff9a' : '#3fa34d', '#1e4a24');
|
||||||
|
ctx.fillStyle = '#ffd45a'; ctx.fillRect(c.regolo.x - 3, c.regolo.y - c.regolo.r - 4, 6, 3);
|
||||||
|
for (const side of ['left', 'right']) flipper(ctx, c, side);
|
||||||
|
plunger(ctx, c);
|
||||||
|
ball(ctx, c.ball);
|
||||||
|
// Da fermo, l'invito: la molla da caricare.
|
||||||
|
if (waiting(c) && !c.holding && Math.sin(time * 5) > 0) text(ctx, 'LANCIA', (LANE.x0 + LANE.x1) / 2, 300, 5, '#ffe08c', 'center');
|
||||||
|
board(ctx, c, def);
|
||||||
|
if (c.step === 'result') result(ctx, c);
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
|
export { SCORE };
|
||||||
@@ -0,0 +1,231 @@
|
|||||||
|
// Il flipper del River (Andrea, 31 ottobre: «una tavola bellissima, con San Piero dentro»). Visto dall'alto, la tavola
|
||||||
|
// in verticale al centro del primo piano 640×360; la pallina parte dalla corsia di destra con la molla, fa il giro
|
||||||
|
// dell'arco in cima, passa in una delle quattro corsie dei rioni, rimbalza sui tre funghi (i bastioni della Fortezza),
|
||||||
|
// abbatte i bersagli della Pieve a sinistra e della torre a destra, tocca il Regolo in mezzo, e giù dalle sponde
|
||||||
|
// elastiche ai due flipper, con la Sieve dipinta che scorre nel buco in mezzo. Tre palline; si vince arrivando al
|
||||||
|
// traguardo di punti del livello. Unità: i pixel del primo piano. La fisica va a passo fisso (come il biliardo).
|
||||||
|
export const FIELD = { x0: 200, x1: 448, y0: 5, y1: 336 }, LANE = { x0: 432, x1: 448 }, R = 4.5;
|
||||||
|
export const BALLS = 3, STEP = 1 / 240, MAX_SPEED = 980, LAUNCH = [420, 900], CHARGE = 1.2, FLIP = { len: 34, rest: 0.5, up: -0.46, speed: 26, r: 3 };
|
||||||
|
export const PIVOT = { left: [280, 314], right: [360, 314] };
|
||||||
|
export const SCORE = { bumper: 100, sling: 50, lane: 200, palio: 3000, target: 500, bank: 2000, regolo: 1000 };
|
||||||
|
// I livelli: la gravità, quanti secondi dura il salvataggio della pallina appena lanciata, la spinta dei funghi, il traguardo.
|
||||||
|
export const LEVELS = {
|
||||||
|
bona: { g: 620, save: 8, kick: 300, goal: 6000 },
|
||||||
|
seria: { g: 780, save: 3, kick: 340, goal: 12000 },
|
||||||
|
male: { g: 880, save: 0, kick: 380, goal: 25000 },
|
||||||
|
};
|
||||||
|
export const RIONI = ['piazza', 'pieve', 'cardetole', 'campomigliaio'];
|
||||||
|
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; }
|
||||||
|
const seg = (a, b, extra = {}) => ({ a, b, e: 0.55, ...extra });
|
||||||
|
|
||||||
|
// La tavola: sponde, corsie, sponde elastiche, funghi, bersagli. Le coordinate seguono il disegno di Codex
|
||||||
|
// (assets/closeups/river-flipper.png): i tre dischi dei funghi, le quattro bandiere dei rioni, la corsia della molla.
|
||||||
|
export function layout() {
|
||||||
|
const arc = [[200, 72], [203, 50], [212, 32], [228, 18], [252, 10], [288, 6], [324, 5], [360, 6], [396, 10], [420, 18], [436, 32], [445, 50], [448, 72]];
|
||||||
|
const walls = [];
|
||||||
|
for (let i = 1; i < arc.length; i++) walls.push(seg(arc[i - 1], arc[i], { e: 0.6 }));
|
||||||
|
walls.push(seg([200, 72], [200, 336]), seg([448, 72], [448, 336]));
|
||||||
|
// La corsia della molla, e in cima il cancelletto che non fa tornare indietro la pallina.
|
||||||
|
walls.push(seg([432, 70], [432, 336]), seg([432, 70], [448, 70], { gate: true }), seg([432, 326], [448, 326], { e: 0.2 }));
|
||||||
|
// Le corsie dei rioni, in cima, fra le bandiere: tre guide.
|
||||||
|
for (const x of [277, 319, 361]) walls.push(seg([x, 70], [x, 102], { e: 0.4 }));
|
||||||
|
// I divisori delle uscite laterali e le guide che portano ai flipper.
|
||||||
|
// (La guida passa sopra il perno e continua un po' lungo il flipper: finendo sul perno la pallina ci restava incastrata.)
|
||||||
|
walls.push(seg([222, 240], [222, 290]), seg([222, 290], [292, 314], { e: 0.3 }));
|
||||||
|
walls.push(seg([418, 240], [418, 290]), seg([418, 290], [348, 314], { e: 0.3 }));
|
||||||
|
// Le sponde elastiche sopra i flipper: il lato lungo spinge.
|
||||||
|
walls.push(seg([236, 246], [236, 284]), seg([236, 284], [262, 290]), seg([236, 246], [262, 290], { sling: true }));
|
||||||
|
walls.push(seg([404, 246], [404, 284]), seg([404, 284], [378, 290]), seg([404, 246], [378, 290], { sling: true }));
|
||||||
|
// I bersagli a caduta: la Pieve a sinistra, la torre a destra, con la sponda dietro.
|
||||||
|
walls.push(seg([210, 146], [210, 198]), seg([429, 146], [429, 198]));
|
||||||
|
const targets = [];
|
||||||
|
for (const [bank, x] of [['pieve', 216], ['torre', 423]]) for (let i = 0; i < 3; i++) targets.push({ bank, i, a: [x, 150 + i * 16], b: [x, 162 + i * 16], down: false });
|
||||||
|
const bumpers = [[281, 120], [360, 120], [320, 153]].map(([x, y]) => ({ x, y, r: 12, hit: 0 }));
|
||||||
|
// Il Regolo, in alto a destra sotto l'arco, dove la pallina scende dal giro.
|
||||||
|
const regolo = { x: 404, y: 62, r: 6, hit: 0 };
|
||||||
|
const lanes = [257, 297, 340, 381].map((x, i) => ({ x, y: 88, rione: RIONI[i], lit: false }));
|
||||||
|
return { walls, targets, bumpers, regolo, lanes };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function newFlipper(seed = Date.now() >>> 0, def = {}) {
|
||||||
|
const level = LEVELS[def.level] ? def.level : 'seria';
|
||||||
|
const c = { step: 'play', t: 0, seed, level, skill: LEVELS[level], current: 'player', balls: BALLS, score: 0, power: 0.7,
|
||||||
|
holding: false, holdTime: 0, pointerId: null, cursor: null, accumulator: 0, events: [], line: null, lineT: 0, saved: false,
|
||||||
|
flippers: { left: { angle: FLIP.rest, omega: 0, held: [] }, right: { angle: Math.PI - FLIP.rest, omega: 0, held: [] } },
|
||||||
|
ball: null, saveUntil: 0, still: 0, visual: { time: 0 }, ...layout() };
|
||||||
|
newBall(c);
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
function newBall(c) { c.ball = { x: (LANE.x0 + LANE.x1) / 2, y: 318, vx: 0, vy: 0, waiting: true }; c.still = 0; c.anchor = null; }
|
||||||
|
export const waiting = c => c.step === 'play' && !!c.ball?.waiting;
|
||||||
|
export const canThrow = c => waiting(c) && !c.holding;
|
||||||
|
export const canPress = canThrow;
|
||||||
|
export const canAdjust = c => c.step !== 'result';
|
||||||
|
export const aimFree = () => true;
|
||||||
|
export const powerAt = time => clamp(time / CHARGE, 0, 1);
|
||||||
|
function say(c, line) { c.line = line; c.lineT = 2; }
|
||||||
|
function add(c, points) { c.score += points; }
|
||||||
|
|
||||||
|
// Il lancio: la molla spinge la pallina su per la corsia.
|
||||||
|
export function launch(c, power) {
|
||||||
|
if (!waiting(c)) return false;
|
||||||
|
c.ball.waiting = false; c.ball.vy = -(LAUNCH[0] + (LAUNCH[1] - LAUNCH[0]) * clamp(power, 0, 1)); c.ball.vx = 0;
|
||||||
|
// Il salvataggio vale una volta per pallina: persa subito, torna; persa di nuovo, è persa.
|
||||||
|
c.saveUntil = c.saved ? 0 : c.t + c.skill.save; c.holding = false; c.holdTime = 0;
|
||||||
|
c.events.push({ kind: 'flipper-lancio', power: 0.5 + 0.5 * power });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Il flipper: il tasto tenuto lo alza, lasciato ricade. Più dita o tasti insieme: resta su finché ne resta uno.
|
||||||
|
export function hold(c, side, who, down) {
|
||||||
|
const f = c.flippers[side]; if (!f) return false;
|
||||||
|
const was = f.held.length > 0;
|
||||||
|
f.held = f.held.filter(h => h !== who); if (down) f.held.push(who);
|
||||||
|
if (!was && f.held.length && c.step === 'play') c.events.push({ kind: 'flipper-flip', power: 0.6 });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// La fisica, a passi fissi.
|
||||||
|
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;
|
||||||
|
if (c.lineT > 0 && (c.lineT -= dt) <= 0) c.line = null;
|
||||||
|
if (c.holding) { c.holdTime += dt; c.power = powerAt(c.holdTime); }
|
||||||
|
for (const b of c.bumpers) b.hit = Math.max(0, b.hit - dt); c.regolo.hit = Math.max(0, c.regolo.hit - dt);
|
||||||
|
c.accumulator += dt;
|
||||||
|
while (c.accumulator >= STEP) { c.accumulator -= STEP; step(c); }
|
||||||
|
}
|
||||||
|
function flipperSeg(c, side) {
|
||||||
|
const f = c.flippers[side], [px, py] = PIVOT[side];
|
||||||
|
return { a: [px, py], b: [px + Math.cos(f.angle) * FLIP.len, py + Math.sin(f.angle) * FLIP.len] };
|
||||||
|
}
|
||||||
|
function step(c) {
|
||||||
|
// I flipper: su se tenuti, giù se no; velocità angolare costante.
|
||||||
|
for (const side of ['left', 'right']) {
|
||||||
|
const f = c.flippers[side];
|
||||||
|
const target = f.held.length ? (side === 'left' ? FLIP.up : Math.PI - FLIP.up) : (side === 'left' ? FLIP.rest : Math.PI - FLIP.rest);
|
||||||
|
const d = target - f.angle, move = FLIP.speed * STEP;
|
||||||
|
f.omega = Math.abs(d) <= move ? 0 : Math.sign(d) * FLIP.speed;
|
||||||
|
f.angle = Math.abs(d) <= move ? target : f.angle + Math.sign(d) * move;
|
||||||
|
}
|
||||||
|
const ball = c.ball;
|
||||||
|
if (c.step !== 'play' || !ball || ball.waiting) return;
|
||||||
|
ball.vy += c.skill.g * STEP;
|
||||||
|
const v = Math.hypot(ball.vx, ball.vy);
|
||||||
|
if (v > MAX_SPEED) { ball.vx *= MAX_SPEED / v; ball.vy *= MAX_SPEED / v; }
|
||||||
|
ball.x += ball.vx * STEP; ball.y += ball.vy * STEP;
|
||||||
|
for (const w of c.walls) collideSeg(c, ball, w);
|
||||||
|
for (const t of c.targets) if (!t.down && collideSeg(c, ball, { a: t.a, b: t.b, e: 0.5 })) dropTarget(c, t);
|
||||||
|
for (const b of c.bumpers) if (collideCircle(ball, b, 0)) { kick(c, ball, b, c.skill.kick); b.hit = 0.15; add(c, SCORE.bumper); c.events.push({ kind: 'flipper-fungo', power: 0.8 }); }
|
||||||
|
if (collideCircle(ball, c.regolo, 0.7)) { c.regolo.hit = 0.3; add(c, SCORE.regolo); say(c, 'Il Regolo!'); c.events.push({ kind: 'flipper-bersaglio', power: 1 }); }
|
||||||
|
for (const side of ['left', 'right']) collideFlipper(c, ball, side);
|
||||||
|
// Le corsie si accendono entrando (non restandoci sopra: dopo il Palio ripartono spente).
|
||||||
|
for (const l of c.lanes) {
|
||||||
|
const inside = Math.hypot(ball.x - l.x, ball.y - l.y) < 7;
|
||||||
|
if (inside && !l.inside && !l.lit) {
|
||||||
|
l.lit = true; add(c, SCORE.lane); c.events.push({ kind: 'flipper-corsia', power: 0.6 });
|
||||||
|
if (c.lanes.every(k => k.lit)) { add(c, SCORE.palio); say(c, 'PALIO! Tutti e quattro i rioni!'); c.events.push({ kind: 'flipper-palio', power: 1 }); for (const k of c.lanes) k.lit = false; }
|
||||||
|
}
|
||||||
|
l.inside = inside;
|
||||||
|
}
|
||||||
|
// La pallina che resta nello stesso punto (ferma, o che trema in un incastro): dopo un po' una scossa.
|
||||||
|
if (!c.anchor || Math.hypot(ball.x - c.anchor.x, ball.y - c.anchor.y) > 3) { c.anchor = { x: ball.x, y: ball.y }; c.still = 0; }
|
||||||
|
else if ((c.still += STEP) > 2.5) { ball.vx += (random(c) - 0.5) * 160; ball.vy -= 90; c.still = 0; c.anchor = null; }
|
||||||
|
// Un lancio fiacco ricasca sulla molla: si rilancia.
|
||||||
|
if (ball.x > LANE.x0 && ball.y > 315 && Math.abs(ball.vy) < 8 && !ball.waiting) newBall(c);
|
||||||
|
if (ball.y > FIELD.y1 + R * 2) drain(c);
|
||||||
|
}
|
||||||
|
function dropTarget(c, t) {
|
||||||
|
t.down = true; add(c, SCORE.target); c.events.push({ kind: 'flipper-bersaglio', power: 0.7 });
|
||||||
|
const bank = c.targets.filter(k => k.bank === t.bank);
|
||||||
|
if (bank.every(k => k.down)) { add(c, SCORE.bank); say(c, t.bank === 'pieve' ? 'La Pieve!' : 'La torre!'); for (const k of bank) k.down = false; }
|
||||||
|
}
|
||||||
|
function drain(c) {
|
||||||
|
c.events.push({ kind: 'flipper-buco', power: 0.8 });
|
||||||
|
if (c.t < c.saveUntil) { say(c, 'Pallina salvata!'); c.saved = true; newBall(c); return; }
|
||||||
|
c.balls--; c.saved = false;
|
||||||
|
if (c.balls > 0) { say(c, `Pallina persa. Ne restano ${c.balls}.`); newBall(c); return; }
|
||||||
|
c.ball = null; c.step = 'result';
|
||||||
|
c.result = { winner: c.score >= c.skill.goal ? 'player' : 'cpu', score: c.score, goal: c.skill.goal };
|
||||||
|
}
|
||||||
|
// Pallina contro un segmento: la si spinge fuori e si riflette la velocità. Torna true se ha toccato.
|
||||||
|
function collideSeg(c, ball, w) {
|
||||||
|
const [ax, ay] = w.a, [bx, by] = w.b, dx = bx - ax, dy = by - ay, len2 = dx * dx + dy * dy || 1;
|
||||||
|
const u = clamp(((ball.x - ax) * dx + (ball.y - ay) * dy) / len2, 0, 1), px = ax + dx * u, py = ay + dy * u;
|
||||||
|
let nx = ball.x - px, ny = ball.y - py; const d = Math.hypot(nx, ny);
|
||||||
|
if (d >= R) return false;
|
||||||
|
if (d < 1e-6) { nx = -dy; ny = dx; const n = Math.hypot(nx, ny); nx /= n; ny /= n; } else { nx /= d; ny /= d; }
|
||||||
|
// Il cancelletto ferma solo chi scende dall'alto.
|
||||||
|
if (w.gate && !(ny < 0 && ball.vy > 0)) return false;
|
||||||
|
ball.x = px + nx * R; ball.y = py + ny * R;
|
||||||
|
const vn = ball.vx * nx + ball.vy * ny;
|
||||||
|
if (vn < 0) {
|
||||||
|
// La sponda elastica spinge via, un po' a caso come la gomma vera (così la pallina non fa il ping-pong in eterno).
|
||||||
|
if (w.sling) { const a = Math.atan2(ny, nx) + (random(c) - 0.5) * 0.5, s = 230 + random(c) * 40; ball.vx = Math.cos(a) * s + (ball.vx - vn * nx) * 0.3; ball.vy = Math.sin(a) * s + (ball.vy - vn * ny) * 0.3; add(c, SCORE.sling); c.events.push({ kind: 'flipper-sponda', power: 0.6 }); }
|
||||||
|
else { ball.vx -= (1 + w.e) * vn * nx; ball.vy -= (1 + w.e) * vn * ny; }
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
function collideCircle(ball, o, e) {
|
||||||
|
const dx = ball.x - o.x, dy = ball.y - o.y, d = Math.hypot(dx, dy), min = R + o.r;
|
||||||
|
if (d >= min) return false;
|
||||||
|
const nx = d > 1e-6 ? dx / d : 0, ny = d > 1e-6 ? dy / d : -1;
|
||||||
|
ball.x = o.x + nx * min; ball.y = o.y + ny * min;
|
||||||
|
const vn = ball.vx * nx + ball.vy * ny;
|
||||||
|
if (vn < 0) { ball.vx -= (1 + e) * vn * nx; ball.vy -= (1 + e) * vn * ny; }
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
function kick(c, ball, o, speed) {
|
||||||
|
const a = Math.atan2(ball.y - o.y, ball.x - o.x) + (random(c) - 0.5) * 0.3;
|
||||||
|
ball.vx = Math.cos(a) * speed; ball.vy = Math.sin(a) * speed;
|
||||||
|
}
|
||||||
|
// Il flipper è una capsula che gira: alla pallina si aggiunge la velocità del punto toccato.
|
||||||
|
function collideFlipper(c, ball, side) {
|
||||||
|
const f = c.flippers[side], { a, b } = flipperSeg(c, side), [ax, ay] = a, dx = b[0] - ax, dy = b[1] - ay, len2 = dx * dx + dy * dy;
|
||||||
|
const u = clamp(((ball.x - ax) * dx + (ball.y - ay) * dy) / len2, 0, 1), px = ax + dx * u, py = ay + dy * u;
|
||||||
|
let nx = ball.x - px, ny = ball.y - py; const d = Math.hypot(nx, ny), min = R + FLIP.r;
|
||||||
|
if (d >= min) return false;
|
||||||
|
if (d < 1e-6) { nx = 0; ny = -1; } else { nx /= d; ny /= d; }
|
||||||
|
ball.x = px + nx * min; ball.y = py + ny * min;
|
||||||
|
// La velocità della superficie: ω × r, col raggio dal perno al punto toccato.
|
||||||
|
const rx = px - ax, ry = py - ay, sx = -f.omega * ry, sy = f.omega * rx;
|
||||||
|
const vn = (ball.vx - sx) * nx + (ball.vy - sy) * ny;
|
||||||
|
if (vn < 0) { ball.vx -= (1 + 0.45) * vn * nx; ball.vy -= (1 + 0.45) * vn * ny; }
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Comandi: col mouse o col dito, prima del lancio tenere premuto carica la molla e lasciare lancia; in gioco
|
||||||
|
// la metà sinistra della scena alza il flipper sinistro, la destra il destro, finché si tiene premuto.
|
||||||
|
export function aimScreen(c, [x, y]) { if (!Number.isFinite(x) || !Number.isFinite(y)) return false; c.cursor = [x, y]; return true; }
|
||||||
|
export function press(c, pointerId = 'ptr') {
|
||||||
|
if (c.step !== 'play') return false;
|
||||||
|
if (waiting(c)) { if (c.holding) return false; c.holding = true; c.holdTime = 0; c.pointerId = pointerId; c.power = 0; return true; }
|
||||||
|
return hold(c, (c.cursor?.[0] ?? 320) < 320 ? 'left' : 'right', pointerId, true);
|
||||||
|
}
|
||||||
|
export function release(c, pointerId = 'ptr') {
|
||||||
|
if (c.holding && c.pointerId === pointerId) return launch(c, powerAt(c.holdTime));
|
||||||
|
let any = false;
|
||||||
|
for (const side of ['left', 'right']) if (c.flippers[side].held.includes(pointerId)) any = hold(c, side, pointerId, false) || any;
|
||||||
|
return any;
|
||||||
|
}
|
||||||
|
export const cancel = (c, pointerId) => pointerId === undefined ? release(c, c.pointerId ?? 'ptr') : release(c, pointerId);
|
||||||
|
// I pulsanti «← Flipper» e «Flipper →» (tenuti premuti), e le frecce.
|
||||||
|
export const turn = (c, dir, who = 'button') => c.step === 'play' && hold(c, dir < 0 ? 'left' : 'right', who, true);
|
||||||
|
export const stopTurn = (c, who = 'button') => hold(c, 'left', who, false) | hold(c, 'right', who, false);
|
||||||
|
export const nudge = () => false;
|
||||||
|
export function setPower(c, v) { if (!canThrow(c) || !Number.isFinite(v)) return false; c.power = clamp(v, 0, 1); return true; }
|
||||||
|
export function key(c, k, down) {
|
||||||
|
if (k === 'ArrowLeft' || k === 'ArrowRight') { hold(c, k === 'ArrowLeft' ? 'left' : 'right', 'key-' + k, down); return true; }
|
||||||
|
if (k === ' ') { if (down) { if (!c.holding && waiting(c)) press(c, 'key'); } else if (c.holding && c.pointerId === 'key') release(c, 'key'); return true; }
|
||||||
|
if (k === 'Enter') { if (down) shoot(c); return true; }
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Il pulsante Lancia: con la forza del cursore.
|
||||||
|
export const shoot = c => canThrow(c) && launch(c, c.power);
|
||||||
|
export const button = c => c.step === 'result' ? 'Finita' : waiting(c) ? 'Lancia' : 'In gioco…';
|
||||||
|
export function status(c) {
|
||||||
|
if (c.step === 'result') return `${c.result.score} punti. ${c.result.winner === 'player' ? `Traguardo ${c.result.goal}: fatto!` : `Ne servivano ${c.result.goal}.`}`;
|
||||||
|
const tally = `Punti ${c.score} (traguardo ${c.skill.goal}), palline ${c.balls}.`;
|
||||||
|
if (c.line) return `${c.line} ${tally}`;
|
||||||
|
return waiting(c) ? `Tieni premuto per caricare la molla e lascia, o premi Lancia. ${tally}` : `Flipper: metà sinistra e destra della scena, o i pulsanti. ${tally}`;
|
||||||
|
}
|
||||||
@@ -19,6 +19,7 @@ import { newTelefono, updateTelefono, tapTelefono } from './telefono.js';
|
|||||||
import * as condor from './condor.js';
|
import * as condor from './condor.js';
|
||||||
import * as campana from './campana.js';
|
import * as campana from './campana.js';
|
||||||
import * as bruciate from './bruciate.js';
|
import * as bruciate from './bruciate.js';
|
||||||
|
import * as flipper from './flipper.js';
|
||||||
import * as poker from './poker.js';
|
import * as poker from './poker.js';
|
||||||
|
|
||||||
// I minigiochi in primo piano: comandi opzionali per il tiro o per la scelta di una carta.
|
// I minigiochi in primo piano: comandi opzionali per il tiro o per la scelta di una carta.
|
||||||
@@ -36,6 +37,7 @@ export const MINIGAMES = {
|
|||||||
condor: { create: (seed, level) => condor.newCondor(seed, { level }), update: condor.update, press: condor.press, release: condor.release, cancel: condor.cancel, aim: condor.aimScreen },
|
condor: { create: (seed, level) => condor.newCondor(seed, { level }), update: condor.update, press: condor.press, release: condor.release, cancel: condor.cancel, aim: condor.aimScreen },
|
||||||
campana: { create: (seed, level) => campana.newCampana(seed, { level }), update: campana.update, press: campana.press, release: campana.release, cancel: campana.cancel, aim: campana.aimScreen },
|
campana: { create: (seed, level) => campana.newCampana(seed, { level }), update: campana.update, press: campana.press, release: campana.release, cancel: campana.cancel, aim: campana.aimScreen },
|
||||||
bruciate: { create: (seed, level) => bruciate.newBruciate(seed, { level }), update: bruciate.update, press: bruciate.press, release: bruciate.release, cancel: bruciate.cancel, aim: bruciate.aimScreen },
|
bruciate: { create: (seed, level) => bruciate.newBruciate(seed, { level }), update: bruciate.update, press: bruciate.press, release: bruciate.release, cancel: bruciate.cancel, aim: bruciate.aimScreen },
|
||||||
|
flipper: { create: (seed, level) => flipper.newFlipper(seed, { level }), update: flipper.update, press: flipper.press, release: flipper.release, cancel: flipper.cancel, aim: flipper.aimScreen },
|
||||||
micro: { create: (seed, level) => micro.newMicro(seed, { level }), update: micro.update, press: micro.press, release: micro.release, cancel: micro.cancel, aim: micro.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 },
|
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 },
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { drawCorsa } from './corsa.js';
|
|||||||
import { drawCondor } from './condor-render.js';
|
import { drawCondor } from './condor-render.js';
|
||||||
import { drawCampana } from './campana-render.js';
|
import { drawCampana } from './campana-render.js';
|
||||||
import { drawBruciate } from './bruciate-render.js';
|
import { drawBruciate } from './bruciate-render.js';
|
||||||
|
import { drawFlipper } from './flipper-render.js';
|
||||||
import { drawPoker } from './poker-render.js';
|
import { drawPoker } from './poker-render.js';
|
||||||
import { drawViaggio } from './viaggio.js';
|
import { drawViaggio } from './viaggio.js';
|
||||||
import { drawIncanto } from './incanto.js';
|
import { drawIncanto } from './incanto.js';
|
||||||
@@ -1055,6 +1056,11 @@ function drawCloseup(ctx, game, time) {
|
|||||||
drawCampana(ctx, c, def, backgrounds.get(def.picture));
|
drawCampana(ctx, c, def, backgrounds.get(def.picture));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (def.type === 'flipper') {
|
||||||
|
if (!backgrounds.has(def.picture)) loadBackground(def.picture);
|
||||||
|
drawFlipper(ctx, c, def, backgrounds.get(def.picture));
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (def.type === 'bruciate') {
|
if (def.type === 'bruciate') {
|
||||||
if (!backgrounds.has(def.picture)) loadBackground(def.picture);
|
if (!backgrounds.has(def.picture)) loadBackground(def.picture);
|
||||||
drawBruciate(ctx, c, def, backgrounds.get(def.picture));
|
drawBruciate(ctx, c, def, backgrounds.get(def.picture));
|
||||||
|
|||||||
@@ -161,6 +161,15 @@ export class Soundscape {
|
|||||||
else if (kind === 'bruciate-scoppio') { this.noiseSound(0.04, 2600, 0.05 * strength, { pan }); this.tone(900, 500, 0.03, 0.03, 0, pan); }
|
else if (kind === 'bruciate-scoppio') { this.noiseSound(0.04, 2600, 0.05 * strength, { pan }); this.tone(900, 500, 0.03, 0.03, 0, pan); }
|
||||||
else if (kind === 'bruciate-cade') { this.noiseSound(0.5, 3200, 0.09, { pan }); this.tone(200, 90, 0.12, 0.05, 0, pan); }
|
else if (kind === 'bruciate-cade') { this.noiseSound(0.5, 3200, 0.09, { pan }); this.tone(200, 90, 0.12, 0.05, 0, pan); }
|
||||||
else if (kind === 'bruciate-sacco') this.noiseSound(0.7, 1100, 0.1, { paper: true, pan });
|
else if (kind === 'bruciate-sacco') this.noiseSound(0.7, 1100, 0.1, { paper: true, pan });
|
||||||
|
// Il flipper: la molla, il colpo del flipper, il fungo, la sponda elastica, il bersaglio, la corsia, il Palio, il buco.
|
||||||
|
else if (kind === 'flipper-lancio') { this.noiseSound(0.12, 1800, 0.05 * strength, { pan }); this.tone(180, 600, 0.12, 0.05, 0, pan); }
|
||||||
|
else if (kind === 'flipper-flip') { this.tone(240, 120, 0.05, 0.07, 0, pan); this.noiseSound(0.03, 2200, 0.04, { pan }); }
|
||||||
|
else if (kind === 'flipper-fungo') { this.tone(900, 500, 0.06, 0.06 * strength + 0.02, 0, pan); this.tone(1800, 900, 0.04, 0.03, 0, pan); }
|
||||||
|
else if (kind === 'flipper-sponda') { this.tone(520, 260, 0.05, 0.05, 0, pan); this.noiseSound(0.03, 1600, 0.03, { pan }); }
|
||||||
|
else if (kind === 'flipper-bersaglio') { this.tone(1400, 1200, 0.05, 0.05, 0, pan); this.tone(2100, 1800, 0.08, 0.04, 0.05, pan); }
|
||||||
|
else if (kind === 'flipper-corsia') { this.tone(1046, 1046, 0.08, 0.04, 0, pan); this.tone(1318, 1318, 0.1, 0.04, 0.08, pan); }
|
||||||
|
else if (kind === 'flipper-palio') for (const [i, f] of [523, 659, 784, 1046].entries()) this.tone(f, f, 0.14, 0.06, i * 0.1, pan);
|
||||||
|
else if (kind === 'flipper-buco') { this.tone(220, 80, 0.3, 0.06, 0, pan); this.noiseSound(0.2, 600, 0.04, { 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); }
|
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.
|
// 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-spinta') { this.tone(1200, 500, 0.04, 0.04 + strength * 0.04, 0, pan); this.noiseSound(0.04, 2600, 0.04, { pan }); }
|
||||||
|
|||||||
+8
-5
@@ -16,6 +16,7 @@ import * as cerbottana from './engine/cerbottana.js';
|
|||||||
import * as condor from './engine/condor.js';
|
import * as condor from './engine/condor.js';
|
||||||
import * as campana from './engine/campana.js';
|
import * as campana from './engine/campana.js';
|
||||||
import * as bruciate from './engine/bruciate.js';
|
import * as bruciate from './engine/bruciate.js';
|
||||||
|
import * as flipper from './engine/flipper.js';
|
||||||
import * as poker from './engine/poker.js';
|
import * as poker from './engine/poker.js';
|
||||||
import { cardAt as pokerAt } from './engine/poker-render.js';
|
import { cardAt as pokerAt } from './engine/poker-render.js';
|
||||||
import * as basket from './engine/basket.js';
|
import * as basket from './engine/basket.js';
|
||||||
@@ -619,6 +620,7 @@ const MINI = {
|
|||||||
condor: { ...condor, back: 'Torna al campone', label: 'La Condor: muovi il mouse o tocca il campo per mirare, tieni premuto per trattenere il fiato e lascia per sparare; oppure premi Spara. Frecce per spostare il mirino.' },
|
condor: { ...condor, back: 'Torna al campone', label: 'La Condor: muovi il mouse o tocca il campo per mirare, tieni premuto per trattenere il fiato e lascia per sparare; oppure premi Spara. Frecce per spostare il mirino.' },
|
||||||
campana: { ...campana, back: 'Torna al campone', label: 'La campana: muovi il mouse o tocca la campana per mirare, tieni premuto per la forza e lascia per tirare il sassolino; oppure regola la forza e premi Tira (frecce su e giù per mirare, destra e sinistra per la forza). Ai salti premi, o Spazio, quando la lancetta è nel verde.' },
|
campana: { ...campana, back: 'Torna al campone', label: 'La campana: muovi il mouse o tocca la campana per mirare, tieni premuto per la forza e lascia per tirare il sassolino; oppure regola la forza e premi Tira (frecce su e giù per mirare, destra e sinistra per la forza). Ai salti premi, o Spazio, quando la lancetta è nel verde.' },
|
||||||
bruciate: { ...bruciate, back: 'Torna alla festa', label: 'Le bruciate: tieni premuto e fai dei giri, col mouse o col dito, per girare la manovella; oppure Spazio o le frecce, una spinta a ogni colpo. Né troppo piano né troppo forte; quando son cotte, Rovescia nel sacco (o Invio).' },
|
bruciate: { ...bruciate, back: 'Torna alla festa', label: 'Le bruciate: tieni premuto e fai dei giri, col mouse o col dito, per girare la manovella; oppure Spazio o le frecce, una spinta a ogni colpo. Né troppo piano né troppo forte; quando son cotte, Rovescia nel sacco (o Invio).' },
|
||||||
|
flipper: { ...flipper, toggle: true, back: 'Torna in strada', label: 'Il flipper del River: tieni premuto per caricare la molla e lascia per lanciare (o regola la forza e premi Lancia, o Spazio). In gioco, tieni premuto sulla metà sinistra o destra della scena per alzare il flipper di quel lato, oppure i pulsanti Flipper o le frecce sinistra e destra.' },
|
||||||
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).
|
||||||
@@ -1048,7 +1050,7 @@ function updateUi() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (mg && !cards) {
|
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', cerbottana: 'Comandi della cerbottana', condor: 'Comandi della Condor', campana: 'Comandi della campana', bruciate: 'Comandi delle bruciate' }[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', condor: 'Comandi della Condor', campana: 'Comandi della campana', bruciate: 'Comandi delle bruciate', flipper: 'Comandi del flipper' }[def.type] ?? 'Comandi delle bocce');
|
||||||
setProp($('panel'), 'hidden', true); setProp($('choices'), 'hidden', true);
|
setProp($('panel'), 'hidden', true); setProp($('choices'), 'hidden', true);
|
||||||
const line = mg.status(c, def.names);
|
const line = mg.status(c, def.names);
|
||||||
if ($('bocce-status').textContent !== line) $('bocce-status').textContent = line;
|
if ($('bocce-status').textContent !== line) $('bocce-status').textContent = line;
|
||||||
@@ -1064,7 +1066,7 @@ function updateUi() {
|
|||||||
const easy = def.type === 'gara' && gara.simple(c);
|
const easy = def.type === 'gara' && gara.simple(c);
|
||||||
setProp(powerLabel, 'hidden', def.type === 'rinquarto' || def.type === 'condor' || def.type === 'bruciate' || easy);
|
setProp(powerLabel, 'hidden', def.type === 'rinquarto' || def.type === 'condor' || def.type === 'bruciate' || easy);
|
||||||
for (const id of ['bocce-left', 'bocce-right']) setProp($(id), 'hidden', easy || def.type === 'campana' || def.type === 'bruciate');
|
for (const id of ['bocce-left', 'bocce-right']) setProp($(id), 'hidden', easy || def.type === 'campana' || def.type === 'bruciate');
|
||||||
const powerName = def.type === 'gara' ? 'Sensibilità ' : def.type === 'cerbottana' ? 'Fiato ' : 'Forza ';
|
const powerName = def.type === 'gara' ? 'Sensibilità ' : def.type === 'cerbottana' ? 'Fiato ' : def.type === 'flipper' ? 'Molla ' : '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' || easy);
|
setProp($('gara-pad'), 'hidden', def.type !== 'gara' || easy);
|
||||||
@@ -1072,8 +1074,8 @@ function updateUi() {
|
|||||||
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' : def.type === 'flipper' ? '← Flipper' : '← Mira');
|
||||||
setText($('bocce-right'), def.type === 'gara' ? 'Sterza →' : 'Mira →');
|
setText($('bocce-right'), def.type === 'gara' ? 'Sterza →' : def.type === 'flipper' ? 'Flipper →' : 'Mira →');
|
||||||
setProp($('gara-brake'), 'hidden', def.type !== 'gara' || easy);
|
setProp($('gara-brake'), 'hidden', def.type !== 'gara' || easy);
|
||||||
$('bocce-throw').classList.toggle('hold', 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);
|
||||||
@@ -1086,6 +1088,7 @@ function updateUi() {
|
|||||||
setProp($('condor-help'), 'hidden', def.type !== 'condor');
|
setProp($('condor-help'), 'hidden', def.type !== 'condor');
|
||||||
setProp($('campana-help'), 'hidden', def.type !== 'campana');
|
setProp($('campana-help'), 'hidden', def.type !== 'campana');
|
||||||
setProp($('bruciate-help'), 'hidden', def.type !== 'bruciate');
|
setProp($('bruciate-help'), 'hidden', def.type !== 'bruciate');
|
||||||
|
setProp($('flipper-help'), 'hidden', def.type !== 'flipper');
|
||||||
setProp($('basket-help'), 'hidden', def.type !== 'basket');
|
setProp($('basket-help'), 'hidden', def.type !== 'basket');
|
||||||
setProp($('pesca-help'), 'hidden', def.type !== 'pesca');
|
setProp($('pesca-help'), 'hidden', def.type !== 'pesca');
|
||||||
setProp($('gara-help'), 'hidden', def.type !== 'gara');
|
setProp($('gara-help'), 'hidden', def.type !== 'gara');
|
||||||
@@ -1117,7 +1120,7 @@ function updateUi() {
|
|||||||
canvas.classList.toggle('torch', !!torch);
|
canvas.classList.toggle('torch', !!torch);
|
||||||
canvas.classList.toggle('bocce', !!mg);
|
canvas.classList.toggle('bocce', !!mg);
|
||||||
// Biliardo e GP di Fortezza: audio, pausa e uscita fuori dal quadro (al GP la pista passa negli angoli).
|
// 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', 'cerbottana', 'condor', 'campana'].includes(story.closeups[c.id].type));
|
$('stage').classList.toggle('biliardo', !!c && ['biliardo', 'rinquarto', 'gara', 'micro', 'cerbottana', 'condor', 'campana', 'flipper'].includes(story.closeups[c.id].type));
|
||||||
$('stage').classList.toggle('briscola', !!cards);
|
$('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");
|
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.
|
// La pagina che comincia a girare fa il suo fruscio.
|
||||||
|
|||||||
+27
-1
@@ -360,6 +360,17 @@ export const story = {
|
|||||||
draw: [],
|
draw: [],
|
||||||
quit: [{ say: 'Il gesso lo lascio lì. Ci giocherà qualche bambino, che è più in allenamento di me.' }],
|
quit: [{ say: 'Il gesso lo lascio lì. Ci giocherà qualche bambino, che è più in allenamento di me.' }],
|
||||||
},
|
},
|
||||||
|
// Il flipper del River (Andrea, 31 ottobre: «una tavola bellissima, con San Piero dentro»): la tavola di Codex,
|
||||||
|
// fisica e parti meccaniche in flipper.js. [DA ANDREA: le battute sono provvisorie]
|
||||||
|
flipper: {
|
||||||
|
type: 'flipper',
|
||||||
|
picture: 'assets/closeups/river-flipper.png',
|
||||||
|
names: { player: 'Andrea', cpu: 'il flipper' },
|
||||||
|
win: [{ say: 'Traguardo! Se c\'era la classifica, oggi ci mettevo le mie iniziali.' }, { set: 'flipper_vinto' }],
|
||||||
|
lose: [{ say: 'Tre palline, e tutte nella Sieve. Il flipper del River non perdona.' }],
|
||||||
|
draw: [],
|
||||||
|
quit: [{ say: 'Lascio la pallina lì. Tanto il River non chiude.' }],
|
||||||
|
},
|
||||||
// Le bruciate della Smarronata, 1993: la manovella del cilindro sul fuoco, coi volontari della Misericordia
|
// Le bruciate della Smarronata, 1993: la manovella del cilindro sul fuoco, coi volontari della Misericordia
|
||||||
// (Andrea, 31 ottobre: «a manovella, a mano»). [DA ANDREA: le battute sono provvisorie]
|
// (Andrea, 31 ottobre: «a manovella, a mano»). [DA ANDREA: le battute sono provvisorie]
|
||||||
bruciate: {
|
bruciate: {
|
||||||
@@ -1667,7 +1678,9 @@ export const story = {
|
|||||||
id: 'river', name: 'il River', box: [60, 128, 190, 110], at: [160, 320], face: 'up',
|
id: 'river', name: 'il River', box: [60, 128, 190, 110], at: [160, 320], face: 'up',
|
||||||
actions: {
|
actions: {
|
||||||
look: [{ say: 'Il River, il bar della rotonda. Fuori gli ombrelloni, dentro i flipper e le figurine.' }],
|
look: [{ say: 'Il River, il bar della rotonda. Fuori gli ombrelloni, dentro i flipper e le figurine.' }],
|
||||||
open: [{ say: 'Un ghiacciolo ci starebbe. Ma prima il palio, poi i ghiaccioli.' }],
|
// Dentro, il flipper (Andrea, 31 ottobre): si entra e si sceglie come giocarlo.
|
||||||
|
open: [{ say: 'Un ghiacciolo ci starebbe. Ma prima... il flipper.' }, { dialogue: 'flipper' }],
|
||||||
|
use: [{ say: 'Dentro, in fondo, fra le figurine: il flipper. Con San Piero dipinto sulla tavola.' }, { dialogue: 'flipper' }],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -4440,6 +4453,19 @@ export const story = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
// Da solo al campo del Circolo: Andrea decide quanto farsi male.
|
// Da solo al campo del Circolo: Andrea decide quanto farsi male.
|
||||||
|
// Il flipper del River: Andrea da solo davanti alla tavola, sceglie il livello. [DA ANDREA: battute provvisorie]
|
||||||
|
flipper: {
|
||||||
|
nodes: {
|
||||||
|
start: {
|
||||||
|
options: [
|
||||||
|
{ text: 'Una partita alla bona, tanto per sentire la molla.', next: 'end', then: [{ say: 'Seimila punti e sono contento.' }, { closeup: 'flipper', level: 'bona' }] },
|
||||||
|
{ text: 'Una partita seria.', next: 'end', then: [{ say: 'Dodicimila. Come quando ci lasciavo le cinquecento lire.' }, { closeup: 'flipper', level: 'seria' }] },
|
||||||
|
{ text: 'A fassi male.', next: 'end', then: [{ say: 'Venticinquemila. Il record del River, o quasi.' }, { closeup: 'flipper', level: 'male' }] },
|
||||||
|
{ text: 'Un\'altra volta.', next: 'end' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
basket: {
|
basket: {
|
||||||
nodes: {
|
nodes: {
|
||||||
start: {
|
start: {
|
||||||
|
|||||||
@@ -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. */
|
/* La guida semplice del GP: il Gas si tiene premuto col pollice, più alto. */
|
||||||
#bocce-throw.hold { min-height: 72px; font-size: 14px; }
|
#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, #micro-help, #cerbottana-help, #condor-help, #campana-help, #bruciate-help { margin-top: 9px; color: #c5c6b6; font-size: 10px; }
|
#bocce-help, #biliardo-help, #basket-help, #pesca-help, #gara-help, #rinquarto-help, #micro-help, #cerbottana-help, #condor-help, #campana-help, #bruciate-help, #flipper-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, #condor-help summary, #campana-help summary, #bruciate-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, #micro-help summary, #cerbottana-help, #condor-help summary, #campana-help summary, #bruciate-help summary, #flipper-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, #condor-help p, #campana-help p, #bruciate-help p { max-width: 76em; line-height: 1.7; }
|
#bocce-help p, #biliardo-help p, #basket-help p, #pesca-help p, #gara-help p, #rinquarto-help p, #micro-help p, #cerbottana-help, #condor-help p, #campana-help p, #bruciate-help p, #flipper-help p { max-width: 76em; line-height: 1.7; }
|
||||||
@media (max-width: 600px), (orientation: landscape) and (max-height: 520px) {
|
@media (max-width: 600px), (orientation: landscape) and (max-height: 520px) {
|
||||||
#bocce-controls { padding: 8px; font-size: 10px; overflow-y: auto; min-height: 0; }
|
#bocce-controls { padding: 8px; font-size: 10px; overflow-y: auto; min-height: 0; }
|
||||||
.bocce-adjust { flex-wrap: wrap; gap: 6px; }
|
.bocce-adjust { flex-wrap: wrap; gap: 6px; }
|
||||||
.bocce-adjust label { order: -1; flex-basis: 100%; max-width: none; }
|
.bocce-adjust label { order: -1; flex-basis: 100%; max-width: none; }
|
||||||
.bocce-adjust button { flex: 1; font-size: 9px; padding: 6px; }
|
.bocce-adjust button { flex: 1; font-size: 9px; padding: 6px; }
|
||||||
#bocce-throw { flex-basis: 100%; }
|
#bocce-throw { flex-basis: 100%; }
|
||||||
#bocce-help, #biliardo-help, #basket-help, #pesca-help, #gara-help, #rinquarto-help, #micro-help, #cerbottana-help, #condor-help, #campana-help, #bruciate-help { font-size: 9px; }
|
#bocce-help, #biliardo-help, #basket-help, #pesca-help, #gara-help, #rinquarto-help, #micro-help, #cerbottana-help, #condor-help, #campana-help, #bruciate-help, #flipper-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-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); }
|
#effects-volume { width: 130px; accent-color: var(--accent); }
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
// Il flipper del River nel browser vero: dal bar, la molla col mouse e col dito, i flipper coi pulsanti e con le
|
||||||
|
// frecce, il risultato. npm run browser:flipper — schermate in /tmp/regolo-flipper.
|
||||||
|
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-flipper';
|
||||||
|
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('via_provinciale'); settle(game);
|
||||||
|
game.act('use', 'river'); settle(game);
|
||||||
|
game.choose(game.options().findIndex(o => o.text.startsWith('Una partita alla bona'))); settle(game);
|
||||||
|
});
|
||||||
|
await page.waitForSelector('#bocce-controls:not([hidden])');
|
||||||
|
await page.waitForFunction(async () => (await import('/src/engine/render.js')).hasBackground('assets/closeups/river-flipper.png'));
|
||||||
|
assert.equal(await page.locator('#bocce-throw').textContent(), 'Lancia');
|
||||||
|
assert.ok(await page.locator('#flipper-help').isVisible());
|
||||||
|
assert.equal(await page.locator('#bocce-left').textContent(), '← Flipper');
|
||||||
|
await page.screenshot({ path: `${out}/${name}-tavola.png` });
|
||||||
|
const box = await page.locator('#scene').boundingBox(), P = (x, y) => [box.x + x / 640 * box.width, box.y + y / 360 * box.height];
|
||||||
|
// La molla: col mouse tenendo premuto sulla scena, col dito il pulsante Lancia.
|
||||||
|
if (mobile) await page.locator('#bocce-throw').tap();
|
||||||
|
else {
|
||||||
|
await page.mouse.move(...P(440, 320)); await page.mouse.down();
|
||||||
|
await page.waitForFunction(() => game.closeup.holding && game.closeup.power > 0.3, null, { polling: 'raf' });
|
||||||
|
await page.mouse.up();
|
||||||
|
}
|
||||||
|
await page.waitForFunction(() => game.closeup && !game.closeup.ball?.waiting, null, { timeout: 5000 });
|
||||||
|
// I flipper: col dito i pulsanti tenuti premuti, col mouse la metà della scena e poi le frecce.
|
||||||
|
if (mobile) {
|
||||||
|
const b = await page.locator('#bocce-left').boundingBox();
|
||||||
|
const cdp = await page.context().newCDPSession(page);
|
||||||
|
await cdp.send('Input.dispatchTouchEvent', { type: 'touchStart', touchPoints: [{ x: b.x + b.width / 2, y: b.y + b.height / 2 }] });
|
||||||
|
await page.waitForFunction(() => game.closeup.flippers.left.held.length > 0);
|
||||||
|
await page.screenshot({ path: `${out}/${name}-flipper.png` });
|
||||||
|
await cdp.send('Input.dispatchTouchEvent', { type: 'touchEnd', touchPoints: [] });
|
||||||
|
await page.waitForFunction(() => game.closeup.flippers.left.held.length === 0);
|
||||||
|
} else {
|
||||||
|
await page.mouse.move(...P(400, 200)); await page.mouse.down();
|
||||||
|
await page.waitForFunction(() => game.closeup.flippers.right.held.length > 0);
|
||||||
|
await page.screenshot({ path: `${out}/${name}-flipper.png` });
|
||||||
|
await page.mouse.up();
|
||||||
|
await page.waitForFunction(() => game.closeup.flippers.right.held.length === 0);
|
||||||
|
await page.keyboard.down('ArrowLeft');
|
||||||
|
await page.waitForFunction(() => game.closeup.flippers.left.held.length > 0);
|
||||||
|
await page.keyboard.up('ArrowLeft');
|
||||||
|
await page.waitForFunction(() => game.closeup.flippers.left.held.length === 0);
|
||||||
|
}
|
||||||
|
// La fine: si fa finire la partita e si guarda il quadro.
|
||||||
|
await page.evaluate(() => { const c = game.closeup; c.step = 'result'; c.ball = null; c.result = { winner: 'player', score: 7350, goal: c.skill.goal }; });
|
||||||
|
await page.waitForFunction(() => document.querySelector('#bocce-throw').textContent === 'Finita');
|
||||||
|
assert.match(await page.locator('#bocce-status').textContent(), /7350 punti/);
|
||||||
|
await page.screenshot({ path: `${out}/${name}-fine.png` });
|
||||||
|
await page.locator('#closeup-close').click();
|
||||||
|
await page.waitForFunction(() => game.state.flags.flipper_vinto);
|
||||||
|
await page.close();
|
||||||
|
}
|
||||||
|
assert.deepEqual(errors, []);
|
||||||
|
console.log(`ok: schermate in ${out}`);
|
||||||
|
} finally { await browser?.close(); server.kill(); }
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
import test from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import * as m from '../src/engine/flipper.js';
|
||||||
|
import { drawFlipper } from '../src/engine/flipper-render.js';
|
||||||
|
import { Game } from '../src/engine/game.js';
|
||||||
|
import { story } from '../src/story/story.js';
|
||||||
|
import { settle } from './helpers.mjs';
|
||||||
|
|
||||||
|
const run = (c, secs) => { for (let i = 0; i < secs * 60; i++) m.update(c, 1 / 60); };
|
||||||
|
// Il giocatore automatico: alza il flipper quando la pallina gli scende addosso, come un umano attento.
|
||||||
|
function autoplay(c, secs = 600) {
|
||||||
|
for (let i = 0; i < secs * 60 && c.step === 'play'; i++) {
|
||||||
|
if (m.waiting(c)) m.launch(c, 0.75);
|
||||||
|
const b = c.ball;
|
||||||
|
for (const side of ['left', 'right']) {
|
||||||
|
const [px, py] = m.PIVOT[side], dir = side === 'left' ? 1 : -1, rel = (b ? b.x - px : 0) * dir;
|
||||||
|
const near = b && !b.waiting && b.vy > 0 && b.y > py - 34 && b.y < py + 14 && rel > -6 && rel < 44;
|
||||||
|
if (near !== c.flippers[side].held.includes('ai')) m.hold(c, side, 'ai', near);
|
||||||
|
}
|
||||||
|
m.update(c, 1 / 60);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test('la tavola: tutto sta dentro il campo, le corsie sotto l\'arco, i funghi sui dischi del disegno', () => {
|
||||||
|
const c = m.newFlipper(1);
|
||||||
|
for (const w of c.walls) for (const [x, y] of [w.a, w.b]) assert.ok(x >= m.FIELD.x0 && x <= m.FIELD.x1 && y >= m.FIELD.y0 && y <= m.FIELD.y1, `${w.a}-${w.b}`);
|
||||||
|
for (const b of c.bumpers) assert.ok(b.x > m.FIELD.x0 + 20 && b.x < m.LANE.x0 - 20);
|
||||||
|
assert.equal(c.lanes.length, 4); assert.deepEqual(c.lanes.map(l => l.rione), m.RIONI);
|
||||||
|
assert.equal(c.targets.length, 6); assert.equal(c.balls, m.BALLS);
|
||||||
|
assert.ok(m.waiting(c) && m.canThrow(c) && c.ball.x > m.LANE.x0, 'la pallina aspetta sulla molla');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('il lancio: la molla manda la pallina su per la corsia, oltre il cancelletto, e non torna più nella corsia', () => {
|
||||||
|
const c = m.newFlipper(2, { level: 'bona' });
|
||||||
|
assert.ok(m.launch(c, 0.8)); assert.ok(!m.waiting(c));
|
||||||
|
let top = 999, left = false;
|
||||||
|
for (let i = 0; i < 180; i++) { m.update(c, 1 / 60); top = Math.min(top, c.ball?.y ?? 999); if (c.ball && c.ball.x < m.LANE.x0 - 30) left = true; }
|
||||||
|
assert.ok(top < 40, `è arrivata in cima (${top.toFixed(0)})`);
|
||||||
|
assert.ok(left, 'è uscita dalla corsia');
|
||||||
|
// Un lancio fiacco ricasca sulla molla e si rilancia.
|
||||||
|
const d = m.newFlipper(3, { level: 'bona' }); m.launch(d, 0); run(d, 3);
|
||||||
|
assert.ok(m.waiting(d) && d.balls === m.BALLS, 'tornata sulla molla, nessuna pallina persa');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('la pallina persa: tre palline, poi il risultato; il salvataggio vale una volta sola', () => {
|
||||||
|
const c = m.newFlipper(4, { level: 'seria' });
|
||||||
|
for (let n = 3; n >= 1; n--) {
|
||||||
|
assert.equal(c.balls, n);
|
||||||
|
m.launch(c, 0.9); c.saveUntil = 0; // niente salvataggio: si prova sotto
|
||||||
|
c.ball.x = 320; c.ball.y = 320; c.ball.vx = 0; c.ball.vy = 200; run(c, 1);
|
||||||
|
}
|
||||||
|
assert.equal(c.step, 'result'); assert.equal(c.result.winner, 'cpu'); assert.ok(!m.canThrow(c));
|
||||||
|
assert.match(m.status(c), /Ne servivano 12000/);
|
||||||
|
const d = m.newFlipper(5, { level: 'bona' });
|
||||||
|
m.launch(d, 0.9); d.ball.x = 320; d.ball.y = 320; d.ball.vx = 0; d.ball.vy = 200; run(d, 1);
|
||||||
|
assert.ok(m.waiting(d) && d.balls === 3, 'salvata: la pallina torna sulla molla');
|
||||||
|
assert.match(d.line ?? '', /salvata/);
|
||||||
|
m.launch(d, 0.9); d.ball.x = 320; d.ball.y = 320; d.ball.vx = 0; d.ball.vy = 200; run(d, 1);
|
||||||
|
assert.equal(d.balls, 2, 'la seconda volta è persa davvero');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('il flipper alzato rimanda su la pallina; abbassato la lascia scendere', () => {
|
||||||
|
const up = m.newFlipper(6, { level: 'seria' }); m.launch(up, 0.9);
|
||||||
|
Object.assign(up.ball, { x: 300, y: 300, vx: 0, vy: 150 });
|
||||||
|
m.hold(up, 'left', 'test', true);
|
||||||
|
let best = 0; for (let i = 0; i < 60; i++) { m.update(up, 1 / 60); best = Math.min(best, up.ball?.vy ?? 0); }
|
||||||
|
assert.ok(best < -250, `tirata su (vy ${best.toFixed(0)})`);
|
||||||
|
assert.ok(up.events.some(e => e.kind === 'flipper-flip'));
|
||||||
|
const down = m.newFlipper(6, { level: 'seria' }); m.launch(down, 0.9);
|
||||||
|
Object.assign(down.ball, { x: 300, y: 300, vx: 0, vy: 150 }); down.saveUntil = 0;
|
||||||
|
run(down, 2);
|
||||||
|
assert.equal(down.balls, 2, 'senza flipper, giù nella Sieve');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('funghi, bersagli, Regolo e corsie fanno punti; le quattro corsie insieme il Palio', () => {
|
||||||
|
const c = m.newFlipper(7, { level: 'bona' }); m.launch(c, 0.9);
|
||||||
|
const b = c.bumpers[0];
|
||||||
|
Object.assign(c.ball, { x: b.x, y: b.y + b.r + 6, vx: 0, vy: -200 }); m.update(c, 1 / 60); m.update(c, 1 / 60);
|
||||||
|
assert.equal(c.score, m.SCORE.bumper); assert.ok(c.ball.vy > 0 && b.hit > 0, 'respinta');
|
||||||
|
assert.ok(c.events.some(e => e.kind === 'flipper-fungo'));
|
||||||
|
c.score = 0;
|
||||||
|
for (const t of c.targets.filter(k => k.bank === 'pieve')) { const before = c.score; Object.assign(c.ball, { x: t.a[0] + 10, y: (t.a[1] + t.b[1]) / 2, vx: -300, vy: 0 }); m.update(c, 1 / 60); m.update(c, 1 / 60); assert.ok(c.score > before, `caduto il ${t.i}`); }
|
||||||
|
assert.equal(c.score, 3 * m.SCORE.target + m.SCORE.bank);
|
||||||
|
assert.ok(c.targets.filter(k => k.bank === 'pieve').every(t => !t.down), 'la fila si rialza');
|
||||||
|
assert.match(c.line, /Pieve/);
|
||||||
|
c.score = 0;
|
||||||
|
Object.assign(c.ball, { x: c.regolo.x, y: c.regolo.y + 12, vx: 0, vy: -200 }); m.update(c, 1 / 60); m.update(c, 1 / 60);
|
||||||
|
assert.equal(c.score, m.SCORE.regolo);
|
||||||
|
c.score = 0;
|
||||||
|
for (const l of c.lanes) { Object.assign(c.ball, { x: l.x, y: l.y - 10, vx: 0, vy: 100 }); m.update(c, 1 / 60); m.update(c, 1 / 60); assert.ok(l.lit || c.lanes.every(k => !k.lit), l.rione); }
|
||||||
|
assert.equal(c.score, 4 * m.SCORE.lane + m.SCORE.palio);
|
||||||
|
assert.match(c.line, /PALIO/); assert.ok(c.lanes.every(l => !l.lit), 'si rispegne per un altro giro');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('comandi: tenere premuto carica la molla e lasciare lancia; in gioco la metà della scena è il flipper; tasti', () => {
|
||||||
|
const c = m.newFlipper(8, { level: 'bona' });
|
||||||
|
m.aimScreen(c, [440, 320]); assert.ok(m.press(c, 1)); run(c, 0.6);
|
||||||
|
assert.ok(c.holding && c.power > 0.4 && c.power < 0.6, `molla a ${c.power.toFixed(2)}`);
|
||||||
|
assert.ok(!m.release(c, 2), 'un altro dito non lancia');
|
||||||
|
assert.ok(m.release(c, 1)); assert.ok(!m.waiting(c));
|
||||||
|
m.aimScreen(c, [250, 200]); assert.ok(m.press(c, 3)); assert.ok(c.flippers.left.held.includes(3));
|
||||||
|
m.aimScreen(c, [400, 200]); assert.ok(m.press(c, 4)); assert.ok(c.flippers.right.held.includes(4));
|
||||||
|
assert.ok(m.release(c, 3)); assert.ok(!c.flippers.left.held.length && c.flippers.right.held.includes(4));
|
||||||
|
assert.ok(m.cancel(c, 4)); assert.ok(!c.flippers.right.held.length);
|
||||||
|
// I pulsanti e le frecce.
|
||||||
|
assert.ok(m.turn(c, -1, 'button')); assert.ok(c.flippers.left.held.length); m.stopTurn(c, 'button'); assert.ok(!c.flippers.left.held.length);
|
||||||
|
assert.ok(m.key(c, 'ArrowRight', true)); assert.ok(c.flippers.right.held.length); m.key(c, 'ArrowRight', false); assert.ok(!c.flippers.right.held.length);
|
||||||
|
assert.ok(!m.key(c, 'x', true));
|
||||||
|
// Spazio: carica e lancia; Invio: lancia con la forza del cursore.
|
||||||
|
const d = m.newFlipper(9); assert.ok(m.key(d, ' ', true)); run(d, 0.3); assert.ok(d.holding); m.key(d, ' ', false); assert.ok(!m.waiting(d));
|
||||||
|
const e = m.newFlipper(10); assert.ok(m.setPower(e, 0.5)); assert.equal(m.button(e), 'Lancia'); m.key(e, 'Enter', true); assert.ok(!m.waiting(e)); assert.equal(m.button(e), 'In gioco…');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('giocando con attenzione, alla bona si arriva al traguardo; nessuna partita si incastra', () => {
|
||||||
|
let won = 0;
|
||||||
|
for (let seed = 1; seed <= 8; seed++) {
|
||||||
|
const c = m.newFlipper(seed * 7919, { level: 'bona' });
|
||||||
|
autoplay(c);
|
||||||
|
assert.equal(c.step, 'result', `seme ${seed}: finita`);
|
||||||
|
if (c.result.winner === 'player') won++;
|
||||||
|
}
|
||||||
|
assert.ok(won >= 4, `vinte ${won} su 8`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('il disegno del flipper non cambia lo stato', () => {
|
||||||
|
const calls = [], gradient = { addColorStop() {} };
|
||||||
|
const ctx = new Proxy({}, { get: (o, k) => k in o ? o[k] : (...args) => { calls.push([k, ...args]); return gradient; }, set: (o, k, v) => { o[k] = v; return true; } });
|
||||||
|
const c = m.newFlipper(11, { level: 'bona' }), bg = {};
|
||||||
|
const draw = () => { const before = structuredClone(c); drawFlipper(ctx, c, story.closeups.flipper, 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 === 'FLIPPER DEL RIVER'));
|
||||||
|
m.launch(c, 0.8); run(c, 1); draw();
|
||||||
|
c.step = 'result'; c.result = { winner: 'player', score: 7000, goal: 6000 }; calls.length = 0; draw();
|
||||||
|
assert.ok(calls.some(([op, t]) => op === 'fillText' && t === 'TRAGUARDO FATTO!'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('al River: «Usa» o «Apri» il bar, il livello, la partita; vincendo il flag', () => {
|
||||||
|
const g = new Game(story); g.go('via_provinciale'); settle(g);
|
||||||
|
g.act('use', 'river'); settle(g);
|
||||||
|
assert.ok(g.options().some(o => o.text === 'Una partita seria.'));
|
||||||
|
g.choose(g.options().findIndex(o => o.text === 'Una partita seria.')); settle(g);
|
||||||
|
assert.equal(g.closeup?.id, 'flipper'); assert.equal(g.closeup.level, 'seria');
|
||||||
|
g.closeup.step = 'result'; g.closeup.result = { winner: 'player', score: 13000, goal: 12000 };
|
||||||
|
g.closeupClose(); settle(g);
|
||||||
|
assert.ok(g.state.flags.flipper_vinto);
|
||||||
|
g.act('open', 'river'); settle(g);
|
||||||
|
g.choose(g.options().findIndex(o => o.text === 'A fassi male.')); settle(g);
|
||||||
|
assert.equal(g.closeup?.level, 'male');
|
||||||
|
g.closeupClose();
|
||||||
|
assert.equal(g.queue[0]?.say, story.closeups.flipper.quit[0].say);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user