Variables and state machines can reset with every scene (#164)
A shared variable or machine used to live either per object or for the
whole run. Levels need a third lifetime: shared like global, but built
fresh from the declarations whenever a scene is loaded.
- New scope `scene` for Variables assets and state machines. A missing
field keeps the old meaning: `global` for assets, `local` for
machines, so existing projects do not change.
- Returning to a scene that was visited before is fresh as well. There
is no memory per scene.
- A `scene` machine announces its start state again after a scene swap,
so `On state entered` in the new scene hears it. A global machine
stays quiet because it never left its state.
- The rule lives in two pure functions in `sharedState.ts`
(`carriedForward`, `restartedAssets`). BehaviorSystem only calls them.
- The inspector shows a Scope dropdown on Variables assets and a third
step on the machine dropdown, with a note on what `scene` means.
Decisions:
- `scene` and `persistent` exclude each other (variant A). The choice
dialog was cancelled, so this is an assumption to revisit. A
scene-scoped asset hides the Persistent switch, and switching to
`scene` drops the flag (undo restores it). The guard sits in
`isPersistent` itself, so no path can save values the scope throws
away anyway.
Findings:
- `clickText('Scene')` hits the Scene tab, not the menu entry. One e2e
check was red because of that, and its partner was falsely green
because it compared `undefined` with `undefined`. Both use `clickLast`
now.
Tests:
- ./check.sh: 144 files, 2142 tests, CHECK EXIT 0 (before 143 / 2133)
- 9 new unit tests in tests/core/scene-scope.test.ts
- e2e 32 fresh: ALL PASSED, 17 checks
- e2e 1-3,17,18,26,32,35 fresh: ALL PASSED, 213 checks
- 3 new e2e checks (35: scene-scoped asset resets on swap, 32h: dropdown,
note, hidden Persistent switch)
- Counter-checks bite: ignoring `restarted` fails 2 unit tests and the
new check in 35, the old announcement rule fails 1, dropping the scope
guard in `isPersistent` fails 1. Originals restored, cmp IDENTICAL.
- Check sum 1307 (1304 + 3) is computed. Last CI measurement: 1293.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
6e3e8d23c5
commit
d7c0b7cfdb
12 files changed
+487
-43
No files matched your search
@@ -43,11 +43,16 @@ export interface BehaviorAsset extends AssetBase {
|
||||
*
|
||||
* A `persistent` asset keeps its values past the end of the run (#166): the next run starts from
|
||||
* what the last one left, not from `props`. Absent is not persistent.
|
||||
*
|
||||
* `scope` says how long one set of values lives: absent is `global`, the whole run, and `scene`
|
||||
* builds them again from the declarations with every scene that is loaded (#164). A `scene` asset
|
||||
* is never persistent – what a scene swap throws away has nothing to keep between runs.
|
||||
*/
|
||||
export interface VariablesAsset extends AssetBase {
|
||||
type: 'variables';
|
||||
variables: PropertyDef[];
|
||||
persistent?: boolean;
|
||||
scope?: Scope;
|
||||
}
|
||||
|
||||
/** Placed as a child of the object it belongs to; its position is the offset from that body. */
|
||||
@@ -159,7 +164,8 @@ export interface AnimationAsset extends AssetBase {
|
||||
/**
|
||||
* 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.
|
||||
* A `global` machine has one current state for the whole run instead of one per behavior instance;
|
||||
* a `scene` machine shares one too, but starts over with every scene that is loaded (#164).
|
||||
*
|
||||
* `start` is the id of the state every instance begins in, empty when the machine has no start –
|
||||
* then it is in no state until `Set state` runs. It lives on the asset rather than on the node
|
||||
|
||||
+42
-11
@@ -1,29 +1,60 @@
|
||||
/**
|
||||
* Whether a state machine keeps its current state per behavior instance or shares one for the whole
|
||||
* run. Everything was `local` before scopes existed, so a missing field means `local` and old
|
||||
* projects load unchanged. Variables are shared by living on a Variables asset instead (#91).
|
||||
* How long a shared value lives. `local` is one per behavior instance, `global` one for the whole
|
||||
* run, `scene` one per scene: rebuilt from the declarations whenever a scene is loaded, so what a
|
||||
* level counts is gone with the level while a score is not (#164).
|
||||
*
|
||||
* Everything was `local` before scopes existed, so a missing field means `local` and old projects
|
||||
* load unchanged. Variables are shared by living on a Variables asset instead (#91) – that asset is
|
||||
* shared by its nature, so a missing field means `global` there (see `sharedScopeOf`).
|
||||
*/
|
||||
export type Scope = 'local' | 'global';
|
||||
export type Scope = 'local' | 'global' | 'scene';
|
||||
|
||||
/** The scopes of something that is shared either way: a Variables asset has no `local`. */
|
||||
export type SharedScope = 'global' | 'scene';
|
||||
|
||||
export interface Scoped {
|
||||
scope?: Scope;
|
||||
}
|
||||
|
||||
export function scopeOf(owner: Scoped): Scope {
|
||||
return owner.scope === 'global' ? 'global' : 'local';
|
||||
if (owner.scope === 'global') return 'global';
|
||||
return owner.scope === 'scene' ? 'scene' : 'local';
|
||||
}
|
||||
|
||||
export function isGlobal(owner: Scoped): boolean {
|
||||
return owner.scope === 'global';
|
||||
/** Whether the value is shared at all: both `global` and `scene` are one value for many instances. */
|
||||
export function isShared(owner: Scoped): boolean {
|
||||
return owner.scope === 'global' || owner.scope === 'scene';
|
||||
}
|
||||
|
||||
/** Reads a scope out of loaded data, where anything but the one known word means `local`. */
|
||||
/** Whether a scene swap rebuilds the value instead of handing it over. */
|
||||
export function isPerScene(owner: Scoped): boolean {
|
||||
return owner.scope === 'scene';
|
||||
}
|
||||
|
||||
/** Reads a scope out of loaded data, where anything but the two known words means `local`. */
|
||||
export function readScope(raw: unknown): Scope {
|
||||
return raw === 'global' ? 'global' : 'local';
|
||||
if (raw === 'global') return 'global';
|
||||
return raw === 'scene' ? 'scene' : 'local';
|
||||
}
|
||||
|
||||
/** Keeps `local` out of the document: the field is present only when it says `global`. */
|
||||
/** Keeps `local` out of the document: the field is present only when it says more than that. */
|
||||
export function setScope(owner: Scoped, scope: Scope): void {
|
||||
if (scope === 'global') owner.scope = 'global';
|
||||
if (scope === 'local') delete owner.scope;
|
||||
else owner.scope = scope;
|
||||
}
|
||||
|
||||
/** The scope of something shared by nature, where a missing field is `global` rather than `local`. */
|
||||
export function sharedScopeOf(owner: Scoped): SharedScope {
|
||||
return owner.scope === 'scene' ? 'scene' : 'global';
|
||||
}
|
||||
|
||||
/** Reads the scope of something shared by nature out of loaded data. */
|
||||
export function readSharedScope(raw: unknown): SharedScope {
|
||||
return raw === 'scene' ? 'scene' : 'global';
|
||||
}
|
||||
|
||||
/** Keeps the default out of the document the other way round: only `scene` is written. */
|
||||
export function setSharedScope(owner: Scoped, scope: SharedScope): void {
|
||||
if (scope === 'scene') owner.scope = 'scene';
|
||||
else delete owner.scope;
|
||||
}
|
||||
Reference in new issue
Block a user