feedback7 dober: the Final Assault and the Hidden Core pay a 战术特训 card's perfect-battle coins, they paid kill coins only (PR #353 by @Sukvii); golden: unchanged
Seen: Match._settleBossBounties banked only the kill coins of the boss field (pp.coins). A perfect-payout bounty (a 战术特训 card, from the 杜宾 Art 教鞭) has its enemies spawned in the Final Assault / Hidden Core like any bounty with battles left, and used a battle there, but its coins were never paid — SETTLE pays them after every normal battle. Official: PRTS 杜宾 "<教鞭>:使用后为下场战斗添加额外敌人,若自身战斗完美作战可获得资金" and the cards' "若各自行动阶段就达成完美作战,获得N资金" have no boss-battle exception. The condition is [ASSUMED]: the team won (the first end the server registered), the result is a real one (not the synthetic stand-in of a field that never reported) and the player's own field let no counted enemy through; paid once per player, spent in the Hidden Core's prep with the kill coins. Now: _finishFinal reads the victory before the per-field loop and hands _settleBossBounties a `perfect` flag. Tests: clientCombat.test.js (the held 'cleared' result pays once, a duplicate never twice; six eligibility cases: team win / synthetic / own leak / uncounted leak / forced team defeat), followups.test.js (R14 -> R15 income and the Hidden Core; a boss win with an own leak keeps the kill coins only). Both fail on the parent. Golden: unchanged (matches: 18 scenarios match).
This commit is contained in:
1 parent
ae22335cfe
commit
05eb2b807f
5 files changed
+113
-11
No files matched your search
@@ -9,7 +9,7 @@ import { PHASE } from '../../shared/constants.js';
|
||||
import { validateC2S, isBattleResult } from '../../shared/protocol.js';
|
||||
import { buildBattleSpec, createBattleFromSpec, resultDigest, compactResult, LocalBossPool, jsonClone } from '../../server/sim/spec.js';
|
||||
import { DataSource } from '../../server/sim/simdata.js';
|
||||
import { validateClientResult, specBounds } from '../../server/match/fields.js';
|
||||
import { validateClientResult, specBounds, syntheticResult } from '../../server/match/fields.js';
|
||||
import { CreditPool, SharedBossPool } from '../../server/match/finalAssault.js';
|
||||
import { FakeBattle } from './fakeBattle.js';
|
||||
import { DATA, makeMatch, checkInvariants, give, chessOfTier } from './harness.js';
|
||||
@@ -509,6 +509,10 @@ test('Final Assault: an early boss b.result (forced at t≈0) does not end the p
|
||||
|
||||
test('Final Assault: a 999-layer kill faster than the budget — the \'cleared\' b.result whose report covers the pool waits for the budget, no takeover (QA 6b)', () => {
|
||||
const { h, m, f } = faWithForger(9122);
|
||||
const ps = h.ps('p_0');
|
||||
const training = DATA.choices.cards.bounty.find((c) => c.payout === 'perfect' && m.gd.enemy(c.enemyKey));
|
||||
m.addBounty(ps, { ...training, rounds: 2, multiRound: false });
|
||||
const pending = ps.pendingFunds;
|
||||
const max = m.bossPool.maxHp;
|
||||
h.sched.advance(500); // 1 game s on the field clock: the budget credits ≤ 20 % of the pool
|
||||
const gt = m._fieldElapsed(f);
|
||||
@@ -537,10 +541,44 @@ test('Final Assault: a 999-layer kill faster than the budget — the \'cleared\'
|
||||
assert.equal(f.resultSource, 'client', 'the held client result completed the field');
|
||||
assert.equal(m.verifyStats.takeovers, takeovers0);
|
||||
assert.equal(m.verifyStats.rejected, rejected0);
|
||||
assert.equal(ps.pendingFunds, pending + training.coin, 'validated held client result earns the server card amount');
|
||||
assert.equal(ps.bounties[0].roundsLeft, 1);
|
||||
assert.deepEqual(m.handle('p_0', { t: 'b.result', battleId: f.battleId, result: res }), { ok: true });
|
||||
assert.equal(ps.pendingFunds, pending + training.coin, 'duplicate accepted result does not pay twice');
|
||||
assert.equal(ps.bounties[0].roundsLeft, 1);
|
||||
for (const pid of f.players) assert.ok(Math.abs((f.bossBy[pid] || 0) - Math.min(by[pid], max)) <= max * 1e-9 + 5, `${pid} credited as reported (${f.bossBy[pid]} vs ${by[pid]})`);
|
||||
m.dispose();
|
||||
});
|
||||
|
||||
test('boss bounty eligibility: forced real results on a team victory qualify; synthetic, leaked and team defeat do not', () => {
|
||||
for (const [ending, synthetic, perfect, leaked, eligible] of [
|
||||
['cleared', false, true, [], true],
|
||||
['cleared', true, true, [], false],
|
||||
['cleared', false, false, [], false],
|
||||
['cleared', false, true, [{ counted: true }], false],
|
||||
['cleared', false, true, [{ counted: false }], true],
|
||||
['forced', false, true, [], false],
|
||||
]) {
|
||||
const { m, f } = faWithForger(9122);
|
||||
const ps = m.players.get(f.players[0]);
|
||||
const card = DATA.choices.cards.bounty.find((c) => c.payout === 'perfect' && m.gd.enemy(c.enemyKey));
|
||||
m.addBounty(ps, { ...card, rounds: 2, multiRound: false });
|
||||
m.addBounty(ps, { ...card, rounds: 2, multiRound: false });
|
||||
const res = syntheticResult(f.players);
|
||||
if (!synthetic) delete res.synthetic;
|
||||
res.reason = 'forced';
|
||||
Object.assign(res.perPlayer[ps.playerId], { perfect, leaked, coins: 3 });
|
||||
const pending = ps.pendingFunds, gained = ps.stats.fundsGained;
|
||||
m._finalEnding = ending;
|
||||
m.bossPool.hp = 0; // even a late clearing report cannot override an already-registered team defeat
|
||||
m._finishFinal(false, () => res);
|
||||
assert.equal(ps.pendingFunds, pending + 3 + (eligible ? 2 * card.coin : 0));
|
||||
assert.equal(ps.stats.fundsGained, gained + 3 + (eligible ? 2 * card.coin : 0));
|
||||
assert.deepEqual(ps.bounties.map((b) => b.roundsLeft), [1, 1]);
|
||||
m.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
test('Final Assault: a \'cleared\' b.result whose report does NOT cover the pool is still handed over (the partner takes the field)', () => {
|
||||
const { h, m, f } = faWithForger(9123);
|
||||
const max = m.bossPool.maxHp;
|
||||
|
||||
@@ -56,6 +56,56 @@ test('multi-round bounties spawn in the Final Assault on the owner\'s half, show
|
||||
m.dispose();
|
||||
});
|
||||
|
||||
// A 战术特训 card pays its coins when the player's own battle is perfect (PRTS "若各自行动阶段就达成完美作战,获得N资金");
|
||||
// the Final Assault and the Hidden Core are such battles — they pay it with the kill coins, at the next income.
|
||||
test('perfect-payout bounties in the boss rounds: a 战术特训 card\'s coins join the kill coins at the next income (R14 → R15, then the Hidden Core)', () => {
|
||||
const h = makeMatch({ mode: 'coop', difficulty: 'NORMAL', humans: 2, seed: 61, fake: true,
|
||||
script: (b) => (b.kind === 'boss' || b.kind === 'hidden' ? { bossDps: 1e9, coins: { p_0: 3 } } : {}) }).start();
|
||||
const m = h.m;
|
||||
h.toPrep(14);
|
||||
const ps = h.ps('p_0');
|
||||
const [card, second] = DATA.choices.cards.bounty.filter((c) => c.payout === 'perfect' && m.gd.enemy(c.enemyKey) && !m.gd.inactiveEnemies.has(c.enemyKey));
|
||||
m.addBounty(ps, card);
|
||||
const pending = ps.pendingFunds, gained = ps.stats.fundsGained;
|
||||
h.drive(() => m.phase === PHASE.FINAL_ASSAULT);
|
||||
m.hiddenLayerSum = 1e6; // the Hidden Core opens
|
||||
assert.ok(bossFields()[0].opts.spawns.some((s) => s.tag === 'bounty' && s.enemyKey === card.enemyKey && s.ownerPlayerId === ps.playerId), 'the card\'s enemies are in the boss battle');
|
||||
h.run(() => m.runner === null);
|
||||
assert.equal(ps.pendingFunds, pending + 3 + card.coin, 'kill coins + one card amount, regardless of its enemy count');
|
||||
assert.equal(ps.stats.fundsGained, gained + 3 + card.coin);
|
||||
const left = bountyBattles(card);
|
||||
assert.equal(ps.bounties.length, left > 1 ? 1 : 0, 'the boss battle used one of its battles');
|
||||
if (left > 1) assert.equal(ps.bounties[0].roundsLeft, left - 1);
|
||||
h.toPrep(15);
|
||||
assert.equal(ps.pendingFunds, 0);
|
||||
assert.equal(ps.funds, m.gd.income(15) + pending + 3 + card.coin, 'the base income is separate from the bounty funds');
|
||||
m.addBounty(ps, second);
|
||||
const owed = ps.bounties.filter((b) => b.card.payout === 'perfect').reduce((n, b) => n + b.card.coin, 0);
|
||||
h.drive(() => m.phase === PHASE.HIDDEN_CORE);
|
||||
assert.ok(bossFields().at(-1).opts.spawns.some((s) => s.tag === 'bounty' && s.enemyKey === second.enemyKey), 'and in the Hidden Core');
|
||||
h.runToEnd();
|
||||
assert.equal(ps.pendingFunds, 3 + owed, 'the Hidden Core pays by the same rule');
|
||||
assert.equal(ps.bounties.length, 0, 'every bounty used its battles');
|
||||
checkInvariants(m);
|
||||
m.dispose();
|
||||
});
|
||||
|
||||
test('boss victory without a perfect own result keeps the kill coins but gives no perfect-card funds', () => {
|
||||
const h = makeMatch({ mode: 'coop', humans: 2, seed: 61, fake: true,
|
||||
script: (b) => (b.kind === 'boss' ? { bossDps: 1e9, leaks: { p_0: 1 }, coins: { p_0: 3 } } : {}) }).start();
|
||||
h.toPrep(14);
|
||||
const m = h.m, ps = h.ps('p_0');
|
||||
const card = DATA.choices.cards.bounty.find((c) => c.payout === 'perfect' && !m.gd.inactiveEnemies.has(c.enemyKey));
|
||||
m.addBounty(ps, { ...card, rounds: 2, multiRound: false });
|
||||
const gained = ps.stats.fundsGained;
|
||||
const result = h.drive(() => h.ended != null);
|
||||
assert.ok(result && h.ended.victory);
|
||||
assert.equal(ps.pendingFunds, 3);
|
||||
assert.equal(ps.stats.fundsGained, gained + 3);
|
||||
assert.equal(ps.bounties[0].roundsLeft, 1);
|
||||
m.dispose();
|
||||
});
|
||||
|
||||
test('solo Final Assault: the bounty spawns on the `_s` template; bountySpawns route choice by side', () => {
|
||||
const h = makeMatch({ mode: 'solo', difficulty: 'HARD', seed: 62, fake: true, script: (b) => (b.kind === 'boss' ? { bossDps: 1e9 } : {}) }).start();
|
||||
const m = h.m;
|
||||
|
||||
Reference in new issue
Block a user