feedback5 CB3: 砾's normal attack hits twice at 50 % 伤害倍率 after DEF, so a 频次 enemy loses 2 per attack (community report of 2026-10-06, item 16.5)

Official: PRTS 砾 特性备注 — every normal attack in her attack animation deals two instances, each 50 % of the final damage
(after DEF / damage reduction, before 重设伤害); the second triggers no 受击回复. Cause: one instance per attack. Now the profile
field hitDmgMul (ai.js resolveHit: DamageInfo mul on each of the `hits` instances, noSp on the later ones) and 砾's kit trait
{ hits: 2, hitDmgMul: 0.5 } — the same total against normal enemies, twice the count against 频次 enemies.

Golden: 7 scenarios with 砾 on the board — roster-004, roster-005, roster-024; bond-indomShip-low / -high and
bond-kazimierzShip-low / -high (she is in 不屈 and 卡西米尔): two damage events per attack; in roster-024 and
bond-kazimierzShip-high the last blow lands on another instance, so kills (and the heals / damage that follow them) shift
between her and her neighbours (roster-024: 砾 19 -> 25 kills, 风丸 13 -> 9). Tests:
test/sim/feedback5-gravel-double-hit.test.js (2, both fail on the parent). Docs: docs/SIM.md (profile flags).
This commit is contained in:
sganggs committed 2026-10-07 02:12:37 +08:00
1 parent 30d9a41c51
commit 212a073af6
7 files changed
+113 -33

No files matched your search

