Schermo intero (pulsante accanto ai volumi e tasto F) e gioco fino a 2560 px; nei minigiochi la scena lascia lo spazio vero dei comandi; flipper: «Solo tavola», sul telefono senza pulsantiera e «Lascia stare» in alto, molla toccando al centro con «TIENI PREMUTO QUI», ponti rifatti da Codex; prove più salde (tressette)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GiECd9RWGMKV7n3K1mdy8T
This commit is contained in:
@@ -349,6 +349,14 @@ versione nuova.
|
||||
`REGOLO_GPU=0/1` forza. Con 6 o 8 prove insieme il giro dura uguale: il limite è la partita intera (243 s, il tempo
|
||||
del gioco, non della grafica). La pipeline dà la GPU ai job (`--gpus all`, runtime NVIDIA di Docker); runner a 6 job.
|
||||
|
||||
- **Schermo intero e flipper (1 novembre sera, Andrea):** pulsante ⛶ accanto ai volumi e tasto F; il gioco non si ferma
|
||||
più a 1280 px (fino a 2560: a 1920×1080 la scena è 1553 px e prende tutta l'altezza). Nei minigiochi la scena lascia
|
||||
sotto lo spazio vero dei comandi (`fitControls` in main.js, `--below`; cresce subito, cala solo oltre 24 px, «Come si
|
||||
gioca» aperto non conta: una frase che va a capo faceva saltare il piano). Al flipper «Solo tavola» nasconde la
|
||||
pulsantiera (ricordato nel browser); sul telefono in orizzontale è nascosta sempre e «Lascia stare» sta in alto. La
|
||||
molla si carica toccando al centro (fascia 256-384), ai lati le palette anche prima del lancio; a pallina ferma la
|
||||
tavola dice «TIENI PREMUTO QUI». Ponti rifatti da Codex (parapetti, portali, luci che seguono la pallina).
|
||||
|
||||
- **La prova del peso (1 novembre, Andrea: «senza GPU scoprivamo anche gli effetti troppo pesanti»):** con la scheda
|
||||
video le prove non vedono più un effetto pesante. `tests/peso.browser.mjs` usa sempre swiftshader e, nel gioco vero,
|
||||
in ogni scena nelle tre fasi e in ogni primo piano (il flipper anche in gioco e nel Regolo del futuro) misura
|
||||
|
||||
@@ -23,12 +23,14 @@
|
||||
<button id="notebook-button" hidden title="Taccuino (N)">Taccuino</button>
|
||||
<button id="spots-button" hidden title="Mostra cosa si può toccare (H)">?</button>
|
||||
<button id="back-button" hidden></button>
|
||||
<button id="table-only" hidden title="Nasconde la pulsantiera: il flipper a tutta altezza (si gioca con mouse e tastiera)">Solo tavola</button>
|
||||
</div>
|
||||
<div id="sound">
|
||||
<button id="mute" aria-label="Togli l'audio" title="Audio (M)">♪</button>
|
||||
<input id="volume" type="range" min="0" max="100" step="5" aria-label="Volume della musica" title="Volume della musica">
|
||||
<label for="voice-volume" class="sound-label" title="Volume delle voci">bla</label>
|
||||
<input id="voice-volume" type="range" min="0" max="100" step="5" aria-label="Volume delle voci" title="Volume delle voci">
|
||||
<button id="fullscreen-hud" aria-label="Schermo intero" title="Schermo intero (F)">⛶</button>
|
||||
</div>
|
||||
<section id="splash" class="overlay" aria-label="Manzolo (Poca) Art Presents">
|
||||
<p class="splash-name">Manzolo</p>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Il Regolo / River: tavolo in lacca scura, metallo e inserti luminosi.
|
||||
// Le coordinate sono quelle della fisica. Le superfici statiche e i bagliori sono in cache a risoluzione doppia.
|
||||
import { R, SCORE, RAMP, FUTURE, layout, flipperSeg, alongRamp, rampHeight, waiting, canThrow, saveLeft, activeBalls, multiball, rampOpen } from './flipper.js';
|
||||
import { R, SCORE, RAMP, FUTURE, PLUNGER_ZONE, layout, flipperSeg, alongRamp, rampHeight, waiting, canThrow, saveLeft, activeBalls, multiball, rampOpen } from './flipper.js';
|
||||
import { ambientHash as hash } from './ambient.js';
|
||||
|
||||
const INK = '#071018', WHITE = '#eff9ff', MUTED = '#7b99aa';
|
||||
@@ -151,7 +151,8 @@ function paintTrack(g, r) {
|
||||
line(g, [[p.x + 5, p.y], [p.x + 7, p.y + 10]], '#050c13', 3);
|
||||
screw(g, p.x + 7, p.y + 10);
|
||||
}
|
||||
const track = (color, width) => {
|
||||
const track = (color, width, points = pts) => {
|
||||
const pts = points;
|
||||
g.beginPath(); g.moveTo(...pts[0]);
|
||||
for (let i = 1; i < pts.length - 1; i++) {
|
||||
const [x, y] = pts[i], a = pts[i - 1], b = pts[i + 1];
|
||||
@@ -165,7 +166,24 @@ function paintTrack(g, r) {
|
||||
track(color + '18', 20); track('#02080e', 16); track(color, 14);
|
||||
track(metal(g, 240, 45, 156, 205), 12); track('#07141e', 10);
|
||||
track(gradient(g, 240, 40, 165, 220, [[0, '#386274'], [0.35, '#152a3b'], [0.7, '#385266'], [1, '#142c3b']]), 8);
|
||||
track(color + '24', 4);
|
||||
track(color + '38', 4);
|
||||
// Parapetti rialzati: montanti e tiranti triangolari fanno leggere il ponte anche da fermo.
|
||||
for (const side of [-1, 1]) {
|
||||
let previous;
|
||||
for (let s = 34; s < r.len - 10; s += 18) {
|
||||
const p = rampPoint(r, s), foot = [p.x + p.dy * 6.5 * side, p.y - p.dx * 6.5 * side];
|
||||
const top = [foot[0], foot[1] - 5];
|
||||
line(g, [foot, top], '#a6c6d2', 0.8);
|
||||
if (previous) line(g, [previous, foot], color + '88', 0.65);
|
||||
disc(g, ...top, 0.9, color); previous = top;
|
||||
}
|
||||
const rail = pts.map(([x, y], i) => {
|
||||
const a = pts[Math.max(0, i - 1)], b = pts[Math.min(pts.length - 1, i + 1)];
|
||||
const len = Math.hypot(b[0] - a[0], b[1] - a[1]) || 1;
|
||||
return [x + (b[1] - a[1]) / len * 6.5 * side, y - (b[0] - a[0]) / len * 6.5 * side - 5];
|
||||
});
|
||||
track('#06121d', 2.6, rail); track(color, 1.3, rail); track('#ffffff99', 0.45, rail);
|
||||
}
|
||||
for (let s = 12; s < r.len - 4; s += 22) {
|
||||
const p = rampPoint(r, s);
|
||||
line(g, [[p.x - p.dy * 4, p.y + p.dx * 4], [p.x + p.dy * 4, p.y - p.dx * 4]], '#7595a344', 0.7);
|
||||
@@ -178,18 +196,50 @@ function paintTrack(g, r) {
|
||||
const pts = [[cx + side * 6, y - 30], [cx + side * (x1 - x0) / 2, y]];
|
||||
line(g, pts, '#07121c', 3); line(g, pts, color, 1); screw(g, cx + side * (x1 - x0) / 2, y);
|
||||
}
|
||||
text(g, r.points ? number(r.points) : r.id === 'super' ? 'SUPER' : '15.000', cx, y - 12, 5, '#c5e0e7', 'center');
|
||||
// Portale d'ingresso in metallo: l'arco e le torrette incorniciano la bocca fisica della rampa.
|
||||
const arch = [[x0 + 2, y - 2], [x0 + 2, y - 17], [cx - 8, y - 27], [cx + 8, y - 27], [x1 - 2, y - 17], [x1 - 2, y - 2]];
|
||||
line(g, arch, '#020910', 5); line(g, arch, metal(g, x0, y - 27, x1 - x0, 27), 3.2); line(g, arch, color, 1);
|
||||
for (const x of [x0 + 2, x1 - 2]) {
|
||||
box(g, x - 2.5, y - 16, 5, 16, '#112735', '#8aaab7', 1);
|
||||
box(g, x - 0.8, y - 13, 1.6, 9, color, null, 0.5);
|
||||
}
|
||||
box(g, cx - 10, y - 31, 20, 8, '#0a1b28', color, 2);
|
||||
crown(g, cx, y - 27, 2.3, color);
|
||||
for (let i = 0; i < 2; i++) line(g, [[cx - 3, y - 5 - i * 5], [cx, y - 8 - i * 5], [cx + 3, y - 5 - i * 5]], color + 'aa', 0.8);
|
||||
text(g, r.points ? number(r.points) : r.id === 'super' ? 'SUPER' : '15.000', cx, y - 18, 4.5, WHITE, 'center');
|
||||
}
|
||||
function rampLights(g, c, r, time) {
|
||||
function trackEnergy(g, c, r, time, hot, balls) {
|
||||
const color = TINT[r.id], riders = balls.filter(b => b.ramp?.id === r.id);
|
||||
const charged = hot || riders.length > 0;
|
||||
// Luci sui bordi: il centro rimane libero per seguire la pallina.
|
||||
for (let k = 0; k < (charged ? 7 : 3); k++) {
|
||||
const p = rampPoint(r, (time * (charged ? 155 : 48) + k * r.len / (charged ? 7 : 3)) % r.len);
|
||||
for (const side of [-1, 1]) {
|
||||
const x = p.x + p.dy * 6.5 * side, y = p.y - p.dx * 6.5 * side - 5;
|
||||
if (charged) glow(g, x, y, 7, color, 0.5);
|
||||
disc(g, x, y, charged ? 1.1 : 0.8, charged ? WHITE : color);
|
||||
}
|
||||
}
|
||||
for (const b of riders) {
|
||||
// La scia segue la distanza percorsa, anche quando un tiro debole ricade indietro.
|
||||
for (let i = 7; i > 0; i--) {
|
||||
const s = b.ramp.s - Math.sign(b.ramp.v) * i * 4;
|
||||
if (s < 0 || s > r.len) continue;
|
||||
const p = rampPoint(r, s);
|
||||
g.save(); g.globalAlpha *= (1 - i / 8) * 0.65;
|
||||
line(g, [[p.x - p.dy * 3, p.y + p.dx * 3], [p.x + p.dy * 3, p.y - p.dx * 3]], color, 2);
|
||||
g.restore();
|
||||
}
|
||||
const p = rampPoint(r, b.ramp.s); glow(g, b.x, p.y, 14, color, 0.8);
|
||||
}
|
||||
const [[x0, y], [x1]] = r.mouth, pulse = 0.45 + Math.sin(time * (charged ? 7 : 2)) * 0.15;
|
||||
for (const x of [x0 + 2, x1 - 2]) glow(g, x, y - 10, 11, color, charged ? 0.85 : pulse);
|
||||
}
|
||||
function rampLights(g, c, r, time, balls) {
|
||||
const color = TINT[r.id], locked = c.locks.some(l => l.rampId === r.id);
|
||||
const combo = !c.locks.length && c.lastRamp && c.lastRamp.id !== r.id && r.id !== 'super';
|
||||
const invited = c.special === r.id || multiball(c), hot = invited || combo || r.lit > 0;
|
||||
if (hot) {
|
||||
for (let k = 0; k < 7; k++) {
|
||||
const p = rampPoint(r, (time * 140 + k * r.len / 7) % r.len);
|
||||
glow(g, p.x, p.y, 10, color, 0.7); disc(g, p.x, p.y, 1.1, WHITE);
|
||||
}
|
||||
}
|
||||
trackEnergy(g, c, r, time, hot, balls);
|
||||
const [[x0, y], [x1]] = r.mouth, x = (x0 + x1) / 2;
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const lit = locked || (c.rampRun.id === r.id && c.rampRun.count > i);
|
||||
@@ -429,9 +479,9 @@ function futureField(g, c, time) {
|
||||
const balls = activeBalls(c);
|
||||
for (const b of balls) if (!b.ramp && b.releaseAt <= c.sim) ball(g, b);
|
||||
for (const r of c.ramps) {
|
||||
cached(g, `track:future:${r.id}`, layer => paintTrack(layer, r), [207, 5, 221, 250]);
|
||||
cached(g, `track:future:${r.id}`, layer => paintTrack(layer, r), [202, 0, 232, 258]);
|
||||
const color = TINT[r.id], x = r.path[0][0], y = r.mouth[0][1];
|
||||
for (let k = 0; k < 4; k++) { const p = rampPoint(r, (time * 120 + k * r.len / 4) % r.len); disc(g, p.x, p.y, 1.3, color); }
|
||||
trackEnergy(g, c, r, time, r.lit > 0 || c.future.hits[r.id] === 2, balls);
|
||||
poly(g, [[x, y + 7], [x - 4, y + 14], [x + 4, y + 14]], color);
|
||||
text(g, r.id.toUpperCase(), x, y + 21, 5.5, color, 'center');
|
||||
for (const b of balls) if (b.ramp?.id === r.id) ball(g, b, color);
|
||||
@@ -505,7 +555,7 @@ export function drawFlipper(g, c, def, bg, glass) {
|
||||
}
|
||||
if (future) {
|
||||
if (cabinetBehind(g, key, paint)) g.clearRect(0, 0, 640, 360); else cached(g, key, paint);
|
||||
futureField(g, c, time); g.restore(); return;
|
||||
futureField(g, c, time); plungerHint(g, c, time); g.restore(); return;
|
||||
}
|
||||
if (shake > 0) { g.fillStyle = INK; g.fillRect(0, 0, 640, 360); g.translate(Math.sin(time * 75) * shake * 1.3, Math.cos(time * 63) * shake); cached(g, key, paint); }
|
||||
else if (cabinetBehind(g, key, paint)) g.clearRect(0, 0, 640, 360);
|
||||
@@ -532,7 +582,7 @@ export function drawFlipper(g, c, def, bg, glass) {
|
||||
if (!rampOpen(c, r)) continue;
|
||||
g.save();
|
||||
if (r.id === 'super') { const rise = clamp((time - c.superOpenedAt) / 0.65, 0, 1); g.globalAlpha = rise; g.translate(0, (1 - rise) * 18); }
|
||||
cached(g, `track:${r.id}`, layer => paintTrack(layer, r), r.id === 'super' ? [213, 50, 138, 202] : [213, 10, 215, 228]); rampLights(g, c, r, time);
|
||||
cached(g, `track:${r.id}`, layer => paintTrack(layer, r), r.id === 'super' ? [208, 45, 148, 207] : [208, 0, 225, 238]); rampLights(g, c, r, time, balls);
|
||||
for (const b of balls) if (b.ramp?.id === r.id) ball(g, b, TINT[r.id]);
|
||||
g.restore();
|
||||
}
|
||||
@@ -542,6 +592,18 @@ export function drawFlipper(g, c, def, bg, glass) {
|
||||
plunger(g, c); effects(g, c, time);
|
||||
// Il riflesso bianco rende visibili le palline vicino alle palette anche durante la festa.
|
||||
for (const b of balls) if (!b.ramp && b.y > 265 && b.releaseAt <= c.sim) ball(g, b);
|
||||
board(g, c, time); mission(g, c, time);
|
||||
board(g, c, time); mission(g, c, time); plungerHint(g, c, time);
|
||||
g.restore();
|
||||
}
|
||||
// A pallina ferma, dove toccare per la molla (Andrea, 1 novembre: «sul telefono non è intuitivo premere al centro»): la
|
||||
// fascia centrale di flipper.js (press), tratteggiata, e la scritta che pulsa; sparisce appena si comincia a caricare.
|
||||
function plungerHint(g, c, time) {
|
||||
if (!waiting(c) || c.holding) return;
|
||||
const x0 = 320 - PLUNGER_ZONE + 4, x1 = 320 + PLUNGER_ZONE - 4, y0 = 196, y1 = 300, pulse = 0.8 + Math.sin(time * 4) * 0.2;
|
||||
g.save(); g.globalAlpha = pulse; g.setLineDash([4, 3]);
|
||||
g.strokeStyle = '#ffe08a'; g.lineWidth = 1.2; g.strokeRect(x0, y0, x1 - x0, y1 - y0); g.setLineDash([]);
|
||||
box(g, 320 - 58, 262, 116, 32, '#07121c', '#ffe08a', 2);
|
||||
text(g, 'TIENI PREMUTO QUI', 320, 272, 8.5, '#ffe08a', 'center', 'sans-serif', 800);
|
||||
text(g, 'e lascia: MOLLA', 320, 285, 7, WHITE, 'center', 'sans-serif', 700);
|
||||
g.restore();
|
||||
}
|
||||
|
||||
@@ -534,10 +534,17 @@ function collideFlipper(c, ball, side) {
|
||||
// --- Comandi: col mouse o col dito, prima del lancio tenere premuto carica la molla e lasciare lancia; in gioco
|
||||
// la metà sinistra della scena alza il flipper sinistro, la destra il destro, finché si tiene premuto (anche due dita).
|
||||
export function aimScreen(c, [x, y]) { if (!Number.isFinite(x) || !Number.isFinite(y)) return false; c.cursor = [x, y]; return true; }
|
||||
// Un tocco (Andrea, 1 novembre: «il pollice destro sulla metà destra, il sinistro sulla sinistra, al centro per lanciare»):
|
||||
// al centro della scena, a pallina ferma, carica la molla; ai lati alza la paletta di quel lato, anche prima del lancio.
|
||||
// La tastiera ('key': Spazio, freccia giù) carica sempre.
|
||||
export const PLUNGER_ZONE = 64; // mezza larghezza della fascia centrale, in pixel di scena (256-384 su 640)
|
||||
export function press(c, pointerId = 'ptr') {
|
||||
if (c.step !== 'play') return false;
|
||||
if (waiting(c)) { if (c.holding) return false; c.holding = true; c.holdTime = 0; c.pointerId = pointerId; c.power = 0; return true; }
|
||||
return hold(c, pointerId === 'mouse-right' || (c.cursor?.[0] ?? 320) >= 320 ? 'right' : 'left', pointerId, true);
|
||||
const x = c.cursor?.[0] ?? 320;
|
||||
if (waiting(c) && pointerId !== 'mouse-right' && (pointerId === 'key' || Math.abs(x - 320) < PLUNGER_ZONE)) {
|
||||
if (c.holding) return false; c.holding = true; c.holdTime = 0; c.pointerId = pointerId; c.power = 0; return true;
|
||||
}
|
||||
return hold(c, pointerId === 'mouse-right' || x >= 320 ? 'right' : 'left', pointerId, true);
|
||||
}
|
||||
export function release(c, pointerId = 'ptr') {
|
||||
if (c.holding && c.pointerId === pointerId) return launch(c, powerAt(c.holdTime));
|
||||
|
||||
+51
-7
@@ -399,16 +399,52 @@ addEventListener('pagehide', () => leave(true));
|
||||
addEventListener('pageshow', () => leave(document.hidden));
|
||||
$('resume').addEventListener('click', closePause);
|
||||
$('pause-map').addEventListener('click', () => { closePause(); openMap(); });
|
||||
// Schermo intero dalla Pausa, dove il browser lo permette (su iPhone no: il pulsante sparisce); Esc o il pulsante per uscire.
|
||||
// I comandi di un minigioco (molla, pulsanti, carte, «Come si gioca») sono più alti del pannello dei verbi: con quelli
|
||||
// aperti la scena lascia intorno lo spazio che occupano davvero, fasce comprese (--below, misurato), così su uno schermo grande o a schermo
|
||||
// intero non finiscono sotto il bordo. Senza comandi vale la riserva del pannello in unità (style.css, #app).
|
||||
// La misura cambia la scena solo per differenze grandi: una frase di stato che va a capo (PONTE SX al flipper) non deve
|
||||
// ridimensionarla a ogni colpo, che a schermo sembra un piano che salta di qualche pixel (Andrea, 1 novembre). E «Come si
|
||||
// gioca» aperto non conta: se lo si apre, la pagina scorre invece di stringere la scena (e poi non torna larga).
|
||||
function fitControls() {
|
||||
const app = $('app'), open = !!document.querySelector('#side > section[id$="-controls"]:not([hidden])');
|
||||
if (!app.classList.toggle('controls', open)) { app.style.removeProperty('--below'); return; }
|
||||
// Tutto quello che non è la scena 16:9: le fasce di audio, pausa e uscita e la pulsantiera (dalla larghezza, non
|
||||
// dalla tela, che mentre cambia risoluzione può avere per un attimo un'altezza qualunque).
|
||||
const box = app.getBoundingClientRect(), help = document.querySelector('#side details[id$="-help"][open]');
|
||||
const extra = help ? help.getBoundingClientRect().height - help.querySelector('summary').getBoundingClientRect().height : 0;
|
||||
const below = Math.ceil(box.height - box.width * 9 / 16 - extra), was = parseFloat(app.style.getPropertyValue('--below'));
|
||||
// Cresce subito (niente deve finire sotto il bordo), cala solo per differenze grandi (niente salti a ogni frase).
|
||||
if (!(below <= was + 1 && below >= was - 24)) app.style.setProperty('--below', `${below}px`);
|
||||
}
|
||||
// «Solo tavola» al flipper (Andrea, 1 novembre): nasconde la pulsantiera, la tavola prende tutta l'altezza; il browser
|
||||
// se lo ricorda (sul telefono in orizzontale la pulsantiera del flipper è già nascosta, style.css).
|
||||
let tableOnly = (() => { try { return storage?.getItem('regolo-solo-tavola') === '1'; } catch { return false; } })();
|
||||
$('table-only').addEventListener('click', e => {
|
||||
tableOnly = !tableOnly;
|
||||
try { storage?.setItem('regolo-solo-tavola', tableOnly ? '1' : '0'); } catch { /* niente memoria: vale per questa volta */ }
|
||||
e.currentTarget.blur();
|
||||
});
|
||||
// Schermo intero, dove il browser lo permette (su iPhone no: i pulsanti spariscono): dalla Pausa, dal pulsante accanto ai
|
||||
// volumi (anche sul titolo) o col tasto F; Esc, il pulsante o F per uscire.
|
||||
function toggleFullscreen() {
|
||||
if (!document.fullscreenEnabled) return false;
|
||||
if (document.fullscreenElement) document.exitFullscreen();
|
||||
else document.documentElement.requestFullscreen({ navigationUI: 'hide' }).catch(() => {});
|
||||
return true;
|
||||
}
|
||||
{
|
||||
const full = $('fullscreen');
|
||||
if (!document.fullscreenEnabled) full.hidden = true;
|
||||
const full = $('fullscreen'), hud = $('fullscreen-hud');
|
||||
if (!document.fullscreenEnabled) full.hidden = hud.hidden = true;
|
||||
else {
|
||||
full.addEventListener('click', () => {
|
||||
if (document.fullscreenElement) document.exitFullscreen();
|
||||
else document.documentElement.requestFullscreen({ navigationUI: 'hide' }).catch(() => {});
|
||||
full.addEventListener('click', toggleFullscreen);
|
||||
hud.addEventListener('click', e => { toggleFullscreen(); e.currentTarget.blur(); });
|
||||
document.addEventListener('fullscreenchange', () => {
|
||||
const on = !!document.fullscreenElement;
|
||||
full.textContent = on ? 'Esci da schermo intero' : 'Schermo intero';
|
||||
hud.setAttribute('aria-label', on ? 'Esci da schermo intero' : 'Schermo intero');
|
||||
hud.title = on ? 'Esci da schermo intero (F o Esc)' : 'Schermo intero (F)';
|
||||
hud.setAttribute('aria-pressed', on);
|
||||
});
|
||||
document.addEventListener('fullscreenchange', () => { full.textContent = document.fullscreenElement ? 'Esci da schermo intero' : 'Schermo intero'; });
|
||||
}
|
||||
}
|
||||
$('to-title').addEventListener('click', () => {
|
||||
@@ -838,6 +874,8 @@ window.addEventListener('keydown', e => {
|
||||
return;
|
||||
}
|
||||
if (debugKey(e)) return;
|
||||
// F: schermo intero ovunque (nessun gioco usa la F; nella ricerca della mappa si scrive, sopra).
|
||||
if ((e.key === 'f' || e.key === 'F') && !e.ctrlKey && !e.metaKey && !e.altKey && !e.repeat && toggleFullscreen()) { e.preventDefault(); return; }
|
||||
if (!$('tutorial').hidden) { if (e.key === 'Escape') closeTutorial(); return; }
|
||||
if (!$('credits').hidden) {
|
||||
if (e.key === 'Escape') { if (!$('face').hidden) $('face').hidden = true; else $('credits').hidden = true; }
|
||||
@@ -1170,6 +1208,12 @@ function updateUi() {
|
||||
// Biliardo e GP di Fortezza: audio, pausa e uscita fuori dal quadro (al GP la pista passa negli angoli).
|
||||
$('stage').classList.toggle('biliardo', !!c && ['biliardo', 'rinquarto', 'gara', 'micro', 'cerbottana', 'condor', 'campana', 'flipper'].includes(story.closeups[c.id].type));
|
||||
$('stage').classList.toggle('briscola', !!cards);
|
||||
const pinball = story.closeups[c?.id]?.type === 'flipper';
|
||||
$('app').classList.toggle('flipper', pinball);
|
||||
$('app').classList.toggle('table-only', pinball && tableOnly);
|
||||
setProp($('table-only'), 'hidden', !pinball);
|
||||
setText($('table-only'), tableOnly ? 'Pulsantiera' : 'Solo tavola');
|
||||
fitControls();
|
||||
setAttr(canvas, '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;
|
||||
|
||||
@@ -20,15 +20,25 @@ html, body { margin: 0; height: 100%; background: var(--bg); color: var(--ink);
|
||||
button, input { font-family: var(--font); }
|
||||
body { line-height: 1.5; }
|
||||
body { display: flex; justify-content: center; overflow-x: hidden; -webkit-tap-highlight-color: transparent; }
|
||||
/* Il gioco è largo quanto può: tutta la finestra fino a 1280px, oppure quanto ci sta in altezza con il pannello sotto la
|
||||
/* Il gioco è largo quanto può: tutta la finestra (fino a 2560px: a schermo intero su un monitor grande), oppure quanto ci sta in altezza con il pannello sotto la
|
||||
scena 16:9. Il pannello (frase, poi verbi e inventario o le risposte di un dialogo) è alto --side-h unità --u, dove --u
|
||||
è un centesimo della larghezza del gioco (1cqw) fra 8,75px e 12,8px: tutto il pannello cresce e cala col gioco, non con
|
||||
è un centesimo della larghezza del gioco (1cqw) fra 8,75px e 25,6px: tutto il pannello cresce e cala col gioco, non con
|
||||
la finestra, e la riserva in altezza è esatta (prima erano 250px fissi, un centinaio di troppo, che in una finestra
|
||||
1040×590 costavano un terzo della scena). I due calc sono i due regimi di --u: proporzionale, e al minimo. */
|
||||
#app { --u: clamp(8.75px, 1cqw, 12.8px); --side-h: 13.3; container-type: inline-size;
|
||||
width: min(100vw, 1280px, calc(100dvh / (9 / 16 + var(--side-h) / 100)), calc((100dvh - var(--side-h) * 8.75px) * 16 / 9));
|
||||
#app { --u: clamp(8.75px, 1cqw, 25.6px); --side-h: 13.3; container-type: inline-size;
|
||||
width: min(100vw, 2560px, calc(100dvh / (9 / 16 + var(--side-h) / 100)), calc((100dvh - var(--side-h) * 8.75px) * 16 / 9));
|
||||
min-width: min(100vw, 320px); margin-block: auto; display: flex; flex-direction: column; padding: env(safe-area-inset-top) 0 env(safe-area-inset-bottom); }
|
||||
|
||||
/* Coi comandi di un minigioco aperti (main.js, fitControls) la scena lascia intorno il loro spazio vero, --below; ma non
|
||||
scende sotto i 700px (nelle finestre basse si scorre un poco per arrivare ai comandi, come prima). */
|
||||
@media (min-width: 601px) and (min-height: 521px) {
|
||||
#app.controls { width: min(100vw, 2560px, calc(100dvh / (9 / 16 + var(--side-h) / 100)), calc((100dvh - var(--side-h) * 8.75px) * 16 / 9),
|
||||
max(calc((100dvh - var(--below, 0px)) * 16 / 9), 700px)); }
|
||||
/* «Solo tavola» (Andrea): la pulsantiera del flipper nascosta, la tavola a tutta altezza. */
|
||||
#app.controls.table-only { width: min(100vw, 2560px, calc((100dvh - var(--below, 88px)) * 16 / 9)); }
|
||||
#app.table-only #side { display: none; }
|
||||
}
|
||||
|
||||
#stage { position: relative; width: 100%; aspect-ratio: 16 / 9; background: #000; user-select: none; touch-action: manipulation; }
|
||||
#scene { width: 100%; height: 100%; display: block; image-rendering: pixelated; cursor: crosshair; }
|
||||
#scene.pinball-hd { image-rendering: auto; }
|
||||
@@ -54,7 +64,7 @@ body { display: flex; justify-content: center; overflow-x: hidden; -webkit-tap-h
|
||||
#poker-controls summary, #briscola-controls summary { cursor: pointer; padding: 10px 0; }
|
||||
#poker-controls p, #briscola-controls p { max-width: 76em; }
|
||||
#bocce-controls { background: #242b28; padding: 10px 14px; border-top: 2px solid #897b57; font-size: 11px; }
|
||||
#bocce-status { color: #f3dcac; margin: 0 0 10px; line-height: 1.5; min-height: 1.5em; }
|
||||
#bocce-status { color: #f3dcac; margin: 0 0 10px; line-height: 1.5; min-height: 3em; } /* due righe: la scena non salta quando va a capo */
|
||||
.bocce-adjust { display: flex; align-items: center; justify-content: center; gap: 12px; }
|
||||
.bocce-adjust label { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; justify-content: space-between; min-width: 120px; max-width: 280px; flex: 1; }
|
||||
#bocce-power { width: 100%; accent-color: #b9d392; min-height: 32px; margin: 0; }
|
||||
@@ -113,6 +123,8 @@ body { display: flex; justify-content: center; overflow-x: hidden; -webkit-tap-h
|
||||
#mute[aria-pressed="true"] { color: var(--dim); text-decoration: line-through; }
|
||||
#volume, #voice-volume { width: calc(7.8 * var(--u)); accent-color: var(--accent); cursor: pointer; }
|
||||
.sound-label { margin-left: 4px; font-size: 9px; color: var(--accent); }
|
||||
#fullscreen-hud { width: 30px; height: 30px; padding: 0; font: bold 17px system-ui, sans-serif; line-height: 1; color: var(--accent); background: none; border: 0; cursor: pointer; }
|
||||
#fullscreen-hud[aria-pressed="true"] { color: var(--blue); }
|
||||
|
||||
/* La frase: una riga sola, alta sempre uguale (2,5 --u); una frase lunghissima finisce coi puntini invece di spostare la scena. */
|
||||
#sentence { height: calc(2.5 * var(--u)); line-height: 1.5; padding: .25em .6em; text-align: center; color: var(--blue); font-size: calc(1.2375 * var(--u)); font-weight: bold; background: var(--panel);
|
||||
@@ -239,7 +251,7 @@ body.full #sound { position: fixed; z-index: 13; top: max(8px, env(safe-area-ins
|
||||
/* Mappa: la pianta del paese a gradini, i posti visti come cartellini, le strade come linee. */
|
||||
/* In alto a sinistra: Pausa e Mappa. */
|
||||
#corner { position: absolute; top: 6px; left: 6px; z-index: 2; display: flex; gap: 6px; }
|
||||
#map-button, #pause-button, #notebook-button, #spots-button, #back-button { font: bold calc(.9 * var(--u)) var(--font); padding: .35em .8em;
|
||||
#map-button, #pause-button, #notebook-button, #spots-button, #back-button, #table-only { font: bold calc(.9 * var(--u)) var(--font); padding: .35em .8em;
|
||||
color: var(--accent); background: rgba(27, 20, 32, .62); border: 0; border-radius: 18px; cursor: pointer; }
|
||||
#back-button { color: #1b1420; background: #9fd0ff; }
|
||||
/* Con molti pulsanti la Pausa può essere più alta della scena: «safe» la fa partire dall'alto e scorrere, invece di tagliare il titolo. */
|
||||
@@ -355,6 +367,12 @@ body.full #sound { position: fixed; z-index: 13; top: max(8px, env(safe-area-ins
|
||||
#stage.biliardo { width: min(calc((100dvh - 90px) * 16 / 9), 68vw); }
|
||||
#app.presenta { justify-content: center; }
|
||||
#app.presenta #stage.biliardo { width: min(calc((100dvh - 44px) * 16 / 9), 100vw); }
|
||||
/* Il flipper sul telefono (Andrea, 1 novembre): pollice sinistro sulla metà sinistra della scena, destro sulla destra,
|
||||
al centro la molla. La scena prende tutta la larghezza e la pulsantiera sparisce: punti e palline sono sulla tavola. */
|
||||
#app.flipper { justify-content: center; }
|
||||
#app.flipper #side, #app.flipper #table-only { display: none; }
|
||||
#app.flipper #stage.biliardo { width: min(calc((100dvh - 44px) * 16 / 9), 100vw); }
|
||||
#app.flipper #stage.biliardo #closeup-close { position: absolute; top: 6px; bottom: auto; left: 86px; right: auto; margin: 0; min-height: 0; height: 30px; padding: 0 .9em; font-size: 10px; }
|
||||
#stage.briscola { width: min(calc((100dvh - 90px) * 16 / 9), 68vw); }
|
||||
#briscola-controls, #tressette-controls, #scopa-controls, #poker-controls { overflow-y: auto; min-height: 0; padding: 8px; font-size: 9px; }
|
||||
/* Nella colonna stretta accanto al tavolo: carte e segni su due colonne, parole che vanno a capo. */
|
||||
|
||||
@@ -508,6 +508,22 @@ try {
|
||||
assert.ok(stage0.width >= 1200, `a 1280×860 la scena è larga ${stage0.width} px`);
|
||||
assert.ok(await page.evaluate(() => document.documentElement.scrollHeight <= innerHeight), 'la pagina scorre in verticale a 1280×860');
|
||||
assert.ok(await page.evaluate(() => [...document.querySelectorAll('#verbs button')].every(b => b.scrollWidth <= b.clientWidth + 1 && b.scrollHeight <= b.clientHeight + 1)), 'verbi tagliati a 1280×860');
|
||||
// Su uno schermo grande (1920×1080, a schermo intero) il gioco non si ferma più a 1280 px: prende tutta l'altezza.
|
||||
await page.setViewportSize({ width: 1920, height: 1080 });
|
||||
await page.waitForFunction(() => document.getElementById('stage').getBoundingClientRect().width > 1500);
|
||||
assert.ok(await page.evaluate(() => document.documentElement.scrollHeight <= innerHeight && document.getElementById('app').getBoundingClientRect().height >= innerHeight - 2), 'a 1920×1080 il gioco prende tutta l\'altezza, senza scorrere');
|
||||
await page.setViewportSize({ width: 1280, height: 860 });
|
||||
// Schermo intero: il pulsante accanto ai volumi e il tasto F, per entrare e per uscire.
|
||||
if (await page.evaluate(() => document.fullscreenEnabled)) {
|
||||
await page.click('#fullscreen-hud');
|
||||
await page.waitForFunction(() => !!document.fullscreenElement && document.getElementById('fullscreen-hud').getAttribute('aria-pressed') === 'true');
|
||||
await page.keyboard.press('f');
|
||||
await page.waitForFunction(() => !document.fullscreenElement);
|
||||
await page.keyboard.press('F');
|
||||
await page.waitForFunction(() => !!document.fullscreenElement);
|
||||
await page.click('#fullscreen-hud');
|
||||
await page.waitForFunction(() => !document.fullscreenElement && document.getElementById('fullscreen-hud').getAttribute('aria-pressed') === 'false');
|
||||
} else assert.ok(await page.evaluate(() => document.getElementById('fullscreen-hud').hidden), 'senza schermo intero il pulsante sparisce');
|
||||
// Pausa: Esc o il pulsante fermano il gioco; Riprendi torna dov'eri; il titolo offre «Continua».
|
||||
await page.keyboard.press('Escape');
|
||||
await page.waitForSelector('#pause:not([hidden])');
|
||||
|
||||
+20
-11
@@ -25,16 +25,31 @@ try {
|
||||
game.act('use', 'river'); settle(game);
|
||||
game.choose(game.options().findIndex(o => o.text.startsWith('Una partita alla bona'))); settle(game);
|
||||
});
|
||||
await page.waitForSelector('#bocce-controls:not([hidden])');
|
||||
await page.waitForSelector('#bocce-controls:not([hidden])', { state: 'attached' });
|
||||
await page.waitForFunction(async () => (await import('/src/engine/render.js')).hasBackground('assets/closeups/river-flipper.png'));
|
||||
// Aspetta che finisca la dissolvenza d'ingresso: le immagini devono mostrare i colori della tavola.
|
||||
const readyAt = await page.evaluate(() => game.closeup.visual.time);
|
||||
await page.waitForFunction(t => game.closeup.visual.time > t + 1, readyAt);
|
||||
assert.equal(await page.locator('#scene').getAttribute('width'), '1280', 'il flipper usa la risoluzione doppia');
|
||||
assert.equal(await page.locator('#bocce-throw').textContent(), 'Lancia');
|
||||
assert.ok(await page.locator('#flipper-help').isVisible());
|
||||
assert.equal(await page.locator('#bocce-left').textContent(), '← Flipper');
|
||||
assert.ok(!(await page.locator('#bocce-left').isDisabled()), 'i pulsanti dei flipper sono attivi anche prima del lancio');
|
||||
if (!mobile) {
|
||||
// «Solo tavola»: la pulsantiera sparisce e la tavola si allarga; ripremuto, torna tutto com'era.
|
||||
const before = (await page.locator('#scene').boundingBox()).width;
|
||||
await page.click('#table-only');
|
||||
await page.waitForFunction(() => getComputedStyle(document.getElementById('side')).display === 'none');
|
||||
await page.waitForFunction(w => document.getElementById('scene').getBoundingClientRect().width > w + 50, before);
|
||||
assert.equal(await page.locator('#table-only').textContent(), 'Pulsantiera');
|
||||
await page.click('#table-only');
|
||||
await page.waitForFunction(() => getComputedStyle(document.getElementById('side')).display !== 'none');
|
||||
assert.ok(await page.locator('#flipper-help').isVisible());
|
||||
assert.equal(await page.locator('#bocce-left').textContent(), '← Flipper');
|
||||
assert.ok(!(await page.locator('#bocce-left').isDisabled()), 'i pulsanti dei flipper sono attivi anche prima del lancio');
|
||||
} else {
|
||||
// Sul telefono in orizzontale (Andrea): niente pulsantiera, la scena larga sotto i due pollici.
|
||||
assert.ok(!(await page.locator('#side').isVisible()), 'sul telefono la pulsantiera del flipper sparisce');
|
||||
const w = (await page.locator('#scene').boundingBox()).width;
|
||||
assert.ok(w > viewport.width * 0.6, `sul telefono la scena del flipper è larga ${w} px`);
|
||||
}
|
||||
await page.screenshot({ path: `${out}/${name}-tavola.png` });
|
||||
const box = await page.locator('#scene').boundingBox(), P = (x, y) => [box.x + x / 640 * box.width, box.y + y / 360 * box.height];
|
||||
if (!mobile) await page.screenshot({ path: `${out}/desktop-dettaglio.png`, clip: { x: P(200, 0)[0], y: box.y, width: 248 / 640 * box.width, height: box.height } });
|
||||
@@ -42,7 +57,7 @@ try {
|
||||
const touch = (type, points) => cdp.send('Input.dispatchTouchEvent', { type, touchPoints: points.map(([x, y]) => ({ x, y })) });
|
||||
// La molla: tenendo premuto sulla scena, col mouse e col dito.
|
||||
if (mobile) {
|
||||
await touch('touchStart', [P(440, 320)]);
|
||||
await touch('touchStart', [P(320, 320)]); // al centro: la molla
|
||||
await page.waitForFunction(() => game.closeup.holding && game.closeup.power > 0.3, null, { polling: 'raf' });
|
||||
await touch('touchEnd', []);
|
||||
} else {
|
||||
@@ -63,12 +78,6 @@ try {
|
||||
await page.waitForFunction(() => game.closeup.flippers.left.held.length === 0 && game.closeup.flippers.right.held.length > 0, null, { timeout: 3000 });
|
||||
await touch('touchEnd', []);
|
||||
await page.waitForFunction(() => game.closeup.flippers.right.held.length === 0);
|
||||
// I pulsanti tenuti premuti.
|
||||
const b = await page.locator('#bocce-left').boundingBox();
|
||||
await touch('touchStart', [[b.x + b.width / 2, b.y + b.height / 2]]);
|
||||
await page.waitForFunction(() => game.closeup.flippers.left.held.length > 0);
|
||||
await touch('touchEnd', []);
|
||||
await page.waitForFunction(() => game.closeup.flippers.left.held.length === 0);
|
||||
} else {
|
||||
await page.mouse.move(...P(400, 200)); await page.mouse.down();
|
||||
await page.waitForFunction(() => game.closeup.flippers.right.held.length > 0);
|
||||
|
||||
@@ -130,13 +130,17 @@ test('funghi, bersagli, Regolo e corsie fanno punti solo a un urto nuovo; le qua
|
||||
assert.ok(!c.lanes[1].lit);
|
||||
});
|
||||
|
||||
test('comandi: la molla si carica e si lancia lasciando; annullare non lancia; due dita, pulsanti e tasti', () => {
|
||||
test('comandi: la molla si carica al centro e si lancia lasciando, ai lati le palette anche prima del lancio; annullare non lancia; due dita, pulsanti e tasti', () => {
|
||||
const c = m.newFlipper(8, { level: 'bona' });
|
||||
m.aimScreen(c, [440, 320]); assert.ok(m.press(c, 1)); run(c, 0.6);
|
||||
// A pallina ferma i lati alzano le palette, il centro carica la molla.
|
||||
m.aimScreen(c, [120, 300]); assert.ok(m.press(c, 5)); assert.ok(c.flippers.left.held.includes(5) && !c.holding, 'a sinistra, la paletta');
|
||||
m.aimScreen(c, [520, 300]); assert.ok(m.press(c, 6)); assert.ok(c.flippers.right.held.includes(6) && !c.holding, 'a destra, la paletta');
|
||||
m.release(c, 5); m.release(c, 6); assert.ok(m.waiting(c));
|
||||
m.aimScreen(c, [320, 320]); assert.ok(m.press(c, 1)); run(c, 0.6);
|
||||
assert.ok(c.holding && c.power > 0.4 && c.power < 0.6, `molla a ${c.power.toFixed(2)}`);
|
||||
assert.ok(!m.release(c, 2), 'un altro dito non lancia');
|
||||
m.cancel(c, 1); assert.ok(m.waiting(c) && !c.holding, 'annullata la carica: niente lancio');
|
||||
m.aimScreen(c, [440, 320]); m.press(c, 1); run(c, 0.6); assert.ok(m.release(c, 1)); assert.ok(!m.waiting(c));
|
||||
m.aimScreen(c, [300, 320]); m.press(c, 1); run(c, 0.6); assert.ok(m.release(c, 1)); assert.ok(!m.waiting(c));
|
||||
m.aimScreen(c, [250, 200]); assert.ok(m.press(c, 3)); assert.ok(c.flippers.left.held.includes(3));
|
||||
m.aimScreen(c, [400, 200]); assert.ok(m.press(c, 4)); assert.ok(c.flippers.right.held.includes(4));
|
||||
assert.ok(m.release(c, 3)); assert.ok(!c.flippers.left.held.length && c.flippers.right.held.includes(4));
|
||||
|
||||
@@ -13,18 +13,32 @@ async function tap(page, mode, selector) {
|
||||
if (mode === 'touch') { const r = await page.locator(selector).boundingBox(); await page.touchscreen.tap(r.x + r.width / 2, r.y + r.height / 2); }
|
||||
else await page.click(selector);
|
||||
}
|
||||
// Una battuta che compare proprio in quell'istante (le battute del giocatore partono dal ciclo del gioco) si prende il
|
||||
// tasto e la salta, come nel gioco: allora, come un giocatore, si ripreme. Al più tre volte.
|
||||
async function pressUntil(page, key, done, arg) {
|
||||
for (let i = 0; i < 3; i++) {
|
||||
await page.keyboard.press(key);
|
||||
if (await page.evaluate(done, arg)) return;
|
||||
await page.waitForFunction(() => !game.speech);
|
||||
}
|
||||
}
|
||||
async function playCard(page, mode, index, signal) {
|
||||
// Un tasto premuto mentre qualcuno parla salta la battuta e basta (come nel gioco): si aspetta che abbia finito.
|
||||
if (mode === 'keyboard') await page.waitForFunction(() => !game.speech);
|
||||
// Un tasto premuto mentre qualcuno parla salta la battuta e basta, e il segnale si sceglie solo quando tocca a noi
|
||||
// (come nel gioco): si aspetta che la battuta sia finita e che si possa giocare.
|
||||
if (mode === 'keyboard') {
|
||||
await page.evaluate(async () => { window.tressetteRules ??= await import('/src/engine/tressette.js'); });
|
||||
await page.waitForFunction(() => !game.speech && tressetteRules.canPlay(game.closeup));
|
||||
}
|
||||
if (signal) {
|
||||
if (mode === 'keyboard') await page.keyboard.press(signal[0]);
|
||||
if (mode === 'keyboard') await pressUntil(page, signal[0], s => game.closeup.signal === s, signal);
|
||||
else await tap(page, mode, `#tressette-${signal}`);
|
||||
assert.equal(await page.evaluate(() => game.closeup.signal), signal);
|
||||
}
|
||||
if (mode === 'keyboard') {
|
||||
await page.keyboard.press(String((index + 1) % 10));
|
||||
await pressUntil(page, String((index + 1) % 10), i => game.closeup.selected === i, index);
|
||||
assert.equal(await page.evaluate(() => game.closeup.selected), index);
|
||||
await page.keyboard.press('Enter');
|
||||
const held = await page.evaluate(() => game.closeup.hands[0].length);
|
||||
await pressUntil(page, 'Enter', n => game.closeup.hands[0].length < n || !!game.closeup.refused, held);
|
||||
return;
|
||||
}
|
||||
const at = await page.evaluate(async i => {
|
||||
|
||||
Reference in New Issue
Block a user