game-maker / suite (push) Failing after 3m59s
A shader is an asset carrying a node graph (Texture color, Output, Property, Color, Add/Subtract/Multiply/Divide, Mix, Luminance) that compiles to the GLSL fragment of a Pixi filter. The Shader tab reuses the Behavior canvas; a sprite wears a shader through its Shader picker, and the rows below it are the graph's Property nodes. Property schema, resolution and deviation take the project because a sprite's rows now live in another asset. The built-in registry of the prototype is gone; "Adjust colors" ships as a graph in the fixture and the three example games. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
292 lines
9.5 KiB
TypeScript
292 lines
9.5 KiB
TypeScript
import { newId } from '../ids';
|
||
import { isSelfOrAncestor, localPositionFor, removeSubtrees, worldPosition } from '../model/hierarchy';
|
||
import {
|
||
captureObject,
|
||
isLiveInstance,
|
||
type ObjectMemo,
|
||
type ObjectRef,
|
||
restoreObject,
|
||
} from '../model/instance';
|
||
import { placementRejection } from '../model/placement';
|
||
import { type Position, type PropertyValue, toPosition, type Vec2 } from '../model/Properties';
|
||
import type { Project } from '../model/Project';
|
||
import { createSceneObject, type SceneObject } from '../model/SceneObject';
|
||
import { coerceValue } from '../properties/PropertySchema';
|
||
import { type Removed, restoreAt } from '../util/list';
|
||
import { type Command, CommandError } from './Command';
|
||
import {
|
||
requireAsset,
|
||
requireObject,
|
||
requireObjectRef,
|
||
requirePropDef,
|
||
requireHolder,
|
||
requireStructuralObject,
|
||
} from './require';
|
||
|
||
/**
|
||
* An edit to one placed object. The base captures the object before the edit and puts it back on
|
||
* undo, so a subclass only says what changes – and, if it wrote somewhere else too, how to take
|
||
* that back (`revert`).
|
||
*/
|
||
export abstract class ObjectEditCommand implements Command {
|
||
abstract readonly label: string;
|
||
private memo: ObjectMemo | undefined;
|
||
|
||
protected constructor(
|
||
private readonly holderId: string,
|
||
private readonly objectId: string,
|
||
) {}
|
||
|
||
protected abstract apply(ref: ObjectRef, project: Project): void;
|
||
|
||
protected revert(): void {}
|
||
|
||
execute(project: Project): void {
|
||
const ref = requireObjectRef(project, this.holderId, this.objectId);
|
||
this.memo = captureObject(ref);
|
||
this.apply(ref, project);
|
||
}
|
||
|
||
undo(): void {
|
||
this.revert();
|
||
if (this.memo) restoreObject(this.memo);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* The position is a world position; with a parent it is converted to the parent's local space.
|
||
* Placing a container adds the container object alone – its content is resolved from the template
|
||
* (see `model/instance.ts`), so it is never copied into the scene.
|
||
*/
|
||
export class PlaceAssetCommand implements Command {
|
||
readonly label = 'Place asset';
|
||
readonly objectId = newId();
|
||
|
||
constructor(
|
||
private readonly holderId: string,
|
||
private readonly assetId: string,
|
||
private readonly position: Vec2 | Position,
|
||
private readonly parentId = '',
|
||
) {}
|
||
|
||
execute(project: Project): void {
|
||
const holder = requireHolder(project, this.holderId);
|
||
const asset = requireAsset(project, this.assetId);
|
||
const rejection = placementRejection(project, holder, asset);
|
||
if (rejection) throw new CommandError(rejection);
|
||
if (this.parentId) requireStructuralObject(project, holder, this.parentId);
|
||
const local = localPositionFor(holder, this.parentId, toPosition(this.position));
|
||
holder.objects.push(createSceneObject(this.objectId, this.assetId, local, this.parentId));
|
||
}
|
||
|
||
undo(project: Project): void {
|
||
const holder = requireHolder(project, this.holderId);
|
||
holder.objects = holder.objects.filter((o) => o.id !== this.objectId);
|
||
}
|
||
}
|
||
|
||
/** Removes objects together with everything parented below them, as one undo step. */
|
||
export class RemoveSceneObjectsCommand implements Command {
|
||
readonly label: string;
|
||
private removed: Removed<SceneObject>[] = [];
|
||
|
||
constructor(
|
||
private readonly holderId: string,
|
||
private readonly objectIds: readonly string[],
|
||
) {
|
||
this.label = objectIds.length === 1 ? 'Remove object' : 'Remove objects';
|
||
}
|
||
|
||
execute(project: Project): void {
|
||
const holder = requireHolder(project, this.holderId);
|
||
for (const id of this.objectIds) requireStructuralObject(project, holder, id);
|
||
this.removed = removeSubtrees(holder, this.objectIds);
|
||
}
|
||
|
||
undo(project: Project): void {
|
||
restoreAt(requireHolder(project, this.holderId).objects, this.removed);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* The previous position is read at execution time, so a caller that previewed the move must
|
||
* restore the model before executing.
|
||
*/
|
||
export class MoveSceneObjectCommand extends ObjectEditCommand {
|
||
readonly label = 'Move object';
|
||
|
||
constructor(
|
||
holderId: string,
|
||
objectId: string,
|
||
private readonly position: Position,
|
||
) {
|
||
super(holderId, objectId);
|
||
}
|
||
|
||
protected override apply(ref: ObjectRef): void {
|
||
ref.setPosition(this.position);
|
||
}
|
||
}
|
||
|
||
/** Editor-only: undoable like any other edit, but it changes nothing the game can see. */
|
||
export class SetEditorOffsetCommand extends ObjectEditCommand {
|
||
readonly label = 'Editor offset';
|
||
|
||
constructor(
|
||
holderId: string,
|
||
objectId: string,
|
||
private readonly offset: Vec2,
|
||
) {
|
||
super(holderId, objectId);
|
||
}
|
||
|
||
protected override apply(ref: ObjectRef): void {
|
||
ref.setEditorOffset(this.offset);
|
||
}
|
||
}
|
||
|
||
export class RenameSceneObjectCommand extends ObjectEditCommand {
|
||
readonly label = 'Rename object';
|
||
|
||
constructor(
|
||
holderId: string,
|
||
objectId: string,
|
||
private readonly name: string,
|
||
) {
|
||
super(holderId, objectId);
|
||
}
|
||
|
||
protected override apply(ref: ObjectRef): void {
|
||
ref.setName(this.name);
|
||
}
|
||
}
|
||
|
||
abstract class OverrideCommand extends ObjectEditCommand {
|
||
protected constructor(
|
||
holderId: string,
|
||
objectId: string,
|
||
protected readonly key: string,
|
||
readonly label: string,
|
||
) {
|
||
super(holderId, objectId);
|
||
}
|
||
|
||
protected abstract nextValue(object: SceneObject, project: Project): PropertyValue | undefined;
|
||
|
||
protected override apply(ref: ObjectRef, project: Project): void {
|
||
ref.setOverride(this.key, this.nextValue(ref.object, project));
|
||
}
|
||
}
|
||
|
||
export class SetOverrideCommand extends OverrideCommand {
|
||
constructor(
|
||
holderId: string,
|
||
objectId: string,
|
||
key: string,
|
||
private readonly value: PropertyValue,
|
||
) {
|
||
super(holderId, objectId, key, `Override ${key}`);
|
||
}
|
||
|
||
protected override nextValue(object: SceneObject, project: Project): PropertyValue {
|
||
return coerceValue(
|
||
requirePropDef(project, requireAsset(project, object.assetId), this.key, object),
|
||
this.value,
|
||
);
|
||
}
|
||
}
|
||
|
||
export class ClearOverrideCommand extends OverrideCommand {
|
||
constructor(holderId: string, objectId: string, key: string) {
|
||
super(holderId, objectId, key, `Reset ${key}`);
|
||
}
|
||
|
||
// No schema check, unlike setting one: a key the asset no longer declares is exactly what one
|
||
// wants to be able to clear.
|
||
protected override nextValue(): undefined {
|
||
return undefined;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Moves an object among its siblings. Sibling order is the order the stored list happens to have
|
||
* (`treeOrder`), so the move is a splice – and because objects sharing a `z` fall back to that
|
||
* order, it is also how two overlapping objects are put in front of each other.
|
||
*/
|
||
export class ReorderObjectCommand implements Command {
|
||
readonly label = 'Reorder object';
|
||
private previousIndex = -1;
|
||
|
||
/** `beforeId` empty means "behind its last sibling". */
|
||
constructor(
|
||
private readonly holderId: string,
|
||
private readonly objectId: string,
|
||
private readonly beforeId: string,
|
||
) {}
|
||
|
||
execute(project: Project): void {
|
||
const holder = requireHolder(project, this.holderId);
|
||
const object = requireStructuralObject(project, holder, this.objectId);
|
||
if (this.beforeId === this.objectId)
|
||
throw new CommandError('An object cannot be moved in front of itself');
|
||
let at = holder.objects.length;
|
||
if (this.beforeId) {
|
||
const before = requireStructuralObject(project, holder, this.beforeId);
|
||
if (before.parentId !== object.parentId)
|
||
throw new CommandError('An object can only be reordered among its own siblings');
|
||
at = holder.objects.indexOf(before);
|
||
}
|
||
this.previousIndex = holder.objects.indexOf(object);
|
||
holder.objects.splice(this.previousIndex, 1);
|
||
// The index was read before the removal, so anything behind it has moved up by one.
|
||
holder.objects.splice(at > this.previousIndex ? at - 1 : at, 0, object);
|
||
}
|
||
|
||
undo(project: Project): void {
|
||
const holder = requireHolder(project, this.holderId);
|
||
const object = requireObject(project, this.holderId, this.objectId);
|
||
holder.objects.splice(holder.objects.indexOf(object), 1);
|
||
holder.objects.splice(this.previousIndex, 0, object);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Re-parents an object ('' = top level). It keeps its world position: the local one is recomputed
|
||
* for the new parent.
|
||
*/
|
||
export class SetParentCommand implements Command {
|
||
readonly label = 'Set parent';
|
||
private previous: { parentId: string; position: Position } = {
|
||
parentId: '',
|
||
position: { x: 0, y: 0, z: 0 },
|
||
};
|
||
|
||
constructor(
|
||
private readonly holderId: string,
|
||
private readonly objectId: string,
|
||
private readonly parentId: string,
|
||
) {}
|
||
|
||
execute(project: Project): void {
|
||
const holder = requireHolder(project, this.holderId);
|
||
const object = requireStructuralObject(project, holder, this.objectId);
|
||
if (this.parentId) {
|
||
if (this.parentId === this.objectId) throw new CommandError('An object cannot be its own parent');
|
||
if (isLiveInstance(project, holder, requireStructuralObject(project, holder, this.parentId)))
|
||
throw new CommandError('A placed container shows its template; unpack it to put objects inside');
|
||
if (isSelfOrAncestor(holder, this.objectId, this.parentId))
|
||
throw new CommandError('An object cannot be parented to one of its own children');
|
||
}
|
||
this.previous = { parentId: object.parentId, position: { ...object.position } };
|
||
const world = worldPosition(holder, object);
|
||
object.parentId = this.parentId;
|
||
object.position = localPositionFor(holder, this.parentId, world);
|
||
}
|
||
|
||
undo(project: Project): void {
|
||
const object = requireObject(project, this.holderId, this.objectId);
|
||
object.parentId = this.previous.parentId;
|
||
object.position = { ...this.previous.position };
|
||
}
|
||
}
|