Engine additions that the example game needed: - `enabled` as a shared property of every placeable asset (default on, an override on the scene object). A switched-off object is not drawn, does not collide and its behavior does not run - and neither does anything parented below it. Disabled objects are now hidden in the scene view as well, so the editor shows what the game shows; they stay in the hierarchy panel, which is where they are selected and switched back on. - Asset type `text` (text, fontSize, color, align), rendered in scene and game. - Pointer input in the game view: `On click`, `Mouse position`, `Mouse button`, `Is hovered`, hit-tested against colliders, else sprite/text footprints. - New nodes: Set/Is enabled, Set text, Is asset, Name of, Equals, To text, Join text, Floor/Ceil/Round, Modulo, Random, Time, Restart game. Tooling: shared CDP harness (tools/harness.mjs), central project registry with a guard that keeps destructive scripts off the playground (tools/projects.mjs), plus smoke suites for the demo scene and the jump-and-run game. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
126 lines
6.2 KiB
JavaScript
126 lines
6.2 KiB
JavaScript
#!/usr/bin/env node
|
||
/**
|
||
* Writes the demo project: a "Player" container (green character with the Mover behavior and a
|
||
* collider) facing two "Enemy" container instances, plus a Spawner that drops more enemies.
|
||
* Usage: PATH=/opt/node22/bin:$PATH node tools/seed-demo.mjs [projectId] [baseUrl]
|
||
* Defaults to the demo project – the only script allowed to overwrite the playground. Pass
|
||
* PROJECTS.demoCheck to seed the throwaway copy that tools/smoke-demo.mjs runs against.
|
||
*/
|
||
import { PROJECTS, cliArgs, writeProject } from './projects.mjs';
|
||
|
||
const { projectId, baseUrl } = cliArgs('seed-demo.mjs', PROJECTS.demo, { allowDemo: true });
|
||
|
||
/**
|
||
* "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 another Enemy container 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: 'enemy', 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 GREEN = 0x45be2d;
|
||
const RED = 0xff4444;
|
||
|
||
/**
|
||
* The "Player" template: the green character sprite, its collider and the Mover behavior that
|
||
* owns it. Positions are local to the container origin (the centre of the group).
|
||
*/
|
||
const playerTemplate = [
|
||
{ id: 't-player-body', assetId: 'crate', name: '', position: { x: 0, y: 0, z: 0 }, overrides: { color: GREEN }, parentId: '', templateId: '' },
|
||
{ id: 't-player-mover', assetId: 'mover', name: '', position: { x: 0, y: 100, z: 0 }, overrides: {}, parentId: 't-player-body', templateId: '' },
|
||
{ id: 't-player-collider', assetId: 'collider', name: '', position: { x: 0, y: 0, z: 0 }, overrides: {}, parentId: 't-player-body', templateId: '' },
|
||
];
|
||
|
||
/** The "Enemy" template: a smaller red sprite with a matching 80×80 collider, no behavior. */
|
||
const enemyTemplate = [
|
||
{ id: 't-enemy-body', assetId: 'crate', name: '', position: { x: 0, y: 0, z: 0 }, overrides: { width: 80, height: 80, color: RED }, parentId: '', templateId: '' },
|
||
{ id: 't-enemy-collider', assetId: 'collider', name: '', position: { x: 0, y: 0, z: 0 }, overrides: { width: 80, height: 80 }, parentId: 't-enemy-body', templateId: '' },
|
||
];
|
||
|
||
/** One placed instance of a container: the container object plus a copy of every template object. */
|
||
const instance = (containerId, assetId, position, template, ids) => [
|
||
{ id: containerId, assetId, name: '', position, overrides: {}, parentId: '', templateId: '' },
|
||
...template.map((t) => ({
|
||
...t,
|
||
id: ids[t.id],
|
||
parentId: t.parentId ? ids[t.parentId] : containerId,
|
||
templateId: t.id,
|
||
})),
|
||
];
|
||
|
||
const project = {
|
||
formatVersion: 1,
|
||
id: projectId,
|
||
name: 'Demo project',
|
||
activeSceneId: 'scene1',
|
||
assets: [
|
||
{
|
||
id: 'scene1',
|
||
type: 'scene',
|
||
name: 'Level 1',
|
||
props: { backgroundColor: 0x22252c },
|
||
objects: [
|
||
// The player container on the left, the camera (zoomed out to 0.5) and the spawner.
|
||
...instance('op', 'player', { x: -100, y: 0, z: 0 }, playerTemplate, {
|
||
't-player-body': 'o1', 't-player-mover': 'o4', 't-player-collider': 'oc1',
|
||
}),
|
||
{ id: 'o3', assetId: 'cam', name: '', position: { x: 0, y: 0, z: 0 }, overrides: { zoom: 0.5 }, parentId: '', templateId: '' },
|
||
{ id: 'o5', assetId: 'spawner', name: '', position: { x: -100, y: 200, z: 0 }, overrides: {}, parentId: '', templateId: '' },
|
||
// Two enemies on the right, above and below the player's line.
|
||
...instance('oe1', 'enemy', { x: 100, y: -100, z: 0 }, enemyTemplate, {
|
||
't-enemy-body': 'o2', 't-enemy-collider': 'oc2',
|
||
}),
|
||
...instance('oe2', 'enemy', { x: 100, y: 100, z: 0 }, enemyTemplate, {
|
||
't-enemy-body': 'o6', 't-enemy-collider': 'oc3',
|
||
}),
|
||
],
|
||
},
|
||
{ id: 'crate', type: 'sprite', name: 'Character Sprite', props: { width: 100, height: 100, color: 0xe0553f } },
|
||
{ id: 'cam', type: 'camera', name: 'Main Camera', props: { width: 640, height: 360, zoom: 1 } },
|
||
{ id: 'collider', type: 'collider', name: 'Character Collider', props: { shape: 'box', width: 100, height: 100, 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 },
|
||
{ id: 'player', type: 'container', name: 'Player', props: {}, objects: playerTemplate },
|
||
{ id: 'enemy', type: 'container', name: 'Enemy', props: {}, objects: enemyTemplate },
|
||
],
|
||
};
|
||
|
||
await writeProject(projectId, baseUrl, project);
|