// 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, even the one // blocking the source (GitHub #97, owner 2026-10-04; the blocked enemy's own attack still lands via canTargetAlly), // 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 recorded the 0.1.2 blocker exception; §23 withdraws it for areas and auras.) 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 even when 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), false, '隐匿 blocker: an area still skips it'); 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(), [hd.def.name], 'areaAllies: the 隐匿 blocker and the 隐匿 bystander are 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), false, 'a buff aura: 隐匿 blocker — not 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 misses its 隐匿 blocker and a 隐匿 bystander, and hits 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), 0, '隐匿 blocker: the blocked enemy\'s blast does not select it'); assert.ok(log.some((x) => x.target === jf && x.attack), 'the blocked enemy\'s own attack still hits that 隐匿 blocker'); 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 is a talent aura (no attack, 0.1.3) — the 迷彩 红豆 is hit by every pulse, with the plain 古米 in range // or alone (until 0.1.3 the pulse rode on its attack, which needed a target: alone it was never hit) 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.tags.includes('nestPulse')).length; assert.equal(log.filter((x) => x.attack).length, 0, 'no normal attack'); assert.ok(on(HD) >= 9, `迷彩 红豆 pulsed every second (${on(HD)}; alone: ${alone})`); if (!alone) assert.equal(on(HD), on(GM), '迷彩 红豆: every pulse the plain 古米 takes'); 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); } });