diff --git a/docs/SIM.md b/docs/SIM.md index d1521bf7..60f03dbd 100644 --- a/docs/SIM.md +++ b/docs/SIM.md @@ -285,7 +285,8 @@ took off (出血, 沙狱, burning DoTs, 淤困, 【自然涌动】 — a tick se sourceless 毒雾 of 假想敌:蚀裂 still reach it [ASSUMED]; a ground enemy's area skill whose cast depends on allies nearby counts only the targets of its trigger selection — 卢西恩's 【aoe】 ("需要目标"), 锏's CircleAttack (`targetsNear`, PRTS 选择器 "所有触发选择器通常不无视迷彩"); a stealthed ally (隐匿, 排气格栅) only for the enemy it blocks — our operators keep 隐匿 while blocking (PRTS 作战机制 -§隐匿; 索敌的概念: a blocked enemy "强行无视对方可选性" attacks its blocker); a camouflaged one (迷彩, flag `camou`: ba.camou +§隐匿; 索敌的概念: a blocked enemy "强行无视对方可选性" attacks its blocker) — except an enemy whose 索敌不受阻挡影响 +(profile `blockFree`: 自制投石机 — it selects as if unblocked, by 仇恨值, so a 隐匿 blocker is no target; DESIGN §25.18); a camouflaged one (迷彩, flag `camou`: ba.camou "不阻挡时不成为敌方普通攻击的目标") likewise (PRTS 异常效果: neither anomaly is "阻挡时解除") — for every target selection (attacks, skill picks, cast conditions, a normal attack on every operator in range — 斩胄之剑 / 破胄之锤's hover attack, “灵幛”). **Enemy area effects** — splash, death and self blasts, area skills and statuses, pulses, the zones an enemy diff --git a/docs/history/0.2.0.md b/docs/history/0.2.0.md index 12b6b4ea..993cde7e 100644 --- a/docs/history/0.2.0.md +++ b/docs/history/0.2.0.md @@ -27,7 +27,7 @@ a merged elite 拉普兰德 as a new one (GitHub #169), the leader pool per play model quirks (PR #211) → §25.13; the community reports of 2026-10-06 (the 孤岛风云 prisoners drawn freed, 忍冬's trigger, a bounty burned to death by the 活性源石, 枯朽之种 paying a bounty) → §25.14; the second batch of community reports of 2026-10-06, stream CB4 (enemies, bosses, waves, movement: the 突袭 landing on the 临时整备区 row and the other half of the -联防 map, a silenced 萨卡兹枯朽战士's death pollution, 萨卡兹穿刺手's speed after a stun and its first hit; the briefing types' +联防 map, a silenced 萨卡兹枯朽战士's death pollution, 萨卡兹穿刺手's speed after a stun and its first hit, 自制投石机 blocked on the 排气格栅; the briefing types' 7–9 waves, 新硎's 断刃 and a freed prisoner in 联防 already answered, …) → §25.18. Decisions of the owner: 2026-10-04 — the server stays on plain `node:http` + `ws`, no web framework (§25.1); @@ -1066,7 +1066,7 @@ fixed (W5AA's commits name every golden scenario moved); two were already fixed is the report; on this tree only the 使徒's own leak carries it. - Golden: unchanged. Pinned: `test/sim/feedback5-community-pins.test.js`. -### 25.18 Community reports of 2026-10-06, stream CB4: enemies, bosses, waves, movement — `sim/battle/players.js onOwnBoard`, `content/bonds/addon/battle.js raidTile`, `content/enemies/archetypes.js kitPolluted`, `content/enemies/special.js kitRush` +### 25.18 Community reports of 2026-10-06, stream CB4: enemies, bosses, waves, movement — `sim/battle/players.js onOwnBoard`, `content/bonds/addon/battle.js raidTile`, `content/enemies/archetypes.js kitPolluted`, `content/enemies/special.js kitRush`, `targeting.js canTargetAlly` The owner's second collection of community reports (2026-10-06/07) was split over five streams; CB4 took the items about enemies, bosses, waves and movement (items 6, 10, 16.3, 24, 29, 35, 36, 38, 39, 40, 47, 52, 53, 57 of the list). Each was @@ -1174,3 +1174,21 @@ The real ones are fixed, one commit each with its golden scenarios named; the re coop2-HARD-4 (160 spawned). Tests: `test/content/enemies_bosses.test.js` (3: the layers and the cap, the resets, the first hit; all fail on the parent). +#### 25.18.7 「玩家的官方原版反馈:「投石车」敌人如果被蒸汽地板上的干员阻挡会不进行攻击」 (item 24): 自制投石机's 索敌不受阻挡影响 — `targeting.js canTargetAlly / sortAllyTargets`, `ai.js attackTargets`, `content/enemies/special.js` (enemy_10162_mnctpt) + +- **The enemy and the terrain**: 「投石车」 = 自制投石机 (enemy_10162_mnctpt, ranged 4.0, the 悬赏·特异III bounty); the 「蒸汽 + 地板」 = 战场#07 (下半)'s 排气格栅 (tile_smog: an operator on it is 隐匿 to enemy ranged attacks). +- **Seen here**: blocked by an operator on the smog, it attacked that operator — every enemy does (the engine's rule: a + blocked enemy attacks its blocker whatever its selectability, PRTS 索敌的概念 「…强行无视对方可选性发动攻击」). +- **Official**: PRTS 自制投石机 天赋 「…索敌不受阻挡影响,且不会因丢失目标而结束攻击」 — the blocker exception does not apply to + it, so a 隐匿 blocker is no target and, with nobody else in its range, it does not attack: the players' report. +- **Now**: the catapult's profile is `blockFree` (spawns.js gives every enemy the field): `attackTargets` takes the + allies in reach as if it were unblocked, `canTargetAlly` refuses a 隐匿 / 迷彩 blocker to it, `sortAllyTargets` puts no + blocker first (仇恨值: taunt, then the latest deployed). Every other enemy keeps attacking a 隐匿 blocker. +- [ASSUMED]: none for the rule. Not changed, for later: its stones' splash radius (PRTS 「目标半径1.5范围」, the data's + `projectile_range` 0.5 × 3; the kit uses 0.5), the 0.3 s between its three stones, and 「不会因丢失目标而结束攻击」. +- Golden: 1 battle — standin-06 (the bounty catapult, blocked by 忍冬, now shot the 斯卡蒂 / 瑕光 pieces by 仇恨值, its blocker no longer + first). Tests: `test/sim/feedback5-catapult-smog.test.js` (3: no attack on a hidden blocker alone, another operator + instead, every other ranged enemy still attacks its hidden blocker; the first two fail on the parent). Docs: + docs/SIM.md. + diff --git a/server/sim/ai.js b/server/sim/ai.js index e43fd173..f1798b82 100644 --- a/server/sim/ai.js +++ b/server/sim/ai.js @@ -718,10 +718,13 @@ export function attackWindup(e) { return a && a.dur > 0 ? clipHit(a) / clipSpeed(e, a) : 0; } -/** The allies `e` could hit now: its blocker (and, with a range, the others in reach), passing its own target rule. */ +/** + * The allies `e` could hit now: its blocker (and, with a range, the others in reach), passing its own target rule. An + * enemy whose 索敌不受阻挡影响 (profile `blockFree`: 自制投石机) selects as if unblocked: the allies in reach it may target. + */ function attackTargets(b, e, radius, reach, own) { let targets = []; - if (e.blockedBy) { + if (e.blockedBy && !(radius > 0 && e.profile && e.profile.blockFree)) { const bl = e.blockedBy; if (radius > 0) { targets = b.alliesInRadius(e.x, e.y, reach, null).filter((a) => a === bl || canTargetAlly(e, a, true)); diff --git a/server/sim/battle/spawns.js b/server/sim/battle/spawns.js index 35857017..17c1c5fd 100644 --- a/server/sim/battle/spawns.js +++ b/server/sim/battle/spawns.js @@ -134,8 +134,9 @@ export class BattleSpawns { e.atkStandUntil = -Infinity; // standing for its attack clip (ai.js attackStand) e.swing = false; // a normal attack swung, its damage frame not reached yet (ai.js enemyAttack) // every enemy profile starts with the same fields (stable object shapes keep the hot loop's property reads fast); - // `dmgType` null = the data's (content may arm a data-unarmed enemy: ai.js enemyAttack) - e.profile = { noAttack: def.dmgType === 'none', maxTargets: 1, atkScale: 1, dmgType: null }; + // `dmgType` null = the data's (content may arm a data-unarmed enemy: ai.js enemyAttack); `blockFree` = its 索敌不受阻挡 + // 影响 (content: 自制投石机 — targeting.js canTargetAlly / sortAllyTargets, ai.js attackTargets) + e.profile = { noAttack: def.dmgType === 'none', maxTargets: 1, atkScale: 1, dmgType: null, blockFree: false }; e.route = { legs: route ? compileRoute(route, this.rect) : [], legIdx: 0, pts: null, ptIdx: 0, suffix: null, version: -1, waitLeft: null }; if (!e.route.legs.length) { const end = this.grid.specialTiles('end')[0]; diff --git a/server/sim/content/devices.js b/server/sim/content/devices.js index 62f5d68b..ebd0c344 100644 --- a/server/sim/content/devices.js +++ b/server/sim/content/devices.js @@ -13,7 +13,8 @@ // there: an enemy gains 1 layer — 2 at 重量 ≥ heavyWeight (the device's `value`, 3) — of ASPD aspdPerStack // (fractions are ×100 ASPD) and move speed × (1 + moveMulPerStack × layers), an operator 1 layer of the // ASPD part only; at most maxStacks layers; cleared on leaving -// 烟雾 g operators on it cannot be targeted by enemy ranged attacks (stealth flag: blocked enemies still hit them) +// 烟雾 g operators on it cannot be targeted by enemy ranged attacks (stealth flag: blocked enemies still hit them — +// not 自制投石机, whose 索敌不受阻挡影响: blocked by one with nobody else in range it does not attack) // 深水 d ground enemies on it: sea_drown[enemy].damage dmg/s (无来源 true 持续伤害, not 环境伤害: tags dot / // periodic / deepsea), ASPD attack_speed (×100), move × move_speed // 活性源石 i a unit on it (allies and ground enemies) gets a timed effect: damage true dmg/s, ATK + atk, ASPD + diff --git a/server/sim/content/enemies/special.js b/server/sim/content/enemies/special.js index 74bd0631..e14efd6c 100644 --- a/server/sim/content/enemies/special.js +++ b/server/sim/content/enemies/special.js @@ -404,7 +404,11 @@ export const SPECIAL_KITS = Object.freeze({ b.fx('explode', { x: e.x, y: e.y, r, kind: 'heal' }); for (const o of b.enemiesInRadius(e.x, e.y, r)) if (o !== e) b.heal(e, o, amt); } }], - enemy_10162_mnctpt: (ab) => [{ // 自制投石机 · 3-hit attacks with small splash + enemy_10162_mnctpt: (ab) => [{ // 自制投石机 · 3-hit attacks with small splash; 索敌不受阻挡影响 + // PRTS 天赋 「索敌不受阻挡影响,且不会因丢失目标而结束攻击」: its target selection ignores its block — a 隐匿 blocker (an + // operator on the 排气格栅) is no target, so with nobody else in range it does not attack (the official game, community + // report of 2026-10-06, item 24); it picks by 仇恨值, not its blocker first + spawn(b, e) { e.profile.blockFree = true; }, dealt(c, b, e) { const n = T(ab, 'Attack.attack@times') ?? 1, r = T(ab, 'Attack.attack@projectile_range') ?? 0; const t = c.target; diff --git a/server/sim/targeting.js b/server/sim/targeting.js index e5110dd4..05ba7190 100644 --- a/server/sim/targeting.js +++ b/server/sim/targeting.js @@ -99,13 +99,15 @@ export function canTargetEnemy(attacker, e, profile) { * PRTS 异常效果 gives both anomalies the note "与'阻挡时解除'没有直接关系" — for this target selection (an attack, a skill * pick, a cast condition, a normal attack on every ally in range: PRTS 选择器 "所有触发选择器通常不无视迷彩"); an area * effect selects with areaSelectable / auraSelectable, which do not check 迷彩. An airborne ally (起飞, flag `liftoff`) is - * never a target of a ground enemy (evadesGround). + * never a target of a ground enemy (evadesGround). An enemy whose 索敌不受阻挡影响 (profile `blockFree`: 自制投石机, PRTS 天赋) + * has no blocker exception: a 隐匿 / 迷彩 ally that blocks it is no target either — so, blocked by an operator on the + * 排气格栅 with nobody else in range, it does not attack (the official game, community report of 2026-10-06, item 24). */ export function canTargetAlly(e, a, ranged) { if (!a.alive || !a.deployed || a.hidden || a.kind === 'device') return false; const f = a.s.flags; if (f.untargetable || f.sleep) return false; - if (ranged && (f.stealth || f.camou) && e.blockedBy !== a) return false; + if (ranged && (f.stealth || f.camou) && (e.blockedBy !== a || (e.profile && e.profile.blockFree))) return false; if (f.liftoff && evadesGround(e, a)) return false; return true; } @@ -221,8 +223,9 @@ export function sortEnemyTargets(battle, attacker, cands, priority) { */ export function sortAllyTargets(enemy, cands) { if (cands.length <= 1) return cands; + const bl = enemy.profile && enemy.profile.blockFree ? null : enemy.blockedBy; // 索敌不受阻挡影响: no blocker first cands.sort((a, b) => { - const ba = enemy.blockedBy === a ? 0 : 1, bb = enemy.blockedBy === b ? 0 : 1; + const ba = bl === a ? 0 : 1, bb = bl === b ? 0 : 1; return ba - bb || aggroCmp(a, b); }); return cands; diff --git a/test/golden/standins.json b/test/golden/standins.json index 19488857..8e76702c 100644 --- a/test/golden/standins.json +++ b/test/golden/standins.json @@ -306,58 +306,46 @@ "time": 183.7, "ticks": 5511, "reason": "cleared", - "killed": 50, + "killed": 25, "total": 98, "unspawned": 0, "bossHpLeft": null, - "rngDraws": 345, + "rngDraws": 272, "errors": {"count":0,"kinds":[]}, "players": { - "p1": { - "killed": 50, - "total": 98, - "perfect": false, - "leaks": 48, - "leaksUncounted": 0, - "coins": 8, - "dmg": 676478, - "bossDmg": 0, - "heal": 30426, - "deaths": 31, - "layerGains": {"raidShip":26,"visiShip":12} - } + "p1": {"killed":25,"total":98,"perfect":false,"leaks":73,"leaksUncounted":0,"coins":4,"dmg":397046,"bossDmg":0,"heal":12225,"deaths":31,"layerGains":{"raidShip":24}} }, "units": [ - ["p1",1,"op","chess_char_3_12_b",2,"none",1904,1,140,10,155395,5504,915,21,3], - ["p1",2,"op","chess_char_4_09_b",0,"none",0,0,6,10,119787,3510,0,39,1], - ["p1",3,"op","chess_char_3_05_b",2,"none",0,0,90,2,23950,8566,0,18,2], - ["p1",4,"op","chess_char_3_18_b",2,"none",0,0,93,3,28745,9190,2808,27,2], - ["p1",5,"op","chess_char_4_04_b",1,"none",0,0,137,0,25476,10224,0,40,9], - ["p1",6,"op","chess_char_4_07_b",1,"none",0,0,101,3,28031,10981,0,27,1], - ["p1",7,"op","chess_char_4_16_b",1,"none",0,0,139,7,42175,16104,0,70,12], - ["p1",8,"op","chess_char_3_01_b",2,"none",0,0,122,1,16716,3020,0,24,1], - ["p1",9,"op","chess_char_3_04_b",2,"none",0,0,4,0,0,15932,26703,25,2], - ["p1",10,"op","chess_char_3_21_b",2,"none",0,0,118,12,175716,7911,0,55,1], - ["p1",11,"op","chess_char_4_02_b",1,"none",0,0,124,2,41362,2966,0,27,1], - ["p1",12,"op","chess_char_4_13_b",1,"none",0,0,103,0,19124,2704,0,26,2] + ["p1",1,"op","chess_char_3_12_b",2,"none",1904,1,138,4,33527,4810,222,6,1], + ["p1",2,"op","chess_char_4_09_b",0,"none",0,0,6,2,12272,3510,0,5,1], + ["p1",3,"op","chess_char_3_05_b",2,"none",0,0,90,1,29963,11344,0,19,2], + ["p1",4,"op","chess_char_3_18_b",2,"none",0,0,12,1,32236,5744,2553,22,1], + ["p1",5,"op","chess_char_4_04_b",1,"none",0,0,137,0,21284,11506,0,43,9], + ["p1",6,"op","chess_char_4_07_b",1,"none",0,0,93,0,13096,7538,0,19,1], + ["p1",7,"op","chess_char_4_16_b",1,"none",0,0,135,2,26045,13645,0,38,12], + ["p1",8,"op","chess_char_3_01_b",2,"none",0,0,128,1,13435,3020,0,21,1], + ["p1",9,"op","chess_char_3_04_b",2,"none",0,0,4,0,0,1374,9451,11,1], + ["p1",10,"op","chess_char_3_21_b",2,"none",0,0,112,11,185272,6695,0,50,1], + ["p1",11,"op","chess_char_4_02_b",1,"none",0,0,2,0,16202,1483,0,17,1], + ["p1",12,"op","chess_char_4_13_b",1,"none",0,0,101,3,13715,3085,0,23,1] ], "enemies": { - "enemy_10162_mnctpt": [1,1,0,7378,39813], - "enemy_1019_jshoot": [1,1,0,295,4479], - "enemy_1019_jshoot_2": [1,1,0,837,6718], - "enemy_1021_bslime": [1,1,0,1648,6121], - "enemy_1023_jmage": [1,1,0,2497,5474], + "enemy_10162_mnctpt": [1,0,1,13739,31746], + "enemy_1019_jshoot": [1,1,0,769,4479], + "enemy_1019_jshoot_2": [1,1,0,1779,6718], + "enemy_1021_bslime": [1,1,0,2095,6121], + "enemy_1023_jmage": [1,0,1,3326,1518], "enemy_1025_reveng": [1,1,0,0,12442], "enemy_1026_aghost": [1,1,0,0,9456], - "enemy_1043_zomsbr": [1,1,0,0,6285], - "enemy_1251_lysyta_2": [57,33,24,13447,289829], - "enemy_1252_lysytb_2": [21,8,13,29953,226804], - "enemy_1329_cbshld_2": [12,1,11,40556,69057] + "enemy_1043_zomsbr": [1,0,1,0,0], + "enemy_1251_lysyta_2": [57,15,42,6671,148451], + "enemy_1252_lysytb_2": [21,4,17,25366,127156], + "enemy_1329_cbshld_2": [12,1,11,20011,48959] }, - "hooks": [1,140,5511,596,596,875,906,94,81,139,37,37,0,7322,36,60,98,48,1,89,7,0,4], - "events": {"atk":684,"bounty":4,"deploy":42,"die":91,"dmg":906,"engage":11,"fx":62,"heal":67,"layer":7,"leak":48,"skill":74,"spawn":110,"status":117}, - "fx": {"blink":10,"camouflage":1,"dodge":6,"dp":13,"explode":1,"garrison":7,"slash":24}, - "snaps": "be6d1821 fa43a30d 52099f77 a860ccb6 ea311a6e 44affa90 9dabf4d8 83adbc77 77dfae35 f02fd736 470fba6a a9871a60 b7e054aa ae8c4b73 55df9303 1c374baa 0d780d78 e03a2b60 b7879d42" + "hooks": [1,138,5511,428,428,618,635,38,56,137,32,32,0,5767,24,40,98,73,1,59,3,0,6], + "events": {"atk":493,"bounty":2,"deploy":40,"die":64,"dmg":635,"engage":11,"fx":32,"heal":36,"layer":3,"leak":73,"skill":64,"spawn":110,"status":83}, + "fx": {"blink":8,"camouflage":1,"dodge":6,"dp":5,"explode":1,"garrison":3,"slash":8}, + "snaps": "be6d1821 69cb5865 7bea5cb3 02fefb75 5eacb174 5bc1814c a70bea6d 0de1251c 172a35f2 f9001627 db7b6b38 a6083013 e2198a24 0adb9d99 8eed5e8b b756dcfe 538dc28e 876d2f95 835a73d1" }, "standin-07": { "about": "elite band 6 as stand-ins: chess_char_4_17_b→char_610_acfend chess_char_4_18_b→char_602_cdfend chess_char_5_05_b→char_602_cdfend chess_char_4_19_b→char_608_acpion chess_char_5_01_b→char_600_cpione chess_char_4_22_b→char_609_acguad chess_char_4_23_b→char_609_acguad chess_char_5_06_b→char_609_acguad chess_char_4_20_b→char_603_csnipe chess_char_4_24_b→char_604_ccast chess_char_5_02_b→char_604_ccast chess_char_5_03_b→char_612_accast", diff --git a/test/sim/feedback5-catapult-smog.test.js b/test/sim/feedback5-catapult-smog.test.js new file mode 100644 index 00000000..37e00458 --- /dev/null +++ b/test/sim/feedback5-catapult-smog.test.js @@ -0,0 +1,56 @@ +// test/sim/feedback5-catapult-smog.test.js — 自制投石机's 「索敌不受阻挡影响」 (community report of 2026-10-06, item 24, a +// player's report of the official game: 「投石车」敌人如果被蒸汽地板上的干员阻挡会不进行攻击; DESIGN §25.18). +// PRTS 自制投石机 天赋: 「普通攻击为3连击…索敌不受阻挡影响,且不会因丢失目标而结束攻击」. An operator on the 排气格栅 smog +// (战场#07 下半, tile_smog) is 隐匿 for enemy ranged attacks; any other enemy still attacks the operator that blocks it +// ("阻挡优先级最高"), but the catapult's targeting ignores its block, so that blocker is no target: alone, it is not attacked. +// Run: node --test test/sim/feedback5-catapult-smog.test.js + +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { makeBattle, chessRec, enemyRec, flatStage, checkInvariants } from '../helpers/battleHarness.js'; + +const KEY = 'enemy_10162_mnctpt'; // 自制投石机 (RANGED, range 4.0) +const wall = chessRec({ id: 't_wall', profession: 'TANK', stats: { atk: 0, maxHp: 1e9, def: 0, blockCnt: 3 }, rangeGrid: [[0, 0]], skill: null }); +const other = chessRec({ id: 't_other', profession: 'SNIPER', stats: { atk: 0, maxHp: 1e9, def: 0, blockCnt: 0 }, rangeGrid: [[0, 0]], skill: null }); +const kits = { t_wall: () => ({ trait: { noAttack: true } }), t_other: () => ({ trait: { noAttack: true } }) }; +const route = { motion: 'WALK', start: [9, 10], end: [9, 2], checkpoints: [] }; +const shooter = enemyRec({ key: 'enemy_t_shooter', hp: 1e9, atk: 100, range: 3, speed: 1, bat: 2 }); // an ordinary ranged walker + +function run({ smog, withOther = false, key = KEY }) { + const stage = flatStage(); + if (smog) stage.rows[9] = stage.rows[9].slice(0, 6) + 'g' + stage.rows[9].slice(7); // the blocker's tile (9,6) + const units = [{ chessId: 't_wall', row: 9, col: 6 }]; + if (withOther) units.push({ chessId: 't_other', row: 11, col: 6 }); + const h = makeBattle({ stage, autoFinish: false, timeLimit: 60, defs: { chess: { t_wall: wall, t_other: other }, enemies: { enemy_t_shooter: shooter } }, kits, units, enemies: [{ key, route }] }); + h.step(); + const e = h.enemy(key); + assert.ok(h.runUntil(() => e.blockedBy, 30), 'blocked by the wall'); + h.run(20); + checkInvariants(h.b); + assert.equal(h.b.errors.length, 0); + return { e, h, wall: h.unit('t_wall'), other: withOther ? h.unit('t_other') : null }; +} + +test('自制投石机 blocked by an operator on the 排气格栅, nobody else in range: no attack', () => { + const { e, wall: w } = run({ smog: true }); + assert.ok(w.s.flags.stealth, 'the smog tile hides the blocker from ranged attacks'); + assert.equal(e.blockedBy, w); + assert.equal(e.stats.attacks, 0); + assert.equal(w.stats.taken, 0); +}); + +test('自制投石机 blocked on the 排气格栅 shoots another operator in its range instead; off the smog it hits its blocker', () => { + const a = run({ smog: true, withOther: true }); + assert.ok(a.e.stats.attacks >= 2); + assert.equal(a.wall.stats.taken, 0, 'the hidden blocker is never its target'); + assert.ok(a.other.stats.taken > 0); + const b = run({ smog: false }); + assert.ok(b.e.stats.attacks >= 2); + assert.ok(b.wall.stats.taken > 0, 'a visible blocker is a target as before'); +}); + +test('any other ranged enemy still attacks the operator that blocks it on the 排气格栅 (阻挡优先级最高)', () => { + const { e, wall: w } = run({ smog: true, key: 'enemy_t_shooter' }); + assert.ok(w.s.flags.stealth); + assert.ok(e.stats.attacks >= 2 && w.stats.taken > 0); +});