Golden regenerated on the merged code and attributed by reverting each gameplay PR in a scratch tree: #66 alone moves the fights of roster-008/010/012/022/027/028/031/040, the boss pairs (boss_2/4/6, hidden boss_10) and the matches coop2-NORMAL-3, coop2-HARD-4, coop3-ABYSS-5, coop4-ABYSS-7, solo-HARD-9-boosted (灵巧's aura no longer comes from merchant exits and retreats; hidden-layer bonds keep their internal layers); #109 changes the skill state of its deploy-timed skills (units / hooks / status events / snapshots); #160 adds the 0.5 s skill events of deploy passives (events.skill only in roster-001/006/025/049). The remaining scenarios listed by the full regenerate are those three effects together.
This commit is contained in:
commit
02b76989fa
49 files changed
+1974
-620
No files matched your search
@@ -71,6 +71,28 @@ test('迅捷: member skill end +12 SP (p=1 at high L), ≥40 layers every operat
|
||||
assert.equal(off.s_x, 0);
|
||||
});
|
||||
|
||||
test('迅捷 / 突袭: SP gifts after end do not recharge a zero-SP deployment skill or trigger a ready raid', () => {
|
||||
const h = makeBattle({
|
||||
defs: { chess: { t_deploy: chessRec({ id: 't_deploy', bonds: ['swiftShip', 'raidShip'], skill: { spCost: 0 } }) } },
|
||||
units: [{ chessId: 't_deploy', row: 10, col: 4 }],
|
||||
bonds: { swiftShip: bond(1, 1000), raidShip: bond(1) }, captureNoisy: true,
|
||||
kits: { t_deploy: () => ({ skill: {
|
||||
kind: 'duration', activateOnDeploy: true, duration: 2, spCost: 0, spType: 'none', trigger: 'NEVER',
|
||||
} }) },
|
||||
});
|
||||
h.b.start();
|
||||
const u = h.unit('t_deploy'), seq = u.deploySeq;
|
||||
h.run(5);
|
||||
assert.deepEqual(h.hooksOf('skillEnd').map((c) => c.reason), ['duration']);
|
||||
assert.ok(h.hooksOf('spGain').some((c) => c.unit === u), 'swift end handler attempted SP gain');
|
||||
assert.equal(u.skill.sp, 0);
|
||||
assert.equal(u.skill.charges, 0);
|
||||
assert.equal(u.skill.ready, false);
|
||||
assert.equal(u.skill.activations, 1);
|
||||
assert.equal(u.deploySeq, seq, 'skill end alone does not meet raid readiness');
|
||||
checkInvariants(h.b);
|
||||
});
|
||||
|
||||
test('迅捷 / 不屈 proc chances: p = min(1, base + per·L) at each layer count', () => {
|
||||
const sw = bondBb('swiftShip'), ind = bondBb('indomShip');
|
||||
close(procChance(sw, 0), 0.20);
|
||||
@@ -83,7 +105,7 @@ test('迅捷 / 不屈 proc chances: p = min(1, base + per·L) at each layer coun
|
||||
close(procChance(ind, 1000), 1, 'capped');
|
||||
});
|
||||
|
||||
test('灵巧: aura on members + 4 neighbours (once), 8 tiles at ≥40 layers, follows death and relocation', () => {
|
||||
test('灵巧: aura on members + 4 neighbours (once), 8 tiles at ≥40 layers, survives death and follows relocation', () => {
|
||||
const defs = { chess: { k_1: op('k_1', ['skillfulShip']), k_2: op('k_2', ['skillfulShip']), k_n: op('k_n', []), k_d: op('k_d', []), k_f: op('k_f', []) } };
|
||||
const h = makeBattle({
|
||||
defs, bonds: { skillfulShip: bond(1, 5) },
|
||||
@@ -107,7 +129,117 @@ test('灵巧: aura on members + 4 neighbours (once), 8 tiles at ≥40 layers, fo
|
||||
assert.equal(a('k_n'), 100, 'relocated out of the aura');
|
||||
h.b.dealDamage(null, h.unit('k_1'), { amount: 1e9, type: 'true' });
|
||||
h.step(2);
|
||||
assert.equal(a('k_d'), 100, 'aura source died');
|
||||
assert.equal(a('k_d'), 150, 'downed aura source still buffs neighbours');
|
||||
checkInvariants(h.b);
|
||||
});
|
||||
|
||||
for (const layers of [5, 40]) {
|
||||
test(`灵巧: downed sources keep one aura at ${layers} layers, update live layers and restore it on redeploy`, () => {
|
||||
const defs = { chess: {
|
||||
k_1: op('k_1', ['skillfulShip']), k_2: op('k_2', ['skillfulShip']),
|
||||
k_n: op('k_n', []), k_d: op('k_d', []),
|
||||
} };
|
||||
const h = makeBattle({
|
||||
defs, bonds: { skillfulShip: bond(1, layers) },
|
||||
units: [
|
||||
{ chessId: 'k_1', row: 10, col: 4 }, { chessId: 'k_2', row: 10, col: 6 },
|
||||
{ chessId: 'k_n', row: 10, col: 5 }, { chessId: 'k_d', row: 11, col: 5 },
|
||||
],
|
||||
});
|
||||
h.step();
|
||||
const first = h.unit('k_1'), second = h.unit('k_2'), neighbour = h.unit('k_n'), diagonal = h.unit('k_d');
|
||||
assert.equal(neighbour.s.aspd, 110 + layers, 'overlapping sources buff once');
|
||||
h.b.kill(first);
|
||||
assert.ok(h.b.isDown(first));
|
||||
assert.equal(neighbour.s.aspd, 110 + layers, 'no attack speed drop on death');
|
||||
h.b.kill(second);
|
||||
assert.ok(h.b.isDown(second));
|
||||
h.run(1);
|
||||
assert.equal(neighbour.s.aspd, 110 + layers, 'all sources down: polling keeps the aura');
|
||||
assert.equal(diagonal.s.aspd, layers >= 40 ? 110 + layers : 100, '4 / 8 tile range still applies');
|
||||
assert.equal(neighbour.buffs.filter((b) => b.key === 'bond:skillfulShip').length, 1);
|
||||
|
||||
gainLayers(h.b, { playerId: 'p1', bonds: 'skillfulShip', n: 40 - layers + 2 });
|
||||
h.step(2);
|
||||
assert.equal(neighbour.s.aspd, 152, 'downed sources use live layer values');
|
||||
assert.equal(diagonal.s.aspd, 152, 'downed sources widen their aura at 40 layers');
|
||||
h.b.kill(neighbour);
|
||||
assert.ok(h.b.redeploy(neighbour, { free: true }));
|
||||
assert.equal(neighbour.s.aspd, 152, 'recipient redeployed next to a downed source');
|
||||
assert.ok(h.b.redeploy(first, { free: true }));
|
||||
assert.equal(first.s.aspd, 152, 'source regains its own bonus on redeploy');
|
||||
assert.equal(neighbour.s.aspd, 152, 'redeploy does not stack the aura');
|
||||
h.b.retreat(first);
|
||||
assert.equal(neighbour.s.aspd, 152, 'remaining downed source still covers the recipient');
|
||||
assert.ok(h.b.redeploy(second, { free: true }));
|
||||
h.b.retreat(second);
|
||||
assert.equal(neighbour.s.aspd, 100, 'withdrawn sources no longer provide an aura');
|
||||
checkInvariants(h.b);
|
||||
});
|
||||
}
|
||||
|
||||
test('灵巧: a downed source provides its aura around the body tile when it returns home after death', () => {
|
||||
const defs = { chess: {
|
||||
k_1: op('k_1', ['skillfulShip']), k_2: op('k_2', ['skillfulShip']),
|
||||
k_home: op('k_home', []), k_away: op('k_away', []), k_tile: op('k_tile', []),
|
||||
} };
|
||||
const h = makeBattle({
|
||||
defs, bonds: { skillfulShip: bond(1, 5) },
|
||||
units: [
|
||||
{ chessId: 'k_1', row: 10, col: 4 }, { chessId: 'k_2', row: 12, col: 3 },
|
||||
{ chessId: 'k_home', row: 10, col: 5 }, { chessId: 'k_away', row: 10, col: 8 },
|
||||
{ chessId: 'k_tile', row: 10, col: 7 },
|
||||
],
|
||||
});
|
||||
h.step();
|
||||
h.b.retreat(h.unit('k_tile'), { permanent: true });
|
||||
assert.ok(h.b.relocate(h.unit('k_1'), 10, 7));
|
||||
h.run(0.4);
|
||||
assert.equal(h.unit('k_home').s.aspd, 100);
|
||||
assert.equal(h.unit('k_away').s.aspd, 115);
|
||||
h.b.kill(h.unit('k_1'));
|
||||
assert.deepEqual(h.unit('k_1').body, [10, 4]);
|
||||
assert.equal(h.unit('k_home').s.aspd, 115, 'aura follows the downed body back home');
|
||||
assert.equal(h.unit('k_away').s.aspd, 100, 'last living tile no longer provides an aura');
|
||||
checkInvariants(h.b);
|
||||
});
|
||||
|
||||
for (const reason of ['retreat', 'forcedExit', 'merchant']) {
|
||||
test(`灵巧: ${reason} leaves a down model without preserving the killed-source aura`, () => {
|
||||
const h = makeBattle({
|
||||
defs: { chess: { k_1: op('k_1', ['skillfulShip']), k_n: op('k_n', []) } },
|
||||
bonds: { skillfulShip: bond(1, 5) },
|
||||
units: [{ chessId: 'k_1', row: 10, col: 4 }, { chessId: 'k_n', row: 10, col: 5 }],
|
||||
});
|
||||
h.step();
|
||||
const source = h.unit('k_1'), recipient = h.unit('k_n');
|
||||
assert.equal(recipient.s.aspd, 115);
|
||||
h.b.retreat(source, { reason });
|
||||
assert.ok(h.b.isDown(source), 'a down model is not proof of being killed');
|
||||
assert.equal(source.removeReason, reason);
|
||||
assert.equal(recipient.s.aspd, 100, 'aura stops immediately on retreat');
|
||||
assert.ok(h.b.redeploy(source, { free: true }));
|
||||
assert.equal(recipient.s.aspd, 115, 'the living source restores its aura');
|
||||
h.b.kill(source);
|
||||
assert.equal(recipient.s.aspd, 115, 'a subsequent real kill keeps the aura');
|
||||
checkInvariants(h.b);
|
||||
});
|
||||
}
|
||||
|
||||
test('灵巧: unite carry-down forcedExit starts without an aura and restores it on redeploy', () => {
|
||||
const h = makeBattle({
|
||||
kind: 'unite',
|
||||
defs: { chess: { k_1: op('k_1', ['skillfulShip']), k_n: op('k_n', []) } },
|
||||
bonds: { skillfulShip: bond(1, 40) },
|
||||
units: [{ chessId: 'k_1', row: 10, col: 4, carryState: { down: true } }, { chessId: 'k_n', row: 11, col: 5 }],
|
||||
});
|
||||
h.step();
|
||||
const source = h.unit('k_1'), recipient = h.unit('k_n');
|
||||
assert.equal(source.removeReason, 'forcedExit');
|
||||
assert.ok(h.b.isDown(source));
|
||||
assert.equal(recipient.s.aspd, 100, 'forcedExit is not a kill in this battle');
|
||||
assert.ok(h.b.redeploy(source, { free: true }));
|
||||
assert.equal(recipient.s.aspd, 150, 'the redeployed source covers all eight neighbours');
|
||||
checkInvariants(h.b);
|
||||
});
|
||||
|
||||
|
||||
@@ -165,6 +165,24 @@ test('炎国短刀: each skill activation ATK +5 % / +8 % (one multiplier), at m
|
||||
}
|
||||
});
|
||||
|
||||
test('炎国短刀: timed deployment skill adds one stack per deployment, including carry.skillActive', () => {
|
||||
const id = 'chess_char_1_19_a';
|
||||
for (const [item, per] of [[A('3_04'), 0.05], [B('3_04'), 0.08]]) {
|
||||
const h = makeBattle({ units: [{ chessId: id, row: 10, col: 4, skillIndex: 0, items: [item], carryState: { skillActive: true } }] });
|
||||
h.b.start();
|
||||
const u = h.unit(id);
|
||||
assert.equal(u.skill.activations, 1);
|
||||
close(u.s.atk, u.base.atk * (1 + per), 'one deployment stack');
|
||||
h.run(u.skill.duration + 1);
|
||||
close(u.s.atk, u.base.atk * (1 + per), 'no extra stack on end');
|
||||
h.b.retreat(u);
|
||||
assert.ok(h.b.redeploy(u, { free: true }));
|
||||
assert.equal(u.skill.activations, 2);
|
||||
close(u.s.atk, u.base.atk * (1 + per * 2), 'one more on redeployment');
|
||||
checkInvariants(h.b);
|
||||
}
|
||||
});
|
||||
|
||||
test('迅捷作战粮: on deploy SP +3 / +6, plus as much per other operator sharing a bond', () => {
|
||||
for (const [id, each] of [[A('3_05'), 3], [B('3_05'), 6]]) {
|
||||
const ops = { t_op: op('t_op', { bonds: ['swiftShip'] }), t_a: op('t_a', { bonds: ['swiftShip'] }), t_b: op('t_b', { bonds: ['yanShip'] }) };
|
||||
|
||||
@@ -444,6 +444,8 @@ test('1_18 宴 S1 分神: no attacks, block 0 (releases), DEF +def, regenerates
|
||||
assert.equal(u.s.blockCnt, 0);
|
||||
assert.equal(u.blocking.length, 0);
|
||||
assert.equal(e.blockedBy, null, 'blocked enemy released');
|
||||
h.b.dealDamage(u, e, { amount: 100, type: 'phys', isAttack: true });
|
||||
assert.equal(dealt(h, u).at(-1).type, 'phys', 'active S1 does not inherit S2 arts conversion');
|
||||
approx(u.s.def, u.base.def * (1 + bb.def), `${id} DEF`);
|
||||
approx(u.s.hpRegen, u.s.maxHp * bb.hp_recovery_per_sec_by_max_hp_ratio, 'regen');
|
||||
u.hp = u.s.maxHp * 0.3;
|
||||
@@ -462,23 +464,71 @@ test('1_18 宴 S1 分神: no attacks, block 0 (releases), DEF +def, regenerates
|
||||
}
|
||||
});
|
||||
|
||||
test('1_19 野鬃 S1 骑枪刺击 (PASSIVE): ASPD +attack_speed for `duration` s after every deployment', () => {
|
||||
test('1_18 宴 S2: deployment loses current HP once; ATK and normal-attack arts conversion end with the duration', () => {
|
||||
for (const id of pair('18')) {
|
||||
const s = rec(id, 'skchr_utage_2'), bb = s.bb;
|
||||
const h = run({ defs: { chess: noGarrison(id), enemies: { e: dummy('e') } },
|
||||
units: [entry(id, s.skillId, { row: 10, col: 4, carryState: { hpPct: 0.5, skillActive: true } })] });
|
||||
h.b.start();
|
||||
const u = h.unit(id);
|
||||
assert.equal(u.skill.kind, 'duration');
|
||||
assert.equal(u.skill.activations, 1);
|
||||
assert.equal(u.skill.charges, 0);
|
||||
approx(u.hp, u.s.maxHp * 0.5 * (1 - bb.hp_ratio), 'one loss from carried current HP');
|
||||
approx(u.s.atk, u.base.atk * (1 + bb.atk));
|
||||
const e = h.spawn('e', { pos: [10, 8] });
|
||||
const hit = () => { h.b.dealDamage(u, e, { amount: 100, type: 'phys', isAttack: true }); return dealt(h, u).at(-1); };
|
||||
assert.equal(hit().type, 'arts');
|
||||
assert.ok(!hit().dmg.isSkill, 'normal attack marker retained');
|
||||
h.run(bb.duration / 2);
|
||||
approx(u.skill.timeLeft, bb.duration / 2);
|
||||
h.run(bb.duration / 2 + 0.1);
|
||||
assert.equal(u.skill.active, false);
|
||||
assert.equal(u.skill.ready, false);
|
||||
approx(u.s.atk, u.base.atk);
|
||||
assert.equal(hit().type, 'phys');
|
||||
assert.deepEqual(ended(h, u).map((c) => c.reason), ['duration']);
|
||||
const hp = u.hp;
|
||||
h.run(3);
|
||||
approx(u.hp, hp, 'no second loss while staying deployed');
|
||||
h.b.retreat(u);
|
||||
assert.ok(h.b.redeploy(u, { free: true }));
|
||||
assert.equal(u.skill.activations, 2);
|
||||
approx(u.hp, u.s.maxHp * (1 - bb.hp_ratio), 'new deployment loses current HP once');
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
|
||||
test('1_19 野鬃 S1 骑枪刺击: ASPD +attack_speed for `duration` s after every deployment', () => {
|
||||
for (const id of pair('19')) {
|
||||
const s = rec(id, 'skchr_wildmn_1');
|
||||
const h = run({ defs: { chess: noGarrison(id) }, units: [entry(id, s.skillId, { row: 9, col: 5 })] });
|
||||
const u = sel(h, id, s.skillId);
|
||||
assert.equal(u.skill.kind, 'passive');
|
||||
h.step();
|
||||
h.b.start();
|
||||
assert.equal(u.skill.kind, 'duration');
|
||||
assert.equal(u.skill.active, true);
|
||||
assert.equal(u.skill.activations, 1);
|
||||
assert.equal(u.skill.charges, 0);
|
||||
assert.equal(u.skill.ready, false);
|
||||
assert.equal(started(h, u)[0].reason, 'deploy');
|
||||
approx(u.s.aspd, u.base.aspd + s.bb.attack_speed, `${id} ASPD after deployment`);
|
||||
h.run(s.duration - 0.5);
|
||||
h.run(s.duration / 2);
|
||||
approx(u.skill.timeLeft, s.duration / 2);
|
||||
assert.deepEqual(h.snapshot().units.find((t) => t[0] === u.id).slice(5, 7), [s.duration / 2, s.duration]);
|
||||
h.run(s.duration / 2 - 0.5);
|
||||
approx(u.s.aspd, u.base.aspd + s.bb.attack_speed, 'still up');
|
||||
h.run(1);
|
||||
approx(u.s.aspd, u.base.aspd, `expired after ${s.duration} s`);
|
||||
assert.equal(u.skill.active, false);
|
||||
assert.equal(u.skill.ready, false);
|
||||
assert.deepEqual(ended(h, u).map((c) => c.reason), ['duration']);
|
||||
h.b.kill(u, null);
|
||||
h.step();
|
||||
assert.ok(h.b.redeploy(u, { free: true }));
|
||||
h.step();
|
||||
approx(u.s.aspd, u.base.aspd + s.bb.attack_speed, 'again after the redeployment');
|
||||
assert.equal(u.skill.activations, 2);
|
||||
assert.equal(ended(h, u).length, 1, 'expired skill does not end again on death');
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -342,12 +342,20 @@ test('2_12 砾 S1 影袭: at deployment DEF +def decaying to 0 over `duration` s
|
||||
h.run(0.5);
|
||||
approx(u.s.def, u.base.def * (1 + aura + bb.def), `${id} full bonus`);
|
||||
assert.equal(u.s.shield, 0, 'no 鼠群 barrier');
|
||||
assert.equal(u.skill.kind, 'duration');
|
||||
assert.equal(u.skill.active, true);
|
||||
assert.equal(u.skill.ready, false);
|
||||
assert.equal(h.snapshot().units.find((t) => t[0] === u.id)[6], bb.duration);
|
||||
h.run(1);
|
||||
approx(u.s.def, u.base.def * (1 + aura + bb.def * (bb.duration - 1) / bb.duration), 'one step down after 1 s');
|
||||
h.run(0.4);
|
||||
approx(u.s.def, u.base.def * (1 + aura + bb.def * (bb.duration - 1) / bb.duration), 'steps once per second');
|
||||
h.run(bb.duration);
|
||||
approx(u.s.def, u.base.def * (1 + aura), 'gone');
|
||||
assert.equal(u.skill.active, false);
|
||||
assert.equal(u.skill.ready, false);
|
||||
assert.equal(u.findBuff('gravel:shadow'), null);
|
||||
assert.equal(h.hooksOf('skillEnd').filter((c) => c.unit === u && c.reason === 'duration').length, 1);
|
||||
done(h);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -368,19 +368,34 @@ test('3_05 斯卡蒂 S1 迅捷打击·γ型 ATK/ASPD; S2 跃浪击 ATK + for `du
|
||||
const g = makeBattle({ timeLimit: 60, units: [U(id, 'skchr_skadi_2', 10, 4)] });
|
||||
const v = g.unit(id);
|
||||
g.step();
|
||||
assert.equal(v.skill.kind, 'passive');
|
||||
assert.equal(v.skill.kind, 'duration');
|
||||
assert.equal(v.skill.active, true);
|
||||
assert.equal(v.skill.charges, 0);
|
||||
assert.equal(v.skill.ready, false);
|
||||
assert.equal(g.hooksOf('skillStart')[0].reason, 'deploy');
|
||||
approx(v.s.atk, v.base.atk * (1 + b2.atk + own));
|
||||
if (id.endsWith('_b')) {
|
||||
const left = v.skill.timeLeft;
|
||||
g.b.dealDamage(null, v, { type: 'true', amount: 1e7 });
|
||||
assert.equal(v.alive, true, 'DRE-Y revives in place during the window');
|
||||
assert.equal(v.skill.activations, 1);
|
||||
approx(v.skill.timeLeft, left, 1e-9, 'revival preserves the remaining skill time');
|
||||
}
|
||||
g.run(b2.duration - 0.5);
|
||||
approx(v.s.atk, v.base.atk * (1 + b2.atk + own));
|
||||
g.run(1);
|
||||
approx(v.s.atk, v.base.atk * (1 + own), 1e-6, 'gone after its duration');
|
||||
assert.equal(v.skill.active, false);
|
||||
assert.equal(v.skill.ready, false);
|
||||
assert.deepEqual(g.hooksOf('skillEnd').map((c) => c.reason), ['duration']);
|
||||
// redeployed ⇒ again
|
||||
g.b.dealDamage(null, v, { type: 'true', amount: 1e7 });
|
||||
if (!v.alive) {
|
||||
g.b.redeploy(v, { free: true });
|
||||
g.step();
|
||||
approx(v.s.atk, v.base.atk * (1 + b2.atk + own), 1e-6, 'every deployment');
|
||||
}
|
||||
assert.equal(v.skill.activations, 1, 'DRE-Y in-place revival does not deploy or restart S2');
|
||||
if (v.alive) g.b.retreat(v);
|
||||
assert.ok(g.b.redeploy(v, { free: true }));
|
||||
approx(v.skill.timeLeft, b2.duration);
|
||||
assert.equal(v.skill.activations, 2);
|
||||
approx(v.s.atk, v.base.atk * (1 + b2.atk + own), 1e-6, 'every deployment');
|
||||
done(g);
|
||||
}
|
||||
// DRE-X: ×atk_scale on blocked enemies; no DRE-Y revive
|
||||
|
||||
@@ -265,6 +265,13 @@ test('伊内丝 S3 独影归途: first deployment leaves a sentry and retreats;
|
||||
const sk = D(id, 2).skill, bb = sk.bb;
|
||||
const h = battle([U(id, 10, 5, 2)], { flags: { dpInit: 99, dpPerSec: 0 } });
|
||||
const u = h.unit(id);
|
||||
h.b.start();
|
||||
assert.equal(u.skill.kind, 'duration');
|
||||
assert.equal(u.skill.active, false, 'first deployment does not activate the skill');
|
||||
assert.equal(u.skill.activations, 0);
|
||||
assert.equal(h.hooksOf('skillStart').filter((c) => c.unit === u).length, 0, 'placing a sentry emits no skillStart');
|
||||
assert.equal(u.skill.timeLeft, 0, 'first deployment places the sentry without a full effect window');
|
||||
assert.equal(u.findBuff('ines:s3'), null);
|
||||
const near = [];
|
||||
for (let i = 0; i < 3; i++) near.push(h.spawn('enemy_dummy', { pos: [10, 6] }), h.spawn('enemy_dummy', { pos: [10, 4] }));
|
||||
const far = h.spawn('enemy_dummy', { pos: [10, 8] });
|
||||
@@ -273,9 +280,14 @@ test('伊内丝 S3 独影归途: first deployment leaves a sentry and retreats;
|
||||
assert.ok(left && left.reason === 'retreat', `${id}: leaves right after the first deployment`);
|
||||
assert.ok(h.runUntil(() => u.alive && u.deployed, 2), 'redeploy timer refreshed: back at once');
|
||||
const buff = u.findBuff('ines:s3');
|
||||
assert.ok(buff, 'passive buff on the redeploy');
|
||||
assert.ok(buff, 'ATK effect on the redeploy');
|
||||
approx(buff.mods.atkPct, bb.atk, 1e-9);
|
||||
approx(buff.timeLeft, sk.duration, 0.1);
|
||||
approx(u.skill.timeLeft, sk.duration, 0.1);
|
||||
assert.equal(u.skill.active, true);
|
||||
assert.equal(u.skill.activations, 1);
|
||||
assert.equal(h.hooksOf('skillStart').filter((c) => c.unit === u).length, 1, 'the redeployment starts the skill once');
|
||||
assert.equal(u.skill.ready, false);
|
||||
assert.equal(h.snapshot().units.find((t) => t[0] === u.id)[6], sk.duration);
|
||||
const recall = dmgBy(h, u, tagged('sentryRecall'));
|
||||
assert.equal(recall.length, bb.max_target, `${id}: recall hits ≤ ${bb.max_target} of 6`);
|
||||
assert.ok(recall.every((c) => near.includes(c.target)) && !recall.some((c) => c.target === far));
|
||||
@@ -287,6 +299,15 @@ test('伊内丝 S3 独影归途: first deployment leaves a sentry and retreats;
|
||||
assert.ok(hits > 0);
|
||||
assert.equal(h.b.getPlayer('p1').dp, dpAt + hits * bb.cost, 'one DP per damage while the passive lasts');
|
||||
assert.equal(h.hooksOf('death').filter((c) => c.unit === u).length, 1, 'only the first deployment leaves');
|
||||
h.run(sk.duration);
|
||||
assert.equal(u.skill.active, false);
|
||||
assert.equal(u.skill.ready, false);
|
||||
assert.equal(u.findBuff('ines:s3'), null);
|
||||
const dpEnd = h.b.getPlayer('p1').dp;
|
||||
h.run(2);
|
||||
assert.equal(h.b.getPlayer('p1').dp, dpEnd, 'no skill DP after the actual window ends');
|
||||
assert.equal(h.hooksOf('death').filter((c) => c.unit === u).length, 1, 'stays deployed after the window');
|
||||
assert.deepEqual(h.hooksOf('skillEnd').filter((c) => c.unit === u).map((c) => c.reason), ['duration']);
|
||||
checkInvariants(h.b);
|
||||
}
|
||||
});
|
||||
@@ -611,10 +632,11 @@ test('缄默德克萨斯 S1 细雨无声: passive ATK up for its duration; hits
|
||||
const h = battle([U(id, 10, 4, 0)]);
|
||||
h.step();
|
||||
const u = h.unit(id);
|
||||
const buff = u.findBuff('texas2:drizzle');
|
||||
assert.ok(buff, `${id}: passive active on deployment`);
|
||||
approx(buff.mods.atkPct, bb.atk, 1e-9);
|
||||
assert.ok(u.findBuff('texas2:rainAtk'), 'talent 德克萨斯传统 ATK during the passive');
|
||||
assert.equal(u.skill.kind, 'duration');
|
||||
assert.equal(u.skill.active, true);
|
||||
assert.equal(u.skill.charges, 0);
|
||||
approx(u.skill.spec.mods.atkPct, bb.atk + D(id, 0).talents[0].bb.atk, 1e-9, 'skill and 德克萨斯传统 ATK');
|
||||
assert.equal(h.snapshot().units.find((t) => t[0] === u.id)[6], sk.duration);
|
||||
const e = h.spawn('enemy_dummy', { pos: [10, 5] });
|
||||
h.run(3);
|
||||
assert.ok(e.s.flags.silence, 'hit target silenced (失去特殊能力)');
|
||||
@@ -623,7 +645,10 @@ test('缄默德克萨斯 S1 细雨无声: passive ATK up for its duration; hits
|
||||
assert.ok(dot.length >= 2, `a DoT tick every second although she re-hits faster (${dot.length})`);
|
||||
for (const c of dot) approx(c.amount, bb['attack@texas2_s_1[dot].dot_damage'], 1e-9, 'fixed arts per second (RES 0)');
|
||||
h.run(sk.duration);
|
||||
assert.ok(!u.findBuff('texas2:drizzle'), 'ends with the skill duration');
|
||||
assert.equal(u.skill.active, false, 'ends with the skill duration');
|
||||
assert.equal(u.skill.ready, false);
|
||||
assert.equal(h.hooksOf('skillEnd').filter((c) => c.unit === u && c.reason === 'duration').length, 1);
|
||||
assert.ok(e.s.flags.silence && e.findBuff(`texas2:drizzleDot:${u.id}`), 'applied enemy effects keep their own duration');
|
||||
h.run(bb['attack@silence'] + 1);
|
||||
assert.ok(!e.s.flags.silence && !e.findBuff(`texas2:drizzleDot:${u.id}`), 'no new silence / DoT after the passive');
|
||||
}
|
||||
@@ -657,8 +682,8 @@ test('缄默德克萨斯 S2 阵雨连绵: deploy burst atk_scale arts + RES down
|
||||
approx(burst[0].amount, u.s.atk * bb.atk_scale * 0.5, 1e-6, 'arts burst vs RES 50');
|
||||
approx(e.s.res, 50 * (1 + bb.magic_resistance), 1e-6, 'RES down');
|
||||
assert.equal(far.s.res, 50);
|
||||
approx(u.findBuff('texas2:shower').mods.atkPct, bb.atk, 1e-9);
|
||||
approx(u.findBuff('texas2:shower').timeLeft, sk.duration, 1e-6);
|
||||
approx(u.skill.spec.mods.atkPct, bb.atk + D(id, 1).talents[0].bb.atk, 1e-9);
|
||||
approx(u.skill.timeLeft, sk.duration, 1e-6);
|
||||
h.run(2);
|
||||
const hits = dmgBy(h, u, (c) => c.dmg.isAttack);
|
||||
assert.ok(hits.length >= 2 && hits.every((c) => c.type === 'arts'), 'arts attacks');
|
||||
@@ -666,6 +691,8 @@ test('缄默德克萨斯 S2 阵雨连绵: deploy burst atk_scale arts + RES down
|
||||
for (const c of hits) per.set(c.dmg.attackId, (per.get(c.dmg.attackId) ?? 0) + 1);
|
||||
assert.ok([...per.values()].every((v) => v === 2), 'double hits');
|
||||
h.run(sk.duration);
|
||||
assert.equal(u.skill.active, false);
|
||||
assert.equal(u.skill.ready, false);
|
||||
const n0 = dmgBy(h, u, (c) => c.dmg.isAttack).length;
|
||||
h.run(3);
|
||||
const late = dmgBy(h, u, (c) => c.dmg.isAttack).slice(n0);
|
||||
@@ -673,6 +700,57 @@ test('缄默德克萨斯 S2 阵雨连绵: deploy burst atk_scale arts + RES down
|
||||
}
|
||||
});
|
||||
|
||||
test('缄默德克萨斯 S1/S2/S3: first kill reopens a full window, including a kill inside the deployment burst', () => {
|
||||
for (const id of pair('chess_char_4_16_a')) for (const index of [0, 1, 2]) {
|
||||
const life = [];
|
||||
const h = battle([U(id, 10, 4, index)], { extra: { setup(b) {
|
||||
for (const name of ['skillStart', 'skillEnd']) b.on(name, (c) => life.push([name, c.reason]), { priority: 1000 });
|
||||
} } });
|
||||
h.b.start();
|
||||
const u = h.unit(id), sk = u.skill;
|
||||
h.run(2);
|
||||
u.hp = u.s.maxHp / 2;
|
||||
const e = h.spawn('enemy_dummy', { pos: [10, 5] });
|
||||
h.b.kill(e, u);
|
||||
assert.equal(u.mem.texasKilled, true);
|
||||
approx(u.hpRatio, 1);
|
||||
assert.equal(u.findBuff('texas2:swordplay'), null);
|
||||
assert.equal(sk.activations, 2);
|
||||
approx(sk.timeLeft, sk.duration);
|
||||
assert.equal(sk.charges, 0);
|
||||
assert.equal(sk.ready, false);
|
||||
assert.deepEqual(h.hooksOf('skillEnd').map((c) => c.reason), ['recast']);
|
||||
h.b.kill(h.spawn('enemy_dummy', { pos: [10, 5] }), u);
|
||||
assert.equal(sk.activations, 2, 'only first kill recasts');
|
||||
h.run(sk.duration + 0.1);
|
||||
assert.equal(sk.active, false);
|
||||
assert.deepEqual(h.hooksOf('skillEnd').map((c) => c.reason), ['recast', 'duration']);
|
||||
assert.deepEqual(h.hooksOf('skillStart').map((c) => c.reason), ['deploy', 'kill']);
|
||||
h.b.retreat(u);
|
||||
const lifeAt = life.length;
|
||||
const weak = h.spawn('enemy_dummy', { pos: [10, 5] });
|
||||
weak.hp = 1;
|
||||
assert.ok(h.b.redeploy(u, { free: true }));
|
||||
if (index === 0) h.b.kill(weak, u); // S1 has no deployment burst
|
||||
assert.equal(u.mem.texasKilled, true);
|
||||
assert.equal(sk.activations, 4, 'new deployment and first-kill recast, including synchronous burst reentry');
|
||||
assert.deepEqual(life.slice(lifeAt), [['skillStart', 'deploy'], ['skillEnd', 'recast'], ['skillStart', 'kill']]);
|
||||
assert.equal(u.findBuff('texas2:swordplay'), null);
|
||||
approx(sk.timeLeft, sk.duration);
|
||||
if (index === 2) {
|
||||
const target = h.spawn('enemy_dummy', { pos: [10, 5] });
|
||||
h.run(sk.duration + 0.1);
|
||||
const waves = new Set(dmgBy(h, u, tagged('swordRain')).filter((c) => c.target === target).map((c) => c.t));
|
||||
assert.equal(waves.size, sk.duration, 'reentrant deployment keeps a full set of rain waves');
|
||||
const n = dmgBy(h, u, tagged('swordRain')).length;
|
||||
h.run(2);
|
||||
assert.equal(dmgBy(h, u, tagged('swordRain')).length, n, 'rain stops at skill end');
|
||||
assert.ok(target.alive);
|
||||
}
|
||||
checkInvariants(h.b);
|
||||
}
|
||||
});
|
||||
|
||||
test('缄默德克萨斯 module none: no ATK +10 % when alone (EXE-Y default has it)', () => {
|
||||
const has = (m) => { const h = battle([U('chess_char_4_16_b', 10, 4, null, m)]); h.run(0.5); return !!h.unit('chess_char_4_16_b').findBuff('texas2:module'); };
|
||||
assert.equal(has(null), true);
|
||||
|
||||
@@ -999,7 +999,11 @@ test('6_17 耀骑士临光 S2 逐夜烁光: on deploy ATK + and 3 护盾 layers
|
||||
const u = h.unit(id);
|
||||
usesSkill(u, sid);
|
||||
h.step();
|
||||
approx(u.findBuff('nearl2:night').mods.atkPct, bb.atk);
|
||||
approx(u.skill.spec.mods.atkPct, bb.atk);
|
||||
assert.equal(u.skill.kind, 'duration');
|
||||
assert.equal(u.skill.active, true);
|
||||
assert.equal(u.skill.ready, false);
|
||||
assert.equal(h.snapshot().units.find((t) => t[0] === u.id)[6], dur);
|
||||
assert.equal(u.findBuff('nearl2:shield').shieldHits, bb.times);
|
||||
h.run(bb.times * 1.2 + 1);
|
||||
const taken = h.hooksOf('damaged').filter((c) => c.target === u);
|
||||
@@ -1009,15 +1013,30 @@ test('6_17 耀骑士临光 S2 逐夜烁光: on deploy ATK + and 3 护盾 layers
|
||||
assert.ok(h.runUntil(() => !u.alive, dur + 1));
|
||||
approx(h.b.time, dur, 'withdraws when it ends', 0.05);
|
||||
approx(u.respawnAt - h.b.time, u.base.respawnTime * bb.respawn_time, 'this redeploy ×1.25', 1e-3);
|
||||
assert.equal(u.removeReason, 'retreat');
|
||||
assert.equal(u.skill.active, false);
|
||||
assert.deepEqual(h.hooksOf('skillEnd').filter((c) => c.unit === u).map((c) => c.reason), ['duration']);
|
||||
done(h);
|
||||
|
||||
const k = run({ defs: { chess: { kaz: plain('kaz', { bonds: ['kazimierzShip'] }) } }, units: [U(id, sid, 10, 4), { chessId: 'kaz', row: 12, col: 4 }] });
|
||||
const ku = k.unit(id);
|
||||
k.step(2);
|
||||
assert.ok(ku.alive && ku.findBuff('nearl2:night'));
|
||||
assert.ok(ku.alive && ku.skill.active);
|
||||
assert.ok(k.runUntil(() => !ku.alive, dur + 1));
|
||||
approx(ku.respawnAt - k.b.time, ku.base.respawnTime, '卡西米尔 before her: no extension', 1e-3);
|
||||
done(k);
|
||||
|
||||
const dead = run({ units: [U(id, sid, 10, 4, { moduleId: 'none' })] });
|
||||
dead.step();
|
||||
const du = dead.unit(id);
|
||||
dead.b.kill(du);
|
||||
const respawnAt = du.respawnAt;
|
||||
approx(respawnAt - dead.b.time, du.base.respawnTime, 'early death keeps ordinary redeployment');
|
||||
assert.equal(du.findBuff('nearl2:shield'), null);
|
||||
dead.run(dur + 1);
|
||||
assert.equal(du.respawnAt, respawnAt, 'no delayed retreat or multiplier after death');
|
||||
assert.deepEqual(dead.hooksOf('skillEnd').filter((c) => c.unit === du).map((c) => c.reason), ['death']);
|
||||
done(dead);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -942,6 +942,10 @@ test('2_12 砾: 鼠群 barrier hp_ratio × max HP decaying over `duration` s;
|
||||
assert.equal(u.base.cost, raw(id).stats.cost + t.cost);
|
||||
h.step();
|
||||
const full = u.s.maxHp * bb.hp_ratio;
|
||||
assert.equal(u.skill.kind, 'duration');
|
||||
assert.equal(u.skill.active, true);
|
||||
assert.equal(u.skill.ready, false);
|
||||
assert.deepEqual(h.snapshot().units.find((t) => t[0] === u.id).slice(5, 7), [Math.round(u.skill.timeLeft * 10) / 10, bb.duration]);
|
||||
approx(u.s.shield, full, 'full barrier', 1e-2);
|
||||
h.run(bb.duration / 2 + 0.5);
|
||||
approx(u.s.shield, full / 2, 'half decayed', 1e-2);
|
||||
@@ -949,6 +953,9 @@ test('2_12 砾: 鼠群 barrier hp_ratio × max HP decaying over `duration` s;
|
||||
approx(u.s.shield, full / 2, 'PRTS: the capacity steps once per second', 1e-2);
|
||||
h.run(bb.duration / 2);
|
||||
assert.equal(u.s.shield, 0);
|
||||
assert.equal(u.skill.active, false);
|
||||
assert.equal(u.skill.ready, false);
|
||||
assert.equal(u.findBuff('gravel:rats'), null);
|
||||
done(h);
|
||||
// PRTS "受到伤害后,剩余容量根据屏障最大容量等比变化": a damaged barrier shrinks in proportion, not to the capacity
|
||||
const hd = run({ units: [{ chessId: id, row: 9, col: 5 }] });
|
||||
@@ -960,6 +967,12 @@ test('2_12 砾: 鼠群 barrier hp_ratio × max HP decaying over `duration` s;
|
||||
hd.run(bb.duration / 2 + 0.5);
|
||||
approx(ud.s.shield, cap0 * 0.4 / 2, 'remaining × capacity ratio (a plain cap would keep 40 %)', 1e-2);
|
||||
assert.ok(ud.hp === ud.s.maxHp);
|
||||
hd.b.retreat(ud);
|
||||
assert.equal(ud.findBuff('gravel:rats'), null);
|
||||
assert.equal(ud.skill.active, false);
|
||||
assert.ok(hd.b.redeploy(ud, { free: true }));
|
||||
approx(ud.skill.timeLeft, bb.duration);
|
||||
approx(ud.s.shield, cap0, 'fresh shield on redeployment');
|
||||
done(hd);
|
||||
const idb = 'chess_char_2_12_b', tb = tal(idb);
|
||||
const cheapRec = chessRec({ id: 't_cheap', stats: { cost: tb['cond.cost'] } });
|
||||
|
||||
@@ -645,7 +645,8 @@ test('缄默德克萨斯 S3 (passive): deploy burst 2 × 115 % arts + 1.5 s stun
|
||||
assert.equal(waves.size, D(id).skill.duration, 'one rain per second while the passive lasts');
|
||||
assert.ok([...waves.values()].every((n) => n === bb.max_target), '≤ 2 distinct targets');
|
||||
approx(rain[0].amount, u.base.atk * (1 + t0.atk) * bb.atk_scale, 1e-6);
|
||||
assert.equal(u.findBuff('texas2:rainAtk'), null, 'ATK bonus ends with the passive');
|
||||
assert.equal(u.skill.active, false, 'ATK bonus ends with the skill');
|
||||
approx(u.s.atk, u.base.atk);
|
||||
});
|
||||
|
||||
test('缄默德克萨斯: first kill ⇒ full heal + recast; 剑术 buff removed', () => {
|
||||
@@ -660,7 +661,8 @@ test('缄默德克萨斯: first kill ⇒ full heal + recast; 剑术 buff removed
|
||||
assert.ok(u.mem.texasKilled);
|
||||
approx(u.hpRatio, 1, 1e-3, 'full heal');
|
||||
assert.equal(u.findBuff('texas2:swordplay'), null);
|
||||
assert.ok(u.findBuff('texas2:rainAtk'), 'passive recast');
|
||||
assert.equal(u.skill.active, true, 'skill recast');
|
||||
assert.equal(u.skill.activations, 2);
|
||||
});
|
||||
|
||||
test('星熊 S2 (passive): DEF +13 % and 65 % ATK thorns; 战术装甲 negates ~25 % of hits; 重装 aura DEF +6 %', () => {
|
||||
|
||||
Reference in new issue
Block a user