Il Regolo del futuro: alla SUPER SPECIAL un'esplosione a sorpresa apre la tavola futuristica (Orbita del Serpente, Ponte della Fortezza sopraelevato, Spira al Neon, Regolo mobile); si torna alla classica colpendo il Regolo dopo due giri per percorso (Codex)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GiECd9RWGMKV7n3K1mdy8T
This commit is contained in:
+128
-18
@@ -316,13 +316,16 @@ test('due LOCK aprono la SUPER SPECIAL: jackpot, pallina premio e multiball real
|
||||
assert.equal(m.activeBalls(c).length, 3); assert.ok(m.multiball(c));
|
||||
assert.equal(new Set(m.activeBalls(c)).size, 3, 'tre palline distinte');
|
||||
assert.ok(c.events.some(e => e.kind === 'flipper-super'));
|
||||
const winScore = c.score, lives = c.balls;
|
||||
rampShot(c, ids[0]);
|
||||
assert.ok(c.score - winScore >= m.SCORE.ramp + m.SCORE.jackpot, 'in multiball le rampe pagano jackpot');
|
||||
assert.ok(c.transform, 'la SUPER avvia la trasformazione');
|
||||
run(c, m.FUTURE.duration);
|
||||
assert.equal(c.table, 'future');
|
||||
const lives = c.balls;
|
||||
for (const b of m.activeBalls(c)) b.releaseAt = 0;
|
||||
// Metti tutte le palline sotto il campo nello stesso passo: la vita si scala una volta soltanto.
|
||||
for (const ball of m.activeBalls(c)) Object.assign(ball, { x: 320, y: 348, vx: 0, vy: 300, ramp: null, waiting: false, releaseAt: 0 });
|
||||
c.saveUntil = 0; m.update(c, m.STEP);
|
||||
assert.equal(c.balls, lives - 1); assert.equal(c.extraBalls.length, 0); assert.ok(m.waiting(c));
|
||||
assert.equal(c.table, 'classic', 'persa l’ultima si torna al River');
|
||||
}
|
||||
});
|
||||
|
||||
@@ -338,25 +341,28 @@ test('la finestra LOCK scade in simulazione, cambiare rampa interrompe la serie
|
||||
assert.equal(c.rampRun.count, 0);
|
||||
});
|
||||
|
||||
test('la SUPER accompagna alla paletta sinistra e il multiball libera le palline sfalsate', () => {
|
||||
test('la trasformazione protegge le tre palline, poi le libera sfalsate verso le palette', () => {
|
||||
for (const level of Object.keys(m.LEVELS)) {
|
||||
const c = m.newFlipper(77, { level }); doubleLock(c); rampShot(c, 'super');
|
||||
const main = c.ball, delayed = c.extraBalls.find(b => b.releaseAt > c.sim);
|
||||
assert.equal(main.x, 229); assert.equal(main.y, 220); assert.equal(main.vx, 0); assert.equal(main.vy, 160);
|
||||
for (const b of c.extraBalls) { assert.ok([229, 411].includes(b.x)); assert.equal(b.vx, 0, 'i lock non sparano verso le buche laterali'); }
|
||||
assert.ok(delayed, 'una serratura aspetta prima di liberare la terza pallina');
|
||||
const position = [delayed.x, delayed.y]; run(c, 0.2);
|
||||
assert.deepEqual([delayed.x, delayed.y], position, 'durante il ritardo resta nel lock');
|
||||
run(c, 0.3); assert.notDeepEqual([delayed.x, delayed.y], position, 'poi viene liberata');
|
||||
// Isola la pallina della SUPER per verificare che un colpo normale possa recuperarla.
|
||||
c.extraBalls = []; c.ball = main;
|
||||
const before = m.activeBalls(c).map(b => [b.x, b.y]), score = c.score, lives = c.balls;
|
||||
run(c, 0.8); assert.deepEqual(m.activeBalls(c).map(b => [b.x, b.y]), before);
|
||||
assert.equal(c.score, score); assert.equal(c.balls, lives);
|
||||
run(c, m.FUTURE.duration - 0.8);
|
||||
assert.equal(c.table, 'future'); assert.equal(c.transform, null);
|
||||
const main = c.ball, delayed = c.extraBalls.at(-1), position = [delayed.x, delayed.y];
|
||||
assert.ok(delayed.releaseAt > c.sim);
|
||||
run(c, 0.2); assert.deepEqual([delayed.x, delayed.y], position);
|
||||
// Isola la prima pallina per verificare il recupero dalla paletta, senza alterare la geometria.
|
||||
const d = structuredClone(c); d.extraBalls = [];
|
||||
let recovered = false;
|
||||
for (let i = 0; i < 480; i++) {
|
||||
if (main.x > 284 && main.y > 300) m.hold(c, 'left', 'test', true);
|
||||
m.update(c, m.STEP);
|
||||
if (main.vy < -250) { recovered = true; break; }
|
||||
if (d.ball.x > 284 && d.ball.y > 300) m.hold(d, 'left', 'test', true);
|
||||
m.update(d, m.STEP);
|
||||
if (d.ball.vy < -250) { recovered = true; break; }
|
||||
}
|
||||
assert.ok(recovered, `${level}: ripresa dalla paletta, senza cadere direttamente nella buca centrale`);
|
||||
assert.ok(recovered, `${level}: prima pallina ripresa dalla paletta`);
|
||||
run(c, 1); assert.notDeepEqual([delayed.x, delayed.y], position);
|
||||
assert.ok(Number.isFinite(main.x));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -373,7 +379,8 @@ test('perdere una sostitutiva restituisce un lock alla volta senza consumare vit
|
||||
});
|
||||
|
||||
test('il multiball ha contatti indipendenti, urti fra palline e palette che avanzano una sola volta', () => {
|
||||
const c = m.newFlipper(24); doubleLock(c); rampShot(c, 'super');
|
||||
const c = m.newFlipper(24); doubleLock(c); rampShot(c, 'super'); run(c, m.FUTURE.duration);
|
||||
for (const b of m.activeBalls(c)) b.releaseAt = 0;
|
||||
const single = m.newFlipper(24); m.hold(single, 'left', 'test', true); m.hold(c, 'left', 'test', true);
|
||||
m.update(single, m.STEP); m.update(c, m.STEP);
|
||||
assert.ok(Math.abs(c.flippers.left.angle - single.flippers.left.angle) < 1e-10);
|
||||
@@ -473,3 +480,106 @@ test('dietro la Pieve non c\'è più il canale che portava dritto all\'uscita si
|
||||
assert.ok(!inChannel, 'mai fra la parete e il dorso dei bersagli');
|
||||
assert.ok(c.ball.x > 210 || c.ball.y < 146, `in campo (${Math.round(c.ball.x)}, ${Math.round(c.ball.y)})`);
|
||||
});
|
||||
|
||||
function inFuture(level = 'seria') {
|
||||
const c = m.newFlipper(96, { level }); doubleLock(c); rampShot(c, 'super'); run(c, m.FUTURE.duration);
|
||||
// Gli altri tiri restano parcheggiati mentre si misura un percorso alla volta.
|
||||
for (const b of c.extraBalls) b.releaseAt = c.sim + 1000;
|
||||
c.ball.releaseAt = 0; return c;
|
||||
}
|
||||
|
||||
test('il futuro è un altro campo: tre giri reali, quote, punti e sigilli solo a percorso completo', () => {
|
||||
for (const level of Object.keys(m.LEVELS)) {
|
||||
const c = inFuture(level), old = m.layout();
|
||||
assert.notDeepEqual(c.walls, old.walls); assert.notDeepEqual(c.bumpers, old.bumpers);
|
||||
assert.equal(c.targets.length, 0); assert.equal(c.lanes.length, 0);
|
||||
for (const w of c.walls) for (const [x, y] of [w.a, w.b]) assert.ok(x >= 200 && x <= 448 && y >= 5 && y <= 336);
|
||||
for (const r of c.ramps) {
|
||||
const score = c.score;
|
||||
for (let n = 1; n <= 3; n++) {
|
||||
const result = rampShot(c, r.id);
|
||||
assert.ok(result.frames > 50); assert.ok(result.top < r.mouth[0][1] - 40);
|
||||
assert.equal(c.score - score, r.points * n); assert.equal(c.future.hits[r.id], Math.min(n, 2));
|
||||
assert.ok(result.exit.x === 229 || result.exit.x === 411, 'ritorno nelle corsie interne');
|
||||
}
|
||||
// Un ingresso debole sulla salita ricade e non carica il sigillo.
|
||||
const d = inFuture(level), rd = d.ramps.find(k => k.id === r.id), points = d.score;
|
||||
const result = ride(d, { x: rd.path[0][0], y: rd.mouth[0][1] + 1, vx: 0, vy: -270, ramp: null });
|
||||
assert.equal(d.future.hits[r.id], 0); assert.equal(d.score, points); assert.ok(result.exit.y >= rd.mouth[0][1]);
|
||||
}
|
||||
const score = c.score, lives = c.balls;
|
||||
const nextX = 320 + Math.sin((c.sim + m.STEP) * 1.6) * 24;
|
||||
put(c, { x: nextX, y: c.regolo.y + c.regolo.r + 4, vx: 0, vy: -250, ramp: null, contacts: [] });
|
||||
m.update(c, m.STEP);
|
||||
assert.equal(c.table, 'classic'); assert.equal(c.score, score + m.FUTURE.finale);
|
||||
assert.equal(c.balls, lives); assert.ok(m.waiting(c)); assert.equal(c.extraBalls.length, 0);
|
||||
assert.deepEqual(c.ramps.map(r => r.id), ['sinistra', 'destra', 'super']);
|
||||
doubleLock(c); rampShot(c, 'super'); assert.ok(c.transform, 'il viaggio è ripetibile');
|
||||
}
|
||||
});
|
||||
|
||||
test('nel futuro il Regolo si muove, gli urti e la molla funzionano, niente finale prima dei sigilli', () => {
|
||||
const c = inFuture(), x = c.regolo.x;
|
||||
c.ball.waiting = true; run(c, 0.5); assert.notEqual(c.regolo.x, x);
|
||||
assert.ok(m.key(c, ' ', true)); run(c, 0.4); m.key(c, ' ', false); assert.ok(!m.waiting(c));
|
||||
// Un respingente devia davvero la pallina.
|
||||
const b = c.bumpers[0], score = c.score;
|
||||
put(c, { x: b.x, y: b.y + b.r + 4, vx: 0, vy: -200, contacts: [] }); m.update(c, m.STEP);
|
||||
assert.ok(c.ball.vy > 0); assert.equal(c.score, score + m.SCORE.bumper);
|
||||
put(c, { x: c.regolo.x, y: c.regolo.y + c.regolo.r + 4, vx: 0, vy: -200, contacts: [] }); m.update(c, m.STEP);
|
||||
assert.equal(c.table, 'future'); assert.equal(c.score, score + m.SCORE.bumper + m.SCORE.regolo);
|
||||
});
|
||||
|
||||
test('futuro e trasformazione sono serializzabili; annullare e disegnare non fanno avanzare la partita', () => {
|
||||
for (const transforming of [false, true]) {
|
||||
const c = inFuture(); if (transforming) { doubleLockAfterReturn(c); }
|
||||
m.hold(c, 'left', 'touch', true); m.cancel(c);
|
||||
const saved = JSON.parse(JSON.stringify(c));
|
||||
const calls = [], gradient = { addColorStop() {} };
|
||||
const ctx = new Proxy({}, { get: (o, k) => k in o ? o[k] : (...args) => { calls.push([k, ...args]); return gradient; }, set: (o, k, v) => { o[k] = v; return true; } });
|
||||
for (let i = 0; i < 10; i++) drawFlipper(ctx, c, story.closeups.flipper, {});
|
||||
assert.deepEqual(JSON.parse(JSON.stringify(c)), saved, 'disegno e pausa non consumano tempo');
|
||||
assert.ok(!c.flippers.left.held.length);
|
||||
run(c, 0.5); run(saved, 0.5);
|
||||
assert.deepEqual(JSON.parse(JSON.stringify(c)), saved, 'ripresa identica dopo il passaggio JSON');
|
||||
}
|
||||
});
|
||||
function doubleLockAfterReturn(c) {
|
||||
for (const b of m.activeBalls(c)) Object.assign(b, { x: 320, y: 348, vx: 0, vy: 300, ramp: null, releaseAt: 0 });
|
||||
m.update(c, m.STEP); doubleLock(c); rampShot(c, 'super');
|
||||
}
|
||||
|
||||
test('il ball save nel futuro salva una sola volta, conserva campo e sigilli, poi torna al classico', () => {
|
||||
const c = inFuture(); c.extraBalls = []; c.future.hits.ponte = 1;
|
||||
c.saveUntil = c.sim + 2; const lives = c.balls;
|
||||
put(c, { x: 320, y: 348, vx: 0, vy: 300, ramp: null }); m.update(c, m.STEP);
|
||||
assert.equal(c.table, 'future'); assert.equal(c.balls, lives); assert.ok(m.waiting(c)); assert.equal(c.future.hits.ponte, 1);
|
||||
m.launch(c, 0.7); assert.equal(m.saveLeft(c), 0);
|
||||
put(c, { x: 320, y: 348, vx: 0, vy: 300, ramp: null }); m.update(c, m.STEP);
|
||||
assert.equal(c.table, 'classic'); assert.equal(c.balls, lives - 1);
|
||||
});
|
||||
|
||||
test('i tre percorsi del futuro sono raggiungibili con il colpo fisico della paletta', () => {
|
||||
for (const [id, x, y] of [['orbita', 288, 300], ['ponte', 290, 306], ['spira', 308, 320]]) {
|
||||
const c = m.newFlipper(9);
|
||||
Object.assign(c, m.futureLayout(), { table: 'future', future: { hits: { orbita: 0, ponte: 0, spira: 0 }, beatAt: 0 } });
|
||||
m.launch(c, 0.8); c.saveUntil = 0;
|
||||
put(c, { x, y, vx: 0, vy: 180, inPlay: true }); m.hold(c, 'left', 'test', true);
|
||||
let entered = null;
|
||||
for (let i = 0; i < 160; i++) { m.update(c, m.STEP); if (c.ball?.ramp) { entered = c.ball.ramp.id; break; } }
|
||||
assert.equal(entered, id, 'il tiro parte dalla paletta, non dall’imbocco');
|
||||
}
|
||||
});
|
||||
|
||||
test('salvare l’avventura durante il futuro conserva il checkpoint, come negli altri minigiochi', () => {
|
||||
const g = new Game(story); g.go('via_provinciale'); settle(g);
|
||||
g.act('use', 'river'); settle(g); g.choose(g.options().findIndex(o => o.text.startsWith('Una partita alla bona'))); settle(g);
|
||||
const checkpoint = structuredClone(g.checkpoint);
|
||||
Object.assign(g.closeup, inFuture('bona'));
|
||||
const before = structuredClone(g.closeup), save = JSON.parse(JSON.stringify(g.savable()));
|
||||
assert.deepEqual(save, checkpoint); assert.deepEqual(g.closeup, before, 'salvare non consuma sigilli o palline');
|
||||
const loaded = new Game(story, { state: save });
|
||||
assert.equal(loaded.state.scene, 'via_provinciale'); assert.equal(loaded.closeup, null);
|
||||
g.closeupCancel(); assert.equal(g.closeup.table, 'future');
|
||||
g.closeupClose(); settle(g); assert.ok(g.state.flags.flipper_vinto, 'il traguardo resta acquisito uscendo dal futuro');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user