Il flipper col LOCK (idea di Andrea, fatto da Codex e completato): lo stesso ponte tre volte trattiene la pallina e ne dà una nuova, l'altro ponte SPECIAL vale il JACKPOT e il MULTIBALL a due palline; serratura, seconde palline e SPECIAL nel disegno, suoni, aiuto, test; imbocchi delle rampe al centro e uscite laterali più strette (Codex); pallina premio al traguardo; la barra del traguardo sotto la finestra invece del pannello sul Trebbio; il Boccino prudente in curva
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GiECd9RWGMKV7n3K1mdy8T
This commit is contained in:
@@ -84,6 +84,16 @@ Online su 50037.manzolo.it tutto fino agli effetti ambientali di Codex. Aperto,
|
|||||||
imbocchi svasati allineati alla fisica, frecce animate e targhette RAMPA; funghi con ghiere metalliche,
|
imbocchi svasati allineati alla fisica, frecce animate e targhette RAMPA; funghi con ghiere metalliche,
|
||||||
sponde smaltate, palette avorio con gomma rossa e Regolo verde con corona. Il traguardo e i punti mancanti
|
sponde smaltate, palette avorio con gomma rossa e Regolo verde con corona. Il traguardo e i punti mancanti
|
||||||
restano visibili sotto il display, con una barra di avanzamento. Prove desktop e touch in `browser:flipper`.
|
restano visibili sotto il display, con una barra di avanzamento. Prove desktop e touch in `browser:flipper`.
|
||||||
|
Accessibilità dei tiri (Andrea): imbocchi delle rampe spostati verso il centro e allargati da 22 a 36 pixel
|
||||||
|
(spazio utile alla pallina da 13 a 27; ridotti dai primi 42 perché troppo larghi), pendenza e attrito ridotti per salire con colpi medi. Uscite laterali
|
||||||
|
ristrette da 22/14 a 14/12 pixel, con più spazio nelle corsie di ritorno alle palette. Test sui tiri obliqui,
|
||||||
|
decentrati e fuori bocca a tutti i livelli, e sulle discese laterali recuperate e ancora perdenti.
|
||||||
|
Al traguardo si vince una pallina extra, una sola volta per partita: il tiro prosegue e il premio si aggiunge
|
||||||
|
alla riserva, con avviso sul tabellone. Test a tutti i livelli anche raggiungendo il traguardo con l'ultima pallina.
|
||||||
|
Il LOCK (idea di Andrea, fatto da Codex e completato, 31 ottobre): lo stesso ponte tre volte di fila trattiene la
|
||||||
|
pallina nella serratura in fondo al ponte e ne dà una nuova sulla molla; l'altro ponte si accende SPECIAL; fatto,
|
||||||
|
JACKPOT (100.000), la pallina si libera e si gioca in MULTIBALL (due palline, ognuna coi suoi contatti e la sua scia;
|
||||||
|
persa una, resta l'altra senza scalare). Persa la pallina nuova prima dello SPECIAL, torna quella trattenuta.
|
||||||
[DA ANDREA] un record per livello che resti salvato (suggerito da Codex).
|
[DA ANDREA] un record per livello che resti salvato (suggerito da Codex).
|
||||||
- **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.»
|
||||||
|
|||||||
+6
-3
@@ -310,14 +310,17 @@
|
|||||||
sono i bastioni della Fortezza (1.000 a botta); i bersagli bianchi a sinistra sono la Pieve e quelli gialli a
|
sono i bastioni della Fortezza (1.000 a botta); i bersagli bianchi a sinistra sono la Pieve e quelli gialli a
|
||||||
destra la torre dell'orologio (5.000 l'uno, 25.000 la fila intera); il bersaglio verde con la corona, in alto a
|
destra la torre dell'orologio (5.000 l'uno, 25.000 la fila intera); il bersaglio verde con la corona, in alto a
|
||||||
destra, è il Regolo (10.000). I due ponti: cerca le targhette RAMPA e le frecce illuminate, ambra a sinistra e
|
destra, è il Regolo (10.000). I due ponti: cerca le targhette RAMPA e le frecce illuminate, ambra a sinistra e
|
||||||
azzurre a destra, appena sopra le sponde elastiche. Imboccali dal basso con un colpo deciso: la pallina ci sale sopra e
|
azzurre a destra, verso il centro sopra le sponde elastiche. Gli ingressi larghi accettano anche tiri un po'
|
||||||
|
decentrati: imboccali dal basso con un colpo di media forza, la pallina ci sale sopra e
|
||||||
scende dall'altra parte, nella corsia di ritorno (15.000); l'altro ponte entro dodici secondi: i PONTI, con un
|
scende dall'altra parte, nella corsia di ritorno (15.000); l'altro ponte entro dodici secondi: i PONTI, con un
|
||||||
bonus di 50.000 in più. Il tasto destro del mouse è sempre il flipper di destra.</p>
|
bonus di 50.000 in più. Lo stesso ponte tre volte di fila: LOCK, la pallina resta trattenuta in cima e te ne arriva
|
||||||
|
una nuova sulla molla, mentre l'altro ponte si accende SPECIAL; fallo e incassi il JACKPOT (100.000), la pallina
|
||||||
|
trattenuta si libera e giochi in MULTIBALL con due palline. Il tasto destro del mouse è sempre il flipper di destra.</p>
|
||||||
<p>I flipper: tieni premuto sulla metà sinistra o destra della scena (anche con due dita insieme, o col tasto
|
<p>I flipper: tieni premuto sulla metà sinistra o destra della scena (anche con due dita insieme, o col tasto
|
||||||
destro del mouse per quello di destra), o i pulsanti «← Flipper» e «Flipper →», o le frecce sinistra e destra.
|
destro del mouse per quello di destra), 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 4, a
|
Tre palline; la prima volta, appena lanciata, se la perdi subito torna (alla bona per 8 secondi, seria per 4, a
|
||||||
fassi male per 1). Si vince arrivando al traguardo, anche se poi si esce: alla bona 60.000 punti, seria 120.000,
|
fassi male per 1). Si vince arrivando al traguardo, anche se poi si esce: alla bona 60.000 punti, seria 120.000,
|
||||||
a fassi male 200.000; raggiunto il traguardo, si continua per il record.</p>
|
a fassi male 200.000. Al traguardo vinci una pallina extra, una sola volta per partita, e continui per il record.</p>
|
||||||
</details>
|
</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
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
// ponti (le rampe), flipper, molla e pallina. A sinistra, sul fianco nero del mobile, il tabellone luminoso a punti
|
// ponti (le rampe), flipper, molla e pallina. A sinistra, sul fianco nero del mobile, il tabellone luminoso a punti
|
||||||
// come sui flipper degli anni 2000 (Andrea): le cifre che corrono, le scritte che lampeggiano, le lampadine che
|
// come sui flipper degli anni 2000 (Andrea): le cifre che corrono, le scritte che lampeggiano, le lampadine che
|
||||||
// girano intorno. Niente filtri né passate a tutto schermo (vedi risveglio.js).
|
// girano intorno. Niente filtri né passate a tutto schermo (vedi risveglio.js).
|
||||||
import { FIELD, LANE, R, SCORE, flipperSeg, alongRamp, rampHeight, waiting, canThrow, saveLeft } from './flipper.js';
|
import { FIELD, LANE, R, SCORE, flipperSeg, alongRamp, rampHeight, waiting, canThrow, saveLeft, activeBalls, multiball } from './flipper.js';
|
||||||
import { ambientHash as hash } from './ambient.js';
|
import { ambientHash as hash } from './ambient.js';
|
||||||
import { text, panel } from './bocce-render.js';
|
import { text, panel } from './bocce-render.js';
|
||||||
|
|
||||||
@@ -112,7 +112,7 @@ function rampEntrance(ctx, r, time, who) {
|
|||||||
}
|
}
|
||||||
const rampCache = new Map();
|
const rampCache = new Map();
|
||||||
function ramp(ctx, r, time, who) {
|
function ramp(ctx, r, time, who) {
|
||||||
const state = r.lit > 0 && Math.sin(time * 14) > 0 ? 'lit' : who && who !== r.id && Math.sin(time * 6) > 0 ? 'invite' : 'base';
|
const state = r.lit > 0 && Math.sin(time * 14) > 0 ? 'lit' : (who === r.id + '!' || (who && who !== r.id)) && Math.sin(time * 6) > 0 ? 'invite' : 'base';
|
||||||
if (typeof document === 'undefined') { paintRamp(ctx, r, state); return; }
|
if (typeof document === 'undefined') { paintRamp(ctx, r, state); return; }
|
||||||
const key = `${r.id}:${state}`;
|
const key = `${r.id}:${state}`;
|
||||||
let layer = rampCache.get(key);
|
let layer = rampCache.get(key);
|
||||||
@@ -161,6 +161,14 @@ function plunger(ctx, c) {
|
|||||||
for (let y = top + pull + 2; y < 346; y += 3) ctx.fillRect(Math.round(x) - 4, Math.round(y), 8, 1);
|
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);
|
ctx.fillStyle = '#d9463a'; ctx.fillRect(Math.round(x) - 5, Math.round(top + pull) - 3, 10, 4);
|
||||||
}
|
}
|
||||||
|
// La serratura: la pallina trattenuta in fondo al ponte, dietro due sbarre, con la luce che pulsa.
|
||||||
|
function lock(ctx, b, time) {
|
||||||
|
ball(ctx, b);
|
||||||
|
ctx.strokeStyle = Math.sin(time * 6) > 0 ? '#ffd45a' : '#8a7a5a'; ctx.lineWidth = 1.5;
|
||||||
|
for (const dx of [-3, 3]) { ctx.beginPath(); ctx.moveTo(b.x + dx, b.y - 9); ctx.lineTo(b.x + dx, b.y + 7); ctx.stroke(); }
|
||||||
|
ctx.beginPath(); ctx.moveTo(b.x - 7, b.y - 9); ctx.lineTo(b.x + 7, b.y - 9); ctx.stroke();
|
||||||
|
label(ctx, 'LOCK', b.x, b.y - 15, 5, '#ffe08c');
|
||||||
|
}
|
||||||
function ball(ctx, b) {
|
function ball(ctx, b) {
|
||||||
if (!b) return;
|
if (!b) return;
|
||||||
// Sul ponte la pallina sta più in alto, a seconda della quota: più su, più grande, con l'ombra più lontana.
|
// Sul ponte la pallina sta più in alto, a seconda della quota: più su, più grande, con l'ombra più lontana.
|
||||||
@@ -261,13 +269,11 @@ function board(ctx, c, time, glass) {
|
|||||||
if (visible) dots(ctx, line.slice(0, 12), wx + ww / 2, wy + 31, 2, flashing ? '#fff6c8' : '#f0a24a', 'rgba(255,170,60,0.08)', 'center');
|
if (visible) dots(ctx, line.slice(0, 12), wx + ww / 2, wy + 31, 2, flashing ? '#fff6c8' : '#f0a24a', 'rgba(255,170,60,0.08)', 'center');
|
||||||
if (flashing) regia(ctx, c.line, time - d.flash, wx, wy, ww, wh);
|
if (flashing) regia(ctx, c.line, time - d.flash, wx, wy, ww, wh);
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
// Il traguardo resta leggibile anche mentre il display celebra un colpo.
|
// Il traguardo resta leggibile anche mentre il display celebra un colpo: una barra sottile sotto la finestra e,
|
||||||
|
// nella riga in basso, quanto manca (il pannello che c'era prima copriva il Trebbio dipinto).
|
||||||
const progress = Math.min(1, c.score / c.skill.goal), remaining = Math.max(0, c.skill.goal - c.score);
|
const progress = Math.min(1, c.score / c.skill.goal), remaining = Math.max(0, c.skill.goal - c.score);
|
||||||
panel(ctx, 20, 134, 160, 31);
|
ctx.fillStyle = '#1a1410'; ctx.fillRect(wx, wy + wh + 3, ww, 3);
|
||||||
label(ctx, `OBIETTIVO ${c.skill.goal.toLocaleString('it-IT')}`, 28, 142, 7, '#f1d7a1', 'left');
|
ctx.fillStyle = remaining ? '#edc777' : '#a9e7bf'; ctx.fillRect(wx, wy + wh + 3, Math.round(ww * progress), 3);
|
||||||
label(ctx, remaining ? `MANCANO ${remaining.toLocaleString('it-IT')}` : 'TRAGUARDO RAGGIUNTO', 28, 152, 6, remaining ? '#becbd0' : '#a9e7bf', 'left');
|
|
||||||
ctx.fillStyle = '#30434a'; ctx.fillRect(28, 159, 144, 2);
|
|
||||||
ctx.fillStyle = '#edc777'; ctx.fillRect(28, 159, 144 * progress, 2);
|
|
||||||
// I ponti: due lampade ai lati della finestra (accese se fatte; quella da fare lampeggia).
|
// I ponti: due lampade ai lati della finestra (accese se fatte; quella da fare lampeggia).
|
||||||
c.ramps.forEach((r, i) => disc(ctx, i ? wx + ww + 8 : wx - 8, wy + wh / 2, 6, r.lit > 0 || c.lastRamp?.id === r.id ? '#ffd45a' : c.lastRamp && Math.sin(time * 6) > 0 ? '#fff6c8' : '#2a2420', '#8a7a5a'));
|
c.ramps.forEach((r, i) => disc(ctx, i ? wx + ww + 8 : wx - 8, wy + wh / 2, 6, r.lit > 0 || c.lastRamp?.id === r.id ? '#ffd45a' : c.lastRamp && Math.sin(time * 6) > 0 ? '#fff6c8' : '#2a2420', '#8a7a5a'));
|
||||||
// I cinque dischi in basso: le palline che restano, e i quattro rioni accesi.
|
// I cinque dischi in basso: le palline che restano, e i quattro rioni accesi.
|
||||||
@@ -276,7 +282,7 @@ function board(ctx, c, time, glass) {
|
|||||||
rioni.forEach(([x, y], i) => { const l = c.lanes[i]; disc(ctx, x, y, 11, l.lit ? COLORS[l.rione] : '#2a2420', l.lit ? '#fff4c8' : '#8a7a5a'); });
|
rioni.forEach(([x, y], i) => { const l = c.lanes[i]; disc(ctx, x, y, 11, l.lit ? COLORS[l.rione] : '#2a2420', l.lit ? '#fff4c8' : '#8a7a5a'); });
|
||||||
// Sotto, in piccolo: il salvataggio o la combo.
|
// Sotto, in piccolo: il salvataggio o la combo.
|
||||||
const save = saveLeft(c);
|
const save = saveLeft(c);
|
||||||
const note = save > 0 ? `PALLINA SALVA ${Math.ceil(save)} s` : c.lastRamp ? `L'ALTRO PONTE ENTRO ${Math.ceil(12 - (c.sim - c.lastRamp.at))} s` : c.ball && !c.ball.waiting && c.saved ? 'SENZA RETE' : '';
|
const note = multiball(c) ? 'MULTIBALL!' : c.locked ? 'LOCK: FAI IL PONTE SPECIAL' : c.rampRun?.count ? `PONTE ${c.rampRun.count}/3 PER IL LOCK` : save > 0 ? `PALLINA SALVA ${Math.ceil(save)} s` : c.lastRamp ? `L'ALTRO PONTE ENTRO ${Math.ceil(12 - (c.sim - c.lastRamp.at))} s` : c.ball && !c.ball.waiting && c.saved ? 'SENZA RETE' : remaining ? `MANCANO ${remaining.toLocaleString('it-IT')}` : 'TRAGUARDO: +1 PALLINA';
|
||||||
if (note) text(ctx, note, 100, 352, 6, save > 0 ? '#9fd27a' : '#ffe08c', 'center');
|
if (note) text(ctx, note, 100, 352, 6, save > 0 ? '#9fd27a' : '#ffe08c', 'center');
|
||||||
}
|
}
|
||||||
// La piccola regia del tabellone ai colpi grossi: al traguardo la corona del Regolo che si compone a colonne, al
|
// La piccola regia del tabellone ai colpi grossi: al traguardo la corona del Regolo che si compone a colonne, al
|
||||||
@@ -351,9 +357,9 @@ function shake(c, time) {
|
|||||||
return k > 0 ? [Math.round(Math.sin(time * 90) * 3 * k), Math.round(Math.cos(time * 70) * 2 * k)] : [0, 0];
|
return k > 0 ? [Math.round(Math.sin(time * 90) * 3 * k), Math.round(Math.cos(time * 70) * 2 * k)] : [0, 0];
|
||||||
}
|
}
|
||||||
// La cometa: le ultime posizioni della pallina, sempre più tenui.
|
// La cometa: le ultime posizioni della pallina, sempre più tenui.
|
||||||
function trail(ctx, c) {
|
function trail(ctx, b) {
|
||||||
const n = c.trail.length;
|
const n = b.trail.length;
|
||||||
c.trail.forEach(([x, y], i) => { ctx.globalAlpha = 0.05 + 0.3 * i / n; ctx.fillStyle = '#e8ecf2'; ctx.beginPath(); ctx.arc(x, y, R * (0.3 + 0.6 * i / n), 0, Math.PI * 2); ctx.fill(); });
|
b.trail.forEach(([x, y], i) => { ctx.globalAlpha = 0.05 + 0.3 * i / n; ctx.fillStyle = '#e8ecf2'; ctx.beginPath(); ctx.arc(x, y, R * (0.3 + 0.6 * i / n), 0, Math.PI * 2); ctx.fill(); });
|
||||||
ctx.globalAlpha = 1;
|
ctx.globalAlpha = 1;
|
||||||
}
|
}
|
||||||
function result(ctx, c) {
|
function result(ctx, c) {
|
||||||
@@ -393,12 +399,16 @@ export function drawFlipper(ctx, c, def, bg, glass) {
|
|||||||
for (const r of c.ramps) rampEntrance(ctx, r, time, c.lastRamp?.id);
|
for (const r of c.ramps) rampEntrance(ctx, r, time, c.lastRamp?.id);
|
||||||
for (const side of ['left', 'right']) flipper(ctx, c, side);
|
for (const side of ['left', 'right']) flipper(ctx, c, side);
|
||||||
plunger(ctx, c);
|
plunger(ctx, c);
|
||||||
// La pallina (con la cometa) sotto i ponti, se è sulla tavola; sopra, se è su un ponte; poi gli effetti.
|
// Le palline (con la cometa) sotto i ponti, se sono sulla tavola; sopra, se sono su un ponte; la pallina
|
||||||
if (c.ball && !c.ball.waiting) trail(ctx, c);
|
// trattenuta (LOCK) nella serratura in fondo al suo ponte; poi gli effetti.
|
||||||
if (!c.ball?.ramp) ball(ctx, c.ball);
|
const balls = activeBalls(c), special = c.special ? c.special + '!' : c.lastRamp?.id;
|
||||||
for (const r of c.ramps) ramp(ctx, r, time, c.lastRamp?.id);
|
for (const b of balls) if (!b.waiting && !b.ramp) { trail(ctx, b); ball(ctx, b); }
|
||||||
if (c.ball?.ramp) ball(ctx, c.ball);
|
for (const b of balls) if (b.waiting) ball(ctx, b);
|
||||||
|
for (const r of c.ramps) ramp(ctx, r, time, special);
|
||||||
|
if (c.locked) lock(ctx, c.locked.ball, time);
|
||||||
|
for (const b of balls) if (b.ramp) { trail(ctx, b); ball(ctx, b); }
|
||||||
drawFx(ctx, c, time);
|
drawFx(ctx, c, time);
|
||||||
|
if (c.special) { const r = c.ramps.find(k => k.id === c.special); if (Math.sin(time * 8) > 0) label(ctx, 'SPECIAL', (r.mouth[0][0] + r.mouth[1][0]) / 2, r.mouth[0][1] - 38, 6, '#fff6d6'); }
|
||||||
// Da fermo, l'invito: la molla da caricare.
|
// 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');
|
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, time, glass);
|
board(ctx, c, time, glass);
|
||||||
|
|||||||
+109
-34
@@ -12,7 +12,7 @@ export const FIELD = { x0: 200, x1: 448, y0: 5, y1: 336 }, LANE = { x0: 432, x1:
|
|||||||
export const BALLS = 3, STEP = 1 / 240, MAX_SPEED = 980, CHARGE = 1.2, FLIP = { len: 34, rest: 0.5, up: -0.46, speed: 26, r: 3 };
|
export const BALLS = 3, STEP = 1 / 240, MAX_SPEED = 980, 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 PIVOT = { left: [280, 314], right: [360, 314] };
|
||||||
// I punti, generosi come sui flipper degli anni 2000 (Andrea: «aumenta i punti»).
|
// I punti, generosi come sui flipper degli anni 2000 (Andrea: «aumenta i punti»).
|
||||||
export const SCORE = { bumper: 1000, sling: 500, lane: 2500, palio: 30000, target: 5000, bank: 25000, regolo: 10000, ramp: 15000, ponti: 50000 };
|
export const SCORE = { bumper: 1000, sling: 500, lane: 2500, palio: 30000, target: 5000, bank: 25000, regolo: 10000, ramp: 15000, ponti: 50000, jackpot: 100000 };
|
||||||
// Le rampe (Andrea: «un paio di ponti sui quali sale la pallina»): si entra dal basso con abbastanza velocità, si
|
// Le rampe (Andrea: «un paio di ponti sui quali sale la pallina»): si entra dal basso con abbastanza velocità, si
|
||||||
// sale, si passa sopra la tavola e si scende dall'altra parte. Fatte tutte e due di fila: i PONTI.
|
// sale, si passa sopra la tavola e si scende dall'altra parte. Fatte tutte e due di fila: i PONTI.
|
||||||
export const RAMP = { speed: 240, slope: 155, drag: 20, combo: 12, blend: 30 };
|
export const RAMP = { speed: 240, slope: 155, drag: 20, combo: 12, blend: 30 };
|
||||||
@@ -64,9 +64,9 @@ export function layout() {
|
|||||||
// fondo al percorso, dall'altra parte, dritta nella corsia di ritorno verso il flipper. Si incrociano una volta sola,
|
// fondo al percorso, dall'altra parte, dritta nella corsia di ritorno verso il flipper. Si incrociano una volta sola,
|
||||||
// in cima, come due ponti.
|
// in cima, come due ponti.
|
||||||
const ramps = [
|
const ramps = [
|
||||||
// Imbocchi verso il centro, davanti alle sponde elastiche: 33 pixel utili alla pallina anziché 13.
|
// Imbocchi verso il centro, davanti alle sponde elastiche: 27 pixel utili alla pallina anziché 13.
|
||||||
{ id: 'sinistra', mouth: [[252, 226], [294, 226]], path: [[273, 226], [273, 196], [247, 150], [250, 96], [262, 52], [290, 30], [322, 34], [352, 52], [378, 84], [394, 116], [411, 128], [411, 142]], lit: 0, touching: false },
|
{ id: 'sinistra', mouth: [[255, 226], [291, 226]], path: [[273, 226], [273, 196], [247, 150], [250, 96], [262, 52], [290, 30], [322, 34], [352, 52], [378, 84], [394, 116], [411, 128], [411, 142]], lit: 0, touching: false },
|
||||||
{ id: 'destra', mouth: [[346, 226], [388, 226]], path: [[367, 226], [367, 196], [393, 150], [390, 96], [378, 52], [350, 30], [318, 34], [288, 52], [262, 84], [246, 116], [229, 128], [229, 142]], lit: 0, touching: false },
|
{ id: 'destra', mouth: [[349, 226], [385, 226]], path: [[367, 226], [367, 196], [393, 150], [390, 96], [378, 52], [350, 30], [318, 34], [288, 52], [262, 84], [246, 116], [229, 128], [229, 142]], lit: 0, touching: false },
|
||||||
];
|
];
|
||||||
// Lunghezze, e la cima (il punto più alto del disegno): fin lì si sale, poi si scende.
|
// Lunghezze, e la cima (il punto più alto del disegno): fin lì si sale, poi si scende.
|
||||||
for (const r of ramps) {
|
for (const r of ramps) {
|
||||||
@@ -74,6 +74,8 @@ export function layout() {
|
|||||||
for (let i = 1; i < r.path.length; i++) { r.len += Math.hypot(r.path[i][0] - r.path[i - 1][0], r.path[i][1] - r.path[i - 1][1]); r.cum.push(r.len); }
|
for (let i = 1; i < r.path.length; i++) { r.len += Math.hypot(r.path[i][0] - r.path[i - 1][0], r.path[i][1] - r.path[i - 1][1]); r.cum.push(r.len); }
|
||||||
r.top = r.cum[r.path.reduce((best, p, i) => p[1] < r.path[best][1] ? i : best, 0)];
|
r.top = r.cum[r.path.reduce((best, p, i) => p[1] < r.path[best][1] ? i : best, 0)];
|
||||||
}
|
}
|
||||||
|
// Ogni pallina ricorda i propri contatti: in multiball un urto non cancella quello dell'altra.
|
||||||
|
[...walls, ...targets, ...bumpers, regolo].forEach((o, i) => { o.contactId = i; });
|
||||||
return { walls, targets, bumpers, regolo, lanes, ramps };
|
return { walls, targets, bumpers, regolo, lanes, ramps };
|
||||||
}
|
}
|
||||||
// Il punto a `s` pixel lungo il percorso di una rampa, e la direzione lì.
|
// Il punto a `s` pixel lungo il percorso di una rampa, e la direzione lì.
|
||||||
@@ -89,11 +91,15 @@ export function newFlipper(seed = Date.now() >>> 0, def = {}) {
|
|||||||
const c = { step: 'play', t: 0, sim: 0, seed, level, skill: LEVELS[level], current: 'player', balls: BALLS, score: 0, power: 0.7,
|
const c = { step: 'play', t: 0, sim: 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,
|
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: [], click: 0 }, right: { angle: Math.PI - FLIP.rest, omega: 0, held: [], click: 0 } },
|
flippers: { left: { angle: FLIP.rest, omega: 0, held: [], click: 0 }, right: { angle: Math.PI - FLIP.rest, omega: 0, held: [], click: 0 } },
|
||||||
ball: null, saveUntil: 0, still: 0, anchor: null, rest: 0, resets: {}, result: null, lastRamp: null, lines: [], lineId: 0, fx: [], trail: [], visual: { time: 0 }, ...layout() };
|
ball: null, extraBalls: [], locked: null, special: null, rampRun: { id: null, count: 0 },
|
||||||
|
saveUntil: 0, resets: {}, result: null, lastRamp: null, lines: [], lineId: 0, fx: [], trail: [], visual: { time: 0 }, ...layout() };
|
||||||
newBall(c);
|
newBall(c);
|
||||||
return c;
|
return c;
|
||||||
}
|
}
|
||||||
function newBall(c) { c.ball = { x: (LANE.x0 + LANE.x1) / 2, y: 318, vx: 0, vy: 0, waiting: true, ramp: null }; c.still = 0; c.anchor = null; c.rest = 0; }
|
function makeBall() { return { x: (LANE.x0 + LANE.x1) / 2, y: 318, vx: 0, vy: 0, waiting: true, ramp: null, contacts: [], trail: [], still: 0, anchor: null, rest: 0 }; }
|
||||||
|
function newBall(c) { c.ball = makeBall(); c.trail = c.ball.trail; }
|
||||||
|
export const activeBalls = c => [c.ball, ...c.extraBalls].filter(Boolean);
|
||||||
|
export const multiball = c => c.extraBalls.length > 0;
|
||||||
export const waiting = c => c.step === 'play' && !!c.ball?.waiting;
|
export const waiting = c => c.step === 'play' && !!c.ball?.waiting;
|
||||||
export const canThrow = c => waiting(c) && !c.holding;
|
export const canThrow = c => waiting(c) && !c.holding;
|
||||||
export const canPress = canThrow;
|
export const canPress = canThrow;
|
||||||
@@ -120,6 +126,8 @@ function add(c, points, at) {
|
|||||||
if (at) fx(c, 'pop', at[0], at[1], { text: `+${points}` });
|
if (at) fx(c, 'pop', at[0], at[1], { text: `+${points}` });
|
||||||
if (!c.result && c.score >= c.skill.goal) {
|
if (!c.result && c.score >= c.skill.goal) {
|
||||||
c.result = { winner: 'player', score: c.score, goal: c.skill.goal }; say(c, 'TRAGUARDO!', true);
|
c.result = { winner: 'player', score: c.score, goal: c.skill.goal }; say(c, 'TRAGUARDO!', true);
|
||||||
|
// La vittoria viene acquisita una volta sola: la pallina premio va nella riserva, senza fermare il tiro.
|
||||||
|
c.balls++; c.lines.unshift('PALLINA +1'); fx(c, 'pop', 324, 235, { text: '+1 PALLINA' });
|
||||||
c.events.push({ kind: 'flipper-traguardo', power: 1 }); fx(c, 'burst', 324, 170, { big: true }); fx(c, 'wash', 0, 0, { color: '255,210,80' }); fx(c, 'shake', 0, 0);
|
c.events.push({ kind: 'flipper-traguardo', power: 1 }); fx(c, 'burst', 324, 170, { big: true }); fx(c, 'wash', 0, 0, { color: '255,210,80' }); fx(c, 'shake', 0, 0);
|
||||||
}
|
}
|
||||||
if (c.result) c.result.score = c.score;
|
if (c.result) c.result.score = c.score;
|
||||||
@@ -166,7 +174,10 @@ export function update(c, dt) {
|
|||||||
c.accumulator += dt;
|
c.accumulator += dt;
|
||||||
while (c.accumulator >= STEP) { c.accumulator -= STEP; step(c); }
|
while (c.accumulator >= STEP) { c.accumulator -= STEP; step(c); }
|
||||||
// La scia della pallina: le ultime posizioni, per la cometa.
|
// La scia della pallina: le ultime posizioni, per la cometa.
|
||||||
if (c.ball && !c.ball.waiting) { c.trail.push([c.ball.x, c.ball.y]); if (c.trail.length > 10) c.trail.shift(); } else c.trail.length = 0;
|
for (const b of activeBalls(c)) {
|
||||||
|
if (!b.waiting) { b.trail.push([b.x, b.y]); if (b.trail.length > 10) b.trail.shift(); } else b.trail.length = 0;
|
||||||
|
}
|
||||||
|
c.trail = c.ball?.trail ?? [];
|
||||||
}
|
}
|
||||||
function moveFlippers(c, h) {
|
function moveFlippers(c, h) {
|
||||||
for (const side of ['left', 'right']) {
|
for (const side of ['left', 'right']) {
|
||||||
@@ -180,19 +191,20 @@ function moveFlippers(c, h) {
|
|||||||
}
|
}
|
||||||
function step(c) {
|
function step(c) {
|
||||||
c.sim += STEP;
|
c.sim += STEP;
|
||||||
const ball = c.ball;
|
|
||||||
for (const r of c.ramps) r.lit = Math.max(0, r.lit - STEP);
|
for (const r of c.ramps) r.lit = Math.max(0, r.lit - STEP);
|
||||||
if (c.lastRamp && c.sim - c.lastRamp.at > RAMP.combo) c.lastRamp = null;
|
if (c.lastRamp && c.sim - c.lastRamp.at > RAMP.combo) c.lastRamp = null;
|
||||||
if (c.step !== 'play' || !ball || ball.waiting) { moveFlippers(c, STEP); return; }
|
if (c.step !== 'play') { moveFlippers(c, STEP); return; }
|
||||||
for (const bank of Object.keys(c.resets)) if (c.sim >= c.resets[bank]) { for (const t of c.targets) if (t.bank === bank) t.down = false; delete c.resets[bank]; }
|
for (const bank of Object.keys(c.resets)) if (c.sim >= c.resets[bank]) { for (const t of c.targets) if (t.bank === bank) t.down = false; delete c.resets[bank]; }
|
||||||
if (ball.ramp) { moveFlippers(c, STEP); rideRamp(c, ball); return; }
|
// Una fotografia delle palline all'inizio del passo: quelle appena liberate si muovono dal prossimo.
|
||||||
ball.vy += c.skill.g * STEP;
|
const balls = activeBalls(c), ground = balls.filter(b => !b.waiting && !b.ramp);
|
||||||
limit(ball);
|
const from = new Map(ground.map(b => [b, { x: b.x, y: b.y }]));
|
||||||
// Sottopassi: pallina e punta del flipper insieme non devono fare più di un raggio per passo.
|
for (const b of ground) { b.vy += c.skill.g * STEP; limit(b); }
|
||||||
const tip = FLIP.speed * FLIP.len, n = clamp(Math.ceil((Math.hypot(ball.vx, ball.vy) + tip) * STEP / R), 1, 6), h = STEP / n;
|
const speed = Math.max(0, ...ground.map(b => Math.hypot(b.vx, b.vy)));
|
||||||
const from = { x: ball.x, y: ball.y };
|
const n = clamp(Math.ceil((speed + FLIP.speed * FLIP.len) * STEP / R), 1, 6), h = STEP / n;
|
||||||
|
// Le palette avanzano una sola volta per sottopasso, indipendentemente dal numero di palline.
|
||||||
for (let i = 0; i < n; i++) {
|
for (let i = 0; i < n; i++) {
|
||||||
moveFlippers(c, h);
|
moveFlippers(c, h);
|
||||||
|
for (const ball of ground) {
|
||||||
ball.x += ball.vx * h; ball.y += ball.vy * h;
|
ball.x += ball.vx * h; ball.y += ball.vy * h;
|
||||||
for (const w of c.walls) collideSeg(c, ball, w);
|
for (const w of c.walls) collideSeg(c, ball, w);
|
||||||
for (const t of c.targets) if (!t.down) collideTarget(c, ball, t);
|
for (const t of c.targets) if (!t.down) collideTarget(c, ball, t);
|
||||||
@@ -201,15 +213,34 @@ function step(c) {
|
|||||||
for (const side of ['left', 'right']) collideFlipper(c, ball, side);
|
for (const side of ['left', 'right']) collideFlipper(c, ball, side);
|
||||||
limit(ball);
|
limit(ball);
|
||||||
}
|
}
|
||||||
crossLanes(c, from, ball);
|
for (let a = 0; a < ground.length; a++) for (let b = a + 1; b < ground.length; b++) collideBalls(ground[a], ground[b]);
|
||||||
enterRamps(c, from, ball);
|
}
|
||||||
// La pallina che resta nello stesso punto (ferma, o che trema in un incastro): dopo un po' una scossa. Non se è
|
for (const ball of balls) {
|
||||||
// trattenuta su un flipper alzato: quella è una mossa del giocatore.
|
if (ball.waiting) continue;
|
||||||
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; }
|
if (ball.ramp) { rideRamp(c, ball); continue; }
|
||||||
else if ((c.still += STEP) > NUDGE_AFTER) { c.still = 0; c.anchor = null; if (!cradled(c, ball)) { ball.vx += (random(c) - 0.5) * 160; ball.vy -= 90; } }
|
crossLanes(c, from.get(ball), ball);
|
||||||
// Un lancio fiacco (o una pallina ricaduta nella corsia) si assesta sul fondo e si rilancia.
|
enterRamps(c, from.get(ball), ball);
|
||||||
if (ball.x > LANE.x0 && ball.y > 318 && Math.hypot(ball.vx, ball.vy) < 12) { if ((c.rest += STEP) > 0.3) newBall(c); } else c.rest = 0;
|
// L'anti-incastro è indipendente per ogni pallina; non scuote quelle trattenute sulla paletta.
|
||||||
if (ball.y > FIELD.y1 + R * 2) drain(c);
|
if (!ball.anchor || Math.hypot(ball.x - ball.anchor.x, ball.y - ball.anchor.y) > 3) { ball.anchor = { x: ball.x, y: ball.y }; ball.still = 0; }
|
||||||
|
else if ((ball.still += STEP) > NUDGE_AFTER) { ball.still = 0; ball.anchor = null; if (!cradled(c, ball)) { ball.vx += (random(c) - 0.5) * 160; ball.vy -= 90; } }
|
||||||
|
if (ball.x > LANE.x0 && ball.y > 318 && Math.hypot(ball.vx, ball.vy) < 12) {
|
||||||
|
if ((ball.rest += STEP) > 0.3) {
|
||||||
|
// In multiball una ricaduta nella molla si rilancia da sola: i comandi restano alle palette.
|
||||||
|
if (multiball(c)) { ball.vx = 0; ball.vy = -launchSpeed(c, 0.7); ball.rest = 0; }
|
||||||
|
else newBall(c);
|
||||||
|
}
|
||||||
|
} else ball.rest = 0;
|
||||||
|
if (ball.y > FIELD.y1 + R * 2) drain(c, ball);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function collideBalls(a, b) {
|
||||||
|
let dx = b.x - a.x, dy = b.y - a.y, d = Math.hypot(dx, dy);
|
||||||
|
if (d >= R * 2) return;
|
||||||
|
const nx = d > 1e-6 ? dx / d : 1, ny = d > 1e-6 ? dy / d : 0, push = (R * 2 - d) / 2;
|
||||||
|
a.x -= nx * push; a.y -= ny * push; b.x += nx * push; b.y += ny * push;
|
||||||
|
const speed = (a.vx - b.vx) * nx + (a.vy - b.vy) * ny;
|
||||||
|
if (speed > 0) { const impulse = speed * 0.95; a.vx -= nx * impulse; a.vy -= ny * impulse; b.vx += nx * impulse; b.vy += ny * impulse; }
|
||||||
|
limit(a); limit(b);
|
||||||
}
|
}
|
||||||
function limit(ball) { const v = Math.hypot(ball.vx, ball.vy); if (v > MAX_SPEED) { ball.vx *= MAX_SPEED / v; ball.vy *= MAX_SPEED / v; } }
|
function limit(ball) { const v = Math.hypot(ball.vx, ball.vy); if (v > MAX_SPEED) { ball.vx *= MAX_SPEED / v; ball.vy *= MAX_SPEED / v; } }
|
||||||
const cradled = (c, ball) => ['left', 'right'].some(side => c.flippers[side].held.length && Math.hypot(ball.x - PIVOT[side][0], ball.y - PIVOT[side][1]) < FLIP.len + 2 * R);
|
const cradled = (c, ball) => ['left', 'right'].some(side => c.flippers[side].held.length && Math.hypot(ball.x - PIVOT[side][0], ball.y - PIVOT[side][1]) < FLIP.len + 2 * R);
|
||||||
@@ -256,8 +287,9 @@ function rideRamp(c, ball) {
|
|||||||
// pallina esce sopra uno di loro, non contano come urto nuovo.
|
// pallina esce sopra uno di loro, non contano come urto nuovo.
|
||||||
const v = Math.max(on.v, 150);
|
const v = Math.max(on.v, 150);
|
||||||
ball.vx = p.dx * v; ball.vy = p.dy * v; ball.ramp = null;
|
ball.vx = p.dx * v; ball.vy = p.dy * v; ball.ramp = null;
|
||||||
for (const o of [...c.bumpers, c.regolo, ...c.targets, ...c.walls]) o.touching = true;
|
ball.contacts = [...c.bumpers, c.regolo, ...c.targets, ...c.walls].map(o => o.contactId);
|
||||||
add(c, SCORE.ramp, [p.x, p.y]); r.lit = 3; c.events.push({ kind: 'flipper-bersaglio', power: 0.9 }); fx(c, 'chase', 0, 0, { ramp: r.id });
|
add(c, SCORE.ramp, [p.x, p.y]); r.lit = 3; c.events.push({ kind: 'flipper-bersaglio', power: 0.9 }); fx(c, 'chase', 0, 0, { ramp: r.id });
|
||||||
|
if (rampFeature(c, r, ball)) return;
|
||||||
if (c.lastRamp && c.lastRamp.id !== r.id) {
|
if (c.lastRamp && c.lastRamp.id !== r.id) {
|
||||||
add(c, SCORE.ponti); say(c, 'I PONTI!'); say(c, `BONUS +${SCORE.ponti}`); c.events.push({ kind: 'flipper-ponti', power: 1 }); c.lastRamp = null;
|
add(c, SCORE.ponti); say(c, 'I PONTI!'); say(c, `BONUS +${SCORE.ponti}`); c.events.push({ kind: 'flipper-ponti', power: 1 }); c.lastRamp = null;
|
||||||
fx(c, 'burst', 324, 60, { big: true }); fx(c, 'wash', 0, 0, { color: '120,200,255' }); fx(c, 'shake', 0, 0); fx(c, 'chase', 0, 0, { ramp: c.ramps.find(k => k.id !== r.id).id });
|
fx(c, 'burst', 324, 60, { big: true }); fx(c, 'wash', 0, 0, { color: '120,200,255' }); fx(c, 'shake', 0, 0); fx(c, 'chase', 0, 0, { ramp: c.ramps.find(k => k.id !== r.id).id });
|
||||||
@@ -268,25 +300,68 @@ function rideRamp(c, ball) {
|
|||||||
ball.vx = -p.dx * on.v; ball.vy = Math.max(60, -p.dy * on.v); ball.ramp = null; ball.y = r.mouth[0][1] + 1;
|
ball.vx = -p.dx * on.v; ball.vy = Math.max(60, -p.dy * on.v); ball.ramp = null; ball.y = r.mouth[0][1] + 1;
|
||||||
} else { ball.vx = p.dx * on.v; ball.vy = p.dy * on.v; }
|
} else { ball.vx = p.dx * on.v; ball.vy = p.dy * on.v; }
|
||||||
}
|
}
|
||||||
function drain(c) {
|
// La sequenza considera i passaggi completi: bersagli e rimbalzi non la interrompono, l'altra rampa sì.
|
||||||
|
function rampFeature(c, r, ball) {
|
||||||
|
if (c.locked) {
|
||||||
|
if (c.special !== r.id) { say(c, 'TENTA SPECIAL'); return true; }
|
||||||
|
const released = c.locked.ball;
|
||||||
|
released.vx = 0; released.vy = 170; released.waiting = false;
|
||||||
|
released.trail = []; released.still = 0; released.anchor = null; released.rest = 0;
|
||||||
|
c.extraBalls.push(released); c.locked = null; c.special = null; c.lastRamp = null;
|
||||||
|
c.rampRun = { id: null, count: 0 }; c.saveUntil = 0; c.saved = true;
|
||||||
|
add(c, SCORE.jackpot, [324, 190]); say(c, 'JACKPOT!', c.line !== 'TRAGUARDO!'); say(c, 'MULTIBALL!');
|
||||||
|
c.events.push({ kind: 'flipper-jackpot', power: 1 }); fx(c, 'burst', 324, 130, { big: true }); fx(c, 'shake', 0, 0);
|
||||||
|
for (const ramp of c.ramps) { ramp.lit = 5; fx(c, 'chase', 0, 0, { ramp: ramp.id }); }
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (multiball(c)) return false;
|
||||||
|
c.rampRun = { id: r.id, count: c.rampRun.id === r.id ? c.rampRun.count + 1 : 1 };
|
||||||
|
if (c.rampRun.count < 3) return false;
|
||||||
|
// La pallina resta visibile nella serratura all'uscita della rampa. Il nuovo lancio è gratuito.
|
||||||
|
c.locked = { rampId: r.id, ball }; ball.vx = 0; ball.vy = 0; ball.trail = [];
|
||||||
|
c.special = c.ramps.find(other => other.id !== r.id).id; c.lastRamp = null;
|
||||||
|
c.saveUntil = 0; c.saved = false; c.holding = false; c.holdTime = 0; c.pointerId = null;
|
||||||
|
newBall(c); say(c, 'LOCK!', c.line !== 'TRAGUARDO!'); say(c, 'SPECIAL!');
|
||||||
|
c.events.push({ kind: 'flipper-lock', power: 1 }); fx(c, 'ring', ball.x, ball.y);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
function drain(c, ball) {
|
||||||
c.events.push({ kind: 'flipper-buco', power: 0.8 }); fx(c, 'wash', 0, 0, { color: '40,60,160' });
|
c.events.push({ kind: 'flipper-buco', power: 0.8 }); fx(c, 'wash', 0, 0, { color: '40,60,160' });
|
||||||
|
if (multiball(c)) {
|
||||||
|
if (c.ball === ball) c.ball = c.extraBalls.shift();
|
||||||
|
else c.extraBalls = c.extraBalls.filter(b => b !== ball);
|
||||||
|
c.saveUntil = 0; c.saved = true; c.rampRun = { id: null, count: 0 };
|
||||||
|
say(c, 'UNA IN GIOCO'); return;
|
||||||
|
}
|
||||||
if (c.sim < c.saveUntil) { say(c, 'SALVATA!'); c.saved = true; newBall(c); return; }
|
if (c.sim < c.saveUntil) { say(c, 'SALVATA!'); c.saved = true; newBall(c); return; }
|
||||||
|
c.rampRun = { id: null, count: 0 };
|
||||||
|
if (c.locked) {
|
||||||
|
// Perduta la sostitutiva: si recupera quella trattenuta, senza scalare una vita o lasciare lock sospesi.
|
||||||
|
c.locked = null; c.special = null; c.lastRamp = null; c.saved = false; c.saveUntil = 0;
|
||||||
|
say(c, 'LOCK LIBERO', true); newBall(c); return;
|
||||||
|
}
|
||||||
c.balls--; c.saved = false; c.lastRamp = null;
|
c.balls--; c.saved = false; c.lastRamp = null;
|
||||||
if (c.balls > 0) { say(c, `PALLINE: ${c.balls}`); newBall(c); return; }
|
if (c.balls > 0) { say(c, `PALLINE: ${c.balls}`); newBall(c); return; }
|
||||||
c.ball = null; c.step = 'result';
|
c.ball = null; c.step = 'result';
|
||||||
c.result = { winner: c.score >= c.skill.goal ? 'player' : 'cpu', score: c.score, goal: c.skill.goal };
|
c.result = { winner: c.score >= c.skill.goal ? 'player' : 'cpu', score: c.score, goal: c.skill.goal };
|
||||||
}
|
}
|
||||||
|
function contact(ball, object, hit) {
|
||||||
|
const index = ball.contacts.indexOf(object.contactId), fresh = index < 0;
|
||||||
|
if (hit && fresh) ball.contacts.push(object.contactId);
|
||||||
|
else if (!hit && !fresh) ball.contacts.splice(index, 1);
|
||||||
|
object.touching = hit;
|
||||||
|
return fresh;
|
||||||
|
}
|
||||||
// Pallina contro un segmento: la si spinge fuori e si riflette la velocità. Torna la velocità d'urto lungo la
|
// Pallina contro un segmento: la si spinge fuori e si riflette la velocità. Torna la velocità d'urto lungo la
|
||||||
// normale (positiva se picchiava), o null se non tocca; `touching` ricorda il contatto, per contare solo gli urti nuovi.
|
// normale (positiva se picchiava), o null se non tocca; `touching` ricorda il contatto, per contare solo gli urti nuovi.
|
||||||
function hitSeg(ball, w, e) {
|
function hitSeg(ball, w, e) {
|
||||||
const [ax, ay] = w.a, [bx, by] = w.b, dx = bx - ax, dy = by - ay, len2 = dx * dx + dy * dy || 1;
|
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;
|
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);
|
let nx = ball.x - px, ny = ball.y - py; const d = Math.hypot(nx, ny);
|
||||||
if (d >= R) { w.touching = false; return null; }
|
if (d >= R) { contact(ball, w, false); return null; }
|
||||||
if (d < 1e-6) { nx = -dy; ny = dx; const n = Math.hypot(nx, ny); nx /= n; ny /= n; } else { nx /= d; ny /= d; }
|
if (d < 1e-6) { nx = -dy; ny = dx; const n = Math.hypot(nx, ny); nx /= n; ny /= n; } else { nx /= d; ny /= d; }
|
||||||
ball.x = px + nx * R; ball.y = py + ny * R;
|
ball.x = px + nx * R; ball.y = py + ny * R;
|
||||||
const vn = ball.vx * nx + ball.vy * ny, fresh = !w.touching;
|
const vn = ball.vx * nx + ball.vy * ny, fresh = contact(ball, w, true);
|
||||||
w.touching = true;
|
|
||||||
if (vn < 0 && e !== null) { ball.vx -= (1 + e) * vn * nx; ball.vy -= (1 + e) * vn * ny; }
|
if (vn < 0 && e !== null) { ball.vx -= (1 + e) * vn * nx; ball.vy -= (1 + e) * vn * ny; }
|
||||||
return { vn: -vn, nx, ny, fresh };
|
return { vn: -vn, nx, ny, fresh };
|
||||||
}
|
}
|
||||||
@@ -313,11 +388,10 @@ function collideTarget(c, ball, t) {
|
|||||||
// Pallina contro un cerchio: spinta fuori; torna la velocità d'urto lungo la normale (null se non tocca).
|
// Pallina contro un cerchio: spinta fuori; torna la velocità d'urto lungo la normale (null se non tocca).
|
||||||
function hitCircle(ball, o, e) {
|
function hitCircle(ball, o, e) {
|
||||||
const dx = ball.x - o.x, dy = ball.y - o.y, d = Math.hypot(dx, dy), min = R + o.r;
|
const dx = ball.x - o.x, dy = ball.y - o.y, d = Math.hypot(dx, dy), min = R + o.r;
|
||||||
if (d >= min) { o.touching = false; return null; }
|
if (d >= min) { contact(ball, o, false); return null; }
|
||||||
const nx = d > 1e-6 ? dx / d : 0, ny = d > 1e-6 ? dy / d : -1;
|
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;
|
ball.x = o.x + nx * min; ball.y = o.y + ny * min;
|
||||||
const vn = ball.vx * nx + ball.vy * ny, fresh = !o.touching;
|
const vn = ball.vx * nx + ball.vy * ny, fresh = contact(ball, o, true);
|
||||||
o.touching = true;
|
|
||||||
if (vn < 0) { ball.vx -= (1 + e) * vn * nx; ball.vy -= (1 + e) * vn * ny; }
|
if (vn < 0) { ball.vx -= (1 + e) * vn * nx; ball.vy -= (1 + e) * vn * ny; }
|
||||||
return { vn: -vn, nx, ny, fresh };
|
return { vn: -vn, nx, ny, fresh };
|
||||||
}
|
}
|
||||||
@@ -390,8 +464,9 @@ export const shoot = c => canThrow(c) && launch(c, c.power);
|
|||||||
export const button = c => c.step === 'result' ? 'Finita' : waiting(c) ? 'Lancia' : 'In gioco…';
|
export const button = c => c.step === 'result' ? 'Finita' : waiting(c) ? 'Lancia' : 'In gioco…';
|
||||||
export function status(c) {
|
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}.`}`;
|
if (c.step === 'result') return `${c.result.score} punti. ${c.result.winner === 'player' ? `Traguardo ${c.result.goal}: fatto!` : `Ne servivano ${c.result.goal}.`}`;
|
||||||
const goal = c.result ? 'traguardo fatto, ora il record' : `traguardo ${c.skill.goal}`;
|
const goal = c.result ? 'traguardo fatto, pallina premio vinta, ora il record' : `traguardo ${c.skill.goal}`;
|
||||||
const tally = `Punti ${c.score} (${goal}), palline ${c.balls}.`;
|
const feature = c.locked ? `LOCK: pallina trattenuta. Rampa ${c.special === 'sinistra' ? 'sinistra' : 'destra'} SPECIAL: jackpot ${SCORE.jackpot} e multiball.` : multiball(c) ? 'MULTIBALL: due palline in gioco!' : c.rampRun.count ? `Rampa ${c.rampRun.id}: ${c.rampRun.count}/3 per il LOCK.` : '';
|
||||||
|
const tally = `Punti ${c.score} (${goal}), palline ${c.balls}.${feature ? ' ' + feature : ''}`;
|
||||||
if (c.line) return `${c.line} ${tally}`;
|
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}`;
|
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}`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -171,6 +171,8 @@ export class Soundscape {
|
|||||||
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-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-ponti') for (const [i, f] of [392, 523, 659, 784, 1046, 1318].entries()) this.tone(f, f, 0.16, 0.06, i * 0.08, pan);
|
else if (kind === 'flipper-ponti') for (const [i, f] of [392, 523, 659, 784, 1046, 1318].entries()) this.tone(f, f, 0.16, 0.06, i * 0.08, pan);
|
||||||
else if (kind === 'flipper-traguardo') { for (const [i, f] of [523, 659, 784, 1046].entries()) this.tone(f, f, 0.12, 0.06, i * 0.09, pan); for (const [i, f] of [784, 1046, 1318].entries()) this.tone(f, f * 1.01, 0.5, 0.05, 0.4 + i * 0.03, pan); }
|
else if (kind === 'flipper-traguardo') { for (const [i, f] of [523, 659, 784, 1046].entries()) this.tone(f, f, 0.12, 0.06, i * 0.09, pan); for (const [i, f] of [784, 1046, 1318].entries()) this.tone(f, f * 1.01, 0.5, 0.05, 0.4 + i * 0.03, pan); }
|
||||||
|
else if (kind === 'flipper-lock') { this.tone(330, 330, 0.25, 0.06, 0, pan); this.tone(220, 220, 0.4, 0.06, 0.2, pan); this.noiseSound(0.08, 1200, 0.05, { pan }); }
|
||||||
|
else if (kind === 'flipper-jackpot') { for (const [i, f] of [523, 659, 784, 1046, 1318, 1568].entries()) this.tone(f, f, 0.14, 0.06, i * 0.07, pan); for (const [i, f] of [1046, 1318, 1568].entries()) this.tone(f, f * 1.01, 0.7, 0.05, 0.5 + i * 0.04, pan); }
|
||||||
else if (kind === 'flipper-rampa') { this.noiseSound(0.25, 2400, 0.04, { pan }); this.tone(300, 900, 0.3, 0.04, 0, pan); }
|
else if (kind === 'flipper-rampa') { this.noiseSound(0.25, 2400, 0.04, { pan }); this.tone(300, 900, 0.3, 0.04, 0, 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 === '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); }
|
||||||
|
|||||||
+2
-2
@@ -24,8 +24,8 @@ export const piloti = [
|
|||||||
// Yuri de' Motori (Juri Lascialfari, consenso del 28 ottobre) col suo Yamaha Spy bianco e rosso, il «Mazinga Spy» (foto di Andrea).
|
// Yuri de' Motori (Juri Lascialfari, consenso del 28 ottobre) col suo Yamaha Spy bianco e rosso, il «Mazinga Spy» (foto di Andrea).
|
||||||
{ id: 'yuri', name: 'Yuri de\' Motori', bike: 'il Mazinga Spy', motore: 3, testa: 5, color: '#f0f0ee', trim: '#d8302a', moto: 'assets/closeups/gp/spy.png', face: 'assets/closeups/gp/yuri.png', consenso: '2026-10-28' },
|
{ id: 'yuri', name: 'Yuri de\' Motori', bike: 'il Mazinga Spy', motore: 3, testa: 5, color: '#f0f0ee', trim: '#d8302a', moto: 'assets/closeups/gp/spy.png', face: 'assets/closeups/gp/yuri.png', consenso: '2026-10-28' },
|
||||||
// Il Boccino (Gabriele Chiesi, consenso del 31 ottobre) col suo Piaggio NRG bianco e rosso col numero 1 (foto di
|
// Il Boccino (Gabriele Chiesi, consenso del 31 ottobre) col suo Piaggio NRG bianco e rosso col numero 1 (foto di
|
||||||
// Andrea); il ritratto di Codex dalla sua foto. [DA ANDREA: motore e testa]
|
// Andrea); il ritratto di Codex dalla sua foto. In curva prudente (Andrea, 31 ottobre). [DA ANDREA: il motore]
|
||||||
{ id: 'boccino', name: 'Il Boccino', bike: 'l\'NRG numero 1', motore: 3, testa: 3, color: '#f4f4f2', trim: '#d8302a', helmet: '#d8302a', stripe: '#f4f4f2', moto: 'assets/closeups/gp/nrg.png', face: 'assets/closeups/gp/boccino.png', consenso: '2026-10-31' },
|
{ id: 'boccino', name: 'Il Boccino', bike: 'l\'NRG numero 1', motore: 3, testa: 5, color: '#f4f4f2', trim: '#d8302a', helmet: '#d8302a', stripe: '#f4f4f2', moto: 'assets/closeups/gp/nrg.png', face: 'assets/closeups/gp/boccino.png', consenso: '2026-10-31' },
|
||||||
// Il Gianno (il Giannini, consenso del 28 ottobre) col suo Piaggio Typhoon rosso e nero (foto di Andrea).
|
// Il Gianno (il Giannini, consenso del 28 ottobre) col suo Piaggio Typhoon rosso e nero (foto di Andrea).
|
||||||
{ id: 'gianno', name: 'Il Gianno', bike: 'il Typhoon', motore: 3, testa: 3, color: '#9e1b22', trim: '#151515', moto: 'assets/closeups/gp/typhoon.png', face: 'assets/closeups/gp/gianno.png', consenso: '2026-10-28' },
|
{ id: 'gianno', name: 'Il Gianno', bike: 'il Typhoon', motore: 3, testa: 3, color: '#9e1b22', trim: '#151515', moto: 'assets/closeups/gp/typhoon.png', face: 'assets/closeups/gp/gianno.png', consenso: '2026-10-28' },
|
||||||
// La Brauda (Giody Brunetti, consenso del 28 ottobre) col suo Aprilia Rally nero (foto di Andrea). [DA ANDREA: motore e testa]
|
// La Brauda (Giody Brunetti, consenso del 28 ottobre) col suo Aprilia Rally nero (foto di Andrea). [DA ANDREA: motore e testa]
|
||||||
|
|||||||
@@ -83,10 +83,31 @@ try {
|
|||||||
await page.click('#resume');
|
await page.click('#resume');
|
||||||
await page.waitForFunction(() => document.querySelector('#pause').hidden);
|
await page.waitForFunction(() => document.querySelector('#pause').hidden);
|
||||||
}
|
}
|
||||||
// La fine: si fa finire la partita e si guarda il quadro.
|
// Il traguardo aggiunge una pallina e lascia continuare la partita.
|
||||||
await page.evaluate(() => { const c = game.closeup; c.score = 7350; c.step = 'result'; c.ball = null; c.result = { winner: 'player', score: 7350, goal: c.skill.goal }; });
|
const reward = await page.evaluate(async () => {
|
||||||
|
const m = await import('/src/engine/flipper.js'), c = game.closeup;
|
||||||
|
m.cancel(c); if (m.waiting(c)) m.launch(c, 0.8);
|
||||||
|
c.balls = 1; c.score = c.skill.goal - m.SCORE.bumper; c.saveUntil = 0;
|
||||||
|
const b = c.bumpers[0]; b.touching = false;
|
||||||
|
Object.assign(c.ball, { x: b.x, y: b.y + b.r + 6, vx: 0, vy: -200, waiting: false, ramp: null });
|
||||||
|
m.update(c, 0.05);
|
||||||
|
return { balls: c.balls, step: c.step, winner: c.result?.winner };
|
||||||
|
});
|
||||||
|
assert.deepEqual(reward, { balls: 2, step: 'play', winner: 'player' });
|
||||||
|
await page.waitForFunction(() => document.querySelector('#bocce-status').textContent.includes('pallina premio vinta'));
|
||||||
|
await page.screenshot({ path: `${out}/${name}-premio.png` });
|
||||||
|
// Si consumano le palline rimaste, compresa quella vinta, per arrivare al risultato reale.
|
||||||
|
const finalScore = await page.evaluate(async () => {
|
||||||
|
const m = await import('/src/engine/flipper.js'), c = game.closeup;
|
||||||
|
for (let i = 0; i < 4 && c.step === 'play'; i++) {
|
||||||
|
if (m.waiting(c)) m.launch(c, 0.8);
|
||||||
|
c.saveUntil = 0;
|
||||||
|
Object.assign(c.ball, { x: 320, y: 342, vx: 0, vy: 300, ramp: null }); m.update(c, 0.1);
|
||||||
|
}
|
||||||
|
return c.score;
|
||||||
|
});
|
||||||
await page.waitForFunction(() => document.querySelector('#bocce-throw').textContent === 'Finita');
|
await page.waitForFunction(() => document.querySelector('#bocce-throw').textContent === 'Finita');
|
||||||
assert.match(await page.locator('#bocce-status').textContent(), /7350 punti/);
|
assert.ok((await page.locator('#bocce-status').textContent()).includes(`${finalScore} punti`));
|
||||||
await page.screenshot({ path: `${out}/${name}-fine.png` });
|
await page.screenshot({ path: `${out}/${name}-fine.png` });
|
||||||
if (mobile) await page.locator('#closeup-close').click(); else await page.keyboard.press('Enter'); // Invio chiude il risultato
|
if (mobile) await page.locator('#closeup-close').click(); else await page.keyboard.press('Enter'); // Invio chiude il risultato
|
||||||
await page.waitForFunction(() => game.state.flags.flipper_vinto && !game.closeup);
|
await page.waitForFunction(() => game.state.flags.flipper_vinto && !game.closeup);
|
||||||
|
|||||||
@@ -170,6 +170,27 @@ test('il traguardo è acquisito appena raggiunto: anche uscendo prima, si vince;
|
|||||||
assert.ok(h.ball && !h.ball.waiting && h.ball.y > 280 && h.ball.x < 320, `ancora sul flipper (${h.ball?.x.toFixed(0)}, ${h.ball?.y.toFixed(0)})`);
|
assert.ok(h.ball && !h.ball.waiting && h.ball.y > 280 && h.ball.x < 320, `ancora sul flipper (${h.ball?.x.toFixed(0)}, ${h.ball?.y.toFixed(0)})`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('il traguardo assegna una sola pallina premio, utilizzabile anche se si era all\'ultima, a tutti i livelli', () => {
|
||||||
|
for (const level of Object.keys(m.LEVELS)) for (const missing of [m.SCORE.bumper, 500]) {
|
||||||
|
const c = m.newFlipper(85, { level }); m.launch(c, 0.8);
|
||||||
|
c.balls = 1; c.score = c.skill.goal - missing;
|
||||||
|
const currentBall = c.ball, b = c.bumpers[0];
|
||||||
|
put(c, { x: b.x, y: b.y + b.r + 6, vx: 0, vy: -200 }); run(c, 0.05);
|
||||||
|
assert.equal(c.balls, 2, `${level}: una pallina in più, raggiungendo o superando la soglia`);
|
||||||
|
assert.equal(c.ball, currentBall, 'il tiro in corso prosegue');
|
||||||
|
assert.equal(c.step, 'play'); assert.equal(c.result.winner, 'player');
|
||||||
|
assert.equal(c.line, 'TRAGUARDO!'); assert.equal(c.lines[0], 'PALLINA +1');
|
||||||
|
assert.match(m.status(c), /pallina premio vinta/);
|
||||||
|
put(c, { x: c.regolo.x, y: c.regolo.y + 12, vx: 0, vy: -200 }); run(c, 0.05);
|
||||||
|
assert.ok(c.score > c.skill.goal); assert.equal(c.balls, 2, 'altri punti non assegnano altri premi');
|
||||||
|
c.saveUntil = 0; put(c, { x: 320, y: 342, vx: 0, vy: 300, ramp: null }); run(c, 0.1);
|
||||||
|
assert.equal(c.balls, 1); assert.ok(m.waiting(c), 'persa l\'ultima originale, si può lanciare la pallina premio');
|
||||||
|
m.launch(c, 0.8); c.saveUntil = 0;
|
||||||
|
put(c, { x: 320, y: 342, vx: 0, vy: 300 }); run(c, 0.1);
|
||||||
|
assert.equal(c.balls, 0); assert.equal(c.step, 'result'); assert.equal(c.result.winner, 'player');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test('i ponti: imboccati forte si sale, si passa sopra la tavola e si scende nella corsia di ritorno; piano si ricasca; di traverso no; tutti e due di fila, i PONTI', () => {
|
test('i ponti: imboccati forte si sale, si passa sopra la tavola e si scende nella corsia di ritorno; piano si ricasca; di traverso no; tutti e due di fila, i PONTI', () => {
|
||||||
const c = m.newFlipper(13, { level: 'bona' }); m.launch(c, 0.9); c.score = 0;
|
const c = m.newFlipper(13, { level: 'bona' }); m.launch(c, 0.9); c.score = 0;
|
||||||
const { frames: onRamp, top, exit } = ride(c, { x: 273, y: 240, vx: 0, vy: -470 });
|
const { frames: onRamp, top, exit } = ride(c, { x: 273, y: 240, vx: 0, vy: -470 });
|
||||||
@@ -201,6 +222,40 @@ test('i ponti: imboccati forte si sale, si passa sopra la tavola e si scende nel
|
|||||||
assert.equal(g.balls, 2); assert.equal(g.lastRamp, null, 'azzerata con la pallina persa');
|
assert.equal(g.balls, 2); assert.equal(g.lastRamp, null, 'azzerata con la pallina persa');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('le rampe centrali si prendono con tiri obliqui dalle palette e anche fuori centro, a tutti i livelli', () => {
|
||||||
|
for (const level of Object.keys(m.LEVELS)) for (const [index, side] of [[0, -1], [1, 1]]) {
|
||||||
|
const c = m.newFlipper(81, { level }); m.launch(c, 0.8);
|
||||||
|
ride(c, { x: 320 + side * 20, y: 290, vx: side * 90, vy: -460 });
|
||||||
|
assert.equal(c.score, m.SCORE.ramp, `${level}: tiro obliquo dalla zona delle palette, lato ${side}`);
|
||||||
|
assert.ok(c.ramps[index].lit > 0);
|
||||||
|
for (const offset of [-12, 12]) {
|
||||||
|
const d = m.newFlipper(82, { level }); m.launch(d, 0.8);
|
||||||
|
const r = d.ramps[index];
|
||||||
|
ride(d, { x: r.path[0][0] + offset, y: r.mouth[0][1] + 1, vx: 0, vy: -330 });
|
||||||
|
assert.equal(d.score, m.SCORE.ramp, `${level}: basta un colpo medio, decentrato di ${offset}`);
|
||||||
|
}
|
||||||
|
const e = m.newFlipper(83, { level }); m.launch(e, 0.8);
|
||||||
|
const r = e.ramps[index];
|
||||||
|
put(e, { x: r.mouth[0][0] + m.R - 1, y: r.mouth[0][1] + 1, vx: 0, vy: -330 }); run(e, 0.05);
|
||||||
|
assert.equal(e.ball.ramp, null, 'fuori dalla bocca disegnata la pallina non viene catturata');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('le uscite laterali sono più strette: i tiri vicini rientrano verso le palette, quelli esterni si perdono ancora', () => {
|
||||||
|
for (const [x, safe] of [[218, true], [419, true], [207, false], [426, false]]) {
|
||||||
|
const c = m.newFlipper(84); m.launch(c, 0.8); c.saveUntil = 0;
|
||||||
|
put(c, { x, y: 250, vx: 0, vy: 100 });
|
||||||
|
let lower = null;
|
||||||
|
for (let i = 0; i < 2 / m.STEP && c.balls === 3; i++) {
|
||||||
|
m.update(c, m.STEP);
|
||||||
|
if (!lower && c.ball?.y >= 300) lower = { x: c.ball.x };
|
||||||
|
}
|
||||||
|
assert.ok(lower, `la pallina ${x} non resta incastrata nella corsia`);
|
||||||
|
if (safe) assert.ok(lower.x > 250 && lower.x < 390, `${x} torna verso le palette (${lower.x})`);
|
||||||
|
else { assert.equal(lower.x, x, 'la corsia esterna resta aperta'); assert.equal(c.balls, 2); }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test('gli effetti: ogni colpo lascia il suo segno (anello, scintille, punti che volano, coriandoli, luci sui ponti, scossa), e dopo un po\' sparisce', () => {
|
test('gli effetti: ogni colpo lascia il suo segno (anello, scintille, punti che volano, coriandoli, luci sui ponti, scossa), e dopo un po\' sparisce', () => {
|
||||||
const c = m.newFlipper(17, { level: 'bona' }); m.launch(c, 0.9);
|
const c = m.newFlipper(17, { level: 'bona' }); m.launch(c, 0.9);
|
||||||
const kinds = () => c.fx.map(f => f.kind);
|
const kinds = () => c.fx.map(f => f.kind);
|
||||||
@@ -222,6 +277,43 @@ test('gli effetti: ogni colpo lascia il suo segno (anello, scintille, punti che
|
|||||||
assert.ok(calls.some(([op]) => op === 'translate'), 'la scossa sposta la scena');
|
assert.ok(calls.some(([op]) => op === 'translate'), 'la scossa sposta la scena');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('il LOCK: lo stesso ponte tre volte di fila trattiene la pallina e ne dà una nuova; l\'altro ponte SPECIAL vale il jackpot e il multiball', () => {
|
||||||
|
const c = m.newFlipper(21, { level: 'bona' }); m.launch(c, 0.9); c.score = 0;
|
||||||
|
const left = c.ramps[0], right = c.ramps[1];
|
||||||
|
// Un tiro nel ponte: si aspetta che la pallina esca in fondo, prima che vada altrove (così il conto non si azzera).
|
||||||
|
const shoot = r => { const [[x0], [x1]] = r.mouth; put(c, { x: (x0 + x1) / 2, y: r.mouth[0][1] + 14, vx: 0, vy: -470, ramp: null }); let was = false; for (let i = 0; i < 240; i++) { m.update(c, 1 / 60); if (c.ball?.ramp) was = true; else if (was) break; } };
|
||||||
|
shoot(left); assert.equal(c.rampRun.count, 1); assert.ok(!c.locked);
|
||||||
|
shoot(left); assert.equal(c.rampRun.count, 2);
|
||||||
|
shoot(left);
|
||||||
|
assert.ok(c.locked && c.locked.rampId === 'sinistra', 'la pallina è trattenuta');
|
||||||
|
assert.equal(c.special, 'destra', 'l\'altro ponte è SPECIAL');
|
||||||
|
assert.ok(m.waiting(c) && c.ball !== c.locked.ball, 'una pallina nuova sulla molla');
|
||||||
|
assert.ok([c.line, ...c.lines].some(l => /LOCK/.test(l ?? '')) && [c.line, ...c.lines].some(l => /SPECIAL/.test(l ?? '')));
|
||||||
|
assert.ok(c.events.some(e => e.kind === 'flipper-lock'));
|
||||||
|
assert.equal(c.balls, 3, 'il lock non costa una pallina');
|
||||||
|
// Il ponte sbagliato non libera niente.
|
||||||
|
m.launch(c, 0.9); c.events.length = 0; shoot(left);
|
||||||
|
assert.ok(c.locked, 'ancora trattenuta');
|
||||||
|
// Il ponte SPECIAL: jackpot, la pallina liberata, due in gioco.
|
||||||
|
const before = c.score; shoot(right);
|
||||||
|
assert.ok(c.score - before >= m.SCORE.jackpot + m.SCORE.ramp, `jackpot (${c.score - before})`);
|
||||||
|
assert.ok(!c.locked && !c.special, 'liberata');
|
||||||
|
assert.ok(m.multiball(c) && m.activeBalls(c).length === 2, 'multiball');
|
||||||
|
assert.ok(c.events.some(e => e.kind === 'flipper-jackpot'));
|
||||||
|
// In multiball perdere una pallina non costa: resta l'altra; persa anche quella, si scala.
|
||||||
|
const riserva = c.balls; // (il jackpot ha passato il traguardo: una pallina premio in più)
|
||||||
|
Object.assign(c.ball, { x: 320, y: 342, vx: 0, vy: 300, waiting: false, ramp: null }); run(c, 0.5);
|
||||||
|
assert.ok(!m.multiball(c) && c.balls === riserva && m.activeBalls(c).length === 1, 'una in gioco, nessuna persa');
|
||||||
|
c.saveUntil = 0; Object.assign(c.ball, { x: 320, y: 342, vx: 0, vy: 300, waiting: false, ramp: null }); run(c, 0.5);
|
||||||
|
assert.equal(c.balls, riserva - 1, 'ora sì');
|
||||||
|
// Persa la pallina nuova prima dello SPECIAL: quella trattenuta torna, senza scalare.
|
||||||
|
const d = m.newFlipper(22, { level: 'bona' }); m.launch(d, 0.9);
|
||||||
|
const go = r => { const [[x0], [x1]] = r.mouth; put(d, { x: (x0 + x1) / 2, y: r.mouth[0][1] + 14, vx: 0, vy: -470, ramp: null }); let was = false; for (let i = 0; i < 240; i++) { m.update(d, 1 / 60); if (d.ball?.ramp) was = true; else if (was) break; } };
|
||||||
|
go(d.ramps[0]); go(d.ramps[0]); go(d.ramps[0]); assert.ok(d.locked);
|
||||||
|
m.launch(d, 0.9); d.saveUntil = 0; Object.assign(d.ball, { x: 320, y: 342, vx: 0, vy: 300, waiting: false, ramp: null }); run(d, 0.5);
|
||||||
|
assert.ok(!d.locked && d.balls === 3 && m.waiting(d), 'lock libero, pallina di nuovo sulla molla');
|
||||||
|
});
|
||||||
|
|
||||||
test('giocando con attenzione, alla bona si arriva al traguardo; nessuna partita si incastra', () => {
|
test('giocando con attenzione, alla bona si arriva al traguardo; nessuna partita si incastra', () => {
|
||||||
let won = 0;
|
let won = 0;
|
||||||
for (let seed = 1; seed <= 8; seed++) {
|
for (let seed = 1; seed <= 8; seed++) {
|
||||||
|
|||||||
Reference in New Issue
Block a user