feedback1f WT: 起飞 review — only selections skip an airborne 蒂比

- A ground enemy's damage zone no longer ignores 对地规避: 集团军重型火炮's 燃烧区域
  ("碰撞不受迷彩制约,不可对空", 迷彩 only) skips her; only 【污染秽蚀】 ("无视无法选择") reaches her.
- Not selections, so they still reach her (ignoreSelect): 假想敌:铳's 【盲信之誓】 chains
  ("无视无法选择"), the 碎铳之簧 法术护盾 counter on its attacker (a direct pick, 无来源), and the
  刺胄之弹 / 斩胄之剑 / 破胄之锤 blasts (flying units, 无来源 DoT, credited to the ground 胄).
- One-shot areas of ground enemies skip her: 烹泉's death blast and steam slow, 鼠王's 沙狱
  (ATK cut + DoT), and “帝国的甲胄”'s entrance barrage (which now hits the next unit instead of
  wasting every shot on her).
- Tests: 5 new cases in test/sim/feedback1f-liftoff.test.js, all failing before this change.
- Docs: enemies.js / bosses.js headers, the evadesGround and DamageInfo docstrings, and SIM.md.
This commit is contained in:
sganggs committed 2026-10-03 08:15:52 +08:00
1 parent 7c9ee6dad8
commit 6286448697
6 files changed
+263 -39

No files matched your search

