85 lines
4.6 KiB
JavaScript
85 lines
4.6 KiB
JavaScript
// 松果 S2: the ATK ramp belongs to a deployment, while Skill.activations stays cumulative.
|
||
// Sources: PRTS 松果 S2 备注; data/chess.json rank-4 / rank-7 blackboards.
|
||
import { test } from 'node:test';
|
||
import assert from 'node:assert/strict';
|
||
import { makeBattle, enemyRec, checkInvariants } from '../helpers/battleHarness.js';
|
||
|
||
const FORMS = [
|
||
{ label: 'normal', id: 'chess_char_3_10_a', ramp: [0.25, 0.45, 0.65, 0.85, 0.85] },
|
||
{ label: 'elite without module', id: 'chess_char_3_10_b', moduleId: 'none', ramp: [0.4, 0.6, 0.8, 1, 1] },
|
||
{ label: 'elite RPR-X', id: 'chess_char_3_10_b', ramp: [0.4, 0.6, 0.8, 1, 1] },
|
||
];
|
||
const close = (actual, expected, msg) => assert.ok(Math.abs(actual - expected) < 1e-6, `${msg}: ${actual} ≈ ${expected}`);
|
||
const done = (h) => { checkInvariants(h.b); assert.deepEqual(h.b.errors, []); };
|
||
function field(form, { atk = 0, units = null } = {}) {
|
||
return makeBattle({
|
||
seed: 5, timeLimit: 600, autoFinish: false,
|
||
flags: { startOpCooldown: 3, dpInit: 99, dpPerSec: 10, dpMax: 999 },
|
||
units: units ?? [{ chessId: form.id, moduleId: form.moduleId, uid: 1, row: 10, col: 4 }],
|
||
defs: { enemies: { enemy_dummy: enemyRec({ key: 'enemy_dummy', hp: 1e7, speed: 0, atk, range: 5, bat: 2 }) } },
|
||
enemies: [{ key: 'enemy_dummy', pos: [10, 5] }],
|
||
});
|
||
}
|
||
|
||
for (const form of FORMS) {
|
||
test(`松果 S2 ${form.label}: natural casts grow within one deployment and stop at the fourth step`, () => {
|
||
const h = field(form), u = h.unit(1);
|
||
h.step();
|
||
assert.equal(u.skill.id, 'skchr_pinecn_2');
|
||
const wide = u.rangeKeys.length;
|
||
for (const [i, atkPct] of form.ramp.entries()) {
|
||
assert.ok(h.runUntil(() => u.skill.activations === i + 1, 60), `cast ${i + 1}`);
|
||
close(u.s.atk, u.base.atk * (1 + atkPct), `cast ${i + 1} ATK`);
|
||
assert.ok(u.rangeKeys.length < wide, 'the skill still shortens her range');
|
||
assert.ok(h.runUntil(() => !u.skill.active, 21), 'the duration ends');
|
||
close(u.s.atk, u.base.atk, 'ATK restored');
|
||
assert.equal(u.rangeKeys.length, wide, 'range restored');
|
||
}
|
||
done(h);
|
||
});
|
||
|
||
test(`松果 S2 ${form.label}: a knock-out during the fourth cast restarts the ramp on redeployment`, () => {
|
||
const h = field(form), u = h.unit(1);
|
||
assert.ok(h.runUntil(() => u.skill.activations === 4, 240), 'four natural casts');
|
||
close(u.s.atk, u.base.atk * (1 + form.ramp[3]), 'at the cap before the knock-out');
|
||
h.b.kill(u, h.enemy('enemy_dummy'));
|
||
assert.equal(u.findBuff('skill:pinecn_atk'), null, 'the active skill buff leaves with her');
|
||
assert.ok(h.runUntil(() => u.alive && u.deployed, 90), 'automatic paid redeployment');
|
||
assert.equal(u.skill.activations, 4, 'the engine activation sequence is still cumulative');
|
||
assert.ok(h.runUntil(() => u.skill.activations === 5, 30), 'first natural cast after redeployment');
|
||
close(u.s.atk, u.base.atk * (1 + form.ramp[0]), 'first ATK step again');
|
||
assert.ok(h.runUntil(() => u.skill.activations === 6, 60), 'second natural cast after redeployment');
|
||
close(u.s.atk, u.base.atk * (1 + form.ramp[1]), 'second ATK step again');
|
||
done(h);
|
||
});
|
||
}
|
||
|
||
test('松果 S2 normal: natural enemy damage, knock-out and redeployment give +25% on both first casts', () => {
|
||
const h = field(FORMS[0], { atk: 225 }), u = h.unit(1);
|
||
assert.ok(h.runUntil(() => u.skill.activations === 1, 30), 'first cast');
|
||
close(u.s.atk, 735, '588 × 1.25');
|
||
assert.ok(h.runUntil(() => !u.alive, 40), 'enemy attacks knock her out');
|
||
assert.ok(h.runUntil(() => u.alive && u.deployed, 90), 'automatically redeployed');
|
||
assert.equal(u.skill.activations, 1, 'the earlier cast remains in the engine sequence');
|
||
assert.ok(h.runUntil(() => u.skill.activations === 2, 30), 'next natural cast');
|
||
close(u.s.atk, 735, 'new deployment: 588 × 1.25');
|
||
done(h);
|
||
});
|
||
|
||
test('松果 S2: redeploying one piece does not restart another piece\'s ramp', () => {
|
||
const h = field(FORMS[0], { units: [
|
||
{ chessId: FORMS[0].id, uid: 1, row: 10, col: 4 },
|
||
{ chessId: FORMS[0].id, uid: 2, row: 11, col: 4 },
|
||
] });
|
||
const a = h.unit(1), b = h.unit(2);
|
||
assert.ok(h.runUntil(() => a.skill.activations === 2 && b.skill.activations === 2, 90));
|
||
h.b.retreat(a, { reason: 'raid' });
|
||
assert.ok(h.b.redeploy(a, { free: true }), 'only the first piece redeployed');
|
||
close(b.s.atk, b.base.atk * 1.45, 'the second piece keeps its running second step');
|
||
assert.ok(h.runUntil(() => a.skill.activations === 3, 30));
|
||
close(a.s.atk, a.base.atk * 1.25, 'the first piece starts again');
|
||
assert.ok(h.runUntil(() => b.skill.activations === 3, 60));
|
||
close(b.s.atk, b.base.atk * 1.65, 'the second piece progresses to its third step');
|
||
done(h);
|
||
});
|