Briscola all'oratorio: alle tavolate il giocatore di carte (segnaposto, sprite di Codex) sfida Andrea a briscola in due. Si sceglie il mazzo, toscane (cuori, mattoni, fiori, picche; gobbo, donna e regio) o napoletane, e il livello: «Alla bona», «Seria», «A fassi male» (conta le carte uscite, finale perfetto a mazzo finito, mai la mano altrui). Regole fedeli, conteggio delle prese a fine partita, rivincita, suoni di carte, comandi con mouse, tocco e tastiera. Carte e dorso col Regolo serpentello disegnati da Codex; motore e prove nel browser rivisti da Claude

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A1dxvtsZx8NE9fwjXUNDBr
This commit is contained in:
2026-10-26 13:56:39 +01:00
co-authored by Claude Opus 5.5
parent 2362fc7741
commit 92e7a289be
41 changed files with 1361 additions and 34 deletions
+63 -9
View File
@@ -10,6 +10,8 @@ import { Soundscape } from './engine/soundscape.js';
import { soundscapeFor } from './story/soundscapes.js';
import * as bocce from './engine/bocce.js';
import * as biliardo from './engine/biliardo.js';
import * as briscola from './engine/briscola.js';
import { cardAt } from './engine/briscola-render.js';
import { VERSION, versioned } from './version.js';
const $ = id => document.getElementById(id);
@@ -399,6 +401,12 @@ canvas.addEventListener('pointerdown', e => {
if (!running || e.button > 0) return;
// Nel primo piano un tocco sfoglia (o salta la battuta): gli oggetti della scena dietro non si toccano.
if (game.closeup) {
if (story.closeups[game.closeup.id].type === 'briscola') {
if (game.skip()) return;
press = { card: cardAt(game.closeup, scenePoint(e)), state: game.closeup };
canvas.setPointerCapture(e.pointerId);
return;
}
const aimed = game.closeupAim(scenePoint(e));
if (story.closeups[game.closeup.id].type === 'biliardo') {
if (!aimed && !game.speech) return;
@@ -415,6 +423,11 @@ canvas.addEventListener('pointerdown', e => {
});
canvas.addEventListener('pointerup', e => {
if (!press) return;
if (press.card !== undefined) {
const p = press; press = null;
if (running && game.closeup === p.state && p.card >= 0 && cardAt(game.closeup, scenePoint(e)) === p.card) game.closeupPlay(p.card);
return;
}
if (press.closeup) { press = null; if (running && !game.closeupRelease() && !game.skip()) game.closeupTap(); return; }
clearTimeout(press.timer);
// Il secondo clic vicino al primo, entro poco, è un doppio clic: stesso comando, al doppio della velocità.
@@ -427,7 +440,7 @@ canvas.addEventListener('pointerup', e => {
press = null;
});
canvas.addEventListener('pointercancel', () => { if (press) clearTimeout(press.timer); press = null; game?.closeupCancel(); });
canvas.addEventListener('lostpointercapture', () => { if (press?.closeup) { press = null; game?.closeupCancel(); } });
canvas.addEventListener('lostpointercapture', () => { if (press?.closeup || press?.card !== undefined) { press = null; game?.closeupCancel(); } });
canvas.addEventListener('contextmenu', e => {
e.preventDefault();
if (running && !game.closeup) command.clickScene(scenePoint(e), { look: true });
@@ -443,19 +456,24 @@ canvas.addEventListener('pointermove', e => {
});
canvas.addEventListener('pointerleave', () => { mouse = null; });
// Comandi dei minigiochi (bocce e biliardo) separati dai verbi dell'avventura: mira a passi, forza regolabile e
// lancio con un solo clic, anche per chi non vuole usare la pressione temporizzata.
// Comandi dei minigiochi separati dai verbi: carte oppure mira, forza e lancio.
const MINI = {
briscola: { ...briscola, back: 'Torna all’oratorio', label: 'Briscola: tocca una carta per giocarla. Frecce o 1, 2, 3 scelgono; Invio gioca. Esc esce.' },
bocce: { ...bocce, nudge: (c, dir) => bocce.aim(c, c.aim.u, c.aim.w + dir * 0.3), back: 'Torna al pallaio', label: 'Bocce: tocca il campo o il dettaglio per mirare. Regola la forza e premi Lancia.' },
biliardo: { ...biliardo, back: 'Torna al bar', label: 'Biliardo: clic o tocco fissa la mira. Frecce per affinarla, Maiusc per passi più piccoli. Regola la forza e premi Tira.' },
};
const miniState = () => running && game?.closeup && MINI[story.closeups[game.closeup.id].type] ? game.closeup : null;
const mini = () => MINI[story.closeups[game.closeup.id].type];
$('bocce-power').addEventListener('input', e => { const c = miniState(); if (c) mini().setPower(c, Number(e.target.value) / 100); });
$('bocce-throw').addEventListener('click', () => { const c = miniState(); if (c && !game.speech) mini().shoot(c); });
$('bocce-power').addEventListener('input', e => { const c = miniState(); if (c) mini().setPower?.(c, Number(e.target.value) / 100); });
$('bocce-throw').addEventListener('click', () => { const c = miniState(); if (c && !game.speech) mini().shoot?.(c); });
for (let i = 0; i < 3; i++) {
const button = $('briscola-card-' + i);
button.addEventListener('click', () => game?.closeupPlay(i));
button.addEventListener('focus', () => { if (game?.closeup?.id === 'briscola') briscola.select(game.closeup, i); });
}
for (const [id, dir] of [['bocce-left', -1], ['bocce-right', 1]])
$(id).addEventListener('click', () => { const c = miniState(); if (c && mini().nudge(c, dir)) c.aimLocked = true; });
addEventListener('blur', () => game?.closeupCancel());
addEventListener('blur', () => { if (press?.card !== undefined) press = null; game?.closeupCancel(); });
// --- Verbi.
for (const verb of VERBS) {
@@ -542,6 +560,26 @@ window.addEventListener('keydown', e => {
if (!$('map').hidden) { if (e.key === 'Escape' || e.key === 'p' || e.key === 'P') $('map').hidden = true; return; }
// Nel primo piano: Invio, spazio o freccia sfogliano (o saltano la battuta), Esc chiude il libro.
if (running && game.closeup) {
if (story.closeups[game.closeup.id].type === 'briscola') {
const inControls = e.target.closest?.('#briscola-controls');
if (e.repeat && ['Enter', ' '].includes(e.key)) { e.preventDefault(); return; }
if (inControls && e.target.tagName === 'SUMMARY' && e.key === 'Enter') return;
if (inControls && e.target.tagName === 'BUTTON' && !e.target.disabled && e.key === 'Enter' && briscola.canPlay(game.closeup)) return;
if (['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', '1', '2', '3', 'Enter', ' '].includes(e.key)) {
e.preventDefault();
if (e.repeat || game.skip()) return;
const c = game.closeup;
if (/^[123]$/.test(e.key)) briscola.select(c, Number(e.key) - 1);
else if (e.key.startsWith('Arrow')) briscola.nudge(c, ['ArrowLeft', 'ArrowUp'].includes(e.key) ? -1 : 1);
else if (c.step === 'result') game.closeupClose();
else if (e.key === 'Enter') game.closeupPlay();
if (inControls && briscola.canPlay(c)) $('briscola-card-' + c.selected).focus();
return;
}
if (e.key === 'Escape') game.closeupClose();
else if (e.key.toLowerCase() === 'm') toggleMute();
return;
}
if (miniState()) {
if (e.target.closest?.('#bocce-controls') && ['BUTTON', 'SUMMARY'].includes(e.target.tagName)) return;
const c = game.closeup, m = mini();
@@ -628,9 +666,24 @@ function updateUi() {
updateChoices();
updateSpeech();
const mg = miniState() && mini(), def = mg && story.closeups[c.id];
$('bocce-controls').hidden = !mg;
const cards = def?.type === 'briscola';
$('bocce-controls').hidden = !mg || cards;
$('bocce-controls').inert = !running;
if (mg) {
$('briscola-controls').hidden = !cards;
$('briscola-controls').inert = !running;
if (cards) {
$('panel').hidden = $('choices').hidden = true;
const line = briscola.status(c, def.names);
if ($('briscola-status').textContent !== line) $('briscola-status').textContent = line;
for (let i = 0; i < 3; i++) {
const button = $('briscola-card-' + i), card = c.hands.player[i];
const label = card ? `${i + 1}. ${briscola.cardName(card)}` : `${i + 1}. —`;
if (button.textContent !== label) button.textContent = label;
button.disabled = !briscola.canPlay(c) || !card || !!game.speech;
button.setAttribute('aria-pressed', String(c.selected === i && !!card));
}
}
if (mg && !cards) {
$('bocce-controls').setAttribute('aria-label', def.type === 'biliardo' ? 'Comandi del biliardo' : 'Comandi delle bocce');
$('panel').hidden = $('choices').hidden = true;
const line = mg.status(c, def.names);
@@ -641,9 +694,9 @@ function updateUi() {
$('bocce-throw').textContent = c.step === 'result' ? 'Finita' : c.step === 'rolling' ? 'In movimento…' : c.current === 'cpu' ? `Tira ${def.names.cpu}…` : def.type === 'bocce' ? 'Lancia' : 'Tira';
$('bocce-help').hidden = def.type !== 'bocce';
$('biliardo-help').hidden = def.type !== 'biliardo';
for (const event of c.events.splice(0)) ambience.play(event.kind, { pan: 0, strength: event.power });
if (!mg.canThrow(c)) c.aimLocked = false; // a ogni nuovo tiro la mira torna libera
}
if (mg) for (const event of c.events.splice(0)) ambience.play(event.kind, { pan: 0, strength: event.power });
// Il pulsante della mappa c'è solo in partita, fuori dai dialoghi, e quando c'è almeno un altro posto dove andare.
$('map-button').hidden = !running || !!game.dialogue || !!game.closeup || game.mapPlaces().length < 2;
$('pause-button').hidden = !running;
@@ -655,6 +708,7 @@ function updateUi() {
canvas.classList.toggle('torch', !!torch);
canvas.classList.toggle('bocce', !!mg);
$('stage').classList.toggle('biliardo', !!c && story.closeups[c.id].type === 'biliardo');
$('stage').classList.toggle('briscola', !!cards);
canvas.setAttribute('aria-label', torch ? 'Fessura: sposta la luce con mouse, dito o frecce. Invio cerca il prossimo dettaglio; Esc esce.' : mg ? mg.label : "La scena dell'avventura");
// La pagina che comincia a girare fa il suo fruscio.
const step = game.closeup?.step ?? null;