feedback2: merge WJ — enemy splash, blasts, area skills, zones and auras skip a 隐匿 operator that does not block them; 迷彩 is still hit by splash (GitHub #32 item 6)
# Conflicts: # docs/DESIGN.md # docs/SIM.md # server/sim/content/enemies.js
This commit is contained in:
commit
a168c99e6a
9 files changed
+674
-130
No files matched your search
+2
-2
@@ -229,7 +229,7 @@ Base stats (from data): `maxHp, atk, def, res` (0–100), `aspd` (100 base), `ba
|
||||
`Buff = { key, source, duration /*s, Infinity*/, stacks=1, maxStacks=1, refresh: 'replace'|'extend'|'stack', mods, flags, onTick?, onExpire?, tags[] }`
|
||||
|
||||
- `mods` keys (all optional, additive within a key): `atkFlat, atkPct, atkMul, defFlat, defPct, defMul, hpFlat, hpPct, hpMul, resFlat, resMul, aspd, batPct, blockCnt, rangeExtend (tiles), moveMul, dmgDealtMul (multiplicative, e.g. 1.2), dmgTakenMul, physTakenMul, artsTakenMul, trueTakenMul, defIgnoreFlat, defIgnorePct, resIgnoreFlat, resIgnorePct, dodgePhys, dodgeArts, healingDealtMul, healingTakenMul, spRecoveryFlat, spRecoveryMul, maxTargets (+n), shield (absorbs damage; consumed)`.
|
||||
- `flags`: `stun, freeze, sleep, silence (no skills), disarm (no attacks), stealth (untargetable by enemies/ops unless blocked or revealed — an operator's radius area damage skips it too, `Battle.foesInRadius`, PRTS 作战机制 §AOE伤害判定, §21.19; an enemy's 隐匿 returns only 3 s after a block ends, or after its PRTS page's "(解除阻挡N秒后恢复)" — `stealthOff` buffs, `targeting.js enemyStealthed`, §22.8; an enemy's b.snap stealth bit is set only while its 隐匿 is on, §21.4), camou (an ally's 迷彩: like stealth for enemy targeting and on screen, but not 隐匿 for 隐匿 conditions — 叙拉古, 家族徽章; §20.2), liftoff (an ally's 起飞, 蒂比's skills: it blocks flyers only and has 对地规避 — no ground enemy selects it, so no selected damage or status of one lands, while what selects nobody still does: `ignoreSelect` / 无来源 — 无视无法选择 abilities, direct picks, flying units' blasts, a debuff's ticks; it stays a ground unit on its tile; `targeting.js evadesGround`, §21.22), invulnerable, unblockable, levitate, fear (enemy: cannot attack, unblockable, runs to random tiles of the ±45° fan away from the source; no source or itself as source = inside its own tile — `fear.js`, §20.4), taunt (+aggro), cold, noHeal (no heals from others: 禁疗 summons and 孤立 units; an HP-regen attribute still applies — PRTS 安洁莉娜 备注), healFree (禁疗 that stops the unit's own heals too, shown as the status 'healFree': 史尔特尔's 余烬 sets it with noHeal — only an HP-regen attribute and her S3's "无视禁疗" start heal pass, §22.7), isolated (孤立: no friendly selector picks it — no heal, buff, aura or talent of an ally, `Battle.allySelectable`; §20.3)`.
|
||||
- `flags`: `stun, freeze, sleep, silence (no skills), disarm (no attacks), stealth (untargetable by enemies/ops unless blocked or revealed — an operator's radius area damage skips it too, `Battle.foesInRadius`, PRTS 作战机制 §AOE伤害判定, §21.19, and so do an enemy's area effects and buff auras unless the 隐匿 ally blocks that enemy, `targeting.js areaSelectable` / `auraSelectable`, §22.12; an enemy's 隐匿 returns only 3 s after a block ends, or after its PRTS page's "(解除阻挡N秒后恢复)" — `stealthOff` buffs, `targeting.js enemyStealthed`, §22.8; an enemy's b.snap stealth bit is set only while its 隐匿 is on, §21.4), camou (an ally's 迷彩: like stealth for enemy targeting and on screen, but not 隐匿 for 隐匿 conditions — 叙拉古, 家族徽章; §20.2 — and an enemy's splash and other area effects still hit it, ba.camou "无法躲避溅射类攻击", §22.12), liftoff (an ally's 起飞, 蒂比's skills: it blocks flyers only and has 对地规避 — no ground enemy selects it, so no selected damage or status of one lands, while what selects nobody still does: `ignoreSelect` / 无来源 — 无视无法选择 abilities, direct picks, flying units' blasts, a debuff's ticks; it stays a ground unit on its tile; `targeting.js evadesGround`, §21.22), invulnerable, unblockable, levitate, fear (enemy: cannot attack, unblockable, runs to random tiles of the ±45° fan away from the source; no source or itself as source = inside its own tile — `fear.js`, §20.4), taunt (+aggro), cold, noHeal (no heals from others: 禁疗 summons and 孤立 units; an HP-regen attribute still applies — PRTS 安洁莉娜 备注), healFree (禁疗 that stops the unit's own heals too, shown as the status 'healFree': 史尔特尔's 余烬 sets it with noHeal — only an HP-regen attribute and her S3's "无视禁疗" start heal pass, §22.7), isolated (孤立: no friendly selector picks it — no heal, buff, aura or talent of an ally, `Battle.allySelectable`; §20.3)`.
|
||||
- **Status catalogue** (`buffs.js`, keys used by content): `stun`, `freeze` (stun + res −15), `cold` (aspd −30; a second cold while cold ⇒ freeze 3 s unless immune), `sleep` (stun; ends on damage? no: in AK sleep = untargetable & unable to act; keep AK: untargetable & inactive), `slow` (moveMul), `bind` (moveMul 0), `fragile` (dmgTakenMul), `artsFragile` (artsTakenMul), `silence`, `fear` (the 恐惧 walk, `fear.js`), `burnGauge`/`necrosis`/`neural` element gauges (see damage), `stealth`, `camou` (迷彩, its own keys: 忍冬 S3 `vulpis:camou` until her next cast, 寒芒克洛丝 S1), `reveal`.
|
||||
- Immunities from enemy data (`stunImmune`, `silenceImmune`, `sleepImmune`, `frozenImmune`, `levitateImmune`) are honoured by `applyStatus`; a unit both `invulnerable` and `untargetable` (a 重生, a 永久无敌 leader part) takes no status from the other side at all (PRTS 无敌 "无法被不同阵营选中", §21.19). Flag `selfBound` = 自缚 (守墓石像's 转换模式, the 自缚 leaders) beside its `noMove`: 余 S2 never teleports such a unit (束缚 sets `noMove` only).
|
||||
- Same-named debuffs on an enemy (奥术's arts damage taken, 灵知 坚冰, the 莱恩哈特 / 缄默德克萨斯 RES cuts): one instance per target whoever applies it, the strongest wins (`Battle.applyStrongest`; PRTS 作战机制 "同名buff…只能表现出一个" — strongest rather than latest [ASSUMED], §20.10). PRTS 盟约记录's current 奥术 note fits this under the default overlap; an equal re-hit refreshes the instance (an earlier PRTS wording forbade re-applying while one's own lasts) [ASSUMED, slightly stronger] (§21.7).
|
||||
@@ -270,7 +270,7 @@ Engine helpers (content must use these, never touch internals): `battle.dealDama
|
||||
- **Blocking** (contact radius, PRTS 游戏数据基础 §阻挡半径 / 作战机制 "中点判定…案例:阻挡"; `constants.js BLOCK_RADIUS`, `Battle._checkBlock`, §19.2): an operator with free capacity blocks an unblocked, blockable enemy whose position lies within its block radius of its centre — ground 0.7071 (squared 0.49999037), 0.8944 for blockFly units vs flyers (起飞), devices 0.4472 (阻隔工事 / 障碍物); air units (FLY, 近地悬浮, 浮空) only by blockFly units. A ground enemy is never blocked by a unit standing on a tile ground units cannot pass (围墙 / 围栏: PRTS 地形机制 "部署在其中的单位,若当前阻挡类型为'地面阻挡'则无法阻挡敌人" — an enemy pushed or pulled against the fence walks on; air blocking there stays [ASSUMED], §21.23) nor by an airborne (起飞) unit, which blocks flyers only (§21.22). The nearest such operator wins (ties: scan order [ASSUMED]). It is re-checked every tick, moving or not, so another operator in contact with room takes the enemy over when its blocker dies, is withdrawn or is stunned, and an operator whose capacity frees up grabs an overlapping enemy; one that finds no room walks on. A head-on enemy stops at contact (≈ 0.71 tile), outside the blocker's tile. Enemy uses `blockCnt` capacity (default 1). Blocked enemies stop moving and attack their blocker if melee (or anyone in range if ranged, blocker first). **Every blocker** — melee units (重装要塞 / 领主 / 哨戒铁卫 included), summons and a ranged operator on a melee tile alike — whose attack hits enemies may always target the enemies it blocks, in range or not, whatever its facing, and targets them first ("可以选择且优先选择阻挡单位", PRTS 选择器; the user's rule after playtest #6, "阻挡了就一定要能打到": officially the collision pushes a blocked enemy to its blocker's front; `Battle.blockedTargets`, `targeting.js sortEnemyTargets`; §20.3 — it replaces the playtest #5 QA's melee-only restriction of §19.2). A healer (dmgType `heal`, or a skill attack turned into a heal) selects injured allies only and keeps healing while it blocks (PRTS 卫戍协议/帮助 "对于医疗干员(咒愈师分支除外),攻击目标为需要治疗的单位"; §21.17); 咒愈师 follow the blocker rule. The huge leaders are 自缚 + unblockable (§7).
|
||||
- **Operator attacks**: every `interval` s if a valid target exists; targets = up to `maxTargets` (default 1) from `unitsInGrid(rangeGrid)` (a `rangeAoe` profile — 阵法术师, 轰击术师 — takes every selectable enemy in range, §21.18), priority: (1) the enemies it blocks (every blocker, in range or not, §20.3), (2) profession/trait priorities (e.g. fly-first snipers, `trait` text parsed by build-data into `targetPriority`), (3) enemy with the **least remaining path distance** to its goal, ties → earliest spawned. Melee (no ranged trait) and ground-only attacks cannot hit air units (`Unit.isFlying`: FLY, or a 近地悬浮 / 浮空 enemy — PRTS "算作空中单位", §19.1). Stealthed enemies can't be targeted unless blocked or revealed — or within 3 s of a block's end (§22.8). Ranged ops fire projectiles (speed ~12 tiles/s, instant for melee/arts-beam classes as data indicates); damage applies on impact; a 锁定攻击范围 AoE (`rangeAoe`) strikes every selectable enemy on its range at once with instant hits (§21.18).
|
||||
- **Healers** (`dmgType heal`): target the ally in range with the lowest HP% below 100%, never a 禁疗 / 孤立 unit (tokens.json `abnormal`, `noHeal`; §20.3; 史尔特尔 during 余烬, §22.7), and never the enemies they block — also while blocking (§21.17); do nothing otherwise (SP still regenerates).
|
||||
- **Enemy AI**: follow the route (checkpoints; WALK paths recomputed on the grid when devices change); when blocked stop & fight (`dmgType 'none'` enemies never attack unless content arms them through `e.profile` — 转译基底's armed forms, §21.4); ranged enemies (`rangeRadius > 0`) attack ops whose 0.25 collider touches their radius (centre distance ≤ `rangeRadius` + 0.25, §3, §20.4): the enemy's own target rule filters the candidates first (`profile.canTarget`: 只攻击地面单位 …), then priority blocker → special priority → highest taunt → **latest deployed** (`aggroSeq`) — PRTS 索敌 "阻挡→特殊优先级→仇恨值→最早出现"; a stealthed ally is attacked only by the enemy it blocks (§19.1); an airborne (起飞) ally is never a ground enemy's selection — picks, area damage, zones and statuses skip it, while 无视无法选择 abilities, direct picks, debuff ticks and flying units still reach it (§21.22); they pause movement for `ATTACK_PAUSE = 0.35 s` per attack. FLY enemies ignore ground obstacles (only blockFly units block them) and follow their checkpoint route. A feared enemy runs between random 恐惧可达地块 away from the source (PRTS 诱发移动; `fear.js`); 恐惧 outranks 诱导; the 恐惧 / 诱导 walks re-plan from where the enemy stands after a push or an obstacle change, and the route re-plans when they end (§20.4).
|
||||
- **Enemy AI**: follow the route (checkpoints; WALK paths recomputed on the grid when devices change); when blocked stop & fight (`dmgType 'none'` enemies never attack unless content arms them through `e.profile` — 转译基底's armed forms, §21.4); ranged enemies (`rangeRadius > 0`) attack ops whose 0.25 collider touches their radius (centre distance ≤ `rangeRadius` + 0.25, §3, §20.4): the enemy's own target rule filters the candidates first (`profile.canTarget`: 只攻击地面单位 …), then priority blocker → special priority → highest taunt → **latest deployed** (`aggroSeq`) — PRTS 索敌 "阻挡→特殊优先级→仇恨值→最早出现"; a stealthed ally is attacked only by the enemy it blocks (§19.1), and an enemy's area effects — splash, blasts, area skills, zones, jumps — and buff auras skip it unless it blocks that enemy, while 迷彩 does not protect from splash and the other area effects (`targeting.js areaSelectable` / `auraSelectable`, §22.12); an airborne (起飞) ally is never a ground enemy's selection — picks, area damage, zones and statuses skip it, while 无视无法选择 abilities, direct picks, debuff ticks and flying units still reach it (§21.22); they pause movement for `ATTACK_PAUSE = 0.35 s` per attack. FLY enemies ignore ground obstacles (only blockFly units block them) and follow their checkpoint route. A feared enemy runs between random 恐惧可达地块 away from the source (PRTS 诱发移动; `fear.js`); 恐惧 outranks 诱导; the 恐惧 / 诱导 walks re-plan from where the enemy stands after a push or an obstacle change, and the route re-plans when they end (§20.4).
|
||||
- **Damage**: phys `max(A − max(0, D×(1−defIgnorePct) − defIgnoreFlat), 0.05·A)`; arts `max(A × (1 − R'/100), 0.05·A)` with `R' = max(0, R×(1−resIgnorePct) − resIgnoreFlat)`; true = A; 元素伤害 ('elemental') `max(A × (1 − 元素抗性/100), 0.05·A)`. Then × `dmgDealtMul` (source) × `dmgTakenMul` & type-specific taken multipliers (target) × `dmg.mul` — 元素伤害 takes 元素脆弱 (`elementalTakenMul`) alone, not `dmgTakenMul` (脆弱 = "受到的物理、法术、真实伤害提升"). Then **限伤**: on a leader (`isBoss` — the 10 bossInfoDict enemyIds and their mirrored copies; never parts, drones or escorts) in a `'boss'` / `'hidden'` battle a hit with `ceil(final) ≥ 300000` is cancelled — no HP or pool loss, no credit, no damage number, no `damaged` / kill (`shared/constants.js BOSS_HIT_LIMIT`, `damage.js leaderHitCancelled`, after every multiplier and the hit-step blocks, before HP shields; `loseHp` too — a part's 传递, a drone's death; the client's `MAX_BATTLE_DAMAGE`, §20.12). Then shields absorb; dodge checks for phys/arts. Element damage fills a gauge (1000; enemy leaders 2000) × `elemTakenMul` × max(0.05, 1 − 损伤抵抗/100), 损伤抵抗 = data `epResistance` (PRTS 元素; the 5 % floor of DMG_e, PRTS 游戏数据基础). An element fill on a target with no HP left is refused (damage.js `hasHp`: no burst on a corpse). A full gauge bursts with the side-specific official effects, as **无来源** damage (no damage-dealt multiplier or penetration of the unit that filled the gauge, which keeps the kill and the stats; §19.5) (sim/constants.js `ELEMENT`; operators: 灼燃 1200 arts + RES −20, 神经 stun 10 s then 1000 true, 凋亡 15 s of 阻回 (`noSp`: no SP gain of any kind) + 静默 with −1 SP/s and 100 arts/s, 侵蚀 DEF −100 then 800 phys; enemies: the "·我方" terms). Every burst starts a **爆发冷却** = its duration (灼燃 / 神经 / 侵蚀 10 s — enemies' 侵蚀 8 s — 凋亡 15 s) during which NO element of the unit fills or can be recovered; when it ends every gauge of the unit resets (§18.3).
|
||||
- **Leak**: an enemy reaching its goal is removed, counted as leaked for its field owner (`sourcePlayerId` for unite). LP accounting is done by the match, not the battle.
|
||||
- **Results**: `damageDealt` / a unit's `dmg` count only HP removed from the other side — a unit's own drain or 流失 and friendly damage are the target's `taken` only (§21.12).
|
||||
|
||||
+1
-1
@@ -70,7 +70,7 @@
|
||||
- **召唤物手动摆放**:把带可部署召唤物的干员放上棋盘后,它的召唤物会进入整备区(赫默的医疗探机、巫恋的诅咒娃娃、凯瑟琳的 2 个支援装置、浊心斯卡蒂的海嗣、伺夜的狼群、缪尔赛思的流形;同一干员的召唤物叠在一格,不占部署位),像干员一样拖到棋盘上并选方向。凯瑟琳的支援装置给**它面朝的那一格**上的干员加屏障,所以要放在干员旁边、朝向干员。**战术家的援军**(伺夜的狼群、缪尔赛思的流形,也就是选战术点)和官方一样**只能放在召唤者的攻击范围内**:拖动时只有这些格子会亮起;之后在原地给战术家换朝向时,不在新攻击范围里的援军会退回整备区(还在范围里的保持不动;整备区和临时整备区都满、援军无处可退时,这次换朝向不生效并提示「整备区已满」)。撤退或出售干员时它的召唤物一起移除;**移动干员到别的格子时(包括把召唤物拖到干员身上交换位置),它已摆放的召唤物会退回整备区**,需要重新摆放。整备区满时退回的召唤物进入临时整备区,休整期结束还没处理的会在下一回合重新回到整备区。
|
||||
- 作战中:海嗣、狼群、流形、支援装置开战时就在摆放的位置部署;医疗探机、诅咒娃娃开战时也在摆放的位置免费部署一次(持续 10 / 15 秒,和官方一样不需要先开技能),之后所属干员每次发动技能时再次在原位出现;所属干员被击倒、再部署回场时,技能给的那一个如果还没出现,会立刻出现。巫恋退场时诅咒娃娃随之撤退,赫默退场时已出现的医疗探机不受影响。医疗探机、诅咒娃娃、支援装置、海嗣没有摆放就不会出现;狼群、流形(战术家的援军)没有摆放时会在战术点出现。
|
||||
- **阻挡**看接触:干员阻挡中心距离约 0.7 格以内、与它接触的敌人(地面干员挡不住飞行和近地悬浮的敌人)。敌人卡在两名干员之间时,阻挡它的干员倒下、撤退或被晕眩后,另一名仍与它接触、还有空余阻挡数的干员会**立刻接替阻挡**;都没有空位时敌人才会走掉。迎面走来的敌人会停在干员格子前面。**阻挡了就一定能打到**:任何干员(包括站在近战位的远程干员和召唤物)总能攻击自己阻挡的敌人,不管它在不在攻击范围内、在干员哪一侧,并且优先攻击它(医疗干员除外:咒愈师以外的医疗挡住敌人时照常治疗队友,不会攻击它;「停止攻击」类技能期间也除外;技能原文另有限制的也按原文:普罗旺斯二技能开启时不以生命值高于 80% 的敌人为目标,寒檀二技能的冰凌随机落在攻击范围内的地块上)。
|
||||
- 开战时干员按**从上到下、从左到右**的顺序部署,召唤物在所有干员之后。被阻挡的敌人先打阻挡它的干员;没被阻挡的远程敌人先打嘲讽等级高的,同级时打最后部署的(召唤物、靠下靠右的干员和刚再部署的干员更容易挨打)。隐匿的干员只会被它阻挡的敌人攻击;**迷彩**(如忍冬三技能期间击倒敌人后、寒芒克洛丝一技能)同样只会被它阻挡的敌人攻击,也显示为半透明,但它不算隐匿(不触发叙拉古等「隐匿时」效果)。叙拉古的隐匿按时(32 秒 + 每层 0.4 秒)结束;忍冬的迷彩按官方持续到她下一次开技能。**蒂比起飞**期间只挡飞行敌人,地面敌人不会选她为目标(普通攻击、溅射、技能范围、燃烧区域和要选中目标的减益都落不到她身上);飞行和悬浮敌人、写明「无视无法选择」的效果(如污染秽蚀)照常能打到她;地面敌人的光环和全场效果(如“墓碑”让全场攻击力、防御力减半)、她攻击敌人时招来的反制(如重装侦察兵的暴露)照常生效,起飞前已挂上的持续伤害也照常跳。
|
||||
- 开战时干员按**从上到下、从左到右**的顺序部署,召唤物在所有干员之后。被阻挡的敌人先打阻挡它的干员;没被阻挡的远程敌人先打嘲讽等级高的,同级时打最后部署的(召唤物、靠下靠右的干员和刚再部署的干员更容易挨打)。隐匿的干员只会被它阻挡的敌人攻击,没被它挡住的敌人的溅射、爆炸、范围技能、留下的伤害区域(包括假想敌:蚀裂被击倒后喷出的毒雾)以及深池伙友卫队、扎罗的减攻速光环也落不到它身上(和官方一样;写明「无视无法选择」的效果如污染秽蚀照常能打到,寒霜的减攻速光环和“墓碑”这类地图效果照常生效);**迷彩**(如忍冬三技能期间击倒敌人后、寒芒克洛丝一技能)同样只会被它阻挡的敌人攻击,也显示为半透明,但躲不开溅射、爆炸和范围伤害(同时打范围内所有单位的普通攻击——如“斩胄之剑”“破胄之锤”“灵幛”——打不到它),也不算隐匿(不触发叙拉古等「隐匿时」效果)。叙拉古的隐匿按时(32 秒 + 每层 0.4 秒)结束;忍冬的迷彩按官方持续到她下一次开技能。**蒂比起飞**期间只挡飞行敌人,地面敌人不会选她为目标(普通攻击、溅射、技能范围、燃烧区域和要选中目标的减益都落不到她身上);飞行和悬浮敌人、写明「无视无法选择」的效果(如污染秽蚀)照常能打到她;地面敌人的光环和全场效果(如“墓碑”让全场攻击力、防御力减半)、她攻击敌人时招来的反制(如重装侦察兵的暴露)照常生效,起飞前已挂上的持续伤害也照常跳。
|
||||
- **技能自动释放**按官方「技能策略」:一般的手动技能在技能就绪、即将攻击 / 治疗且初始攻击范围内有敌人(治疗技能:有受伤的队友)时释放;**带独立技能范围**的技能(如德克萨斯「剑雨」、凛御银灰「御敌的锋锐」、锏「归于宁静」,不含「攻击范围扩大」类)在**技能范围内出现敌人**时就释放,不必先攻击;**重装干员**的手动技能在受到伤害时释放(例外:深巡、雷蛇的二技能,号角的二、三技能,灰毫的一、二技能按玩家反馈改为攻击范围内有敌人时就释放,这是本作有意与官方不同的地方;深巡一技能仍要受到伤害);执旗手 / 战术家 / 吟游者技能就绪即释放,解放者 / 阵法术师在初始攻击范围内有敌人时释放。**自动触发**的技能按其本身规则,例如古米「备用军粮」在身边(技能范围内)有受伤的队友时就会发动,下一次攻击改为治疗该队友(不用先接敌;她的二技能「食粮烹制」是手动技能,按重装规则在受到伤害时释放);塞雷娅「急救」在她即将攻击时、身边有生命不高于一半的队友才发动,这次攻击直接改为治疗;新约能天使二技能的护盾按官方给攻击范围内**仇恨最高**的队友(嘲讽等级高的优先,同级时给最后部署的),不一定是站在最前面挨打的那个。手动技能的自动释放有 3 秒冷却:开战部署后先等 3 秒,之后同一干员两次自动释放至少间隔 3 秒(可充能的技能不会同时放完)。
|
||||
- **推开 / 拉拽**按官方「力度 − 重量」决定距离:例如野鬃二技能(中等力度)把重量 0 / 1 / 2 / 3 的敌人推开约 2.1 / 1.7 / 0.4 / 0.1 格,重量 4 以上推不动——轻型敌人反而推得更远。野鬃二技能沿她的朝向推开:正前方 45° 以内按上面的距离,身后、侧面或贴身的敌人只被轻推一下(约 0.4 格以内);琳琅诗怀雅三技能的金币则从她身边向外推开,落在她前方 2–4 格的范围(官方「范围2-4」)内的地面敌人和她阻挡的敌人身上。攻击速度最低为 20(沼泽、寒霜等减速叠满时也不会更慢)。
|
||||
- **元素损伤条**在单位自己的血条正下方:左边是元素图标,白条是剩余的元素值,只有真正爆发时才会耗尽;爆发后条变成元素的颜色、图标闪烁,在冷却期间慢慢回满(这段时间不会再积累元素损伤)。
|
||||
|
||||
+35
-15
@@ -242,20 +242,37 @@ latest). `enemy.profile.canTarget(ally)` (content: 萨卡兹枯朽战车 "只攻
|
||||
a special priority (假想敌:铳 / 昆图斯 highest DEF, 假想敌:胄 highest / lowest ATK, “自在” nearest …) sorts by its key and
|
||||
breaks ties by taunt, then latest deployed (`aggroCmp`); `untargetable` / sleeping allies and devices are never targets;
|
||||
an airborne ally (起飞, flag `liftoff`: 蒂比's skills) never for a ground enemy (对地规避 — `targeting.js evadesGround`,
|
||||
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 【盲信之誓】, 萨卡兹悖谬
|
||||
暴虐兵长's 暴击 splash), direct picks (碎铳之簧's 法术护盾 counter and 重装侦察兵's 暴露 on their attacker — PRTS 异常效果
|
||||
PRTS 术语释义 起飞 "无法被不同阵营行动方式为地面的单位选中"; flyers, 近地悬浮 and 浮空 enemies still pick it; the enemy
|
||||
area selections skip it too (below); not selections, so they still reach it (`ignoreSelect`): abilities PRTS marks
|
||||
"无视无法选择 / 无视(目标)可选性" (【污染秽蚀】, 假想敌:铳's 【盲信之誓】, 萨卡兹悖谬暴虐兵长's 暴击 splash, 假想敌:淤困's
|
||||
burst spread), direct picks (碎铳之簧's 法术护盾 counter and 重装侦察兵's 暴露 on their 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 ground enemy's area skill whose cast depends on allies nearby counts only those it can hurt — 卢西恩's
|
||||
【aoe】, 锏's CircleAttack [ASSUMED: no source states the trigger]);
|
||||
took off (出血, 沙狱, burning DoTs, 淤困, 【自然涌动】 — a tick selects nobody); the buff auras of ground enemies and the
|
||||
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 选择器 "所有触发选择器通常不无视迷彩");
|
||||
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
|
||||
splash selectors skip it too]
|
||||
"不阻挡时不成为敌方普通攻击的目标") 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,
|
||||
“灵幛”). **Enemy area effects** — splash, death and self blasts, area skills and statuses, pulses, the zones an enemy
|
||||
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
|
||||
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
|
||||
(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
|
||||
(PRTS names its 攻速下降 Debuff among the effects that "无视隐匿状态起作用") and map / tile / terrain effects (“墓碑”'s
|
||||
halving — PRTS: a map effect —, 【国度】, a chimera's 源石污染区 aura [ASSUMED for the last two])
|
||||
and pause `ATTACK_PAUSE` (0.35 s) after each unblocked attack; `fear`/`disarm` stop attacks; `dmgType 'none'` enemies
|
||||
never attack — unless content arms them through `enemy.profile` (`noAttack: false`, `melee`, `dmgType`, `maxTargets`:
|
||||
转译基底·α's 寻仇者 / 特战术师 forms, which then attack like any enemy); `dmgType 'heal'` enemies heal the lowest-HP% enemy in their radius instead. A `noMove` enemy stands (not `moving`, drawn idle). Content can take over an
|
||||
@@ -276,15 +293,18 @@ research stages without `active` use `!hidden`. Active platforms/mounds (射击
|
||||
ground obstacles, and an operator standing on one is elevated (`unit.ground = false`: never blocks).
|
||||
|
||||
**Enemy damage zones** (`content/enemies.js zone` / `dmgZone` / `pollution`): a zone ticks on the allies inside it
|
||||
(flyers, stealthed and untargetable ones included; `alliesInRadius`) through `dealDamage`, so shields absorb a damage
|
||||
that it selects — an enemy's zone through its area selection (`areaAllies`, above; the sourceless 毒雾 with no selecting
|
||||
enemy: 隐匿 kept out, 起飞 not), 【污染秽蚀】 on every ally inside (flyers, stealthed and untargetable ones included;
|
||||
`alliesInRadius`) — through `dealDamage`, so shields absorb a damage
|
||||
tick, damage-taken modifiers scale it and it counts for 受击回复 SP and TAKE_DAMAGE skills like any hit (§4; element fills
|
||||
excepted). 【污染秽蚀】 (萨卡兹枯朽战车's 秽蚀轰击,
|
||||
萨卡兹枯朽战士's death) is **true** damage, 50 / 25 per second on low / high ground (PRTS "每秒受到50/25点真实普通伤害 …
|
||||
同名效果不叠加", 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; the sourceless 毒雾 of
|
||||
假想敌:蚀裂 reaches everyone inside. An activated 孽罪奇美拉's aura (`kitChimera`, PRTS "自身半径1.2
|
||||
燃烧区域 (集团军重型火炮, PRTS "碰撞不受迷彩制约,不可对空" — 迷彩 only) skips an airborne 起飞 ally and an unblocking 隐匿
|
||||
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
|
||||
kind of damage — 无来源 true (like the terrain it stands for [ASSUMED]), the chimera credited — not a 流失 (player report
|
||||
D1 audit): radius 1.2, a tick every 0.5 s, one tick per unit per 0.5 s however many chimeras reach it (`mem.chimeraAt`).
|
||||
@@ -542,8 +562,8 @@ 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) / 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) 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 | – |
|
||||
| `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) | – |
|
||||
| `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 | – |
|
||||
| `attract` (诱导) | 无法被阻挡并向目标位置移动: unblockable (released); the engine walks it (own speed, grid path re-planned on obstacle changes, after a push and after an outranking 恐惧; flyers straight) to `opts.point` (`[r, c]` or `{x, y}`, default the source's tile, clamped to the rect) and keeps it there; stun/bind/sleep stop it; its route re-plans from where it stands when the status ends. A new application moves the point | point |
|
||||
|
||||
@@ -1647,6 +1647,10 @@ export class Battle {
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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).
|
||||
*/
|
||||
alliesInRadius(x, y, r, ownerId = null, { includeDevices = false } = {}) {
|
||||
const out = [];
|
||||
const r2 = r * r + 1e-9;
|
||||
|
||||
@@ -54,19 +54,24 @@
|
||||
// 自缚 + 无法被阻挡 (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; 掷剑 /
|
||||
// the area selectors); still reach it (`ignoreSelect`): the 刺胄之弹 / 剑 / 锤 blasts (flying units, 无来源 DoT; 掷剑 /
|
||||
// 掷锤 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).
|
||||
// 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
|
||||
// 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).
|
||||
// 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 …}).
|
||||
|
||||
import { MOVE_SCALE } from '../constants.js';
|
||||
import { canTargetAlly, aggroCmp, evadesGround } from '../targeting.js';
|
||||
import { aggroCmp, areaSelectable } from '../targeting.js';
|
||||
import { compileRoute } from '../ai.js';
|
||||
import { normalizeRoute } from '../simdata.js';
|
||||
import {
|
||||
ensureInstalled, abOf, attach, T, elem, hurt, alliesInTiles, targetsNear, allTargets, byPriority,
|
||||
ensureInstalled, abOf, attach, T, elem, hurt, targetsNear, allTargets, byPriority, areaAllies, areaAlliesInTiles, fieldAllies,
|
||||
remainingRoute, stayRoute, stepToward, setHits, hitCount, lpLoss, blinkForward, canCast, absorbArts, nthOf,
|
||||
} from './enemies.js';
|
||||
|
||||
@@ -325,13 +330,15 @@ export function fairOrder(b, e, list, P) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 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).
|
||||
* Stun + phys DoT on the 3×3 around (r, c), credited to `src` (胄). The blast is a flying unit's — `by`: 刺胄之弹 /
|
||||
* 斩胄之剑 / 破胄之锤 (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).
|
||||
*/
|
||||
function stunBlast(b, src, r, c, stun, dot, dur, kind) {
|
||||
function stunBlast(b, src, by, 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)) {
|
||||
for (const u of areaAlliesInTiles(b, by, r, c, 'box', 1)) {
|
||||
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, ignoreSelect: true, tags: ['enemyAbility', kind] }) });
|
||||
@@ -436,7 +443,7 @@ function kitShell(ab, e) {
|
||||
if (!s || a.done) return;
|
||||
if (!stepToward(e2, s.tc, s.tr, e2.s.moveSpeed * MOVE_SCALE * dt)) return;
|
||||
a.done = true; // arrival: 3×3 long stun + phys DoT
|
||||
stunBlast(b, s.boss && s.boss.alive ? s.boss : e2, s.tr, s.tc, stun, dot, stun, 'helmShell');
|
||||
stunBlast(b, s.boss && s.boss.alive ? s.boss : e2, e2, s.tr, s.tc, stun, dot, stun, 'helmShell');
|
||||
b.kill(e2, null);
|
||||
},
|
||||
}];
|
||||
@@ -495,7 +502,10 @@ function kitBlade(ab, e, b, tpl) {
|
||||
b2.addBuff(e2, { key: 'boss:anchor', persist: true, flags: { noMove: true, selfBound: true, unblockable: true, noDisplace: true } });
|
||||
if (BLADE_ATK_SCALE[e2.defId]) e2.profile.atkScale = BLADE_ATK_SCALE[e2.defId];
|
||||
},
|
||||
before(c, b2, e2) { const l = targetsNear(b2, e2, e2.base.rangeRadius || 1.6, { ranged: false }); if (l.length) c.targets = l; }, // 范围物理伤害
|
||||
// 范围物理伤害 — "普通攻击对攻击范围内的所有我方单位造成…物理普通伤害": a normal attack on every operator it can target
|
||||
// (canTargetAlly: no 隐匿 or 迷彩 one — it is never blocked —, PRTS 选择器: a normal attack does not ignore 迷彩; until
|
||||
// 0.1.2 it took them all, `ranged: false`)
|
||||
before(c, b2, e2) { const l = targetsNear(b2, e2, e2.base.rangeRadius || 1.6); if (l.length) c.targets = l; },
|
||||
taken(c, b2, e2) {
|
||||
if (c.amount > 0) { // 受到伤害时令假想敌:胄受到等量的无来源生命流失 (【瘫痪】; 出击模式 [ASSUMED], see PART_TRANSFER)
|
||||
const L = leader(b2);
|
||||
@@ -509,7 +519,7 @@ function kitBlade(ab, e, b, tpl) {
|
||||
const t = P.target;
|
||||
if (!stepToward(e2, t.c, t.r, e2.s.moveSpeed * MOVE_SCALE * dt)) return;
|
||||
// arrival: 3×3 stun + DoT (无来源: credited to 胄, as the shell's), then the 初始模式 copy takes over
|
||||
stunBlast(b2, leader(b2) || e2, t.r, t.c, bb.stun ?? 0, bb.dot_damage ?? 0, bb.dot_duration ?? 0, 'bladeDive');
|
||||
stunBlast(b2, leader(b2) || e2, e2, t.r, t.c, bb.stun ?? 0, bb.dot_damage ?? 0, bb.dot_duration ?? 0, 'bladeDive');
|
||||
replace(b2, e2);
|
||||
},
|
||||
},
|
||||
@@ -565,7 +575,8 @@ function kitGun(ab, e, b) {
|
||||
const nx = e2.x, ny = e2.y;
|
||||
const arrived = stepToward(e2, t.x, t.y, sp);
|
||||
if (!b2.grid.groundPassable(Math.round(e2.y), Math.round(e2.x))) { e2.x = nx; e2.y = ny; }
|
||||
for (const u of b2.alliesInRadius(e2.x, e2.y, CHARGE_RADIUS)) if (!ch.hit.has(u)) { ch.hit.add(u); hurt(b2, e2, u, e2.s.atk * (T(ab, '3.atk_scale') ?? 1), 'phys'); }
|
||||
// "对进入自身0.35半径范围内的我方单位(包括飞行单位)造成一次…": an area selection (areaAllies)
|
||||
for (const u of areaAllies(b2, e2, e2.x, e2.y, CHARGE_RADIUS)) if (!ch.hit.has(u)) { ch.hit.add(u); hurt(b2, e2, u, e2.s.atk * (T(ab, '3.atk_scale') ?? 1), 'phys'); }
|
||||
if (arrived || b2.time >= ch.until || (e2.x === nx && e2.y === ny)) {
|
||||
P.charge = null;
|
||||
b2.removeBuff(e2, 'boss:charge');
|
||||
@@ -675,7 +686,7 @@ function kitSpring(ab, e) {
|
||||
if (!d) return;
|
||||
const g = d.gun && d.gun.alive ? d.gun : gun(b);
|
||||
const arrived = !g || stepToward(e2, g.x, g.y, e2.s.moveSpeed * d.mul * MOVE_SCALE * dt) || Math.hypot(g.x - e2.x, g.y - e2.y) < 1;
|
||||
for (const u of b.alliesInRadius(e2.x, e2.y, 0.5)) if (!d.hit.has(u)) { d.hit.add(u); hurt(b, e2, u, e2.s.atk, 'phys'); }
|
||||
for (const u of areaAllies(b, e2, e2.x, e2.y, 0.5)) if (!d.hit.has(u)) { d.hit.add(u); hurt(b, e2, u, e2.s.atk, 'phys'); }
|
||||
if (arrived || b.time >= d.until) { e2.mem.ab.dash = null; b.removeBuff(e2, 'boss:dash'); if (e2.route) e2.route.pts = null; }
|
||||
},
|
||||
},
|
||||
@@ -697,7 +708,7 @@ function kitSpring(ab, e) {
|
||||
b.fx('beam', { x: e2.x, y: e2.y, from: e2.id, to: t.id, kind: 'springBullet' });
|
||||
elem(b, e2, t, 'erosion', e2.s.atk * ratio * Math.pow(SPRING_BOUNCE_FALLOFF, k));
|
||||
const prev = t;
|
||||
t = b.alliesInRadius(prev.x, prev.y, SPRING_BOUNCE_RANGE).filter((u) => !hit.has(u) && !evadesGround(e2, u)).sort((p, q) => Math.hypot(p.x - prev.x, p.y - prev.y) - Math.hypot(q.x - prev.x, q.y - prev.y) || aggroCmp(p, q))[0];
|
||||
t = areaAllies(b, e2, prev.x, prev.y, SPRING_BOUNCE_RANGE).filter((u) => !hit.has(u)).sort((p, q) => Math.hypot(p.x - prev.x, p.y - prev.y) - Math.hypot(q.x - prev.x, q.y - prev.y) || aggroCmp(p, q))[0];
|
||||
}
|
||||
} else { // 十连击
|
||||
b.fx('beam', { x: e2.x, y: e2.y, from: e2.id, to: t0.id, kind: 'springCombo' });
|
||||
@@ -724,13 +735,14 @@ export function setEchoForm(b, echo, form) {
|
||||
b.fx('phase', { x: echo.x, y: echo.y, id: echo.id, kind: `echo_${form}` });
|
||||
}
|
||||
|
||||
/** An echo takes a strike/hit: pulse around it and count towards the form switch. */
|
||||
/** An echo takes a strike/hit: pulse around it (PRTS “余音” "受到伤害时对半径1.6范围内的我方单位造成…": an area selection of the
|
||||
* echo — no unblocking 隐匿 operator) and count towards the form switch. */
|
||||
export function echoHit(b, echo) {
|
||||
const ab = echo.mem.ab;
|
||||
if (!ab || !echo.alive) return;
|
||||
const atk = echo.s.atk;
|
||||
b.fx('explode', { x: echo.x, y: echo.y, r: ECHO_PULSE_RADIUS, kind: 'echoPulse' });
|
||||
for (const u of b.alliesInRadius(echo.x, echo.y, ECHO_PULSE_RADIUS)) {
|
||||
for (const u of areaAllies(b, echo, echo.x, echo.y, ECHO_PULSE_RADIUS)) {
|
||||
hurt(b, echo, u, atk * (T(ab, '3.atk_scale') ?? 0), 'arts');
|
||||
elem(b, echo, u, 'apoptosis', atk * (T(ab, '3.ep_damage_ratio') ?? 0));
|
||||
}
|
||||
@@ -753,7 +765,7 @@ function kitEcho(ab, e) {
|
||||
const r = gold ? ECHO_ENSEMBLE_RADIUS.gold : ECHO_ENSEMBLE_RADIUS.dark;
|
||||
const ratio = gold ? T(ab, '4.ep_damage_ratio_passion') ?? 0 : T(ab, '4.ep_damage_ratio_depassion') ?? 0;
|
||||
b.fx('explode', { x: e2.x, y: e2.y, r, kind: gold ? 'ensembleGold' : 'ensembleDark' });
|
||||
for (const u of b.alliesInRadius(e2.x, e2.y, r)) { hurt(b, e2, u, e2.s.atk, 'arts'); elem(b, e2, u, 'apoptosis', e2.s.atk * ratio); }
|
||||
for (const u of areaAllies(b, e2, e2.x, e2.y, r)) { hurt(b, e2, u, e2.s.atk, 'arts'); elem(b, e2, u, 'apoptosis', e2.s.atk * ratio); }
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -802,7 +814,7 @@ function kitPipe(ab, e, b, tpl) {
|
||||
for (const echo of echoesOf(b2, 'dark')) for (let k = 0; k < 3; k++) b2.after(k * PIPE_STRIKE_GAP, () => {
|
||||
if (!e2.alive || !echo.alive) return;
|
||||
b2.fx('explode', { x: echo.x, y: echo.y, r, kind: 'pipeStrike' });
|
||||
for (const u of b2.alliesInRadius(echo.x, echo.y, r)) { hurt(b2, e2, u, e2.s.atk, 'arts'); elem(b2, e2, u, 'apoptosis', e2.s.atk * ep); }
|
||||
for (const u of areaAllies(b2, e2, echo.x, echo.y, r)) { hurt(b2, e2, u, e2.s.atk, 'arts'); elem(b2, e2, u, 'apoptosis', e2.s.atk * ep); }
|
||||
echoHit(b2, echo);
|
||||
}, { owner: e2 });
|
||||
},
|
||||
@@ -821,7 +833,7 @@ function kitString(ab, e, b, tpl) {
|
||||
const r = s.bb.range_radius ?? 0, ep = s.bb.ep_damage_ratio ?? 0;
|
||||
for (const echo of echoesOf(b2, 'gold')) {
|
||||
b2.fx('explode', { x: echo.x, y: echo.y, r, kind: 'stringStrike' });
|
||||
for (const u of b2.alliesInRadius(echo.x, echo.y, r)) { hurt(b2, e2, u, e2.s.atk, 'arts'); elem(b2, e2, u, 'apoptosis', e2.s.atk * ep); }
|
||||
for (const u of areaAllies(b2, e2, echo.x, echo.y, r)) { hurt(b2, e2, u, e2.s.atk, 'arts'); elem(b2, e2, u, 'apoptosis', e2.s.atk * ep); }
|
||||
setEchoForm(b2, echo, 'dark');
|
||||
}
|
||||
},
|
||||
@@ -870,9 +882,9 @@ function kitQuintus(ab, e, b, tpl) {
|
||||
}
|
||||
|
||||
const QUINTUS = {
|
||||
Tidewater(b, e, s) { // 【大潮】 every operator: arts + neural
|
||||
Tidewater(b, e, s) { // 【大潮】 "对场上所有我方单位…": every operator it can select (fieldAllies): arts + neural
|
||||
b.fx('tide', { x: e.x, y: e.y, id: e.id });
|
||||
for (const u of b.allies()) { hurt(b, e, u, e.s.atk * (s.bb.atk_scale ?? 0), 'arts'); elem(b, e, u, 'neural', e.s.atk * (s.bb.ep_damage_ratio ?? 0)); }
|
||||
for (const u of fieldAllies(b, e)) { hurt(b, e, u, e.s.atk * (s.bb.atk_scale ?? 0), 'arts'); elem(b, e, u, 'neural', e.s.atk * (s.bb.ep_damage_ratio ?? 0)); }
|
||||
},
|
||||
Rockfall(b, e, s, P) { // 【崩坍】 delayed strikes on the N highest-DEF units
|
||||
const n = ROCKFALL_TARGETS[Math.min(P.stage, ROCKFALL_TARGETS.length - 1)];
|
||||
@@ -894,7 +906,7 @@ const QUINTUS = {
|
||||
P.tent++;
|
||||
for (const [r, c] of spots) {
|
||||
b.fx('tentacle', { x: c, y: r, r: TENTACLE_RADIUS, dur: TENTACLE_STUN });
|
||||
for (const u of b.alliesInRadius(c, r, TENTACLE_RADIUS)) b.applyStatus(u, 'stun', { duration: TENTACLE_STUN, source: e });
|
||||
for (const u of areaAllies(b, e, c, r, TENTACLE_RADIUS)) b.applyStatus(u, 'stun', { duration: TENTACLE_STUN, source: e });
|
||||
}
|
||||
},
|
||||
Doom(b, e, s) { // 【物种爆发】
|
||||
@@ -922,12 +934,14 @@ function lucienCore(ab, e) {
|
||||
dealt(c, b, e2) { elem(b, e2, c.target, 'neural', e2.s.atk * epr); },
|
||||
},
|
||||
s && {
|
||||
// cast only with someone the blast can hurt: an airborne (起飞) operator evades a ground leader (对地规避 — the
|
||||
// blast skips her in damage.js; counting her spent the skill on nobody)
|
||||
cd: s.cd, icd: s.icd, cond: (b) => b.alliesInRadius(e.x, e.y, LUCIEN_AOE_RADIUS).some((u) => !evadesGround(e, u)),
|
||||
// cast with a target in the radius (PRTS 技能 "需要目标"; 不祥幻影 "仅在半径2范围内存在我方单位时触发"): the trigger
|
||||
// selection (targetsNear — PRTS 选择器 "所有触发选择器通常不无视迷彩"): an airborne (起飞) operator evades a ground
|
||||
// leader (对地规避 — counting her spent the skill on nobody, §21.22), nor does an unblocking 隐匿 or 迷彩 one count
|
||||
cd: s.cd, icd: s.icd, cond: (b) => targetsNear(b, e, LUCIEN_AOE_RADIUS).length > 0,
|
||||
fire(b, e2) {
|
||||
// "该技能伤害无视迷彩": an area selection (areaAllies) — a 迷彩 operator is hit, an unblocking 隐匿 one not
|
||||
b.fx('explode', { x: e2.x, y: e2.y, r: LUCIEN_AOE_RADIUS, kind: 'crimsonAoe' });
|
||||
for (const u of b.alliesInRadius(e2.x, e2.y, LUCIEN_AOE_RADIUS)) { hurt(b, e2, u, e2.s.atk * (s.bb.atk_scale ?? 0), 'phys'); elem(b, e2, u, 'neural', e2.s.atk * (s.bb.ep_damage_ratio ?? 0)); }
|
||||
for (const u of areaAllies(b, e2, e2.x, e2.y, LUCIEN_AOE_RADIUS)) { hurt(b, e2, u, e2.s.atk * (s.bb.atk_scale ?? 0), 'phys'); elem(b, e2, u, 'neural', e2.s.atk * (s.bb.ep_damage_ratio ?? 0)); }
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -966,7 +980,7 @@ function kitLion(ab, e, b) {
|
||||
const r0 = t.tileR, c0 = t.tileC;
|
||||
const tiles = [[r0, c0], [r0 + 1, c0], [r0 - 1, c0], [r0, c0 + 1], [r0, c0 - 1]];
|
||||
b2.fx('telegraph', { x: c0, y: r0, r: 1, kind: 'royalDecree', tiles: 'plus', id: e2.id });
|
||||
for (const u of alliesInTiles(b2, r0, c0, 'plus', 1)) hurt(b2, e2, u, e2.s.atk * ((sk && sk.bb.atk_scale) || 0), 'phys');
|
||||
for (const u of areaAlliesInTiles(b2, e2, r0, c0, 'plus', 1)) hurt(b2, e2, u, e2.s.atk * ((sk && sk.bb.atk_scale) || 0), 'phys');
|
||||
const taken = equipment(b2);
|
||||
const free = tiles.filter(([r, c]) => b2.grid.inRect(r, c) && b2.grid.isLow(r, c) && b2.grid.groundPassable(r, c) && !b2.unitAt(r, c)
|
||||
&& !taken.some((q) => Math.round(q.y) === r && Math.round(q.x) === c));
|
||||
@@ -1022,9 +1036,9 @@ function kitLion(ab, e, b) {
|
||||
},
|
||||
su && {
|
||||
cd: su.cd, icd: su.icd,
|
||||
fire(b2, e2) { // 【斥退】
|
||||
fire(b2, e2) { // 【斥退】 "对场上所有我方单位…": every operator it can select (fieldAllies)
|
||||
b2.fx('telegraph', { x: e2.x, y: e2.y, r: 99, kind: 'repel', id: e2.id });
|
||||
for (const u of b2.allies()) hurt(b2, e2, u, e2.s.atk * (su.bb.atk_scale ?? 0), 'arts');
|
||||
for (const u of fieldAllies(b2, e2)) hurt(b2, e2, u, e2.s.atk * (su.bb.atk_scale ?? 0), 'arts');
|
||||
lpLoss(b2, Math.abs(su.bb.value ?? 0), 'repel', e2);
|
||||
},
|
||||
},
|
||||
@@ -1060,8 +1074,8 @@ function kitDeer(ab, e) {
|
||||
P.acc = 0;
|
||||
const cols = [];
|
||||
for (const t of cands) { if (!cols.includes(t.tileC)) cols.push(t.tileC); if (cols.length >= (P.low ? atkN : 1)) break; }
|
||||
for (const c of cols) { // 【冰凌】 its normal attack: the whole column
|
||||
const hit = b.allies().filter((u) => u.tileC === c);
|
||||
for (const c of cols) { // 【冰凌】 its normal attack: the whole column (an area selection: no unblocking 隐匿 operator)
|
||||
const hit = b.allies().filter((u) => u.tileC === c && areaSelectable(e2, u));
|
||||
b.fx('column', { x: c, y: e2.y, c, id: e2.id });
|
||||
for (const u of hit) b.dealDamage(e2, u, { amount: e2.s.atk, type: 'phys', isAttack: true });
|
||||
}
|
||||
@@ -1086,7 +1100,7 @@ function kitDeer(ab, e) {
|
||||
cd: doom.cd, icd: doom.icd,
|
||||
fire(b, e2) {
|
||||
b.fx('telegraph', { x: e2.x, y: e2.y, r: 99, kind: 'samiDoom', id: e2.id });
|
||||
for (const u of b.allies()) hurt(b, e2, u, e2.s.atk * (doom.bb.atk_scale ?? 0), 'arts');
|
||||
for (const u of fieldAllies(b, e2)) hurt(b, e2, u, e2.s.atk * (doom.bb.atk_scale ?? 0), 'arts');
|
||||
lpLoss(b, Math.abs(doom.bb.value ?? 0), 'samiDoom', e2);
|
||||
},
|
||||
},
|
||||
|
||||
+174
-72
@@ -33,15 +33,25 @@
|
||||
// keeps its ground path, Unit.isFlying; kitSyufo / kitParrot lose it when stunned). `e.profile.canTarget(ally)` = the
|
||||
// enemy's own target rule (只攻击地面单位 …), applied by the engine to the candidates before its priority order; a
|
||||
// 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
|
||||
// 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 "无视无法选择" (【污染秽蚀】, 【盲信之誓】, 萨卡兹悖谬暴虐兵长's 暴击 splash),
|
||||
// (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
|
||||
// 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). Auras of ground enemies still
|
||||
// apply [ASSUMED].
|
||||
// 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, 护障, 寒霜, 萨科塔之翼/眼, 黑云 …).
|
||||
@@ -52,14 +62,17 @@
|
||||
// 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, "可对空,无视无法选择"
|
||||
// — `ignoreSelect`, 起飞 allies included; 毒雾, 燃烧区域: a ground enemy's zone skips a 起飞 ally — PRTS
|
||||
// 集团军重型火炮 "碰撞不受迷彩制约,不可对空", no 无视无法选择), bleeding (removed by healing), pulsing auras.
|
||||
// — `ignoreSelect`, 起飞 / 隐匿 allies included; 燃烧区域: the enemy's area selection — PRTS 集团军重型火炮
|
||||
// "碰撞不受迷彩制约,不可对空", no 无视无法选择 —, so no 起飞 or unblocking 隐匿 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
|
||||
// damage skips an unblocked one too (Battle.foesInRadius: profession splash around a struck target,
|
||||
// skill circles — PRTS 作战机制 §AOE伤害判定 "对攻击范围内的每个可以被选中的敌人进行判定"; until 0.1.1 it
|
||||
// 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).
|
||||
// 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.
|
||||
@@ -80,7 +93,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, evadesGround, enemyStealthed } from '../targeting.js';
|
||||
import { canTargetAlly, sortAllyTargets, aggroCmp, enemyStealthed, areaSelectable, auraSelectable } from '../targeting.js';
|
||||
import { mitigate, periodicDamage } from '../damage.js';
|
||||
|
||||
// ---------------------------------------------------------------------------------------------------------------
|
||||
@@ -347,11 +360,12 @@ function onBurst(b, c) {
|
||||
const t = c.target;
|
||||
if (!t) return;
|
||||
if (t.side === 'enemy') { if (t.mem.ab) dispatch(b, t, 'burst', c); return; }
|
||||
// 假想敌:淤困 parasite: the host's burst spreads the same element to the 4 neighbouring allies
|
||||
// 假想敌:淤困 parasite: the host's burst spreads the same element to the 4 neighbouring allies — PRTS 天赋 "(中点判定,
|
||||
// 无视目标可选性,不受迷彩制约)": every ally there, 隐匿 / untargetable / airborne 起飞 ones included (`ignoreSelect`)
|
||||
const par = t.findBuff('ab:parasite');
|
||||
if (par && par.data && par.data.src) {
|
||||
const src = par.data.src;
|
||||
for (const o of alliesInTiles(b, t.tileR, t.tileC, 'plus', 1)) if (o !== t) elem(b, src, o, c.element, par.data.spread);
|
||||
for (const o of alliesInTiles(b, t.tileR, t.tileC, 'plus', 1)) if (o !== t) elem(b, src, o, c.element, par.data.spread, { ignoreSelect: true });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -429,15 +443,58 @@ export function allTargets(b, e) {
|
||||
return b.allies().filter((a) => canTargetAlly(e, a, true));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* 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.
|
||||
*/
|
||||
export function areaAllies(b, src, x, y, r) {
|
||||
return b.alliesInRadius(x, y, r).filter((a) => areaSelectable(src, a));
|
||||
}
|
||||
|
||||
/** alliesInTiles for an area effect of `src` (周围四格 / 周围八格 / cross / 3×3 areas — 格子判定): areaSelectable. */
|
||||
export function areaAlliesInTiles(b, src, r, c, shape = 'plus', n = 1) {
|
||||
return alliesInTiles(b, r, c, shape, n).filter((a) => areaSelectable(src, a));
|
||||
}
|
||||
|
||||
/** Every ally on the field a whole-field ability of `src` selects ("对场上所有我方单位…": 【大潮】, 【斥退】 …). */
|
||||
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. */
|
||||
export function zone(b, { x, y, r, life, iv = 1, kind = 'zone', tick }) {
|
||||
/**
|
||||
* 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 (【污染秽蚀】 "无视无法选择"); 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 });
|
||||
let left = life;
|
||||
const h = b.every(iv, () => {
|
||||
tick(b.alliesInRadius(x, y, r));
|
||||
tick(pick ? pick(x, y, r) : b.alliesInRadius(x, y, r));
|
||||
left -= iv;
|
||||
if (left <= 1e-6) h.cancel();
|
||||
});
|
||||
@@ -618,8 +675,11 @@ const maxTargets = (n) => ({ spawn(b, e) { if (n > 1) e.profile.maxTargets = n;
|
||||
const ep = (el, ratio, sil = false) => ({ sil, dealt(c, b, e) { if (ratio > 0) elem(b, e, c.target, el, e.s.atk * ratio); } });
|
||||
/** Status on every attack hit. */
|
||||
const onHitStatus = (key, dur, value) => ({ dealt(c, b, e) { if (dur > 0) b.applyStatus(c.target, key, { duration: dur, source: e, value }); } });
|
||||
/** Attack hits every targetable ally within radius r (+ optional extra selector). */
|
||||
const hitAllInRadius = (r) => ({ before(c, b, e) { const l = targetsNear(b, e, r); if (l.length) c.targets = byPriority(e, l); } });
|
||||
/**
|
||||
* The attack becomes a hit on every ally within radius r it can select as an area (深溟巢涌者 "令攻击范围内的所有我方单位每秒
|
||||
* 受到…伤害": areaAllies — a 迷彩 ally is hit, an unblocking 隐匿 one not; the attack itself still needs a target).
|
||||
*/
|
||||
const hitAllInRadius = (r) => ({ before(c, b, e) { const l = areaAllies(b, e, e.x, e.y, r); if (l.length) c.targets = byPriority(e, l); } });
|
||||
/**
|
||||
* 抵抗 (ba.buffres, the engine `resist` status: control durations halved, 麻痹 loses 1 stack every 5 s) + optional
|
||||
* immunities. Value from the talent's `Buff.one_minus_status_resistance` when present (−0.5 ⇒ half).
|
||||
@@ -657,7 +717,10 @@ const enemyAura = (r, key, mods, sil = true, iv = 0.5) => ({
|
||||
sil, iv,
|
||||
tick(b, e) { for (const o of b.enemiesInRadius(e.x, e.y, r)) if (o !== e) auraBuff(b, o, key, iv, mods); },
|
||||
});
|
||||
/** Aura on allies within r. */
|
||||
/**
|
||||
* Aura on allies within r — every ally there, 隐匿 ones included: its one user, 寒霜, is PRTS's own example of an enemy
|
||||
* debuff that "无视隐匿状态起作用" (PRTS 作战机制 §隐匿与Buff的关系: "敌方寒霜的攻速下降Debuff").
|
||||
*/
|
||||
const allyAura = (r, key, mods, sil = true, iv = 0.5, flags = null) => ({
|
||||
sil, iv,
|
||||
tick(b, e) { for (const u of b.alliesInRadius(e.x, e.y, r)) auraBuff(b, u, key, iv, mods, flags, true); },
|
||||
@@ -677,14 +740,17 @@ const deathSpawn = (key, cnt, extra = {}) => ({
|
||||
death(c, b, e) { if (c.reason === 'killed' && key && cnt > 0) spawnChildren(b, e, key, cnt, extra); },
|
||||
});
|
||||
|
||||
/** Death explosion on allies within r. */
|
||||
/**
|
||||
* Death explosion on the allies within r it can select (areaAllies — PRTS 高能源石虫 / 冰爆源石虫 / 卷心籽 死亡爆炸 "…无视迷彩,
|
||||
* 不可对空", no 无视无法选择: a 隐匿 ally is spared; the dead enemy blocks nobody).
|
||||
*/
|
||||
const deathBoom = ({ scale, type = 'phys', r = BOOM_RADIUS, status = null, sil = true, cond = null }) => ({
|
||||
sil,
|
||||
death(c, b, e, a) {
|
||||
if (c.reason !== 'killed' || (cond && !cond(e, a))) return;
|
||||
const atk = e.s.atk;
|
||||
b.fx('explode', { x: e.x, y: e.y, r, kind: 'deathBoom', id: e.id });
|
||||
for (const u of b.alliesInRadius(e.x, e.y, r)) {
|
||||
for (const u of areaAllies(b, e, e.x, e.y, r)) {
|
||||
if (scale > 0) hurt(b, e, u, atk * scale, type);
|
||||
if (status && u.alive) b.applyStatus(u, status.key, { duration: status.dur, source: e, value: status.value });
|
||||
}
|
||||
@@ -861,7 +927,8 @@ function frontGuard(cut, face) {
|
||||
}
|
||||
/** Faces its walking direction (horizontal component; keeps the last one while standing). */
|
||||
const faceMove = (b, e, a) => { const dx = e.x - a.px; if (Math.abs(dx) > 1e-6) a.facing = dx > 0 ? 1 : -1; a.px = e.x; };
|
||||
/** "始终面向我方干员数量较多的方向": the side with more deployed operators (ties keep the current facing). */
|
||||
/** "始终面向我方干员数量较多的方向": the side with more deployed operators (ties keep the current facing; PRTS 圆仔 "无视其可选性":
|
||||
* 隐匿 operators count). */
|
||||
const faceCrowd = (b, e, a) => {
|
||||
let l = 0, r = 0;
|
||||
for (const u of b.allies()) { if (u.kind !== 'op') continue; if (u.x > e.x + 1e-6) r++; else if (u.x < e.x - 1e-6) l++; }
|
||||
@@ -1049,12 +1116,14 @@ function pollution(b, src, x, y, r, life, low, high) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* 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, 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] });
|
||||
@@ -1189,7 +1258,8 @@ function kitChimera(ab) {
|
||||
auraBuff(b, u, 'ab:originium', 0.25, { spRecoveryMul: Math.max(0, 1 + spr) }, null, true);
|
||||
// PRTS "持续视为受到源石污染区影响…每0.5秒受到50真实持续伤害": a damage instance (受击回复, the 重装 trigger), not a
|
||||
// 流失 — 无来源 like the 源石污染区 terrain it stands for [ASSUMED], the chimera keeps the credit; "同类效果取最高": one
|
||||
// tick per unit per period however many chimeras reach it (`mem.chimeraAt`; every chimera's is 50)
|
||||
// tick per unit per period however many chimeras reach it (`mem.chimeraAt`; every chimera's is 50). A terrain
|
||||
// effect, not an area selection: 隐匿 allies inside are reached too ("所有单位" [ASSUMED], DESIGN §22.12)
|
||||
if (!pulse || !(dmg > 0) || b.time - (u.mem.chimeraAt ?? -Infinity) < CHIMERA_AURA_EVERY - 1e-6) continue;
|
||||
u.mem.chimeraAt = b.time;
|
||||
b.dealDamage(e, u, { ...periodicDamage(dmg), tags: ['dot', 'pollution'] });
|
||||
@@ -1212,10 +1282,12 @@ function kitNazg(ab, e) {
|
||||
function kitTidmag(ab) {
|
||||
return [ep('erosion', T(ab, 'EpDamage.attack@ep_damage_ratio') ?? 0), {
|
||||
before(c, b, e) {
|
||||
// PRTS 控潮术师 "对目标所在地块及周围四格内的所有我方单位造成法术普通伤害": the 周围四格 are an area selection (格子判定:
|
||||
// a 迷彩 ally is hit; an unblocking 隐匿 or an airborne 起飞 one is not — areaAlliesInTiles)
|
||||
const t0 = c.targets[0];
|
||||
if (!t0) return;
|
||||
const l = c.targets.slice();
|
||||
for (const u of alliesInTiles(b, t0.tileR, t0.tileC, 'plus', 1)) if (!l.includes(u) && !evadesGround(e, u)) l.push(u);
|
||||
for (const u of areaAlliesInTiles(b, e, t0.tileR, t0.tileC, 'plus', 1)) if (!l.includes(u)) l.push(u);
|
||||
c.targets = l;
|
||||
},
|
||||
}];
|
||||
@@ -1342,7 +1414,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 b.alliesInRadius(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 })];
|
||||
@@ -1445,7 +1518,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;
|
||||
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);
|
||||
},
|
||||
}];
|
||||
}
|
||||
@@ -1504,6 +1579,7 @@ function kitExposeOnHit(ab) {
|
||||
return [{ sil: true, taken(c, b) { const s = c.source; if (s && s.side === 'ally' && s.alive && s.kind !== 'device') expose(b, s, dur, scale); } }];
|
||||
}
|
||||
|
||||
/** 远眺: knocked out ⇒ 暴露 on every ally within r — PRTS 天赋 "(可被沉默;无视其可选性)": 隐匿 / untargetable ones too. */
|
||||
function kitExposeOnDeath(ab) {
|
||||
const dur = T(ab, 'Expose.weak[limit]') ?? 0, scale = T(ab, 'Expose.damage_scale') ?? 1, r = T(ab, 'Expose.range_radius') ?? 1;
|
||||
return [{ sil: true, death(c, b, e) { if (c.reason !== 'killed') return; b.fx('explode', { x: e.x, y: e.y, r, kind: 'expose' }); for (const u of b.alliesInRadius(e.x, e.y, r)) expose(b, u, dur, scale); } }];
|
||||
@@ -1640,11 +1716,13 @@ function kitSteal(ab) {
|
||||
}];
|
||||
}
|
||||
|
||||
/** “萨科塔昂首” 【祈祷邀约】 (PRTS "令全场我方单位(不可对空,无视迷彩)获得15s【受邀祈祷】攻击速度-30"): a whole-field skill
|
||||
* selection — every ally but an unblocking 隐匿 / untargetable / sleeping one (fieldAllies; no 无视无法选择). */
|
||||
function kitRoar(ab) {
|
||||
const s = ab.sk.Roar;
|
||||
return [selfFear(ab), skill(s, (b, e) => {
|
||||
b.fx('telegraph', { x: e.x, y: e.y, r: 99, kind: 'roar', id: e.id });
|
||||
for (const u of b.allies()) b.addBuff(u, { key: 'ab:roar', duration: s.bb.duration ?? 0, refresh: 'extend', mods: { aspd: s.bb.attack_speed ?? 0 }, visible: true });
|
||||
for (const u of fieldAllies(b, e)) b.addBuff(u, { key: 'ab:roar', duration: s.bb.duration ?? 0, refresh: 'extend', mods: { aspd: s.bb.attack_speed ?? 0 }, visible: true });
|
||||
}, { sil: true })];
|
||||
}
|
||||
|
||||
@@ -1678,7 +1756,8 @@ function kitBombd(ab) {
|
||||
const r = t ? t.tileR : Math.round(y), c = t ? t.tileC : Math.round(x);
|
||||
b.fx('explode', { x, y, r: BOMB_REACH + 0.5, kind: 'bomb', tiles: 'box' });
|
||||
if (t) hurt(b, e, t, atk, 'phys');
|
||||
for (const u of alliesInTiles(b, r, c, 'box', BOMB_REACH)) if (u !== t) hurt(b, e, u, atk, 'phys', { tags: ['splash'] });
|
||||
// the splash on the 8 tiles is an area selection ("伤害无视迷彩", no 无视无法选择): no unblocking 隐匿 ally
|
||||
for (const u of areaAlliesInTiles(b, e, r, c, 'box', BOMB_REACH)) if (u !== t) hurt(b, e, u, atk, 'phys', { tags: ['splash'] });
|
||||
};
|
||||
// the end of the cast (bomb_s): 移速最终提升至200%; it flies on
|
||||
const finish = (b, e) => {
|
||||
@@ -1727,7 +1806,8 @@ function kitBombd(ab) {
|
||||
/**
|
||||
* 帝国炮火先兆者 / 中枢先兆者 (PRTS): every normal attack fires a shell at the target's position that lands SHELL_FLIGHT s
|
||||
* later and deals 100 % of the ATK at launch as physical damage without a source to every ally within SHELL_RADIUS
|
||||
* (it may miss the target that moved away; camouflage does not matter). The attack itself is the engine's (cooldown,
|
||||
* (it may miss the target that moved away; "碰撞无视迷彩"). The landing is the shell's area selection (areaAllies of the
|
||||
* enemy that fired it: no unblocking 隐匿 ally; the damage stays 无来源). The attack itself is the engine's (cooldown,
|
||||
* pause, 'atk' event of kind 'mortar'); the damage is the shell's (ai.js `profile.deferHit`).
|
||||
*/
|
||||
function kitShell() {
|
||||
@@ -1740,7 +1820,7 @@ function kitShell() {
|
||||
b.fx('bombardShell', { x, y, id: e.id, r: SHELL_RADIUS, t: SHELL_FLIGHT });
|
||||
b.after(SHELL_FLIGHT, () => {
|
||||
b.fx('bombard', { x, y, r: SHELL_RADIUS, kind: 'emppnt' });
|
||||
for (const u of b.alliesInRadius(x, y, SHELL_RADIUS)) hurt(b, null, u, atk, 'phys', { isSkill: false, tags: ['shell'] });
|
||||
for (const u of areaAllies(b, e, x, y, SHELL_RADIUS)) hurt(b, null, u, atk, 'phys', { isSkill: false, tags: ['shell'] });
|
||||
});
|
||||
}
|
||||
},
|
||||
@@ -1765,11 +1845,12 @@ function kitFlameVine(ab) {
|
||||
dealt(c, b, e, a) {
|
||||
const t = c.target;
|
||||
if (a.first) {
|
||||
// 首次攻击 (PRTS 灼藤): every other ally of the target's 3×3 (range x-4, tile test) takes 100 % ATK arts splash and
|
||||
// everyone there 25 % ATK burn; then the ATK ramp ends and later attacks add 20 %
|
||||
// 首次攻击 (PRTS 灼藤): every other ally of the target's 3×3 (range x-4, "格子判定,不受迷彩制约") takes 100 % ATK
|
||||
// arts splash and everyone there 25 % ATK burn — an area selection (no unblocking 隐匿 ally); then the ATK ramp
|
||||
// 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 alliesInTiles(b, 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);
|
||||
},
|
||||
@@ -1801,16 +1882,17 @@ 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));
|
||||
// an area selection (PRTS 烹泉 "死亡爆炸(…无视迷彩,不可对空)", no 无视无法选择): an airborne 起飞 ally and an
|
||||
// unblocking 隐匿 one are skipped, the steam's ticks too (the blast's debuff)
|
||||
const pick = (zx, zy, zr) => areaAllies(b, e, zx, zy, zr);
|
||||
// 被击倒后爆炸造成范围法术伤害 (one blast of ATK ×1 [ASSUMED scale]) …
|
||||
b.fx('explode', { x, y, r, kind: 'teaBoom', id: e.id });
|
||||
for (const u of hit(b.alliesInRadius(x, y, r))) hurt(b, null, u, atk, 'arts', { tags: ['teaBoom'] });
|
||||
for (const u of pick(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 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 });
|
||||
const slow = (units) => { for (const u of units) b.addBuff(u, { key: 'ab:teaBoom', duration: iv + 0.1, refresh: 'extend', mods: { aspd }, visible: true }); };
|
||||
slow(pick(x, y, r));
|
||||
if (dur > 0) zone(b, { x, y, r, life: dur, iv, kind: 'teaSteam', tick: slow, pick });
|
||||
},
|
||||
}])(ab);
|
||||
}
|
||||
@@ -1819,7 +1901,8 @@ function kitNucleus(ab) {
|
||||
return [{
|
||||
taken(c, b, e, a) { if (a.on) return; a.on = true; b.addBuff(e, { key: 'ab:combat', persist: true, visible: true, mods: { moveMul: T(ab, '0.move_speed') ?? 1 } }); },
|
||||
iv: T(ab, '1.interval') ?? 1,
|
||||
tick(b, e, a) { if (!a.on) return; for (const u of b.alliesInRadius(e.x, e.y, T(ab, '1.range_radius') ?? 0)) elem(b, e, u, 'neural', e.s.atk * (T(ab, '1.ep_damage_ratio') ?? 0)); },
|
||||
// 【孽生者的神经毒素】 "(无视迷彩,同名效果不叠加…)": an area selection (no unblocking 隐匿 ally)
|
||||
tick(b, e, a) { if (!a.on) return; for (const u of areaAllies(b, e, e.x, e.y, T(ab, '1.range_radius') ?? 0)) elem(b, e, u, 'neural', e.s.atk * (T(ab, '1.ep_damage_ratio') ?? 0)); },
|
||||
}];
|
||||
}
|
||||
|
||||
@@ -1838,7 +1921,7 @@ function kitLeaderMisc(key, ab, e) {
|
||||
tick(b, e2) {
|
||||
if (!e2.blockedBy) return; // 被阻挡时会进行自爆造成高额伤害
|
||||
b.fx('explode', { x: e2.x, y: e2.y, r: 1, kind: 'selfBlast' });
|
||||
for (const u of b.alliesInRadius(e2.x, e2.y, 1)) hurt(b, e2, u, T(ab, 'rangedamage.attack@damage') ?? 0, 'arts');
|
||||
for (const u of areaAllies(b, e2, e2.x, e2.y, 1)) hurt(b, e2, u, T(ab, 'rangedamage.attack@damage') ?? 0, 'arts');
|
||||
},
|
||||
}];
|
||||
}
|
||||
@@ -1846,7 +1929,8 @@ function kitLeaderMisc(key, ab, e) {
|
||||
return [lowHpBuff(T(ab, 'atkup.hp_ratio') ?? 0.5, { atkPct: T(ab, 'atkup.atk') ?? 0 }), {
|
||||
dealt(c, b, e2) {
|
||||
if (e2.blockedBy) return; // 未被阻挡时会发射榴弹
|
||||
for (const u of b.alliesInRadius(c.target.x, c.target.y, 1)) {
|
||||
// the grenade's splash: an area selection — a 隐匿 ally next to the target is spared (GitHub issue #32 item 6)
|
||||
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) });
|
||||
}
|
||||
@@ -1864,7 +1948,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 b.alliesInRadius(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 })];
|
||||
}
|
||||
@@ -1878,7 +1963,8 @@ function kitLeaderMisc(key, ab, e) {
|
||||
const t = allTargets(b, e2).sort((p, q) => q.s.maxHp - p.s.maxHp || aggroCmp(p, q))[0];
|
||||
if (!t) return;
|
||||
b.fx('telegraph', { x: t.tileC, y: t.tileR, r: DRIFT_REACH, kind: 'driftSand', tiles: 'plus', id: e2.id });
|
||||
for (const u of alliesInTiles(b, t.tileR, t.tileC, 'plus', DRIFT_REACH)) hurt(b, e2, u, ds.bb.damage ?? 0, 'phys');
|
||||
// the cross ("伤害无视迷彩", no 无视无法选择): an area selection
|
||||
for (const u of areaAlliesInTiles(b, e2, t.tileR, t.tileC, 'plus', DRIFT_REACH)) hurt(b, e2, u, ds.bb.damage ?? 0, 'phys');
|
||||
}, { cond: (b, e2) => allTargets(b, e2).length > 0 }),
|
||||
// 【沙狱】 the lowest-max-HP unit and those around it: ATK −`atk` and `damage` arts per second for `duration` s
|
||||
skill(ss, (b, e2) => {
|
||||
@@ -1886,9 +1972,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;
|
||||
// the 沙狱弹道 "击中…范围内的所有我方单位(弹道可对空)" selects: an airborne 起飞 ally and an unblocking 隐匿 one
|
||||
// are skipped (no 无视无法选择 — areaAllies)
|
||||
for (const u of areaAllies(b, e2, x, y, SANDSTORM_RADIUS)) {
|
||||
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, ignoreSelect: true, tags: ['enemyAbility', 'sandStorm'] }) });
|
||||
}
|
||||
@@ -1908,7 +1994,8 @@ function kitLeaderMisc(key, ab, e) {
|
||||
case 'enemy_1513_dekght': {
|
||||
// 攻击时使目标与周围四格的单位受到物理伤害; 【蓄力攻击】 charge `duration` s, then ATK×atk_scale on the target's cross
|
||||
const s = ab.sk.ChargeAttack;
|
||||
return [kitDekght(ab), { before(c, b, e2) { const t = c.targets[0]; if (t) for (const u of alliesInTiles(b, t.tileR, t.tileC, 'plus', 1)) if (!c.targets.includes(u) && !evadesGround(e2, u)) c.targets.push(u); } },
|
||||
// the 周围四格 of every attack (PRTS "对溅射目标造成物理溅射伤害") and 【蓄力锤】's cross: area selections
|
||||
return [kitDekght(ab), { before(c, b, e2) { const t = c.targets[0]; if (t) for (const u of areaAlliesInTiles(b, e2, t.tileR, t.tileC, 'plus', 1)) if (!c.targets.includes(u)) c.targets.push(u); } },
|
||||
skill(s, (b, e2) => {
|
||||
const t = e2.blockedBy;
|
||||
const dur = s.bb.duration ?? 0, r = t.tileR, cc = t.tileC;
|
||||
@@ -1917,7 +2004,7 @@ function kitLeaderMisc(key, ab, e) {
|
||||
b.after(dur, () => {
|
||||
if (!e2.alive || e2.s.flags.stun) return;
|
||||
b.fx('explode', { x: cc, y: r, r: 1, kind: 'chargeAttack', tiles: 'plus' });
|
||||
for (const u of alliesInTiles(b, r, cc, 'plus', 1)) hurt(b, e2, u, e2.s.atk * (s.bb['dekght[aoe].atk_scale'] ?? s.bb.atk_scale ?? 1), 'phys');
|
||||
for (const u of areaAlliesInTiles(b, e2, r, cc, 'plus', 1)) hurt(b, e2, u, e2.s.atk * (s.bb['dekght[aoe].atk_scale'] ?? s.bb.atk_scale ?? 1), 'phys');
|
||||
}, { owner: e2 });
|
||||
}, { cond: (b, e2) => !!(e2.blockedBy && e2.blockedBy.alive) })];
|
||||
}
|
||||
@@ -1933,7 +2020,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 alliesInTiles(b, 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 })];
|
||||
@@ -1990,7 +2078,8 @@ function kitLeaderMisc(key, ab, e) {
|
||||
}];
|
||||
case 'enemy_2008_flking': {
|
||||
// 使全战场我方所有单位攻击力、防御力减半 (text; the blackboard's atkdown.atk_scale is not the halving),
|
||||
// 部署费用回复速度减半,再部署时间加倍; 周期性地添加正比于自身最大生命值的伤害防护屏障
|
||||
// 部署费用回复速度减半,再部署时间加倍; 周期性地添加正比于自身最大生命值的伤害防护屏障. The halving is a map effect
|
||||
// (PRTS “墓碑” "※文字描述中的削弱实际属于地图效果,不属于敌人本身的能力"): every ally, 隐匿 / airborne ones included
|
||||
const s = ab.sk.refreshshield;
|
||||
return [{
|
||||
iv: 0.5,
|
||||
@@ -2006,7 +2095,8 @@ function kitLeaderMisc(key, ab, e) {
|
||||
}, skill(s, (b, e2) => b.addBuff(e2, { key: 'ab:tombShield', shield: e2.s.maxHp * (s.bb.hp_ratio ?? 0), persist: true }))];
|
||||
}
|
||||
case 'enemy_2048_smgrd':
|
||||
// 持续在自身周围8格生成【国度】 (the 3×3 around its tile), 大幅降低其中我方单位的攻击速度; 对【国度】中的我方单位造成高额物理伤害
|
||||
// 持续在自身周围8格生成【国度】 (the 3×3 around its tile), 大幅降低其中我方单位的攻击速度; 对【国度】中的我方单位造成高额物理伤害.
|
||||
// 【国度】 is a tile effect ("将该地块及其周围8格范围内的可部署位生成【国度】"): every ally on it, 隐匿 ones too [ASSUMED]
|
||||
return [{
|
||||
iv: 0.5,
|
||||
tick(b, e2) { for (const u of alliesInTiles(b, Math.round(e2.y), Math.round(e2.x), 'box', 1)) auraBuff(b, u, 'ab:blackFog', 0.5, { aspd: T(ab, 'BlackFog.attack_speed') ?? 0 }, null, true); },
|
||||
@@ -2024,7 +2114,8 @@ function kitLeaderMisc(key, ab, e) {
|
||||
let prev = c.target;
|
||||
const hit = new Set([prev]);
|
||||
for (let k = 1; k < n; k++) {
|
||||
const nx = b.alliesInRadius(prev.x, prev.y, jr).find((u) => !hit.has(u) && !evadesGround(e2, u));
|
||||
// a jump selects within jr of the last target (中点判定): no unblocking 隐匿 or airborne 起飞 ally (areaAllies)
|
||||
const nx = areaAllies(b, e2, prev.x, prev.y, jr).find((u) => !hit.has(u));
|
||||
if (!nx) break;
|
||||
hit.add(nx);
|
||||
hurt(b, e2, nx, e2.s.atk * Math.pow(fall, k), 'arts');
|
||||
@@ -2095,9 +2186,9 @@ function kitUglyThing(ab) {
|
||||
{ hitOut(c, b, e) { if (!P.pilot && c.dmg.isAttack && c.target === e.blockedBy) c.dmg.amount *= rage; } }, // 近战攻击造成更高伤害
|
||||
reborn({
|
||||
dur: T(ab, 'reborn.duration') ?? 0,
|
||||
onKo(b, e) { // 生命值降至0后自爆,造成大范围晕眩
|
||||
onKo(b, e) { // 生命值降至0后自爆,造成大范围晕眩 (an area selection)
|
||||
b.fx('explode', { x: e.x, y: e.y, r: MCM_BOMB_RADIUS, kind: 'mechBomb', id: e.id });
|
||||
for (const u of b.alliesInRadius(e.x, e.y, MCM_BOMB_RADIUS)) {
|
||||
for (const u of areaAllies(b, e, e.x, e.y, MCM_BOMB_RADIUS)) {
|
||||
hurt(b, e, u, e.s.atk * (bomb.atk_scale ?? 0), 'phys');
|
||||
if (u.alive && bomb.stun > 0) b.applyStatus(u, 'stun', { duration: bomb.stun, source: e });
|
||||
}
|
||||
@@ -2165,7 +2256,8 @@ function kitXi(ab) {
|
||||
const crossAt = (b, e, t) => {
|
||||
const r0 = t.tileR, c0 = t.tileC;
|
||||
b.fx('telegraph', { x: c0, y: r0, r: XI_CROSS_REACH, kind: 'xiCross', tiles: 'plus', id: e.id });
|
||||
for (const u of alliesInTiles(b, r0, c0, 'plus', XI_CROSS_REACH)) if (u.tileR === r0 || u.tileC === c0) hurt(b, e, u, e.s.atk * ((cross && cross.bb.atk_scale) ?? 1), 'arts');
|
||||
// "伤害无视迷彩", no 无视无法选择: the cross is an area selection
|
||||
for (const u of areaAlliesInTiles(b, e, r0, c0, 'plus', XI_CROSS_REACH)) if (u.tileR === r0 || u.tileC === c0) hurt(b, e, u, e.s.atk * ((cross && cross.bb.atk_scale) ?? 1), 'arts');
|
||||
};
|
||||
const burst = (s) => (b, e) => {
|
||||
const dur = s.bb.duration ?? 0, r = s.bb.range_radius ?? 0;
|
||||
@@ -2175,7 +2267,7 @@ function kitXi(ab) {
|
||||
onExpire: ({ battle }) => {
|
||||
if (!e.alive) return;
|
||||
battle.fx('explode', { x: e.x, y: e.y, r, kind: 'breakFree', id: e.id });
|
||||
for (const u of battle.alliesInRadius(e.x, e.y, r)) hurt(battle, e, u, e.s.atk * (s.bb.atk_scale ?? 0), 'arts');
|
||||
for (const u of areaAllies(battle, e, e.x, e.y, r)) hurt(battle, e, u, e.s.atk * (s.bb.atk_scale ?? 0), 'arts');
|
||||
} });
|
||||
};
|
||||
const re = reborn({
|
||||
@@ -2206,22 +2298,24 @@ function kitMace(ab) {
|
||||
const cs = { ratio: T(ab, 'ClearSp.hp_ratio') ?? 0, dur: T(ab, 'ClearSp.duration') ?? 0, r: T(ab, 'ClearSp.range_radius') ?? 0 };
|
||||
const P = { form2: false, next: 1 };
|
||||
const penOn = (c, e) => (c.target === e.blockedBy ? (P.form2 ? pen2 : pen1) : 0);
|
||||
// its blocker is hit (PRTS 技能 "对之前阻挡自身的单位造成…"); the others on the way are an area selection (areaSelectable)
|
||||
const blink = (s) => (b, e) => {
|
||||
const bl = e.blockedBy;
|
||||
const from = blinkForward(b, e, s.bb.dist ?? 1.5);
|
||||
if (!from) return;
|
||||
for (const u of b.allies()) {
|
||||
if (u !== bl && segDist(u.x, u.y, from.x, from.y, e.x, e.y) > 0.5) continue;
|
||||
if (u !== bl && (segDist(u.x, u.y, from.x, from.y, e.x, e.y) > 0.5 || !areaSelectable(e, u))) continue;
|
||||
hurt(b, e, u, e.s.atk * (s.bb.atk_scale ?? 1), 'phys');
|
||||
}
|
||||
};
|
||||
const circle = (s) => (b, e) => {
|
||||
const r = s.bb.range_radius ?? 0;
|
||||
b.fx('explode', { x: e.x, y: e.y, r, kind: 'blizzard', id: e.id });
|
||||
for (const u of b.alliesInRadius(e.x, e.y, r)) hurt(b, e, u, e.s.atk * (s.bb.atk_scale ?? 1), 'phys');
|
||||
for (const u of areaAllies(b, e, e.x, e.y, r)) hurt(b, e, u, e.s.atk * (s.bb.atk_scale ?? 1), 'phys');
|
||||
};
|
||||
// someone the circle can hurt: an airborne (起飞) operator evades a ground enemy (对地规避; damage.js skips her)
|
||||
const inR = (b, e, s) => b.alliesInRadius(e.x, e.y, (s && s.bb.range_radius) || 0).some((u) => !evadesGround(e, u));
|
||||
// cast with a target in the circle — the trigger selection (targetsNear: PRTS 选择器 "所有触发选择器通常不无视迷彩"): no
|
||||
// airborne (起飞) operator for a ground enemy (§21.22), no unblocking 隐匿 or 迷彩 one
|
||||
const inR = (b, e, s) => targetsNear(b, e, (s && s.bb.range_radius) || 0).length > 0;
|
||||
return [resist(), {
|
||||
hitOut(c, b, e) { const p = penOn(c, e); if (c.dmg.isAttack && p > 0) c.dmg.defIgnorePct = Math.min(1, (c.dmg.defIgnorePct || 0) + p); },
|
||||
taken(c, b, e) { // 【瞬息杀机】 every hp_ratio of max HP lost (second form)
|
||||
@@ -2229,7 +2323,7 @@ function kitMace(ab) {
|
||||
while (e.alive && e.hpRatio <= P.next - cs.ratio + 1e-9 && P.next - cs.ratio > 1e-9) {
|
||||
P.next -= cs.ratio;
|
||||
b.fx('explode', { x: e.x, y: e.y, r: cs.r, kind: 'clearSp', id: e.id });
|
||||
for (const u of b.alliesInRadius(e.x, e.y, cs.r)) {
|
||||
for (const u of areaAllies(b, e, e.x, e.y, cs.r)) {
|
||||
const sk = u.skill;
|
||||
if (sk && !sk.noSkill && sk.kind !== 'passive' && !sk.active && sk.spCost > 0) { sk.sp = 0; sk.charges = 0; } // 清空技力 (stored charges too)
|
||||
if (cs.dur > 0) b.applyStatus(u, 'disarm', { duration: cs.dur, source: e });
|
||||
@@ -2290,7 +2384,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)));
|
||||
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;
|
||||
@@ -2503,6 +2598,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);
|
||||
@@ -2518,7 +2615,7 @@ export const KITS = Object.freeze({
|
||||
tick(b, e2) {
|
||||
const r = (e2.def.raw.stats && e2.def.raw.stats.rawRangeRadius) || 1.6;
|
||||
b.fx('explode', { x: e2.x, y: e2.y, r, kind: 'boil' });
|
||||
for (const u of b.alliesInRadius(e2.x, e2.y, r)) { hurt(b, e2, u, e2.s.atk * (T(ab, 'aoe.atk_scale') ?? 1), 'arts'); elem(b, e2, u, 'burn', e2.s.atk * (T(ab, 'aoe.ep_damage_ratio') ?? 0)); }
|
||||
for (const u of areaAllies(b, e2, e2.x, e2.y, r)) { hurt(b, e2, u, e2.s.atk * (T(ab, 'aoe.atk_scale') ?? 1), 'arts'); elem(b, e2, u, 'burn', e2.s.atk * (T(ab, 'aoe.ep_damage_ratio') ?? 0)); }
|
||||
},
|
||||
}],
|
||||
|
||||
@@ -2619,7 +2716,9 @@ export const KITS = Object.freeze({
|
||||
for (let i = 1; i <= CANNON_SHOTS; i++) b.after(i * CANNON_EVERY, () => {
|
||||
if (!e.alive) return;
|
||||
if (i === CANNON_SHOTS) ab2.immune = had;
|
||||
const t = b.allies().filter((a) => canTargetAlly(e, a, true) && Math.abs(Math.round(a.y) - r0) <= 1 && Math.abs(Math.round(a.x) - c0) <= 1)
|
||||
// each shot picks inside the 9-tile zone (格子判定): an area selection — a 迷彩 ally may be shot, an unblocking 隐匿
|
||||
// one not (areaSelectable; it used to skip 迷彩 like a normal attack)
|
||||
const t = b.allies().filter((a) => areaSelectable(e, a) && Math.abs(Math.round(a.y) - r0) <= 1 && Math.abs(Math.round(a.x) - c0) <= 1)
|
||||
.sort((p, q) => q.hpRatio - p.hpRatio)[0];
|
||||
if (t) { b.fx('explode', { x: t.x, y: t.y, r: 0.5, kind: 'cannon' }); hurt(b, e, t, e.s.atk * (ab.sk.Cannon.bb.atk_scale ?? 0), 'arts'); }
|
||||
}, { owner: e });
|
||||
@@ -2673,8 +2772,9 @@ 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++) {
|
||||
// "对碰撞范围内的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];
|
||||
// "对碰撞范围内的1名当前生命值最高的我方单位" — an area selection: never an airborne 起飞 ally nor an unblocking
|
||||
// 隐匿 one (PRTS “帝国的甲胄”; areaAlliesInTiles)
|
||||
const t = areaAlliesInTiles(b, e, lock.tileR, lock.tileC, 'box', 1).sort((p, q) => q.hp - p.hp)[0];
|
||||
if (t) hurt(b, e, t, e.s.atk, 'phys');
|
||||
}
|
||||
}, { owner: e });
|
||||
@@ -2734,7 +2834,8 @@ export const KITS = Object.freeze({
|
||||
sil: true, cd: ab.sk.SwitchModeTrigger ? ab.sk.SwitchModeTrigger.cd : 60, icd: ab.sk.SwitchModeTrigger ? ab.sk.SwitchModeTrigger.icd : 10,
|
||||
fire(b, e, a) { a.until = b.time + (T(ab, 'EndRotate.rotate_duration') ?? 0); b.fx('telegraph', { x: e.x, y: e.y, r: 1, kind: 'spin', id: e.id }); },
|
||||
iv: T(ab, 'RotateDamage.interval') ?? 1,
|
||||
tick(b, e, a) { if (!(a.until > b.time)) return; for (const u of b.alliesInRadius(e.x, e.y, T(ab, 'RotateDamage.attack@range_radius') ?? 1)) hurt(b, e, u, e.s.atk * (T(ab, 'RotateDamage.attack@atk_scale') ?? 1), 'phys'); },
|
||||
// "每秒对半径1.0范围内的所有我方单位造成…(无视迷彩,不可对空)": an area selection
|
||||
tick(b, e, a) { if (!(a.until > b.time)) return; for (const u of areaAllies(b, e, e.x, e.y, T(ab, 'RotateDamage.attack@range_radius') ?? 1)) hurt(b, e, u, e.s.atk * (T(ab, 'RotateDamage.attack@atk_scale') ?? 1), 'phys'); },
|
||||
};
|
||||
return [spin, unbalanced((b, e) => { if (spin.until > b.time) { spin.until = 0; b.fx('phase', { x: e.x, y: e.y, id: e.id, kind: 'spinStop' }); } })];
|
||||
},
|
||||
@@ -2759,7 +2860,8 @@ export const KITS = Object.freeze({
|
||||
const n = T(ab, 'Attack.attack@times') ?? 1, r = T(ab, 'Attack.attack@projectile_range') ?? 0;
|
||||
const t = c.target;
|
||||
for (let i = 1; i < n; i++) hurt(b, e, t, e.s.atk, 'phys');
|
||||
if (r > 0) for (const u of b.alliesInRadius(t.x, t.y, r)) if (u !== t) for (let i = 0; i < n; i++) hurt(b, e, u, e.s.atk, 'phys');
|
||||
// the stone's splash ("碰撞无视迷彩"): an area selection — no unblocking 隐匿 ally
|
||||
if (r > 0) for (const u of areaAllies(b, e, t.x, t.y, r)) if (u !== t) for (let i = 0; i < n; i++) hurt(b, e, u, e.s.atk, 'phys');
|
||||
},
|
||||
}],
|
||||
enemy_1050_lslime: (ab, e) => kitLeaderMisc('enemy_1050_lslime', ab, e), // “庞贝” · 4 targets, burning DoT, self-blast when blocked, ASPD up below half
|
||||
|
||||
+48
-5
@@ -11,6 +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).
|
||||
|
||||
import { COLS, ROWS } from './constants.js';
|
||||
import { normDir, rotateOffset } from './dir.js';
|
||||
@@ -89,9 +91,10 @@ export function canTargetEnemy(attacker, e, profile) {
|
||||
* 概念 "敌人会在自身被干员阻挡情况下强行无视对方可选性发动攻击" (the term text "不阻挡时…" is the short form; 异常效果:
|
||||
* "隐匿与'阻挡时解除'没有直接关系"). 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 "与'阻挡时解除'没有直接关系" [ASSUMED: enemy skills and splash selectors
|
||||
* treat 迷彩 like target selection — officially splash and selectors without a projectile ignore it]. An airborne ally
|
||||
* (起飞, flag `liftoff`) is never a target of a ground enemy (evadesGround).
|
||||
* 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
|
||||
* 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;
|
||||
@@ -102,6 +105,46 @@ export function canTargetAlly(e, a, ranged) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 异常效果 §无法选择: 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 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 无法选择
|
||||
* [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;
|
||||
return !(f.stealth && !(src && src.alive && src.blockedBy === a));
|
||||
}
|
||||
|
||||
/**
|
||||
* 对地规避 of an airborne ally (起飞, buff flag `liftoff` — 蒂比's skills): true when `src` is an enemy whose 行动方式 is
|
||||
* ground, which then cannot select `a` (gamedata_const ba.liftoff "不阻挡地面敌人且不会被地面敌人攻击,可以阻挡飞行敌人";
|
||||
@@ -110,8 +153,8 @@ export function canTargetAlly(e, a, ranged) {
|
||||
* 行动方式: a hovering unit "是真正的飞行单位", a levitated one is seen as a flyer) still select it; it stays a ground unit
|
||||
* itself ("起飞的干员仍然是地面单位"), so they need no 对空 check. Used by every enemy selection (canTargetAlly), the damage
|
||||
* 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:
|
||||
* 的判定是对攻击范围内的每个可以被选中的敌人进行判定"), and every enemy area selection (areaSelectable: splash, blasts,
|
||||
* chain / bounce jumps, 周围四格 additions, barrages, 沙狱, death blasts). Not selections, so they still reach it:
|
||||
* 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
|
||||
|
||||
@@ -747,9 +747,11 @@ test('batch 6 QA residuals (DESIGN §21.21–§21.25): the lock per deployment f
|
||||
assert.ok(!/the carrier's own non-initial `deploy` re-arms it/.test(SIM), 'SIM: the per-grant deploy hook is gone');
|
||||
assert.match(PLAYING, /被 M3茧甲 \/ 埃芒加德复活)后又能锁一次,娜仁图亚策略借来的锤子也一样/);
|
||||
assert.ok(!/First time per battle carrier would take lethal damage/.test(doc('docs/research/04-items.md')), 'research 04: once per deployment');
|
||||
// F3: 卢西恩 / 锏 count only the allies they can hurt; the player text keeps auras and counters
|
||||
assert.match(doc('server/sim/content/bosses.js'), /LUCIEN_AOE_RADIUS\)\.some\(\(u\) => !evadesGround\(e, u\)\)/);
|
||||
assert.match(doc('server/sim/content/enemies.js'), /const inR = \(b, e, s\) => b\.alliesInRadius\([^\n]*\.some\(\(u\) => !evadesGround\(e, u\)\)/);
|
||||
// F3: 卢西恩 / 锏 count only the allies they can hurt — since 0.1.2 (§22.12) the targets of their trigger selection
|
||||
// (targetsNear → canTargetAlly, which skips an airborne 起飞 ally for a ground enemy); the player text keeps auras and counters
|
||||
assert.match(doc('server/sim/content/bosses.js'), /cond: \(b\) => targetsNear\(b, e, LUCIEN_AOE_RADIUS\)\.length > 0/);
|
||||
assert.match(doc('server/sim/content/enemies.js'), /const inR = \(b, e, s\) => targetsNear\(b, e, [^\n]*\)\.length > 0/);
|
||||
assert.match(doc('server/sim/targeting.js'), /if \(f\.liftoff && evadesGround\(e, a\)\) return false;/);
|
||||
assert.match(sub(22), /they count only the allies they can hurt \(`!evadesGround`\)/);
|
||||
assert.match(sub(20), /an area skill cast because allies are near counts only those it can hurt/);
|
||||
assert.ok(!/燃烧区域和减益都落不到她身上/.test(PLAYING), 'PLAYING: no blanket 减益 claim');
|
||||
|
||||
@@ -0,0 +1,359 @@
|
||||
// GitHub issue #32 item 6 (after 0.1.1): 「隐蔽单位仍会受到溅射伤害」 — a 隐匿 operator still took enemy splash.
|
||||
//
|
||||
// Official: PRTS 作战机制 §隐匿 "隐匿效果使得获得该效果的单位无法被任何敌方的能力索敌选中" (a stealthed operator that blocks an
|
||||
// enemy is attacked by it whatever its selectability: "因为“阻挡优先级最高”的效果,敌人会无视一切可选性对该干员进行攻击");
|
||||
// §AOE伤害判定 "AOE的判定是对攻击范围内的每个可以被选中的敌人进行判定"; §隐匿与Buff的关系 "隐匿状态下的单位一般无法被敌方的索敌
|
||||
// 机制和Buff选择器选中为目标" (寒霜's debuff being its example of one that ignores 隐匿); PRTS 异常效果 §无法选择: a unit with
|
||||
// 隐匿 / 不可选中 / 无敌 / 对地规避 is skipped by "常见的、来自不同阵营的“选择”行为" unless the ability "无视可选性" — as PRTS
|
||||
// marks 萨卡兹悖谬暴虐兵长's 暴击 "(无视无法选择,无视迷彩)" and 假想敌:淤困's burst spread "(中点判定,无视目标可选性,不受迷彩
|
||||
// 制约)", while "'直接选中'的能力…不受这些仅在选择时生效的异常效果制约"; gamedata_const ba.invisible 隐匿 "不阻挡时不成为敌方攻击
|
||||
// 的目标" vs ba.camou 迷彩 "不阻挡时不成为敌方普通攻击的目标(无法躲避溅射类攻击)"; PRTS 异常效果 迷彩 "所有光环类能力、以及涉及
|
||||
// 中点判定/格子判定的效果均不受迷彩制约"; PRTS 选择器 "所有触发选择器通常不无视迷彩" (an area skill is cast for a target it may
|
||||
// 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:
|
||||
// auraSelectable / auraAllies (the same without 对地规避). A locked target is hit as a direct pick (targetAndArea).
|
||||
// (DESIGN §22.12.)
|
||||
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { makeBattle, enemyRec, checkInvariants } from '../helpers/battleHarness.js';
|
||||
import { hasGeneratedData } from '../../server/sim/simdata.js';
|
||||
import { areaSelectable, auraSelectable } from '../../server/sim/targeting.js';
|
||||
import { areaAllies } from '../../server/sim/content/enemies.js';
|
||||
|
||||
const REAL = { skip: !hasGeneratedData() };
|
||||
const JF = 'chess_char_1_02_a'; // 角峰 (重装, blocks 3)
|
||||
const GM = 'chess_char_1_10_a'; // 古米 (重装, blocks 3)
|
||||
const HD = 'chess_char_1_05_a'; // 红豆 (先锋)
|
||||
const SKULSR = 'enemy_1500_skulsr'; // 碎骨: unblocked, its attacks are grenades that splash the target's surroundings
|
||||
const POMPEII = 'enemy_1050_lslime'; // “庞贝”: blocked ⇒ every 10 s a radius-1 arts self-blast
|
||||
const LUCIEN = 'enemy_2016_csphtm'; // 卢西恩,“猩红血钻” (boss_5): 【aoe】 radius 2, "需要目标", "该技能伤害无视迷彩"
|
||||
const BRUTE = 'enemy_1320_wdrrl_2'; // 萨卡兹悖谬暴虐兵长: first attack's 暴击 splash "无视无法选择,无视迷彩"
|
||||
const PARASITE = 'enemy_9007_acelem'; // 假想敌:淤困: parasitises its blocker; the host's burst spreads "无视目标可选性"
|
||||
const FAKE_SUIT = 'chess_item_4_04_e_a'; // 伪装服: first damage taken ⇒ 隐匿 15 s
|
||||
|
||||
const done = (h) => { checkInvariants(h.b); assert.deepEqual(h.b.errors.map((e) => `${e.label} ${e.message}`), []); };
|
||||
const stealth = (h, u) => h.b.applyStatus(u, 'stealth', { duration: 999, source: u });
|
||||
const camou = (h, u) => h.b.applyStatus(u, 'camou', { duration: 999, source: u });
|
||||
/** Every damage instance the enemy `key` deals: { t, target, attack (a normal attack), skill, amount, tags }. */
|
||||
function damageLog(h, key) {
|
||||
const log = [];
|
||||
h.b.on('damaged', (c) => {
|
||||
if (!c.source || c.source.defId !== key) return;
|
||||
log.push({ t: h.b.time, target: c.target, attack: !!c.dmg.isAttack, skill: !!c.dmg.isSkill, amount: c.amount, tags: c.dmg.tags || [] });
|
||||
}, { priority: 1000 });
|
||||
return log;
|
||||
}
|
||||
function countFx(h, kind) {
|
||||
const n = { v: 0 };
|
||||
const fx0 = h.b.fx.bind(h.b);
|
||||
h.b.fx = (k, p) => { if (k === 'explode' && p && p.kind === kind) n.v++; return fx0(k, p); };
|
||||
return n;
|
||||
}
|
||||
|
||||
test('areaSelectable: no 隐匿 ally unless 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' }) } },
|
||||
units: [{ chessId: JF, row: 9, col: 6 }, { chessId: GM, row: 10, col: 6 }, { chessId: HD, row: 9, col: 7 }],
|
||||
enemies: [{ key: 'enemy_g', pos: [9, 6] }, { key: 'enemy_f', pos: [11, 6] }],
|
||||
});
|
||||
h.step(2);
|
||||
const jf = h.unit(JF), gm = h.unit(GM), hd = h.unit(HD);
|
||||
const g = h.b.enemies.find((e) => e.defId === 'enemy_g'), f = h.b.enemies.find((e) => e.defId === 'enemy_f');
|
||||
assert.equal(g.blockedBy, jf, '角峰 blocks the ground enemy');
|
||||
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(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');
|
||||
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');
|
||||
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 (对地规避)');
|
||||
assert.equal(areaSelectable(f, gm), true, '起飞 vs a flying source');
|
||||
assert.equal(areaSelectable(null, gm), true, '起飞 vs no source');
|
||||
assert.equal(auraSelectable(g, gm), true, '起飞 vs a ground enemy\'s buff aura: still taken (§21.22)');
|
||||
h.b.removeBuff(gm, 'test:liftoff');
|
||||
h.b.addBuff(gm, { key: 'test:untargetable', persist: true, flags: { untargetable: true } });
|
||||
assert.equal(areaSelectable(f, gm), false, 'untargetable (不可选中)');
|
||||
h.b.removeBuff(gm, 'test:untargetable');
|
||||
h.b.addBuff(gm, { key: 'test:sleep', persist: true, flags: { sleep: true } });
|
||||
assert.equal(areaSelectable(f, gm), false, 'asleep (沉睡 = 无敌)');
|
||||
h.b.removeBuff(gm, 'test:sleep');
|
||||
h.b.kill(g, null);
|
||||
assert.equal(areaSelectable(g, jf), false, 'a dead source (death blast) blocks nobody: its 隐匿 former blocker is spared');
|
||||
done(h);
|
||||
});
|
||||
|
||||
/** 碎骨 walks lane row 9 and grenades 角峰 on (10,6); 古米 stands next to it (distance 1 = the splash radius). */
|
||||
function grenades({ gm = [10, 7], items = [], status = null, seconds = 25 } = {}) {
|
||||
const h = makeBattle({
|
||||
seed: 5, timeLimit: 120, autoFinish: false,
|
||||
units: [{ chessId: JF, row: 10, col: 6, dir: 'RIGHT' }, { chessId: GM, row: gm[0], col: gm[1], dir: 'RIGHT', items }],
|
||||
enemies: [{ key: SKULSR, route: 0, time: 3 }],
|
||||
});
|
||||
h.step(1);
|
||||
const jf = h.unit(JF), g = h.unit(GM);
|
||||
if (status) h.b.applyStatus(g, status, { duration: 999, source: g });
|
||||
const log = damageLog(h, SKULSR);
|
||||
h.run(seconds);
|
||||
return { h, jf, g, log, onJf: log.filter((x) => x.target === jf && x.attack), splashOnG: log.filter((x) => x.target === g && !x.attack), attacksOnG: log.filter((x) => x.target === g && x.attack) };
|
||||
}
|
||||
|
||||
test('#32.6 碎骨: a 隐匿 operator next to the grenade\'s target takes no splash; a 迷彩 one still does', REAL, () => {
|
||||
const hid = grenades({ status: 'stealth' });
|
||||
assert.ok(hid.onJf.length >= 4, `4+ grenades on 角峰 (${hid.onJf.length})`);
|
||||
assert.ok(hid.g.s.flags.stealth, '古米 stays 隐匿 (999 s)');
|
||||
assert.deepEqual(hid.log.filter((x) => x.target === hid.g).map((x) => `${x.t.toFixed(2)} ${Math.round(x.amount)}`), [], '隐匿, not blocking: nothing reaches it (in 0.1.1: 4 splashes, knocked out)');
|
||||
done(hid.h);
|
||||
const cam = grenades({ status: 'camou' });
|
||||
assert.ok(cam.onJf.length >= 4, `4+ grenades on 角峰 (${cam.onJf.length})`);
|
||||
assert.equal(cam.attacksOnG.length, 0, '迷彩: never the grenade\'s target');
|
||||
const standing = (t) => cam.g.alive || t <= cam.g.deathAt + 1e-6; // the splashes may knock it out
|
||||
assert.ok(cam.splashOnG.length >= 3, `迷彩: splashed (${cam.splashOnG.length})`);
|
||||
assert.equal(cam.splashOnG.length, cam.onJf.filter((x) => standing(x.t)).length, '迷彩: splashed by every grenade while it stands (ba.camou "无法躲避溅射类攻击")');
|
||||
assert.ok(cam.splashOnG.every((x) => x.skill && x.amount > 0));
|
||||
done(cam.h);
|
||||
});
|
||||
|
||||
test('#32.6 碎骨: 伪装服\'s 隐匿 (its carrier\'s first damage) spares the carrier from the following splashes until it ends', REAL, () => {
|
||||
// 古米 on (10,5) — deployed before 角峰, so 角峰 is the grenades' target (latest deployed) and 古米 its neighbour
|
||||
const r = grenades({ gm: [10, 5], items: [FAKE_SUIT], seconds: 32 });
|
||||
const first = r.splashOnG[0];
|
||||
assert.ok(first && first.amount > 0, 'the first grenade splashes 古米 — its first damage');
|
||||
const end = first.t + 15;
|
||||
const during = r.log.filter((x) => x.target === r.g && x.t > first.t + 1e-6 && x.t < end - 1e-6);
|
||||
const jfDuring = r.onJf.filter((x) => x.t > first.t + 1e-6 && x.t < end - 1e-6);
|
||||
assert.ok(jfDuring.length >= 3, `grenades kept landing next to it (${jfDuring.length})`);
|
||||
assert.deepEqual(during.map((x) => `${x.t.toFixed(2)} ${Math.round(x.amount)}`), [], '伪装服 隐匿 (flag `stealth`): no splash for its 15 s');
|
||||
assert.ok(r.log.some((x) => x.target === r.g && x.t > end), '…and hit again once its 隐匿 ends');
|
||||
done(r.h);
|
||||
});
|
||||
|
||||
test('#32.6 “庞贝”\'s self-blast while blocked hits its 隐匿 blocker and a 迷彩 bystander, not 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' }],
|
||||
enemies: [{ key: POMPEII, pos: [9, 6], time: 1, mods: { hpMul: 1e3, speedMul: 0, atkMul: 0.01 } }],
|
||||
});
|
||||
h.step(1);
|
||||
const jf = h.unit(JF), gm = h.unit(GM), hd = h.unit(HD);
|
||||
stealth(h, jf); stealth(h, gm); camou(h, hd);
|
||||
const log = damageLog(h, POMPEII), blasts = countFx(h, 'selfBlast');
|
||||
h.run(12);
|
||||
const p = h.b.enemies.find((e) => e.defId === POMPEII);
|
||||
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(hd), 1, '迷彩 bystander: hit');
|
||||
assert.equal(blast(gm), 0, '隐匿 bystander (distance 1, not blocking): spared');
|
||||
done(h);
|
||||
});
|
||||
|
||||
test('#32.6 boss: 卢西恩\'s 【aoe】 is not cast for 隐匿 / 迷彩 operators alone; cast, it hits the 迷彩 and plain ones, not the 隐匿 one', REAL, () => {
|
||||
const run = (plain) => {
|
||||
const units = [{ chessId: JF, row: 10, col: 5, dir: 'RIGHT' }, { chessId: GM, row: 11, col: 6, dir: 'RIGHT' }];
|
||||
if (plain) units.push({ chessId: HD, row: 9, col: 6, dir: 'RIGHT' });
|
||||
const h = makeBattle({ seed: 3, timeLimit: 400, autoFinish: false, units, enemies: [{ key: LUCIEN, pos: [10, 6], time: 1, mods: { hpMul: 1e3, speedMul: 0, atkMul: 0.01 } }] });
|
||||
h.step(1);
|
||||
stealth(h, h.unit(JF)); camou(h, h.unit(GM));
|
||||
const log = damageLog(h, LUCIEN), casts = countFx(h, 'crimsonAoe');
|
||||
h.run(30);
|
||||
const aoe = (id) => log.filter((x) => x.target === h.unit(id) && x.skill && !x.attack).length;
|
||||
return { h, casts: casts.v, aoe };
|
||||
};
|
||||
const alone = run(false);
|
||||
assert.equal(alone.casts, 0, 'only a 隐匿 and a 迷彩 operator within 2: no cast (its trigger selection skips both)');
|
||||
done(alone.h);
|
||||
const r = run(true);
|
||||
assert.ok(r.casts >= 2, `cast with 红豆 in range (${r.casts})`);
|
||||
assert.equal(r.aoe(HD), r.casts, 'plain: hit by every cast');
|
||||
assert.equal(r.aoe(GM), r.casts, '迷彩: hit by every cast ("该技能伤害无视迷彩")');
|
||||
assert.equal(r.aoe(JF), 0, '隐匿, not blocking: never hit');
|
||||
done(r.h);
|
||||
});
|
||||
|
||||
test('#32.6 ignoreSelect: 萨卡兹悖谬暴虐兵长\'s 暴击 ("无视无法选择") and 假想敌:淤困\'s burst spread ("无视目标可选性") still reach a 隐匿 operator', 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' }],
|
||||
enemies: [{ key: BRUTE, pos: [9, 6.4], time: 1, mods: { hpMul: 1e3, speedMul: 0 } }],
|
||||
});
|
||||
h.step(1);
|
||||
const gm = h.unit(GM);
|
||||
stealth(h, gm);
|
||||
const log = damageLog(h, BRUTE);
|
||||
h.run(6);
|
||||
assert.equal(h.b.enemies.find((e) => e.defId === BRUTE).blockedBy, h.unit(JF), '角峰 (block 3) holds it');
|
||||
const splash = log.filter((x) => x.target === gm && x.tags.includes('aoeAttack'));
|
||||
assert.equal(splash.length, 1, '暴击 splash on the 隐匿 古米 next to its target (once: "此技能仅能触发一次")');
|
||||
assert.ok(splash[0].amount > 0);
|
||||
done(h);
|
||||
}
|
||||
{
|
||||
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' }],
|
||||
enemies: [{ key: PARASITE, pos: [9, 6.4], time: 1, mods: { hpMul: 1e3, speedMul: 0 } }],
|
||||
});
|
||||
h.step(1);
|
||||
const jf = h.unit(JF), gm = h.unit(GM), hd = h.unit(HD);
|
||||
stealth(h, gm);
|
||||
h.b.addBuff(hd, { key: 'test:liftoff', persist: true, flags: { liftoff: true, blockFly: true } }); // 起飞 (蒂比's flag)
|
||||
h.run(1.5);
|
||||
assert.ok(jf.findBuff('ab:parasite'), '淤困 parasitises its blocker 角峰');
|
||||
const src = h.b.enemies.find((e) => e.defId === PARASITE);
|
||||
h.b.dealDamage(src, jf, { type: 'element', element: 'burn', amount: 5000, tags: ['test'] });
|
||||
h.step(1);
|
||||
assert.ok(jf.findBuff('burnBurst') || jf.elem.burn >= jf.gaugeMax - 1e-6, 'the host bursts');
|
||||
assert.ok(gm.elem.burn > 0, '隐匿 neighbour: 1000 burn spread onto it');
|
||||
assert.ok(hd.elem.burn > 0, 'airborne 起飞 neighbour: reached too (until 0.1.2 the pipeline refused a ground enemy\'s fill)');
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------------------------------------------
|
||||
// review of the first round
|
||||
|
||||
const liftoff = (h, u) => h.b.addBuff(u, { key: 'test:liftoff', persist: true, flags: { liftoff: true, blockFly: true } }); // 起飞 (蒂比's flag)
|
||||
const pool = (hp) => ({ hp, maxHp: hp, damage(pid, a) { this.hp = Math.max(0, this.hp - a); } });
|
||||
|
||||
test('#32.6 review: 蚀裂\'s 毒雾 and the buff auras of 深池伙友卫队 / 扎罗 (远古威慑) keep a 隐匿 operator out; an airborne 起飞 one is still reached', REAL, () => {
|
||||
// 假想敌:蚀裂 knocked out with no killer: its cloud (r 0.8) covers 角峰 (9,6) and 古米 (10,6) around its spot (9.5, 6)
|
||||
for (const mode of ['stealth', 'liftoff']) {
|
||||
const h = makeBattle({
|
||||
seed: 3, timeLimit: 60, autoFinish: false,
|
||||
units: [{ chessId: JF, row: 9, col: 6, dir: 'RIGHT' }, { chessId: GM, row: 10, col: 6, dir: 'RIGHT' }],
|
||||
enemies: [{ key: 'enemy_9006_actoxi', pos: [9.5, 6], time: 1, mods: { hpMul: 1e3, speedMul: 0, atkMul: 0.01 } }],
|
||||
});
|
||||
h.step(1);
|
||||
const jf = h.unit(JF), gm = h.unit(GM);
|
||||
if (mode === 'stealth') stealth(h, gm); else liftoff(h, gm);
|
||||
const ticks = new Map([[jf, 0], [gm, 0]]);
|
||||
h.b.on('damaged', (c) => { if ((c.dmg.tags || []).includes('poison') && ticks.has(c.target)) ticks.set(c.target, ticks.get(c.target) + 1); }, { priority: 1000 });
|
||||
h.run(1);
|
||||
h.b.kill(h.b.enemies.find((e) => e.defId === 'enemy_9006_actoxi'), null);
|
||||
h.run(10);
|
||||
assert.ok(ticks.get(jf) >= 8, `the cloud ticks on 角峰 (${ticks.get(jf)})`);
|
||||
if (mode === 'stealth') assert.equal(ticks.get(gm), 0, '隐匿 古米: no tick (in 0.1.1 every one)');
|
||||
else assert.equal(ticks.get(gm), ticks.get(jf), 'airborne 古米: every tick (a sourceless cloud selects no ground enemy, §21.22)');
|
||||
done(h);
|
||||
}
|
||||
// 深池伙友卫队's field (with 影刃 next to it) and 扎罗's 远古威慑 (during its 重生): 角峰 plain, 古米 隐匿, 红豆 起飞
|
||||
const aura = (enemies, buff, ko) => {
|
||||
const h = makeBattle({
|
||||
seed: 3, timeLimit: 120, autoFinish: false,
|
||||
units: [{ chessId: JF, row: 10, col: 6, dir: 'RIGHT' }, { chessId: GM, row: 9, col: 7, dir: 'RIGHT' }, { chessId: HD, row: 10, col: 8, dir: 'RIGHT' }],
|
||||
enemies: enemies.map((key, i) => ({ key, pos: [10 + i, 7], time: 1, mods: { hpMul: 1e3, speedMul: 0, atkMul: 0.001 } })),
|
||||
});
|
||||
h.step(1);
|
||||
stealth(h, h.unit(GM)); liftoff(h, h.unit(HD));
|
||||
h.run(1);
|
||||
if (ko) h.b.kill(h.b.enemies.find((e) => e.defId === ko), null);
|
||||
h.run(1);
|
||||
const got = [JF, GM, HD].map((id) => !!h.unit(id).findBuff(buff));
|
||||
done(h);
|
||||
return got;
|
||||
};
|
||||
assert.deepEqual(aura(['enemy_1174_duholy', 'enemy_1175_dushdo_2'], 'ab:forceField'), [true, false, true], '深池伙友卫队: 角峰 yes, 隐匿 古米 no, 起飞 红豆 yes');
|
||||
assert.deepEqual(aura(['enemy_1535_wlfmster'], 'ab:ancientAwe', 'enemy_1535_wlfmster'), [true, false, true], '扎罗 远古威慑: 角峰 yes, 隐匿 古米 no, 起飞 红豆 yes');
|
||||
});
|
||||
|
||||
test('#32.6 review: 死亡之眼\'s burst still hits its locked target that turned 隐匿 mid-channel (a direct pick); a 隐匿 bystander is spared', REAL, () => {
|
||||
const h = makeBattle({
|
||||
seed: 3, timeLimit: 120, autoFinish: false,
|
||||
units: [{ chessId: GM, row: 9, col: 6, dir: 'RIGHT' }, { chessId: JF, row: 10, col: 6, dir: 'RIGHT' }, { chessId: HD, row: 9, col: 5, dir: 'RIGHT' }],
|
||||
enemies: [{ key: 'enemy_1275_dwlock_2', pos: [9, 8], time: 1, mods: { hpMul: 1e3, speedMul: 0, atkMul: 0.2 } }],
|
||||
});
|
||||
h.step(1);
|
||||
const gm = h.unit(GM), jf = h.unit(JF), hd = h.unit(HD);
|
||||
stealth(h, jf);
|
||||
let locked = null;
|
||||
const fx0 = h.b.fx.bind(h.b);
|
||||
h.b.fx = (k, p) => {
|
||||
if (k === 'beam' && p && p.kind === 'deathEye' && !locked) { locked = p.to; h.b.after(1.5, () => stealth(h, gm)); } // 隐匿 mid-channel
|
||||
return fx0(k, p);
|
||||
};
|
||||
const burst = [];
|
||||
h.b.on('damaged', (c) => { if (c.source && c.source.defId === 'enemy_1275_dwlock_2' && c.dmg.element === 'apoptosis' && c.amount > 50) burst.push(c.target); }, { priority: 1000 });
|
||||
h.run(30);
|
||||
assert.equal(locked, gm.id, 'the channel locks 古米');
|
||||
assert.ok(gm.s.flags.stealth, '古米 turned 隐匿 during it');
|
||||
assert.ok(burst.includes(gm), 'the burst hits its locked target');
|
||||
assert.ok(burst.includes(hd), '…and the plain 红豆 next to it');
|
||||
assert.ok(!burst.includes(jf), '…not the 隐匿 角峰 next to it');
|
||||
done(h);
|
||||
});
|
||||
|
||||
test('#32.6 review: the 斩胄之剑 / 破胄之锤 hover attack skips 隐匿 and 迷彩; the 自行炮\'s shots and 深溟巢涌者\'s pulse hit 迷彩; 祈祷邀约 (whole field) skips 隐匿', REAL, () => {
|
||||
// hover attack (a normal attack on every operator in range): 角峰 plain, 古米 隐匿, 红豆 迷彩 — all within 1.6
|
||||
for (const key of ['enemy_9014_acstma', 'enemy_9015_acstmb']) {
|
||||
const h = makeBattle({
|
||||
kind: 'boss', sharedBoss: pool(1e6), seed: 7, autoFinish: false, timeLimit: 600,
|
||||
units: [{ chessId: JF, row: 10, col: 9, dir: 'RIGHT' }, { chessId: GM, row: 10, col: 7, dir: 'RIGHT' }, { chessId: HD, row: 10, col: 8, dir: 'RIGHT' }],
|
||||
});
|
||||
h.step();
|
||||
stealth(h, h.unit(GM)); camou(h, h.unit(HD));
|
||||
const blade = h.spawn(key, { pos: [3, 8], routeIndex: 0, mods: { speedMul: 0 }, tag: 'part' });
|
||||
const hit = [];
|
||||
h.b.on('damaged', (c) => { if (c.source === blade) hit.push(c.target); }, { priority: 1000 });
|
||||
h.run(3);
|
||||
assert.ok(hit.includes(h.unit(JF)), `${key}: hits the plain operator`);
|
||||
assert.ok(!hit.includes(h.unit(GM)) && !hit.includes(h.unit(HD)), `${key}: not the 隐匿 or the 迷彩 one (in 0.1.1 it hit all three)`);
|
||||
done(h);
|
||||
}
|
||||
// 自行炮: 红豆 (迷彩, full HP ratio) in the 9-tile zone around 角峰 (the highest max HP) is shot too
|
||||
{
|
||||
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: 10, col: 5, dir: 'RIGHT' }],
|
||||
enemies: [{ key: 'enemy_1273_stmgun_2', pos: [9, 9], time: 1, mods: { hpMul: 1e3, speedMul: 0 } }],
|
||||
});
|
||||
h.step(1);
|
||||
camou(h, h.unit(HD));
|
||||
const log = damageLog(h, 'enemy_1273_stmgun_2');
|
||||
h.run(40);
|
||||
assert.ok(log.some((x) => x.target === h.unit(HD) && !x.attack), '迷彩 红豆: shot inside the zone (in 0.1.1 never)');
|
||||
done(h);
|
||||
}
|
||||
// 深溟巢涌者: its pulse rides on its attack — with the plain 古米 in range the 迷彩 红豆 is hit by every pulse; alone it is never
|
||||
for (const alone of [false, true]) {
|
||||
const units = [{ chessId: HD, row: 9, col: 6, dir: 'RIGHT' }];
|
||||
if (!alone) units.push({ chessId: GM, row: 10, col: 7, dir: 'RIGHT' });
|
||||
const h = makeBattle({ seed: 3, timeLimit: 120, autoFinish: false, units, enemies: [{ key: 'enemy_1234_dsubrl', pos: [9, 7], time: 1, mods: { hpMul: 1e3, speedMul: 0 } }] });
|
||||
h.step(1);
|
||||
camou(h, h.unit(HD));
|
||||
const log = damageLog(h, 'enemy_1234_dsubrl');
|
||||
h.run(10);
|
||||
const on = (id) => log.filter((x) => x.target === h.unit(id) && x.attack).length;
|
||||
if (alone) assert.equal(on(HD), 0, '迷彩 alone in range: no attack, so no pulse [the engine attack needs a target]');
|
||||
else { assert.ok(on(GM) >= 5, `plain 古米 pulsed (${on(GM)})`); assert.equal(on(HD), on(GM), '迷彩 红豆: every pulse too (in 0.1.1 none)'); }
|
||||
done(h);
|
||||
}
|
||||
// “萨科塔昂首”'s 【祈祷邀约】 (whole field, "无视迷彩", no 无视无法选择): 角峰 slowed, the 隐匿 古米 not
|
||||
{
|
||||
const h = makeBattle({
|
||||
seed: 3, timeLimit: 120, autoFinish: false,
|
||||
units: [{ chessId: JF, row: 10, col: 6, dir: 'RIGHT' }, { chessId: GM, row: 9, col: 7, dir: 'RIGHT' }],
|
||||
enemies: [{ key: 'enemy_10085_hllevi_2', pos: [10, 9], time: 1, route: 2, mods: { hpMul: 1e3, speedMul: 0 } }],
|
||||
});
|
||||
h.step(1);
|
||||
stealth(h, h.unit(GM));
|
||||
h.run(22);
|
||||
assert.ok(h.unit(JF).findBuff('ab:roar'), '角峰: 受邀祈祷');
|
||||
assert.ok(!h.unit(GM).findBuff('ab:roar'), '隐匿 古米: not selected');
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
Reference in new issue
Block a user