Biliardo più preciso (revisione di Codex): urti calcolati nel punto esatto di contatto, anche nei colpi di taglio e veloci; la guida simula il tiro vero e mostra le due biglie dopo l'urto e i falli; la mira resta ferma andando sui comandi; frecce e Maiusc per la mira fine; comandi che non coprono il tavolo; test nel browser con mouse, touch e telefono. Più un tetto agli urti per passo, così due biglie a contatto non possono bloccare il gioco
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018ks2K99uqCZjAxvy7Rdj1R
This commit is contained in:
@@ -1,11 +1,11 @@
|
||||
import { LEN, WID, R, PIN, VIEW, canThrow, predict, lastLine, status } from './biliardo.js';
|
||||
import { LEN, WID, R, VIEW, canThrow, predict, status } from './biliardo.js';
|
||||
import { text, panel } from './bocce-render.js';
|
||||
import { ambientHash as hash } from './ambient.js';
|
||||
|
||||
const BALL = { player: ['#8f8160', '#f1e6c8', '#fffdf4'], cpu: ['#8a7220', '#ead77a', '#fff9d2'], pallino: ['#5e1612', '#c9372b', '#ffb3a4'] };
|
||||
const sx = x => VIEW.x + x * VIEW.k, sy = y => VIEW.y + y * VIEW.k;
|
||||
// Le biglie si disegnano un po' più grandi del vero (la fisica resta quella), sennò sul telefono spariscono.
|
||||
const rr = R * VIEW.k * 1.3;
|
||||
// Biglia reale e fantasma hanno esattamente il raggio usato per gli urti.
|
||||
const rr = R * VIEW.k;
|
||||
|
||||
function sphere(ctx, role, x, y, r) {
|
||||
const [dark, mid, light] = BALL[role];
|
||||
@@ -51,17 +51,28 @@ function table(ctx) {
|
||||
|
||||
function aimGuide(ctx, c) {
|
||||
const p = predict(c), me = c.balls[c.current];
|
||||
ctx.strokeStyle = 'rgba(255,248,220,0.75)'; ctx.lineWidth = 1; ctx.setLineDash([3, 4]);
|
||||
const foul = p.hit === 'pin' || p.hit === 'pallino';
|
||||
ctx.strokeStyle = foul ? '#ffae8b' : 'rgba(255,248,220,0.85)'; ctx.lineWidth = 1; ctx.setLineDash([4, 3]);
|
||||
ctx.beginPath(); ctx.moveTo(sx(p.points[0][0]), sy(p.points[0][1]));
|
||||
for (const [x, y] of p.points.slice(1)) ctx.lineTo(sx(x), sy(y));
|
||||
ctx.stroke(); ctx.setLineDash([]);
|
||||
if (p.ghost) {
|
||||
ctx.strokeStyle = 'rgba(255,255,255,0.8)'; ctx.beginPath(); ctx.arc(sx(p.ghost.x), sy(p.ghost.y), rr, 0, Math.PI * 2); ctx.stroke();
|
||||
if (p.dir) {
|
||||
const o = c.balls[p.hit], L = 22;
|
||||
ctx.strokeStyle = p.hit === 'pallino' ? 'rgba(255,120,100,0.9)' : 'rgba(150,210,255,0.9)';
|
||||
ctx.beginPath(); ctx.moveTo(sx(o.x), sy(o.y)); ctx.lineTo(sx(o.x) + p.dir[0] * L, sy(o.y) + p.dir[1] * L); ctx.stroke();
|
||||
ctx.strokeStyle = foul ? '#ffae8b' : 'rgba(255,255,255,0.8)'; ctx.beginPath(); ctx.arc(sx(p.ghost.x), sy(p.ghost.y), rr, 0, Math.PI * 2); ctx.stroke();
|
||||
for (const track of p.outgoing) {
|
||||
const [start, end] = track.points;
|
||||
if (!end || Math.hypot(end[0] - start[0], end[1] - start[1]) < 1) continue;
|
||||
const x = sx(end[0]), y = sy(end[1]), a = Math.atan2(end[1] - start[1], end[0] - start[0]);
|
||||
ctx.strokeStyle = track.role === c.current ? '#f4df8d' : track.role === 'pallino' ? '#ffae8b' : '#96dfff';
|
||||
ctx.setLineDash(track.role === c.current ? [2, 3] : []);
|
||||
ctx.beginPath(); ctx.moveTo(sx(start[0]), sy(start[1])); ctx.lineTo(x, y); ctx.stroke();
|
||||
ctx.setLineDash([]);
|
||||
ctx.beginPath(); ctx.moveTo(x - Math.cos(a - 0.5) * 5, y - Math.sin(a - 0.5) * 5);
|
||||
ctx.lineTo(x, y); ctx.lineTo(x - Math.cos(a + 0.5) * 5, y - Math.sin(a + 0.5) * 5); ctx.stroke();
|
||||
}
|
||||
} else {
|
||||
// Il cerchietto finale è l'arresto effettivo con la forza scelta.
|
||||
const [x, y] = p.points.at(-1);
|
||||
ctx.strokeStyle = '#fff8dc'; ctx.beginPath(); ctx.arc(sx(x), sy(y), 2, 0, Math.PI * 2); ctx.stroke();
|
||||
}
|
||||
// La stecca dietro la biglia, tirata indietro quanto la forza.
|
||||
const a = c.angle, back = rr + 6 + c.power * 26, len = 150;
|
||||
@@ -90,20 +101,21 @@ export function drawBiliardo(ctx, c, def) {
|
||||
// Punteggio: i due nomi, i punti, chi deve tirare.
|
||||
panel(ctx, 18, 8, 604, 62);
|
||||
for (const [i, who] of ['player', 'cpu'].entries()) {
|
||||
const x = 30 + i * 300, y = 24;
|
||||
const x = 30 + i * 420, y = 24;
|
||||
sphere(ctx, who, x + 4, y, 4);
|
||||
text(ctx, def.names[who], x + 14, y, 7, BALL[who][2]);
|
||||
text(ctx, String(c.points[who]), x + 14, y + 16, 10, c.step === 'aim' && c.current === who ? '#fff2a3' : '#f4e3c2');
|
||||
if (c.step === 'aim' && c.current === who) text(ctx, '< al tiro', x + 44, y + 16, 6, '#fff2a3');
|
||||
}
|
||||
const last = c.last && c.last.age < 6 ? lastLine(c, def.names) : '';
|
||||
if (last) text(ctx, last, 610, 42, 6, c.last.foul ? '#f3b0a0' : '#c9eab3', 'right');
|
||||
text(ctx, c.step === 'aim' ? c.current === 'player' ? 'Tocca il tavolo per mirare; tieni premuto e lascia per tirare.' : `${def.names.cpu} studia il colpo…` : status(c, def.names), 320, 60, 6, '#d7ccb4', 'center');
|
||||
const hint = canThrow(c) ? predict(c).hit : null;
|
||||
const instruction = hint === 'pin' ? 'Attenzione: la tua biglia va sui birilli (fallo).'
|
||||
: hint === 'pallino' ? 'Attenzione: tocchi prima il pallino (fallo).'
|
||||
: 'Clic fissa la mira. Azzurro: avversaria; giallo: la tua dopo l’urto.';
|
||||
text(ctx, c.step === 'aim' ? c.current === 'player' ? instruction : `${def.names.cpu} studia il colpo…` : status(c, def.names), 320, 60, 6, hint === 'pin' || hint === 'pallino' ? '#ffae8b' : '#d7ccb4', 'center');
|
||||
if (canThrow(c)) {
|
||||
panel(ctx, 18, 318, 176, 34);
|
||||
text(ctx, `FORZA ${Math.round(c.power * 100)}%`, 30, 329, 7);
|
||||
ctx.fillStyle = '#3e3e36'; ctx.fillRect(30, 341, 150, 5);
|
||||
ctx.fillStyle = '#b9d392'; ctx.fillRect(30, 341, Math.round(c.power * 150), 5);
|
||||
text(ctx, `FORZA ${Math.round(c.power * 100)}%`, 320, 25, 7, '#f4e3c2', 'center');
|
||||
ctx.fillStyle = '#3e3e36'; ctx.fillRect(245, 36, 150, 5);
|
||||
ctx.fillStyle = '#b9d392'; ctx.fillRect(245, 36, Math.round(c.power * 150), 5);
|
||||
}
|
||||
if (c.step === 'result') {
|
||||
panel(ctx, 170, 140, 300, 80);
|
||||
|
||||
+121
-41
@@ -8,9 +8,10 @@ export const LEN = 284, WID = 142, R = 3.05, PIN = 0.9, GAP = 6.3, TARGET = 20;
|
||||
export const MIN_SPEED = 50, MAX_SPEED = 520, FRICTION = 70, DRAG = 0.35, CUSHION = 0.78;
|
||||
export const SWING = 1.4, FIXED_STEP = 1 / 240;
|
||||
const EPS = 0.5, REST = 0.6, CPU_THINK = 0.2, CPU_SHOT = 1.5;
|
||||
const CONTACT_EPS = 1e-7, BALL_RESTITUTION = 0.95;
|
||||
export const OTHER = { player: 'cpu', cpu: 'player' };
|
||||
// Dove sta il tavolo nel primo piano 640×360: angolo in alto a sinistra del panno e pixel per centimetro.
|
||||
export const VIEW = { x: 57, y: 78, k: 1.85 };
|
||||
export const VIEW = { x: 70, y: 88, k: 1.76 };
|
||||
|
||||
const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
|
||||
function random(c) { c.seed = (Math.imul(c.seed, 1664525) + 1013904223) >>> 0; return c.seed / 4294967296; }
|
||||
@@ -72,12 +73,12 @@ function cushions(b) {
|
||||
// Urto fra due biglie uguali, quasi elastico; separa anche quelle ferme e sovrapposte.
|
||||
export function collide(a, b) {
|
||||
const dx = b.x - a.x, dy = b.y - a.y, d = Math.hypot(dx, dy);
|
||||
if (d >= 2 * R) return 0;
|
||||
const nx = d > 1e-9 ? dx / d : 1, ny = d > 1e-9 ? dy / d : 0, overlap = (2 * R - d) / 2;
|
||||
if (d > 2 * R + CONTACT_EPS) return 0;
|
||||
const nx = d > 1e-9 ? dx / d : 1, ny = d > 1e-9 ? dy / d : 0, overlap = Math.max(0, (2 * R - d) / 2);
|
||||
a.x -= nx * overlap; a.y -= ny * overlap; b.x += nx * overlap; b.y += ny * overlap;
|
||||
const closing = (a.vx - b.vx) * nx + (a.vy - b.vy) * ny;
|
||||
if (closing <= 0) return 0;
|
||||
const j = closing * 0.975;
|
||||
if (closing <= CONTACT_EPS) return 0;
|
||||
const j = closing * (1 + BALL_RESTITUTION) / 2;
|
||||
a.vx -= j * nx; a.vy -= j * ny; b.vx += j * nx; b.vy += j * ny;
|
||||
return closing;
|
||||
}
|
||||
@@ -87,28 +88,70 @@ function touch(shot, a, b) {
|
||||
if (shot.first === null && roles.includes(shot.shooter)) shot.first = roles.find(r => r !== shot.shooter);
|
||||
if (roles.includes(other) && roles.includes('pallino')) shot.pallino = true;
|
||||
}
|
||||
// Tempo al primo contatto: si risolve l'intersezione del moto relativo con il cerchio.
|
||||
// Anche un colpo veloce o molto sottile viene risolto sul punto d'urto, non dopo la compenetrazione.
|
||||
function contactTime(dx, dy, vx, vy, radius) {
|
||||
const closing = dx * vx + dy * vy, speed2 = vx * vx + vy * vy;
|
||||
if (closing >= -CONTACT_EPS * radius || speed2 === 0) return Infinity;
|
||||
const gap = dx * dx + dy * dy - radius * radius;
|
||||
if (gap <= CONTACT_EPS) return 0;
|
||||
const discriminant = closing * closing - speed2 * gap;
|
||||
if (discriminant <= 0) return Infinity;
|
||||
return gap / (-closing + Math.sqrt(discriminant));
|
||||
}
|
||||
function slow(b, dt) {
|
||||
const v = Math.hypot(b.vx, b.vy);
|
||||
if (!v) return;
|
||||
const nv = (v + FRICTION / DRAG) * Math.exp(-DRAG * dt) - FRICTION / DRAG;
|
||||
const scale = nv < EPS ? 0 : nv / v;
|
||||
b.vx *= scale; b.vy *= scale;
|
||||
}
|
||||
function advance(balls, dt) {
|
||||
for (const b of balls) { b.x += b.vx * dt; b.y += b.vy * dt; }
|
||||
}
|
||||
function step(w, dt) {
|
||||
let hit = 0, rail = 0;
|
||||
for (const b of w.balls) {
|
||||
const v = Math.hypot(b.vx, b.vy);
|
||||
if (v < EPS) { b.vx = b.vy = 0; continue; }
|
||||
const nv = Math.max(0, v - (FRICTION + DRAG * v) * dt), travel = (v + nv) / 2 * dt;
|
||||
b.x += b.vx / v * travel; b.y += b.vy / v * travel;
|
||||
b.vx *= nv / v; b.vy *= nv / v;
|
||||
rail = Math.max(rail, cushions(b));
|
||||
for (const p of w.pins) {
|
||||
if (p.down || Math.hypot(b.x - p.x, b.y - p.y) >= R + PIN) continue;
|
||||
// Il birillo cade dalla parte opposta a chi lo colpisce; la biglia perde un po' di slancio.
|
||||
Object.assign(p, { down: true, by: b.role, dir: Math.atan2(p.y - b.y, p.x - b.x) });
|
||||
b.vx *= 0.88; b.vy *= 0.88;
|
||||
w.shot?.pins.push(p);
|
||||
// Mezzo passo d'attrito, moto con urti continui, mezzo passo d'attrito.
|
||||
// Questa stessa integrazione serve al tiro, alla guida e alle prove dell'avversario.
|
||||
for (const b of w.balls) slow(b, dt / 2);
|
||||
let left = dt, hit = 0, rail = 0;
|
||||
// Un tetto agli urti per passo: due biglie a contatto con un arrotondamento sfortunato non devono bloccare il gioco.
|
||||
for (let events = 0; left > 1e-10 && events < 64; events++) {
|
||||
let time = left, event = null;
|
||||
const consider = (t, kind, a, b, axis, edge) => {
|
||||
if (t >= -1e-10 && t <= time) { time = Math.max(0, t); event = { kind, a, b, axis, edge }; }
|
||||
};
|
||||
for (let i = 0; i < w.balls.length; i++) {
|
||||
const a = w.balls[i];
|
||||
if (a.vx) consider(((a.vx < 0 ? R : LEN - R) - a.x) / a.vx, 'rail', a, null, 'x', a.vx < 0 ? R : LEN - R);
|
||||
if (a.vy) consider(((a.vy < 0 ? R : WID - R) - a.y) / a.vy, 'rail', a, null, 'y', a.vy < 0 ? R : WID - R);
|
||||
for (let j = i + 1; j < w.balls.length; j++) {
|
||||
const b = w.balls[j];
|
||||
consider(contactTime(a.x - b.x, a.y - b.y, a.vx - b.vx, a.vy - b.vy, 2 * R), 'ball', a, b);
|
||||
}
|
||||
if (!a.vx && !a.vy) continue;
|
||||
for (const p of w.pins) if (!p.down)
|
||||
consider(contactTime(a.x - p.x, a.y - p.y, a.vx, a.vy, R + PIN), 'pin', a, p);
|
||||
}
|
||||
advance(w.balls, time); left -= time;
|
||||
if (!event) break;
|
||||
const { kind, a, b, axis, edge } = event;
|
||||
if (kind === 'rail') {
|
||||
const v = axis === 'x' ? 'vx' : 'vy';
|
||||
a[axis] = edge;
|
||||
rail = Math.max(rail, Math.abs(a[v])); a[v] *= -CUSHION;
|
||||
} else if (kind === 'ball') {
|
||||
const impact = collide(a, b);
|
||||
if (impact > 0) { touch(w.shot, a, b); hit = Math.max(hit, impact); }
|
||||
} else {
|
||||
const v = Math.hypot(a.vx, a.vy);
|
||||
Object.assign(b, { down: true, by: a.role, dir: Math.atan2(b.y - a.y, b.x - a.x) });
|
||||
a.vx *= 0.88; a.vy *= 0.88;
|
||||
w.shot?.pins.push(b);
|
||||
if (w.events && w.events.length < 16) w.events.push({ kind: 'biliardo-pin', power: Math.min(1, v / 300) });
|
||||
}
|
||||
w.onContact?.(event);
|
||||
}
|
||||
for (let i = 0; i < w.balls.length; i++) for (let j = i + 1; j < w.balls.length; j++) {
|
||||
const s = collide(w.balls[i], w.balls[j]);
|
||||
if (s > 0) { touch(w.shot, w.balls[i], w.balls[j]); hit = Math.max(hit, s); }
|
||||
}
|
||||
for (const b of w.balls) slow(b, dt / 2);
|
||||
if (w.events && w.events.length < 16 && (hit > 12 || rail > 12))
|
||||
w.events.push({ kind: hit >= rail ? 'biliardo-hit' : 'biliardo-rail', power: Math.min(1, Math.max(hit, rail) / 400) });
|
||||
}
|
||||
@@ -197,13 +240,20 @@ export function update(c, dt) {
|
||||
|
||||
// --- Comandi del giocatore: gli stessi delle bocce.
|
||||
export function aimAt(c, x, y) {
|
||||
if (!canThrow(c) || !Number.isFinite(x) || !Number.isFinite(y)) return false;
|
||||
if (!canThrow(c) || c.holding || !Number.isFinite(x) || !Number.isFinite(y)) return false;
|
||||
const b = c.balls.player;
|
||||
if (Math.hypot(x - b.x, y - b.y) < R) return false;
|
||||
c.angle = Math.atan2(y - b.y, x - b.x); return true;
|
||||
}
|
||||
export function aimScreen(c, [sx, sy]) { return aimAt(c, (sx - VIEW.x) / VIEW.k, (sy - VIEW.y) / VIEW.k); }
|
||||
export function nudge(c, dir) { if (!canThrow(c) || c.holding) return false; c.angle += dir * 0.01; return true; }
|
||||
export function aimScreen(c, [sx, sy]) {
|
||||
const x = (sx - VIEW.x) / VIEW.k, y = (sy - VIEW.y) / VIEW.k;
|
||||
if (x < 0 || x > LEN || y < 0 || y > WID) return false;
|
||||
return aimAt(c, x, y);
|
||||
}
|
||||
export function nudge(c, dir, fine = false) {
|
||||
if (!canThrow(c) || c.holding || !Number.isFinite(dir)) return false;
|
||||
c.angle += dir * (fine ? 0.0005 : 0.002); return true;
|
||||
}
|
||||
export function setPower(c, value) {
|
||||
if (!canThrow(c) || c.holding || !Number.isFinite(value)) return false;
|
||||
c.power = clamp(value, 0, 1); return true;
|
||||
@@ -223,24 +273,54 @@ export function release(c) {
|
||||
c.holding = false; return strike(c, 'player', c.angle, c.power);
|
||||
}
|
||||
|
||||
// Dove andrebbe la biglia: la linea fino al primo urto (con le sponde, fino a dove arriva con quella forza),
|
||||
// la biglia fantasma dove tocca, e verso dove partirebbe quella colpita.
|
||||
// La guida riproduce il tiro su una copia, con gli stessi urti e lo stesso attrito.
|
||||
// Dopo il primo contatto mostra il tratto iniziale delle due biglie, fino al prossimo
|
||||
// ostacolo, all'arresto o a 50 cm. La cache evita di riprovare il tiro a ogni disegno.
|
||||
const predictions = new WeakMap();
|
||||
export function predict(c) {
|
||||
const me = c.balls[c.current], others = list(c).filter(b => b !== me);
|
||||
let x = me.x, y = me.y, dx = Math.cos(c.angle), dy = Math.sin(c.angle), left = reach(speedFor(c.power));
|
||||
const points = [[x, y]], STEP = 0.5;
|
||||
for (let n = 0; left > 0 && n < 6000; n++) {
|
||||
x += dx * STEP; y += dy * STEP; left -= STEP;
|
||||
if (x < R || x > LEN - R) { x = clamp(x, R, LEN - R); dx = -dx; left *= 0.6; points.push([x, y]); }
|
||||
if (y < R || y > WID - R) { y = clamp(y, R, WID - R); dy = -dy; left *= 0.6; points.push([x, y]); }
|
||||
for (const o of others) {
|
||||
const d = Math.hypot(o.x - x, o.y - y);
|
||||
if (d < 2 * R) { points.push([x, y]); return { points, ghost: { x, y }, hit: o.role, dir: [(o.x - x) / d, (o.y - y) / d] }; }
|
||||
const key = [c.current, c.angle, c.power, ...list(c).flatMap(b => [b.x, b.y, b.vx, b.vy]),
|
||||
...c.pins.flatMap(p => [p.x, p.y, p.down])].join('|');
|
||||
const cached = predictions.get(c);
|
||||
if (cached?.key === key) return cached.result;
|
||||
const balls = list(c).map(b => ({ ...b })), me = balls.find(b => b.role === c.current);
|
||||
const v = speedFor(c.power);
|
||||
me.vx = Math.cos(c.angle) * v; me.vy = Math.sin(c.angle) * v;
|
||||
const result = { points: [[me.x, me.y]], ghost: null, hit: null, dir: null, outgoing: [] };
|
||||
const tracks = [];
|
||||
const point = b => [b.x, b.y];
|
||||
const endTrack = track => {
|
||||
track.points.push(point(track.ball)); track.done = true;
|
||||
};
|
||||
const w = { balls, pins: c.pins.map(p => ({ ...p })), onContact(event) {
|
||||
const { kind, a, b } = event;
|
||||
for (const track of tracks) if (!track.done && (track.ball === a || track.ball === b)) endTrack(track);
|
||||
if (result.hit || (a !== me && b !== me)) return;
|
||||
result.points.push(point(me));
|
||||
if (kind === 'rail') return;
|
||||
result.ghost = { x: me.x, y: me.y };
|
||||
result.hit = kind === 'pin' ? 'pin' : (a === me ? b : a).role;
|
||||
if (kind === 'pin') return;
|
||||
const other = a === me ? b : a, speed = Math.hypot(other.vx, other.vy);
|
||||
result.dir = speed ? [other.vx / speed, other.vy / speed] : null;
|
||||
for (const ball of [me, other]) {
|
||||
const points = [point(ball)];
|
||||
tracks.push({ ball, points, done: false });
|
||||
result.outgoing.push({ role: ball.role, points });
|
||||
}
|
||||
for (const p of c.pins) if (!p.down && Math.hypot(p.x - x, p.y - y) < R + PIN) { points.push([x, y]); return { points, ghost: { x, y }, hit: 'pin', dir: null }; }
|
||||
} };
|
||||
for (let i = 0; i < 15 / FIXED_STEP; i++) {
|
||||
step(w, FIXED_STEP);
|
||||
for (const track of tracks) {
|
||||
if (track.done) continue;
|
||||
const [x, y] = track.points[0], dx = track.ball.x - x, dy = track.ball.y - y, d = Math.hypot(dx, dy);
|
||||
if (d >= 50) { track.points.push([x + dx * 50 / d, y + dy * 50 / d]); track.done = true; }
|
||||
else if (!track.ball.vx && !track.ball.vy) endTrack(track);
|
||||
}
|
||||
if (result.hit ? tracks.every(t => t.done) : !me.vx && !me.vy) break;
|
||||
}
|
||||
points.push([x, y]);
|
||||
return { points, ghost: null, hit: null, dir: null };
|
||||
if (!result.hit) result.points.push(point(me));
|
||||
predictions.set(c, { key, result });
|
||||
return result;
|
||||
}
|
||||
|
||||
const FOULS = { miss: 'biglia avversaria mancata', pallino: 'prima il pallino', own: 'birilli con la propria biglia' };
|
||||
|
||||
+10
-4
@@ -399,8 +399,9 @@ 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) {
|
||||
const aimed = game.closeupAim(scenePoint(e));
|
||||
if (story.closeups[game.closeup.id].type === 'biliardo' && !aimed && !game.speech) return;
|
||||
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();
|
||||
canvas.setPointerCapture(e.pointerId);
|
||||
@@ -431,7 +432,10 @@ canvas.addEventListener('contextmenu', e => {
|
||||
canvas.addEventListener('pointermove', e => {
|
||||
if (!running) return;
|
||||
mouse = e.pointerType === 'mouse' ? scenePoint(e) : null;
|
||||
if (e.pointerType === 'mouse' || press?.closeup) game.closeupAim(scenePoint(e));
|
||||
// Al biliardo il clic fissa la mira: attraversare il tavolo per raggiungere «Tira»
|
||||
// o sfiorare il mouse dopo una regolazione fine non deve cambiare il colpo.
|
||||
const billiards = game.closeup && story.closeups[game.closeup.id].type === 'biliardo';
|
||||
if (!billiards && (e.pointerType === 'mouse' || press?.closeup)) game.closeupAim(scenePoint(e));
|
||||
});
|
||||
canvas.addEventListener('pointerleave', () => { mouse = null; });
|
||||
|
||||
@@ -439,7 +443,7 @@ canvas.addEventListener('pointerleave', () => { mouse = null; });
|
||||
// lancio con un solo clic, anche per chi non vuole usare la pressione temporizzata.
|
||||
const MINI = {
|
||||
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: tocca il tavolo per mirare. Regola la forza e premi Tira.' },
|
||||
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];
|
||||
@@ -539,7 +543,7 @@ window.addEventListener('keydown', e => {
|
||||
const c = game.closeup, m = mini();
|
||||
if (['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].includes(e.key)) {
|
||||
e.preventDefault();
|
||||
if (e.key === 'ArrowLeft' || e.key === 'ArrowRight') m.nudge(c, e.key === 'ArrowLeft' ? -1 : 1);
|
||||
if (e.key === 'ArrowLeft' || e.key === 'ArrowRight') m.nudge(c, e.key === 'ArrowLeft' ? -1 : 1, e.shiftKey);
|
||||
else m.setPower(c, c.power + (e.key === 'ArrowUp' ? 0.01 : -0.01));
|
||||
return;
|
||||
}
|
||||
@@ -623,6 +627,7 @@ function updateUi() {
|
||||
$('bocce-controls').hidden = !mg;
|
||||
$('bocce-controls').inert = !running;
|
||||
if (mg) {
|
||||
$('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);
|
||||
if ($('bocce-status').textContent !== line) $('bocce-status').textContent = line;
|
||||
@@ -644,6 +649,7 @@ function updateUi() {
|
||||
$('panel').inert = !!c;
|
||||
canvas.classList.toggle('torch', !!torch);
|
||||
canvas.classList.toggle('bocce', !!mg);
|
||||
$('stage').classList.toggle('biliardo', !!c && story.closeups[c.id].type === 'biliardo');
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user