Files
feuerwerk/tools/seed-demo.mjs
T
rootandClaude Opus 5 76aa441dce Enabled switch, text assets, mouse input and the jump-and-run example game
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>
2026-09-07 20:52:10 +02:00

126 lines
6.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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);