Files
feuerwerk/tools/seed-jumprun.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

409 lines
22 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 example game: a small jump and run built entirely from assets, behaviors and the
* node graph – no engine code is involved.
*
* Title screen (a container with a panel, texts and a clickable Start button)
* Level (ground, platforms, walls, ten coins, the player and the HUD labels)
* Win / Lose (containers shown when the coins are collected or the two minutes are up)
*
* Screens are containers that are switched on and off with the `enabled` property – the same
* switch the inspector shows for every object. Everything else is nodes: gravity and jumping
* on the player, timer, coin counter and screen flow on the "Game" behavior.
*
* Usage: PATH=/opt/node22/bin:$PATH node tools/seed-jumprun.mjs [projectId] [baseUrl]
*/
import { PROJECTS, cliArgs, writeProject } from './projects.mjs';
const { projectId, baseUrl } = cliArgs('seed-jumprun.mjs', PROJECTS.jumpRun);
// --- colours ---------------------------------------------------------------------------------
const SKY = 0x151821;
const STONE = 0x3c4454;
const GRASS = 0x4a7d3a;
const PLAYER = 0x63c74d;
const COIN = 0xf5c542;
const PANEL = 0x11141c;
const BUTTON = 0x63c74d;
const WHITE = 0xf2f5f7;
// --- level geometry (the camera shows x −320…320, y −180…180; +y is down) ---------------------
/** A solid box: the sprite plus a collider child of the same size. */
const solid = (id, x, y, w, h, color, name = '') => [
{ id, assetId: 'box', name, position: { x, y, z: 0 }, overrides: { width: w, height: h, color }, parentId: '', templateId: '' },
{ id: `${id}-c`, assetId: 'solid', name: '', position: { x: 0, y: 0, z: 0 }, overrides: { width: w, height: h }, parentId: id, templateId: '' },
];
/** A coin: sprite plus a trigger collider, so it reports the touch without blocking. */
const coin = (id, x, y) => [
{ id, assetId: 'coin-sprite', name: 'Coin', position: { x, y, z: 1 }, overrides: {}, parentId: '', templateId: '' },
{ id: `${id}-c`, assetId: 'trigger', name: '', position: { x: 0, y: 0, z: 0 }, overrides: {}, parentId: id, templateId: '' },
];
const text = (id, x, y, content, { size = 28, color = WHITE, align = 'center', name = '', parentId = '', z = 1 } = {}) => ({
id,
assetId: 'label',
name,
position: { x, y, z },
overrides: { text: content, fontSize: size, color, align },
parentId,
templateId: '',
});
const levelTemplate = [
// Ground, side walls and six platforms.
...solid('ground', 0, 165, 660, 30, GRASS),
...solid('wall-l', -315, 0, 10, 360, STONE),
...solid('wall-r', 315, 0, 10, 360, STONE),
...solid('p1', -170, 90, 120, 16, STONE),
...solid('p2', -30, 30, 140, 16, STONE),
...solid('p3', 170, 60, 140, 16, STONE),
...solid('p4', 60, -50, 120, 16, STONE),
...solid('p5', -230, -30, 120, 16, STONE),
...solid('p6', 255, -60, 110, 16, STONE),
// Ten coins: three on the ground, one above each platform, one in the far corner.
...coin('c1', -150, 128),
...coin('c2', 0, 128),
...coin('c3', 150, 128),
...coin('c4', -170, 60),
...coin('c5', -30, 0),
...coin('c6', 170, 30),
...coin('c7', 60, -80),
...coin('c8', -230, -60),
...coin('c9', 255, -90),
...coin('c10', 290, 128),
// The player: sprite, solid collider and the behavior that owns it.
{ id: 'player', assetId: 'player-sprite', name: 'Player', position: { x: -270, y: 125, z: 2 }, overrides: {}, parentId: '', templateId: '' },
{ id: 'player-c', assetId: 'solid', name: '', position: { x: 0, y: 0, z: 0 }, overrides: { width: 30, height: 40 }, parentId: 'player', templateId: '' },
{ id: 'player-logic', assetId: 'player', name: '', position: { x: 0, y: -60, z: 0 }, overrides: {}, parentId: 'player', templateId: '' },
// HUD, top left and top right of the camera frame.
text('time-label', -300, -160, 'Time 120', { size: 22, align: 'left', name: 'TimeLabel', z: 3 }),
text('coin-label', 300, -160, 'Coins 0/10', { size: 22, align: 'right', name: 'CoinLabel', z: 3 }),
];
/** A full-frame panel with a headline, a line of help text and a button carrying a behavior. */
const screenTemplate = (prefix, headline, hint, buttonLabel, behaviorId, headlineSize = 52) => [
{ id: `${prefix}-panel`, assetId: 'box', name: '', position: { x: 0, y: 0, z: 0 }, overrides: { width: 640, height: 360, color: PANEL }, parentId: '', templateId: '' },
text(`${prefix}-title`, 0, -70, headline, { size: headlineSize }),
text(`${prefix}-hint`, 0, -10, hint, { size: 20, color: 0x9aa3b2 }),
{ id: `${prefix}-button`, assetId: 'box', name: '', position: { x: 0, y: 80, z: 1 }, overrides: { width: 240, height: 60, color: BUTTON }, parentId: '', templateId: '' },
text(`${prefix}-button-label`, 0, 0, buttonLabel, { size: 26, color: 0x10240a, parentId: `${prefix}-button`, z: 1 }),
{ id: `${prefix}-button-logic`, assetId: behaviorId, name: '', position: { x: 0, y: -60, z: 0 }, overrides: {}, parentId: `${prefix}-button`, templateId: '' },
];
const titleTemplate = screenTemplate('title', 'JUMP & RUN', 'Arrow keys to move, Space to jump.', 'START', 'start-button');
const winTemplate = screenTemplate('win', 'YOU WIN!', 'All ten coins collected in time.', 'PLAY AGAIN', 'restart-button');
const loseTemplate = screenTemplate('lose', 'TIME UP', 'The two minutes are over – try again.', 'PLAY AGAIN', 'restart-button');
/** One placed container: the container object plus a copy of every template object. */
const instantiate = (containerId, assetId, name, position, template, overrides = {}) => [
{ id: containerId, assetId, name, position, overrides, parentId: '', templateId: '' },
...template.map((t) => ({ ...t, id: `${containerId}-${t.id}`, parentId: t.parentId ? `${containerId}-${t.parentId}` : containerId, templateId: t.id })),
];
// --- graphs ----------------------------------------------------------------------------------
const edge = (id, from, fromPort, to, toPort) => ({ id, from: { node: from, port: fromPort }, to: { node: to, port: toPort } });
/**
* "Player": gravity, walking, jumping and picking coins up – 33 nodes in three strands.
*
* On update vy += gravity·dt, then Move(owner) by (axis·speed, vy)·dt with collisions on.
* Blocked vertically while falling means standing: vy back to 0, onGround true.
* On key down Space while onGround shoots vy up (negative is up).
* On collision Touching a coin adds one to the "Game" behavior's counter and destroys it.
*/
const playerGraph = {
nodes: [
{ id: 'p-upd', type: 'OnUpdate', x: 0, y: 0, params: {} },
{ id: 'p-owner', type: 'Owner', x: 0, y: 140, params: {} },
{ id: 'p-axis', type: 'AxisInput', x: 0, y: 220, params: { left: 'ArrowLeft', right: 'ArrowRight', up: '', down: '' } },
{ id: 'p-speed', type: 'GetVariable', x: 0, y: 360, params: { variable: 'speed' } },
{ id: 'p-mulx', type: 'Multiply', x: 220, y: 300, params: {} },
{ id: 'p-vy', type: 'GetVariable', x: 0, y: 440, params: { variable: 'vy' } },
{ id: 'p-grav', type: 'GetVariable', x: 0, y: 520, params: { variable: 'gravity' } },
{ id: 'p-mulg', type: 'Multiply', x: 220, y: 500, params: {} },
{ id: 'p-addvy', type: 'Add', x: 420, y: 460, params: {} },
{ id: 'p-setvy', type: 'SetVariable', x: 620, y: 0, params: { variable: 'vy' } },
{ id: 'p-vy2', type: 'GetVariable', x: 620, y: 340, params: { variable: 'vy' } },
{ id: 'p-move', type: 'Move', x: 840, y: 0, params: { collide: true } },
{ id: 'p-br', type: 'Branch', x: 1080, y: 0, params: {} },
{ id: 'p-seq', type: 'Sequence', x: 1260, y: 0, params: {} },
{ id: 'p-vy3', type: 'GetVariable', x: 1080, y: 260, params: { variable: 'vy' } },
{ id: 'p-cmp', type: 'Compare', x: 1260, y: 260, params: { op: '>=', b: 0 } },
{ id: 'p-ground', type: 'SetVariable', x: 1460, y: 0, params: { variable: 'onGround' } },
{ id: 'p-zero', type: 'SetVariable', x: 1460, y: 140, params: { variable: 'vy', value: 0 } },
{ id: 'p-air', type: 'SetVariable', x: 1260, y: 400, params: { variable: 'onGround', value: false } },
{ id: 'p-key', type: 'OnKeyDown', x: 0, y: 700, params: { key: ' ' } },
{ id: 'p-og', type: 'GetVariable', x: 0, y: 820, params: { variable: 'onGround' } },
{ id: 'p-brj', type: 'Branch', x: 240, y: 700, params: {} },
{ id: 'p-jump', type: 'GetVariable', x: 240, y: 880, params: { variable: 'jumpSpeed' } },
{ id: 'p-neg', type: 'Multiply', x: 440, y: 880, params: { b: -1 } },
{ id: 'p-setjump', type: 'SetVariable', x: 660, y: 700, params: { variable: 'vy' } },
{ id: 'p-col', type: 'OnCollisionEnter', x: 0, y: 1060, params: {} },
{ id: 'p-iscoin', type: 'IsAsset', x: 260, y: 1180, params: { asset: 'coin-sprite' } },
{ id: 'p-brc', type: 'Branch', x: 480, y: 1060, params: {} },
{ id: 'p-seqc', type: 'Sequence', x: 680, y: 1060, params: {} },
{ id: 'p-game', type: 'ObjectByName', x: 680, y: 1280, params: { name: 'Game' } },
{ id: 'p-coins', type: 'GetProperty', x: 880, y: 1280, params: { property: 'coins' } },
{ id: 'p-add1', type: 'Add', x: 1080, y: 1280, params: { b: 1 } },
{ id: 'p-setcoins', type: 'SetProperty', x: 1280, y: 1060, params: { property: 'coins' } },
{ id: 'p-destroy', type: 'Destroy', x: 1280, y: 1200, params: {} },
],
edges: [
edge('pe1', 'p-upd', 'exec', 'p-setvy', 'exec'),
edge('pe2', 'p-vy', 'value', 'p-addvy', 'a'),
edge('pe3', 'p-grav', 'value', 'p-mulg', 'a'),
edge('pe4', 'p-upd', 'dt', 'p-mulg', 'b'),
edge('pe5', 'p-mulg', 'result', 'p-addvy', 'b'),
edge('pe6', 'p-addvy', 'result', 'p-setvy', 'value'),
edge('pe7', 'p-setvy', 'exec', 'p-move', 'exec'),
edge('pe8', 'p-owner', 'object', 'p-move', 'object'),
edge('pe9', 'p-axis', 'x', 'p-mulx', 'a'),
edge('pe10', 'p-speed', 'value', 'p-mulx', 'b'),
edge('pe11', 'p-mulx', 'result', 'p-move', 'dx'),
edge('pe12', 'p-vy2', 'value', 'p-move', 'dy'),
edge('pe13', 'p-upd', 'dt', 'p-move', 'dt'),
edge('pe14', 'p-move', 'exec', 'p-br', 'exec'),
edge('pe15', 'p-move', 'blockedY', 'p-br', 'condition'),
edge('pe16', 'p-br', 'then', 'p-seq', 'exec'),
edge('pe17', 'p-seq', 'a', 'p-ground', 'exec'),
edge('pe18', 'p-vy3', 'value', 'p-cmp', 'a'),
edge('pe19', 'p-cmp', 'result', 'p-ground', 'value'),
edge('pe20', 'p-seq', 'b', 'p-zero', 'exec'),
edge('pe21', 'p-br', 'else', 'p-air', 'exec'),
edge('pe22', 'p-key', 'exec', 'p-brj', 'exec'),
edge('pe23', 'p-og', 'value', 'p-brj', 'condition'),
edge('pe24', 'p-brj', 'then', 'p-setjump', 'exec'),
edge('pe25', 'p-jump', 'value', 'p-neg', 'a'),
edge('pe26', 'p-neg', 'result', 'p-setjump', 'value'),
edge('pe27', 'p-col', 'exec', 'p-brc', 'exec'),
edge('pe28', 'p-col', 'other', 'p-iscoin', 'object'),
edge('pe29', 'p-iscoin', 'result', 'p-brc', 'condition'),
edge('pe30', 'p-brc', 'then', 'p-seqc', 'exec'),
edge('pe31', 'p-seqc', 'a', 'p-setcoins', 'exec'),
edge('pe32', 'p-game', 'object', 'p-coins', 'object'),
edge('pe33', 'p-coins', 'value', 'p-add1', 'a'),
edge('pe34', 'p-add1', 'result', 'p-setcoins', 'value'),
edge('pe35', 'p-game', 'object', 'p-setcoins', 'object'),
edge('pe36', 'p-seqc', 'b', 'p-destroy', 'exec'),
edge('pe37', 'p-col', 'other', 'p-destroy', 'object'),
],
};
/**
* "Game": owns the run. On start it shows the title and hides everything else; while
* `running` it counts the clock down, writes both HUD labels and decides win (coins reached)
* or loss (time gone) by switching the screens.
*/
const gameGraph = {
nodes: [
{ id: 'g-start', type: 'OnStart', x: 0, y: 0, params: {} },
{ id: 'g-title', type: 'ObjectByName', x: 0, y: 140, params: { name: 'Title' } },
{ id: 'g-level', type: 'ObjectByName', x: 0, y: 220, params: { name: 'Level' } },
{ id: 'g-win', type: 'ObjectByName', x: 0, y: 300, params: { name: 'Win' } },
{ id: 'g-lose', type: 'ObjectByName', x: 0, y: 380, params: { name: 'Lose' } },
{ id: 'g-on1', type: 'SetEnabled', x: 260, y: 0, params: { enabled: true } },
{ id: 'g-off1', type: 'SetEnabled', x: 480, y: 0, params: { enabled: false } },
{ id: 'g-off2', type: 'SetEnabled', x: 700, y: 0, params: { enabled: false } },
{ id: 'g-off3', type: 'SetEnabled', x: 920, y: 0, params: { enabled: false } },
{ id: 'g-upd', type: 'OnUpdate', x: 0, y: 520, params: {} },
{ id: 'g-running', type: 'GetVariable', x: 0, y: 640, params: { variable: 'running' } },
{ id: 'g-brrun', type: 'Branch', x: 220, y: 520, params: {} },
{ id: 'g-time', type: 'GetVariable', x: 220, y: 700, params: { variable: 'timeLeft' } },
{ id: 'g-sub', type: 'Subtract', x: 420, y: 700, params: {} },
{ id: 'g-max', type: 'Max', x: 600, y: 700, params: { b: 0 } },
{ id: 'g-settime', type: 'SetVariable', x: 440, y: 520, params: { variable: 'timeLeft' } },
{ id: 'g-timelabel', type: 'ObjectByName', x: 440, y: 860, params: { name: 'TimeLabel' } },
{ id: 'g-time2', type: 'GetVariable', x: 440, y: 940, params: { variable: 'timeLeft' } },
{ id: 'g-totext', type: 'ToText', x: 640, y: 940, params: { decimals: 0 } },
{ id: 'g-join', type: 'Join', x: 840, y: 940, params: { a: 'Time ' } },
{ id: 'g-settext', type: 'SetText', x: 700, y: 520, params: {} },
{ id: 'g-coinlabel', type: 'ObjectByName', x: 700, y: 1080, params: { name: 'CoinLabel' } },
{ id: 'g-coins', type: 'GetVariable', x: 700, y: 1160, params: { variable: 'coins' } },
{ id: 'g-totext2', type: 'ToText', x: 900, y: 1160, params: { decimals: 0 } },
{ id: 'g-join2', type: 'Join', x: 1080, y: 1160, params: { a: 'Coins ' } },
{ id: 'g-join3', type: 'Join', x: 1260, y: 1160, params: { b: '/10' } },
{ id: 'g-settext2', type: 'SetText', x: 960, y: 520, params: {} },
{ id: 'g-needed', type: 'GetVariable', x: 960, y: 700, params: { variable: 'needed' } },
{ id: 'g-cmpwin', type: 'Compare', x: 1160, y: 700, params: { op: '>=' } },
{ id: 'g-brwin', type: 'Branch', x: 1220, y: 520, params: {} },
{ id: 'g-stop1', type: 'SetVariable', x: 1420, y: 440, params: { variable: 'running', value: false } },
{ id: 'g-leveloff1', type: 'SetEnabled', x: 1620, y: 440, params: { enabled: false } },
{ id: 'g-winon', type: 'SetEnabled', x: 1820, y: 440, params: { enabled: true } },
{ id: 'g-cmplose', type: 'Compare', x: 1160, y: 860, params: { op: '<=', b: 0 } },
{ id: 'g-brlose', type: 'Branch', x: 1420, y: 640, params: {} },
{ id: 'g-stop2', type: 'SetVariable', x: 1620, y: 640, params: { variable: 'running', value: false } },
{ id: 'g-leveloff2', type: 'SetEnabled', x: 1820, y: 640, params: { enabled: false } },
{ id: 'g-loseon', type: 'SetEnabled', x: 2020, y: 640, params: { enabled: true } },
],
edges: [
edge('ge1', 'g-start', 'exec', 'g-on1', 'exec'),
edge('ge2', 'g-title', 'object', 'g-on1', 'object'),
edge('ge3', 'g-on1', 'exec', 'g-off1', 'exec'),
edge('ge4', 'g-level', 'object', 'g-off1', 'object'),
edge('ge5', 'g-off1', 'exec', 'g-off2', 'exec'),
edge('ge6', 'g-win', 'object', 'g-off2', 'object'),
edge('ge7', 'g-off2', 'exec', 'g-off3', 'exec'),
edge('ge8', 'g-lose', 'object', 'g-off3', 'object'),
edge('ge9', 'g-upd', 'exec', 'g-brrun', 'exec'),
edge('ge10', 'g-running', 'value', 'g-brrun', 'condition'),
edge('ge11', 'g-brrun', 'then', 'g-settime', 'exec'),
edge('ge12', 'g-time', 'value', 'g-sub', 'a'),
edge('ge13', 'g-upd', 'dt', 'g-sub', 'b'),
edge('ge14', 'g-sub', 'result', 'g-max', 'a'),
edge('ge15', 'g-max', 'result', 'g-settime', 'value'),
edge('ge16', 'g-settime', 'exec', 'g-settext', 'exec'),
edge('ge17', 'g-timelabel', 'object', 'g-settext', 'object'),
edge('ge18', 'g-time2', 'value', 'g-totext', 'value'),
edge('ge19', 'g-totext', 'text', 'g-join', 'b'),
edge('ge20', 'g-join', 'text', 'g-settext', 'text'),
edge('ge21', 'g-settext', 'exec', 'g-settext2', 'exec'),
edge('ge22', 'g-coinlabel', 'object', 'g-settext2', 'object'),
edge('ge23', 'g-coins', 'value', 'g-totext2', 'value'),
edge('ge24', 'g-totext2', 'text', 'g-join2', 'b'),
edge('ge25', 'g-join2', 'text', 'g-join3', 'a'),
edge('ge26', 'g-join3', 'text', 'g-settext2', 'text'),
edge('ge27', 'g-settext2', 'exec', 'g-brwin', 'exec'),
edge('ge28', 'g-coins', 'value', 'g-cmpwin', 'a'),
edge('ge29', 'g-needed', 'value', 'g-cmpwin', 'b'),
edge('ge30', 'g-cmpwin', 'result', 'g-brwin', 'condition'),
edge('ge31', 'g-brwin', 'then', 'g-stop1', 'exec'),
edge('ge32', 'g-stop1', 'exec', 'g-leveloff1', 'exec'),
edge('ge33', 'g-level', 'object', 'g-leveloff1', 'object'),
edge('ge34', 'g-leveloff1', 'exec', 'g-winon', 'exec'),
edge('ge35', 'g-win', 'object', 'g-winon', 'object'),
edge('ge36', 'g-brwin', 'else', 'g-brlose', 'exec'),
edge('ge37', 'g-time2', 'value', 'g-cmplose', 'a'),
edge('ge38', 'g-cmplose', 'result', 'g-brlose', 'condition'),
edge('ge39', 'g-brlose', 'then', 'g-stop2', 'exec'),
edge('ge40', 'g-stop2', 'exec', 'g-leveloff2', 'exec'),
edge('ge41', 'g-level', 'object', 'g-leveloff2', 'object'),
edge('ge42', 'g-leveloff2', 'exec', 'g-loseon', 'exec'),
edge('ge43', 'g-lose', 'object', 'g-loseon', 'object'),
],
};
/** "Start button": a click resets the counters on the Game behavior and swaps title for level. */
const startButtonGraph = {
nodes: [
{ id: 's-click', type: 'OnClick', x: 0, y: 0, params: {} },
{ id: 's-game', type: 'ObjectByName', x: 0, y: 160, params: { name: 'Game' } },
{ id: 's-title', type: 'ObjectByName', x: 0, y: 240, params: { name: 'Title' } },
{ id: 's-level', type: 'ObjectByName', x: 0, y: 320, params: { name: 'Level' } },
{ id: 's-coins', type: 'SetProperty', x: 240, y: 0, params: { property: 'coins', value: 0 } },
{ id: 's-time', type: 'SetProperty', x: 460, y: 0, params: { property: 'timeLeft', value: 120 } },
{ id: 's-run', type: 'SetProperty', x: 680, y: 0, params: { property: 'running', value: true } },
{ id: 's-hide', type: 'SetEnabled', x: 900, y: 0, params: { enabled: false } },
{ id: 's-show', type: 'SetEnabled', x: 1120, y: 0, params: { enabled: true } },
],
edges: [
edge('se1', 's-click', 'exec', 's-coins', 'exec'),
edge('se2', 's-game', 'object', 's-coins', 'object'),
edge('se3', 's-coins', 'exec', 's-time', 'exec'),
edge('se4', 's-game', 'object', 's-time', 'object'),
edge('se5', 's-time', 'exec', 's-run', 'exec'),
edge('se6', 's-game', 'object', 's-run', 'object'),
edge('se7', 's-run', 'exec', 's-hide', 'exec'),
edge('se8', 's-title', 'object', 's-hide', 'object'),
edge('se9', 's-hide', 'exec', 's-show', 'exec'),
edge('se10', 's-level', 'object', 's-show', 'object'),
],
};
/** "Restart button": one click, one node – the scene starts over from its saved state. */
const restartButtonGraph = {
nodes: [
{ id: 'r-click', type: 'OnClick', x: 0, y: 0, params: {} },
{ id: 'r-restart', type: 'RestartGame', x: 260, y: 0, params: {} },
],
edges: [edge('re1', 'r-click', 'exec', 'r-restart', 'exec')],
};
const variable = (key, kind, value) => ({ key, label: key, kind, default: value });
// --- project ---------------------------------------------------------------------------------
const project = {
formatVersion: 1,
id: projectId,
name: 'Jump and Run',
activeSceneId: 'scene1',
assets: [
{
id: 'scene1',
type: 'scene',
name: 'Level 1',
props: { backgroundColor: SKY },
objects: [
...instantiate('L', 'level', 'Level', { x: 0, y: 0, z: 0 }, levelTemplate),
// All three screens sit on the camera frame and start switched off, so the level stays
// visible in the editor; the Game behavior decides on `On start` which one is up.
...instantiate('T', 'title', 'Title', { x: 0, y: 0, z: 10 }, titleTemplate, { enabled: false }),
...instantiate('W', 'win', 'Win', { x: 0, y: 0, z: 10 }, winTemplate, { enabled: false }),
...instantiate('B', 'lose', 'Lose', { x: 0, y: 0, z: 10 }, loseTemplate, { enabled: false }),
{ id: 'ocam', assetId: 'cam', name: 'Main Camera', position: { x: 0, y: 0, z: 0 }, overrides: {}, parentId: '', templateId: '' },
{ id: 'ogame', assetId: 'game', name: 'Game', position: { x: -200, y: -220, z: 0 }, overrides: {}, parentId: '', templateId: '' },
],
},
{ id: 'cam', type: 'camera', name: 'Main Camera', props: { width: 640, height: 360, zoom: 1 } },
{ id: 'box', type: 'sprite', name: 'Box', props: { width: 100, height: 100, color: STONE } },
{ id: 'player-sprite', type: 'sprite', name: 'Player Sprite', props: { width: 30, height: 40, color: PLAYER } },
{ id: 'coin-sprite', type: 'sprite', name: 'Coin Sprite', props: { width: 24, height: 24, color: COIN } },
{ id: 'label', type: 'text', name: 'Label', props: { text: 'Text', fontSize: 28, color: WHITE, align: 'center' } },
{ id: 'solid', type: 'collider', name: 'Solid Collider', props: { shape: 'box', width: 100, height: 100, radius: 50, isTrigger: false } },
{ id: 'trigger', type: 'collider', name: 'Coin Trigger', props: { shape: 'box', width: 24, height: 24, radius: 12, isTrigger: true } },
{
id: 'player',
type: 'behavior',
name: 'Player',
props: { speed: 170, jumpSpeed: 480, gravity: 1200, vy: 0, onGround: false },
variables: [
variable('speed', 'number', 0),
variable('jumpSpeed', 'number', 0),
variable('gravity', 'number', 0),
variable('vy', 'number', 0),
variable('onGround', 'boolean', false),
],
graph: playerGraph,
},
{
id: 'game',
type: 'behavior',
name: 'Game',
props: { timeLeft: 120, coins: 0, needed: 10, running: false },
variables: [
variable('timeLeft', 'number', 120),
variable('coins', 'number', 0),
variable('needed', 'number', 10),
variable('running', 'boolean', false),
],
graph: gameGraph,
},
{ id: 'start-button', type: 'behavior', name: 'Start Button', props: {}, variables: [], graph: startButtonGraph },
{ id: 'restart-button', type: 'behavior', name: 'Restart Button', props: {}, variables: [], graph: restartButtonGraph },
{ id: 'level', type: 'container', name: 'Level', props: {}, objects: levelTemplate },
{ id: 'title', type: 'container', name: 'Title screen', props: {}, objects: titleTemplate },
{ id: 'win', type: 'container', name: 'Win screen', props: {}, objects: winTemplate },
{ id: 'lose', type: 'container', name: 'Lose screen', props: {}, objects: loseTemplate },
],
};
await writeProject(projectId, baseUrl, project);