La pesca al lago dell'Anatre: «S'ha andà a fallo?!?» coi ragazzi delle canne

Minigioco nuovo (idea di Andrea, 28 ottobre): parlando coi ragazzi in Fortezza si va a pescare. Primo piano del lago
(sfondo di Codex) con Andrea sulla riva con la canna di bambù (foglio di Codex: aspetta, lancia, recupera, mostra il
pesce) e i ragazzi accanto. Si tiene premuto per lanciare (più lontano, pesci più grossi: lasca, persico, cavedano,
tinca, carpa; sotto riva anche lo stivale, che non conta), il galleggiante ha tocchi finti e un'abboccata vera, si
ferra premendo in tempo e, senza lasciare, si recupera; il pesce strattona, la lenza si tende e bisogna mollare un
attimo o si spezza. Sei lanci, tre livelli (2/3/4 pesci per la gloria, finestra di ferrata e forza dei pesci).
Suoni di lancio, tuffo, tocco, abboccata, lenza, rottura e pesce a riva; stessi comandi delle bocce; test del
motore e prova nel browser. Revisione critica di Codex applicata nei punti gravi (galleggiante che saltava, sesto
lancio bloccato dal pannello, invito a ferrare fuori tempo, lenza che non si spezzava mai, mira che si sbloccava) e
in parecchi utili; il resto in docs/STATO.md. Nelle idee, le lavandaie del Palio come le racconta Andrea.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GiECdBYE8csKd4tcFvwntT
This commit is contained in:
2026-10-28 06:31:23 +01:00
co-authored by Claude Fable 5.1
parent f2a0f22f4a
commit 208e1d9535
36 changed files with 751 additions and 19 deletions
+2
View File
@@ -4,6 +4,7 @@ import { findPath, dist, inBox, boxDistance, nearestInside } from './geometry.js
import * as bocce from './bocce.js';
import * as biliardo from './biliardo.js';
import * as basket from './basket.js';
import * as pesca from './pesca.js';
import * as briscola from './briscola.js';
import * as tressette from './tressette.js';
import * as scopa from './scopa.js';
@@ -16,6 +17,7 @@ export const MINIGAMES = {
bocce: { create: (seed, level) => bocce.newBocce(seed, { level }), update: bocce.update, press: bocce.press, release: bocce.release, cancel: bocce.cancel, aim: bocce.aimScreen },
biliardo: { create: (seed, level) => biliardo.newBiliardo(seed, { level }), update: biliardo.update, press: biliardo.press, release: biliardo.release, cancel: biliardo.cancel, aim: biliardo.aimScreen },
basket: { create: (seed, level) => basket.newBasket(seed, { level }), update: basket.update, press: basket.press, release: basket.release, cancel: basket.cancel, aim: basket.aimScreen },
pesca: { create: (seed, level) => pesca.newPesca(seed, { level }), update: pesca.update, press: pesca.press, release: pesca.release, cancel: pesca.cancel, aim: pesca.aimScreen },
};
export const VERBS = ['walk', 'look', 'take', 'use', 'talk', 'open', 'give', 'drop'];
+125
View File
@@ -0,0 +1,125 @@
import { BANK, WATER, CASTS, left, fishCount, distanceFor, status } from './pesca.js';
import { text, panel } from './bocce-render.js';
import { ambientHash as hash } from './ambient.js';
const ANDREA = { x: 92, y: 258, scale: 0.68 }, BOYS = { x: 44, y: 262, scale: 0.5 };
// La punta della canna in ogni posa, rispetto ai piedi di Andrea (misurata sul foglio di Codex): la lenza parte da lì.
const TIPS = { hold: [60, -81], cast: [80, -57], reel: [43, -66], show: [60, -81] };
const ROD_TIP = { x: ANDREA.x + TIPS.hold[0], y: ANDREA.y + TIPS.hold[1] };
export function andreaPose(c) {
if (c.step === 'cast') return 'cast';
if (c.step === 'fight') return 'reel';
if (c.step === 'settle' && c.how === 'landed') return c.fish?.gag ? 'hold' : 'show';
return 'hold';
}
function sprite(ctx, sheet, pose, { x, y, scale }) {
if (!sheet) return;
const frame = sheet.atlas.frames[pose] ?? Object.values(sheet.atlas.frames)[0];
ctx.drawImage(sheet.img, frame.x, frame.y, frame.w, frame.h,
Math.round(x - frame.ax * scale), Math.round(y - frame.ay * scale), Math.round(frame.w * scale), Math.round(frame.h * scale));
}
// Dove sta la punta della canna: segue la posa, così la lenza parte sempre dalla cima.
function rodTip(c) {
const [dx, dy] = TIPS[andreaPose(c)] ?? TIPS.hold;
return { x: ANDREA.x + dx, y: ANDREA.y + dy };
}
function floatAt(c) {
const f = c.float;
if (!f) return null;
if (c.step === 'cast') {
// La parabola del lancio dalla punta della canna all'acqua.
const tip = rodTip(c), k = f.fly, x = tip.x + (f.x - tip.x) * k, y = tip.y + (WATER - tip.y) * k - Math.sin(k * Math.PI) * 60;
return { x, y };
}
return { x: f.x, y: WATER + f.dip + Math.sin(c.t * 2.2 + f.x) * 0.8 };
}
function fishShadow(ctx, c, p) {
const fish = c.fish; if (!fish || c.step !== 'fight') return;
const w = 10 + fish.weight * 9, h = 4 + fish.weight * 2.5, x = p.x + 8 + Math.sin(c.t * 3) * 6, y = WATER + 22 + Math.sin(c.t * 1.7) * 5;
ctx.fillStyle = 'rgba(20,40,38,0.55)';
ctx.beginPath(); ctx.ellipse(x, y, w / 2, h / 2, 0, 0, Math.PI * 2); ctx.fill();
ctx.beginPath(); ctx.moveTo(x + w / 2, y); ctx.lineTo(x + w / 2 + 5, y - 4); ctx.lineTo(x + w / 2 + 5, y + 4); ctx.fill();
}
function fishIcon(ctx, f, x, y) {
if (f.gag) { ctx.fillStyle = '#4a3a2a'; ctx.fillRect(x - 3, y - 5, 4, 8); ctx.fillRect(x - 3, y + 1, 8, 3); return; }
const w = 6 + f.weight * 3;
ctx.fillStyle = f.kind === 'carpa' ? '#c9a24a' : f.kind === 'tinca' ? '#5a7a3a' : f.kind === 'persico' ? '#d9b14a' : '#9fb3b8';
ctx.beginPath(); ctx.ellipse(x, y, w / 2, 2.5 + f.weight, 0, 0, Math.PI * 2); ctx.fill();
ctx.beginPath(); ctx.moveTo(x - w / 2, y); ctx.lineTo(x - w / 2 - 3, y - 3); ctx.lineTo(x - w / 2 - 3, y + 3); ctx.fill();
ctx.fillStyle = '#1a1a1a'; ctx.fillRect(Math.round(x + w / 2 - 3), Math.round(y - 1), 1, 1);
}
export function drawPesca(ctx, c, def, bg, sheet, boys) {
ctx.save();
if (bg) ctx.drawImage(bg, 0, 0, 640, 360);
else { ctx.fillStyle = '#5c7f7a'; ctx.fillRect(0, 0, 640, 360); ctx.fillStyle = '#7a9a4a'; ctx.fillRect(0, 240, BANK, 120); }
// I ragazzi sulla riva, dietro Andrea, intenti alle loro canne.
sprite(ctx, boys, 'work', BOYS);
sprite(ctx, sheet, andreaPose(c), ANDREA);
// I cerchi nell'acqua: al tuffo del galleggiante e quando il pesce abbocca.
for (const r of c.ripples) {
ctx.strokeStyle = `rgba(220,240,235,${0.6 * (1 - r.age / 1.6)})`; ctx.lineWidth = 1;
for (const k of [0, 0.4]) { const a = r.age - k; if (a < 0) continue; ctx.beginPath(); ctx.ellipse(r.x, WATER, 4 + a * 18, 1.5 + a * 5, 0, 0, Math.PI * 2); ctx.stroke(); }
}
// A lancio finito (pesce in mano, lenza rotta o vuota) niente galleggiante: la lenza è fuori dall'acqua.
const p = ['cast', 'wait', 'fight'].includes(c.step) ? floatAt(c) : null;
if (p) {
// La lenza, con un po' di pancia; tesa e tremante quando il pesce tira.
const tip = rodTip(c), sag = c.step === 'fight' ? 2 - c.tension * 2 : c.step === 'wait' ? 10 : 4;
// Trema già allo strattone, prima che la lenza si tenda: il tempo di mollare.
const shake = c.step === 'fight' && (c.tension > 0.75 || c.danger > 0.55) ? Math.sin(c.t * 60) * 1.2 : 0;
ctx.strokeStyle = c.step === 'fight' && c.tension > 0.85 ? '#ffb0a0' : 'rgba(240,240,230,0.85)'; ctx.lineWidth = 1;
ctx.beginPath(); ctx.moveTo(tip.x, tip.y); ctx.quadraticCurveTo((tip.x + p.x) / 2, Math.max(tip.y, p.y) + sag + shake, p.x, p.y); ctx.stroke();
fishShadow(ctx, c, p);
// Il galleggiante: rosso sopra, bianco sotto; sott'acqua si vede appena.
const under = Math.max(0, p.y - WATER);
ctx.fillStyle = '#e2e2d8'; ctx.fillRect(Math.round(p.x) - 1, Math.round(p.y) - 1, 3, 4);
ctx.fillStyle = '#d8402a'; ctx.fillRect(Math.round(p.x) - 1, Math.round(p.y) - 6, 3, 5);
if (under > 0) { ctx.fillStyle = 'rgba(40,70,80,0.55)'; ctx.fillRect(Math.round(p.x) - 2, WATER, 5, Math.round(under) + 1); }
}
// La mira: dove cadrà il galleggiante, e il filo tratteggiato della parabola.
if (c.step === 'aim' && c.casts < CASTS) {
const x = distanceFor(c.power);
ctx.strokeStyle = 'rgba(255,246,210,0.9)'; ctx.setLineDash([2, 4]);
ctx.beginPath(); ctx.moveTo(ROD_TIP.x, ROD_TIP.y); ctx.quadraticCurveTo((ROD_TIP.x + x) / 2, ROD_TIP.y - 70, x, WATER); ctx.stroke(); ctx.setLineDash([]);
ctx.beginPath(); ctx.ellipse(x, WATER, 7, 2.5, 0, 0, Math.PI * 2); ctx.stroke();
ctx.beginPath(); ctx.moveTo(x, WATER - 8); ctx.lineTo(x, WATER + 8); ctx.stroke();
}
panel(ctx, 182, 50, 440, 24);
text(ctx, status(c, def.names), 402, 63, 6, '#fff0cb', 'center');
// I lanci rimasti, il cestino dei pesci.
panel(ctx, 18, 86, 196, 58);
text(ctx, 'LANCI', 26, 98, 6, '#eed5a0');
for (let n = 0; n < CASTS; n++) {
const x = 70 + n * 12, y = 98;
if (n < left(c)) { ctx.fillStyle = '#d8402a'; ctx.fillRect(x - 1, y - 4, 3, 4); ctx.fillStyle = '#e2e2d8'; ctx.fillRect(x - 1, y, 3, 3); }
else { ctx.strokeStyle = '#766e5c'; ctx.strokeRect(x - 2, y - 3, 5, 6); }
}
text(ctx, 'CESTINO', 26, 116, 6, '#eed5a0');
c.caught.forEach((f, i) => fishIcon(ctx, f, 80 + i * 22, 116));
const fish = fishCount(c);
text(ctx, `${fish} su ${c.skill.goal} per la gloria · ${c.weight} kg`, 26, 133, 6, fish >= c.skill.goal ? '#b9e5b3' : '#d7ccb4');
// In basso a sinistra: la forza del lancio, o la tensione della lenza.
if (c.step === 'aim' || c.holding) {
panel(ctx, 18, 306, 176, 37);
text(ctx, `FORZA ${Math.round(c.power * 100)}%`, 30, 318, 7);
ctx.fillStyle = '#3e3e36'; ctx.fillRect(30, 331, 150, 5);
ctx.fillStyle = '#b9d392'; ctx.fillRect(30, 331, Math.round(c.power * 150), 5);
} else if (c.step === 'fight') {
panel(ctx, 18, 306, 176, 37);
text(ctx, `LENZA ${Math.round(c.tension * 100)}%`, 30, 318, 7, c.tension > 0.85 ? '#ffb0a0' : '#f4e3c2');
text(ctx, `riva ${Math.round((1 - Math.min(1, c.fish.dist)) * 100)}%`, 182, 318, 6, '#d7ccb4', 'right');
ctx.fillStyle = '#3e3e36'; ctx.fillRect(30, 331, 150, 5);
ctx.fillStyle = 'rgba(224,87,74,0.35)'; ctx.fillRect(30 + 127, 331, 23, 5); // la zona rossa: qui si molla
ctx.fillStyle = c.tension > 0.85 ? '#e0574a' : c.tension > 0.65 ? '#e0b24a' : '#b9d392'; ctx.fillRect(30, 331, Math.round(c.tension * 150), 5);
ctx.fillStyle = '#fff'; ctx.fillRect(30 + 127, 329, 1, 9);
}
if (c.step === 'result') {
panel(ctx, 206, 91, 397, 78);
text(ctx, c.result.winner === 'player' ? 'BELLA PESCATA!' : 'ACQUA CHETA', 404, 108, 9, '#efd49e', 'center');
text(ctx, `${c.result.fish} pesci su ${CASTS} lanci, ${c.result.weight} kg`, 404, 130, 9, c.result.winner === 'player' ? '#b9e5b3' : '#f1d9be', 'center');
text(ctx, c.result.winner === 'player' ? `Obiettivo ${c.result.goal}: fatto.` : `Per la gloria ne servivano ${c.result.goal}.`, 404, 148, 6, '#d6c7af', 'center');
c.result.caught.forEach((f, i) => fishIcon(ctx, f, 404 - (c.result.caught.length - 1) * 11 + i * 22, 160));
}
ctx.restore();
}
+199
View File
@@ -0,0 +1,199 @@
// La pesca al lago dell'Anatre coi ragazzi delle canne (idea di Andrea, 28 ottobre). Vista di lato, coordinate dello
// schermo (640x360): Andrea sulla riva a sinistra, l'acqua a destra. Si tiene premuto per lanciare (più forza, più
// lontano: e al largo i pesci sono più grossi); il galleggiante balla, ai tocchi finti non si abbocca, quando va sotto
// davvero si ferra premendo in tempo e, senza lasciare, si recupera: quando il pesce strattona la lenza si tende, e
// bisogna mollare un attimo. Sei lanci; la gloria dipende da quanti pesci si tirano a riva (LEVELS).
// Rivista con la revisione di Codex del 28 ottobre (galleggiante che saltava, sesto lancio bloccato dal pannello,
// invito a ferrare fuori tempo, lenza che non si spezzava mai, mira che si sbloccava da sola).
export const BANK = 150, WATER = 250, NEAR = 190, FAR = 600, SHORE = NEAR - 20;
export const CASTS = 6, SWING = 1.4, FLY = 0.8, REST = 1.4, WAIT_MAX = 45;
export const LEVELS = {
bona: { goal: 2, window: 1.0, pull: 0.5 },
seria: { goal: 3, window: 0.62, pull: 0.78 },
male: { goal: 4, window: 0.38, pull: 1.0 },
};
// I pesci del lago, dal più piccolo al più grosso: al largo si pescano i grossi. Lo stivale è la beffa della riva.
export const FISHES = [
{ kind: 'stivale', name: 'uno stivale', weight: 0.9, strength: 0.25, stamina: 0.3, far: [0, 0.25], gag: true },
{ kind: 'lasca', name: 'una lasca', weight: 0.15, strength: 0.3, stamina: 0.5, far: [0, 0.55] },
{ kind: 'persico', name: 'un persico sole', weight: 0.3, strength: 0.45, stamina: 0.7, far: [0.1, 0.7] },
{ kind: 'cavedano', name: 'un cavedano', weight: 0.8, strength: 0.6, stamina: 0.9, far: [0.3, 0.9] },
{ kind: 'tinca', name: 'una tinca', weight: 1.4, strength: 0.75, stamina: 1.1, far: [0.45, 1] },
{ kind: 'carpa', name: 'una carpa', weight: 3.2, strength: 1, stamina: 1.4, far: [0.7, 1] },
];
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; }
export function newPesca(seed = Date.now() >>> 0, def = {}) {
const level = LEVELS[def.level] ? def.level : 'seria';
return { step: 'aim', t: 0, age: 0, seed, level, skill: LEVELS[level], current: 'player', casts: 0, caught: [], weight: 0,
power: 0.5, holding: false, holdTime: 0, reeling: false, float: null, fish: null, tension: 0, pull: 0, events: [], ripples: [], last: null };
}
export const left = c => Math.max(0, CASTS - c.casts);
export const fishCount = c => c.caught.filter(f => !f.gag).length;
// Tre domande diverse: si può lanciare, si può agire (ferrare, recuperare), si può regolare la distanza.
export const canCast = c => c.step === 'aim' && !c.holding && c.casts < CASTS;
export const canThrow = c => canCast(c) || ((c.step === 'wait' || c.step === 'fight') && !c.holding);
export const canAdjust = c => c.step === 'aim' && !c.holding && c.casts < CASTS;
export const aimFree = c => c.step !== 'aim';
export const powerAt = time => clamp(time / SWING, 0, 1);
export const distanceFor = power => NEAR + (FAR - NEAR) * clamp(power, 0, 1);
export function setPower(c, value) {
if (!canAdjust(c) || !Number.isFinite(value)) return false;
c.power = clamp(value, 0, 1); return true;
}
// Un tocco sull'acqua (non sul cielo, non sulla riva) sceglie dove lanciare: la forza che ci vuole per arrivarci.
export function aimScreen(c, [x, y]) {
if (!canAdjust(c) || !Number.isFinite(x) || !Number.isFinite(y) || x < BANK + 10 || y < WATER - 40) return false;
return setPower(c, (x - NEAR) / (FAR - NEAR));
}
export const nudge = (c, dir, fine = false) => setPower(c, c.power + dir * (fine ? 0.01 : 0.05));
function event(c, kind, strength = 0.5) { if (c.events.length < 12) c.events.push({ kind, power: clamp(strength, 0, 1) }); }
// Quando arriva il pesce: da uno a tre tocchi finti, a ritmo diverso, poi quello vero. Al largo si aspetta un po' di
// più; dopo un'abboccata mancata l'attesa è la metà.
function schedule(c) {
const f = c.float, far = (f.x - NEAR) / (FAR - NEAR);
f.biteAt = f.t + (2.5 + random(c) * 4 + far * 2) * (f.missed ? 0.5 : 1);
const n = 1 + Math.floor(random(c) * 3);
f.nibbles = Array.from({ length: n }, (_, i) => ({ at: f.biteAt - (n - i) * (0.6 + random(c) * 0.9), for: 0.18 + random(c) * 0.2 }));
f.bitten = false;
}
function pickFish(c, far) {
const pool = FISHES.filter(f => far >= f.far[0] && far <= f.far[1] && (!f.gag || random(c) < 0.35));
const f = pool[Math.floor(random(c) * pool.length)] ?? FISHES[1];
const size = 0.8 + random(c) * 0.5; // non tutti i cavedani sono uguali
return { ...f, weight: Math.round(f.weight * size * 100) / 100, stamina: f.stamina * size, dist: 1, energy: 1, burst: random(c) * 6,
reel: 0.22 / (0.7 + f.stamina * size) }; // i piccoli vengono a riva in fretta, le carpe no
}
export function cast(c) {
if (!canCast(c)) return false;
c.float = { x: distanceFor(c.power), y: WATER, t: 0, total: 0, fly: 0, dip: 0, missed: 0, from: null };
c.step = 'cast'; c.t = 0; c.casts++; c.fish = null; c.tension = 0; c.pull = 0; c.reeling = false; c.last = null;
event(c, 'pesca-lancio', c.power);
return true;
}
// Il tasto dei comandi: in attesa ferra; nel recupero alterna recupera e molla (col dito si tiene premuto da sé).
export function shoot(c) {
if (c.step === 'aim') return cast(c);
if (c.step === 'wait') return strike(c);
if (c.step === 'fight') { c.reeling = !c.reeling; return true; }
return false;
}
// Un gesto solo: premendo si ferra e, se il pesce c'è, si comincia subito a recuperare; lasciando si molla.
export function press(c) {
if (c.step === 'aim') { if (!canCast(c)) return false; c.previousPower = c.power; c.holding = true; c.holdTime = c.power = 0; return true; }
if (c.step === 'wait') { strike(c); if (c.step === 'fight') c.reeling = true; return true; }
if (c.step === 'fight') { c.reeling = true; return true; }
return false;
}
export function cancel(c) {
if (c.holding) { c.holding = false; c.power = c.previousPower; c.holdTime = 0; return true; }
if (c.reeling) { c.reeling = false; return true; }
return false;
}
export function release(c) {
if (c.holding) {
if (c.holdTime < 0.14) return cancel(c); // un tocco breve non spreca il lancio
c.holding = false; return cast(c);
}
if (c.reeling) { c.reeling = false; return true; }
return false;
}
export const biting = c => c.step === 'wait' && c.float?.bitten && c.float.t - c.float.biteAt <= c.skill.window;
// Ferrare: dentro la finestra il pesce è preso; prima, o dopo, si recupera la lenza vuota (lancio sprecato).
export function strike(c) {
const f = c.float;
if (c.step !== 'wait' || !f) return false;
if (biting(c)) {
c.fish = pickFish(c, (f.x - NEAR) / (FAR - NEAR));
f.from = f.x;
c.step = 'fight'; c.t = 0; c.tension = 0.3; c.reeling = false; c.danger = 0;
event(c, 'pesca-ferrata', 0.8);
} else {
const late = f.bitten && f.t > f.biteAt;
const touched = f.nibbles.some(n => f.t >= n.at);
c.last = { kind: late ? 'late' : touched ? 'nibble' : 'early',
text: late ? 'Troppo tardi: l\'ha già sputato.' : touched ? 'Era solo un tocco: a tirare così l\'ho spaventato.' : 'Ho tirato a vuoto: non aveva ancora abboccato.' };
event(c, 'pesca-vuoto', 0.4);
end(c, 'empty');
}
return true;
}
function end(c, how) {
if (how === 'landed') {
c.caught.push(c.fish);
if (!c.fish.gag) c.weight = Math.round((c.weight + c.fish.weight) * 100) / 100;
event(c, 'pesca-preso', 0.9);
}
if (how === 'broken') event(c, 'pesca-rottura', 1);
if (how === 'free') event(c, 'pesca-vuoto', 0.6);
c.step = 'settle'; c.t = 0; c.how = how; c.reeling = false; c.pull = 0;
// L'ultimo lancio decide subito il risultato: uscire durante la posa non è abbandonare.
if (c.casts >= CASTS) finish(c);
}
function finish(c) {
c.result = { winner: fishCount(c) >= c.skill.goal ? 'player' : 'cpu', fish: fishCount(c), weight: c.weight, goal: c.skill.goal, caught: c.caught };
}
export function update(c, dt) {
if (!Number.isFinite(dt) || dt <= 0) return;
dt = Math.min(dt, 0.25); c.t += dt;
if (c.holding) { c.holdTime += dt; c.power = powerAt(c.holdTime); }
const f = c.float;
if (c.step === 'cast') {
f.fly = Math.min(1, c.t / FLY);
if (c.t >= FLY) { c.step = 'wait'; f.t = 0; schedule(c); c.ripples.push({ x: f.x, age: 0 }); event(c, 'pesca-tuffo', 0.4); }
} else if (c.step === 'wait') {
f.t += dt; f.total += dt;
const nibbling = f.nibbles.some(n => f.t >= n.at && f.t < n.at + n.for);
if (!f.bitten && f.t >= f.biteAt) { f.bitten = true; event(c, 'pesca-abbocca', 0.6); c.ripples.push({ x: f.x, age: 0 }); }
const since = f.bitten ? f.t - f.biteAt : -1;
if (f.bitten && since > c.skill.window + 0.4) {
// Non ferrato in tempo: il pesce se ne va, si riprova, ma non all'infinito.
f.missed++; f.t = 0; schedule(c);
if (f.missed >= 3) { c.last = { kind: 'gone', text: 'Tre abboccate e niente: qui non abbocca più nessuno.' }; end(c, 'empty'); }
} else if (nibbling) { if (!f.nibbleSound) { event(c, 'pesca-tocco', 0.3); f.nibbleSound = true; } }
else f.nibbleSound = false;
// Sotto finché si può ferrare; poi riaffiora mentre il pesce lo sputa; i tocchi lo abbassano appena.
f.dip = biting(c) ? 7 : f.bitten && since <= c.skill.window + 0.4 ? 3 : nibbling ? 2.5 : 0;
if (f.total > WAIT_MAX) { c.last = { kind: 'gone', text: 'Niente. Qui i pesci dormono.' }; end(c, 'empty'); }
} else if (c.step === 'fight') {
const fish = c.fish, k = c.skill.pull;
fish.burst += dt;
// Gli strattoni: brevi, a picco; la lenza tesa li sente poco dopo, e c'è il tempo di mollare.
const burst = 0.35 + 0.95 * Math.max(0, Math.sin(fish.burst * 2.1)) ** 3, peak = fish.strength * k * (0.3 + 0.7 * fish.energy);
c.pull = peak * burst;
c.danger = burst / 1.3; // quanto è forte lo strattone rispetto al massimo di questo pesce: la lenza trema prima
const target = c.reeling ? 0.42 + c.pull * 1.1 : c.pull * 0.3;
c.tension += (target - c.tension) * Math.min(1, dt * 6.5);
if (c.reeling) {
fish.dist -= dt * fish.reel * (1 - c.pull * 0.4); fish.energy = Math.max(0, fish.energy - dt * 0.09 / fish.stamina);
// Il cigolio della lenza tesa, non a ogni fotogramma.
c.reelSound = (c.reelSound ?? 0) - dt;
if (c.tension > 0.6 && c.reelSound <= 0) { event(c, 'pesca-mulinello', c.tension); c.reelSound = 0.14; }
} else { fish.dist += dt * (0.02 + 0.05 * c.pull); fish.energy = Math.max(0, fish.energy - dt * 0.03 / fish.stamina); }
f.x = SHORE + fish.dist * (f.from - SHORE);
f.dip = 5 + Math.sin(c.t * 7) * 2;
if (c.tension >= 1) { c.last = { kind: 'broken', text: 'Crac! La lenza non ha retto.' }; end(c, 'broken'); }
else if (fish.dist >= 1.35) { c.last = { kind: 'free', text: 'Ha preso troppo filo: si è slamato.' }; end(c, 'free'); }
else if (fish.dist <= 0) {
c.last = { kind: 'landed', text: fish.gag ? 'Uno stivale. Numero quarantatré. Di chi sarà?' : `${fish.name[0].toUpperCase()}${fish.name.slice(1)}, ${fish.weight} kg!` };
end(c, 'landed');
}
} else if (c.step === 'settle' && c.t >= REST) {
c.float = null; c.fish = null; c.tension = 0;
if (c.casts >= CASTS) c.step = 'result'; else { c.step = 'aim'; c.t = 0; }
}
for (const r of c.ripples) r.age += dt;
c.ripples = c.ripples.filter(r => r.age < 1.6);
}
export function status(c, names = { player: 'Andrea' }) {
const tally = `Pesci ${fishCount(c)} (ne servono ${c.skill.goal}), ${c.weight} kg.`;
if (c.step === 'result') return `${c.result.fish} pesci su ${CASTS} lanci, ${c.result.weight} kg. ${c.result.winner === 'player' ? `Obiettivo ${c.result.goal}: fatto!` : `Ne servivano ${c.result.goal}.`}`;
if (c.step === 'cast') return 'Lancio…';
if (c.step === 'wait') return biting(c) ? 'ABBOCCA! Ferra!' : c.float.bitten ? 'Lo sta sputando…' : c.float.dip > 0 ? 'Un tocco… aspetta.' : c.float.missed ? `Riprova: ${3 - c.float.missed} abboccate ancora.` : 'Il galleggiante è fermo. Si aspetta.';
if (c.step === 'fight') return c.tension > 0.85 ? 'La lenza tira troppo: MOLLA!' : c.danger > 0.55 ? 'Strattona…' : c.reeling ? 'Recupera…' : 'Tieni premuto per recuperare.';
if (c.step === 'settle') return c.last?.text ?? '';
return `${names.player}, lancio ${c.casts + 1} di ${CASTS}. ${tally}`;
}
export const button = c => c.step === 'result' ? 'Finita' : c.step === 'wait' ? 'Ferra' : c.step === 'fight' ? (c.reeling ? 'Molla' : 'Recupera') : c.step === 'aim' ? 'Lancia' : '…';
+6
View File
@@ -6,6 +6,7 @@ import { drawTorch } from './torch.js';
import { drawBocce } from './bocce-render.js';
import { drawBiliardo } from './biliardo-render.js';
import { drawBasket } from './basket-render.js';
import { drawPesca } from './pesca-render.js';
import { drawBriscola } from './briscola-render.js';
import { drawTressette } from './tressette-render.js';
import { drawScopa } from './scopa-render.js';
@@ -683,6 +684,11 @@ function drawCloseup(ctx, game, time) {
drawBocce(ctx, c, def, backgrounds.get(def.picture), time);
return;
}
if (def.type === 'pesca') {
if (!backgrounds.has(def.picture)) loadBackground(def.picture);
drawPesca(ctx, c, def, backgrounds.get(def.picture), sprites.get('andrea_pesca'), sprites.get('pescatori'));
return;
}
if (def.type === 'basket') {
if (!backgrounds.has(def.picture)) loadBackground(def.picture);
drawBasket(ctx, c, def, backgrounds.get(def.picture), sprites.get('andrea_basket'));
+10
View File
@@ -125,6 +125,16 @@ export class Soundscape {
else if (kind === 'basket-board') { this.tone(280, 170, 0.09, 0.05 + strength * 0.08, 0, pan); this.noiseSound(0.07, 900, 0.07, { pan }); }
else if (kind === 'basket-swish') this.noiseSound(0.28, 3200, 0.1, { paper: true, pan });
else if (kind === 'basket-in') { this.tone(1300, 1000, 0.08, 0.04, 0, pan); this.noiseSound(0.22, 2800, 0.08, { paper: true, pan }); }
// La pesca: il lancio, il tuffo del galleggiante, il tocco, l'abboccata, la ferrata, la lenza che tira, lo strappo, il pesce a riva.
else if (kind === 'pesca-lancio') this.noiseSound(0.22, 1800, 0.05 + strength * 0.06, { pan });
else if (kind === 'pesca-tuffo') { this.tone(420, 180, 0.07, 0.07, 0, pan); this.noiseSound(0.12, 900, 0.06, { pan }); }
else if (kind === 'pesca-tocco') this.tone(900, 700, 0.04, 0.03, 0, pan);
else if (kind === 'pesca-abbocca') { this.tone(520, 260, 0.1, 0.08, 0, pan); this.noiseSound(0.1, 1100, 0.06, { pan }); }
else if (kind === 'pesca-ferrata') this.noiseSound(0.1, 2600, 0.08, { pan });
else if (kind === 'pesca-mulinello') this.tone(1800 + strength * 900, 1700 + strength * 900, 0.05, 0.015 + strength * 0.02, 0, pan);
else if (kind === 'pesca-rottura') { this.tone(2400, 300, 0.12, 0.1, 0, pan); this.noiseSound(0.08, 3000, 0.06, { pan }); }
else if (kind === 'pesca-vuoto') this.noiseSound(0.14, 1400, 0.05, { pan });
else if (kind === 'pesca-preso') { this.noiseSound(0.25, 1000, 0.1, { pan }); this.tone(660, 990, 0.18, 0.05, 0.1, pan); }
else if (kind === 'wood') this.noiseSound(0.3, 750, 0.13, { pan });
else if (kind === 'tavolo-lontano') {
// Su un altro tavolo: il colpo di stecca, poi la biglia che ne prende un'altra, piano, da lontano.