feedback1 integrate: a form fx handed out late skips a change clip that would already have ended; browser check stalls then lets the clips catch up
This commit is contained in:
1 parent
f26e2014c4
commit
4bab58e508
4 files changed
+34
-16
No files matched your search
@@ -1458,7 +1458,7 @@ export async function createFieldView(host, options = {}) {
|
||||
if (form !== undefined) {
|
||||
const inf = infos.get(e[4].id);
|
||||
if (inf && (form === null || FORMS[inf.spine || inf.defId]?.[form])) inf.form = form;
|
||||
const x = late > 0 && Number(e[4].dur) > 0 ? { ...e[4], dur: Math.max(0, Number(e[4].dur) - late) } : e[4];
|
||||
const x = late > 0 ? { ...e[4], late, ...(Number(e[4].dur) > 0 ? { dur: Math.max(0, Number(e[4].dur) - late) } : {}) } : e[4];
|
||||
views.get(e[4].id)?.setForm?.(form, x);
|
||||
}
|
||||
if (!(late > 0)) fx.simFx(e[1], Number(e[2]), Number(e[3]), e[4]);
|
||||
|
||||
@@ -449,7 +449,10 @@ export class UnitView {
|
||||
const dur = fx && Number(fx.dur);
|
||||
let next = f && f.next ? FORMS[this.info.spine || this.info.defId]?.[f.next]?.roles || null : null;
|
||||
if (next && typeof f.roles.die === 'string') next = { ...next, die: f.roles.die };
|
||||
if (f) this.actor.setForm(f.roles, f.change || null, f.end && dur > 0 ? { clip: f.end, in: dur, roles: next } : null);
|
||||
// an fx handed out late (render/app.js: a stall, a hidden tab) skips a change clip that would already have ended
|
||||
const late = fx && Number(fx.late) > 0 ? Number(fx.late) : 0;
|
||||
const change = f && f.change && !(late > 0 && late >= (this.actor.dur?.(f.change) ?? Infinity)) ? f.change : null;
|
||||
if (f) this.actor.setForm(f.roles, change, f.end && dur > 0 ? { clip: f.end, in: dur, roles: next } : null);
|
||||
else if (had) this.actor.setForm(null);
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,9 @@
|
||||
// blocking 1 s long tasks (every frame a catch-up, and the render clock jumps past its 1.5 game s stale-event
|
||||
// window) across the change of a 转译基底·α that walks into 百炼嘉维尔's block: the view turns 幽灵 (C_*) — the
|
||||
// catch-up filter, and then render/interp.js's stale-event drop, used to lose the form fx, so it stayed on A_Move
|
||||
// and died on B_Die.
|
||||
// and died on B_Die. Whether a stall lands where a queued form fx falls out of the 1.5 game s window depends on the
|
||||
// page's frame timing, so this is the real-page check; the deterministic one is test/match/feedback1-runner-forms
|
||||
// (the runner feeding the render engine's buffer through a 1.2 s stall).
|
||||
//
|
||||
// Opt-in (starts Chrome): RENDER_E2E=1 node --test test/render/feedback1-forms.browser.test.js
|
||||
// Chrome path: $CHROME_PATH or the macOS default. Screenshots → test/e2e/out/feedback1-*.png.
|
||||
@@ -132,10 +134,10 @@ async function lateViewInPage(page, port, s, atSecs, setupSrc) {
|
||||
* real ms long task, for `secs` real s. Returns samples { t, simForm, form, clip, spine, alive } of the 转译基底·α's view and the
|
||||
* runner's catch-up count.
|
||||
*/
|
||||
async function runnerInPage(page, port, s, jankMs, secs) {
|
||||
async function runnerInPage(page, port, s, jankMs, secs, calmSecs = 0) {
|
||||
await page.goto(`http://127.0.0.1:${port}/dev/render-demo.html?scene=normal-m01&paused=1&panel=0`);
|
||||
await page.waitForFunction('window.__demo && (window.__demo.ready || window.__demo.error)', { timeout: 30000 });
|
||||
return page.evaluate(async (spec, jankMs, secs) => {
|
||||
return page.evaluate(async (spec, jankMs, secs, calmSecs) => {
|
||||
const { createBattleRunner } = await import('/js/battle/runner.js');
|
||||
const { data } = await import('/js/data.js');
|
||||
const v = window.__demo.view;
|
||||
@@ -156,21 +158,28 @@ async function runnerInPage(page, port, s, jankMs, secs) {
|
||||
const e = [...runner._entries.values()][0];
|
||||
const out = [];
|
||||
const end = performance.now() + secs * 1000;
|
||||
while (performance.now() < end) {
|
||||
const calmFrom = end;
|
||||
const calmEnd = end + calmSecs * 1000;
|
||||
while (performance.now() < calmEnd) {
|
||||
// a blocking long task (the main thread stalls: the render engine's own ticker stalls too, so its clock jumps
|
||||
// jankMs × 2 game s at once — past the 1.5 game s stale-event window), then one free frame
|
||||
const until = performance.now() + jankMs;
|
||||
while (performance.now() < until) { /* stall */ }
|
||||
await sleep(16);
|
||||
// jankMs × 2 game s at once — past the 1.5 game s stale-event window), one free frame for the renderer, then the
|
||||
// runner's catch-up frame. After `secs` the stalls stop and the frames come smoothly for `calmSecs` (the clips
|
||||
// catch up: PIXI caps a frame's delta).
|
||||
const calm = performance.now() >= calmFrom;
|
||||
if (!calm) {
|
||||
const until = performance.now() + jankMs;
|
||||
while (performance.now() < until) { /* stall */ }
|
||||
}
|
||||
await sleep(calm ? 33 : 16);
|
||||
for (const fn of queue.splice(0)) fn(performance.now());
|
||||
const tr = e.battle.units.find((u) => u.defId === 'enemy_10081_mpplai');
|
||||
const view = tr && v.debug.views.get(tr.id);
|
||||
if (tr) out.push({ t: +e.battle.time.toFixed(2), simForm: tr.form, form: view?.form ?? null, clip: view?.actor?.current ?? null, spine: !!view?.spineReady, alive: tr.alive });
|
||||
if (tr) out.push({ t: +e.battle.time.toFixed(2), simForm: tr.form, form: view?.form ?? null, clip: view?.actor?.current ?? null, spine: !!view?.spineReady, alive: tr.alive, calm });
|
||||
}
|
||||
const catchups = runner.stats().catchups;
|
||||
runner.dispose();
|
||||
return { out, catchups };
|
||||
}, s, jankMs, secs);
|
||||
}, s, jankMs, secs, calmSecs);
|
||||
}
|
||||
|
||||
describe('player reports after 0.1.0: the models follow the knock-out forms (headless Chrome, real sim)', { skip }, () => {
|
||||
@@ -280,14 +289,18 @@ describe('player reports after 0.1.0: the models follow the knock-out forms (hea
|
||||
test('the real client runner with blocking 1 s long tasks (every frame a catch-up, the render clock jumps 2 game s): 转译基底·α blocked by 百炼嘉维尔 turns 幽灵 in the view (C_*), never back to A_Move / B_Die', async () => {
|
||||
const { p, problems } = await page();
|
||||
try {
|
||||
const { out, catchups } = await runnerInPage(p, srv.port, spec('enemy_10081_mpplai', 'runner', 9, 7), 1000, 12);
|
||||
const { out, catchups } = await runnerInPage(p, srv.port, spec('enemy_10081_mpplai', 'runner', 9, 7), 1000, 9, 3);
|
||||
await p.screenshot({ path: path.join(OUT, 'feedback1-runner.png') });
|
||||
assert.ok(catchups >= 5, `catch-up frames (${catchups})`);
|
||||
const changed = out.find((x) => x.simForm === 'translator_youling');
|
||||
assert.ok(changed, `the sim's 转译基底·α turned 幽灵 (${JSON.stringify(out.slice(-3))})`);
|
||||
const after = out.filter((x) => x.spine && x.t > changed.t + 2 + LAG);
|
||||
const after = out.filter((x) => x.spine && x.t > changed.t + 2 + LAG && x.alive);
|
||||
assert.ok(after.length > 0, 'samples after the change');
|
||||
assert.ok(after.every((x) => x.form === 'translator_youling' && /^C_/.test(x.clip)), `幽灵's clips (${[...new Set(after.map((x) => `${x.form}:${x.clip}`))]})`);
|
||||
// through the stalls the view is in the 幽灵 form (its 2 s change clip may still play: PIXI caps a frame's delta,
|
||||
// so a clip advances 0.1 s per stalled frame) — never the first form's walk again
|
||||
assert.ok(after.every((x) => x.form === 'translator_youling' && /^(C_|A_Die_C)/.test(x.clip)), `幽灵's clips (${[...new Set(after.map((x) => `${x.form}:${x.clip}`))]})`);
|
||||
const calm = after.filter((x) => x.calm);
|
||||
assert.ok(calm.length > 0 && /^C_/.test(calm[calm.length - 1].clip), `smooth frames again: 幽灵's own clips (${[...new Set(calm.map((x) => x.clip))]})`);
|
||||
assert.ok(!out.some((x) => x.spine && /^B_/.test(x.clip)), 'never the 寻仇者\'s clips (B_Die was the look of report #5)');
|
||||
assert.deepEqual(problems, []);
|
||||
} finally {
|
||||
|
||||
@@ -372,7 +372,9 @@ test('render/app.js hands the `form` of a sim fx (shared/protocol.js fxForm) wit
|
||||
assert.match(src, /const form = fxForm\(e\);[\s\S]{0,250}inf\.form = form[\s\S]{0,250}setForm\?\.\(form, x\)/);
|
||||
// a form fx handed out late (render/interp.js takeEvents `late`) switches the model with its closing clip shortened by
|
||||
// the lateness and without replaying its telegraph
|
||||
assert.match(src, /const x = late > 0 && Number\(e\[4\]\.dur\) > 0 \? \{ \.\.\.e\[4\], dur: Math\.max\(0, Number\(e\[4\]\.dur\) - late\) \} : e\[4\];/);
|
||||
assert.match(src, /const x = late > 0 \? \{ \.\.\.e\[4\], late, \.\.\.\(Number\(e\[4\]\.dur\) > 0 \? \{ dur: Math\.max\(0, Number\(e\[4\]\.dur\) - late\) \} : \{\}\) \} : e\[4\];/);
|
||||
// …and UnitView.setForm skips a change clip that would already have ended
|
||||
assert.match(readFileSync(path.join(ROOT, 'public/js/render/units.js'), 'utf8'), /!\(late > 0 && late >= \(this\.actor\.dur\?\.\(f\.change\) \?\? Infinity\)\)/);
|
||||
assert.match(src, /if \(!\(late > 0\)\) fx\.simFx\(/);
|
||||
assert.match(src, /interp\.takeEvents\(renderT, EVS, renderT - 1\.5, LATE\);/);
|
||||
});
|
||||
Reference in new issue
Block a user