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:
1 parent
4900d7bbf6
commit
76aa441dce
33 files changed
+1905
-301
No files matched your search
@@ -7,7 +7,7 @@ import { findAsset, findUsages } from '../../core/model/Project';
|
||||
import { Button, Dialog, ListView, Menu, type MenuItem, Panel, ScrollContainer } from '../../ui';
|
||||
import { type EditorState, objectSelection } from '../state';
|
||||
|
||||
const TYPE_LABELS: Record<AssetType, string> = { scene: 'Scene', camera: 'Camera', sprite: 'Sprite', behavior: 'Behavior', collider: 'Collider', container: 'Container' };
|
||||
const TYPE_LABELS: Record<AssetType, string> = { scene: 'Scene', camera: 'Camera', sprite: 'Sprite', text: 'Text', behavior: 'Behavior', collider: 'Collider', container: 'Container' };
|
||||
|
||||
/** Lists all assets of the project; creates, places and deletes them. */
|
||||
export class AssetPanel extends Panel {
|
||||
@@ -45,7 +45,7 @@ export class AssetPanel extends Panel {
|
||||
id: asset.id,
|
||||
label: asset.name,
|
||||
hint: asset.type === 'scene' && asset.id === active ? 'scene ●' : asset.type,
|
||||
swatch: asset.type === 'sprite' ? Number(asset.props.color ?? 0) : undefined,
|
||||
swatch: asset.type === 'sprite' || asset.type === 'text' ? Number(asset.props.color ?? 0) : undefined,
|
||||
})),
|
||||
);
|
||||
this.syncSelection();
|
||||
|
||||
@@ -25,7 +25,7 @@ import { isSelfOrAncestor, treeOrder } from '../../core/model/hierarchy';
|
||||
import type { Position, PropertyValue } from '../../core/model/Properties';
|
||||
import { findAsset, findScene, findSceneObject } from '../../core/model/Project';
|
||||
import type { SceneObject } from '../../core/model/SceneObject';
|
||||
import { type PropertyDef, schemaForAsset, VARIABLE_KINDS, type VariableKind } from '../../core/properties/PropertySchema';
|
||||
import { COMMON_PROPS, isCommonProp, type PropertyDef, schemaForAsset, VARIABLE_KINDS, type VariableKind } from '../../core/properties/PropertySchema';
|
||||
import { isModified, isOverridden, resolveAssetProps, resolveSceneObjectProps } from '../../core/properties/resolve';
|
||||
import { Button, HStack, Label, Menu, type MenuItem, Panel, ScrollContainer, TextField, Theme, ui, VStack } from '../../ui';
|
||||
import { createContainer, removeObjects } from '../actions';
|
||||
@@ -135,13 +135,19 @@ export class Inspector extends Panel {
|
||||
this.body.add(this.labelled('Name', this.nameField));
|
||||
|
||||
if (asset.type === 'behavior') {
|
||||
this.buildAssetProps(asset, COMMON_PROPS);
|
||||
this.buildVariables(asset);
|
||||
return;
|
||||
}
|
||||
|
||||
// Asset properties: values that differ from the schema default are marked and resettable.
|
||||
this.buildAssetProps(asset, schemaForAsset(asset));
|
||||
}
|
||||
|
||||
/** Asset properties: values that differ from the schema default are marked and resettable. */
|
||||
private buildAssetProps(asset: Asset, schema: readonly PropertyDef[]): void {
|
||||
const assetId = asset.id;
|
||||
const values = resolveAssetProps(asset);
|
||||
for (const def of schemaForAsset(asset)) {
|
||||
for (const def of schema) {
|
||||
const row = createFieldRow(def, values[def.key] ?? def.default, this.assetHandlers(assetId, def), () => this.state.execute(new ResetAssetPropertyCommand(assetId, def.key)));
|
||||
row.setModified(isModified(asset, def.key));
|
||||
this.fields.set(def.key, row);
|
||||
@@ -269,6 +275,11 @@ export class Inspector extends Panel {
|
||||
this.body.add(linkRow);
|
||||
}
|
||||
|
||||
// The master switch, right where it is looked for: above everything type-specific.
|
||||
for (const def of COMMON_PROPS) {
|
||||
this.body.add(this.objectPropertyRow(sceneId, objectId, def, resolveSceneObjectProps(asset, object), templateLinkOf(this.state.project, scene, object) ?? null).row);
|
||||
}
|
||||
|
||||
// Position – belongs to the scene object only, local to the parent. Z is the draw order (higher = on top).
|
||||
const parent = object.parentId ? findSceneObject(scene, object.parentId) : undefined;
|
||||
this.body.add(new Label(parent ? 'POSITION (LOCAL TO PARENT)' : 'POSITION', { style: 'title' }));
|
||||
@@ -302,48 +313,15 @@ export class Inspector extends Panel {
|
||||
this.body.add(row.row);
|
||||
}
|
||||
|
||||
// Asset properties (or behavior variables) with overrides.
|
||||
// Asset properties (or behavior variables) with overrides. The common switch (Enabled)
|
||||
// is shown on its own, above the position – it exists for every type and is not a
|
||||
// "property of a sprite" in the same sense.
|
||||
const schema = schemaForAsset(asset);
|
||||
if (schema.length > 0 || asset.type === 'behavior') this.body.add(new Label(asset.type === 'behavior' ? 'VARIABLES' : 'PROPERTIES', { style: 'title' }));
|
||||
if (schema.length === 0 && asset.type === 'behavior') this.body.add(new Label('The behavior declares no variables.', { style: 'muted' }));
|
||||
const values = resolveSceneObjectProps(asset, object);
|
||||
for (const def of schema) {
|
||||
let original: PropertyValue | undefined;
|
||||
let hadOverride = false;
|
||||
const row = createFieldRow(
|
||||
def,
|
||||
values[def.key] ?? def.default,
|
||||
{
|
||||
onInput: (value) => {
|
||||
const target = this.currentObject(sceneId, objectId);
|
||||
if (!target) return;
|
||||
if (original === undefined) {
|
||||
hadOverride = isOverridden(target, def.key);
|
||||
original = target.overrides[def.key] ?? values[def.key] ?? def.default;
|
||||
}
|
||||
target.overrides[def.key] = value;
|
||||
this.state.preview();
|
||||
},
|
||||
onChange: (value) => {
|
||||
const target = this.currentObject(sceneId, objectId);
|
||||
if (!target) return;
|
||||
if (original !== undefined) {
|
||||
if (hadOverride) target.overrides[def.key] = original;
|
||||
else delete target.overrides[def.key];
|
||||
}
|
||||
original = undefined;
|
||||
this.state.execute(new SetOverrideCommand(sceneId, objectId, def.key, value));
|
||||
},
|
||||
},
|
||||
() => this.state.execute(link ? new RevertOverrideToTemplateCommand(sceneId, objectId, def.key) : new ClearOverrideCommand(sceneId, objectId, def.key)),
|
||||
[
|
||||
...(link ? [{ label: 'Apply to container', onSelect: () => this.state.execute(new ApplyOverrideToContainerCommand(sceneId, objectId, def.key)) }] : []),
|
||||
{ label: 'Apply to asset', onSelect: () => this.state.execute(new ApplyOverrideToAssetCommand(sceneId, objectId, def.key)) },
|
||||
],
|
||||
);
|
||||
this.fields.set(def.key, row);
|
||||
this.body.add(row.row);
|
||||
}
|
||||
const own = schema.filter((def) => !isCommonProp(def.key));
|
||||
if (own.length > 0 || asset.type === 'behavior') this.body.add(new Label(asset.type === 'behavior' ? 'VARIABLES' : 'PROPERTIES', { style: 'title' }));
|
||||
if (own.length === 0 && asset.type === 'behavior') this.body.add(new Label('The behavior declares no variables.', { style: 'muted' }));
|
||||
for (const def of own) this.body.add(this.objectPropertyRow(sceneId, objectId, def, values, link ?? null).row);
|
||||
this.markObjectRows(object, asset, link);
|
||||
|
||||
if (asset.type === 'container') {
|
||||
@@ -368,6 +346,49 @@ export class Inspector extends Panel {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* One editable property of a scene object: writes an override, offers ↺ (back to the asset
|
||||
* or, inside a container, to the template) and ↑ (apply upwards). Registered in `fields`
|
||||
* so live refreshes and `markObjectRows` find it.
|
||||
*/
|
||||
private objectPropertyRow(sceneId: string, objectId: string, def: PropertyDef, values: Record<string, PropertyValue>, link: TemplateLink | null): FieldRow {
|
||||
let original: PropertyValue | undefined;
|
||||
let hadOverride = false;
|
||||
const row = createFieldRow(
|
||||
def,
|
||||
values[def.key] ?? def.default,
|
||||
{
|
||||
onInput: (value) => {
|
||||
const target = this.currentObject(sceneId, objectId);
|
||||
if (!target) return;
|
||||
if (original === undefined) {
|
||||
hadOverride = isOverridden(target, def.key);
|
||||
original = target.overrides[def.key] ?? values[def.key] ?? def.default;
|
||||
}
|
||||
target.overrides[def.key] = value;
|
||||
this.state.preview();
|
||||
},
|
||||
onChange: (value) => {
|
||||
const target = this.currentObject(sceneId, objectId);
|
||||
if (!target) return;
|
||||
if (original !== undefined) {
|
||||
if (hadOverride) target.overrides[def.key] = original;
|
||||
else delete target.overrides[def.key];
|
||||
}
|
||||
original = undefined;
|
||||
this.state.execute(new SetOverrideCommand(sceneId, objectId, def.key, value));
|
||||
},
|
||||
},
|
||||
() => this.state.execute(link ? new RevertOverrideToTemplateCommand(sceneId, objectId, def.key) : new ClearOverrideCommand(sceneId, objectId, def.key)),
|
||||
[
|
||||
...(link ? [{ label: 'Apply to container', onSelect: () => this.state.execute(new ApplyOverrideToContainerCommand(sceneId, objectId, def.key)) }] : []),
|
||||
{ label: 'Apply to asset', onSelect: () => this.state.execute(new ApplyOverrideToAssetCommand(sceneId, objectId, def.key)) },
|
||||
],
|
||||
);
|
||||
this.fields.set(def.key, row);
|
||||
return row;
|
||||
}
|
||||
|
||||
/**
|
||||
* "Parent: <name> ▾" – picks another object of the same scene, or none. The object's own
|
||||
* subtree is excluded (that would be a cycle). For behaviors the parent is the owner.
|
||||
|
||||
Reference in new issue
Block a user