Add a four-direction walk demo that seeds its own sprite sheet

tools/seed-actor.mjs builds the project Actor demo: it uploads
tools/assets/Actor1.png, slices the 576x384 sheet into the 96 cells of a
12x8 grid (48x48 each, no remainder) and wears the character in the top
left corner. Walking is four ping-pong animations of three frames, which
is the original RPG-Maker walk cycle; standing is the middle frame of the
row, not an animation.

The graph pastes walk- or idle- in front of the direction faced and wears
the result, so eight cases need no branch tree - the List node only holds
numbers. Which way it looks is the key pressed last, corrected whenever
only one axis is still held, or letting go of one key of a diagonal would
walk backwards. Axis input already normalizes its vector.

tools/lib/projects.mjs gains uploadTexture, since data/textures/ is
git-ignored and a seed that ships its picture has to send it. The harness
gains w/a/s/d and keyDown/keyUp halves, because holdKey cannot express two
presses that overlap - which is what a diagonal is.

tools/smoke-actor.mjs plays it in 19 checks and reads the drawn pixels at
the camera edge, where the bounds hold him walking on the spot: three
distinct frames walking, one standing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
rootandClaude Opus 5 committed 2026-09-12 12:13:13 +02:00
1 parent 1135bf4562
commit e147dd6d21
7 files changed
+534

No files matched your search

+39
View File
@@ -6,14 +6,19 @@
* fixture rewritten by every e2e run (tools/seed-e2e.mjs + tools/e2e.mjs).
* jumpRun the example game (tools/seed-jumprun.mjs, played by tools/smoke-jumprun.mjs).
* tetris the second example game (tools/seed-tetris.mjs, played by tools/smoke-tetris.mjs).
* actor the four-direction walk demo (tools/seed-actor.mjs).
*
* All ids exist on the server (created via POST ?r=projects); PUT does not create projects.
*/
import { readFile } from 'node:fs/promises';
import { extname } from 'node:path';
export const PROJECTS = {
playground: '649188c15414',
fixture: '3caa9b6a0c92',
jumpRun: '3d271f3bb58b',
tetris: '2072b56583af',
actor: '3ccb5c67220b',
};
export const DEFAULT_BASE_URL = 'https://nockal.com/game-maker/';
@@ -32,6 +37,40 @@ export function cliArgs(script, defaultId) {
return { projectId, baseUrl };
}
const MIME = {
png: 'image/png',
jpg: 'image/jpeg',
jpeg: 'image/jpeg',
webp: 'image/webp',
gif: 'image/gif',
};
/**
* Uploads an image and returns `{file, width, height}` for a texture asset. Storage is
* content-addressed, so re-seeding the same file is a no-op rather than a second copy – which is
* what makes a seed that ships its own image reproducible (`data/textures/` is git-ignored).
*/
export async function uploadTexture(baseUrl, filePath) {
const bytes = await readFile(filePath);
const type = MIME[extname(filePath).slice(1).toLowerCase()];
if (!type) {
console.error(`unsupported image type: ${filePath}`);
process.exit(1);
}
const res = await fetch(`${baseUrl}api/index.php?r=textures`, {
method: 'POST',
headers: { 'Content-Type': type },
body: bytes,
signal: AbortSignal.timeout(20_000),
});
const result = await res.json();
if (!res.ok) {
console.error('texture upload failed', res.status, result);
process.exit(1);
}
return { file: result.file, width: result.width, height: result.height };
}
/** Overwrites a project, keeping the name it already has on the server. */
export async function writeProject(projectId, baseUrl, project) {
const url = `${baseUrl}api/index.php?r=projects/${projectId}`;