Make the actor smoke reach every sub pixel edge position again

Since #225 the game view opens at 100 % instead of fit. The actor walks
150 px/s, so its top edge only lands on .0 or .5 of a pixel. The check
for the frame top edge needs 0.6 to 0.9 and gave up in 6 of 7 CI runs.
The picture itself was fine: the pixels above and at the edge were the
same in every red run.

- While nudging the actor into place the smoke slows time to 0.08 with
  __gm.setTimeScale. The actor then moves 0.2 px per frame and can
  reach any position inside a pixel.
- Time scale goes back to 1 before the edge is measured.

Tests:
- Old smoke on the same seeded project fails locally like CI
  (fraction 0.08).
- New smoke passed three times in a row (fraction 0.80, 21 checks).
- The actor project file was saved first and restored byte exact.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5.5 committed 2026-09-24 13:22:43 +02:00
1 parent f4cc681df4
commit 8f29b88cc9
1 file changed
+19 -1
+19 -1
View File
@@ -38,7 +38,21 @@ const walkOf = (e) =>
const rowOfFrame = (texture) => Number(String(texture).split('-')[1]); const rowOfFrame = (texture) => Number(String(texture).split('-')[1]);
await runChecks({ projectId, baseUrl, port: 9341, shotPrefix: 'gm-actor' }, async (t) => { await runChecks({ projectId, baseUrl, port: 9341, shotPrefix: 'gm-actor' }, async (t) => {
const { check, sleep, json, shot, entity, play, stop, navigate, exceptions, holdKey, keyDown, keyUp } = t; const {
check,
sleep,
json,
evaluate,
shot,
entity,
play,
stop,
navigate,
exceptions,
holdKey,
keyDown,
keyUp,
} = t;
await navigate(); await navigate();
@@ -257,10 +271,13 @@ await runChecks({ projectId, baseUrl, port: 9341, shotPrefix: 'gm-actor' }, asyn
// 12. A frame's top edge shows nothing of the cell above it, wherever the edge lands inside a // 12. A frame's top edge shows nothing of the cell above it, wherever the edge lands inside a
// pixel. Drawn from the sheet, a partly covered edge pixel read the boots of the cell above – // pixel. Drawn from the sheet, a partly covered edge pixel read the boots of the cell above –
// the hairline over the head. Nudged upwards until the edge sits in the lower half of a pixel. // the hairline over the head. Nudged upwards until the edge sits in the lower half of a pixel.
// At the game view's 100% a frame at full speed moves 2.5 px, so the edge only ever lands on
// .0 or .5. Slowed to 0.2 px a frame, it walks through every part of a pixel.
const topEdge = async () => { const topEdge = async () => {
const r = await json(`__gm.gameRect('oplayer')`); const r = await json(`__gm.gameRect('oplayer')`);
return { cx: Math.round(r.x), y0: r.y - r.h / 2 }; return { cx: Math.round(r.x), y0: r.y - r.h / 2 };
}; };
await evaluate('__gm.setTimeScale(0.08)');
let edge; let edge;
let nudges = 0; let nudges = 0;
do { do {
@@ -269,6 +286,7 @@ await runChecks({ projectId, baseUrl, port: 9341, shotPrefix: 'gm-actor' }, asyn
edge = await topEdge(); edge = await topEdge();
nudges += 1; nudges += 1;
} while (nudges < 40 && !(edge.y0 % 1 > 0.6 && edge.y0 % 1 < 0.9)); } while (nudges < 40 && !(edge.y0 % 1 > 0.6 && edge.y0 % 1 < 0.9));
await evaluate('__gm.setTimeScale(1)');
const facingUp = await entity('oplayer'); const facingUp = await entity('oplayer');
const rowAt = async (y) => const rowAt = async (y) =>
json(`[-8,-6,-4,-2,0,2,4,6,8].map((dx) => __gm.pixelAt(${edge.cx} + dx, ${y}))`).then((p) => p.join(' ')); json(`[-8,-6,-4,-2,0,2,4,6,8].map((dx) => __gm.pixelAt(${edge.cx} + dx, ${y}))`).then((p) => p.join(' '));