+17 -7
View File
@@ -53,6 +53,10 @@
// The huge leaders (SELF_BOUND: 胄 ×2, 管 ×2, 昆图斯, 阿利斯泰尔, 萨米的意志 — the 巨型单位 with a data `hitArea`) are
// 自缚 + 无法被阻挡 (PRTS 天赋): a persistent noMove + unblockable buff from spawn, so they never walk their route.
// Every leader (tag boss) ignores 侵蚀 gauge damage ("最终攻势中,敌方领袖不会受到侵蚀损伤").
// An airborne (起飞) operator is no selection of a ground leader or part (对地规避: canTargetAlly, the damage pipeline,
// the 碎铳之簧 bounce); still reach it (`ignoreSelect`): the 刺胄之弹 / 剑 / 锤 blasts (flying units, 无来源 DoT; 掷剑 /
// 掷锤 pick their operator "(无视无法选择)"), the 盲信之誓 chains ("无视无法选择") and the 法术护盾 counter on its attacker
// (a direct pick).
// LP effects ('lpLoss' hook + result.lpLoss) must be applied by the match (see the report of this module's owner).
// fx kinds: 'beam' 'shell' 'explode' 'telegraph' 'charge' 'link' 'dash' 'column' 'tide' 'rockfall' 'tentacle' 'equip'
// 'sword' 'vest' 'blink' 'summon' 'grow' 'phase' 'lpLoss' (x, y + extra {id, r, tiles, kind, tx, ty …}).
@@ -320,13 +324,17 @@ export function fairOrder(b, e, list, P) {
return out.concat(rest);
}
/** Stun + phys DoT on the 3×3 around (r, c). */
/**
* Stun + phys DoT on the 3×3 around (r, c), credited to `src` (胄). The blast is a flying unit's — 刺胄之弹 / 斩胄之剑 /
* 破胄之锤 (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).
*/
function stunBlast(b, src, r, c, stun, dot, dur, kind) {
b.fx('explode', { x: c, y: r, r: 1.5, kind, tiles: 'box' });
for (const u of alliesInTiles(b, r, c, 'box', 1)) {
if (stun > 0) b.applyStatus(u, 'stun', { duration: stun, source: src });
if (stun > 0) b.applyStatus(u, 'stun', { duration: stun, source: src, ignoreSelect: true });
if (dot > 0 && dur > 0) b.addBuff(u, { key: `boss:${kind}Dot`, duration: dur, refresh: 'replace', interval: 1, visible: true,
onTick: ({ battle, unit }) => battle.dealDamage(src, unit, { amount: dot, type: 'phys', canDodge: false, tags: ['enemyAbility', kind] }) });
onTick: ({ battle, unit }) => battle.dealDamage(src, unit, { amount: dot, type: 'phys', canDodge: false, ignoreSelect: true, tags: ['enemyAbility', kind] }) });
}
}
/** Distance from point p to segment a–b. */
@@ -587,11 +595,11 @@ function kitGun(ab, e, b) {
});
if (s3) list.push({
iv: s3.bb.interval ?? 1,
tick(b2, e2) { // 【盲信之誓】 links: phys per second on operators standing on a line
tick(b2, e2) { // 【盲信之誓】 links: phys per second on operators standing on a line ("无视无法选择、迷彩": 起飞 too)
for (const sp of b2.enemies) {
if (!sp.alive || !isSpring(sp)) continue;
b2.fx('link', { x: e2.x, y: e2.y, from: e2.id, to: sp.id, kind: 'faithLink', dur: s3.bb.interval ?? 1 });
for (const u of b2.allies()) if (segDist(u.x, u.y, e2.x, e2.y, sp.x, sp.y) <= LINK_WIDTH) hurt(b2, e2, u, s3.bb.value ?? 0, 'phys');
for (const u of b2.allies()) if (segDist(u.x, u.y, e2.x, e2.y, sp.x, sp.y) <= LINK_WIDTH) hurt(b2, e2, u, s3.bb.value ?? 0, 'phys', { ignoreSelect: true, tags: ['faithLink'] });
}
},
});
@@ -636,8 +644,10 @@ function kitSpring(ab, e) {
if (P.barrier <= 1e-6) { P.barrier = 0; drop(b); }
} else if (ty === 'phys') {
c.dmg.mul *= scale;
const s = c.source;
if (s && s.side === 'ally' && s.alive) { hurt(b, e2, s, e2.s.atk * (T(ab, '1.atk_scale') ?? 0), 'phys'); elem(b, e2, s, 'erosion', e2.s.atk * (T(ab, '1.ep_damage_ratio') ?? 0)); }
// the counter "对来源造成…无来源物理附加伤害" picks its attacker directly — no selection, so an airborne 起飞
// attacker takes it too (PRTS 异常效果 无法选择: "'直接选中'的能力…不受这些仅在选择时生效的异常效果制约")
const s = c.source, o = { ignoreSelect: true, tags: ['springCounter'] };
if (s && s.side === 'ally' && s.alive) { hurt(b, e2, s, e2.s.atk * (T(ab, '1.atk_scale') ?? 0), 'phys', o); elem(b, e2, s, 'erosion', e2.s.atk * (T(ab, '1.ep_damage_ratio') ?? 0), o); }
}
} else if (kind === 'element') { // 元素护盾: phys/arts heavily reduced
if (ty === 'phys' || ty === 'arts') c.dmg.mul *= scale;
+26 -16
View File
@@ -30,8 +30,11 @@
// special priority (优先攻击防御力最高的… / 生命上限最高的…) sorts by its key, ties by taunt then latest deployed
// (targeting.js aggroCmp — PRTS 索敌: 特殊优先级 → 仇恨值). An airborne ally (起飞, flag `liftoff`) is no target of a
// ground enemy (对地规避, targeting.js evadesGround): targetsNear / allTargets skip it through canTargetAlly, the picks
// that bypass it (周围四格 additions, chain and bounce jumps) filter it, and the engine refuses a ground enemy's damage
// and statuses on it; damage zones "无视无法选择" (`ignoreSelect`) still reach it.
// and one-shot areas that bypass it (周围四格 additions, chain and bounce jumps, shells, barrages, 沙狱, death blasts)
// filter it, and the engine refuses a ground enemy's damage and statuses on it. Not selections, so they still reach
// it (`ignoreSelect`): abilities PRTS marks "无视无法选择" (【污染秽蚀】, 【盲信之誓】), direct picks of the attacker
// (碎铳之簧's counter — PRTS 异常效果 "'直接选中'的能力…不受这些仅在选择时生效的异常效果制约") and the blasts of flying
// units credited to a ground leader (刺胄之弹, 斩胄之剑 / 破胄之锤). Auras of ground enemies still apply [ASSUMED].
//
// Special types (factions.json):
// FLY — engine (FLY motion, ranged-only targeting). Flyer kits below (御4, 护障, 寒霜, 萨科塔之翼/眼, 黑云 …).
@@ -41,9 +44,9 @@
// create those units (death spawns, embers, blades, 再生).
// ELEMENT — element damage on hit = ATK × ep_damage_ratio into the ally's gauge. 侵蚀 (erosion) is an engine
// 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; 毒雾, 燃烧区域 —
// they ignore 无法选择, PRTS 污染秽蚀 "可对空,无视无法选择": `ignoreSelect`, 起飞 allies included),
// bleeding (removed by healing), pulsing auras.
// DOT — damage zones (污染秽蚀: true damage, one tick per second however many cover a unit, "可对空,无视无法选择"
// — `ignoreSelect`, 起飞 allies included; 毒雾, 燃烧区域: a ground enemy's zone skips a 起飞 ally — PRTS
// 集团军重型火炮 "碰撞不受迷彩制约,不可对空", no 无视无法选择), bleeding (removed by healing), pulsing auras.
// INVISIBLE — permanent `stealth` flag (engine: untargetable unless blocked or revealed; AoE still hits).
// 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.
@@ -333,15 +336,16 @@ function onTick(b, dt) {
// ---------------------------------------------------------------------------------------------------------------
// helpers (exported for bosses.js)
/** Element damage (erosion mapped onto the engine gauge). */
export function elem(b, src, tgt, el, amount, { ignoreSelect = false } = {}) {
/** Element damage (erosion mapped onto the engine gauge); `ignoreSelect` / `tags` as hurt(). */
export function elem(b, src, tgt, el, amount, { ignoreSelect = false, tags = [] } = {}) {
if (!tgt || !tgt.alive || !(amount > 0)) return 0;
const element = el === 'erosion' ? EROSION : el;
return b.dealDamage(src, tgt, { type: 'element', element, amount, ignoreSelect, tags: ['enemyAbility'] });
return b.dealDamage(src, tgt, { type: 'element', element, amount, ignoreSelect, tags: ['enemyAbility', ...tags] });
}
/**
* Skill / ability damage (no dodge unless asked). `ignoreSelect` = the ability "无视无法选择" (zones): it also reaches an
* Skill / ability damage (no dodge unless asked). `ignoreSelect` = no selection the target's 无法选择 effects stop
* (abilities that "无视无法选择", direct picks, a flying unit's blast credited to a ground leader): it also reaches an
* airborne 起飞 ally, which a ground enemy's damage otherwise skips (damage.js, targeting.js evadesGround).
*/
export function hurt(b, src, tgt, amount, type = 'phys', { canDodge = false, tags = [], isSkill = true, ignoreSelect = false } = {}) {
@@ -917,14 +921,15 @@ function pollution(b, src, x, y, r, life, low, high) {
}
/**
* Damage zone (arts per tick) — on every ally inside, like 【污染秽蚀】 (`ignoreSelect`: 起飞 allies too) [ASSUMED for the
* zones PRTS gives no 无法选择 note: 毒雾, 燃烧区域].
* Damage zone (`amount` per tick, tagged `kind`) on every ally inside. Unlike 【污染秽蚀】 it does not ignore 无法选择:
* a ground enemy's zone skips an airborne 起飞 ally (the damage pipeline; PRTS 集团军重型火炮 【燃烧区域】 "碰撞不受迷彩
* 制约,不可对空" — 迷彩 only). A sourceless zone (假想敌:蚀裂's 毒雾) reaches everyone inside.
*/
function dmgZone(b, src, x, y, r, life, iv, amount, type = 'arts', kind = 'zone', el = null, elAmount = 0) {
zone(b, { x, y, r, life, iv, kind, tick(units) {
for (const u of units) {
hurt(b, src, u, amount, type, { ignoreSelect: true });
if (el && elAmount > 0) elem(b, src, u, el, elAmount, { ignoreSelect: true });
hurt(b, src, u, amount, type, { tags: [kind] });
if (el && elAmount > 0) elem(b, src, u, el, elAmount, { tags: [kind] });
}
} });
}
@@ -1583,12 +1588,14 @@ function kitTeapot(ab) {
if (c.reason !== 'killed') return;
const aspd = T(ab, 'DeadBoom.attack_speed') ?? 0, dur = T(ab, 'DeadBoom.duration') ?? 0, iv = T(ab, 'DeadBoom.interval') ?? 1;
const r = e.base.rangeRadius || 2, atk = e.s.atk, x = e.x, y = e.y;
// a selection (PRTS 烹泉 "死亡爆炸(…无视迷彩,不可对空)", no 无视无法选择): an airborne 起飞 ally is skipped
const hit = (units) => units.filter((u) => !evadesGround(e, u));
// 被击倒后爆炸造成范围法术伤害 (one blast of ATK ×1 [ASSUMED scale]) …
b.fx('explode', { x, y, r, kind: 'teaBoom', id: e.id });
for (const u of b.alliesInRadius(x, y, r)) hurt(b, null, u, atk, 'arts');
for (const u of hit(b.alliesInRadius(x, y, r))) hurt(b, null, u, atk, 'arts', { tags: ['teaBoom'] });
// … 使我方攻击速度大幅降低(可被抵抗): the steam lingers `duration` s; allies inside keep the ASPD cut (re-applied
// every `interval` s)
const slow = (units) => { for (const u of units) b.addBuff(u, { key: 'ab:teaBoom', duration: iv + 0.1, refresh: 'extend', mods: { aspd }, visible: true }); };
const slow = (units) => { for (const u of hit(units)) b.addBuff(u, { key: 'ab:teaBoom', duration: iv + 0.1, refresh: 'extend', mods: { aspd }, visible: true }); };
slow(b.alliesInRadius(x, y, r));
if (dur > 0) zone(b, { x, y, r, life: dur, iv, kind: 'teaSteam', tick: slow });
},
@@ -1666,7 +1673,9 @@ function kitLeaderMisc(key, ab, e) {
if (!t) return;
const dur = ss.bb.duration ?? 0, x = t.x, y = t.y;
b.fx('zone', { x, y, r: SANDSTORM_RADIUS, dur, kind: 'sandStorm', id: e2.id });
// the 沙狱弹道 "击中…范围内的所有我方单位(弹道可对空)" selects: an airborne 起飞 ally is skipped (no 无视无法选择)
for (const u of b.alliesInRadius(x, y, SANDSTORM_RADIUS)) {
if (evadesGround(e2, u)) continue;
b.addBuff(u, { key: 'ab:sandStorm', duration: dur, refresh: 'replace', interval: 1, visible: true, mods: { atkPct: ss.bb.atk ?? 0 },
onTick: ({ battle, unit }) => battle.dealDamage(e2, unit, { amount: ss.bb.damage ?? 0, type: 'arts', canDodge: false, tags: ['enemyAbility', 'sandStorm'] }) });
}
@@ -2385,7 +2394,8 @@ export const KITS = Object.freeze({
if (!lock) return;
b.fx('telegraph', { x: lock.x, y: lock.y, r: 1, kind: 'barrage', id: e.id });
for (let i = 0; i < n; i++) {
const t = alliesInTiles(b, lock.tileR, lock.tileC, 'box', 1).sort((p, q) => q.hp - p.hp)[0];
// "对碰撞范围内的1名当前生命值最高的我方单位" — a selection: never an airborne 起飞 ally (PRTS “帝国的甲胄”)
const t = alliesInTiles(b, lock.tileR, lock.tileC, 'box', 1).filter((u) => !evadesGround(e, u)).sort((p, q) => q.hp - p.hp)[0];
if (t) hurt(b, e, t, e.s.atk, 'phys');
}
}, { owner: e });