0.2.5-dev: the 外勤医疗 (Touch) reserve medic takes the initial aggro order of PR #523 and its detail opens in the preparation phase (PR #523 by @YinqiMargatroid, ported; the owner's decision of 2026-10-12); golden: one more scenario moves (named in docs/history/0.2.5.md)

This commit is contained in:
sganggs committed 2026-10-12 14:59:19 +08:00
1 parent 7fd0478dc0
commit 37e0fffb34
22 files changed
+293 -59

No files matched your search

+85
View File
@@ -711,6 +711,91 @@ test('Touch 外勤医疗: every player of a match with the band gets 预备干
cover('band_amedic');
});
test('外勤医疗: 预备干员-医疗 and Touch keep preplaced targeting order against a flying 守墓石像', () => {
const defs = {
harold: chessRec({ id: 'harold', profession: 'TANK', skill: null, stats: { atk: 0, maxHp: 1e7, blockCnt: 0 } }),
elite1: chessRec({ id: 'elite1', golden: true, profession: 'TANK', skill: null, stats: { atk: 0, maxHp: 1e7, blockCnt: 0 } }),
elite2: chessRec({ id: 'elite2', golden: true, profession: 'TANK', skill: null, stats: { atk: 0, maxHp: 1e7, blockCnt: 0 } }),
};
for (const [eliteCount, mapCharId] of [[1, 'char_605_cmedic'], [2, 'char_613_acmedc']]) {
const h = makeBattle({
stageId: 'act2autochess_m01', bandId: 'band_amedic', autoFinish: false, timeLimit: 100,
defs: { chess: defs }, units: [
{ chessId: 'harold', row: 11, col: 3 },
{ chessId: 'elite1', row: 12, col: 8 },
...(eliteCount === 2 ? [{ chessId: 'elite2', row: 9, col: 8 }] : []),
],
});
h.step();
const medic = h.unit(mapCharId), harold = h.unit('harold');
const ops = h.allies().filter((u) => u.kind === 'op');
assert.ok(medic?.alive && harold?.alive, mapCharId);
assert.ok(ops.every((u) => medic.deploySeq > u.deploySeq), 'battleStart creates the map character after initial deployment');
assert.ok(ops.every((u) => medic.aggroSeq < u.aggroSeq), 'its targeting order precedes every initial operator');
assert.equal(medic.s.taunt, harold.s.taunt, 'the two targets have equal taunt');
const gargoyle = h.spawn('enemy_1172_dugago', { pos: [10, 3], routeIndex: 0, mods: { speedMul: 0 } });
assert.ok(gargoyle);
h.b.kill(gargoyle, null); // first knock-out changes 守墓石像 into its flying form
assert.ok(gargoyle.alive);
const attacks = [];
h.b.on('enemyAttackStart', ({ enemy, targets }) => {
if (enemy === gargoyle && gargoyle.motion === 'FLY') attacks.push({ target: targets[0], x: enemy.x, y: enemy.y });
});
assert.ok(h.runUntil(() => attacks.length > 0, 20), 'flying gargoyle starts an attack');
const { target, x, y } = attacks[0];
const reach = gargoyle.base.rangeRadius + 0.25;
for (const u of [harold, medic]) assert.ok((u.x - x) ** 2 + (u.y - y) ** 2 <= reach ** 2, `${u.defId} is in range at target lock`);
assert.equal(gargoyle.blockedBy, null);
assert.equal(target, harold, `${mapCharId}: the later ordinary operator is selected`);
h.b.addBuff(medic, { key: 'test:taunt', mods: { taunt: 1 }, persist: true });
assert.ok(h.runUntil(() => attacks.length > 1, 20), 'gargoyle starts another attack');
assert.equal(attacks[1].target, medic, 'higher taunt still takes precedence over deployment order');
checkInvariants(h.b);
}
});
test('外勤医疗: 联防右侧 Touch retains its prepared targeting order', () => {
const defs = {
harold: chessRec({ id: 'harold', profession: 'TANK', skill: null, stats: { atk: 0, maxHp: 1e7, blockCnt: 0 } }),
elite1: chessRec({ id: 'elite1', golden: true, profession: 'TANK', skill: null, stats: { atk: 0, maxHp: 1e7, blockCnt: 0 } }),
elite2: chessRec({ id: 'elite2', golden: true, profession: 'TANK', skill: null, stats: { atk: 0, maxHp: 1e7, blockCnt: 0 } }),
};
const players = [
{ playerId: 'L', seat: 0, side: 'L', colOffset: 0, bandId: 'band_amedic', bonds: {}, playerEffects: [], units: [] },
{ playerId: 'R', seat: 1, side: 'R', colOffset: 8, bandId: 'band_amedic', bonds: {}, playerEffects: [], units: [
{ uid: 1, kind: 'chess', chessId: 'harold', row: 11, col: 3 },
{ uid: 2, kind: 'chess', chessId: 'elite1', row: 12, col: 8 },
{ uid: 3, kind: 'chess', chessId: 'elite2', row: 9, col: 8 },
] },
];
const h = makeBattle({ kind: 'unite', stageId: 'act2autochess_m01', players, defs: { chess: defs }, autoFinish: false });
h.step();
const leftMedic = h.allies().find((u) => u.ownerId === 'L' && u.defId === 'char_605_cmedic');
const rightTouch = h.allies().find((u) => u.ownerId === 'R' && u.defId === 'char_613_acmedc');
const harold = h.allies().find((u) => u.ownerId === 'R' && u.defId === 'harold');
assert.ok(leftMedic && rightTouch && harold);
assert.deepEqual([leftMedic.tileR, leftMedic.tileC], [10, 2]);
assert.deepEqual([rightTouch.tileR, rightTouch.tileC], [10, 10]);
assert.ok(leftMedic.aggroSeq < rightTouch.aggroSeq && rightTouch.aggroSeq < harold.aggroSeq);
assert.ok(rightTouch.deploySeq > harold.deploySeq);
const gargoyle = h.spawn('enemy_1172_dugago', { pos: [10, 11], routeIndex: 0, mods: { speedMul: 0 } });
assert.ok(gargoyle);
h.b.kill(gargoyle, null);
const attacks = [];
h.b.on('enemyAttackStart', ({ enemy, targets }) => {
if (enemy === gargoyle && enemy.motion === 'FLY') attacks.push({ target: targets[0], x: enemy.x, y: enemy.y });
});
assert.ok(h.runUntil(() => attacks.length > 0, 20));
const { target, x, y } = attacks[0];
const reach = gargoyle.base.rangeRadius + 0.25;
for (const u of [harold, rightTouch]) assert.ok((u.x - x) ** 2 + (u.y - y) ** 2 <= reach ** 2);
assert.equal(gargoyle.blockedBy, null);
assert.equal(target, harold);
checkInvariants(h.b);
});
test('coverage: every band is exercised by a test above', () => {
cover('band_dusk'); // 夕: 画卷 grant (fixed-round test) + 墨色真颜 (battle test)
const missing = Object.keys(DATA.bands).filter((id) => !COVER.has(id)).sort();
+2
View File
@@ -511,6 +511,7 @@ test('band map characters: spawnMapChar puts 预备干员-医疗 at its stage sl
assert.ok(med);
assert.deepEqual([med.tileR, med.tileC], [10, 2]);
const gu = h.unit('test_guard');
assert.ok(med.aggroSeq > gu.aggroSeq, 'an ad-hoc map character keeps ordinary summon priority');
gu.hp = 5000;
h.run(4);
assert.ok(gu.hp > 5000, 'healed');
@@ -518,6 +519,7 @@ test('band map characters: spawnMapChar puts 预备干员-医疗 at its stage sl
h.step();
const touch = spawnMapChar(h.b, 'p1', TOKEN_IDS.touch);
assert.ok(touch);
assert.ok(touch.aggroSeq > gu.aggroSeq, 'an ad-hoc Touch also keeps ordinary summon priority');
touch.skill.activate('test', { free: true });
gu.hp = 3000;
h.run(4);