Files
feuerwerk/src/core/model/Scope.ts
T
adminandClaude Opus 5.5 d7c0b7cfdb
game-maker / gate (push) Successful in 2m43s
game-maker / suite (push) Successful in 8m40s
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>
2026-09-23 06:06:53 +02:00

61 lines
2.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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' | '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 {
if (owner.scope === 'global') return 'global';
return owner.scope === 'scene' ? 'scene' : 'local';
}
/** 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';
}
/** 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 {
if (raw === 'global') return 'global';
return raw === 'scene' ? 'scene' : 'local';
}
/** 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 === '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;
}