Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ca3035e33f | ||
|
|
22be1c306e |
@@ -13,6 +13,8 @@ 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/favicon /usr/share/nginx/html/assets/favicon
|
||||
COPY assets/video /usr/share/nginx/html/assets/video
|
||||
|
||||
# Le immagini e i font citati da index.html e style.css prendono ?v=<versione>, come quelli caricati dal codice.
|
||||
RUN cd /usr/share/nginx/html \
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 68 KiB |
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 126 KiB |
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 81 KiB |
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 79 KiB |
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 82 KiB |
Binary file not shown.
+1
-1
@@ -94,7 +94,7 @@
|
||||
<section id="tutorial" class="overlay" hidden aria-labelledby="tutorial-title">
|
||||
<h2 id="tutorial-title">Come si gioca</h2>
|
||||
<video id="tutorial-video" controls playsinline preload="metadata"></video>
|
||||
<button id="tutorial-close">Torna al gioco</button>
|
||||
<div class="buttons"><button id="tutorial-close">Torna al gioco</button></div>
|
||||
</section>
|
||||
<section id="credits" class="overlay" hidden aria-label="Ringraziamenti">
|
||||
<h2>Ringraziamenti</h2>
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "leggenda-del-regolo",
|
||||
"version": "0.1.14",
|
||||
"version": "0.1.15",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -203,6 +203,14 @@ export const story = {
|
||||
paper: '#fbe6bc',
|
||||
},
|
||||
},
|
||||
// I video «Come si gioca» (video/SCALETTA-CARTE.md): il pulsante «Guarda il video» nell'aiuto di ogni gioco.
|
||||
tutorials: {
|
||||
briscola: { title: 'Come si gioca: la briscola', src: 'assets/video/briscola.mp4', poster: 'assets/video/briscola.jpg' },
|
||||
tressette: { title: 'Come si gioca: il tressette ai 21', src: 'assets/video/tressette.mp4', poster: 'assets/video/tressette.jpg' },
|
||||
scopa: { title: 'Come si gioca: la scopa', src: 'assets/video/scopa.mp4', poster: 'assets/video/scopa.jpg' },
|
||||
bocce: { title: 'Come si gioca: le bocce', src: 'assets/video/bocce.mp4', poster: 'assets/video/bocce.jpg' },
|
||||
biliardo: { title: 'Come si gioca: il biliardo', src: 'assets/video/biliardo.mp4', poster: 'assets/video/biliardo.jpg' },
|
||||
},
|
||||
characters: {
|
||||
// [DA ANDREA: chi è] Figura inventata, nessuna persona reale.
|
||||
giocatore_carte: { name: 'Il giocatore di carte', color: '#edc476', sprite: 'assets/characters/giocatore-carte.json' },
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
// La versione del gioco: la cambia scripts/bump.sh, insieme a package.json.
|
||||
export const VERSION = '0.1.14';
|
||||
export const VERSION = '0.1.15';
|
||||
|
||||
// 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).
|
||||
|
||||
@@ -202,8 +202,8 @@ body.full #sound { position: fixed; z-index: 13; top: max(8px, env(safe-area-ins
|
||||
color: var(--accent); background: rgba(27, 20, 32, .62); border: 0; border-radius: 18px; cursor: pointer; }
|
||||
#pause { z-index: 3; background: rgba(15, 10, 20, .9); }
|
||||
/* Il player dei video «Come si gioca»: sopra a tutto, il video grande quanto può. */
|
||||
#tutorial { z-index: 4; background: rgba(10, 8, 14, .96); padding: 8px; }
|
||||
#tutorial video { max-width: 100%; max-height: calc(100% - 7em); background: #000; }
|
||||
#tutorial { position: fixed; inset: 0; z-index: 20; background: rgba(10, 8, 14, .97); padding: 8px; }
|
||||
#tutorial video { max-width: min(100%, 1100px); max-height: calc(100dvh - 9em); background: #000; }
|
||||
.tutorial-open { display: block; margin: 4px 0 8px; min-height: 36px; padding: 6px 12px; background: #4a3a1c; color: #ffe09b; border: 2px solid #c8a24a; font: inherit; cursor: pointer; }
|
||||
.pause-buttons { flex-direction: column; align-items: stretch; min-width: min(70%, 16em); }
|
||||
#pause .hint { color: var(--dim); max-width: 34em; text-align: center; }
|
||||
@@ -284,7 +284,7 @@ body.full #sound { position: fixed; z-index: 13; top: max(8px, env(safe-area-ins
|
||||
@media (orientation: portrait) and (max-width: 700px) { #rotate { display: block; } }
|
||||
/* Telefono in verticale: la scena è bassa e la Pausa non ci sta. Prende tutto lo schermo, così il cursore, i pulsanti,
|
||||
la spiegazione e la versione non finiscono sopra i verbi. */
|
||||
@media (orientation: portrait) and (max-width: 700px) { #tutorial { position: fixed; inset: 0; z-index: 10; }
|
||||
@media (orientation: portrait) and (max-width: 700px) {
|
||||
#pause { position: fixed; inset: 0; z-index: 10; overflow-y: auto; background: rgba(15, 10, 20, .97); } }
|
||||
/* Schermi stretti: il carattere a pixel è largo, i verbi vanno sopra l'inventario e su due righe da quattro. */
|
||||
@media (max-width: 600px) {
|
||||
|
||||
@@ -91,6 +91,20 @@ try {
|
||||
await page.waitForSelector('#briscola-controls:not([hidden])');
|
||||
// Il tavolo ha la sua musica (il valzer delle carte), non quella dell'oratorio.
|
||||
await page.waitForFunction(() => music.want === 'assets/audio/carte.mp3');
|
||||
if (name === 'desktop-mouse') {
|
||||
// Il video «Come si gioca»: dall'aiuto del tavolo si apre il player, il gioco si ferma, Esc torna al tavolo.
|
||||
await page.click('#briscola-controls summary');
|
||||
await page.click('#briscola-controls .tutorial-open');
|
||||
await page.waitForSelector('#tutorial:not([hidden])');
|
||||
assert.match(await page.locator('#tutorial-video').getAttribute('src'), /assets\/video\/briscola\.mp4\?v=/);
|
||||
assert.equal(await page.locator('#tutorial-title').textContent(), 'Come si gioca: la briscola');
|
||||
assert.equal(await page.evaluate(() => music.hold), true, 'la musica del gioco tace');
|
||||
await page.screenshot({ path: `${out}/briscola-video.png` });
|
||||
await page.keyboard.press('Escape');
|
||||
assert.ok(await page.isHidden('#tutorial'));
|
||||
assert.equal(await page.evaluate(() => !music.hold && game.closeup?.id), 'briscola', 'si torna al tavolo');
|
||||
await page.click('#briscola-controls summary');
|
||||
}
|
||||
assert.ok(await page.isHidden('#bocce-controls'));
|
||||
const scene = await page.locator('#scene').boundingBox(), exit = await page.locator('#closeup-close').boundingBox();
|
||||
assert.ok(exit.y >= scene.y + scene.height, 'uscita fuori dal tavolo');
|
||||
|
||||
@@ -606,6 +606,19 @@ test('al Trebbio Andrea ci sale a piedi: la torre lo sa, e lui ha trovato la sua
|
||||
assert.ok(settle(g).some(s => s.who === 'castello' && s.text === 'Il castello.'));
|
||||
});
|
||||
|
||||
// Il favicon c'era in pagina ma non nell'immagine Docker (404 online, 26 ottobre): ogni cartella di assets che il gioco
|
||||
// usa deve essere fra quelle copiate, e ogni video «Come si gioca» deve esistere.
|
||||
test('ogni cartella di assets usata dal gioco finisce nell\'immagine Docker', async () => {
|
||||
const { readdirSync } = await import('node:fs');
|
||||
const root = new URL('../', import.meta.url);
|
||||
const copied = new Set(readFileSync(new URL('Dockerfile', root), 'utf8').match(/^COPY assets\/(\S+)\s/gm).map(l => l.split(' ')[1].slice(7)));
|
||||
const files = ['index.html', 'style.css', ...readdirSync(new URL('src/', root), { recursive: true }).filter(f => f.endsWith('.js')).map(f => 'src/' + f)];
|
||||
const used = new Set(files.flatMap(f => [...readFileSync(new URL(f, root), 'utf8').matchAll(/assets\/([a-z0-9_-]+)\//g)].map(m => m[1])));
|
||||
// raw e generated sono cartelle di lavoro, citate solo nei commenti.
|
||||
assert.deepEqual([...used].filter(d => !copied.has(d) && !['raw', 'generated'].includes(d)).sort(), []);
|
||||
for (const t of Object.values(story.tutorials)) for (const f of [t.src, t.poster]) assert.ok(existsSync(new URL(f, root)), f);
|
||||
});
|
||||
|
||||
test('i file che il Docker copia sono leggibili da tutti (nginx gira con un altro utente)', async () => {
|
||||
const { readdirSync, statSync } = await import('node:fs');
|
||||
const root = new URL('../', import.meta.url);
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Porta i video «Come si gioca» nel gioco (assets/video/, story.tutorials): da quando si arriva nel posto del gioco
|
||||
(la prima battuta del narratore), più piccoli e compressi per il web, con una copertina.
|
||||
|
||||
python3 video/pubblica.py briscola tressette scopa bocce biliardo
|
||||
|
||||
Prima la ripresa e il montaggio (video/SCALETTA-CARTE.md): legge video/out/tutorial-<gioco>.mp4 e il cues.json della
|
||||
ripresa. Il titolo del gioco all'inizio della ripresa non serve, e col ritaglio dei tavoli verrebbe tagliato ai lati.
|
||||
"""
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
VIDEO = Path(__file__).resolve().parent
|
||||
GIOCO = VIDEO.parent
|
||||
RIPRESE = Path.home() / "Workspaces/qemu/qemu-iso-lab/artifacts/tour/out"
|
||||
|
||||
|
||||
def durata(f: Path) -> float:
|
||||
return float(subprocess.run(["ffprobe", "-v", "error", "-show_entries", "format=duration", "-of", "csv=p=0", str(f)],
|
||||
capture_output=True, text=True, check=True).stdout)
|
||||
|
||||
|
||||
for gioco in sys.argv[1:]:
|
||||
ripresa = RIPRESE / f"tutorial-{gioco}"
|
||||
cues = json.loads((ripresa / "cues.json").read_text())["cues"]
|
||||
# Lo stesso scarto di monta.py: la ripresa parte un po' prima del copione.
|
||||
scarto = max(0.0, durata(ripresa / "raw.mkv") - (cues[-1]["end"] + 1.2))
|
||||
inizio = max(0.0, cues[0]["start"] + scarto - 0.4)
|
||||
dentro, fuori = VIDEO / "out" / f"tutorial-{gioco}.mp4", GIOCO / "assets" / "video" / f"{gioco}.mp4"
|
||||
fuori.parent.mkdir(exist_ok=True)
|
||||
subprocess.run(["ffmpeg", "-loglevel", "error", "-y", "-ss", f"{inizio:.2f}", "-i", str(dentro),
|
||||
"-vf", "scale=trunc(iw*0.75/2)*2:-2", "-c:v", "libx264", "-preset", "slow", "-crf", "27", "-pix_fmt", "yuv420p",
|
||||
"-af", "afade=t=in:d=0.4", "-c:a", "aac", "-b:a", "96k", "-movflags", "+faststart", str(fuori)], check=True)
|
||||
# La copertina: un momento al tavolo, a un terzo del video.
|
||||
subprocess.run(["ffmpeg", "-loglevel", "error", "-y", "-ss", f"{durata(fuori) / 3:.1f}", "-i", str(fuori), "-frames:v", "1",
|
||||
"-q:v", "4", str(fuori.with_suffix(".jpg"))], check=True)
|
||||
for f in (fuori, fuori.with_suffix(".jpg")):
|
||||
f.chmod(0o644)
|
||||
print(f"-> {fuori} ({durata(fuori):.0f} s, {fuori.stat().st_size / 1e6:.1f} MB), da {inizio:.1f} s")
|
||||
Reference in New Issue
Block a user