diff --git a/server/sim/content/bosses.js b/server/sim/content/bosses.js index 271d0236..1e717de5 100644 --- a/server/sim/content/bosses.js +++ b/server/sim/content/bosses.js @@ -127,7 +127,7 @@ const PIPE_STRIKE_GAP = 0.4; const TENTACLE_STUN = 5, TENTACLE_RADIUS = 1; /** 崩坍 delay (PRTS 1 s), targets per growth stage (PRTS 2/4/8) and 物种爆发 delay (PRTS 4 s). */ const ROCKFALL_DELAY = 1, ROCKFALL_TARGETS = [2, 4, 8], DOOM_DELAY = 4; -/** 铳: minimum attack speed (PRTS "最低20") and charge hit radius (PRTS 0.35). */ +/** 铳: minimum attack speed (PRTS "最低20") and charge hit radius (PRTS 0.35 — also “碎铳之簧”'s 追逐模式 trample). */ const ASPD_FLOOR = 20, CHARGE_RADIUS = 0.35; /** 盲信之誓 link half-width (PRTS 0.5). */ const LINK_WIDTH = 0.5; @@ -617,10 +617,11 @@ function kitGun(ab, e, b) { if (s2) list.push({ cd: s2.cd, icd: s2.icd, cond: (b2) => b2.enemies.some((o) => o.alive && isSpring(o)), fire(b2, e2) { // 【末日布道】 springs dash to 铳, invulnerable, trampling operators + // PRTS “碎铳之簧” 追逐模式 "不进行普通攻击": `disarm` for the dash (until 0.1.3 it kept shooting while it ran) for (const sp of b2.enemies) { if (!sp.alive || !isSpring(sp) || !sp.mem.ab) continue; sp.mem.ab.dash = { until: b2.time + (s2.bb.dog_duration ?? 0), mul: 1 + (s2.bb.move_speed ?? 0), gun: e2, hit: new Set() }; - b2.addBuff(sp, { key: 'boss:dash', duration: s2.bb.dog_duration ?? 0, visible: true, flags: { invulnerable: true, noMove: true, unblockable: true } }); + b2.addBuff(sp, { key: 'boss:dash', duration: s2.bb.dog_duration ?? 0, visible: true, flags: { invulnerable: true, noMove: true, unblockable: true, disarm: true } }); b2.fx('dash', { x: sp.x, y: sp.y, id: sp.id, tx: e2.x, ty: e2.y }); } }, @@ -686,7 +687,8 @@ function kitSpring(ab, e) { if (!d) return; const g = d.gun && d.gun.alive ? d.gun : gun(b); const arrived = !g || stepToward(e2, g.x, g.y, e2.s.moveSpeed * d.mul * MOVE_SCALE * dt) || Math.hypot(g.x - e2.x, g.y - e2.y) < 1; - for (const u of areaAllies(b, e2, e2.x, e2.y, 0.5)) if (!d.hit.has(u)) { d.hit.add(u); hurt(b, e2, u, e2.s.atk, 'phys'); } + // 追逐模式 "对进入自身0.35半径范围内的我方单位(包括飞行单位)造成一次攻击力100%的物理普通伤害" (until 0.1.3: radius 0.5) + for (const u of areaAllies(b, e2, e2.x, e2.y, CHARGE_RADIUS)) if (!d.hit.has(u)) { d.hit.add(u); hurt(b, e2, u, e2.s.atk, 'phys'); } if (arrived || b.time >= d.until) { e2.mem.ab.dash = null; b.removeBuff(e2, 'boss:dash'); if (e2.route) e2.route.pts = null; } }, }, diff --git a/server/sim/content/enemies.js b/server/sim/content/enemies.js index ee62250f..ecafa4a1 100644 --- a/server/sim/content/enemies.js +++ b/server/sim/content/enemies.js @@ -701,11 +701,6 @@ const maxTargets = (n) => ({ spawn(b, e) { if (n > 1) e.profile.maxTargets = n; const ep = (el, ratio, sil = false) => ({ sil, dealt(c, b, e) { if (ratio > 0) elem(b, e, c.target, el, e.s.atk * ratio); } }); /** Status on every attack hit. */ const onHitStatus = (key, dur, value) => ({ dealt(c, b, e) { if (dur > 0) b.applyStatus(c.target, key, { duration: dur, source: e, value }); } }); -/** - * The attack becomes a hit on every ally within radius r it can select as an area (深溟巢涌者 "令攻击范围内的所有我方单位每秒 - * 受到…伤害": areaAllies — a 迷彩 ally is hit, an unblocking 隐匿 one not; the attack itself still needs a target). - */ -const hitAllInRadius = (r) => ({ before(c, b, e) { const l = areaAllies(b, e, e.x, e.y, r); if (l.length) c.targets = byPriority(e, l); } }); /** * A normal attack that splashes around its target (PRTS: "对主目标造成…普通伤害,对溅射目标造成…溅射伤害"): 碎骨's grenade, * “巨大的丑东西” / “墓碑”'s unblocked ranged attack, 烹泉 / 沏虹's attack, 集团军重型火炮's shell. Its mode is fixed when the @@ -1369,8 +1364,37 @@ function kitTidmag(ab) { }]; } +/** 深溟巢涌者 / 富营养的巢涌者: one pulse every second (PRTS "每秒"; not its attack interval, so 攻速 changes leave it alone). */ +const NEST_PULSE_INTERVAL = 1; +const NEST_PULSE_TAG = 'nestPulse'; +/** + * 深溟巢涌者 / 富营养的巢涌者 (PRTS 天赋): "不进行普通攻击", 抵抗 (一减状态抵抗率 −0.5) and 停顿免疫; "未处于消失状态时,令攻击范围 + * 内的所有我方单位每秒受到攻击力100%的无途径法术伤害" and "每次输出伤害时,再造成攻击力5%的神经损伤" (`EpDamage.ep_damage_ratio`). + * A talent aura, not an attack: `noAttack`, so it walks on while it hurts (ai.js attackStand holds only attackers — until + * 0.1.3 the pulse was its normal attack, which made it stand for its whole 1 s clip whenever an ally was in range: + * community report 「…错误的设置了攻击时不移动导致卡在原地」, GitHub #93), needs no target and pauses while it is hidden on a + * DISAPPEAR leg (消失). The pulse is an area selection (areaAllies: a 迷彩 ally and a flying one — the 炎佑 dragon — are + * hit, a 隐匿 one is not) of radius `rangeRadius` around its centre, like 鼎沸's. [ASSUMED] the pulse can be dodged (闪避: + * PRTS names no attack type, which the site reads as 普通伤害 — the 烹泉 splash rule) and a dodged one adds no 神经. + */ function kitDsubrl(ab, e) { - return [hitAllInRadius(e.base.rangeRadius || 1.6), ep('neural', T(ab, 'EpDamage.ep_damage_ratio') ?? 0), resist(['sluggish'])]; + const ratio = T(ab, 'EpDamage.ep_damage_ratio') ?? 0; + return [resist(['sluggish']), { + spawn(b, e2) { + e2.profile.noAttack = true; + // 每次输出伤害时: every pulse that reaches the unit (not dodged, not cancelled — the old attack's `dealt` rule) + if (ratio > 0) b.on('damaged', (c) => { if (c.source === e2 && c.dmg && c.dmg.tags.includes(NEST_PULSE_TAG)) elem(b, e2, c.target, 'neural', e2.s.atk * ratio); }, { owner: e2 }); + }, + iv: NEST_PULSE_INTERVAL, + tick(b, e2) { + if (e2.hidden) return; + const r = e2.base.rangeRadius || 1.6; + const l = areaAllies(b, e2, e2.x, e2.y, r); + if (!l.length) return; + b.fx('pulse', { x: e2.x, y: e2.y, r, id: e2.id, element: 'neural' }); + for (const u of l) hurt(b, e2, u, e2.s.atk, 'arts', { canDodge: true, tags: [NEST_PULSE_TAG] }); + }, + }]; } /** 掠海漂移体 爬行模式: the stun it takes on dropping (PRTS "进入爬行模式并晕眩0.5秒"; not in its blackboard). */ @@ -1561,7 +1585,10 @@ function kitJazz(ab, e) { const r = (e.def.raw && e.def.raw.stats && e.def.raw.stats.rawRangeRadius) || 2.5; const revealed = (u) => !enemyStealthed(u); // blocked, 反隐 (its 隐匿 returns 0 s after a block) return [stealth(), { - spawn(b, e2) { e2.profile.noAttack = true; }, + // PRTS 天赋 "隐匿期间不进行普通攻击;未受隐匿影响时进入反击模式:仅进行阻挡攻击,造成100%物理伤害": its normal attack only ever + // hits its blocker (`melee`), so a revealed (反隐) unblocked one neither shoots nor stands for its attack clip (until + // 0.1.3 it shot everyone within its 2.5 range and stood still while it had a target — ai.js attackStand) + spawn(b, e2) { e2.profile.noAttack = true; e2.profile.melee = true; }, tick(b, e2) { e2.profile.noAttack = !revealed(e2); }, // 平时不攻击,失去隐匿时反击 }, s ? { // 狂欢式演奏 (PRTS 节日爵士乐手, 反击模式): "仅攻击范围内存在我方单位时可触发:锁定目标持续施法,最多持续10.6s,每0.5s对目标 @@ -2673,7 +2700,7 @@ export const KITS = Object.freeze({ enemy_1389_winbab_2: kitStealth, // 访问团强攻冠军 · stealth (供暖器 priority n/a) enemy_1404_msnip: kitCrossbow, // 重弩突袭者 · stealth + 直击 (row/column bolt: arts + stun, reveals itself) enemy_10031_cnvsld: kitStealth, // 业余竞演者 · stealth - enemy_10034_cnvsax: kitJazz, // 节日爵士乐手 · stealth, attacks only when revealed; channel on one target (arts + burn) + enemy_10034_cnvsax: kitJazz, // 节日爵士乐手 · stealth; revealed: attacks its blocker only; channel on one target (arts + burn) enemy_10042_prtrop: kitStealth, // 架桥船工 · stealth (bridges n/a) enemy_10042_prtrop_2: kitStealth, // 扶桥老手 · stealth enemy_9008_acbunn: kitBoneSpike, // 假想敌:骨刺 · stealth; hits 3 targets while stealthed @@ -2735,7 +2762,7 @@ export const KITS = Object.freeze({ enemy_10067_ftsjc: kitFlameVine, // 灼藤 · ATK ramps until the 1st attack (3×3 arts + burn), burn on hit, ignites 卷心籽 // --- DOT 持续 - enemy_1234_dsubrl: kitDsubrl, // 深溟巢涌者 · pulse hits every ally in range + neural; 抵抗, immune 停顿 + enemy_1234_dsubrl: kitDsubrl, // 深溟巢涌者 · no attack: an arts pulse on every ally in range each second + neural; 抵抗, immune 停顿 enemy_1234_dsubrl_2: kitDsubrl, // 富营养的巢涌者 · same enemy_1267_nhpbr: kitPolluted, // 萨卡兹枯朽战士 · death: 污染秽蚀 zone (50 / 25 true per second) enemy_1267_nhpbr_2: kitPolluted, // 萨卡兹枯朽战士组长 · same @@ -2989,10 +3016,16 @@ export const KITS = Object.freeze({ sil: true, cd: ab.sk.SwitchModeTrigger ? ab.sk.SwitchModeTrigger.cd : 60, icd: ab.sk.SwitchModeTrigger ? ab.sk.SwitchModeTrigger.icd : 10, fire(b, e, a) { a.until = b.time + (T(ab, 'EndRotate.rotate_duration') ?? 0); b.fx('telegraph', { x: e.x, y: e.y, r: 1, kind: 'spin', id: e.id }); }, iv: T(ab, 'RotateDamage.interval') ?? 1, - // "每秒对半径1.0范围内的所有我方单位造成…(无视迷彩,不可对空)": an area selection - tick(b, e, a) { if (!(a.until > b.time)) return; for (const u of areaAllies(b, e, e.x, e.y, T(ab, 'RotateDamage.attack@range_radius') ?? 1)) hurt(b, e, u, e.s.atk * (T(ab, 'RotateDamage.attack@atk_scale') ?? 1), 'phys'); }, + // "每秒对半径1.0范围内的所有我方单位造成…(无视迷彩,不可对空),自身受阻止攻击类异常效果影响期间无法造成此伤害": an area + // selection, skipped while 晕眩 / 冻结 / 浮空 (flag stun), 沉睡 or 缴械 hold it (PRTS 异常效果 §阻止攻击; until 0.1.3 it spun on) + tick(b, e, a) { + if (!(a.until > b.time) || e.s.flags.stun || e.s.flags.sleep || e.s.flags.disarm) return; + for (const u of areaAllies(b, e, e.x, e.y, T(ab, 'RotateDamage.attack@range_radius') ?? 1)) hurt(b, e, u, e.s.atk * (T(ab, 'RotateDamage.attack@atk_scale') ?? 1), 'phys'); + }, }; - return [spin, unbalanced((b, e) => { if (spin.until > b.time) { spin.until = 0; b.fx('phase', { x: e.x, y: e.y, id: e.id, kind: 'spinStop' }); } })]; + // 漩涡形态 "不进行普通攻击" (PRTS 天赋): no blocked attack while it spins (until 0.1.3 its blocker took both) + const noAtk = { tick(b, e) { e.profile.noAttack = spin.until > b.time; } }; + return [spin, noAtk, unbalanced((b, e) => { if (spin.until > b.time) { spin.until = 0; b.fx('phase', { x: e.x, y: e.y, id: e.id, kind: 'spinStop' }); } })]; }, enemy_10118_ymgprc: (ab) => { // 澪 · double hits; 寒晖: every 4th attack (spCost 3) is a ×1.5 double hit const scale = (ab.sk.PowerAttack && ab.sk.PowerAttack.bb.atk_scale) || 1; @@ -3067,9 +3100,12 @@ export const KITS = Object.freeze({ b.fx('explode', { x: e.x, y: e.y, r: 0.4, kind: 'wallHit', id: e.id }); hurt(b, null, e, T(ab, 'hitWall.value') ?? 0, 'true', { tags: ['wallHit'] }); })], - enemy_10141_xdpeng_2: (ab) => [unbalanced((b, e, a) => { // 拥霜羽兽 · unbalanced once ⇒ drops its egg: faster, unblockable + // 拥霜羽兽 · PRTS 天赋 "不会攻击飞行单位"; unbalanced once ⇒ 失去蛋的模式 "不进行普通攻击,不可阻挡,移动速度最终提升至200%" (until + // 0.1.3 it kept its ranged attack without the egg and stood for each attack clip — ai.js attackStand —, and shot the 炎佑 dragon) + enemy_10141_xdpeng_2: (ab) => [noAirTargets(), unbalanced((b, e, a) => { if (a.done) return; a.done = true; + e.profile.noAttack = true; b.addBuff(e, { key: 'ab:noEgg', persist: true, visible: true, flags: { unblockable: true }, mods: { moveMul: 1 + (T(ab, 'speed.move_speed') ?? 0) } }); })], }); diff --git a/test/content/enemies_bosses.test.js b/test/content/enemies_bosses.test.js index 5f8494de..80d5652d 100644 --- a/test/content/enemies_bosses.test.js +++ b/test/content/enemies_bosses.test.js @@ -159,6 +159,18 @@ test(`${nm('enemy_10034_cnvsax')}: never attacks while stealthed; once blocked i assert.ok(w.elem.burn > 0, '狂欢式演奏 burns its target (the blocker)'); }); +test(`${nm('enemy_10034_cnvsax')}: revealed (反隐) and not blocked it makes no normal attack and walks on — 反击模式 "仅进行阻挡攻击"`, () => { + const h = arena({ units: [{ chessId: 't_wall', row: 10, col: 6 }] }); + h.step(); + h.b.addBuff(h.unit('t_wall'), { key: 'test:noBlock', persist: true, flags: { noBlock: true } }); + const e = put(h, 'enemy_10034_cnvsax', [9, 8], { move: true, route: { motion: 'WALK', start: [9, 8], end: [9, 1], checkpoints: [] } }); + h.b.addBuff(e, { key: 'test:reveal', persist: true, flags: { reveal: true } }); + const x0 = e.x; + h.run(4); + assert.equal(e.stats.attacks, 0, 'no ranged attack (until 0.1.3 it shot the wall and stood still)'); + approx(x0 - e.x, 4 * e.s.moveSpeed * 0.5, 0.02, 'walked the whole 4 s'); +}); + test(`${nm('enemy_9008_acbunn')}: attacks several targets at once while stealthed`, () => { const h = arena({ units: [{ chessId: 't_gun', row: 10, col: 6 }, { chessId: 't_mage', row: 11, col: 6 }, { chessId: 't_blade', row: 10, col: 7 }], captureNoisy: true, hooks: ['attack'] }); h.step(); @@ -584,8 +596,55 @@ for (const key of ['enemy_1234_dsubrl', 'enemy_1234_dsubrl_2']) { approx(e.findBuff('stun').timeLeft, 2); assert.equal(h.b.applyStatus(e, 'sluggish', { duration: 4 }), false); }); + + // community report 「有个持续造成范围伤害的海嗣敌人错误的设置了攻击时不移动导致卡在原地」: PRTS 天赋 "不进行普通攻击", "未处于消失状态 + // 时,令攻击范围内的所有我方单位每秒受到攻击力100%的无途径法术伤害", "每次输出伤害时,再造成攻击力5%的神经损伤" + test(`${nm(key)}: no normal attack — walks on past an unblocking operator while it pulses 100 % ATK arts + 5 % neural each second; no pulse while hidden`, () => { + const h = arena({ units: [{ chessId: 't_gun', row: 10, col: 6 }], hooks: ['damaged'], captureNoisy: true }); + h.step(); + const g = h.unit('t_gun'); + h.b.addBuff(g, { key: 'test:noBlock', persist: true, flags: { noBlock: true } }); + g.profile.noAttack = true; + const e = put(h, key, [9, 8], { move: true, route: { motion: 'WALK', start: [9, 8], end: [9, 1], checkpoints: [] } }); + const x0 = e.x; + h.run(4); + assert.ok(e.profile.noAttack && e.stats.attacks === 0, 'never attacks'); + approx(x0 - e.x, 4 * e.s.moveSpeed * 0.5, 0.02, 'walked the whole 4 s (MOVE_SCALE 0.5) with the operator in range'); + const mine = h.hooksOf('damaged').filter((c) => c.source === e && c.target === g); + const arts = mine.filter((c) => c.dmg.type === 'arts'); + assert.ok(arts.length >= 3 && arts.every((c) => !c.dmg.isAttack && c.dmg.tags.includes('nestPulse')), `pulses, not attacks (${arts.length})`); + approx(arts[0].dmg.amount, e.s.atk, 1e-9, '100 % ATK'); + const neural = mine.filter((c) => c.dmg.type === 'element'); + assert.equal(neural.length, arts.length, 'one 神经 per pulse'); + approx(neural[0].dmg.amount, e.s.atk * tb(key, 'EpDamage.ep_damage_ratio'), 1e-9, '5 % ATK 神经'); + const times = arts.map((c) => c.t ?? null).filter((t) => t != null); + if (times.length > 1) approx(times[1] - times[0], 1, 1e-6, 'one per second'); + // 消失 (a DISAPPEAR leg): no pulse + const n0 = arts.length; + h.b._setHidden(e, true); + e.x = g.x + 1; e.y = g.y; + h.run(2.05); + assert.equal(h.hooksOf('damaged').filter((c) => c.source === e && c.dmg.type === 'arts').length, n0, 'hidden: no pulse'); + }); } +test('GitHub #93: 深溟巢涌者 keeps walking with the 炎佑 dragon in range; only its pulse (no attack) reaches the dragon', () => { + const h = makeBattle({ seed: 7, autoFinish: false, timeLimit: 120, defs: { chess: { t_wall: WALL('t_wall') } }, kits: { t_wall: NOATK }, + units: [{ chessId: 't_wall', row: 12, col: 1 }], hooks: ['damaged'], captureNoisy: true }); + h.step(); + const [y] = spawnYanyou(h.b, 'p1', { atk: 0, hp: 50000 }); + assert.ok(y && y.isFlying, 'a flying ally'); + const e = h.spawn('enemy_1234_dsubrl', { pos: [Math.round(y.y), Math.round(y.x) + 2], routeIndex: 0, mods: { speedMul: 1 }, + route: { motion: 'WALK', start: [Math.round(y.y), Math.round(y.x) + 2], end: [Math.round(y.y), 0], checkpoints: [] } }); + const x0 = e.x; + h.run(5); + assert.equal(e.stats.attacks, 0, 'no normal attack'); + assert.ok(x0 - e.x >= 5 * e.s.moveSpeed * 0.5 - 0.05, `walked on (${(x0 - e.x).toFixed(2)} tiles in 5 s)`); + const onDragon = h.hooksOf('damaged').filter((c) => c.source === e && c.target === y); + assert.ok(onDragon.length > 0, 'the dragon in range is pulsed'); + assert.ok(onDragon.every((c) => !c.dmg.isAttack && (c.dmg.type === 'element' || c.dmg.tags.includes('nestPulse'))), 'pulse and its 神经 only'); +}); + for (const key of ['enemy_1267_nhpbr', 'enemy_1267_nhpbr_2']) { test(`${nm(key)}: death releases 污染秽蚀 — ground allies within ${tb(key, 'PollutedDie.projectile_range')} lose ${tb(key, 'PollutedDie.polluted_damage_low')} HP/s`, () => { const h = arena({ units: [{ chessId: 't_wall', row: 10, col: 6 }] }); @@ -1276,6 +1335,24 @@ test(`${nm('enemy_10116_ymgtop')}: spinning phase deals ATK×${tb('enemy_10116_y assert.equal(w.stats.taken, t0, 'no more spin damage'); }); +test(`${nm('enemy_10116_ymgtop')}: 漩涡形态 — no normal attack on its blocker while it spins; no spin damage while stunned (阻止攻击)`, () => { + const h = arena({ units: [{ chessId: 't_wall', row: 10, col: 7 }] }); + h.step(); + const e = put(h, 'enemy_10116_ymgtop', [10, 7]); // blocked by the wall + const icd = skb('enemy_10116_ymgtop', 'SwitchModeTrigger').initCooldown; + h.run(icd - 1); + assert.ok(e.blockedBy && e.stats.attacks > 0, 'its blocked attack before the spin (初始形态 仅进行阻挡攻击)'); + h.run(1.5); + const a0 = e.stats.attacks; + h.run(4); + assert.ok(e.profile.noAttack && e.stats.attacks === a0, `no normal attack while spinning (${e.stats.attacks - a0})`); + const w = h.unit('t_wall'); + h.b.applyStatus(e, 'stun', { duration: 3, force: true }); + const t0 = w.stats.taken; + h.run(2.5); + assert.equal(w.stats.taken, t0, 'stunned: no spin damage'); +}); + test(`${nm('enemy_10118_ymgprc')}: double hits; after ${skb('enemy_10118_ymgprc', 'PowerAttack').spCost} attacks the next double hit is ×${skb('enemy_10118_ymgprc', 'PowerAttack').bb.atk_scale}`, () => { const h = arena({ units: [{ chessId: 't_wall', row: 9, col: 5 }], captureNoisy: true, hooks: ['damaged'] }); h.step(); @@ -1795,6 +1872,8 @@ test('失衡: 弧光锋卫 bleeds per tile pushed; 冒失的小弟 is stunned; approx(statuses(h, g.id, 'stun')[0].duration, tb('enemy_10112_ymgds', 'StunAfterUnbalance.stun')); assert.ok(p.s.flags.unblockable); assert.equal(p.findBuff('ab:noEgg').mods.moveMul, 1 + tb('enemy_10141_xdpeng_2', 'speed.move_speed')); + assert.ok(p.profile.noAttack, '拥霜羽兽 失去蛋的模式: 不进行普通攻击 (no attack, so no stand for its clip)'); + assert.equal(typeof p.profile.canTarget === 'function' && p.profile.canTarget({ isFlying: true }), false, '拥霜羽兽: 不会攻击飞行单位'); // 雪孩子: pushed into high ground (row 12 col 2 is 'h' on the flat stage) ⇒ hitWall.value const sn = put(h, 'enemy_10138_xdsnow', [10, 3]); const sn2 = put(h, 'enemy_10138_xdsnow', [11, 6]); @@ -2372,6 +2451,7 @@ test('假想敌:铳 (隐秘核心): damage ×0.2 while springs live; 盲信之 const s2 = W.act1autochess_h08_02.overrides.enemy_9017_achunt_2.skills.find((s) => s.prefabKey === '2'); h.run(s2.initCooldown - h.b.time + 0.2); assert.ok(sp.s.flags.invulnerable && sp.mem.ab.dash); + assert.ok(sp.s.flags.disarm, '追逐模式: 不进行普通攻击'); }); test('“碎铳之簧” 法术护盾 (9018): barrier absorbs arts, physical ×0.1 with a counter; damage passes to 铳', () => { diff --git a/test/sim/feedback2-stealth-splash.test.js b/test/sim/feedback2-stealth-splash.test.js index 79f7908f..e2201210 100644 --- a/test/sim/feedback2-stealth-splash.test.js +++ b/test/sim/feedback2-stealth-splash.test.js @@ -328,7 +328,8 @@ test('#32.6 review: the 斩胄之剑 / 破胄之锤 hover attack skips 隐匿 an 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 + // 深溟巢涌者: 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' }); @@ -337,9 +338,10 @@ test('#32.6 review: the 斩胄之剑 / 破胄之锤 hover attack skips 隐匿 an 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)'); } + 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