Bocce più vere (lavoro di Codex): mira libera sul campo o nel dettaglio del pallino, forza regolabile senza tempismo, urti fra masse diverse, tira chi non ha il punto, distanze in centimetri, suoni di rotolamento e urto, comandi anche a pulsanti e tastiera; test aggiornati alle nuove regole

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018ks2K99uqCZjAxvy7Rdj1R
This commit is contained in:
2026-10-25 19:26:23 +01:00
co-authored by Claude Opus 5.5
parent a0201fea18
commit 44485cff52
10 changed files with 386 additions and 154 deletions
+41 -4
View File
@@ -8,6 +8,7 @@ import { Music, trackFor } from './engine/music.js';
import { Voices } from './engine/voices.js';
import { Soundscape } from './engine/soundscape.js';
import { soundscapeFor } from './story/soundscapes.js';
import { aim as bocceAim, shoot as bocceShoot, setPower as boccePower, canThrow as bocceReady, status as bocceStatus } from './engine/bocce.js';
import { VERSION, versioned } from './version.js';
const $ = id => document.getElementById(id);
@@ -265,6 +266,7 @@ $('spots-button').addEventListener('click', showSpots);
function openPause() {
if (!running) return;
running = false;
game.closeupCancel();
music.hold = true;
voices.pause();
ambience.configure({ active: false });
@@ -397,9 +399,9 @@ canvas.addEventListener('pointerdown', e => {
// Nel primo piano un tocco sfoglia (o salta la battuta): gli oggetti della scena dietro non si toccano.
if (game.closeup) {
press = { closeup: true };
game.closeupAim(scenePoint(e));
// Alle bocce si tiene premuto per caricare la forza (la battuta in corso, un tocco la salta).
if (!game.skip()) game.closeupPress();
game.closeupAim(scenePoint(e));
canvas.setPointerCapture(e.pointerId);
return;
}
@@ -419,7 +421,8 @@ canvas.addEventListener('pointerup', e => {
}
press = null;
});
canvas.addEventListener('pointercancel', () => { if (press) clearTimeout(press.timer); 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('contextmenu', e => {
e.preventDefault();
if (running && !game.closeup) command.clickScene(scenePoint(e), { look: true });
@@ -431,6 +434,15 @@ canvas.addEventListener('pointermove', e => {
});
canvas.addEventListener('pointerleave', () => { mouse = null; });
// Comandi delle bocce separati dai verbi dell'avventura: forza regolabile e lancio
// con un solo clic, anche per chi non vuole usare la pressione temporizzata.
const bocceState = () => running && game?.closeup?.id === 'bocce' ? game.closeup : null;
$('bocce-power').addEventListener('input', e => { const c = bocceState(); if (c) boccePower(c, Number(e.target.value) / 100); });
$('bocce-throw').addEventListener('click', () => { const c = bocceState(); if (c && !game.speech) bocceShoot(c); });
for (const [id, move] of [['bocce-left', -0.3], ['bocce-right', 0.3]])
$(id).addEventListener('click', () => { const c = bocceState(); if (c) bocceAim(c, c.aim.u, c.aim.w + move); });
addEventListener('blur', () => game?.closeupCancel());
// --- Verbi.
for (const verb of VERBS) {
const b = document.createElement('button');
@@ -516,6 +528,17 @@ 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 (game.closeup.id === 'bocce') {
if (e.target.closest?.('#bocce-controls') && ['BUTTON', 'SUMMARY'].includes(e.target.tagName)) return;
const c = game.closeup;
if (['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].includes(e.key)) {
e.preventDefault();
if (e.key === 'ArrowLeft' || e.key === 'ArrowRight') bocceAim(c, c.aim.u, c.aim.w + (e.key === 'ArrowLeft' ? -0.3 : 0.3));
else boccePower(c, c.power + (e.key === 'ArrowUp' ? 0.01 : -0.01));
return;
}
if (c.step === 'result' && [' ', 'Enter'].includes(e.key)) { e.preventDefault(); game.closeupClose(); return; }
}
if (story.closeups[game.closeup.id].type === 'torch' && ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Enter'].includes(e.key)) {
// Invio cerca il prossimo dettaglio anche per chi non può usare il puntatore.
e.preventDefault();
@@ -590,16 +613,30 @@ function updateUi() {
updateInventory();
updateChoices();
updateSpeech();
const bocce = c?.id === 'bocce';
$('bocce-controls').hidden = !bocce;
$('bocce-controls').inert = !running;
if (bocce) {
$('panel').hidden = $('choices').hidden = true;
const line = bocceStatus(c, story.closeups.bocce.names);
if ($('bocce-status').textContent !== line) $('bocce-status').textContent = line;
$('bocce-power').value = Math.round(c.power * 100);
$('bocce-power-value').value = `${Math.round(c.power * 100)}%`;
for (const id of ['bocce-power', 'bocce-left', 'bocce-right', 'bocce-throw']) $(id).disabled = !bocceReady(c) || c.holding || !!game.speech;
$('bocce-throw').textContent = c.step === 'result' ? 'Mano conclusa' : c.step === 'rolling' ? 'In movimento…' : c.current === 'cpu' ? 'Tira Mariano…' : 'Lancia';
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;
$('spots-button').hidden = !running || !!game.closeup;
$('notebook-button').hidden = !running || !story.notebook || !!game.closeup;
$('closeup-close').hidden = !running || !game.closeup;
$('closeup-close').textContent = torch ? c.step === 'done' ? 'Continua' : 'Torna fuori' : c && story.closeups[c.id].type === 'bocce' ? 'Lascia stare' : 'Chiudi';
$('closeup-close').textContent = torch ? c.step === 'done' ? 'Continua' : 'Torna fuori' : bocce ? c.step === 'result' ? 'Torna al pallaio' : 'Lascia stare' : 'Chiudi';
$('panel').inert = !!c;
canvas.classList.toggle('torch', !!torch);
canvas.setAttribute('aria-label', torch ? 'Fessura: sposta la luce con mouse, dito o frecce. Invio cerca il prossimo dettaglio; Esc esce.' : "La scena dell'avventura");
canvas.classList.toggle('bocce', bocce);
canvas.setAttribute('aria-label', torch ? 'Fessura: sposta la luce con mouse, dito o frecce. Invio cerca il prossimo dettaglio; Esc esce.' : bocce ? 'Bocce: tocca il campo o il dettaglio per mirare. Regola la forza e premi Lancia.' : "La scena dell'avventura");
// La pagina che comincia a girare fa il suo fruscio.
const step = game.closeup?.step ?? null;
if (step === 'turn' && turning !== 'turn') ambience.play('page');