Define a struct once as an asset and reuse its shape (#253)
game-maker / gate (push) Successful in 3m30s
game-maker / suite (push) Successful in 10m33s

A struct used to declare its fields wherever it stood. The same shape
in two variables meant typing it twice, and Pack/Expand copied it once
without staying in sync. A Struct asset now holds the shape in one
place, and every struct that picks it as its type follows it.

- New asset type "Struct" (not placeable, group Structs, own icon).
  The inspector shows its fields with name, kind and default value,
  and "Open" shows them in the variables tab.
- Struct variables and struct fields get a Type row: own fields, any
  Struct asset, or "Make struct type". With a type the fields come
  from the asset and cannot be added, renamed or removed there. The
  values stay per variable, overrides on placed objects still work.
- Pack and Expand get the setting Type. With a type their ports are
  the fields of the asset and read-only in the inspector.
- A change to the asset reaches every typed place in one undo step
  (syncStructTypes). A new field arrives with its default. A removed
  field goes along with its port, edge and typed value. A renamed
  field keeps its value in variables, overrides and on ports with
  edges, also through nested types.
- Undo for these edits records plain assignments (Recorder), because
  the reach of a change is not known in advance.
- Cycles such as Loot in Loot, also through another type, are refused
  and not offered in the picker.
- Deleting the asset names how many places use it. Afterwards each
  place keeps its fields as its own.
- Loading reshapes a stale copy in a project file to its type.
- "Make struct type" creates an asset from an existing struct and
  links the place to it.

Differs from the plan:

- A changed default value applies to new fields and newly typed
  places only. Existing values stay.
- Removing a single field shows no warning. Undo brings it back.

Typed lists are left for #254. Lists stay dynamic for now.

Found by e2e: after deleting or renaming the type the inspector did
not rebuild and kept showing the old name. Fixed, the check was red
before.

Tests:
- ./check.sh: 182 files, 2661 tests, CHECK EXIT 0
- GM_STEPS=1,4,21,22,54: ALL PASSED, 176 checks
- 14 unit tests in tests/core/struct-types.test.ts
- New e2e module 54 with 14 checks, real mouse. Module 04 counts
  23 asset types.
- Counter-probes: without the rename pass 4 unit tests fail, without
  reshaping values 8, without the cycle rule 1. In the browser,
  without spreading changes and with + Field despite a type, 3
  checks fail.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5.5 committed 2026-09-28 06:04:40 +02:00
1 parent 4857bb75e6
commit 61bbb96a96
29 files changed
+1523 -66

No files matched your search

+4 -2
View File
@@ -4,6 +4,7 @@
* One editor for every such tab, pointed at the target of the one showing, like the asset editors.
*/
import type { Container } from 'pixi.js';
import { isVariableOwner } from '../../core/model/Asset';
import { findObject } from '../../core/model/hierarchy';
import { findAsset } from '../../core/model/Project';
import { Label, ScrollContainer, Theme, ui, VStack, HStack } from '../../ui';
@@ -20,7 +21,7 @@ function ownerOf(state: EditorState, target: Extract<ValuesTarget, { kind: 'vari
const { owner } = target;
if (owner.kind === 'asset') {
const asset = findAsset(state.project, owner.assetId);
return asset?.type === 'behavior' || asset?.type === 'variables' ? { asset, name: asset.name } : null;
return isVariableOwner(asset) ? { asset, name: asset.name } : null;
}
const view = viewOf(state.project, owner.holderId);
const object = view && findObject(view, owner.objectId);
@@ -32,7 +33,8 @@ function ownerOf(state: EditorState, target: Extract<ValuesTarget, { kind: 'vari
export function valuesTitle(state: EditorState, target: ValuesTarget, live?: LiveVariables): string | null {
if (target.kind === 'list') return resolveList(state, target.slot, live)?.title ?? null;
const owner = ownerOf(state, target);
return owner ? `${owner.name} · Variables` : null;
if (!owner) return null;
return `${owner.name} · ${owner.asset.type === 'struct' ? 'Fields' : 'Variables'}`;
}
export class VariablesEditor extends VStack {