From a973bbe35f1add94b4ee16049410dd4d4936fbac Mon Sep 17 00:00:00 2001 From: sganggs Date: Sat, 3 Oct 2026 09:07:51 +0800 Subject: [PATCH] =?UTF-8?q?feedback1f=20WT:=20=E8=B5=B7=E9=A3=9E=20review?= =?UTF-8?q?=202=20=E2=80=94=20=E6=9A=B4=E8=99=90=E5=85=B5=E9=95=BF's=20?= =?UTF-8?q?=E6=9A=B4=E5=87=BB=20splash=20and=20the=20ticks=20of=20debuffs?= =?UTF-8?q?=20already=20on=20her=20reach=20an=20airborne=20=E8=92=82?= =?UTF-8?q?=E6=AF=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PRTS 萨卡兹悖谬暴虐兵长 技能0 暴击 "对目标和周围4格的我方单位造成攻击力150%的物理普通伤害(无视无法选择,无视迷彩)": the splash passes ignoreSelect. The ticks of a debuff a ground enemy put on her before she took off (出血, 沙狱, the burning DoT, 淤困, 【自然涌动】) select nobody (PRTS 异常效果: 无法选择 effects only act on selection), so they pass ignoreSelect too; a shot already in flight is still refused [ASSUMED]. Comments in tier2.js and SIM.md no longer say that nothing of a ground enemy damages her. --- docs/SIM.md | 20 ++++---- server/sim/content/bosses.js | 6 +-- server/sim/content/enemies.js | 30 ++++++++---- server/sim/content/kits/tier2.js | 7 +-- server/sim/damage.js | 14 +++--- server/sim/targeting.js | 8 ++-- test/sim/feedback1f-liftoff.test.js | 71 +++++++++++++++++++++++++++-- 7 files changed, 120 insertions(+), 36 deletions(-) diff --git a/docs/SIM.md b/docs/SIM.md index 57b5fdd0..ba700418 100644 --- a/docs/SIM.md +++ b/docs/SIM.md @@ -209,10 +209,11 @@ an airborne ally (起飞, flag `liftoff`: 蒂比's skills) never for a ground en PRTS 术语释义 起飞 "无法被不同阵营行动方式为地面的单位选中"; flyers, 近地悬浮 and 浮空 enemies still pick it; content picks and one-shot areas that bypass `canTargetAlly` — 控潮术师 / 腐败骑士's 周围四格, 陷落雪祀's chain, the 碎铳之簧 bounce, the 自行炮 shells, “帝国的甲胄”'s barrage, 鼠王's 沙狱, 烹泉's death blast and steam — filter it too; not selections, so they -still reach it (`ignoreSelect`): abilities PRTS marks "无视无法选择" (【污染秽蚀】, 假想敌:铳's 【盲信之誓】), direct picks -(碎铳之簧's 法术护盾 counter on its attacker — PRTS 异常效果 "'直接选中'的能力…不受这些仅在选择时生效的异常效果制约") and -the blasts of flying units credited to the ground 胄 (刺胄之弹, 斩胄之剑 / 破胄之锤 — whose 掷剑 / 掷锤 pick "(无视无法选择)"); -auras of ground enemies still apply [ASSUMED]); +still reach it (`ignoreSelect`): abilities PRTS marks "无视无法选择" (【污染秽蚀】, 假想敌:铳's 【盲信之誓】, 萨卡兹悖谬 +暴虐兵长's 暴击 splash), direct picks (碎铳之簧's 法术护盾 counter on its attacker — PRTS 异常效果 "'直接选中'的能力…不受这些 +仅在选择时生效的异常效果制约"), the blasts of flying units credited to the ground 胄 (刺胄之弹, 斩胄之剑 / 破胄之锤 — whose +掷剑 / 掷锤 pick "(无视无法选择)") and the ticks of a debuff a ground enemy put on it before it took off (出血, 沙狱, +burning DoTs, 淤困, 【自然涌动】 — a tick selects nobody); auras of ground enemies still apply [ASSUMED]); 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 "不阻挡时不成为敌方普通攻击的目标") likewise (PRTS 异常效果: neither anomaly is "阻挡时解除") [ASSUMED: enemy skills and @@ -384,7 +385,9 @@ A `rangeExtend` on a `persist` never-expiring buff is **permanent**: it also wid untargetable blockFly noMove noSp burstLock hidden attract float noDisplace isolated camou liftoff` (`float` = 近地悬浮 (an air unit, `Unit.isFlying`), `liftoff` = an ally's 起飞 (蒂比's skills; gamedata_const ba.liftoff "不阻挡地面敌人且不会被地面敌人攻击, 可以阻挡飞行敌人"): it blocks no ground enemy (`Battle._blockerFor`) and has 对地规避 — no ground enemy (not `isFlying`) -selects it, damages it or puts a status on it (`targeting.js evadesGround`); flyers, 近地悬浮 and 浮空 enemies still do, +selects it, so no selected damage or status of one lands on it (`targeting.js evadesGround`), while what selects nobody +still does (`ignoreSelect` / 无来源: 无视无法选择 abilities, direct picks, flying units' blasts, a debuff's ticks); flyers, +近地悬浮 and 浮空 enemies still select it, with no 对空 check, since it stays a ground unit on its tile (`unit.ground` unchanged; PRTS 行动方式 "起飞的干员仍然是地面单位"), `noDisplace` = 失衡免疫 (`displace()` moves nothing), `isolated` = 孤立 ("无法被同阵营选中": no ally ability selects it — no heal, buff, aura or talent pick from another ally (`battle.allySelectable` / `alliesFor`, @@ -490,7 +493,7 @@ HP left is refused (damage.js `hasHp`: a lethal hit's `damaged` hook runs before element?, defIgnoreFlat, defIgnorePct, resIgnoreFlat, resIgnorePct, mul=1, canDodge (phys/arts), isSkill, isSplash, isAttack, attackId, ignoreSleep, ignoreSelect, sourceless, tags[], cancel }` (`ignoreSelect`: no selection 无法选择 effects stop — an ability "无视无法选择", a direct pick such as a counter on the attacker, a flying unit's blast credited to -a ground leader — it reaches an airborne 起飞 ally whatever its source; `sourceless`: 无来源 damage — the source's stats add +a ground leader, the tick of a debuff already on the unit — it reaches an airborne 起飞 ally whatever its source; `sourceless`: 无来源 damage — the source's stats add nothing and the hooks get `source: null` plus `credit` = the source, which keeps the stats and the kill; a `loseHp` whose `from` is 无来源 is 无来源 too; element bursts, leader-part transfers and 坚守 thorns use it — content damage that has a responsible unit should pass it as `source` with `sourceless: true` rather than `source: null`, which credits nobody) @@ -500,8 +503,9 @@ for "本次攻击" procs that must roll once per attack. **Dodge** from several `s.dodgePhys` = 1 − Π(1 − pᵢ) (a single source keeps its exact value). Order: invulnerable / asleep / 对地规避? (a ground enemy's damage or element fill on an airborne 起飞 ally — flag `liftoff`, `targeting.js evadesGround` — is refused unless `ignoreSelect` or 无来源: it cannot select her, so its splash and area -abilities skip her and a shot in flight or a DoT ticking when she took off lands on nothing [ASSUMED for the last two: -PRTS 伤害流程 7 "取消掉隐匿/无敌状态下的攻击" read for 对地规避]; checked before `hit` only — 蒂比 S2 takes off inside the +abilities skip her and a shot in flight when she took off lands on nothing [ASSUMED: PRTS 伤害流程 7 "取消掉隐匿/无敌状态 +下的攻击" read for 对地规避]; the ticks of a debuff already on her select nobody and land (content passes `ignoreSelect`; +PRTS 异常效果: 无法选择 effects "仅在选择时生效"); checked before `hit` only — 蒂比 S2 takes off inside the `hit` of the attack that set it off, which resolves as usual) → **`hit`** (mutate `dmg`, set `dmg.cancel`) → dodge (`rng()`) → mitigation (phys `max(A − max(0, D×(1−defIgnorePct) − defIgnoreFlat), 5 %A)`, arts `max(A×(1 − R′/100), 5 %A)`, elemental diff --git a/server/sim/content/bosses.js b/server/sim/content/bosses.js index 3263b82f..c6715c69 100644 --- a/server/sim/content/bosses.js +++ b/server/sim/content/bosses.js @@ -55,8 +55,8 @@ // 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). +// 掷锤 pick their operator "(无视无法选择)"), the 盲信之誓 chains ("无视无法选择"), the 法术护盾 counter on its attacker +// (a direct pick) and the ticks of a debuff already on it (【自然涌动】: a tick selects nobody). // 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 …}). @@ -1076,7 +1076,7 @@ function kitDeer(ab, e) { b.fx('beam', { x: e2.x, y: e2.y, from: e2.id, to: t.id, kind: 'naturalSurge', dur }); b.applyStatus(t, 'stun', { duration: dur, source: e2 }); b.addBuff(t, { key: 'boss:surge', duration: dur, interval: 1, visible: true, - onTick: ({ battle, unit }) => battle.dealDamage(e2, unit, { amount: e2.s.atk * (lasso.bb.atk_scale ?? 0), type: 'arts', canDodge: false, tags: ['enemyAbility'] }) }); + onTick: ({ battle, unit }) => battle.dealDamage(e2, unit, { amount: e2.s.atk * (lasso.bb.atk_scale ?? 0), type: 'arts', canDodge: false, ignoreSelect: true, tags: ['enemyAbility'] }) }); } }, }, diff --git a/server/sim/content/enemies.js b/server/sim/content/enemies.js index 67a62fdd..57d5dc53 100644 --- a/server/sim/content/enemies.js +++ b/server/sim/content/enemies.js @@ -32,9 +32,11 @@ // ground enemy (对地规避, targeting.js evadesGround): targetsNear / allTargets skip it through canTargetAlly, the picks // 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]. +// it (`ignoreSelect`): abilities PRTS marks "无视无法选择" (【污染秽蚀】, 【盲信之誓】, 萨卡兹悖谬暴虐兵长's 暴击 splash), +// direct picks of the attacker (碎铳之簧's counter — PRTS 异常效果 "'直接选中'的能力…不受这些仅在选择时生效的异常效果 +// 制约"), the blasts of flying units credited to a ground leader (刺胄之弹, 斩胄之剑 / 破胄之锤) and the ticks of a debuff +// already on it (出血, 沙狱, “庞贝”'s burning, 淤困, 【自然涌动】 — a tick selects nobody). Auras of ground enemies still +// apply [ASSUMED]. // // Special types (factions.json): // FLY — engine (FLY motion, ranged-only targeting). Flyer kits below (御4, 护障, 寒霜, 萨科塔之翼/眼, 黑云 …). @@ -889,7 +891,10 @@ function statue(ab) { }; } -/** Bleeding on hit (逐腐兽): arts damage per second, removed by healing. */ +/** + * Bleeding on hit (逐腐兽): arts damage per second, removed by healing. A tick selects nobody (`ignoreSelect`): it keeps + * hurting an ally that took off (起飞) after the bleed landed; so do the other enemy debuff DoTs (沙狱, burnDot, 淤困). + */ function bleed(ab) { const dmg = T(ab, 'Bleeding.attack@bleeding_damage') ?? 0, dur = T(ab, 'Bleeding.attack@duration') ?? 0; return { @@ -897,7 +902,7 @@ function bleed(ab) { if (!(dmg > 0 && dur > 0)) return; b.addBuff(c.target, { key: 'ab:bleed', duration: dur, refresh: 'replace', interval: 1, visible: true, - onTick: ({ battle, unit }) => battle.dealDamage(e, unit, { amount: dmg, type: 'arts', canDodge: false, tags: ['enemyAbility', 'bleed'] }), + onTick: ({ battle, unit }) => battle.dealDamage(e, unit, { amount: dmg, type: 'arts', canDodge: false, ignoreSelect: true, tags: ['enemyAbility', 'bleed'] }), }); }, }; @@ -1321,6 +1326,11 @@ function kitRush(ab) { }]; } +/** + * 萨卡兹悖谬暴虐兵长: blocked only by a blocker with ≥ 3 free block; its first hit also strikes the units around its target + * (PRTS 技能0 暴击 "对目标和周围4格的我方单位造成攻击力150%的物理普通伤害(无视无法选择,无视迷彩)※此技能仅能触发一次"): + * the splash ignores 无法选择, so it reaches an airborne 起飞 ally too (`ignoreSelect`). + */ function kitFirstAoe(ab) { const scale = T(ab, 'AOEAttack.atk_scale') ?? 0; return [blockWeight(3), { @@ -1328,7 +1338,9 @@ function kitFirstAoe(ab) { if (a.done) return; a.done = true; b.fx('explode', { x: c.target.x, y: c.target.y, r: 1, kind: 'aoeAttack' }); - for (const u of b.alliesInRadius(c.target.x, c.target.y, 1)) if (u !== c.target) hurt(b, e, u, e.s.atk * scale, 'phys'); + for (const u of b.alliesInRadius(c.target.x, c.target.y, 1)) { + if (u !== c.target) hurt(b, e, u, e.s.atk * scale, 'phys', { ignoreSelect: true, tags: ['aoeAttack'] }); + } }, }]; } @@ -1382,7 +1394,7 @@ function kitParasite(ab) { b.addBuff(host, { // "受到的元素损伤提高至130%": a 元素损伤 multiplier on the element hit (ensureParasiteHook), not 元素伤害 key: 'ab:parasite', visible: true, interval: 1, data: { src: e, spread: PARASITE_SPREAD, epMul: elMul }, - onTick: ({ battle, unit }) => { if (e.alive) battle.dealDamage(e, unit, { amount: e.s.atk * scale, type: 'arts', canDodge: false, tags: ['enemyAbility', 'parasite'] }); }, + onTick: ({ battle, unit }) => { if (e.alive) battle.dealDamage(e, unit, { amount: e.s.atk * scale, type: 'arts', canDodge: false, ignoreSelect: true, tags: ['enemyAbility', 'parasite'] }); }, }); }, death(c, b, e, a) { if (a.host) b.removeBuff(a.host, 'ab:parasite'); }, @@ -1618,7 +1630,7 @@ function kitLeaderMisc(key, ab, e) { dealt(c, b, e2) { if (!(dot.dur > 0)) return; b.addBuff(c.target, { key: 'ab:burnDot', duration: dot.dur, refresh: 'replace', interval: dot.iv, visible: true, - onTick: ({ battle, unit }) => battle.dealDamage(e2, unit, { amount: dot.dmg, type: 'arts', canDodge: false, tags: ['enemyAbility'] }) }); + onTick: ({ battle, unit }) => battle.dealDamage(e2, unit, { amount: dot.dmg, type: 'arts', canDodge: false, ignoreSelect: true, tags: ['enemyAbility'] }) }); }, }, { iv: T(ab, 'rangedamage.interval') ?? 10, @@ -1677,7 +1689,7 @@ function kitLeaderMisc(key, ab, e) { 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'] }) }); + onTick: ({ battle, unit }) => battle.dealDamage(e2, unit, { amount: ss.bb.damage ?? 0, type: 'arts', canDodge: false, ignoreSelect: true, tags: ['enemyAbility', 'sandStorm'] }) }); } }, { cond: (b, e2) => allTargets(b, e2).length > 0 }), ]; diff --git a/server/sim/content/kits/tier2.js b/server/sim/content/kits/tier2.js index 9e140968..94be315c 100644 --- a/server/sim/content/kits/tier2.js +++ b/server/sim/content/kits/tier2.js @@ -37,8 +37,9 @@ function onDefaultSkill(chess) { /** * 起飞 of 蒂比's skills (gamedata_const ba.liftoff "不阻挡地面敌人且不会被地面敌人攻击,可以阻挡飞行敌人"): the skill's flags - * `liftoff` (no ground enemy blocked — Battle._blockerFor; 对地规避 — targeting.js evadesGround: no ground enemy selects or - * damages her) and `blockFly` (blocks flyers at the air radius). She stays a ground unit on her tile (PRTS 行动方式 "起飞的 + * `liftoff` (no ground enemy blocked — Battle._blockerFor; 对地规避 — targeting.js evadesGround: no ground enemy selects + * her, while what selects nobody still lands — 无视无法选择 abilities, direct picks, flying units' blasts, 无来源 damage, the + * ticks of a debuff already on her) and `blockFly` (blocks flyers at the air radius). She stays a ground unit on her tile (PRTS 行动方式 "起飞的 * 干员仍然是地面单位": `unit.ground` unchanged — 隐德来希's 血镰, 地面干员 bonds / items still count her). */ const LIFTOFF_FLAGS = Object.freeze({ blockFly: true, liftoff: true }); @@ -597,7 +598,7 @@ export default { // --------------------------------------------------------------------------------------------------------------- // 2_13 蒂比 紧急赶场通知 (AUTO): "受到攻击后触发" — any incoming enemy attack sets it off (the kit is the only // trigger: the engine rule is disabled so non-attack damage never fires it) and a physical/arts one is dodged; takes - // off for the duration (起飞, LIFTOFF_FLAGS: she blocks no ground enemy and none attacks or damages her): skill range, + // off for the duration (起飞, LIFTOFF_FLAGS: she blocks no ground enemy and none selects her — 对地规避): skill range, // ATK +atk, attacks become 3 shots, blocks flying (not ground) enemies. Trait "起飞后能够阻挡2个飞行敌人": flying enemies // are blocked only while airborne. 片场工作指南: if not attacked for stack_time s, the next physical/arts attack is // dodged (prob); every attack restarts that timer. diff --git a/server/sim/damage.js b/server/sim/damage.js index eabceffe..650adfef 100644 --- a/server/sim/damage.js +++ b/server/sim/damage.js @@ -1,7 +1,7 @@ // server/sim/damage.js — damage & heal pipeline, shields, dodge, element gauges (DESIGN §5.5). // // dealDamage order: (element → gauge path) | invulnerable? / 对地规避 (a ground enemy's damage to an airborne 起飞 ally — -// targeting.js evadesGround; not `ignoreSelect` damage) → 'hit' hook (mutable DamageInfo, may set cancel) +// targeting.js evadesGround; not `ignoreSelect` damage: no selection, e.g. a debuff's tick) → 'hit' hook (mutable DamageInfo, may set cancel) // → dodge (phys/arts, canDodge) → mitigation (phys: DEF, arts: RES, true: none) // → × source dmgDealtMul (× phys/artsDealtMul) × target dmgTakenMul (not for 元素伤害) × type-taken mul × dmg.mul // → 限伤 (leaders in boss / hidden battles: a hit of ceil(final) ≥ BOSS_HIT_LIMIT is cancelled, see leaderHitCancelled) @@ -85,7 +85,8 @@ export function makeDamageInfo(d = {}) { noSp: !!d.noSp, ignoreSleep: !!d.ignoreSleep, // no selection 无法选择 effects stop (an ability that "无视无法选择" such as PRTS 【污染秽蚀】, a direct pick, a flying - // unit's blast credited to a ground leader): reaches an airborne 起飞 ally whatever the source's 行动方式 + // unit's blast credited to a ground leader, the tick of a debuff already on the unit): reaches an airborne 起飞 ally + // whatever the source's 行动方式 ignoreSelect: !!d.ignoreSelect, sourceless: !!d.sourceless, attackId: d.attackId ?? 0, @@ -100,10 +101,11 @@ function sleepBlocks(target, source, dmg) { /** * 起飞 (flag `liftoff`): a ground enemy's damage never reaches the airborne ally — it cannot select it (对地规避, PRTS 作战机制 * "AOE的判定是对攻击范围内的每个可以被选中的敌人进行判定"), so its splash, area abilities and element fills skip it, and a shot - * already in flight or a DoT ticking when it took off lands on nothing [ASSUMED: PRTS 伤害流程 7 "取消掉隐匿/无敌状态下的攻击" - * read for 对地规避]. Checked before the `hit` hook only: 蒂比 S2 takes off inside the hook of the hit that set it off, which - * then resolves as usual (dodged if physical / arts — PRTS 备注). Sourceless damage and `ignoreSelect` damage (no - * selection: 无视无法选择, direct picks, flying units' blasts — targeting.js evadesGround) still land. + * already in flight when it took off lands on nothing [ASSUMED: PRTS 伤害流程 7 "取消掉隐匿/无敌状态下的攻击" read for + * 对地规避]. Checked before the `hit` hook only: 蒂比 S2 takes off inside the hook of the hit that set it off, which then + * resolves as usual (dodged if physical / arts — PRTS 备注). Sourceless damage and `ignoreSelect` damage still land: no + * selection (targeting.js evadesGround) — 无视无法选择 abilities, direct picks, flying units' blasts, and the ticks of a + * debuff already on it (PRTS 异常效果: 无法选择 effects "仅在选择时生效"; the debuff's mods stay too). */ function liftoffEvades(target, source, dmg) { return !!target.s.flags.liftoff && !dmg.ignoreSelect && evadesGround(source, target); diff --git a/server/sim/targeting.js b/server/sim/targeting.js index f72b0f94..ec3f212e 100644 --- a/server/sim/targeting.js +++ b/server/sim/targeting.js @@ -90,9 +90,11 @@ export function canTargetAlly(e, a, ranged) { * pipeline and Battle.applyStatus (a ground enemy's area damage, statuses and hits under way skip it — PRTS 作战机制 "AOE * 的判定是对攻击范围内的每个可以被选中的敌人进行判定"), and the content picks and one-shot areas that bypass canTargetAlly * (chain / bounce jumps, 周围四格 additions, shells, barrages, 沙狱, death blasts). Not selections, so they still reach it: - * sourceless damage and DamageInfo / applyStatus `ignoreSelect` — abilities that "无视无法选择" (【污染秽蚀】, 【盲信之誓】), - * direct picks (碎铳之簧's counter on its attacker: PRTS 异常效果 "'直接选中'的能力…不受这些仅在选择时生效的异常效果制约") - * and the blasts of flying units credited to a ground leader (刺胄之弹, 斩胄之剑 / 破胄之锤). + * sourceless damage and DamageInfo / applyStatus `ignoreSelect` — abilities that "无视无法选择" (【污染秽蚀】, 【盲信之誓】, + * 萨卡兹悖谬暴虐兵长's 暴击 splash), direct picks (碎铳之簧's counter on its attacker: PRTS 异常效果 "'直接选中'的能力…不受这些 + * 仅在选择时生效的异常效果制约"), the blasts of flying units credited to a ground leader (刺胄之弹, 斩胄之剑 / 破胄之锤) and + * the ticks of a debuff a ground enemy put on it before it took off (出血, 沙狱, burning DoTs, 【自然涌动】: a tick selects + * nobody). A shot already in flight is refused [ASSUMED] (damage.js). */ export function evadesGround(src, a) { return !!(a && a.s && a.s.flags.liftoff && src && src.side === 'enemy' && !src.isFlying); diff --git a/test/sim/feedback1f-liftoff.test.js b/test/sim/feedback1f-liftoff.test.js index aa546bbf..e1fdf8d7 100644 --- a/test/sim/feedback1f-liftoff.test.js +++ b/test/sim/feedback1f-liftoff.test.js @@ -9,14 +9,15 @@ // the targets its abilities add (控潮术师's 周围四格, chain / bounce jumps) and its area damage and statuses skip her; // - flyers, 近地悬浮 and 浮空 enemies still select her (she stays a ground unit: no 对空 check); // - what is no selection still reaches her: sourceless damage, abilities that "无视无法选择" (【污染秽蚀】 at the -// low-ground rate — she is still on her low tile; 【盲信之誓】), direct picks (碎铳之簧's counter on its attacker — -// PRTS 异常效果 "'直接选中'的能力…不受这些仅在选择时生效的异常效果制约") and flying units' blasts (刺胄之弹, 斩胄之剑); +// low-ground rate — she is still on her low tile; 【盲信之誓】; 萨卡兹悖谬暴虐兵长's 暴击 splash), direct picks +// (碎铳之簧's counter on its attacker — PRTS 异常效果 "'直接选中'的能力…不受这些仅在选择时生效的异常效果制约"), flying +// units' blasts (刺胄之弹, 斩胄之剑) and the ticks of a debuff put on her before she took off (出血, 沙狱, burning); // a ground enemy's zone does not (集团军重型火炮 【燃烧区域】 "碰撞不受迷彩制约,不可对空": 迷彩 only); // - she stays a 地面单位 for ally rules (隐德来希 S2 puts a 血镰 on her; PRTS 备注 "被添加血镰的单位处于起飞时,血镰可对空"). // Cause: 起飞 was `unit.ground = false` (an operator on a high tile): melee enemies could not reach her (she released them), // but every ranged ground enemy kept shooting her and the AoE / skills of ground enemies hit her. -// [ASSUMED] a ground enemy's damage already under way (a shot in flight, a DoT ticking) is cancelled too (PRTS 作战机制 伤害 -// 流程 7 "取消掉隐匿/无敌状态下的攻击", read for 对地规避); auras of ground enemies (光环, field-wide debuffs) still apply. +// [ASSUMED] a ground enemy's shot already in flight is cancelled too (PRTS 作战机制 伤害流程 7 "取消掉隐匿/无敌状态下的攻击", +// read for 对地规避); auras of ground enemies (光环, field-wide debuffs) still apply. import { test } from 'node:test'; import assert from 'node:assert/strict'; @@ -358,3 +359,65 @@ test('F3 review: one-shot area abilities of ground enemies skip an airborne 蒂 done(h); } }); + +// --------------------------------------------------------------------------------------------------------------- +// Second review round: 萨卡兹悖谬暴虐兵长's 暴击 splash ("无视无法选择") and the ticks of debuffs already on her. + +const WDRRL = 'enemy_1320_wdrrl_2'; // 萨卡兹悖谬暴虐兵长 (ground, block ≥ 3): 暴击 "对目标和周围4格…(无视无法选择,无视迷彩)" +const NHSTLK = 'enemy_1270_nhstlk'; // 逐腐兽 (ground melee): 出血 on hit — arts per second +const LSLIME = 'enemy_1050_lslime'; // “庞贝” (ground): its hits leave a burning DoT + +test('F3 review 2: 萨卡兹悖谬暴虐兵长\'s 暴击 splash ("无视无法选择") reaches an airborne 蒂比 next to its blocker', REAL, () => { + const h = makeBattle({ + stageId: 'act2autochess_m02', seed: 3, autoFinish: false, timeLimit: 400, hooks: ['damaged'], captureNoisy: true, + units: [{ chessId: TIPPI[0], row: 9, col: 5, carryState: { sp: 999 }, skillIndex: skillIndex(TIPPI[0], S1) }, { chessId: BAIT, row: 9, col: 4 }], + enemies: [{ key: WDRRL, route: { motion: 'WALK', start: [9, 8], end: [9, 1], checkpoints: [] }, mods: { hpMul: 1e3, atkMul: 0.3 } }], + }); + const u = h.unit(TIPPI[0]), bait = h.unit(BAIT); + let e = null, airAtBlock = null; + for (let i = 0; i < 14 * 30; i++) { // it reaches 角峰 at ≈ 9 s, S1 lasts 32 s + h.step(); + e ??= h.b.enemies.find((x) => x.defId === WDRRL) ?? null; + if (e && airAtBlock == null && e.blockedBy) airAtBlock = !!u.s.flags.liftoff; + } + assert.ok(e && e.blockedBy === bait, '角峰 (block 3) blocks it; she cannot (block < 3)'); + assert.equal(airAtBlock, true, 'she is airborne when it reaches 角峰'); + const splash = h.hooksOf('damaged').filter((c) => c.target === u && c.source === e); + assert.equal(splash.length, 1, `its one-off splash lands on her: ${splash.map((c) => Math.round(c.amount))}`); + assert.ok(splash[0].amount > 0 && !splash[0].dmg.isAttack && splash[0].dmg.ignoreSelect); + assert.ok(u.s.flags.liftoff, 'while airborne'); + assert.ok(h.hooksOf('damaged').some((c) => c.target === bait && c.source === e && c.dmg.isAttack), 'its attack itself is on 角峰'); + done(h); +}); + +test('F3 review 2: a debuff a ground enemy put on 蒂比 before she took off keeps ticking (no selection); its new attacks still skip her', REAL, () => { + // 鼠王 【沙狱】 (centred near 角峰 and her), 逐腐兽's 出血, “庞贝”'s burning DoT: attached on the ground, then S1 + const cases = [ + { key: MOUSEK, pos: [9, 8], buff: 'ab:sandStorm', bait: true, fire: (h, m) => { m.mem.ab.list.find((a) => a && a.cd === m.mem.ab.sk.SandStorm.cd && a.fire).left = 0.05; } }, + { key: NHSTLK, pos: [9, 5.3], buff: 'ab:bleed' }, + { key: LSLIME, pos: [9, 7], buff: 'ab:burnDot' }, + ]; + for (const k of cases) { + const units = [{ chessId: TIPPI[0], row: 9, col: 5, carryState: { sp: 0 }, skillIndex: skillIndex(TIPPI[0], S1) }]; + if (k.bait) units.push({ chessId: BAIT, row: 10, col: 5 }); + const h = makeBattle({ + stageId: 'act2autochess_m02', seed: 3, autoFinish: false, timeLimit: 400, hooks: ['damaged'], captureNoisy: true, units, + enemies: [{ key: k.key, pos: k.pos, mods: { hpMul: 1e4, speedMul: 0, atkMul: k.bait ? 0.01 : 0.05 } }, { key: LAZERD, pos: [9, 7.2], mods: { hpMul: 1e4, speedMul: 0, atkMul: 0 } }], + }); + const u = h.unit(TIPPI[0]); + h.step(); + const e = h.b.enemies.find((x) => x.defId === k.key); + k.fire?.(h, e); + assert.ok(h.runUntil(() => u.findBuff(k.buff), 10), `${k.key}: ${k.buff} on her`); + assert.ok(!u.s.flags.liftoff, `${k.key}: before she takes off`); + u.skill.gainSp(999, 'init'); + assert.ok(h.runUntil(() => u.s.flags.liftoff, 3), `${k.key}: takes off`); + const up = h.b.time; + h.run(4); + assert.ok(u.s.flags.liftoff && u.findBuff(k.buff), `${k.key}: still airborne with the debuff`); + const air = h.hooksOf('damaged').filter((c) => c.target === u && c.source === e && tAt(c) > up + 1e-6); + assert.ok(air.filter((c) => !c.dmg.isAttack).length >= 3, `${k.key}: its ticks land while airborne: ${air.length}`); + assert.deepEqual(air.filter((c) => c.dmg.isAttack).map((c) => Math.round(c.amount)), [], `${k.key}: no new attack on her`); + done(h); + } +});