// 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' : '…';