diff --git a/src/main.js b/src/main.js index b4e85f2..aa1d19f 100644 --- a/src/main.js +++ b/src/main.js @@ -457,6 +457,7 @@ canvas.addEventListener('pointerdown', e => { if (!aimed && !game.speech) return; if (aimed) game.closeup.aimLocked = true; // il clic fissa la mira } + if (story.closeups[game.closeup.id].type === 'bocce' && aimed) game.closeup.aimLocked = true; press = { closeup: true }; // Alle bocce si tiene premuto per caricare la forza (la battuta in corso, un tocco la salta). if (!game.skip()) game.closeupPress(); @@ -503,10 +504,11 @@ canvas.addEventListener('contextmenu', e => { canvas.addEventListener('pointermove', e => { if (!running) return; mouse = e.pointerType === 'mouse' ? scenePoint(e) : null; - // Al biliardo la stecca segue il mouse finché un clic (o una regolazione fine) non fissa la mira: dopo, - // attraversare il tavolo per raggiungere «Tira» non cambia il colpo. Al tiro successivo torna libera. - const billiards = game.closeup && story.closeups[game.closeup.id].type === 'biliardo'; - if (billiards) { if (e.pointerType === 'mouse' && !press && !game.closeup.aimLocked) game.closeupAim(scenePoint(e)); } + // Al biliardo e alle bocce la mira segue il mouse finché un clic (o una regolazione fine) non la fissa: dopo, + // attraversare il campo per raggiungere la forza, «Tira» o «Lancia» non cambia il colpo. Al tiro dopo torna libera. + // (Bocce: scoperto girando il video «Come si gioca», la mira finiva a due passi andando verso il cursore.) + const aiming = game.closeup && ['biliardo', 'bocce'].includes(story.closeups[game.closeup.id].type); + if (aiming) { if (e.pointerType === 'mouse' && !press && !game.closeup.aimLocked) game.closeupAim(scenePoint(e)); } else if (e.pointerType === 'mouse' || press?.closeup) game.closeupAim(scenePoint(e)); }); canvas.addEventListener('pointerleave', () => { mouse = null; }); diff --git a/tests/browser.mjs b/tests/browser.mjs index 6e7f82a..fd30cd8 100644 --- a/tests/browser.mjs +++ b/tests/browser.mjs @@ -387,6 +387,12 @@ try { await bocce.mouse.up(); assert.equal(await bocce.evaluate(() => game.closeup.balls.length), 1, 'lasciando, la boccia parte'); await bocce.waitForFunction(() => game.closeup.balls.length === 2, null, { timeout: 15000 }); + // Il clic fissa la mira: andando verso la forza o «Lancia» il puntatore attraversa il campo e non la sposta. + await bocce.waitForFunction(() => game.closeup.step === 'aim' && game.closeup.current === 'player', null, { timeout: 30000 }); + await bocce.mouse.click(r.x + r.width * 0.45, r.y + r.height * 0.6); + const fixed = await bocce.evaluate(() => JSON.stringify(game.closeup.aim)); + await bocce.mouse.move(r.x + r.width * 0.5, r.y + r.height * 0.95, { steps: 8 }); + assert.equal(await bocce.evaluate(() => JSON.stringify(game.closeup.aim)), fixed, 'la mira resta dove si è cliccato'); await bocce.keyboard.press('Escape'); await bocce.waitForFunction(() => game.speech?.text.startsWith('Già stanco?'), null, { timeout: 5000 }); await bocce.close(); diff --git a/video/sovrapposte.py b/video/sovrapposte.py new file mode 100644 index 0000000..2c16944 --- /dev/null +++ b/video/sovrapposte.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python3 +"""Controlla che nel video «Come si gioca» il narratore non parli sopra i personaggi del gioco. + python3 video/sovrapposte.py tutorial-briscola (dopo la ripresa: cues.json, registro audio, narrazione) +""" +import json +import sys +from pathlib import Path + +VIDEO = Path(__file__).resolve().parent +clip = sys.argv[1] +cues = json.loads((Path.home() / "Workspaces/qemu/qemu-iso-lab/artifacts/tour/out" / clip / "cues.json").read_text())["cues"] +narr = {v["id"]: v["sec"] for v in json.loads((VIDEO / "out" / f"{clip}-narrazione.json").read_text())["voci"]} +voci, aperta = [], None +for e in json.loads((VIDEO / "out" / f"{clip}-audio.json").read_text())["eventi"]: + if "voice" in e: + aperta = [e["t"], None, e["voice"]] + voci.append(aperta) + elif "voiceEnd" in e and aperta: + aperta[1], aperta = e["t"], None +male = [(c["id"], v[2]) for c in cues for v in voci if v[1] and v[0] < c["start"] + narr[c["id"]] and v[1] > c["start"]] +for id_, voce in male: + print(f"✗ «{id_}» sopra {voce}") +print(f"{clip}: {len(voci)} voci del gioco, {len(cues)} frasi del narratore, {len(male)} sovrapposte") +sys.exit(1 if male else 0) diff --git a/video/tutorial-biliardo.mjs b/video/tutorial-biliardo.mjs new file mode 100644 index 0000000..c4fe6e5 --- /dev/null +++ b/video/tutorial-biliardo.mjs @@ -0,0 +1,92 @@ +// Copione del video «Come si gioca: il biliardo» (video/SCALETTA-CARTE.md, clip 5). +// Giro: video/narra.py, poi rec.mjs come in video/SCALETTA.md, poi video/monta.py --tutorial. +import { c, sleep, setup as base, regista } from "./tutorial-comune.mjs"; + +export const title = { it: "Come si gioca · il biliardo", en: "Come si gioca · il biliardo" }; +export const cues = [ + c("dove", "Il biliardo è al Circolo 4 Maggio, in piazza. L'Ingegner Tozzi gioca ai cinque birilli, e si va a venti"), + c("livello", "Anche qui si sceglie quanto deve giocare forte"), + c("tavolo", "Il tavolo dall'alto: la tua biglia, quella dell'avversario, il pallino, e in mezzo il castello dei birilli"), + c("scopo", "Si colpisce la propria biglia e la si manda su quella dell'avversario: i birilli che lei abbatte fanno punti a chi tira"), + c("mira", "La stecca segue il mouse, e un clic fissa la mira. La linea tratteggiata è la strada della biglia; le frecce, dove vanno le due biglie dopo l'urto"), + c("tira", "Con le frecce si gira la stecca, con Maiuscolo ancora più piano. Poi si regola la forza, e si preme Tira"), + c("valori", "Il birillo bianco vale due, il rosso quattro, il rosso da solo dieci. Il pallino toccato dalla biglia avversaria, tre"), + c("fallo", "Attenzione ai falli: mancare la biglia avversaria, toccare prima il pallino, o buttare giù birilli con la propria. I punti vanno all'avversario"), + c("fine", "Un colpo a testa, finché qualcuno arriva a venti. Buona partita!"), +]; + +export const setup = base; + +export async function run(d) { + const p = d.page, r = regista(d, "tutorial-biliardo"); + await r.ascolta(); + await r.vaiA("circolo_4_maggio", [470, 300]); + await r.cue("dove"); + await r.parlaCon("ing_tozzi"); + await r.scegli("una partita ai birilli"); + await r.cue("livello"); + await r.scegli("Alla bona."); + // Una partita fissa: un colpo da otto, un fallo voluto per farlo vedere, poi quattro e tre; vince Andrea 21 a 2. + await p.evaluate(async () => { + const { newBiliardo } = await import("/src/engine/biliardo.js"); + game.closeup = { id: "biliardo", ...newBiliardo(2, { level: "bona" }) }; + }); + await sleep(800); + await r.cue("tavolo"); + await r.muovi([200, 200], 0.9); await sleep(300); await r.muovi([400, 180], 0.9); + await r.cue("scopo"); + + // Il colpo: il migliore fra quelli provati a vuoto (o, la seconda volta, un fallo apposta). Si mira col clic sul + // tavolo lungo la direzione scelta, poi forza col cursore e Tira. + const colpo = (sbaglia) => p.evaluate(async (sbaglia) => { + const L = await import("/src/engine/biliardo.js"), c = game.closeup, b = c.balls.player, o = c.balls.cpu; + let top = null; + if (sbaglia) for (let a = 0; a < Math.PI * 2 && !top; a += 0.05) { if (L.simulate(c, "player", a, 0.12).foul === "miss") top = { angle: a, power: 0.12 }; } + else { + const base = Math.atan2(o.y - b.y, o.x - b.x); + for (let d = -0.35; d <= 0.35; d += 0.01) for (const power of [0.3, 0.4, 0.5, 0.6, 0.7, 0.8]) { + const j = L.simulate(c, "player", base + d, power), gain = j.to === "player" ? j.points : -j.points; + if (!top || gain > top.gain) top = { angle: base + d, power, gain }; + } + } + const x = b.x + Math.cos(top.angle) * 45, y = b.y + Math.sin(top.angle) * 45; + return { ...top, at: [L.VIEW.x + x * L.VIEW.k, L.VIEW.y + y * L.VIEW.k] }; + }, sbaglia); + const tocca = async () => { + for (let i = 0; i < 900; i++) { + const s = await p.evaluate(async () => { + const { canThrow } = await import("/src/engine/biliardo.js"); + return game.closeup.step === "result" ? "fine" : canThrow(game.closeup) && !game.speech ? "tocca" : "aspetta"; + }); + if (s !== "aspetta") return s === "tocca"; + await sleep(150); + } + return false; + }; + let n = 0; + while (await tocca()) { + n++; + if (n === 1) await r.cue("mira"); + if (n === 2) await r.cue("fallo"); + const m = await colpo(n === 2); + await r.muovi(m.at, 0.9); await sleep(400); + await r.clicca(m.at, 0.3); + await p.evaluate(({ angle }) => { game.closeup.angle = angle; game.closeup.aimLocked = true; }, m); + if (n === 1) await r.cue("tira"); + await d.hover(p.locator("#bocce-power"), 0.6); + await p.locator("#bocce-power").fill(String(Math.round(m.power * 100))); + await p.evaluate((v) => { game.closeup.power = v; }, m.power); + await sleep(500); + await d.click(p.locator("#bocce-throw"), 0.6); + await p.locator("#scene").scrollIntoViewIfNeeded(); + if (n === 1) await r.cue("valori"); + } + await p.waitForFunction(() => game.closeup.step === "result", null, { timeout: 120000 }); + await sleep(1500); + await r.cue("fine"); + await r.aspetta(); + await d.click(p.locator("#closeup-close"), 0.6); + await r.fermi(800); + await r.fine(); + await r.salvaAudio(); +} diff --git a/video/tutorial-bocce.mjs b/video/tutorial-bocce.mjs new file mode 100644 index 0000000..2c440f1 --- /dev/null +++ b/video/tutorial-bocce.mjs @@ -0,0 +1,90 @@ +// Copione del video «Come si gioca: le bocce» (video/SCALETTA-CARTE.md, clip 4). +// Giro: video/narra.py, poi rec.mjs come in video/SCALETTA.md, poi video/monta.py --tutorial. +import { c, sleep, setup as base, regista } from "./tutorial-comune.mjs"; + +export const title = { it: "Come si gioca · le bocce", en: "Come si gioca · le bocce" }; +export const cues = [ + c("dove", "Le bocce si giocano al pallaio, accanto al tennis della chiesa. Mariano, il campione, sfida chiunque"), + c("livello", "Si sceglie quanto deve impegnarsi: alla bona, seria, o a fassi male"), + c("campo", "Il campo visto da dietro: in fondo c'è il pallino, e a sinistra il dettaglio, per vedere da vicino chi è più accosto"), + c("mira", "Per mirare si tocca il campo, o il dettaglio vicino al pallino. Il cerchio azzurro dice dove si fermerebbe la boccia"), + c("forza", "Poi si regola la forza col cursore, e si preme Lancia"), + c("turno", "Tira sempre chi non ha il punto. Chi è dietro può accostare, oppure bocciare: colpire la boccia dell'altro per toglierla di lì"), + c("tieni", "C'è anche un altro modo: si tiene premuto sul campo, la forza sale, e lasciando parte la boccia. Vanno bene anche le frecce e la barra spaziatrice"), + c("punti", "Tre bocce a testa. Alla fine fa un punto ogni boccia più vicina al pallino della migliore dell'avversario"), + c("fine", "E se batti Mariano, ti porta a Brento Sanico. Buona partita!"), +]; + +export const setup = base; + +export async function run(d) { + const p = d.page, r = regista(d, "tutorial-bocce"); + await r.ascolta(); + await r.vaiA("tennis_chiesa", [430, 310]); + await r.cue("dove"); + await r.parlaCon("mariano"); + await r.scegli("Facciamo una partita"); + await r.cue("livello"); + await r.scegli("Alla bona."); + // Una mano fissa: accostando bene, Andrea fa tre punti (anche se il tiro a pressione viene un po' corto o lungo). + await p.evaluate(async () => { + const { newBocce } = await import("/src/engine/bocce.js"); + game.closeup = { id: "bocce", ...newBocce(9, { level: "bona" }) }; + }); + await sleep(800); + await p.waitForFunction(() => window.music?.want === "assets/audio/bocce.mp3", null, { timeout: 10000 }); + await r.cue("campo"); + await r.muovi([300, 200], 1.0); await sleep(400); await r.muovi([90, 170], 1.0); + + // Dove mirare: poco prima del pallino, così la boccia si ferma accosto (il cerchio azzurro ci finisce sopra). + const mira = () => p.evaluate(async () => { + const B = await import("/src/engine/bocce.js"), c = game.closeup, u = c.jack.u - 0.55, w = c.jack.w; + const s = B.project(u, w); + return { at: [s.x, s.y], u, w, power: B.powerFor(Math.hypot(u, w) - 0.29) }; + }); + const tocca = async () => { + for (let i = 0; i < 600; i++) { + const s = await p.evaluate(async () => { + const { canThrow } = await import("/src/engine/bocce.js"); + return game.closeup.step === "result" ? "fine" : canThrow(game.closeup) && !game.speech ? "tocca" : "aspetta"; + }); + if (s !== "aspetta") return s === "tocca"; + await sleep(150); + } + return false; + }; + let tiro = 0; + while (await tocca()) { + tiro++; + const m = await mira(); + if (tiro === 1) await r.cue("mira"); + if (tiro < 3) { + // Mira col clic, forza col cursore, poi Lancia. + await r.clicca(m.at, 0.8); + await p.evaluate(async ({ u, w }) => (await import("/src/engine/bocce.js")).aim(game.closeup, u, w), m); + if (tiro === 1) await r.cue("forza"); + await d.hover(p.locator("#bocce-power"), 0.6); + await p.locator("#bocce-power").fill(String(Math.round(m.power * 100))); + await p.evaluate(async (v) => (await import("/src/engine/bocce.js")).setPower(game.closeup, v), m.power); + await sleep(600); + await d.click(p.locator("#bocce-throw"), 0.6); + if (tiro === 1) await r.cue("turno"); + } else { + // L'ultima tenendo premuto: la forza sale finché non si lascia. + await r.cue("tieni"); + await r.muovi(m.at, 0.8); + const [a, b] = await r.punto(...m.at); + d.vm(`DISPLAY=:0 xdotool mousemove ${Math.round(a)} ${Math.round(b)} mousedown 1 sleep ${(m.power * 1.6).toFixed(2)} mouseup 1`); + } + await p.locator("#scene").scrollIntoViewIfNeeded(); + } + await p.waitForFunction(() => game.closeup.step === "result", null, { timeout: 90000 }); + await sleep(1000); + await r.cue("punti"); + await r.cue("fine"); + await r.aspetta(); + await d.click(p.locator("#closeup-close"), 0.6); + await r.fermi(800); + await r.fine(); + await r.salvaAudio(); +} diff --git a/video/tutorial-scopa.mjs b/video/tutorial-scopa.mjs new file mode 100644 index 0000000..e556655 --- /dev/null +++ b/video/tutorial-scopa.mjs @@ -0,0 +1,79 @@ +// Copione del video «Come si gioca: la scopa» (video/SCALETTA-CARTE.md, clip 3). +// Giro: video/narra.py, poi rec.mjs come in video/SCALETTA.md, poi video/monta.py --tutorial. +import { c, sleep, setup as base, regista } from "./tutorial-comune.mjs"; + +export const title = { it: "Come si gioca · la scopa", en: "Come si gioca · la scopa" }; +export const cues = [ + c("dove", "Allo stesso tavolo dell'oratorio si gioca anche a scopa, in due: basta chiedere «Una scopa?»"), + c("mazzo", "Mazzo e livello si scelgono come per la briscola"), + c("valori", "Quattro carte in tavola e tre in mano. L'Asso vale uno, le altre il loro numero, il gobbo otto, la donna nove e il regio dieci"), + c("presa", "Con una carta prendi quella dello stesso valore; se non c'è, un gruppo di carte che fa la stessa somma. Se puoi prendere, devi"), + c("scelta", "Quando le prese possibili sono più d'una, scegli tu: tocca una carta in tavola, oppure usa i pulsanti qui sotto"), + c("scopa", "Se con la tua carta pulisci tutta la tavola, è scopa: un punto in più"), + c("resto", "Finite le tre carte se ne danno altre tre, finché c'è mazzo. Quel che resta in tavola alla fine va all'ultimo che ha preso"), + c("conto", "A fine smazzata un punto a chi ha più carte, uno a chi ha più mattoni, uno per il settebello, il sette di mattoni, e uno per la primiera"), + c("primiera", "La primiera somma la carta migliore di ogni seme: il sette vale ventuno, il sei diciotto, l'Asso sedici"), + c("fine", "Si va agli undici punti, una smazzata dopo l'altra. Buona scopa!"), +]; + +export const setup = base; + +export async function run(d) { + const p = d.page, r = regista(d, "tutorial-scopa"); + await r.ascolta(); + await r.vaiA("oratorio", [470, 300]); + await r.cue("dove"); + await r.parlaCon("giocatore_carte"); + await r.scegli("Una scopa?"); + await r.cue("mazzo"); + await r.scegli("Con le toscane."); + await r.scegli("Seria."); + // Una smazzata fissa: Andrea sceglie fra due prese, fa due scope e la vince 5 a 1. + await p.evaluate(async () => { + const { newScopa } = await import("/src/engine/scopa.js"); + game.closeup = { id: "scopa", ...newScopa(73, { level: "seria", deck: "toscane" }) }; + }); + await sleep(800); + await p.waitForFunction(() => window.music?.want === "assets/audio/carte.mp3", null, { timeout: 10000 }); + await r.cue("valori"); + await r.muovi([320, 160], 1.0); await sleep(500); await r.muovi([320, 300], 1.0); + await r.cue("presa"); + + // Una mossa di Andrea, come la sceglierebbe un giocatore attento; la scelta fra più prese si fa col pulsante. + let k = 1, scelta = false, scopa = false; + for (let i = 0; i < 400; i++) { + const s = await p.evaluate(async () => { + const { canPlay } = await import("/src/engine/scopa.js"); + return ["smazzata", "result"].includes(game.closeup.step) ? "fine" : canPlay(game.closeup) && !game.speech ? "tocca" : "aspetta"; + }); + if (s === "fine") break; + if (s === "aspetta") { await sleep(150); continue; } + const m = await p.evaluate(async (k) => { + const S = await import("/src/engine/scopa.js"), c = game.closeup, m = S.choose(S.observation(c, "player"), "male", k); + const card = c.hands.player[m.index], many = S.captures(c.table, card).length > 1; + const clean = !!m.captured && m.captured.length === c.table.length; + const { handX, HAND } = await import("/src/engine/scopa-render.js"); + return { ...m, many, clean, at: [handX(c.hands.player.length, m.index) + 32, HAND.y + 46] }; + }, k++); + if (m.many && !scelta) { scelta = true; await r.cue("scelta"); } + if (m.clean && !scopa) { scopa = true; await r.cue("scopa"); } + await r.clicca(m.at, 0.7); + if (m.many) { + await p.waitForFunction(() => game.closeup.step === "choose", null, { timeout: 5000 }); + await sleep(900); + await d.click(p.locator(`#scopa-options [data-option="${m.option}"]`), 0.7); + await p.locator("#scene").scrollIntoViewIfNeeded(); + } + if (k === 9) await r.cue("resto"); + } + await p.waitForFunction(() => game.closeup.step === "smazzata", null, { timeout: 60000 }); + await sleep(1200); + await r.cue("conto"); + await r.cue("primiera"); + await r.cue("fine"); + await r.aspetta(); + await d.click(p.locator("#closeup-close"), 0.6); + await r.fermi(800); + await r.fine(); + await r.salvaAudio(); +} diff --git a/video/tutorial-tressette.mjs b/video/tutorial-tressette.mjs new file mode 100644 index 0000000..1265eac --- /dev/null +++ b/video/tutorial-tressette.mjs @@ -0,0 +1,79 @@ +// Copione del video «Come si gioca: il tressette ai 21» (video/SCALETTA-CARTE.md, clip 2). +// Giro: video/narra.py, poi rec.mjs come in video/SCALETTA.md, poi video/monta.py --tutorial. +import { c, sleep, setup as base, regista } from "./tutorial-comune.mjs"; + +export const title = { it: "Come si gioca · il tressette ai 21", en: "Come si gioca · il tressette ai 21" }; +export const cues = [ + c("dove", "Per il tressette ai ventuno servono quattro giocatori: il giocatore di carte chiama due amici, e uno fa da socio ad Andrea"), + c("mazzo", "Mazzo e livello si scelgono come per la briscola"), + c("tavolo", "Il socio è di fronte, gli avversari ai lati. Dieci carte a testa, e si gira verso destra"), + c("accuse", "In alto le accuse: la napoletana, cioè Asso, due e tre dello stesso seme, oppure tre Assi, tre due o tre tre. Valgono punti subito"), + c("seme", "Chi esce gioca quel che vuole; gli altri devono rispondere al seme, se ce l'hanno. Le carte che non si possono giocare diventano scure"), + c("ordine", "Prende la più alta del seme di uscita: il tre, poi il due, l'Asso, il regio, la donna, il gobbo, e giù fino al quattro"), + c("segni", "Chi esce può fare un segno al socio. Si sceglie coi pulsanti qui sotto, o coi tasti B, S e V, e poi si gioca la carta"), + c("striscio", "Striscio vuol dire: di questo seme ne ho ancora, non è l'ultima"), + c("busso", "Busso: prendi con la migliore che hai, e poi ritornami il seme"), + c("volo", "Volo: era l'ultima carta di questo seme"), + c("punti", "L'Asso vale un punto; il tre, il due e le figure un terzo di punto. L'ultima presa vale un punto in più"), + c("smazzata", "A fine smazzata si contano i punti interi, e le accuse si sommano"), + c("fine", "Si gioca smazzata dopo smazzata: la coppia che arriva per prima a ventuno è fuori, e ha vinto. Buon tressette!"), +]; + +export const setup = base; + +export async function run(d) { + const p = d.page, r = regista(d, "tutorial-tressette"); + await r.ascolta(); + await r.vaiA("oratorio", [470, 300]); + await r.cue("dove"); + await r.parlaCon("giocatore_carte"); + await r.scegli("Un tressette ai 21?"); + await r.cue("mazzo"); + await r.scegli("Con le toscane."); + await r.scegli("Seria."); + // Una smazzata fissa: accuse, e Andrea che usa striscio, busso e volo. + await p.evaluate(async () => { + const { newTressette } = await import("/src/engine/tressette.js"); + game.closeup = { id: "tressette", ...newTressette(11, { level: "seria", deck: "toscane" }) }; + }); + await sleep(800); + await p.waitForFunction(() => window.music?.want === "assets/audio/carte.mp3", null, { timeout: 10000 }); + await r.cue("tavolo"); + await r.muovi([320, 60], 0.9); await sleep(300); await r.muovi([600, 150], 0.9); await sleep(300); await r.muovi([40, 150], 0.9); + + let k = 1, detto = new Set(), seme = false, accuse = false; + for (let i = 0; i < 600; i++) { + const s = await p.evaluate(async () => { + const { canPlay } = await import("/src/engine/tressette.js"); + return ["smazzata", "result"].includes(game.closeup.step) ? "fine" : canPlay(game.closeup) && !game.speech ? "tocca" : "aspetta"; + }); + if (s === "fine") break; + if (s === "aspetta") { await sleep(150); continue; } + // Le accuse si dicono giocando la prima carta: se ne parla quando sono scritte in alto. + if (!accuse && await p.evaluate(() => game.closeup.accuse.length > 0)) { accuse = true; await r.cue("accuse"); } + const m = await p.evaluate(async (k) => { + const T = await import("/src/engine/tressette.js"), c = game.closeup, m = T.choose(T.observation(c, 0), "male", k); + const { handX, CARD } = await import("/src/engine/tressette-render.js"); + return { ...m, follow: c.table.length > 0, at: [handX(c.hands[0].length, m.index) + 21, CARD.y + 30] }; + }, k++); + if (m.follow && !seme) { seme = true; await r.cue("seme"); await r.cue("ordine"); } + if (m.signal && !detto.has(m.signal)) { + if (!detto.size) await r.cue("segni"); + detto.add(m.signal); + await r.cue(m.signal); + } + if (m.signal) await d.click(p.locator(`#tressette-${m.signal}`), 0.6); + await p.locator("#scene").scrollIntoViewIfNeeded(); + await r.clicca(m.at, 0.7); + if (k === 7) await r.cue("punti"); + } + await p.waitForFunction(() => ["smazzata", "result"].includes(game.closeup.step), null, { timeout: 60000 }); + await sleep(1200); + await r.cue("smazzata"); + await r.cue("fine"); + await r.aspetta(); + await d.click(p.locator("#closeup-close"), 0.6); + await r.fermi(800); + await r.fine(); + await r.salvaAudio(); +}