diff --git a/docs/SIM.md b/docs/SIM.md index 3049d9f0..f7a69ebe 100644 --- a/docs/SIM.md +++ b/docs/SIM.md @@ -268,15 +268,15 @@ a stealthed ally (隐匿, 排气格栅) only for the enemy it blocks — our ope leaves, chain / bounce jumps, 周围四格 additions, whole-column / whole-field skills — select with `targeting.js areaSelectable` (`content/enemies.js areaAllies` / `areaAlliesInTiles` / `fieldAllies`; PRTS 作战机制 §AOE伤害判定 "AOE的判定是对攻击范围内的每个可以被选中的敌人进行判定"; PRTS 异常效果 §无法选择: 隐匿, 不可选中, 无敌 and 对地规避 make -"常见的、来自不同阵营的“选择”行为" skip a unit unless the ability "无视可选性"): no 隐匿 ally unless it blocks that enemy -[ASSUMED: the blocker exception, read from 索敌, for its area abilities too], no untargetable or sleeping one, no +"常见的、来自不同阵营的“选择”行为" skip a unit unless the ability "无视可选性"): no 隐匿 ally, the one blocking that enemy included +(GitHub #97, owner 2026-10-04; the 0.1.2 [ASSUMED] that the blocker's area also hit is withdrawn — the blocked enemy's attack still lands), no untargetable or sleeping one, no airborne 起飞 one for a ground enemy; an invulnerable one still [ASSUMED, as for attacks — §21.22]. 迷彩 is not checked: splash-type damage, 中点判定 / 格子判定 effects and auras ignore it (ba.camou "无法躲避溅射类攻击"; PRTS 异常效果 迷彩 "所有光环类能力、以及涉及中点判定/格子判定的效果均不受迷彩制约") or the enemy's PRTS page says "无视迷彩" — the few sites with none of these hit it [ASSUMED] (DESIGN §22.12). A target the ability locked beforehand (an attack's target, 死亡之眼's channel, a C4, an 爆炸箭) is a direct pick — a 隐匿 it gained meanwhile does not save it (PRTS 异常效果 "'直接选中'的能力 …不受这些仅在选择时生效的异常效果制约"); only the units around it are an area selection (`targetAndArea`). An enemy's -buff aura (深池伙友卫队's field, 扎罗's 远古威慑) takes the allies `targeting.js auraSelectable` accepts — 隐匿 kept out +buff aura (深池伙友卫队's field, 扎罗's 远古威慑) takes the allies `targeting.js auraSelectable` accepts — 隐匿 kept out, the blocker included, (PRTS 作战机制 §隐匿与Buff的关系 "隐匿状态下的单位一般无法被敌方的索敌机制和Buff选择器选中为目标"), an airborne 起飞 ally not [ASSUMED, §21.22]. Until 0.1.2 they took every ally in the area (GitHub issue #32 item 6; DESIGN §22.12). Still on every ally there: the "无视无法选择 / 无视可选性" abilities above (and 远眺's death 暴露, 圆仔's facing count), 寒霜's aura @@ -315,7 +315,7 @@ excepted). 【污染秽蚀】 (萨卡兹枯朽战车's 秽蚀轰击, 同名效果不叠加", user playtest #6): a unit covered by several zones takes one tick per second (`unit.mem.pollutedAt`), so a crowd of dying 萨卡兹枯朽战士 totals 50 / s, not 50 × n. It is "可对空,无视无法选择": it also burns an airborne 起飞 ally (`ignoreSelect`), at the low-ground rate on a low tile. The other zones are no exception to 对地规避: a ground enemy's -燃烧区域 (集团军重型火炮, PRTS "碰撞不受迷彩制约,不可对空" — 迷彩 only) skips an airborne 起飞 ally and an unblocking 隐匿 +燃烧区域 (集团军重型火炮, PRTS "碰撞不受迷彩制约,不可对空" — 迷彩 only) skips an airborne 起飞 ally and a 隐匿 one; the sourceless 毒雾 of 假想敌:蚀裂 skips a 隐匿 one but still reaches an airborne one (PRTS 作战机制's "可以受到无来源的 毒雾伤害" is the stage hazard; until 0.1.2 the cloud took everyone inside). An activated 孽罪奇美拉's aura (`kitChimera`, PRTS "自身半径1.2 范围内的所有单位持续视为受到源石污染区影响,技力自然回复速度倍率-80%,每0.5秒受到50真实持续伤害(同类效果取最高)") is the same @@ -575,7 +575,7 @@ of coverage per 3 s), kept because the current wording no longer says so (feedba | `tremble` (战栗) | 被阻挡后无法进行普通攻击: no normal attack **while blocked** (abilities still fire) | – | | `palsy` (麻痹) | each stack cancels one enemy normal attack (max 3, lasts until consumed); refused by 麻痹免疫 (data `palsyImmune`) | stacks, default 1 | | `disarm` | no normal attacks | – | -| `stealth` / `reveal` | 隐匿: untargetable unless blocked (an ally: only the enemy it blocks attacks it, and an enemy's area effects and buff auras skip it unless it blocks that enemy — `targeting.js areaSelectable` / `auraSelectable`, since 0.1.2) / cancels stealth. An enemy's 隐匿 also stays off after a block: each block's end (`Battle._stealthSwitch`, every release path) switches each of its 隐匿 sources off for `STEALTH_RESTORE` (3) s — PRTS 作战机制 §隐匿 "不被阻挡的3秒后重新进入隐匿" — or the source's own "(解除阻挡N秒后恢复)" (buff `data.stealthRestore`: 0 s for 业余竞演者, 节日爵士乐手, 假想敌:骨刺, 流泪小子, 访问团强攻冠军 and 清明's veil, 1 s for the 家族灭迹人); a new block inside it lifts it again and its end restarts the window; our operators' 隐匿 / 迷彩 never lift by blocking (DESIGN §22.8). `targeting.js enemyStealthed` is the one test: the b.snap stealth bit is set only while its 隐匿 is on (drawn solid otherwise); an operator's radius area damage (`foesInRadius`) skips it too (PRTS 作战机制 §AOE伤害判定 "对攻击范围内的每个可以被选中的敌人进行判定"; until 0.1.1 the splash still hit it) | – | +| `stealth` / `reveal` | 隐匿: untargetable unless blocked (an ally: only the enemy it blocks attacks it, and an enemy's area effects and buff auras skip it even when it blocks that enemy — `targeting.js areaSelectable` / `auraSelectable`, GitHub #97, owner 2026-10-04; 0.1.2 made an exception for that blocker) / cancels stealth. An enemy's 隐匿 also stays off after a block: each block's end (`Battle._stealthSwitch`, every release path) switches each of its 隐匿 sources off for `STEALTH_RESTORE` (3) s — PRTS 作战机制 §隐匿 "不被阻挡的3秒后重新进入隐匿" — or the source's own "(解除阻挡N秒后恢复)" (buff `data.stealthRestore`: 0 s for 业余竞演者, 节日爵士乐手, 假想敌:骨刺, 流泪小子, 访问团强攻冠军 and 清明's veil, 1 s for the 家族灭迹人); a new block inside it lifts it again and its end restarts the window; our operators' 隐匿 / 迷彩 never lift by blocking (DESIGN §22.8). `targeting.js enemyStealthed` is the one test: the b.snap stealth bit is set only while its 隐匿 is on (drawn solid otherwise); an operator's radius area damage (`foesInRadius`) skips it too (PRTS 作战机制 §AOE伤害判定 "对攻击范围内的每个可以被选中的敌人进行判定"; until 0.1.1 the splash still hit it) | – | | `camou` (迷彩) | an ally's camouflage (ba.camou "不阻挡时不成为敌方普通攻击的目标(无法躲避溅射类攻击)"): like `stealth` for enemy targeting (only the enemy it blocks attacks it) — but an enemy's splash and other area effects still hit it (`areaSelectable` does not check it) — and on screen (b.snap stealth bit, `snapshot.js flagsOf`), but not 隐匿 for 隐匿-conditions (叙拉古, 家族徽章) and under its own buff keys. 忍冬 S3 (key `vulpis:camou`, until her next cast), 寒芒克洛丝 S1 | – | | `invulnerable` | ignores damage | – | | `levitate` (浮空) | stun + unblockable (unblocks enemies) + 失衡免疫 (`noDisplace`); an air unit meanwhile (`isFlying`: melee cannot hit it); **half duration on units with (current) massLevel > 3**; refused on data flyers (`motion` FLY) and units already levitated (PRTS 异常效果 "若单位数据上为飞行单位…或是持有浮空异常则Buff取消") — a 近地悬浮 enemy is WALK in its data, so it can be levitated; 浮空 is not one of the 近地悬浮 enemies' drop triggers | – | diff --git a/server/sim/Battle.js b/server/sim/Battle.js index 5b52e929..e50bd35d 100644 --- a/server/sim/Battle.js +++ b/server/sim/Battle.js @@ -1668,7 +1668,7 @@ export class Battle { /** * Every deployed ally within `r` of (x, y) — no selection rule: an enemy's area effect selects among them with - * targeting.js areaSelectable (content/enemies.js areaAllies: no unblocking 隐匿 ally, DESIGN §22.12). + * targeting.js areaSelectable (content/enemies.js areaAllies: no 隐匿 ally, the blocker included, GitHub #97; DESIGN §22.12). */ alliesInRadius(x, y, r, ownerId = null, { includeDevices = false } = {}) { const out = []; diff --git a/server/sim/content/bosses.js b/server/sim/content/bosses.js index 1e717de5..66c22709 100644 --- a/server/sim/content/bosses.js +++ b/server/sim/content/bosses.js @@ -59,7 +59,7 @@ // (a direct pick) and the ticks of a debuff already on it (【自然涌动】: a tick selects nobody). // Every area effect of a leader or part — pulses, strikes around an echo, blasts, charges and tramples, crosses, columns, // whole-field skills — selects with enemies.js areaAllies / areaAlliesInTiles / fieldAllies (targeting.js -// areaSelectable): no 隐匿 operator that does not block the unit, no untargetable or sleeping one, no 起飞 one for a +// areaSelectable): no 隐匿 operator, the one blocking the unit included (GitHub #97), no untargetable or sleeping one, no 起飞 one for a // ground unit; 迷彩 is not checked (splash-type, 中点判定 / 格子判定 or "无视迷彩" on PRTS; the rest [ASSUMED], DESIGN // §22.12). Only 【盲信之誓】 ("无视无法选择、迷彩") takes everyone on its lines. // LP effects ('lpLoss' hook + result.lpLoss) must be applied by the match (see the report of this module's owner). @@ -334,7 +334,7 @@ export function fairOrder(b, e, list, P) { * 斩胄之剑 / 破胄之锤 (PRTS 行动方式 飞行) — and its damage 无来源, so 对地规避 does not stop it: `ignoreSelect` (an airborne * 起飞 ally in the 3×3 is stunned and hurt like the others, although the credited 胄 walks). It is still that unit's area * selection (PRTS: "令自身周围8格内的所有我方单位…" / "…无视迷彩,可对空", no 无视无法选择): areaAlliesInTiles of `by` — no - * 隐匿 ally that does not block it (掷剑 / 掷锤's "无视无法选择" is their pick of the operator they fly at, not the blast). + * 隐匿 ally, the one blocking it included (GitHub #97; 掷剑 / 掷锤's "无视无法选择" is their pick of the operator they fly at, not the blast). */ function stunBlast(b, src, by, r, c, stun, dot, dur, kind) { b.fx('explode', { x: c, y: r, r: 1.5, kind, tiles: 'box' }); diff --git a/server/sim/content/enemies.js b/server/sim/content/enemies.js index 0816c14c..934e9815 100644 --- a/server/sim/content/enemies.js +++ b/server/sim/content/enemies.js @@ -36,7 +36,7 @@ // (targeting.js aggroCmp — PRTS 索敌: 特殊优先级 → 仇恨值). Area effects — splash, blasts, area skills and statuses, // pulses, the zones an enemy leaves, chain / bounce jumps, 周围四格 additions, whole-column / whole-field skills — // select with areaAllies / areaAlliesInTiles / fieldAllies (targeting.js areaSelectable; PRTS 作战机制 §AOE伤害判定 -// "AOE的判定是对攻击范围内的每个可以被选中的敌人进行判定"): no 隐匿 ally that does not block the enemy, no untargetable +// "AOE的判定是对攻击范围内的每个可以被选中的敌人进行判定"): no 隐匿 ally, the one blocking the enemy included (GitHub #97), no untargetable // or sleeping one, no airborne 起飞 one for a ground enemy; 迷彩 is not checked (splash-type, 中点判定 / 格子判定 and // aura effects ignore it — ba.camou "无法躲避溅射类攻击", PRTS 异常效果 迷彩; the sites with no such reading are // [ASSUMED] in DESIGN §22.12). Buff auras select with auraAllies (auraSelectable: 隐匿 kept out, 起飞 not). A locked @@ -63,7 +63,7 @@ // gauge whose burst is the official one (termDescription ba.dt.erosion: "永久降低100点防御力并受到800点物理伤害"). // DOT — damage zones (污染秽蚀: true damage, one tick per second however many cover a unit, "可对空,无视无法选择" // — `ignoreSelect`, 起飞 / 隐匿 allies included; 燃烧区域: the enemy's area selection — PRTS 集团军重型火炮 -// "碰撞不受迷彩制约,不可对空", no 无视无法选择 —, so no 起飞 or unblocking 隐匿 ally; 假想敌:蚀裂's sourceless +// "碰撞不受迷彩制约,不可对空", no 无视无法选择 —, so no 起飞 or 隐匿 ally; 假想敌:蚀裂's sourceless // 毒雾 skips a 隐匿 ally, not a 起飞 one), bleeding (removed by healing), pulsing damage around an enemy // (area selection). // INVISIBLE — permanent `stealth` flag (engine: untargetable unless blocked or revealed). An operator's radius area @@ -72,7 +72,7 @@ // still hit it); tile selectors (enemiesInKeys) always skipped it. After a block it hides again only // STEALTH_RESTORE (3) s later — or after the "(解除阻挡N秒后恢复)" of its PRTS page (STEALTH_RESTORE_BY_KEY, // 清明's veil 0 s) — Battle._stealthSwitch / targeting.js enemyStealthed (until 0.1.2: at once). -// The mirror rule for an ally's 隐匿 — enemy area effects skip it unless it blocks the enemy — is areaAllies (since 0.1.2, DESIGN §22.12). +// The mirror rule for an ally's 隐匿 — enemy area effects skip it even when it blocks the enemy (0.1.3, GitHub #97) — is areaAllies. // REFLECTION — 折射 (ba.refraction "生效时,法术抗性+70"): RES +refracting.magic_resistance while NOT silenced // (the ability line is SILENCE-flagged: silencing turns it off); 镜膜 also gets max HP +100 % while on. // SPECIAL — mostly stats; prisoners, 穿刺手, 暴虐兵长, 镜卫, 动力装甲 … below. @@ -471,7 +471,7 @@ export function allTargets(b, e) { /** * The allies an AREA effect of enemy `src` selects within `r` of (x, y) — splash, blast, area skill / status, pulse, zone - * tick, chain or bounce jump (targeting.js areaSelectable: no unblocking 隐匿, untargetable or sleeping ally, no 起飞 one + * tick, chain or bounce jump (targeting.js areaSelectable: no 隐匿 ally, the blocker included, no untargetable or sleeping ally, no 起飞 one * for a ground `src`; 迷彩 is not checked). `src` = the enemy whose effect it is (null: none — 隐匿 still applies). A * locked target goes first through targetAndArea. Abilities PRTS marks "无视无法选择 / 无视可选性" use b.alliesInRadius with * `ignoreSelect` instead. @@ -491,7 +491,7 @@ export function fieldAllies(b, src) { } /** - * The allies within `r` of (x, y) a buff aura of enemy `src` takes (targeting.js auraSelectable: no unblocking 隐匿, + * The allies within `r` of (x, y) a buff aura of enemy `src` takes (targeting.js auraSelectable: no 隐匿 ally, the blocker included, * untargetable or sleeping ally — PRTS "隐匿状态下的单位一般无法被敌方的…Buff选择器选中"; an airborne 起飞 ally is still * taken [ASSUMED, §21.22]). 寒霜's aura, which PRTS says ignores 隐匿, keeps b.alliesInRadius (allyAura). */ @@ -722,8 +722,8 @@ const onHitStatus = (key, dur, value) => ({ dealt(c, b, e) { if (dur > 0) b.appl * attack starts (`before`): `unblocked` = only an attack begun while not blocked splashes (the others are the plain melee * hit at `meleeScale`). The main target takes the engine's attack at `scale` × ATK (e.profile.atkScale, a normal attack: * on-hit effects apply); when it lands (the main hit's 'hit', before its dodge) the others around the target take - * `scale` × ATK as a splash — an area selection (areaAllies / areaAlliesInTiles: no 隐匿 ally that does not block the - * enemy, no airborne 起飞 one for a ground enemy; 迷彩 is not checked — DESIGN §22.12). `tiles` 1 = the target's tile and + * `scale` × ATK as a splash — an area selection (areaAllies / areaAlliesInTiles: no 隐匿 ally, the blocker included + * (GitHub #97), no airborne 起飞 one for a ground enemy; 迷彩 is not checked — DESIGN §22.12). `tiles` 1 = the target's tile and * its 8 neighbours (格子判定), else `radius` around the target (中点判定). `noAir`: "不可对空" (a flying ally — the 炎佑 * dragon — is skipped; without it "可溅射飞行单位"). `dodge`: the splash hits may be dodged (烹泉 / 沏虹, whose PRTS 天赋 calls * them 法术普通伤害 — a normal attack's damage); a 溅射伤害 splash cannot. `onEach(b, e, u)` runs on every unit hit (the target @@ -1651,7 +1651,7 @@ function kitHolyGuard(ab) { tick(b, e) { const on = b.enemiesInRadius(e.x, e.y, r).some((o) => /enemy_1175_dushdo/.test(o.defId)); if (!on) return; - // a buff aura (auraAllies): no 隐匿 operator that does not block it (PRTS 作战机制 §隐匿与Buff的关系 — no 无视 note on + // a buff aura (auraAllies): no 隐匿 operator, the blocker included (PRTS 作战机制 §隐匿与Buff的关系 — no 无视 note on // its page), but an airborne 起飞 one still [ASSUMED, as §21.22's ground-enemy auras] for (const u of auraAllies(b, e, e.x, e.y, r)) auraBuff(b, u, 'ab:forceField', 0.5, { aspd }, null, true); }, @@ -2074,7 +2074,7 @@ function kitLeaderMisc(key, ab, e) { // PRTS 碎骨 天赋: "不会攻击飞行单位"; below half HP ATK +50 %; "未被阻挡时发射榴弹对目标及其周围八格的我方单位造成相当于攻击力 // 26%的物理伤害,并令其在5秒内防御力下降50%" ("榴弹对主目标造成物理普通伤害,对溅射目标造成物理溅射伤害,可溅射飞行单位") — the // 26 % / 5 s / 3×3 are PRTS's, the −50 % is the blackboard's defdown.def. A blocked attack is its plain melee hit. The - // splash is an area selection: a 隐匿 operator next to the target that does not block it is spared (GitHub issue #32 + // splash is an area selection: a 隐匿 operator is spared, the one blocking this enemy included (GitHub #97; #32 // item 6, DESIGN §22.12). Community report #14 (0.1.2): the grenade used to be a full attack + 100 % on 4 neighbours. const dd = -(T(ab, 'defdown.def') ?? 0); return [lowHpBuff(T(ab, 'atkup.hp_ratio') ?? 0.5, { atkPct: T(ab, 'atkup.atk') ?? 0 }), noAirTargets(), @@ -2520,7 +2520,7 @@ function kitWolfLord(ab) { const P = { form2: false, caught: 0, waitUntil: -Infinity, cast: null }; const caught = (b) => b.allies().some((u) => u.findBuff('ab:fearCage')); const cure = (b) => { for (const u of b.allies()) { const d = u.findBuff('ab:fearCage'); if (d) b.removeBuff(u, d); } }; - // 【远古威慑】 (a buff aura, auraAllies): no 隐匿 operator that does not block it, an airborne 起飞 one still — as 深池伙友卫队's + // 【远古威慑】 (a buff aura, auraAllies): no 隐匿 operator, the blocker included, an airborne 起飞 one still — as 深池伙友卫队's const awe = (b, e) => { for (const u of auraAllies(b, e, e.x, e.y, WOLF_AWE_RADIUS)) auraBuff(b, u, 'ab:ancientAwe', 0.2, { aspd: WOLF_AWE_ASPD }, null, true); }; return [{ spawn(b, e, a, ab2) { diff --git a/server/sim/targeting.js b/server/sim/targeting.js index 65f406ab..286cb695 100644 --- a/server/sim/targeting.js +++ b/server/sim/targeting.js @@ -11,8 +11,8 @@ // (3) higher enemy taunt, (4) least remaining path distance to the goal, (5) earliest spawned. Air units // (Unit.isFlying: FLY, 近地悬浮, 浮空) need a profile that can hit them (`canHitFly`, never `groundOnly`). Enemy // priority: sortAllyTargets. 起飞 (an ally's flag `liftoff`) = 对地规避: no ground enemy selects it (evadesGround). -// An enemy's area effects select allies with areaSelectable, its buff auras with auraSelectable (no unblocking 隐匿 -// ally; 迷彩 is not checked — DESIGN §22.12). +// An enemy's area effects select allies with areaSelectable, its buff auras with auraSelectable (no 隐匿 ally, the one +// blocking the source included — GitHub #97; 迷彩 is not checked — DESIGN §22.12). import { COLS, ROWS } from './constants.js'; import { normDir, rotateOffset } from './dir.js'; @@ -112,9 +112,10 @@ export function canTargetAlly(e, a, ranged) { * 任何敌方的能力索敌选中"; PRTS 异常效果 §无法选择: with 隐匿, 不可选中, 无敌, 塔不可选中 or 对地规避 "常见的、来自不同阵营的 * “选择”行为将无视这些单位进行(如同范围内不存在这个单位)", and the abilities PRTS marks "无视可选性" are those that skip * that check. So not: - * - a 隐匿 ally (flag `stealth`: 伪装服, 叙拉古 6, 伊内丝 S2, the 排气格栅 tile) unless it blocks `src` — a blocked enemy - * attacks its blocker whatever its selectability (PRTS 作战机制 "因为“阻挡优先级最高”的效果,敌人会无视一切可选性对该干员 - * 进行攻击"; read for its area abilities too [ASSUMED]); a dead `src` (death blasts) blocks nobody; + * - a 隐匿 ally (flag `stealth`: 伪装服, 叙拉古 6, 伊内丝 S2, the 排气格栅 tile), even when it blocks `src`. The blocked + * enemy still attacks that blocker (canTargetAlly; PRTS 作战机制 "因为“阻挡优先级最高”的效果,敌人会无视一切可选性对该干员 + * 进行攻击"). Its splash, blast, zone and aura do not (GitHub #97, owner 2026-10-04; the 0.1.2 [ASSUMED] that the + * blocker's area also hit is withdrawn). A dead `src` (death blasts) blocks nobody; * - an untargetable (不可选中) or sleeping (沉睡 = 无敌 + 无法行动) ally; * - an airborne 起飞 ally when `src` walks (evadesGround — the same refusal the damage pipeline makes). * An invulnerable (`invulnerable`, 无敌) ally is still selected, as by canTargetAlly — a known deviation from 无法选择 @@ -134,15 +135,15 @@ export function areaSelectable(src, a) { * May a BUFF AURA of enemy-side `src` (a 光环 refreshed on whoever stands in it — 深池伙友卫队's force field, 扎罗's * 远古威慑; content/enemies.js auraAllies) take ally `a`? PRTS 作战机制 §隐匿与Buff的关系 "隐匿状态下的单位一般无法被敌方的 * 索敌机制和Buff选择器选中为目标", "目前明日方舟中使用能选中隐匿状态单位的Buff效果一定是无视隐匿状态起作用的" (its example: - * 寒霜's 攻速下降 Debuff — content/enemies.js allyAura keeps that one on every ally): no 隐匿 ally unless it blocks `src`, - * no untargetable or sleeping one; 迷彩 does not protect ("所有光环类能力…均不受迷彩制约"). Unlike areaSelectable it does not + * 寒霜's 攻速下降 Debuff — content/enemies.js allyAura keeps that one on every ally): no 隐匿 ally, the one blocking `src` + * included (GitHub #97), no untargetable or sleeping one; 迷彩 does not protect ("所有光环类能力…均不受迷彩制约"). Unlike areaSelectable it does not * apply 对地规避: a ground enemy's aura still reaches an airborne 起飞 ally [ASSUMED, DESIGN §21.20 / §21.22]. */ export function auraSelectable(src, a) { if (!a || !a.alive || !a.deployed || a.hidden || a.kind === 'device') return false; const f = a.s.flags; - if (f.untargetable || f.sleep) return false; - return !(f.stealth && !(src && src.alive && src.blockedBy === a)); + if (f.untargetable || f.sleep || f.stealth) return false; + return true; } /** diff --git a/test/sim/feedback2-stealth-splash.test.js b/test/sim/feedback2-stealth-splash.test.js index e2201210..74b5c7b2 100644 --- a/test/sim/feedback2-stealth-splash.test.js +++ b/test/sim/feedback2-stealth-splash.test.js @@ -12,10 +12,11 @@ // 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: +// 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.) +// (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'; @@ -54,7 +55,7 @@ function countFx(h, kind) { return n; } -test('areaSelectable: no 隐匿 ally unless it blocks the source, no untargetable / sleeping one, no 起飞 one for a ground source; 迷彩 is selected', () => { +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' }) } }, @@ -68,15 +69,15 @@ test('areaSelectable: no 隐匿 ally unless it blocks the source, no untargetabl 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(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(), [jf, hd].map((u) => u.def.name).sort(), 'areaAllies: the 隐匿 bystander is left out'); + 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), true, 'a buff aura: 隐匿 blocker — 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 (对地规避)'); @@ -140,7 +141,7 @@ test('#32.6 碎骨: 伪装服\'s 隐匿 (its carrier\'s first damage) spares the done(r.h); }); -test('#32.6 “庞贝”\'s self-blast while blocked hits its 隐匿 blocker and a 迷彩 bystander, not a 隐匿 bystander', REAL, () => { +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' }], @@ -155,7 +156,8 @@ test('#32.6 “庞贝”\'s self-blast while blocked hits its 隐匿 blocker and 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(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);