feedback5 CB4: 自制投石机's targeting ignores its block (索敌不受阻挡影响) — blocked by an operator on the 排气格栅 with nobody else in range it does not attack, as in the official game (community report of 2026-10-06, item 24)

The 「投石车」 is 自制投石机 (enemy_10162_mnctpt), the 「蒸汽地板」 the 排气格栅 smog (隐匿 to enemy ranged attacks). PRTS
天赋 「索敌不受阻挡影响」: the blocker exception (a blocked enemy attacks its blocker whatever its selectability) does not
apply to it. New enemy profile field `blockFree` (spawns.js): attackTargets selects as if unblocked, canTargetAlly
refuses a 隐匿 / 迷彩 blocker to it, sortAllyTargets puts no blocker first. Other enemies unchanged.

Golden: standin-06 (the bounty catapult, blocked by 忍冬, now shoots by 仇恨值 — checked instrumented).
Tests: test/sim/feedback5-catapult-smog.test.js (3; 2 fail on the parent). Docs: DESIGN §25.18.7, docs/SIM.md.
This commit is contained in:
sganggs committed 2026-10-07 02:07:51 +08:00
1 parent d40cb4e79e
commit fdb3ae1b46
9 files changed
+127 -52

No files matched your search

+2 -1
View File
@@ -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 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 选择器 "所有触发选择器通常不无视迷彩"); selection — 卢西恩's 【aoe】 ("需要目标"), 锏's CircleAttack (`targetsNear`, PRTS 选择器 "所有触发选择器通常不无视迷彩");
a stealthed ally (隐匿, 排气格栅) only for the enemy it blocks — our operators keep 隐匿 while blocking (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 "不阻挡时不成为敌方普通攻击的目标") 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, (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 “灵幛”). **Enemy area effects** — splash, death and self blasts, area skills and statuses, pulses, the zones an enemy
+20 -2
View File
@@ -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 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 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 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. 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); 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. is the report; on this tree only the 使徒's own leak carries it.
- Golden: unchanged. Pinned: `test/sim/feedback5-community-pins.test.js`. - 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 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 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 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). 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.
+5 -2
View File
@@ -718,10 +718,13 @@ export function attackWindup(e) {
return a && a.dur > 0 ? clipHit(a) / clipSpeed(e, a) : 0; 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) { function attackTargets(b, e, radius, reach, own) {
let targets = []; let targets = [];
if (e.blockedBy) { if (e.blockedBy && !(radius > 0 && e.profile && e.profile.blockFree)) {
const bl = e.blockedBy; const bl = e.blockedBy;
if (radius > 0) { if (radius > 0) {
targets = b.alliesInRadius(e.x, e.y, reach, null).filter((a) => a === bl || canTargetAlly(e, a, true)); targets = b.alliesInRadius(e.x, e.y, reach, null).filter((a) => a === bl || canTargetAlly(e, a, true));
+3 -2
View File
@@ -134,8 +134,9 @@ export class BattleSpawns {
e.atkStandUntil = -Infinity; // standing for its attack clip (ai.js attackStand) 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) 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); // 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) // `dmgType` null = the data's (content may arm a data-unarmed enemy: ai.js enemyAttack); `blockFree` = its 索敌不受阻挡
e.profile = { noAttack: def.dmgType === 'none', maxTargets: 1, atkScale: 1, dmgType: null }; // 影响 (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 }; 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) { if (!e.route.legs.length) {
const end = this.grid.specialTiles('end')[0]; const end = this.grid.specialTiles('end')[0];
+2 -1
View File
@@ -13,7 +13,8 @@
// there: an enemy gains 1 layer — 2 at 重量 ≥ heavyWeight (the device's `value`, 3) — of ASPD aspdPerStack // 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 // (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 // 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 / // 深水 d ground enemies on it: sea_drown[enemy].damage dmg/s (无来源 true 持续伤害, not 环境伤害: tags dot /
// periodic / deepsea), ASPD attack_speed (×100), move × move_speed // 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 + // 活性源石 i a unit on it (allies and ground enemies) gets a timed effect: damage true dmg/s, ATK + atk, ASPD +
+5 -1
View File
@@ -404,7 +404,11 @@ export const SPECIAL_KITS = Object.freeze({
b.fx('explode', { x: e.x, y: e.y, r, kind: 'heal' }); 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); 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) { dealt(c, b, e) {
const n = T(ab, 'Attack.attack@times') ?? 1, r = T(ab, 'Attack.attack@projectile_range') ?? 0; const n = T(ab, 'Attack.attack@times') ?? 1, r = T(ab, 'Attack.attack@projectile_range') ?? 0;
const t = c.target; const t = c.target;
+6 -3
View File
@@ -99,13 +99,15 @@ export function canTargetEnemy(attacker, e, profile) {
* PRTS 异常效果 gives both anomalies the note "与'阻挡时解除'没有直接关系" — for this target selection (an attack, a skill * 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 * 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 * 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) { export function canTargetAlly(e, a, ranged) {
if (!a.alive || !a.deployed || a.hidden || a.kind === 'device') return false; if (!a.alive || !a.deployed || a.hidden || a.kind === 'device') return false;
const f = a.s.flags; const f = a.s.flags;
if (f.untargetable || f.sleep) return false; 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; if (f.liftoff && evadesGround(e, a)) return false;
return true; return true;
} }
@@ -221,8 +223,9 @@ export function sortEnemyTargets(battle, attacker, cands, priority) {
*/ */
export function sortAllyTargets(enemy, cands) { export function sortAllyTargets(enemy, cands) {
if (cands.length <= 1) return cands; if (cands.length <= 1) return cands;
const bl = enemy.profile && enemy.profile.blockFree ? null : enemy.blockedBy; // 索敌不受阻挡影响: no blocker first
cands.sort((a, b) => { 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 ba - bb || aggroCmp(a, b);
}); });
return cands; return cands;
+28 -40
View File
@@ -306,58 +306,46 @@
"time": 183.7, "time": 183.7,
"ticks": 5511, "ticks": 5511,
"reason": "cleared", "reason": "cleared",
"killed": 50, "killed": 25,
"total": 98, "total": 98,
"unspawned": 0, "unspawned": 0,
"bossHpLeft": null, "bossHpLeft": null,
"rngDraws": 345, "rngDraws": 272,
"errors": {"count":0,"kinds":[]}, "errors": {"count":0,"kinds":[]},
"players": { "players": {
"p1": { "p1": {"killed":25,"total":98,"perfect":false,"leaks":73,"leaksUncounted":0,"coins":4,"dmg":397046,"bossDmg":0,"heal":12225,"deaths":31,"layerGains":{"raidShip":24}}
"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}
}
}, },
"units": [ "units": [
["p1",1,"op","chess_char_3_12_b",2,"none",1904,1,140,10,155395,5504,915,21,3], ["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,10,119787,3510,0,39,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,2,23950,8566,0,18,2], ["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,93,3,28745,9190,2808,27,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,25476,10224,0,40,9], ["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,101,3,28031,10981,0,27,1], ["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,139,7,42175,16104,0,70,12], ["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,122,1,16716,3020,0,24,1], ["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,15932,26703,25,2], ["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,118,12,175716,7911,0,55,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,124,2,41362,2966,0,27,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,103,0,19124,2704,0,26,2] ["p1",12,"op","chess_char_4_13_b",1,"none",0,0,101,3,13715,3085,0,23,1]
], ],
"enemies": { "enemies": {
"enemy_10162_mnctpt": [1,1,0,7378,39813], "enemy_10162_mnctpt": [1,0,1,13739,31746],
"enemy_1019_jshoot": [1,1,0,295,4479], "enemy_1019_jshoot": [1,1,0,769,4479],
"enemy_1019_jshoot_2": [1,1,0,837,6718], "enemy_1019_jshoot_2": [1,1,0,1779,6718],
"enemy_1021_bslime": [1,1,0,1648,6121], "enemy_1021_bslime": [1,1,0,2095,6121],
"enemy_1023_jmage": [1,1,0,2497,5474], "enemy_1023_jmage": [1,0,1,3326,1518],
"enemy_1025_reveng": [1,1,0,0,12442], "enemy_1025_reveng": [1,1,0,0,12442],
"enemy_1026_aghost": [1,1,0,0,9456], "enemy_1026_aghost": [1,1,0,0,9456],
"enemy_1043_zomsbr": [1,1,0,0,6285], "enemy_1043_zomsbr": [1,0,1,0,0],
"enemy_1251_lysyta_2": [57,33,24,13447,289829], "enemy_1251_lysyta_2": [57,15,42,6671,148451],
"enemy_1252_lysytb_2": [21,8,13,29953,226804], "enemy_1252_lysytb_2": [21,4,17,25366,127156],
"enemy_1329_cbshld_2": [12,1,11,40556,69057] "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], "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":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}, "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":10,"camouflage":1,"dodge":6,"dp":13,"explode":1,"garrison":7,"slash":24}, "fx": {"blink":8,"camouflage":1,"dodge":6,"dp":5,"explode":1,"garrison":3,"slash":8},
"snaps": "be6d1821 fa43a30d 52099f77 a860ccb6 ea311a6e 44affa90 9dabf4d8 83adbc77 77dfae35 f02fd736 470fba6a a9871a60 b7e054aa ae8c4b73 55df9303 1c374baa 0d780d78 e03a2b60 b7879d42" "snaps": "be6d1821 69cb5865 7bea5cb3 02fefb75 5eacb174 5bc1814c a70bea6d 0de1251c 172a35f2 f9001627 db7b6b38 a6083013 e2198a24 0adb9d99 8eed5e8b b756dcfe 538dc28e 876d2f95 835a73d1"
}, },
"standin-07": { "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", "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",
+56
View File
@@ -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);
});