feedback5: merge feedback4 (583899d) — W4B ported: 阿戈尔's devoured base ATK as a 最终加算 (atkFinal), a fallen marker's marks still resolve, 缇缇 S2 pulses re-enter sleep for her trait (applyStatus reenter in server/sim/battle/status.js; the 缇缇 kit in kits/ops/chess_char_5_02-titi.js; Battle.js / tier5.js stay feedback5's façade and shim); golden: roster-042, bond-egirShip-high, coop2-ABYSS-10-boosted moved, the same three as on feedback4 (bond-egirShip-high identical to it)
This commit is contained in:
commit
c6827a0eaf
20 files changed
+302
-96
No files matched your search
@@ -335,7 +335,29 @@ test('阿戈尔: HP ×(1.35+0.01L); devour chain (left first): 5000 物理流失
|
||||
checkInvariants(h.b);
|
||||
});
|
||||
|
||||
test('阿戈尔 devour: 物理流失 ignores the marker’s damage bonuses and the target’s shields; a dead marker’s marks are cancelled; kill → marker', () => {
|
||||
test('阿戈尔 devour ATK is a 最终加算 (GitHub #165 point 2, PR #176): 1000 base ATK, +100 % skill ATK, +2000 devoured → 4000, not 6000', () => {
|
||||
// PRTS 盟约记录 "该付与来源获得所有标记单位的基础攻击力(最终加算)"; PRTS 游戏数据基础 A_f = F_t[(A + D_p)(1 + D_t) + F_p]
|
||||
const list = [
|
||||
['g1_a', ['egirShip']], ['g2_a', ['egirShip']], ['g3_a', ['egirShip']],
|
||||
['fod_a', ['preciShip'], { stats: { atk: 2000, maxHp: 20000, def: 0, blockCnt: 1 } }],
|
||||
];
|
||||
// g1 (10,3) → fodder (10,4, 2000 base ATK); g2 / g3 face empty tiles
|
||||
const units = [{ chessId: 'g1_a', row: 10, col: 3 }, { chessId: 'fod_a', row: 10, col: 4 }, { chessId: 'g2_a', row: 12, col: 3 }, { chessId: 'g3_a', row: 12, col: 5 }];
|
||||
const h = makeBattle({ defs: defsOf(list), units, bonds: { egirShip: bondOn(3, 0, null, [3, 5]) } });
|
||||
h.step(1);
|
||||
const g1 = h.unit('g1_a');
|
||||
assert.deepEqual(buffOf(g1, 'bond:egir:devour')?.mods, { atkFinal: 2000, blockCnt: 1 }, 'the fodder\'s base ATK as a 最终加算, its block count');
|
||||
close(g1.s.atk, 1000 + 2000, 1e-6, 'no percentage: as before');
|
||||
h.b.addBuff(g1, { key: 'test:skill', mods: { atkPct: 1 } });
|
||||
close(g1.s.atk, 1000 * 2 + 2000, 1e-6, '+100 %: 4000 (0.1.3: (1000 + 2000) × 2 = 6000)');
|
||||
h.b.addBuff(g1, { key: 'test:flat', mods: { atkFlat: 100 } });
|
||||
close(g1.s.atk, (1000 + 100) * 2 + 2000, 1e-6, 'a 直接加算 is still scaled by the percentages');
|
||||
h.b.addBuff(g1, { key: 'test:weaken', mods: { atkMul: 0.5 } });
|
||||
close(g1.s.atk, ((1000 + 100) * 2 + 2000) * 0.5, 1e-6, 'the 最终乘算 (Πmul) scales the whole, the 最终加算 included');
|
||||
checkInvariants(h.b);
|
||||
});
|
||||
|
||||
test('阿戈尔 devour: 物理流失 ignores the marker’s damage bonuses and the target’s shields; a fallen marker’s marks still resolve (GitHub #165 point 3); kill → marker', () => {
|
||||
const list = [
|
||||
['g1_a', ['egirShip', 'kjeragShip']], ['g2_a', ['egirShip'], { stats: { maxHp: 3000 } }], ['g3_a', ['egirShip']],
|
||||
['fod_a', ['preciShip'], { tier: 2, stats: { maxHp: 20000, def: 500 } }],
|
||||
@@ -353,14 +375,36 @@ test('阿戈尔 devour: 物理流失 ignores the marker’s damage bonuses and t
|
||||
assert.ok(!g2.alive, 'g2 devoured');
|
||||
assert.ok(h.hooksOf('kill').some((c) => c.victim === g2 && c.killer === g1), 'kill credited to the marker');
|
||||
const dv = tagged(h, 'bond:egir:devour');
|
||||
assert.deepEqual(dv.map((c) => [c.source.defId, c.target.defId]), [['g1_a', 'g2_a'], ['g1_a', 'fod_a']], 'g2’s own mark on the fodder is cancelled');
|
||||
close(f.hp, 20000 - (5000 - 500), 1e-6, '5000 less its DEF 500: no ×1.25, shield untouched');
|
||||
// PRTS 盟约记录: only the knocked-out TARGET's pending marks are cancelled — g2, knocked out by the first mark, still
|
||||
// resolves its own (until 0.1.3 a marker off the field gave no further mark)
|
||||
assert.deepEqual(dv.map((c) => [c.source.defId, c.target.defId]), [['g1_a', 'g2_a'], ['g1_a', 'fod_a'], ['g2_a', 'fod_a']], 'g2’s own mark on the fodder resolves');
|
||||
close(f.hp, 20000 - 2 * (5000 - 500), 1e-6, '5000 less its DEF 500, twice: no ×1.25, shield untouched');
|
||||
assert.equal(f.buffs.find((b) => b.key === 'test:shield')?.shield, 3000);
|
||||
close(g1.s.atk, 1000 + 1000 + 1000, 1e-6, 'the base ATK of everything it marked stays');
|
||||
assert.equal(h.b.getPlayer('p1').bonds.egirShip.layers, 1 + 2, 'layers = tiers of the devoured units');
|
||||
checkInvariants(h.b);
|
||||
});
|
||||
|
||||
test('阿戈尔 devour, A → B → C with B devoured first (GitHub #165 point 3, PR #176): C still takes B\'s 5000 and the kill is B\'s', () => {
|
||||
const list = [
|
||||
['A_a', ['egirShip']], ['B_a', ['egirShip'], { stats: { maxHp: 3000 } }], ['g3_a', ['egirShip']],
|
||||
['C_a', ['preciShip'], { tier: 2, stats: { maxHp: 9000, def: 0 } }],
|
||||
];
|
||||
// A (10,3) → B (10,4, 3000 × 1.35 HP: knocked out by A's mark) → C (10,5, 9000 HP, DEF 0: A's mark leaves 4000, B's
|
||||
// knocks it out); g3 faces an empty tile. Marks: A → B, A → C (through B), B → C.
|
||||
const units = [{ chessId: 'A_a', row: 10, col: 3 }, { chessId: 'B_a', row: 10, col: 4 }, { chessId: 'C_a', row: 10, col: 5 }, { chessId: 'g3_a', row: 12, col: 3 }];
|
||||
const h = makeBattle({ defs: defsOf(list), units, bonds: { egirShip: bondOn(3, 0, null, [3, 5]) }, hooks: ['damaged', 'kill'], captureNoisy: true });
|
||||
h.step(1);
|
||||
const [A, B, C] = ['A_a', 'B_a', 'C_a'].map((id) => h.unit(id));
|
||||
assert.deepEqual(tagged(h, 'bond:egir:devour').map((c) => [c.source.defId, c.target.defId]), [['A_a', 'B_a'], ['A_a', 'C_a'], ['B_a', 'C_a']]);
|
||||
assert.ok(!B.alive && !C.alive, 'B falls to A\'s mark, C to B\'s (until 0.1.3 B\'s mark was dropped: C stood at 4000 HP)');
|
||||
// PRTS 盟约记录 "单位被【吞噬】击杀时,击杀来源始终为对应标记的付与来源"
|
||||
assert.deepEqual(h.hooksOf('kill').map((c) => [c.victim.defId, c.killer?.defId]), [['B_a', 'A_a'], ['C_a', 'B_a']], 'the kill credit is the mark\'s marker, down or not');
|
||||
close(A.s.atk, 1000 + 1000 + 1000, 1e-6, 'A: the base ATK of B and C');
|
||||
assert.equal(h.b.getPlayer('p1').bonds.egirShip.layers, 1 + 2, 'each devoured unit adds its tier once');
|
||||
checkInvariants(h.b);
|
||||
});
|
||||
|
||||
test('阿戈尔 devour order: left first, then top first (row 0 is the bottom: the top board row is 12)', () => {
|
||||
const list = [['g1_a', ['egirShip']], ['g2_a', ['egirShip']], ['g3_a', ['egirShip']], ['fod_a', ['preciShip']]];
|
||||
const units = [
|
||||
@@ -600,7 +644,7 @@ test('阿戈尔 devour, a unit back during the pass: a marker revived by the 5-t
|
||||
h.step(1);
|
||||
const [g2, f] = ['g2_a', 'fod_a'].map((id) => h.unit(id));
|
||||
assert.ok(g2.alive && h.hooksOf('deploy').some((c) => c.unit === g2 && !c.initial), 'g2 knocked out and revived');
|
||||
// only a marker off the field gives no further mark (the rule since 0.1.0): g2, back at once, still devours g3 and the fodder
|
||||
// every mark resolves whatever became of its marker (DESIGN §24.7): g2, back at once, still devours g3 and the fodder
|
||||
assert.deepEqual(tagged(h, 'bond:egir:devour').map((c) => [c.source.defId, c.target.defId]),
|
||||
[['g1_a', 'g2_a'], ['g1_a', 'g3_a'], ['g1_a', 'fod_a'], ['g2_a', 'g3_a'], ['g2_a', 'fod_a'], ['g3_a', 'fod_a']]);
|
||||
close(f.hp, 20000 - 3 * 5000, 1e-6, 'the fodder: g1, g2 and g3');
|
||||
|
||||
@@ -39,7 +39,7 @@ test('联防 阿戈尔 chain 乌尔比安 → 幽灵鲨 (down) → 歌蕾蒂娅
|
||||
const got = buffOf(ulpia, 'bond:egir:devour');
|
||||
assert.ok(ref && got, 'both 乌尔比安 devour');
|
||||
const base = (id) => down.unit(id).base;
|
||||
close(got.mods.atkFlat, base(GHOST).atk + base(GLADY).atk + base(FODDER).atk, 1e-6, 'the base ATK of 幽灵鲨, 歌蕾蒂娅 and the fodder');
|
||||
close(got.mods.atkFinal, base(GHOST).atk + base(GLADY).atk + base(FODDER).atk, 1e-6, 'the base ATK of 幽灵鲨, 歌蕾蒂娅 and the fodder');
|
||||
assert.equal(got.mods.blockCnt, base(GHOST).blockCnt + base(GLADY).blockCnt + base(FODDER).blockCnt);
|
||||
assert.deepEqual(got.mods, ref.mods, 'exactly the gains of the chain with 幽灵鲨 standing');
|
||||
close(ulpia.s.atk, up.unit(ULPIA).s.atk, 1e-6, '乌尔比安 ATK');
|
||||
@@ -57,7 +57,7 @@ test('联防 阿戈尔 chain 乌尔比安 → 幽灵鲨 (down) → 歌蕾蒂娅
|
||||
assert.equal(fodder.alive, up.unit(FODDER).alive);
|
||||
// her own marks (歌蕾蒂娅, the fodder) still give her their base ATK / block for when she is back
|
||||
const own = buffOf(ghost, 'bond:egir:devour');
|
||||
close(own ? own.mods.atkFlat : 0, base(GLADY).atk + base(FODDER).atk, 1e-6, '幽灵鲨 keeps her marking gains (a persistent buff)');
|
||||
close(own ? own.mods.atkFinal : 0, base(GLADY).atk + base(FODDER).atk, 1e-6, '幽灵鲨 keeps her marking gains (a persistent buff)');
|
||||
checkInvariants(down.b);
|
||||
assert.equal(down.result().perPlayer.p1.deaths, up.result().perPlayer.p1.deaths - 1, 'no knock-out counted for her (standing, the devour knocks her out)');
|
||||
});
|
||||
@@ -84,7 +84,7 @@ test('联防 阿戈尔: a down member at the head of the chain devours as if it
|
||||
const down = head(true, five);
|
||||
const [ghost, glady, fodder] = [GHOST, GLADY, FODDER].map((id) => down.unit(id));
|
||||
const g = buffOf(ghost, 'bond:egir:devour');
|
||||
close(g.mods.atkFlat, glady.base.atk + fodder.base.atk, 1e-6, `${tag}: marks 歌蕾蒂娅 and, through her, the fodder`);
|
||||
close(g.mods.atkFinal, glady.base.atk + fodder.base.atk, 1e-6, `${tag}: marks 歌蕾蒂娅 and, through her, the fodder`);
|
||||
const pairs = (h) => devours(h).map((c) => [c.source.defId, c.target.defId]);
|
||||
assert.deepEqual(pairs(down), pairs(up), `${tag}: the marks resolve as with 幽灵鲨 standing`);
|
||||
assert.deepEqual(pairs(down), [[GHOST, GLADY], [GHOST, FODDER]], `${tag}: her two marks; 歌蕾蒂娅's on the fodder is cancelled (knocked out first)`);
|
||||
|
||||
@@ -52,7 +52,7 @@ test('联防: an 阿戈尔 devours a teammate\'s living operator in front — th
|
||||
const ref = buffOf(solo.unit(1), 'bond:egir:devour');
|
||||
const fodderBase = solo.unit(100).base;
|
||||
assert.ok(ref, 'solo: 乌尔比安 devours the operator in front');
|
||||
close(ref.mods.atkFlat, fodderBase.atk, 1e-9, 'its base ATK');
|
||||
close(ref.mods.atkFinal, fodderBase.atk, 1e-9, 'its base ATK');
|
||||
assert.equal(ref.mods.blockCnt, fodderBase.blockCnt, 'its block count');
|
||||
for (const [tag, h] of [['own', own], ['teammate\'s', mate]]) {
|
||||
const ulpia = h.unit(1), t = h.unit(100);
|
||||
@@ -92,7 +92,7 @@ test('联防: the chain goes on through a teammate\'s 阿戈尔 — the gains eq
|
||||
const p2Units = [HN, MIZUKI, GLADY, DEEP].map((chessId, i) => ({ uid: 200 + i, kind: 'chess', chessId, row: 12, col: 3 + 2 * i, dir: 'RIGHT' }));
|
||||
const mate = field({ owner: 'mate', front: [GHOST, FODDER], p2Units, p2Bonds: { egirShip: bondOn(5) } });
|
||||
const ref = buffOf(own.unit(1), 'bond:egir:devour');
|
||||
close(ref.mods.atkFlat, own.unit(100).base.atk + own.unit(101).base.atk, 1e-9, 'own chain: 幽灵鲨 and, through her, the fodder');
|
||||
close(ref.mods.atkFinal, own.unit(100).base.atk + own.unit(101).base.atk, 1e-9, 'own chain: 幽灵鲨 and, through her, the fodder');
|
||||
assert.deepEqual(buffOf(mate.unit(1), 'bond:egir:devour')?.mods, ref.mods, 'the teammate\'s chain gives the same');
|
||||
assert.deepEqual(devours(mate).map((c) => [c.source.uid, c.target.uid]), devours(own).map((c) => [c.source.uid, c.target.uid]));
|
||||
const ghost = mate.unit(100);
|
||||
@@ -134,7 +134,7 @@ test('联防 through unite.js: food knocked out by the own combat\'s devour ente
|
||||
u.step();
|
||||
const ulpia = u.unit(1);
|
||||
assert.deepEqual(buffOf(ulpia, 'bond:egir:devour')?.mods, gain, 'the own combat\'s gains');
|
||||
close(gain.atkFlat, [2, 3, 4].reduce((s, uid) => s + u.unit(uid).base.atk, 0), 1e-9, '幽灵鲨 + 歌蕾蒂娅 + fodder base ATK');
|
||||
close(gain.atkFinal, [2, 3, 4].reduce((s, uid) => s + u.unit(uid).base.atk, 0), 1e-9, '幽灵鲨 + 歌蕾蒂娅 + fodder base ATK');
|
||||
assert.equal(gain.blockCnt, [2, 3, 4].reduce((s, uid) => s + u.unit(uid).base.blockCnt, 0));
|
||||
for (const uid of [2, 3, 4]) assert.ok(!u.unit(uid).alive && u.unit(uid).removeReason === FORCED_EXIT, `${uid} forced out`);
|
||||
assert.equal(u.b.getPlayer('p1').bonds.egirShip.layers, 10, 'no layers in 联防');
|
||||
|
||||
@@ -302,6 +302,59 @@ test('allyenemy_sleepstun_inrange (缇缇 125): enemies or operators entering
|
||||
assert.equal(capOf('garrison_125_b'), 48);
|
||||
});
|
||||
|
||||
test('缇缇 125 + S2 封护 (GitHub #162): every 0.25 s sleep pulse of the ward is a new entry — 8 s climb to the cap; a sleeper outside her range does not count', () => {
|
||||
// the reporter's memory of the official mode: 「攻击范围内有陷入沉睡就开始迅速增加直至上限」; the pulse timing is [ASSUMED]
|
||||
for (const id of ['chess_char_5_02_a', 'chess_char_5_02_b']) {
|
||||
const gid = D.chess[id].garrisonIds.find((g) => GR(g).effectKey === 'act2autochess_gar_event_allyenemy_sleepstun_inrange');
|
||||
const per = num(GR(gid).bb.bond_add_count), cap = capOf(gid);
|
||||
const s2 = D.chess[id].skills.find((x) => x.skillId === 'skchr_titi_2').index;
|
||||
// 缇缇 (10,4) facing right: range 3-3 = rows 9–11, cols 4–7. The ward ally (10,6) is the only operator in it; the
|
||||
// enemy (10,5) stands beside both wards (in her range), the one at (10,3) beside her back (out of her range)
|
||||
const run = (enemies) => {
|
||||
const h = makeBattle({
|
||||
seed: 3, autoFinish: false, timeLimit: 400, hooks: ['statusApplied'],
|
||||
defs: { chess: { ward: rec('ward', []) }, enemies: { e_dummy: dummy() } },
|
||||
units: [{ chessId: id, skillIndex: s2, row: 10, col: 4 }, { chessId: 'ward', row: 10, col: 6 }],
|
||||
bonds: { sargonShip: B(0), preciShip: B(0) }, enemies,
|
||||
});
|
||||
h.step(1);
|
||||
const tt = h.unit(id);
|
||||
tt.skill.gainSp(1000);
|
||||
assert.ok(tt.skill.activate('test'), `${id}: S2 cast`);
|
||||
assert.equal(tt.skill.id, 'skchr_titi_2');
|
||||
return { h, tt };
|
||||
};
|
||||
// out of range only: the pulses sleep it (each one an entry for the engine) but her trait ignores it
|
||||
{
|
||||
const { h, tt } = run([{ key: 'e_dummy', pos: [10, 3] }]);
|
||||
const back = h.enemies()[0];
|
||||
expectAll(h, ['sargonShip', 'preciShip'], 2 * per, `${id}: the ward's two operators fall asleep in her range`);
|
||||
h.run(8);
|
||||
assert.ok(tt.skill.active && back.s.flags.sleep, `${id}: the enemy at her back sleeps`);
|
||||
const pulses = h.hooksOf('statusApplied').filter((c) => c.target === back && c.source === tt && c.entered);
|
||||
assert.ok(pulses.length >= 30, `${id}: ${pulses.length} pulse entries in 8 s`);
|
||||
expectAll(h, ['sargonShip', 'preciShip'], 2 * per, `${id}: an out-of-range sleeper does not count`);
|
||||
checkInvariants(h.b);
|
||||
}
|
||||
// in range: one entry per pulse (beside both wards: still one), the cap within the 8 s
|
||||
{
|
||||
const { h, tt } = run([{ key: 'e_dummy', pos: [10, 5] }, { key: 'e_dummy', pos: [10, 3] }]);
|
||||
const near = h.enemies().find((e) => Math.round(e.x) === 5);
|
||||
let capAt = null;
|
||||
for (let t = 0; t < 8 - 1e-9; t += 0.25) {
|
||||
h.run(0.25);
|
||||
if (capAt == null && (gains(h).sargonShip ?? 0) >= cap) capAt = t + 0.25;
|
||||
}
|
||||
assert.ok(tt.skill.active && near.s.flags.sleep, `${id}: still asleep after 8 s`);
|
||||
const mine = h.hooksOf('statusApplied').filter((c) => c.target === near && c.source === tt && c.status === 'sleep');
|
||||
assert.equal(mine.filter((c) => c.entered).length * 2, mine.length, `${id}: two wards, one entry per pulse`);
|
||||
expectAll(h, ['sargonShip', 'preciShip'], cap, `${id}: the cap (${cap})`);
|
||||
assert.ok(capAt != null && capAt <= 6.5, `${id}: capped after ${capAt} s`);
|
||||
checkInvariants(h.b);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
// ADD_BOND grants
|
||||
|
||||
|
||||
Reference in new issue
Block a user