Fix collider tunneling, seed the container demo, split the e2e fixture
Move with "Collide with solids" now steps in sub-steps of at most half the thinnest collider, so a fast step can no longer jump across a collider (the crates are only 60 high, which is why it showed up mostly on the y axis). A pre-existing overlap is still never resolved, but it can no longer be deepened either, so a small overlap cannot open a wall permanently. seed-demo.mjs now writes the saved demo scene: a "Player" container (green sprite with collider and the Mover behavior) against two "Enemy" container instances, camera at zoom 0.5, Spawner dropping another enemy on Space. The e2e checks assert against the old two-crate layout and locate widgets by text, which the demo can no longer provide (three identically named sprite instances, both crates nested in containers). Its fixture therefore moves to tools/seed-e2e.mjs so the shipped demo can evolve without rewriting 100+ checks: seed-e2e -> e2e -> seed-demo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
3379232e64
commit
4900d7bbf6
6 files changed
+269
-39
No files matched your search
@@ -0,0 +1,109 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Writes the fixture scene tools/e2e.mjs asserts against: two top-level crates with a collider
|
||||
* child each, a camera, the Mover behavior owning the red crate and a Spawner. Kept separate
|
||||
* from tools/seed-demo.mjs so the shipped demo can change without rewriting 100+ checks; the
|
||||
* fixture's objects are uniquely named and positioned exactly where the checks expect them.
|
||||
*
|
||||
* Usage: PATH=/opt/node22/bin:$PATH node tools/seed-e2e.mjs <projectId> [baseUrl]
|
||||
* Run tools/seed-demo.mjs afterwards to put the demo project back.
|
||||
*/
|
||||
const projectId = process.argv[2];
|
||||
const baseUrl = process.argv[3] ?? 'https://nockal.com/game-maker/';
|
||||
if (!projectId) {
|
||||
console.error('usage: node tools/seed-e2e.mjs <projectId> [baseUrl]');
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
/**
|
||||
* "Mover" behavior in five nodes: every frame, move the owner along the normalized arrow-key
|
||||
* axis, scaled by the `speed` variable per second (On update's dt feeds the dt input).
|
||||
* Diagonals are not faster than straight moves because Axis input normalizes the vector, and
|
||||
* Move's "Collide with solids" stops the owner's solid colliders at other solid colliders.
|
||||
*/
|
||||
const moverGraph = {
|
||||
nodes: [
|
||||
{ id: 'n-update', type: 'OnUpdate', x: 0, y: 0, params: {} },
|
||||
{ id: 'n-owner', type: 'Owner', x: 0, y: 120, params: {} },
|
||||
{ id: 'n-axis', type: 'AxisInput', x: 0, y: 200, params: {} },
|
||||
{ id: 'n-speed', type: 'GetVariable', x: 0, y: 400, params: { variable: 'speed' } },
|
||||
{ id: 'n-move', type: 'Move', x: 340, y: 0, params: { collide: true } },
|
||||
],
|
||||
edges: [
|
||||
{ id: 'e1', from: { node: 'n-update', port: 'exec' }, to: { node: 'n-move', port: 'exec' } },
|
||||
{ id: 'e6', from: { node: 'n-update', port: 'dt' }, to: { node: 'n-move', port: 'dt' } },
|
||||
{ id: 'e2', from: { node: 'n-owner', port: 'object' }, to: { node: 'n-move', port: 'object' } },
|
||||
{ id: 'e3', from: { node: 'n-axis', port: 'x' }, to: { node: 'n-move', port: 'dx' } },
|
||||
{ id: 'e4', from: { node: 'n-axis', port: 'y' }, to: { node: 'n-move', port: 'dy' } },
|
||||
{ id: 'e5', from: { node: 'n-speed', port: 'value' }, to: { node: 'n-move', port: 'scale' } },
|
||||
],
|
||||
};
|
||||
|
||||
/** "Spawner": pressing Space places a new Crate at world (0, 150) and logs its name. */
|
||||
const spawnerGraph = {
|
||||
nodes: [
|
||||
{ id: 's-key', type: 'OnKeyDown', x: 0, y: 0, params: { key: ' ' } },
|
||||
{ id: 's-spawn', type: 'Spawn', x: 260, y: 0, params: { asset: 'crate', y: 150 } },
|
||||
{ id: 's-log', type: 'Log', x: 520, y: 0, params: {} },
|
||||
],
|
||||
edges: [
|
||||
{ id: 's-e1', from: { node: 's-key', port: 'exec' }, to: { node: 's-spawn', port: 'exec' } },
|
||||
{ id: 's-e2', from: { node: 's-spawn', port: 'exec' }, to: { node: 's-log', port: 'exec' } },
|
||||
{ id: 's-e3', from: { node: 's-spawn', port: 'object' }, to: { node: 's-log', port: 'message' } },
|
||||
],
|
||||
};
|
||||
|
||||
const project = {
|
||||
formatVersion: 1,
|
||||
id: projectId,
|
||||
name: 'Demo project',
|
||||
activeSceneId: 'scene1',
|
||||
assets: [
|
||||
{
|
||||
id: 'scene1',
|
||||
type: 'scene',
|
||||
name: 'Level 1',
|
||||
props: { backgroundColor: 0x22252c },
|
||||
objects: [
|
||||
{ id: 'o1', assetId: 'crate', name: '', position: { x: -150, y: 0, z: 0 }, overrides: {}, parentId: '' },
|
||||
{ id: 'o2', assetId: 'crate', name: 'Blue crate', position: { x: 120, y: -80, z: 0 }, overrides: { width: 140, color: 0x4488ff }, parentId: '' },
|
||||
{ id: 'o3', assetId: 'cam', name: '', position: { x: 0, y: 0, z: 0 }, overrides: {}, parentId: '' },
|
||||
// The mover is parented to the red crate (its owner); its position is local to it.
|
||||
{ id: 'o4', assetId: 'mover', name: '', position: { x: 0, y: 110, z: 0 }, overrides: {}, parentId: 'o1' },
|
||||
// Both crates carry the shared "Crate collider" as a centred child; the blue crate's
|
||||
// instance is widened to match its 140-wide sprite override.
|
||||
{ id: 'oc1', assetId: 'collider', name: '', position: { x: 0, y: 0, z: 0 }, overrides: {}, parentId: 'o1' },
|
||||
{ id: 'oc2', assetId: 'collider', name: '', position: { x: 0, y: 0, z: 0 }, overrides: { width: 140 }, parentId: 'o2' },
|
||||
// Top-level spawner (no owner) left of the red crate: Space spawns a crate.
|
||||
{ id: 'o5', assetId: 'spawner', name: '', position: { x: -300, y: 0, z: 0 }, overrides: {}, parentId: '' },
|
||||
],
|
||||
},
|
||||
{ id: 'crate', type: 'sprite', name: 'Crate', props: { width: 100, height: 60, color: 0xe0553f } },
|
||||
{ id: 'cam', type: 'camera', name: 'Main camera', props: { width: 640, height: 360, zoom: 1 } },
|
||||
{ id: 'collider', type: 'collider', name: 'Crate collider', props: { shape: 'box', width: 100, height: 60, radius: 50, isTrigger: false } },
|
||||
{
|
||||
id: 'mover',
|
||||
type: 'behavior',
|
||||
name: 'Mover',
|
||||
props: { speed: 200 },
|
||||
variables: [{ key: 'speed', label: 'speed', kind: 'number', default: 0 }],
|
||||
graph: moverGraph,
|
||||
},
|
||||
{ id: 'spawner', type: 'behavior', name: 'Spawner', props: {}, variables: [], graph: spawnerGraph },
|
||||
],
|
||||
};
|
||||
|
||||
const url = `${baseUrl}api/index.php?r=projects/${projectId}`;
|
||||
const current = await fetch(url);
|
||||
if (!current.ok) {
|
||||
console.error(`GET failed: ${current.status}`);
|
||||
process.exit(1);
|
||||
}
|
||||
const { revision } = await current.json();
|
||||
const res = await fetch(url, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ revision, project }) });
|
||||
const body = await res.json();
|
||||
if (!res.ok) {
|
||||
console.error('PUT failed', res.status, body);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(`seeded ${projectId} → revision ${body.revision}`);
|
||||
Reference in new issue
Block a user