feedback1d WL: 源石溶剂 drain is 无来源 true damage; 信仰搅拌机 S3 counters any damage
Player report D1: the 源石溶剂 HP loss should set off 信仰搅拌机 S3 and, next to 莫斯提马, stack 拉特兰 layers on its own. The drain was a Battle.loseHp 流失, which skips 受击回复 SP and the 重装 TAKE_DAMAGE trigger, and the S3 counter needed an enemy attack and a target. Official: PRTS 盟约记录 源石溶剂 "受到60真实伤害" (修正 "并非流失", 备注 "造成无来源真实持续环境伤害"); PRTS 信仰搅拌机 S3 备注 "反击受到任何伤害后均可触发, 无需目标,视为普通攻击"; PRTS 作战机制: 生命流失 skips 反伤 / 受击回复. - damage.js periodicDamage: the periodic_damage template (源石溶剂, 狂暴宿主组长) as 无来源 true damage (shields, damage-taken modifiers, hit effects, 受击回复 SP, TAKE_DAMAGE); the credit is unchanged. - 信仰搅拌机 S3: any damage except 流失 / element 损伤 counters; with no enemy in range the counter still spends its bullet. - Audit, same rule both ways (damage.js isHpLoss): 孽罪奇美拉's aura on operators is damage; 雷蛇 战术防御 and 蒂比 S2 / 片场工作指南 respond to any damage (PRTS notes); 乌尔比安, 伪装服, 远牙, 录武官's guard and 坚守 thorn chances no longer respond to a 流失. - data/items.json regenerated (源石溶剂 implFormula text only).
This commit is contained in:
1 parent
7f134737ce
commit
01f5b216c6
15 files changed
+447
-38
No files matched your search
+1
-1
@@ -1 +1 @@
|
||||
{"chess_item_1_01_e_a":{"id":"chess_item_1_01_e_a","baseId":"chess_item_1_01_e_a","goldenId":"chess_item_1_01_e_b","isGolden":false,"trapId":"trap_1041_acarm041","iconId":"trap_1041_acarm041","identifier":267,"name":"维式重锤","itemType":"EQUIP","tier":1,"shopSortId":1,"price":1,"hideInShop":false,"shopExcluded":false,"shopExcludedBy":null,"mergeable":true,"upgradeNum":2,"upgradeChessId":"chess_item_1_01_e_b","duration":-1,"giveBondId":"victoriaShip","givePowerId":null,"canGiveBond":false,"requiresBondId":null,"effectId":"eff_acarm041","effectName":"维式重锤","desc":"攻击力+15%","descRaw":"攻击力<@ba.vup>+15%</>","buffs":[{"key":"env_gbuff_new_with_verify","countType":"NONE","bb":{"atk":0.15},"bbStr":{"key":"attr_common_global_buff"}}],"params":{"atk":0.15,"key":"attr_common_global_buff"},"category":"STAT","kind":"passive","family":null,"implFormula":"ATK% += atk (additive with other ATK% buffs)","rangeGrid":[[0,0]],"flavor":"一柄曾在维多利亚的工厂内铸造和平的重锤。\n“用同样的铁与火,去终结那场战争。”"},"chess_item_1_01_e_b":{"id":"chess_item_1_01_e_b","baseId":"chess_item_1_01_e_a","goldenId":"chess_item_1_01_e_b","isGolden":true,"trapId":"trap_1041_acarm041","iconId":"trap_1041_acarm041","identifier":326,"name":"维式重锤","itemType":"EQUIP","tier":1,"shopSortId":1,"price":5,"hideInShop":false,"shopExcluded":false,"shopExcludedBy":null,"mergeable":false,"upgradeNum":0,"upgradeChessId":null,"duration":0,"giveBondId":"victoriaShip","givePowerId":null,"canGiveBond":false,"requiresBondId":null,"effectId":"eff_acgarm041","effectName":"维式重锤","desc":"攻击力+25%","descRaw":"攻击力<@ba.vup>+25%</>","buffs":[{"key":"env_gbuff_new_with_verify","countType":"NONE","bb":{"atk":0.25},"bbStr":{"key":"attr_common_global_buff"}}],"params":{"atk":0.25,"key":"attr_common_global_buff"},"category":"STAT","kind":"passive","family":null,"implFormula":"ATK% += atk (additive with other ATK% buffs)","rangeGrid":[[0,0]],"flavor":"一柄曾在维多利亚的工厂内铸造和平的重锤。\n“用同样的铁与火,去终结那场战争。”"},"chess_item_1_02_e_a":{"id":"chess_item_1_02_e_a","baseId":"chess_item_1_02_e_a","goldenId":"chess_item_1_02_e_b","isGolden":false,"trapId":"trap_1047_acarm047","iconId":"trap_1047_acarm047","identifier":273,"name":"坚守盾牌","itemType":"EQUIP","tier":1,"shopSortId":2,"price":1,"hideInShop":false,"shopExcluded":false,"shopExcludedBy":null,"mergeable":true,"upgradeNum":2,"upgradeChessId":"chess_item_1_02_e_b","duration":-1,"giveBondId":"steadShip","givePowerId":null,"canGiveBond":false,"requiresBondId":null,"effectId":"eff_acarm047","effectName":"坚守盾牌","desc":"防御力+20%","descRaw":"防御力<@ba.vup>+20%</>","buffs":[{"key":"env_gbuff_new_with_verify","countType":"NONE","bb":{"def":0.2},"bbStr":{"key":"attr_common_global_buff"}}],"params":{"def":0.2,"key":"attr_common_global_buff"},"category":"STAT","kind":"passive","family":null,"implFormula":"DEF% += def","rangeGrid":[[0,0]],"flavor":"一面属于坚守之人的盾牌。\n“推击、拨挡、压制,主动防御的基础在于对盾的运用。”"},"chess_item_1_02_e_b":{"id":"chess_item_1_02_e_b","baseId":"chess_item_1_02_e_a","goldenId":"chess_item_1_02_e_b","isGolden":true,"trapId":"trap_1047_acarm047","iconId":"trap_1047_acarm047","identifier":332,"name":"坚守盾牌","itemType":"EQUIP","tier":1,"shopSortId":2,"price":5,"hideInShop":false,"shopExcluded":false,"shopExcludedBy":null,"mergeable":false,"upgradeNum":0,"upgradeChessId":null,"duration":0,"giveBondId":"steadShip","givePowerId":null,"canGiveBond":false,"requiresBondId":null,"effectId":"eff_acgarm047","effectName":"坚守盾牌","desc":"防御力+35%","descRaw":"防御力<@ba.vup>+35%</>","buffs":[{"key":"env_gbuff_new_with_verify","countType":"NONE","bb":{"def":0.35},"bbStr":{"key":"attr_common_global_buff"}}],"params":{"def":0.35,"key":"attr_common_global_buff"},"category":"STAT","kind":"passive","family":null,"implFormula":"DEF% += def","rangeGrid":[[0,0]],"flavor":"一面属于坚守之人的盾牌。\n“推击、拨挡、压制,主动防御的基础在于对盾的运用。”"},"chess_item_1_03_e_a":{"id":"chess_item_1_03_e_a","baseId":"chess_item_1_03_e_a","goldenId":"chess_item_1_03_e_b","isGolden":false,"trapId":"trap_1066_acarm066","iconId":"trap_1066_acarm066","identifier":292,"name":"盟约之币","itemType":"EQUIP","tier":1,"shopSortId":3,"price":1,"hideInShop":false,"shopExcluded":false,"shopExcludedBy":null,"mergeable":true,"upgradeNum":2,"upgradeChessId":"chess_item_1_03_e_b","duration":-1,"giveBondId":null,"givePowerId":null,"canGiveBond":false,"requiresBondId":null,"effectId":"eff_acarm066","effectName":"盟约之币","desc":"装备时销毁,并获得1资金","descRaw":"装备时销毁,并获得<@ba.vup>1</>资金","buffs":[{"key":"equip_destory_gain_random_coin","countType":"NONE","bb":{"min":1,"max":1},"bbStr":{}}],"params":{"min":1,"max":1},"category":"ECONOMY",Line truncated
|
||||
{"chess_item_1_01_e_a":{"id":"chess_item_1_01_e_a","baseId":"chess_item_1_01_e_a","goldenId":"chess_item_1_01_e_b","isGolden":false,"trapId":"trap_1041_acarm041","iconId":"trap_1041_acarm041","identifier":267,"name":"维式重锤","itemType":"EQUIP","tier":1,"shopSortId":1,"price":1,"hideInShop":false,"shopExcluded":false,"shopExcludedBy":null,"mergeable":true,"upgradeNum":2,"upgradeChessId":"chess_item_1_01_e_b","duration":-1,"giveBondId":"victoriaShip","givePowerId":null,"canGiveBond":false,"requiresBondId":null,"effectId":"eff_acarm041","effectName":"维式重锤","desc":"攻击力+15%","descRaw":"攻击力<@ba.vup>+15%</>","buffs":[{"key":"env_gbuff_new_with_verify","countType":"NONE","bb":{"atk":0.15},"bbStr":{"key":"attr_common_global_buff"}}],"params":{"atk":0.15,"key":"attr_common_global_buff"},"category":"STAT","kind":"passive","family":null,"implFormula":"ATK% += atk (additive with other ATK% buffs)","rangeGrid":[[0,0]],"flavor":"一柄曾在维多利亚的工厂内铸造和平的重锤。\n“用同样的铁与火,去终结那场战争。”"},"chess_item_1_01_e_b":{"id":"chess_item_1_01_e_b","baseId":"chess_item_1_01_e_a","goldenId":"chess_item_1_01_e_b","isGolden":true,"trapId":"trap_1041_acarm041","iconId":"trap_1041_acarm041","identifier":326,"name":"维式重锤","itemType":"EQUIP","tier":1,"shopSortId":1,"price":5,"hideInShop":false,"shopExcluded":false,"shopExcludedBy":null,"mergeable":false,"upgradeNum":0,"upgradeChessId":null,"duration":0,"giveBondId":"victoriaShip","givePowerId":null,"canGiveBond":false,"requiresBondId":null,"effectId":"eff_acgarm041","effectName":"维式重锤","desc":"攻击力+25%","descRaw":"攻击力<@ba.vup>+25%</>","buffs":[{"key":"env_gbuff_new_with_verify","countType":"NONE","bb":{"atk":0.25},"bbStr":{"key":"attr_common_global_buff"}}],"params":{"atk":0.25,"key":"attr_common_global_buff"},"category":"STAT","kind":"passive","family":null,"implFormula":"ATK% += atk (additive with other ATK% buffs)","rangeGrid":[[0,0]],"flavor":"一柄曾在维多利亚的工厂内铸造和平的重锤。\n“用同样的铁与火,去终结那场战争。”"},"chess_item_1_02_e_a":{"id":"chess_item_1_02_e_a","baseId":"chess_item_1_02_e_a","goldenId":"chess_item_1_02_e_b","isGolden":false,"trapId":"trap_1047_acarm047","iconId":"trap_1047_acarm047","identifier":273,"name":"坚守盾牌","itemType":"EQUIP","tier":1,"shopSortId":2,"price":1,"hideInShop":false,"shopExcluded":false,"shopExcludedBy":null,"mergeable":true,"upgradeNum":2,"upgradeChessId":"chess_item_1_02_e_b","duration":-1,"giveBondId":"steadShip","givePowerId":null,"canGiveBond":false,"requiresBondId":null,"effectId":"eff_acarm047","effectName":"坚守盾牌","desc":"防御力+20%","descRaw":"防御力<@ba.vup>+20%</>","buffs":[{"key":"env_gbuff_new_with_verify","countType":"NONE","bb":{"def":0.2},"bbStr":{"key":"attr_common_global_buff"}}],"params":{"def":0.2,"key":"attr_common_global_buff"},"category":"STAT","kind":"passive","family":null,"implFormula":"DEF% += def","rangeGrid":[[0,0]],"flavor":"一面属于坚守之人的盾牌。\n“推击、拨挡、压制,主动防御的基础在于对盾的运用。”"},"chess_item_1_02_e_b":{"id":"chess_item_1_02_e_b","baseId":"chess_item_1_02_e_a","goldenId":"chess_item_1_02_e_b","isGolden":true,"trapId":"trap_1047_acarm047","iconId":"trap_1047_acarm047","identifier":332,"name":"坚守盾牌","itemType":"EQUIP","tier":1,"shopSortId":2,"price":5,"hideInShop":false,"shopExcluded":false,"shopExcludedBy":null,"mergeable":false,"upgradeNum":0,"upgradeChessId":null,"duration":0,"giveBondId":"steadShip","givePowerId":null,"canGiveBond":false,"requiresBondId":null,"effectId":"eff_acgarm047","effectName":"坚守盾牌","desc":"防御力+35%","descRaw":"防御力<@ba.vup>+35%</>","buffs":[{"key":"env_gbuff_new_with_verify","countType":"NONE","bb":{"def":0.35},"bbStr":{"key":"attr_common_global_buff"}}],"params":{"def":0.35,"key":"attr_common_global_buff"},"category":"STAT","kind":"passive","family":null,"implFormula":"DEF% += def","rangeGrid":[[0,0]],"flavor":"一面属于坚守之人的盾牌。\n“推击、拨挡、压制,主动防御的基础在于对盾的运用。”"},"chess_item_1_03_e_a":{"id":"chess_item_1_03_e_a","baseId":"chess_item_1_03_e_a","goldenId":"chess_item_1_03_e_b","isGolden":false,"trapId":"trap_1066_acarm066","iconId":"trap_1066_acarm066","identifier":292,"name":"盟约之币","itemType":"EQUIP","tier":1,"shopSortId":3,"price":1,"hideInShop":false,"shopExcluded":false,"shopExcludedBy":null,"mergeable":true,"upgradeNum":2,"upgradeChessId":"chess_item_1_03_e_b","duration":-1,"giveBondId":null,"givePowerId":null,"canGiveBond":false,"requiresBondId":null,"effectId":"eff_acarm066","effectName":"盟约之币","desc":"装备时销毁,并获得1资金","descRaw":"装备时销毁,并获得<@ba.vup>1</>资金","buffs":[{"key":"equip_destory_gain_random_coin","countType":"NONE","bb":{"min":1,"max":1},"bbStr":{}}],"params":{"min":1,"max":1},"category":"ECONOMY",Line truncated
|
||||
+13
-3
@@ -224,7 +224,9 @@ tick, damage-taken modifiers scale it and it counts for 受击回复 SP and TAKE
|
||||
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.
|
||||
a crowd of dying 萨卡兹枯朽战士 totals 50 / s, not 50 × n. An activated 孽罪奇美拉's aura (`kitChimera`, PRTS "持续视为受到源石
|
||||
污染区影响…受到50真实持续伤害") is the same kind of damage — 无来源 true (like the terrain it stands for [ASSUMED]), the
|
||||
chimera credited — not a 流失 (player report D1 audit; its radius 1 and 1 s pulse stay [ASSUMED] against PRTS's 1.2 / 0.5 s).
|
||||
|
||||
**Ownership** (`enemy.ownerId`, used for `killed/total` and leak attribution): `ownerPlayerId` if given, else the
|
||||
player whose half contains the spawn tile (cols ≥ 11 = right half / player with colOffset 8 or side R). A leak is
|
||||
@@ -497,8 +499,16 @@ unit at ≥ 1 HP) → **`damaged`** → SP-on-hurt / TAKE_DAMAGE → `kill` + `d
|
||||
× source `healingDealtMul` × target `healingTakenMul`; **`heal`** hook (mutable amount); capped at max HP; `overheal`
|
||||
turns the excess into an `overheal` shield. `battle.loseHp(target, amount, { source, from, tags, silent, sourceless })` = HP
|
||||
loss ignoring DEF/RES/shields/dodge (流失); `sourceless: true` makes it 无来源 ("受到等量的无来源生命流失": hooks see no source,
|
||||
`source` keeps the credit — stats and the per-player shared-pool tally), as does a 无来源 `from`. On a leader in a boss /
|
||||
hidden battle a loss of ≥ `BOSS_HIT_LIMIT` (a part's 传递, a drone's death) is cancelled like a hit. Every HP-damage kind
|
||||
`source` keeps the credit — stats and the per-player shared-pool tally), as does a 无来源 `from`. A 流失 skips the damage
|
||||
events (PRTS 作战机制 "生命流失不会触发反伤、受击回复等受到攻击触发的时点"): no `hit`, no 受击回复 SP, no TAKE_DAMAGE; its
|
||||
`damaged` ctx carries the tag `'hpLoss'` (and `noSp`; `damage.js isHpLoss`), and "受到伤害时" content skips it (信仰搅拌机 S3
|
||||
counters, 雷蛇 战术防御, 乌尔比安 本性的坚守, 录武官's guard, 远牙's 未受伤害 timer, 伪装服, 坚守 thorn chances, 机变 自愈) —
|
||||
while it counts every real damage instance, 无来源 ones included (a zone tick, the 源石溶剂 drain; 蒂比 S2 "受到伤害前
|
||||
触发"). Use it only for what the official calls 流失 (operator skill / trait 流失, 阿戈尔's 物理流失, leader-part 传递);
|
||||
"受到N真实伤害" over time is damage — the official `periodic_damage` template (源石溶剂: PRTS 盟约记录 修正 "并非流失",
|
||||
"造成无来源真实持续环境伤害"; 狂暴宿主 "自身每秒受到N无来源真实伤害") is `dealDamage(credit, target,
|
||||
damage.js periodicDamage(n))`: 无来源 true, `canDodge: false`, tags `'dot'` / `'periodic'` (player report D1). On a leader
|
||||
in a boss / hidden battle a loss of ≥ `BOSS_HIT_LIMIT` (a part's 传递, a drone's death) is cancelled like a hit. Every HP-damage kind
|
||||
meets the limit (phys / arts / true / 元素伤害 incl. element bursts, DoT ticks); element 损伤 (the gauge, `type: 'element'`)
|
||||
removes no HP and never does.
|
||||
|
||||
|
||||
@@ -1670,7 +1670,7 @@
|
||||
],
|
||||
"flavor": "“经典的工业材料军事化应用案例。”",
|
||||
"talentNotes": [],
|
||||
"implFormula": "ATK% += atk; in battle carrier loses `damage` HP per second (periodic self-damage, true/unmitigated [ASSUMED], can kill the carrier [ASSUMED]).",
|
||||
"implFormula": "ATK% += atk; in battle the carrier takes `damage` true damage per second — 无来源 持续 damage, not a 流失 (PRTS 盟约记录 修正 \"并非流失\", 备注 \"造成无来源真实持续环境伤害\"): shields and damage-taken modifiers apply, it counts as 受到伤害 (受击回复 SP, the 重装 skill trigger, 信仰搅拌机 S3 counters); can kill the carrier.",
|
||||
"normal": {
|
||||
"chessId": "chess_item_1_05_e_a",
|
||||
"isGolden": false,
|
||||
|
||||
@@ -155,7 +155,7 @@ Param names below are the keys of `items[].normal.params` / `items[].golden.para
|
||||
params: `atk=0.15/0.25`; buff keys: `env_gbuff_new_with_verify`
|
||||
- **坚守盾牌** (`chess_item_1_02_e_a`, TI, 1) - DEF% += def
|
||||
params: `def=0.2/0.35`; buff keys: `env_gbuff_new_with_verify`
|
||||
- **源石溶剂** (`chess_item_1_05_e_a`, TI, 1) - ATK% += atk; in battle carrier loses `damage` HP per second (periodic self-damage, true/unmitigated [ASSUMED], can kill the carrier [ASSUMED]).
|
||||
- **源石溶剂** (`chess_item_1_05_e_a`, TI, 1) - ATK% += atk; in battle the carrier takes `damage` true damage per second — 无来源 持续 damage, not a 流失 (PRTS 盟约记录 修正 "并非流失", 备注 "造成无来源真实持续环境伤害"): shields and damage-taken modifiers apply, it counts as 受到伤害 (受击回复 SP, the 重装 skill trigger, 信仰搅拌机 S3 counters); can kill the carrier.
|
||||
params: `atk=0.4/0.6, damage=60.0`; buff keys: `env_gbuff_new_with_verify, env_gbuff_new_with_verify`
|
||||
- **不屈弹射器** (`chess_item_2_01_e_a`, TII, 1) - respawnTime *= (1 + respawn_time) (i.e. -30%/-50%); maxHP% += max_hp (-30%).
|
||||
params: `respawn_time=-0.3/-0.5, max_hp=-0.3`; buff keys: `env_gbuff_new_with_verify`
|
||||
|
||||
@@ -19,7 +19,8 @@
|
||||
// instances multiplied (×5.4 × ×5.6 on a leader at ~250 layers, DESIGN §20.10)
|
||||
// 坚守 steadShip all operators max HP +(base + per·L); tier 2: 40 % of a non-member operator's damage is borne by
|
||||
// the members on the field (split evenly, sourceless true damage — already mitigated), members’ thorns
|
||||
// (base + per·L arts, sourceless but credited to the member hit, ≤ 1 per cd_duration per member)
|
||||
// (base + per·L arts, sourceless but credited to the member hit, ≤ 1 per cd_duration per member;
|
||||
// 无来源 damage uses the chance and hits nobody, a 流失 never does — PRTS 备注 / 作战机制)
|
||||
// + 脆弱 ×damage_scale for weak[limit] s
|
||||
// 助力 deputShip all operators DEF +(base + per·L), redeploy time ×(1 + respawn_time)
|
||||
// 突袭 raidShip member idle ≥ no_attack_duration s (or skill ready) with no enemy in range → "保留技力立即再部署"
|
||||
@@ -41,6 +42,7 @@
|
||||
|
||||
import { absoluteRangeKeys, canTargetEnemy } from '../../../targeting.js';
|
||||
import { localOrder, localBefore } from '../../../dir.js';
|
||||
import { isHpLoss } from '../../../damage.js';
|
||||
import {
|
||||
num, bondRecord, buffParams, bondTier, bondLayers, isMember, isElite, isGroundOp, onField, playerOps, passiveBuff,
|
||||
fxOn, N4, N8, bodyInKeys, directMods,
|
||||
@@ -440,7 +442,9 @@ export function install(battle) {
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (dmg.steadShare || dmg.steadThorn || c.type === 'element' || !st.members[ID.stead].has(t)) return;
|
||||
// a 流失 never uses a thorn chance (PRTS 作战机制 "生命流失不会触发反伤"); 无来源 damage does, hitting nobody (备注
|
||||
// "可被无来源伤害消耗反伤机会")
|
||||
if (dmg.steadShare || dmg.steadThorn || c.type === 'element' || isHpLoss(dmg) || !st.members[ID.stead].has(t)) return;
|
||||
const bb = st.bb[ID.stead];
|
||||
const last = t.mem[STEAD_CD] ?? -Infinity;
|
||||
if (battle.time - last < num(bb.cd_duration) - 1e-9) return;
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
|
||||
import { TICK, MOVE_SCALE, ELEMENT, ATTACK_PAUSE } from '../constants.js';
|
||||
import { canTargetAlly, sortAllyTargets, aggroCmp } from '../targeting.js';
|
||||
import { mitigate } from '../damage.js';
|
||||
import { mitigate, periodicDamage } from '../damage.js';
|
||||
|
||||
// ---------------------------------------------------------------------------------------------------------------
|
||||
// constants (numbers that exist nowhere in the data)
|
||||
@@ -1026,7 +1026,9 @@ function kitChimera(ab) {
|
||||
if (pulse) a.acc -= 1;
|
||||
for (const u of b.alliesInRadius(e.x, e.y, CHIMERA_AURA_RADIUS)) {
|
||||
auraBuff(b, u, 'ab:originium', 0.25, { spRecoveryMul: Math.max(0, 1 + spr) }, null, true);
|
||||
if (pulse && dmg > 0) b.loseHp(u, dmg, { source: e });
|
||||
// PRTS "持续视为受到源石污染区影响…受到50真实持续伤害": a damage instance (受击回复, the 重装 trigger), not a 流失 —
|
||||
// 无来源 like the 源石污染区 terrain it stands for [ASSUMED], the chimera keeps the credit
|
||||
if (pulse && dmg > 0) b.dealDamage(e, u, { ...periodicDamage(dmg), tags: ['dot', 'pollution'] });
|
||||
}
|
||||
},
|
||||
}];
|
||||
@@ -2294,7 +2296,7 @@ export const KITS = Object.freeze({
|
||||
enemy_1067_snslime: (ab, e) => [deathBoom({ scale: T(ab, 'boom.atk_scale') ?? 0, r: (e.def.raw.stats && e.def.raw.stats.rawRangeRadius) || BOOM_RADIUS, status: { key: 'cold', dur: T(ab, 'boom.freeze') ?? 0 } })], // 冰爆源石虫 · death: phys blast + cold
|
||||
enemy_1069_icebrk_2: (ab) => [{ hitOut(c, b, e) { if (c.dmg.isAttack && c.target.s.flags.freeze) c.dmg.amount *= T(ab, 'atkup.atk_scale') ?? 1; } }], // 雪怪小队破冰者 · ×3 vs frozen
|
||||
enemy_1026_aghost: () => [unblockable()], // 幽灵组长 · unblockable
|
||||
enemy_1062_rager_2: (ab) => [{ iv: 1, tick(b, e) { b.loseHp(e, T(ab, 'periodic_damage.damage') ?? 0, { source: null }); } }], // 狂暴宿主组长 · loses HP over time
|
||||
enemy_1062_rager_2: (ab) => [{ iv: 1, tick(b, e) { const v = T(ab, 'periodic_damage.damage') ?? 0; if (v > 0) b.dealDamage(null, e, periodicDamage(v)); } }], // 狂暴宿主组长 · "自身每秒受到500无来源真实伤害" (damage, not 流失)
|
||||
enemy_1183_mlasrt: (ab) => [ep('erosion', T(ab, 'EpDamage.attack@ep_damage_ratio') ?? 0), nthAttackPower(nthOf(ab.sk.PowerAttack), (ab.sk.PowerAttack && ab.sk.PowerAttack.bb.atk_scale) || 1)], // 无胄盟清扫小队 · erosion; every 4th attack ×1.5
|
||||
enemy_1273_stmgun_2: (ab) => [skill(ab.sk.Cannon, (b, e) => { // 高准度伦蒂尼姆城防自行炮 · locks the highest max-HP unit, bombards the highest HP% around it
|
||||
const lock = allTargets(b, e).sort((p, q) => q.s.maxHp - p.s.maxHp)[0];
|
||||
|
||||
@@ -36,7 +36,7 @@ import {
|
||||
num, itemRecord, itemKeyOf, buffsOf, isOp, onField, unitBonds, isMember, bondActive, isGroundOp, frontTile,
|
||||
alliesAround, passiveBuff, fxOn, battleStore, contentInfo, itemsOf, directMods,
|
||||
} from '../support/index.js';
|
||||
import { mitigate, hasHp } from '../../damage.js';
|
||||
import { mitigate, hasHp, periodicDamage, isHpLoss } from '../../damage.js';
|
||||
|
||||
// =====================================================================================================================
|
||||
// data helpers
|
||||
@@ -315,11 +315,15 @@ function onAttackEnemies(S, u, fn) {
|
||||
|
||||
/** Generic buff-key behaviours (shared by several items). */
|
||||
const BY_BUFF = {
|
||||
// 源石溶剂: 每秒流失 damage 点生命值 (HP loss, may kill)
|
||||
// 源石溶剂: the text's "每秒流失 damage 点生命值" is officially 每秒受到 damage 点真实伤害 (PRTS 盟约记录 修正 "并非流失", 备注
|
||||
// "造成无来源真实持续环境伤害"; the `periodic_damage` template — also 狂暴宿主 "自身每秒受到N无来源真实伤害"): a damage instance,
|
||||
// not a 流失 — shields, damage-taken modifiers and the target-side `hit` effects apply, and it is a "受到伤害" for 受击回复
|
||||
// SP, the 重装 TAKE_DAMAGE trigger and 信仰搅拌机 S3's counters (player report D1). 无来源: hooks see no source; the carrier
|
||||
// keeps the credit (a carrier the drain finishes off is its own kill, as before). May kill.
|
||||
periodic_damage(battle, u, p, S) {
|
||||
const d = num(p.damage);
|
||||
if (!(d > 0)) return;
|
||||
S.stat('drain', null, { interval: 1, onTick: ({ unit }) => { if (unit.deployed) battle.loseHp(unit, d, { source: unit }); } });
|
||||
S.stat('drain', null, { interval: 1, onTick: ({ unit }) => { if (unit.deployed) battle.dealDamage(unit, unit, periodicDamage(d)); } });
|
||||
},
|
||||
// 奥术法阵: 攻击使目标失去特殊能力 silence s
|
||||
silence_attachment(battle, u, p, S) {
|
||||
@@ -439,14 +443,14 @@ const BY_ITEM = {
|
||||
S.buff(u, { key, mods, refresh: 'stack', stacks: 1, maxStacks: cap, persist: true, allowDead: true });
|
||||
});
|
||||
},
|
||||
// 伪装服: first damage taken in the battle ⇒ 隐匿 `duration` s
|
||||
// 伪装服: first damage taken in the battle ⇒ 隐匿 `duration` s (a 流失 is not 受到伤害: damage.js isHpLoss)
|
||||
chess_item_4_04_e(battle, u, rec, S) {
|
||||
const p = bp(rec, 'act2autochess_equip_acarm055_global_buff');
|
||||
const d = p ? num(p.duration) : 0;
|
||||
if (!(d > 0)) return;
|
||||
let used = false;
|
||||
S.on('damaged', (c) => {
|
||||
if (used || c.target !== u || !(c.amount > 0) || !u.alive) return;
|
||||
if (used || c.target !== u || !(c.amount > 0) || !u.alive || isHpLoss(c.dmg)) return;
|
||||
used = true;
|
||||
battle.applyStatus(u, 'stealth', { duration: d, source: u });
|
||||
fxOn(battle, 'camouflage', u, 'item:chess_item_4_04_e', rec.id, { duration: d });
|
||||
|
||||
@@ -61,6 +61,11 @@ export const cheb = (a, b) => (b.hitArea ? bodyTileReach(b, Math.round(a.y), Mat
|
||||
export const isMainHit = (dmg) => !!dmg && dmg.isAttack && !dmg.isSplash && !(dmg.tags && dmg.tags.includes('chain'));
|
||||
/** Damage ctx caused by an enemy's attack. */
|
||||
export const byEnemyAttack = (ctx) => !!ctx.source && ctx.source.side === 'enemy' && !!ctx.dmg && ctx.dmg.isAttack;
|
||||
/**
|
||||
* `damaged` ctx of a damage that removed HP and can give 受击回复 SP — the engine's rule (damage.js applyHpLoss: not a 流失
|
||||
* (`noSp`, Battle.loseHp), not an element 损伤), whatever its source: an attack, a zone, the 无来源 源石溶剂 tick.
|
||||
*/
|
||||
export const hurtSpDamage = (ctx) => !!ctx.dmg && !ctx.dmg.noSp && ctx.type !== 'element' && ctx.amount > 0;
|
||||
/** A timed skill is running (no SP may be gained). */
|
||||
export const skillBusy = (u) => !!(u.skill && u.skill.active && u.skill.isTimed);
|
||||
/** Give SP unless a timed skill is running (AK: no SP gain during a skill). */
|
||||
@@ -1033,8 +1038,10 @@ export default {
|
||||
talents: [{ install(battle, unit) {
|
||||
const sp = num(t.sp);
|
||||
if (sp > 0) {
|
||||
// PRTS 备注 "仅伤害量不为0且能够触发受击回复的伤害才能触发此天赋": any such damage, not only an enemy attack
|
||||
// (a zone tick, the 源石溶剂 drain — player report D1 audit)
|
||||
onDamagedOn(battle, unit, (ctx) => {
|
||||
if (!byEnemyAttack(ctx) || !up(unit)) return;
|
||||
if (!hurtSpDamage(ctx) || !up(unit)) return;
|
||||
giveSp(unit, sp);
|
||||
const mates = alliesInGridOf(battle, unit, grid ?? [[1, 0], [0, -1], [0, 1], [-1, 0]]).filter((a) => a !== unit && a.skill && !a.skill.noSkill);
|
||||
const m = battle.rng.pick(mates);
|
||||
|
||||
@@ -592,11 +592,13 @@ export default {
|
||||
},
|
||||
|
||||
// ---------------------------------------------------------------------------------------------------------------
|
||||
// 2_13 蒂比 紧急赶场通知 (AUTO): "受到攻击后触发" — any incoming enemy attack sets it off (the kit is the only
|
||||
// trigger: the engine rule is disabled so non-attack damage never fires it) and a physical/arts one is dodged; takes
|
||||
// off for the duration: skill range, ATK +atk, attacks become 3 shots, blocks flying (not ground) enemies. Trait
|
||||
// "起飞后能够阻挡2个飞行敌人": flying enemies are blocked only while airborne. 片场工作指南: if not attacked for
|
||||
// stack_time s, the next physical/arts attack is dodged (prob); every attack restarts that timer.
|
||||
// 2_13 蒂比 紧急赶场通知 (AUTO): "受到攻击后触发" is officially "受到伤害前触发" (PRTS 修正 原因 6; 备注 "在受到伤害前自动
|
||||
// 触发技能…若本次伤害为物理或法术,再将本次伤害闪避") — any incoming damage instance (an attack, a zone tick, the 源石溶剂
|
||||
// drain; never a 流失, which has no `hit`) sets it off (the kit is the only trigger: the engine rule is disabled) and a
|
||||
// physical/arts one is dodged; takes off for the duration: skill range, ATK +atk, attacks become 3 shots, blocks flying
|
||||
// (not ground) enemies. Trait "起飞后能够阻挡2个飞行敌人": flying enemies are blocked only while airborne. 片场工作指南
|
||||
// ("若最近N秒内未受伤害" — 修正 原文 攻击): if no damage for stack_time s, the next physical/arts damage is dodged (prob);
|
||||
// every damage instance restarts that timer (dodged or not; a 流失 does not — PRTS 备注).
|
||||
// S1 专业喷绘技巧 (alt, DEFAULT trigger from data): takes off at once for its duration — skill range, ATK +atk, blocks
|
||||
// flyers only; no triple shot, and incoming attacks never set it off.
|
||||
chess_char_2_13_a: (bb, chess, def) => {
|
||||
@@ -627,7 +629,7 @@ export default {
|
||||
battle.on('deploy', ({ unit: u }) => { if (u === unit) last = battle.time - st; }, { owner: unit });
|
||||
onHitOn(battle, unit, (ctx) => {
|
||||
const { dmg } = ctx;
|
||||
if (!byEnemyAttack(ctx) || dmg.cancel || !up(unit)) return;
|
||||
if (!dmg || dmg.cancel || !up(unit)) return;
|
||||
const dodgeable = dmg.type === 'phys' || dmg.type === 'arts';
|
||||
const sk = unit.skill;
|
||||
let dodged = false;
|
||||
|
||||
@@ -31,6 +31,7 @@ import { absoluteRangeKeys, canTargetEnemy, sortEnemyTargets } from '../../targe
|
||||
import { normalizeChess } from '../../simdata.js';
|
||||
import { aggregateMods } from '../../buffs.js';
|
||||
import { CAT_SHIELD_KEY } from '../tokens.js';
|
||||
import { isHpLoss } from '../../damage.js';
|
||||
|
||||
const TICK_EPS = 0.01; // minimal status duration (s)
|
||||
const AURA = 0.2; // aura refresh period (s)
|
||||
@@ -286,18 +287,27 @@ const kits = {
|
||||
// module SPT-X (elite default): 攻击范围内敌人的隐匿效果失效
|
||||
if (moduleIs(def, 'uniequip_002_rmixer')) whileDeployed(battle, unit, AURA, () => reveal(battle, enemiesOnRange(battle, unit)));
|
||||
if (!S3) return;
|
||||
// S3 counter: when hit, fire one volley at ≤ 3 enemies in range (min interval = interval × ratio)
|
||||
// S3 counter (PRTS 备注 "反击受到任何伤害后均可触发,无需目标,视为普通攻击,受各类无法触发普通攻击效果的影响"): ANY damage
|
||||
// she takes — an enemy's attack, a zone, the 无来源 源石溶剂 tick (items/battle.js periodic_damage) — fires one volley at
|
||||
// ≤ 3 enemies in range (the attacker first), at most once per actual interval × ratio. With no enemy in range the
|
||||
// counter still happens and spends its bullet (无需目标: the drain alone empties the skill — 莫斯提马's 特质 turns those
|
||||
// bullets into 拉特兰 layers, player report D1). A 流失 (Battle.loseHp, tag 'hpLoss': it skips every damage event —
|
||||
// PRTS 作战机制) and an element 损伤 never counter; stunned / disarmed: no counter
|
||||
battle.on('damaged', (c) => {
|
||||
if (c.target !== unit || !unit.canAct || !skillActive(unit) || !c.source || c.source.side !== 'enemy' || !c.dmg?.isAttack) return;
|
||||
const d = c.dmg;
|
||||
if (c.target !== unit || !unit.canAct || unit.s.flags.disarm || !skillActive(unit) || !d || c.type === 'element') return;
|
||||
if (isHpLoss(d)) return;
|
||||
if (battle.time < (unit.mem.counterReady ?? -Infinity) - 1e-9) return;
|
||||
const list = targetsInRange(battle, unit);
|
||||
const i = list.indexOf(c.source);
|
||||
const i = c.source ? list.indexOf(c.source) : -1;
|
||||
if (i > 0) { list.splice(i, 1); list.unshift(c.source); }
|
||||
const targets = list.slice(0, counterMax);
|
||||
if (!targets.length) return;
|
||||
unit.mem.counterReady = battle.time + unit.s.interval * counterRatio;
|
||||
unit.mem.inCounter = true;
|
||||
try { battle.forceAttack(unit, targets); } finally { unit.mem.inCounter = false; }
|
||||
try {
|
||||
if (targets.length) battle.forceAttack(unit, targets);
|
||||
else { unit.stats.attacks++; unit.skill.onAttackPerformed([], true); } // a counter into nothing: its bullet goes
|
||||
} finally { unit.mem.inCounter = false; }
|
||||
battle.fx('counter', { x: unit.x, y: unit.y, id: unit.id, n: targets.length });
|
||||
}, { owner: unit });
|
||||
},
|
||||
@@ -1151,7 +1161,7 @@ const kits = {
|
||||
install(battle, unit) {
|
||||
battle.on('damaged', (c) => {
|
||||
const t = c.target;
|
||||
if (t.side !== 'ally' || !t.alive || !(c.amount > 0) || !t.findBuff(`reckpr:guard:${unit.id}`)) return;
|
||||
if (t.side !== 'ally' || !t.alive || !(c.amount > 0) || isHpLoss(c.dmg) || !t.findBuff(`reckpr:guard:${unit.id}`)) return;
|
||||
battle.heal(unit, t, num(bb['attack@fixed_heal_value'], 80));
|
||||
}, { owner: unit });
|
||||
installLowHpHealBonus(battle, unit, tb);
|
||||
@@ -1544,7 +1554,7 @@ const kits = {
|
||||
onStart({ battle, unit }) { battle.fx('featherArrow', { x: unit.x, y: unit.y, id: unit.id }); } },
|
||||
talents: [
|
||||
{ install(battle, unit) { // 凝神: ATK +15 % when not hurt for 10 s
|
||||
battle.on('damaged', (c) => { if (c.target === unit && c.amount > 0) unit.mem.lastHurt = battle.time; }, { owner: unit });
|
||||
battle.on('damaged', (c) => { if (c.target === unit && c.amount > 0 && !isHpLoss(c.dmg)) unit.mem.lastHurt = battle.time; }, { owner: unit }); // (a 流失 is not 受伤害)
|
||||
battle.on('deploy', (c) => { if (c.unit === unit) unit.mem.lastHurt = -Infinity; }, { owner: unit });
|
||||
whileDeployed(battle, unit, 0.1, () => toggleBuff(battle, unit, 'fartth:focus', battle.time - (unit.mem.lastHurt ?? -Infinity) >= num(t0.delay, 10) - 1e-9, { atkPct: num(t0.atk, 0.15) }));
|
||||
} },
|
||||
|
||||
@@ -35,7 +35,7 @@ import { COLS } from '../../constants.js';
|
||||
import { bodyInKeys, bodyInRadius, bodyKeys } from '../../body.js';
|
||||
import { absoluteRangeKeys, sortEnemyTargets } from '../../targeting.js';
|
||||
import { frontOf, rotateOffset, toLocal } from '../../dir.js';
|
||||
import { mitigate, hasHp } from '../../damage.js';
|
||||
import { mitigate, hasHp, isHpLoss } from '../../damage.js';
|
||||
|
||||
// ---- text-only constants (the official blackboards carry no key for these) --------------------------------------
|
||||
/** 华法琳 S1 "只当目标生命值不满一半时才会触发"; 塞雷娅 S1 "血量小于等于一半"; 山 module "生命值高于50%时". */
|
||||
@@ -834,7 +834,7 @@ const KITS = {
|
||||
talents: [
|
||||
{ install(battle, unit) { // 本性的坚守
|
||||
battle.on('damaged', (c) => {
|
||||
if (c.target !== unit || !on(unit) || unit.hp <= 0 || !(c.amount > 0) || c.type === 'element') return;
|
||||
if (c.target !== unit || !on(unit) || unit.hp <= 0 || !(c.amount > 0) || c.type === 'element' || isHpLoss(c.dmg)) return; // (not a 流失)
|
||||
const v = (unit.hpRatio < num(t0.hp_ratio, 0.5) ? num(t0.value2) : num(t0.value1)) * (unit.skill?.active ? t0Scale : 1);
|
||||
if (v > 0) battle.heal(unit, unit, v, { self: true });
|
||||
}, { owner: unit });
|
||||
@@ -2495,7 +2495,7 @@ const KITS = {
|
||||
install(battle, unit) {
|
||||
battle.on('damaged', (c) => {
|
||||
const a = c.target;
|
||||
if (a.side !== 'ally' || !(c.amount > 0) || a.hp <= 0 || c.type === 'element') return;
|
||||
if (a.side !== 'ally' || !(c.amount > 0) || a.hp <= 0 || c.type === 'element' || isHpLoss(c.dmg)) return; // (not a 流失)
|
||||
const b = a.findBuff('reckpr:guard');
|
||||
if (b && b.data.src === unit) battle.heal(unit, a, num(b.data.value));
|
||||
}, { owner: unit });
|
||||
|
||||
@@ -87,6 +87,27 @@ export function makeDamageInfo(d = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* DamageInfo of the official `periodic_damage` template — 源石溶剂 (PRTS 盟约记录: "受到60真实伤害", 修正 "并非流失", 备注
|
||||
* "造成无来源真实持续环境伤害"), 狂暴宿主组长 ("自身每秒受到500无来源真实伤害"): 无来源 true 持续 damage (PRTS 伤害分类: attack
|
||||
* type BUFF, "自残类型"). A damage instance, not a 流失 (Battle.loseHp skips every damage event — PRTS 作战机制 "生命流失不会
|
||||
* 触发反伤、受击回复"): shields, damage-taken multipliers and target-side `hit` effects apply, and it is a "受到伤害" for
|
||||
* 受击回复 SP and the 重装 TAKE_DAMAGE trigger. Tagged 'dot' (a fixed-value DoT: ATK multipliers skip it; 锡人's 持续伤害
|
||||
* boost takes it) and 'periodic'. The caller passes the credit (the carrier / null) as the dealDamage source.
|
||||
*/
|
||||
export function periodicDamage(amount) {
|
||||
return makeDamageInfo({ amount, type: 'true', canDodge: false, sourceless: true, tags: ['dot', 'periodic'] });
|
||||
}
|
||||
|
||||
/**
|
||||
* Is this the DamageInfo of a 流失 (Battle.loseHp, tag 'hpLoss')? Its `damaged` hook still runs (stats, leader parts,
|
||||
* HP thresholds), but a 流失 is not "受到伤害": "受到伤害时" content (heals, counters, 未受伤害 timers, 反伤 chances)
|
||||
* skips it — PRTS 作战机制 "生命流失…跳过所有结算与伤判效果…不会触发反伤、受击回复等受到攻击触发的时点".
|
||||
*/
|
||||
export function isHpLoss(dmg) {
|
||||
return !!dmg && Array.isArray(dmg.tags) && dmg.tags.includes('hpLoss');
|
||||
}
|
||||
|
||||
/** 沉睡 (ba.sleep "无敌且无法行动"): only attackers whose profile has `hitSleep` (or `ignoreSleep` damage) reach a sleeper. */
|
||||
function sleepBlocks(target, source, dmg) {
|
||||
return !!target.s.flags.sleep && !dmg.ignoreSleep && !(source && source.profile && source.profile.hitSleep);
|
||||
|
||||
@@ -0,0 +1,341 @@
|
||||
// Player report D1 (community, after 0.1.0): "干员信仰搅拌机原版装备源石溶剂的装备带来的自动扣血会触发3技能的释放,这样配合
|
||||
// 莫斯提马能自动叠层". Official (PRTS 卫戍协议:盟约 下半/PRTS盟约记录, 源石溶剂): the item's "每秒流失60点生命值" is corrected to
|
||||
// "受到60真实伤害" (修正 原因 6, "并非流失"), 备注 "造成无来源真实持续环境伤害" — a damage instance, not a 生命流失 (PRTS
|
||||
// 作战机制: a 流失 skips every damage event — 反伤, 受击回复 …). So the drain is a "受到伤害" for the 重装 技能策略 (PRTS
|
||||
// 卫戍协议/帮助 "受到伤害时释放技能"), and 信仰搅拌机 S3's counter (PRTS 备注 "反击受到任何伤害后均可触发,无需目标,视为普通
|
||||
// 攻击") fires on every drain tick, spending a bullet even with no enemy in range — next to 莫斯提马 (特质 "自身周围4格的干员
|
||||
// 每消耗6发弹药,使已激活的【拉特兰】层数+1/+2") that is 拉特兰 layers with no enemy involved.
|
||||
// The same `periodic_damage` template drives 狂暴宿主组长 (PRTS "自身每秒受到500无来源真实伤害"), and 孽罪奇美拉's pollution aura
|
||||
// on operators is "受到…真实持续伤害" (PRTS) — both damage too. Operator 生命流失 (华法琳, 史尔特尔, 号角 …) stays a 流失.
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { makeBattle, enemyRec, checkInvariants } from '../helpers/battleHarness.js';
|
||||
import { makeMatch, give, giveItem, DATA } from '../match/harness.js';
|
||||
import { buildBattleSpec, createBattleFromSpec } from '../../server/sim/spec.js';
|
||||
import { getDefaultSource } from '../../server/sim/simdata.js';
|
||||
|
||||
const ds = getDefaultSource();
|
||||
const SOLVENT = 'chess_item_1_05_e_a', SOLVENT_B = 'chess_item_1_05_e_b';
|
||||
const MIXER = 'chess_char_4_01_a', MOSTIMA = 'chess_char_4_02_a', INSIDE = 'chess_char_1_01_a', YAK = 'chess_char_1_02_a';
|
||||
const gold = (id) => id.replace(/_a$/, '_b');
|
||||
const DRAIN = DATA.items[SOLVENT].params.damage; // 60
|
||||
const isMixer = (u) => !!u && u.defId.startsWith('chess_char_4_01_');
|
||||
const approx = (a, b, eps = 1e-6, msg = '') => assert.ok(Math.abs(a - b) <= eps * Math.max(1, Math.abs(b)), `${msg} ${a} ≈ ${b}`);
|
||||
const dummy = (o = {}) => enemyRec({ key: 'enemy_dummy', hp: 1e8, atk: 0, speed: 0, ...o });
|
||||
const hpLoss = (c) => !!(c.dmg && (c.dmg.tags || []).includes('hpLoss'));
|
||||
|
||||
/**
|
||||
* The real match path: a solo NORMAL match at PREP R3, 信仰搅拌机 (default S3) carrying 源石溶剂 with 莫斯提马 on the tile
|
||||
* above (her 周围4格) and 隐现 as the third 拉特兰 (3 distinct ⇒ 拉特兰 active), the BattleSpec of that board
|
||||
* (Match._normalOpts → buildBattleSpec → createBattleFromSpec — what the server and the browser run). `noEnemies`: the
|
||||
* wave's spawns are dropped and the field runs to its time limit, so whatever happens comes from the drain alone.
|
||||
*/
|
||||
function realBattle({ elite = false, noEnemies = false, item = SOLVENT } = {}) {
|
||||
const h = makeMatch({ mode: 'solo', difficulty: 'NORMAL', seed: 5 }).start();
|
||||
const m = h.m;
|
||||
h.toPrep(3);
|
||||
const ps = h.ps('p_0');
|
||||
for (const p of [...ps.board.values()]) ps.returnCopies(p);
|
||||
ps.board.clear();
|
||||
const mixId = elite ? gold(MIXER) : MIXER, mosId = elite ? gold(MOSTIMA) : MOSTIMA;
|
||||
const mix = give(m, ps, mixId, 'board', [9, 4]);
|
||||
give(m, ps, mosId, 'board', [10, 4]);
|
||||
give(m, ps, INSIDE, 'board', [11, 4]);
|
||||
const it = giveItem(m, ps, item);
|
||||
assert.deepEqual(ps.equip(it.uid, mix.uid), { ok: true });
|
||||
ps.recompute();
|
||||
const spec = buildBattleSpec(m._normalOpts(ps));
|
||||
assert.ok(spec.players[0].units.find((u) => u.chessId === mixId).items.includes(item), 'the BattleSpec carries the item');
|
||||
if (noEnemies) { spec.spawns = []; spec.timeLimit = 150; }
|
||||
const b = createBattleFromSpec(spec, m.ds);
|
||||
if (noEnemies) b.autoFinish = false;
|
||||
const rec = { casts: [], ammo: [], enemyHits: [], layers: [], counters: 0 };
|
||||
b.on('skillStart', ({ unit, reason }) => { if (isMixer(unit)) rec.casts.push({ t: b.time, reason, enemies: b.enemies.filter((e) => e.alive).length }); });
|
||||
b.on('ammoUsed', ({ unit }) => { if (isMixer(unit)) rec.ammo.push({ t: b.time, enemies: b.enemies.filter((e) => e.alive).length }); });
|
||||
b.on('damaged', (c) => { if (isMixer(c.target) && c.source && c.source.side === 'enemy') rec.enemyHits.push(b.time); });
|
||||
b.on('layerGain', (c) => { if (c.bondId === 'lateranoShip') rec.layers.push({ t: b.time, n: c.n }); });
|
||||
return { h, m, ps, b, rec, mixId, mosId };
|
||||
}
|
||||
|
||||
test('D1 real path, drain alone: 信仰搅拌机 S3 fires on a 源石溶剂 tick (重装 "受到伤害时") and spends its bullets on target-less counters; 莫斯提马 turns them into 拉特兰 layers', () => {
|
||||
for (const elite of [false, true]) {
|
||||
const { m, b, rec, mixId, mosId } = realBattle({ elite, noEnemies: true });
|
||||
const u = b.allyUnits.find(isMixer);
|
||||
const sk = ds.getChess(mixId).skill;
|
||||
assert.equal(sk.id, 'skchr_rmixer_3', 'S3 is her default loadout');
|
||||
const res = b.runToEnd(400);
|
||||
assert.equal(b.errorCount, 0, JSON.stringify(b.errors));
|
||||
assert.equal(b.enemies.length, 0, 'no enemy ever entered the field');
|
||||
// the cast: the first drain tick once the SP (initSp → spCost at 1/s) is full — not "never" (v0.1.0: the 流失 skipped
|
||||
// the 重装 trigger, she drained to death with a full SP bar)
|
||||
assert.ok(rec.casts.length >= 1, `${mixId}: S3 cast from the drain alone`);
|
||||
const first = rec.casts[0];
|
||||
assert.equal(first.reason, 'TAKE_DAMAGE');
|
||||
const ready = sk.spCost - sk.initSp;
|
||||
assert.ok(first.t >= ready - 1e-6 && first.t <= ready + 1 + 1e-6, `${mixId}: cast at the first tick after ${ready} s (${first.t})`);
|
||||
// counters with nobody in range: one bullet per drain tick, every one with no enemy on the field
|
||||
assert.ok(rec.ammo.length >= 6, `${mixId}: bullets spent (${rec.ammo.length})`);
|
||||
assert.ok(rec.ammo.every((a) => a.enemies === 0));
|
||||
const gaps = rec.ammo.slice(1).map((a, i) => a.t - rec.ammo[i].t);
|
||||
assert.ok(gaps.every((g) => g > 1 - 1e-6), `${mixId}: at most one counter per drain tick`);
|
||||
// 莫斯提马's 特质 (garrison_22): +bond_add_count 拉特兰 per 6 bullets of the operators on her 4 tiles
|
||||
const gar = DATA.garrisons[DATA.chess[mosId].garrisonIds[0]].bb;
|
||||
const expected = Math.floor(rec.ammo.length / gar.consume_count) * gar.bond_add_count;
|
||||
assert.ok(expected >= gar.bond_add_count);
|
||||
assert.equal(res.perPlayer.p_0.layerGains.lateranoShip, expected, `${mixId}: ${rec.ammo.length} bullets → ${expected} layers`);
|
||||
assert.ok(u.stats.attacks >= rec.ammo.length, 'each counter counts as an attack (视为普通攻击)');
|
||||
checkInvariants(b);
|
||||
m.dispose?.();
|
||||
}
|
||||
});
|
||||
|
||||
test('D1 real path, the round-3 wave: the drain casts S3 before any enemy touched her; the counters and 莫斯提马 add 拉特兰 layers', () => {
|
||||
const { m, b, rec } = realBattle();
|
||||
const res = b.runToEnd(400);
|
||||
assert.equal(b.errorCount, 0, JSON.stringify(b.errors));
|
||||
assert.ok(rec.casts.length >= 1, 'S3 cast');
|
||||
const firstHit = rec.enemyHits.length ? rec.enemyHits[0] : Infinity;
|
||||
assert.ok(rec.casts[0].t < firstHit, `the first cast (${rec.casts[0].t}) came before any enemy hit her (${firstHit})`);
|
||||
assert.equal(rec.casts[0].reason, 'TAKE_DAMAGE');
|
||||
assert.ok(rec.ammo.length >= 6, `bullets spent (${rec.ammo.length})`);
|
||||
assert.ok((res.perPlayer.p_0.layerGains.lateranoShip ?? 0) >= 1, JSON.stringify(res.perPlayer.p_0.layerGains));
|
||||
m.dispose?.();
|
||||
});
|
||||
|
||||
test('源石溶剂 drain = 无来源 true damage each second (not 流失): hooks see no source, the carrier keeps the credit; shields absorb it, 脆弱 scales it', () => {
|
||||
for (const item of [SOLVENT, SOLVENT_B]) {
|
||||
const h = makeBattle({ units: [{ chessId: YAK, row: 10, col: 4, items: [item], skillIndex: 1 }], timeLimit: 60, hooks: ['hit', 'damaged', 'kill'], captureNoisy: true });
|
||||
h.step();
|
||||
const u = h.unit(YAK);
|
||||
u.skill.sp = 0; // keep 角峰's own skill out of the way
|
||||
const hp0 = u.hp;
|
||||
h.run(1.02);
|
||||
const ticks = h.hooksOf('damaged').filter((c) => c.target === u);
|
||||
assert.equal(ticks.length, 1, `${item}: one tick per second`);
|
||||
const c = ticks[0];
|
||||
assert.equal(c.source, null, 'no source (无来源)');
|
||||
assert.equal(c.credit, u, 'the carrier keeps the credit');
|
||||
assert.equal(c.type, 'true');
|
||||
assert.ok(!hpLoss(c), 'a damage instance, not a 流失');
|
||||
assert.ok(h.hooksOf('hit').some((x) => x.target === u && x.source === null), "the 'hit' hook runs (伤判 effects apply)");
|
||||
approx(hp0 - u.hp, DRAIN, 1e-9, `${item}: −${DRAIN}`);
|
||||
// a shield takes the tick
|
||||
h.b.addBuff(u, { key: 'test:shield', shield: 1000 });
|
||||
const hp1 = u.hp;
|
||||
h.run(1);
|
||||
approx(u.hp, hp1, 1e-9, 'absorbed by the shield');
|
||||
approx(u.findBuff('test:shield').shield, 1000 - DRAIN, 1e-9);
|
||||
h.b.removeBuff(u, 'test:shield');
|
||||
// 脆弱 (受到的物理、法术、真实伤害提升) scales it
|
||||
h.b.applyStatus(u, 'fragile', { duration: 30, value: 0.5 });
|
||||
const hp2 = u.hp;
|
||||
h.run(1);
|
||||
approx(hp2 - u.hp, DRAIN * 1.5, 1e-9, 'fragile ×1.5');
|
||||
// lethal: the carrier's own kill (credit untouched)
|
||||
u.hp = 10;
|
||||
h.run(1);
|
||||
assert.ok(!u.alive);
|
||||
const k = h.hooksOf('kill').find((x) => x.victim === u);
|
||||
assert.ok(k && k.killer === u, 'killed by the drain: credited to the carrier');
|
||||
checkInvariants(h.b);
|
||||
}
|
||||
});
|
||||
|
||||
test('the drain feeds 受击回复 SP and the 重装 TAKE_DAMAGE trigger with no enemy around; a 流失 (Battle.loseHp) feeds neither', () => {
|
||||
// 信仰搅拌机 S1 铳骑主考官: 受击回复 (+1 per hit taken)
|
||||
const h = makeBattle({ units: [{ chessId: MIXER, row: 10, col: 4, items: [SOLVENT], skillIndex: 0 }], timeLimit: 60, hooks: ['spGain'], captureNoisy: true });
|
||||
h.step();
|
||||
const u = h.unit(MIXER);
|
||||
assert.equal(u.skill.spType, 'hurt');
|
||||
const sp0 = u.skill.sp;
|
||||
h.run(3.02);
|
||||
approx(u.skill.sp - sp0, 3, 1e-9, 'three drain ticks → +3 SP');
|
||||
h.b.loseHp(u, 50);
|
||||
approx(u.skill.sp - sp0, 3, 1e-9, 'a 流失 gives no 受击回复 SP');
|
||||
// 角峰 S1 (MANUAL, 重装 ⇒ TAKE_DAMAGE): ready, no enemy — the drain casts it; a 流失 does not
|
||||
for (const drain of [true, false]) {
|
||||
const g = makeBattle({ units: [{ chessId: YAK, row: 10, col: 4, items: drain ? [SOLVENT] : [], skillIndex: 0 }], timeLimit: 60, hooks: ['skillStart'] });
|
||||
g.step();
|
||||
const y = g.unit(YAK);
|
||||
assert.equal(y.skill.rule, 'TAKE_DAMAGE');
|
||||
y.skill.gainSp(1000);
|
||||
if (!drain) g.b.every(1, () => { if (y.alive) g.b.loseHp(y, DRAIN); });
|
||||
g.run(1.5);
|
||||
const cast = g.hooksOf('skillStart').filter((c) => c.unit === y);
|
||||
if (drain) assert.equal(cast.length && cast[0].reason, 'TAKE_DAMAGE', 'the drain is a 受到伤害');
|
||||
else assert.equal(cast.length, 0, '生命流失 skips the 重装 trigger');
|
||||
}
|
||||
});
|
||||
|
||||
test('信仰搅拌机 S3 counter: any damage (an attack, a sourceless zone tick) counters, with no target it still spends a bullet; 流失, element 损伤, stun and the min interval do not', () => {
|
||||
for (const id of [MIXER, gold(MIXER)]) {
|
||||
const bb = ds.getChess(id).skill.bb;
|
||||
const h = makeBattle({ defs: { enemies: { enemy_dummy: dummy() } }, units: [{ chessId: id, row: 10, col: 4 }], timeLimit: 120, hooks: ['ammoUsed', 'attack', 'damaged'], captureNoisy: true });
|
||||
h.step();
|
||||
const u = h.unit(id);
|
||||
assert.ok(u.skill.activate('test', { free: true }));
|
||||
const left0 = u.skill.ammoLeft;
|
||||
const used = () => h.hooksOf('ammoUsed').filter((c) => c.unit === u).length;
|
||||
const zone = (amount = 10) => h.b.dealDamage(null, u, { amount, type: 'true', canDodge: false, tags: ['zone'] });
|
||||
// no enemy anywhere: a sourceless tick still counters (无需目标) — one bullet, no attack hook
|
||||
zone();
|
||||
assert.equal(used(), 1, `${id}: target-less counter spends a bullet`);
|
||||
assert.equal(u.skill.ammoLeft, left0 - 1);
|
||||
// within the min interval (actual interval × ratio): nothing
|
||||
zone();
|
||||
assert.equal(used(), 1, 'min interval');
|
||||
const gap = u.s.interval * bb.base_attack_time;
|
||||
h.run(gap + 0.05);
|
||||
// 流失 and element 损伤 never counter
|
||||
h.b.loseHp(u, 10);
|
||||
h.b.dealDamage(null, u, { amount: 10, type: 'element', element: 'burn' });
|
||||
assert.equal(used(), 1, '流失 / element 损伤: no counter');
|
||||
// stunned: no counter
|
||||
h.b.applyStatus(u, 'stun', { duration: 0.5 });
|
||||
zone();
|
||||
assert.equal(used(), 1, 'stunned: no counter');
|
||||
h.run(0.6);
|
||||
// with an enemy in range: the counter hits it (a real attack) and spends a bullet
|
||||
const e = h.spawn('enemy_dummy', { pos: [10, 5] });
|
||||
h.step();
|
||||
zone();
|
||||
assert.equal(used(), 2);
|
||||
h.run(0.5); // (her shot is a projectile)
|
||||
const atk = h.hooksOf('attack').filter((c) => c.attacker === u);
|
||||
assert.equal(atk.length, 1, 'one counter attack');
|
||||
assert.deepEqual(atk[0].targets, [e]);
|
||||
assert.ok(h.hooksOf('damaged').some((c) => c.source === u && c.target === e && c.dmg.isAttack), 'the counter hit the enemy');
|
||||
assert.equal(h.b.errorCount, 0, JSON.stringify(h.b.errors));
|
||||
checkInvariants(h.b);
|
||||
}
|
||||
});
|
||||
|
||||
test('狂暴宿主组长 (periodic_damage, PRTS "自身每秒受到500无来源真实伤害"): sourceless true damage per second, scaled by 脆弱', () => {
|
||||
const key = 'enemy_1062_rager_2';
|
||||
const h = makeBattle({ units: [], timeLimit: 60, hooks: ['damaged'], captureNoisy: true, autoFinish: false });
|
||||
h.step();
|
||||
const e = h.spawn(key, { pos: [10, 8] });
|
||||
h.b.applyStatus(e, 'stun', { duration: 30 });
|
||||
h.run(2.05);
|
||||
const ticks = h.hooksOf('damaged').filter((c) => c.target === e);
|
||||
assert.ok(ticks.length >= 2, `ticks ${ticks.length}`);
|
||||
for (const c of ticks) { assert.equal(c.source, null); assert.equal(c.type, 'true'); assert.ok(!hpLoss(c), 'damage, not 流失'); }
|
||||
const per = ticks[0].amount;
|
||||
assert.ok(per > 0);
|
||||
h.b.applyStatus(e, 'fragile', { duration: 30, value: 0.3 });
|
||||
const n = ticks.length;
|
||||
h.run(1);
|
||||
const t2 = h.hooksOf('damaged').filter((c) => c.target === e).slice(n);
|
||||
assert.ok(t2.length >= 1);
|
||||
approx(t2[0].amount, per * 1.3, 1e-9, '脆弱 scales it');
|
||||
});
|
||||
|
||||
test('孽罪奇美拉 pollution aura on operators is 真实持续伤害 (not 流失): 受击回复 SP and the 重装 trigger respond to it', () => {
|
||||
// act1 m04: the chimera switches to 污染模式 on its infection tiles (10,6) / (11,6); stunned, it never attacks
|
||||
const h = makeBattle({
|
||||
stageId: 'act1autochess_m04', units: [{ chessId: YAK, row: 10, col: 5, skillIndex: 0 }, { chessId: MIXER, row: 10, col: 7, skillIndex: 0 }],
|
||||
timeLimit: 60, hooks: ['damaged', 'skillStart'], captureNoisy: true, autoFinish: false,
|
||||
});
|
||||
h.step();
|
||||
const y = h.unit(YAK), mx = h.unit(MIXER);
|
||||
const e = h.spawn('enemy_1425_lrcmra', { pos: [10, 6] });
|
||||
h.b.applyStatus(e, 'stun', { duration: 60 });
|
||||
y.skill.gainSp(1000);
|
||||
const sp0 = mx.skill.sp;
|
||||
h.run(3);
|
||||
const aura = h.hooksOf('damaged').filter((c) => c.target === y && c.credit === e);
|
||||
assert.ok(aura.length >= 1, 'aura ticks on 角峰');
|
||||
for (const c of aura) { assert.equal(c.type, 'true'); assert.ok(!hpLoss(c), 'damage, not 流失'); }
|
||||
assert.ok(h.hooksOf('skillStart').some((c) => c.unit === y && c.reason === 'TAKE_DAMAGE'), '角峰 S1 cast from the aura');
|
||||
assert.ok(mx.skill.sp > sp0, '受击回复 SP from the aura');
|
||||
assert.equal(h.b.errorCount, 0, JSON.stringify(h.b.errors));
|
||||
});
|
||||
|
||||
test('audit — 雷蛇 战术防御 (PRTS "仅伤害量不为0且能够触发受击回复的伤害"): the drain gives him and a neighbour SP; a 流失 or a shielded tick does not', () => {
|
||||
const LISKAM = 'chess_char_1_20_a';
|
||||
const h = makeBattle({ units: [{ chessId: LISKAM, row: 10, col: 4, items: [SOLVENT] }, { chessId: YAK, row: 10, col: 5, skillIndex: 1 }], timeLimit: 60, hooks: ['spGain'], captureNoisy: true });
|
||||
h.step();
|
||||
const u = h.unit(LISKAM), y = h.unit(YAK);
|
||||
const gains = (who, reason) => h.hooksOf('spGain').filter((c) => c.unit === who && c.reason === reason).reduce((s, c) => s + c.amount, 0);
|
||||
const tsp = ds.getChess(LISKAM).talents[0].bb.sp;
|
||||
h.run(2.02);
|
||||
approx(gains(u, 'hurt'), 2, 1e-9, 'two ticks: 受击回复');
|
||||
approx(gains(u, 'talent'), 2 * tsp, 1e-9, 'two ticks: 战术防御 on himself');
|
||||
approx(gains(y, 'talent'), 2 * tsp, 1e-9, 'the neighbour (the only one on his 4 tiles) +1 per tick');
|
||||
h.b.loseHp(u, 10);
|
||||
approx(gains(u, 'talent') + gains(u, 'hurt'), 2 + 2 * tsp, 1e-9, 'a 流失: nothing');
|
||||
h.b.addBuff(u, { key: 'test:shield', shield: 1000 });
|
||||
h.run(1);
|
||||
approx(gains(u, 'hurt'), 3, 1e-9, 'a tick the shield took still gives 受击回复');
|
||||
approx(gains(u, 'talent'), 2 * tsp, 1e-9, '…but no 战术防御 ("伤害量不为0")');
|
||||
});
|
||||
|
||||
test('audit — 蒂比 紧急赶场通知 (PRTS 修正 "受到伤害前触发"): a drain tick sets the skill off; a true-damage tick is not dodged', () => {
|
||||
const TIPPI = 'chess_char_2_13_a';
|
||||
const h = makeBattle({ units: [{ chessId: TIPPI, row: 10, col: 4, items: [SOLVENT] }], timeLimit: 60, hooks: ['skillStart'] });
|
||||
h.step();
|
||||
const u = h.unit(TIPPI);
|
||||
u.skill.gainSp(1000);
|
||||
h.run(1.02);
|
||||
const c = h.hooksOf('skillStart').filter((x) => x.unit === u);
|
||||
assert.equal(c.length, 1, 'the drain tick popped S2');
|
||||
assert.equal(c[0].reason, 'TAKE_DAMAGE');
|
||||
approx(u.s.maxHp - u.hp, DRAIN, 1e-9, 'a true-damage tick is not dodged (物理或法术 only)');
|
||||
});
|
||||
|
||||
test('audit — "受到伤害时" content counts the drain and never a 流失: 乌尔比安 本性的坚守, 伪装服, 远牙 未受伤害, 坚守 thorn chances', () => {
|
||||
const ULPIAN = 'chess_char_5_05_a', FARTOOTH = 'chess_char_4_20_a', DISGUISE = 'chess_item_4_04_e_a';
|
||||
// 乌尔比安: heals on each drain tick, not on a 流失
|
||||
{
|
||||
const h = makeBattle({ units: [{ chessId: ULPIAN, row: 10, col: 4 }], timeLimit: 60, hooks: ['heal'], captureNoisy: true });
|
||||
h.step();
|
||||
const u = h.unit(ULPIAN);
|
||||
u.hp = u.s.maxHp * 0.9;
|
||||
h.b.loseHp(u, 100);
|
||||
assert.equal(h.hooksOf('heal').filter((c) => c.target === u).length, 0, 'a 流失 is not 受到伤害');
|
||||
h.b.dealDamage(null, u, { amount: 100, type: 'true', canDodge: false });
|
||||
assert.equal(h.hooksOf('heal').filter((c) => c.target === u).length, 1, 'real damage heals');
|
||||
}
|
||||
// 伪装服: a 流失 does not use it up; the first drain tick does
|
||||
{
|
||||
const h = makeBattle({ units: [{ chessId: YAK, row: 10, col: 4, skillIndex: 1, items: [DISGUISE] }], timeLimit: 60, hooks: [] });
|
||||
h.step();
|
||||
const u = h.unit(YAK);
|
||||
h.b.loseHp(u, 100);
|
||||
assert.ok(!u.s.flags.stealth, 'no 隐匿 from a 流失');
|
||||
h.b.dealDamage(null, u, { amount: 100, type: 'true', canDodge: false });
|
||||
assert.ok(u.s.flags.stealth, '隐匿 after the first damage');
|
||||
}
|
||||
// 远牙 "最近10秒内未受伤害时,攻击力+15%": a 流失 keeps the bonus, a drain tick resets the timer
|
||||
{
|
||||
const h = makeBattle({ units: [{ chessId: FARTOOTH, row: 11, col: 4 }], timeLimit: 60, hooks: [] });
|
||||
h.step();
|
||||
const u = h.unit(FARTOOTH);
|
||||
h.run(10.5);
|
||||
assert.ok(u.findBuff('fartth:focus'), 'bonus after 10 s');
|
||||
h.b.loseHp(u, 50);
|
||||
h.run(0.3);
|
||||
assert.ok(u.findBuff('fartth:focus'), 'a 流失 does not reset it');
|
||||
h.b.dealDamage(null, u, { amount: 50, type: 'true', canDodge: false });
|
||||
h.run(0.3);
|
||||
assert.ok(!u.findBuff('fartth:focus'), 'damage resets it');
|
||||
}
|
||||
// 坚守 thorns (3 members): a 流失 leaves the 0.2 s chance; 无来源 damage uses it up and hits nobody (PRTS 备注)
|
||||
{
|
||||
const stead = DATA.bonds.steadShip;
|
||||
const members = ['chess_char_1_02_a', 'chess_char_1_10_a', 'chess_char_2_08_a'];
|
||||
for (const m of members) assert.ok(stead.members.includes(m), m);
|
||||
const h = makeBattle({
|
||||
defs: { enemies: { enemy_dummy: dummy({ atk: 10, bat: 1 }) } },
|
||||
units: members.map((id, i) => ({ chessId: id, row: 10, col: 3 + i, skillIndex: 0 })),
|
||||
bonds: { steadShip: { count: 3, active: true, tier: 2, layers: 0 } },
|
||||
timeLimit: 60, hooks: [],
|
||||
});
|
||||
h.step();
|
||||
const u = h.unit(members[0]);
|
||||
h.b.loseHp(u, 10);
|
||||
assert.equal(u.mem['bond:steadShip:cd'], undefined, 'a 流失 used no thorn chance');
|
||||
h.b.dealDamage(null, u, { amount: 10, type: 'true', canDodge: false });
|
||||
approx(u.mem['bond:steadShip:cd'], h.b.time, 1e-9, '无来源 damage used it');
|
||||
}
|
||||
});
|
||||
@@ -96,7 +96,7 @@ test('stat equipment: percentages are 直接乘算 — additive with each other
|
||||
checkInvariants(h2.b);
|
||||
});
|
||||
|
||||
test('源石溶剂: −60 HP per second on the field (流失), ATK +40 %', () => {
|
||||
test('源石溶剂: −60 HP per second on the field (无来源真实伤害, not 流失 — feedback1d-solvent.test.js), ATK +40 %', () => {
|
||||
for (const id of [A('1_05'), B('1_05')]) {
|
||||
const h = fight({ units: [{ chessId: 't_op', row: 10, col: 4, items: [id] }] });
|
||||
h.step(1);
|
||||
|
||||
@@ -1292,14 +1292,22 @@ test('2_11 风丸: while substituted she fights with the <替身> ATK/DEF (+modu
|
||||
}
|
||||
});
|
||||
|
||||
test('2_13 蒂比: only enemy attacks set off 紧急赶场通知; a true-damage attack triggers it but is not dodged', () => {
|
||||
test('2_13 蒂比: any damage instance sets off 紧急赶场通知 (PRTS 修正 "受到伤害前触发") and a physical one is dodged; a 流失 does not; a true-damage attack triggers it but is not dodged', () => {
|
||||
const id = 'chess_char_2_13_a';
|
||||
const g = run({ units: [{ chessId: id, row: 9, col: 5, carryState: READY }] });
|
||||
const v = g.unit(id);
|
||||
g.step();
|
||||
g.b.loseHp(v, 50);
|
||||
g.step();
|
||||
assert.equal(v.skill.active, false, 'a 流失 never triggers it');
|
||||
const hp = v.hp;
|
||||
g.b.dealDamage(null, v, { amount: 50, type: 'phys' });
|
||||
assert.ok(v.skill.active, 'a non-attack (sourceless) damage instance triggers it');
|
||||
assert.equal(v.hp, hp, '…and the physical damage is dodged');
|
||||
done(g);
|
||||
const h = run({ defs: { enemies: { e: dummy('e', { atk: 300, bat: 1, dmgType: 'true' }) } }, units: [{ chessId: id, row: 9, col: 5, carryState: READY }] });
|
||||
const u = h.unit(id);
|
||||
h.step();
|
||||
h.b.dealDamage(null, u, { amount: 50, type: 'phys' });
|
||||
h.step();
|
||||
assert.equal(u.skill.active, false, 'non-attack damage never triggers it');
|
||||
h.spawn('e', { pos: [9, 5] });
|
||||
h.runUntil(() => u.skill.active, 3);
|
||||
assert.ok(u.skill.active, 'the attack triggered it');
|
||||
|
||||
Reference in new issue
Block a user