- 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.
54 lines
3.5 KiB
JavaScript
54 lines
3.5 KiB
JavaScript
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { existsSync } from 'node:fs';
|
|
import { Client, ROOT, hasChrome, startRealServer } from '../e2e/client.mjs';
|
|
|
|
const enabled = process.env.SP_E2E === '1' && hasChrome() && existsSync(`${ROOT}/public/assets`);
|
|
|
|
test('buying Matterhorn sends two live layer cues through the real client; resync/reload stays silent',
|
|
{ skip: !enabled, timeout: 120000 }, async () => {
|
|
const P = (await import('puppeteer-core')).default;
|
|
const srv = await startRealServer({ fast: { timerScale: 0.1, startRound: 2, kit: 0, shop: ['chess_char_1_02_a'] } });
|
|
const c = new Client(P, srv.base, 'bond-audio', { prefix: 'bond-audio' });
|
|
try {
|
|
await c.open(); await c.enter('叠层音效测试');
|
|
await c.click('.mode-card', '独立模拟'); await c.click('.diff-card', '险境');
|
|
await c.click('.create-box button', '开始独立模拟');
|
|
await c.waitFor(s => !!s.room, 'solo room');
|
|
if (!(await c.st()).phase) await c.click('.room-bar__right button', '开始模拟');
|
|
await c.waitFor(s => s.phase === 'INFO_CHECK', 'briefing');
|
|
await c.click('.brief__foot .btn--primary', '准备就绪');
|
|
await c.waitFor(s => s.phase === 'BAND_DRAFT', 'band draft');
|
|
await c.page.click('.dband'); await c.click('.draft-detail__btns .btn--primary', '确认选择');
|
|
await c.waitFor(s => s.phase === 'PREP' && !s.ready, 'prep');
|
|
await c.page.evaluate(async () => {
|
|
const { audio } = await import('/js/audio.js');
|
|
const { net } = await import('/js/net.js');
|
|
window.__layerCues = []; window.__layerFrames = [];
|
|
const play = audio._playBondCue.bind(audio);
|
|
audio._playBondCue = (url, volume) => { window.__layerCues.push({ url, volume }); play(url, volume); };
|
|
net.on('m.layer', m => window.__layerFrames.push(m));
|
|
});
|
|
await c.click('.scard', '角峰'); await c.click('.scard', '角峰');
|
|
await c.page.waitForFunction(() => window.__layerFrames.length === 2);
|
|
const result = await c.page.evaluate(async () => {
|
|
const { audio } = await import('/js/audio.js');
|
|
// Wait for the actual local asset decoder; then its queued cue continuation starts the source.
|
|
await Promise.all([...audio.buffers.values()]); await Promise.resolve();
|
|
return { frames: window.__layerFrames, cues: window.__layerCues, playing: !!audio.bondCueNode,
|
|
state: audio.ctx?.state, layers: globalThis.__SP__.store.get().match.private.bonds };
|
|
});
|
|
assert.deepEqual(result.frames.map(m => [m.bondId, m.add]).sort(), [['kjeragShip', 2], ['steadShip', 2]]);
|
|
assert.equal(result.cues.length, 2); assert.equal(result.playing, true); assert.equal(result.state, 'running');
|
|
assert.ok(result.cues.every(x => /act1autochess_b_ui_buffup/.test(x.url)));
|
|
const count = result.cues.length;
|
|
// g.watch on own prep asks the real server for state again, without a new gain.
|
|
await c.page.evaluate(() => globalThis.__SP__.net.request('g.watch', { fieldId: `n:${globalThis.__SP__.store.get().me.playerId}` }));
|
|
assert.equal(await c.page.evaluate(() => window.__layerCues.length), count);
|
|
await c.page.reload({ waitUntil: 'networkidle0' });
|
|
await c.waitFor(s => s.phase === 'PREP', 'restored prep');
|
|
assert.equal(await c.page.evaluate(async () => !!(await import('/js/audio.js')).audio.bondCueNode), false);
|
|
assert.equal(c.problems.filter(x => x.startsWith('pageerror:')).length, 0, c.problems.join('\n'));
|
|
} finally { await c.close(); await srv.stop(); }
|
|
});
|