+3 -1
View File
@@ -1208,7 +1208,9 @@ S3 未照耀的荣光 — its CUSTOM_RANGE trigger also counts flyers). A stun /
3.75 back = `BOOMERANG_RETURN_SPEED`, PRTS 跃跃; droneBomb 5 = 暴鸰's bomb, the official projectile_bombd); melee/`none` hits are
instant, and so are `'beam'` hits (a 锁定攻击范围 AoE without a projectile — `rangeAoe` profiles: "在攻击前摇结束时选取范围内的全体目标,同时造成伤害", PRTS 作战机制). Kit-settable profile flags beyond the
table: `hitSleep` (targets and damages sleeping enemies — "可以攻击沉睡的敌人"), `onEachHit(b, u, victim, hctx)`, `dmgMul`,
`afterHit`, `afterAttack`, `canAttack`, `hitsFn(b, u, info)`, `storeEnergy` / `releaseEnergy` (the 秘术师 store, below), `priority` (targeting.js PRIORITY_FNS — `'heaviest'`: the 攻城手 trait
`afterHit`, `afterAttack`, `canAttack`, `hitsFn(b, u, info)`, `hitDmgMul` (the 伤害倍率 of each of the `hits` instances on the
main target — DamageInfo `mul`, after DEF / RES; the later instances give no 受击回复: 砾's two 50 % hits, PRTS 砾 特性备注),
`storeEnergy` / `releaseEnergy` (the 秘术师 store, below), `priority` (targeting.js PRIORITY_FNS — `'heaviest'`: the 攻城手 trait
"优先攻击重量最重的敌人", the highest current 重量等级 first: 早露 / 提丰; `'elite'`: "优先攻击精英或领袖敌人", an ELITE / BOSS
rank enemy or a leader first: 薇薇安娜 S3), `blockFly`, `noHeal`, `skipEnemy(e)` (an enemy the unit never
selects — its attacks, the enemies it blocks and its skill-trigger targets: targeting.js canTargetEnemy; 嵯峨 "不攻击重伤
+6 -1
View File
@@ -286,9 +286,14 @@ export function resolveHit(b, u, prof, target, info, x, y) {
let mulT = skillMul;
if (prof.dmgMul) { const m = typeof prof.dmgMul === 'function' ? prof.dmgMul(b, u, target) : prof.dmgMul; if (Number.isFinite(m)) mulT *= m; }
const hits = prof.hitsFn ? prof.hitsFn(b, u, info) : Math.max(1, prof.hits || 1);
// `hitDmgMul` (professions.js header): each instance's 伤害倍率 — the DamageInfo `mul`, after DEF / RES — and only the
// first instance may give 受击回复 (砾's two 50 % hits, PRTS 砾 特性备注)
const split = Number.isFinite(prof.hitDmgMul) && prof.hitDmgMul > 0 ? prof.hitDmgMul : null;
let dealtMain = 0;
for (let h = 0; h < hits && target.alive; h++) {
dealtMain += b.dealDamage(u, target, { amount: atk * scale * mulT, type: baseType, isAttack: true, isSkill: info.isSkill, tags: prof.tags || [], attackId });
const d = { amount: atk * scale * mulT, type: baseType, isAttack: true, isSkill: info.isSkill, tags: prof.tags || [], attackId };
if (split != null) { d.mul = split; if (h > 0) d.noSp = true; }
dealtMain += b.dealDamage(u, target, d);
}
dealtTotal += dealtMain;
if (prof.onHitStatus && target.alive) b.applyStatus(target, prof.onHitStatus.key, { duration: prof.onHitStatus.duration, source: u, value: prof.onHitStatus.value });
@@ -3,6 +3,15 @@
import { num, talentBb, installAura } from '../shared/tier1.js';
/**
* 砾's normal attack (PRTS 砾 特性备注): "在砾的攻击动作下,每次普通攻击造成两段伤害,但是每段最终只会造成50%的伤害(在计算防御/减伤后,在
* 重设伤害前);第二段伤害不会触发目标的受击回复" — two damage instances of 50 % 伤害倍率 each (DamageInfo `mul`, after DEF: not a 50 %
* 攻击倍率), so a 频次 enemy loses 2 per attack; the second carries no 受击回复 (ai.js resolveHit `hitDmgMul`). Every attack she
* makes is a normal attack (both her skills are passive).
*/
const GRAVEL_HITS = 2;
const GRAVEL_HIT_DMG_MUL = 0.5;
export default {
// ---------------------------------------------------------------------------------------------------------------
// 2_12 砾 鼠群 (passive): at each deployment a barrier of hp_ratio × max HP that decays to 0 over `duration` s
@@ -10,10 +19,11 @@ export default {
// 快速部署: own deployment cost +cost (−1). Elite 小个子支援: every unit whose initial deployment cost ≤ cond.cost
// DEF +def while 砾 is on the field. (Elite module withdraw refund: no manual retreat in battle ⇒ no effect.)
// S1 影袭 (alt, passive): at each deployment DEF +def, decaying linearly to 0 over `duration` s, updated once per
// second (PRTS: "防御力加成每秒更新一次").
// second (PRTS: "防御力加成每秒更新一次"). Trait note: every normal attack hits twice at 50 % (GRAVEL_HIT_DMG_MUL above).
chess_char_2_12_a: (bb, chess) => {
const t = talentBb(chess, 0);
return {
trait: { hits: GRAVEL_HITS, hitDmgMul: GRAVEL_HIT_DMG_MUL },
skill: {
kind: 'duration', activateOnDeploy: true, duration: num(bb.duration, 10), spCost: 0, spType: 'none', trigger: 'NEVER',
onStart({ battle, unit }) {
+3
View File
@@ -26,6 +26,9 @@
// healThrough(healer, ally) → bool (a healer that selects and heals that ally through its 禁疗 — Battle
// injuredAlliesInKeys, damage.js heal; 凯尔希 on her Mon3tr)
// hitsFn(battle, unit, info) → n (info: the hit's { isSkill, index, attackId, energy })
// hitDmgMul n (伤害倍率 of each of the `hits` instances on the main target: DamageInfo `mul`, applied after DEF / RES
// and the damage multipliers, not 攻击倍率 — a 频次 enemy still loses 1 per instance; the instances after
// the first carry `noSp`, no 受击回复: 砾's two 50 % hits, PRTS 砾 特性备注 — ai.js resolveHit)
// install(battle, unit) — per-unit hooks, called once at setup
// storeEnergy(battle, unit) → bool / releaseEnergy(battle, unit) → n (秘术师: the attack check found no valid target ⇒
// store one energy, false when full; the energies leaving with an attack — installMystic)
+18 -18
View File
@@ -357,8 +357,8 @@
["-","~3","device","trap_1105_accrate",null,null,100,1,3,0,0,0,0,0,null]
],
"enemies": {"enemy_1158_divman":[15,15,0,2017,57024],"enemy_1160_hvyslr":[2,2,0,5010,23040],"enemy_1161_tidmag":[1,1,0,1491,6912]},
"hooks": [1,32,2070,168,168,255,260,1042,21,21,12,12,0,6047,106,8,18,0,1,21,1,0,3],
"events": {"atk":215,"deploy":11,"die":21,"dmg":260,"engage":6,"fx":25,"layer":1,"skill":24,"spawn":31,"status":77},
"hooks": [1,32,2070,168,168,265,270,1042,21,21,12,12,0,6047,106,8,18,0,1,21,1,0,3],
"events": {"atk":215,"deploy":11,"die":21,"dmg":270,"engage":6,"fx":25,"layer":1,"skill":24,"spawn":31,"status":77},
"fx": {"dilemma":1,"dodge":3,"explosion":5,"frostNova":2,"garrison":1,"levitate":7,"overclock":1,"shield":2,"slippery":1,"splash":2},
"snaps": "01da0610 a11d4743 dacec57c 0fe89241 8359f8a8 3545ad4d 1d9e9ae3"
},
@@ -392,8 +392,8 @@
["p1",8,"op","chess_char_4_26_b",0,"uniequip_002_bldsk",1522,1,55,0,0,17721,26801,37,9]
],
"enemies": {"enemy_1166_dusbr_2":[14,11,3,33134,133646],"enemy_1170_dushld_2":[4,1,3,37533,54717],"enemy_9011_acrefr":[4,1,0,75921,133613]},
"hooks": [1,58,5700,890,890,1254,1237,4113,41,47,35,33,100,4048,235,79,22,6,1,41,0,0,17],
"events": {"atk":1291,"deploy":36,"die":41,"dmg":1233,"engage":4,"fx":185,"heal":212,"leak":6,"skill":68,"spawn":30,"status":588},
"hooks": [1,58,5700,890,890,1451,1434,4113,41,47,35,33,100,4048,235,79,22,6,1,41,0,0,17],
"events": {"atk":1291,"deploy":36,"die":41,"dmg":1430,"engage":4,"fx":185,"heal":212,"leak":6,"skill":68,"spawn":30,"status":588},
"fx": {"bandage":9,"dodge":17,"extraAttack":56,"garrisonGrant":1,"mote":35,"redistribute":30,"revive":25,"reweave":2,"sandChains":4,"shield":5,"spGift":1},
"snaps": "7d5f326c b27b8121 c23f4b8e b11158be 745858a1 bc233c02 36600c75 96f7bb77 dd25f731 4dcd794a acc9ff4c c950f1bb dc098c51 333aecb6 1657b56a 3af3a6ec ae8a08a1 d78ef285 3ebb7cc5"
},
@@ -523,10 +523,10 @@
["-","~3","device","trap_1105_accrate",null,null,100,1,3,0,0,0,0,0,null]
],
"enemies": {"enemy_1116_liprr":[11,11,0,4995,69696],"enemy_1116_liprr_2":[4,4,0,89,34560],"enemy_1121_lifbos":[1,1,0,7831,23040]},
"hooks": [1,27,2084,238,238,381,381,1730,18,18,8,7,0,7480,13,18,16,0,1,18,1,0,0],
"events": {"atk":263,"deploy":8,"die":18,"dmg":373,"engage":5,"fx":60,"heal":49,"layer":1,"skill":15,"spawn":27,"status":49},
"hooks": [1,27,2084,238,238,439,439,1730,18,18,8,7,0,7480,13,18,16,0,1,18,1,0,0],
"events": {"atk":263,"deploy":8,"die":18,"dmg":431,"engage":5,"fx":60,"heal":49,"layer":1,"skill":15,"spawn":27,"status":49},
"fx": {"displace":16,"garrison":1,"healField":1,"liberate":3,"phase":2,"redistribute":15,"reweave":1,"sandChains":2,"shield":1,"slash":17,"truesilver":1},
"snaps": "86cad855 f20e9695 1939f464 021173dc 58cc5398 439c2028 813a103c"
"snaps": "86cad855 f20e9695 1939f464 021173dc a7a02fb2 bf56e726 24bf526e"
},
"bond-kazimierzShip-high": {
"about": "卡西米尔 high: tier 2 layers 999; chess_char_2_18_b chess_char_3_12_b chess_char_5_08_b chess_char_4_19_b chess_char_1_19_b chess_char_2_12_b chess_char_3_17_b chess_char_5_10_b",
@@ -546,24 +546,24 @@
"bossHpLeft": null,
"rngDraws": 19,
"errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":30,"total":30,"perfect":true,"leaks":0,"leaksUncounted":0,"coins":0,"dmg":403782,"bossDmg":0,"heal":1898,"deaths":3,"layerGains":{}}},
"players": {"p1":{"killed":30,"total":30,"perfect":true,"leaks":0,"leaksUncounted":0,"coins":0,"dmg":403710,"bossDmg":0,"heal":1898,"deaths":3,"layerGains":{}}},
"units": [
["p1",1,"op","chess_char_2_18_b",0,"uniequip_002_ashlok",1062.9,1,3,7,96672,2092,0,20,2],
["p1",1,"op","chess_char_2_18_b",0,"uniequip_002_ashlok",1062.9,1,3,5,95295,2092,0,20,2],
["p1",2,"op","chess_char_3_12_b",2,"uniequip_002_blemsh",3096,1,5,0,0,0,0,0,0],
["p1",3,"op","chess_char_5_08_b",2,"uniequip_002_horn",610.48,1,7,11,69745,4063,1898,53,2],
["p1",4,"op","chess_char_4_19_b",2,"uniequip_002_flamtl",2118,1,8,1,19858,0,0,17,2],
["p1",5,"op","chess_char_1_19_b",1,"uniequip_002_wildmn",0,0,9,2,84656,1971,0,51,2],
["p1",6,"op","chess_char_2_12_b",1,"uniequip_002_gravel",0,0,37,2,45811,4646,0,42,2],
["p1",3,"op","chess_char_5_08_b",2,"uniequip_002_horn",610.48,1,7,10,66609,4063,1898,52,2],
["p1",4,"op","chess_char_4_19_b",2,"uniequip_002_flamtl",2118,1,8,1,21184,0,0,17,2],
["p1",5,"op","chess_char_1_19_b",1,"uniequip_002_wildmn",0,0,9,4,81694,1971,0,50,2],
["p1",6,"op","chess_char_2_12_b",1,"uniequip_002_gravel",0,0,37,3,51889,4646,0,42,2],
["p1",7,"op","chess_char_3_17_b",1,"uniequip_002_shotst",3441.6,1,4,7,84143,0,0,29,3],
["p1",8,"op","chess_char_5_10_b",2,"uniequip_002_lisa",1480,1,6,0,2897,0,0,10,2],
["-","~1","device","trap_1105_accrate",null,null,100,1,1,0,0,0,0,0,null],
["-","~2","device","trap_1105_accrate",null,null,100,1,2,0,0,0,0,0,null]
],
"enemies": {"enemy_10124_uashld_2":[2,2,0,4508,124416],"enemy_1043_zomsbr":[23,23,0,20,147057],"enemy_1061_zomshd":[5,5,0,4181,132309]},
"hooks": [1,41,2613,241,241,430,545,3492,33,33,15,13,0,5189,999,11,30,0,1,44,0,0,5],
"events": {"atk":241,"deploy":9,"die":33,"dmg":425,"engage":7,"fx":54,"skill":28,"spawn":40,"status":121},
"fx": {"aoe":24,"displace":15,"dodge":5,"flame":2,"overload":2,"riposte":1,"shield":2,"volley":3},
"snaps": "5a72bba9 0659d371 e189ce8b f0de7f75 b7e6c462 bbd2a9c5 9a77b0eb d7d4a16d db3990ea"
"enemies": {"enemy_10124_uashld_2":[2,2,0,4508,124416],"enemy_1043_zomsbr":[23,23,0,20,146972],"enemy_1061_zomshd":[5,5,0,4181,132323]},
"hooks": [1,41,2613,239,239,479,594,3462,33,33,15,13,0,5189,999,11,30,0,1,43,0,0,5],
"events": {"atk":239,"deploy":9,"die":33,"dmg":474,"engage":7,"fx":53,"skill":28,"spawn":40,"status":121},
"fx": {"aoe":24,"displace":14,"dodge":5,"flame":2,"overload":2,"riposte":1,"shield":2,"volley":3},
"snaps": "5a72bba9 62d17d06 7af61b0e de61a8e0 ff0837e9 7a7f5062 854b1dff e111ddbe ecf8b583"
},
"bond-kjeragShip-low": {
"about": "谢拉格 low: tier 1 layers 1; chess_char_1_02_a chess_char_3_02_a chess_char_3_03_a chess_char_3_04_a chess_char_2_05_a chess_char_3_11_a chess_char_4_26_a chess_char_3_01_a",
+12 -12
View File
@@ -240,8 +240,8 @@
"enemy_1504_cqbw": [1,0,1,652,630],
"enemy_2009_csaudc": [1,0,1,0,490]
},
"hooks": [1,57,3300,435,435,1379,1400,1360,23,49,17,17,0,10761,29,32,36,26,1,28,0,2,0],
"events": {"atk":511,"bounty":1,"deploy":18,"die":23,"dmg":1333,"engage":11,"fx":21,"heal":88,"leak":26,"skill":34,"spawn":52,"status":166},
"hooks": [1,57,3300,435,435,1418,1439,1360,23,49,17,17,0,10761,29,32,36,26,1,28,0,2,0],
"events": {"atk":511,"bounty":1,"deploy":18,"die":23,"dmg":1372,"engage":11,"fx":21,"heal":88,"leak":26,"skill":34,"spawn":52,"status":166},
"fx": {"aoe":4,"burst":2,"camouflage":2,"dodge":3,"dollEnd":1,"liberate":1,"overload":1,"phase":1,"shield":2,"shieldBreak":1,"substitute":1,"summon":1,"takeoff":1},
"snaps": "95c7197d e941625e 33d925c4 1cb46719 50ed098d ac7e8334 ebe1dbfe ec21f87b 8d1c3a8b ca59f49a 4fa1da8c"
},
@@ -295,8 +295,8 @@
"enemy_1509_mousek": [1,1,0,7102,17600],
"enemy_2010_csdcr": [1,1,0,1896,9600]
},
"hooks": [1,102,4200,702,702,1650,1643,1107,83,89,36,35,0,28577,142,53,84,0,1,83,0,0,0],
"events": {"atk":917,"bounty":5,"deploy":18,"die":89,"dmg":1602,"engage":7,"fx":66,"heal":122,"skill":71,"spawn":97,"status":441},
"hooks": [1,102,4200,702,702,1777,1770,1107,83,89,36,35,0,28577,142,53,84,0,1,83,0,0,0],
"events": {"atk":917,"bounty":5,"deploy":18,"die":89,"dmg":1729,"engage":7,"fx":66,"heal":122,"skill":71,"spawn":97,"status":441},
"fx": {"counter":2,"explode":3,"expose":9,"overload":2,"phase":1,"shield":9,"shieldBreak":16,"sleep":7,"summon":5,"telegraph":6,"zone":6},
"snaps": "de0d6fce aa9dbd18 e5f78211 369608f9 eaf0206b 8ca00225 879ade74 10585017 e8565d0c e354c612 a50e5370 07ad00b4 54071507 c18f3d28"
},
@@ -1531,19 +1531,19 @@
"bossHpLeft": null,
"rngDraws": 11,
"errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":167,"total":191,"perfect":false,"leaks":24,"leaksUncounted":0,"coins":6,"dmg":194675,"bossDmg":0,"heal":20738,"deaths":2,"layerGains":{}}},
"players": {"p1":{"killed":167,"total":191,"perfect":false,"leaks":24,"leaksUncounted":0,"coins":6,"dmg":194675,"bossDmg":0,"heal":20517,"deaths":2,"layerGains":{}}},
"units": [
["p1",1,"op","chess_char_2_07_b",0,"none",0,0,3,45,68672,6586,3921,58,2],
["p1",2,"op","chess_char_2_08_b",0,"none",3193,1,1,0,0,0,0,0,0],
["p1",3,"op","chess_char_2_09_b",0,"none",0,0,4,1,1,2079,46,1,0],
["p1",4,"op","chess_char_2_11_b",0,"none",1992.6,1,7,13,4490,2214,0,31,10],
["p1",5,"op","chess_char_2_12_b",0,"none",1352,1,10,19,23053,2194,0,61,1],
["p1",4,"op","chess_char_2_11_b",0,"none",1992.6,1,7,9,4486,1993,0,27,9],
["p1",5,"op","chess_char_2_12_b",0,"none",1352,1,10,25,23059,2194,0,61,1],
["p1",6,"op","chess_char_2_19_a",0,null,1021.2,1,5,50,33036,0,0,73,10],
["p1",7,"op","chess_char_2_01_b",0,"none",2146,1,8,15,3240,0,0,19,2],
["p1",7,"op","chess_char_2_01_b",0,"none",2146,1,8,13,3238,0,0,19,2],
["p1",8,"op","chess_char_2_02_b",0,"none",2624.8,1,6,0,0,400,1289,9,1],
["p1",9,"op","chess_char_2_04_b",0,"none",1180,1,9,23,55082,889,0,65,32],
["p1",10,"op","chess_char_2_05_b",0,"none",1873.2,1,11,0,0,6919,3700,46,3],
["p1",11,"op","chess_char_2_06_b",1,"none",1639,1,12,0,0,4376,11782,61,4],
["p1",11,"op","chess_char_2_06_b",1,"none",1639,1,12,0,0,4376,11560,61,4],
["p1",12,"op","chess_char_2_10_b",0,"none",1356,1,2,1,7101,0,0,77,3]
],
"enemies": {
@@ -1562,10 +1562,10 @@
"enemy_1381_winman_2": [1,1,0,33,7488],
"enemy_1535_wlfmster": [1,0,1,8984,18836]
},
"hooks": [1,203,4200,756,756,1044,1111,2414,169,190,68,65,0,13911,1032,49,191,21,1,177,0,0,0],
"events": {"atk":965,"bounty":3,"deploy":12,"die":169,"dmg":1064,"engage":8,"fx":78,"heal":151,"leak":21,"skill":133,"spawn":203,"status":529},
"hooks": [1,203,4200,752,752,1080,1146,2414,169,190,67,64,0,13908,1032,48,191,21,1,177,0,0,0],
"events": {"atk":961,"bounty":3,"deploy":12,"die":169,"dmg":1099,"engage":8,"fx":78,"heal":151,"leak":21,"skill":131,"spawn":203,"status":531},
"fx": {"beam":3,"buff":5,"phase":1,"summon":57,"undying":1,"zone":11},
"snaps": "3528dfb7 48905850 aff1c414 d4b44f71 7e706a8f c4a07a93 5e5052bc b45c5d36 ebbfc908 a20e4f9f 666701cc e6e998e9 e042a19b 9fc624e8"
"snaps": "3528dfb7 48905850 b9883203 0254972f 68bbe7b1 21b5d4c5 99820c60 e553a893 cf5cc1ce 5b1d1c6a bea6d936 32e95e04 058b1d27 6bf58b14"
},
"roster-025": {
"about": "pass 1 band 3: chess_char_2_17_b chess_char_2_18_b chess_char_3_06_a chess_char_2_13_b chess_char_2_16_b chess_char_3_02_a chess_char_3_03_a chess_char_3_04_a chess_char_3_05_a chess_char_2_14_b chess_char_2_19_b chess_char_3_01_a",
@@ -0,0 +1,60 @@
// test/sim/feedback5-gravel-double-hit.test.js — community report of 2026-10-06 (item 16.5) 「砾的平A是比较特殊的双判,是两次50%
// 伤害倍率(不是攻击倍率),对频次怪的效率应该比正常干员高,但是现在游戏内还是单次判定」: PRTS 砾 特性备注 "在砾的攻击动作下,每次普通攻击造成
// 两段伤害,但是每段最终只会造成50%的伤害(在计算防御/减伤后,在重设伤害前);第二段伤害不会触发目标的受击回复". Up to 0.2.0 every attack was
// one instance. Now two instances of 50 % 伤害倍率 (DamageInfo `mul`, after DEF — the profile's `hitDmgMul`, ai.js resolveHit).
// Run: node --test test/sim/feedback5-gravel-double-hit.test.js
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { makeBattle, enemyRec, checkInvariants } from '../helpers/battleHarness.js';
import { hitCount } from '../../server/sim/content/enemies/helpers.js';
import { MIN_DAMAGE_RATIO } from '../../server/sim/constants.js';
const near = (a, b, msg) => assert.ok(Math.abs(a - b) <= 1e-6, `${msg}: ${a} vs ${b}`);
/** 砾 on (10,4) facing right, one parked enemy on (10,5); returns her attacks and her damage instances on it. */
function fight(id, enemy, seconds = 5) {
const h = makeBattle({
seed: 2, autoFinish: false, timeLimit: 60, captureNoisy: true, hooks: ['damaged', 'attack'],
defs: { enemies: { enemy_g: enemyRec({ key: 'enemy_g', hp: 1e9, speed: 0, mass: 0, ...enemy }) } },
units: [{ chessId: id, row: 10, col: 4 }],
});
h.step();
const e = h.spawn('enemy_g', { pos: [10, 5] });
return { h, e, run: () => h.run(seconds) };
}
test('砾 (normal, elite): every normal attack is two damage instances of 50 % 伤害倍率 after DEF — the same total, not 50 % 攻击倍率', () => {
for (const id of ['chess_char_2_12_a', 'chess_char_2_12_b']) {
for (const def of [0, 300, 5000]) {
const { h, run } = fight(id, { def });
run();
const u = h.unit(id);
const attacks = h.hooksOf('attack').filter((c) => c.attacker === u).length;
const hits = h.hooksOf('damaged').filter((c) => c.source === u && c.dmg?.isAttack);
assert.ok(attacks >= 3, `${id}: she attacks (${attacks})`);
assert.equal(hits.length, 2 * attacks, `${id} DEF ${def}: two instances per attack`);
const atk = u.s.atk;
const full = Math.max(atk - def, MIN_DAMAGE_RATIO * atk); // one instance of the whole attack, after DEF
for (const c of hits) near(c.amount, 0.5 * full, `${id} DEF ${def}: each instance is 50 % of the mitigated hit`);
// first instance of each attack may give 受击回复, the second may not
const byAttack = new Map();
for (const c of hits) { const l = byAttack.get(c.dmg.attackId) ?? []; l.push(c); byAttack.set(c.dmg.attackId, l); }
for (const l of byAttack.values()) assert.deepEqual(l.map((c) => !!c.dmg.noSp), [false, true], `${id}: the second instance carries no 受击回复`);
checkInvariants(h.b);
}
}
});
test('砾 against a 频次 enemy: two counted hits per attack (one before 0.2.0)', () => {
for (const id of ['chess_char_2_12_a', 'chess_char_2_12_b']) {
const { h, e, run } = fight(id, { hp: 100 });
hitCount(h.b, e, true);
run();
const u = h.unit(id);
const attacks = h.hooksOf('attack').filter((c) => c.attacker === u).length;
assert.ok(attacks >= 3, `${id}: she attacks`);
assert.equal(100 - e.hp, 2 * attacks, `${id}: 2 per attack`);
checkInvariants(h.b);
}
});