Editor offset, global On click, Coin container in the jump-and-run seed

- SceneObject.editorOffset (x/y): shifts only where an object is drawn in
  the Scene tab, cascades to children, undo-able with a reset action.
  Never written into container templates.
- On click without an owner now fires for any click inside the camera
  frame instead of hit-testing the (editor-only) behavior symbol.
- seed-jumprun: coins are a "Coin" container (sprite + trigger), nested
  containers are expanded recursively, Destroy targets the coin's parent,
  and the clock uses a Ceil node instead of ToText.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
rootandClaude Opus 5 committed 2026-09-08 07:34:00 +02:00
1 parent 2511483a67
commit 3234eb480f
14 files changed
+229 -53

No files matched your search

+48 -1
View File
@@ -13,6 +13,7 @@ import {
ClearOverrideCommand,
MoveSceneObjectCommand,
RenameSceneObjectCommand,
SetEditorOffsetCommand,
SetOverrideCommand,
SetParentCommand,
} from '../../core/commands/sceneObjectCommands';
@@ -22,7 +23,7 @@ import type { Asset, BehaviorAsset, SceneAsset } from '../../core/model/Asset';
import { isPlaceableType } from '../../core/model/AssetTypes';
import { type TemplateLink, templateLinkOf } from '../../core/model/container';
import { isSelfOrAncestor, treeOrder } from '../../core/model/hierarchy';
import type { Position, PropertyValue } from '../../core/model/Properties';
import type { Position, PropertyValue, Vec2 } from '../../core/model/Properties';
import { findAsset, findScene, findSceneObject } from '../../core/model/Project';
import type { SceneObject } from '../../core/model/SceneObject';
import { COMMON_PROPS, isCommonProp, type PropertyDef, schemaForAsset, VARIABLE_KINDS, type VariableKind } from '../../core/properties/PropertySchema';
@@ -323,6 +324,41 @@ export class Inspector extends Panel {
this.body.add(row.row);
}
// Editor offset – where the object is *drawn* in the scene view, nothing else. Handy for
// objects that sit on top of each other (stacked screens): pull them apart to work on them.
this.body.add(new Label('EDITOR OFFSET (SCENE VIEW ONLY)', { style: 'title' }));
for (const axis of ['x', 'y'] as const) {
let original: Vec2 | undefined;
const row = createFieldRow(
{ key: `editorOffset${axis.toUpperCase()}`, label: `Offset ${axis.toUpperCase()}`, kind: 'number', default: 0, step: 1 },
object.editorOffset[axis],
{
onInput: (value) => {
const target = this.currentObject(sceneId, objectId);
if (!target) return;
if (!original) original = { ...target.editorOffset };
target.editorOffset[axis] = Number(value);
this.state.preview();
},
onChange: (value) => {
const target = this.currentObject(sceneId, objectId);
if (!target) return;
const previous = original ?? { ...target.editorOffset };
if (original) target.editorOffset = { ...original };
original = undefined;
this.state.execute(new SetEditorOffsetCommand(sceneId, objectId, { ...previous, [axis]: Number(value) }, previous));
},
},
// Back to "drawn where it really is". No apply: the offset never leaves the scene object.
() =>
this.currentObject(sceneId, objectId)?.editorOffset[axis]
? [{ label: 'Reset to 0', onSelect: () => this.resetEditorOffset(sceneId, objectId, axis) }]
: [],
);
this.fields.set(`editorOffset${axis.toUpperCase()}`, row);
this.body.add(row.row);
}
// 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.
@@ -557,6 +593,8 @@ export class Inspector extends Panel {
this.fields.get('x')?.setValue(object.position.x);
this.fields.get('y')?.setValue(object.position.y);
this.fields.get('z')?.setValue(object.position.z);
this.fields.get('editorOffsetX')?.setValue(object.editorOffset.x);
this.fields.get('editorOffsetY')?.setValue(object.editorOffset.y);
const values = resolveSceneObjectProps(asset, object);
for (const def of schemaForAsset(asset)) this.fields.get(def.key)?.setValue(values[def.key] ?? def.default);
this.markObjectRows(object, asset, templateLinkOf(this.state.project, scene, object));
@@ -569,6 +607,8 @@ export class Inspector extends Panel {
*/
private markObjectRows(object: SceneObject, asset: Asset, link: TemplateLink | undefined): void {
for (const axis of ['x', 'y', 'z'] as const) this.fields.get(axis)?.setModified(Boolean(link) && object.position[axis] !== link!.template.position[axis]);
// The offset has one base only: no offset at all. Bold means "shown somewhere else than it is".
for (const axis of ['x', 'y'] as const) this.fields.get(`editorOffset${axis.toUpperCase()}`)?.setModified(object.editorOffset[axis] !== 0);
for (const def of schemaForAsset(asset)) {
const deviates = link ? deviatesFromTemplate(object, link.template, def.key) : deviatesFromAsset(object, asset, def.key);
this.fields.get(def.key)?.setModified(deviates);
@@ -593,6 +633,13 @@ export class Inspector extends Panel {
};
}
/** Drops the editor offset on one axis (the row's ↺). */
private resetEditorOffset(sceneId: string, objectId: string, axis: 'x' | 'y'): void {
const object = this.currentObject(sceneId, objectId);
if (!object || object.editorOffset[axis] === 0) return;
this.state.execute(new SetEditorOffsetCommand(sceneId, objectId, { ...object.editorOffset, [axis]: 0 }));
}
/** Whether the object sits somewhere else on `axis` than the container template says. */
private positionMoved(sceneId: string, objectId: string, axis: 'x' | 'y' | 'z'): boolean {
const object = this.currentObject(sceneId, objectId);