Variables and state machines can reset with every scene (#164)
game-maker / gate (push) Successful in 2m43s
game-maker / suite (push) Successful in 8m40s

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:
adminandClaude Opus 5.5 committed 2026-09-23 06:06:53 +02:00
1 parent 6e3e8d23c5
commit d7c0b7cfdb
12 files changed
+487 -43

No files matched your search

+57 -3
View File
@@ -15,7 +15,14 @@ import { isSoundFile } from '../model/sound';
import { removeSubtrees } from '../model/hierarchy';
import type { PropertyBag, PropertyValue } from '../model/Properties';
import { findAssetOf, objectHolders, type Project } from '../model/Project';
import { type Scope, scopeOf, setScope } from '../model/Scope';
import {
type Scope,
scopeOf,
setScope,
setSharedScope,
type SharedScope,
sharedScopeOf,
} from '../model/Scope';
import type { SceneObject } from '../model/SceneObject';
import { isTextureFile } from '../model/texture';
import { coerceValue, defaultsFor } from '../properties/PropertySchema';
@@ -254,7 +261,16 @@ export class RenameAssetCommand implements Command {
}
}
/** A `global` machine keeps one current state for the whole run instead of one per behavior instance. */
const SCOPE_LABELS: Record<Scope, string> = {
local: 'local',
global: 'global',
scene: 'per scene',
};
/**
* A `global` machine keeps 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 (#164).
*/
export class SetMachineScopeCommand implements Command {
readonly label: string;
private previous: Scope = 'local';
@@ -263,7 +279,7 @@ export class SetMachineScopeCommand implements Command {
private readonly assetId: string,
private readonly scope: Scope,
) {
this.label = `Make state machine ${scope}`;
this.label = `Make state machine ${SCOPE_LABELS[scope]}`;
}
execute(project: Project): void {
@@ -311,6 +327,44 @@ function setPersistent(asset: VariablesAsset, on: boolean): void {
else delete asset.persistent;
}
/**
* How long one set of shared values lives: `global` for the whole run, `scene` built again from the
* declarations with every scene that is loaded (#164). Going to `scene` takes the persistent switch
* off, because the two say opposite things about the same values and the scope is what was just asked for.
*/
export class SetVariablesScopeCommand implements Command {
readonly label: string;
private previous: SharedScope = 'global';
private wasPersistent = false;
constructor(
private readonly assetId: string,
private readonly scope: SharedScope,
) {
this.label = scope === 'scene' ? 'Make variables per scene' : 'Make variables global';
}
execute(project: Project): void {
const asset = this.target(project);
this.previous = sharedScopeOf(asset);
this.wasPersistent = asset.persistent === true;
setSharedScope(asset, this.scope);
if (this.scope === 'scene') setPersistent(asset, false);
}
undo(project: Project): void {
const asset = this.target(project);
setSharedScope(asset, this.previous);
setPersistent(asset, this.wasPersistent);
}
private target(project: Project): VariablesAsset {
const asset = findAssetOf(project, this.assetId, 'variables');
if (!asset) throw new CommandError(`Variables ${this.assetId} not found`);
return asset;
}
}
/**
* Which state a machine starts in, as a state node's id – `''` takes the mark off again, and the
* machine is back to starting in no state. Only one state can be the start because there is only