Refactoring: one file per node, consistent comments, shared helpers, split large modules

- Graph: nodes.ts split into nodes/<category>/<Type>.ts (62 files) with
  shared.ts factories and index.ts registry; NodeContext/values/NodeRegistry
  separated, writeTyped() replaces duplicated typed writes
- Comments: one style (JSDoc on non-obvious exports, // for decisions and
  traps), wrong/stale comments fixed or removed, ids.ts now real 10 base-36 chars
- Core: chainOf, shared worldPosition/editorPosition core, commands/require.ts,
  commands/naming.ts, util/list.ts, setBagValue/toPosition/propDef; dead code
  removed (isBehaviorAsset, SCENE_OBJECT_SCHEMA, CommandStack.subscribe,
  Input.axis, setMode, titleText, World.worldPosition, unused ctor params)
- Editor: Inspector split into inspector/ sections, App.ts into Editor.ts +
  debugHook.ts + shortcuts.ts, Viewport into items.ts + symbols.ts, shared
  "Add collider" action, unified type labels/swatches
- UI toolkit: Widget.trackHover/onPointerRelease, drawControlBox, math helpers,
  Popup.openBelow, Menu/Dialog/ColorPicker/ListRow in own files, named Theme sizes
- Tools: seed-demo/smoke-demo and demo-check project removed, tools/lib
  (harness, projects, builders), e2e.mjs as runner over tools/e2e/*.mjs sections
- Tests: tests/helpers.ts replaces copied fixtures and NodeContext doubles

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
rootandClaude Fable 5.1 committed 2026-09-08 08:37:22 +02:00
1 parent 3234eb480f
commit 1eb28314cf
189 files changed
+6231 -5543

No files matched your search

+130 -174
View File
@@ -6,7 +6,7 @@
* 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)
* Coin (sprite plus trigger collider – placed ten times inside the level container)
* Win / Lose (containers shown when the coins are collected or the 30 seconds are up)
* Win / Lose (containers shown when the coins are collected or the time is 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
@@ -14,7 +14,8 @@
*
* Usage: PATH=/opt/node22/bin:$PATH node tools/seed-jumprun.mjs [projectId] [baseUrl]
*/
import { PROJECTS, cliArgs, writeProject } from './projects.mjs';
import { containerFactory, edge, node, sceneObject, variable } from './lib/builders.mjs';
import { PROJECTS, cliArgs, writeProject } from './lib/projects.mjs';
const { projectId, baseUrl } = cliArgs('seed-jumprun.mjs', PROJECTS.jumpRun);
@@ -28,38 +29,29 @@ const COIN = 0xf5c542;
const PANEL = 0x11141c;
const BUTTON = 0x63c74d;
const WHITE = 0xf2f5f7;
const HINT = 0x9aa3b2;
const BUTTON_TEXT = 0x10240a;
// --- 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: '' },
const solid = (id, x, y, w, h, color) => [
sceneObject(id, 'box', { x, y, overrides: { width: w, height: h, color } }),
sceneObject(`${id}-c`, 'solid', { parentId: id, overrides: { width: w, height: h } }),
];
/**
* The coin is a container asset of its own: a sprite with a trigger collider child, so it reports
* the touch without blocking. The ten coins in the level are instances of it – changing the
* template changes all of them.
*/
const text = (id, x, y, content, { size = 28, color = WHITE, align = 'center', name = '', parentId = '', z = 1 } = {}) =>
sceneObject(id, 'label', { name, x, y, z, parentId, overrides: { text: content, fontSize: size, color, align } });
/** One placed coin: the container object; its content comes from the coin template. */
const coin = (id, x, y) => sceneObject(id, 'coin', { name: 'Coin', x, y });
/** The coin: a sprite with a trigger collider child, so it reports the touch without blocking. */
const coinTemplate = [
{ id: 'coin-body', assetId: 'coin-sprite', name: '', position: { x: 0, y: 0, z: 1 }, overrides: {}, parentId: '', templateId: '' },
{ id: 'coin-body-c', assetId: 'trigger', name: '', position: { x: 0, y: 0, z: 0 }, overrides: {}, parentId: 'coin-body', templateId: '' },
sceneObject('coin-body', 'coin-sprite', { z: 1 }),
sceneObject('coin-body-c', 'trigger', { parentId: 'coin-body' }),
];
/** One placed coin: the container object, its content comes from the template above. */
const coin = (id, x, y) => ({ id, assetId: 'coin', name: 'Coin', position: { x, y, z: 0 }, overrides: {}, parentId: '', 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),
@@ -83,65 +75,37 @@ const levelTemplate = [
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: '' },
sceneObject('player', 'player-sprite', { name: 'Player', x: -270, y: 125, z: 2 }),
sceneObject('player-c', 'solid', { parentId: 'player', overrides: { width: 30, height: 40 } }),
sceneObject('player-logic', 'player', { parentId: 'player', y: -60 }),
// HUD, top left and top right of the camera frame.
text('time-label', -300, -160, 'Time 30', { 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 screenTemplate = (prefix, headline, hint, buttonLabel, behaviorId) => [
sceneObject(`${prefix}-panel`, 'box', { overrides: { width: 640, height: 360, color: PANEL } }),
text(`${prefix}-title`, 0, -70, headline, { size: 52 }),
text(`${prefix}-hint`, 0, -10, hint, { size: 20, color: HINT }),
sceneObject(`${prefix}-button`, 'box', { y: 80, z: 1, overrides: { width: 240, height: 60, color: BUTTON } }),
text(`${prefix}-button-label`, 0, 0, buttonLabel, { size: 26, color: BUTTON_TEXT, parentId: `${prefix}-button` }),
sceneObject(`${prefix}-button-logic`, behaviorId, { parentId: `${prefix}-button`, y: -60 }),
];
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 time is over - try again.', 'PLAY AGAIN', 'restart-button');
/** Which template belongs to which container asset – the lookup `expand` needs for nesting. */
const containerTemplates = {
const templates = {
level: levelTemplate,
coin: coinTemplate,
title: titleTemplate,
win: winTemplate,
lose: loseTemplate,
title: screenTemplate('title', 'JUMP & RUN', 'Arrow keys to move, Space to jump.', 'START', 'start-button'),
win: screenTemplate('win', 'YOU WIN!', 'All ten coins collected in time.', 'PLAY AGAIN', 'restart-button'),
lose: screenTemplate('lose', 'TIME UP', 'The time is over - try again.', 'PLAY AGAIN', 'restart-button'),
};
/**
* A copy of every object of `template`, parented below `hostId` and remembering its template id.
* A template object that is itself a container is expanded in turn (unless the template already
* spells its content out) – exactly what `instantiateContainer` does in the editor.
*/
const expand = (template, hostId) => {
const objects = [];
for (const t of template) {
const id = `${hostId}-${t.id}`;
objects.push({ ...t, id, parentId: t.parentId ? `${hostId}-${t.parentId}` : hostId, templateId: t.id });
const nested = containerTemplates[t.assetId];
if (nested && !template.some((o) => o.parentId === t.id)) objects.push(...expand(nested, id));
}
return objects;
};
/** One placed container: the container object plus a copy of every template object. */
const instantiate = (containerId, assetId, name, position, overrides = {}, extra = {}) => [
{ id: containerId, assetId, name, position, overrides, parentId: '', templateId: '', ...extra },
...expand(containerTemplates[assetId], containerId),
];
const { instantiate } = containerFactory(templates);
// --- 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 – 35 nodes in three strands.
* "Player": gravity, walking, jumping and picking coins up – 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.
@@ -151,45 +115,43 @@ const edge = (id, from, fromPort, to, toPort) => ({ id, from: { node: from, port
*/
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 } },
node('p-upd', 'OnUpdate', 0, 0),
node('p-owner', 'Owner', 0, 140),
node('p-axis', 'AxisInput', 0, 220, { left: 'ArrowLeft', right: 'ArrowRight', up: '', down: '' }),
node('p-speed', 'GetVariable', 0, 360, { variable: 'speed' }),
node('p-mulx', 'Multiply', 220, 300),
node('p-vy', 'GetVariable', 0, 440, { variable: 'vy' }),
node('p-grav', 'GetVariable', 0, 520, { variable: 'gravity' }),
node('p-mulg', 'Multiply', 220, 500),
node('p-addvy', 'Add', 420, 460),
node('p-setvy', 'SetVariable', 620, 0, { variable: 'vy' }),
node('p-vy2', 'GetVariable', 620, 340, { variable: 'vy' }),
node('p-move', 'Move', 840, 0, { collide: true }),
node('p-br', 'Branch', 1080, 0),
node('p-seq', 'Sequence', 1260, 0),
node('p-vy3', 'GetVariable', 1080, 260, { variable: 'vy' }),
node('p-cmp', 'Compare', 1260, 260, { op: '>=', b: 0 }),
node('p-ground', 'SetVariable', 1460, 0, { variable: 'onGround' }),
node('p-zero', 'SetVariable', 1460, 140, { variable: 'vy', value: 0 }),
node('p-air', 'SetVariable', 1260, 400, { 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' } },
node('p-key', 'OnKeyDown', 0, 700, { key: ' ' }),
node('p-og', 'GetVariable', 0, 820, { variable: 'onGround' }),
node('p-brj', 'Branch', 240, 700),
node('p-jump', 'GetVariable', 240, 880, { variable: 'jumpSpeed' }),
node('p-neg', 'Multiply', 440, 880, { b: -1 }),
node('p-setjump', 'SetVariable', 660, 700, { 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' } },
// The collider reports its body, which is the coin sprite; the whole coin is the container
// above it, so that is what gets destroyed.
{ id: 'p-coinroot', type: 'Parent', x: 1080, y: 1420, params: {} },
{ id: 'p-destroy', type: 'Destroy', x: 1280, y: 1200, params: {} },
node('p-col', 'OnCollisionEnter', 0, 1060),
node('p-iscoin', 'IsAsset', 260, 1180, { asset: 'coin-sprite' }),
node('p-brc', 'Branch', 480, 1060),
node('p-seqc', 'Sequence', 680, 1060),
node('p-game', 'ObjectByName', 680, 1280, { name: 'Game' }),
node('p-coins', 'GetProperty', 880, 1280, { property: 'coins' }),
node('p-add1', 'Add', 1080, 1280, { b: 1 }),
node('p-setcoins', 'SetProperty', 1280, 1060, { property: 'coins' }),
node('p-coinroot', 'Parent', 1080, 1420),
node('p-destroy', 'Destroy', 1280, 1200),
],
edges: [
edge('pe1', 'p-upd', 'exec', 'p-setvy', 'exec'),
@@ -236,55 +198,55 @@ const playerGraph = {
};
/**
* "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.
* "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 } },
node('g-start', 'OnStart', 0, 0),
node('g-title', 'ObjectByName', 0, 140, { name: 'Title' }),
node('g-level', 'ObjectByName', 0, 220, { name: 'Level' }),
node('g-win', 'ObjectByName', 0, 300, { name: 'Win' }),
node('g-lose', 'ObjectByName', 0, 380, { name: 'Lose' }),
node('g-on1', 'SetEnabled', 260, 0, { enabled: true }),
node('g-off1', 'SetEnabled', 480, 0, { enabled: false }),
node('g-off2', 'SetEnabled', 700, 0, { enabled: false }),
node('g-off3', 'SetEnabled', 920, 0, { 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' } },
node('g-upd', 'OnUpdate', 0, 520),
node('g-running', 'GetVariable', 0, 640, { variable: 'running' }),
node('g-brrun', 'Branch', 220, 520),
node('g-time', 'GetVariable', 220, 700, { variable: 'timeLeft' }),
node('g-sub', 'Subtract', 420, 700),
node('g-max', 'Max', 600, 700, { b: 0 }),
node('g-settime', 'SetVariable', 440, 520, { 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-ceil', type: 'Ceil', x: 640, y: 940, params: {} },
{ id: 'g-join', type: 'Join', x: 840, y: 940, params: { a: 'Time ' } },
{ id: 'g-settext', type: 'SetText', x: 700, y: 520, params: {} },
node('g-timelabel', 'ObjectByName', 440, 860, { name: 'TimeLabel' }),
node('g-time2', 'GetVariable', 440, 940, { variable: 'timeLeft' }),
node('g-ceil', 'Ceil', 640, 940),
node('g-join', 'Join', 840, 940, { a: 'Time ' }),
node('g-settext', 'SetText', 700, 520),
{ 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: {} },
node('g-coinlabel', 'ObjectByName', 700, 1080, { name: 'CoinLabel' }),
node('g-coins', 'GetVariable', 700, 1160, { variable: 'coins' }),
node('g-totext2', 'ToText', 900, 1160, { decimals: 0 }),
node('g-join2', 'Join', 1080, 1160, { a: 'Coins ' }),
node('g-join3', 'Join', 1260, 1160, { b: '/10' }),
node('g-settext2', 'SetText', 960, 520),
{ 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 } },
node('g-needed', 'GetVariable', 960, 700, { variable: 'needed' }),
node('g-cmpwin', 'Compare', 1160, 700, { op: '>=' }),
node('g-brwin', 'Branch', 1220, 520),
node('g-stop1', 'SetVariable', 1420, 440, { variable: 'running', value: false }),
node('g-leveloff1', 'SetEnabled', 1620, 440, { enabled: false }),
node('g-winon', 'SetEnabled', 1820, 440, { 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 } },
node('g-cmplose', 'Compare', 1160, 860, { op: '<=', b: 0 }),
node('g-brlose', 'Branch', 1420, 640),
node('g-stop2', 'SetVariable', 1620, 640, { variable: 'running', value: false }),
node('g-leveloff2', 'SetEnabled', 1820, 640, { enabled: false }),
node('g-loseon', 'SetEnabled', 2020, 640, { enabled: true }),
],
edges: [
edge('ge1', 'g-start', 'exec', 'g-on1', 'exec'),
@@ -341,15 +303,15 @@ const gameGraph = {
/** "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: 30 } },
{ 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 } },
node('s-click', 'OnClick', 0, 0),
node('s-game', 'ObjectByName', 0, 160, { name: 'Game' }),
node('s-title', 'ObjectByName', 0, 240, { name: 'Title' }),
node('s-level', 'ObjectByName', 0, 320, { name: 'Level' }),
node('s-coins', 'SetProperty', 240, 0, { property: 'coins', value: 0 }),
node('s-time', 'SetProperty', 460, 0, { property: 'timeLeft', value: 30 }),
node('s-run', 'SetProperty', 680, 0, { property: 'running', value: true }),
node('s-hide', 'SetEnabled', 900, 0, { enabled: false }),
node('s-show', 'SetEnabled', 1120, 0, { enabled: true }),
],
edges: [
edge('se1', 's-click', 'exec', 's-coins', 'exec'),
@@ -367,15 +329,10 @@ const startButtonGraph = {
/** "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: {} },
],
nodes: [node('r-click', 'OnClick', 0, 0), node('r-restart', 'RestartGame', 260, 0)],
edges: [edge('re1', 'r-click', 'exec', 'r-restart', 'exec')],
};
const variable = (key, kind, value) => ({ key, label: key, kind, default: value });
// --- project ---------------------------------------------------------------------------------
const project = {
@@ -390,15 +347,14 @@ const project = {
name: 'Level 1',
props: { backgroundColor: SKY },
objects: [
...instantiate('L', 'level', 'Level', { x: 0, y: 0, z: 0 }),
// 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.
// The title screen carries an editor offset so it does not cover the level in the scene view.
...instantiate('T', 'title', 'Title', { x: 0, y: 0, z: 10 }, { enabled: true }, { editorOffset: { x: -750, y: -500 } }),
...instantiate('W', 'win', 'Win', { x: 0, y: 0, z: 10 }, { enabled: true }, { editorOffset: { x: 0, y: -500 } }),
...instantiate('B', 'lose', 'Lose', { x: 0, y: 0, z: 10 }, { enabled: true }, { editorOffset: { x: 750, y: -500 } }),
{ 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: '' },
...instantiate('L', 'level', { name: 'Level' }),
// All three screens sit on the camera frame; the Game behavior decides on `On start` which
// one is up. Editor offsets pull them apart in the scene view so they do not cover the level.
...instantiate('T', 'title', { name: 'Title', z: 10, overrides: { enabled: true }, editorOffset: { x: -750, y: -500 } }),
...instantiate('W', 'win', { name: 'Win', z: 10, overrides: { enabled: true }, editorOffset: { x: 0, y: -500 } }),
...instantiate('B', 'lose', { name: 'Lose', z: 10, overrides: { enabled: true }, editorOffset: { x: 750, y: -500 } }),
sceneObject('ocam', 'cam', { name: 'Main Camera' }),
sceneObject('ogame', 'game', { name: 'Game', x: -200, y: -220 }),
],
},
{ id: 'cam', type: 'camera', name: 'Main Camera', props: { width: 640, height: 360, zoom: 1 } },
@@ -439,9 +395,9 @@ const project = {
{ id: 'restart-button', type: 'behavior', name: 'Restart Button', props: {}, variables: [], graph: restartButtonGraph },
{ id: 'coin', type: 'container', name: 'Coin', props: {}, objects: coinTemplate },
{ 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 },
{ id: 'title', type: 'container', name: 'Title screen', props: {}, objects: templates.title },
{ id: 'win', type: 'container', name: 'Win screen', props: {}, objects: templates.win },
{ id: 'lose', type: 'container', name: 'Lose screen', props: {}, objects: templates.lose },
],
};