- Stock: fixed trait gifts (缪尔赛思 / 变形者集群 变形同构体, 耶拉 不融冰, 蜜蜡 / 卡涅利安 迅捷作战粮, 诗怀雅 盟约之币) still occupy stock but are granted past the cap; an operator card already shown in the shop or a reward can be taken after the pool runs out and overdraws (GitHub #498, the owner's decision); item buys and item rewards keep the stock check. 洛洛 / 焰尾 sub-pools use the measured weights (PR #497 by @2321Robin; the occupancy idea of PR #496 by @2321Robin re-implemented differently). - Boss pairs are drawn at random once per match and fixed to battle start; the Hidden Core reuses them; a leaver does not split a full pair (the owner's decision). - 阿戈尔 in 联防 no longer re-applies the opening devour HP loss or a false revive; Touch's team medic stays for the match after its holder leaves (owner-approved, official lifetime unverified). - Auto-skill ranges: 司霆惊蛰 S2, 斩业星熊 S3 against air, 予愿安洁莉娜 S3; 自在's post-rebirth invulnerability is also untargetable (#510); Horn / Clementia / catapult / 忍冬 first hit (PR #502 by @Xinglan233); 不融冰 / tear gas trigger on real damage. - Bond layer-up sound in round start / draft / prep / battle (#482): one channel, a new cue replaces the old, no extra cooldown; a prep update that adds layers and activates a bond plays one cue. - Client: multi-swing fix (#506), refresh single-flight (PR #507 by @robll-v1), LP tower keeps the settled value with a −N tick (PR #504 by @robll-v1), export with only 潜能 / 练度 changes (PR #503 by @Xinglan233), spectator rosters list DIY members (PR #505 by @Xinglan233), banned members beside bond tooltips (PR #494 by @YuriRestia), recovery action above the away overlay (PR #511 by @RainoutOvO), the /dev/sim-check.html consistency page (PR #492 by @Cloudnyco), Korean (PR #490 by @D-Roote) and English Victoria wording (PR #499 by @YuriRestia). - golden: 45 of 287 scenarios move against 0.2.4 (roster 17, bonds 3, fields 6, matches 17, standins 2), each attributed in docs/history/0.2.5.md §30.9 / §30.16.
60 lines
3.3 KiB
JavaScript
60 lines
3.3 KiB
JavaScript
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { makeMatch } from './harness.js';
|
|
import { Battle } from '../../server/sim/Battle.js';
|
|
import { getDefaultSource } from '../../server/sim/simdata.js';
|
|
|
|
const MEDICS = new Set(['char_605_cmedic', 'char_613_acmedc']);
|
|
const medics = b => b.allyUnits.filter(u => MEDICS.has(u.defId) && u.alive && !u.removed);
|
|
const clean = () => ({ leaked: [], perfect: true, coins: 0, layerGains: {}, killed: 0, damageDealt: 0 });
|
|
function setup(t) {
|
|
const h = makeMatch({ mode: 'coop', humans: 3, seed: 334401, fake: true }).start().toPrep();
|
|
t.after(() => h.m.dispose()); h.setStage('act2autochess_m01');
|
|
for (const p of h.m.order) p.bandId = null;
|
|
h.ps('p_0').bandId = 'band_amedic'; return h;
|
|
}
|
|
function field(h, elites = 0) {
|
|
const opts = h.m._normalOpts(h.ps('p_1'));
|
|
opts.players[0].units = ['chess_char_1_01_b', 'chess_char_1_02_b'].slice(0, elites)
|
|
.map((chessId, i) => ({ uid: 100 + i, kind: 'chess', chessId, row: 10 + i, col: 4 }));
|
|
const b = new Battle({ ...opts, data: getDefaultSource(), spawns: [], timeLimit: 300, autoFinish: false });
|
|
b.start(); assert.equal(b.errorCount, 0); return b;
|
|
}
|
|
|
|
for (const reason of ['eliminated', 'quit']) for (const elites of [0, 2]) {
|
|
test(`Touch team grant survives holder ${reason}, receiver elites=${elites}`, t => {
|
|
const h = setup(t), before = field(h, elites), expected = elites ? 'char_613_acmedc' : 'char_605_cmedic';
|
|
assert.deepEqual(medics(before).map(u => u.defId), [expected]);
|
|
if (reason === 'quit') h.m.onLeave('p_0');
|
|
else {
|
|
h.ps('p_0').lp = 1;
|
|
h.m.lastResults = new Map(h.m.order.map(p => [p.playerId, clean()]));
|
|
h.m.lastResults.set('p_0', { ...clean(), perfect: false, leaked: [{ enemyKey: 'enemy_1422_lrsldr', counted: true }] });
|
|
h.m.settle(null, null); h.toPrep(2); h.setStage('act2autochess_m01');
|
|
}
|
|
assert.equal(h.ps('p_0').alive, false);
|
|
assert.equal(h.ps('p_1').privateView().prepMapChars.length, 1);
|
|
const after = field(h, elites);
|
|
assert.deepEqual(medics(after).map(u => u.defId), [expected]);
|
|
assert.equal(after.players[0].input.contentInfo.teammates.some(p => p.playerId === 'p_0'), false,
|
|
'retaining Touch does not revive dead teammate contexts');
|
|
for (let i = 0; i < 30; i++) before.step();
|
|
assert.deepEqual(medics(before).map(u => u.defId), [expected]);
|
|
h.ps('p_0').bandId = 'band_amiya';
|
|
assert.equal(field(h).players[0].input.contentInfo.matchBands.includes('band_amiya'), false,
|
|
'other eliminated strategies are not retained by this exception');
|
|
});
|
|
}
|
|
|
|
test('Touch requires a confirmed selection and does not leak to another match', t => {
|
|
const h = makeMatch({ mode: 'coop', humans: 3, seed: 334405, fake: true }).start();
|
|
t.after(() => h.m.dispose()); h.drive(() => h.m.phase === 'BAND_DRAFT', { ready: false });
|
|
const holder = h.m.players.get(h.m.draftTurn());
|
|
assert.equal(h.m.bandFocus(holder, 'band_amedic').ok, true);
|
|
assert.equal(h.m.teamMedicGranted(), false);
|
|
assert.equal(h.m.pickBand(holder, 'band_amedic').ok, true);
|
|
h.m.onLeave(holder.playerId); assert.equal(h.m.teamMedicGranted(), true);
|
|
const next = makeMatch({ mode: 'coop', humans: 3, seed: 334405, fake: true }).start();
|
|
t.after(() => next.m.dispose()); assert.equal(next.m.teamMedicGranted(), false);
|
|
});
|