A variable could only be a number, a text or a yes/no, so every set of values had to be smuggled through a string - which is where Tetris got its unreadable "03066326048763714" colours from. A list is now a value of its own, with an item kind (number, text, yes/no, colour) that decides how the inspector reads the one comma-separated line it is typed as, and eight nodes to work with: List, Fill, Count, Item at, Set item, Sub list, Join lists, Index of. Two traps came with it. An array is typeof 'object', so every place that told a value from an entity had to learn the difference, or a list would have been passed around as a game object. And a value was compared by identity in three places - with lists that marks everything as an overridden property after each reload, so those comparisons now compare contents. There is no list of objects: a list is written to the project file and an object is a live handle that no reload survives. A set of objects stays a list of names. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
49 lines
1.7 KiB
TypeScript
49 lines
1.7 KiB
TypeScript
/**
|
||
* A list is deliberately flat – a list of lists would need a nested editor and a deep compare on
|
||
* every override, and nothing so far wants one: a board is addressed as `row · width + column`.
|
||
*/
|
||
export type ListValue = readonly (number | string | boolean)[];
|
||
|
||
/** Colors are stored as 0xRRGGBB numbers. */
|
||
export type PropertyValue = number | string | boolean | ListValue;
|
||
|
||
export type PropertyBag = Record<string, PropertyValue>;
|
||
|
||
export const isList = (value: unknown): value is ListValue => Array.isArray(value);
|
||
|
||
/** Values are compared by content, because two equal lists are never the same array. */
|
||
export function sameValue(a: PropertyValue | undefined, b: PropertyValue | undefined): boolean {
|
||
if (isList(a) || isList(b)) {
|
||
if (!isList(a) || !isList(b) || a.length !== b.length) return false;
|
||
return a.every((item, i) => item === b[i]);
|
||
}
|
||
return a === b;
|
||
}
|
||
|
||
/** `undefined` removes the key, which is how an undo of "no value" works. */
|
||
export function setBagValue(bag: PropertyBag, key: string, value: PropertyValue | undefined): void {
|
||
if (value === undefined) delete bag[key];
|
||
else bag[key] = value;
|
||
}
|
||
|
||
export interface Vec2 {
|
||
x: number;
|
||
y: number;
|
||
}
|
||
|
||
/** `x`/`y` in world units; `z` is the draw order – higher is on top, ties keep hierarchy order. */
|
||
export interface Position extends Vec2 {
|
||
z: number;
|
||
}
|
||
|
||
export type Axis = keyof Position;
|
||
|
||
export function toPosition(point: Vec2 | Position): Position {
|
||
return { x: point.x, y: point.y, z: 'z' in point ? point.z : 0 };
|
||
}
|
||
|
||
/** Stable draw order: ascending `z`, ties keep the input (hierarchy) order. */
|
||
export function sortByZ<T extends { z: number }>(items: readonly T[]): T[] {
|
||
return [...items].sort((a, b) => a.z - b.z);
|
||
}
|