Playtest #6 follow-up (v2.5.1): leader HP review — 直接乘算, one debuff instance, 999 layer cap, 限伤; elite to the board; 假想敌:胄 kit

- co-op leaders died within seconds: leader HP is the official data (40 values pinned); bond / strategy / item /
  机变 / 特质 "+X%" bonuses now add up (PRTS 直接乘算) instead of compounding; a same-named enemy debuff keeps one
  (strongest) instance per target (奥术 across a pair field); leader parts pass damage 1:1; fixed co-op pool (the user's call)
- official limits read from the client: each bond stops at 999 layers (MAX_GARRISON_STACK); a leader hit of 300000+ in
  boss / hidden battles is cancelled (MAX_BATTLE_DAMAGE, 限伤) — shared/constants.js switches, research 11
- a merge that consumes a deployed copy puts the elite on that tile and facing (user-confirmed); gold target highlight,
  promotion effect
- 假想敌:胄 per PRTS: one shell, shells within range, any drone death costs 2 % max HP, 剑/锤 sorties and 瘫痪
- DESIGN §20.10–§20.14; 2842 unit tests, all 28 browser suites green; Chrome = Node on boss fields
This commit is contained in:
sganggs committed 2026-10-02 04:19:28 +08:00
1 parent 6464e4b46a
commit 620f88c0d8
81 files changed
+4194 -447

No files matched your search

+65
View File
@@ -142,6 +142,47 @@ test('奥术: member arts damage → target arts taken ×(1.2+0.01·L) for 3 s;
checkInvariants(h2.b);
});
test('奥术 with two players on one field: one instance per target, the strongest wins (never multiplied); a weaker one resumes after it', () => {
// DESIGN §20.10: PRTS 作战机制 "同名buff的默认叠加策略buff只能表现出一个"; 巴哈姆特 12316 "共享型buff會跟對面搶 如果對面層數比你高
// 就不需要再特別激活直接吃他的奧術buff". v2.5 kept one instance per player: ×(1.3)·×(1.7) here.
const bb = bondBb('arcaneShip');
const caster = (id) => chessRec({ id, bonds: ['arcaneShip'], profession: 'CASTER', skill: null, rangeGrid: [[0, 0]] });
const defs = { chess: { a_1: caster('a_1'), a_2: caster('a_2'), a_x: op('a_x', []) }, enemies: DUMMY };
const h = makeBattle({
kind: 'boss', defs,
players: [
{ playerId: 'p1', seat: 0, side: 'L', colOffset: 0, units: [{ uid: 1, kind: 'chess', chessId: 'a_1', row: 12, col: 3 }], bonds: { arcaneShip: bond(1, 10) } },
{ playerId: 'p2', seat: 1, side: 'R', colOffset: 0, units: [{ uid: 2, kind: 'chess', chessId: 'a_2', row: 12, col: 3 }, { uid: 3, kind: 'chess', chessId: 'a_x', row: 11, col: 3 }], bonds: { arcaneShip: bond(1, 50) } },
],
enemies: [{ key: 'enemy_addon_dummy', pos: [9, 9] }],
});
h.step(2);
const e = h.enemy('enemy_addon_dummy');
const u1 = h.b.allyUnits.find((u) => u.defId === 'a_1'), u2 = h.b.allyUnits.find((u) => u.defId === 'a_2');
const m1 = bb.base_damage_scale + bb.damage_scale_per_stack * 10, m2 = bb.base_damage_scale + bb.damage_scale_per_stack * 50;
h.b.dealDamage(u1, e, { amount: 100, type: 'arts' });
close(e.s.artsTakenMul, m1, 'p1 alone');
h.b.dealDamage(u2, e, { amount: 100, type: 'arts' });
close(e.s.artsTakenMul, m2, 'the stronger p2 instance takes over — not m1 × m2');
h.b.dealDamage(u1, e, { amount: 100, type: 'arts' });
close(e.s.artsTakenMul, m2, 'a weaker application never overrides it');
assert.equal(e.buffs.filter((b) => String(b.key).startsWith('bond:arcaneShip')).length, 1, 'one buff on the target');
// the partner's non-member arts damage benefits too (it is a debuff on the target)
const x = h.b.allyUnits.find((u) => u.defId === 'a_x');
const hp0 = e.hp;
h.b.dealDamage(x, e, { amount: 100, type: 'arts' });
close(hp0 - e.hp, 100 * m2, 'any arts damage on the target');
// p2 stops: its instance expires at 3 s, p1's later application (≈ 3 s too) resumes for the rest
h.run(1);
h.b.dealDamage(u1, e, { amount: 100, type: 'arts' });
close(e.s.artsTakenMul, m2, 'still the stronger one');
h.run(bb.weak_duration - 1 + 0.1);
close(e.s.artsTakenMul, m1, 'the stronger expired, the weaker (applied later) resumes');
h.run(1.1);
close(e.s.artsTakenMul, 1, 'both expired');
checkInvariants(h.b);
});
test('坚守: all operators HP ×(1.25+0.012·L); tier 2 redirect 40% to members, thorns + fragile, cooldown, no loops', () => {
const bb = bondBb('steadShip');
const defs = { chess: { d_1: op('d_1', ['steadShip']), d_2: op('d_2', ['steadShip']), d_n: op('d_n', []) }, enemies: DUMMY };
@@ -180,6 +221,30 @@ test('坚守: all operators HP ×(1.25+0.012·L); tier 2 redirect 40% to members
checkInvariants(h.b);
});
test('坚守 thorns: 无来源 (no attacker bonus, hooks see no source) but credited to the member — its stats and the shared-pool tally', async () => {
// DESIGN §20.10: v2.5 dealt them with no source at all, so no player's boss damage counted them (19 % of a co-op pool)
const { SharedBossPool } = await import('../../server/match/finalAssault.js');
const bb = bondBb('steadShip');
const defs = { chess: { d_1: op('d_1', ['steadShip']), d_2: op('d_2', ['steadShip']) }, enemies: DUMMY };
const pool = new SharedBossPool(1e6);
const h = makeBattle({ defs, kind: 'boss', sharedBoss: pool, units: [{ chessId: 'd_1', row: 10, col: 3 }, { chessId: 'd_2', row: 10, col: 5 }],
bonds: { steadShip: bond(2, 10) }, enemies: [{ key: 'enemy_addon_dummy', pos: [9, 9], tag: 'boss' }], hooks: ['damaged'], captureNoisy: true });
h.step(2);
const e = h.enemy('enemy_addon_dummy'), d1 = h.unit('d_1');
assert.ok(e.bossPool, 'the leader drains the pool');
h.b.addBuff(d1, { key: 'test:rage', mods: { dmgDealtMul: 2 } });
const p0 = pool.hp, dmg0 = d1.stats.dmg;
h.b.dealDamage(e, d1, { amount: 100, type: 'true' });
const thorn = bb.base_damage_value + bb.damage_value_per_stack * 10;
close(p0 - pool.hp, thorn, 'thorns hit the pool, the member\'s ×2 does not apply');
const ev = h.hooksOf('damaged').filter((c) => c.target === e);
assert.equal(ev.length, 1);
assert.equal(ev[0].source, null, '无来源: hooks see no source');
assert.equal(ev[0].credit, d1, 'credited to the member');
close(d1.stats.dmg - dmg0, thorn, 'member stats');
close(h.result().perPlayer.p1.bossDamage, thorn, 'its player\'s shared-pool tally');
});
test('坚守 tier 2: the redirected 40 % is the already-scaled damage — the attacker’s damage bonus is not applied twice', () => {
const defs = { chess: { d_1: op('d_1', ['steadShip']), d_2: op('d_2', ['steadShip']), d_n: op('d_n', []) }, enemies: DUMMY };
const units = [{ chessId: 'd_1', row: 10, col: 3 }, { chessId: 'd_2', row: 10, col: 5 }, { chessId: 'd_n', row: 12, col: 3 }];