Fix source-owned Lemuen detection and wanted skill triggers (#428)

This commit is contained in:
sganggs committed 2026-10-09 17:29:46 +08:00
1 parent 08c93460db
commit 85b694f206
8 files changed
+495 -379

No files matched your search

+3 -3
View File
@@ -121,15 +121,15 @@ test('6_01 蕾缪安 S2 归乡邀约: ASPD/ATK +; a wanted enemy is aimed at for
const h = run({
defs: { enemies: { el: dummy('el', { rank: 'ELITE' }) } },
units: [U(id, sid, 10, 4, { carryState: READY })], enemies: [{ key: 'el', pos: [10, 6] }],
setup(b) { b.on('enemySpawn', ({ enemy }) => b.addBuff(enemy, { key: 'lemuen:wanted' })); },
});
const u = h.unit(id);
usesSkill(u, sid);
assert.ok(h.runUntil(() => u.skill.active, 10));
approx(skillBuff(u).mods.aspd, bb.attack_speed, 'ASPD +');
approx(skillBuff(u).mods.atkPct, bb.atk, 'ATK +');
assert.ok(h.runUntil(() => !!h.enemies()[0]?.findBuff('lemuen:wanted'), 10), 'real detection marks the enemy');
assert.ok(h.runUntil(() => !!u.mem.lemAim, 5), 'aims at the wanted enemy');
const ammo = u.skill.ammoLeft, t0 = h.b.time;
const ammo = u.skill.ammoLeft, t0 = h.b.time, aimingAtk = u.s.atk;
const atks0 = h.hooksOf('attack').filter((c) => c.attacker === u).length;
assert.ok(h.runUntil(() => !u.mem.lemAim, 10));
approx(h.b.time - t0, bb['attack@aim_duration'], 'aims the full time (1e7 HP)', 0.03);
@@ -139,7 +139,7 @@ test('6_01 蕾缪安 S2 归乡邀约: ASPD/ATK +; a wanted enemy is aimed at for
assert.equal(snipe.length, 1);
assert.equal(snipe[0].dmg.canDodge, false);
const wanted = u.def.bonds.includes('lateranoShip') ? tal(id).damage_scale : 1;
approx(snipe[0].amount, u.s.atk * bb['attack@fin_atk_scale'] * wanted, `${id} fin × wanted`);
approx(snipe[0].amount, aimingAtk * bb['attack@fin_atk_scale'] * wanted, `${id} fin × wanted`);
done(h);
}
});
+61
View File
@@ -0,0 +1,61 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { makeBattle, enemyRec, checkInvariants } from '../helpers/battleHarness.js';
const LEM = 'chess_char_6_01_a';
function field({ skillIndex = 2, second = false, unite = false } = {}) {
const units = [{ uid: 1, chessId: LEM, row: 12, col: 2, skillIndex, potential: 1 },
{ uid: 2, chessId: 'chess_char_2_01_a', row: 9, col: 7 }];
if (second) units.push({ uid: 3, chessId: LEM, row: 12, col: 4, skillIndex, potential: 1 });
const config = { defs: { enemies: { target: enemyRec({ key: 'target', rank: 'ELITE', hp: 1e9, speed: 0 }) } },
units, enemies: [{ key: 'target', pos: [9, 8] }], autoFinish: false, hooks: ['skillStart'] };
if (unite) {
config.kind = 'unite';
config.players = [{ playerId: 'p1', seat: 0, side: 'L', colOffset: 0, bonds: {}, units: [{ kind: 'chess', ...units[0] }] },
{ playerId: 'p2', seat: 1, side: 'L', colOffset: 0, bonds: {}, units: [{ kind: 'chess', ...units[1] }] }];
delete config.units;
}
const h = makeBattle(config);h.step();return { h, u: h.unit(1), e: h.enemies()[0] };
}
function done(h) { checkInvariants(h.b); assert.deepEqual(h.b.errors, []); }
for (const skillIndex of [0, 1, 2]) for (const unite of [false, true]) {
test(`通缉 opens S${skillIndex + 1} outside original range (unite=${unite})`, () => {
const { h, u, e } = field({ skillIndex, unite });
u.skill.gainSp(999);h.run(8.5);
assert.ok(e.findBuff('lemuen:wanted'));
assert.ok(h.hooksOf('skillStart').some(c => c.unit === u));
done(h);
});
}
test('a new 通缉 timer waits for selectability; an existing timer continues through sleep', () => {
for (const startBeforeSleep of [false, true]) {
const { h, u, e } = field();
if (startBeforeSleep) h.run(0.5);
h.b.applyStatus(e, 'sleep', { duration: 10 });
u.skill.gainSp(999);h.run(8.5);
assert.equal(!!e.findBuff('lemuen:wanted'), startBeforeSleep);
assert.equal(u.skill.active, false, 'sleep cannot trigger the skill');
h.run(2);
assert.equal(u.skill.active, startBeforeSleep, 'waking a marked enemy opens S3');
if (!startBeforeSleep) { h.run(8);assert.ok(e.findBuff('lemuen:wanted')); }
done(h);
}
});
test('each 蕾缪安 clears only her own timer/mark on exit and starts fresh on redeployment', () => {
const { h, u, e } = field({ second: true });const other = h.unit(3);
h.run(8.5);assert.ok(e.findBuff('lemuen:wanted'));
h.b.retreat(u, { reason: 'test' });assert.ok(e.findBuff('lemuen:wanted'), 'other source remains');
h.b.retreat(other, { reason: 'test' });assert.equal(e.findBuff('lemuen:wanted'), null);
h.b.redeploy(u, { free: true });h.run(0.5);assert.equal(e.findBuff('lemuen:wanted'), null);
h.run(8);assert.ok(e.findBuff('lemuen:wanted'));
done(h);
});
test('immediate redeployment cannot attack through a cleared wanted range', () => {
const { h, u, e } = field();h.run(8.5);assert.ok(e.findBuff('lemuen:wanted'));
h.b.retreat(u, { reason: 'test' });
assert.equal(e.findBuff('lemuen:wanted'), null);
assert.equal(h.b.enemiesInKeys(u.extraRangeKeys || [], u, u.profile).length, 0);
h.b.redeploy(u, { free: true });const before = u.stats.attacks;u.atkCd = 0;
h.step();assert.equal(u.stats.attacks, before, 'the allies phase sees the cleared range immediately');
done(h);
});