feedback5 WE2: 黑键 S2 casts at full SP once a placeable tile of his range is free, no enemy needed (#23)

PRTS 黑键 S2 备注 "攻击范围内不存在可部署位时,技能不会被触发" (skill prefab _minTileNum 1):
the kit casts it (trigger NEVER + a tick: ready, able to act, one free deployable tile
in his range — hasRemnantTile, no rng) instead of the data's DEFAULT waiting for an
enemy. Checked and kept: 温蒂's cannon (summons follow the basic strategy, PRTS
"对于拥有手动释放技能的召唤物,亦会强制开启技能"), 凯尔希·思衡托 S2 (a medic's target is an
injured ally; the owner's ACTIVE_RANGE on her y-11).
Golden: diy-114, diy-115, diy-116 moved — the scenarios fielding 黑键 S2 (his S2 fires at
full SP from the battle start).
This commit is contained in:
sganggs committed 2026-10-06 15:25:07 +08:00
1 parent ac6f712142
commit 708719f382
4 files changed
+144 -105

No files matched your search

+27 -11
View File
@@ -76,7 +76,7 @@ function storeUp(h, u, n, e, max = 60) {
return log;
}
test('黑键 in every 自选 form: his operator kit (all three skills authored), the form\'s stats + module attributes, 3-14, ranged arts every 3 s that hits air units, blocks 1, ground-targetable, the data\'s DEFAULT triggers, 空 bond, no 特质', () => {
test('黑键 in every 自选 form: his operator kit (all three skills authored), the form\'s stats + module attributes, 3-14, ranged arts every 3 s that hits air units, blocks 1, ground-targetable, the data\'s DEFAULT triggers (S2: the kit casts it), 空 bond, no 特质', () => {
assert.equal(OPERATOR_KITS[EBN], KITS[EBN]);
for (const f of FORMS_ALL) {
const [tier, elite, mod] = f;
@@ -88,7 +88,7 @@ test('黑键 in every 自选 form: his operator kit (all three skills authored),
[form.stats.maxHp + (m?.attr.maxHp ?? 0), form.stats.atk + (m?.attr.atk ?? 0), form.stats.def + (m?.attr.def ?? 0), form.stats.res + (m?.attr.res ?? 0), 100 + (m?.attr.aspd ?? 0)], `${label(f)}: stats`);
assert.deepEqual([u.s.blockCnt, u.profile.attack, u.profile.dmgType, u.profile.canHitFly, u.base.bat], [1, 'ranged', 'arts', true, 3], `${label(f)}: 秘术师`);
assert.deepEqual(u.liveRangeGrid, form.rangeGrid, `${label(f)}: 3-14`);
assert.equal(u.skill.rule, 'DEFAULT', `${label(f)}: the data's trigger`);
assert.equal(u.skill.rule, skill === 1 ? 'NEVER' : 'DEFAULT', `${label(f)}: the data's trigger (S2: the kit's own cast)`);
assert.deepEqual([u.def.bonds, u.def.raw.garrisonIds], [['emptyShip'], []], `${label(f)}: bonds / 特质`);
assert.ok(!u.s.flags.liftoff && !u.s.flags.camou && !u.s.flags.stealth, `${label(f)}: ground enemies target him`);
done(h);
@@ -292,16 +292,29 @@ test('S1 渐快急板 (MANUAL, data DEFAULT): 5 s, range 4-1, attack interval ×
}
});
test('S2 荒芜回响 (AUTO, data DEFAULT): spends every energy (elite first) on energies + 1 旧日残影 on free tiles of his range — the enemy\'s tile first, then the nearest; fewer tiles ⇒ fewer spent; no free tile ⇒ no cast (the charge back)', () => {
test('S2 荒芜回响 (AUTO: full SP and one free placeable tile in his range, no enemy needed — PRTS 备注, prefab _minTileNum 1): spends every energy (elite first) on energies + 1 旧日残影 on free tiles of his range — the enemy\'s tile first, then the nearest; fewer tiles ⇒ fewer spent; no free tile ⇒ no cast', () => {
for (const [tier, elite, mod] of [[5, false, null], [6, true, MX]]) {
const sk = skillOf(tier, elite, S2);
assert.deepEqual([sk.skillType, sk.bb.atk_scale, sk.bb.force, sk.spCost, sk.initSp], ['AUTO', elite ? 2 : 1.7, 1, elite ? 14 : 15, 0]);
// no enemy on the field: it casts as soon as its SP is full
{
const { h, u } = field({ tier, elite, mod, skill: 1 });
assert.equal(u.skill.rule, 'NEVER', 'the kit casts it');
let before = stored(u);
for (let i = 0; i < (sk.spCost + 1) * 30 && u.skill.activations === 0; i++) { before = stored(u); h.step(); }
assert.equal(u.skill.activations, 1, 'cast at full SP with no enemy anywhere');
approx(h.b.time, sk.spCost, `T${tier}: at ${sk.spCost} s`, 0.2);
assert.equal(h.b.allyUnits.filter((t) => t.kind === 'token' && t.defId === REMNANT && t.alive).length, before.n + before.e + 1, 'the energies stored meanwhile + 1 remnants');
assert.deepEqual(stored(u), { n: 0, e: 0 }, 'every energy spent');
done(h);
}
const { h, u } = field({ tier, elite, mod, skill: 1 });
const N = traitOf(tier, elite, mod).times;
holdSkill(u);
storeUp(h, u, N, 1);
u.skill.gainSp(999);
const e = h.spawn('enemy_dummy', { pos: [10, 7] });
assert.ok(h.runUntil(() => u.skill.activations === 1, 5), 'cast with an enemy in range');
u.skill.gainSp(999);
assert.ok(h.runUntil(() => u.skill.activations === 1, 5), 'cast');
const rem = h.b.allyUnits.filter((t) => t.kind === 'token' && t.defId === REMNANT);
assert.equal(rem.length, N + 2, `${N} + 1 energies + 1 remnants`);
assert.deepEqual([rem[0].tileR, rem[0].tileC], [10, 7], 'the enemy\'s tile first');
@@ -315,27 +328,30 @@ test('S2 荒芜回响 (AUTO, data DEFAULT): spends every energy (elite first) on
// the flat stage's edge (col 2: RANGED tiles; cols 0–1 none): facing LEFT from (10, 2) no tile of his range is free
{
const { h, u } = field({ tier: 5, skill: 1, row: 10, col: 2, dir: 'LEFT' });
holdSkill(u);
storeUp(h, u, 3, 1);
u.skill.gainSp(999);
h.spawn('enemy_dummy', { pos: [10, 1] });
assert.ok(h.runUntil(() => u.skill.activations >= 1, 5), 'the DEFAULT condition holds (an enemy in range)');
assert.equal(h.b.allyUnits.filter((t) => t.kind === 'token').length, 0, 'no placeable tile: no remnant');
assert.ok(u.skill.ready, 'the charge is back');
u.skill.gainSp(999);
h.run(5);
assert.equal(u.skill.activations, 0, 'no placeable tile: no cast, an enemy in range notwithstanding');
assert.equal(h.b.allyUnits.filter((t) => t.kind === 'token').length, 0, 'no remnant');
assert.ok(u.skill.ready, 'the charge waits');
done(h);
}
// fewer free tiles than energies + 1: facing LEFT from (10, 3) only (10, 2) and (11, 2) are placeable
{
const { h, u } = field({ tier: 5, skill: 1, row: 10, col: 3, dir: 'LEFT' });
holdSkill(u);
storeUp(h, u, 3, 1);
u.skill.gainSp(999);
let atEnd = null;
h.b.on('skillEnd', (c) => { if (c.unit === u) atEnd = stored(u); });
h.spawn('enemy_dummy', { pos: [10, 2] });
u.skill.gainSp(999);
assert.ok(h.runUntil(() => u.skill.activations === 1, 5));
const rem = h.b.allyUnits.filter((t) => t.kind === 'token' && t.defId === REMNANT);
assert.deepEqual(rem.map((t) => [t.tileR, t.tileC]), [[10, 2], [11, 2]], 'the enemy\'s tile, then the other free one');
assert.deepEqual(atEnd, { n: 3, e: 0 }, 'one energy spent — the elite one first');
assert.ok(h.runUntil(() => energyHits(h, u).length === 3, 2), 'the attack that follows releases the three left');
assert.ok(h.runUntil(() => energyHits(h, u).length === 3, 4), 'his next attack (3 s interval) releases the three left');
done(h);
}
});