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>
This commit is contained in:
rootandClaude Opus 5 committed 2026-09-07 20:52:10 +02:00
1 parent 4900d7bbf6
commit 76aa441dce
33 files changed
+1905 -301

No files matched your search

+18 -3
View File
@@ -1,4 +1,4 @@
import { type EntityHandle, GraphInterpreter, type NodeContext, type TraceListener } from '../../core/graph';
import { type EntityHandle, GraphInterpreter, isHandleEnabled, type NodeContext, type TraceListener } from '../../core/graph';
import { findAsset, type Project } from '../../core/model/Project';
import type { Entity } from '../Entity';
import type { World } from '../World';
@@ -44,12 +44,14 @@ export class BehaviorSystem implements System {
/**
* One instance per behavior entity: entities that appeared (placed while playing) get an
* interpreter and their `On start`; instances whose entity is gone are dropped.
* interpreter and their `On start`; instances whose entity is gone are dropped. A behavior
* that is switched off has no instance yet – it gets one, and its `On start`, the moment it
* is switched on. That is what lets a hidden screen wake up when it is shown.
*/
private syncInstances(world: World): void {
this.instances = this.instances.filter((i) => world.entities.includes(i.entity));
for (const entity of world.entities) {
if (entity.type !== 'behavior' || this.instances.some((i) => i.entity === entity)) continue;
if (entity.type !== 'behavior' || !isHandleEnabled(entity) || this.instances.some((i) => i.entity === entity)) continue;
const asset = findAsset(this.project, entity.assetId);
if (!asset || asset.type !== 'behavior') continue;
const trace: Trace = { edges: new Map(), nodes: new Map() };
@@ -66,6 +68,7 @@ export class BehaviorSystem implements System {
const keyOf = (node: { params: Record<string, unknown> }) => String(node.params.key ?? '');
for (const instance of this.instances) {
if (!world.entities.includes(instance.entity)) continue; // destroyed during play
if (!isHandleEnabled(instance.entity)) continue; // switched off (itself or a parent)
this.run(instance, (i, ctx) => {
for (const key of this.input.justPressed) i.fire('OnKeyDown', ctx, {}, (n) => keyOf(n) === key);
for (const key of this.input.justReleased) i.fire('OnKeyUp', ctx, {}, (n) => keyOf(n) === key);
@@ -81,6 +84,10 @@ export class BehaviorSystem implements System {
}
}
this.fireCollisions(instance, i, ctx);
const pointer = world.pointer;
if (pointer.pressed && world.hitTest(instance.entity.parent ?? instance.entity, pointer.x, pointer.y)) {
i.fire('OnClick', ctx, { x: pointer.x, y: pointer.y });
}
i.fire('OnUpdate', ctx, { dt });
}, dt);
}
@@ -144,6 +151,11 @@ export class BehaviorSystem implements System {
dt,
findByName: (name) => world.findByName(name) ?? null,
isKeyDown: (key) => this.input.isDown(key),
pointer: world.pointer,
hitTest: (target, x, y) => {
const found = world.entities.find((e) => e === target || e.id === target.id);
return Boolean(found && world.hitTest(found, x, y));
},
isTouching: (a, b) => {
const ea = world.entities.find((e) => e === a || e.id === a.id);
const eb = world.entities.find((e) => e === b || e.id === b.id);
@@ -172,6 +184,9 @@ export class BehaviorSystem implements System {
stop: () => {
world.stopRequested = true;
},
restart: () => {
world.restartRequested = true;
},
};
}