diff --git a/docs/SIM.md b/docs/SIM.md index 85886c4a..0aa00ee4 100644 --- a/docs/SIM.md +++ b/docs/SIM.md @@ -1213,7 +1213,7 @@ header of professions.js). | phalanx | no attack & DEF +200 %, RES +20 (bb) while the skill is off; while on, `rangeAoe`: each attack strikes every selectable enemy on its range at once (blocked enemies included; a stealthed one only when revealed or blocked), the same damage near and far, instant (`'beam'`) — "群体法术伤害" (secondary: Terra Wiki, Phalanx Caster: "attacks hit all enemies within their range"; supporting: PRTS 林 S3 备注 "单次普攻最多触发1次效果" — one normal attack can kill several — the same 锁定攻击范围 shape as the 轰击术师, and PRTS 溅射半径一览 documents no splash radius for it (it omits the 撼地者 too, so this is not proof); no primary source states a target cap; community report E3: it used to be one bolt + a 1.1 splash). 卡涅利安's charged S1 keeps the skill-off trait, 不攻击 included (kit `canAttack`, PRTS 备注) | | physician | heal the lowest HP% injured ally in range (a skill `targeting.maxTargets` widens any heal profile) | | ringhealer | heal 3 allies | -| chainhealer | heal bounces 3× (−25 %, bb chain.*) within 2.5 tiles | +| chainhealer | the heal jumps on to 3 units in all (−25 % per jump, bb chain.*), each jump inside the 3×3 of tiles around the last one healed (range x-4), never twice to one unit, the lowest HP ratio first — a full-HP ally too (healed for nothing, the chain goes on from it) — then the latest deployed; no 禁疗 / 孤立 unit (a healer's `healThrough` summon excepted) — PRTS 分支特性信息 链愈师 (`ai.js chainHealNext`, shared with Mon3tr's kit; until 0.2.0: the most injured ally within 2.5 tiles) | | healer (流明) | heal ×0.8 (bb heal_scale) beyond 2 tiles | | wandermedic | heal + reduce element gauges by 50 % ATK (bb ep_heal_ratio); also targets uninjured allies with gauge | | incantationmedic | arts attack; EVERY damage the unit deals heals the lowest ally in range for 50 % (bb scale) of it — the official trait buff (`vendla_tr` / `reed2_tr` / `titi_tr`) is ON_AFTER_OUTPUT_DAMAGE, so skill and DoT damage heals too (缇缇's 凝固的时光 ticks, 焰影苇草's S2 fireballs while she is disarmed); a skill that triggers it for one named ally says so ("仅对该角色触发…特性") and the damage instance carries that ally (`DamageInfo.traitAlly`) | diff --git a/server/sim/ai.js b/server/sim/ai.js index 9c68d4b1..03c94405 100644 --- a/server/sim/ai.js +++ b/server/sim/ai.js @@ -333,6 +333,27 @@ export function resolveHit(b, u, prof, target, info, x, y) { } } +/** + * The next unit of a 链愈师 heal chain of `healer` from `prev` — PRTS 分支特性信息 链愈师: "跳跃范围为x-4,无特殊说明的场合一次 + * 治疗链不会对已跳跃过的单位重复跳跃", "优先跳跃至范围内生命比例最低>部署时间点最晚的我方单位。可选择满生命我方单位为跳跃目标,但仍受 + * 禁疗制约": an ally on the 3×3 of tiles around prev's (range x-4), not yet in this chain (`seen` ids), no device, no hidden + * or 孤立 unit, no 禁疗 / 无法被治疗 one (as Battle.injuredAlliesInKeys: the healer's `healThrough` lets its own summon in — + * 凯尔希 / Mon3tr); the lowest HP ratio first — a full-HP ally too, healed for nothing, and the chain jumps on from it —, + * then the latest deployed (aggroSeq). Null when none. Shared by the profession (doHeal) and Mon3tr's kit. + */ +export function chainHealNext(b, healer, prev, seen) { + const through = healer && healer.profile && typeof healer.profile.healThrough === 'function' ? healer.profile.healThrough : null; + const r0 = prev.tileR, c0 = prev.tileC; + let best = null; + for (const a of b.allyUnits) { + if (!a.alive || !a.deployed || a.hidden || a.kind === 'device' || seen.has(a.id)) continue; + if (Math.abs(a.tileR - r0) > 1 || Math.abs(a.tileC - c0) > 1) continue; + if (a !== healer && (a.s.flags.isolated || (a.s.flags.noHeal && !(through && through(healer, a))) || (a.profile && a.profile.noHeal))) continue; + if (!best || a.hpRatio < best.hpRatio - 1e-12 || (Math.abs(a.hpRatio - best.hpRatio) <= 1e-12 && a.aggroSeq > best.aggroSeq)) best = a; + } + return best; +} + function doHeal(b, u, prof, t) { const atk = u.s.atk; const scale = (prof.atkScale ?? 1) * (prof.healScale ?? 1) * u.s.atkScaleMul; @@ -346,13 +367,7 @@ function doHeal(b, u, prof, t) { let prev = t; const n = Math.max(1, h.count || 3); for (let k = 1; k < n; k++) { - let best = null, bd = Infinity; - for (const a of b.alliesInRadius(prev.x, prev.y, 2.5, null)) { - // 禁疗 / noHeal units are no heal target for the bounces either (as injuredAlliesInKeys; 史尔特尔's 余烬, GitHub #52) - if (seen.has(a.id) || a.hp >= a.s.maxHp || a.kind === 'device' || a.s.flags.noHeal || (a.profile && a.profile.noHeal)) continue; - const d = a.hpRatio; - if (d < bd) { bd = d; best = a; } - } + const best = chainHealNext(b, u, prev, seen); if (!best) break; seen.add(best.id); b._ev(['atk', prev.id, best.id, 'chainHeal']); diff --git a/server/sim/content/kits/ops/op-monstr.js b/server/sim/content/kits/ops/op-monstr.js index 6455b102..01425766 100644 --- a/server/sim/content/kits/ops/op-monstr.js +++ b/server/sim/content/kits/ops/op-monstr.js @@ -27,8 +27,9 @@ // prosts_t_1[born_charge] BLOCK_CNT ×0, interval 0.1, firstTriggerInterval 1, FixedValueDamage PURE `damage` on itself); // buff_template_data (monstr_t_2[atk_spd] ON_OUTPUT_MODIFIER IsHeal; monstr_s_3[undead] ON_BEFORE_TRY_SET_HP_ZERO). // - Trait (链愈师) "恢复友方单位生命,且会在3个友方单位间跳跃,每次跳跃治疗量降低25%" — the chain is this kit's (the profession's -// 2.5-tile chain is replaced: heal mode single + `chainFrom`), as her heal projectiles have it: after the heal on the main -// target the heal jumps to the lowest-HP-ratio injured ally of the 3×3 around the last one healed (x-4), each jump × +// chain is replaced: heal mode single + `chainFrom`, its links the profession's — ai.js chainHealNext): after the heal on +// the main target the heal jumps to the lowest-HP-ratio ally (a full-HP one too — PRTS 分支特性信息 链愈师; ties: the latest +// deployed) of the 3×3 around the last one healed (x-4), each jump × // attack@chain.atk_scale (0.75; XAH-X 0.85), up to attack@chain.max_target units healed (3). A 重构体 (and she herself while S3 // runs: monstr[free_jump]) is a free link: healing it uses no target of the count and the next jump does not decay (XAH-X // stage 3: the next attack@chain.extra_cnt jumps after that neither). Her heals pass 禁疗 ("治疗效果无视禁疗"); her selection @@ -63,6 +64,7 @@ import { num, talentBb, traitBb, skillRec, batMod, installAura, up } from '../shared/tier1.js'; import { COLS } from '../../../constants.js'; +import { chainHealNext } from '../../../ai.js'; const S1 = 'skchr_monstr_1'; const S2 = 'skchr_monstr_2'; @@ -101,28 +103,13 @@ const prostsInRange = (battle, unit) => prostsOf(battle, unit).find((t) => up(t) /** `a` stands on the 3×3 around `b` (x-4: a heal jump's reach, the 重构体's aura). */ const nearX4 = (a, b) => Math.abs(a.tileR - b.tileR) <= 1 && Math.abs(a.tileC - b.tileC) <= 1; -/** - * The next link of a chain heal from `cur`: the lowest-HP-ratio injured ally of its 3×3 not healed yet by this chain (her - * own 重构体 even at full HP when `fullProsts`); never a 禁疗 unit but her 重构体. - */ -function nextLink(battle, unit, cur, seen, fullProsts) { - let best = null; - for (const a of battle.allyUnits) { - if (!a.alive || !a.deployed || a.hidden || a.kind === 'device' || seen.has(a.id) || !nearX4(a, cur)) continue; - const mine = isProstsOf(a, unit); - if (a !== unit && ((a.s.flags.noHeal && !mine) || (a.profile && a.profile.noHeal))) continue; - if (!(a.hp < a.s.maxHp - 1e-6) && !(fullProsts && mine)) continue; - if (!best || a.hpRatio < best.hpRatio - 1e-12 || (Math.abs(a.hpRatio - best.hpRatio) <= 1e-12 && a.deploySeq < best.deploySeq)) best = a; - } - return best; -} - /** * A chain heal of hers from `main` (already healed by her attack when `mainDone`): up to `count` units healed, × `step` per * jump; a free link (her 重构体; herself with `selfFree`) uses no target of the count and the next 1 + `extra` jumps keep the - * amount. + * amount. Each link is the 链愈师 one (ai.js chainHealNext — the 3×3 around the last one healed, the lowest HP ratio a + * full-HP ally included, then the latest deployed; no 禁疗 unit but her 重构体, her `healThrough`). */ -function chainFrom(battle, unit, main, base, { count, step, extra, fullProsts = false, mainDone = false, selfFree = false }) { +function chainFrom(battle, unit, main, base, { count, step, extra, mainDone = false, selfFree = false }) { const free = (t) => isProstsOf(t, unit) || (selfFree && t === unit); let amount = base; if (!mainDone) battle.heal(unit, main, amount, HEAL_OPTS); @@ -131,7 +118,7 @@ function chainFrom(battle, unit, main, base, { count, step, extra, fullProsts = const seen = new Set([main.id]); let cur = main; while (counted < count) { - const next = nextLink(battle, unit, cur, seen, fullProsts); + const next = chainHealNext(battle, unit, cur, seen); if (!next) break; seen.add(next.id); if (calm > 0) calm--; else amount *= step; @@ -280,12 +267,12 @@ export default { const base = unit.s.atk * (prof.atkScale ?? 1) * (prof.healScale ?? 1) * unit.s.atkScaleMul; for (const t of c.targets || []) { if (!t || t.side !== 'ally') continue; - chainFrom(battle, unit, t, base, { ...chain, count: s1on ? s1Count : chain.count, fullProsts: s2on, mainDone: true }); + chainFrom(battle, unit, t, base, { ...chain, count: s1on ? s1Count : chain.count, mainDone: true }); // S2: 0.6 s after her attack healed her 重构体, a second chain on it (her ATK × 1) if (s2on && isProstsOf(t, unit)) { battle.after(S2_EXTRA_DELAY, () => { if (!unit.alive || !up(t)) return; - chainFrom(battle, unit, t, unit.s.atk * unit.s.atkScaleMul, { ...chain, fullProsts: skillOn(unit, S2) }); + chainFrom(battle, unit, t, unit.s.atk * unit.s.atkScaleMul, chain); }, { owner: unit }); } } diff --git a/test/golden/bonds.json b/test/golden/bonds.json index f2a3614e..09ea8bf7 100644 --- a/test/golden/bonds.json +++ b/test/golden/bonds.json @@ -1065,23 +1065,23 @@ "rngDraws": 2, "errors": {"count":0,"kinds":[]}, "players": { - "p1": {"killed":15,"total":17,"perfect":false,"leaks":2,"leaksUncounted":0,"coins":0,"dmg":70415,"bossDmg":0,"heal":16499,"deaths":0,"layerGains":{"sargonShip":5}} + "p1": {"killed":15,"total":17,"perfect":false,"leaks":2,"leaksUncounted":0,"coins":0,"dmg":70415,"bossDmg":0,"heal":16347,"deaths":0,"layerGains":{"sargonShip":5}} }, "units": [ - ["p1",1,"op","chess_char_2_08_a",1,null,2300.11,1,2,2,2745,6042,2156,34,1], - ["p1",2,"op","chess_char_1_12_a",1,null,1840,1,1,0,0,0,63,0,0], - ["p1",3,"op","chess_char_4_22_a",2,null,2022,1,3,3,30743,1182,694,23,1], - ["p1",4,"op","chess_char_2_06_a",0,null,1440,1,4,0,0,916,6860,31,15], - ["p1",5,"op","chess_char_4_21_a",1,null,1271,1,7,0,0,2019,3276,40,1], - ["p1",6,"op","chess_char_4_25_a",2,null,1221,1,5,0,0,2137,698,0,1], - ["p1",7,"op","chess_char_4_26_a",0,null,1368,1,8,0,0,3145,2223,13,0], - ["p1",8,"op","chess_char_3_01_a",2,null,1471.8,1,6,10,36927,1389,530,51,1] + ["p1",1,"op","chess_char_2_08_a",1,null,2300.11,1,2,2,2745,6110,2375,34,1], + ["p1",2,"op","chess_char_1_12_a",1,null,1840,1,1,0,0,0,111,0,0], + ["p1",3,"op","chess_char_4_22_a",2,null,2022,1,3,3,30743,1232,716,23,1], + ["p1",4,"op","chess_char_2_06_a",0,null,1440,1,4,0,0,797,6475,32,15], + ["p1",5,"op","chess_char_4_21_a",1,null,1271,1,7,0,0,1951,3665,43,1], + ["p1",6,"op","chess_char_4_25_a",2,null,1221,1,5,0,0,2053,696,0,1], + ["p1",7,"op","chess_char_4_26_a",0,null,1368,1,8,0,0,3044,1697,15,0], + ["p1",8,"op","chess_char_3_01_a",2,null,1471.8,1,6,10,36927,1490,613,51,1] ], - "enemies": {"enemy_1267_nhpbr":[11,10,0,4553,43172],"enemy_1267_nhpbr_2":[5,5,0,1783,24750],"enemy_1272_nhtank":[1,0,0,10494,2493]}, - "hooks": [1,25,3300,234,234,523,523,2355,15,15,20,20,0,14437,0,5,17,0,1,15,1,0,0], - "events": {"atk":316,"deploy":8,"die":15,"dmg":483,"engage":3,"fx":112,"heal":156,"layer":1,"skill":40,"spawn":25,"status":235}, + "enemies": {"enemy_1267_nhpbr":[11,10,0,4678,43172],"enemy_1267_nhpbr_2":[5,5,0,1676,24750],"enemy_1272_nhtank":[1,0,0,10323,2493]}, + "hooks": [1,25,3300,240,240,523,523,2589,15,15,20,20,0,14437,0,5,17,0,1,15,1,0,0], + "events": {"atk":332,"deploy":8,"die":15,"dmg":481,"engage":3,"fx":112,"heal":155,"layer":1,"skill":40,"spawn":25,"status":247}, "fx": {"buff":1,"counter":15,"garrison":1,"healField":1,"mote":38,"redistribute":15,"reweave":1,"slash":17,"spGift":1,"truesilver":1,"zone":21}, - "snaps": "0bdd9351 b24b323e bd7fa0e6 4703d650 28cb05fc cf42c370 8691643e b07ee7e3 98f5edc6 aed38d5e c73b513b" + "snaps": "0bdd9351 b24b323e ab95517a c55fba39 176cd8a6 f77ba7e2 c1b9c3f4 2b4fa92b 246b1e91 0f8ceadd 18c1b994" }, "bond-sargonShip-high": { "about": "萨尔贡 high: tier 2 layers 999; chess_char_1_12_b chess_char_2_08_b chess_char_3_06_b chess_char_4_23_b chess_char_4_22_b chess_char_2_06_b chess_char_3_13_b chess_char_4_25_b", diff --git a/test/golden/diy.json b/test/golden/diy.json index 6b3d46b0..2d49b213 100644 --- a/test/golden/diy.json +++ b/test/golden/diy.json @@ -5782,35 +5782,35 @@ "time": 230, "ticks": 6900, "reason": "timeout", - "killed": 69, + "killed": 70, "total": 123, "unspawned": 0, "bossHpLeft": null, "rngDraws": 0, "errors": {"count":0,"kinds":[]}, - "players": {"p1":{"killed":69,"total":123,"perfect":false,"leaks":54,"leaksUncounted":0,"coins":0,"dmg":1779789,"bossDmg":0,"heal":458058,"deaths":1,"layerGains":{}}}, + "players": {"p1":{"killed":70,"total":123,"perfect":false,"leaks":53,"leaksUncounted":0,"coins":0,"dmg":1791852,"bossDmg":0,"heal":430240,"deaths":1,"layerGains":{}}}, "units": [ - ["p1",1,"op","chess_char_6_diy1_a",0,null,1741.25,1,4,0,0,0,35265,71,3], - ["p1",2,"op","chess_char_6_diy2_a",1,null,1741.25,1,5,49,1130811,0,13326,72,4], - ["p1",3,"op","chess_char_6_diy1_a",2,null,1741.25,1,11,0,0,0,48902,100,2], - ["p1",4,"op","chess_char_6_diy2_b",0,"none",1928.75,1,13,0,0,0,40135,79,3], - ["p1",5,"op","chess_char_6_diy1_b",1,"none",1928.75,1,6,20,648978,0,15029,60,3], - ["p1",6,"op","chess_char_6_diy2_b",2,"none",1928.75,1,14,0,0,0,31322,80,3], - ["p1",7,"op","chess_char_5_diy1_a",0,null,0,0,7,0,0,77284,43526,67,11], - ["p1",8,"op","chess_char_5_diy2_a",1,null,1492,1,9,0,0,98191,56296,94,4], - ["p1",9,"op","chess_char_5_diy1_a",2,null,1113.03,1,10,0,0,107130,55404,89,0], - ["p1",10,"op","chess_char_5_diy2_b",0,"none",1653,1,12,0,0,22049,49896,78,19], - ["p1",11,"op","chess_char_5_diy1_b",1,"none",1653,1,15,0,0,64626,22045,53,3], - ["p1",12,"op","chess_char_5_diy2_b",2,"none",1653,1,8,0,0,90649,46914,80,0], + ["p1",1,"op","chess_char_6_diy1_a",0,null,1741.25,1,4,0,0,0,34388,69,3], + ["p1",2,"op","chess_char_6_diy2_a",1,null,1741.25,1,5,52,1139811,0,13928,72,4], + ["p1",3,"op","chess_char_6_diy1_a",2,null,1741.25,1,11,0,0,0,56572,116,2], + ["p1",4,"op","chess_char_6_diy2_b",0,"none",1928.75,1,13,0,0,0,43866,93,4], + ["p1",5,"op","chess_char_6_diy1_b",1,"none",1928.75,1,6,18,652042,0,16199,65,3], + ["p1",6,"op","chess_char_6_diy2_b",2,"none",1928.75,1,14,0,0,0,24897,85,3], + ["p1",7,"op","chess_char_5_diy1_a",0,null,1492,1,139,0,0,49198,26898,47,7], + ["p1",8,"op","chess_char_5_diy2_a",1,null,1492,1,9,0,0,98191,48518,91,4], + ["p1",9,"op","chess_char_5_diy1_a",2,null,1415.53,1,10,0,0,107130,46829,85,0], + ["p1",10,"op","chess_char_5_diy2_b",0,"none",1653,1,12,0,0,22049,56243,75,18], + ["p1",11,"op","chess_char_5_diy1_b",1,"none",1653,1,15,0,0,64626,22341,51,2], + ["p1",12,"op","chess_char_5_diy2_b",2,"none",1617.78,1,8,0,0,90649,39561,82,0], ["-","~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], ["-","~3","device","trap_1105_accrate",null,null,100,1,3,0,0,0,0,0,null] ], - "enemies": {"enemy_1006_shield_3":[21,0,17,367121,555485],"enemy_1043_zomsbr":[66,60,5,22690,476621],"enemy_1402_tgshd_2":[36,9,20,70117,747683]}, - "hooks": [1,138,6900,1402,1402,1128,1128,13267,70,112,55,52,70,22887,578,8,123,42,1,70,0,0,0], - "events": {"atk":1777,"deploy":12,"die":70,"dmg":1118,"engage":2,"fx":458,"heal":1235,"leak":42,"skill":107,"spawn":138,"status":184}, - "fx": {"aoe":69,"expose":274,"healAoe":69,"shield":40,"takeoff":6}, - "snaps": "10b9b952 5a9c72b0 63328571 c14679f9 6590e801 46960fe3 e8ef4c65 d292fcf9 a7316f10 feac6b4b a477a19f 09fa6c39 ee21d1da 779f3d36 9390eeb1 5ae18e38 143ac30c 9f960c8a 88a1a740 47c6d6cd 5abd21eb 3f870752 b34f3d2c" + "enemies": {"enemy_1006_shield_3":[21,0,18,335375,546424],"enemy_1043_zomsbr":[66,61,4,22690,482592],"enemy_1402_tgshd_2":[36,9,20,73778,762837]}, + "hooks": [1,139,6900,1392,1392,1111,1111,13434,71,113,50,46,69,22945,578,9,123,42,1,71,0,0,0], + "events": {"atk":2285,"deploy":13,"die":71,"dmg":1100,"engage":2,"fx":461,"heal":1136,"leak":42,"skill":96,"spawn":138,"status":196}, + "fx": {"aoe":68,"expose":276,"healAoe":68,"shield":43,"takeoff":6}, + "snaps": "10b9b952 5a9c72b0 a18bcace ad14730d 2738cb6e b6480676 ce3143fe 8325b936 65ce32cb e08469c0 e89b323b ef437c41 2c092c6c 276a47df ec4d8626 4df04cb8 9b1aa959 6947de2d 09278232 1248b5da 89e60aa4 cc242745 850b5133" }, "diy-134": { "about": "5_diy1_b=monstr/S1/002_monstr 5_diy2_b=monstr/S2/002_monstr 5_diy1_b=monstr/S3/002_monstr 6_diy1_a=monstr/S1 6_diy2_a=monstr/S2 6_diy1_a=monstr/S3 6_diy2_b=monstr/S1 6_diy1_b=monstr/S2 6_diy2_b=monstr/S3 6_diy1_b=monstr/S1/002_monstr 6_diy2_b=monstr/S2/002_monstr 6_diy1_b=monstr/S3/002_monstr", @@ -5832,24 +5832,24 @@ "errors": {"count":0,"kinds":[]}, "players": {"p1":{"killed":0,"total":87,"perfect":false,"leaks":87,"leaksUncounted":0,"coins":0,"dmg":0,"bossDmg":0,"heal":114994,"deaths":14,"layerGains":{}}}, "units": [ - ["p1",1,"op","chess_char_5_diy1_b",0,"uniequip_002_monstr",1653,1,4,0,0,1184,27568,59,14], + ["p1",1,"op","chess_char_5_diy1_b",0,"uniequip_002_monstr",1653,1,4,0,0,1184,27652,59,14], ["p1",2,"op","chess_char_5_diy2_b",1,"uniequip_002_monstr",1653,1,109,0,0,5064,13988,43,3], ["p1",3,"op","chess_char_5_diy1_b",2,"uniequip_002_monstr",1653,1,5,0,0,0,8140,16,0], - ["p1",4,"op","chess_char_6_diy1_a",0,null,1492,1,8,0,0,2368,2773,3,0], - ["p1",5,"op","chess_char_6_diy2_a",1,null,0,0,91,0,0,11963,7318,19,2], - ["p1",6,"op","chess_char_6_diy1_a",2,null,0,0,87,0,0,11963,7558,18,0], - ["p1",7,"op","chess_char_6_diy2_b",0,"none",1653,1,67,0,0,22845,11839,31,7], + ["p1",4,"op","chess_char_6_diy1_a",0,null,1492,1,8,0,0,2368,2736,3,0], + ["p1",5,"op","chess_char_6_diy2_a",1,null,0,0,91,0,0,11963,6982,19,2], + ["p1",6,"op","chess_char_6_diy1_a",2,null,0,0,87,0,0,11963,7534,18,0], + ["p1",7,"op","chess_char_6_diy2_b",0,"none",1653,1,67,0,0,22845,12161,31,7], ["p1",8,"op","chess_char_6_diy1_b",1,"none",1653,1,110,0,0,11254,7251,27,2], - ["p1",9,"op","chess_char_6_diy2_b",2,"none",1653,1,103,0,0,18899,10855,43,0], - ["p1",10,"op","chess_char_6_diy1_b",0,"uniequip_002_monstr",1653,1,59,0,0,22285,10465,41,9], + ["p1",9,"op","chess_char_6_diy2_b",2,"none",1653,1,103,0,0,18899,11054,42,0], + ["p1",10,"op","chess_char_6_diy1_b",0,"uniequip_002_monstr",1653,1,59,0,0,22285,9926,41,9], ["p1",11,"op","chess_char_6_diy2_b",1,"uniequip_002_monstr",0,0,97,0,0,12660,3464,12,1], - ["p1",12,"op","chess_char_6_diy1_b",2,"uniequip_002_monstr",1653,1,108,0,0,17008,3775,16,0] + ["p1",12,"op","chess_char_6_diy1_b",2,"uniequip_002_monstr",1653,1,108,0,0,17008,4107,16,0] ], "enemies": {"enemy_10065_ftzlc":[51,0,48,51035,0],"enemy_10067_ftsjc":[36,0,36,86456,0]}, - "hooks": [1,110,6900,607,607,311,398,371,14,98,38,37,0,234,0,16,87,84,1,14,0,14,0], - "events": {"atk":650,"deploy":23,"die":14,"dmg":398,"fx":46,"heal":371,"leak":84,"skill":75,"spawn":99,"status":28}, + "hooks": [1,110,6900,606,606,311,398,832,14,98,38,37,0,234,0,16,87,84,1,14,0,14,0], + "events": {"atk":1111,"deploy":23,"die":14,"dmg":398,"fx":46,"heal":370,"leak":84,"skill":75,"spawn":99,"status":28}, "fx": {"burst":14,"explode":6,"ignite":26}, - "snaps": "aa8c2889 84dd5395 436c2e1a db08ee72 fca93f07 733c2a97 a8318be6 26978ed8 3aa1f439 f0f47853 ed44b5d5 873c241d 88686c26 cee18565 630d1823 3aeb0532 c16f713b 188da44a 227babd7 16a63da4 23e05e48 7234fd43 57257e52" + "snaps": "aa8c2889 28ac65ad 6c156e1f 0313ba2d e88ae5bc 503ddf10 7a3e79f5 5b384e0f 0a95ba3e 52107200 87a44c5a 9f1db072 78e294b9 bf8f3de2 02649f88 e742948d c4c8db4c 25ffa105 2059891c 259b3e43 371952cb 22f79984 36d42c19" }, "diy-135": { "about": "5_diy1_a=cdfend 5_diy2_b=cdfend 5_diy1_a=cguard 5_diy2_b=cguard 5_diy1_a=csnipe 5_diy2_b=csnipe 5_diy1_a=ccast 5_diy2_b=ccast 5_diy1_a=cmedic 5_diy2_b=cmedic 5_diy1_a=csuppo 5_diy2_b=csuppo", diff --git a/test/golden/roster.json b/test/golden/roster.json index b9dd816e..be27ccfb 100644 --- a/test/golden/roster.json +++ b/test/golden/roster.json @@ -204,19 +204,19 @@ "bossHpLeft": null, "rngDraws": 2, "errors": {"count":0,"kinds":[]}, - "players": {"p1":{"killed":9,"total":36,"perfect":false,"leaks":27,"leaksUncounted":0,"coins":2,"dmg":103318,"bossDmg":0,"heal":20175,"deaths":13,"layerGains":{}}}, + "players": {"p1":{"killed":9,"total":36,"perfect":false,"leaks":27,"leaksUncounted":0,"coins":2,"dmg":112604,"bossDmg":0,"heal":20365,"deaths":13,"layerGains":{}}}, "units": [ - ["p1",1,"op","chess_char_2_08_a",1,null,0,0,8,1,4451,9857,148,4,0], - ["p1",2,"op","chess_char_2_17_a",1,null,0,0,6,1,4245,7897,1734,3,2], - ["p1",3,"op","chess_char_2_18_a",0,null,0,0,9,0,18340,7055,147,11,1], - ["p1",4,"op","chess_char_2_07_a",1,null,0,0,10,0,6974,6231,0,9,1], + ["p1",1,"op","chess_char_2_08_a",1,null,0,0,8,1,5701,10301,148,4,0], + ["p1",2,"op","chess_char_2_17_a",1,null,0,0,6,0,3626,7897,1734,2,2], + ["p1",3,"op","chess_char_2_18_a",0,null,0,0,9,0,26482,8005,147,14,1], + ["p1",4,"op","chess_char_2_07_a",1,null,0,0,10,0,6924,6231,0,9,1], ["p1",5,"op","chess_char_2_09_a",1,null,1513.21,1,56,0,6902,5702,1638,19,1], - ["p1",6,"op","chess_char_2_11_a",1,null,0,0,12,1,18402,8304,113,26,1], + ["p1",6,"op","chess_char_2_11_a",1,null,0,0,12,1,18832,7470,113,25,1], ["p1",7,"op","chess_char_2_13_a",1,null,3122.65,1,57,2,10661,3169,22,20,1], ["p1",8,"op","chess_char_2_16_a",1,null,3098.43,1,54,0,5438,4790,139,15,0], - ["p1",9,"op","chess_char_2_12_a",1,null,0,0,52,4,11743,4506,0,40,2], - ["p1",10,"op","chess_char_2_06_a",0,null,0,0,4,0,0,3150,11880,22,7], - ["p1",11,"op","chess_char_2_10_a",1,null,0,0,5,0,10120,2406,0,51,1], + ["p1",9,"op","chess_char_2_12_a",1,null,0,0,52,4,12322,4404,0,42,2], + ["p1",10,"op","chess_char_2_06_a",0,null,0,0,4,0,0,3062,12070,22,7], + ["p1",11,"op","chess_char_2_10_a",1,null,0,0,5,1,9674,2406,0,51,1], ["p1",12,"op","chess_char_2_14_a",1,null,2131.36,1,55,0,0,3763,4306,9,0], ["-","~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], @@ -224,26 +224,26 @@ ["p1","~1","token","token_10022_kazema_shadow",null,null,0,0,26,0,6042,1864,48,6,null] ], "enemies": { - "enemy_1001_bigbo": [1,0,1,6870,6391], - "enemy_1006_shield": [1,0,1,3430,6233], - "enemy_10118_ymgprc": [1,0,1,6271,8291], - "enemy_1041_lazerd_2": [1,0,1,6316,4317], - "enemy_1118_lidbox_2": [1,1,0,4232,16416], - "enemy_1158_divman": [18,8,10,7018,62451], - "enemy_1160_hvyslr": [3,0,3,3830,9897], - "enemy_1161_tidmag": [3,0,2,7246,8350], - "enemy_1169_duphlx_2": [1,0,1,621,6838], + "enemy_1001_bigbo": [1,0,1,6870,4832], + "enemy_1006_shield": [1,0,1,3483,9381], + "enemy_10118_ymgprc": [1,0,1,6271,9669], + "enemy_1041_lazerd_2": [1,0,1,6338,4317], + "enemy_1118_lidbox_2": [1,1,0,3643,16416], + "enemy_1158_divman": [18,8,10,6618,62957], + "enemy_1160_hvyslr": [3,0,3,3906,11840], + "enemy_1161_tidmag": [3,0,2,6444,10438], + "enemy_1169_duphlx_2": [1,0,1,1292,9579], "enemy_1200_msfjin_2": [1,0,1,0,21], - "enemy_1249_lysdb_2": [1,0,1,0,1249], + "enemy_1249_lysdb_2": [1,0,1,0,1467], "enemy_1299_ymkilr": [1,0,1,0,560], - "enemy_1389_winbab_2": [1,0,1,942,1330], - "enemy_1504_cqbw": [1,0,1,207,630], + "enemy_1389_winbab_2": [1,0,1,1089,1330], + "enemy_1504_cqbw": [1,0,1,652,630], "enemy_2009_csaudc": [1,0,1,0,490] }, - "hooks": [1,57,3300,438,438,1364,1385,1339,23,49,17,17,0,10847,31,30,36,26,1,28,0,2,0], - "events": {"atk":492,"bounty":1,"deploy":18,"die":23,"dmg":1318,"engage":11,"fx":20,"heal":85,"leak":26,"skill":34,"spawn":52,"status":157}, + "hooks": [1,57,3300,442,442,1430,1450,1360,23,49,17,17,0,10816,33,32,36,26,1,28,0,2,0], + "events": {"atk":515,"bounty":1,"deploy":18,"die":23,"dmg":1381,"engage":11,"fx":20,"heal":87,"leak":26,"skill":34,"spawn":52,"status":171}, "fx": {"aoe":4,"burst":2,"camouflage":2,"dodge":3,"dollEnd":1,"liberate":1,"overload":1,"shield":2,"shieldBreak":1,"substitute":1,"summon":1,"takeoff":1}, - "snaps": "78c9be45 59671eba dea6e147 ecf77961 f0504ef1 7c7c3e8c 51d85363 171262cf f3ae9337 f8af7602 bab13f4b" + "snaps": "95c7197d e941625e 33d925c4 1e508419 2508f788 7d3585ce 94e8c1e6 d30888a6 6fa26a6a 3cc039cd 59559726" }, "roster-005": { "about": "pass 0 band 3: chess_char_2_07_b chess_char_2_08_b chess_char_2_09_b chess_char_2_11_b chess_char_2_12_b chess_char_2_19_a chess_char_2_01_b chess_char_2_02_b chess_char_2_04_b chess_char_2_05_b chess_char_2_06_b chess_char_2_10_b token_10000_silent_healrb", @@ -261,44 +261,44 @@ "total": 84, "unspawned": 0, "bossHpLeft": null, - "rngDraws": 331, + "rngDraws": 333, "errors": {"count":0,"kinds":[]}, - "players": {"p1":{"killed":84,"total":84,"perfect":true,"leaks":0,"leaksUncounted":0,"coins":12,"dmg":308157,"bossDmg":0,"heal":37346,"deaths":0,"layerGains":{}}}, + "players": {"p1":{"killed":84,"total":84,"perfect":true,"leaks":0,"leaksUncounted":0,"coins":12,"dmg":308197,"bossDmg":0,"heal":36826,"deaths":0,"layerGains":{}}}, "units": [ - ["p1",1,"op","chess_char_2_07_b",1,"uniequip_002_ghost",2664.2,1,2,0,0,1707,1103,0,0], + ["p1",1,"op","chess_char_2_07_b",1,"uniequip_002_ghost",2664.2,1,2,0,0,1707,1318,0,0], ["p1",2,"op","chess_char_2_08_b",1,"uniequip_002_bubble",3373,1,4,0,49,1818,0,0,1], - ["p1",3,"op","chess_char_2_09_b",1,"uniequip_002_humus",2133,1,6,32,85807,0,0,56,2], - ["p1",4,"op","chess_char_2_11_b",1,"uniequip_002_kazema",2214,1,8,11,87316,15458,0,89,3], - ["p1",5,"op","chess_char_2_12_b",1,"uniequip_002_gravel",1452,1,10,2,7115,9463,0,124,1], - ["p1",6,"op","chess_char_2_19_a",1,null,1021.2,1,11,1,14957,1433,314,26,3], - ["p1",7,"op","chess_char_2_01_b",1,"uniequip_002_excu",2146,1,12,20,36079,4543,0,22,1], - ["p1",8,"op","chess_char_2_02_b",1,"uniequip_002_silent",1614,1,1,0,0,0,8988,27,4], - ["p1",9,"op","chess_char_2_04_b",1,"uniequip_002_grabds",1270,1,3,6,39117,2115,0,51,3], - ["p1",10,"op","chess_char_2_05_b",1,"uniequip_002_harold",1971.2,1,5,0,0,0,11003,40,2], - ["p1",11,"op","chess_char_2_06_b",0,"uniequip_002_papyrs",1639,1,7,0,0,0,13785,27,12], + ["p1",3,"op","chess_char_2_09_b",1,"uniequip_002_humus",2133,1,6,31,86159,0,0,56,2], + ["p1",4,"op","chess_char_2_11_b",1,"uniequip_002_kazema",2214,1,8,11,87383,15748,0,89,3], + ["p1",5,"op","chess_char_2_12_b",1,"uniequip_002_gravel",1452,1,10,2,7115,8923,0,124,1], + ["p1",6,"op","chess_char_2_19_a",1,null,1021.2,1,11,1,14957,1523,407,26,3], + ["p1",7,"op","chess_char_2_01_b",1,"uniequip_002_excu",2146,1,12,20,36079,4183,0,22,1], + ["p1",8,"op","chess_char_2_02_b",1,"uniequip_002_silent",1614,1,1,0,0,0,9551,28,5], + ["p1",9,"op","chess_char_2_04_b",1,"uniequip_002_grabds",1270,1,3,7,38738,2115,0,51,3], + ["p1",10,"op","chess_char_2_05_b",1,"uniequip_002_harold",1971.2,1,5,0,0,0,11743,39,2], + ["p1",11,"op","chess_char_2_06_b",0,"uniequip_002_papyrs",1639,1,7,0,0,0,11657,25,11], ["p1",12,"op","chess_char_2_10_b",1,"uniequip_002_rockr",1406,1,9,6,37719,810,0,107,2], - ["p1",13,"token","token_10000_silent_healrb",null,null,1000,0,97,0,0,0,2153,21,null] + ["p1",13,"token","token_10000_silent_healrb",null,null,1000,1,103,0,0,0,2150,21,null] ], "enemies": { - "enemy_10027_vtsk": [1,1,0,10598,24000], + "enemy_10027_vtsk": [1,1,0,10058,24000], "enemy_1006_shield_2": [1,1,0,0,8000], - "enemy_10122_uacann_2": [1,1,0,3386,8000], + "enemy_10122_uacann_2": [1,1,0,3476,8000], "enemy_1042_frostd": [1,1,0,0,5200], "enemy_1119_vofsd": [1,1,0,125,8800], "enemy_1170_dushld": [1,1,0,221,5600], "enemy_1202_msfzhi": [1,1,0,0,3], - "enemy_1249_lysdb_2": [15,15,0,198,72000], + "enemy_1249_lysdb_2": [15,15,0,310,72000], "enemy_1251_lysyta": [52,52,0,50,114400], "enemy_1299_ymkilr_2": [1,1,0,1178,8800], - "enemy_1328_cbjedi": [6,6,0,7780,48000], - "enemy_1402_tgshd_2": [1,1,0,110,10400], - "enemy_1509_mousek": [1,1,0,7838,17600], - "enemy_2010_csdcr": [1,1,0,2542,9600] + "enemy_1328_cbjedi": [6,6,0,8244,48000], + "enemy_1402_tgshd_2": [1,1,0,200,10400], + "enemy_1509_mousek": [1,1,0,7192,17600], + "enemy_2010_csdcr": [1,1,0,2452,9600] }, - "hooks": [1,101,4063,685,685,1621,1615,920,84,89,34,33,0,27121,145,51,84,0,1,84,0,0,0], - "events": {"atk":874,"bounty":6,"deploy":17,"die":89,"dmg":1572,"engage":7,"fx":64,"heal":131,"skill":67,"spawn":97,"status":420}, - "fx": {"counter":2,"explode":3,"expose":9,"overload":2,"phase":1,"shield":8,"shieldBreak":16,"sleep":7,"summon":4,"telegraph":6,"zone":6}, - "snaps": "de0d6fce aa9dbd18 24c987bd 0f416484 692cfae7 3f26cb76 377b5a69 9d5ebf16 9df2f8fe bb2d9188 246affc9 69fd644b 837cdc8a 5814f3d6" + "hooks": [1,102,4063,682,682,1618,1612,1098,84,89,34,33,0,26938,145,50,84,0,1,84,0,0,0], + "events": {"atk":896,"bounty":6,"deploy":18,"die":89,"dmg":1572,"engage":7,"fx":65,"heal":123,"skill":67,"spawn":97,"status":430}, + "fx": {"counter":2,"explode":3,"expose":9,"overload":2,"phase":1,"shield":8,"shieldBreak":16,"sleep":7,"summon":5,"telegraph":6,"zone":6}, + "snaps": "de0d6fce aa9dbd18 e5f78211 369608f9 eaf0206b 8ca00225 56597ada 3c3af39f 867867b0 406379b7 654df676 66745bf5 fdd72e8b 1ff822b8" }, "roster-006": { "about": "pass 0 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", @@ -1470,47 +1470,48 @@ "time": 110, "ticks": 3300, "reason": "timeout", - "killed": 31, - "total": 35, + "killed": 33, + "total": 36, "unspawned": 0, "bossHpLeft": null, "rngDraws": 0, "errors": {"count":0,"kinds":[]}, - "players": {"p1":{"killed":31,"total":35,"perfect":false,"leaks":4,"leaksUncounted":0,"coins":4,"dmg":150801,"bossDmg":0,"heal":24108,"deaths":4,"layerGains":{}}}, + "players": {"p1":{"killed":33,"total":36,"perfect":false,"leaks":3,"leaksUncounted":0,"coins":6,"dmg":160899,"bossDmg":0,"heal":23475,"deaths":4,"layerGains":{}}}, "units": [ - ["p1",1,"op","chess_char_2_08_a",0,null,1382.46,1,7,1,3411,5678,321,4,1], - ["p1",2,"op","chess_char_2_17_a",0,null,0,0,11,7,29473,7967,4343,19,4], - ["p1",3,"op","chess_char_2_18_a",1,null,0,0,12,12,43236,7352,323,19,2], - ["p1",4,"op","chess_char_2_07_a",0,null,2806.23,1,52,4,19876,9391,361,35,1], - ["p1",5,"op","chess_char_2_09_a",0,null,2208.44,1,50,6,46469,3673,1465,37,7], - ["p1",6,"op","chess_char_2_11_a",0,null,2404.18,1,8,0,711,90,6,1,0], - ["p1",7,"op","chess_char_2_13_a",0,null,2472.85,1,9,1,1566,0,0,4,1], - ["p1",8,"op","chess_char_2_16_a",0,null,2453.67,1,6,0,0,0,0,0,0], - ["p1",9,"op","chess_char_2_12_a",0,null,1516.59,1,10,0,0,0,0,0,1], - ["p1",10,"op","chess_char_2_06_a",1,null,1972.8,1,4,0,0,0,11343,26,2], - ["p1",11,"op","chess_char_2_10_a",0,null,1476.86,1,5,0,6057,0,0,35,1], - ["p1",12,"op","chess_char_2_14_a",0,null,1687.84,1,13,0,0,3611,5947,20,2], + ["p1",1,"op","chess_char_2_08_a",0,null,2984.92,1,7,3,9513,6371,340,21,1], + ["p1",2,"op","chess_char_2_17_a",0,null,0,0,11,6,28522,7736,4112,21,3], + ["p1",3,"op","chess_char_2_18_a",1,null,0,0,12,13,47196,8094,566,20,2], + ["p1",4,"op","chess_char_2_07_a",0,null,1401.83,1,53,4,22612,7936,390,37,1], + ["p1",5,"op","chess_char_2_09_a",0,null,2208.44,1,50,5,46192,3668,1459,36,7], + ["p1",6,"op","chess_char_2_11_a",0,null,2487.92,1,8,0,4,249,0,4,1], + ["p1",7,"op","chess_char_2_13_a",0,null,2472.85,1,9,0,11,0,0,11,1], + ["p1",8,"op","chess_char_2_16_a",0,null,2453.67,1,6,1,8,0,0,8,0], + ["p1",9,"op","chess_char_2_12_a",0,null,1516.59,1,10,0,5,0,0,5,1], + ["p1",10,"op","chess_char_2_06_a",1,null,1972.8,1,4,0,0,0,5989,31,2], + ["p1",11,"op","chess_char_2_10_a",0,null,1476.86,1,5,1,6835,0,0,42,1], + ["p1",12,"op","chess_char_2_14_a",0,null,1039.13,1,13,0,0,3442,10617,21,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], ["-","~3","device","trap_1105_accrate",null,null,100,1,3,0,0,0,0,0,null] ], "enemies": { - "enemy_10034_cnvsax": [1,1,0,2873,8000], - "enemy_1010_demon": [6,6,0,1712,36000], - "enemy_1010_demon_2": [3,2,0,3600,25901], + "enemy_10034_cnvsax": [1,1,0,2870,8000], + "enemy_1010_demon": [6,6,0,2281,36000], + "enemy_1010_demon_2": [3,2,0,4743,27107], "enemy_10112_ymgds": [1,1,0,22,4000], - "enemy_1067_snslime": [1,1,0,419,2600], - "enemy_1071_dftman": [18,18,0,288,40320], - "enemy_1170_dushld": [1,1,0,1215,5600], - "enemy_1209_sfden_2": [1,0,0,8494,10199], - "enemy_1305_mhslim_2": [1,1,0,294,1440], - "enemy_1439_dslntf_2": [1,0,0,646,11814], - "enemy_2048_smgrd": [1,0,0,8843,4925] + "enemy_1067_snslime": [1,1,0,462,2600], + "enemy_1071_dftman": [18,18,0,211,40320], + "enemy_1170_dushld": [1,1,0,1813,5600], + "enemy_1209_sfden_2": [1,1,0,5807,14400], + "enemy_1210_msfden_2": [1,1,0,0,45], + "enemy_1305_mhslim_2": [1,1,0,333,1440], + "enemy_1439_dslntf_2": [1,0,0,960,13867], + "enemy_2048_smgrd": [1,0,0,8683,7520] }, - "hooks": [1,52,3300,358,358,827,942,1960,35,35,22,20,0,10743,62,45,35,0,1,35,0,3,0], - "events": {"atk":497,"bounty":2,"deploy":14,"die":35,"dmg":827,"engage":8,"fx":65,"heal":116,"skill":42,"spawn":50,"status":256}, - "fx": {"beam":51,"buff":3,"burst":3,"explode":1,"takeoff":1,"telegraph":6}, - "snaps": "dd24c30b 998fef35 67166def b786a3cd 7dfcb3ef de9df651 3687ea1a 38be8416 332a7a64 b8e90f9d 99c0ed59" + "hooks": [1,53,3300,409,409,877,983,2396,37,37,22,20,0,10966,74,43,36,0,1,37,0,3,0], + "events": {"atk":594,"bounty":3,"deploy":14,"die":37,"dmg":909,"engage":10,"fx":64,"heal":90,"skill":42,"spawn":51,"status":184}, + "fx": {"beam":51,"buff":3,"burst":3,"explode":1,"summon":1,"takeoff":1,"telegraph":4}, + "snaps": "dd24c30b 998fef35 285beff0 28c4534a be20f6d2 b0466f87 88bdd89e b929dbdd 78371cf9 0f00293b 91358da2" }, "roster-024": { "about": "pass 1 band 3: chess_char_2_07_b chess_char_2_08_b chess_char_2_09_b chess_char_2_11_b chess_char_2_12_b chess_char_2_19_a chess_char_2_01_b chess_char_2_02_b chess_char_2_04_b chess_char_2_05_b chess_char_2_06_b chess_char_2_10_b", @@ -1530,41 +1531,41 @@ "bossHpLeft": null, "rngDraws": 11, "errors": {"count":0,"kinds":[]}, - "players": {"p1":{"killed":150,"total":191,"perfect":false,"leaks":41,"leaksUncounted":0,"coins":6,"dmg":194174,"bossDmg":0,"heal":22504,"deaths":2,"layerGains":{}}}, + "players": {"p1":{"killed":150,"total":191,"perfect":false,"leaks":41,"leaksUncounted":0,"coins":6,"dmg":194174,"bossDmg":0,"heal":20762,"deaths":2,"layerGains":{}}}, "units": [ - ["p1",1,"op","chess_char_2_07_b",0,"none",0,0,3,28,65300,6473,3522,59,2], + ["p1",1,"op","chess_char_2_07_b",0,"none",0,0,3,28,65300,6609,3945,59,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",2214,1,7,13,4490,2214,0,31,10], - ["p1",5,"op","chess_char_2_12_b",0,"none",1352,1,10,19,23053,2207,0,61,1], + ["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",6,"op","chess_char_2_19_a",0,null,1021.2,1,5,49,33272,0,0,73,10], ["p1",7,"op","chess_char_2_01_b",0,"none",2146,1,8,15,3240,0,0,19,2], - ["p1",8,"op","chess_char_2_02_b",0,"none",2624.8,1,6,0,0,264,868,9,1], - ["p1",9,"op","chess_char_2_04_b",0,"none",1180,1,9,24,57718,753,0,65,32], - ["p1",10,"op","chess_char_2_05_b",0,"none",1873.2,1,11,0,0,7611,4583,46,3], - ["p1",11,"op","chess_char_2_06_b",1,"none",1639,1,12,0,0,5599,13485,63,4], + ["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,24,57718,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",12,"op","chess_char_2_10_b",0,"none",1356,1,2,1,7101,0,0,77,3] ], "enemies": { "enemy_1006_shield": [1,1,0,0,5760], - "enemy_1019_jshoot": [1,1,0,748,1728], + "enemy_1019_jshoot": [1,1,0,721,1728], "enemy_1121_lifbos_2": [1,1,0,1001,24000], - "enemy_1195_sfyin": [36,34,0,13160,65280], - "enemy_1195_sfyin_2": [15,13,2,124,41210], + "enemy_1195_sfyin": [36,34,0,11258,65280], + "enemy_1195_sfyin_2": [15,13,2,140,41210], "enemy_1196_msfyin": [68,61,7,0,126], "enemy_1196_msfyin_2": [26,14,11,0,56], "enemy_1197_sfshu": [9,9,0,91,21600], "enemy_1197_sfshu_2": [1,1,0,14,3216], "enemy_1198_msfshu": [27,10,17,0,56], "enemy_1198_msfshu_2": [3,3,0,0,12], - "enemy_1267_nhpbr_2": [1,1,0,865,5280], + "enemy_1267_nhpbr_2": [1,1,0,1256,5280], "enemy_1381_winman_2": [1,1,0,30,7488], "enemy_1535_wlfmster": [1,0,1,8955,18362] }, - "hooks": [1,203,4200,763,763,1020,1087,2166,152,190,68,67,0,13939,1140,53,191,38,1,160,0,0,0], - "events": {"atk":892,"bounty":3,"deploy":12,"die":152,"dmg":1043,"engage":8,"fx":77,"heal":169,"leak":38,"skill":135,"spawn":203,"status":542}, + "hooks": [1,203,4200,761,761,1020,1087,2427,152,190,68,65,0,13911,1140,53,191,38,1,160,0,0,0], + "events": {"atk":919,"bounty":3,"deploy":12,"die":152,"dmg":1040,"engage":8,"fx":77,"heal":151,"leak":38,"skill":133,"spawn":203,"status":548}, "fx": {"beam":3,"buff":5,"summon":57,"undying":1,"zone":11}, - "snaps": "5aee07b4 43516039 123ac569 7e5abe04 0078416f cb4c36c7 279569a2 a39cd06e dd946a0a 753030d4 47fceaec 24a8f1a0 8923779d 317bf2ff" + "snaps": "3528dfb7 48905850 aff1c414 d9acb51f ff5e053b 66e23a01 ad945452 7b153731 2b2109c6 09f352e7 7e7ed5ff 28419b29 50075fae 2743a756" }, "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", diff --git a/test/sim/feedback5-chain-heal.test.js b/test/sim/feedback5-chain-heal.test.js new file mode 100644 index 00000000..0f074427 --- /dev/null +++ b/test/sim/feedback5-chain-heal.test.js @@ -0,0 +1,58 @@ +// test/sim/feedback5-chain-heal.test.js — the 链愈师 heal chain (follow-up 27; PRTS 分支特性信息 链愈师 "跳跃范围为x-4,无特殊说明 +// 的场合一次治疗链不会对已跳跃过的单位重复跳跃", "优先跳跃至范围内生命比例最低>部署时间点最晚的我方单位。可选择满生命我方单位为跳跃目标, +// 但仍受禁疗制约") — server/sim/ai.js chainHealNext / doHeal (莎草's profession chain; Mon3tr's kit uses the same links). It used +// to jump to the most injured ally within 2.5 tiles (never a full-HP one, ties by list order). +// Run: node --test test/sim/feedback5-chain-heal.test.js + +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { makeBattle, chessRec, checkInvariants } from '../helpers/battleHarness.js'; + +const GRID = []; for (let r = -2; r <= 2; r++) for (let c = 0; c <= 6; c++) GRID.push([r, c]); +const HEALER = chessRec({ id: 't_chain', profession: 'MEDIC', subProfessionId: 'chainhealer', stats: { maxHp: 1e4, atk: 100, bat: 1 }, skill: null, rangeGrid: GRID }); +const wall = (id) => chessRec({ id, stats: { maxHp: 1e4, atk: 0, def: 0 }, skill: null }); + +/** The healer on (10, 2) and walls at `at` = { id: [row, col, hpRatio] }; returns the chain of its first heal (ids → wall ids). */ +function firstChain(at, setup = null) { + const ids = Object.keys(at); + const h = makeBattle({ + defs: { chess: { t_chain: HEALER, ...Object.fromEntries(ids.map((id) => [id, wall(id)])) } }, + units: [{ chessId: 't_chain', row: 10, col: 2 }, ...ids.map((id) => ({ chessId: id, row: at[id][0], col: at[id][1] }))], + content: 'none', autoFinish: false, timeLimit: 30, seed: 3, + }); + h.step(); // deployed (the healer heals nobody yet: everyone is at full HP) + for (const id of ids) { const u = h.unit(id); u.hp = u.s.maxHp * at[id][2]; } + if (setup) setup(h); + const n0 = h.events.length; + for (let i = 0; i < 90 && !h.events.slice(n0).some((e) => e[0] === 'atk'); i++) h.step(); // (step drains the events) + const name = new Map(h.b.allyUnits.map((u) => [u.id, u.defId])); + const ev = h.events.slice(n0).filter((e) => e[0] === 'atk'); + checkInvariants(h.b); + return { h, main: name.get(ev.find((e) => e[3] !== 'chainHeal')[2]), jumps: ev.filter((e) => e[3] === 'chainHeal').map((e) => [name.get(e[1]), name.get(e[2])]) }; +} + +test('the jump reaches the 3×3 around the last one healed (x-4), not an ally two tiles away; each jump goes on from where it landed', () => { + // A (most injured) on (10, 5); B two tiles right of it (inside 2.5 tiles, outside its 3×3), C on its diagonal + const r = firstChain({ t_a: [10, 5, 0.5], t_b: [10, 7, 0.6], t_c: [11, 6, 0.7] }); + assert.equal(r.main, 't_a'); + assert.deepEqual(r.jumps, [['t_a', 't_c'], ['t_c', 't_b']], 'A → C (its 3×3) → B (C\'s 3×3); it used to jump A → B → C'); +}); + +test('a full-HP ally is a jump target when no injured one is in reach — healed for nothing, the chain jumps on from it', () => { + const r = firstChain({ t_a: [10, 5, 0.5], t_d: [10, 6, 1], t_e: [10, 7, 0.6] }); + assert.equal(r.main, 't_a'); + assert.deepEqual(r.jumps, [['t_a', 't_d'], ['t_d', 't_e']], 'A → D (full HP) → E'); + assert.equal(r.h.unit('t_d').hp, r.h.unit('t_d').s.maxHp); +}); + +test('ties of the HP ratio: the latest deployed first; a 禁疗 unit is never a jump target', () => { + const tie = firstChain({ t_a: [10, 5, 0.5], t_f: [9, 5, 0.8], t_g: [11, 5, 0.8] }); + const [f, g] = [tie.h.unit('t_f'), tie.h.unit('t_g')]; + const later = f.aggroSeq > g.aggroSeq ? 't_f' : 't_g'; + assert.deepEqual(tie.jumps[0], ['t_a', later], 'the later deployed of the two'); + const banned = firstChain({ t_a: [10, 5, 0.5], t_n: [10, 6, 0.2], t_m: [11, 5, 0.9] }, (h) => { + h.b.addBuff(h.unit('t_n'), { key: 'test:healFree', status: 'healFree', flags: { noHeal: true, healFree: true } }); + }); + assert.equal(banned.main, 't_a', 'the 禁疗 unit is no main target either'); + assert.deepEqual(banned.jumps[0], ['t_a', 't_m'], 'the 禁疗 unit is skipped'); +});