Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
428fb4bbec | ||
|
|
14af51668f | ||
|
|
3bae622c2b | ||
|
|
a8b7d599c2 | ||
|
|
26da5030a6 | ||
|
|
a9f8f88010 | ||
|
|
f3d3fa1b00 | ||
|
|
f7aa92acc6 | ||
|
|
7045aa9fcc | ||
|
|
efff8623d8 | ||
|
|
6fdbef1dd7 | ||
|
|
a926ad6423 | ||
|
|
8319f58f1e | ||
|
|
21cfe8bb89 | ||
|
|
b5a38531c4 | ||
|
|
666e274140 |
@@ -1,11 +1,13 @@
|
|||||||
# Le Gitea Actions del gioco: girano sul runner del PC di Andrea (make runner; label ubuntu-latest = node:22-bookworm).
|
# Le Gitea Actions del gioco: girano sul runner del PC di Andrea (make runner; label ubuntu-latest = node:22-bookworm).
|
||||||
# A ogni push su main e a ogni tag i test unitari; su un tag vX.Y.Z, se passano, la release con i commit dal tag prima.
|
# A ogni push su main e a ogni tag i test unitari; su un tag vX.Y.Z (o a mano, «Run workflow») anche le venti prove nel
|
||||||
|
# browser, una per job nell'immagine di Playwright, sulla scheda video del PC; se passa tutto, la release.
|
||||||
# Il controllo dei file pubblicabili non sta qui (a pipeline partita il push è già pubblico): è l'hook pre-push.
|
# Il controllo dei file pubblicabili non sta qui (a pipeline partita il push è già pubblico): è l'hook pre-push.
|
||||||
name: test
|
name: test
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [main]
|
branches: [main]
|
||||||
tags: ['v*']
|
tags: ['v*']
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
@@ -15,8 +17,29 @@ jobs:
|
|||||||
- name: Test unitari
|
- name: Test unitari
|
||||||
run: node --version && node --test tests/*.test.mjs
|
run: node --version && node --test tests/*.test.mjs
|
||||||
|
|
||||||
release:
|
# Le prove nel browser, una per job: l'elenco è SUITES di scripts/verify.mjs (verify.test.mjs pretende che coincidano),
|
||||||
|
# l'immagine ha la stessa versione di Playwright di package-lock.json (idem).
|
||||||
|
browser:
|
||||||
needs: test
|
needs: test
|
||||||
|
if: startsWith(github.ref, 'refs/tags/v') || github.event_name == 'workflow_dispatch'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
container:
|
||||||
|
image: mcr.microsoft.com/playwright:v1.64.1-noble
|
||||||
|
# La scheda video del PC (runtime NVIDIA di Docker): Chromium disegna lì, tests/chromium.mjs la trova da solo.
|
||||||
|
options: --gpus all -e NVIDIA_DRIVER_CAPABILITIES=all
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
prova: [principale, minigiochi, telefoni, ambient, veicoli, ingorgo, smarronata, briscola, tressette, scopa, biliardo, flipper, micro, cerbottana, condor, campana, bruciate, gara, telefono, risveglio, peso]
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Playwright
|
||||||
|
run: npm ci --no-audit --no-fund
|
||||||
|
- name: Prova ${{ matrix.prova }}
|
||||||
|
run: node scripts/verify.mjs ${{ matrix.prova }}
|
||||||
|
|
||||||
|
release:
|
||||||
|
needs: [test, browser]
|
||||||
if: startsWith(github.ref, 'refs/tags/v')
|
if: startsWith(github.ref, 'refs/tags/v')
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
|
|||||||
@@ -18,8 +18,11 @@ i suoi ricordi: non inventare persone, luoghi o fatti del paese al posto suoi; p
|
|||||||
- Ogni funzionalità con il suo test; un test automatico percorre tutta la soluzione dell'avventura.
|
- Ogni funzionalità con il suo test; un test automatico percorre tutta la soluzione dell'avventura.
|
||||||
- Le prove nel browser sono a sezioni (`scripts/verify.mjs`, dal 2026-11-01): `make verify` rifà solo quelle i cui ingressi
|
- Le prove nel browser sono a sezioni (`scripts/verify.mjs`, dal 2026-11-01): `make verify` rifà solo quelle i cui ingressi
|
||||||
sono cambiati dall'ultimo verde (la briscola se cambia la briscola, la partita intera se cambiano storia, motore, stile
|
sono cambiati dall'ultimo verde (la briscola se cambia la briscola, la partita intera se cambiano storia, motore, stile
|
||||||
o risorse). Una prova nuova in `tests/*.browser.mjs` va aggiunta a `SUITES` (il test `verify.test.mjs` lo pretende).
|
o risorse). Un `chromium.launch` nuovo nelle prove usa `args: ARGS` di `tests/chromium.mjs`.
|
||||||
- Verifica visiva nel browser con Playwright e swiftshader prima di dichiarare finito qualcosa di grafico.
|
Gli effetti pesanti li scopre la prova `peso` (`tests/peso.browser.mjs`): sempre con swiftshader, misura arretrato grafico e
|
||||||
|
fotogrammi di ogni scena e primo piano; si rifà quando cambia un disegno (`*-render.js`, `render.js`, effetti). Una prova nuova in `tests/*.browser.mjs` va aggiunta a `SUITES` (il test `verify.test.mjs` lo pretende).
|
||||||
|
- Verifica visiva nel browser con Playwright prima di dichiarare finito qualcosa di grafico. Chromium disegna sulla scheda
|
||||||
|
video del PC se c'è, altrimenti con swiftshader (`tests/chromium.mjs`, dal 2026-11-01: giro intero ~4 min invece di ~16).
|
||||||
- Pubblicare (tag/bump/push) solo quando Andrea lo chiede.
|
- Pubblicare (tag/bump/push) solo quando Andrea lo chiede.
|
||||||
- Persone vere nel gioco solo con consenso; niente nomi, personaggi o musiche di giochi esistenti.
|
- Persone vere nel gioco solo con consenso; niente nomi, personaggi o musiche di giochi esistenti.
|
||||||
- Prompt delle immagini in `prompts/` e fogli grezzi in `assets/raw/`: esclusi da git.
|
- Prompt delle immagini in `prompts/` e fogli grezzi in `assets/raw/`: esclusi da git.
|
||||||
@@ -28,4 +31,4 @@ i suoi ricordi: non inventare persone, luoghi o fatti del paese al posto suoi; p
|
|||||||
- **Pubblicare (dal 2026-11-01)**: origin è https://git.manzolo.it/manzolo/leggenda-del-regolo, pubblico: prima `make check-public`, poi `git push origin main --tags`, solo se Andrea lo chiede.
|
- **Pubblicare (dal 2026-11-01)**: origin è https://git.manzolo.it/manzolo/leggenda-del-regolo, pubblico: prima `make check-public`, poi `git push origin main --tags`, solo se Andrea lo chiede.
|
||||||
`make deploy` costruisce l'immagine dal tag della versione, la spinge su `git.manzolo.it/manzolo/leggenda-regolo:vX.Y.Z` (e `:latest`); sul VPS il compose fissa quel tag e fa `docker compose pull && up -d`.
|
`make deploy` costruisce l'immagine dal tag della versione, la spinge su `git.manzolo.it/manzolo/leggenda-regolo:vX.Y.Z` (e `:latest`); sul VPS il compose fissa quel tag e fa `docker compose pull && up -d`.
|
||||||
Per tornare indietro: sul VPS (`Workspaces/docker/leggenda-regolo/docker-compose.yml`) si rimette il tag precedente e si rifà pull e up.
|
Per tornare indietro: sul VPS (`Workspaces/docker/leggenda-regolo/docker-compose.yml`) si rimette il tag precedente e si rifà pull e up.
|
||||||
Hook pre-push (`make hooks`, una volta per clone) lancia check-public; le Gitea Actions (`.gitea/workflows/test.yml`: test, e release sui tag) girano sul runner del PC, che si crea o riavvia con `make runner`.
|
Hook pre-push (`make hooks`, una volta per clone) lancia check-public; le Gitea Actions (`.gitea/workflows/test.yml`: test unitari a ogni push; sui tag, o a mano, anche le venti prove nel browser, una per job, poi la release) girano sul runner del PC, che si crea o riavvia con `make runner`.
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
+52
-8
@@ -63,10 +63,21 @@ Online su 50037.manzolo.it tutto fino agli effetti ambientali di Codex. Aperto,
|
|||||||
medi, 15.000 punti. Tre passaggi sulla stessa rampa entro 20 secondi dal primo: LOCK e nuova pallina gratis.
|
medi, 15.000 punti. Tre passaggi sulla stessa rampa entro 20 secondi dal primo: LOCK e nuova pallina gratis.
|
||||||
Un'altra rampa completata o il tempo esaurito interrompono la serie. Dopo il primo LOCK, primo passaggio
|
Un'altra rampa completata o il tempo esaurito interrompono la serie. Dopo il primo LOCK, primo passaggio
|
||||||
nell'altra rampa SPECIAL: jackpot da 100.000; tre passaggi ravvicinati fanno il secondo LOCK.
|
nell'altra rampa SPECIAL: jackpot da 100.000; tre passaggi ravvicinati fanno il secondo LOCK.
|
||||||
- Due LOCK alzano la rampa centrale SUPER SPECIAL: 250.000 punti, una pallina extra in riserva e multiball a
|
- Due LOCK alzano la rampa centrale SUPER SPECIAL: 250.000 punti (× moltiplicatore), una pallina extra in
|
||||||
tre. Uscita SUPER raccordata alla corsia sinistra sopra la paletta, a velocità ridotta; i due lock liberano
|
riserva e, dal 1 novembre, **Il Regolo del futuro**. Un botto sintetico grave, lampo, onde d'urto, schegge
|
||||||
le palline dai lati, sfalsati di 0,45 secondi. In multiball le rampe pagano jackpot, e una vita si perde solo
|
colorate e scossa del mobile precedono l'apertura in due metà del piano classico. Trasformazione di 3,2 s:
|
||||||
all'ultima pallina. Prima della SUPER, perdere la sostitutiva recupera l'ultimo lock senza scalare vite.
|
le tre palline restano protette, poi vengono servite sfalsate di 0,55 s verso le palette.
|
||||||
|
Prima della SUPER, perdere la sostitutiva recupera l'ultimo lock senza scalare vite.
|
||||||
|
- **Secondo campo vero**, disegnato in codice: niente corsie dei rioni o banchi classici; altri respingenti,
|
||||||
|
raccordi, tre percorsi sopraelevati e Regolo mobile sopra le palette. ORBITA DEL SERPENTE (loop esterno,
|
||||||
|
35.000 a giro), PONTE DELLA FORTEZZA (ponte incrociato, 50.000), SPIRA AL NEON (spirale più ripida,
|
||||||
|
65.000). Imbocchi dal basso, salita con perdita d'energia, quota e uscita nelle corsie interne; un tiro
|
||||||
|
debole ricade senza punti. Tabellone digitale, circuiti e bastioni immaginari, luci ciano/viola/rosa,
|
||||||
|
pulsazione musicale originale sintetizzata con Web Audio. È fantasia del mobile, nessun fatto nuovo sul paese.
|
||||||
|
- **Tre sigilli**: completare due volte ciascun percorso, in qualsiasi ordine e senza scadenza, poi colpire
|
||||||
|
il Regolo mobile: +500.000 e ritorno al classico con pallina sulla molla, senza consumare vite. Anche
|
||||||
|
perdendo l'ultima pallina si torna al classico, consumando una sola vita; il punteggio rimane. La SUPER
|
||||||
|
si può riaprire. Durante il multiball perdere una delle altre palline non scala la riserva.
|
||||||
- Completare una serie di bersagli alza il moltiplicatore jackpot fino a ×3, azzerato alla perdita di una vita.
|
- Completare una serie di bersagli alza il moltiplicatore jackpot fino a ×3, azzerato alla perdita di una vita.
|
||||||
Restano PONTI (+50.000 alternando le rampe entro 12 secondi), PALIO (+30.000), respingenti, bersagli e Regolo.
|
Restano PONTI (+50.000 alternando le rampe entro 12 secondi), PALIO (+30.000), respingenti, bersagli e Regolo.
|
||||||
Traguardi 60.000/120.000/200.000: una pallina premio una sola volta, vittoria acquisita anche uscendo prima.
|
Traguardi 60.000/120.000/200.000: una pallina premio una sola volta, vittoria acquisita anche uscendo prima.
|
||||||
@@ -77,10 +88,22 @@ Online su 50037.manzolo.it tutto fino agli effetti ambientali di Codex. Aperto,
|
|||||||
- Luci che scorrono sulle rampe attive, frecce e spie 1/3–2/3–LOCK, rampa SUPER che si alza, fasci luminosi,
|
- Luci che scorrono sulle rampe attive, frecce e spie 1/3–2/3–LOCK, rampa SUPER che si alza, fasci luminosi,
|
||||||
scintille, particelle e fanfare distinte per jackpot, apertura SUPER e multiball; la pallina resta leggibile.
|
scintille, particelle e fanfare distinte per jackpot, apertura SUPER e multiball; la pallina resta leggibile.
|
||||||
Comandi: mouse, due dita, tasto destro, frecce, Spazio e pulsanti; istruzioni in «Come si gioca».
|
Comandi: mouse, due dita, tasto destro, frecce, Spazio e pulsanti; istruzioni in «Come si gioca».
|
||||||
- Test `flipper.test.mjs`: sequenze in entrambi gli ordini e a tutti i livelli, scadenze, premi, collisioni,
|
- Test `flipper.test.mjs`: sequenze a tutti i livelli e in entrambi gli ordini, trasformazione protetta,
|
||||||
recupero della SUPER con la paletta, rilascio sfalsato e valvola. `npm run browser:flipper`: desktop e touch,
|
rilascio sfalsato, fisica e punteggi dei nuovi percorsi, tiri reali dalla paletta, Regolo mobile, finale,
|
||||||
screenshot di tavolo, LOCK, SUPER, multiball e risultato in `/tmp/regolo-flipper`, e la misura dell'arretrato
|
ritorno e nuova SUPER, ball save, stato serializzabile e disegno senza mutazioni. I salvataggi dell'avventura
|
||||||
grafico dopo 150 fotogrammi in multiball (deve restare sotto 1,5 s).
|
continuano a usare il checkpoint prima del minigioco, come gli altri primi piani; la pausa mantiene il
|
||||||
|
campo in corso e rilascia i comandi senza avanzare il tempo. `npm test`: 326 test verdi.
|
||||||
|
- `node scripts/verify.mjs flipper`: desktop e touch, comandi e pausa nel futuro, percorso fino al finale;
|
||||||
|
schermate di esplosione, apertura, futuro, sigilli e ritorno in `/tmp/regolo-flipper`, guardate e corrette.
|
||||||
|
Arretrato grafico misurato separatamente su 150 fotogrammi classici, esplosione/apertura e futuro, con
|
||||||
|
lettura dei pixel prima e dopo. Fondali sullo sfondo CSS, binari e pannelli in cache, onde e schegge leggere;
|
||||||
|
niente filtri o tratti arrotondati nel disegno dinamico. Soglia per ogni misura: 1,5 s.
|
||||||
|
Verifica finale (1 novembre): `REGOLO_GPU=0`, SwiftShader, classico/esplosione/futuro **31/56/26 ms**
|
||||||
|
desktop e **27/22/22 ms** touch. Anche il comando senza variabili passa, selezionando SwiftShader.
|
||||||
|
`REGOLO_GPU=1`: **30/17/25 ms** desktop e **23/18/20 ms** touch, ma il renderer effettivo è
|
||||||
|
`ANGLE (Mesa, llvmpipe (LLVM 21.1.8 256 bits), OpenGL ES 3.2)`: ancora software. In questo ambiente
|
||||||
|
non sono esposti dispositivi GPU; la misura su scheda video fisica resta da fare su un host che la esponga.
|
||||||
|
Schermate del secondo backend in `/tmp/regolo-flipper/llvmpipe`.
|
||||||
[DA ANDREA] battute, premio nell'avventura (`flipper_vinto`), record persistente per livello.
|
[DA ANDREA] battute, premio nell'avventura (`flipper_vinto`), record persistente per livello.
|
||||||
- **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.»
|
||||||
@@ -316,6 +339,27 @@ versione nuova.
|
|||||||
`make bump` dopo una modifica alla briscola riprova la briscola e basta; dopo una modifica alla storia la partita intera
|
`make bump` dopo una modifica alla briscola riprova la briscola e basta; dopo una modifica alla storia la partita intera
|
||||||
e le scene speciali (ambient, veicoli, ingorgo, smarronata), non la briscola; dopo `style.css` o `game.js` tutto.
|
e le scene speciali (ambient, veicoli, ingorgo, smarronata), non la briscola; dopo `style.css` o `game.js` tutto.
|
||||||
Le prove dei minigiochi, che prima non entravano in `make verify`, ora ci stanno. `make verify-plan` spiega.
|
Le prove dei minigiochi, che prima non entravano in `make verify`, ora ci stanno. `make verify-plan` spiega.
|
||||||
|
Nella pipeline di Gitea (runner sul PC) sui tag, o a mano, girano tutte e venti, una per job nell'immagine di
|
||||||
|
Playwright, prima della release: il primo giro completo (1 novembre, a mano su main) verde in 17 minuti.
|
||||||
|
|
||||||
|
- **Prove sulla scheda video (1 novembre, Andrea: «prova con la scheda video»):** Chromium disegnava con swiftshader,
|
||||||
|
sul processore: una schermata 633 ms. Con la RTX del PC (`--use-angle=gl-egl`) 41 ms; micro da 283 a 27 s, briscola da
|
||||||
|
259 a 29, flipper da 186 a 10; tutte e venti da ~16 minuti (due alla volta) a ~4 (sei alla volta), tutte verdi. Le
|
||||||
|
opzioni stanno in `tests/chromium.mjs`: scheda video se c'è (`/dev/dri`, `/dev/nvidia0`), altrimenti swiftshader;
|
||||||
|
`REGOLO_GPU=0/1` forza. Con 6 o 8 prove insieme il giro dura uguale: il limite è la partita intera (243 s, il tempo
|
||||||
|
del gioco, non della grafica). La pipeline dà la GPU ai job (`--gpus all`, runtime NVIDIA di Docker); runner a 6 job.
|
||||||
|
|
||||||
|
- **La prova del peso (1 novembre, Andrea: «senza GPU scoprivamo anche gli effetti troppo pesanti»):** con la scheda
|
||||||
|
video le prove non vedono più un effetto pesante. `tests/peso.browser.mjs` usa sempre swiftshader e, nel gioco vero,
|
||||||
|
in ogni scena nelle tre fasi e in ogni primo piano (il flipper anche in gioco e nel Regolo del futuro) misura
|
||||||
|
fotogrammi al secondo e arretrato grafico: soglie 30 e 400 ms (una scena normale pochi ms; un `ctx.filter` blur a
|
||||||
|
tutto schermo 816, quaranta tratti larghi trasparenti 718). Due trappole trovate: leggere i pixel della tela del
|
||||||
|
gioco la fa passare al processore (Chrome) e la misura cambia ciò che misura, quindi si legge una tela nuova
|
||||||
|
640×360; la prima visita di una scena paga il caricamento delle immagini, quindi prima si smaltisce. ~5 minuti, in
|
||||||
|
parallelo con le altre; si rifà quando cambia un disegno (`watch` in verify.mjs), e nella pipeline sempre.
|
||||||
|
|
||||||
|
- **Flipper da tastiera (1 novembre, Andrea):** la freccia giù carica la molla come Spazio, i due Shift comandano le
|
||||||
|
palette (sinistro e destro, `e.code` passato da main.js ai minigiochi). Test unitario e nel browser.
|
||||||
|
|
||||||
## Materiale ricevuto da Andrea (in `assets/raw/`, escluso da git)
|
## Materiale ricevuto da Andrea (in `assets/raw/`, escluso da git)
|
||||||
|
|
||||||
|
|||||||
+12
-6
@@ -305,18 +305,24 @@
|
|||||||
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>
|
<details id="flipper-help" hidden><summary>Come si gioca</summary>
|
||||||
<p>Il Regolo, il flipper del River. Tieni premuto per caricare la molla e lascia per lanciare, oppure usa
|
<p>Il Regolo, il flipper del River. Tieni premuto per caricare la molla e lascia per lanciare, oppure tieni
|
||||||
Spazio o il pulsante Lancia. Una valvola impedisce alla pallina entrata in campo di tornare sulla molla.
|
premuto Spazio o la freccia giù, o usa il pulsante Lancia. Una valvola impedisce alla pallina entrata in campo di tornare sulla molla.
|
||||||
Le frecce sinistra e destra, i pulsanti Flipper o le due metà della scena comandano le palette;
|
Le frecce sinistra e destra, i due Shift, i pulsanti Flipper o le due metà della scena comandano le palette;
|
||||||
puoi usare due dita insieme e il tasto destro del mouse comanda sempre la paletta destra.</p>
|
puoi usare due dita insieme e il tasto destro del mouse comanda sempre la paletta destra.</p>
|
||||||
<p>Le rampe FORTEZZA (ciano) e TREBBIO (ambra) valgono 15.000 punti. Tre passaggi completi sulla stessa
|
<p>Le rampe FORTEZZA (ciano) e TREBBIO (ambra) valgono 15.000 punti. Tre passaggi completi sulla stessa
|
||||||
rampa entro 20 secondi dal primo fanno LOCK: la pallina viene trattenuta e ne arriva una nuova gratis.
|
rampa entro 20 secondi dal primo fanno LOCK: la pallina viene trattenuta e ne arriva una nuova gratis.
|
||||||
Completare l'altra rampa interrompe la serie. Dopo il primo LOCK l'altra rampa diventa SPECIAL: il primo
|
Completare l'altra rampa interrompe la serie. Dopo il primo LOCK l'altra rampa diventa SPECIAL: il primo
|
||||||
passaggio paga un JACKPOT da 100.000, e tre passaggi ravvicinati fanno il secondo LOCK.</p>
|
passaggio paga un JACKPOT da 100.000, e tre passaggi ravvicinati fanno il secondo LOCK.</p>
|
||||||
<p>Con entrambi i LOCK si alza la rampa viola centrale, SUPER SPECIAL. Completala: 250.000 punti,
|
<p>Con entrambi i LOCK si alza la rampa viola centrale, SUPER SPECIAL. Completala: 250.000 punti,
|
||||||
una pallina extra in riserva e MULTIBALL a tre palline! La SUPER restituisce la pallina nella corsia
|
una pallina extra in riserva e una sorpresa esplosiva! Il piano si apre e rivela <strong>Il Regolo del futuro</strong>,
|
||||||
laterale verso la paletta sinistra; quelle trattenute vengono liberate dai due lati, sfalsate.
|
un secondo campo al neon. Durante la trasformazione le tre palline sono al sicuro; poi parte il MULTIBALL,
|
||||||
In multiball le rampe pagano altri jackpot e perdi una vita solo quando perdi l'ultima pallina in gioco.
|
con uscite sfalsate verso le palette. I comandi restano gli stessi.</p>
|
||||||
|
<p>Nel futuro completa due volte ciascun percorso: il loop ORBITA DEL SERPENTE vale 35.000 a giro,
|
||||||
|
la rampa sopraelevata PONTE DELLA FORTEZZA 50.000 e la SPIRA AL NEON 65.000. Contano solo i giri
|
||||||
|
completi: un tiro troppo debole ricade senza premio. Ogni percorso carica un sigillo, senza limite di tempo.
|
||||||
|
Accesi tutti e tre, colpisci il Regolo coronato che si muove al centro: 500.000 punti e ritorno al tavolo
|
||||||
|
classico, con una pallina pronta sulla molla senza consumare la riserva. Torni al classico anche perdendo
|
||||||
|
l'ultima pallina in gioco: in quel caso consumi una sola vita. I punti restano e puoi riaprire la SUPER.
|
||||||
Se perdi la sostitutiva prima della SUPER, recuperi l'ultimo LOCK senza consumare una vita.</p>
|
Se perdi la sostitutiva prima della SUPER, recuperi l'ultimo LOCK senza consumare una vita.</p>
|
||||||
<p>Completa i tre bersagli di un lato per alzare il moltiplicatore dei jackpot, fino a ×3 (si azzera alla
|
<p>Completa i tre bersagli di un lato per alzare il moltiplicatore dei jackpot, fino a ×3 (si azzera alla
|
||||||
perdita di una vita). Alterna le due rampe entro 12 secondi per il bonus PONTI da 50.000; accendi le
|
perdita di una vita). Alterna le due rampe entro 12 secondi per il bonus PONTI da 50.000; accendi le
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "leggenda-del-regolo",
|
"name": "leggenda-del-regolo",
|
||||||
"version": "0.1.69",
|
"version": "0.1.71",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -26,4 +26,6 @@ if [[ ! -s $RUNNER_DATA/.runner ]]; then
|
|||||||
fi
|
fi
|
||||||
# Senza token nell'ambiente del container: da qui in poi si autentica con .runner.
|
# Senza token nell'ambiente del container: da qui in poi si autentica con .runner.
|
||||||
docker compose -f "$COMPOSE" up -d
|
docker compose -f "$COMPOSE" up -d
|
||||||
|
# config.yaml si legge solo all'avvio: se è cambiato (capacità, opzioni) il runner riparte.
|
||||||
|
[[ $(docker inspect -f '{{.State.StartedAt}}' gitea-runner | date -f - +%s) -lt $(stat -c %Y "$(dirname "$COMPOSE")/config.yaml") ]] && docker restart gitea-runner >/dev/null
|
||||||
sleep 2; docker logs --tail 1 gitea-runner
|
sleep 2; docker logs --tail 1 gitea-runner
|
||||||
|
|||||||
+19
-7
@@ -3,7 +3,8 @@
|
|||||||
// si rifà solo se la loro impronta (i blob di git, più i file modificati nella cartella di lavoro) è cambiata dall'ultimo
|
// si rifà solo se la loro impronta (i blob di git, più i file modificati nella cartella di lavoro) è cambiata dall'ultimo
|
||||||
// verde, ricordato in .verify-ok.json (fuori da git). Se cambia la briscola si riprova la briscola; se cambia la storia, il
|
// verde, ricordato in .verify-ok.json (fuori da git). Se cambia la briscola si riprova la briscola; se cambia la storia, il
|
||||||
// motore, l'impaginazione o le risorse si riprova la partita intera (e le prove delle scene speciali), non la briscola.
|
// motore, l'impaginazione o le risorse si riprova la partita intera (e le prove delle scene speciali), non la briscola.
|
||||||
// node scripts/verify.mjs le prove da rifare, in parallelo (JOBS=2: con quattro Chromium swiftshader insieme le schermate vanno in timeout), e aggiorna i verdi
|
// node scripts/verify.mjs le prove da rifare, in parallelo, e aggiorna i verdi (JOBS: quante insieme; con la
|
||||||
|
// scheda video JOBS_GPU, senza 2: con quattro Chromium swiftshader le schermate vanno in timeout)
|
||||||
// node scripts/verify.mjs --plan dice solo cosa rifarebbe e perché
|
// node scripts/verify.mjs --plan dice solo cosa rifarebbe e perché
|
||||||
// node scripts/verify.mjs --all tutte, senza guardare i verdi
|
// node scripts/verify.mjs --all tutte, senza guardare i verdi
|
||||||
// node scripts/verify.mjs briscola solo quelle nominate (anche se verdi)
|
// node scripts/verify.mjs briscola solo quelle nominate (anche se verdi)
|
||||||
@@ -13,10 +14,11 @@ import { createHash } from 'node:crypto';
|
|||||||
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
|
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
|
||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath } from 'node:url';
|
||||||
import { cpus } from 'node:os';
|
import { cpus } from 'node:os';
|
||||||
|
import { GPU } from '../tests/chromium.mjs';
|
||||||
|
|
||||||
// Il motore comune a tutto: cambia questo e si riprova ogni minigioco.
|
// Il motore comune a tutto: cambia questo e si riprova ogni minigioco.
|
||||||
export const ENGINE = ['index.html', 'style.css', 'src/main.js', 'src/engine/game.js', 'src/engine/render.js',
|
export const ENGINE = ['index.html', 'style.css', 'src/main.js', 'src/engine/game.js', 'src/engine/render.js',
|
||||||
'tests/helpers.mjs', 'package-lock.json'];
|
'tests/helpers.mjs', 'tests/chromium.mjs', 'package-lock.json'];
|
||||||
// Non toccano il gioco nel browser: documenti, strumenti, pubblicazione, versione (make bump la cambia prima di verificare),
|
// Non toccano il gioco nel browser: documenti, strumenti, pubblicazione, versione (make bump la cambia prima di verificare),
|
||||||
// test unitari (girano sempre, a parte), e questo stesso script (lo prova verify.test.mjs).
|
// test unitari (girano sempre, a parte), e questo stesso script (lo prova verify.test.mjs).
|
||||||
export const IGNORED = [/^docs\//, /^[^/]*\.md$/, /^src\/version\.js$/, /^package\.json$/, /^Makefile$/, /^scripts\//,
|
export const IGNORED = [/^docs\//, /^[^/]*\.md$/, /^src\/version\.js$/, /^package\.json$/, /^Makefile$/, /^scripts\//,
|
||||||
@@ -25,7 +27,9 @@ export const IGNORED = [/^docs\//, /^[^/]*\.md$/, /^src\/version\.js$/, /^packag
|
|||||||
|
|
||||||
// Le prove. `core: true` = dipende da tutto il gioco tranne i file dei minigiochi a sé (che hanno la loro prova); altrimenti
|
// Le prove. `core: true` = dipende da tutto il gioco tranne i file dei minigiochi a sé (che hanno la loro prova); altrimenti
|
||||||
// solo dal motore e dai propri file: quelli che si chiamano come la prova (src/engine/flipper.js, flipper-render.js,
|
// solo dal motore e dai propri file: quelli che si chiamano come la prova (src/engine/flipper.js, flipper-render.js,
|
||||||
// assets/closeups/flipper-*.png, docs/BRISCOLA.md…) più gli `extra`. `run` sono gli argomenti di node.
|
// assets/closeups/flipper-*.png, docs/BRISCOLA.md…) più gli `extra`. `watch`: file che la prova guarda senza toglierli
|
||||||
|
// alle altre (il peso della grafica si rifà quando cambia un disegno, ma quel disegno resta anche della sua prova).
|
||||||
|
// `run` sono gli argomenti di node.
|
||||||
export const SUITES = {
|
export const SUITES = {
|
||||||
principale: { core: true, run: ['tests/browser.mjs', '--parte=principale'], note: 'la partita intera, pausa e salvataggi' },
|
principale: { core: true, run: ['tests/browser.mjs', '--parte=principale'], note: 'la partita intera, pausa e salvataggi' },
|
||||||
minigiochi: { core: true, run: ['tests/browser.mjs', '--parte=minigiochi'], note: 'libro, bocce, tiri, pesca e biliardo dalla partita' },
|
minigiochi: { core: true, run: ['tests/browser.mjs', '--parte=minigiochi'], note: 'libro, bocce, tiri, pesca e biliardo dalla partita' },
|
||||||
@@ -47,8 +51,14 @@ export const SUITES = {
|
|||||||
gara: { run: ['tests/gara.browser.mjs'], extra: ['src/engine/corsa.js', 'src/story/piloti.js', /^assets\/[^/]*\/(gara|gp|pilot)/] },
|
gara: { run: ['tests/gara.browser.mjs'], extra: ['src/engine/corsa.js', 'src/story/piloti.js', /^assets\/[^/]*\/(gara|gp|pilot)/] },
|
||||||
telefono: { run: ['tests/telefono.browser.mjs'], extra: [/^assets\/muro\//] },
|
telefono: { run: ['tests/telefono.browser.mjs'], extra: [/^assets\/muro\//] },
|
||||||
risveglio: { run: ['tests/risveglio.browser.mjs'] },
|
risveglio: { run: ['tests/risveglio.browser.mjs'] },
|
||||||
|
// Sempre con swiftshader, come un telefono lento: con la scheda video le altre prove non vedono gli effetti pesanti.
|
||||||
|
peso: { run: ['tests/peso.browser.mjs'], note: 'il peso della grafica, senza scheda video',
|
||||||
|
watch: [/-render\.js$/, /^src\/engine\/(ambient|incanto|risveglio|torch|viaggio|corsa|telefono)\.js$/, /^src\/story\/ambience\.js$/, /^assets\/effects\//] },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Prove insieme con la scheda video (misurato il 1° novembre 2026 sul PC di Andrea, 16 thread e RTX 3080 Ti).
|
||||||
|
const JOBS_GPU = 6; // con 6 e con 8 il giro intero dura uguale (~4 min): da solo «principale» ne prende 243 s
|
||||||
|
|
||||||
const ownRe = name => new RegExp(`(^|/)${name}([-_.][^/]*)?$`, 'i');
|
const ownRe = name => new RegExp(`(^|/)${name}([-_.][^/]*)?$`, 'i');
|
||||||
const matches = (path, rule) => rule instanceof RegExp ? rule.test(path) : rule === path;
|
const matches = (path, rule) => rule instanceof RegExp ? rule.test(path) : rule === path;
|
||||||
export const ignored = path => IGNORED.some(r => r.test(path));
|
export const ignored = path => IGNORED.some(r => r.test(path));
|
||||||
@@ -59,7 +69,8 @@ export function ownFiles(name, files) {
|
|||||||
// Gli ingressi di una prova fra i file dati (tutti quelli di git più quelli nuovi nella cartella).
|
// Gli ingressi di una prova fra i file dati (tutti quelli di git più quelli nuovi nella cartella).
|
||||||
export function inputsOf(name, files) {
|
export function inputsOf(name, files) {
|
||||||
const s = SUITES[name], live = files.filter(p => !ignored(p));
|
const s = SUITES[name], live = files.filter(p => !ignored(p));
|
||||||
if (!s.core) return [...new Set([...ENGINE.filter(p => files.includes(p)), ...ownFiles(name, live)])].sort();
|
if (!s.core) return [...new Set([...ENGINE.filter(p => files.includes(p)), ...ownFiles(name, live),
|
||||||
|
...live.filter(p => (s.watch || []).some(r => matches(p, r)))])].sort();
|
||||||
const others = Object.keys(SUITES).filter(n => !SUITES[n].core);
|
const others = Object.keys(SUITES).filter(n => !SUITES[n].core);
|
||||||
// I file degli altri minigiochi, e i file di prova delle altre prove (tests/ambient.browser.mjs non riguarda la partita).
|
// I file degli altri minigiochi, e i file di prova delle altre prove (tests/ambient.browser.mjs non riguarda la partita).
|
||||||
const foreign = new Set([...others.flatMap(n => ownFiles(n, live)),
|
const foreign = new Set([...others.flatMap(n => ownFiles(n, live)),
|
||||||
@@ -121,9 +132,10 @@ async function main() {
|
|||||||
const why = all ? 'tutte' : named.includes(t.name) ? 'chiesta' : !ok[t.name] ? 'mai provata' : !t.changed.length ? "cambiato l'elenco dei file da cui dipende" : `cambiati ${t.changed.slice(0, 4).join(', ')}${t.changed.length > 4 ? ` e altri ${t.changed.length - 4}` : ''}`;
|
const why = all ? 'tutte' : named.includes(t.name) ? 'chiesta' : !ok[t.name] ? 'mai provata' : !t.changed.length ? "cambiato l'elenco dei file da cui dipende" : `cambiati ${t.changed.slice(0, 4).join(', ')}${t.changed.length > 4 ? ` e altri ${t.changed.length - 4}` : ''}`;
|
||||||
console.log(`da rifare: ${t.name}${t.seconds ? ` (~${t.seconds} s)` : ''}: ${why}`);
|
console.log(`da rifare: ${t.name}${t.seconds ? ` (~${t.seconds} s)` : ''}: ${why}`);
|
||||||
}
|
}
|
||||||
if (green.length) console.log(`già verdi: ${green.join(', ')}`);
|
if (green.length && !named.length) console.log(`già verdi: ${green.join(', ')}`);
|
||||||
if (onlyPlan) return;
|
if (onlyPlan) return;
|
||||||
const jobs = Math.max(1, Number(process.env.JOBS) || Math.min(2, Math.floor(cpus().length / 4)));
|
const jobs = Math.max(1, Number(process.env.JOBS) || Math.min(GPU ? JOBS_GPU : 2, Math.floor(cpus().length / (GPU ? 2 : 4))));
|
||||||
|
console.log(`Chromium ${GPU ? 'sulla scheda video' : 'con swiftshader'}, ${Math.min(jobs, todo.length) === 1 ? "una prova" : `${Math.min(jobs, todo.length)} prove`} alla volta.`);
|
||||||
const queue = todo.sort((a, b) => (b.seconds || 999) - (a.seconds || 999)), results = [];
|
const queue = todo.sort((a, b) => (b.seconds || 999) - (a.seconds || 999)), results = [];
|
||||||
const worker = async () => { for (let t; (t = queue.shift());) {
|
const worker = async () => { for (let t; (t = queue.shift());) {
|
||||||
const r = await run(t.name); results.push(r);
|
const r = await run(t.name); results.push(r);
|
||||||
@@ -137,7 +149,7 @@ async function main() {
|
|||||||
for (const r of results.sort((a, b) => a.name.localeCompare(b.name))) console.log(`${r.code === 0 ? 'OK ' : 'FAIL'} ${r.name} (${r.seconds} s)`);
|
for (const r of results.sort((a, b) => a.name.localeCompare(b.name))) console.log(`${r.code === 0 ? 'OK ' : 'FAIL'} ${r.name} (${r.seconds} s)`);
|
||||||
const failed = results.filter(r => r.code !== 0).map(r => r.name);
|
const failed = results.filter(r => r.code !== 0).map(r => r.name);
|
||||||
if (failed.length) { console.log(`Prove fallite: ${failed.join(', ')}`); process.exit(1); }
|
if (failed.length) { console.log(`Prove fallite: ${failed.join(', ')}`); process.exit(1); }
|
||||||
console.log(`Prove nel browser OK: ${results.length} rifatte, ${green.length} già verdi.`);
|
console.log(`Prove nel browser OK: ${results.length} rifatte${named.length ? '' : `, ${green.length} già verdi`}.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) main();
|
if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) main();
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
// Il Regolo / River: tavolo in lacca scura, metallo e inserti luminosi.
|
// Il Regolo / River: tavolo in lacca scura, metallo e inserti luminosi.
|
||||||
// Le coordinate sono quelle della fisica. Le superfici statiche e i bagliori sono in cache a risoluzione doppia.
|
// Le coordinate sono quelle della fisica. Le superfici statiche e i bagliori sono in cache a risoluzione doppia.
|
||||||
import { R, SCORE, RAMP, flipperSeg, alongRamp, rampHeight, waiting, canThrow, saveLeft, activeBalls, multiball, rampOpen } from './flipper.js';
|
import { R, SCORE, RAMP, FUTURE, layout, flipperSeg, alongRamp, rampHeight, waiting, canThrow, saveLeft, activeBalls, multiball, rampOpen } from './flipper.js';
|
||||||
import { ambientHash as hash } from './ambient.js';
|
import { ambientHash as hash } from './ambient.js';
|
||||||
|
|
||||||
const INK = '#071018', WHITE = '#eff9ff', MUTED = '#7b99aa';
|
const INK = '#071018', WHITE = '#eff9ff', MUTED = '#7b99aa';
|
||||||
const TINT = { sinistra: '#65ddec', destra: '#ffc16c', super: '#dba5ff' };
|
const TINT = { sinistra: '#65ddec', destra: '#ffc16c', super: '#dba5ff', orbita: '#58f4e2', ponte: '#c1a0ff', spira: '#ff729e' };
|
||||||
const RIONI = ['#f97980', '#f7eac9', '#78e2ad', '#7bb6ff'];
|
const RIONI = ['#f97980', '#f7eac9', '#78e2ad', '#7bb6ff'];
|
||||||
const TAU = Math.PI * 2, clamp = (v, a, b) => Math.max(a, Math.min(b, v));
|
const TAU = Math.PI * 2, clamp = (v, a, b) => Math.max(a, Math.min(b, v));
|
||||||
const number = n => Math.round(n).toLocaleString('it-IT');
|
const number = n => Math.round(n).toLocaleString('it-IT');
|
||||||
@@ -68,7 +68,7 @@ function cabinetBehind(g, key, paint) {
|
|||||||
if (canvas.dataset.cabinet !== key) { canvas.style.backgroundImage = url; canvas.style.backgroundSize = '100% 100%'; canvas.dataset.cabinet = key; }
|
if (canvas.dataset.cabinet !== key) { canvas.style.backgroundImage = url; canvas.style.backgroundSize = '100% 100%'; canvas.dataset.cabinet = key; }
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
export function cabinetAway(canvas) { canvas.style.backgroundImage = ''; canvas.style.backgroundSize = ''; delete canvas.dataset.cabinet; }
|
export function cabinetAway(canvas) { canvas.style.backgroundPosition = ''; canvas.style.backgroundImage = ''; canvas.style.backgroundSize = ''; delete canvas.dataset.cabinet; }
|
||||||
function crown(g, x, y, size, color) {
|
function crown(g, x, y, size, color) {
|
||||||
poly(g, [[x - size, y + size / 2], [x - size * 1.1, y - size / 2], [x - size / 2, y], [x, y - size], [x + size / 2, y], [x + size * 1.1, y - size / 2], [x + size, y + size / 2]], color);
|
poly(g, [[x - size, y + size / 2], [x - size * 1.1, y - size / 2], [x - size / 2, y], [x, y - size], [x + size / 2, y], [x + size * 1.1, y - size / 2], [x + size, y + size / 2]], color);
|
||||||
line(g, [[x - size, y + size * 0.8], [x + size, y + size * 0.8]], color, 1);
|
line(g, [[x - size, y + size * 0.8], [x + size, y + size * 0.8]], color, 1);
|
||||||
@@ -178,7 +178,7 @@ function paintTrack(g, r) {
|
|||||||
const pts = [[cx + side * 6, y - 30], [cx + side * (x1 - x0) / 2, y]];
|
const pts = [[cx + side * 6, y - 30], [cx + side * (x1 - x0) / 2, y]];
|
||||||
line(g, pts, '#07121c', 3); line(g, pts, color, 1); screw(g, cx + side * (x1 - x0) / 2, y);
|
line(g, pts, '#07121c', 3); line(g, pts, color, 1); screw(g, cx + side * (x1 - x0) / 2, y);
|
||||||
}
|
}
|
||||||
text(g, r.id === 'super' ? 'SUPER' : '15.000', cx, y - 12, 5, '#c5e0e7', 'center');
|
text(g, r.points ? number(r.points) : r.id === 'super' ? 'SUPER' : '15.000', cx, y - 12, 5, '#c5e0e7', 'center');
|
||||||
}
|
}
|
||||||
function rampLights(g, c, r, time) {
|
function rampLights(g, c, r, time) {
|
||||||
const color = TINT[r.id], locked = c.locks.some(l => l.rampId === r.id);
|
const color = TINT[r.id], locked = c.locks.some(l => l.rampId === r.id);
|
||||||
@@ -228,7 +228,7 @@ function target(g, c) {
|
|||||||
disc(g, o.x + 1, o.y + 3, o.r + 1, '#02090b');
|
disc(g, o.x + 1, o.y + 3, o.r + 1, '#02090b');
|
||||||
disc(g, o.x, o.y, o.r + 1, metal(g, o.x - 6, o.y - 6), '#c5b682');
|
disc(g, o.x, o.y, o.r + 1, metal(g, o.x - 6, o.y - 6), '#c5b682');
|
||||||
disc(g, o.x, o.y, o.r - 1, '#18483e', '#94d7a5'); crown(g, o.x, o.y, 3.4, '#f7dda0');
|
disc(g, o.x, o.y, o.r - 1, '#18483e', '#94d7a5'); crown(g, o.x, o.y, 3.4, '#f7dda0');
|
||||||
text(g, 'REGOLO', o.x, o.y - 14, 5, '#acd6b7', 'center');
|
text(g, c.future && Object.values(c.future.hits).every(n => n === 2) ? 'LIBERAMI!' : 'REGOLO', o.x, o.y + (c.table === 'future' ? 20 : -14), 5, '#acd6b7', 'center');
|
||||||
}
|
}
|
||||||
function paddle(g, c, side) {
|
function paddle(g, c, side) {
|
||||||
const f = c.flippers[side], { a: [x, y], b: [tx, ty] } = flipperSeg(c, side), len = Math.hypot(tx - x, ty - y);
|
const f = c.flippers[side], { a: [x, y], b: [tx, ty] } = flipperSeg(c, side), len = Math.hypot(tx - x, ty - y);
|
||||||
@@ -371,13 +371,142 @@ function effects(g, c, time) {
|
|||||||
g.restore();
|
g.restore();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Il secondo campo ha il suo fondale: circuiti incisi, bastioni sospesi e il serpente come costellazione.
|
||||||
|
// Tutto questo, compresi i binari, si dipinge una volta sola; sul telefono restano solo luci e palline.
|
||||||
|
function futureCabinet(g, c, bg, glass) {
|
||||||
|
cabinet(g, c, bg, glass);
|
||||||
|
g.save(); outline(g, c); g.clip();
|
||||||
|
g.fillStyle = gradient(g, 200, 0, 230, 340, [[0, '#080c29'], [0.5, '#14112d'], [1, '#061f2a']]); g.fillRect(200, 5, 232, 331);
|
||||||
|
for (let i = 0; i < 13; i++) {
|
||||||
|
const x = 205 + i * 18;
|
||||||
|
line(g, [[x, 320], [320 + (x - 320) * 0.35, 50]], '#18364a', 0.6);
|
||||||
|
line(g, [[201, 90 + i * 19], [432, 90 + i * 19]], '#18364a', 0.6);
|
||||||
|
}
|
||||||
|
glow(g, 320, 105, 115, '#975cff', 0.6); glow(g, 320, 281, 90, '#24dcda', 0.4);
|
||||||
|
poly(g, [[276, 99], [276, 73], [287, 73], [287, 62], [298, 62], [298, 76], [310, 76], [310, 53], [330, 53], [330, 76], [343, 76], [343, 62], [355, 62], [355, 75], [365, 75], [365, 99]], '#161e41', '#9c86ee');
|
||||||
|
line(g, [[278, 103], [320, 118], [363, 103]], '#58f4e2', 1);
|
||||||
|
text(g, 'FORTEZZA / NUCLEO', 320, 91, 5, '#d1c7ff', 'center');
|
||||||
|
const serpent = [[304, 277], [288, 265], [295, 252], [326, 252], [343, 270], [332, 284], [313, 284], [313, 271], [323, 267]];
|
||||||
|
line(g, serpent, '#143d50', 8); line(g, serpent, '#398c9c', 1); crown(g, 323, 264, 5, '#58f4e2');
|
||||||
|
for (const w of c.walls) if (!w.gate) { line(g, [w.a, w.b], '#2e526a', 3); line(g, [w.a, w.b], '#69b7c7', 0.7); }
|
||||||
|
g.restore();
|
||||||
|
// Il mobile stesso cambia: pannelli a circuito, la finestra sul River resta il legame col 2007.
|
||||||
|
for (const x of [12, 467]) { line(g, [[x + 9, 100], [x + 9, 31], [x + 31, 31]], '#58f4e2', 1.2); }
|
||||||
|
box(g, 198, 336, 252, 17, '#080f20', '#396c84', 2);
|
||||||
|
text(g, 'R I V E R / IL REGOLO DEL FUTURO', 320, 345, 6, '#99ffee', 'center');
|
||||||
|
}
|
||||||
|
function futureBoard(g, c) {
|
||||||
|
const ready = Object.values(c.future.hits).every(n => n === 2);
|
||||||
|
text(g, 'IL REGOLO', 99, 51, 21, WHITE, 'center', 'sans-serif', 800);
|
||||||
|
text(g, 'D E L F U T U R O', 99, 76, 9, TINT.orbita, 'center');
|
||||||
|
text(g, 'IL RIVER HA UN SEGRETO.', 99, 94, 5.5, '#a69abf', 'center');
|
||||||
|
box(g, 23, 111, 152, 82, '#040c1c', '#396c84');
|
||||||
|
text(g, 'PUNTI / CONNESSIONE ATTIVA', 34, 124, 5.5, TINT.orbita);
|
||||||
|
text(g, number(c.score), 99, 148, 22, WHITE, 'center', 'monospace');
|
||||||
|
text(g, c.transform ? 'RISVEGLIO DEL NUCLEO' : ready ? 'COLPISCI IL REGOLO!' : c.line ?? 'CARICA I TRE SIGILLI', 99, 178, 6.5, '#d6c1ff', 'center');
|
||||||
|
text(g, `${activeBalls(c).length} IN CAMPO / ${c.balls} IN RISERVA`, 99, 210, 6, '#b7e1e7', 'center');
|
||||||
|
text(g, 'STESSI TASTI. UN ALTRO MONDO.', 99, 225, 5.5, MUTED, 'center');
|
||||||
|
text(g, 'I TRE SIGILLI', 480, 49, 16, WHITE);
|
||||||
|
text(g, 'DUE PERCORSI COMPLETI CIASCUNO', 480, 69, 5.5, TINT.orbita);
|
||||||
|
for (const [i, r] of c.ramps.entries()) {
|
||||||
|
const y = 85 + i * 57, color = TINT[r.id], n = c.future.hits[r.id];
|
||||||
|
box(g, 479, y, 137, 49, '#101d34', n === 2 ? color : '#334a68');
|
||||||
|
text(g, r.name, 487, y + 12, 6.5, color);
|
||||||
|
for (let k = 0; k < 2; k++) box(g, 488 + k * 34, y + 25, 27, 5, k < n ? color : '#29374a', null, 1);
|
||||||
|
text(g, `${n}/2`, 603, y + 27, 9, WHITE, 'right', 'monospace');
|
||||||
|
text(g, `OGNI GIRO +${number(r.points)}`, 488, y + 40, 5.5, MUTED);
|
||||||
|
}
|
||||||
|
text(g, ready ? 'IL REGOLO È VULNERABILE' : 'POI COLPISCI IL REGOLO MOBILE', 548, 274, 6, ready ? TINT.orbita : '#d6c1ff', 'center');
|
||||||
|
text(g, '+500.000', 548, 294, 22, WHITE, 'center', 'monospace');
|
||||||
|
text(g, 'FINALE O ULTIMA PALLINA PERSA:', 548, 318, 5.5, MUTED, 'center');
|
||||||
|
text(g, 'SI TORNA AL RIVER', 548, 332, 7, TINT.orbita, 'center');
|
||||||
|
}
|
||||||
|
function futureField(g, c, time) {
|
||||||
|
for (const b of c.bumpers) bumper(g, b, time);
|
||||||
|
target(g, c);
|
||||||
|
const ready = Object.values(c.future.hits).every(n => n === 2);
|
||||||
|
if (ready) glow(g, c.regolo.x, c.regolo.y, 30, TINT.orbita, 0.6);
|
||||||
|
const balls = activeBalls(c);
|
||||||
|
for (const b of balls) if (!b.ramp && b.releaseAt <= c.sim) ball(g, b);
|
||||||
|
for (const r of c.ramps) {
|
||||||
|
cached(g, `track:future:${r.id}`, layer => paintTrack(layer, r), [207, 5, 221, 250]);
|
||||||
|
const color = TINT[r.id], x = r.path[0][0], y = r.mouth[0][1];
|
||||||
|
for (let k = 0; k < 4; k++) { const p = rampPoint(r, (time * 120 + k * r.len / 4) % r.len); disc(g, p.x, p.y, 1.3, color); }
|
||||||
|
poly(g, [[x, y + 7], [x - 4, y + 14], [x + 4, y + 14]], color);
|
||||||
|
text(g, r.id.toUpperCase(), x, y + 21, 5.5, color, 'center');
|
||||||
|
for (const b of balls) if (b.ramp?.id === r.id) ball(g, b, color);
|
||||||
|
}
|
||||||
|
for (const side of ['left', 'right']) paddle(g, c, side);
|
||||||
|
plunger(g, c); effects(g, c, time); futureBoard(g, c);
|
||||||
|
}
|
||||||
|
function oldPlayfield(c, bg, glass) {
|
||||||
|
return l => {
|
||||||
|
const classic = { ...c, ...layout(), table: 'classic' };
|
||||||
|
cabinet(l, classic, bg, glass); target(l, classic);
|
||||||
|
classic.bumpers.forEach(b => bumper(l, b, 0)); classic.ramps.forEach(r => paintTrack(l, r));
|
||||||
|
for (const side of ['left', 'right']) paddle(l, classic, side);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function transformation(g, c, bg, glass) {
|
||||||
|
const age = c.sim - c.transform.at;
|
||||||
|
// Le due metà del vecchio piano scorrono via. Sono ritagli piccoli, mai copie dello schermo intero.
|
||||||
|
if (age >= FUTURE.reveal && age < 2.7 && typeof document !== 'undefined') {
|
||||||
|
const old = layerOf(`split:${!!bg}:${!!glass}`, oldPlayfield(c, bg, glass), [200, 5, 248, 331]);
|
||||||
|
const k = clamp((age - FUTURE.reveal) / 1.6, 0, 1), shift = k * k * 150;
|
||||||
|
g.save(); g.beginPath(); g.rect(200, 5, 248, 331); g.clip();
|
||||||
|
g.drawImage(old, 0, 0, 248, 662, 200 - shift, 5, 124, 331);
|
||||||
|
g.drawImage(old, 248, 0, 248, 662, 324 + shift, 5, 124, 331);
|
||||||
|
line(g, [[324 - shift, 5], [324 - shift, 336]], TINT.orbita, 2);
|
||||||
|
line(g, [[324 + shift, 5], [324 + shift, 336]], TINT.ponte, 2); g.restore();
|
||||||
|
}
|
||||||
|
// Una detonazione sola, poi schegge e onde sottili: nessun filtro o lampeggio continuo.
|
||||||
|
const blast = Math.min(age, 1.7);
|
||||||
|
if (age < 1.7) {
|
||||||
|
glow(g, 320, 165, 60 + blast * 100, '#b9faff', Math.max(0, 1 - blast / 1.7));
|
||||||
|
for (let i = 0; i < 3; i++) {
|
||||||
|
const radius = 10 + Math.max(0, blast - i * 0.13) * 210;
|
||||||
|
g.beginPath(); g.ellipse(320, 165, radius, radius * 0.7, 0, 0, TAU); g.strokeStyle = [WHITE, TINT.orbita, TINT.ponte][i]; g.lineWidth = 1.5; g.stroke();
|
||||||
|
}
|
||||||
|
for (let i = 0; i < 20; i++) {
|
||||||
|
const a = i * TAU / 20 + 0.12, d = 25 + blast * 210;
|
||||||
|
line(g, [[320 + Math.cos(a) * d, 165 + Math.sin(a) * d * 0.7], [320 + Math.cos(a) * (d + 24), 165 + Math.sin(a) * (d + 24) * 0.7]], i % 2 ? TINT.orbita : '#ffe3a4', 1.2);
|
||||||
|
}
|
||||||
|
for (let i = 0; i < 64; i++) {
|
||||||
|
const a = hash(i * 11) * TAU, speed = 80 + hash(i * 17) * 180, d = speed * blast;
|
||||||
|
const x = 320 + Math.cos(a) * d, y = 165 + Math.sin(a) * d * 0.7 + blast * blast * 22;
|
||||||
|
const color = [TINT.orbita, TINT.spira, '#ffe3a4', WHITE][i % 4];
|
||||||
|
poly(g, [[x, y - 3], [x + 2 + i % 3, y], [x, y + 4], [x - 2, y]], color);
|
||||||
|
}
|
||||||
|
if (age < 0.09) { g.fillStyle = '#e4ffff'; g.fillRect(201, 6, 230, 328); }
|
||||||
|
}
|
||||||
|
const title = age < FUTURE.reveal ? 'SUPER SPECIAL' : 'IL REGOLO DEL FUTURO';
|
||||||
|
box(g, 215, 145, 210, 47, '#050e24', age < FUTURE.reveal ? TINT.spira : TINT.orbita, 3);
|
||||||
|
text(g, title, 320, 163, age < FUTURE.reveal ? 22 : 16, WHITE, 'center', 'sans-serif', 900);
|
||||||
|
text(g, age < FUTURE.reveal ? 'IL RIVER NON SE L’ASPETTAVA.' : 'IL PIANO SI APRE. IL SERPENTE SI SVEGLIA.', 320, 181, 6, TINT.orbita, 'center');
|
||||||
|
}
|
||||||
export function drawFlipper(g, c, def, bg, glass) {
|
export function drawFlipper(g, c, def, bg, glass) {
|
||||||
const time = c.visual.time;
|
const time = c.visual.time;
|
||||||
// Niente estremità e giunti arrotondati nei tratti di ogni fotogramma: costano più di tutto il resto (vedi la
|
// Niente estremità e giunti arrotondati nei tratti di ogni fotogramma: costano più di tutto il resto (vedi la
|
||||||
// memoria «effetti leggeri»); il mobile e i binari, in cache, li tengono.
|
// memoria «effetti leggeri»); il mobile e i binari, in cache, li tengono.
|
||||||
g.save(); g.imageSmoothingEnabled = true; g.lineCap = 'butt'; g.lineJoin = 'miter';
|
g.save(); g.imageSmoothingEnabled = true; g.lineCap = 'butt'; g.lineJoin = 'miter';
|
||||||
const shake = c.fx.reduce((n, f) => f.kind === 'shake' ? Math.max(n, 1 - (time - f.t) / 0.3) : n, 0);
|
const shake = c.fx.reduce((n, f) => f.kind === 'shake' ? Math.max(n, 1 - (time - f.t) / 0.3) : n, 0);
|
||||||
const key = `cabinet:${!!bg}:${!!glass}`, paint = layer => cabinet(layer, c, bg, glass);
|
if (g.canvas?.style) g.canvas.style.backgroundPosition = '';
|
||||||
|
const future = c.table === 'future';
|
||||||
|
const key = `cabinet:${future}:${!!bg}:${!!glass}`, paint = layer => (future ? futureCabinet : cabinet)(layer, c, bg, glass);
|
||||||
|
if (c.transform) {
|
||||||
|
if (cabinetBehind(g, key, paint)) g.clearRect(0, 0, 640, 360); else cached(g, key, paint);
|
||||||
|
const age = c.sim - c.transform.at, force = Math.max(0, 1 - age / 1.1);
|
||||||
|
const dx = Math.sin(age * 79) * force * 4, dy = Math.cos(age * 67) * force * 3;
|
||||||
|
g.translate(dx, dy);
|
||||||
|
if (g.canvas?.style) g.canvas.style.backgroundPosition = `${dx}px ${dy}px`;
|
||||||
|
if (!future) cached(g, `split:${!!bg}:${!!glass}`, oldPlayfield(c, bg, glass), [200, 5, 248, 331]);
|
||||||
|
if (future) futureField(g, c, time); else { board(g, c, time); mission(g, c, time); }
|
||||||
|
transformation(g, c, bg, glass); g.restore(); return;
|
||||||
|
}
|
||||||
|
if (future) {
|
||||||
|
if (cabinetBehind(g, key, paint)) g.clearRect(0, 0, 640, 360); else cached(g, key, paint);
|
||||||
|
futureField(g, c, time); g.restore(); return;
|
||||||
|
}
|
||||||
if (shake > 0) { g.fillStyle = INK; g.fillRect(0, 0, 640, 360); g.translate(Math.sin(time * 75) * shake * 1.3, Math.cos(time * 63) * shake); cached(g, key, paint); }
|
if (shake > 0) { g.fillStyle = INK; g.fillRect(0, 0, 640, 360); g.translate(Math.sin(time * 75) * shake * 1.3, Math.cos(time * 63) * shake); cached(g, key, paint); }
|
||||||
else if (cabinetBehind(g, key, paint)) g.clearRect(0, 0, 640, 360);
|
else if (cabinetBehind(g, key, paint)) g.clearRect(0, 0, 640, 360);
|
||||||
else cached(g, key, paint);
|
else cached(g, key, paint);
|
||||||
|
|||||||
+75
-7
@@ -85,6 +85,42 @@ export function layout() {
|
|||||||
[...walls, ...targets, ...bumpers, regolo].forEach((o, i) => { o.contactId = i; });
|
[...walls, ...targets, ...bumpers, regolo].forEach((o, i) => { o.contactId = i; });
|
||||||
return { walls, targets, bumpers, regolo, lanes, ramps };
|
return { walls, targets, bumpers, regolo, lanes, ramps };
|
||||||
}
|
}
|
||||||
|
// Sotto il legno c'è una macchina impossibile: fantasia del mobile, non storia del paese.
|
||||||
|
export const FUTURE = { reveal: 1.1, duration: 3.2, finale: 500000 };
|
||||||
|
export function futureLayout() {
|
||||||
|
const base = layout();
|
||||||
|
// Restano il mobile, la molla e le guide alle palette; spariscono corsie e banchi classici.
|
||||||
|
const walls = base.walls.filter((w, i) => i < 17 || (i >= 20 && i < 30));
|
||||||
|
walls.push(seg([205, 90], [228, 125]), seg([428, 92], [409, 120]));
|
||||||
|
const bumpers = [[280, 157], [368, 169]].map(([x, y]) => ({ x, y, r: 11, hit: 0, touching: false }));
|
||||||
|
const regolo = { x: 320, y: 276, r: 9, hit: 0, touching: false };
|
||||||
|
const ramps = [
|
||||||
|
{ id: 'orbita', name: 'ORBITA DEL SERPENTE', points: 35000, mouth: [[246, 235], [282, 235]], path: [[264, 235], [253, 197], [228, 158], [224, 89], [245, 42], [293, 25], [346, 29], [394, 50], [410, 89], [396, 116], [357, 112], [345, 85], [368, 64], [397, 85], [411, 135], [411, 220]] },
|
||||||
|
{ id: 'ponte', name: 'PONTE DELLA FORTEZZA', points: 50000, mouth: [[302, 238], [338, 238]], path: [[320, 238], [320, 201], [300, 167], [300, 108], [315, 66], [338, 53], [356, 73], [349, 105], [313, 128], [267, 133], [229, 174], [229, 220]] },
|
||||||
|
{ id: 'spira', slope: 300, name: 'SPIRA AL NEON', points: 65000, mouth: [[364, 235], [400, 235]], path: [[382, 235], [386, 203], [400, 165], [388, 132], [363, 128], [347, 151], [353, 178], [376, 187], [401, 180], [411, 204], [411, 245]] },
|
||||||
|
];
|
||||||
|
for (const r of ramps) {
|
||||||
|
r.len = 0; r.cum = [0]; r.lit = 0;
|
||||||
|
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)];
|
||||||
|
}
|
||||||
|
[...walls, ...bumpers, regolo].forEach((o, i) => { o.contactId = i; });
|
||||||
|
return { walls, targets: [], lanes: [], bumpers, regolo, ramps };
|
||||||
|
}
|
||||||
|
function openFuture(c) {
|
||||||
|
Object.assign(c, futureLayout()); c.table = 'future'; c.resets = {}; c.fx = [];
|
||||||
|
c.future = { hits: { orbita: 0, ponte: 0, spira: 0 } };
|
||||||
|
// La trasformazione parcheggia le tre palline al sicuro, poi le serve una alla volta.
|
||||||
|
activeBalls(c).forEach((b, i) => Object.assign(b, { x: i === 1 ? 411 : 229, y: 220 - i * 32, vx: 0, vy: 160,
|
||||||
|
ramp: null, contacts: [], trail: [], releaseAt: c.transform.until + i * 0.55, releasedFrom: i === 1 ? 'spira' : 'orbita' }));
|
||||||
|
}
|
||||||
|
function returnClassic(c, won = false) {
|
||||||
|
Object.assign(c, layout()); c.table = 'classic'; c.future = null; c.transform = null;
|
||||||
|
c.extraBalls = []; c.resets = {}; c.lastRamp = null; c.jackpotX = 1; c.saveUntil = 0; c.saved = false;
|
||||||
|
cancel(c); newBall(c);
|
||||||
|
if (won) { celebrate(c, 'REGOLO LIBERATO!', FUTURE.finale, '#74ffe0'); c.events.push({ kind: 'flipper-futuro-finale', power: 1 }); }
|
||||||
|
else announce(c, 'RITORNO AL RIVER');
|
||||||
|
}
|
||||||
// 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ì.
|
||||||
export function alongRamp(r, s) {
|
export function alongRamp(r, s) {
|
||||||
s = clamp(s, 0, r.len);
|
s = clamp(s, 0, r.len);
|
||||||
@@ -98,7 +134,7 @@ 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, extraBalls: [], locks: [], special: null, specialPaid: false, jackpotX: 1,
|
table: 'classic', transform: null, future: null, ball: null, extraBalls: [], locks: [], special: null, specialPaid: false, jackpotX: 1,
|
||||||
rampRun: { id: null, count: 0, until: 0 }, superOpenedAt: null,
|
rampRun: { id: null, count: 0, until: 0 }, superOpenedAt: null,
|
||||||
saveUntil: 0, resets: {}, result: null, lastRamp: null, lines: [], lineId: 0, fx: [], trail: [], visual: { time: 0 }, ...layout() };
|
saveUntil: 0, resets: {}, result: null, lastRamp: null, lines: [], lineId: 0, fx: [], trail: [], visual: { time: 0 }, ...layout() };
|
||||||
newBall(c);
|
newBall(c);
|
||||||
@@ -200,6 +236,14 @@ function moveFlippers(c, h) {
|
|||||||
}
|
}
|
||||||
function step(c) {
|
function step(c) {
|
||||||
c.sim += STEP;
|
c.sim += STEP;
|
||||||
|
if (c.transform) {
|
||||||
|
if (c.table !== 'future' && c.sim >= c.transform.at + FUTURE.reveal) openFuture(c);
|
||||||
|
if (c.sim < c.transform.until) { moveFlippers(c, STEP); return; }
|
||||||
|
c.transform = null; announce(c, 'CARICA I TRE SIGILLI');
|
||||||
|
}
|
||||||
|
if (c.table === 'future') {
|
||||||
|
c.regolo.x = 320 + Math.sin(c.sim * 1.6) * 24;
|
||||||
|
}
|
||||||
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.rampRun.count && c.sim > c.rampRun.until) c.rampRun = { id: null, count: 0, until: 0 };
|
if (c.rampRun.count && c.sim > c.rampRun.until) c.rampRun = { id: null, count: 0, until: 0 };
|
||||||
@@ -243,6 +287,7 @@ function step(c) {
|
|||||||
} else ball.rest = 0;
|
} else ball.rest = 0;
|
||||||
if (ball.y > FIELD.y1 + R * 2) drain(c, ball);
|
if (ball.y > FIELD.y1 + R * 2) drain(c, ball);
|
||||||
}
|
}
|
||||||
|
if (c.future?.won) returnClassic(c, true);
|
||||||
}
|
}
|
||||||
function collideBalls(a, b) {
|
function collideBalls(a, b) {
|
||||||
let dx = b.x - a.x, dy = b.y - a.y, d = Math.hypot(dx, dy);
|
let dx = b.x - a.x, dy = b.y - a.y, d = Math.hypot(dx, dy);
|
||||||
@@ -289,8 +334,8 @@ function enterRamps(c, from, ball) {
|
|||||||
// d'attrito); in cima la quota è massima. Da lì la velocità lungo il percorso, la posizione e la quota (`z`, 0..1).
|
// d'attrito); in cima la quota è massima. Da lì la velocità lungo il percorso, la posizione e la quota (`z`, 0..1).
|
||||||
export function rampHeight(r, s) { return s < r.top ? s / r.top : 1 - (s - r.top) / (r.len - r.top); }
|
export function rampHeight(r, s) { return s < r.top ? s / r.top : 1 - (s - r.top) / (r.len - r.top); }
|
||||||
function rideRamp(c, ball) {
|
function rideRamp(c, ball) {
|
||||||
const r = c.ramps.find(k => k.id === ball.ramp.id), on = ball.ramp, uphill = on.s < r.top;
|
const r = c.ramps.find(k => k.id === ball.ramp.id), on = ball.ramp, uphill = on.s < r.top, slope = r.slope ?? RAMP.slope;
|
||||||
on.v += (uphill ? -RAMP.slope : RAMP.slope * r.top / (r.len - r.top)) * STEP - Math.sign(on.v) * RAMP.drag * STEP;
|
on.v += (uphill ? -slope : slope * r.top / (r.len - r.top)) * STEP - Math.sign(on.v) * RAMP.drag * STEP;
|
||||||
on.s += on.v * STEP;
|
on.s += on.v * STEP;
|
||||||
const p = alongRamp(r, on.s), blend = Math.max(0, 1 - on.s / RAMP.blend);
|
const p = alongRamp(r, on.s), blend = Math.max(0, 1 - on.s / RAMP.blend);
|
||||||
ball.x = p.x + on.off * blend; ball.y = p.y; on.z = rampHeight(r, on.s);
|
ball.x = p.x + on.off * blend; ball.y = p.y; on.z = rampHeight(r, on.s);
|
||||||
@@ -300,6 +345,11 @@ function rideRamp(c, ball) {
|
|||||||
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;
|
||||||
ball.contacts = [...c.bumpers, c.regolo, ...c.targets, ...c.walls].map(o => o.contactId);
|
ball.contacts = [...c.bumpers, c.regolo, ...c.targets, ...c.walls].map(o => o.contactId);
|
||||||
|
if (c.table === 'future') {
|
||||||
|
c.future.hits[r.id] = Math.min(2, c.future.hits[r.id] + 1); r.lit = 3;
|
||||||
|
add(c, r.points, [p.x, p.y]); announce(c, `${r.id.toUpperCase()} ${c.future.hits[r.id]}/2`);
|
||||||
|
fx(c, 'ring', p.x, p.y); c.events.push({ kind: 'flipper-futuro-giro', power: 1 }); return;
|
||||||
|
}
|
||||||
if (r.id !== 'super') add(c, SCORE.ramp, [p.x, p.y]);
|
if (r.id !== 'super') 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 });
|
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 (rampFeature(c, r, ball)) return;
|
||||||
@@ -343,6 +393,8 @@ function rampFeature(c, r, ball) {
|
|||||||
c.locks = []; c.special = null; c.specialPaid = false; c.superOpenedAt = null; c.lastRamp = null;
|
c.locks = []; c.special = null; c.specialPaid = false; c.superOpenedAt = null; c.lastRamp = null;
|
||||||
c.rampRun = { id: null, count: 0, until: 0 }; c.saveUntil = 0; c.saved = true; c.balls++;
|
c.rampRun = { id: null, count: 0, until: 0 }; c.saveUntil = 0; c.saved = true; c.balls++;
|
||||||
celebrate(c, 'SUPER SPECIAL', SCORE.super * c.jackpotX, '#e1a0ff');
|
celebrate(c, 'SUPER SPECIAL', SCORE.super * c.jackpotX, '#e1a0ff');
|
||||||
|
c.transform = { at: c.sim, until: c.sim + FUTURE.duration };
|
||||||
|
c.events.push({ kind: 'flipper-esplosione', power: 1 });
|
||||||
say(c, 'PALLINA +1'); say(c, 'MULTIBALL!');
|
say(c, 'PALLINA +1'); say(c, 'MULTIBALL!');
|
||||||
c.events.push({ kind: 'flipper-super', power: 1 });
|
c.events.push({ kind: 'flipper-super', power: 1 });
|
||||||
for (const ramp of c.ramps) { ramp.lit = 5; fx(c, 'chase', 0, 0, { ramp: ramp.id }); }
|
for (const ramp of c.ramps) { ramp.lit = 5; fx(c, 'chase', 0, 0, { ramp: ramp.id }); }
|
||||||
@@ -389,6 +441,7 @@ function drain(c, ball) {
|
|||||||
c.lastRamp = null; c.saved = false; c.saveUntil = 0;
|
c.lastRamp = null; c.saved = false; c.saveUntil = 0;
|
||||||
announce(c, 'LOCK LIBERO'); newBall(c); return;
|
announce(c, 'LOCK LIBERO'); newBall(c); return;
|
||||||
}
|
}
|
||||||
|
if (c.table === 'future') returnClassic(c);
|
||||||
c.balls--; c.saved = false; c.lastRamp = null; c.jackpotX = 1;
|
c.balls--; c.saved = false; c.lastRamp = null; c.jackpotX = 1;
|
||||||
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';
|
||||||
@@ -460,6 +513,7 @@ function collideBumper(c, ball, b) {
|
|||||||
function collideRegolo(c, ball) {
|
function collideRegolo(c, ball) {
|
||||||
const hit = hitCircle(ball, c.regolo, 0.7);
|
const hit = hitCircle(ball, c.regolo, 0.7);
|
||||||
if (!hit || !hit.fresh || hit.vn < IMPACT) return;
|
if (!hit || !hit.fresh || hit.vn < IMPACT) return;
|
||||||
|
if (c.table === 'future' && Object.values(c.future.hits).every(n => n === 2)) { c.future.won = true; return; }
|
||||||
c.regolo.hit = 0.3; add(c, SCORE.regolo, [c.regolo.x, c.regolo.y - 14]); say(c, 'IL REGOLO!'); c.events.push({ kind: 'flipper-bersaglio', power: 1 }); fx(c, 'burst', c.regolo.x, c.regolo.y); fx(c, 'ring', c.regolo.x, c.regolo.y);
|
c.regolo.hit = 0.3; add(c, SCORE.regolo, [c.regolo.x, c.regolo.y - 14]); say(c, 'IL REGOLO!'); c.events.push({ kind: 'flipper-bersaglio', power: 1 }); fx(c, 'burst', c.regolo.x, c.regolo.y); fx(c, 'ring', c.regolo.x, c.regolo.y);
|
||||||
}
|
}
|
||||||
// Il flipper è una capsula che gira: alla pallina si aggiunge la velocità del punto toccato.
|
// Il flipper è una capsula che gira: alla pallina si aggiunge la velocità del punto toccato.
|
||||||
@@ -498,7 +552,7 @@ export function cancel(c, pointerId) {
|
|||||||
for (const f of Object.values(c.flippers)) f.held = all ? [] : f.held.filter(id => id !== pointerId);
|
for (const f of Object.values(c.flippers)) f.held = all ? [] : f.held.filter(id => id !== pointerId);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// I pulsanti «← Flipper» e «Flipper →» (tenuti premuti; da tastiera un colpo breve), e le frecce.
|
// I pulsanti «← Flipper» e «Flipper →» (tenuti premuti; da tastiera un colpo breve), le frecce e i due Shift.
|
||||||
export const turn = (c, dir, who = 'button') => c.step === 'play' && hold(c, dir < 0 ? 'left' : 'right', who, true);
|
export const turn = (c, dir, who = 'button') => c.step === 'play' && hold(c, dir < 0 ? 'left' : 'right', who, true);
|
||||||
export function stopTurn(c, who = 'button') {
|
export function stopTurn(c, who = 'button') {
|
||||||
if (who === 'click') return false; // il colpo breve finisce da sé
|
if (who === 'click') return false; // il colpo breve finisce da sé
|
||||||
@@ -509,18 +563,32 @@ 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 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) {
|
export function key(c, k, down) {
|
||||||
if (c.step !== 'play') return false; // a partita finita Spazio e Invio chiudono il risultato (main.js)
|
if (c.step !== 'play') return false; // a partita finita Spazio e Invio chiudono il risultato (main.js)
|
||||||
if (k === 'ArrowLeft' || k === 'ArrowRight') { hold(c, k === 'ArrowLeft' ? 'left' : 'right', 'key-' + k, down); return true; }
|
if (['ArrowLeft', 'ArrowRight', 'ShiftLeft', 'ShiftRight'].includes(k)) { hold(c, k.endsWith('Left') ? '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; }
|
// La molla: Spazio, o la freccia giù (si tira indietro come la molla vera).
|
||||||
|
if (k === ' ' || k === 'ArrowDown') { 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; }
|
if (k === 'Enter') { if (down) shoot(c); return true; }
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
// Per provare (main.js, con ?debug, tasto S): i due LOCK fatti davvero, tre giri a rampa, e la SUPER SPECIAL aperta.
|
||||||
|
export function openSuper(c) {
|
||||||
|
for (const id of ['sinistra', 'sinistra', 'sinistra', 'destra', 'destra', 'destra']) {
|
||||||
|
if (c.special === 'super') break;
|
||||||
|
const r = c.ramps.find(r => r.id === id);
|
||||||
|
if (waiting(c)) launch(c, 0.8);
|
||||||
|
if (!c.ball) return false;
|
||||||
|
Object.assign(c.ball, { x: r.path[0][0], y: r.mouth[0][1] + 14, vx: 0, vy: -470, ramp: null });
|
||||||
|
let entered = false;
|
||||||
|
for (let i = 0; i < 1000; i++) { update(c, STEP); if (c.ball?.ramp) entered = true; else if (entered) break; }
|
||||||
|
}
|
||||||
|
return c.special === 'super';
|
||||||
|
}
|
||||||
// Il pulsante Lancia: con la forza del cursore.
|
// Il pulsante Lancia: con la forza del cursore.
|
||||||
export const shoot = c => canThrow(c) && launch(c, c.power);
|
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, pallina premio vinta, ora il record' : `traguardo ${c.skill.goal}`;
|
const goal = c.result ? 'traguardo fatto, pallina premio vinta, ora il record' : `traguardo ${c.skill.goal}`;
|
||||||
const feature = c.special === 'super' ? 'SUPER SPECIAL aperta al centro: pallina premio e multiball!' : multiball(c) ? `MULTIBALL: ${activeBalls(c).length} palline. Le rampe valgono JACKPOT!` : c.locks.length ? `LOCK ${c.locks.length}/2. Completa tre passaggi sulla rampa ${c.special} per aprire la SUPER SPECIAL.` : c.rampRun.count ? `Rampa ${c.rampRun.id}: ${c.rampRun.count}/3, restano ${Math.ceil(c.rampRun.until - c.sim)} secondi.` : 'Tre passaggi sulla stessa rampa in 20 secondi: LOCK.';
|
const feature = c.transform ? 'SUPER SPECIAL! Il piano si apre: arriva il Regolo del futuro.' : c.table === 'future' ? `FUTURO: Orbita ${c.future.hits.orbita}/2 · Ponte ${c.future.hits.ponte}/2 · Spira ${c.future.hits.spira}/2. Poi Regolo mobile: +500000 e ritorno. Ultima pallina persa: ritorno, −1 vita.` : c.special === 'super' ? 'SUPER SPECIAL aperta al centro: pallina premio e multiball!' : multiball(c) ? `MULTIBALL: ${activeBalls(c).length} palline. Le rampe valgono JACKPOT!` : c.locks.length ? `LOCK ${c.locks.length}/2. Completa tre passaggi sulla rampa ${c.special} per aprire la SUPER SPECIAL.` : c.rampRun.count ? `Rampa ${c.rampRun.id}: ${c.rampRun.count}/3, restano ${Math.ceil(c.rampRun.until - c.sim)} secondi.` : 'Tre passaggi sulla stessa rampa in 20 secondi: LOCK.';
|
||||||
const tally = `Punti ${c.score} (${goal}), palline ${c.balls}.${feature ? ' ' + feature : ''}`;
|
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}`;
|
||||||
|
|||||||
+4
-2
@@ -13,12 +13,14 @@ import { versioned } from '../version.js';
|
|||||||
// suona il suo, più forte quanto più Andrea (`at`) le è vicino (mix). Le console possono stare anche fuori dal quadro:
|
// suona il suo, più forte quanto più Andrea (`at`) le è vicino (mix). Le console possono stare anche fuori dal quadro:
|
||||||
// è la musica della piazza accanto, che arriva da lontano.
|
// è la musica della piazza accanto, che arriva da lontano.
|
||||||
// `ok(cond)` dice se una condizione della storia vale adesso (game.js, check).
|
// `ok(cond)` dice se una condizione della storia vale adesso (game.js, check).
|
||||||
export function trackFor(story, { screen, scene, phase, closeup, at, ok = () => false }) {
|
// `table`: la tavola del primo piano, se ne ha più d'una (il flipper: 'classic' o 'future'), che può avere la sua musica.
|
||||||
|
export function trackFor(story, { screen, scene, phase, closeup, table, at, ok = () => false }) {
|
||||||
const m = story.music ?? {};
|
const m = story.music ?? {};
|
||||||
if (screen === 'splash') return null;
|
if (screen === 'splash') return null;
|
||||||
if (screen === 'title' || screen === 'intro') return m.title ?? null;
|
if (screen === 'title' || screen === 'intro') return m.title ?? null;
|
||||||
if (screen === 'ending') return m.ending ?? m.game ?? null;
|
if (screen === 'ending') return m.ending ?? m.game ?? null;
|
||||||
if (closeup && story.closeups?.[closeup]?.music) return story.closeups[closeup].music;
|
const cu = closeup && story.closeups?.[closeup];
|
||||||
|
if (cu?.music) return cu.tableMusic?.[table] ?? cu.music;
|
||||||
const sc = story.scenes[scene];
|
const sc = story.scenes[scene];
|
||||||
const stages = (sc?.stages ?? []).filter(st => ok(st.when));
|
const stages = (sc?.stages ?? []).filter(st => ok(st.when));
|
||||||
if (stages.length) return mix(stages, at ?? sc.entry ?? [320, 300]);
|
if (stages.length) return mix(stages, at ?? sc.entry ?? [320, 300]);
|
||||||
|
|||||||
@@ -175,6 +175,10 @@ export class Soundscape {
|
|||||||
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-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-super-open') { this.noiseSound(0.45, 1600, 0.04, { pan }); this.tone(180, 1200, 0.65, 0.06, 0, pan); for (const [i, f] of [659, 880, 1318].entries()) this.tone(f, f, 0.18, 0.05, 0.5 + i * 0.12, pan); }
|
else if (kind === 'flipper-super-open') { this.noiseSound(0.45, 1600, 0.04, { pan }); this.tone(180, 1200, 0.65, 0.06, 0, pan); for (const [i, f] of [659, 880, 1318].entries()) this.tone(f, f, 0.18, 0.05, 0.5 + i * 0.12, pan); }
|
||||||
else if (kind === 'flipper-super') { for (const [i, f] of [392, 523, 659, 784, 1046, 1318, 1568, 2093].entries()) this.tone(f, f * 1.005, 0.24, 0.055, i * 0.08, pan); for (const [i, f] of [523, 784, 1046].entries()) this.tone(f, f, 0.9, 0.045, 0.65 + i * 0.04, pan); }
|
else if (kind === 'flipper-super') { for (const [i, f] of [392, 523, 659, 784, 1046, 1318, 1568, 2093].entries()) this.tone(f, f * 1.005, 0.24, 0.055, i * 0.08, pan); for (const [i, f] of [523, 784, 1046].entries()) this.tone(f, f, 0.9, 0.045, 0.65 + i * 0.04, pan); }
|
||||||
|
// Timbri originali della tavola nascosta: botto grave e scintille (la musica è flipper-futuro.mp3, di Andrea).
|
||||||
|
else if (kind === 'flipper-esplosione') { this.noiseSound(1.1, 700, 0.14, { pan }); this.tone(130, 32, 1.2, 0.13, 0, pan); this.tone(90, 1800, 1.6, 0.06, 0.7, pan); }
|
||||||
|
else if (kind === 'flipper-futuro-giro') { this.tone(390, 1560, 0.38, 0.065, 0, pan); this.tone(1174, 1174, 0.25, 0.04, 0.25, pan); }
|
||||||
|
else if (kind === 'flipper-futuro-finale') { this.noiseSound(0.7, 1600, 0.07, { pan }); for (const [i, f] of [196, 294, 494, 740, 988, 1480].entries()) this.tone(f, f, 0.5, 0.055, i * 0.12, 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); }
|
||||||
|
|||||||
+9
-4
@@ -498,6 +498,9 @@ function debugKey(e) {
|
|||||||
if (k === 'k') { const text = `[${debugPoints.map(p => `[${p.join(', ')}]`).join(', ')}]`; copy(text); console.log(text); return true; }
|
if (k === 'k') { const text = `[${debugPoints.map(p => `[${p.join(', ')}]`).join(', ')}]`; copy(text); console.log(text); return true; }
|
||||||
if (k === 'x') { debugPoints.length = 0; return true; }
|
if (k === 'x') { debugPoints.length = 0; return true; }
|
||||||
if (k === 'g') { debugShown = !debugShown; return true; }
|
if (k === 'g') { debugShown = !debugShown; return true; }
|
||||||
|
// Nel flipper, S: tre giri sulla rampa sinistra e tre sulla destra (due LOCK), così la SUPER SPECIAL al centro è
|
||||||
|
// aperta e si prova subito il Regolo del futuro.
|
||||||
|
if (k === 's' && story.closeups[game?.closeup?.id]?.type === 'flipper' && game.closeup.table !== 'future') { flipper.openSuper(game.closeup); return true; }
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const debugView = () => debug && debugShown && game && { mouse, points: debugPoints, help: `Maiusc+clic punto · K copia ${debugPoints.length} · X azzera · G` };
|
const debugView = () => debug && debugShown && game && { mouse, points: debugPoints, help: `Maiusc+clic punto · K copia ${debugPoints.length} · X azzera · G` };
|
||||||
@@ -661,7 +664,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, anche con due dita o col tasto destro del mouse; oppure i pulsanti Flipper o le frecce sinistra e destra.' },
|
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 tieni premuto Spazio o la freccia giù). In gioco, tieni premuto sulla metà sinistra o destra della scena per alzare il flipper di quel lato, anche con due dita o col tasto destro del mouse; oppure i pulsanti Flipper, le frecce sinistra e destra o i due Shift. La SUPER SPECIAL fa esplodere la festa e apre Il Regolo del futuro: due giri completi su Orbita (35.000), Ponte (50.000) e Spira (65.000), poi colpisci il Regolo mobile per 500.000 e il ritorno al classico senza perdere vite. Perdere l’ultima pallina del multiball riporta al classico consumando una vita. Durante la trasformazione le palline sono al sicuro; comandi invariati.' },
|
||||||
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).
|
||||||
@@ -825,6 +828,8 @@ function updateChoices() {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ai minigiochi lo Shift arriva coi due lati distinti (ShiftLeft, ShiftRight): nel flipper comandano le due palette.
|
||||||
|
const keyName = e => e.key === 'Shift' ? e.code : e.key;
|
||||||
window.addEventListener('keydown', e => {
|
window.addEventListener('keydown', e => {
|
||||||
if (e.target instanceof HTMLInputElement && e.target.type === 'range' && e.key !== 'Escape') return;
|
if (e.target instanceof HTMLInputElement && e.target.type === 'range' && e.key !== 'Escape') return;
|
||||||
// Nella ricerca della mappa le lettere si scrivono; Esc prima svuota la casella, poi chiude.
|
// Nella ricerca della mappa le lettere si scrivono; Esc prima svuota la casella, poi chiude.
|
||||||
@@ -917,7 +922,7 @@ window.addEventListener('keydown', e => {
|
|||||||
if (e.key === 'Escape') { game.closeupClose(); return; }
|
if (e.key === 'Escape') { game.closeupClose(); return; }
|
||||||
const onButton = ['BUTTON', 'SUMMARY'].includes(e.target.tagName), onControl = onButton && e.target.closest?.('#bocce-controls');
|
const onButton = ['BUTTON', 'SUMMARY'].includes(e.target.tagName), onControl = onButton && e.target.closest?.('#bocce-controls');
|
||||||
// Il GP di Fortezza: frecce, Spazio e Invio finché si tengono giù (su un pulsante Spazio e Invio lo premono).
|
// Il GP di Fortezza: frecce, Spazio e Invio finché si tengono giù (su un pulsante Spazio e Invio lo premono).
|
||||||
if (!(onButton && [' ', 'Enter'].includes(e.key)) && mini().key?.(game.closeup, e.key, true, e.shiftKey)) { e.preventDefault(); return; }
|
if (!(onButton && [' ', 'Enter'].includes(e.key)) && mini().key?.(game.closeup, keyName(e), true, e.shiftKey)) { e.preventDefault(); return; }
|
||||||
if (onControl && [' ', 'Enter'].includes(e.key)) return;
|
if (onControl && [' ', 'Enter'].includes(e.key)) return;
|
||||||
const c = game.closeup, m = mini();
|
const c = game.closeup, m = mini();
|
||||||
if (['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].includes(e.key)) {
|
if (['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].includes(e.key)) {
|
||||||
@@ -964,7 +969,7 @@ window.addEventListener('keydown', e => {
|
|||||||
// Alle bocce e al biliardo, lasciando spazio o Invio parte il tiro.
|
// Alle bocce e al biliardo, lasciando spazio o Invio parte il tiro.
|
||||||
window.addEventListener('keyup', e => {
|
window.addEventListener('keyup', e => {
|
||||||
// Il GP di Fortezza tiene conto di ogni tasto per conto suo: lasciarne uno non toglie il gas del dito o degli altri.
|
// Il GP di Fortezza tiene conto di ogni tasto per conto suo: lasciarne uno non toglie il gas del dito o degli altri.
|
||||||
if (miniState() && mini().key?.(game.closeup, e.key, false)) return;
|
if (miniState() && mini().key?.(game.closeup, keyName(e), false)) return;
|
||||||
if (running && [' ', 'Enter'].includes(e.key)) game?.closeupRelease();
|
if (running && [' ', 'Enter'].includes(e.key)) game?.closeupRelease();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1192,7 +1197,7 @@ function frame(now) {
|
|||||||
const searching = game?.closeup?.id === 'fessura';
|
const searching = game?.closeup?.id === 'fessura';
|
||||||
music.duck = searching ? game.closeup.found >= 2 ? 0.05 : 0.25 : game?.state.flags.occhi_regolo ? 0.12 : voices.playing ? 0.45 : 1;
|
music.duck = searching ? game.closeup.found >= 2 ? 0.05 : 0.25 : game?.state.flags.occhi_regolo ? 0.12 : voices.playing ? 0.45 : 1;
|
||||||
music.update(trackFor(story, { screen, scene: game?.state.scene, phase: game?.phase(), closeup: game?.closeup?.id,
|
music.update(trackFor(story, { screen, scene: game?.state.scene, phase: game?.phase(), closeup: game?.closeup?.id,
|
||||||
at: game && [game.state.x, game.state.y], ok: cond => !!game && check(cond, game.state) }), dt);
|
table: game?.closeup?.table, at: game && [game.state.x, game.state.y], ok: cond => !!game && check(cond, game.state) }), dt);
|
||||||
// Un minuto senza toccare niente, a gioco fermo: pausa da sola.
|
// Un minuto senza toccare niente, a gioco fermo: pausa da sola.
|
||||||
if (running && now - idleSince > IDLE_PAUSE && !game.speech && !game.walking && !game.queue.length) openPause();
|
if (running && now - idleSince > IDLE_PAUSE && !game.speech && !game.walking && !game.queue.length) openPause();
|
||||||
ambience.configure({ active: screen === 'game' && running && !document.hidden && $('credits').hidden, muted: music.muted, duck: voices.playing ? 0.4 : 1 });
|
ambience.configure({ active: screen === 'game' && running && !document.hidden && $('credits').hidden, muted: music.muted, duck: voices.playing ? 0.4 : 1 });
|
||||||
|
|||||||
+2
-1
@@ -363,7 +363,8 @@ export const story = {
|
|||||||
// Il flipper del River (Andrea, 31 ottobre: «una tavola bellissima, con San Piero dentro»): la tavola di Codex,
|
// 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]
|
// fisica e parti meccaniche in flipper.js. [DA ANDREA: le battute sono provvisorie]
|
||||||
flipper: {
|
flipper: {
|
||||||
type: 'flipper',
|
// Le musiche di Andrea (Suno, 1 novembre): la classica, e quella del Regolo del futuro dopo la SUPER SPECIAL.
|
||||||
|
type: 'flipper', music: 'assets/audio/flipper.mp3', tableMusic: { future: 'assets/audio/flipper-futuro.mp3' },
|
||||||
picture: 'assets/closeups/river-flipper.png',
|
picture: 'assets/closeups/river-flipper.png',
|
||||||
backglass: 'assets/closeups/river-flipper-tabellone.png', // il fondale luminoso del tabellone (Codex: Trebbio, Fortezza, Regolo, fuochi)
|
backglass: 'assets/closeups/river-flipper-tabellone.png', // il fondale luminoso del tabellone (Codex: Trebbio, Fortezza, Regolo, fuochi)
|
||||||
names: { player: 'Andrea', cpu: 'il flipper' },
|
names: { player: 'Andrea', cpu: 'il flipper' },
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
// La versione del gioco: la cambia scripts/bump.sh, insieme a package.json.
|
// La versione del gioco: la cambia scripts/bump.sh, insieme a package.json.
|
||||||
export const VERSION = '0.1.69';
|
export const VERSION = '0.1.71';
|
||||||
|
|
||||||
// Immagini, musiche e voci con ?v=<versione>: a ogni versione l'indirizzo cambia e il browser le riscarica,
|
// Immagini, musiche e voci con ?v=<versione>: a ogni versione l'indirizzo cambia e il browser le riscarica,
|
||||||
// mentre fra una versione e l'altra le tiene in cache a lungo (nginx.conf).
|
// mentre fra una versione e l'altra le tiene in cache a lungo (nginx.conf).
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
// npm run browser:effects — immagini in /tmp/regolo-effects (oppure $OUT).
|
// npm run browser:effects — immagini in /tmp/regolo-effects (oppure $OUT).
|
||||||
import { chromium } from 'playwright';
|
import { chromium } from 'playwright';
|
||||||
import { staticServer } from './helpers.mjs';
|
import { staticServer } from './helpers.mjs';
|
||||||
|
import { ARGS } from './chromium.mjs';
|
||||||
import { mkdirSync, writeFileSync } from 'node:fs';
|
import { mkdirSync, writeFileSync } from 'node:fs';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
|
|
||||||
@@ -11,7 +12,7 @@ mkdirSync(out, { recursive: true });
|
|||||||
const server = await staticServer(), port = server.port;
|
const server = await staticServer(), port = server.port;
|
||||||
let browser;
|
let browser;
|
||||||
try {
|
try {
|
||||||
browser = await chromium.launch({ args: ['--autoplay-policy=no-user-gesture-required', '--use-gl=angle', '--use-angle=swiftshader', '--enable-unsafe-swiftshader'] });
|
browser = await chromium.launch({ args: ARGS });
|
||||||
const page = await browser.newPage({ viewport: { width: 640, height: 360 } });
|
const page = await browser.newPage({ viewport: { width: 640, height: 360 } });
|
||||||
const errors = [];
|
const errors = [];
|
||||||
page.on('pageerror', e => errors.push(e.message));
|
page.on('pageerror', e => errors.push(e.message));
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { chromium } from 'playwright';
|
|||||||
import { mkdirSync, writeFileSync } from 'node:fs';
|
import { mkdirSync, writeFileSync } from 'node:fs';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { staticServer } from './helpers.mjs';
|
import { staticServer } from './helpers.mjs';
|
||||||
|
import { ARGS } from './chromium.mjs';
|
||||||
|
|
||||||
const out = process.env.OUT ?? '/tmp/regolo-biliardo';
|
const out = process.env.OUT ?? '/tmp/regolo-biliardo';
|
||||||
mkdirSync(out, { recursive: true });
|
mkdirSync(out, { recursive: true });
|
||||||
@@ -64,7 +65,7 @@ async function checkArtwork(page, name) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
browser = await chromium.launch({ args: ['--autoplay-policy=no-user-gesture-required', '--use-gl=angle', '--use-angle=swiftshader', '--enable-unsafe-swiftshader'] });
|
browser = await chromium.launch({ args: ARGS });
|
||||||
for (const [name, mobile, viewport] of [
|
for (const [name, mobile, viewport] of [
|
||||||
['desktop', false, { width: 1280, height: 860 }],
|
['desktop', false, { width: 1280, height: 860 }],
|
||||||
['touch', true, { width: 844, height: 390 }],
|
['touch', true, { width: 844, height: 390 }],
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { chromium } from 'playwright';
|
|||||||
import { mkdirSync, writeFileSync } from 'node:fs';
|
import { mkdirSync, writeFileSync } from 'node:fs';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { staticServer } from './helpers.mjs';
|
import { staticServer } from './helpers.mjs';
|
||||||
|
import { ARGS } from './chromium.mjs';
|
||||||
|
|
||||||
const out = '/tmp/regolo-shots'; mkdirSync(out, { recursive: true });
|
const out = '/tmp/regolo-shots'; mkdirSync(out, { recursive: true });
|
||||||
const server = await staticServer(), errors = [];
|
const server = await staticServer(), errors = [];
|
||||||
@@ -51,7 +52,7 @@ async function artwork(page) {
|
|||||||
for (const [name, png] of Object.entries(pictures)) writeFileSync(`${out}/briscola-${name}.png`, Buffer.from(png, 'base64'));
|
for (const [name, png] of Object.entries(pictures)) writeFileSync(`${out}/briscola-${name}.png`, Buffer.from(png, 'base64'));
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
browser = await chromium.launch({ args: ['--autoplay-policy=no-user-gesture-required', '--use-gl=angle', '--use-angle=swiftshader', '--enable-unsafe-swiftshader'] });
|
browser = await chromium.launch({ args: ARGS });
|
||||||
const scenarios = ['mouse', 'touch', 'keyboard'].flatMap(mode => [
|
const scenarios = ['mouse', 'touch', 'keyboard'].flatMap(mode => [
|
||||||
{ name: `desktop-${mode}`, mode, viewport: { width: 1280, height: 860 } },
|
{ name: `desktop-${mode}`, mode, viewport: { width: 1280, height: 860 } },
|
||||||
{ name: `telefono-${mode}`, mode, viewport: { width: 390, height: 844 } },
|
{ name: `telefono-${mode}`, mode, viewport: { width: 390, height: 844 } },
|
||||||
|
|||||||
+2
-1
@@ -5,6 +5,7 @@
|
|||||||
// attese si sovrappongono. Una parte sola: node tests/browser.mjs --parte=minigiochi (--media-only: solo multimedia).
|
// attese si sovrappongono. Una parte sola: node tests/browser.mjs --parte=minigiochi (--media-only: solo multimedia).
|
||||||
import { chromium } from 'playwright';
|
import { chromium } from 'playwright';
|
||||||
import { staticServer } from './helpers.mjs';
|
import { staticServer } from './helpers.mjs';
|
||||||
|
import { ARGS } from './chromium.mjs';
|
||||||
import { mkdirSync, readFileSync } from 'node:fs';
|
import { mkdirSync, readFileSync } from 'node:fs';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { solution, route } from '../src/story/solution.js';
|
import { solution, route } from '../src/story/solution.js';
|
||||||
@@ -31,7 +32,7 @@ if (!PARTE && !process.argv.includes('--media-only')) {
|
|||||||
process.exit(esiti.every(e => e.code === 0) ? 0 : 1);
|
process.exit(esiti.every(e => e.code === 0) ? 0 : 1);
|
||||||
}
|
}
|
||||||
const server = await staticServer(), PORT = server.port;
|
const server = await staticServer(), PORT = server.port;
|
||||||
const browser = await chromium.launch({ args: ['--autoplay-policy=no-user-gesture-required', '--use-gl=angle', '--use-angle=swiftshader', '--enable-unsafe-swiftshader'] });
|
const browser = await chromium.launch({ args: ARGS });
|
||||||
const errors = [];
|
const errors = [];
|
||||||
|
|
||||||
async function open(viewport, query = '') {
|
async function open(viewport, query = '') {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { chromium } from 'playwright';
|
|||||||
import { mkdirSync } from 'node:fs';
|
import { mkdirSync } from 'node:fs';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { staticServer } from './helpers.mjs';
|
import { staticServer } from './helpers.mjs';
|
||||||
|
import { ARGS } from './chromium.mjs';
|
||||||
|
|
||||||
const out = process.env.OUT ?? '/tmp/regolo-bruciate';
|
const out = process.env.OUT ?? '/tmp/regolo-bruciate';
|
||||||
mkdirSync(out, { recursive: true });
|
mkdirSync(out, { recursive: true });
|
||||||
@@ -11,7 +12,7 @@ const server = await staticServer();
|
|||||||
let browser;
|
let browser;
|
||||||
const errors = [];
|
const errors = [];
|
||||||
try {
|
try {
|
||||||
browser = await chromium.launch({ args: ['--autoplay-policy=no-user-gesture-required', '--use-gl=angle', '--use-angle=swiftshader', '--enable-unsafe-swiftshader'] });
|
browser = await chromium.launch({ args: ARGS });
|
||||||
for (const [name, mobile, viewport] of [['desktop', false, { width: 1280, height: 860 }], ['touch', true, { width: 844, height: 390 }]]) {
|
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 });
|
const page = await browser.newPage({ viewport, hasTouch: mobile, isMobile: mobile });
|
||||||
page.on('pageerror', e => errors.push(e.message));
|
page.on('pageerror', e => errors.push(e.message));
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { chromium } from 'playwright';
|
|||||||
import { mkdirSync } from 'node:fs';
|
import { mkdirSync } from 'node:fs';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { staticServer } from './helpers.mjs';
|
import { staticServer } from './helpers.mjs';
|
||||||
|
import { ARGS } from './chromium.mjs';
|
||||||
|
|
||||||
const out = process.env.OUT ?? '/tmp/regolo-campana';
|
const out = process.env.OUT ?? '/tmp/regolo-campana';
|
||||||
mkdirSync(out, { recursive: true });
|
mkdirSync(out, { recursive: true });
|
||||||
@@ -11,7 +12,7 @@ const server = await staticServer();
|
|||||||
let browser;
|
let browser;
|
||||||
const errors = [];
|
const errors = [];
|
||||||
try {
|
try {
|
||||||
browser = await chromium.launch({ args: ['--autoplay-policy=no-user-gesture-required', '--use-gl=angle', '--use-angle=swiftshader', '--enable-unsafe-swiftshader'] });
|
browser = await chromium.launch({ args: ARGS });
|
||||||
for (const [name, mobile, viewport] of [['desktop', false, { width: 1280, height: 860 }], ['touch', true, { width: 844, height: 390 }]]) {
|
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 });
|
const page = await browser.newPage({ viewport, hasTouch: mobile, isMobile: mobile });
|
||||||
page.on('pageerror', e => errors.push(e.message));
|
page.on('pageerror', e => errors.push(e.message));
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { chromium } from 'playwright';
|
|||||||
import { mkdirSync } from 'node:fs';
|
import { mkdirSync } from 'node:fs';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { staticServer } from './helpers.mjs';
|
import { staticServer } from './helpers.mjs';
|
||||||
|
import { ARGS } from './chromium.mjs';
|
||||||
|
|
||||||
const out = process.env.OUT ?? '/tmp/regolo-cerbottana';
|
const out = process.env.OUT ?? '/tmp/regolo-cerbottana';
|
||||||
mkdirSync(out, { recursive: true });
|
mkdirSync(out, { recursive: true });
|
||||||
@@ -11,7 +12,7 @@ const server = await staticServer();
|
|||||||
let browser;
|
let browser;
|
||||||
const errors = [];
|
const errors = [];
|
||||||
try {
|
try {
|
||||||
browser = await chromium.launch({ args: ['--autoplay-policy=no-user-gesture-required', '--use-gl=angle', '--use-angle=swiftshader', '--enable-unsafe-swiftshader'] });
|
browser = await chromium.launch({ args: ARGS });
|
||||||
for (const [name, mobile, viewport] of [['desktop', false, { width: 1280, height: 860 }], ['touch', true, { width: 844, height: 390 }]]) {
|
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 });
|
const page = await browser.newPage({ viewport, hasTouch: mobile, isMobile: mobile });
|
||||||
page.on('pageerror', e => errors.push(e.message));
|
page.on('pageerror', e => errors.push(e.message));
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
// Come si apre Chromium nelle prove nel browser (solo da node: helpers.mjs lo carica anche la pagina).
|
||||||
|
// Con una scheda video (/dev/dri o /dev/nvidia0, anche nei contenitori lanciati con --gpus) si disegna lì: una
|
||||||
|
// schermata costa ~45 ms invece di ~600 con swiftshader, e le prove girano 5-10 volte più in fretta. Senza, swiftshader.
|
||||||
|
// REGOLO_GPU=0 forza swiftshader, REGOLO_GPU=1 la scheda video.
|
||||||
|
import { existsSync } from 'node:fs';
|
||||||
|
|
||||||
|
export const GPU = process.env.REGOLO_GPU ? process.env.REGOLO_GPU === '1' : ['/dev/dri/renderD128', '/dev/nvidia0'].some(existsSync);
|
||||||
|
const AUTOPLAY = '--autoplay-policy=no-user-gesture-required';
|
||||||
|
// Sempre sul processore: la prova del peso (peso.browser.mjs) misura così gli effetti, come su un telefono lento.
|
||||||
|
export const SWIFTSHADER = [AUTOPLAY, '--use-gl=angle', '--use-angle=swiftshader', '--enable-unsafe-swiftshader'];
|
||||||
|
export const ARGS = GPU ? [AUTOPLAY, '--use-gl=angle', '--use-angle=gl-egl', '--ignore-gpu-blocklist', '--enable-gpu-rasterization'] : SWIFTSHADER;
|
||||||
@@ -4,6 +4,7 @@ import { chromium } from 'playwright';
|
|||||||
import { mkdirSync } from 'node:fs';
|
import { mkdirSync } from 'node:fs';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { staticServer } from './helpers.mjs';
|
import { staticServer } from './helpers.mjs';
|
||||||
|
import { ARGS } from './chromium.mjs';
|
||||||
|
|
||||||
const out = process.env.OUT ?? '/tmp/regolo-condor';
|
const out = process.env.OUT ?? '/tmp/regolo-condor';
|
||||||
mkdirSync(out, { recursive: true });
|
mkdirSync(out, { recursive: true });
|
||||||
@@ -11,7 +12,7 @@ const server = await staticServer();
|
|||||||
let browser;
|
let browser;
|
||||||
const errors = [];
|
const errors = [];
|
||||||
try {
|
try {
|
||||||
browser = await chromium.launch({ args: ['--autoplay-policy=no-user-gesture-required', '--use-gl=angle', '--use-angle=swiftshader', '--enable-unsafe-swiftshader'] });
|
browser = await chromium.launch({ args: ARGS });
|
||||||
for (const [name, mobile, viewport] of [['desktop', false, { width: 1280, height: 860 }], ['touch', true, { width: 844, height: 390 }]]) {
|
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 });
|
const page = await browser.newPage({ viewport, hasTouch: mobile, isMobile: mobile });
|
||||||
page.on('pageerror', e => errors.push(e.message));
|
page.on('pageerror', e => errors.push(e.message));
|
||||||
|
|||||||
+93
-20
@@ -5,6 +5,7 @@ import { chromium } from 'playwright';
|
|||||||
import { mkdirSync } from 'node:fs';
|
import { mkdirSync } from 'node:fs';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { staticServer } from './helpers.mjs';
|
import { staticServer } from './helpers.mjs';
|
||||||
|
import { ARGS } from './chromium.mjs';
|
||||||
|
|
||||||
const out = process.env.OUT ?? '/tmp/regolo-flipper';
|
const out = process.env.OUT ?? '/tmp/regolo-flipper';
|
||||||
mkdirSync(out, { recursive: true });
|
mkdirSync(out, { recursive: true });
|
||||||
@@ -12,7 +13,7 @@ const server = await staticServer();
|
|||||||
let browser;
|
let browser;
|
||||||
const errors = [];
|
const errors = [];
|
||||||
try {
|
try {
|
||||||
browser = await chromium.launch({ args: ['--autoplay-policy=no-user-gesture-required', '--use-gl=angle', '--use-angle=swiftshader', '--enable-unsafe-swiftshader'] });
|
browser = await chromium.launch({ args: ARGS });
|
||||||
for (const [name, mobile, viewport] of [['desktop', false, { width: 1280, height: 860 }], ['touch', true, { width: 844, height: 390 }]]) {
|
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 });
|
const page = await browser.newPage({ viewport, hasTouch: mobile, isMobile: mobile });
|
||||||
page.on('pageerror', e => errors.push(e.message));
|
page.on('pageerror', e => errors.push(e.message));
|
||||||
@@ -45,9 +46,10 @@ try {
|
|||||||
await page.waitForFunction(() => game.closeup.holding && game.closeup.power > 0.3, null, { polling: 'raf' });
|
await page.waitForFunction(() => game.closeup.holding && game.closeup.power > 0.3, null, { polling: 'raf' });
|
||||||
await touch('touchEnd', []);
|
await touch('touchEnd', []);
|
||||||
} else {
|
} else {
|
||||||
await page.mouse.move(...P(440, 320)); await page.mouse.down();
|
// Da tastiera con la freccia giù (il mouse tiene la molla come il dito, provato sul telefono).
|
||||||
|
await page.keyboard.down('ArrowDown');
|
||||||
await page.waitForFunction(() => game.closeup.holding && game.closeup.power > 0.3, null, { polling: 'raf' });
|
await page.waitForFunction(() => game.closeup.holding && game.closeup.power > 0.3, null, { polling: 'raf' });
|
||||||
await page.mouse.up();
|
await page.keyboard.up('ArrowDown');
|
||||||
}
|
}
|
||||||
await page.waitForFunction(() => game.closeup && !game.closeup.ball?.waiting, null, { timeout: 5000 });
|
await page.waitForFunction(() => game.closeup && !game.closeup.ball?.waiting, null, { timeout: 5000 });
|
||||||
// Due dita sulla scena: sinistra, poi anche destra, poi si alza la sinistra e la destra resta su.
|
// Due dita sulla scena: sinistra, poi anche destra, poi si alza la sinistra e la destra resta su.
|
||||||
@@ -83,6 +85,13 @@ try {
|
|||||||
await page.keyboard.up('ArrowLeft');
|
await page.keyboard.up('ArrowLeft');
|
||||||
await page.click('#resume');
|
await page.click('#resume');
|
||||||
await page.waitForFunction(() => document.querySelector('#pause').hidden);
|
await page.waitForFunction(() => document.querySelector('#pause').hidden);
|
||||||
|
// I due Shift: il sinistro alza la paletta sinistra, il destro la destra.
|
||||||
|
await page.keyboard.down('ShiftRight');
|
||||||
|
await page.waitForFunction(() => game.closeup.flippers.right.held.includes('key-ShiftRight') && !game.closeup.flippers.left.held.length);
|
||||||
|
await page.keyboard.down('ShiftLeft');
|
||||||
|
await page.waitForFunction(() => game.closeup.flippers.left.held.includes('key-ShiftLeft'));
|
||||||
|
await page.keyboard.up('ShiftRight'); await page.keyboard.up('ShiftLeft');
|
||||||
|
await page.waitForFunction(() => !game.closeup.flippers.left.held.length && !game.closeup.flippers.right.held.length);
|
||||||
}
|
}
|
||||||
// Il traguardo aggiunge una pallina e lascia continuare la partita.
|
// Il traguardo aggiunge una pallina e lascia continuare la partita.
|
||||||
const reward = await page.evaluate(async () => {
|
const reward = await page.evaluate(async () => {
|
||||||
@@ -122,28 +131,92 @@ try {
|
|||||||
await page.screenshot({ path: `${out}/${name}-super.png` });
|
await page.screenshot({ path: `${out}/${name}-super.png` });
|
||||||
const multi = await page.evaluate(async () => {
|
const multi = await page.evaluate(async () => {
|
||||||
window.pinballShot('super');
|
window.pinballShot('super');
|
||||||
const m = await import('/src/engine/flipper.js'); m.update(game.closeup, 0.25); m.update(game.closeup, 0.25);
|
const m = await import('/src/engine/flipper.js'), c = game.closeup;
|
||||||
// Ferma solo la simulazione per fotografare le tre palline, prima che una possa scendere.
|
window.pinballResume = game.update.bind(game); game.update = () => {};
|
||||||
window.pinballResume = game.update.bind(game); game.update = dt => { game.closeup.visual.time += dt; };
|
window.pinballClassic = structuredClone(c); window.pinballClassic.transform = null;
|
||||||
return { balls: m.activeBalls(game.closeup).length, locks: game.closeup.locks.length, reserve: game.closeup.balls, time: game.closeup.visual.time };
|
(await import('/src/engine/render.js')).render(document.querySelector('#scene').getContext('2d'), game, { time: performance.now() });
|
||||||
|
m.update(c, 0.25); m.update(c, 0.2);
|
||||||
|
window.pinballExplosion = structuredClone(c);
|
||||||
|
return { balls: m.activeBalls(c).length, locks: c.locks.length, reserve: c.balls };
|
||||||
});
|
});
|
||||||
assert.deepEqual({ balls: multi.balls, locks: multi.locks, reserve: multi.reserve }, { balls: 3, locks: 0, reserve: 5 });
|
assert.deepEqual(multi, { balls: 3, locks: 0, reserve: 5 });
|
||||||
await page.waitForFunction(t => game.closeup.visual.time > t + 0.7, multi.time);
|
await page.screenshot({ path: `${out}/${name}-esplosione.png` });
|
||||||
|
await page.evaluate(async () => {
|
||||||
|
const m = await import('/src/engine/flipper.js');
|
||||||
|
for (let i = 0; i < 6; i++) m.update(game.closeup, 0.25);
|
||||||
|
});
|
||||||
|
await page.screenshot({ path: `${out}/${name}-trasformazione.png` });
|
||||||
|
await page.evaluate(async () => {
|
||||||
|
const m = await import('/src/engine/flipper.js');
|
||||||
|
for (let i = 0; i < 6; i++) m.update(game.closeup, 0.25);
|
||||||
|
window.pinballFuture = structuredClone(game.closeup);
|
||||||
|
});
|
||||||
|
assert.equal(await page.evaluate(() => game.closeup.table), 'future');
|
||||||
|
await page.screenshot({ path: `${out}/${name}-futuro.png` });
|
||||||
await page.screenshot({ path: `${out}/${name}-multiball.png` });
|
await page.screenshot({ path: `${out}/${name}-multiball.png` });
|
||||||
// L'arretrato grafico (vedi la memoria «effetti leggeri»): 150 fotogrammi disegnati di fila con tre palline e gli
|
// Anche nel nuovo campo: due dita indipendenti, i due Shift e la pausa vera dell'applicazione.
|
||||||
// effetti vivi, poi una lettura dei pixel che aspetta la grafica. Il flipper di Codex ne lasciava 4,2 s; deve
|
if (mobile) {
|
||||||
// restare sotto il secondo e mezzo anche col rendering software di queste prove.
|
await touch('touchStart', [P(250, 200), P(400, 200)]);
|
||||||
const backlog = await page.evaluate(async () => {
|
await page.waitForFunction(() => game.closeup.flippers.left.held.length && game.closeup.flippers.right.held.length);
|
||||||
|
await touch('touchEnd', []);
|
||||||
|
} else {
|
||||||
|
await page.keyboard.down('ShiftLeft'); await page.keyboard.down('ShiftRight');
|
||||||
|
await page.waitForFunction(() => game.closeup.flippers.left.held.length && game.closeup.flippers.right.held.length);
|
||||||
|
}
|
||||||
|
await page.evaluate(() => { game.update = window.pinballResume; });
|
||||||
|
await page.click('#pause-button');
|
||||||
|
const paused = await page.evaluate(() => ({ sim: game.closeup.sim, hits: game.closeup.future.hits, score: game.closeup.score }));
|
||||||
|
await page.waitForTimeout(250);
|
||||||
|
assert.deepEqual(await page.evaluate(() => ({ sim: game.closeup.sim, hits: game.closeup.future.hits, score: game.closeup.score })), paused);
|
||||||
|
assert.ok(await page.evaluate(() => !game.closeup.flippers.left.held.length && !game.closeup.flippers.right.held.length));
|
||||||
|
if (!mobile) { await page.keyboard.up('ShiftLeft'); await page.keyboard.up('ShiftRight'); }
|
||||||
|
await page.click('#resume');
|
||||||
|
await page.evaluate(() => { game.update = () => {}; Object.assign(game.closeup, structuredClone(window.pinballFuture)); });
|
||||||
|
// Stessa lettura sincrona dei pixel per classico, detonazione/trasformazione e futuro. Si svuota prima
|
||||||
|
// l'arretrato precedente; in gioco si riportano in alto solo le palline prossime alla buca per misurare
|
||||||
|
// tutti i 150 fotogrammi con tre palline, senza finire accidentalmente sul campo classico.
|
||||||
|
for (const mode of ['classico', 'esplosione', 'futuro']) {
|
||||||
|
const backlog = await page.evaluate(async mode => {
|
||||||
const m = await import('/src/engine/flipper.js'), r = await import('/src/engine/render.js'), c = game.closeup;
|
const m = await import('/src/engine/flipper.js'), r = await import('/src/engine/render.js'), c = game.closeup;
|
||||||
|
Object.assign(c, structuredClone(mode === 'classico' ? window.pinballClassic : mode === 'esplosione' ? window.pinballExplosion : window.pinballFuture));
|
||||||
const ctx = document.querySelector('#scene').getContext('2d');
|
const ctx = document.querySelector('#scene').getContext('2d');
|
||||||
ctx.getImageData(0, 0, 4, 4); // si aspetta prima l'arretrato di tutta la prova fin qui
|
r.render(ctx, game, { time: performance.now() }); ctx.getImageData(0, 0, 4, 4);
|
||||||
for (let i = 0; i < 150; i++) { m.update(c, 1 / 60); r.render(ctx, game, { time: performance.now() }); }
|
for (let i = 0; i < 150; i++) {
|
||||||
const t = performance.now(); ctx.getImageData(0, 0, 4, 4); return Math.round(performance.now() - t);
|
if (mode !== 'esplosione') for (const [k, b] of m.activeBalls(c).entries()) {
|
||||||
|
b.releaseAt = 0;
|
||||||
|
if (b.y > 295) Object.assign(b, { x: 260 + k * 55, y: 200, vx: 20, vy: -150, ramp: null });
|
||||||
|
}
|
||||||
|
m.update(c, 1 / 60); r.render(ctx, game, { time: performance.now() });
|
||||||
|
}
|
||||||
|
const t = performance.now(); ctx.getImageData(0, 0, 4, 4); const ms = Math.round(performance.now() - t);
|
||||||
|
if (mode === 'futuro' && c.table !== 'future') throw new Error('misura uscita dal futuro');
|
||||||
|
return ms;
|
||||||
|
}, mode);
|
||||||
|
console.log(`${name}: arretrato grafico ${mode} dopo 150 fotogrammi ${backlog} ms`);
|
||||||
|
assert.ok(backlog < 1500, `${mode}: arretrato grafico ${backlog} ms`);
|
||||||
|
}
|
||||||
|
// Percorsi veri fino al finale: la schermata mostra sigilli caricati e il bersaglio mobile da colpire.
|
||||||
|
await page.evaluate(async () => {
|
||||||
|
const m = await import('/src/engine/flipper.js'), c = game.closeup;
|
||||||
|
Object.assign(c, structuredClone(window.pinballFuture));
|
||||||
|
for (const b of c.extraBalls) b.releaseAt = c.sim + 1000;
|
||||||
|
for (const id of ['orbita', 'ponte', 'spira']) for (let i = 0; i < 2; i++) window.pinballShot(id);
|
||||||
});
|
});
|
||||||
console.log(`${name}: arretrato grafico dopo 150 fotogrammi ${backlog} ms`);
|
assert.deepEqual(await page.evaluate(() => game.closeup.future.hits), { orbita: 2, ponte: 2, spira: 2 });
|
||||||
assert.ok(backlog < 1500, `arretrato grafico ${backlog} ms`);
|
await page.screenshot({ path: `${out}/${name}-sigilli.png` });
|
||||||
await page.waitForFunction(() => document.querySelector('#scene').style.backgroundImage.startsWith('url('), null, { timeout: 2000 }); // il mobile sullo sfondo CSS
|
await page.evaluate(async () => {
|
||||||
await page.evaluate(() => { game.update = window.pinballResume; delete window.pinballResume; delete window.pinballShot; });
|
const m = await import('/src/engine/flipper.js'), c = game.closeup;
|
||||||
|
Object.assign(c.ball, { x: 320 + Math.sin((c.sim + m.STEP) * 1.6) * 24, y: c.regolo.y + c.regolo.r + 4, vx: 0, vy: -250, ramp: null, contacts: [] });
|
||||||
|
m.update(c, m.STEP);
|
||||||
|
});
|
||||||
|
assert.equal(await page.evaluate(() => game.closeup.table), 'classic');
|
||||||
|
await page.evaluate(async () => {
|
||||||
|
(await import('/src/engine/render.js')).render(document.querySelector('#scene').getContext('2d'), game, { time: performance.now() });
|
||||||
|
const m = await import('/src/engine/flipper.js'); m.update(game.closeup, 0.25); m.update(game.closeup, 0.25);
|
||||||
|
});
|
||||||
|
await page.screenshot({ path: `${out}/${name}-ritorno.png` });
|
||||||
|
await page.waitForFunction(() => document.querySelector('#scene').style.backgroundImage.startsWith('url('));
|
||||||
|
await page.evaluate(() => { game.update = window.pinballResume; for (const key of ['pinballResume', 'pinballShot', 'pinballClassic', 'pinballExplosion', 'pinballFuture']) delete window[key]; });
|
||||||
// Si consumano le palline rimaste, compresa quella vinta, per arrivare al risultato reale.
|
// Si consumano le palline rimaste, compresa quella vinta, per arrivare al risultato reale.
|
||||||
const finalScore = await page.evaluate(async () => {
|
const finalScore = await page.evaluate(async () => {
|
||||||
const m = await import('/src/engine/flipper.js'), c = game.closeup;
|
const m = await import('/src/engine/flipper.js'), c = game.closeup;
|
||||||
|
|||||||
+138
-18
@@ -146,10 +146,14 @@ test('comandi: la molla si carica e si lancia lasciando; annullare non lancia; d
|
|||||||
m.turn(c, 1, 'click'); m.stopTurn(c, 'click'); assert.ok(c.flippers.right.held.includes('click'));
|
m.turn(c, 1, 'click'); m.stopTurn(c, 'click'); assert.ok(c.flippers.right.held.includes('click'));
|
||||||
const a0 = c.flippers.right.angle; run(c, 0.1); assert.notEqual(c.flippers.right.angle, a0, 'il flipper si è mosso'); run(c, 0.3); assert.ok(!c.flippers.right.held.length);
|
const a0 = c.flippers.right.angle; run(c, 0.1); assert.notEqual(c.flippers.right.angle, a0, 'il flipper si è mosso'); run(c, 0.3); assert.ok(!c.flippers.right.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, '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, 'ShiftLeft', true)); assert.ok(c.flippers.left.held.includes('key-ShiftLeft') && !c.flippers.right.held.length);
|
||||||
|
m.key(c, 'ShiftRight', true); assert.ok(c.flippers.right.held.includes('key-ShiftRight'));
|
||||||
|
m.key(c, 'ShiftLeft', false); m.key(c, 'ShiftRight', false); assert.ok(!c.flippers.left.held.length && !c.flippers.right.held.length);
|
||||||
assert.ok(!m.key(c, 'x', true));
|
assert.ok(!m.key(c, 'x', true));
|
||||||
assert.ok(!m.setPower(c, 0.3), 'in gioco il cursore della molla non vale');
|
assert.ok(!m.setPower(c, 0.3), 'in gioco il cursore della molla non vale');
|
||||||
// Spazio: carica e lancia; Invio: lancia con la forza del cursore; a partita finita i tasti non sono suoi.
|
// Spazio: carica e lancia; Invio: lancia con la forza del cursore; a partita finita i tasti non sono suoi.
|
||||||
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 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 g = m.newFlipper(11); assert.ok(m.key(g, 'ArrowDown', true)); run(g, 0.3); assert.ok(g.holding, 'anche la freccia giù carica la molla'); m.key(g, 'ArrowDown', false); assert.ok(!m.waiting(g));
|
||||||
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…');
|
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…');
|
||||||
e.step = 'result'; e.result = { winner: 'cpu', score: 0, goal: 120000 }; assert.ok(!m.key(e, ' ', true) && !m.key(e, 'Enter', true));
|
e.step = 'result'; e.result = { winner: 'cpu', score: 0, goal: 120000 }; assert.ok(!m.key(e, ' ', true) && !m.key(e, 'Enter', true));
|
||||||
});
|
});
|
||||||
@@ -312,13 +316,16 @@ test('due LOCK aprono la SUPER SPECIAL: jackpot, pallina premio e multiball real
|
|||||||
assert.equal(m.activeBalls(c).length, 3); assert.ok(m.multiball(c));
|
assert.equal(m.activeBalls(c).length, 3); assert.ok(m.multiball(c));
|
||||||
assert.equal(new Set(m.activeBalls(c)).size, 3, 'tre palline distinte');
|
assert.equal(new Set(m.activeBalls(c)).size, 3, 'tre palline distinte');
|
||||||
assert.ok(c.events.some(e => e.kind === 'flipper-super'));
|
assert.ok(c.events.some(e => e.kind === 'flipper-super'));
|
||||||
const winScore = c.score, lives = c.balls;
|
assert.ok(c.transform, 'la SUPER avvia la trasformazione');
|
||||||
rampShot(c, ids[0]);
|
run(c, m.FUTURE.duration);
|
||||||
assert.ok(c.score - winScore >= m.SCORE.ramp + m.SCORE.jackpot, 'in multiball le rampe pagano jackpot');
|
assert.equal(c.table, 'future');
|
||||||
|
const lives = c.balls;
|
||||||
|
for (const b of m.activeBalls(c)) b.releaseAt = 0;
|
||||||
// Metti tutte le palline sotto il campo nello stesso passo: la vita si scala una volta soltanto.
|
// Metti tutte le palline sotto il campo nello stesso passo: la vita si scala una volta soltanto.
|
||||||
for (const ball of m.activeBalls(c)) Object.assign(ball, { x: 320, y: 348, vx: 0, vy: 300, ramp: null, waiting: false, releaseAt: 0 });
|
for (const ball of m.activeBalls(c)) Object.assign(ball, { x: 320, y: 348, vx: 0, vy: 300, ramp: null, waiting: false, releaseAt: 0 });
|
||||||
c.saveUntil = 0; m.update(c, m.STEP);
|
c.saveUntil = 0; m.update(c, m.STEP);
|
||||||
assert.equal(c.balls, lives - 1); assert.equal(c.extraBalls.length, 0); assert.ok(m.waiting(c));
|
assert.equal(c.balls, lives - 1); assert.equal(c.extraBalls.length, 0); assert.ok(m.waiting(c));
|
||||||
|
assert.equal(c.table, 'classic', 'persa l’ultima si torna al River');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -334,25 +341,28 @@ test('la finestra LOCK scade in simulazione, cambiare rampa interrompe la serie
|
|||||||
assert.equal(c.rampRun.count, 0);
|
assert.equal(c.rampRun.count, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('la SUPER accompagna alla paletta sinistra e il multiball libera le palline sfalsate', () => {
|
test('la trasformazione protegge le tre palline, poi le libera sfalsate verso le palette', () => {
|
||||||
for (const level of Object.keys(m.LEVELS)) {
|
for (const level of Object.keys(m.LEVELS)) {
|
||||||
const c = m.newFlipper(77, { level }); doubleLock(c); rampShot(c, 'super');
|
const c = m.newFlipper(77, { level }); doubleLock(c); rampShot(c, 'super');
|
||||||
const main = c.ball, delayed = c.extraBalls.find(b => b.releaseAt > c.sim);
|
const before = m.activeBalls(c).map(b => [b.x, b.y]), score = c.score, lives = c.balls;
|
||||||
assert.equal(main.x, 229); assert.equal(main.y, 220); assert.equal(main.vx, 0); assert.equal(main.vy, 160);
|
run(c, 0.8); assert.deepEqual(m.activeBalls(c).map(b => [b.x, b.y]), before);
|
||||||
for (const b of c.extraBalls) { assert.ok([229, 411].includes(b.x)); assert.equal(b.vx, 0, 'i lock non sparano verso le buche laterali'); }
|
assert.equal(c.score, score); assert.equal(c.balls, lives);
|
||||||
assert.ok(delayed, 'una serratura aspetta prima di liberare la terza pallina');
|
run(c, m.FUTURE.duration - 0.8);
|
||||||
const position = [delayed.x, delayed.y]; run(c, 0.2);
|
assert.equal(c.table, 'future'); assert.equal(c.transform, null);
|
||||||
assert.deepEqual([delayed.x, delayed.y], position, 'durante il ritardo resta nel lock');
|
const main = c.ball, delayed = c.extraBalls.at(-1), position = [delayed.x, delayed.y];
|
||||||
run(c, 0.3); assert.notDeepEqual([delayed.x, delayed.y], position, 'poi viene liberata');
|
assert.ok(delayed.releaseAt > c.sim);
|
||||||
// Isola la pallina della SUPER per verificare che un colpo normale possa recuperarla.
|
run(c, 0.2); assert.deepEqual([delayed.x, delayed.y], position);
|
||||||
c.extraBalls = []; c.ball = main;
|
// Isola la prima pallina per verificare il recupero dalla paletta, senza alterare la geometria.
|
||||||
|
const d = structuredClone(c); d.extraBalls = [];
|
||||||
let recovered = false;
|
let recovered = false;
|
||||||
for (let i = 0; i < 480; i++) {
|
for (let i = 0; i < 480; i++) {
|
||||||
if (main.x > 284 && main.y > 300) m.hold(c, 'left', 'test', true);
|
if (d.ball.x > 284 && d.ball.y > 300) m.hold(d, 'left', 'test', true);
|
||||||
m.update(c, m.STEP);
|
m.update(d, m.STEP);
|
||||||
if (main.vy < -250) { recovered = true; break; }
|
if (d.ball.vy < -250) { recovered = true; break; }
|
||||||
}
|
}
|
||||||
assert.ok(recovered, `${level}: ripresa dalla paletta, senza cadere direttamente nella buca centrale`);
|
assert.ok(recovered, `${level}: prima pallina ripresa dalla paletta`);
|
||||||
|
run(c, 1); assert.notDeepEqual([delayed.x, delayed.y], position);
|
||||||
|
assert.ok(Number.isFinite(main.x));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -369,7 +379,8 @@ test('perdere una sostitutiva restituisce un lock alla volta senza consumare vit
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('il multiball ha contatti indipendenti, urti fra palline e palette che avanzano una sola volta', () => {
|
test('il multiball ha contatti indipendenti, urti fra palline e palette che avanzano una sola volta', () => {
|
||||||
const c = m.newFlipper(24); doubleLock(c); rampShot(c, 'super');
|
const c = m.newFlipper(24); doubleLock(c); rampShot(c, 'super'); run(c, m.FUTURE.duration);
|
||||||
|
for (const b of m.activeBalls(c)) b.releaseAt = 0;
|
||||||
const single = m.newFlipper(24); m.hold(single, 'left', 'test', true); m.hold(c, 'left', 'test', true);
|
const single = m.newFlipper(24); m.hold(single, 'left', 'test', true); m.hold(c, 'left', 'test', true);
|
||||||
m.update(single, m.STEP); m.update(c, m.STEP);
|
m.update(single, m.STEP); m.update(c, m.STEP);
|
||||||
assert.ok(Math.abs(c.flippers.left.angle - single.flippers.left.angle) < 1e-10);
|
assert.ok(Math.abs(c.flippers.left.angle - single.flippers.left.angle) < 1e-10);
|
||||||
@@ -469,3 +480,112 @@ test('dietro la Pieve non c\'è più il canale che portava dritto all\'uscita si
|
|||||||
assert.ok(!inChannel, 'mai fra la parete e il dorso dei bersagli');
|
assert.ok(!inChannel, 'mai fra la parete e il dorso dei bersagli');
|
||||||
assert.ok(c.ball.x > 210 || c.ball.y < 146, `in campo (${Math.round(c.ball.x)}, ${Math.round(c.ball.y)})`);
|
assert.ok(c.ball.x > 210 || c.ball.y < 146, `in campo (${Math.round(c.ball.x)}, ${Math.round(c.ball.y)})`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function inFuture(level = 'seria') {
|
||||||
|
const c = m.newFlipper(96, { level }); doubleLock(c); rampShot(c, 'super'); run(c, m.FUTURE.duration);
|
||||||
|
// Gli altri tiri restano parcheggiati mentre si misura un percorso alla volta.
|
||||||
|
for (const b of c.extraBalls) b.releaseAt = c.sim + 1000;
|
||||||
|
c.ball.releaseAt = 0; return c;
|
||||||
|
}
|
||||||
|
|
||||||
|
test('il futuro è un altro campo: tre giri reali, quote, punti e sigilli solo a percorso completo', () => {
|
||||||
|
for (const level of Object.keys(m.LEVELS)) {
|
||||||
|
const c = inFuture(level), old = m.layout();
|
||||||
|
assert.notDeepEqual(c.walls, old.walls); assert.notDeepEqual(c.bumpers, old.bumpers);
|
||||||
|
assert.equal(c.targets.length, 0); assert.equal(c.lanes.length, 0);
|
||||||
|
for (const w of c.walls) for (const [x, y] of [w.a, w.b]) assert.ok(x >= 200 && x <= 448 && y >= 5 && y <= 336);
|
||||||
|
for (const r of c.ramps) {
|
||||||
|
const score = c.score;
|
||||||
|
for (let n = 1; n <= 3; n++) {
|
||||||
|
const result = rampShot(c, r.id);
|
||||||
|
assert.ok(result.frames > 50); assert.ok(result.top < r.mouth[0][1] - 40);
|
||||||
|
assert.equal(c.score - score, r.points * n); assert.equal(c.future.hits[r.id], Math.min(n, 2));
|
||||||
|
assert.ok(result.exit.x === 229 || result.exit.x === 411, 'ritorno nelle corsie interne');
|
||||||
|
}
|
||||||
|
// Un ingresso debole sulla salita ricade e non carica il sigillo.
|
||||||
|
const d = inFuture(level), rd = d.ramps.find(k => k.id === r.id), points = d.score;
|
||||||
|
const result = ride(d, { x: rd.path[0][0], y: rd.mouth[0][1] + 1, vx: 0, vy: -270, ramp: null });
|
||||||
|
assert.equal(d.future.hits[r.id], 0); assert.equal(d.score, points); assert.ok(result.exit.y >= rd.mouth[0][1]);
|
||||||
|
}
|
||||||
|
const score = c.score, lives = c.balls;
|
||||||
|
const nextX = 320 + Math.sin((c.sim + m.STEP) * 1.6) * 24;
|
||||||
|
put(c, { x: nextX, y: c.regolo.y + c.regolo.r + 4, vx: 0, vy: -250, ramp: null, contacts: [] });
|
||||||
|
m.update(c, m.STEP);
|
||||||
|
assert.equal(c.table, 'classic'); assert.equal(c.score, score + m.FUTURE.finale);
|
||||||
|
assert.equal(c.balls, lives); assert.ok(m.waiting(c)); assert.equal(c.extraBalls.length, 0);
|
||||||
|
assert.deepEqual(c.ramps.map(r => r.id), ['sinistra', 'destra', 'super']);
|
||||||
|
doubleLock(c); rampShot(c, 'super'); assert.ok(c.transform, 'il viaggio è ripetibile');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('nel futuro il Regolo si muove, gli urti e la molla funzionano, niente finale prima dei sigilli', () => {
|
||||||
|
const c = inFuture(), x = c.regolo.x;
|
||||||
|
c.ball.waiting = true; run(c, 0.5); assert.notEqual(c.regolo.x, x);
|
||||||
|
assert.ok(m.key(c, ' ', true)); run(c, 0.4); m.key(c, ' ', false); assert.ok(!m.waiting(c));
|
||||||
|
// Un respingente devia davvero la pallina.
|
||||||
|
const b = c.bumpers[0], score = c.score;
|
||||||
|
put(c, { x: b.x, y: b.y + b.r + 4, vx: 0, vy: -200, contacts: [] }); m.update(c, m.STEP);
|
||||||
|
assert.ok(c.ball.vy > 0); assert.equal(c.score, score + m.SCORE.bumper);
|
||||||
|
put(c, { x: c.regolo.x, y: c.regolo.y + c.regolo.r + 4, vx: 0, vy: -200, contacts: [] }); m.update(c, m.STEP);
|
||||||
|
assert.equal(c.table, 'future'); assert.equal(c.score, score + m.SCORE.bumper + m.SCORE.regolo);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('futuro e trasformazione sono serializzabili; annullare e disegnare non fanno avanzare la partita', () => {
|
||||||
|
for (const transforming of [false, true]) {
|
||||||
|
const c = inFuture(); if (transforming) { doubleLockAfterReturn(c); }
|
||||||
|
m.hold(c, 'left', 'touch', true); m.cancel(c);
|
||||||
|
const saved = JSON.parse(JSON.stringify(c));
|
||||||
|
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; } });
|
||||||
|
for (let i = 0; i < 10; i++) drawFlipper(ctx, c, story.closeups.flipper, {});
|
||||||
|
assert.deepEqual(JSON.parse(JSON.stringify(c)), saved, 'disegno e pausa non consumano tempo');
|
||||||
|
assert.ok(!c.flippers.left.held.length);
|
||||||
|
run(c, 0.5); run(saved, 0.5);
|
||||||
|
assert.deepEqual(JSON.parse(JSON.stringify(c)), saved, 'ripresa identica dopo il passaggio JSON');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
function doubleLockAfterReturn(c) {
|
||||||
|
for (const b of m.activeBalls(c)) Object.assign(b, { x: 320, y: 348, vx: 0, vy: 300, ramp: null, releaseAt: 0 });
|
||||||
|
m.update(c, m.STEP); doubleLock(c); rampShot(c, 'super');
|
||||||
|
}
|
||||||
|
|
||||||
|
test('il ball save nel futuro salva una sola volta, conserva campo e sigilli, poi torna al classico', () => {
|
||||||
|
const c = inFuture(); c.extraBalls = []; c.future.hits.ponte = 1;
|
||||||
|
c.saveUntil = c.sim + 2; const lives = c.balls;
|
||||||
|
put(c, { x: 320, y: 348, vx: 0, vy: 300, ramp: null }); m.update(c, m.STEP);
|
||||||
|
assert.equal(c.table, 'future'); assert.equal(c.balls, lives); assert.ok(m.waiting(c)); assert.equal(c.future.hits.ponte, 1);
|
||||||
|
m.launch(c, 0.7); assert.equal(m.saveLeft(c), 0);
|
||||||
|
put(c, { x: 320, y: 348, vx: 0, vy: 300, ramp: null }); m.update(c, m.STEP);
|
||||||
|
assert.equal(c.table, 'classic'); assert.equal(c.balls, lives - 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('i tre percorsi del futuro sono raggiungibili con il colpo fisico della paletta', () => {
|
||||||
|
for (const [id, x, y] of [['orbita', 288, 300], ['ponte', 290, 306], ['spira', 308, 320]]) {
|
||||||
|
const c = m.newFlipper(9);
|
||||||
|
Object.assign(c, m.futureLayout(), { table: 'future', future: { hits: { orbita: 0, ponte: 0, spira: 0 } } });
|
||||||
|
m.launch(c, 0.8); c.saveUntil = 0;
|
||||||
|
put(c, { x, y, vx: 0, vy: 180, inPlay: true }); m.hold(c, 'left', 'test', true);
|
||||||
|
let entered = null;
|
||||||
|
for (let i = 0; i < 160; i++) { m.update(c, m.STEP); if (c.ball?.ramp) { entered = c.ball.ramp.id; break; } }
|
||||||
|
assert.equal(entered, id, 'il tiro parte dalla paletta, non dall’imbocco');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('salvare l’avventura durante il futuro conserva il checkpoint, come negli altri minigiochi', () => {
|
||||||
|
const g = new Game(story); g.go('via_provinciale'); settle(g);
|
||||||
|
g.act('use', 'river'); settle(g); g.choose(g.options().findIndex(o => o.text.startsWith('Una partita alla bona'))); settle(g);
|
||||||
|
const checkpoint = structuredClone(g.checkpoint);
|
||||||
|
Object.assign(g.closeup, inFuture('bona'));
|
||||||
|
const before = structuredClone(g.closeup), save = JSON.parse(JSON.stringify(g.savable()));
|
||||||
|
assert.deepEqual(save, checkpoint); assert.deepEqual(g.closeup, before, 'salvare non consuma sigilli o palline');
|
||||||
|
const loaded = new Game(story, { state: save });
|
||||||
|
assert.equal(loaded.state.scene, 'via_provinciale'); assert.equal(loaded.closeup, null);
|
||||||
|
g.closeupCancel(); assert.equal(g.closeup.table, 'future');
|
||||||
|
g.closeupClose(); settle(g); assert.ok(g.state.flags.flipper_vinto, 'il traguardo resta acquisito uscendo dal futuro');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('per provare, openSuper fa davvero i due LOCK e apre la SUPER SPECIAL (main.js, ?debug, tasto S)', () => {
|
||||||
|
const c = m.newFlipper(73, { level: 'bona' });
|
||||||
|
assert.ok(m.openSuper(c));
|
||||||
|
assert.equal(c.special, 'super'); assert.equal(c.locks.length, 2); assert.equal(c.table, 'classic');
|
||||||
|
});
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { chromium } from 'playwright';
|
|||||||
import { mkdirSync } from 'node:fs';
|
import { mkdirSync } from 'node:fs';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { staticServer } from './helpers.mjs';
|
import { staticServer } from './helpers.mjs';
|
||||||
|
import { ARGS } from './chromium.mjs';
|
||||||
|
|
||||||
const out = process.env.OUT ?? '/tmp/regolo-gara';
|
const out = process.env.OUT ?? '/tmp/regolo-gara';
|
||||||
mkdirSync(out, { recursive: true });
|
mkdirSync(out, { recursive: true });
|
||||||
@@ -12,7 +13,7 @@ let browser;
|
|||||||
const errors = [];
|
const errors = [];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
browser = await chromium.launch({ args: ['--autoplay-policy=no-user-gesture-required', '--use-gl=angle', '--use-angle=swiftshader', '--enable-unsafe-swiftshader'] });
|
browser = await chromium.launch({ args: ARGS });
|
||||||
for (const [name, mobile, viewport, level] of [
|
for (const [name, mobile, viewport, level] of [
|
||||||
['touch', true, { width: 844, height: 390 }, 'seria'],
|
['touch', true, { width: 844, height: 390 }, 'seria'],
|
||||||
['portrait', true, { width: 390, height: 844 }, 'bona'],
|
['portrait', true, { width: 390, height: 844 }, 'bona'],
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { chromium } from 'playwright';
|
|||||||
import { mkdirSync } from 'node:fs';
|
import { mkdirSync } from 'node:fs';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { staticServer } from './helpers.mjs';
|
import { staticServer } from './helpers.mjs';
|
||||||
|
import { ARGS } from './chromium.mjs';
|
||||||
|
|
||||||
const out = process.env.OUT ?? '/tmp/regolo-ingorgo';
|
const out = process.env.OUT ?? '/tmp/regolo-ingorgo';
|
||||||
mkdirSync(out, { recursive: true });
|
mkdirSync(out, { recursive: true });
|
||||||
@@ -12,7 +13,7 @@ const server = await staticServer();
|
|||||||
let browser;
|
let browser;
|
||||||
const errors = [];
|
const errors = [];
|
||||||
try {
|
try {
|
||||||
browser = await chromium.launch({ args: ['--autoplay-policy=no-user-gesture-required', '--use-gl=angle', '--use-angle=swiftshader', '--enable-unsafe-swiftshader'] });
|
browser = await chromium.launch({ args: ARGS });
|
||||||
const page = await browser.newPage({ viewport: { width: 1280, height: 860 } });
|
const page = await browser.newPage({ viewport: { width: 1280, height: 860 } });
|
||||||
page.on('pageerror', e => { errors.push(e.message); console.log('ERR', e.message); });
|
page.on('pageerror', e => { errors.push(e.message); console.log('ERR', e.message); });
|
||||||
await page.goto(`http://127.0.0.1:${server.port}/`);
|
await page.goto(`http://127.0.0.1:${server.port}/`);
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { chromium } from 'playwright';
|
|||||||
import { mkdirSync } from 'node:fs';
|
import { mkdirSync } from 'node:fs';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { staticServer } from './helpers.mjs';
|
import { staticServer } from './helpers.mjs';
|
||||||
|
import { ARGS } from './chromium.mjs';
|
||||||
|
|
||||||
const out = process.env.OUT ?? '/tmp/regolo-micro';
|
const out = process.env.OUT ?? '/tmp/regolo-micro';
|
||||||
mkdirSync(out, { recursive: true });
|
mkdirSync(out, { recursive: true });
|
||||||
@@ -12,7 +13,7 @@ let browser;
|
|||||||
const errors = [];
|
const errors = [];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
browser = await chromium.launch({ args: ['--autoplay-policy=no-user-gesture-required', '--use-gl=angle', '--use-angle=swiftshader', '--enable-unsafe-swiftshader'] });
|
browser = await chromium.launch({ args: ARGS });
|
||||||
for (const [name, mobile, viewport] of [
|
for (const [name, mobile, viewport] of [
|
||||||
['desktop', false, { width: 1280, height: 860 }],
|
['desktop', false, { width: 1280, height: 860 }],
|
||||||
['touch', true, { width: 844, height: 390 }],
|
['touch', true, { width: 844, height: 390 }],
|
||||||
|
|||||||
@@ -97,6 +97,8 @@ export async function checkMultimedia(browser, port, out, errors) {
|
|||||||
await settle();
|
await settle();
|
||||||
await page.waitForSelector('#ending:not([hidden])');
|
await page.waitForSelector('#ending:not([hidden])');
|
||||||
assert.ok(await page.evaluate(() => game.checkpoint.ended));
|
assert.ok(await page.evaluate(() => game.checkpoint.ended));
|
||||||
|
// Il vento sfuma in un attimo: con la scheda video la prova ci arriva prima che abbia finito.
|
||||||
|
await page.waitForFunction(() => ambience.nodes.size === 0, null, { timeout: 3000 }).catch(() => {});
|
||||||
assert.equal(await page.evaluate(() => ambience.nodes.size), 0, 'alla fine non resta il vento della fessura');
|
assert.equal(await page.evaluate(() => ambience.nodes.size), 0, 'alla fine non resta il vento della fessura');
|
||||||
// Misura il segnale realmente prodotto da Web Audio: udibile, finito, senza saturazione.
|
// Misura il segnale realmente prodotto da Web Audio: udibile, finito, senza saturazione.
|
||||||
if (!mobile) {
|
if (!mobile) {
|
||||||
|
|||||||
@@ -24,6 +24,15 @@ test('il brano giusto per titolo, fine e scena', () => {
|
|||||||
assert.equal(trackFor({ ...s, scenes: { ...s.scenes, i: { indoor: true } } }, { screen: 'game', scene: 'i', phase: sera }), 'g.mp3');
|
assert.equal(trackFor({ ...s, scenes: { ...s.scenes, i: { indoor: true } } }, { screen: 'game', scene: 'i', phase: sera }), 'g.mp3');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('il flipper del River ha la sua musica, e un\'altra la tavola del futuro', () => {
|
||||||
|
const track = trackFor(story, { screen: 'game', scene: 'river', closeup: 'flipper' });
|
||||||
|
assert.equal(track, 'assets/audio/flipper.mp3');
|
||||||
|
assert.equal(trackFor(story, { screen: 'game', scene: 'river', closeup: 'flipper', table: 'classic' }), track);
|
||||||
|
const future = trackFor(story, { screen: 'game', scene: 'river', closeup: 'flipper', table: 'future' });
|
||||||
|
assert.equal(future, 'assets/audio/flipper-futuro.mp3', 'il Regolo del futuro ha la sua musica');
|
||||||
|
for (const f of [track, future]) assert.ok(existsSync(f), f);
|
||||||
|
});
|
||||||
|
|
||||||
test('volume e muto', () => {
|
test('volume e muto', () => {
|
||||||
const m = new Music({ volume: 0.5, muted: true });
|
const m = new Music({ volume: 0.5, muted: true });
|
||||||
assert.equal(m.setVolume(1.4), 1);
|
assert.equal(m.setVolume(1.4), 1);
|
||||||
|
|||||||
@@ -0,0 +1,106 @@
|
|||||||
|
// Il peso della grafica (Andrea, 1 novembre 2026: «prima senza GPU scoprivamo anche gli effetti troppo pesanti»): le
|
||||||
|
// altre prove disegnano sulla scheda video e un effetto pesante non le rallenta più. Questa usa sempre swiftshader, il
|
||||||
|
// disegno sul processore, come un telefono lento: nel gioco vero, in ogni scena nelle tre fasi e in ogni primo piano,
|
||||||
|
// lascia girare il gioco e misura i fotogrammi al secondo e l'arretrato grafico (il disegno chiesto e non ancora fatto).
|
||||||
|
// node tests/peso.browser.mjs (PESO_TUTTI=1 stampa ogni misura).
|
||||||
|
//
|
||||||
|
// Come si misura, e perché così (vedi la memoria «effetti leggeri»): mai leggere i pixel della tela del gioco, perché
|
||||||
|
// dopo qualche lettura Chrome la passa al processore e la misura cambia ciò che misura; l'arretrato si cronometra
|
||||||
|
// leggendo una tela nuova, abbastanza grande da stare anche lei sulla grafica, che deve aspettare tutto il disegno
|
||||||
|
// in coda prima della sua. Tarata il 1° novembre 2026 sul PC di Andrea: una scena normale, a immagini caricate, pochi ms
|
||||||
|
// di arretrato e 60 fotogrammi; un ctx.filter blur a tutto schermo 816 ms e 34, quaranta tratti larghi trasparenti 718 ms
|
||||||
|
// e 36, trenta veli trasparenti a tutto schermo 374 ms e 45.
|
||||||
|
import { chromium } from 'playwright';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { staticServer } from './helpers.mjs';
|
||||||
|
import { SWIFTSHADER } from './chromium.mjs';
|
||||||
|
|
||||||
|
const MAX_BACKLOG = 500, MIN_FPS = 30; // la scena più cara, pieve_interno di pomeriggio, fino a 355 ms sotto carico // ms di arretrato dopo un secondo di gioco; fotogrammi al secondo
|
||||||
|
const server = await staticServer();
|
||||||
|
let browser;
|
||||||
|
const errors = [], slow = [], rows = [];
|
||||||
|
try {
|
||||||
|
browser = await chromium.launch({ args: SWIFTSHADER });
|
||||||
|
const page = await browser.newPage({ viewport: { width: 1280, height: 860 } });
|
||||||
|
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);
|
||||||
|
const probe = () => {
|
||||||
|
const c = document.createElement('canvas'); c.width = 640; c.height = 360;
|
||||||
|
const g = c.getContext('2d'); g.fillRect(0, 0, 9, 9);
|
||||||
|
const t = performance.now(); g.getImageData(0, 0, 1, 1); return performance.now() - t;
|
||||||
|
};
|
||||||
|
window.peso = async () => {
|
||||||
|
// Prima si lascia smaltire l'arretrato di prima, compreso il caricamento delle immagini alla prima visita di una
|
||||||
|
// scena (che costa una volta sola e non è un effetto): finché la lettura non torna svelta, al più due secondi.
|
||||||
|
await new Promise(r => setTimeout(r, 150));
|
||||||
|
for (let i = 0; i < 20 && probe() > 30; i++) await new Promise(r => setTimeout(r, 100));
|
||||||
|
let n = 0; const t0 = performance.now();
|
||||||
|
await new Promise(done => { const tick = () => { n++; if (performance.now() - t0 < 800) requestAnimationFrame(tick); else done(); }; requestAnimationFrame(tick); });
|
||||||
|
const backlog = Math.round(probe());
|
||||||
|
return { fps: Math.round(n * 1000 / (performance.now() - t0)), backlog };
|
||||||
|
};
|
||||||
|
});
|
||||||
|
// Una misura storta per un attimo di carico del PC (le altre prove girano insieme) si ripete dopo un secondo di
|
||||||
|
// respiro: boccia solo se è pesante due volte.
|
||||||
|
const measure = async label => {
|
||||||
|
let m = await page.evaluate(() => peso());
|
||||||
|
const heavy = m => m.backlog > MAX_BACKLOG || m.fps < MIN_FPS;
|
||||||
|
if (heavy(m)) { await page.waitForTimeout(1000); m = await page.evaluate(() => peso()); }
|
||||||
|
rows.push({ label, ...m });
|
||||||
|
if (heavy(m)) slow.push(`${label}: ${m.fps} fotogrammi al secondo, arretrato ${m.backlog} ms`);
|
||||||
|
if (process.env.PESO_TUTTI) console.log(`${String(m.fps).padStart(3)} fps ${String(m.backlog).padStart(5)} ms ${label}`);
|
||||||
|
};
|
||||||
|
// Le scene, nelle tre fasi (le condizioni delle fasi come in ambient.browser.mjs). Prima un giro in tutte, perché la
|
||||||
|
// prima visita paga il caricamento delle immagini, che non è un effetto.
|
||||||
|
const scenes = await page.evaluate(() => Object.keys(game.story.scenes));
|
||||||
|
for (const id of scenes) {
|
||||||
|
await page.evaluate(id => { const sc = game.story.scenes[id]; game.state.scene = id; [game.state.x, game.state.y] = sc.entry; }, id);
|
||||||
|
await page.waitForTimeout(120);
|
||||||
|
}
|
||||||
|
for (const [phase, flags] of [['pomeriggio', {}], ['tramonto', { indizio_regolo: true }], ['sera', { casetta_aperta: true }]]) {
|
||||||
|
for (const id of scenes) {
|
||||||
|
await page.evaluate(({ id, flags }) => {
|
||||||
|
const g = game, sc = g.story.scenes[id];
|
||||||
|
Object.assign(g.state.flags, flags);
|
||||||
|
g.state.scene = id; [g.state.x, g.state.y] = sc.entry; g.enteredAt = g.time;
|
||||||
|
}, { id, flags });
|
||||||
|
await measure(`${id} (${phase})`);
|
||||||
|
await page.evaluate(flags => { for (const k of Object.keys(flags)) delete game.state.flags[k]; }, flags);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// I primi piani, aperti come li apre la storia; il flipper anche con la pallina in gioco e nel Regolo del futuro.
|
||||||
|
const closeups = await page.evaluate(() => Object.keys(game.story.closeups));
|
||||||
|
for (const id of closeups) {
|
||||||
|
const opened = await page.evaluate(id => {
|
||||||
|
game.state.scene = 'piazza';
|
||||||
|
try { game.run({ closeup: id }); } catch (e) { return String(e); }
|
||||||
|
return true;
|
||||||
|
}, id);
|
||||||
|
if (opened !== true) { console.log(`${id}: non si apre da solo (${opened})`); continue; }
|
||||||
|
await measure(`primo piano ${id}`);
|
||||||
|
if (await page.evaluate(() => game.story.closeups[game.closeup.id].type === 'flipper')) {
|
||||||
|
await page.evaluate(async () => { const m = await import('/src/engine/flipper.js'); m.launch(game.closeup, 0.8); });
|
||||||
|
await measure(`primo piano ${id} (in gioco)`);
|
||||||
|
const future = await page.evaluate(async () => {
|
||||||
|
const m = await import('/src/engine/flipper.js');
|
||||||
|
if (!m.futureLayout) return false;
|
||||||
|
Object.assign(game.closeup, m.futureLayout(), { table: 'future', future: { hits: { orbita: 0, ponte: 0, spira: 0 } } });
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
if (future) await measure(`primo piano ${id} (Regolo del futuro)`);
|
||||||
|
}
|
||||||
|
await page.evaluate(() => { game.closeup = null; game.queue.length = 0; game.speech = null; });
|
||||||
|
}
|
||||||
|
const worst = [...rows].sort((a, b) => b.backlog - a.backlog).slice(0, 4).map(r => `${r.label} ${r.backlog} ms`).join(', ');
|
||||||
|
console.log(`Peso della grafica con swiftshader: ${rows.length} misure; gli arretrati più alti: ${worst}`);
|
||||||
|
assert.deepEqual(slow, [], `troppo pesanti con swiftshader (soglie: ${MAX_BACKLOG} ms, ${MIN_FPS} fotogrammi al secondo)`);
|
||||||
|
assert.deepEqual(errors, []);
|
||||||
|
console.log('Prova del peso: OK');
|
||||||
|
} finally {
|
||||||
|
await browser?.close();
|
||||||
|
server.kill();
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import { chromium } from 'playwright';
|
|||||||
import { mkdirSync } from 'node:fs';
|
import { mkdirSync } from 'node:fs';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { staticServer } from './helpers.mjs';
|
import { staticServer } from './helpers.mjs';
|
||||||
|
import { ARGS } from './chromium.mjs';
|
||||||
|
|
||||||
const out = process.env.OUT ?? '/tmp/regolo-risveglio';
|
const out = process.env.OUT ?? '/tmp/regolo-risveglio';
|
||||||
mkdirSync(out, { recursive: true });
|
mkdirSync(out, { recursive: true });
|
||||||
@@ -11,7 +12,7 @@ const server = await staticServer();
|
|||||||
let browser;
|
let browser;
|
||||||
const errors = [];
|
const errors = [];
|
||||||
try {
|
try {
|
||||||
browser = await chromium.launch({ args: ['--autoplay-policy=no-user-gesture-required', '--use-gl=angle', '--use-angle=swiftshader', '--enable-unsafe-swiftshader'] });
|
browser = await chromium.launch({ args: ARGS });
|
||||||
// Sul computer la versione con la sfocatura vera, sul telefono (touch) quella leggera senza filtri.
|
// Sul computer la versione con la sfocatura vera, sul telefono (touch) quella leggera senza filtri.
|
||||||
for (const mobile of [false, true]) {
|
for (const mobile of [false, true]) {
|
||||||
const page = await browser.newPage(mobile ? { viewport: { width: 844, height: 390 }, hasTouch: true, isMobile: true } : { viewport: { width: 1280, height: 860 } });
|
const page = await browser.newPage(mobile ? { viewport: { width: 844, height: 390 }, hasTouch: true, isMobile: true } : { viewport: { width: 1280, height: 860 } });
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { chromium } from 'playwright';
|
|||||||
import { mkdirSync } from 'node:fs';
|
import { mkdirSync } from 'node:fs';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { staticServer } from './helpers.mjs';
|
import { staticServer } from './helpers.mjs';
|
||||||
|
import { ARGS } from './chromium.mjs';
|
||||||
|
|
||||||
const out = '/tmp/regolo-shots'; mkdirSync(out, { recursive: true });
|
const out = '/tmp/regolo-shots'; mkdirSync(out, { recursive: true });
|
||||||
const server = await staticServer(), errors = [];
|
const server = await staticServer(), errors = [];
|
||||||
@@ -44,7 +45,7 @@ async function wait(page) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
browser = await chromium.launch({ args: ['--autoplay-policy=no-user-gesture-required', '--use-gl=angle', '--use-angle=swiftshader', '--enable-unsafe-swiftshader'] });
|
browser = await chromium.launch({ args: ARGS });
|
||||||
const scenarios = [
|
const scenarios = [
|
||||||
{ name: 'desktop-mouse', mode: 'mouse', viewport: { width: 1280, height: 860 }, full: true },
|
{ name: 'desktop-mouse', mode: 'mouse', viewport: { width: 1280, height: 860 }, full: true },
|
||||||
{ name: 'telefono-touch', mode: 'touch', viewport: { width: 390, height: 844 } },
|
{ name: 'telefono-touch', mode: 'touch', viewport: { width: 390, height: 844 } },
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { chromium } from 'playwright';
|
|||||||
import { mkdirSync } from 'node:fs';
|
import { mkdirSync } from 'node:fs';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { staticServer } from './helpers.mjs';
|
import { staticServer } from './helpers.mjs';
|
||||||
|
import { ARGS } from './chromium.mjs';
|
||||||
|
|
||||||
const out = process.env.OUT ?? '/tmp/regolo-smarronata';
|
const out = process.env.OUT ?? '/tmp/regolo-smarronata';
|
||||||
mkdirSync(out, { recursive: true });
|
mkdirSync(out, { recursive: true });
|
||||||
@@ -11,7 +12,7 @@ const server = await staticServer();
|
|||||||
let browser;
|
let browser;
|
||||||
const errors = [];
|
const errors = [];
|
||||||
try {
|
try {
|
||||||
browser = await chromium.launch({ args: ['--autoplay-policy=no-user-gesture-required', '--use-gl=angle', '--use-angle=swiftshader', '--enable-unsafe-swiftshader'] });
|
browser = await chromium.launch({ args: ARGS });
|
||||||
const page = await browser.newPage({ viewport: { width: 1280, height: 860 } });
|
const page = await browser.newPage({ viewport: { width: 1280, height: 860 } });
|
||||||
page.on('pageerror', e => { errors.push(e.message); console.log('ERR', e.message); });
|
page.on('pageerror', e => { errors.push(e.message); console.log('ERR', e.message); });
|
||||||
await page.goto(`http://127.0.0.1:${server.port}/`);
|
await page.goto(`http://127.0.0.1:${server.port}/`);
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { chromium } from 'playwright';
|
|||||||
import { mkdirSync } from 'node:fs';
|
import { mkdirSync } from 'node:fs';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { staticServer } from './helpers.mjs';
|
import { staticServer } from './helpers.mjs';
|
||||||
|
import { ARGS } from './chromium.mjs';
|
||||||
|
|
||||||
const out = process.env.OUT ?? '/tmp/regolo-telefono';
|
const out = process.env.OUT ?? '/tmp/regolo-telefono';
|
||||||
mkdirSync(out, { recursive: true });
|
mkdirSync(out, { recursive: true });
|
||||||
@@ -11,7 +12,7 @@ const server = await staticServer();
|
|||||||
let browser;
|
let browser;
|
||||||
const errors = [];
|
const errors = [];
|
||||||
try {
|
try {
|
||||||
browser = await chromium.launch({ args: ['--autoplay-policy=no-user-gesture-required', '--use-gl=angle', '--use-angle=swiftshader', '--enable-unsafe-swiftshader'] });
|
browser = await chromium.launch({ args: ARGS });
|
||||||
const page = await browser.newPage({ viewport: { width: 1280, height: 860 } });
|
const page = await browser.newPage({ viewport: { width: 1280, height: 860 } });
|
||||||
page.on('pageerror', e => { errors.push(e.message); console.log('ERR', e.message); });
|
page.on('pageerror', e => { errors.push(e.message); console.log('ERR', e.message); });
|
||||||
await page.goto(`http://127.0.0.1:${server.port}/`);
|
await page.goto(`http://127.0.0.1:${server.port}/`);
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { chromium } from 'playwright';
|
|||||||
import { mkdirSync } from 'node:fs';
|
import { mkdirSync } from 'node:fs';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { staticServer } from './helpers.mjs';
|
import { staticServer } from './helpers.mjs';
|
||||||
|
import { ARGS } from './chromium.mjs';
|
||||||
|
|
||||||
const out = '/tmp/regolo-shots'; mkdirSync(out, { recursive: true });
|
const out = '/tmp/regolo-shots'; mkdirSync(out, { recursive: true });
|
||||||
const server = await staticServer(), errors = [];
|
const server = await staticServer(), errors = [];
|
||||||
@@ -13,6 +14,8 @@ async function tap(page, mode, selector) {
|
|||||||
else await page.click(selector);
|
else await page.click(selector);
|
||||||
}
|
}
|
||||||
async function playCard(page, mode, index, signal) {
|
async function playCard(page, mode, index, signal) {
|
||||||
|
// Un tasto premuto mentre qualcuno parla salta la battuta e basta (come nel gioco): si aspetta che abbia finito.
|
||||||
|
if (mode === 'keyboard') await page.waitForFunction(() => !game.speech);
|
||||||
if (signal) {
|
if (signal) {
|
||||||
if (mode === 'keyboard') await page.keyboard.press(signal[0]);
|
if (mode === 'keyboard') await page.keyboard.press(signal[0]);
|
||||||
else await tap(page, mode, `#tressette-${signal}`);
|
else await tap(page, mode, `#tressette-${signal}`);
|
||||||
@@ -46,7 +49,7 @@ async function wait(page) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
browser = await chromium.launch({ args: ['--autoplay-policy=no-user-gesture-required', '--use-gl=angle', '--use-angle=swiftshader', '--enable-unsafe-swiftshader'] });
|
browser = await chromium.launch({ args: ARGS });
|
||||||
const scenarios = [
|
const scenarios = [
|
||||||
{ name: 'desktop-mouse', mode: 'mouse', viewport: { width: 1280, height: 860 }, full: true },
|
{ name: 'desktop-mouse', mode: 'mouse', viewport: { width: 1280, height: 860 }, full: true },
|
||||||
{ name: 'telefono-touch', mode: 'touch', viewport: { width: 390, height: 844 } },
|
{ name: 'telefono-touch', mode: 'touch', viewport: { width: 390, height: 844 } },
|
||||||
|
|||||||
@@ -4,13 +4,14 @@ import { chromium } from 'playwright';
|
|||||||
import { mkdirSync } from 'node:fs';
|
import { mkdirSync } from 'node:fs';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { staticServer } from './helpers.mjs';
|
import { staticServer } from './helpers.mjs';
|
||||||
|
import { ARGS } from './chromium.mjs';
|
||||||
|
|
||||||
const out = process.env.OUT ?? '/tmp/regolo-shots';
|
const out = process.env.OUT ?? '/tmp/regolo-shots';
|
||||||
mkdirSync(out, { recursive: true });
|
mkdirSync(out, { recursive: true });
|
||||||
const server = await staticServer();
|
const server = await staticServer();
|
||||||
let browser;
|
let browser;
|
||||||
try {
|
try {
|
||||||
browser = await chromium.launch({ args: ['--autoplay-policy=no-user-gesture-required', '--use-gl=angle', '--use-angle=swiftshader', '--enable-unsafe-swiftshader'] });
|
browser = await chromium.launch({ args: ARGS });
|
||||||
const page = await browser.newPage({ viewport: { width: 1280, height: 720 } });
|
const page = await browser.newPage({ viewport: { width: 1280, height: 720 } });
|
||||||
const errors = [];
|
const errors = [];
|
||||||
page.on('pageerror', e => errors.push(e.message));
|
page.on('pageerror', e => errors.push(e.message));
|
||||||
|
|||||||
+23
-6
@@ -2,15 +2,15 @@
|
|||||||
// e l'impronta cambia solo se cambia un ingresso.
|
// e l'impronta cambia solo se cambia un ingresso.
|
||||||
import { test } from 'node:test';
|
import { test } from 'node:test';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { readdirSync } from 'node:fs';
|
import { readdirSync, readFileSync } from 'node:fs';
|
||||||
import { SUITES, ENGINE, inputsOf, ignored, fingerprint, plan } from '../scripts/verify.mjs';
|
import { SUITES, ENGINE, inputsOf, ignored, fingerprint, plan } from '../scripts/verify.mjs';
|
||||||
|
|
||||||
const FILES = ['index.html', 'style.css', 'src/main.js', 'src/version.js', 'package.json', 'package-lock.json', 'Makefile', 'README.md',
|
const FILES = ['index.html', 'style.css', 'src/main.js', 'src/version.js', 'package.json', 'package-lock.json', 'Makefile', 'README.md',
|
||||||
'src/engine/game.js', 'src/engine/render.js', 'src/engine/ambient.js', 'src/engine/bocce.js', 'src/engine/nuovo.js',
|
'src/engine/game.js', 'src/engine/render.js', 'src/engine/ambient.js', 'src/engine/bocce.js', 'src/engine/nuovo.js',
|
||||||
'src/engine/briscola.js', 'src/engine/briscola-render.js', 'src/engine/flipper.js', 'src/engine/corsa.js', 'src/story/piloti.js',
|
'src/engine/briscola.js', 'src/engine/briscola-render.js', 'src/engine/flipper.js', 'src/engine/flipper-render.js', 'src/engine/corsa.js', 'src/story/piloti.js',
|
||||||
'src/story/story.js', 'src/story/solution.js', 'assets/scenes/piazza.png', 'assets/closeups/micro-parcheggio.png',
|
'src/story/story.js', 'src/story/solution.js', 'assets/scenes/piazza.png', 'assets/closeups/micro-parcheggio.png',
|
||||||
'assets/closeups/gp/boccino.png', 'assets/muro/index.html', 'assets/video/briscola.mp4', 'docs/BRISCOLA.md', 'docs/STATO.md',
|
'assets/closeups/gp/boccino.png', 'assets/muro/index.html', 'assets/video/briscola.mp4', 'docs/BRISCOLA.md', 'docs/STATO.md',
|
||||||
'tests/helpers.mjs', 'tests/browser.mjs', 'tests/briscola.browser.mjs', 'tests/briscola.test.mjs', 'tests/story.test.mjs',
|
'tests/helpers.mjs', 'tests/chromium.mjs', 'tests/browser.mjs', 'tests/briscola.browser.mjs', 'tests/briscola.test.mjs', 'tests/story.test.mjs',
|
||||||
'scripts/verify.mjs', 'scripts/bump.sh', 'tools/slice_sprites.py', '.verify-ok.json'];
|
'scripts/verify.mjs', 'scripts/bump.sh', 'tools/slice_sprites.py', '.verify-ok.json'];
|
||||||
const suitesWith = path => Object.keys(SUITES).filter(n => inputsOf(n, FILES).includes(path));
|
const suitesWith = path => Object.keys(SUITES).filter(n => inputsOf(n, FILES).includes(path));
|
||||||
const islands = Object.keys(SUITES).filter(n => !SUITES[n].core), cores = Object.keys(SUITES).filter(n => SUITES[n].core);
|
const islands = Object.keys(SUITES).filter(n => !SUITES[n].core), cores = Object.keys(SUITES).filter(n => SUITES[n].core);
|
||||||
@@ -26,12 +26,12 @@ test('ogni prova nel browser di tests/ ha la sua voce, e una sola', () => {
|
|||||||
|
|
||||||
test('la briscola si riprova solo se cambia la briscola o il motore', () => {
|
test('la briscola si riprova solo se cambia la briscola o il motore', () => {
|
||||||
assert.deepEqual(suitesWith('src/engine/briscola.js'), ['briscola']);
|
assert.deepEqual(suitesWith('src/engine/briscola.js'), ['briscola']);
|
||||||
assert.deepEqual(suitesWith('src/engine/briscola-render.js'), ['briscola']);
|
assert.deepEqual(suitesWith('src/engine/briscola-render.js'), ['briscola', 'peso'], 'un disegno fa rifare anche il peso della grafica');
|
||||||
assert.deepEqual(suitesWith('assets/video/briscola.mp4'), ['briscola']);
|
assert.deepEqual(suitesWith('assets/video/briscola.mp4'), ['briscola']);
|
||||||
assert.deepEqual(suitesWith('tests/briscola.browser.mjs'), ['briscola']);
|
assert.deepEqual(suitesWith('tests/briscola.browser.mjs'), ['briscola']);
|
||||||
assert.deepEqual(suitesWith('docs/BRISCOLA.md'), [], 'i documenti non fanno rifare niente');
|
assert.deepEqual(suitesWith('docs/BRISCOLA.md'), [], 'i documenti non fanno rifare niente');
|
||||||
assert.ok(!inputsOf('briscola', FILES).includes('src/story/story.js'), 'la storia non fa riprovare la briscola');
|
assert.ok(!inputsOf('briscola', FILES).includes('src/story/story.js'), 'la storia non fa riprovare la briscola');
|
||||||
assert.deepEqual(suitesWith('src/engine/corsa.js'), ['gara']);
|
assert.deepEqual(suitesWith('src/engine/corsa.js'), ['gara', 'peso']);
|
||||||
assert.deepEqual(suitesWith('src/story/piloti.js'), ['gara']);
|
assert.deepEqual(suitesWith('src/story/piloti.js'), ['gara']);
|
||||||
assert.deepEqual(suitesWith('assets/closeups/gp/boccino.png'), ['gara']);
|
assert.deepEqual(suitesWith('assets/closeups/gp/boccino.png'), ['gara']);
|
||||||
assert.deepEqual(suitesWith('assets/muro/index.html'), ['telefono']);
|
assert.deepEqual(suitesWith('assets/muro/index.html'), ['telefono']);
|
||||||
@@ -40,9 +40,18 @@ test('la briscola si riprova solo se cambia la briscola o il motore', () => {
|
|||||||
|
|
||||||
test('la storia, le scene, i minigiochi dentro la partita e un modulo nuovo fanno rifare la partita intera, non i minigiochi a sé', () => {
|
test('la storia, le scene, i minigiochi dentro la partita e un modulo nuovo fanno rifare la partita intera, non i minigiochi a sé', () => {
|
||||||
assert.deepEqual(suitesWith('tests/browser.mjs'), ['principale', 'minigiochi', 'telefoni'], 'il file di una prova conta solo per lei');
|
assert.deepEqual(suitesWith('tests/browser.mjs'), ['principale', 'minigiochi', 'telefoni'], 'il file di una prova conta solo per lei');
|
||||||
for (const path of ['src/story/story.js', 'src/story/solution.js', 'assets/scenes/piazza.png', 'src/engine/bocce.js', 'src/engine/ambient.js', 'src/engine/nuovo.js']) {
|
for (const path of ['src/story/story.js', 'src/story/solution.js', 'assets/scenes/piazza.png', 'src/engine/bocce.js', 'src/engine/nuovo.js']) {
|
||||||
assert.deepEqual(suitesWith(path), cores, path);
|
assert.deepEqual(suitesWith(path), cores, path);
|
||||||
}
|
}
|
||||||
|
assert.deepEqual(suitesWith('src/engine/ambient.js'), [...cores, 'peso'], 'gli effetti delle scene: la partita e il peso');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('il peso della grafica si rifà quando cambia un disegno o un effetto, non la storia, e non toglie quei file alle altre prove', () => {
|
||||||
|
const peso = inputsOf('peso', FILES);
|
||||||
|
for (const path of ['src/engine/render.js', 'src/engine/ambient.js', 'src/engine/briscola-render.js', 'src/engine/flipper-render.js']) assert.ok(peso.includes(path), path);
|
||||||
|
for (const path of ['src/story/story.js', 'src/engine/briscola.js', 'src/engine/flipper.js']) assert.ok(!peso.includes(path), path);
|
||||||
|
assert.ok(inputsOf('principale', FILES).includes('src/engine/ambient.js'));
|
||||||
|
assert.deepEqual(suitesWith('src/engine/flipper-render.js'), ['flipper', 'peso']);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('il motore comune fa rifare tutto', () => {
|
test('il motore comune fa rifare tutto', () => {
|
||||||
@@ -71,3 +80,11 @@ test("l'impronta cambia solo se cambia un ingresso, e il piano dice quali", () =
|
|||||||
assert.deepEqual(after.find(p => p.name === 'briscola').changed, ['src/engine/briscola.js']);
|
assert.deepEqual(after.find(p => p.name === 'briscola').changed, ['src/engine/briscola.js']);
|
||||||
assert.notEqual(fingerprint(['a'], () => '1'), fingerprint(['a'], () => '2'));
|
assert.notEqual(fingerprint(['a'], () => '1'), fingerprint(['a'], () => '2'));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('la pipeline di Gitea prova le stesse prove, con lo stesso Playwright del progetto', () => {
|
||||||
|
const wf = readFileSync('.gitea/workflows/test.yml', 'utf8');
|
||||||
|
const listed = wf.match(/prova: \[([^\]]*)\]/)[1].split(',').map(s => s.trim());
|
||||||
|
assert.deepEqual([...listed].sort(), Object.keys(SUITES).sort());
|
||||||
|
const lock = JSON.parse(readFileSync('package-lock.json', 'utf8')).packages['node_modules/playwright'].version;
|
||||||
|
assert.match(wf, new RegExp(`mcr\\.microsoft\\.com/playwright:v${lock.replace(/\./g, '\\.')}-`), `immagine di Playwright diversa da ${lock}`);
|
||||||
|
});
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ log:
|
|||||||
level: info
|
level: info
|
||||||
runner:
|
runner:
|
||||||
file: .runner
|
file: .runner
|
||||||
capacity: 2
|
capacity: 6
|
||||||
timeout: 30m
|
timeout: 30m
|
||||||
container:
|
container:
|
||||||
network: ""
|
network: ""
|
||||||
|
|||||||
Reference in New Issue
Block a user