feedback7 R22-GHSIM: #298 rework — the one-interval gap of an instant skill with no attack of its own sits on the skill (nextCastAt), not on the operator's attack cooldown: its casts no longer replace the attacks / heals that were due; golden: back to 2 moves against the code before #298 (roster-033, roster-034)
Review of fb7-gh-sim (MERGE AFTER FIXES, high):4128965awrote u.atkCd after such a cast and gated the tick casts on it, so each cast took the attack that was due — 度算浪波 with 迅捷: 15 casts, 0 attacks in 20 s (silenced: 15 attacks); 冲锋号令·γ型 and 医疗无人机 lost one attack / heal per cast; after an idle cast the next attack waited the armed cooldown. PRTS 度算浪波 is an automatic instant throw, not an attack. Now (skills.js): such a cast (instant / charges, no spec.attack, not the DEFAULT cast right before an attack) sets skill.nextCastAt = now + the unit's attack interval, and the casts checked in skill.tick wait for it; the attack cooldown is not touched (reset on deployment, like opReadyAt). Measured (20 s, 引星棘刺 S1, 迅捷 229 layers): 12 casts — the first at 4 s as SP fills (#124), then one per 1.36 s — and 14 attacks, as many as silenced; without 迅捷 3 casts and 14 attacks; an idle cast does not delay the next attack (1 tick). [ASSUMED] one attack interval stays. Tests: bonds_addon «迅捷 refills 引星棘刺 S1 at once …» now asserts the casts take no attack (attacks = the silenced run's; fails on4128965a: 0 vs 4, and before it: 180 casts in 6 s); op_wang waits skill.nextCastAt; playtest6-skills (锏) back to its original timing. test/content + test/sim: 2835 pass. Docs: SIM.md §7, PLAYING.md 技能自动释放 (no attack is taken). Golden: against 4128965a's files roster (21) bonds (9) fields (9) matches (16) standins (6) diy (22) scenarios move back; against the same code with the gap disabled only roster-033 and roster-034 differ (引星棘刺 S1 recast within one interval) — the 84-scenario move of4128965acame from the attack cooldown, now gone.
This commit is contained in:
1 parent
38d4c3e4b5
commit
db29acf907
12 files changed
+1528
-1493
No files matched your search
@@ -71,9 +71,9 @@ test('迅捷: member skill end +12 SP (p=1 at high L), ≥40 layers every operat
|
||||
assert.equal(off.s_x, 0);
|
||||
});
|
||||
|
||||
test('迅捷 refills 引星棘刺 S1 at once: the instant cast still takes one attack cycle each, so attack speed paces it (GitHub #298)', () => {
|
||||
test('迅捷 refills 引星棘刺 S1 at once: it is cast at most once per attack interval, and her attacks go on untouched (GitHub #298)', () => {
|
||||
// S1 度算浪波: AUTO, no duration, no attack of its own, SP_FULL, cost 6; 迅捷 refunds 12 + 15 SP at ≥ 40 layers
|
||||
const casts = (aspd = 0, enemy = true) => {
|
||||
const run = ({ aspd = 0, enemy = true, silence = false } = {}) => {
|
||||
const h = makeBattle({
|
||||
seed: 1, bonds: { swiftShip: bond(2, 229, 3) }, autoFinish: false, timeLimit: 30,
|
||||
units: [{ chessId: 'chess_char_5_15_b', skillIndex: 0, row: 10, col: 4 }],
|
||||
@@ -83,21 +83,23 @@ test('迅捷 refills 引星棘刺 S1 at once: the instant cast still takes one a
|
||||
h.step();
|
||||
const u = h.unit('chess_char_5_15_b');
|
||||
if (aspd) h.b.addBuff(u, { key: 'test:aspd', mods: { aspd } });
|
||||
if (silence) h.b.addBuff(u, { key: 'test:mute', flags: { silence: true } });
|
||||
if (enemy) assert.ok(h.b.enemiesInKeys(u.rangeKeys, u, u.profile).length, 'an enemy stays in range');
|
||||
u.atkCd = 0;
|
||||
u.skill.gainSp(u.skill.spCost, 'test');
|
||||
const n0 = u.skill.activations, a0 = u.stats.attacks;
|
||||
h.run(2);
|
||||
h.run(6);
|
||||
checkInvariants(h.b);
|
||||
return { n: u.skill.activations - n0, attacks: u.stats.attacks - a0, interval: u.s.interval };
|
||||
};
|
||||
const base = casts();
|
||||
assert.ok(base.n >= 2 && base.n <= 3, `one cast per attack interval (${base.interval.toFixed(2)} s): ${base.n} in 2 s, not every tick`);
|
||||
assert.equal(base.attacks, 0, 'each cast takes the attack cycle');
|
||||
const fast = casts(500);
|
||||
assert.ok(fast.n >= 8 && fast.n <= 9, `ASPD +500 (interval ${fast.interval.toFixed(2)} s): ${fast.n} casts`);
|
||||
const alone = casts(0, false);
|
||||
assert.ok(alone.n >= 2 && alone.n <= 3, `no enemy: still fires at full SP (#124), one per cycle — ${alone.n}`);
|
||||
const base = run(), muted = run({ silence: true });
|
||||
// the first at once, then one per interval (1.36 s): 5 in 6 s — every tick before (180)
|
||||
assert.ok(base.n >= 4 && base.n <= 5, `one cast per attack interval (${base.interval.toFixed(2)} s): ${base.n} in 6 s`);
|
||||
assert.ok(base.attacks > 0 && base.attacks === muted.attacks, `the casts take no attack (${base.attacks} vs ${muted.attacks} silenced)`);
|
||||
const fast = run({ aspd: 500 }), fastMuted = run({ aspd: 500, silence: true });
|
||||
assert.ok(fast.n >= 23 && fast.n <= 25, `ASPD +500 (interval ${fast.interval.toFixed(2)} s): ${fast.n} casts`);
|
||||
assert.equal(fast.attacks, fastMuted.attacks, 'still no attack taken');
|
||||
const alone = run({ enemy: false });
|
||||
assert.ok(alone.n >= 4 && alone.n <= 5 && alone.attacks === 0, `no enemy: still fires at full SP (#124), one per interval — ${alone.n}`);
|
||||
});
|
||||
|
||||
test('迅捷 / 突袭: SP gifts after end do not recharge a zero-SP deployment skill or trigger a ready raid', () => {
|
||||
|
||||
@@ -269,8 +269,8 @@ test('her stock (the owner\'s decision of 2026-10-06): the placed 棋子 occupy
|
||||
assert.deepEqual([u.skill.activations, w.stock], [1, 7], `${S}: 立即获得两枚棋子`);
|
||||
h.step();
|
||||
assert.ok(!u.s.flags.noSp, `${S}: below the cap, no 阻回`);
|
||||
assert.ok(u.atkCd > 0, `${S}: the cast took her attack cycle (GitHub #298)`);
|
||||
h.run(u.atkCd); // the next cast waits it out
|
||||
assert.ok(u.skill.nextCastAt > h.b.time, `${S}: the next cast waits one attack interval (GitHub #298)`);
|
||||
h.run(u.skill.nextCastAt - h.b.time); // wait it out
|
||||
u.skill.gainSp(999);
|
||||
h.step();
|
||||
assert.deepEqual([u.skill.activations, w.stock], [2, 8], `${S}: 最多拥有8枚`);
|
||||
|
||||
Reference in new issue
Block a user