import test from 'node:test'; import assert from 'node:assert/strict'; import { newPesca, press, release, cancel, update, cast, strike, shoot, setPower, aimScreen, nudge, status, canThrow, canCast, canAdjust, distanceFor, biting, CASTS, LEVELS, FISHES, NEAR, FAR, BANK, SHORE, WATER, FLY, REST } from '../src/engine/pesca.js'; import { story } from '../src/story/story.js'; import { Game } from '../src/engine/game.js'; import { settle } from './helpers.mjs'; // Come il gioco: a ogni fotogramma gli eventi sonori si consumano (e qui si ricordano). const heard = new Set(); const run = (c, seconds, dt = 1 / 60) => { for (let t = 0; t < seconds; t += dt) { update(c, dt); for (const e of c.events.splice(0)) heard.add(e.kind); } }; // Lancia e aspetta l'abboccata vera (non i tocchi), poi ferra: la mano ferma dei test. function hook(c) { assert.ok(cast(c)); run(c, FLY + 0.05); assert.equal(c.step, 'wait'); for (let i = 0; i < 60 * 15 && !c.float.bitten; i++) update(c, 1 / 60); assert.ok(c.float.bitten, 'prima o poi abbocca'); assert.ok(strike(c)); assert.equal(c.step, 'fight'); } // Recupera con giudizio: tiene premuto finché la lenza non è quasi al limite, poi molla un attimo. function land(c) { for (let i = 0; i < 60 * 90 && c.step === 'fight'; i++) { if (c.tension > 0.8 && c.reeling) release(c); else if (c.tension < 0.55 && !c.reeling) press(c); update(c, 1 / 60); for (const e of c.events.splice(0)) heard.add(e.kind); } assert.notEqual(c.step, 'fight', 'il recupero finisce'); } test('la forza sale finché si tiene premuto; un tocco breve non spreca il lancio; dove si tocca l\'acqua si lancia', () => { const c = newPesca(1); assert.ok(press(c)); update(c, 0.05); assert.ok(release(c)); assert.equal(c.casts, 0); assert.equal(c.power, 0.5); assert.ok(aimScreen(c, [FAR, 250])); assert.equal(c.power, 1); assert.ok(aimScreen(c, [NEAR, 250])); assert.equal(c.power, 0); assert.ok(!aimScreen(c, [BANK - 20, 250]), 'sulla riva non si lancia'); assert.ok(!aimScreen(c, [400, 60]), 'il cielo non è acqua'); assert.ok(nudge(c, 1)); assert.ok(Math.abs(c.power - 0.05) < 1e-9); setPower(c, 0.5); assert.ok(press(c)); update(c, 0.7); assert.ok(release(c)); assert.equal(c.casts, 1); assert.equal(c.step, 'cast'); assert.ok(Math.abs(c.float.x - distanceFor(c.power)) < 1e-9); assert.ok(c.events.some(e => e.kind === 'pesca-lancio')); assert.ok(!setPower(c, 0.2), 'a lenza in acqua la forza non si tocca'); }); test('il galleggiante: i tocchi finti abbassano appena, l\'abboccata lo porta sotto; ferrare a vuoto perde il lancio', () => { const c = newPesca(2); cast(c); run(c, FLY + 0.05); const f = c.float; assert.ok(f.nibbles.length >= 1 && f.nibbles.every(n => n.at < f.biteAt), 'i tocchi vengono prima'); assert.ok(strike(c)); assert.equal(c.step, 'settle'); assert.equal(c.how, 'empty'); assert.match(c.last.text, /a vuoto|spaventato/); run(c, REST + 0.1); assert.equal(c.step, 'aim'); assert.equal(c.casts, 1); const d = newPesca(3); cast(d); run(d, FLY + 0.05); const g = d.float; for (let i = 0; i < 60 * 15 && !(g.t >= g.nibbles[0].at && g.t < g.nibbles[0].at + g.nibbles[0].for); i++) update(d, 1 / 60); assert.ok(g.dip > 0 && g.dip < 5, `un tocco finto: ${g.dip}`); for (let i = 0; i < 60 * 15 && !g.bitten; i++) update(d, 1 / 60); assert.equal(g.dip, 7, 'abboccata: sotto'); assert.ok(d.events.some(e => e.kind === 'pesca-abbocca')); }); test('non ferrata in tempo, l\'abboccata passa; alla terza il pesce se ne va', () => { const c = newPesca(4, { level: 'male' }); cast(c); run(c, FLY + 0.05); for (let i = 0; i < 60 * 15 && !c.float.bitten; i++) update(c, 1 / 60); // Il segnale e la finestra coincidono: finché dice «Ferra!» si ferra; dopo, lo sta sputando. assert.ok(biting(c) && c.float.dip === 7 && /Ferra!/.test(status(c))); run(c, c.skill.window + 0.05); assert.ok(!biting(c) && c.float.bitten && c.float.dip === 3 && /sputando/.test(status(c))); const late = structuredClone(c); strike(late); assert.equal(late.last.kind, 'late'); assert.match(late.last.text, /tardi/); run(c, 0.5); assert.ok(!c.float.bitten && c.float.missed === 1, 'persa la prima'); assert.equal(c.step, 'wait'); assert.match(status(c), /2 abboccate ancora/); run(c, 60); assert.equal(c.how, 'empty', 'dopo tre, o dopo quaranta secondi, si ricupera a vuoto'); assert.equal(c.last.kind, 'gone'); assert.equal(c.step, 'aim'); }); test('ferrato in tempo si combatte: tenendo premuto la lenza tira e il pesce si avvicina, mollando si allontana; troppo tesa si spezza', () => { const c = newPesca(5, { level: 'bona' }); setPower(c, 0.5); hook(c); assert.ok(c.fish && c.fish.dist === 1); const x0 = c.float.x; press(c); run(c, 1.5); assert.ok(c.float.x < x0 && c.float.x > SHORE, `il galleggiante viene verso riva senza salti: ${x0} → ${c.float.x}`); assert.ok(c.fish.dist < 1 && c.tension > 0.3, 'recuperando si avvicina'); const d0 = c.fish.dist; release(c); run(c, 1.5); assert.ok(c.fish.dist > d0 - 0.001, 'mollando non si avvicina'); land(c); assert.equal(c.how, 'landed', 'con giudizio si tira a riva'); assert.equal(c.caught.length, 1); assert.ok(c.weight > 0); assert.ok(heard.has('pesca-preso')); // A fassi male, una carpa al largo tenuta sempre tesa spezza la lenza. const d = newPesca(6, { level: 'male' }); setPower(d, 1); hook(d); d.fish = { ...FISHES.at(-1), dist: 1, energy: 1, burst: 0, reel: 0.11 }; press(d); run(d, 8); assert.equal(d.how, 'broken', `invece: ${d.step} ${d.how}`); assert.ok(heard.has('pesca-rottura')); }); test('un gesto solo: premendo all\'abboccata si ferra e si recupera; il pannello alterna recupera e molla; il sesto lancio si può finire', () => { const c = newPesca(21, { level: 'bona' }); setPower(c, 0.3); cast(c); run(c, FLY + 0.05); for (let i = 0; i < 60 * 15 && !c.float.bitten; i++) update(c, 1 / 60); assert.ok(press(c)); assert.equal(c.step, 'fight'); assert.ok(c.reeling, 'ferrato e già in recupero'); assert.ok(release(c)); assert.ok(!c.reeling); assert.ok(shoot(c)); assert.ok(c.reeling, 'dal pannello: recupera'); assert.ok(shoot(c)); assert.ok(!c.reeling, 'dal pannello: molla'); const d = newPesca(22); d.casts = CASTS - 1; assert.ok(canCast(d)); cast(d); run(d, FLY + 0.05); assert.ok(!canCast(d) && canThrow(d), 'al sesto lancio si può ancora ferrare'); assert.ok(!canAdjust(d), 'ma non regolare la distanza'); for (let i = 0; i < 60 * 15 && !d.float.bitten; i++) update(d, 1 / 60); assert.ok(press(d)); assert.equal(d.step, 'fight'); assert.ok(canThrow(d), 'e recuperare'); d.fish = { ...FISHES[1], dist: 1, energy: 1, burst: 0, reel: 0.2 }; land(d); assert.equal(d.how, 'landed'); assert.ok(d.result, 'il risultato è deciso appena il pesce è a riva, prima della posa'); run(d, REST + 0.1); assert.equal(d.step, 'result'); }); test('la lenza si spezza davvero: tenendo sempre premuto, a seria un cavedano o più grosso la rompe, una lasca no', () => { const fight = (kind, level) => { const c = newPesca(23, { level }); setPower(c, 0.5); hook(c); c.fish = { ...FISHES.find(f => f.kind === kind), dist: 1, energy: 1, burst: 0, reel: 0.15 }; press(c); run(c, 20); return c.how; }; assert.equal(fight('lasca', 'seria'), 'landed'); assert.equal(fight('cavedano', 'seria'), 'broken'); assert.equal(fight('carpa', 'bona'), 'broken'); assert.equal(fight('cavedano', 'bona'), 'landed'); // Mollata e ripresa, la stessa carpa viene a riva. const c = newPesca(24, { level: 'male' }); setPower(c, 1); hook(c); c.fish = { ...FISHES.at(-1), dist: 1, energy: 1, burst: 0, reel: 0.11 }; land(c); assert.equal(c.how, 'landed'); }); test('al largo i pesci sono più grossi; lo stivale solo sotto riva e non conta per la gloria', () => { const near = new Set(), far = new Set(); for (let seed = 1; seed < 40; seed++) { for (const [power, set] of [[0, near], [1, far]]) { const c = newPesca(seed); setPower(c, power); hook(c); set.add(c.fish.kind); } } assert.ok(far.has('carpa') || far.has('tinca'), `al largo: ${[...far]}`); assert.ok(!near.has('carpa'), 'sotto riva niente carpe'); assert.ok(!far.has('stivale'), 'lo stivale sta sotto riva'); const c = newPesca(7, { level: 'bona' }); c.caught = [{ ...FISHES[0] }, { ...FISHES[1] }]; c.weight = 1; assert.match(status(c), /Pesci 1 /, 'lo stivale non è un pesce'); const d = newPesca(25); setPower(d, 0); hook(d); d.fish = { ...FISHES[0], dist: 1, energy: 1, burst: 0, reel: 0.3 }; land(d); assert.equal(d.weight, 0, 'e non pesa'); assert.match(d.last.text, /stivale/); }); test('sei lanci, poi il risultato: la gloria dipende dal livello', () => { for (const [level, { goal }] of Object.entries(LEVELS)) { const c = newPesca(8, { level }), fishes = []; for (let i = 0; i < CASTS; i++) { assert.ok(canThrow(c)); // Finché serve si pesca per davvero (con una lasca docile sotto riva), poi si ricupera a vuoto. if (c.caught.length < goal) { setPower(c, 0); hook(c); c.fish = { ...FISHES[1], dist: 1, energy: 1, burst: 0, reel: 0.2 }; land(c); assert.equal(c.how, 'landed'); } else { cast(c); run(c, FLY + 0.05); strike(c); } run(c, REST + 0.1); } assert.equal(c.step, 'result'); assert.ok(!canThrow(c)); assert.ok(!cast(c)); assert.equal(c.result.fish, goal); assert.equal(c.result.winner, 'player', level); assert.match(status(c), /pesci su 6 lanci/); } const miss = newPesca(9, { level: 'bona' }); for (let i = 0; i < CASTS; i++) { cast(miss); run(miss, FLY + 0.05); strike(miss); run(miss, REST + 0.1); } assert.equal(miss.result.winner, 'cpu'); }); test('nella storia: coi ragazzi in Fortezza «S\'ha andà a fallo?!?», loro accettano, si sceglie e si pesca; la gloria lascia il flag', () => { const g = new Game(story); g.go('fortezza', [240, 334]); settle(g); g.act('talk', 'pescatori'); const said = settle(g).map(s => s.text); assert.ok(said.some(t => t.includes('mia mamma aveva ragione')), 'la prima volta le canne'); assert.ok(g.dialogue, 'poi si parla'); g.choose(g.options().findIndex(o => o.text === 'S\'ha andà a fallo?!?')); const yes = settle(g).map(s => `${s.who ?? 'andrea'}: ${s.text}`); assert.ok(yes.includes('pescatori: S\'ha andà! La canna te la diamo noi.')); g.choose(g.options().findIndex(o => o.text === 'Seria.')); settle(g); assert.equal(g.closeup?.id, 'pesca'); assert.equal(g.closeup.level, 'seria'); g.closeup.result = { winner: 'player', fish: 3, weight: 2.1, goal: 3, caught: [] }; g.closeup.step = 'result'; g.closeupClose(); const after = settle(g).map(s => s.text); assert.ok(g.state.flags.pesca_vinta); assert.ok(after.some(t => t.includes('Stasera si frigge'))); // La seconda volta si va dritti al dialogo. g.act('talk', 'pescatori'); settle(g); assert.ok(g.dialogue); });