Files
ark-stronghold-protocol/test/sim/feedback2-stealth-splash.test.js
T

360 lines
23 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// GitHub issue #32 item 6 (after 0.1.1): 「隐蔽单位仍会受到溅射伤害」 — a 隐匿 operator still took enemy splash.
//
// Official: PRTS 作战机制 §隐匿 "隐匿效果使得获得该效果的单位无法被任何敌方的能力索敌选中" (a stealthed operator that blocks an
// enemy is attacked by it whatever its selectability: "因为“阻挡优先级最高”的效果,敌人会无视一切可选性对该干员进行攻击");
// §AOE伤害判定 "AOE的判定是对攻击范围内的每个可以被选中的敌人进行判定"; §隐匿与Buff的关系 "隐匿状态下的单位一般无法被敌方的索敌
// 机制和Buff选择器选中为目标" (寒霜's debuff being its example of one that ignores 隐匿); PRTS 异常效果 §无法选择: a unit with
// 隐匿 / 不可选中 / 无敌 / 对地规避 is skipped by "常见的、来自不同阵营的“选择”行为" unless the ability "无视可选性" — as PRTS
// marks 萨卡兹悖谬暴虐兵长's 暴击 "(无视无法选择,无视迷彩)" and 假想敌:淤困's burst spread "(中点判定,无视目标可选性,不受迷彩
// 制约)", while "'直接选中'的能力…不受这些仅在选择时生效的异常效果制约"; gamedata_const ba.invisible 隐匿 "不阻挡时不成为敌方攻击
// 的目标" vs ba.camou 迷彩 "不阻挡时不成为敌方普通攻击的目标(无法躲避溅射类攻击)"; PRTS 异常效果 迷彩 "所有光环类能力、以及涉及
// 中点判定/格子判定的效果均不受迷彩制约"; PRTS 选择器 "所有触发选择器通常不无视迷彩" (an area skill is cast for a target it may
// attack; a normal attack on every operator in range skips 迷彩 too).
// Cause: the enemy side's area effects (content/enemies.js, content/bosses.js) took every ally in the area; only the
// operator side skipped an unblocked 隐匿 enemy (Battle.foesInRadius, 0.1.1).
// Now: targeting.js areaSelectable / enemies.js areaAllies, areaAlliesInTiles, fieldAllies — no 隐匿 ally unless it blocks
// the enemy, no untargetable or sleeping one, no airborne 起飞 one for a ground enemy; 迷彩 is not checked. Buff auras:
// auraSelectable / auraAllies (the same without 对地规避). A locked target is hit as a direct pick (targetAndArea).
// (DESIGN §22.12.)
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { makeBattle, enemyRec, checkInvariants } from '../helpers/battleHarness.js';
import { hasGeneratedData } from '../../server/sim/simdata.js';
import { areaSelectable, auraSelectable } from '../../server/sim/targeting.js';
import { areaAllies } from '../../server/sim/content/enemies.js';
const REAL = { skip: !hasGeneratedData() };
const JF = 'chess_char_1_02_a'; // 角峰 (重装, blocks 3)
const GM = 'chess_char_1_10_a'; // 古米 (重装, blocks 3)
const HD = 'chess_char_1_05_a'; // 红豆 (先锋)
const SKULSR = 'enemy_1500_skulsr'; // 碎骨: unblocked, its attacks are grenades that splash the target's surroundings
const POMPEII = 'enemy_1050_lslime'; // “庞贝”: blocked ⇒ every 10 s a radius-1 arts self-blast
const LUCIEN = 'enemy_2016_csphtm'; // 卢西恩,“猩红血钻” (boss_5): 【aoe】 radius 2, "需要目标", "该技能伤害无视迷彩"
const BRUTE = 'enemy_1320_wdrrl_2'; // 萨卡兹悖谬暴虐兵长: first attack's 暴击 splash "无视无法选择,无视迷彩"
const PARASITE = 'enemy_9007_acelem'; // 假想敌:淤困: parasitises its blocker; the host's burst spreads "无视目标可选性"
const FAKE_SUIT = 'chess_item_4_04_e_a'; // 伪装服: first damage taken ⇒ 隐匿 15 s
const done = (h) => { checkInvariants(h.b); assert.deepEqual(h.b.errors.map((e) => `${e.label} ${e.message}`), []); };
const stealth = (h, u) => h.b.applyStatus(u, 'stealth', { duration: 999, source: u });
const camou = (h, u) => h.b.applyStatus(u, 'camou', { duration: 999, source: u });
/** Every damage instance the enemy `key` deals: { t, target, attack (a normal attack), skill, amount, tags }. */
function damageLog(h, key) {
const log = [];
h.b.on('damaged', (c) => {
if (!c.source || c.source.defId !== key) return;
log.push({ t: h.b.time, target: c.target, attack: !!c.dmg.isAttack, skill: !!c.dmg.isSkill, amount: c.amount, tags: c.dmg.tags || [] });
}, { priority: 1000 });
return log;
}
function countFx(h, kind) {
const n = { v: 0 };
const fx0 = h.b.fx.bind(h.b);
h.b.fx = (k, p) => { if (k === 'explode' && p && p.kind === kind) n.v++; return fx0(k, p); };
return n;
}
test('areaSelectable: no 隐匿 ally unless it blocks the source, no untargetable / sleeping one, no 起飞 one for a ground source; 迷彩 is selected', () => {
const h = makeBattle({
seed: 2, autoFinish: false, timeLimit: 60,
defs: { enemies: { enemy_g: enemyRec({ key: 'enemy_g', hp: 1e7, speed: 0 }), enemy_f: enemyRec({ key: 'enemy_f', hp: 1e7, speed: 0, motion: 'FLY' }) } },
units: [{ chessId: JF, row: 9, col: 6 }, { chessId: GM, row: 10, col: 6 }, { chessId: HD, row: 9, col: 7 }],
enemies: [{ key: 'enemy_g', pos: [9, 6] }, { key: 'enemy_f', pos: [11, 6] }],
});
h.step(2);
const jf = h.unit(JF), gm = h.unit(GM), hd = h.unit(HD);
const g = h.b.enemies.find((e) => e.defId === 'enemy_g'), f = h.b.enemies.find((e) => e.defId === 'enemy_f');
assert.equal(g.blockedBy, jf, '角峰 blocks the ground enemy');
assert.ok(f.isFlying && !g.isFlying);
for (const u of [jf, gm, hd]) assert.ok(areaSelectable(g, u) && areaSelectable(f, u) && areaSelectable(null, u), `${u.def.name}: plain ⇒ selected`);
stealth(h, jf); stealth(h, gm);
assert.equal(areaSelectable(g, jf), true, '隐匿 but blocking the source: selected');
assert.equal(areaSelectable(f, jf), false, '隐匿, blocking another enemy: not selected');
assert.equal(areaSelectable(g, gm), false, '隐匿, not blocking: not selected');
assert.equal(areaSelectable(null, gm), false, '隐匿 vs an effect with no selecting enemy');
assert.deepEqual(areaAllies(h.b, g, g.x, g.y, 1.01).map((u) => u.def.name).sort(), [jf, hd].map((u) => u.def.name).sort(), 'areaAllies: the 隐匿 bystander is left out');
camou(h, hd);
assert.equal(areaSelectable(g, hd), true, '迷彩: still selected by an area effect ("无法躲避溅射类攻击")');
assert.equal(auraSelectable(g, gm), false, 'a buff aura: 隐匿, not blocking — not taken');
assert.equal(auraSelectable(g, jf), true, 'a buff aura: 隐匿 blocker — taken');
h.b.addBuff(gm, { key: 'test:liftoff', persist: true, flags: { liftoff: true } });
h.b.removeBuff(gm, 'stealth');
assert.equal(areaSelectable(g, gm), false, '起飞 vs a ground source (对地规避)');
assert.equal(areaSelectable(f, gm), true, '起飞 vs a flying source');
assert.equal(areaSelectable(null, gm), true, '起飞 vs no source');
assert.equal(auraSelectable(g, gm), true, '起飞 vs a ground enemy\'s buff aura: still taken (§21.22)');
h.b.removeBuff(gm, 'test:liftoff');
h.b.addBuff(gm, { key: 'test:untargetable', persist: true, flags: { untargetable: true } });
assert.equal(areaSelectable(f, gm), false, 'untargetable (不可选中)');
h.b.removeBuff(gm, 'test:untargetable');
h.b.addBuff(gm, { key: 'test:sleep', persist: true, flags: { sleep: true } });
assert.equal(areaSelectable(f, gm), false, 'asleep (沉睡 = 无敌)');
h.b.removeBuff(gm, 'test:sleep');
h.b.kill(g, null);
assert.equal(areaSelectable(g, jf), false, 'a dead source (death blast) blocks nobody: its 隐匿 former blocker is spared');
done(h);
});
/** 碎骨 walks lane row 9 and grenades 角峰 on (10,6); 古米 stands next to it (inside the grenade's 3×3 — 0.1.3, PRTS 碎骨). */
function grenades({ gm = [10, 7], items = [], status = null, seconds = 25 } = {}) {
const h = makeBattle({
seed: 5, timeLimit: 120, autoFinish: false,
units: [{ chessId: JF, row: 10, col: 6, dir: 'RIGHT' }, { chessId: GM, row: gm[0], col: gm[1], dir: 'RIGHT', items }],
enemies: [{ key: SKULSR, route: 0, time: 3 }],
});
h.step(1);
const jf = h.unit(JF), g = h.unit(GM);
if (status) h.b.applyStatus(g, status, { duration: 999, source: g });
const log = damageLog(h, SKULSR);
h.run(seconds);
return { h, jf, g, log, onJf: log.filter((x) => x.target === jf && x.attack), splashOnG: log.filter((x) => x.target === g && !x.attack), attacksOnG: log.filter((x) => x.target === g && x.attack) };
}
test('#32.6 碎骨: a 隐匿 operator next to the grenade\'s target takes no splash; a 迷彩 one still does', REAL, () => {
const hid = grenades({ status: 'stealth' });
assert.ok(hid.onJf.length >= 4, `4+ grenades on 角峰 (${hid.onJf.length})`);
assert.ok(hid.g.s.flags.stealth, '古米 stays 隐匿 (999 s)');
assert.deepEqual(hid.log.filter((x) => x.target === hid.g).map((x) => `${x.t.toFixed(2)} ${Math.round(x.amount)}`), [], '隐匿, not blocking: nothing reaches it (in 0.1.1: 4 splashes, knocked out)');
done(hid.h);
const cam = grenades({ status: 'camou' });
assert.ok(cam.onJf.length >= 4, `4+ grenades on 角峰 (${cam.onJf.length})`);
assert.equal(cam.attacksOnG.length, 0, '迷彩: never the grenade\'s target');
const standing = (t) => cam.g.alive || t <= cam.g.deathAt + 1e-6; // the splashes may knock it out
assert.ok(cam.splashOnG.length >= 3, `迷彩: splashed (${cam.splashOnG.length})`);
assert.equal(cam.splashOnG.length, cam.onJf.filter((x) => standing(x.t)).length, '迷彩: splashed by every grenade while it stands (ba.camou "无法躲避溅射类攻击")');
assert.ok(cam.splashOnG.every((x) => x.skill && x.amount > 0));
done(cam.h);
});
test('#32.6 碎骨: 伪装服\'s 隐匿 (its carrier\'s first damage) spares the carrier from the following splashes until it ends', REAL, () => {
// 古米 on (10,5) — deployed before 角峰, so 角峰 is the grenades' target (latest deployed) and 古米 its neighbour
const r = grenades({ gm: [10, 5], items: [FAKE_SUIT], seconds: 32 });
const first = r.splashOnG[0];
assert.ok(first && first.amount > 0, 'the first grenade splashes 古米 — its first damage');
const end = first.t + 15;
const during = r.log.filter((x) => x.target === r.g && x.t > first.t + 1e-6 && x.t < end - 1e-6);
const jfDuring = r.onJf.filter((x) => x.t > first.t + 1e-6 && x.t < end - 1e-6);
assert.ok(jfDuring.length >= 3, `grenades kept landing next to it (${jfDuring.length})`);
assert.deepEqual(during.map((x) => `${x.t.toFixed(2)} ${Math.round(x.amount)}`), [], '伪装服 隐匿 (flag `stealth`): no splash for its 15 s');
assert.ok(r.log.some((x) => x.target === r.g && x.t > end), '…and hit again once its 隐匿 ends');
done(r.h);
});
test('#32.6 “庞贝”\'s self-blast while blocked hits its 隐匿 blocker and a 迷彩 bystander, not a 隐匿 bystander', REAL, () => {
const h = makeBattle({
seed: 3, timeLimit: 120, autoFinish: false,
units: [{ chessId: JF, row: 9, col: 6, dir: 'RIGHT' }, { chessId: GM, row: 10, col: 6, dir: 'RIGHT' }, { chessId: HD, row: 9, col: 7, dir: 'RIGHT' }],
enemies: [{ key: POMPEII, pos: [9, 6], time: 1, mods: { hpMul: 1e3, speedMul: 0, atkMul: 0.01 } }],
});
h.step(1);
const jf = h.unit(JF), gm = h.unit(GM), hd = h.unit(HD);
stealth(h, jf); stealth(h, gm); camou(h, hd);
const log = damageLog(h, POMPEII), blasts = countFx(h, 'selfBlast');
h.run(12);
const p = h.b.enemies.find((e) => e.defId === POMPEII);
assert.equal(p.blockedBy, jf, 'blocked by the 隐匿 角峰');
assert.equal(blasts.v, 1, 'one self-blast in 12 s');
const blast = (u) => log.filter((x) => x.target === u && x.skill && !x.attack).length;
assert.equal(blast(jf), 1, '隐匿 blocker: hit (a blocked enemy ignores its blocker\'s selectability)');
assert.equal(blast(hd), 1, '迷彩 bystander: hit');
assert.equal(blast(gm), 0, '隐匿 bystander (distance 1, not blocking): spared');
done(h);
});
test('#32.6 boss: 卢西恩\'s 【aoe】 is not cast for 隐匿 / 迷彩 operators alone; cast, it hits the 迷彩 and plain ones, not the 隐匿 one', REAL, () => {
const run = (plain) => {
const units = [{ chessId: JF, row: 10, col: 5, dir: 'RIGHT' }, { chessId: GM, row: 11, col: 6, dir: 'RIGHT' }];
if (plain) units.push({ chessId: HD, row: 9, col: 6, dir: 'RIGHT' });
const h = makeBattle({ seed: 3, timeLimit: 400, autoFinish: false, units, enemies: [{ key: LUCIEN, pos: [10, 6], time: 1, mods: { hpMul: 1e3, speedMul: 0, atkMul: 0.01 } }] });
h.step(1);
stealth(h, h.unit(JF)); camou(h, h.unit(GM));
const log = damageLog(h, LUCIEN), casts = countFx(h, 'crimsonAoe');
h.run(30);
const aoe = (id) => log.filter((x) => x.target === h.unit(id) && x.skill && !x.attack).length;
return { h, casts: casts.v, aoe };
};
const alone = run(false);
assert.equal(alone.casts, 0, 'only a 隐匿 and a 迷彩 operator within 2: no cast (its trigger selection skips both)');
done(alone.h);
const r = run(true);
assert.ok(r.casts >= 2, `cast with 红豆 in range (${r.casts})`);
assert.equal(r.aoe(HD), r.casts, 'plain: hit by every cast');
assert.equal(r.aoe(GM), r.casts, '迷彩: hit by every cast ("该技能伤害无视迷彩")');
assert.equal(r.aoe(JF), 0, '隐匿, not blocking: never hit');
done(r.h);
});
test('#32.6 ignoreSelect: 萨卡兹悖谬暴虐兵长\'s 暴击 ("无视无法选择") and 假想敌:淤困\'s burst spread ("无视目标可选性") still reach a 隐匿 operator', REAL, () => {
{
const h = makeBattle({
seed: 3, timeLimit: 120, autoFinish: false,
units: [{ chessId: JF, row: 9, col: 6, dir: 'RIGHT' }, { chessId: GM, row: 10, col: 6, dir: 'RIGHT' }],
enemies: [{ key: BRUTE, pos: [9, 6.4], time: 1, mods: { hpMul: 1e3, speedMul: 0 } }],
});
h.step(1);
const gm = h.unit(GM);
stealth(h, gm);
const log = damageLog(h, BRUTE);
h.run(6);
assert.equal(h.b.enemies.find((e) => e.defId === BRUTE).blockedBy, h.unit(JF), '角峰 (block 3) holds it');
const splash = log.filter((x) => x.target === gm && x.tags.includes('aoeAttack'));
assert.equal(splash.length, 1, '暴击 splash on the 隐匿 古米 next to its target (once: "此技能仅能触发一次")');
assert.ok(splash[0].amount > 0);
done(h);
}
{
const h = makeBattle({
seed: 3, timeLimit: 120, autoFinish: false,
units: [{ chessId: JF, row: 9, col: 6, dir: 'RIGHT' }, { chessId: GM, row: 10, col: 6, dir: 'RIGHT' }, { chessId: HD, row: 9, col: 7, dir: 'RIGHT' }],
enemies: [{ key: PARASITE, pos: [9, 6.4], time: 1, mods: { hpMul: 1e3, speedMul: 0 } }],
});
h.step(1);
const jf = h.unit(JF), gm = h.unit(GM), hd = h.unit(HD);
stealth(h, gm);
h.b.addBuff(hd, { key: 'test:liftoff', persist: true, flags: { liftoff: true, blockFly: true } }); // 起飞 (蒂比's flag)
h.run(1.5);
assert.ok(jf.findBuff('ab:parasite'), '淤困 parasitises its blocker 角峰');
const src = h.b.enemies.find((e) => e.defId === PARASITE);
h.b.dealDamage(src, jf, { type: 'element', element: 'burn', amount: 5000, tags: ['test'] });
h.step(1);
assert.ok(jf.findBuff('burnBurst') || jf.elem.burn >= jf.gaugeMax - 1e-6, 'the host bursts');
assert.ok(gm.elem.burn > 0, '隐匿 neighbour: 1000 burn spread onto it');
assert.ok(hd.elem.burn > 0, 'airborne 起飞 neighbour: reached too (until 0.1.2 the pipeline refused a ground enemy\'s fill)');
done(h);
}
});
// ---------------------------------------------------------------------------------------------------------------
// review of the first round
const liftoff = (h, u) => h.b.addBuff(u, { key: 'test:liftoff', persist: true, flags: { liftoff: true, blockFly: true } }); // 起飞 (蒂比's flag)
const pool = (hp) => ({ hp, maxHp: hp, damage(pid, a) { this.hp = Math.max(0, this.hp - a); } });
test('#32.6 review: 蚀裂\'s 毒雾 and the buff auras of 深池伙友卫队 / 扎罗 (远古威慑) keep a 隐匿 operator out; an airborne 起飞 one is still reached', REAL, () => {
// 假想敌:蚀裂 knocked out with no killer: its cloud (r 0.8) covers 角峰 (9,6) and 古米 (10,6) around its spot (9.5, 6)
for (const mode of ['stealth', 'liftoff']) {
const h = makeBattle({
seed: 3, timeLimit: 60, autoFinish: false,
units: [{ chessId: JF, row: 9, col: 6, dir: 'RIGHT' }, { chessId: GM, row: 10, col: 6, dir: 'RIGHT' }],
enemies: [{ key: 'enemy_9006_actoxi', pos: [9.5, 6], time: 1, mods: { hpMul: 1e3, speedMul: 0, atkMul: 0.01 } }],
});
h.step(1);
const jf = h.unit(JF), gm = h.unit(GM);
if (mode === 'stealth') stealth(h, gm); else liftoff(h, gm);
const ticks = new Map([[jf, 0], [gm, 0]]);
h.b.on('damaged', (c) => { if ((c.dmg.tags || []).includes('poison') && ticks.has(c.target)) ticks.set(c.target, ticks.get(c.target) + 1); }, { priority: 1000 });
h.run(1);
h.b.kill(h.b.enemies.find((e) => e.defId === 'enemy_9006_actoxi'), null);
h.run(10);
assert.ok(ticks.get(jf) >= 8, `the cloud ticks on 角峰 (${ticks.get(jf)})`);
if (mode === 'stealth') assert.equal(ticks.get(gm), 0, '隐匿 古米: no tick (in 0.1.1 every one)');
else assert.equal(ticks.get(gm), ticks.get(jf), 'airborne 古米: every tick (a sourceless cloud selects no ground enemy, §21.22)');
done(h);
}
// 深池伙友卫队's field (with 影刃 next to it) and 扎罗's 远古威慑 (during its 重生): 角峰 plain, 古米 隐匿, 红豆 起飞
const aura = (enemies, buff, ko) => {
const h = makeBattle({
seed: 3, timeLimit: 120, autoFinish: false,
units: [{ chessId: JF, row: 10, col: 6, dir: 'RIGHT' }, { chessId: GM, row: 9, col: 7, dir: 'RIGHT' }, { chessId: HD, row: 10, col: 8, dir: 'RIGHT' }],
enemies: enemies.map((key, i) => ({ key, pos: [10 + i, 7], time: 1, mods: { hpMul: 1e3, speedMul: 0, atkMul: 0.001 } })),
});
h.step(1);
stealth(h, h.unit(GM)); liftoff(h, h.unit(HD));
h.run(1);
if (ko) h.b.kill(h.b.enemies.find((e) => e.defId === ko), null);
h.run(1);
const got = [JF, GM, HD].map((id) => !!h.unit(id).findBuff(buff));
done(h);
return got;
};
assert.deepEqual(aura(['enemy_1174_duholy', 'enemy_1175_dushdo_2'], 'ab:forceField'), [true, false, true], '深池伙友卫队: 角峰 yes, 隐匿 古米 no, 起飞 红豆 yes');
assert.deepEqual(aura(['enemy_1535_wlfmster'], 'ab:ancientAwe', 'enemy_1535_wlfmster'), [true, false, true], '扎罗 远古威慑: 角峰 yes, 隐匿 古米 no, 起飞 红豆 yes');
});
test('#32.6 review: 死亡之眼\'s burst still hits its locked target that turned 隐匿 mid-channel (a direct pick); a 隐匿 bystander is spared', REAL, () => {
const h = makeBattle({
seed: 3, timeLimit: 120, autoFinish: false,
units: [{ chessId: GM, row: 9, col: 6, dir: 'RIGHT' }, { chessId: JF, row: 10, col: 6, dir: 'RIGHT' }, { chessId: HD, row: 9, col: 5, dir: 'RIGHT' }],
enemies: [{ key: 'enemy_1275_dwlock_2', pos: [9, 8], time: 1, mods: { hpMul: 1e3, speedMul: 0, atkMul: 0.2 } }],
});
h.step(1);
const gm = h.unit(GM), jf = h.unit(JF), hd = h.unit(HD);
stealth(h, jf);
let locked = null;
const fx0 = h.b.fx.bind(h.b);
h.b.fx = (k, p) => {
if (k === 'beam' && p && p.kind === 'deathEye' && !locked) { locked = p.to; h.b.after(1.5, () => stealth(h, gm)); } // 隐匿 mid-channel
return fx0(k, p);
};
const burst = [];
h.b.on('damaged', (c) => { if (c.source && c.source.defId === 'enemy_1275_dwlock_2' && c.dmg.element === 'apoptosis' && c.amount > 50) burst.push(c.target); }, { priority: 1000 });
h.run(30);
assert.equal(locked, gm.id, 'the channel locks 古米');
assert.ok(gm.s.flags.stealth, '古米 turned 隐匿 during it');
assert.ok(burst.includes(gm), 'the burst hits its locked target');
assert.ok(burst.includes(hd), '…and the plain 红豆 next to it');
assert.ok(!burst.includes(jf), '…not the 隐匿 角峰 next to it');
done(h);
});
test('#32.6 review: the 斩胄之剑 / 破胄之锤 hover attack skips 隐匿 and 迷彩; the 自行炮\'s shots and 深溟巢涌者\'s pulse hit 迷彩; 祈祷邀约 (whole field) skips 隐匿', REAL, () => {
// hover attack (a normal attack on every operator in range): 角峰 plain, 古米 隐匿, 红豆 迷彩 — all within 1.6
for (const key of ['enemy_9014_acstma', 'enemy_9015_acstmb']) {
const h = makeBattle({
kind: 'boss', sharedBoss: pool(1e6), seed: 7, autoFinish: false, timeLimit: 600,
units: [{ chessId: JF, row: 10, col: 9, dir: 'RIGHT' }, { chessId: GM, row: 10, col: 7, dir: 'RIGHT' }, { chessId: HD, row: 10, col: 8, dir: 'RIGHT' }],
});
h.step();
stealth(h, h.unit(GM)); camou(h, h.unit(HD));
const blade = h.spawn(key, { pos: [3, 8], routeIndex: 0, mods: { speedMul: 0 }, tag: 'part' });
const hit = [];
h.b.on('damaged', (c) => { if (c.source === blade) hit.push(c.target); }, { priority: 1000 });
h.run(3);
assert.ok(hit.includes(h.unit(JF)), `${key}: hits the plain operator`);
assert.ok(!hit.includes(h.unit(GM)) && !hit.includes(h.unit(HD)), `${key}: not the 隐匿 or the 迷彩 one (in 0.1.1 it hit all three)`);
done(h);
}
// 自行炮: 红豆 (迷彩, full HP ratio) in the 9-tile zone around 角峰 (the highest max HP) is shot too
{
const h = makeBattle({
seed: 3, timeLimit: 120, autoFinish: false,
units: [{ chessId: JF, row: 9, col: 6, dir: 'RIGHT' }, { chessId: GM, row: 10, col: 6, dir: 'RIGHT' }, { chessId: HD, row: 10, col: 5, dir: 'RIGHT' }],
enemies: [{ key: 'enemy_1273_stmgun_2', pos: [9, 9], time: 1, mods: { hpMul: 1e3, speedMul: 0 } }],
});
h.step(1);
camou(h, h.unit(HD));
const log = damageLog(h, 'enemy_1273_stmgun_2');
h.run(40);
assert.ok(log.some((x) => x.target === h.unit(HD) && !x.attack), '迷彩 红豆: shot inside the zone (in 0.1.1 never)');
done(h);
}
// 深溟巢涌者: its pulse rides on its attack — with the plain 古米 in range the 迷彩 红豆 is hit by every pulse; alone it is never
for (const alone of [false, true]) {
const units = [{ chessId: HD, row: 9, col: 6, dir: 'RIGHT' }];
if (!alone) units.push({ chessId: GM, row: 10, col: 7, dir: 'RIGHT' });
const h = makeBattle({ seed: 3, timeLimit: 120, autoFinish: false, units, enemies: [{ key: 'enemy_1234_dsubrl', pos: [9, 7], time: 1, mods: { hpMul: 1e3, speedMul: 0 } }] });
h.step(1);
camou(h, h.unit(HD));
const log = damageLog(h, 'enemy_1234_dsubrl');
h.run(10);
const on = (id) => log.filter((x) => x.target === h.unit(id) && x.attack).length;
if (alone) assert.equal(on(HD), 0, '迷彩 alone in range: no attack, so no pulse [the engine attack needs a target]');
else { assert.ok(on(GM) >= 5, `plain 古米 pulsed (${on(GM)})`); assert.equal(on(HD), on(GM), '迷彩 红豆: every pulse too (in 0.1.1 none)'); }
done(h);
}
// “萨科塔昂首”'s 【祈祷邀约】 (whole field, "无视迷彩", no 无视无法选择): 角峰 slowed, the 隐匿 古米 not
{
const h = makeBattle({
seed: 3, timeLimit: 120, autoFinish: false,
units: [{ chessId: JF, row: 10, col: 6, dir: 'RIGHT' }, { chessId: GM, row: 9, col: 7, dir: 'RIGHT' }],
enemies: [{ key: 'enemy_10085_hllevi_2', pos: [10, 9], time: 1, route: 2, mods: { hpMul: 1e3, speedMul: 0 } }],
});
h.step(1);
stealth(h, h.unit(GM));
h.run(22);
assert.ok(h.unit(JF).findBuff('ab:roar'), '角峰: 受邀祈祷');
assert.ok(!h.unit(GM).findBuff('ab:roar'), '隐匿 古米: not selected');
done(h);
}
});