Compare commits

...
10 Commits
Author SHA1 Message Date
manzolo 5b64292ec9 Versione 0.1.62 2026-10-31 11:08:32 +01:00
manzoloandClaude Opus 5.5 b9bd50c8c3 Il risveglio non rallenta più il gioco: la nebbia è lo sfondo sfocato disegnato sopra, senza filtri (ctx.filter e il filter CSS sulla tela facevano andare a passo d'uomo tutto, anche dopo), e l'iframe del muro c'è solo a muro aperto; le prove nel browser aspettano la fine del risveglio e l'avvio del gioco
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GiECd9RWGMKV7n3K1mdy8T
2026-10-31 10:59:03 +01:00
manzoloandClaude Opus 5.5 c5953f04c0 Si comincia svegliandosi sulla panchina dei vecchietti dopo una notte brava: gli occhi che si aprono a fatica, la piazza sfocata e doppia; e dal Barone il telefonino col sito di San Piero, che si apre a tutto schermo, e il Muro del Sanpierino vero coi messaggi del 2007, «Aggiorna» compreso (Andrea)
- Il risveglio (src/engine/risveglio.js): primo piano sopra la scena, con le battute mentre le palpebre si aprono e
  i vecchietti che danno la notizia del palio; «Salta» lo salta.
- Il telefonino del Barone (src/engine/telefono.js): sale dal basso come il libro, la home di sanpiero50037.it dalla
  Wayback Machine nello schermo, il mattone del muro apre il muro.
- Il muro (assets/muro, tools/muro.py): la pagina vera rifatta statica dal muro riesumato, coi 24 messaggi del dump
  (senza email né IP) e quelli del gioco, che arrivano con la storia; «Scrivi» spento.
- Il gioco sgancia i service worker di altre app rimasti sul suo indirizzo (la Bussola su localhost:8080).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GiECd9RWGMKV7n3K1mdy8T
2026-10-31 09:52:34 +01:00
manzoloandClaude Opus 5.5 1ea78161d1 Segnato il minigioco della birra all'Ingorgo: spillare e bere (Andrea)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GiECd9Quo9nEdXyjXYF8ma
2026-10-31 00:01:11 +01:00
manzolo 5c0f4856bd Versione 0.1.61 2026-10-30 23:45:42 +01:00
manzoloandClaude Opus 5.5 d4b472119d All'underground dell'Ingorgo le luci di Codex: la palla a specchi appesa e colpita dal faro, i riflessi a tempo sui muri e sulla folla, le lampade a inseguimento; altrove restano le nostre, esagerate (Andrea)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GiECd9Quo9nEdXyjXYF8ma
2026-10-30 23:38:56 +01:00
manzolo aba2e7a0aa Versione 0.1.60 2026-10-30 23:18:43 +01:00
manzoloandClaude Opus 5.5 94902edc57 Più emozione all'arrivo della festa: «Mahhh... è l'INGORGOOOO!!!», da sola, e la voce che lo grida due volte (Andrea)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GiECd9Quo9nEdXyjXYF8ma
2026-10-30 23:11:51 +01:00
manzolo 721df30937 Versione 0.1.59 2026-10-30 23:06:42 +01:00
manzoloandClaude Opus 5.5 40d5780255 L'Ingorgo più da discoteca: fasci più forti e numerosi, colpo di colore a ogni battuta, strobo a raffica, macchie di luce sulla folla, laser anche in piazza, palla a specchi su tutta la scena; alla festa si clicca solo per spostarsi; il manifesto più facile da toccare col dito; il video breve dell'Ingorgo (Andrea)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GiECd9Quo9nEdXyjXYF8ma
2026-10-30 22:59:54 +01:00
103 changed files with 1531 additions and 110 deletions
+1
View File
@@ -13,6 +13,7 @@ COPY assets/voices /usr/share/nginx/html/assets/voices
COPY assets/credits /usr/share/nginx/html/assets/credits
COPY assets/effects /usr/share/nginx/html/assets/effects
COPY assets/closeups /usr/share/nginx/html/assets/closeups
COPY assets/muro /usr/share/nginx/html/assets/muro
COPY assets/favicon /usr/share/nginx/html/assets/favicon
COPY assets/video /usr/share/nginx/html/assets/video
Binary file not shown.

After

Width:  |  Height:  |  Size: 239 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

+57
View File
@@ -0,0 +1,57 @@
<html>
<head>
<meta http-equiv="Content-Language" content="it">
<meta name="GENERATOR" content="Microsoft FrontPage 5.0">
<meta name="ProgId" content="FrontPage.Editor.Document">
<meta http-equiv="Content-Type" content="text/html; charset=windows-1252">
<title>Responsabilità degli Iscritti</title>
</head>
<body>
<div align="center">
<center>
<table border="0" cellpadding="0" cellspacing="0" style="border-collapse: collapse" bordercolor="#111111" width="600">
<tr>
<td width="100%">
<p align="center" style="margin-top: 0; margin-bottom: 0"><b>
<font size="6" face="Arial">Responsabilità degli Iscritti</font></b></p>
<p align="center" style="margin-top: 0; margin-bottom: 0">&nbsp;</p>
<p align="justify" style="margin-top: 0; margin-bottom: 0">
<font face="Arial">Si fa presente che lo <b>Staff di WWW.SANPIERO50037.IT</b>,
non effettuerà controllo preventivo sul contenuto dei commenti e degli
interventi sul &quot;MURO DEL SANPIERINO&quot;, ma, <b><u>vista la possibilità di
risalire agli autori degli stessi</u></b>, si sentirà autorizzato alla
comunicazione di tali dati ai <b>Soggetti Interessati e all'Autorità
Giudiziaria</b> in caso di violazione di qualsiasi articolo del C.C. e del
C.P., si invita pertanto a prendere coscienza delle<b> responsabilità
civili e penali </b>che l'utente si assume in prima persona per il
contenuto di ciò che scrive.</font></p>
<p align="justify" style="margin-top: 0; margin-bottom: 0">&nbsp;</p>
<p align="justify" style="margin-top: 0; margin-bottom: 0"><b>
<font face="Arial">E' pertanto necessario che tutti gli utenti garantiscano il contenuto:</font></b></p>
<p align="justify" style="margin-top: 0; margin-bottom: 0">
<font face="Arial"><b>1 -</b> Non sia volgare, diffamatorio e
pregiudizievole;</font></p>
<p align="justify" style="margin-top: 0; margin-bottom: 0">
<font face="Arial"><b>2 -</b> Non violi in alcun modo i diritti di terzi;</font></p>
<p align="justify" style="margin-top: 0; margin-bottom: 0">
<font face="Arial"><b>3 -</b> Non violi i diritti stabiliti dal
<a href="Privacy.htm">D.Lgs. 30.06.2003 n°196</a> e ogni altra normativa
in</font></p>
<p align="justify" style="margin-top: 0; margin-bottom: 0">
<font face="Arial">&nbsp;materia di Privacy;</font></p>
<p align="justify" style="margin-top: 0; margin-bottom: 0">
<font face="Arial"><b>4 -</b> Non violi la vigente normativa in materia di
Pubblica Sicurezza e Buon Costume e/o altre norme imperative;</font></p>
<p align="justify" style="margin-top: 0; margin-bottom: 0">
<font face="Arial"><b>5 -</b> Non venga inserito per fini di &quot;Spamming&quot;.</font></td>
</tr>
</table>
</center>
</div>
<p align="center"><a href="index.html" style="font: bold 14px Tahoma; color: #800000">&laquo; Torna al Muro</a></p></body>
</html>
@@ -0,0 +1,97 @@
<html>
<head>
<meta name="GENERATOR" content="Microsoft FrontPage 5.0">
<meta name="ProgId" content="FrontPage.Editor.Document">
<meta http-equiv="Content-Type" content="text/html; charset=windows-1252">
<title>Guida al Muro del Sanpierino</title>
</head>
<body>
<div align="center">
<center>
<table border="0" cellpadding="0" cellspacing="0" style="border-collapse: collapse" bordercolor="#111111" width="1024">
<tr>
<td width="100%">
<p class="MsoNormal" align="center" style="text-align: center; margin-top: 0; margin-bottom: 0">
<b><span style="font-size: 22.0pt">Guida al Muro del Sanpierino</span></b></p>
<p class="MsoNormal" style="text-align: justify; margin-top: 0; margin-bottom: 0">
<b><u><span style="text-decoration: none; font-size: 18.0pt">&nbsp;</span></u></b></p>
<p class="MsoNormal" style="text-align: justify; margin-top: 0; margin-bottom: 0">
<b><u><span style="font-size: 18.0pt">1 – Iscrizione</span></u></b></p>
<p class="MsoNormal" style="text-align: justify; margin-top: 0; margin-bottom: 0">
<span style="font-size: 14.0pt">Se vuoi scrivere un messaggio sul Muro del
Sanpierino adesso dovrai prima essere iscritto. Iscriversi è gratuito e
semplicissimo, sarà sufficiente andare sul Muro e cliccare sull’icona
“Scrivi”, da lì verrai inviato nella pagina di “Login” dove troverai il
tasto “Iscriviti”,&nbsp; cliccandolo ti troverai direttamente nella pagina
d’Iscrizione. Appena riceveremo la tua richiesta attiveremo il tuo account
e riceverai una mail, all’indirizzo inserito, a conferma dell’attivazione.
A quel punto potrai scrivere sempre e in qualunque momento sul Muro.</span></p>
<p class="MsoNormal" style="text-align: justify; margin-top: 0; margin-bottom: 0">
<span style="font-size: 14.0pt">&nbsp;</span></p>
<p class="MsoNormal" style="text-align: justify; margin-top: 0; margin-bottom: 0">
<b><u><span style="font-size: 18.0pt">2 – Scrivere</span></u></b></p>
<p class="MsoNormal" style="text-align: justify; margin-top: 0; margin-bottom: 0">
<span style="font-size: 14.0pt">Per scrivere sul Muro sarà sufficiente
cliccare sul tasto “Scrivi” e inserire il proprio Nickname e Password (se
non si è iscritti segui le istruzioni al paragrafo 1- Iscrizione). Vi
troverete nella pagina di scrittura vera e propria dove potrete scegliere
se mantenere il vostro Nickname d’iscrizione o variarlo, se rendere
visibile il vostro indirizzo e-mail oppure no e ancora scegliere fra le
oltre 470 smiles disponibili e ancora variare l’aspetto grafico del vostro
messaggio consultando la pagiana di aiuto per la personalizzazione… tante
alternative che sicuramente saranno di vostro gradimento!</span></p>
<p class="MsoNormal" style="text-align: justify; margin-top: 0; margin-bottom: 0">
<span style="font-size: 14.0pt">&nbsp;</span></p>
<p class="MsoNormal" style="text-align: justify; margin-top: 0; margin-bottom: 0">
<b><u><span style="font-size: 18.0pt">3 – Privacy &amp; Responsabilità</span></u></b></p>
<p class="MsoNormal" style="text-align: justify; margin-top: 0; margin-bottom: 0">
<span style="font-size: 14.0pt">L’aspetto più importante di questo Nuovo
Muro del Sanpierino è proprio quello relativo alle Resposabilità che ogni
utente si assume nei post inseriti. Ogni Utente, pur facendo salva la
propria Privacy, non dovrà in alcun modo ledere gli altrui diritti e
quindi:</span></p>
<p style="text-align: justify; margin: 0 0cm">
<span style="font-size: 14.0pt">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <b>1 -</b> Non dovrà essere
volgare, diffamatorio e pregiudizievole;</span></p>
<p style="text-align: justify; text-indent: 35.4pt; margin: 0 0cm"><b>
<span style="font-size: 14.0pt">2 -</span></b><span style="font-size: 14.0pt">
Non dovrà violare in alcun modo i diritti di terzi;</span></p>
<p style="text-align: justify; text-indent: 35.4pt; margin: 0 0cm"><b>
<span style="font-size: 14.0pt">3 -</span></b><span style="font-size: 14.0pt">
Non dovrà violare i diritti stabiliti dal
<a href="http://www.sanpiero50037.it/spswall/Privacy.htm" style="color: blue; text-decoration: underline; text-underline: single">
D.Lgs. 30.06.2003 n°196</a> e ogni altra </span></p>
<p style="text-align: justify; text-indent: 35.4pt; margin: 0 0cm">
<span style="font-size: 14.0pt">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; normativa in materia di Privacy;</span></p>
<p style="text-align: justify; margin-left: 35.4pt; margin-right: 0cm; margin-top: 0; margin-bottom: 0">
<b><span style="font-size: 14.0pt">4 -</span></b><span style="font-size: 14.0pt">
Non dovrà violare la vigente normativa in materia di Pubblica Sicurezza e
</span></p>
<p style="text-align: justify; margin-left: 35.4pt; margin-right: 0cm; margin-top: 0; margin-bottom: 0">
<b><span style="font-size: 14.0pt">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; </span></b>
<span style="font-size: 14.0pt">Buon Costume e/o altre norme imperative;</span></p>
<p style="text-align: justify; text-indent: 35.4pt; margin: 0 0cm"><b>
<span style="font-size: 14.0pt">5 -</span></b><span style="font-size: 14.0pt">
Non dovrà inserire post per fini di &quot;Spamming&quot;.</span></p>
<p style="text-align: justify; margin: 0 0cm">
<span style="font-size: 14.0pt">In caso di violazione di una delle regole
descritte qui sopra il messaggio verrà cancellato quanto prima e, in caso
di richiesta da parte del Soggetto interessato o dell’Autorità
Giudiziaria, verranno comunicati i suoi dati oltre, naturalmente, al
blocco dell’Utente che si renderà responsabile di tale messaggio. Per
tutti gli altri messaggi la Privacy dei nostri Utenti sarà tutelata come
descritto dal
<a href="http://www.sanpiero50037.it/spswall/Privacy.htm" style="color: blue; text-decoration: underline; text-underline: single">
D.Lgs. 30.06.2003 n°196</a>, tranne nei caso di violazione dei nostri Data
Base da attacchi esterni.</span></td>
</tr>
</table>
</center>
</div>
<p align="center"><a href="index.html" style="font: bold 14px Tahoma; color: #800000">&laquo; Torna al Muro</a></p></body>
</html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 177 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 173 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 202 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 284 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 278 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 176 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 182 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 484 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 247 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 904 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 219 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 208 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 284 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 610 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 269 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 758 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 605 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 198 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 363 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 573 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 202 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 234 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 303 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 340 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

+40
View File
@@ -0,0 +1,40 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" lang="it" xml:lang="it">
<head>
<!-- Il Muro del Sanpierino (2007-2009, Manzolo e Baron De Duke) dentro il gioco, sul telefonino del Barone: lo stesso
markup di thewall.php, coi messaggi veri (tools/muro.py: niente email né IP) e quelli del gioco
(story.closeups.muro_sanpierino.messages, ognuno con la sua condizione). «Scrivi» è spento (Andrea, 31 ottobre). -->
<link rel="stylesheet" type="text/css" href="./muro.css" />
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>Il Muro del Sanpierino</title>
</head>
<body>
<table width="1024" border="0" align="center">
<td width="250">&nbsp;</td>
<td width="524" align="center">
<a href="#" id="logo"><img class="imgnoborder" src="./img/logo_trasparente_obliquo.gif" alt="Return to Website" /></a>
</td>
<td width="250">&nbsp;</td>
</table>
<div><br /></div>
<div class="divcenter">
<div class="divcenter">
<span class="title">Il Muro del Sanpierino</span><br />
<a href="./Responsabilita.htm"><span class="pblue">Responsabilita'</span></a>
<span class="pblack"> - </span>
<a href="./guida_al_muro_del_sanpierino.htm"><span class="pred">Guida al Muro<br /></span></a>
<span class="pnormal" id="count"></span>
<br />
<div class="bar"></div>
<div id="wall"></div>
<div class="bar"></div>
<div>
<p class="powered">Powered by</p>
<p><img src="./img/logomanzolo.jpg" alt=""/></p>
<p class="poweredby">Manzolo</p>
</div>
</div>
</div>
<script type="module" src="./muro.js"></script>
</body>
</html>
+162
View File
@@ -0,0 +1,162 @@
{
"smiles": {
"[13_grin]": "13_grin.gif",
"[14_bigmouth]": "14_bigmouth.gif",
"[15_weeping]": "15_weeping.gif",
"[16_angry]": "16_angry.gif",
"[17_wink]": "17_wink.gif",
"[1_grin]": "1_grin.gif",
"[20_grumpy]": "20_grumpy.gif",
"[21_irked]": "21_irked.gif",
"[22_yikes]": "22_yikes.gif",
"[24_shocked]": "24_shocked.gif",
"[8-)]": "25_coolguy.gif",
"[28_spin]": "28_spin.gif",
"[ambulance]": "ambulance.gif",
"[banghead]": "banghead.gif",
"[beret]": "beret.gif",
"[bouncer]": "bouncer.gif",
"[bowtie]": "bowtie.gif",
"[detective]": "detective.gif",
"[duh2]": "duh2.gif",
"[fryingpan]": "fryingpan.gif",
"[highfive]": "highfive.gif",
"[paperbag]": "paperbag.gif",
"[shocking]": "shocking.gif",
"[simpsonhomer]": "simpsonhomer.gif",
"[splat]": "splat.gif",
"[surfing]": "surfing.gif",
"[thumbsup2]": "thumbsup2.gif",
"[tombstone]": "tombstone.gif",
"[trophy]": "trophy.gif",
"[user]": "user.gif",
"[vader2]": "vader2.gif",
"[wave2]": "wave2.gif",
"[weightlifter]": "weightlifter.gif",
"[wonderwoman]": "wonderwoman.gif",
"[yipes]": "yipes.gif",
"[pampa]": "pampa.gif"
},
"messages": [
{
"nick": "WWW.SANPIERO50037.IT X IL SOCIALE",
"date": "2007-11-14 22:48:20",
"text": "<br>"
},
{
"nick": "Porc@ontha$..!",
"date": "2007-11-14 22:24:36",
"text": "Eccomi anche io a sverginare il nuovo muro...[1_grin]"
},
{
"nick": "Baron De Duke",
"date": "2007-11-14 21:14:31",
"text": "<font color=#0000FF>X BANTU\r\nL'INDIRIZZO E-MAIL INSERITO NON E' VALIDO, PER CORTESIA CONTATTAMI VIA E-MAIL. GRAZIE</font>"
},
{
"nick": "Megy",
"date": "2007-11-14 19:04:25",
"text": "Ieri ed oggi,con la Ferrari è andato in pista un ragazzo di 38 anni,tedesco...dice prometta bene!!![15_weeping]"
},
{
"nick": "?",
"date": "2007-11-14 18:49:21",
"text": "Il problema del Reconquista è che non la buttano dentro nemmeno con le mani...tanto l'importante è divertirsi e che nessuno si faccia del male.[ambulance]"
},
{
"nick": "tetsuyaharada",
"date": "2007-11-14 17:37:19",
"text": "glazie pel il nuovo mulo"
},
{
"nick": "manutd",
"date": "2007-11-14 16:50:29",
"text": "bravi ragazzi ottimo lavoro ci si deve sempre un po rompere le [bouncer][bouncer]però poi le cose vengon fuori [28_spin][28_spin][28_spin]"
},
{
"nick": "fraus",
"date": "2007-11-14 14:59:18",
"text": "Mariolino Sindaco![1_grin]"
},
{
"nick": "TeoNick",
"date": "2007-11-14 14:37:34",
"text": "Ma che tipo di Mariolino sei?"
},
{
"nick": "Mariolino for President",
"date": "2007-11-14 12:04:12",
"text": "[14_bigmouth] effettivamente ci sarebbe da infamare, ma dato che vu siete voi che correte in campo lasciamo fare, se vi piace così!\r\nVolevo dire soltanto che quello che manca, non è il gioco, il fiato la panchina ecc ecc ecc, ma l'attaccamento alla maglia, mi parete un pò carenti di grinta, nessuno letica con nessuno, il piedino si tira sempre indietro (e non dite che il giorno dopo c'è da andà a lavorare che la un regge)ma soprattutto NESSUNO DICE NULLA ALL'ARBITRO, volete mettere le leticate di Manolo con conseguenti espulsioni? superiori.\r\nIn conclusione, va bene così, basta che vu vi diverta, ma dubito che il risultato non vi scossi! SEMPRE E COMUNQUE FORZA RECONQUISTA"
},
{
"nick": "Reconquista",
"date": "2007-11-14 10:34:48",
"text": "Ovvia...su con le infamate!!!ce le meritiamo...[16_angry][16_angry][16_angry]"
},
{
"nick": "pizzu",
"date": "2007-11-14 10:21:32",
"text": "Finalmente il nuovo muro da imbrattare!! Complimenti per l'ottimo lavoro a Barone e Manzolo! [8-)]"
},
{
"nick": "Mariolino for President",
"date": "2007-11-14 08:51:48",
"text": "[22_yikes] MA CHE NEVA IN MONTAGNA?[17_wink]\r\nPENSIERO DEL MATTINO:\r\n\"Meglio vivere con due pensioni oggi, che con una soltanto domani\""
},
{
"nick": "TeoNick",
"date": "2007-11-14 00:41:57",
"text": "Buonasera Muraioli! Ho un questito: è sempre censurato faenza? su quello vecchio UN si poteva [22_yikes][22_yikes][22_yikes][22_yikes][22_yikes]\r\n[yipes][wave2]\r\n\r\nP.s. Mariolino chi? secondo me l'è lui l'untore!![bouncer][bouncer]"
},
{
"nick": "Baron De Duke",
"date": "2007-11-14 00:32:47",
"text": "NONOSTANTE QUALCHE INGEGNOSO IMPAZIENTE... [simpsonhomer] ECCO A VOI IL \"NUOVO MURO DEL SANPIERINO\"!!![banghead]\r\n\r\nAVANTI CON GLI SPRAY!!!! [beret][splat][thumbsup2][wave2]"
},
{
"nick": "Manzolo (Welcome to the real world)",
"date": "2007-11-14 00:32:12",
"text": "Ci siamo!!!!\r\nCari paesani e non, il nuovo muro è online!!!\r\nOgni vostro suggerimento sarà prezioso per migliorare questo nostro spazio che da oggi perde la sua \"staticità\" [tombstone] ...\r\nFacciamolo crescere con idee e critiche...\r\nYour potential. Our passion!\r\n\r\nANDIAMO A PRENDERCI LA [trophy]\r\n\r\nIl presidente [surfing]\r\n\r\nP.s. sconfitta e sculo (per) ora si possono scrivere [24_shocked]"
},
{
"nick": "Cecchino",
"date": "2007-11-14 00:31:58",
"text": "Grandi ragazzi!! Manzolo ora ricordati che c'è da fissare la vacanza rider!! Pampa quest'anno ti svernicio!!"
},
{
"nick": "?",
"date": "2007-11-13 23:19:45",
"text": "Reconquista...datevi foco!!![22_yikes]"
},
{
"nick": "coccinella",
"date": "2007-11-13 22:21:52",
"text": "buona idea questo nuovo muro, complimenti a gabry e al manzolo che si sono impegnati a rendere un po di vita a questo muro. ciao ciao"
},
{
"nick": "Mariolino for President",
"date": "2007-11-13 17:04:04",
"text": "ma funziono?[22_yikes]"
},
{
"nick": "m.d.b.",
"date": "2007-11-13 10:24:51",
"text": "ovvia....m'è riuscito entrare....grande barone e grande manzolo, vediamo se ora si ripiglia ivvia con questo nuovo metodo!!!!\r\ndomenica s'è beccato, mi sa che sono io che porto s c u l o!"
},
{
"nick": "Mariolino for President",
"date": "2007-11-13 09:23:34",
"text": "in attesa di nuove [16_angry]auguro allo staff buon lavoro ed un felice anno nuovo[22_yikes] fose sono un pò prematuro[24_shocked][15_weeping][13_grin][14_bigmouth][17_wink][21_irked][20_grumpy][pampa][pampa][wonderwoman][wonderwoman][weightlifter][shocking][fryingpan][duh2][detective]"
},
{
"nick": "Kiara",
"date": "2007-11-10 21:51:53",
"text": "CIAO MURO!COMPLIMENTI agli autori, barone e manzolo.... è stato uno sporco lavoro ma qualcuno lo doveva pur fare....[8-)]"
},
{
"nick": "Manzolo e Baron De Duke [surfing]",
"date": "2007-11-10 19:22:23",
"text": "IL DADO E' TRATTO!!! [highfive]<br>Finalmente possiamo presentarvi il rinnovato \"MURO DEL SANPIERINO\"! [8-)]<br>Da oggi i messaggi andranno nuovamente On-Line istantaneamente... l'unica cosa che vi chiediamo è una piccola iscrizione. [bowtie]<br>Facciamo presente che l'anonimato rimarrà garantito, unica eccezione sarà fatta quando i messaggi avranno contenuti in disaccordo con le condizioni espresse in fase di registrazione. Potrete consultare i casi nella pagina \"Responsabilità\" che troverete qui sopra. [paperbag]<br>Speriamo che il nostro lavoro sia di vostro gradimento e rimaniamo in attesa dei numerosi nuovi messaggi!!! [user]<br>The Wall is come back... Nothing is Impossible!!! [vader2]"
}
]
}
+31
View File
@@ -0,0 +1,31 @@
body {text-align:center;margin:0 auto;padding:0px;background-color:#F7E1C0;font: 20px Arial; color: #800000; font-weight: bold; font-style: normal; text-decoration: none;}
.pagerbutton { text-align: center; padding:2px; border:1px solid #191E66;font:12px verdana,arial,helvetica;background-color: #DBE0F5;cursor:pointer; color: #191E66; }
.pagerbutton_over { background-color: #F2F4FA; color: #A1A1A1; }
.pagerbutton_disabled { cursor: default; padding:2px; color: #aaaaaa;}
.response { font: 14px Tahoma; color: #800000; font-weight: bold; font-style: italic; text-decoration: none; }
.title {font: 30px Tahoma; color: #800000; font-weight: bold; font-style: normal; text-decoration: none;}
.text { font: 20px Arial; color: #800000; font-weight: bold; font-style: normal; text-decoration: none; }
.question { font: 14px Tahoma; color: #002288; font-weight: bold; font-style: normal; text-decoration: none; }
.name { font: 24px Tahoma; color: #800000; font-weight: bold; font-style: normal; text-decoration: none; }
a { font: 12px Tahoma; color: #800000; font-weight: normal; font-style: normal; text-decoration: none; }
a:visited { font: 12px Tahoma; color: #800000; font-weight: normal; font-style: normal; text-decoration: none; }
a:hover { font: 12px Tahoma; color: #800000; font-weight: normal; font-style: normal; text-decoration: none; }
.divcenter {margin-right:auto;margin-left:auto;width:87%}
.pblue {font: 18px Tahoma; color: #0000A0;font-weight: bold;}
.pred {font: 18px Tahoma; color: #FF0000;font-weight: bold;}
.pblack {font: 18px Tahoma; color: #000000;font-weight: bold;}
.pnormal {font: 12px Tahoma; color: #800000;font-weight: bold;}
.powered {font: 12px Tahoma; color: #800000;font-weight: bold;}
.poweredby {font: 18px Tahoma; color: #800000;font-weight: bold;}
.spacer {border-top: 3px dotted #800000; margin-top: 10px; margin-bottom: 0px}
.td30 {width:30%}
.td70 {width:70%;text-align:right}
.tdcenter {text-align:center}
.imgnoborder {border:0px;}
.imgbar {border:0px;vertical-align:middle;}
+56
View File
@@ -0,0 +1,56 @@
// Il Muro del Sanpierino nel telefonino del Barone: rifà quello che facevano thewall.php, drawbar.php,
// getsmile.php e messagebody.php. I messaggi del gioco (story.closeups.muro_sanpierino.messages) stanno in cima,
// quelli che valgono adesso (ognuno col suo `when`); sotto i messaggi veri del 2007. «Aggiorna» ricarica la pagina:
// se nel frattempo la storia è andata avanti, compaiono i messaggi nuovi.
import { check } from '../../src/engine/game.js';
const PER_PAGE = 50;
const game = (() => { try { return parent.game; } catch { return null; } })();
const def = game?.story.closeups.muro_sanpierino;
const { smiles, messages: real } = await (await fetch('./messaggi.json')).json();
const ours = (def?.messages ?? []).filter(m => check(m.when, game.state)).sort((a, b) => b.date.localeCompare(a.date));
const all = [...ours, ...real];
const from = Math.max(0, Number(new URLSearchParams(location.search).get('pag')) || 0);
// Gli smile come li metteva getsmile.php, e gli a capo.
const smile = text => Object.entries(smiles).reduce((t, [code, link]) => t.split(code).join(`&nbsp;<img src="./img/${link}" alt="" />`), text);
const body = m => smile(m.text).replace(/\r\n|\r|\n/g, '<br />');
const when = d => { const [day, hour] = d.split(' '), [y, mo, dd] = day.split('-'); return `${dd}/${mo}/${y}<br />${hour}`; };
document.getElementById('count').textContent = `(Messaggi lasciati sul muro: ${all.length})`;
document.getElementById('wall').innerHTML = all.slice(from, from + PER_PAGE).map(m => `
<p class="spacer"><img src="./img/spacer.gif" alt="" /></p>
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="padding:5px;">
<tr><td class="td30"><div class="question"></div></td><td class="td70"><div class="text">${when(m.date)}</div></td></tr>
<tr><td align="left"><div class="question">Chi Sei?&nbsp;&nbsp;</div><br /></td><td align="left"><div class="name"> ${smile(m.nick)}</div><br /></td></tr>
<tr><td align="left" valign="top"><div class="question">Messaggio&nbsp;&nbsp;</div><br /></td><td align="left"><div class="response"><p>${body(m)}</p><br /><br /></div></td></tr>
</table>`).join('');
// La barra coi pulsanti (drawbar.php): Inizio e Precedente spenti sulla prima pagina, Successiva e Ultima
// sull'ultima; «Scrivi» sempre spento.
const page = n => `./index.html${n > 0 ? '?pag=' + n : ''}`, last = Math.max(0, Math.floor((all.length - 1) / PER_PAGE) * PER_PAGE);
const buttons = [
['Inizio', 'pagefirst.gif', from > 0 && (() => location.href = page(0)), true],
['Precedente', 'pageback.gif', from > 0 && (() => location.href = page(from - PER_PAGE)), true],
['SCRIVI', 'postmessage.gif', null, true],
['Aggiorna', 'refresh.gif', () => location.reload(), true],
['Home', 'home.gif', () => game?.closeupHome(), true],
['Successiva', 'pageahead.gif', from < last && (() => location.href = page(from + PER_PAGE)), false],
['Ultima', 'pagelast.gif', from < last && (() => location.href = page(last)), false],
];
for (const bar of document.querySelectorAll('.bar')) {
const table = document.createElement('table'), tr = table.insertRow();
Object.assign(table, { width: '100%', border: 0, cellPadding: 0, cellSpacing: 5 });
for (const [label, icon, go, before] of buttons) {
const div = tr.insertCell().appendChild(document.createElement('div')), img = `<img class="imgbar" src="./img/${icon}" alt="" />`;
div.className = 'pagerbutton' + (go ? '' : ' pagerbutton_disabled');
div.innerHTML = before ? `${img}&nbsp;&nbsp;${label}` : `${label}&nbsp;&nbsp;${img}`;
if (go) {
div.onmouseover = () => { div.className = 'pagerbutton pagerbutton_over'; };
div.onmouseout = () => { div.className = 'pagerbutton'; };
div.onclick = go;
}
}
bar.replaceWith(table);
}
document.getElementById('logo').onclick = e => { e.preventDefault(); game?.closeupHome(); };
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+98 -46
View File
@@ -23,9 +23,9 @@
"file": "andrea-aa668b37c2.mp3",
"sec": 2.48
},
"andrea|E stamattina, in piazza, è successo qualcosa...": {
"file": "andrea-4ab41e1b8e.mp3",
"sec": 3.28
"andrea|Ieri sera s'è fatto tardi. Molto tardi...": {
"file": "andrea-03bff1514f.mp3",
"sec": 2.92
},
"andrea|La colonna, il centro del paese. Da qui passa tutto: le notizie, il Palio, i pettegolezzi.": {
"file": "andrea-c0e71bfd36.mp3",
@@ -199,18 +199,6 @@
"file": "andrea-b444f5f09a.mp3",
"sec": 2.6
},
"andrea|Il palio è sparito! Il quadro che hanno dipinto i ragazzi delle medie.": {
"file": "andrea-f07440a0ee.mp3",
"sec": 4.48
},
"andrea|Lo tenevano a scuola: stamani la Gianna ha trovato il posto vuoto.": {
"file": "andrea-42f13445d4.mp3",
"sec": 4.08
},
"andrea|La festa è domani: se nessuno lo ritrova, salta tutto. Tocca a me.": {
"file": "andrea-69337973d0.mp3",
"sec": 5.94
},
"andrea|Quattro ragazzi che affilano canne di bambù col coltellino e ci legano le lenze.": {
"file": "andrea-98ff3bf0a3.mp3",
"sec": 6.41
@@ -1071,34 +1059,6 @@
"file": "andrea-0bd1b0f32e.mp3",
"sec": 8.7
},
"andrea|Barone! Che ci fai qui?": {
"file": "andrea-83abae1419.mp3",
"sec": 2.91
},
"barondeduke|Aspetto uno che mi deve riportare le mutande.": {
"file": "barondeduke-3ea60b7083.mp3",
"sec": 4.12
},
"andrea|Le... mutande?": {
"file": "andrea-f83ef038f8.mp3",
"sec": 3.94
},
"barondeduke|Le ho perse a poker, ieri sera.": {
"file": "barondeduke-5542a06624.mp3",
"sec": 1.65
},
"barondeduke|Avevo due donne e due jack. Entra la terza donna, e penso: «Male male si divide».": {
"file": "barondeduke-d4ff967179.mp3",
"sec": 7.64
},
"andrea|E invece?": {
"file": "andrea-435306c242.mp3",
"sec": 0.67
},
"barondeduke|L'altro aveva colore.": {
"file": "barondeduke-238f525535.mp3",
"sec": 1.65
},
"andrea|Baldoria, con la maglia blucerchiata e un bandierone della Sampdoria grande come lui. Qui a San Piero.": {
"file": "andrea-a85f0e0a9a.mp3",
"sec": 6.47
@@ -2491,6 +2451,46 @@
"file": "vecchietto-c8eae2e59e.mp3",
"sec": 4.88
},
"andrea|Barone! Che ci fai qui?": {
"file": "andrea-83abae1419.mp3",
"sec": 2.91
},
"andrea|Hai visto cosa c'era sul muro del Sanpierino?": {
"file": "andrea-e79919a1db.mp3",
"sec": 2.26
},
"andrea|Ci vediamo, Barone.": {
"file": "andrea-6fb991782a.mp3",
"sec": 1.02
},
"barondeduke|Aspetto uno che mi deve riportare le mutande.": {
"file": "barondeduke-3ea60b7083.mp3",
"sec": 4.12
},
"andrea|Le... mutande?": {
"file": "andrea-f83ef038f8.mp3",
"sec": 3.94
},
"barondeduke|Le ho perse a poker, ieri sera.": {
"file": "barondeduke-5542a06624.mp3",
"sec": 1.65
},
"barondeduke|Avevo due donne e due jack. Entra la terza donna, e penso: «Male male si divide».": {
"file": "barondeduke-d4ff967179.mp3",
"sec": 7.64
},
"andrea|E invece?": {
"file": "andrea-435306c242.mp3",
"sec": 0.67
},
"barondeduke|L'altro aveva colore.": {
"file": "barondeduke-238f525535.mp3",
"sec": 1.65
},
"barondeduke|Il muro? Aspetta, ce l'ho qui sul telefonino.": {
"file": "barondeduke-224533634d.mp3",
"sec": 3.72
},
"andrea|Ma che è sempre là, Borgo?": {
"file": "andrea-73c12c835b.mp3",
"sec": 2.34
@@ -3439,9 +3439,61 @@
"file": "andrea-366c066d68.mp3",
"sec": 2.28
},
"andrea|Ma... è l'Ingorgo! Le console, la gente, le spine della birra!": {
"file": "andrea-bc7d5f4dc0.mp3",
"sec": 7.5
"andrea|«Il palio me lo sono ripreso io». Firmato: il Regolo. Alle tre di notte.": {
"file": "andrea-567eeb2111.mp3",
"sec": 6.11
},
"barondeduke|E sotto ci sono io, con le mutande. Nessuno ha risposto.": {
"file": "barondeduke-e54e591474.mp3",
"sec": 4.45
},
"andrea|Barone, il sito è tuo: non puoi vedere chi l'ha scritto?": {
"file": "andrea-2144daa6ff.mp3",
"sec": 3.36
},
"barondeduke|Potrei. Ma ci vuole il computer di casa. Il telefonino, più di così, non fa.": {
"file": "barondeduke-77e0c6cc1d.mp3",
"sec": 6.93
},
"andrea|Mmmh... che botta.": {
"file": "andrea-1438d29b12.mp3",
"sec": 2.06
},
"andrea|Ma... dove sono?": {
"file": "andrea-3c23967804.mp3",
"sec": 2.72
},
"andrea|Una panchina. In piazza. Coi vecchietti.": {
"file": "andrea-c24a2f247f.mp3",
"sec": 4
},
"vecchietta|Oh, s'è svegliato il bello addormentato! Bella nottata, eh?": {
"file": "vecchietta-b8cf548a6c.mp3",
"sec": 4.97
},
"vecchietto|Ma l'hai saputo? Il palio è sparito!": {
"file": "vecchietto-8b557de040.mp3",
"sec": 2.87
},
"andrea|Il palio? Il quadro che hanno dipinto i ragazzi delle medie?": {
"file": "andrea-ce8cefa019.mp3",
"sec": 4.21
},
"vecchietta|Lo tenevano a scuola: stamani la Gianna ha trovato il posto vuoto.": {
"file": "vecchietta-ad38926925.mp3",
"sec": 3.67
},
"andrea|La festa è domani: se nessuno lo ritrova, salta tutto. Tocca a me.": {
"file": "andrea-69337973d0.mp3",
"sec": 5.94
},
"andrea|Mahhh... è l'INGORGOOOO!!!": {
"file": "andrea-5694cd3779.mp3",
"sec": 3.74
},
"andrea|Le console, la gente, le spine della birra!": {
"file": "andrea-a6bceec795.mp3",
"sec": 2.57
},
"andrea|Il palio può aspettare una sera. O no?": {
"file": "andrea-24e5ac53da.mp3",
Binary file not shown.
Binary file not shown.
Binary file not shown.
+10
View File
@@ -13,6 +13,12 @@ si segna qui con la data e il punto di `src/story/story.js` dove sta; le persone
## In attesa
- **Spillare e bere la birra all'Ingorgo** (Andrea, 30 ottobre: «bar a tema a spillare birre e bere»; foto di
riferimento `assets/raw/foto/ingorgo-bar-spine.png`). Il bar c'è già, dipinto da Codex sui gradini di Via dell'Antica
Posta nella notte della festa. Proposta: un minigioco da spillatore, col bicchiere da inclinare e la spina da
aprire e chiudere al momento giusto. Troppa schiuma e si rifà, troppo poca e il cliente brontola. Poi si beve: a
ogni birra la scena ondeggia un po' di più e Andrea cammina storto, finché si esce dalla festa. Da decidere con
Andrea: quali bar e di che tema, chi spilla (persone vere solo col consenso), se ci sono più bar.
- **Il gioco in due parti** (Yuri Baldi, 2026-10-23). Prima la storia, l'avventura che stiamo facendo; finita
quella, si aprono i giochi del paese, uno per uno:
- il gioco del Regolo;
@@ -93,6 +99,10 @@ si segna qui con la data e il punto di `src/story/story.js` dove sta; le persone
- **Le mutande del Barone** (Andrea, 25 ottobre; ci pensa ancora): si ritrovano e gli si riportano, e lui in cambio
dà qualcosa che serve alla storia. Proposta: appese come trofeo all'insegna del circolino di Campomigliaio
(l'Arcu ci scherza), e in cambio due pile per la torcia della casetta, scarica.
- **Il sito del Barone e il Muro del Sanpierino** (Andrea, 31 ottobre): parlando col Barone, «Hai visto cosa hanno
scritto oggi sul muro del Sanpierino?»; si apre la home di sanpiero50037.it (schermata in arrivo da Andrea) e
cliccando sul Muro si apre il muro. Il muro vero è riesumato in `~/Workspaces/misc/spswall` (solo in locale).
Da decidere: i messaggi veri (nomi di persone vere, e il muro aprì a novembre 2007) o scritti per il gioco.
- **La racchetta del Tozzino** (Andrea, 25 ottobre): non la trova, Federer sta arrivando, ha paura di perdere e chiede
aiuto ad Andrea. Si potrebbe legare al poker del Barone (persa la stessa sera?). Serve un foglio sprite del Tozzino
senza racchetta, più una posa con la racchetta ritrovata.
+24 -1
View File
@@ -18,13 +18,36 @@ Online su 50037.manzolo.it tutto fino agli effetti ambientali di Codex. Aperto,
## Dove siamo arrivati
- **Il Muro del Sanpierino sul telefonino del Barone (31 ottobre)**: il Barone ora ha un dialogo a scelte (le
mutande, oppure «Hai visto cosa c'era sul muro del Sanpierino?»). Col muro tira fuori il telefonino, che sale dal
basso come il libro (primo piano `muro_sanpierino`, `src/engine/telefono.js`, telefonino di Codex); nello schermo la
home di sanpiero50037.it dalla Wayback Machine (31 dicembre 2007, senza la data in alto); un tocco la porta a tutto
schermo, e il mattone «Nuovo muro del Sanpierino» apre il muro: una pagina vera sopra la scena (`assets/muro`, iframe
`#muro`), rifatta da `tools/muro.py` dal muro riesumato (`~/Workspaces/misc/spswall`): stesso markup e CSS, i 24
messaggi veri del dump di novembre 2007 (Andrea: «quelli veri»; senza email né IP). I 654 ritrovati nella Wayback
Machine (2004-2012) restano solo nel muro riesumato in locale, separati dal gioco (Andrea, 31 ottobre). In
cima quelli del gioco (`messages` del primo piano, ognuno col suo `when`). «Scrivi» spento, niente email (Andrea);
«Aggiorna» ricarica e fa comparire i messaggi arrivati con la storia; «Home» torna alla home sul telefonino.
Test in `story.test.mjs`, prova `npm run browser:telefono`. [DA ANDREA] battute e messaggi del gioco, le cose del
2007-2008 sulla home (Ingorgo Sottozero, dicembre), e i **consensi** di chi ha scritto sul muro (i soprannomi dei 24
messaggi; per i 654 dell'archivio, se mai entrassero nel gioco: togliere cognomi e messaggi pesanti, e un modo per
chiedere di toglierli).
- **Il risveglio sulla panchina (31 ottobre)**: la partita non comincia più con Andrea in piedi in piazza ma col
risveglio dopo una notte brava (primo piano `risveglio`, `src/engine/risveglio.js`): visto da dentro gli occhi, le
palpebre si aprono a fatica e ricascano due volte, la piazza è sfocata e doppia e ondeggia, poi si schiarisce; si
è sulla panchina dei vecchietti, che danno la notizia del palio sparito. Il pulsante «Salta» lo salta. Test
`risveglio.test.mjs`, prova `npm run browser:risveglio`. [DA ANDREA] le battute, e l'ultima riga dell'introduzione.
- **L'Ingorgo Sonoro (30 ottobre, extra)**: in piazza il vecchio manifesto dell'Ingorgo (`manifesto_ingorgo`); guardandolo
si apre ingrandito (primo piano `poster`, `manifesto-ingorgo.png`: Codex dal manifesto vero del 2014, senza sponsor né
data); toccandolo, «Quanto darei per essere all'Ingorgo stasera?!?», poi l'effetto speciale (primo piano `incanto`, `incanto.js`: fasci di
luce, cassa che accelera, lampo bianco, coriandoli) e il centro diventa la notte della festa (flag `ingorgo`, fase
`ingorgo`, sfondi di notte di Codex in `backdrops`). La zona (`story.zones`): Piazza, Via dell'Antica Posta, Piazzetta
del Comune, cima di Via dell'Antica Posta, Piazzetta Cambray Digny; uscendo (a piedi o con la mappa) si torna a maggio.
Le luci da palco animate solo alla festa (`stagelights`, `lasers`, `mirrorball` in render.js): fasci che spazzano, bagliore a
Alla festa si clicca solo per spostarsi (zona con `exitsOnly`: panchine, porte e statue non rispondono).
Le luci da discoteca animate solo alla festa (`stagelights`, `lasers`, `mirrorball`, `floorspots` in render.js), rinforzate
su richiesta di Andrea: colpo di colore a ogni battuta, strobo a raffica a fine frase, macchie che corrono sulla folla.
In cima a Via dell'Antica Posta le luci sono quelle proposte da Codex (`pixelbeams`, `mirrorball`, `stagechase`,
`pixelspots`): Andrea ha tenuto la sua palla a specchi lì, e le nostre «caciarone» altrove. Fasci che spazzano, bagliore a
tempo, strobo, laser alla tecno, palla a specchi all'underground. Le console (`stages` delle scene, `music.js` `mix`): più brani insieme, ognuno più forte vicino alla sua console, anche
da fuori quadro (la piazza accanto). Generi provvisori: house in Piazza, latino al Comune, underground in cima a Via
dell'Antica Posta, tecno a Cambray Digny; i brani sono di Andrea (Suno), portati al volume del gioco. Gli sfondi sono
+3 -2
View File
@@ -17,11 +17,12 @@ server {
# I moduli ES richiedono il MIME type corretto.
types {
text/html html;
text/html html htm;
text/css css;
application/javascript js mjs;
application/json json map;
image/png png;
image/gif gif;
image/webp webp;
audio/mpeg mp3;
image/svg+xml svg;
@@ -39,7 +40,7 @@ server {
location ~* \.(css|js|mjs|json)$ {
add_header Cache-Control "no-cache";
}
location ~* \.(png|webp|jpg|mp3|mp4|woff2|ico)$ {
location ~* \.(png|gif|webp|jpg|mp3|mp4|woff2|ico)$ {
add_header Cache-Control $media_cache;
}
location = /index.html {
+3 -1
View File
@@ -1,6 +1,6 @@
{
"name": "leggenda-del-regolo",
"version": "0.1.58",
"version": "0.1.62",
"private": true,
"type": "module",
"scripts": {
@@ -16,6 +16,8 @@
"browser:tressette": "node tests/tressette.browser.mjs",
"browser:scopa": "node tests/scopa.browser.mjs",
"browser:briscola": "node tests/briscola.browser.mjs",
"browser:telefono": "node tests/telefono.browser.mjs",
"browser:risveglio": "node tests/risveglio.browser.mjs",
"browser:effects": "node tests/ambient.browser.mjs",
"serve": "python3 -m http.server 8000"
},
+42 -1
View File
@@ -14,6 +14,8 @@ import * as micro from './micro.js';
import * as cerbottana from './cerbottana.js';
import { newCorsa, updateCorsa } from './corsa.js';
import { newIncanto, updateIncanto } from './incanto.js';
import { newRisveglio, updateRisveglio } from './risveglio.js';
import { newTelefono, updateTelefono, tapTelefono } from './telefono.js';
import * as condor from './condor.js';
import * as campana from './campana.js';
import * as poker from './poker.js';
@@ -203,7 +205,13 @@ export class Game {
return { scene, x: Math.round(x), y: Math.round(y), facing, inventory: [...inventory], flags: { ...flags }, used: { ...used }, ended };
}
hotspots() { return (this.scene.hotspots ?? []).filter(h => check(h.when, this.state)); }
// In una zona accesa con `exitsOnly` (la festa dell'Ingorgo: Andrea, 30 ottobre) si clicca solo per andare da un
// posto all'altro: panchine, porte e statue restano dove sono, ma non rispondono.
hotspots() {
const list = (this.scene.hotspots ?? []).filter(h => check(h.when, this.state));
const busy = (this.story.zones ?? []).some(z => z.exitsOnly && this.state.flags[z.flag] && z.scenes.includes(this.state.scene));
return busy ? list.filter(h => h.exit) : list;
}
hotspot(id) { return this.hotspots().find(h => h.id === id); }
people() { return (this.scene.people ?? []).filter(p => check(p.when, this.state)); }
// Col mouse vale il punto preciso (l'ultimo della lista sta sopra). Col dito, `reach` allarga l'area: prima gli
@@ -306,6 +314,8 @@ export class Game {
this.queue.unshift(...(this.story.closeups[c.id].tap ?? []));
return true;
}
// Il telefonino del Barone: un tocco apre il sito, e sul sito il link apre il muro.
if (this.story.closeups[c.id].type === 'telefono') return tapTelefono(c, this.story.closeups[c.id]);
if (this.story.closeups[c.id].type) return false; // torcia e minigiochi non si sfogliano
if (c.step === 'cover') Object.assign(c, { step: 'title', t: 0 });
else if (c.step === 'title') Object.assign(c, { step: 'turn', t: 0 });
@@ -346,6 +356,14 @@ export class Game {
this.queue.unshift(...(def.complete ?? []));
return true;
}
// Il telefonino: chi ha visto il muro, chiudendo, lo commenta.
if (def?.type === 'telefono') { if (c.step === 'wall') this.queue.unshift(...(def.closing ?? [])); return true; }
// Il risveglio, saltato a metà (Esc), finisce comunque: le battute rimaste e poi quelle di dopo.
if (def?.type === 'risveglio') {
const left = (def.lines ?? []).slice(c.fired).map(({ at, ...e }) => e);
this.queue.unshift(...left, ...(def.complete ?? []));
return true;
}
// I minigiochi: a partita finita (risultato deciso, anche se il conteggio si sta ancora mostrando) la battuta
// di chi ha vinto; uscendo prima, quella di chi lascia.
if (MINIGAMES[def?.type]) {
@@ -358,6 +376,13 @@ export class Game {
return true;
}
// Il muro del telefonino (una pagina vera, assets/muro): «Home» torna alla home del sito.
closeupHome() {
const c = this.closeup;
if (!c || this.story.closeups[c.id].type !== 'telefono' || c.step !== 'wall') return false;
Object.assign(c, { step: 'site', t: 0 });
return true;
}
// Bocce e biliardo: si tiene premuto per caricare la forza e si lascia per tirare (mouse, dito o spazio).
minigame() { const c = this.closeup; return c ? MINIGAMES[this.story.closeups[c.id].type] ?? null : null; }
closeupPress(pointerId) { const m = this.minigame(); return !!m && !this.speech && !!m.press?.(this.closeup, pointerId); }
@@ -371,6 +396,8 @@ export class Game {
const c = this.closeup;
const m = this.minigame();
if (m) return !!m.aim?.(c, [x, y]);
// Sul sito del telefonino conta dove si tocca (il link del muro).
if (c && this.story.closeups[c.id].type === 'telefono') { c.aim = [x, y]; return true; }
if (!c || this.story.closeups[c.id].type !== 'torch' || !Number.isFinite(x) || !Number.isFinite(y)) return false;
c.beam = [Math.max(70, Math.min(570, x)), Math.max(65, Math.min(295, y))];
return true;
@@ -521,6 +548,18 @@ export class Game {
if (now === 'end') this.closeupClose();
return;
}
if (this.story.closeups[c.id].type === 'telefono') {
updateTelefono(c, dt);
if (c.step === 'wall' && !c.lines && c.t >= CLOSEUP_PAUSE) c.lines = [...(this.story.closeups[c.id].comment ?? [])];
if (c.lines?.length) this.run(c.lines.shift());
return;
}
if (this.story.closeups[c.id].type === 'risveglio') {
const { due, end } = updateRisveglio(c, this.story.closeups[c.id], dt);
for (const e of due) this.run(e);
if (end) this.closeupClose();
return;
}
const m = this.minigame();
if (m) {
m.update(c, dt);
@@ -565,6 +604,8 @@ export class Game {
: type === 'viaggio' ? { id: e.closeup, t: 0, age: 0, events: [], typed: 0 }
: type === 'corsa' ? newCorsa(e.closeup)
: type === 'incanto' ? newIncanto(e.closeup)
: type === 'risveglio' ? newRisveglio(e.closeup)
: type === 'telefono' ? newTelefono(e.closeup)
: type === 'poster' ? { id: e.closeup, step: 'poster', t: 0, age: 0 }
// `level`: «Alla bona», «Seria» o «A fassi male», scelto nel dialogo con l'avversario.
// `decks`: il mazzo da usare quando il suo flag è acceso (briscola: le napoletane al posto delle toscane).
+181 -24
View File
@@ -17,6 +17,8 @@ import { drawCampana } from './campana-render.js';
import { drawPoker } from './poker-render.js';
import { drawViaggio } from './viaggio.js';
import { drawIncanto } from './incanto.js';
import { drawRisveglio } from './risveglio.js';
import { drawTelefono, telefonoHint } from './telefono.js';
import { drawBriscola } from './briscola-render.js';
import { drawTressette } from './tressette-render.js';
import { drawScopa } from './scopa-render.js';
@@ -178,7 +180,9 @@ export function render(ctx, game, { hover, spots, debug, time = performance.now(
const andrea = sprites.get('andrea');
const drawAndreaNow = () => andrea ? drawSprite(ctx, andrea, game.pose(), s.x, s.y, game.scaleAt(s.y), s.facing)
: drawAndrea(ctx, s.x, s.y, game.scaleAt(s.y), s.facing, game.walking ? game.stride / 6 : null);
const queue = [{ y: s.y, draw: drawAndreaNow }];
// Durante il risveglio si guarda dagli occhi di Andrea: lui non si vede.
const pov = game.closeup && game.story.closeups?.[game.closeup.id]?.type === 'risveglio';
const queue = pov ? [] : [{ y: s.y, draw: drawAndreaNow }];
for (const p of game.people()) {
const sheet = sprites.get(p.who);
if (sheet) queue.push({ y: p.at[1], draw: () => drawSprite(ctx, sheet, game.personPose(p), ...p.at, game.scaleAt(p.at[1]), p.facing ?? 1, p.size) });
@@ -279,6 +283,35 @@ function drawPhase(ctx, game) {
// Un numero «a caso» ma ripetibile: lo stesso istante dà sempre lo stesso disegno (e i test lo possono controllare).
const hash = n => (((n | 0) * 2654435761) >>> 0) / 4294967296;
// Fasci della festa: alone e nucleo sulla griglia dei pixel, attenuati in tre fasce nette lungo il percorso.
// Bastano sei poligoni per fascio, senza sfocature o letture del canvas.
function discoCone(ctx, x, y, angle, len, width, color, power) {
for (let layer = 0; layer < 2; layer++) {
const half = width * (layer ? 0.32 : 1);
const lx = Math.cos(angle - half), ly = Math.sin(angle - half), rx = Math.cos(angle + half), ry = Math.sin(angle + half);
for (let band = 0; band < 3; band++) {
const near = len * band / 3, far = len * (band + 1) / 3;
ctx.fillStyle = `rgba(${color},${power * (layer ? 0.8 : 0.45) * (1 - band * 0.36)})`;
ctx.beginPath();
ctx.moveTo(Math.round(x + lx * near), Math.round(y + ly * near));
ctx.lineTo(Math.round(x + lx * far), Math.round(y + ly * far));
ctx.lineTo(Math.round(x + rx * far), Math.round(y + ry * far));
ctx.lineTo(Math.round(x + rx * near), Math.round(y + ry * near));
ctx.closePath(); ctx.fill();
}
}
}
// Una chiazza schiacciata a scalini: cinque strisce bastano, anche sulla folla in primo piano.
function discoPool(ctx, x, y, rx, ry, color, power) {
ctx.fillStyle = `rgba(${color},${power})`;
for (let row = -2; row <= 2; row++) {
const w = Math.round(rx * (Math.abs(row) === 2 ? 0.6 : Math.abs(row) === 1 ? 0.88 : 1));
const top = Math.round(y + (row - 0.5) * ry * 0.4), bottom = Math.round(y + (row + 0.5) * ry * 0.4);
ctx.fillRect(Math.round(x) - w, top, w * 2, bottom - top);
}
}
// Gli effetti delle scene (`effects`), ognuno col suo `type`: piccole animazioni disegnate sopra lo sfondo.
export const EFFECTS = {
...ambientEffects,
@@ -497,18 +530,18 @@ export const EFFECTS = {
if (e.wash) {
const [x, y, r] = e.wash, c = colors[bar % colors.length];
const g = ctx.createRadialGradient(x, y, 0, x, y, r);
g.addColorStop(0, `rgba(${c},${0.1 + 0.18 * hit ** 2})`);
g.addColorStop(0, `rgba(${c},${0.16 + 0.3 * hit ** 2})`);
g.addColorStop(1, `rgba(${c},0)`);
ctx.fillStyle = g;
ctx.fillRect(x - r, y - r, r * 2, r * 2);
}
for (const [k, [x, y]] of (e.from ?? []).entries()) {
for (let i = 0; i < (e.beams ?? 2); i++) {
const n = k * 3 + i, swing = Math.sin(time / (900 + n * 170) + n * 1.9) * (e.spread ?? 0.75);
const a = -Math.PI / 2 + swing + (k % 2 ? 0.25 : -0.25), len = e.len ?? 260, half = 0.045;
for (let i = 0; i < (e.beams ?? 3); i++) {
const n = k * 3 + i, swing = Math.sin(time / (700 + n * 150) + n * 1.9) * (e.spread ?? 0.85);
const a = -Math.PI / 2 + swing + (k % 2 ? 0.25 : -0.25), len = e.len ?? 340, half = 0.055;
const c = colors[(n + bar) % colors.length], tipX = x + Math.cos(a) * len, tipY = y + Math.sin(a) * len;
const g = ctx.createLinearGradient(x, y, tipX, tipY);
g.addColorStop(0, `rgba(${c},0.32)`);
g.addColorStop(0, `rgba(${c},${0.38 + 0.2 * hit})`);
g.addColorStop(1, `rgba(${c},0)`);
ctx.fillStyle = g;
ctx.beginPath();
@@ -521,19 +554,46 @@ export const EFFECTS = {
ctx.fillRect(Math.round(x) - 1, Math.round(y) - 1, 3, 3);
}
}
ctx.restore();
// Lo strobo: un lampo bianco brevissimo ogni `strobe` battute.
if (e.strobe && Math.floor(beat) % e.strobe === 0 && beat % 1 < 0.12) {
ctx.fillStyle = 'rgba(255,255,255,0.13)';
// Il colpo di colore su tutta la scena a ogni battuta (`pulse`: quanto forte).
if (e.pulse) {
ctx.fillStyle = `rgba(${colors[Math.floor(beat) % colors.length]},${e.pulse * hit ** 3})`;
ctx.fillRect(0, 0, WIDTH, HEIGHT);
}
ctx.restore();
// Lo strobo: un lampo bianco brevissimo ogni `strobe` battute, a raffica nell'ultima battuta di ogni frase.
const roll = Math.floor(beat) % 16 === 15 && (beat * 4) % 1 < 0.3;
if (e.strobe && ((Math.floor(beat) % e.strobe === 0 && beat % 1 < 0.12) || roll)) {
ctx.fillStyle = 'rgba(255,255,255,0.18)';
ctx.fillRect(0, 0, WIDTH, HEIGHT);
}
},
// Le macchie di luce che corrono sulla folla (`area`), colorate, a tempo: la pista da ballo.
floorspots(ctx, e, time) {
const [ax, ay, aw, ah] = e.area, colors = e.colors ?? ['255,60,200', '60,220,255', '120,255,90', '255,190,40', '170,90,255'];
const beat = time / (60000 / (e.bpm ?? 124)), hit = 1 - (beat % 1);
ctx.save();
ctx.globalCompositeOperation = 'lighter';
for (let i = 0; i < (e.count ?? 6); i++) {
const u = 0.5 + 0.5 * Math.sin(time / (1300 + i * 230) + i * 2.1), v = 0.5 + 0.5 * Math.sin(time / (1700 + i * 190) + i * 1.3);
const x = ax + u * aw, y = ay + v * ah, r = (e.size ?? 34) * (0.7 + 0.5 * v);
const c = colors[(i + Math.floor(beat / 2)) % colors.length];
const g = ctx.createRadialGradient(x, y, 0, x, y, r);
g.addColorStop(0, `rgba(${c},${0.22 + 0.2 * hit})`);
g.addColorStop(1, `rgba(${c},0)`);
ctx.fillStyle = g;
ctx.save();
ctx.translate(x, y); ctx.scale(1, 0.45); ctx.translate(-x, -y);
ctx.fillRect(x - r, y - r, r * 2, r * 2);
ctx.restore();
}
ctx.restore();
},
// I laser della tecno: righe sottili verdi (o del colore dato) che si aprono a ventaglio e si richiudono.
lasers(ctx, e, time) {
const [x, y] = e.at, n = e.count ?? 7, open = 0.35 + 0.35 * Math.sin(time / 1300), tilt = Math.sin(time / 2100) * 0.4;
ctx.save();
ctx.globalCompositeOperation = 'lighter';
ctx.strokeStyle = `rgba(${e.color ?? '90,255,110'},${0.35 + 0.2 * Math.sin(time / 90) ** 2})`;
ctx.strokeStyle = `rgba(${e.color ?? '90,255,110'},${0.45 + 0.25 * Math.sin(time / 90) ** 2})`;
ctx.lineWidth = 1;
ctx.beginPath();
for (let i = 0; i < n; i++) {
@@ -544,22 +604,107 @@ export const EFFECTS = {
ctx.stroke();
ctx.restore();
},
// La palla a specchi: puntini di luce che girano sui muri intorno (`area`), più chiari vicino alla palla.
// La palla a specchi «di Codex» (Andrea, 30 ottobre: «l'effetto qui mi piaceva molto»): appesa al suo filo, un faro
// la colpisce, e i riflessi girano a tempo sui muri e sulla folla (`surfaces`), con qualche scintilla.
mirrorball(ctx, e, time) {
const [bx, by] = e.at, [ax, ay, aw, ah] = e.area, turn = time / 4200;
const [bx, by] = e.at, beat = time * (e.bpm ?? 122) / 60000, turn = beat / 32;
const surfaces = e.surfaces ?? [e.area];
ctx.save();
ctx.fillStyle = '#71718d'; ctx.fillRect(bx, e.hang ?? by - 26, 1, by - (e.hang ?? by - 26) - 7);
ctx.globalCompositeOperation = 'lighter';
if (e.from) {
const [x, y] = e.from, dx = bx - x, dy = by - y;
discoCone(ctx, x, y, Math.atan2(dy, dx), Math.hypot(dx, dy), 0.035, '195,215,255', 0.32);
}
for (let i = 0; i < (e.count ?? 60); i++) {
const [ax, ay, aw, ah] = surfaces[i % surfaces.length];
const u = (hash(i * 13) + turn) % 1, v = 0.5 + 0.42 * Math.sin(hash(i * 7 + 3) * Math.PI * 2 + turn * Math.PI * 2);
const x = ax + Math.floor(u * (aw - 4)), y = ay + Math.floor(v * (ah - 3));
const glint = Math.max(0, Math.sin(turn * Math.PI * 4 + i * 2.3)) ** 4;
ctx.fillStyle = `rgba(${e.color ?? '200,215,255'},${0.13 + 0.65 * glint})`;
ctx.fillRect(x, y, glint > 0.7 ? 3 : 2, 2);
if (glint > 0.95) ctx.fillRect(x + 1, y - 1, 1, 4);
}
ctx.globalCompositeOperation = 'source-over';
for (let row = -3; row <= 3; row++) {
for (let col = -3; col <= 3; col++) {
if (row * row + col * col > 11) continue;
const light = Math.sin(col * 0.8 + turn * Math.PI * 2) * 0.65 - row * 0.12;
ctx.fillStyle = light > 0.55 ? '#f3f5ff' : light > 0.1 ? '#b9bcdf' : light > -0.4 ? '#6e78ab' : '#343856';
ctx.fillRect(bx + col * 2, by + row * 2, 2, 2);
}
}
ctx.fillStyle = '#ffffff'; ctx.fillRect(bx - 3, by - 4, 3, 1); ctx.fillRect(bx - 2, by - 5, 1, 3);
ctx.restore();
},
// Le teste mobili «di Codex» (30 ottobre, usate all'underground): incrocio, ventaglio, passaggio parallelo. Il programma cambia ogni otto battiti,
// sempre ricavato dal tempo: anche tornando indietro a un istante si ritrova esattamente la stessa luce.
pixelbeams(ctx, e, time) {
const colors = e.colors ?? ['255,45,170', '45,190,255'], beat = time * (e.bpm ?? 124) / 60000;
const hit = (1 - beat % 1) ** 3, bar = Math.floor(beat / 4), cue = Math.floor(beat / 8) % 3;
const from = e.from ?? [], swing = Math.sin(beat * Math.PI / (e.sweep ?? 4));
ctx.save();
ctx.globalCompositeOperation = 'lighter';
for (let i = 0; i < (e.count ?? 40); i++) {
const u = (hash(i * 13) + turn) % 1, x = ax + Math.round(u * aw), y = ay + Math.round(hash(i * 7 + 3) * ah);
const glint = 0.5 + 0.5 * Math.sin(time / 160 + i * 2.3);
ctx.fillStyle = `rgba(${e.color ?? '220,200,255'},${0.35 + 0.5 * glint})`;
ctx.fillRect(x, y, 2, 2);
if (e.wash) {
const [x, y, r] = e.wash, c = colors[bar % colors.length];
discoPool(ctx, x, y, r, r * 0.32, c, 0.045 + 0.045 * hit);
discoPool(ctx, x, y, r * 0.55, r * 0.18, c, 0.04 + 0.07 * hit);
}
for (let k = 0; k < from.length; k++) {
const [x, y] = from[k], side = k % 2 ? 1 : -1;
const fan = from.length > 1 ? k / (from.length - 1) * 2 - 1 : 0;
const pan = cue === 0 ? side * swing : cue === 1 ? fan * (0.6 + 0.25 * swing) : swing;
const angle = -Math.PI / 2 + (e.aim ?? 0) + pan * (e.spread ?? 0.7);
const color = colors[(k + bar) % colors.length];
// Un breve buio di fine frase rende leggibile il rientro della cassa; il latino resta più continuo.
const power = (e.power ?? 0.38) * (0.65 + 0.35 * hit) * (e.shutter && beat % 16 > 15.5 ? 0.15 : 1);
for (let i = 0; i < (e.beams ?? 1); i++) {
const offset = (i - ((e.beams ?? 1) - 1) / 2) * 0.15;
discoCone(ctx, x, y, angle + offset, e.len ?? 270, e.width ?? 0.055, color, power);
}
ctx.fillStyle = `rgb(${color})`;
ctx.fillRect(x - 2, y - 1, 5, 3);
ctx.fillStyle = '#fff5df'; ctx.fillRect(x, y, 1, 1);
}
ctx.restore();
},
// Lampade sulla traversa: inseguimento a ottavi, poi accensione insieme sul primo colpo della frase.
// I blinder illuminano solo davanti al palco: nessun velo bianco su tutta la scena.
stagechase(ctx, e, time) {
const beat = time * (e.bpm ?? 124) / 60000, step = Math.floor(beat * 2), points = e.points;
const colors = e.colors ?? ['255,45,170', '45,190,255'];
const flash = e.blinder && (beat % 16 < 0.14 || (Math.floor(beat) % 16 === 15 && beat * 2 % 1 < 0.16));
ctx.save();
ctx.globalCompositeOperation = 'lighter';
for (let i = 0; i < points.length; i++) {
const [x, y] = points[i], on = step % points.length === i || beat % 8 < 0.22;
const color = flash ? '255,240,210' : colors[(i + Math.floor(beat / 4)) % colors.length];
if (on || flash) {
ctx.fillStyle = `rgba(${color},0.18)`; ctx.fillRect(x - 4, y - 3, 9, 7);
if (flash) discoCone(ctx, x, y, Math.PI / 2, e.len ?? 65, 0.3, color, 0.2);
}
ctx.fillStyle = `rgba(${color},${on || flash ? 0.95 : 0.15})`;
ctx.fillRect(x - 2, y - 1, 5, 3);
if (on || flash) { ctx.fillStyle = '#fff5df'; ctx.fillRect(x, y, 1, 1); }
}
ctx.restore();
},
// Gobo sulla folla «di Codex»: macchie a gradini, con tagli scuri, più grandi verso il primo piano. L'area le contiene tutte.
pixelspots(ctx, e, time) {
const [ax, ay, aw, ah] = e.area, colors = e.colors ?? ['255,45,170', '45,190,255'];
const beat = time * (e.bpm ?? 124) / 60000, hit = (1 - beat % 1) ** 2;
ctx.save();
ctx.globalCompositeOperation = 'lighter';
for (let i = 0; i < (e.count ?? 6); i++) {
const phase = beat * Math.PI / 8 + i * 2.4;
const v = 0.5 + 0.5 * Math.sin(phase * 0.7 + i), u = 0.5 + 0.5 * Math.sin(phase);
const rx = Math.min((e.size ?? 24) * (0.55 + 0.45 * v), aw / 2), ry = Math.min(rx * 0.3, ah / 2);
const x = ax + rx + u * (aw - rx * 2), y = ay + ry + v * (ah - ry * 2);
const c = colors[(i + Math.floor(beat / 4)) % colors.length];
// Le tre impronte separate ricordano un disco sagomato davanti al proiettore.
for (let part = -1; part <= 1; part++)
discoPool(ctx, x + part * rx * 0.65, y, rx * 0.25, ry * (part ? 0.7 : 1), c, 0.12 + 0.16 * hit);
}
const g = ctx.createRadialGradient(bx, by, 0, bx, by, 9);
g.addColorStop(0, 'rgba(255,255,255,0.8)');
g.addColorStop(1, 'rgba(200,180,255,0)');
ctx.fillStyle = g;
ctx.fillRect(bx - 9, by - 9, 18, 18);
ctx.restore();
},
// Lampioni: un alone caldo che si accende (di solito solo di sera, con `phase`).
@@ -778,6 +923,15 @@ function drawCloseup(ctx, game, time) {
}
// L'incantesimo va sopra la scena (con Andrea dentro), già disegnata.
if (def.type === 'incanto') { drawIncanto(ctx, c); return; }
// Il risveglio, visto da dentro gli occhi: anche lui sopra la scena già disegnata.
if (def.type === 'risveglio') { drawRisveglio(ctx, c, backgrounds.get(game.backdrop())); return; }
// Il telefonino del Barone: sale come il libro, e il sito nello schermo si allarga a tutta la scena.
if (def.type === 'telefono') {
const pic = src => { if (!backgrounds.has(src)) loadBackground(src); return backgrounds.get(src); };
drawTelefono(ctx, c, def, { phone: pic(def.phone), site: pic(def.site) }, time);
closeupHint(ctx, game.speech ? null : telefonoHint(c), time);
return;
}
// Il manifesto ingrandito: la scena si scurisce e lui viene avanti, poi l'invito a toccarlo.
if (def.type === 'poster') {
if (!backgrounds.has(def.picture)) loadBackground(def.picture);
@@ -902,7 +1056,10 @@ function drawCloseup(ctx, game, time) {
ctx.restore();
// Il suggerimento in alto, finché c'è qualcosa da sfogliare.
const hint = c.step === 'cover' ? 'Clic o tocco: apri il libro' : c.step === 'title' ? 'Clic o tocco: gira la pagina' : null;
if (hint && !game.speech && c.age > 0.5) {
closeupHint(ctx, !game.speech && c.age > 0.5 ? hint : null, time);
}
function closeupHint(ctx, hint, time) {
if (hint) {
ctx.save();
ctx.globalAlpha = 0.65 + 0.35 * Math.sin(time / 300);
ctx.font = `7px ${FONT}`;
+90
View File
@@ -0,0 +1,90 @@
// Il risveglio (story.closeups, type 'risveglio'; Andrea, 31 ottobre: «si ritrova aprendo piano piano gli occhi su
// una panchina, come svegliato da una notte brava, con effetto annebbiato»). Sopra la scena di adesso, vista da
// dentro gli occhi: le palpebre si aprono a fatica, ricascano, si riaprono; tutto è sfocato e doppio, e ondeggia.
// Le battute (`lines`, ognuna col suo `at` in secondi) partono mentre gli occhi si aprono; mentre Andrea parla il
// tempo del risveglio si ferma (la battuta va letta), l'ondeggiare no. Unità: i pixel della scena 640×360.
// Quanto sono aperti gli occhi (0 chiusi, 1 aperti) nel tempo: tre tentativi, l'ultimo è quello buono.
const OPEN = [[0, 0], [1.4, 0], [2.1, 0.22], [2.5, 0.04], [2.9, 0.04], [3.8, 0.5], [4.15, 0.12], [4.45, 0.2], [5.6, 0.85], [5.85, 0.55], [6.9, 1]];
export const END = 8.2;
const clamp01 = k => Math.max(0, Math.min(1, k));
const smooth = k => k * k * (3 - 2 * k);
export function openness(t) {
for (let i = 1; i < OPEN.length; i++) {
const [t1, o1] = OPEN[i];
if (t <= t1) { const [t0, o0] = OPEN[i - 1]; return o0 + (o1 - o0) * smooth(clamp01((t - t0) / (t1 - t0))); }
}
return 1;
}
// La nebbia: sfocatura e vista doppia, che si diradano fino a sparire alla fine.
export const haze = t => 1 - smooth(clamp01((t - 1.4) / (END - 1.6)));
export function newRisveglio(id) { return { id, t: 0, age: 0, fired: 0 }; }
// Il tempo che passa: torna le battute da dire adesso, e 'end' alla fine.
export function updateRisveglio(c, def, dt) {
c.t += dt;
const lines = def.lines ?? [], due = [];
while (c.fired < lines.length && c.t >= lines[c.fired].at) { const { at, ...e } = lines[c.fired++]; due.push(e); }
return { due, end: c.t >= END && c.fired >= lines.length };
}
// La nebbia è lo sfondo rimpicciolito e riallargato morbido, sopra la scena: niente filtri. Sfocare con ctx.filter
// (rileggendo la tela) o col filter CSS sulla tela rallentava tutto il gioco anche a risveglio finito.
const SMALL = [80, 45];
let small = null, smallOf = null;
function blurred(bg) {
if (smallOf !== bg) {
small ??= Object.assign(document.createElement('canvas'), { width: SMALL[0], height: SMALL[1] });
const s = small.getContext('2d');
s.imageSmoothingEnabled = true;
s.drawImage(bg, 0, 0, ...SMALL);
smallOf = bg;
}
return small;
}
export function drawRisveglio(ctx, c, bg) {
const t = c.t, h = haze(t), open = openness(t), W = 640, H = 360;
ctx.save();
// La scena annebbiata (lo sfondo sfocato sopra) e la vista doppia (una seconda copia, spostata, che ondeggia).
if (h > 0.01 && bg && typeof document !== 'undefined') {
const fog = blurred(bg), sway = 14 * h, dx = Math.sin(c.age * 1.3) * sway, dy = Math.sin(c.age * 0.9 + 1) * sway * 0.35;
ctx.imageSmoothingEnabled = true;
ctx.globalAlpha = 0.85 * h;
ctx.drawImage(fog, 0, 0, W, H);
ctx.globalAlpha = 0.4 * h;
ctx.drawImage(fog, dx, dy, W, H);
ctx.globalAlpha = 1;
ctx.imageSmoothingEnabled = false;
}
// La luce del mattino che abbaglia.
if (h > 0.01) {
ctx.fillStyle = `rgba(255,244,214,${0.22 * h})`;
ctx.fillRect(0, 0, W, H);
}
// Gli angoli scuri, che si aprono con gli occhi.
const v = ctx.createRadialGradient(W / 2, H / 2, 60 + 220 * open, W / 2, H / 2, 420);
v.addColorStop(0, 'rgba(10,2,4,0)');
v.addColorStop(1, `rgba(10,2,4,${0.85 * (1 - open * 0.75) * Math.max(h, 0.3)})`);
ctx.fillStyle = v;
ctx.fillRect(0, 0, W, H);
// Le palpebre: sopra e sotto, col bordo curvo; il bordo morbido sono quattro passate sempre più chiuse e più
// scure. Chiuse, si vede il rosso scuro della luce che passa.
if (open < 0.999) {
const mid = H / 2, lid = (gap, bulge, sign) => {
const edge = mid + sign * gap, side = edge - sign * bulge, far = sign < 0 ? -20 : H + 20;
ctx.beginPath();
ctx.moveTo(-20, far); ctx.lineTo(W + 20, far); ctx.lineTo(W + 20, side);
ctx.quadraticCurveTo(W / 2, edge * 2 - side, -20, side);
ctx.closePath(); ctx.fill();
};
for (let i = 0; i < 4; i++) {
const gap = 200 * open + (3 - i) * 4, bulge = 70 + 60 * open;
ctx.fillStyle = `rgba(22,4,6,${i === 3 ? 1 : 0.3})`;
lid(gap, bulge, -1); lid(gap, bulge, 1);
}
}
ctx.restore();
}
+81
View File
@@ -0,0 +1,81 @@
// Il telefonino del Barone (story.closeups, type 'telefono'; Andrea, 31 ottobre: «prenda il cellulare, nello stile
// di come si sfoglia la storia infinita, e quando si clicca viene a tutto schermo il sito di San Piero»).
// Come il libro: la scena si scurisce e le mani alzano il telefonino dal basso; nello schermo c'è il sito
// (`site`, un'immagine 16:9 della home di sanpiero50037.it). Un tocco: lo schermo si allarga a tutta la scena.
// Sul sito si tocca il link (`link`, in pixel dell'immagine del sito) e si apre il muro: una pagina vera
// (assets/muro, la mette main.js sopra la scena), che qui sotto lascia solo il fondo. A muro aperto partono le
// battute `comment`, e chiudendo `closing`. Unità: i pixel della scena 640×360.
export const RISE = 0.45, ZOOM = 0.55;
const W = 640, H = 360;
const clamp01 = k => Math.max(0, Math.min(1, k));
const ease = k => (1 - Math.cos(Math.PI * clamp01(k))) / 2;
export function newTelefono(id) { return { id, step: 'phone', t: 0, age: 0, aim: null }; }
// Il tempo: lo zoom finisce sul sito; a muro aperto, dopo un attimo, le battute (una alla volta, come il libro).
export function updateTelefono(c, dt) {
c.t += dt;
if (c.step === 'zoom' && c.t >= ZOOM) Object.assign(c, { step: 'site', t: 0 });
}
// Dove sta il link del muro sulla scena, a sito aperto: il riquadro `link` riportato da immagine a scena.
export function linkBox(def, site) {
const [x, y, w, h] = def.link, kx = W / site[0], ky = H / site[1];
return [x * kx, y * ky, w * kx, h * ky];
}
const inside = ([x, y, w, h], [px, py]) => px >= x && px <= x + w && py >= y && py <= y + h;
// Un tocco: sul telefonino apre il sito; sul sito, solo sul link, apre il muro. Torna true se è successo qualcosa.
export function tapTelefono(c, def) {
if (c.step === 'phone') { if (c.age < RISE) return false; Object.assign(c, { step: 'zoom', t: 0 }); return true; }
if (c.step === 'site' && c.aim && inside(linkBox(def, def.siteSize), c.aim)) { Object.assign(c, { step: 'wall', t: 0 }); return true; }
return false;
}
// Il riquadro dello schermo sulla scena, col telefonino che sale dal basso.
function phoneRect(def, img, age) {
const rise = 1 - clamp01(age / RISE), k = def.phoneScale ?? 1;
const w = img.width * k, h = img.height * k, x = Math.round((W - w) / 2), y = Math.round(H - h + 140 * rise * rise);
return { x, y, w, h, k };
}
export function drawTelefono(ctx, c, def, { phone, site }, time) {
ctx.save();
ctx.fillStyle = `rgba(8, 5, 12, ${Math.min(0.78, c.age / 0.35 * 0.78)})`;
ctx.fillRect(0, 0, W, H);
if (!phone || !site) { ctx.restore(); return; }
const p = phoneRect(def, phone, c.age), [sx, sy, sw, sh] = def.screen;
const screen = [p.x + sx * p.k, p.y + sy * p.k, sw * p.k, sh * p.k];
const z = c.step === 'phone' ? 0 : c.step === 'zoom' ? ease(c.t / ZOOM) : 1;
// Il telefonino, finché lo schermo non ha preso tutta la scena; poi sfuma.
if (z < 1) {
ctx.globalAlpha = 1 - z;
ctx.imageSmoothingEnabled = false;
ctx.drawImage(phone, p.x, p.y, p.w, p.h);
ctx.globalAlpha = 1;
}
// Il sito: dentro lo schermo, poi sempre più grande fino a tutta la scena. Lo schermo ha le sue proporzioni:
// il sito ci sta intero, centrato, sul fondo del sito.
const r = screen.map((v, i) => v + ([0, 0, W, H][i] - v) * z);
const fit = Math.min(r[2] / site.width, r[3] / site.height), dw = site.width * fit, dh = site.height * fit;
ctx.fillStyle = def.paper ?? '#f7e0c0';
ctx.fillRect(r[0], r[1], r[2], r[3]);
ctx.imageSmoothingEnabled = true;
ctx.drawImage(site, r[0] + (r[2] - dw) / 2, r[1] + (r[3] - dh) / 2, dw, dh);
// Sotto il muro (la pagina vera) resta il fondo del sito.
if (c.step === 'wall') { ctx.fillStyle = def.paper ?? '#f7e0c0'; ctx.fillRect(0, 0, W, H); }
// Il link da toccare: un riquadro che pulsa intorno al mattone del muro.
if (c.step === 'site') {
const [x, y, w, h] = linkBox(def, def.siteSize), pulse = 0.5 + 0.5 * Math.sin(time / 260);
ctx.strokeStyle = `rgba(255, 210, 90, ${0.5 + 0.5 * pulse})`;
ctx.lineWidth = 2 + pulse;
ctx.strokeRect(Math.round(x) - 2, Math.round(y) - 2, Math.round(w) + 4, Math.round(h) + 4);
}
ctx.imageSmoothingEnabled = false;
ctx.restore();
}
// Il suggerimento in alto, finché c'è qualcosa da toccare.
export const telefonoHint = c => c.step === 'phone' && c.age > RISE ? 'Clic o tocco: guarda il sito'
: c.step === 'site' ? 'Clic o tocco sul Muro del Sanpierino' : null;
+17 -2
View File
@@ -65,6 +65,10 @@ $('effects-volume').addEventListener('input', e => { ambience.configure({ volume
showSound();
window.music = music; // per i test nel browser
// Il gioco non usa service worker: se ne è rimasto uno di un'altra app passata su questo indirizzo (la Bussola su
// localhost:8080, 31 ottobre), si sgancia, sennò intercetta le pagine aperte dal gioco (il muro nel telefonino).
navigator.serviceWorker?.getRegistrations().then(list => list.forEach(r => r.unregister())).catch(() => {});
fetch(versioned('assets/voices/index.json')).then(r => r.ok ? r.json() : {}).then(index => { voices.index = index; }).catch(() => {});
window.voices = voices;
let voiced = null;
@@ -75,7 +79,7 @@ function start(state, rione = null) {
game = new Game(story, { state, onSave: data => storage && writeSave(storage, data), voiceTime: (who, text) => voices.seconds(who, text) });
command = new Command(game);
if (!state && rione) game.state.flags['rione:' + rione] = true;
if (!state) game.go(story.start.scene, story.start.at);
if (!state) game.go(story.start.scene, story.start.at, story.start.face);
$('title').hidden = true;
$('ending').hidden = true;
running = true;
@@ -1094,7 +1098,18 @@ function updateUi() {
setProp($('spots-button'), 'hidden', !running || !!game.closeup);
setProp($('notebook-button'), 'hidden', !running || !story.notebook || !!game.closeup);
setProp($('closeup-close'), 'hidden', !running || !game.closeup);
setText($('closeup-close'), torch ? c.step === 'done' ? 'Continua' : 'Torna fuori' : mg ? c.step === 'result' ? mg.back : 'Lascia stare' : 'Chiudi');
// Il Muro del Sanpierino: a muro aperto sul telefonino del Barone, la pagina vera sopra la scena. L'iframe c'è solo
// allora (sempre presente, anche nascosto, rallentava la pagina): ogni volta che si riapre si ricarica, coi
// messaggi nuovi della storia.
const wall = running && c && story.closeups[c.id]?.type === 'telefono' && c.step === 'wall';
let muro = $('muro');
if (wall && !muro) {
muro = Object.assign(document.createElement('iframe'), { id: 'muro', title: 'Il Muro del Sanpierino', src: versioned('assets/muro/index.html') });
canvas.after(muro);
}
if (!wall && muro) muro.remove();
if (wall) muro.style.setProperty('--muro-scale', String(canvas.clientWidth / 1067));
setText($('closeup-close'), torch ? c.step === 'done' ? 'Continua' : 'Torna fuori' : mg ? c.step === 'result' ? mg.back : 'Lascia stare' : c && story.closeups[c.id]?.type === 'risveglio' ? 'Salta' : 'Chiudi');
setProp($('panel'), 'inert', !!c);
canvas.classList.toggle('torch', !!torch);
canvas.classList.toggle('bocce', !!mg);
+94 -19
View File
@@ -64,7 +64,9 @@ export const story = {
title: 'La Leggenda del Regolo',
// La mappa (pulsante «Mappa»): ogni scena con `map: [x, y]` (percentuali della pianta) ci compare quando è
// stata visitata; le strade sono le uscite fra scene. Un posto nuovo entra con la sua riga `map`.
start: { scene: 'piazza', at: [250, 320] },
// Si comincia sulla panchina dei vecchietti, dopo una notte brava (Andrea, 31 ottobre): il risveglio è il primo
// piano `risveglio`, che parte entrando in piazza la prima volta.
start: { scene: 'piazza', at: [96, 328], face: 'left' },
// Le fasi del giorno (idea di Andrea, 24 ottobre): seguono la storia, non l'orologio. Il primo capitolo comincia
// di pomeriggio; con le impronte del Regolo viene il tramonto, e quando Andrea ha la torcia è sera. Sulle scene
// all'aperto un velo di colore (`tint`); luci ed effetti possono valere solo in certe fasi (`phase`).
@@ -82,6 +84,7 @@ export const story = {
zones: [
{
flag: 'ingorgo', scenes: ['piazza', 'via_antica_posta', 'piazzetta_comune', 'antica_posta_alta', 'piazzetta'],
exitsOnly: true, // alla festa si clicca solo per spostarsi (Andrea)
leave: [
{ say: 'Mi stropiccio gli occhi. La musica non c\'è più: è di nuovo maggio.' },
{ say: 'E il palio è sempre sparito. Però che serata.' },
@@ -119,7 +122,8 @@ export const story = {
cardetole: 'Io corro per Cardetole, i verdi: gioco in casa.',
campomigliaio: 'Io corro per Campomigliaio, i blu.',
} },
'E stamattina, in piazza, è successo qualcosa...',
// [DA ANDREA: la riga che porta al risveglio sulla panchina]
'Ieri sera s\'è fatto tardi. Molto tardi...',
],
// Colonna sonora di Andrea (fatta con Suno): titolo, paese, ricordi (fine e Trebbio).
// splash: la musichetta del cartello d'apertura (tools/jingle.py), suonata una volta sola.
@@ -339,12 +343,62 @@ export const story = {
{ closeup: 'ingorgo' },
],
},
// Il Muro del Sanpierino sul telefonino del Barone (Andrea, 31 ottobre): sale come il libro, nello schermo la
// home di sanpiero50037.it (la Wayback Machine, 31 dicembre 2007, senza la data in alto); un tocco la porta a
// tutto schermo, e il mattone «Nuovo muro del Sanpierino» apre il muro: la pagina vera (assets/muro, fatta da
// tools/muro.py dal muro riesumato in ~/Workspaces/misc/spswall), coi messaggi veri del 2007 (Andrea: «quelli
// veri»; niente email, «Scrivi» spento) e sopra quelli del gioco qui sotto, ognuno con la sua condizione:
// «Aggiorna» ricarica, e la storia che va avanti ne fa comparire di nuovi. [DA ANDREA: messaggi e battute]
muro_sanpierino: {
type: 'telefono',
phone: 'assets/closeups/telefonino-barone.png', // Codex, prompts/closeups/telefonino-barone.txt
screen: [120, 25, 106, 142], // lo schermo, in pixel del telefonino
site: 'assets/closeups/sanpiero50037-home.png', siteSize: [1067, 600],
link: [48, 42, 302, 99], // il mattone del muro, in pixel del sito
paper: '#f7e1c0',
messages: [
{ nick: 'Uno di Cardetole', date: '2007-05-26 00:55:12', text: 'Quest\'anno il palio è nostro!!! [bouncer]\nGli altri rioni si preparino a piangere. [14_bigmouth]' },
{ nick: 'Baron De Duke', date: '2007-05-26 01:40:37', text: 'Chi ha trovato un paio di mutande, le riporti al circolino.\nSenza fare domande. [15_weeping]' },
{ nick: 'Il Regolo', date: '2007-05-26 03:12:08', text: 'IL PALIO ME LO SONO RIPRESO IO. [22_yikes]\nChi lo rivuole, venga a prenderselo. Se ha coraggio.' },
// Quelli che arrivano con la storia: si vedono riaprendo il muro, o con «Aggiorna».
{ when: 'indizio_regolo', nick: 'Uno che passava', date: '2007-05-26 18:20:44', text: 'Ma avete visto le impronte sotto la Fortezza??? Grandi come una pizza!!! [24_shocked]' },
{ when: 'casetta_aperta', nick: 'Il Regolo', date: '2007-05-26 21:03:51', text: 'Ho sentito che qualcuno ha trovato una torcia. Io ho sempre la luna. [28_spin]' },
],
comment: [
{ say: '«Il palio me lo sono ripreso io». Firmato: il Regolo. Alle tre di notte.' },
{ say: 'E sotto ci sono io, con le mutande. Nessuno ha risposto.', who: 'barondeduke', pose: 'facepalm' },
],
closing: [
{ say: 'Barone, il sito è tuo: non puoi vedere chi l\'ha scritto?' },
{ say: 'Potrei. Ma ci vuole il computer di casa. Il telefonino, più di così, non fa.', who: 'barondeduke', pose: 'shrug' },
],
},
// L'inizio della partita: Andrea apre gli occhi sulla panchina dei vecchietti, dopo una notte brava (Andrea,
// 31 ottobre). Le battute `lines` arrivano mentre gli occhi si aprono; `complete` a occhi aperti.
// [DA ANDREA: le battute sono provvisorie]
risveglio: {
type: 'risveglio',
lines: [
{ at: 0.8, say: 'Mmmh... che botta.' },
{ at: 3.0, say: 'Ma... dove sono?' },
{ at: 5.9, say: 'Una panchina. In piazza. Coi vecchietti.' },
],
complete: [
{ say: 'Oh, s\'è svegliato il bello addormentato! Bella nottata, eh?', who: 'vecchietta', pose: 'laugh' },
{ say: 'Ma l\'hai saputo? Il palio è sparito!', who: 'vecchietto', pose: 'point' },
{ say: 'Il palio? Il quadro che hanno dipinto i ragazzi delle medie?' },
{ say: 'Lo tenevano a scuola: stamani la Gianna ha trovato il posto vuoto.', who: 'vecchietta', pose: 'explain' },
{ say: 'La festa è domani: se nessuno lo ritrova, salta tutto. Tocca a me.' },
],
},
// L'incantesimo dell'Ingorgo Sonoro, dal manifesto in piazza: luci, cassa, lampo, ed è la notte della festa.
ingorgo: {
type: 'incanto',
swap: [{ set: 'ingorgo' }],
complete: [
{ say: 'Ma... è l\'Ingorgo! Le console, la gente, le spine della birra!' },
// Più emozione (Andrea: «MAHHH E' L'INGORGOOOOO!!!!!»); la voce legge la versione di tools/pronuncia.json.
{ say: 'Mahhh... è l\'INGORGOOOO!!!' },
{ say: 'Le console, la gente, le spine della birra!' },
{ say: 'Il palio può aspettare una sera. O no?' },
],
},
@@ -588,7 +642,7 @@ export const story = {
{ when: 'ingorgo', music: 'assets/audio/ingorgo-house.mp3', at: [195, 235] },
{ when: 'ingorgo', music: 'assets/audio/ingorgo-latino.mp3', at: [-160, 250], far: 600 },
],
effects: [{ type: 'stagelights', phase: 'ingorgo', from: [[116, 146], [276, 146]], wash: [200, 215, 150], bpm: 124, beams: 2, strobe: 8 }, { type: 'clouds', area: [160, 0, 440, 80], count: 3, big: true, phase: ['pomeriggio', 'tramonto', 'sera'] }, { type: 'windows', phase: 'sera', rects: [[16, 37, 8, 18], [48, 42, 8, 18], [50, 110, 8, 16], [97, 77, 8, 12], [122, 80, 8, 12], [170, 90, 8, 12], [204, 103, 8, 12], [239, 111, 8, 12], [268, 117, 7, 14], [287, 125, 6, 14], [97, 130, 9, 14], [127, 133, 9, 14], [170, 142, 9, 14], [204, 150, 8, 12], [239, 155, 8, 12], [268, 160, 7, 14], [287, 163, 6, 14], [386, 163, 14, 16], [424, 160, 14, 16], [472, 157, 14, 16], [524, 155, 14, 16], [390, 212, 14, 16], [526, 208, 14, 16]] }, { type: 'birds', phase: ['pomeriggio', 'tramonto'], area: [150, 20, 460, 80] }],
effects: [{ type: 'stagelights', phase: 'ingorgo', from: [[116, 146], [276, 146], [196, 140]], wash: [200, 215, 170], bpm: 124, beams: 3, strobe: 4, pulse: 0.08 }, { type: 'lasers', phase: 'ingorgo', at: [196, 150], count: 7, color: '255,60,200', len: 360 }, { type: 'floorspots', phase: 'ingorgo', area: [40, 240, 560, 110], bpm: 124, count: 7 }, { type: 'clouds', area: [160, 0, 440, 80], count: 3, big: true, phase: ['pomeriggio', 'tramonto', 'sera'] }, { type: 'windows', phase: 'sera', rects: [[16, 37, 8, 18], [48, 42, 8, 18], [50, 110, 8, 16], [97, 77, 8, 12], [122, 80, 8, 12], [170, 90, 8, 12], [204, 103, 8, 12], [239, 111, 8, 12], [268, 117, 7, 14], [287, 125, 6, 14], [97, 130, 9, 14], [127, 133, 9, 14], [170, 142, 9, 14], [204, 150, 8, 12], [239, 155, 8, 12], [268, 160, 7, 14], [287, 163, 6, 14], [386, 163, 14, 16], [424, 160, 14, 16], [472, 157, 14, 16], [524, 155, 14, 16], [390, 212, 14, 16], [526, 208, 14, 16]] }, { type: 'birds', phase: ['pomeriggio', 'tramonto'], area: [150, 20, 460, 80] }],
walk: [[84, 244], [330, 238], [345, 230], [362, 238], [462, 246], [466, 276], [544, 276], [548, 248], [588, 250], [600, 290], [640, 300], [640, 360], [0, 360], [0, 330], [84, 318]],
depth: [[236, 0.5], [360, 1.35]],
entry: [345, 240],
@@ -672,7 +726,7 @@ export const story = {
{
// Il manifesto dell'Ingorgo Sonoro dell'estate prima, mezzo strappato: guardandolo, Andrea esprime il
// desiderio, e la festa arriva (Andrea, 30 ottobre). [DA ANDREA: le battute sono provvisorie]
id: 'manifesto_ingorgo', name: 'vecchio manifesto dell\'Ingorgo Sonoro', box: [226, 198, 16, 22], at: [234, 250], face: 'up', when: '!ingorgo',
id: 'manifesto_ingorgo', name: 'vecchio manifesto dell\'Ingorgo Sonoro', box: [223, 195, 22, 26], at: [234, 250], face: 'up', when: '!ingorgo',
// Come quello vero (assets/raw/riferimenti/manifesto-ingorgo-2014.png): blu, il cartello bianco e rosso col DJ.
art: [{ rect: [226, 198, 16, 22], fill: '#1c2050' }, { rect: [233, 201, 2, 2], fill: '#d8262e' }, { rect: [231, 203, 6, 2], fill: '#d8262e' },
{ rect: [229, 205, 10, 4], fill: '#d8262e' }, { rect: [232, 205, 4, 3], fill: '#f4f1e8' }, { rect: [228, 211, 12, 2], fill: '#f4f1e8' },
@@ -732,7 +786,7 @@ export const story = {
id: 'verso_calimara', name: 'Via Calimara, verso la farmacia', box: [180, 334, 380, 26], at: [360, 354], exit: { scene: 'via_calimara', at: [365, 212], face: 'down' },
},
],
enter: [{ if: '!intro', then: [{ set: 'intro' }, { say: 'Il palio è sparito! Il quadro che hanno dipinto i ragazzi delle medie.' }, { say: 'Lo tenevano a scuola: stamani la Gianna ha trovato il posto vuoto.' }, { say: 'La festa è domani: se nessuno lo ritrova, salta tutto. Tocca a me.' }] }],
enter: [{ if: '!intro', then: [{ set: 'intro' }, { closeup: 'risveglio' }] }],
},
fortezza: {
name: 'Sotto le mura della Fortezza',
@@ -1625,7 +1679,7 @@ export const story = {
// Le console dell'Ingorgo Sonoro (music.js, mix). [DA ANDREA: quale genere suonava in quale piazza]
// La tecno davanti alla casetta bianca; l'underground arriva da giù, da Via dell'Antica Posta.
// Le luci da palco della festa (render.js: stagelights, lasers, mirrorball).
effects: [{ type: 'lasers', phase: 'ingorgo', at: [400, 198], count: 9 }, { type: 'stagelights', phase: 'ingorgo', from: [[356, 192], [460, 192]], wash: [408, 225, 150], bpm: 132, beams: 2, strobe: 4, colors: ['90,255,110', '255,255,255', '60,220,255'] }],
effects: [{ type: 'lasers', phase: 'ingorgo', at: [400, 198], count: 13, len: 460 }, { type: 'lasers', phase: 'ingorgo', at: [370, 200], count: 7, color: '60,220,255' }, { type: 'stagelights', phase: 'ingorgo', from: [[356, 192], [460, 192], [408, 186]], wash: [408, 225, 180], bpm: 132, beams: 3, strobe: 2, pulse: 0.1, colors: ['90,255,110', '255,255,255', '60,220,255'] }, { type: 'floorspots', phase: 'ingorgo', area: [40, 250, 560, 100], bpm: 132, count: 8, colors: ['90,255,110', '255,255,255', '60,220,255'] }],
stages: [
{ when: 'ingorgo', music: 'assets/audio/ingorgo-techno.mp3', at: [495, 245] },
{ when: 'ingorgo', music: 'assets/audio/ingorgo-underground.mp3', at: [700, 300], far: 650 },
@@ -1671,7 +1725,7 @@ export const story = {
// Le console dell'Ingorgo Sonoro (music.js, mix). [DA ANDREA: quale genere suonava in quale piazza]
// Il bar della birra sui gradini; il latino dal Comune a sinistra, la house giù dalla piazza, l'underground su in cima.
// Le luci da palco della festa (render.js: stagelights, lasers, mirrorball).
effects: [{ type: 'stagelights', phase: 'ingorgo', wash: [90, 190, 110], bpm: 98, colors: ['255,170,60', '255,90,160'] }, { type: 'stagelights', phase: 'ingorgo', from: [[430, 205]], beams: 1, len: 160, bpm: 98 }],
effects: [{ type: 'stagelights', phase: 'ingorgo', wash: [90, 190, 130], bpm: 98, colors: ['255,170,60', '255,90,160'], pulse: 0.05 }, { type: 'stagelights', phase: 'ingorgo', from: [[430, 205], [130, 150]], beams: 2, len: 260, bpm: 98 }, { type: 'floorspots', phase: 'ingorgo', area: [40, 230, 560, 120], bpm: 98, count: 5 }],
stages: [
{ when: 'ingorgo', music: 'assets/audio/ingorgo-latino.mp3', at: [-110, 250] },
{ when: 'ingorgo', music: 'assets/audio/ingorgo-house.mp3', at: [320, 560] },
@@ -1720,7 +1774,7 @@ export const story = {
// Le console dell'Ingorgo Sonoro (music.js, mix). [DA ANDREA: quale genere suonava in quale piazza]
// La console del latino davanti al Comune; la house arriva da giù, dalla piazza.
// Le luci da palco della festa (render.js: stagelights, lasers, mirrorball).
effects: [{ type: 'stagelights', phase: 'ingorgo', from: [[232, 212], [332, 212]], wash: [282, 228, 140], bpm: 98, beams: 2, colors: ['255,120,40', '255,60,120', '255,200,60', '200,80,255'] }],
effects: [{ type: 'stagelights', phase: 'ingorgo', from: [[232, 212], [332, 212], [282, 205]], wash: [282, 228, 170], bpm: 98, beams: 3, strobe: 8, pulse: 0.08, colors: ['255,120,40', '255,60,120', '255,200,60', '200,80,255'] }, { type: 'floorspots', phase: 'ingorgo', area: [40, 250, 560, 100], bpm: 98, count: 7, colors: ['255,120,40', '255,60,120', '255,200,60', '200,80,255'] }],
stages: [
{ when: 'ingorgo', music: 'assets/audio/ingorgo-latino.mp3', at: [290, 235] },
{ when: 'ingorgo', music: 'assets/audio/ingorgo-house.mp3', at: [260, 640], far: 700 },
@@ -1776,7 +1830,14 @@ export const story = {
// Le console dell'Ingorgo Sonoro (music.js, mix). [DA ANDREA: quale genere suonava in quale piazza]
// L'underground sotto il muro della villa; su, in fondo, la tecno di Cambray Digny; giù, lontano, la house.
// Le luci da palco della festa (render.js: stagelights, lasers, mirrorball).
effects: [{ type: 'mirrorball', phase: 'ingorgo', at: [550, 131], area: [380, 40, 260, 200], count: 46 }, { type: 'stagelights', phase: 'ingorgo', wash: [552, 205, 120], bpm: 122, colors: ['170,60,255', '90,40,220', '220,60,255'] }],
// Qui le luci sono quelle di Codex (Andrea, 30 ottobre: «l'effetto qui mi piaceva molto»): teste sulle casse a
// destra, la palla a specchi che proietta sui due muri e sulla folla, le lampade a inseguimento sulla traversa.
effects: [
{ type: 'pixelbeams', phase: 'ingorgo', from: [[499, 177], [620, 153]], wash: [568, 220, 70], bpm: 122, aim: -0.35, spread: 0.55, sweep: 8, power: 0.28, colors: ['155,65,255', '65,110,255'] },
{ type: 'mirrorball', phase: 'ingorgo', at: [550, 131], hang: 91, from: [499, 177], surfaces: [[430, 60, 205, 140], [18, 105, 155, 85], [35, 245, 570, 105]], bpm: 122, count: 72 },
{ type: 'stagechase', phase: 'ingorgo', points: [[533, 210], [557, 210], [583, 209], [609, 208]], bpm: 122, colors: ['155,65,255', '65,110,255'] },
{ type: 'pixelspots', phase: 'ingorgo', area: [130, 242, 490, 108], bpm: 122, count: 5, size: 20, colors: ['155,65,255', '65,110,255'] },
],
stages: [
{ when: 'ingorgo', music: 'assets/audio/ingorgo-underground.mp3', at: [565, 235] },
{ when: 'ingorgo', music: 'assets/audio/ingorgo-techno.mp3', at: [270, 120], far: 600 },
@@ -2074,15 +2135,7 @@ export const story = {
},
actions: {
look: [{ say: 'Il BaronDeDuke, col cappello da cowboy. Ha l\'aria di uno che aspetta qualcuno, e che spera non lo veda nessuno.' }],
talk: [
{ say: 'Barone! Che ci fai qui?' },
{ say: 'Aspetto uno che mi deve riportare le mutande.', who: 'barondeduke' },
{ say: 'Le... mutande?' },
{ say: 'Le ho perse a poker, ieri sera.', who: 'barondeduke', pose: 'facepalm' },
{ say: 'Avevo due donne e due jack. Entra la terza donna, e penso: «Male male si divide».', who: 'barondeduke', pose: 'cards' },
{ say: 'E invece?' },
{ say: 'L\'altro aveva colore.', who: 'barondeduke', pose: 'shrug' },
],
talk: [{ dialogue: 'barondeduke' }],
},
},
],
@@ -3889,6 +3942,28 @@ export const story = {
},
},
},
// Il BaronDeDuke nella discesa verso la polisportiva: le mutande perse a poker, e il Muro del Sanpierino sul
// telefonino (Andrea, 31 ottobre: lui è il webmaster di sanpiero50037.it).
barondeduke: {
nodes: {
start: { options: [
{ text: 'Barone! Che ci fai qui?', then: [
{ say: 'Aspetto uno che mi deve riportare le mutande.', who: 'barondeduke' },
{ say: 'Le... mutande?' },
{ say: 'Le ho perse a poker, ieri sera.', who: 'barondeduke', pose: 'facepalm' },
{ say: 'Avevo due donne e due jack. Entra la terza donna, e penso: «Male male si divide».', who: 'barondeduke', pose: 'cards' },
{ say: 'E invece?' },
{ say: 'L\'altro aveva colore.', who: 'barondeduke', pose: 'shrug' },
] },
// [DA ANDREA: le battute sono provvisorie]
{ text: 'Hai visto cosa c\'era sul muro del Sanpierino?', next: 'end', then: [
{ say: 'Il muro? Aspetta, ce l\'ho qui sul telefonino.', who: 'barondeduke', pose: 'explain' },
{ closeup: 'muro_sanpierino' },
] },
{ text: 'Ci vediamo, Barone.', next: 'end' },
] },
},
},
// L'Arcu davanti al circolino (battute di Andrea, 25 ottobre). Lavora alla Casa dell'Armadio: «Armando» è apposta.
arcu: {
nodes: {
+1 -1
View File
@@ -1,5 +1,5 @@
// La versione del gioco: la cambia scripts/bump.sh, insieme a package.json.
export const VERSION = '0.1.58';
export const VERSION = '0.1.62';
// 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).
+5
View File
@@ -81,6 +81,11 @@ body { display: flex; justify-content: center; overflow-x: hidden; -webkit-tap-h
font-size: clamp(10.08px, 1.66vw, 18.72px); font-weight: bold; line-height: 1.2; text-align: center;
text-shadow: 2px 0 #000, -2px 0 #000, 0 2px #000, 0 -2px #000, 2px 2px #000, -2px -2px #000; }
#speech:empty { display: none; }
#speech { z-index: 3; }
/* Il Muro del Sanpierino sul telefonino del Barone: la pagina vera (assets/muro) larga 1067 pixel come la home del
sito, rimpicciolita sulla scena da main.js (--muro-scale). */
#muro { position: absolute; left: 0; top: 0; width: 1067px; height: 600px; border: 0; z-index: 1; background: #f7e1c0;
transform: scale(var(--muro-scale, 0.6)); transform-origin: 0 0; }
/* Il primo piano del libro si chiude da qui (o con Esc). */
#closeup-close { position: absolute; right: 10px; bottom: 10px; z-index: 2; font: bold clamp(10px, 1.5vw, 15px) var(--font);
padding: .45em 1.2em; border-radius: 6px; border: 0; cursor: pointer; color: var(--bg); background: var(--accent); }
+2
View File
@@ -56,6 +56,8 @@ try {
// Il cartello d'apertura, poi da solo il titolo.
assert.match(await page.textContent('#splash'), /Manzolo\s*\(Poca\)\s*Art\s*Presents/);
assert.ok(await page.isHidden('#title'), 'il titolo aspetta la fine del cartello');
// Il gioco parte con un import() dinamico, che l'evento load della pagina non aspetta.
await page.waitForFunction(() => window.jingle, null, { timeout: 10000 });
assert.match(await page.evaluate(() => jingle.src), /assets\/audio\/cartello\.mp3\?v=\d+\.\d+\.\d+$/, 'la musichetta del cartello');
assert.equal(await page.evaluate(() => music.want), null, 'la musica del titolo aspetta il titolo');
await page.waitForTimeout(2800);
+2 -1
View File
@@ -7,7 +7,8 @@ export function settle(game, { maxSteps = 20000, explore = false } = {}) {
game.skip();
}
// Un primo piano aperto aspetta il giocatore (sfogliare, chiudere): qui ci si ferma e decide il test.
if (game.closeup && !game.speech && game.reach <= 0) {
// Il risveglio dell'inizio no: va avanti da solo, come una battuta.
if (game.closeup && !game.speech && game.reach <= 0 && game.story.closeups[game.closeup.id].type !== 'risveglio') {
if (!explore || game.story.closeups[game.closeup.id].type !== 'torch') return said;
exploreTorch(game);
}
+1 -1
View File
@@ -36,7 +36,7 @@ try {
await page.screenshot({ path: `${out}/2-fasci.png` });
await page.waitForFunction(() => game.closeup?.t > 2.5, null, { timeout: 5000 });
await page.screenshot({ path: `${out}/3-coriandoli.png` });
await page.waitForFunction(() => !game.closeup && game.state.flags.ingorgo && game.speech?.text.includes('Ingorgo'), null, { timeout: 5000 });
await page.waitForFunction(() => !game.closeup && game.state.flags.ingorgo && game.speech?.text.toLowerCase().includes('ingorgo'), null, { timeout: 5000 });
await page.screenshot({ path: `${out}/3b-festa.png` });
await page.evaluate(() => settle(game));
// La festa, scena per scena: lo sfondo di notte, e la musica di più console insieme.
+21
View File
@@ -96,6 +96,15 @@ test('chiudendo l\'effetto prima del lampo, la festa arriva lo stesso', () => {
assert.ok(g.state.flags.ingorgo);
});
test('col dito il manifesto dell\'Ingorgo si prende anche toccando vicino, e non si scambia con quello del Palio', () => {
const g = new Game(story); g.go('piazza'); settle(g);
const [x, y, w, h] = g.hotspot('manifesto_ingorgo').box, reach = 28; // 22 pixel di schermo su un iPhone in orizzontale
for (const p of [[x + w / 2, y + h / 2], [x - 6, y + h / 2], [x + w + 4, y + h / 2], [x + w / 2, y + h + 6]])
assert.equal(g.hotspotAt(p, reach)?.id, 'manifesto_ingorgo', `tocco in ${p}`);
const palio = g.hotspot('manifesto').box;
assert.equal(g.hotspotAt([palio[0] + palio[2] / 2, palio[1] + palio[3] / 2], reach)?.id, 'manifesto');
});
test('il manifesto ingrandito si chiude senza fare niente: niente festa', () => {
const g = new Game(story); g.go('piazza'); settle(g);
g.act('look', 'manifesto_ingorgo'); settle(g);
@@ -103,6 +112,18 @@ test('il manifesto ingrandito si chiude senza fare niente: niente festa', () =>
assert.ok(!g.state.flags.ingorgo && !g.closeup);
});
test('alla festa si clicca solo per andare da un posto all\'altro', () => {
const g = fromPiazza();
for (const scene of ZONE.scenes) {
g.go(scene); settle(g);
const list = g.hotspots();
assert.ok(list.length && list.every(h => h.exit), `${scene}: solo le uscite`);
assert.ok(story.scenes[scene].hotspots.some(h => !h.exit), `${scene}: di giorno c'è anche altro`);
}
g.go('piazza'); settle(g);
assert.equal(g.hotspotAt([505, 200]), null, 'la colonna, alla festa, non risponde');
});
test('alla festa le luci da palco si muovono, e solo alla festa', () => {
globalThis.Image = class { set src(v) {} };
try {
+4 -3
View File
@@ -8,9 +8,10 @@ export async function checkMultimedia(browser, port, out, errors) {
await page.click('#splash'); await page.click('#new-game');
await page.click('[data-rione="campomigliaio"]'); await page.click('#skip-intro');
const settle = async () => {
for (let i = 0; i < 200; i++) {
for (let i = 0; i < 600; i++) {
if (await page.evaluate(() => !!game.speech)) await page.keyboard.press('Space');
if (await page.evaluate(() => game.closeup || game.idle)) return;
// Il risveglio dell'inizio è un primo piano che va avanti da solo: si aspetta che finisca.
if (await page.evaluate(() => (game.closeup && game.story.closeups[game.closeup.id].type !== 'risveglio') || game.idle)) return;
await page.waitForTimeout(30);
}
throw new Error('la sequenza non si ferma');
@@ -56,10 +57,10 @@ export async function checkMultimedia(browser, port, out, errors) {
await page.waitForFunction(() => game.closeup?.id === 'fessura');
};
await openTorch();
const saved = await page.evaluate(() => game.savable());
await page.keyboard.press('Escape'); await settle();
assert.equal(await page.evaluate(() => game.state.ended), false, 'uscire presto non vince');
await openTorch();
const saved = await page.evaluate(() => game.savable());
await page.click('#pause-button');
const age = await page.evaluate(() => game.closeup.age);
await page.waitForTimeout(150);
+30
View File
@@ -0,0 +1,30 @@
// Il risveglio sulla panchina nel browser vero: Nuova partita, l'introduzione, gli occhi che si aprono annebbiati.
// npm run browser:risveglio — schermate in /tmp/regolo-risveglio.
import { chromium } from 'playwright';
import { mkdirSync } from 'node:fs';
import assert from 'node:assert/strict';
import { staticServer } from './helpers.mjs';
const out = process.env.OUT ?? '/tmp/regolo-risveglio';
mkdirSync(out, { recursive: true });
const server = await staticServer();
let browser;
const errors = [];
try {
browser = await chromium.launch({ args: ['--autoplay-policy=no-user-gesture-required', '--use-gl=angle', '--use-angle=swiftshader', '--enable-unsafe-swiftshader'] });
const page = await browser.newPage({ viewport: { width: 1280, height: 860 } });
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.click('#splash'); await page.click('#new-game'); await page.click('#rione [data-rione="campomigliaio"]'); await page.click('#skip-intro');
await page.waitForFunction(() => game.closeup?.id === 'risveglio', null, { timeout: 10000 });
await page.waitForFunction(() => document.getElementById('closeup-close').textContent === 'Salta', null, { timeout: 5000 });
// Le battute si saltano solo per far correre il tempo fino ai momenti da fotografare.
for (const [t, name] of [[0.5, '0-buio'], [2.1, '1-primo-tentativo'], [3.8, '2-secondo'], [5.6, '3-quasi'], [7.2, '4-aperti']]) {
await page.waitForFunction(at => { if (game.speech && game.closeup) game.skip(); return !game.closeup || game.closeup.t >= at; }, t, { timeout: 30000, polling: 50 });
await page.screenshot({ path: `${out}/${name}.png` });
}
await page.waitForFunction(() => !game.closeup && game.state.flags.intro, null, { timeout: 15000 });
await page.screenshot({ path: `${out}/5-vecchietti.png` });
assert.deepEqual(errors, []);
console.log(`ok: schermate in ${out}`);
} finally { await browser?.close(); server.kill(); }
+43
View File
@@ -0,0 +1,43 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { openness, haze, END } from '../src/engine/risveglio.js';
import { Game } from '../src/engine/game.js';
import { story } from '../src/story/story.js';
import { settle } from './helpers.mjs';
test('il risveglio: gli occhi chiusi, due tentativi che ricascano, poi aperti; la nebbia si dirada', () => {
assert.equal(openness(0), 0);
assert.ok(openness(2.1) > 0.15 && openness(2.7) < 0.1, 'il primo tentativo ricasca');
assert.ok(openness(3.8) > openness(4.15), 'il secondo pure');
assert.equal(openness(END), 1);
assert.equal(haze(0), 1);
assert.equal(haze(END), 0);
});
test('la partita comincia sulla panchina: le battute a occhi semichiusi, poi i vecchietti col palio sparito', () => {
const g = new Game(story);
g.go(story.start.scene, story.start.at, story.start.face);
g.update(0);
assert.equal(g.closeup?.id, 'risveglio');
const said = [];
for (let i = 0; i < 2000 && (g.closeup || g.speech || g.queue.length); i++) {
if (g.speech) { said.push([g.speech.who, g.closeup ? openness(g.closeup.t) : 1]); g.skip(); }
g.update(0.05);
}
assert.deepEqual(said.slice(0, 3).map(s => s[0]), ['andrea', 'andrea', 'andrea']);
assert.ok(said[0][1] < 0.05, 'la prima battuta a occhi chiusi');
assert.ok(said.some(([who]) => who === 'vecchietto'), 'il vecchietto dà la notizia');
assert.ok(g.state.flags.intro && !g.closeup);
// Rientrando in piazza non si risveglia più.
g.go('via_cipriani'); settle(g); g.go('piazza'); g.update(0);
assert.equal(g.closeup, null);
});
test('saltando il risveglio (Esc) le battute arrivano lo stesso', () => {
const g = new Game(story);
g.go(story.start.scene, story.start.at);
g.update(0);
assert.ok(g.closeupClose());
const said = settle(g).map(s => s.text);
assert.ok(said.includes('Mmmh... che botta.') && said.some(t => t.includes('Il palio è sparito')));
});
+23 -1
View File
@@ -545,11 +545,33 @@ test('gli amici in giro: l\'Arcu al circolino, il Tozzino al cancello del tennis
ask('Ci si becca da i\' Trappola, ai\' Barrettino!');
assert.equal(g.dialogue, null);
assert.ok(talk('carza', 'tozzino').includes('tozzino: E chi sennò? Ha chiamato ora: s\'è fermato a Pratolino a mettere le catene. A breve arriva.'));
const baron = talk('polisportiva', 'barondeduke');
talk('polisportiva', 'barondeduke');
const baron = ask('Barone! Che ci fai qui?');
assert.ok(baron.includes('barondeduke: Avevo due donne e due jack. Entra la terza donna, e penso: «Male male si divide».'));
assert.equal(baron.at(-1), 'barondeduke: L\'altro aveva colore.');
});
test('il Muro del Sanpierino sul telefonino del Barone: sale, il sito a tutto schermo, il link apre il muro', async () => {
const { RISE, ZOOM, linkBox } = await import('../src/engine/telefono.js');
const g = new Game(story);
settle(g); g.go('polisportiva'); settle(g); g.act('talk', 'barondeduke'); settle(g);
g.choose(g.options().findIndex(o => o.text.includes('muro del Sanpierino'))); settle(g);
assert.equal(g.closeup?.id, 'muro_sanpierino');
const def = story.closeups.muro_sanpierino;
assert.ok(!g.closeupTap(), 'mentre sale, un tocco non conta');
g.update(RISE); assert.ok(g.closeupTap()); assert.equal(g.closeup.step, 'zoom');
for (let i = 0; i < 20; i++) g.update(ZOOM / 10);
assert.equal(g.closeup.step, 'site');
g.closeupAim([600, 340]); assert.ok(!g.closeupTap(), 'fuori dal link non succede niente');
const [x, y, w, h] = linkBox(def, def.siteSize);
g.closeupAim([x + w / 2, y + h / 2]); assert.ok(g.closeupTap()); assert.equal(g.closeup.step, 'wall');
const said = [];
for (let i = 0; i < 400 && (g.speech || !g.closeup.lines || g.closeup.lines.length); i++) { if (g.speech) { said.push(g.speech.text); g.skip(); } g.update(0.05); }
assert.ok(said[0].includes('Firmato: il Regolo'));
g.closeupClose();
assert.ok(settle(g).some(s => s.text.includes('il sito è tuo')), 'chiudendo, il commento');
});
test('i tentativi sbagliati hanno una risposta loro, non quella generica', () => {
const g = new Game(story);
settle(g);
+57
View File
@@ -0,0 +1,57 @@
// Il Muro del Sanpierino sul telefonino del Barone, nel browser vero: il telefonino che sale, il sito a tutto
// schermo, il clic sul mattone del muro. npm run browser:telefono — schermate in /tmp/regolo-telefono.
import { chromium } from 'playwright';
import { mkdirSync } from 'node:fs';
import assert from 'node:assert/strict';
import { staticServer } from './helpers.mjs';
const out = process.env.OUT ?? '/tmp/regolo-telefono';
mkdirSync(out, { recursive: true });
const server = await staticServer();
let browser;
const errors = [];
try {
browser = await chromium.launch({ args: ['--autoplay-policy=no-user-gesture-required', '--use-gl=angle', '--use-angle=swiftshader', '--enable-unsafe-swiftshader'] });
const page = await browser.newPage({ viewport: { width: 1280, height: 860 } });
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.click('#splash'); await page.click('#new-game'); await page.click('#rione [data-rione="campomigliaio"]'); await page.click('#skip-intro');
await page.evaluate(async () => {
const { settle } = await import('/tests/helpers.mjs');
settle(game); game.go('polisportiva'); settle(game);
game.act('talk', 'barondeduke'); settle(game);
game.choose(game.options().findIndex(o => o.text.includes('muro del Sanpierino'))); settle(game);
});
await page.waitForFunction(() => game.closeup?.id === 'muro_sanpierino' && game.closeup.age > 1.2, null, { timeout: 15000 });
await page.screenshot({ path: `${out}/1-telefonino.png` });
const box = await page.locator('#scene').boundingBox(), k = box.width / 640;
const click = ([x, y]) => page.mouse.click(box.x + x * k, box.y + y * k);
await click([320, 180]);
await page.waitForFunction(() => game.closeup.step === 'site', null, { timeout: 5000 });
await page.waitForTimeout(300);
await page.screenshot({ path: `${out}/2-sito.png` });
const link = await page.evaluate(async () => { const t = await import('/src/engine/telefono.js'); const d = story => story.closeups.muro_sanpierino; const def = d(game.story); const [x, y, w, h] = t.linkBox(def, def.siteSize); return [x + w / 2, y + h / 2]; });
await click(link);
await page.waitForFunction(() => game.closeup.step === 'wall', null, { timeout: 8000 });
// Il muro è la pagina vera: i messaggi del gioco in cima, sotto quelli del 2007; «Scrivi» spento, niente email.
const frame = page.frameLocator('#muro');
await frame.locator('.name').first().waitFor({ timeout: 10000 });
assert.match(await frame.locator('.name').first().textContent(), /Il Regolo/);
assert.ok((await frame.locator('.name').allTextContents()).some(t => t.includes('Kiara')), 'i messaggi veri del 2007');
assert.match(await frame.locator('.pagerbutton', { hasText: 'SCRIVI' }).first().getAttribute('class'), /disabled/);
assert.equal(await frame.locator('a[href^="mailto:"]').count(), 0);
await page.waitForTimeout(500);
await page.screenshot({ path: `${out}/3-muro.png` });
// La storia va avanti: «Aggiorna» fa comparire il messaggio nuovo.
await page.evaluate(() => { game.speech = null; game.closeup.lines = []; game.state.flags.indizio_regolo = true; });
await frame.locator('.pagerbutton', { hasText: 'Aggiorna' }).first().click();
await page.waitForTimeout(800);
assert.match(await frame.locator('.name').first().textContent(), /Uno che passava/);
await page.screenshot({ path: `${out}/4-aggiornato.png` });
// «Home» torna alla home del sito sul telefonino.
await frame.locator('.pagerbutton', { hasText: 'Home' }).first().click();
await page.waitForFunction(() => game.closeup.step === 'site', null, { timeout: 5000 });
await page.waitForSelector('#muro', { state: 'detached', timeout: 3000 });
assert.deepEqual(errors, []);
console.log(`ok: schermate in ${out}`);
} finally { await browser?.close(); server.kill(); }
+7 -5
View File
@@ -13,7 +13,8 @@ const voci = JSON.parse(readFileSync(new URL('../tools/voci.json', import.meta.u
test('le battute si trovano ovunque: introduzione, scene, oggetti, dialoghi e rami if', () => {
const lines = voiceLines(story), has = (who, part) => lines.some(l => l.who === who && l.text.includes(part));
assert.ok(has('andrea', 'Fine maggio 2007'), 'introduzione');
assert.ok(has('andrea', 'Il palio è sparito!'), 'ingresso in una scena, dentro un if');
assert.ok(has('vecchietto', 'Il palio è sparito!'), 'primo piano del risveglio, a occhi aperti');
assert.ok(has('andrea', 'che botta'), 'primo piano del risveglio, mentre gli occhi si aprono');
assert.ok(has('vecchietta', 'cadde dal cielo'), 'dialogo, con chi parla');
assert.ok(has('castello', 'vicino alle sue impronte'), 'ramo else annidato');
assert.ok(has('andrea', 'pagina sola'), 'azione di un oggetto');
@@ -61,15 +62,16 @@ test('il castello e il leone hanno voci diverse da tutti gli altri', () => {
});
test('il fumetto resta a schermo almeno quanto la voce', () => {
const g = new Game(story, { voiceTime: (who, text) => (text.startsWith('Il palio') ? 9 : undefined) });
// La prima battuta della partita è quella del risveglio, mentre gli occhi si aprono.
const g = new Game(story, { voiceTime: (who, text) => (text.startsWith('Mmmh') ? 9 : undefined) });
g.go(story.start.scene, story.start.at);
g.update(0.05);
assert.ok(g.speech.text.startsWith('Il palio'));
for (let i = 0; i < 40 && !g.speech; i++) g.update(0.05);
assert.ok(g.speech.text.startsWith('Mmmh'));
assert.ok(g.speech.left > 9, 'la voce lunga allunga il fumetto');
settle(g);
const quiet = new Game(story);
quiet.go(story.start.scene, story.start.at);
quiet.update(0.05);
for (let i = 0; i < 40 && !quiet.speech; i++) quiet.update(0.05);
assert.ok(quiet.speech.left < 9, 'senza voce vale la durata di lettura');
});
+80
View File
@@ -0,0 +1,80 @@
#!/usr/bin/env python3
"""Il Muro del Sanpierino dentro il gioco (Andrea, 31 ottobre): dal dump del muro vero riesumato
(~/Workspaces/misc/spswall) prepara assets/muro/ per la pagina statica del telefonino del Barone.
- messaggi.json: i messaggi veri (soprannome, data, testo), con gli accenti rimessi a posto. Niente email né IP.
Solo quelli del dump (i primi 24, novembre 2007): i 654 ritrovati nella Wayback Machine restano nel muro riesumato
(spswall/archivio/importa.py), fuori dal gioco (Andrea, 31 ottobre).
- img/: solo gli smile usati e le icone della pagina; muro.css è il CSS originale; le due pagine di testo, com'erano.
python3 tools/muro.py [percorso del dump] [cartella www del muro]
"""
import json, re, shutil, sys
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
SPS = Path.home() / 'Workspaces/misc/spswall'
DUMP = Path(sys.argv[1]) if len(sys.argv) > 1 else SPS / 'db/01-spswall.sql'
WWW = Path(sys.argv[2]) if len(sys.argv) > 2 else SPS / 'www'
OUT = ROOT / 'assets/muro'
# Le icone della pagina, oltre agli smile.
PAGE = ['logo_trasparente_obliquo.gif', 'spacer.gif', 'pagefirst.gif', 'pageback.gif', 'postmessage.gif', 'refresh.gif',
'home.gif', 'pageahead.gif', 'pagelast.gif', 'logomanzolo.jpg']
def rows(sql, table):
"""Le righe di INSERT INTO `table` del dump di phpMyAdmin, come liste di valori."""
m = re.search(r'INSERT INTO `%s` \(([^)]*)\) VALUES\s*(.*?);\n' % table, sql, re.S)
cols = [c.strip(' `') for c in m.group(1).split(',')]
body, out, row, val, i, quoted = m.group(2), [], [], '', 0, False
while i < len(body):
ch = body[i]
if quoted:
if ch == '\\': val += {'n': '\n', 'r': '\r', 't': '\t', '0': ''}.get(body[i + 1], body[i + 1]); i += 2; continue
if ch == "'" and body[i + 1:i + 2] == "'": val += "'"; i += 2; continue
if ch == "'": quoted = False
else: val += ch
elif ch == "'": quoted = True
elif ch == '(': row, val = [], ''
elif ch == ',': row.append(val.strip()); val = ''
elif ch == ')': row.append(val.strip()); out.append(dict(zip(cols, row)))
elif not ch.isspace(): val += ch
i += 1
return out
def fix(text):
"""Il muro scriveva UTF-8 su una connessione latin1: nel dump gli accenti sono «Ã¨». Si rimettono a posto."""
for enc in ('latin1', 'cp1252'):
try: return text.encode(enc).decode('utf8')
except (UnicodeEncodeError, UnicodeDecodeError): pass
return text
def main():
sql = DUMP.read_text('utf8')
smiles = {r['codsml']: r['linksml'] for r in rows(sql, 'tpsmiles')}
# Le immagini prese da fuori (http://...) non si caricano: il vecchio dominio oggi può essere di chiunque.
outside = lambda t: re.sub(r'<img[^>]*src="?http[^>]*>', '', t, flags=re.I)
messages = [{'nick': fix(r['nikute']), 'date': r['datmsg'], 'text': outside(fix(r['desmsg']))} for r in rows(sql, 'tpmsgute')]
messages.sort(key=lambda m: m['date'], reverse=True)
# Gli smile usati: dai messaggi veri e da quelli del gioco (story.closeups.muro_sanpierino.messages).
ours = (ROOT / 'src/story/story.js').read_text('utf8')
used = {code: link for code, link in smiles.items() if code in ours or any(code in m['text'] or code in m['nick'] for m in messages)}
shutil.rmtree(OUT / 'img', ignore_errors=True)
(OUT / 'img').mkdir(parents=True)
pictures = {n for m in messages for n in re.findall(r'src="\./img/([^"]+)"', m['text'] + m['nick'])}
for name in PAGE + sorted(set(used.values()) | pictures):
shutil.copy(WWW / 'imgwall' / name, OUT / 'img' / name)
shutil.copy(WWW / 'class/thewall.css', OUT / 'muro.css')
# Le due pagine di testo, com'erano; in fondo un link per tornare al muro (nel telefonino non c'è il «Indietro»).
for page in ('Responsabilita.htm', 'guida_al_muro_del_sanpierino.htm'):
html = (WWW / page).read_bytes()
back = b'<p align="center"><a href="index.html" style="font: bold 14px Tahoma; color: #800000">&laquo; Torna al Muro</a></p>'
(OUT / page).write_bytes(re.sub(rb'</body>', back + b'</body>', html, count=1, flags=re.I))
(OUT / 'messaggi.json').write_text(json.dumps({'smiles': used, 'messages': messages}, ensure_ascii=False, indent=1) + '\n')
print(f'{len(messages)} messaggi, {len(used)} smile -> {OUT.relative_to(ROOT)}')
if __name__ == '__main__':
main()

Some files were not shown because too many files have changed in this diff Show More