Compare commits

..
9 Commits
Author SHA1 Message Date
manzolo bc19d6be53 Versione 0.1.3 2026-10-25 13:15:45 +01:00
manzoloandClaude Opus 5.5 c54023083a Il Tozzino con una voce da ragazzo (Xavier Hayasaka, più alta)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018ks2K99uqCZjAxvy7Rdj1R
2026-10-25 12:46:01 +01:00
manzoloandClaude Opus 5.5 af6861350d Mappa: filtrando, i posti trovati si vedono (la mappa non resta lunga e scorsa in fondo); il Barone e le sue due donne e due jack; prove nel browser su una porta libera (con la 8080 giravano sul container)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018ks2K99uqCZjAxvy7Rdj1R
2026-10-25 12:40:48 +01:00
manzolo 53da1e48f8 Versione 0.1.2 2026-10-25 11:38:04 +01:00
manzoloandClaude Opus 5.5 019c3110f9 L'Arcu davanti al circolino («E chi lo sposta???»), il Tozzino al cancello del tennis che aspetta Federer, il BaronDeDuke alla polisportiva senza mutande: sprite di Codex, voci e test
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018ks2K99uqCZjAxvy7Rdj1R
2026-10-25 11:27:24 +01:00
manzoloandClaude Opus 5.5 17a4a01f6d make deploy: in locale tiene solo latest e l'immagine appena pubblicata
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018ks2K99uqCZjAxvy7Rdj1R
2026-10-25 10:55:45 +01:00
manzolo fab64bb718 Versione 0.1.1 2026-10-25 10:55:22 +01:00
manzoloandClaude Opus 5.5 877b29a6e3 La versione del gioco nell'angolo del titolo e in fondo alla pausa
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018ks2K99uqCZjAxvy7Rdj1R
2026-10-25 10:52:00 +01:00
manzoloandClaude Opus 5.5 acc437a33c Cartello → titolo: la dissolvenza parte insieme al titolo (prima il CSS sfumava il cartello prima che il codice arrivasse, e si vedeva la piazza); titolo e introduzione con fondo pieno
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018ks2K99uqCZjAxvy7Rdj1R
2026-10-25 10:44:46 +01:00
36 changed files with 412 additions and 18 deletions
+3
View File
@@ -143,6 +143,9 @@ deploy: ## Pubblica l'ultimo commit: test, immagine costruita e provata qui, spe
scp -q $(DEPLOY_TMP)/docker-compose.homeserver.yml $(DEPLOY_HOST):$(DEPLOY_DIR)/docker-compose.yml
docker save $(IMAGE):latest | gzip | ssh $(DEPLOY_HOST) 'gunzip | docker load -q && cd $(DEPLOY_DIR) && docker compose up -d && docker image prune -f >/dev/null'
rm -rf $(DEPLOY_TMP)
@# Sul server il prune toglie l'immagine vecchia; qui restano solo latest e quella appena pubblicata.
@docker image ls $(IMAGE) --format '{{.Tag}}' | grep -v -x -e latest -e $$(git rev-parse --short HEAD) | \
xargs -r -I{} docker image rm -f $(IMAGE):{} >/dev/null
@echo "→ $(DEPLOY_URL) ($$(git log -1 --format='%h %s'))"
deploy-logs: ## Log del container sul server di deploy
+70
View File
@@ -0,0 +1,70 @@
{
"image": "arcu.png",
"height": 120,
"frames": {
"stand": {
"x": 0,
"y": 0,
"w": 42,
"h": 120,
"ax": 20.9,
"ay": 120
},
"blink": {
"x": 44,
"y": 0,
"w": 42,
"h": 120,
"ax": 20.3,
"ay": 120
},
"talk": {
"x": 88,
"y": 0,
"w": 42,
"h": 120,
"ax": 20.9,
"ay": 120
},
"explain": {
"x": 132,
"y": 0,
"w": 66,
"h": 120,
"ax": 24.3,
"ay": 120
},
"laugh": {
"x": 200,
"y": 0,
"w": 46,
"h": 121,
"ax": 17.8,
"ay": 121
},
"point": {
"x": 248,
"y": 0,
"w": 65,
"h": 121,
"ax": 24.9,
"ay": 121
},
"shrug": {
"x": 315,
"y": 0,
"w": 78,
"h": 121,
"ax": 36.9,
"ay": 121
},
"arms_crossed": {
"x": 395,
"y": 0,
"w": 43,
"h": 121,
"ax": 19.4,
"ay": 121
}
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

+70
View File
@@ -0,0 +1,70 @@
{
"image": "barondeduke.png",
"height": 120,
"frames": {
"stand": {
"x": 0,
"y": 0,
"w": 47,
"h": 120,
"ax": 21.7,
"ay": 120
},
"blink": {
"x": 49,
"y": 0,
"w": 47,
"h": 120,
"ax": 21.5,
"ay": 120
},
"talk": {
"x": 98,
"y": 0,
"w": 48,
"h": 120,
"ax": 21.8,
"ay": 120
},
"explain": {
"x": 148,
"y": 0,
"w": 61,
"h": 120,
"ax": 23.9,
"ay": 120
},
"laugh": {
"x": 211,
"y": 0,
"w": 53,
"h": 117,
"ax": 26.6,
"ay": 117
},
"cards": {
"x": 266,
"y": 0,
"w": 56,
"h": 118,
"ax": 24.1,
"ay": 118
},
"shrug": {
"x": 324,
"y": 0,
"w": 73,
"h": 117,
"ax": 35.7,
"ay": 117
},
"facepalm": {
"x": 399,
"y": 0,
"w": 53,
"h": 117,
"ax": 27.3,
"ay": 117
}
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

+70
View File
@@ -0,0 +1,70 @@
{
"image": "tozzino.png",
"height": 120,
"frames": {
"stand": {
"x": 0,
"y": 0,
"w": 40,
"h": 120,
"ax": 20.6,
"ay": 120
},
"blink": {
"x": 42,
"y": 0,
"w": 41,
"h": 120,
"ax": 20.5,
"ay": 120
},
"talk": {
"x": 85,
"y": 0,
"w": 41,
"h": 120,
"ax": 20.6,
"ay": 120
},
"explain": {
"x": 128,
"y": 0,
"w": 65,
"h": 120,
"ax": 24.4,
"ay": 120
},
"laugh": {
"x": 195,
"y": 0,
"w": 40,
"h": 120,
"ax": 20.1,
"ay": 120
},
"watch": {
"x": 237,
"y": 0,
"w": 51,
"h": 119,
"ax": 25.5,
"ay": 119
},
"look_far": {
"x": 290,
"y": 0,
"w": 56,
"h": 120,
"ax": 27.9,
"ay": 120
},
"bounce": {
"x": 348,
"y": 0,
"w": 67,
"h": 117,
"ax": 21.1,
"ay": 117
}
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+68
View File
@@ -623,6 +623,38 @@
"file": "andrea-e47b3cebb5.mp3",
"sec": 4.87
},
"andrea|Il BaronDeDuke, col cappello da cowboy. Ha l'aria di uno che aspetta qualcuno, e che spera non lo veda nessuno.": {
"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|La tribuna bianca e rossa. La domenica qui c'è più tifo che al Palio. Quasi.": {
"file": "andrea-a50071085e.mp3",
"sec": 6.84
@@ -767,6 +799,18 @@
"file": "andrea-bb70dea2a6.mp3",
"sec": 2.15
},
"andrea|L'Arcu, il mio amico di sempre. Davanti al circolino, come se ci fosse nato.": {
"file": "andrea-8ddab2d024.mp3",
"sec": 5.79
},
"andrea|Arcu! Ma che è sempre là, Borgo?": {
"file": "andrea-aaeb40d873.mp3",
"sec": 3.42
},
"arcu|E chi lo sposta???": {
"file": "arcu-64735a5925.mp3",
"sec": 0.97
},
"andrea|Il ponte vecchio, coi suoi due archi. Chissà quante piene ha visto passare: io al primo temporale cerco riparo.": {
"file": "andrea-01ec1d03ec.mp3",
"sec": 7.67
@@ -935,6 +979,30 @@
"file": "andrea-f5d55187e8.mp3",
"sec": 5.11
},
"andrea|Il Tozzino, vestito di bianco da capo a piedi, con la racchetta. Guarda l'orologio ogni dieci secondi.": {
"file": "andrea-3c32888a15.mp3",
"sec": 8.1
},
"andrea|Tozzino! Che fai lì davanti al cancello?": {
"file": "andrea-1fe1ee6236.mp3",
"sec": 3.1
},
"tozzino|Aspetto Federer. Oggi si fa una partita.": {
"file": "tozzino-c3898e9bdb.mp3",
"sec": 3.23
},
"andrea|Federer? Roger Federer?": {
"file": "andrea-70f150bdec.mp3",
"sec": 2.67
},
"tozzino|E chi sennò? Ha chiamato ora: è imbottigliato in Bolognese.": {
"file": "tozzino-d51159c364.mp3",
"sec": 4.08
},
"tozzino|Se non arriva, la vinco a tavolino.": {
"file": "tozzino-c06cc01612.mp3",
"sec": 2.06
},
"andrea|La Carza. A maggio c'è poca acqua: d'estate si scendeva a fare le dighe coi sassi.": {
"file": "andrea-91be1de49b.mp3",
"sec": 6.99
Binary file not shown.
Binary file not shown.
Binary file not shown.
+2
View File
@@ -35,6 +35,7 @@
<input id="load-file" type="file" accept=".json,application/json" hidden>
<p id="load-error" class="load-error" hidden role="alert"></p>
<p class="dedication">Al Lele, che mi ha fatto conoscere Monkey Island.</p>
<p class="version"></p>
</section>
<section id="rione" class="overlay" hidden aria-label="Scegli il rione">
<h2>Per chi corri quest'anno?</h2>
@@ -79,6 +80,7 @@
<button id="to-title" class="secondary">Torna al titolo</button>
</div>
<p class="hint">La partita è salvata: dal titolo si riprende con «Continua». Con «Salva su file» la porti su un altro dispositivo e la apri con «Carica».</p>
<p class="version"></p>
</section>
<section id="credits" class="overlay" hidden aria-label="Ringraziamenti">
<h2>Ringraziamenti</h2>
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "leggenda-del-regolo",
"version": "0.1.0",
"version": "0.1.3",
"private": true,
"type": "module",
"scripts": {
+11 -5
View File
@@ -6,7 +6,7 @@ import { render, loadAssets, hasBackground, WIDTH, HEIGHT } from './engine/rende
import { readSave, writeSave, clearSave, readAudio, writeAudio, saveFile, saveFileName, readSaveFile } from './engine/storage.js';
import { Music, trackFor } from './engine/music.js';
import { Voices } from './engine/voices.js';
import { versioned } from './version.js';
import { VERSION, versioned } from './version.js';
const $ = id => document.getElementById(id);
const canvas = $('scene'), ctx = canvas.getContext('2d');
@@ -217,6 +217,7 @@ function filterMap() {
$('map-none').hidden = shown > 0;
$('map-none').textContent = `Nessun posto con «${query.trim()}»`;
drawRoads();
$('map-scroll').scrollTo(0, 0); // i posti rimasti stanno in cima: niente mappa vuota scorsa in fondo
}
$('map-search').addEventListener('input', filterMap);
$('map-search').addEventListener('keydown', e => {
@@ -226,7 +227,11 @@ $('map-search').addEventListener('keydown', e => {
});
// Le strade fra le schede, misurate dopo l'impaginazione (e di nuovo se la finestra cambia misura).
function drawRoads() {
const board = $('map-board'), svg = $('map-roads'), origin = board.getBoundingClientRect();
const board = $('map-board'), svg = $('map-roads');
// Prima si toglie la misura vecchia: il disegno delle strade, grande quanto la mappa intera, la terrebbe lunga
// anche dopo un filtro.
svg.style.width = svg.style.height = '0';
const origin = board.getBoundingClientRect();
const center = id => {
const card = board.querySelector(`[data-scene="${id}"]`);
const r = card && !card.hidden && !card.parentElement.hidden && card.querySelector('img').getBoundingClientRect();
@@ -582,12 +587,12 @@ $('restart').addEventListener('click', () => {
// Dietro il titolo si vede già la piazza.
// --- Cartello d'apertura «Manzolo (Poca) Art Presents»: 5 secondi (un clic o un tasto lo salta), poi il titolo.
// Il titolo compare sotto il cartello quando questo comincia a sfumare (style.css, 4,3 s): dissolvenza incrociata,
// senza intravedere la piazza nel mezzo.
// Il titolo compare sotto il cartello quando questo comincia a sfumare (#splash.out in style.css): dissolvenza
// incrociata, senza intravedere la piazza nel mezzo. I tempi partono da qui, non dal primo disegno della pagina.
const SPLASH_FADE = 4300, SPLASH_END = 5000;
function showSplash() {
let done = false, revealed = false;
const reveal = () => { if (!revealed) { revealed = true; showTitle(); } };
const reveal = () => { if (!revealed) { revealed = true; showTitle(); $('splash').classList.add('out'); } };
const end = () => {
if (done) return;
done = true;
@@ -608,6 +613,7 @@ function showSplash() {
addEventListener('keydown', end, { once: true });
}
for (const el of document.querySelectorAll('.version')) el.textContent = `v${VERSION}`;
game = new Game(story);
command = new Command(game);
showSplash();
+47
View File
@@ -112,6 +112,9 @@ export const story = {
leone: { name: 'Il leone', color: '#f0a070' },
san_pietro: { name: 'San Pietro', color: '#e8e0c8' },
tosca: { name: 'La Tosca', color: '#ffb0d0' },
arcu: { name: 'L\'Arcu', color: '#7fb2ff', sprite: 'assets/characters/arcu.json' },
tozzino: { name: 'Il Tozzino', color: '#c8f5b0', sprite: 'assets/characters/tozzino.json' },
barondeduke: { name: 'Il BaronDeDuke', color: '#e0c090', sprite: 'assets/characters/barondeduke.json' },
},
items: {
squama: {
@@ -974,6 +977,9 @@ export const story = {
walk: [[0, 360], [0, 330], [300, 262], [430, 222], [600, 210], [640, 212], [640, 360]],
depth: [[210, 0.4], [360, 1.15]],
entry: [100, 340],
// Il BaronDeDuke nella discesa verso la polisportiva: aspetta chi gli riporta le mutande perse a poker.
speakers: { barondeduke: [230, 243] },
people: [{ who: 'barondeduke', at: [230, 300], facing: -1, idle: 'stand', talk: ['talk', 'explain'], blink: 'blink', every: 4.6 }],
hotspots: [
{
id: 'campo', name: 'campo secondario', box: [140, 160, 240, 60], at: [330, 254], face: 'up',
@@ -1007,6 +1013,21 @@ export const story = {
{
id: 'al_principale', name: 'indietro, verso il campo principale', box: [0, 330, 640, 30], at: [100, 354], exit: { scene: 'polisportiva_principale', at: [620, 330], face: 'left' },
},
{
id: 'barondeduke', name: 'il BaronDeDuke', box: [212, 245, 36, 55], at: [190, 316], face: 'right', default: 'talk',
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' },
],
},
},
],
},
polisportiva_principale: {
@@ -1248,6 +1269,9 @@ export const story = {
walk: [[0, 360], [0, 335], [60, 300], [400, 280], [640, 250], [640, 360]],
depth: [[250, 1.0], [360, 1.5]],
entry: [320, 330],
// L'Arcu davanti al circolino, accanto alla fontanella (sprite di Codex, 25 ottobre).
speakers: { arcu: [418, 214] },
people: [{ who: 'arcu', at: [418, 292], facing: -1, idle: 'stand', talk: ['talk', 'explain'], blink: 'blink', every: 4.2 }],
hotspots: [
{
id: 'circolo', name: 'il circolino', box: [100, 0, 380, 205], at: [300, 290], face: 'up',
@@ -1293,6 +1317,13 @@ export const story = {
look: [{ say: 'La strada va avanti verso Tagliaferro.' }],
},
},
{
id: 'arcu', name: 'l\'Arcu', box: [400, 216, 38, 76], at: [362, 300], face: 'right', default: 'talk',
actions: {
look: [{ say: 'L\'Arcu, il mio amico di sempre. Davanti al circolino, come se ci fosse nato.' }],
talk: [{ say: 'Arcu! Ma che è sempre là, Borgo?' }, { say: 'E chi lo sposta???', who: 'arcu', pose: 'shrug' }],
},
},
],
},
campomigliaio_ponte: {
@@ -1557,6 +1588,9 @@ export const story = {
walk: [[60, 360], [250, 262], [300, 218], [350, 218], [420, 262], [600, 360]],
depth: [[218, 0.55], [360, 1.65]],
entry: [300, 350],
// Il Tozzino davanti al cancello del tennis, in tenuta, che aspetta Federer.
speakers: { tozzino: [440, 222] },
people: [{ who: 'tozzino', at: [440, 300], facing: -1, idle: 'stand', talk: ['talk', 'explain'], blink: 'blink', every: 3.9 }],
hotspots: [
{
id: 'muro', name: 'casa dell\'ing. Tozzi', box: [0, 120, 200, 140], at: [190, 300], face: 'left',
@@ -1576,6 +1610,19 @@ export const story = {
{
id: 'alla_pieve', name: 'su verso la Pieve', box: [0, 330, 640, 30], at: [300, 354], exit: { scene: 'pieve', at: [95, 250], face: 'right' },
},
{
id: 'tozzino', name: 'il Tozzino', box: [422, 224, 36, 76], at: [392, 312], face: 'right', default: 'talk',
actions: {
look: [{ say: 'Il Tozzino, vestito di bianco da capo a piedi, con la racchetta. Guarda l\'orologio ogni dieci secondi.' }],
talk: [
{ say: 'Tozzino! Che fai lì davanti al cancello?' },
{ say: 'Aspetto Federer. Oggi si fa una partita.', who: 'tozzino', pose: 'explain' },
{ say: 'Federer? Roger Federer?' },
{ say: 'E chi sennò? Ha chiamato ora: è imbottigliato in Bolognese.', who: 'tozzino', pose: 'watch' },
{ say: 'Se non arriva, la vinco a tavolino.', who: 'tozzino', pose: 'look_far' },
],
},
},
],
},
ponticino: {
+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.0';
export const VERSION = '0.1.3';
// 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).
+9 -4
View File
@@ -68,12 +68,15 @@ body { display: flex; justify-content: center; overflow-x: hidden; -webkit-tap-h
.overlay[hidden] { display: none; }
/* Titolo e introduzione: le illustrazioni a pixel netti, con una velatura per leggere il testo. */
#title { justify-content: flex-start; padding-top: 6%; image-rendering: pixelated;
background: none; }
background: #0d0910; } /* fondo pieno: finché l'illustrazione non arriva, sotto non si vede il gioco */
#title .buttons { margin-top: auto; }
#title .dedication { margin-bottom: 2%; text-shadow: 2px 2px 0 #000; }
/* La versione (src/version.js), piccola: nell'angolo del titolo e in fondo alla pausa. */
.version { margin: 0; font-size: 9px; color: var(--dim); opacity: .75; }
#title .version { position: absolute; right: 8px; bottom: 6px; text-shadow: 1px 1px 0 #000; }
#title .subtitle { color: var(--ink); text-shadow: 2px 2px 0 #000; }
#intro { justify-content: flex-start; padding-top: 7%; cursor: pointer; image-rendering: pixelated;
background: none; }
background: #0d0910; } /* fondo pieno: finché l'illustrazione non arriva, sotto non si vede il gioco */
#intro-text { min-height: 4.2em; max-width: 30em; font-size: clamp(11.52px, 1.87vw, 21.6px); font-weight: bold; line-height: 1.3; color: var(--ink);
text-shadow: 2px 0 #000, -2px 0 #000, 0 2px #000, 0 -2px #000; animation: appear .8s ease-out; }
#intro .hint { margin-top: auto; margin-bottom: 40px; color: var(--dim); font-size: clamp(8.64px, 1.08vw, 11.52px); text-shadow: 1px 1px 0 #000; }
@@ -110,8 +113,10 @@ body.full #sound { position: fixed; z-index: 13; top: max(8px, env(safe-area-ins
#title .dedication { font-size: max(9px, calc(var(--u) * 1.5)); }
#intro-text { font-size: max(11px, calc(var(--u) * 2)); }
#title .buttons button { font-size: max(11px, calc(var(--u) * 1.9)); }
#splash { z-index: 11; gap: .15em; cursor: pointer; background: radial-gradient(circle at 50% 45%, #2a2030, #0d0910 70%);
animation: splash-out .7s ease-in 4.3s forwards; }
#splash { z-index: 11; gap: .15em; cursor: pointer; background: radial-gradient(circle at 50% 45%, #2a2030, #0d0910 70%); }
/* La dissolvenza la avvia main.js insieme al titolo: il CSS arriva prima del codice, e con un orologio suo il cartello
sparirebbe prima che il titolo ci sia. */
#splash.out { animation: splash-out .7s ease-in forwards; }
#splash p { margin: 0; max-width: none; font-weight: bold; text-shadow: 3px 3px 0 #000; opacity: 0; }
#splash .splash-name { font-size: clamp(24.48px, 5.76vw, 63.36px); letter-spacing: .14em; text-transform: uppercase; color: var(--ink);
animation: splash-in .9s ease-out .2s forwards; }
+2 -3
View File
@@ -2,14 +2,13 @@
// ritagli dei nuovi effetti, pause degli animali e nessuna modifica alla partita.
// npm run browser:effects — immagini in /tmp/regolo-effects (oppure $OUT).
import { chromium } from 'playwright';
import { spawn } from 'node:child_process';
import { staticServer } from './helpers.mjs';
import { mkdirSync, writeFileSync } from 'node:fs';
import assert from 'node:assert/strict';
const out = process.env.OUT ?? '/tmp/regolo-effects';
mkdirSync(out, { recursive: true });
const port = 9000 + Math.floor(Math.random() * 900);
const server = spawn('python3', ['-m', 'http.server', String(port), '--bind', '127.0.0.1'], { stdio: 'ignore' });
const server = await staticServer(), port = server.port;
let browser;
try {
browser = await chromium.launch({ args: ['--use-gl=angle', '--use-angle=swiftshader', '--enable-unsafe-swiftshader'] });
+30 -3
View File
@@ -2,14 +2,14 @@
// fino alla fine, salvataggio e impaginazione su telefono. Schermate in $OUT (predefinito: /tmp/regolo-shots).
// npm run browser
import { chromium } from 'playwright';
import { spawn } from 'node:child_process';
import { staticServer } from './helpers.mjs';
import { mkdirSync, readFileSync } from 'node:fs';
import assert from 'node:assert/strict';
import { solution, route } from '../src/story/solution.js';
const OUT = process.env.OUT ?? '/tmp/regolo-shots', PORT = 8000 + Math.floor(Math.random() * 900);
const OUT = process.env.OUT ?? '/tmp/regolo-shots';
mkdirSync(OUT, { recursive: true });
const server = spawn('python3', ['-m', 'http.server', String(PORT), '--bind', '127.0.0.1'], { stdio: 'ignore' });
const server = await staticServer(), PORT = server.port;
const browser = await chromium.launch({ args: ['--use-gl=angle', '--use-angle=swiftshader', '--enable-unsafe-swiftshader'] });
const errors = [];
@@ -62,6 +62,17 @@ try {
await page.screenshot({ path: `${OUT}/00b-dissolvenza.png` });
await page.waitForSelector('#splash', { state: 'hidden', timeout: 3000 });
await page.screenshot({ path: `${OUT}/01-titolo.png` });
assert.match(await page.textContent('#title .version'), /^v\d+\.\d+\.\d+$/, 'la versione nell\'angolo del titolo');
// Col codice lento ad arrivare (rete vera) il cartello aspetta il titolo: non sfuma prima per conto suo.
{
const slow = await browser.newPage({ viewport: { width: 1280, height: 860 } });
await slow.route('**/src/main.js', async route => { await new Promise(r => setTimeout(r, 2000)); await route.continue(); });
await slow.goto(`http://127.0.0.1:${PORT}/`);
await slow.waitForSelector('#title:not([hidden])', { timeout: 12000 });
const opacity = await slow.evaluate(() => Number(getComputedStyle(document.getElementById('splash')).opacity));
assert.ok(opacity > 0.9, `il cartello era già sfumato (${opacity}) quando è comparso il titolo`);
await slow.close();
}
// I crediti dal titolo: si aprono, mostrano i nomi, Esc li chiude.
await page.click('#title .open-credits');
assert.match(await page.textContent('#credits'), /Ringraziamenti\s*Al Lele/, 'la dedica al Lele viene per prima');
@@ -265,6 +276,22 @@ try {
await page.keyboard.press('Enter');
assert.ok(await page.isHidden('#map'));
assert.equal(await page.evaluate(() => game.state.scene), 'piazza');
// Con tutti i posti visti e la finestra piccola la mappa si scorre: filtrando da in fondo, i posti trovati si vedono
// (prima la mappa restava lunga quanto quella intera, scorsa su un fondo vuoto).
await page.setViewportSize({ width: 560, height: 340 });
await page.evaluate(() => { for (const id of Object.keys(game.story.scenes)) game.state.flags['visto:' + id] = true; });
await page.click('#map-button');
assert.ok(await page.evaluate(() => { const s = document.getElementById('map-scroll'); s.scrollTop = s.scrollHeight; return s.scrollTop > 0; }), 'la mappa piena si scorre');
await page.fill('#map-search', 'campo');
const found = await page.evaluate(() => {
const s = document.getElementById('map-scroll'), r = s.getBoundingClientRect();
const c = [...document.querySelectorAll('#map-board button')].find(b => !b.hidden && !b.parentElement.hidden).getBoundingClientRect();
return { inside: c.top >= r.top && c.bottom <= r.bottom, scrollTop: s.scrollTop, h: s.scrollHeight, ch: s.clientHeight, top: c.top - r.top, q: document.getElementById('map-search').value };
});
assert.ok(found.inside, `filtrando, il primo posto trovato non si vede: ${JSON.stringify(found)}`);
await page.screenshot({ path: `${OUT}/07d-mappa-ricerca-piccola.png` });
await page.click('#close-map');
await page.setViewportSize({ width: 1280, height: 860 });
const phone = await open({ width: 390, height: 844 });
await phone.click('#splash'); // un clic salta il cartello
+13
View File
@@ -16,3 +16,16 @@ export function settle(game, { maxSteps = 20000 } = {}) {
export function memoryStorage() {
return { data: {}, getItem(k) { return this.data[k] ?? null; }, setItem(k, v) { this.data[k] = String(v); }, removeItem(k) { delete this.data[k]; } };
}
// Il server statico delle prove nel browser, su una porta libera scelta dal sistema: con una porta a caso poteva
// capitare la 8080 del gioco in Docker (make up), e la prova girava sul codice vecchio del container.
export async function staticServer() {
const { spawn } = await import('node:child_process');
const server = spawn('python3', ['-u', '-m', 'http.server', '0', '--bind', '127.0.0.1'], { stdio: ['ignore', 'pipe', 'ignore'] });
const port = await new Promise((resolve, reject) => {
let out = '';
server.stdout.on('data', d => { out += d; const m = out.match(/port (\d+)/); if (m) resolve(Number(m[1])); });
server.on('exit', () => reject(new Error('il server di prova non è partito')));
});
return { port, kill: () => server.kill() };
}
+11
View File
@@ -470,3 +470,14 @@ test('i quattro rioni: ognuno ha la sua riga nell\'introduzione, e le battute su
assert.doesNotMatch(look('pieve', 'piazza', 'colonna'), /noi rossi/);
assert.match(look(null, 'pieve', 'chiesa'), /quelli in bianco/, 'un salvataggio di prima dei rioni funziona');
});
test('gli amici in giro: l\'Arcu al circolino, il Tozzino al cancello del tennis, il BaronDeDuke alla polisportiva', () => {
const g = new Game(story);
settle(g);
const talk = (scene, who) => { g.go(scene); settle(g); g.act('talk', who); return settle(g).map(s => `${s.who ?? 'andrea'}: ${s.text}`); };
assert.deepEqual(talk('campomigliaio_circolo', 'arcu'), ['andrea: Arcu! Ma che è sempre là, Borgo?', 'arcu: E chi lo sposta???']);
assert.ok(talk('carza', 'tozzino').includes('tozzino: E chi sennò? Ha chiamato ora: è imbottigliato in Bolognese.'));
const baron = talk('polisportiva', 'barondeduke');
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.');
});
+4 -1
View File
@@ -6,5 +6,8 @@
"castello": { "speaker": "Damien Black", "filtro": "asetrate=24000*0.9,aresample=24000,atempo=0.95,aecho=0.8:0.7:90|180:0.25|0.15" },
"leone": { "speaker": "Baldur Sanjin", "filtro": "asetrate=24000*0.84,aresample=24000,atempo=1.08" },
"san_pietro": { "speaker": "Wulf Carlevaro", "filtro": "asetrate=24000*0.92,aresample=24000,atempo=0.95,aecho=0.8:0.6:60:0.2" },
"tosca": { "speaker": "Gitta Nikolina" }
"tosca": { "speaker": "Gitta Nikolina" },
"arcu": { "speaker": "Luis Moray" },
"tozzino": { "speaker": "Xavier Hayasaka", "filtro": "asetrate=24000*1.12,aresample=24000,atempo=0.92" },
"barondeduke": { "speaker": "Craig Gutsy" }
}