A variable or a state machine declared `global` keeps one value for the whole run instead of one per behavior instance. The scope sits on the declaration -- `PropertyDef.scope` and `StateMachineAsset.scope`, both optional, absence meaning `local` -- and `local` is never written, so every project made before this saves back unchanged. **The name is the key.** Two behaviors declaring a global `score` mean one variable; a behavior declaring `score` locally keeps its own. Kinds that disagree about one global name stop the run before it starts, with the two behaviors named; starting values that disagree are a warning on the behavior that declares it, and the first declaration wins (`globalConflicts`). The runtime needs no redirection layer. A global variable falls out of `schemaForObject`, so it never reaches `entity.props` -- and then the whole rule is "whoever declares the name takes their own value" (`graph/sharedState.ts`). The `Variables` node is untouched and stays transparent: switching a variable to global breaks no existing graph, because `GraphInterpreter.readAll` builds a fresh object per read anyway and only asks `port.key in all`. That same cut answers the inspector. A shared value has no owner, so the placed object shows no row for it and can hold no override -- `SetVariableScopeCommand` takes the deviations that were there and undo gives them back. Both places say it in words rather than leaving a glyph to be guessed: the declaration explains the mark below the list, and the placed object names the variable it no longer holds instead of claiming the behavior declares none -- which is what it said before, untruthfully. Screenshots of the e2e run turned that up; no check had been looking at it. `SharedState` hangs off `NodeContext`, is built in `BehaviorSystem.start()` from the *declarations* rather than from any instance (a global variable exists before its behavior is placed), and is thrown away with the run: nothing is written back, not even in the editor run. `GraphInterpreter.call` passes it on through `...ctx` while nested calls keep getting fresh local variables, which is why `program.defaults` is stripped of global keys. Two traps this hit. `defSignature`/`assetSignature` have to carry the scope or the inspector does not rebuild after the switch -- and a state machine has no rows at all, so its scope is appended to the signature by hand. And the object inspector does not go through `schemaForObject` for behaviors, it reads `asset.variables` directly; filtering in one place was not enough, and no unit test covered that path. The e2e caught it (`global: true` where false belonged), and the rule now lives in one named function, `objectRows`. Scopes `object` and `scene` are deliberately not built. The object case already has a route (`Object by name` -> `Properties`), and a run knows exactly one scene, so `scene` would be indistinguishable from `global` until #39 exists. Closes #59: a machine's scope is a word on the machine asset, not a setting per `Set state` node, so the same machine cannot live in two places at once. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
153 lines
4.5 KiB
TypeScript
153 lines
4.5 KiB
TypeScript
import type { Graph } from '../graph/Graph';
|
||
import type { PropertyDef } from '../properties/PropertySchema';
|
||
import type { AssetType } from './AssetTypes';
|
||
import type { PropertyBag } from './Properties';
|
||
import type { Scope } from './Scope';
|
||
import type { SceneObject } from './SceneObject';
|
||
|
||
export interface AssetBase {
|
||
id: string;
|
||
type: AssetType;
|
||
name: string;
|
||
props: PropertyBag;
|
||
}
|
||
|
||
export interface SpriteAsset extends AssetBase {
|
||
type: 'sprite';
|
||
}
|
||
|
||
export interface CameraAsset extends AssetBase {
|
||
type: 'camera';
|
||
}
|
||
|
||
export interface TextAsset extends AssetBase {
|
||
type: 'text';
|
||
}
|
||
|
||
export interface SceneAsset extends AssetBase {
|
||
type: 'scene';
|
||
objects: SceneObject[];
|
||
}
|
||
|
||
/** `variables` is this asset's own property schema; `props` holds their default values. */
|
||
export interface BehaviorAsset extends AssetBase {
|
||
type: 'behavior';
|
||
variables: PropertyDef[];
|
||
graph: Graph;
|
||
}
|
||
|
||
/** Placed as a child of the object it belongs to; its position is the offset from that body. */
|
||
export interface ColliderAsset extends AssetBase {
|
||
type: 'collider';
|
||
}
|
||
|
||
/**
|
||
* `objects` is a template, not content: a placed container shows it without copying it, so an
|
||
* edit here reaches every instance (see `model/instance.ts`). Positions are local to the
|
||
* container's origin.
|
||
*/
|
||
export interface ContainerAsset extends AssetBase {
|
||
type: 'container';
|
||
objects: SceneObject[];
|
||
}
|
||
|
||
/** `id` shares the generator with asset ids and is unique across the document (see `model/texture.ts`). */
|
||
export interface TextureFrame {
|
||
id: string;
|
||
name: string;
|
||
x: number;
|
||
y: number;
|
||
w: number;
|
||
h: number;
|
||
}
|
||
|
||
/**
|
||
* `file` names the image in `data/textures/` and is content-addressed (`{sha256}.{ext}`), so it
|
||
* can be cached forever; empty means the upload failed or has not happened yet. `width`/`height`
|
||
* are the size as uploaded, so frames can be sliced and validated before the image loads.
|
||
*/
|
||
export interface TextureAsset extends AssetBase {
|
||
type: 'texture';
|
||
file: string;
|
||
width: number;
|
||
height: number;
|
||
frames: TextureFrame[];
|
||
}
|
||
|
||
/**
|
||
* `frames` holds what a sprite's `texture` holds – a texture id or a frame id – in playing order.
|
||
* A picture may appear more than once, which is how one frame is held longer.
|
||
*/
|
||
export interface AnimationAsset extends AssetBase {
|
||
type: 'animation';
|
||
frames: string[];
|
||
}
|
||
|
||
/**
|
||
* The graph holds one `State` node per state and an edge per transition; a behavior reads the
|
||
* machine's current state through `State branch` and moves it through `Set state` or `State transition`.
|
||
* A `global` machine has one current state for the whole run instead of one per behavior instance.
|
||
*/
|
||
export interface StateMachineAsset extends AssetBase {
|
||
type: 'statemachine';
|
||
graph: Graph;
|
||
scope?: Scope;
|
||
}
|
||
|
||
/**
|
||
* The graph is GLSL in nodes: `Property` nodes declare the sprite rows, the `Output` node ends
|
||
* the fragment (see `core/shader/compile.ts`). A sprite wears it through its `shader` property.
|
||
*/
|
||
export interface ShaderAsset extends AssetBase {
|
||
type: 'shader';
|
||
graph: Graph;
|
||
}
|
||
|
||
/**
|
||
* `file` names the typeface in `data/fonts/` the way a texture names its image: content-addressed
|
||
* `{sha256}.{ext}`, empty when the upload failed. A text object wears it through its `font`
|
||
* property; the family the browser knows it under is derived from the file name, see `model/font`.
|
||
*/
|
||
export interface FontAsset extends AssetBase {
|
||
type: 'font';
|
||
file: string;
|
||
}
|
||
|
||
/**
|
||
* The graph is the motion of one particle: every output of its `Output` node is a function of the
|
||
* particle's age and its own random number (see `core/particles/compile.ts`). A placed object is
|
||
* the source that emits them; how many and for how long are its properties.
|
||
*/
|
||
export interface ParticlesAsset extends AssetBase {
|
||
type: 'particles';
|
||
graph: Graph;
|
||
}
|
||
|
||
export type Asset =
|
||
| SpriteAsset
|
||
| CameraAsset
|
||
| TextAsset
|
||
| SceneAsset
|
||
| BehaviorAsset
|
||
| ColliderAsset
|
||
| ContainerAsset
|
||
| TextureAsset
|
||
| AnimationAsset
|
||
| StateMachineAsset
|
||
| ShaderAsset
|
||
| FontAsset
|
||
| ParticlesAsset;
|
||
|
||
/** The assets edited as a node graph – what the graph canvas and the graph commands work on. */
|
||
export type GraphAsset = BehaviorAsset | StateMachineAsset | ShaderAsset | ParticlesAsset;
|
||
|
||
const GRAPH_TYPES: ReadonlySet<AssetType> = new Set(['behavior', 'statemachine', 'shader', 'particles']);
|
||
|
||
export function isGraphAsset(asset: Asset): asset is GraphAsset {
|
||
return GRAPH_TYPES.has(asset.type);
|
||
}
|
||
|
||
export function isSceneAsset(asset: Asset): asset is SceneAsset {
|
||
return asset.type === 'scene';
|
||
}
|