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>
409 lines
22 KiB
JavaScript
409 lines
22 KiB
JavaScript
#!/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);
|