Files
ark-stronghold-protocol/test/sim/feedback3-stunsp.test.js

77 lines
4.5 KiB
JavaScript

// test/sim/feedback3-stunsp.test.js — community report #18: 洛洛's S2 self-stun lasted the right time, but her SP did not
// recover while she was stunned (skills.js returned before the natural recovery when the unit could not act).
// Official: PRTS 技能 "在阻回状态或技力条已满时,保留剩余冷却时间,计时暂停" — only 阻回 pauses natural recovery; PRTS 异常效果
// STUNNED "无法攻击、释放技能、阻挡敌人类单位" (gamedata_const ba.stun "无法移动、阻挡、攻击及使用技能") names no SP effect.
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { makeBattle, chessRec, enemyRec } from '../helpers/battleHarness.js';
import { getDefaultSource } from '../../server/sim/simdata.js';
const close = (a, b, eps, msg = '') => assert.ok(Math.abs(a - b) <= eps, `${msg} expected ${b} ± ${eps}, got ${a}`);
const dummy = enemyRec({ key: 'e_dummy', hp: 1e9, speed: 0 });
test('洛洛 S2 自负此轭: the data matches skill_table (lv 4 / 7), the stun lasts the overload, and her SP recovers while she is stunned', () => {
const ds = getDefaultSource();
// skill_table skchr_rockr_2: level 4 duration 40, spCost 45, initSp 15, atk 0.4, attack_speed 45, scale 1.6; level 7
// (the elite piece) 40 / 40 / 20, 0.5 / 60 / 1.7 — the chess status skillLevel 4 / 7
for (const [id, lv, cost, init, atk, aspd, scale] of [['chess_char_2_10_a', 4, 45, 15, 0.4, 45, 1.6], ['chess_char_2_10_b', 7, 40, 20, 0.5, 60, 1.7]]) {
const sk = ds.rawChess(id).skill;
assert.equal(sk.skillId, 'skchr_rockr_2', id);
assert.equal(sk.level, lv, id);
assert.deepEqual([sk.duration, sk.spCost, sk.initSp, sk.bb.atk, sk.bb.attack_speed, sk.bb.scale], [40, cost, init, atk, aspd, scale], id);
}
const h = makeBattle({
defs: { enemies: { e_dummy: dummy } }, units: [{ chessId: 'chess_char_2_10_a', row: 10, col: 4, carryState: { sp: 999 } }],
enemies: [{ key: 'e_dummy', pos: [10, 6] }], autoFinish: false, timeLimit: 200, hooks: ['statusApplied', 'skillStart'],
});
const u = h.unit('chess_char_2_10_a');
h.step();
assert.ok(u.skill.active, 'cast at once');
h.runUntil(() => !u.skill.active, 45);
h.step();
const stun = h.hooksOf('statusApplied').find((c) => c.status === 'stun' && c.target === u);
close(stun.duration, 20, 0.05, 'stunned as long as the overload (the second half of the 40 s)');
assert.ok(!u.canAct);
const sp0 = u.skill.sp;
assert.ok(sp0 < 0.2, `the cast spent the SP, got ${sp0}`);
h.run(10);
assert.ok(!u.canAct, 'still stunned');
close(u.skill.sp - sp0, 10, 0.1, 'SP +1/s while stunned (it stayed at 0 before)');
h.run(10.5);
assert.ok(u.canAct, 'the stun is over after 20 s');
close(u.skill.sp, 20.5, 0.2);
// ready 45 s after the skill end (not 45 s after the stun): cast again then
const casts = () => h.hooksOf('skillStart').filter((c) => c.unit === u).length;
assert.equal(casts(), 1);
h.run(25);
assert.equal(casts(), 2, 'recast 45 s after the end of the skill');
});
test('frozen or stunned operators keep their natural SP recovery; 阻回 (noSp) still stops it; no cast while stunned', () => {
const op = (id) => chessRec({ id, profession: 'SNIPER', rangeGrid: [[0, 0], [0, 1], [0, 2]], stats: { atk: 100 }, skill: { duration: 5, spCost: 30, initSp: 0, bb: { atk: 1 } } });
const h = makeBattle({
defs: { chess: { t_a: op('t_a'), t_b: op('t_b'), t_c: op('t_c') }, enemies: { e_dummy: dummy } },
units: [{ chessId: 't_a', row: 9, col: 3 }, { chessId: 't_b', row: 10, col: 3 }, { chessId: 't_c', row: 11, col: 3, carryState: { sp: 25 } }],
enemies: [{ key: 'e_dummy', pos: [11, 4] }], content: 'generic', autoFinish: false, timeLimit: 60,
});
h.step();
const [a, b, c] = ['t_a', 't_b', 't_c'].map((id) => h.unit(id));
h.b.applyStatus(a, 'cold', { duration: 10 });
h.b.applyStatus(a, 'cold', { duration: 10 }); // cold on cold → 冻结 (3 s)
assert.ok(a.findBuff('freeze'), 'frozen');
h.b.addBuff(b, { key: 't:noSp', duration: 3, flags: { noSp: true } });
h.b.applyStatus(c, 'stun', { duration: 8 });
const [a0, b0] = [a.skill.sp, b.skill.sp];
h.run(2.9);
close(a.skill.sp - a0, 2.9, 0.05, 'frozen: +1/s');
close(b.skill.sp - b0, 0, 1e-9, '阻回: nothing');
// t_c fills its 30 SP while stunned (25 + 5 s) but casts only once the stun is over (no cast while stunned)
h.run(4.5);
assert.ok(!c.canAct && c.skill.ready && c.skill.activations === 0, 'ready, stunned, no cast');
h.run(1);
assert.ok(c.canAct);
h.run(1);
assert.equal(c.skill.activations, 1, 'cast once it can act again');
});