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>
62 lines
2.4 KiB
TypeScript
62 lines
2.4 KiB
TypeScript
import type { VariablesAsset } from '../model/Asset';
|
||
import type { PropertyBag, PropertyValue } from '../model/Properties';
|
||
import type { Project } from '../model/Project';
|
||
import { sharedScopeOf } from '../model/Scope';
|
||
import { coerceValue } from '../properties/PropertySchema';
|
||
|
||
/** What a save area holds: the values of each persistent Variables asset, keyed by asset id. */
|
||
export type SavedValues = Record<string, PropertyBag>;
|
||
|
||
/**
|
||
* Where the values of persistent Variables assets outlive the run (#166). Played and editor runs
|
||
* are handed different areas, so trying a behavior in the editor leaves the player's save alone.
|
||
*/
|
||
export interface SaveArea {
|
||
read(): SavedValues;
|
||
write(saved: SavedValues): void;
|
||
}
|
||
|
||
/**
|
||
* A `scene` asset is never persistent, whatever the flag says (#164): its values are thrown away
|
||
* with every scene, so there is nothing about them worth keeping between runs. Answered here rather
|
||
* than left to the commands, so no path can produce a saved value the scope would drop anyway.
|
||
*/
|
||
export function isPersistent(asset: VariablesAsset): boolean {
|
||
return asset.persistent === true && sharedScopeOf(asset) !== 'scene';
|
||
}
|
||
|
||
function persistentAssets(project: Project): VariablesAsset[] {
|
||
return project.assets.filter((a): a is VariablesAsset => a.type === 'variables' && isPersistent(a));
|
||
}
|
||
|
||
/**
|
||
* Puts what was saved over the declared start values. The declaration is the truth: a key it no
|
||
* longer has is ignored, and a value is coerced to the kind it declares now.
|
||
*/
|
||
export function restoreSaved(
|
||
project: Project,
|
||
variables: Record<string, PropertyBag>,
|
||
saved: SavedValues,
|
||
): void {
|
||
for (const asset of persistentAssets(project)) {
|
||
const stored = saved[asset.id];
|
||
const bag = variables[asset.id];
|
||
if (!stored || !bag) continue;
|
||
for (const def of asset.variables)
|
||
if (def.key in stored) bag[def.key] = coerceValue(def, stored[def.key] as PropertyValue);
|
||
}
|
||
}
|
||
|
||
/** The declared values of every persistent asset as they stand in the run – what a save writes. */
|
||
export function savedSnapshot(project: Project, variables: Record<string, PropertyBag>): SavedValues {
|
||
const saved: SavedValues = {};
|
||
for (const asset of persistentAssets(project)) {
|
||
const bag = variables[asset.id];
|
||
if (!bag) continue;
|
||
const values: PropertyBag = {};
|
||
for (const def of asset.variables) if (def.key in bag) values[def.key] = bag[def.key]!;
|
||
saved[asset.id] = values;
|
||
}
|
||
return saved;
|
||
}
|