Let charged guardian heals act without enemies and fix mixed S2 triggers (#406)

This commit is contained in:
sganggs committed 2026-10-09 17:31:42 +08:00
1 parent c9481c40bb
commit e86c7c8304
9 files changed
+155 -50

No files matched your search

+71
View File
@@ -0,0 +1,71 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { makeBattle, chessRec, enemyRec, checkInvariants } from '../helpers/battleHarness.js';
const GUARDIANS = [
{ chessId: 'chess_char_5_11_b', skillIndex: 0 },
{ diy: { slot: 6, charId: 'char_2025_shu', skillIndex: 0 }, elite: true },
];
function guardian(entry) {
const h = makeBattle({ autoFinish: false, captureNoisy: true, hooks: ['heal', 'skillStart'],
defs: { chess: { patient: chessRec({ id: 'patient', skill: null, stats: { maxHp: 100000 } }) } },
units: [{ uid: 1, ...entry, row: 10, col: 4 }, { uid: 2, chessId: 'patient', row: 10, col: 5 }] });
h.step();const u = h.unit(1), p = h.unit(2);u.skill.gainSp(999);return { h, u, p };
}
for (const entry of GUARDIANS) {
const name = entry.chessId ? '塞雷娅' : '黍';
test(`${name} S1 heals without enemies, respects half HP and keeps attack cadence`, () => {
const { h, u, p } = guardian(entry);
p.hp = p.s.maxHp * 0.5001;h.run(0.5);assert.equal(u.skill.activations, 0);
p.hp = p.s.maxHp * 0.5;u.atkCd = 1;
h.run(0.5);assert.equal(u.skill.activations, 0, 'wait for the attack interval');
assert.ok(h.runUntil(() => u.skill.activations > 0, 1));
assert.ok(p.hp > p.s.maxHp * 0.5, 'exact half HP is eligible');
p.hp = p.s.maxHp * 0.2;
assert.ok(h.runUntil(() => u.skill.activations >= 2, 4));
const casts = h.hooksOf('skillStart').filter(c => c.unit === u);
assert.ok(casts[1].t - casts[0].t >= u.base.bat - 0.04, 'charges are paced by attacks');
checkInvariants(h.b);assert.deepEqual(h.b.errors, []);
});
test(`${name} S1 preserves exact normal/fast attack intervals and waits out disarm`, () => {
for (const aspd of [0, 100]) {
const { h, u, p } = guardian(entry);
p.hp *= 0.1;
if (aspd) h.b.addBuff(u, { key: 'test:aspd', mods: { aspd } });
h.b.on('heal', c => { if (c.source === u) u.skill.gainSp(999); });
const charges = u.skill.charges;
h.b.applyStatus(u, 'disarm', { duration: 1 });
h.run(0.5);
assert.equal(u.skill.activations, 0);
assert.equal(u.skill.pending, false);
assert.equal(u.skill.charges, charges);
h.run(4);
const times = h.hooksOf('heal').filter(c => c.source === u).map(c => c.t);
assert.ok(times.length >= 3);
for (let i = 1; i < times.length; i++) assert.ok(Math.abs(times[i] - times[i - 1] - u.s.interval) < 1e-9);
checkInvariants(h.b);assert.deepEqual(h.b.errors, []);
}
});
test(`${name} S1 cannot heal a forbidden patient or cast while silenced; invalid pending heal refunds the charge`, () => {
const { h, u, p } = guardian(entry);p.hp *= 0.2;
h.b.addBuff(p, { key: 'test:noheal', flags: { noHeal: true } });h.run(2);assert.equal(u.skill.activations, 0);
h.b.removeBuff(p, 'test:noheal');h.b.applyStatus(u, 'silence', { duration: 2 });h.run(1);assert.equal(u.skill.activations, 0);
h.run(1.1);assert.ok(u.skill.activations > 0);
u.skill.gainSp(999);const charges = u.skill.charges;u.atkCd = 1;
assert.ok(u.skill.activate('test'));p.hp = p.s.maxHp;h.step();
assert.equal(u.skill.pending, false);assert.equal(u.skill.charges, charges);
checkInvariants(h.b);assert.deepEqual(h.b.errors, []);
});
}
for (const motion of ['WALK', 'FLY']) {
test(`凯尔希·思衡托 S2 opens for a selectable ${motion} enemy only in the expanded range, without an injured ally`, () => {
const h = makeBattle({ autoFinish: false,
defs: { enemies: { target: enemyRec({ key: 'target', motion, hp: 1e8, atk: 0, speed: 0 }) } },
units: [{ uid: 1, diy: { slot: 6, charId: 'char_1052_kalts2', skillIndex: 1 }, elite: true, row: 10, col: 4 }] });
h.step();const u = h.unit(1);u.skill.gainSp(999);h.run(0.5);assert.equal(u.skill.activations, 0);
const e = h.spawn('target', { pos: [10, 7] });h.b.applyStatus(e, 'sleep', { duration: 1 });
assert.equal(h.b.enemiesInKeys(u.baseRangeKeys, u, { canHitFly: true }).length, 0);
h.run(0.5);assert.equal(u.skill.activations, 0, 'sleep cannot open the mixed skill');
assert.ok(h.runUntil(() => u.skill.active, 1));
assert.equal(u.skill.activations, 1);checkInvariants(h.b);assert.deepEqual(h.b.errors, []);
});
}
+35 -35
View File
@@ -2394,38 +2394,38 @@
"round": 13,
"seed": 4104625554,
"waveId": "act1autochess_h06",
"time": 213.53,
"ticks": 6406,
"time": 224.2,
"ticks": 6726,
"reason": "cleared",
"killed": 54,
"killed": 60,
"total": 96,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 0,
"errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":54,"total":96,"perfect":false,"leaks":42,"leaksUncounted":0,"coins":0,"dmg":325631,"bossDmg":0,"heal":201962,"deaths":14,"layerGains":{}}},
"players": {"p1":{"killed":60,"total":96,"perfect":false,"leaks":36,"leaksUncounted":0,"coins":0,"dmg":328165,"bossDmg":0,"heal":215296,"deaths":15,"layerGains":{}}},
"units": [
["p1",1,"op","chess_char_5_diy1_b",0,"uniequip_002_shu",0,0,6,3,28220,22841,8556,68,3],
["p1",2,"op","chess_char_5_diy2_b",1,"uniequip_002_shu",0,0,4,0,3764,7244,15696,24,1],
["p1",3,"op","chess_char_5_diy1_b",2,"uniequip_002_shu",0,0,7,3,36219,18295,24201,93,2],
["p1",4,"op","chess_char_5_diy2_b",0,"uniequip_003_shu",0,0,9,8,29213,25714,7056,77,1],
["p1",5,"op","chess_char_5_diy1_b",1,"uniequip_003_shu",0,0,11,8,40747,35870,26790,94,1],
["p1",6,"op","chess_char_5_diy2_b",2,"uniequip_003_shu",0,0,13,9,55531,31153,19129,132,1],
["p1",7,"op","chess_char_6_diy1_a",0,null,0,0,105,4,26549,22529,10129,96,5],
["p1",8,"op","chess_char_6_diy2_a",1,null,0,0,94,2,12140,19351,10447,60,1],
["p1",9,"op","chess_char_6_diy1_a",2,null,0,0,5,3,15284,4893,22539,66,2],
["p1",10,"op","chess_char_6_diy2_b",0,"none",0,0,8,0,3357,5468,2925,12,1],
["p1",11,"op","chess_char_6_diy1_b",1,"none",0,0,10,4,20997,29429,30955,60,1],
["p1",12,"op","chess_char_6_diy2_b",2,"none",3421,1,114,5,53610,24940,23541,116,2],
["p1",1,"op","chess_char_5_diy1_b",0,"uniequip_002_shu",2027.59,1,116,4,28271,24195,6202,68,1],
["p1",2,"op","chess_char_5_diy2_b",1,"uniequip_002_shu",0,0,4,0,1711,23126,33329,30,2],
["p1",3,"op","chess_char_5_diy1_b",2,"uniequip_002_shu",0,0,7,12,35861,22460,27238,104,2],
["p1",4,"op","chess_char_5_diy2_b",0,"uniequip_003_shu",0,0,9,3,39020,25122,12684,97,4],
["p1",5,"op","chess_char_5_diy1_b",1,"uniequip_003_shu",0,0,11,11,48461,41096,28378,112,1],
["p1",6,"op","chess_char_5_diy2_b",2,"uniequip_003_shu",0,0,114,9,47371,22141,16394,112,1],
["p1",7,"op","chess_char_6_diy1_a",0,null,0,0,103,5,24150,24095,7966,92,3],
["p1",8,"op","chess_char_6_diy2_a",1,null,2764.3,1,117,1,10276,17246,9358,51,1],
["p1",9,"op","chess_char_6_diy1_a",2,null,0,0,5,1,15673,5966,23558,68,2],
["p1",10,"op","chess_char_6_diy2_b",0,"none",3421,1,119,1,4178,7509,4920,14,2],
["p1",11,"op","chess_char_6_diy1_b",1,"none",3421,1,118,3,20756,28537,30597,59,1],
["p1",12,"op","chess_char_6_diy2_b",2,"none",3421,1,115,6,52439,24767,14672,123,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_1229_darmy":[27,17,10,37119,84479],"enemy_1229_darmy_2":[57,37,20,116481,226930],"enemy_9007_acelem":[12,0,12,75361,111113]},
"hooks": [1,114,6406,1483,1483,4018,4279,24024,68,110,21,21,0,15323,0,130,96,42,1,68,0,40,0],
"events": {"atk":1530,"deploy":15,"die":68,"dmg":4279,"engage":12,"fx":129,"heal":304,"leak":42,"skill":42,"spawn":111,"status":104},
"fx": {"beam":18,"burst":40,"healField":35,"teleport":36},
"snaps": "caea0314 70d99341 a190ed01 c66b7d44 cc4aa097 1a3e11e7 b9309122 fcf78826 cd12b374 81495da7 877f63e5 87ffd853 2c5e098e 7fa84dec 5d10862d d52e4b5d b483e93a 33405189 e58dd069 11d14bc3 7d55f750 e98d98ef"
"enemies": {"enemy_1229_darmy":[27,22,5,39408,94374],"enemy_1229_darmy_2":[57,38,19,128503,234099],"enemy_9007_acelem":[12,0,12,77871,113345]},
"hooks": [1,119,6726,1553,1553,4410,4689,25170,75,111,22,21,0,17046,0,126,96,36,1,75,0,44,0],
"events": {"atk":1612,"deploy":20,"die":75,"dmg":4689,"engage":12,"fx":131,"heal":320,"leak":36,"skill":43,"spawn":111,"status":111},
"fx": {"beam":23,"burst":44,"healField":36,"teleport":28},
"snaps": "caea0314 cba33632 f90e1b1d 5e82f1ff 929abe9c 394a1be8 5e0e99e5 eab4fee5 adfb57e8 aad6dce8 05e8f51c 3738b6d0 721871ea b6845c30 4ce6f2df 47cfe53a 1562c368 1bcdcadc 929a1dfb a34d9a94 d6a40dfd b6073386 8f66b944"
},
"diy-060": {
"about": "6_diy1_b=shu/S1/002_shu 6_diy2_b=shu/S2/002_shu 6_diy1_b=shu/S3/002_shu 6_diy2_b=shu/S1/003_shu 6_diy1_b=shu/S2/003_shu 6_diy2_b=shu/S3/003_shu 5_diy1_a=pallas/S1 5_diy2_a=pallas/S2 5_diy1_a=pallas/S3 5_diy2_b=pallas/S1 5_diy1_b=pallas/S2 5_diy2_b=pallas/S3",
@@ -5769,38 +5769,38 @@
"round": 13,
"seed": 953190138,
"waveId": "act1autochess_h06",
"time": 229,
"ticks": 6870,
"reason": "cleared",
"killed": 0,
"time": 230,
"ticks": 6900,
"reason": "timeout",
"killed": 97,
"total": 123,
"unspawned": 0,
"bossHpLeft": null,
"rngDraws": 0,
"errors": {"count":0,"kinds":[]},
"players": {"p1":{"killed":0,"total":123,"perfect":false,"leaks":123,"leaksUncounted":0,"coins":0,"dmg":0,"bossDmg":0,"heal":0,"deaths":0,"layerGains":{}}},
"players": {"p1":{"killed":97,"total":123,"perfect":false,"leaks":26,"leaksUncounted":0,"coins":0,"dmg":3464054,"bossDmg":0,"heal":0,"deaths":0,"layerGains":{}}},
"units": [
["p1",1,"op","chess_char_5_diy1_a",0,null,1991.99,1,4,0,0,0,0,0,0],
["p1",2,"op","chess_char_5_diy2_a",1,null,1991.99,1,5,0,0,0,0,0,0],
["p1",2,"op","chess_char_5_diy2_a",1,null,1991.99,1,5,10,602379,0,0,40,4],
["p1",3,"op","chess_char_5_diy1_a",2,null,1991.99,1,11,0,0,0,0,0,0],
["p1",4,"op","chess_char_5_diy2_b",0,"none",2206.49,1,13,0,0,0,0,0,0],
["p1",5,"op","chess_char_5_diy1_b",1,"none",2206.49,1,6,0,0,0,0,0,0],
["p1",5,"op","chess_char_5_diy1_b",1,"none",2206.49,1,6,26,1100819,0,0,30,3],
["p1",6,"op","chess_char_5_diy2_b",2,"none",2206.49,1,14,0,0,0,0,0,0],
["p1",7,"op","chess_char_6_diy1_a",0,null,1991.99,1,7,0,0,0,0,0,0],
["p1",8,"op","chess_char_6_diy2_a",1,null,1991.99,1,9,0,0,0,0,0,0],
["p1",8,"op","chess_char_6_diy2_a",1,null,1991.99,1,9,16,531690,0,0,40,4],
["p1",9,"op","chess_char_6_diy1_a",2,null,1991.99,1,10,0,0,0,0,0,0],
["p1",10,"op","chess_char_6_diy2_b",0,"none",2206.49,1,12,0,0,0,0,0,0],
["p1",11,"op","chess_char_6_diy1_b",1,"none",2206.49,1,15,0,0,0,0,0,0],
["p1",11,"op","chess_char_6_diy1_b",1,"none",2206.49,1,15,45,1229167,0,0,40,4],
["p1",12,"op","chess_char_6_diy2_b",2,"none",2206.49,1,8,0,0,0,0,0,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,21,0,0],"enemy_1043_zomsbr":[66,0,66,0,0],"enemy_1402_tgshd_2":[36,0,36,0,0]},
"hooks": [1,138,6870,0,0,0,0,0,0,123,0,0,0,8040,0,0,123,123,1,0,0,0,0],
"events": {"deploy":12,"fx":72,"leak":123,"spawn":138},
"fx": {"shield":60,"takeoff":12},
"snaps": "73ed7f8a 7b620bb2 4d91bff8 2b838d89 4e28c0a4 4d3057b7 dd413269 d9e2dba6 2f326b81 83e4122d e47cd4da b766c561 ea452dcc 749d7006 6e97bed7 aeb7002d a0cf0a74 38ae1555 f0426635 2ddc66fa 83570095 8351e611 7abf7fb8"
"enemies": {"enemy_1006_shield_3":[21,5,11,0,1729904],"enemy_1043_zomsbr":[66,64,2,0,484262],"enemy_1402_tgshd_2":[36,28,3,0,1249889]},
"hooks": [1,138,6900,150,150,988,988,6824,97,113,15,15,150,20923,891,0,123,16,1,97,0,0,0],
"events": {"atk":150,"deploy":12,"die":97,"dmg":988,"engage":4,"fx":744,"leak":16,"skill":30,"spawn":138,"status":246},
"fx": {"aoe":149,"expose":359,"healAoe":149,"shield":75,"takeoff":12},
"snaps": "73ed7f8a ad542748 3ff80098 ae6a8ec3 59598083 93567826 ed19865f e6485e06 aa8d6124 079e6a4e 392585c2 28570fb0 7bfd39c4 1fddcff9 9573a597 de3a6b22 d1d3361c 838f78dd e71a4502 a1225cf5 639d3bd5 6e01ec4b f97fa410"
},
"diy-134": {
"about": "5_diy1_a=monstr/S1 5_diy2_a=monstr/S2 5_diy1_a=monstr/S3 5_diy2_b=monstr/S1 5_diy1_b=monstr/S2 5_diy2_b=monstr/S3 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",