- 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.
69 lines
4.1 KiB
JavaScript
69 lines
4.1 KiB
JavaScript
// The cross-engine check page (docs/SIM.md「Cross-engine check」): this browser runs the golden battle corpus with the
|
|
// client's own sim loader (public/js/battle/runner.js loadBrowserSim) and compares every tick with the Node reference
|
|
// that `node tools/sim-check.mjs` wrote to /dev/simcheck/data/ — any device on the LAN can open it from the server.
|
|
import { mountCheck } from './simcheck/ui.js';
|
|
import { strongholdAdapter, scenariosOf } from './simcheck/stronghold.js';
|
|
import { VERSION, textFingerprint } from './simcheck/core.js';
|
|
|
|
const app = document.getElementById('app');
|
|
globalThis.__simcheck = { status: 'loading', results: [], summary: null, error: null };
|
|
const get = async (url) => {
|
|
const r = await fetch(url, { cache: 'no-cache' });
|
|
if (!r.ok) throw Object.assign(new Error(`${r.status} ${url}`), { missing: r.status === 404 });
|
|
return r.json();
|
|
};
|
|
|
|
try {
|
|
const [doc, reference, { loadBrowserSim }, { APP_VERSION }] = await Promise.all([
|
|
get('/dev/simcheck/data/specs.json'), get('/dev/simcheck/data/reference.json'), import('/js/battle/runner.js'), import('/shared/constants.js'),
|
|
]);
|
|
if (doc.simcheck !== VERSION || reference.simcheck !== VERSION || !Array.isArray(doc.files) || !doc.files.length
|
|
|| doc.identity !== reference.identity || doc.identity !== textFingerprint(JSON.stringify({ specs: doc.specs, files: doc.files }))) {
|
|
throw new Error('参考与场景文件不匹配,请重新生成参考数据。');
|
|
}
|
|
const verifiedData = new Map();
|
|
const validate = async () => {
|
|
for (let i = 0; i < doc.files.length; i += 8) await Promise.all(doc.files.slice(i, i + 8).map(async ({ url, hash }) => {
|
|
if (typeof url !== 'string' || !/^\/(?:sim\/|shared\/|data\/|data\.js$|js\/battle\/runner\.js$|dev\/simcheck\/)/.test(url) || url.includes('..')) throw new Error('invalid reference input path');
|
|
const response = await fetch(url, { cache: 'no-cache' });
|
|
if (!response.ok) throw new Error(`参考输入不可用:${url}`);
|
|
const text = await response.text();
|
|
if (textFingerprint(text) !== hash) throw new Error(`参考数据已过期:${url}。请重新生成参考并刷新页面。`);
|
|
if (url.startsWith('/data/')) verifiedData.set(url, text);
|
|
}));
|
|
};
|
|
await validate();
|
|
const { spec, ds } = await loadBrowserSim({ fetchFn: url => {
|
|
const text = verifiedData.get(url);
|
|
if (text == null) throw new Error(`unverified data input: ${url}`);
|
|
return Promise.resolve(new Response(text, { status: 200, headers: { 'Content-Type': 'application/json' } }));
|
|
} });
|
|
const scenarios = scenariosOf(doc);
|
|
const FAMILIES = [['roster', '干员'], ['bonds', '盟约'], ['fields', '首领战场与联防'], ['standins', '补位'], ['diy', '自选']];
|
|
if (reference.made?.version !== APP_VERSION) throw new Error('参考游戏版本已过期,请重新生成。');
|
|
mountCheck(app, {
|
|
project: 'Stronghold Protocol',
|
|
title: '模拟一致性检查',
|
|
subtitle: '逐帧比较已记录的单位状态与 Node 参考,并抽样比较快照和部分内部状态。输入文件按内容指纹核对;通过不代表全部画面、事件流或内部内存均已覆盖。状态下载仅定位状态向量字段。',
|
|
validate,
|
|
adapter: strongholdAdapter({ spec, ds, doc }),
|
|
scenarios,
|
|
subsets: [
|
|
{ id: 'fast', label: '快速(黄金结果的快速子集)', pick: (sc) => sc.fast },
|
|
{ id: 'all', label: '全部', pick: () => true },
|
|
...FAMILIES.map(([id, label]) => ({ id, label, pick: (sc) => sc.family === id })),
|
|
],
|
|
reference,
|
|
info: [['游戏版本', APP_VERSION]],
|
|
});
|
|
} catch (e) {
|
|
globalThis.__simcheck = { status: 'error', results: [], summary: null, error: String(e.message || e) };
|
|
app.innerHTML = '';
|
|
const p = document.createElement('p');
|
|
p.style.cssText = 'color:var(--text-md);padding:16px;max-width:760px;margin:0 auto;font:14px/1.6 var(--font-cjk)';
|
|
p.textContent = e.missing
|
|
? '还没有参考数据:请在服务器的目录里运行 node tools/sim-check.mjs(生成 public/dev/simcheck/data/),然后刷新这个页面。'
|
|
: `载入失败:${e && e.message || e}`;
|
|
app.appendChild(p);
|
|
}
|