feedback7 G-TOOLS: add the battle performance bench under public/dev (PR #198 by @Cloudnyco)
Applied as-is. Captured real battles live in public/dev/perf (including the round-13 联防 on the wide round battlefield), with a browser speed page and a headless benchmark. public/dev stays out of the package. The Chrome benchmark was not run. The numbers in the original pull request are from 0.1.4 and are not quoted here. Tests: node --test test/perfbench.test.js test/render/battle-perf.browser.test.js — 13 pass, 0 fail; the Chrome suite skipped (RENDER_E2E unset).
This commit is contained in:
1 parent
1f147a288f
commit
e8ba091298
13 files changed
+984
No files matched your search
@@ -0,0 +1,128 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="color-scheme" content="dark" />
|
||||
<title>Battle perf · 卫戍协议:盟约</title>
|
||||
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Crect width='24' height='24' fill='%230c0f0e'/%3E%3Cpath fill='%234ed8af' d='M5 3h3v2h2V3h4v2h2V3h3v5l-2 2v7l2 2v2H5v-2l2-2v-7L5 8z'/%3E%3C/svg%3E" />
|
||||
<link rel="stylesheet" href="/fonts/fonts.css" />
|
||||
<link rel="stylesheet" href="/css/theme.css" />
|
||||
<script type="importmap">
|
||||
{ "imports": { "preact": "/vendor/preact.module.js", "preact/hooks": "/vendor/hooks.module.js", "htm": "/vendor/htm.module.js" } }
|
||||
</script>
|
||||
<!-- PixiJS 7 + pixi-spine as classic scripts (global PIXI / PIXI.spine), like the game page -->
|
||||
<script src="/vendor/pixi.min.js"></script>
|
||||
<script src="/vendor/pixi-spine.js"></script>
|
||||
<style>
|
||||
/* Battle perf panel: the game's own tokens (css/theme.css — green-grey glass, mint, Bender figures, bracket corners)
|
||||
in px, not the game's viewport-scaled rem. The one loud element is the frame-time strip. */
|
||||
html, body { margin: 0; height: 100%; background: var(--bg-0); overflow: hidden; }
|
||||
#field { position: fixed; inset: 0; }
|
||||
.bp { position: fixed; top: 10px; right: 10px; z-index: 5; width: 320px; max-width: calc(100vw - 20px); max-height: calc(100vh - 20px);
|
||||
overflow: auto; background: var(--bg-glass); border: 1px solid var(--line); color: var(--text-md);
|
||||
font: 13px/1.45 var(--font-cjk); -webkit-backdrop-filter: blur(6px); backdrop-filter: blur(6px); --bk-l: 10px; }
|
||||
.bp.is-hidden, .bp [hidden] { display: none; }
|
||||
.bp-body { display: grid; gap: 10px; padding: 10px 12px 12px; }
|
||||
.bp-head { display: flex; align-items: center; gap: 8px; }
|
||||
.bp-head h1 { margin: 0; font-size: 14px; font-weight: 700; color: var(--text-hi); flex: 1; }
|
||||
.bp button, .bp select { font: 13px/1.2 var(--font-cjk); color: var(--text-hi); background: var(--bg-2); border: 1px solid var(--line-2);
|
||||
padding: 5px 8px; cursor: pointer; min-height: 30px; }
|
||||
.bp button:hover, .bp select:hover { border-color: var(--line-3); }
|
||||
.bp button:focus-visible, .bp select:focus-visible, .bp summary:focus-visible { outline: 2px solid var(--mint-500); outline-offset: 1px; }
|
||||
.bp button:disabled { opacity: .5; cursor: default; }
|
||||
.bp-ctl { display: grid; grid-template-columns: 1fr auto; gap: 6px; }
|
||||
.bp-ctl select { min-width: 0; }
|
||||
.bp-ctl #spec { grid-column: 1 / -1; }
|
||||
|
||||
.bp-live { display: grid; grid-template-columns: auto 1fr; gap: 4px 14px; align-items: end; }
|
||||
.bp-fps { font: 600 44px/0.95 var(--font-num); color: var(--text-hi); font-variant-numeric: tabular-nums; white-space: nowrap; }
|
||||
.bp-fps small { font: 400 13px var(--font-cjk); color: var(--text-lo); margin-left: 4px; }
|
||||
.bp-figs { display: grid; grid-template-columns: auto auto; gap: 1px 10px; justify-content: end; font-size: 12px; color: var(--text-lo); }
|
||||
/* decimals: Bender's point is nearly invisible at small sizes (4.2 reads as 42), so small figures use the CJK face */
|
||||
.bp-figs b { font: 600 13px var(--font-cjk); color: var(--text-hi); text-align: right; font-variant-numeric: tabular-nums; }
|
||||
.bp-meta { grid-column: 1 / -1; font-size: 12px; color: var(--text-lo); }
|
||||
|
||||
.bp-strip { position: relative; height: 70px; background: var(--bg-0); border: 1px solid var(--line); }
|
||||
.bp-strip canvas { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }
|
||||
.bp-freeze { position: absolute; inset: 0; display: grid; place-items: center; text-align: center; padding: 0 12px;
|
||||
font-size: 12px; color: var(--text-md); background: rgba(12, 15, 14, .72); }
|
||||
.bp-key { display: flex; flex-wrap: wrap; gap: 2px 12px; font-size: 11.5px; color: var(--text-lo); }
|
||||
.bp-key i { display: inline-block; width: 8px; height: 8px; margin-right: 4px; }
|
||||
|
||||
.bp .bp-measure { position: relative; overflow: hidden; width: 100%; min-height: 36px; font-weight: 700;
|
||||
color: var(--text-on-mint); background: var(--mint-500); border-color: var(--mint-500); }
|
||||
.bp .bp-measure:hover { background: var(--mint-400); border-color: var(--mint-400); }
|
||||
.bp-fill { position: absolute; inset: 0; background: var(--mint-800); transform-origin: left; transform: scaleX(0); }
|
||||
.bp .bp-measure.is-running { color: var(--text-hi); background: var(--mint-900); border-color: var(--mint-700); cursor: default; opacity: 1; }
|
||||
.bp-measure.is-running .bp-fill { animation: bp-fill 10s linear forwards; }
|
||||
.bp-measure span { position: relative; }
|
||||
@keyframes bp-fill { to { transform: scaleX(1); } }
|
||||
|
||||
.bp-result { display: grid; gap: 8px; border-top: 1px solid var(--line); padding-top: 10px; }
|
||||
.bp-verdict { display: flex; align-items: baseline; flex-wrap: wrap; gap: 2px 8px; color: var(--text-hi); font-weight: 700; }
|
||||
.bp-verdict::before { content: ''; width: 10px; height: 10px; flex: none; align-self: center; background: var(--v, var(--mint-500)); }
|
||||
.bp-verdict small { font-weight: 400; color: var(--text-lo); font-size: 12px; }
|
||||
.bp-sum { display: grid; grid-template-columns: repeat(3, 1fr); gap: 6px; }
|
||||
.bp-sum div { display: grid; background: var(--bg-1); border: 1px solid var(--line); padding: 5px 7px; min-width: 0; }
|
||||
.bp-sum span { font-size: 11.5px; color: var(--text-lo); }
|
||||
.bp-sum b { font: 700 16px/1.3 var(--font-cjk); color: var(--text-hi); font-variant-numeric: tabular-nums; }
|
||||
.bp-result details { font-size: 12px; }
|
||||
.bp-result summary { cursor: pointer; color: var(--text-md); }
|
||||
.bp-report { margin: 6px 0 0; font: 12px/1.6 var(--font-cjk); color: var(--text-hi); white-space: pre-wrap; word-break: break-word;
|
||||
background: var(--bg-0); border: 1px solid var(--line); padding: 6px 8px; max-height: 30vh; overflow: auto; user-select: text; }
|
||||
|
||||
/* collapsed: a chip with the live figures and a mini strip */
|
||||
.bp .bp-chip { display: none; align-items: center; gap: 8px; width: 100%; padding: 4px 8px 4px 10px; background: none; border: 0; }
|
||||
.bp-chip b { font: 600 20px/1 var(--font-num); color: var(--text-hi); font-variant-numeric: tabular-nums; }
|
||||
.bp-chip span { font-size: 12px; color: var(--text-lo); white-space: nowrap; }
|
||||
.bp-chip canvas { width: 72px; height: 22px; display: block; margin-left: 4px; }
|
||||
.bp.is-collapsed { width: auto; }
|
||||
.bp.is-collapsed .bp-body { display: none; }
|
||||
.bp.is-collapsed .bp-chip { display: flex; }
|
||||
@media (prefers-reduced-motion: reduce) { .bp-measure.is-running .bp-fill { animation: none; transform: scaleX(1); } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="field"></div>
|
||||
<section class="bp brackets" id="panel" aria-label="战斗性能测试">
|
||||
<button class="bp-chip" id="expand" type="button" aria-expanded="false" aria-controls="bp-body" title="展开面板">
|
||||
<b id="chip-fps">–</b><span id="chip-ms">FPS</span><canvas id="chip-strip" aria-hidden="true"></canvas>
|
||||
</button>
|
||||
<div class="bp-body" id="bp-body">
|
||||
<div class="bp-head"><h1>战斗性能测试</h1><button id="collapse" type="button" aria-expanded="true" aria-controls="bp-body">收起</button></div>
|
||||
<div class="bp-ctl">
|
||||
<select id="spec" aria-label="战斗场景"></select>
|
||||
<select id="quality" aria-label="画质"><option value="high">高画质</option><option value="medium">中画质</option><option value="low">低画质</option></select>
|
||||
<button id="restart" type="button">重新开始</button>
|
||||
</div>
|
||||
<div class="bp-live">
|
||||
<div class="bp-fps"><span id="fps">–</span><small>FPS</small></div>
|
||||
<div class="bp-figs">
|
||||
<span>帧耗时</span><b id="f-frame">–</b>
|
||||
<span>渲染 CPU</span><b id="f-view">–</b>
|
||||
<span>模拟单步</span><b id="f-sim">–</b>
|
||||
</div>
|
||||
<div class="bp-meta" id="meta">正在加载战斗…</div>
|
||||
</div>
|
||||
<div class="bp-strip" title="近 10 秒逐帧耗时">
|
||||
<canvas id="strip" role="img" aria-label="近 10 秒逐帧耗时"></canvas>
|
||||
<div class="bp-freeze" id="freeze" hidden>测量进行中,图表暂停刷新以避免干扰结果</div>
|
||||
</div>
|
||||
<div class="bp-key"><span><i style="background:var(--mint-500)"></i>≤ 16.7 ms(≥ 60 FPS)</span><span><i style="background:var(--amber)"></i>16.7–33.3 ms</span><span><i style="background:var(--red)"></i>> 33.3 ms(卡顿帧)</span></div>
|
||||
<button class="bp-measure" id="measure" type="button" disabled><i class="bp-fill" aria-hidden="true"></i><span id="measure-label">开始测量(10 秒)</span></button>
|
||||
<div class="bp-result" id="result" hidden aria-live="polite">
|
||||
<div class="bp-verdict" id="verdict"></div>
|
||||
<div class="bp-sum">
|
||||
<div><span>平均帧率</span><b id="r-fps">–</b></div>
|
||||
<div><span>P95 帧耗时</span><b id="r-p95">–</b></div>
|
||||
<div><span>卡顿帧占比</span><b id="r-over">–</b></div>
|
||||
</div>
|
||||
<details><summary>完整报告(提交反馈时请附上)</summary><pre class="bp-report" id="out"></pre></details>
|
||||
<button id="copy" type="button">复制报告</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<script type="module" src="/dev/battle-perf.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,331 @@
|
||||
// Battle perf page (dev): one real battle in the browser, run the way the match screen runs it — the client-side battle
|
||||
// runner (js/battle/runner.js: the shared sim at the battle speed) feeding the field view (js/render/app.js) — from a
|
||||
// BattleSpec captured off a bot match (tools/capture-specs.mjs → /dev/perf/<name>.json). Players can open it on a
|
||||
// phone and report the numbers (「开始测量」→ a verdict and a copyable report); tools/perfbench.mjs drives it headlessly.
|
||||
//
|
||||
// Query: ?spec=<name> (default: the first of /dev/perf/index.json) &quality=high|medium|low &panel=0 (hide the panel)
|
||||
// &board=2d|3d (render/app.js reads it)
|
||||
// Puppeteer hook: window.__perf = { ready, error, view, runner, sample(ms) → figures over `ms` of animation frames }
|
||||
//
|
||||
// The panel must not disturb what it measures: frame times go into a ring buffer (one store per frame), the strip is
|
||||
// redrawn 4× a second from it, binned to pixel columns, and both the strip and the live figures stand still during a
|
||||
// measurement (only a CSS progress animation runs).
|
||||
|
||||
import { createFieldView } from '../js/render/app.js';
|
||||
import { data } from '../js/data.js';
|
||||
import { assets } from '../js/assets.js';
|
||||
import { createBattleRunner } from '../js/battle/runner.js';
|
||||
import { round, frameFigures } from './frame-stats.js';
|
||||
|
||||
const $ = (id) => document.getElementById(id);
|
||||
const q = new URLSearchParams(location.search);
|
||||
const perf = { ready: false, error: null };
|
||||
window.__perf = perf;
|
||||
|
||||
const longTasks = [];
|
||||
try {
|
||||
new PerformanceObserver((list) => { for (const e of list.getEntries()) longTasks.push(e.duration); }).observe({ type: 'longtask', buffered: true });
|
||||
} catch { /* not supported */ }
|
||||
|
||||
// Frame-time budgets (ms) shared by the verdict, the strip colours and the stutter share: 16.7 / 33.3 ms are 60 / 30 FPS.
|
||||
// The 60 FPS budget allows 5% for vsync jitter (a 60 Hz display measures frames of 16.6–17.4 ms); a frame over the 30 FPS
|
||||
// budget counts as a stutter frame.
|
||||
export const FRAME_60_MS = 17.5;
|
||||
export const FRAME_30_MS = 33.4;
|
||||
const QUALITY_NAME = { high: '高', medium: '中', low: '低' };
|
||||
|
||||
/**
|
||||
* Frame figures over `ms` of animation frames: average fps, frame-time percentiles, the share of frames over 33.4 ms
|
||||
* (below 30 fps), long tasks, the sim's ticks and their cost (runner.stats), and the view's own CPU / render times.
|
||||
* The first frame time is partial (from the call to the first frame) and dropped, so the sample runs for at least two
|
||||
* frames whatever `ms` is: a sample that saw a single frame (a short `ms`, a frame longer than `ms`) had no frame time
|
||||
* left and divided by zero.
|
||||
*/
|
||||
perf.sample = (ms = 10000) => new Promise((resolve) => {
|
||||
const deltas = [];
|
||||
let last = performance.now();
|
||||
const t0 = last;
|
||||
const r0 = { ...perf.runner.stats() };
|
||||
const lt0 = longTasks.length;
|
||||
const step = (t) => {
|
||||
deltas.push(t - last);
|
||||
last = t;
|
||||
if (t - t0 < ms || deltas.length < 2) { requestAnimationFrame(step); return; }
|
||||
deltas.shift();
|
||||
const r1 = perf.runner.stats();
|
||||
const v = perf.view.stats();
|
||||
const lt = longTasks.slice(lt0);
|
||||
const ticks = r1.ticks - r0.ticks;
|
||||
resolve({
|
||||
...frameFigures(deltas, FRAME_30_MS),
|
||||
longTasks: lt.length, longTaskMs: Math.round(lt.reduce((a, b) => a + b, 0)),
|
||||
ticks, simMs: round(r1.stepMs - r0.stepMs), simMsPerTick: ticks > 0 ? round((r1.stepMs - r0.stepMs) / ticks, 3) : null,
|
||||
viewCpuMs: v.cpuMs, renderMs: v.renderMs, units: v.units, lod: v.lod, particles: v.particles,
|
||||
board: v.board3d?.on ? '3d' : '2d', done: !!perf.runner.state()?.done,
|
||||
});
|
||||
};
|
||||
requestAnimationFrame(step);
|
||||
});
|
||||
|
||||
/** The device line of a report: renderer, screen, cores (what a phone report needs to be comparable). */
|
||||
function deviceInfo() {
|
||||
let gpu = '?';
|
||||
try {
|
||||
const gl = document.createElement('canvas').getContext('webgl2') || document.createElement('canvas').getContext('webgl');
|
||||
const ext = gl && gl.getExtension('WEBGL_debug_renderer_info');
|
||||
gpu = ext ? gl.getParameter(ext.UNMASKED_RENDERER_WEBGL) : (gl ? gl.getParameter(gl.RENDERER) : 'no WebGL');
|
||||
} catch { /* ignore */ }
|
||||
return {
|
||||
ua: navigator.userAgent, gpu, cores: navigator.hardwareConcurrency || null, memoryGB: navigator.deviceMemory || null,
|
||||
viewport: `${innerWidth}×${innerHeight}`, dpr: devicePixelRatio,
|
||||
};
|
||||
}
|
||||
|
||||
/** The copyable report: one item per line, the panel's terms and units (pasted into feedback as is). */
|
||||
function report(spec, title, quality, s) {
|
||||
const d = deviceInfo();
|
||||
const lines = [
|
||||
'战斗性能测试报告(/dev/battle-perf.html)',
|
||||
`场景:${title}(${spec})`,
|
||||
`画质:${QUALITY_NAME[quality] || quality};棋盘:${s.board === '3d' ? '3D' : '2D'}`,
|
||||
`帧率:${s.fps} FPS`,
|
||||
`帧耗时:P50 ${s.p50} ms,P95 ${s.p95} ms,P99 ${s.p99} ms`,
|
||||
`卡顿帧占比(单帧 > 33.3 ms):${s.over33}%`,
|
||||
`模拟单步:${s.simMsPerTick} ms(共 ${s.ticks} 步)`,
|
||||
`渲染 CPU:${s.viewCpuMs} ms;渲染提交:${s.renderMs} ms`,
|
||||
`场上单位:${s.units};自适应负载档位:${s.lod}`,
|
||||
`长任务:${s.longTasks} 次,共 ${s.longTaskMs} ms`,
|
||||
`GPU:${d.gpu}`,
|
||||
`设备:${d.cores ?? '未知'} 个 CPU 线程${d.memoryGB ? `,内存约 ${d.memoryGB} GB` : ''};视口 ${d.viewport},像素比 ${d.dpr}`,
|
||||
`浏览器:${d.ua}`,
|
||||
];
|
||||
if (s.done) lines.splice(10, 0, '注意:战斗在测量期间结束,结果仅供参考');
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
/** The verdict a player reads first, on the frame-time budgets of the strip (60 / 30 FPS). */
|
||||
export function verdictOf(s) {
|
||||
if (s.p95 <= FRAME_60_MS && s.over33 < 1) return { label: '流畅', note: `P95 帧耗时 ${s.p95} ms,达到 60 FPS`, color: '--mint-500' };
|
||||
if (s.p95 <= FRAME_30_MS && s.over33 < 1) return { label: '基本流畅', note: `P95 帧耗时 ${s.p95} ms,介于 30–60 FPS`, color: '--amber' };
|
||||
return { label: '卡顿', note: `卡顿帧占比 ${s.over33}%,P95 帧耗时 ${s.p95} ms`, color: '--red' };
|
||||
}
|
||||
|
||||
// ---- frame ring buffer + strip ------------------------------------------------------------------------------------
|
||||
|
||||
const RING = 4096;
|
||||
const ringT = new Float64Array(RING); // frame end (performance.now)
|
||||
const ringD = new Float32Array(RING); // frame time (ms)
|
||||
let ringN = 0;
|
||||
let lastT = 0;
|
||||
function recordFrames(t) {
|
||||
if (lastT) { const i = ringN % RING; ringT[i] = t; ringD[i] = t - lastT; ringN++; }
|
||||
lastT = t;
|
||||
requestAnimationFrame(recordFrames);
|
||||
}
|
||||
|
||||
const SPAN_MS = 10000; // the strip shows the last 10 s
|
||||
const TOP_MS = 50; // frame times above clip to the top (a red spike)
|
||||
let palette = null;
|
||||
function colors() {
|
||||
if (!palette) {
|
||||
const cs = getComputedStyle(document.documentElement);
|
||||
const v = (n) => cs.getPropertyValue(n).trim();
|
||||
palette = { good: v('--mint-500'), warn: v('--amber'), bad: v('--red'), line: v('--line-2'), text: v('--text-lo') };
|
||||
}
|
||||
return palette;
|
||||
}
|
||||
const band = (ms) => (ms <= FRAME_60_MS ? 'good' : ms <= FRAME_30_MS ? 'warn' : 'bad');
|
||||
|
||||
/** Draw the last SPAN_MS of frame times into `cv`: one column per pixel (its slowest frame), budget lines at 60 / 30 fps. */
|
||||
function drawStrip(cv, { labels = true } = {}) {
|
||||
const w = cv.clientWidth, h = cv.clientHeight;
|
||||
if (!w || !h) return;
|
||||
const dpr = Math.min(2, devicePixelRatio || 1);
|
||||
if (cv.width !== Math.round(w * dpr) || cv.height !== Math.round(h * dpr)) { cv.width = Math.round(w * dpr); cv.height = Math.round(h * dpr); }
|
||||
const g = cv.getContext('2d');
|
||||
g.setTransform(dpr, 0, 0, dpr, 0, 0);
|
||||
g.clearRect(0, 0, w, h);
|
||||
const c = colors();
|
||||
const pad = labels ? 4 : 1;
|
||||
const y = (ms) => h - pad - (Math.min(ms, TOP_MS) / TOP_MS) * (h - pad * 2);
|
||||
g.fillStyle = c.line;
|
||||
for (const ms of [16.7, 33.3]) g.fillRect(0, Math.round(y(ms)), w, 1);
|
||||
if (labels) {
|
||||
g.fillStyle = c.text;
|
||||
g.font = '10px "Noto Sans SC", sans-serif';
|
||||
g.textAlign = 'right';
|
||||
g.fillText('60 FPS', w - 3, y(16.7) - 3);
|
||||
g.fillText('30 FPS', w - 3, y(33.3) - 3);
|
||||
}
|
||||
const now = performance.now();
|
||||
const cols = new Float32Array(Math.ceil(w));
|
||||
for (let k = 1; k <= Math.min(ringN, RING); k++) {
|
||||
const i = (ringN - k) % RING;
|
||||
const age = now - ringT[i];
|
||||
if (age > SPAN_MS) break;
|
||||
const x = Math.floor(w - 1 - (age / SPAN_MS) * w);
|
||||
if (x >= 0 && ringD[i] > cols[x]) cols[x] = ringD[i];
|
||||
}
|
||||
const barW = labels ? 1 : 1.5;
|
||||
for (let x = 0; x < cols.length; x++) {
|
||||
const ms = cols[x];
|
||||
if (!ms) continue;
|
||||
g.fillStyle = c[band(ms)];
|
||||
const top = y(ms);
|
||||
g.fillRect(x, top, barW, h - pad - top);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- panel ----------------------------------------------------------------------------------------------------------
|
||||
|
||||
const PANEL_KEY = 'sp.dev.battlePerfPanel';
|
||||
function loadPanelPref() {
|
||||
try { return localStorage.getItem(PANEL_KEY); } catch { return null; }
|
||||
}
|
||||
function savePanelPref(v) {
|
||||
try { localStorage.setItem(PANEL_KEY, v); } catch { /* private mode */ }
|
||||
}
|
||||
|
||||
function setupPanel() {
|
||||
const panel = $('panel');
|
||||
const set = (collapsed, save = true) => {
|
||||
panel.classList.toggle('is-collapsed', collapsed);
|
||||
$('expand').setAttribute('aria-expanded', String(!collapsed));
|
||||
$('collapse').setAttribute('aria-expanded', String(!collapsed));
|
||||
if (save) savePanelPref(collapsed ? 'collapsed' : 'open');
|
||||
};
|
||||
const pref = loadPanelPref();
|
||||
// phones (a landscape phone is short): start folded, the battle is what is being measured
|
||||
set(pref ? pref === 'collapsed' : (innerWidth < 900 || innerHeight < 500), false);
|
||||
$('collapse').onclick = () => { set(true); $('expand').focus(); };
|
||||
$('expand').onclick = () => { set(false); $('collapse').focus(); };
|
||||
}
|
||||
|
||||
async function main() {
|
||||
if (q.get('panel') === '0') $('panel').classList.add('is-hidden');
|
||||
setupPanel();
|
||||
const index = await fetch('/dev/perf/index.json').then((r) => (r.ok ? r.json() : []), () => []);
|
||||
const specName = q.get('spec') || index[0]?.name;
|
||||
if (!specName) throw new Error('未找到战斗数据。请先运行 node tools/capture-specs.mjs 生成。');
|
||||
const quality = ['high', 'medium', 'low'].includes(q.get('quality')) ? q.get('quality') : 'high';
|
||||
for (const s of index) $('spec').append(new Option(s.title || s.name, s.name));
|
||||
$('spec').value = specName;
|
||||
$('quality').value = quality;
|
||||
const reload = () => {
|
||||
const p = new URLSearchParams(location.search);
|
||||
p.set('spec', $('spec').value);
|
||||
p.set('quality', $('quality').value);
|
||||
location.search = p.toString();
|
||||
};
|
||||
$('restart').onclick = reload;
|
||||
$('spec').onchange = reload;
|
||||
$('quality').onchange = reload;
|
||||
|
||||
await data.loadAll('chess', 'tokens', 'items', 'enemies', 'stages', 'bonds', 'config');
|
||||
await assets.ready();
|
||||
const msg = await fetch(`/dev/perf/${encodeURIComponent(specName)}.json`).then((r) => {
|
||||
if (!r.ok) throw new Error(`战斗数据 ${specName} 加载失败(HTTP ${r.status})。请确认该文件位于 public/dev/perf/。`);
|
||||
return r.json();
|
||||
});
|
||||
const view = await createFieldView($('field'), { data, assets, settings: { quality, damageNumbers: true } });
|
||||
view.setStage(data.lookup('stages', msg.spec.stageId));
|
||||
// the runner needs no server here: reports go nowhere, the battle runs from its start
|
||||
const net = { on: () => () => {}, send() {}, request: async () => ({ ok: true }) };
|
||||
const runner = createBattleRunner({ net, store: null });
|
||||
const camKind = msg.kind === 'hidden' ? 'boss' : msg.kind;
|
||||
runner.on('field', (field) => { view.enterBattle(field); view.setCamera(camKind, { rect: field.rect, side: 'L', instant: true }); });
|
||||
runner.on('snap', (snap) => view.pushSnapshot(snap));
|
||||
runner.on('ev', (ev) => view.pushEvents(ev));
|
||||
perf.view = view;
|
||||
perf.runner = runner;
|
||||
await runner.onStart({ ...msg, elapsed: 0, authoritative: true, watch: false, done: false });
|
||||
perf.ready = true;
|
||||
// frame times from here on: the loading hitches before the battle are not the battle's
|
||||
requestAnimationFrame(recordFrames);
|
||||
|
||||
let measuring = false;
|
||||
let prev = { ...runner.stats() };
|
||||
const live = () => {
|
||||
if (measuring) return;
|
||||
const v = view.stats();
|
||||
const r = runner.stats();
|
||||
const ticks = r.ticks - prev.ticks;
|
||||
const simMs = ticks > 0 ? (r.stepMs - prev.stepMs) / ticks : 0;
|
||||
prev = { ...r };
|
||||
const fps = Math.round(v.fps);
|
||||
$('fps').textContent = fps;
|
||||
$('f-frame').textContent = `${v.frameMs.toFixed(1)} ms`;
|
||||
$('f-view').textContent = `${v.cpuMs.toFixed(1)} ms`;
|
||||
$('f-sim').textContent = `${simMs.toFixed(2)} ms`;
|
||||
$('chip-fps').textContent = fps;
|
||||
$('chip-ms').textContent = `FPS / ${v.frameMs.toFixed(1)} ms`;
|
||||
const st = runner.state();
|
||||
$('meta').textContent = st?.done
|
||||
? '战斗已结束。如需继续测量,请点击「重新开始」。'
|
||||
: `场上单位 ${v.units} 个,自适应负载 ${v.lod} 档,${v.board3d?.on ? '3D' : '2D'} 棋盘`;
|
||||
};
|
||||
setInterval(live, 500);
|
||||
setInterval(() => {
|
||||
if (measuring) return;
|
||||
if ($('panel').classList.contains('is-collapsed')) drawStrip($('chip-strip'), { labels: false });
|
||||
else drawStrip($('strip'));
|
||||
}, 250);
|
||||
|
||||
const btn = $('measure');
|
||||
btn.disabled = false;
|
||||
btn.onclick = async () => {
|
||||
if (measuring) return;
|
||||
measuring = true;
|
||||
btn.classList.add('is-running');
|
||||
btn.setAttribute('aria-disabled', 'true');
|
||||
$('freeze').hidden = false;
|
||||
const t0 = Date.now();
|
||||
const tick = setInterval(() => { $('measure-label').textContent = `测量中(剩余 ${Math.max(1, 10 - Math.floor((Date.now() - t0) / 1000))} 秒)`; }, 1000);
|
||||
$('measure-label').textContent = '测量中(剩余 10 秒)';
|
||||
const s = await perf.sample(10000);
|
||||
clearInterval(tick);
|
||||
measuring = false;
|
||||
btn.classList.remove('is-running');
|
||||
btn.removeAttribute('aria-disabled');
|
||||
$('freeze').hidden = true;
|
||||
$('measure-label').textContent = '重新测量(10 秒)';
|
||||
|
||||
const v = verdictOf(s);
|
||||
const verdict = $('verdict');
|
||||
verdict.style.setProperty('--v', `var(${v.color})`);
|
||||
verdict.replaceChildren(document.createTextNode(v.label));
|
||||
const note = document.createElement('small');
|
||||
note.textContent = v.note;
|
||||
verdict.append(note);
|
||||
$('r-fps').textContent = s.fps;
|
||||
$('r-p95').textContent = `${s.p95} ms`;
|
||||
$('r-over').textContent = `${s.over33}%`;
|
||||
const text = report(specName, index.find((x) => x.name === specName)?.title || specName, quality, s);
|
||||
$('out').textContent = text;
|
||||
$('result').hidden = false;
|
||||
const copy = $('copy');
|
||||
copy.textContent = '复制报告';
|
||||
copy.onclick = () => {
|
||||
const done = () => { copy.textContent = '已复制到剪贴板'; };
|
||||
const fallback = () => {
|
||||
const d = $('result').querySelector('details');
|
||||
d.open = true;
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents($('out'));
|
||||
const sel = getSelection();
|
||||
sel.removeAllRanges();
|
||||
sel.addRange(range);
|
||||
copy.textContent = '无法写入剪贴板,已选中报告文本,请手动复制';
|
||||
};
|
||||
if (navigator.clipboard?.writeText) navigator.clipboard.writeText(text).then(done, fallback);
|
||||
else fallback();
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
perf.error = String(err?.stack || err);
|
||||
console.error(err);
|
||||
const meta = document.getElementById('meta');
|
||||
if (meta) meta.textContent = `加载失败:${err?.message || err}`;
|
||||
});
|
||||
@@ -0,0 +1,22 @@
|
||||
// public/dev/frame-stats.js — the frame figures of the battle perf page (battle-perf.js), free of the DOM so the Node
|
||||
// tests can import them (test/perfbench.test.js).
|
||||
|
||||
/** `v` rounded to `d` decimals; null for NaN / ±Infinity (a figure the report shows as missing, not as 0). */
|
||||
export const round = (v, d = 1) => (Number.isFinite(v) ? Math.round(v * 10 ** d) / 10 ** d : null);
|
||||
|
||||
/**
|
||||
* Average fps, frame-time percentiles (ms) and the share (%) of frames over `stutterMs`, from a sample's frame times.
|
||||
* Every figure is null without a frame time (perf.sample() always records one, but an empty run must not divide by 0).
|
||||
* @param {number[]} deltas frame times in ms (sorted in place)
|
||||
* @param {number} stutterMs a frame over this counts as a stutter frame
|
||||
*/
|
||||
export function frameFigures(deltas, stutterMs) {
|
||||
if (!deltas.length) return { fps: null, p50: null, p95: null, p99: null, over33: null };
|
||||
deltas.sort((a, b) => a - b);
|
||||
const pick = (p) => deltas[Math.min(deltas.length - 1, Math.floor(deltas.length * p))];
|
||||
const avg = deltas.reduce((a, b) => a + b, 0) / deltas.length;
|
||||
return {
|
||||
fps: avg > 0 ? round(1000 / avg) : null, p50: round(pick(0.5)), p95: round(pick(0.95)), p99: round(pick(0.99)),
|
||||
over33: round((deltas.filter((x) => x > stutterMs).length / deltas.length) * 100),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{"t":"b.start","battleId":"7.14.63.b1","fieldId":"b1","kind":"boss","spec":{"v":1,"battleId":"7.14.63.b1","fieldId":"b1","kind":"boss","seed":3380376830,"modeId":"mode_multi_hard","round":14,"stageId":"act2autochess_m01","rect":{"r0":0,"r1":5,"c0":0,"c1":20},"timeLimit":null,"players":[{"playerId":"p_0","seat":0,"side":"L","colOffset":0,"units":[{"uid":123,"kind":"chess","chessId":"chess_char_4_02_b","row":12,"col":8,"dir":"DOWN","items":[],"skillIndex":2,"moduleId":"uniequip_002_mostma"},{"uid":125,"kind":"chess","chessId":"chess_char_4_20_a","row":11,"col":4,"dir":"RIGHT","items":["chess_item_1_05_e_a","chess_item_4_03_e_a"],"skillIndex":2},{"uid":208,"kind":"chess","chessId":"chess_char_3_17_b","row":11,"col":8,"dir":"RIGHT","items":["chess_item_3_01_e_a","chess_item_2_01_e_a"],"skillIndex":1,"moduleId":"uniequip_002_shotst"},{"uid":41,"kind":"chess","chessId":"chess_char_1_17_b","row":10,"col":4,"dir":"RIGHT","items":[],"skillIndex":1,"moduleId":"uniequip_002_indigo"},{"uid":169,"kind":"chess","chessId":"chess_char_4_10_a","row":10,"col":5,"dir":"RIGHT","items":[],"skillIndex":1},{"uid":96,"kind":"chess","chessId":"chess_char_1_19_b","row":10,"col":8,"dir":"RIGHT","items":["chess_item_1_01_e_a","chess_item_4_04_e_b"],"skillIndex":1,"moduleId":"uniequip_002_wildmn"},{"uid":168,"kind":"chess","chessId":"chess_char_4_14_b","row":9,"col":8,"dir":"RIGHT","items":["chess_item_3_04_e_a","chess_item_5_02_e_b"],"skillIndex":1,"moduleId":"uniequip_002_lionhd"},{"uid":209,"kind":"chess","chessId":"chess_char_6_19_a","row":9,"col":9,"dir":"UP","items":["chess_item_3_05_e_a","chess_item_4_05_e_a"],"skillIndex":2}],"bonds":{"yanShip":{"count":0,"active":false,"tier":0,"layers":0},"sargonShip":{"count":0,"active":false,"tier":0,"layers":0},"victoriaShip":{"count":0,"active":false,"tier":0,"layers":0},"kjeragShip":{"count":1,"active":false,"tier":0,"layers":8},"lateranoShip":{"count":1,"active":false,"tier":0,"layers":0},"egirShip":{"count":0,"active":false,"tier":0,"layers":0},"siracusaShip":{"count":1,"active":false,"tier":0,"layers":0},"kazimierzShip":{"count":4,"active":true,"tier":1,"layers":74},"preciShip":{"count":3,"active":true,"tier":2,"layers":98},"swiftShip":{"count":3,"active":true,"tier":1,"layers":82},"skillfulShip":{"count":0,"active":false,"tier":0,"layers":0},"arcaneShip":{"count":3,"active":true,"tier":2,"layers":26},"steadShip":{"count":0,"active":false,"tier":0,"layers":0},"deputShip":{"count":0,"active":false,"tier":0,"layers":0},"visiShip":{"count":0,"active":false,"tier":0,"layers":0},"miraShip":{"count":0,"active":false,"tier":0,"layers":0},"investShip":{"count":0,"active":false,"tier":0,"layers":0},"raidShip":{"count":0,"active":false,"tier":0,"layers":0},"indomShip":{"count":0,"active":false,"tier":0,"layers":4},"maniShip":{"count":0,"active":false,"tier":0,"layers":0},"emptyShip":{"count":0,"active":false,"tier":0,"layers":0},"soloShip":{"count":1,"active":true,"tier":1,"layers":0},"suntShip":{"count":5,"active":true,"tier":2,"layers":0}},"bandId":"band_sarkazb","playerEffects":[],"deviceOverrides":{},"lpForBoss":1572,"contentInfo":{"round":14,"kind":"boss","shopLevel":6,"funds":0,"handUnits":4,"handChess":4,"roundStats":{"refreshes":3,"buys":4,"sells":0,"spent":10,"gainedChess":0,"arts":0},"matchBands":["band_sarkazb","band_kirara","band_bldsk","band_cathy"],"teammates":[{"playerId":"p_1","bandId":"band_kirara"},{"playerId":"ai_2","bandId":"band_bldsk"},{"playerId":"ai_3","bandId":"band_cathy"}]}},{"playerId":"p_1","seat":1,"side":"R","colOffset":8,"units":[{"uid":243,"kind":"chess","chessId":"chess_char_4_20_b","row":12,"col":8,"dir":"DOWN","items":["chess_item_4_12_e_a","chess_item_3_08_e_b"],"skillIndex":2,"moduleId":"uniequip_002_fartth"},{"uid":240,"kind":"chess","chessId":"chess_char_6_16_a","row":11,"col":4,"dir":"DOWN","items":[],"skillIndex":2},{"uid":73,"kind":"chess","chessId":"chess_char_1_01_b","row":11,"col":8,"dir":"RIGHT","items":["chess_item_1_01_e_a"],"skillIndex":1,"moduleId":"uniequip_002_inside"},{"uid":246,"kind":"chess","chessId":"chess_char_6_05_a","row":10,"col":4,"dir":"RIGHT","items":["chess_item_4_02_e_a","chess_item_4_08_e_a"],"skillIndex":2},{"uid":175,"kind":"chess","chessId":"chess_char_2_18_b","row":10,"col":5,"dir":"RIGHT","items":["chess_item_4_10_e_b","chess_item_2_03_e_a"],"skillIndex":0,"moduleId":"uniequip_002_ashlok"},{"uid":130,"kind":"chess","chessId":"chess_char_4_01_a","row":10,"col":7,"dir":"RIGHT","items":["chess_item_4_04_e_a","chess_item_3_05_e_a"],"skillIndex":2},{"uid":177,"kind":"chess","chessId":"chess_char_6_19_a","row":10,"col":8,"dir":"RIGHT","items":["chess_item_2_01_e_a","chess_item_4_06_e_a"],"skillIndex":2},{"uid":100,"kind":"chess","chessId":"chess_char_4_02_a","row":9,"col":9,"dir":"UP","items":["chess_item_3_07_e_a","chess_item_3_06_e_b"],"skillIndex":2}],"bonds":{"yanShip":{"count":0,"active":false,"tier":0,"layers":11},"sargonShip":{"count":1,"active":false,"tier":0,"layers":4},"victoriaShip":{"count":0,"active":false,"tier":0Line truncated
|
||||
@@ -0,0 +1 @@
|
||||
{"t":"b.start","battleId":"7.15.65.b1","fieldId":"b1","kind":"hidden","spec":{"v":1,"battleId":"7.15.65.b1","fieldId":"b1","kind":"hidden","seed":725852008,"modeId":"mode_multi_hard","round":15,"stageId":"act2autochess_m01","rect":{"r0":0,"r1":5,"c0":0,"c1":20},"timeLimit":null,"players":[{"playerId":"p_0","seat":0,"side":"L","colOffset":0,"units":[{"uid":123,"kind":"chess","chessId":"chess_char_4_02_b","row":12,"col":8,"dir":"DOWN","items":["chess_item_3_03_e_a"],"skillIndex":2,"moduleId":"uniequip_002_mostma"},{"uid":125,"kind":"chess","chessId":"chess_char_4_20_a","row":11,"col":4,"dir":"RIGHT","items":["chess_item_1_05_e_a","chess_item_4_03_e_a"],"skillIndex":2},{"uid":208,"kind":"chess","chessId":"chess_char_3_17_b","row":11,"col":8,"dir":"RIGHT","items":["chess_item_3_01_e_a","chess_item_2_01_e_a"],"skillIndex":1,"moduleId":"uniequip_002_shotst"},{"uid":41,"kind":"chess","chessId":"chess_char_1_17_b","row":10,"col":4,"dir":"RIGHT","items":[],"skillIndex":1,"moduleId":"uniequip_002_indigo"},{"uid":169,"kind":"chess","chessId":"chess_char_4_10_a","row":10,"col":5,"dir":"RIGHT","items":[],"skillIndex":1},{"uid":96,"kind":"chess","chessId":"chess_char_1_19_b","row":10,"col":8,"dir":"RIGHT","items":["chess_item_1_01_e_a","chess_item_4_04_e_b"],"skillIndex":1,"moduleId":"uniequip_002_wildmn"},{"uid":168,"kind":"chess","chessId":"chess_char_4_14_b","row":9,"col":8,"dir":"RIGHT","items":["chess_item_3_04_e_a","chess_item_5_02_e_b"],"skillIndex":1,"moduleId":"uniequip_002_lionhd"},{"uid":209,"kind":"chess","chessId":"chess_char_6_19_a","row":9,"col":9,"dir":"UP","items":["chess_item_4_05_e_a","chess_item_4_02_e_a"],"skillIndex":2}],"bonds":{"yanShip":{"count":0,"active":false,"tier":0,"layers":0},"sargonShip":{"count":0,"active":false,"tier":0,"layers":0},"victoriaShip":{"count":0,"active":false,"tier":0,"layers":0},"kjeragShip":{"count":1,"active":false,"tier":0,"layers":8},"lateranoShip":{"count":1,"active":false,"tier":0,"layers":0},"egirShip":{"count":0,"active":false,"tier":0,"layers":0},"siracusaShip":{"count":1,"active":false,"tier":0,"layers":0},"kazimierzShip":{"count":4,"active":true,"tier":1,"layers":74},"preciShip":{"count":3,"active":true,"tier":2,"layers":108},"swiftShip":{"count":3,"active":true,"tier":1,"layers":92},"skillfulShip":{"count":0,"active":false,"tier":0,"layers":0},"arcaneShip":{"count":3,"active":true,"tier":2,"layers":32},"steadShip":{"count":0,"active":false,"tier":0,"layers":0},"deputShip":{"count":0,"active":false,"tier":0,"layers":0},"visiShip":{"count":0,"active":false,"tier":0,"layers":0},"miraShip":{"count":0,"active":false,"tier":0,"layers":0},"investShip":{"count":0,"active":false,"tier":0,"layers":0},"raidShip":{"count":0,"active":false,"tier":0,"layers":0},"indomShip":{"count":0,"active":false,"tier":0,"layers":4},"maniShip":{"count":0,"active":false,"tier":0,"layers":0},"emptyShip":{"count":0,"active":false,"tier":0,"layers":0},"soloShip":{"count":1,"active":true,"tier":1,"layers":0},"suntShip":{"count":5,"active":true,"tier":2,"layers":0}},"bandId":"band_sarkazb","playerEffects":[],"deviceOverrides":{},"lpForBoss":1205,"contentInfo":{"round":15,"kind":"hidden","shopLevel":6,"funds":0,"handUnits":6,"handChess":6,"roundStats":{"refreshes":3,"buys":4,"sells":0,"spent":11,"gainedChess":2,"arts":0},"matchBands":["band_sarkazb","band_kirara","band_bldsk","band_cathy"],"teammates":[{"playerId":"p_1","bandId":"band_kirara"},{"playerId":"ai_2","bandId":"band_bldsk"},{"playerId":"ai_3","bandId":"band_cathy"}]}},{"playerId":"p_1","seat":1,"side":"R","colOffset":8,"units":[{"uid":246,"kind":"chess","chessId":"chess_char_6_05_a","row":12,"col":8,"dir":"DOWN","items":["chess_item_4_02_e_a","chess_item_4_08_e_a"],"skillIndex":2},{"uid":243,"kind":"chess","chessId":"chess_char_4_20_b","row":11,"col":8,"dir":"DOWN","items":["chess_item_4_12_e_a","chess_item_3_08_e_b"],"skillIndex":2,"moduleId":"uniequip_002_fartth"},{"uid":240,"kind":"chess","chessId":"chess_char_6_16_a","row":10,"col":4,"dir":"RIGHT","items":[],"skillIndex":2},{"uid":175,"kind":"chess","chessId":"chess_char_2_18_b","row":10,"col":5,"dir":"RIGHT","items":["chess_item_4_10_e_b","chess_item_2_03_e_a"],"skillIndex":0,"moduleId":"uniequip_002_ashlok"},{"uid":274,"kind":"chess","chessId":"chess_char_2_17_b","row":10,"col":7,"dir":"RIGHT","items":[],"skillIndex":1,"moduleId":"uniequip_002_branch"},{"uid":177,"kind":"chess","chessId":"chess_char_6_19_a","row":10,"col":8,"dir":"RIGHT","items":["chess_item_2_01_e_a","chess_item_4_06_e_b"],"skillIndex":2},{"uid":100,"kind":"chess","chessId":"chess_char_4_02_a","row":9,"col":8,"dir":"RIGHT","items":["chess_item_3_07_e_a","chess_item_3_06_e_b"],"skillIndex":2},{"uid":130,"kind":"chess","chessId":"chess_char_4_01_a","row":9,"col":9,"dir":"UP","items":["chess_item_4_04_e_a","chess_item_3_05_e_a"],"skillIndex":2}],"bonds":{"yanShip":{"count":0,"active":false,"tier":0,"layers":11},"sargonShip":{"count":1,"active":false,"tier":0,"layers":4},"victoriaShip":{"count":0,"active":false,"tLine truncated
|
||||
@@ -0,0 +1 @@
|
||||
{"t":"b.start","battleId":"7.13.58.n:p_0","fieldId":"n:p_0","kind":"normal","spec":{"v":1,"battleId":"7.13.58.n:p_0","fieldId":"n:p_0","kind":"normal","seed":720961930,"modeId":"mode_multi_hard","round":13,"stageId":"act2autochess_m01","rect":{"r0":9,"r1":12,"c0":0,"c1":10},"timeLimit":230,"players":[{"playerId":"p_0","seat":0,"side":"L","colOffset":0,"units":[{"uid":169,"kind":"chess","chessId":"chess_char_4_10_a","row":12,"col":4,"dir":"RIGHT","items":[],"skillIndex":1},{"uid":41,"kind":"chess","chessId":"chess_char_1_17_b","row":12,"col":5,"dir":"DOWN","items":[],"skillIndex":1,"moduleId":"uniequip_002_indigo"},{"uid":208,"kind":"chess","chessId":"chess_char_3_17_b","row":11,"col":4,"dir":"RIGHT","items":["chess_item_3_01_e_a","chess_item_2_01_e_a"],"skillIndex":1,"moduleId":"uniequip_002_shotst"},{"uid":96,"kind":"chess","chessId":"chess_char_1_19_b","row":11,"col":5,"dir":"DOWN","items":["chess_item_4_04_e_a","chess_item_1_01_e_a"],"skillIndex":1,"moduleId":"uniequip_002_wildmn"},{"uid":168,"kind":"chess","chessId":"chess_char_4_14_b","row":11,"col":7,"dir":"LEFT","items":[],"skillIndex":1,"moduleId":"uniequip_002_lionhd"},{"uid":123,"kind":"chess","chessId":"chess_char_4_02_b","row":10,"col":4,"dir":"RIGHT","items":[],"skillIndex":2,"moduleId":"uniequip_002_mostma"},{"uid":209,"kind":"chess","chessId":"chess_char_6_19_a","row":10,"col":5,"dir":"DOWN","items":["chess_item_3_05_e_a","chess_item_4_05_e_a"],"skillIndex":2},{"uid":125,"kind":"chess","chessId":"chess_char_4_20_a","row":9,"col":7,"dir":"UP","items":["chess_item_1_05_e_a","chess_item_4_03_e_a"],"skillIndex":2}],"bonds":{"yanShip":{"count":0,"active":false,"tier":0,"layers":0},"sargonShip":{"count":0,"active":false,"tier":0,"layers":0},"victoriaShip":{"count":0,"active":false,"tier":0,"layers":0},"kjeragShip":{"count":1,"active":false,"tier":0,"layers":8},"lateranoShip":{"count":1,"active":false,"tier":0,"layers":0},"egirShip":{"count":0,"active":false,"tier":0,"layers":0},"siracusaShip":{"count":1,"active":false,"tier":0,"layers":0},"kazimierzShip":{"count":4,"active":true,"tier":1,"layers":58},"preciShip":{"count":3,"active":true,"tier":2,"layers":80},"swiftShip":{"count":3,"active":true,"tier":1,"layers":64},"skillfulShip":{"count":0,"active":false,"tier":0,"layers":0},"arcaneShip":{"count":3,"active":true,"tier":2,"layers":20},"steadShip":{"count":0,"active":false,"tier":0,"layers":0},"deputShip":{"count":0,"active":false,"tier":0,"layers":0},"visiShip":{"count":0,"active":false,"tier":0,"layers":0},"miraShip":{"count":0,"active":false,"tier":0,"layers":0},"investShip":{"count":0,"active":false,"tier":0,"layers":0},"raidShip":{"count":0,"active":false,"tier":0,"layers":0},"indomShip":{"count":0,"active":false,"tier":0,"layers":4},"maniShip":{"count":0,"active":false,"tier":0,"layers":0},"emptyShip":{"count":0,"active":false,"tier":0,"layers":0},"soloShip":{"count":1,"active":true,"tier":1,"layers":0},"suntShip":{"count":5,"active":true,"tier":2,"layers":0}},"bandId":"band_sarkazb","playerEffects":[],"deviceOverrides":{},"contentInfo":{"round":13,"kind":"normal","shopLevel":6,"funds":0,"handUnits":4,"handChess":4,"roundStats":{"refreshes":4,"buys":5,"sells":2,"spent":14,"gainedChess":2,"arts":0},"matchBands":["band_sarkazb","band_kirara","band_bldsk","band_cathy"],"teammates":[{"playerId":"p_1","bandId":"band_kirara"},{"playerId":"ai_2","bandId":"band_bldsk"},{"playerId":"ai_3","bandId":"band_cathy"}]}}],"spawns":[{"time":7,"enemyKey":"enemy_1166_dusbr_2","routeIndex":0,"count":3,"interval":3,"mods":{"hpMul":3.583181,"atkMul":1.948717,"speedMul":1,"slot":"N"},"actionIndex":0,"preview":{"gate":"lower","start":[9,10],"fly":false,"elite":false,"boss":false},"ownerPlayerId":"p_0"},{"time":10,"enemyKey":"enemy_1166_dusbr_2","routeIndex":1,"count":3,"interval":5,"mods":{"hpMul":3.583181,"atkMul":1.948717,"speedMul":1,"slot":"N"},"actionIndex":1,"preview":{"gate":"lower","start":[9,10],"fly":false,"elite":false,"boss":false},"ownerPlayerId":"p_0"},{"time":39,"enemyKey":"enemy_1166_dusbr_2","routeIndex":2,"count":3,"interval":5,"mods":{"hpMul":3.583181,"atkMul":1.948717,"speedMul":1,"slot":"N"},"actionIndex":2,"preview":{"gate":"lower","start":[9,10],"fly":false,"elite":false,"boss":false},"ownerPlayerId":"p_0"},{"time":62,"enemyKey":"enemy_1166_dusbr_2","routeIndex":3,"count":3,"interval":4,"mods":{"hpMul":3.583181,"atkMul":1.948717,"speedMul":1,"slot":"N"},"actionIndex":3,"preview":{"gate":"lower","start":[9,10],"fly":false,"elite":false,"boss":false},"ownerPlayerId":"p_0"},{"time":11,"enemyKey":"enemy_1169_duphlx_2","routeIndex":4,"count":5,"interval":5.4,"mods":{"hpMul":3.583181,"atkMul":1.948717,"speedMul":1,"slot":"E"},"actionIndex":4,"preview":{"gate":"lower","start":[9,10],"fly":false,"elite":false,"boss":false},"ownerPlayerId":"p_0"},{"time":37,"enemyKey":"enemy_1169_duphlx_2","routeIndex":5,"count":5,"interval":7.2,"mods":{"hpMul":3.583181,"atkMul":1.948717,"speedMul":1,"slot":"E"},"actionIndex":5,"preview":{"gate":"lower","start":[9,10]Line truncated
|
||||
@@ -0,0 +1 @@
|
||||
{"t":"b.start","battleId":"7.13.62.u","fieldId":"u","kind":"unite","spec":{"v":1,"battleId":"7.13.62.u","fieldId":"u","kind":"unite","seed":3495193848,"modeId":"mode_multi_hard","round":13,"stageId":"act2autochess_m01","rect":{"r0":9,"r1":12,"c0":0,"c1":20},"timeLimit":230,"players":[{"playerId":"p_0","seat":0,"side":"L","colOffset":8,"units":[{"uid":169,"kind":"chess","chessId":"chess_char_4_10_a","row":12,"col":4,"dir":"RIGHT","items":[],"skillIndex":1,"carryState":{"hpPct":0.3342892236024844,"sp":0}},{"uid":41,"kind":"chess","chessId":"chess_char_1_17_b","row":12,"col":5,"dir":"DOWN","items":[],"skillIndex":1,"moduleId":"uniequip_002_indigo","carryState":{"down":true}},{"uid":208,"kind":"chess","chessId":"chess_char_3_17_b","row":11,"col":4,"dir":"RIGHT","items":["chess_item_3_01_e_a","chess_item_2_01_e_a"],"skillIndex":1,"moduleId":"uniequip_002_shotst","carryState":{"hpPct":1,"sp":8}},{"uid":96,"kind":"chess","chessId":"chess_char_1_19_b","row":11,"col":5,"dir":"DOWN","items":["chess_item_4_04_e_a","chess_item_1_01_e_a"],"skillIndex":1,"moduleId":"uniequip_002_wildmn","carryState":{"down":true}},{"uid":168,"kind":"chess","chessId":"chess_char_4_14_b","row":11,"col":7,"dir":"LEFT","items":[],"skillIndex":1,"moduleId":"uniequip_002_lionhd","carryState":{"down":true}},{"uid":123,"kind":"chess","chessId":"chess_char_4_02_b","row":10,"col":4,"dir":"RIGHT","items":[],"skillIndex":2,"moduleId":"uniequip_002_mostma","carryState":{"hpPct":1,"sp":0}},{"uid":209,"kind":"chess","chessId":"chess_char_6_19_a","row":10,"col":5,"dir":"DOWN","items":["chess_item_3_05_e_a","chess_item_4_05_e_a"],"skillIndex":2,"carryState":{"hpPct":0.7218991466789668,"sp":0}},{"uid":125,"kind":"chess","chessId":"chess_char_4_20_a","row":9,"col":7,"dir":"UP","items":["chess_item_1_05_e_a","chess_item_4_03_e_a"],"skillIndex":2,"carryState":{"down":true}}],"bonds":{"yanShip":{"count":0,"active":false,"tier":0,"layers":0},"sargonShip":{"count":0,"active":false,"tier":0,"layers":0},"victoriaShip":{"count":0,"active":false,"tier":0,"layers":0},"kjeragShip":{"count":1,"active":false,"tier":0,"layers":8},"lateranoShip":{"count":1,"active":false,"tier":0,"layers":0},"egirShip":{"count":0,"active":false,"tier":0,"layers":0},"siracusaShip":{"count":1,"active":false,"tier":0,"layers":0},"kazimierzShip":{"count":4,"active":true,"tier":1,"layers":74},"preciShip":{"count":3,"active":true,"tier":2,"layers":88},"swiftShip":{"count":3,"active":true,"tier":1,"layers":72},"skillfulShip":{"count":0,"active":false,"tier":0,"layers":0},"arcaneShip":{"count":3,"active":true,"tier":2,"layers":20},"steadShip":{"count":0,"active":false,"tier":0,"layers":0},"deputShip":{"count":0,"active":false,"tier":0,"layers":0},"visiShip":{"count":0,"active":false,"tier":0,"layers":0},"miraShip":{"count":0,"active":false,"tier":0,"layers":0},"investShip":{"count":0,"active":false,"tier":0,"layers":0},"raidShip":{"count":0,"active":false,"tier":0,"layers":0},"indomShip":{"count":0,"active":false,"tier":0,"layers":4},"maniShip":{"count":0,"active":false,"tier":0,"layers":0},"emptyShip":{"count":0,"active":false,"tier":0,"layers":0},"soloShip":{"count":1,"active":true,"tier":1,"layers":0},"suntShip":{"count":5,"active":true,"tier":2,"layers":0}},"bandId":"band_sarkazb","playerEffects":[],"deviceOverrides":{},"contentInfo":{"round":13,"kind":"unite","shopLevel":6,"funds":0,"handUnits":4,"handChess":4,"roundStats":{"refreshes":4,"buys":5,"sells":2,"spent":14,"gainedChess":2,"arts":0},"matchBands":["band_sarkazb","band_kirara","band_bldsk","band_cathy"],"teammates":[{"playerId":"p_1","bandId":"band_kirara"},{"playerId":"ai_2","bandId":"band_bldsk"},{"playerId":"ai_3","bandId":"band_cathy"}]}},{"playerId":"ai_3","seat":3,"side":"L","colOffset":0,"units":[{"uid":80,"kind":"chess","chessId":"chess_char_3_11_a","row":12,"col":4,"dir":"RIGHT","items":["chess_item_1_02_e_b","chess_item_6_04_e_a"],"skillIndex":1,"carryState":{"hpPct":1,"sp":13.13}},{"uid":143,"kind":"chess","chessId":"chess_char_4_22_a","row":12,"col":5,"dir":"DOWN","items":["chess_item_3_02_e_a","chess_item_6_09_e_a"],"skillIndex":2,"carryState":{"hpPct":1,"sp":0.17}},{"uid":158,"kind":"chess","chessId":"chess_char_5_11_a","row":12,"col":6,"dir":"RIGHT","items":["chess_item_4_06_e_a","chess_item_4_04_e_b"],"skillIndex":1,"carryState":{"hpPct":0.9422580123247037,"sp":4.57}},{"uid":112,"kind":"chess","chessId":"chess_char_4_13_a","row":11,"col":4,"dir":"RIGHT","items":["chess_item_3_10_e_a"],"skillIndex":1,"carryState":{"hpPct":1,"sp":1.85}},{"uid":59,"kind":"chess","chessId":"chess_char_3_20_a","row":11,"col":5,"dir":"RIGHT","items":["chess_item_6_06_e_a","chess_item_6_07_e_a"],"skillIndex":1,"carryState":{"hpPct":1,"sp":0}},{"uid":146,"kind":"chess","chessId":"chess_char_4_21_a","row":11,"col":7,"dir":"LEFT","items":[],"skillIndex":1,"carryState":{"down":true}},{"uid":81,"kind":"chess","chessId":"chess_char_3_14_a","row":10,"col":4,"dir":"RIGHT","items":["chess_item_6_01_e_a","chess_item_3_11_e_b"],"skillIndex":1,"carrySLine truncated
|
||||
@@ -0,0 +1,38 @@
|
||||
[
|
||||
{
|
||||
"name": "hard-normal-r13",
|
||||
"title": "普通战 · 第 13 回合",
|
||||
"kind": "normal",
|
||||
"round": 13,
|
||||
"stageId": "act2autochess_m01",
|
||||
"players": 1,
|
||||
"units": 8
|
||||
},
|
||||
{
|
||||
"name": "hard-unite-r13",
|
||||
"title": "联防 · 第 13 回合",
|
||||
"kind": "unite",
|
||||
"round": 13,
|
||||
"stageId": "act2autochess_m01",
|
||||
"players": 2,
|
||||
"units": 16
|
||||
},
|
||||
{
|
||||
"name": "hard-boss-r14",
|
||||
"title": "最终攻势 · 第 14 回合",
|
||||
"kind": "boss",
|
||||
"round": 14,
|
||||
"stageId": "act2autochess_m01",
|
||||
"players": 2,
|
||||
"units": 16
|
||||
},
|
||||
{
|
||||
"name": "hard-hidden-r15",
|
||||
"title": "隐秘核心 · 第 15 回合",
|
||||
"kind": "hidden",
|
||||
"round": 15,
|
||||
"stageId": "act2autochess_m01",
|
||||
"players": 2,
|
||||
"units": 16
|
||||
}
|
||||
]
|
||||
Reference in new issue
Block a user