diff --git a/server/sim/content/enemies.js b/server/sim/content/enemies.js index eb058579..aa105c54 100644 --- a/server/sim/content/enemies.js +++ b/server/sim/content/enemies.js @@ -37,17 +37,21 @@ // 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 -// or sleeping one, no airborne 起飞 one for a ground enemy — 迷彩 does not protect from them (ba.camou "无法躲避溅射类 -// 攻击"). Until 0.1.2 they took every ally in the area (GitHub issue #32 item 6; DESIGN §22.12). An airborne ally +// 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 +// target (an attack's, a channel's, a C4's) is hit as a direct pick, only the others around it as an area +// (targetAndArea). Until 0.1.2 they took every ally in the area (GitHub issue #32 item 6; DESIGN §22.12). An airborne ally // (起飞, flag `liftoff`) is no target of a ground enemy (对地规避, targeting.js evadesGround): targetsNear / allTargets // skip it through canTargetAlly, the area selectors through areaSelectable, and the engine refuses a ground enemy's // damage and statuses on it. Not selections, so they still reach it (`ignoreSelect`): abilities PRTS marks "无视无法 // 选择 / 无视(目标)可选性" (【污染秽蚀】, 【盲信之誓】, 萨卡兹悖谬暴虐兵长's 暴击 splash, 假想敌:淤困's burst spread), // 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). The buff auras of enemies -// (寒霜 — PRTS: its debuff ignores 隐匿 —, 深池伙友卫队, 扎罗's 远古威慑), map / tile effects (“墓碑”, 【国度】, the -// chimera's 源石污染区) and the sourceless 毒雾 of 假想敌:蚀裂 still reach every ally [ASSUMED for all but 寒霜 and “墓碑”]. +// already on it (出血, 沙狱, “庞贝”'s burning, 淤困, 【自然涌动】 — a tick selects nobody). Ground enemies' buff auras +// (深池伙友卫队, 扎罗's 远古威慑) and the sourceless 毒雾 of 假想敌:蚀裂 still reach it [ASSUMED, §21.22] while keeping +// a 隐匿 ally out; 寒霜's aura (PRTS: its debuff "无视隐匿状态起作用") and map / tile effects (“墓碑” — PRTS: a map effect —, +// 【国度】, the chimera's 源石污染区 [ASSUMED]) reach every ally. // // Special types (factions.json): // FLY — engine (FLY motion, ranged-only targeting). Flyer kits below (御4, 护障, 寒霜, 萨科塔之翼/眼, 黑云 …). @@ -60,8 +64,8 @@ // 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 -// 毒雾 reaches everyone inside), bleeding (removed by healing), pulsing damage around an enemy (area -// selection). +// 毒雾 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 // damage skips an unblocked one too (Battle.foesInRadius: profession splash around a struck target, // skill circles — PRTS 作战机制 §AOE伤害判定 "对攻击范围内的每个可以被选中的敌人进行判定"; until 0.1.1 it @@ -87,7 +91,7 @@ // Custom hook: 'lpLoss' {amount, reason, source} — leader "扣除目标生命" effects; also summed into result.lpLoss. import { TICK, MOVE_SCALE, ELEMENT, ATTACK_PAUSE, PROJECTILE_SPEEDS, ALLY_COLLIDER_RADIUS } from '../constants.js'; -import { canTargetAlly, sortAllyTargets, aggroCmp, areaSelectable } from '../targeting.js'; +import { canTargetAlly, sortAllyTargets, aggroCmp, areaSelectable, auraSelectable } from '../targeting.js'; import { mitigate, periodicDamage } from '../damage.js'; // --------------------------------------------------------------------------------------------------------------- @@ -440,13 +444,31 @@ export function fieldAllies(b, src) { return b.allies().filter((a) => areaSelectable(src, a)); } +/** + * The allies within `r` of (x, y) a buff aura of enemy `src` takes (targeting.js auraSelectable: no unblocking 隐匿, + * 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). + */ +export function auraAllies(b, src, x, y, r) { + return b.alliesInRadius(x, y, r).filter((a) => auraSelectable(src, a)); +} + +/** + * A locked target `t` and the allies an area takes around it: the target was picked directly when the ability started + * (an attack's target, a channel's or a C4's lock), so a 隐匿 it gains meanwhile does not save it — PRTS 异常效果 "'直接选中' + * 的能力不会进行具体的目标选择,故同样不受这些仅在选择时生效的异常效果制约"; only the others are an area selection (`area`). + */ +export function targetAndArea(t, area) { + return t && t.alive && t.deployed ? [t, ...area.filter((u) => u !== t)] : area; +} + /** Engine priority (blocker → taunt → latest deployed) on a copy. */ export function byPriority(e, list) { return sortAllyTargets(e, list.slice()); } /** * An HP-loss / damage zone. `tick(units)` runs every `iv` s for `life` s on the allies inside: `pick(x, y, r)` chooses - * them — by default every ally in the circle (【污染秽蚀】 "无视无法选择", a sourceless 毒雾); a zone an enemy's ability - * leaves passes its area selection (areaAllies). + * them — by default every ally in the circle (【污染秽蚀】 "无视无法选择"); the other zones (dmgZone, 烹泉's steam) pass + * their area selection (areaAllies). */ export function zone(b, { x, y, r, life, iv = 1, kind = 'zone', tick, pick = null }) { b.fx('zone', { x, y, r, dur: life, kind }); @@ -1052,14 +1074,14 @@ function pollution(b, src, x, y, r, life, low, high) { } /** - * Damage zone (`amount` per tick, tagged `kind`). Unlike 【污染秽蚀】 it does not ignore 无法选择: an enemy's zone ticks on - * the allies its area selection takes (areaAllies: no unblocking 隐匿 ally, no airborne 起飞 one for a ground enemy — - * PRTS 集团军重型火炮 【燃烧区域】 "碰撞不受迷彩制约,不可对空": 迷彩 only). A sourceless zone (`src` null: 假想敌:蚀裂's - * 毒雾) reaches everyone inside [ASSUMED: PRTS 作战机制 "可以受到无来源的毒雾伤害" names the stage hazard; PRTS 假想敌:蚀裂 - * gives its cloud no 无视可选性 either]. + * Damage zone (`amount` per tick, tagged `kind`). Unlike 【污染秽蚀】 it does not ignore 无法选择: it ticks on the allies + * its area selection takes (areaAllies: no unblocking 隐匿, untargetable or sleeping ally; no airborne 起飞 one for a ground + * enemy — PRTS 集团军重型火炮 【燃烧区域】 "碰撞不受迷彩制约,不可对空": 迷彩 only). A sourceless zone (`src` null: 假想敌:蚀裂's + * 毒雾, an enemy's 死亡爆炸 with no 无视可选性 note on PRTS) selects as an effect with no selecting enemy: 隐匿 kept out, an + * airborne 起飞 ally reached (§21.22; until 0.1.2 it took 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, pick: src ? (zx, zy, zr) => areaAllies(b, src, zx, zy, zr) : null, tick(units) { + zone(b, { x, y, r, life, iv, kind, pick: (zx, zy, zr) => areaAllies(b, src, zx, zy, zr), tick(units) { for (const u of units) { hurt(b, src, u, amount, type, { tags: [kind] }); if (el && elAmount > 0) elem(b, src, u, el, elAmount, { tags: [kind] }); @@ -1348,7 +1370,8 @@ function kitDeathEye(ab, e) { if (++n >= dur) { h.cancel(); b.fx('explode', { x: t.x, y: t.y, r: 1, kind: 'apoptosis' }); - for (const u of areaAllies(b, e2, t.x, t.y, 1)) elem(b, e2, u, 'apoptosis', e2.s.atk * (s.bb.ep_damage_ratio ?? 0)); + // "对目标及其周围4格的我方单位": the locked target (a 隐匿 it gained mid-channel does not save it) + an area around it + for (const u of targetAndArea(t, areaAllies(b, e2, t.x, t.y, 1))) elem(b, e2, u, 'apoptosis', e2.s.atk * (s.bb.ep_damage_ratio ?? 0)); } }, { owner: e2 }); }, { sil: true, cond: (b, e2) => targetsNear(b, e2, r).length > 0 })]; @@ -1442,9 +1465,9 @@ 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: every ally in it, 隐匿 / airborne ones included [ASSUMED — PRTS 作战机制 says a Buff selector skips - // 隐匿 unless it ignores it; kept as §21.22 kept ground-enemy auras for 起飞, DESIGN §22.12] - for (const u of b.alliesInRadius(e.x, e.y, r)) auraBuff(b, u, 'ab:forceField', 0.5, { aspd }, null, true); + // a buff aura (auraAllies): no 隐匿 operator that does not block it (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); }, }]; } @@ -1774,7 +1797,7 @@ function kitFlameVine(ab) { // ends and later attacks add 20 % a.first = false; b.fx('explode', { x: t.x, y: t.y, r: 1.5, kind: 'flameVine' }); - for (const u of areaAlliesInTiles(b, e, t.tileR, t.tileC, 'box', 1)) { if (u !== t) hurt(b, e, u, e.s.atk, 'arts'); elem(b, e, u, 'burn', e.s.atk * first); } + for (const u of targetAndArea(t, areaAlliesInTiles(b, e, t.tileR, t.tileC, 'box', 1))) { if (u !== t) hurt(b, e, u, e.s.atk, 'arts'); elem(b, e, u, 'burn', e.s.atk * first); } b.removeBuff(e, 'ab:pow'); } else elem(b, e, t, 'burn', e.s.atk * normal); }, @@ -1854,7 +1877,7 @@ function kitLeaderMisc(key, ab, e) { dealt(c, b, e2) { if (e2.blockedBy) return; // 未被阻挡时会发射榴弹 // the grenade's splash: an area selection — a 隐匿 ally next to the target is spared (GitHub issue #32 item 6) - for (const u of areaAllies(b, e2, c.target.x, c.target.y, 1)) { + for (const u of targetAndArea(c.target, areaAllies(b, e2, c.target.x, c.target.y, 1))) { if (u !== c.target) hurt(b, e2, u, e2.s.atk, 'phys'); b.applyStatus(u, 'defDown', { duration: SKULSR_DEFDOWN_DUR, source: e2, value: -(T(ab, 'defdown.def') ?? 0) }); } @@ -1872,7 +1895,8 @@ function kitLeaderMisc(key, ab, e) { for (const t of l.slice(0, n)) { const x = t.x, y = t.y; b.fx('telegraph', { x, y, r: C4_RADIUS, dur: C4_FUSE, kind: 'c4' }); - b.after(C4_FUSE, () => { b.fx('explode', { x, y, r: C4_RADIUS, kind: 'c4' }); for (const u of areaAllies(b, e2, x, y, C4_RADIUS)) hurt(b, e2, u, e2.s.atk * (s.bb.atk_scale ?? 1), 'phys'); }); + // the C4 sits on its target (PRTS "在…1个非飞行的我方单位身上安装C4"): it is hit; the blast around it is an area + b.after(C4_FUSE, () => { b.fx('explode', { x, y, r: C4_RADIUS, kind: 'c4' }); for (const u of targetAndArea(t, areaAllies(b, e2, x, y, C4_RADIUS))) hurt(b, e2, u, e2.s.atk * (s.bb.atk_scale ?? 1), 'phys'); }); } }, { cond: (b, e2) => targetsNear(b, e2, s.bb.range_radius ?? e2.base.rangeRadius).length > 0 })]; } @@ -1943,7 +1967,8 @@ function kitLeaderMisc(key, ab, e) { b.after(delay, () => { if (!e2.alive) return; b.fx('explode', { x: cc, y: r, r: 1, kind: 'blastArrow', tiles: 'plus' }); - for (const u of areaAlliesInTiles(b, e2, r, cc, 'plus', 1)) hurt(b, e2, u, e2.s.atk * scale, 'arts'); + // "对主目标造成法术普通伤害,对溅射目标造成法术溅射伤害": the arrow's target + the 周围四格 (an area) + for (const u of targetAndArea(t, areaAlliesInTiles(b, e2, r, cc, 'plus', 1))) hurt(b, e2, u, e2.s.atk * scale, 'arts'); }, { owner: e2 }); } }, { cond: (b, e2) => cands(b, e2).length > 0 })]; @@ -2306,8 +2331,8 @@ function kitWolfLord(ab) { during(b, e, a, t) { // 【远古威慑】: nearby operators slowed; its HP refills slowly const dur = T(ab, 'Reborn.duration') ?? 1; e.hp = Math.max(1, e.s.maxHp * Math.min(1, t / Math.max(TICK, dur))); - // a buff aura: every ally in it, 隐匿 / airborne ones included [ASSUMED, as 深池伙友卫队's — DESIGN §22.12] - for (const u of b.alliesInRadius(e.x, e.y, WOLF_AWE_RADIUS)) auraBuff(b, u, 'ab:ancientAwe', 0.2, { aspd: WOLF_AWE_ASPD }, null, true); + // a buff aura (auraAllies): no 隐匿 operator that does not block it, an airborne 起飞 one still — as 深池伙友卫队's + 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); }, onReborn(b, e) { P.form2 = true; @@ -2520,6 +2545,8 @@ export const KITS = Object.freeze({ if (c.reason !== 'killed') return; const k = c.killer && c.killer.side === 'ally' && c.killer.alive ? c.killer : null; const atk = e.s.atk; + // a sourceless zone (`src` null): its ticks skip a 隐匿 / untargetable / sleeping operator (PRTS 假想敌:蚀裂: no 无视 + // 可选性 note — PRTS 作战机制's "无来源的毒雾" that reaches 隐匿 units is the stage hazard), not an airborne 起飞 one const cloud = (x, y) => dmgZone(b, null, x, y, T(ab, '1.projectile_range') ?? 0.8, T(ab, '1.projectile_life_time') ?? 0, T(ab, '1.interval') ?? 1, atk * (T(ab, '1.damage_atk_scale') ?? 0), 'arts', 'poison'); if (k) b.addProjectile({ from: e, target: k, speed: 8, visual: 'lob', source: null, hitDead: true, onHit: (h) => cloud(h.x ?? k.x, h.y ?? k.y) }); else cloud(e.x, e.y); diff --git a/server/sim/targeting.js b/server/sim/targeting.js index 9917279e..2e7f805e 100644 --- a/server/sim/targeting.js +++ b/server/sim/targeting.js @@ -11,7 +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 (no unblocking 隐匿 ally; 迷彩 is hit — DESIGN §22.12). +// An enemy's area effects select allies with areaSelectable, its buff auras with auraSelectable (no unblocking 隐匿 +// ally; 迷彩 is not checked — DESIGN §22.12). import { COLS, ROWS } from './constants.js'; import { normDir, rotateOffset } from './dir.js'; @@ -74,8 +75,9 @@ export function canTargetEnemy(attacker, e, profile) { * "隐匿与'阻挡时解除'没有直接关系"). Devices are never targets (阻隔工事: obstacles nobody can select; “双眼皮”: 迷彩 and * off the enemy paths — PRTS). 迷彩 (flag `camou`, term ba.camou "不阻挡时不成为敌方普通攻击的目标") works the same way: * PRTS 异常效果 gives both anomalies the note "与'阻挡时解除'没有直接关系" — for this target selection (an attack, a skill - * pick, a cast condition: PRTS 选择器 "所有触发选择器通常不无视迷彩"); an area effect selects with areaSelectable, where - * 迷彩 no longer protects. An airborne ally (起飞, flag `liftoff`) is never a target of a ground enemy (evadesGround). + * 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). */ export function canTargetAlly(e, a, ranged) { if (!a.alive || !a.deployed || a.hidden || a.kind === 'device') return false; @@ -90,26 +92,40 @@ export function canTargetAlly(e, a, ranged) { * May an AREA effect of enemy-side `src` select ally `a` — a splash, a blast, an area skill or status, a pulse, a zone it * leaves, a chain / bounce jump, a 周围四格 addition (content/enemies.js areaAllies / areaAlliesInTiles / fieldAllies)? * PRTS 作战机制 §AOE伤害判定 "AOE的判定是对攻击范围内的每个可以被选中的敌人进行判定"; §隐匿 "隐匿效果使得获得该效果的单位无法被 - * 任何敌方的能力索敌选中"; PRTS 选择器 (可选判定): a selector checks 无法选择 by default — "站内所记载的大部分“无视可选性”指 - * 的正是不进行这次可选判定"; PRTS 异常效果 §无法选择 = 隐匿, 不可选中, 无敌 (沉睡 is 无敌), 对地规避. So not: + * 任何敌方的能力索敌选中"; 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; - * - an untargetable (不可选中) or sleeping (沉睡: 无敌) ally; + * - an untargetable (不可选中) or sleeping (沉睡 = 无敌 + 无法行动) ally; * - an airborne 起飞 ally when `src` walks (evadesGround — the same refusal the damage pipeline makes). - * 迷彩 (flag `camou`) does not protect: gamedata_const ba.camou "(无法躲避溅射类攻击)"; PRTS 异常效果 迷彩 "所有光环类能力、以及 - * 涉及中点判定/格子判定的效果均不受迷彩制约"; PRTS 选择器 "非弹道类型的溅射攻击会自动无视迷彩". `src` null (an effect - * with no selecting enemy): 隐匿 still applies, 对地规避 not. Abilities PRTS marks "无视无法选择 / 无视(目标)可选性" never - * call this (【污染秽蚀】, 【盲信之誓】, 萨卡兹悖谬暴虐兵长's 暴击, 假想敌:淤困's burst spread, 远眺's 暴露 — DamageInfo - * `ignoreSelect`); neither do map / terrain effects nor the buff auras kept as they were (DESIGN §22.12). + * An invulnerable (`invulnerable`, 无敌) ally is still selected, as by canTargetAlly — a known deviation from 无法选择 + * [ASSUMED, DESIGN §21.22] (its damage is 0 anyway). 迷彩 (flag `camou`) is not checked: the area effects that call this + * are splash-type, 中点判定 / 格子判定, auras or carry a PRTS "无视迷彩" note (gamedata_const ba.camou "(无法躲避溅射类 + * 攻击)"; PRTS 异常效果 迷彩 "所有光环类能力、以及涉及中点判定/格子判定的效果均不受迷彩制约"; PRTS 选择器 "非弹道类型的溅射 + * 攻击会自动无视迷彩") — the few without any of these are listed [ASSUMED] in DESIGN §22.12. `src` null (an effect with no + * selecting enemy): 隐匿 still applies, 对地规避 not. Abilities PRTS marks "无视无法选择 / 无视(目标)可选性" never call + * this (【污染秽蚀】, 【盲信之誓】, 萨卡兹悖谬暴虐兵长's 暴击, 假想敌:淤困's burst spread, 远眺's 暴露 — DamageInfo + * `ignoreSelect`); neither do map / terrain effects nor 寒霜's aura. Enemy buff auras use auraSelectable. */ export function areaSelectable(src, a) { + return auraSelectable(src, a) && !(a.s.flags.liftoff && evadesGround(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 + * 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; - if (f.stealth && !(src && src.alive && src.blockedBy === a)) return false; - if (f.liftoff && evadesGround(src, a)) return false; - return true; + return !(f.stealth && !(src && src.alive && src.blockedBy === a)); } /**