feedback5: merge W5F — ACTIVE_RANGE also replaces SEARCH rows and covers 深巡 S2, 纯烬艾雅法拉 S1 at full SP, 伺夜 S1 only with his pack on the field

Golden regenerated on the merged code (only test/golden/matches.json conflicted): against feedback5 it moves exactly W5F's scenarios (roster-001/002/016/026/027/028/038/041/042/044, bond-egirShip-low, bond-suntShip-high, boss-boss_2-pair, boss-boss_6-pair, solo-NORMAL-1, coop2-NORMAL-3, coop4-FUNNY-6, coop2-NORMAL-8-serverrun); against fb5-bugs3 exactly W5E's (unite-1/2 and the 8 match scenarios of the 联防 map and the full-hand rule). data/ equals an offline build of the merged tools/build-data.mjs.
This commit is contained in:
sganggs committed 2026-10-05 19:05:06 +08:00
commit 17d9960fb4
30 files changed
+552 -407

No files matched your search

+37 -9
View File
@@ -432,20 +432,22 @@ test('3_06 菲莱 S1 灵河护佑 (TAKE_DAMAGE): HP +, clears her element gauges
}
});
test('3_08 薄绿 S1 风语 (阵法术师 技能1: "初始攻击范围内出现敌人后自动释放"): wider range, attacks at attack@atk_scale; guard/taunt rules kept', () => {
test('3_08 薄绿 S1 风语 (阵法术师 row SEARCH "在初始攻击范围内存在敌人时", widened to its running x-2 by the owner\'s ACTIVE_RANGE rule of 2026-10-05): wider range, attacks at attack@atk_scale; guard/taunt rules kept', () => {
for (const id of BOTH('chess_char_3_08_a')) {
const b = SB(id, 'skchr_mint_1'), t0 = TB(id, 0);
assert.equal(LD(id, 'skchr_mint_1').skill.trigger?.rule, 'SEARCH', 'data: the phalanx S1 row');
const h = makeBattle({ defs: { enemies: { enemy_d: dummy('enemy_d'), enemy_in: dummy('enemy_in') } }, timeLimit: 60, hooks: ['damaged'], captureNoisy: true,
units: [U(id, 'skchr_mint_1', 10, 4)], enemies: [{ key: 'enemy_d', pos: [11, 6] }] }); // [1,2]: x-2 only
const sk = LD(id, 'skchr_mint_1').skill;
assert.equal(sk.trigger?.rule, 'ACTIVE_RANGE', 'data: the phalanx row (rawRule SEARCH) on the x-2 she attacks with');
assert.deepEqual(sk.trigger.grid, sk.rangeGrid, 'trigger grid = the S1 x-2');
const h = makeBattle({ defs: { enemies: { enemy_d: dummy('enemy_d'), enemy_far: dummy('enemy_far') } }, timeLimit: 60, hooks: ['damaged'], captureNoisy: true,
units: [U(id, 'skchr_mint_1', 10, 4)], enemies: [{ key: 'enemy_far', pos: [10, 7] }] }); // [0,3]: outside the x-2 too
const u = h.unit(id);
h.run(1);
assert.equal(atkHits(h, u).length, 0, 'phalanx: no attack while the skill is off');
fill(u);
h.run(2);
assert.equal(u.skill.activations, 0, 'an enemy outside her INITIAL range does not open it (not a whole-field search)');
h.spawn('enemy_in', { pos: [10, 6] }); // [0,2]: initial range
assert.ok(h.runUntil(() => u.skill.active, 1), 'an enemy inside her initial range opens it at once (she never attacks before)');
assert.equal(u.skill.activations, 0, 'an enemy outside the x-2 does not open it (not a whole-field search)');
h.spawn('enemy_d', { pos: [11, 6] }); // [1,2]: the x-2 only, outside her initial x-1
assert.ok(h.runUntil(() => u.skill.active, 1), 'an enemy inside the S1 x-2 opens it at once (she never attacks before)');
assert.ok(u.findBuff('talent:mint_taunt'));
approx(u.s.taunt, (u.base.tauntLevel ?? 0) + t0.taunt_level);
h.run(3);
@@ -747,12 +749,12 @@ test('3_18 忍冬 S1 小施惩戒: next attack + extra arts and +DP; S2 坠刃
}
});
test('3_19 伺夜 S1 领袖的呼唤 (ALWAYS): +DP and one more “狼影” (≤ max)', () => {
test('3_19 伺夜 S1 领袖的呼唤 (自动触发, the pack on the field): +DP and one more “狼影” (≤ max)', () => {
for (const id of BOTH('chess_char_3_19_a')) {
const b = SB(id, 'skchr_vigil_1');
const h = makeBattle({ defs: { chess: noGarrison(id) }, timeLimit: 60, flags: { dpPerSec: 0 }, units: [U(id, 'skchr_vigil_1', 10, 3)] });
const u = h.unit(id), p = h.b.getPlayer('p1');
assert.equal(u.skill.rule, 'SP_FULL');
assert.equal(u.skill.rule, 'NEVER', 'the kit casts it (the pack check)');
h.step();
const w = u.trait.reinforcement;
assert.equal(w.mem.wolves, 2);
@@ -784,6 +786,32 @@ test('3_19 伺夜 S1 领袖的呼唤 (ALWAYS): +DP and one more “狼影” (
done(g);
});
test('3_19 伺夜 S1 领袖的呼唤: PRTS 备注 「仅场上存在狼群时可触发技能」 — no pack, no cast (the SP waits full, no DP); cast once the pack stands again', () => {
for (const id of BOTH('chess_char_3_19_a')) {
const b = SB(id, 'skchr_vigil_1');
const h = makeBattle({ defs: { chess: noGarrison(id) }, timeLimit: 60, flags: { dpPerSec: 0 }, units: [U(id, 'skchr_vigil_1', 10, 3)] });
const u = h.unit(id), p = h.b.getPlayer('p1');
h.run(1);
const w = u.trait.reinforcement;
assert.ok(w && w.alive, 'the pack stands');
for (let i = 0; i < 10 && w.alive; i++) h.b.dealDamage(null, w, { amount: 1e9, type: 'true' }); // a wolf is lost per KO
assert.ok(!u.trait.reinforcement?.alive, 'no pack on the field');
const dp0 = p.dp;
fill(u);
h.run(2);
assert.equal(u.skill.activations, 0, 'no pack, no cast');
assert.ok(u.skill.ready, 'the SP waits full');
approx(p.dp, dp0, 1e-9, 'no DP without the pack');
// the knocked-out pack comes back after its respawn time: cast at once — +DP and one more 狼影
assert.ok(h.runUntil(() => u.skill.activations === 1, 15), 'cast once the pack stands again');
const w2 = u.trait.reinforcement;
assert.ok(w2 && w2.alive, 'the pack is back');
approx(p.dp, dp0 + b.cost, 1e-9, '+cost DP');
assert.equal(w2.mem.wolves, 3, 'one more 狼影 on the returned pack');
done(h);
}
});
test('3_19 伺夜 S2 领袖的馈赠: +DP, the pack recovers HP, its next attack ×atk_scale, a kill by it pays +DP', () => {
for (const id of BOTH('chess_char_3_19_a')) {
const b = SB(id, 'skchr_vigil_2');
+20
View File
@@ -1165,6 +1165,26 @@ test('6_20 纯烬艾雅法拉 S1 无声润物: toggle heal; ATK +, 2 heal target
}
});
test('6_20 纯烬艾雅法拉 S1 无声润物 (自动触发, effects on her allies only): on as soon as its SP is full — nobody injured, no enemy (the owner\'s decision of 2026-10-05)', () => {
for (const id of both('chess_char_6_20')) {
const sid = 'skchr_agoat2_1', bb = bbOf(id, sid);
const h = run({
defs: { chess: { a1: plain('a1', { stats: { maxHp: 1e5 } }) } },
units: [U(id, sid, 10, 3, { carryState: READY }), { chessId: 'a1', row: 10, col: 5 }],
});
const u = h.unit(id), a1 = h.unit('a1');
usesSkill(u, sid);
assert.equal(u.skill.rule, 'SP_FULL');
assert.ok(h.runUntil(() => u.skill.active, 2), 'on with every ally at full HP and no enemy (the data DEFAULT heal rule waited for an injured ally)');
assert.equal(a1.hp, a1.s.maxHp);
h.b.addBuff(u, { key: 'test:disarm', flags: { disarm: true } }); // (no heals: only the skill's 元素损伤 recovery)
a1.elem.burn = 800;
h.run(1.05);
approx(800 - a1.elem.burn, u.s.atk * bb['agoat2_s_1[aura].ep_heal_ratio'], 'its 元素损伤 recovery runs at full HP', 1e-3);
done(h);
}
});
test('6_20 纯烬艾雅法拉 S2 云霭荫佑: one heal on every ally of her range, then a barrier absorbing atk_scale × ATK of 元素损伤 for `duration` s', () => {
for (const id of both('chess_char_6_20')) {
const sid = 'skchr_agoat2_2', bb = bbOf(id, sid);
+13 -9
View File
@@ -132,12 +132,12 @@ test('1_04 深巡: fin darts pierce attack@max_target enemies on the line + 1 s
});
const u = h.unit(id);
h.step();
// 技能策略 (issue #4; the deliberate deviation of DESIGN §21.29): an enemy inside her initial 2-2 range is enough — the
// basic strategy casts it, no hit required
assert.equal(u.skill.rule, 'DEFAULT', '行动能力剥夺 is an offensive ranged skill, not a 重装 TAKE_DAMAGE one');
// 技能策略 (issue #4; the deliberate deviation of DESIGN §21.29, and since 0.2.0 the owner's ACTIVE_RANGE on top): an
// enemy inside the 3-2 she attacks with is enough — no hit required
assert.equal(u.skill.rule, 'ACTIVE_RANGE', '行动能力剥夺 is an offensive ranged skill, not a 重装 TAKE_DAMAGE one');
h.run(3.5);
assert.ok(u.skill.active);
assert.equal(h.hooksOf('skillStart').find((c) => c.unit === u).reason, 'DEFAULT', 'cast by the basic strategy');
assert.equal(h.hooksOf('skillStart').find((c) => c.unit === u).reason, 'ACTIVE_RANGE', 'cast on its running range');
approx(u.s.atk, u.base.atk * (1 + bb.atk));
const firstAtk = h.hooksOf('attack').find((c) => c.attacker === u && c.isSkill);
assert.equal(firstAtk.targets.length, bb['attack@max_target'], 'hits every enemy of the line');
@@ -150,16 +150,20 @@ test('1_04 深巡: fin darts pierce attack@max_target enemies on the line + 1 s
done(h);
});
test('1_04 深巡 / 1_20 雷蛇 S2 技能策略: both 哨戒铁卫 S2s cast with an enemy in range, no hit needed (issue #4)', () => {
test('1_04 深巡 / 1_20 雷蛇 S2 技能策略: both 哨戒铁卫 S2s cast with an enemy in range, no hit needed (issue #4); 深巡 on her 3-2', () => {
// both S2s are offensive (深巡: range up + ATK/ASPD + piercing darts; 雷蛇: ATK +125 % + arts on 3 + stun); the official
// 下半 重装 row (TAKE_DAMAGE for every MANUAL 重装 skill) made both wait for a hit, and the owner's deliberate deviation
// from it (DESIGN §21.29, after community feedback) gives them the basic strategy — the data and the kit say DEFAULT.
// from it (DESIGN §21.29, after community feedback) gives them the basic strategy — 雷蛇's data and kit say DEFAULT;
// 深巡's 3-2 strictly contains her 2-2, so since 0.2.0 the owner's ACTIVE_RANGE rule casts it on the 3-2 (2026-10-05).
for (const id of ['chess_char_1_04_a', 'chess_char_1_04_b', 'chess_char_1_20_a', 'chess_char_1_20_b']) {
const h = run({ units: [{ chessId: id, row: 9, col: 4, carryState: READY }], enemies: [{ key: 'e', pos: [9, 6] }] });
const udflow = id.startsWith('chess_char_1_04');
// (9,7) = [0,3]: on 深巡's 3-2, not on her 2-2
const h = run({ units: [{ chessId: id, row: 9, col: 4, carryState: READY }], enemies: [{ key: 'e', pos: [9, udflow ? 7 : 6] }] });
const u = h.unit(id);
assert.equal(u.skill.rule, 'DEFAULT', `${id}: an offensive skill, not the 重装 TAKE_DAMAGE row`);
const rule = udflow ? 'ACTIVE_RANGE' : 'DEFAULT';
assert.equal(u.skill.rule, rule, `${id}: an offensive skill, not the 重装 TAKE_DAMAGE row`);
assert.ok(h.runUntil(() => u.skill.activations >= 1, 5), `${id}: casts while an enemy is in range and untouched`);
assert.equal(h.hooksOf('skillStart').find((c) => c.unit === u).reason, 'DEFAULT', `${id}: cast by the basic strategy`);
assert.equal(h.hooksOf('skillStart').find((c) => c.unit === u).reason, rule, `${id}: cast by ${rule}`);
done(h);
}
});