diff --git a/docs/getting-started/pattern-demo.md b/docs/getting-started/pattern-demo.md
index 85e16117..93d17f71 100644
--- a/docs/getting-started/pattern-demo.md
+++ b/docs/getting-started/pattern-demo.md
@@ -54,7 +54,7 @@ Success check:
**NOTE:** This will create one text entity called 't'.
-2. Rough-place the text entity (i.e., drag it) near where the top-left label will go.
+2. Rough-place the text entity (i.e., drag it) near the top-left quarter of the viewport.
3. Hit the F2 key and rename the text entity to "Wave"
@@ -69,11 +69,18 @@ Success check:
6. Select the `Wave` label. In the Inspector's Transform panel, set `X = 130` and `Y = 120`. This anchors the label row to the same center point the first top-row ship will use later.
+
+
+
Figure 5. On-canvas selection bar for multi-selection actions.
+
+
+
+Figure 6. Layout popover for spacing and set-position operations.
+
7. Drag-select the top-row labels, and in the popup Selection Bar, use `Layout …`:
- Under Spacing, click `Pixels`, then type `180` in the `Spacing X` box, and click `Apply Spacing X`.
- Under Advanced, level the labels with `Match top edges`.
- - Under Position Selection, type `120` under `Y`, then click `Set Y`.
- Click the "Close" button at the bottom of the Layout popup, or just click in a blank area of the canvas to close it.
8. Click a blank space somewhere in the canvas to deselect the top-row labels.
@@ -84,12 +91,11 @@ Success check:
- Under Spacing, click `Pixels`, then type `180` in the `Spacing X` box, and click `Apply Spacing X`.
- Under Advanced, level the labels with `Match top edges`.
- - Under Position Selection, type `340` under `Y`, then click `Set Y`.
- Click the "Close" button at the bottom of the Layout popup, or just click in a blank area of the canvas to close it.
-Figure 5. Titles lined up - success check.
+Figure 7. Titles lined up - success check.
Success check:
- There is one top row with four labels
@@ -127,7 +133,7 @@ Success check:
-Figure 6. Scene graph Sprites list after renaming all seven ships.
+Figure 8. Scene graph Sprites list after renaming all seven ships.
Success check:
- You can see seven separate sprite entities in the scene graph.
@@ -136,15 +142,8 @@ Success check:
## Position the Ships with Selection Tools and Layout
-
-
-Figure 7. On-canvas selection bar for multi-selection actions.
-
-
-
-Figure 8. Layout popover for spacing and set-position operations.
-
1. For the top row, drag-select the top four ships and click the `Layout…` button to change their positions as a group:
+
**NOTE:** A Layout pop-up window will appear.
- Under Spacing, click `Pixels`, then type `180` in the `Spacing X` box, and hit `Apply Spacing X`
- Under Align Selection, center the ships with `Center X`
@@ -203,7 +202,6 @@ Success check:
10. Set the `Loop Body` step properties to the following:
- `Type = Wave`
- The other defaults are fine.
-11. Click the Back arrow in the properties panel to return to the Actions list.
Success check:
- Click the `Play Mode` button in the upper right of the canvas to check that the ship moves in a wave pattern.
diff --git a/src/editor/projectPersistence.ts b/src/editor/projectPersistence.ts
index 8e181cfb..840fe81d 100644
--- a/src/editor/projectPersistence.ts
+++ b/src/editor/projectPersistence.ts
@@ -797,25 +797,13 @@ async function loadIndexedDbSnapshot(): Promise {
const existingIndex = localProjects.findIndex((record) => record.id === latestActiveSnapshot.recordId);
const snapshotRecord = existingIndex >= 0 ? localProjects[existingIndex] : null;
const currentActiveRecord = localProjects.find((record) => record.id === workspace.activeProjectId) ?? null;
- const snapshotMs = Date.parse(latestActiveSnapshot.updatedAt || latestActiveSnapshot.savedAt);
- const currentActiveMs = Date.parse(currentActiveRecord?.updatedAt ?? '');
const snapshotLooksPlaceholder = isPlaceholderProjectRecord(snapshotRecord);
- const currentActiveLooksPlaceholder = isPlaceholderProjectRecord(currentActiveRecord);
const currentActiveLooksMeaningful = currentActiveRecord != null && !isPlaceholderProjectRecord(currentActiveRecord);
const snapshotMatchesWorkspace = latestActiveSnapshot.recordId === workspace.activeProjectId;
- const workspaceHasExplicitMeaningfulActiveProject =
- Boolean(workspace.activeProjectId && currentActiveLooksMeaningful);
- const shouldUseTimestampTiebreak =
- !(snapshotLooksPlaceholder && currentActiveLooksMeaningful);
const shouldPreferSnapshot =
- !currentActiveRecord
- || !workspace.activeProjectId
- || snapshotMatchesWorkspace
- || (!snapshotLooksPlaceholder && currentActiveLooksPlaceholder)
- || (!workspaceHasExplicitMeaningfulActiveProject && shouldUseTimestampTiebreak && (
- !Number.isFinite(currentActiveMs)
- || (Number.isFinite(snapshotMs) && snapshotMs >= currentActiveMs)
- ));
+ snapshotMatchesWorkspace
+ || !snapshotLooksPlaceholder
+ || !currentActiveLooksMeaningful;
if (shouldPreferSnapshot && snapshotRecord) {
if (existingIndex > 0) {
localProjects.splice(existingIndex, 1);
diff --git a/tests/e2e/cloud-reload-preserves-latest-head.spec.ts b/tests/e2e/cloud-reload-preserves-latest-head.spec.ts
index 8066f3e9..1befada5 100644
--- a/tests/e2e/cloud-reload-preserves-latest-head.spec.ts
+++ b/tests/e2e/cloud-reload-preserves-latest-head.spec.ts
@@ -49,8 +49,21 @@ test('cloud-backed active project reload restores the latest IndexedDB head and
id: 'cloud:g1',
projectId: latestProject.id,
};
+ const olderProject = structuredClone(sampleProject);
+ olderProject.title = 'Older Cloud Game';
+ const olderRecord = {
+ ...buildStoredProjectRecord(olderProject, {
+ id: 'cloud:g-old',
+ updatedAt: staleUpdatedAt,
+ origin: 'cloud-cache',
+ syncStatus: 'cloud',
+ cloudProjectId: 'g-old',
+ }),
+ id: 'cloud:g-old',
+ projectId: olderProject.id,
+ };
- await page.addInitScript(async ({ record }) => {
+ await page.addInitScript(async ({ record, staleRecord }) => {
const openDb = () => new Promise((resolve, reject) => {
const request = window.indexedDB.open('phaserforge.persistence.v1', 1);
request.onerror = () => reject(request.error);
@@ -69,14 +82,21 @@ test('cloud-backed active project reload restores the latest IndexedDB head and
tx.oncomplete = () => resolve();
tx.onerror = () => reject(tx.error);
tx.objectStore('projects').put(record);
+ tx.objectStore('projects').put(staleRecord);
tx.objectStore('workspaceState').put({
- activeProjectId: 'cloud:g1',
+ activeProjectId: 'cloud:g-old',
syncMode: 'online',
}, 'workspace');
+ tx.objectStore('workspaceState').put({
+ recordId: 'cloud:g1',
+ updatedAt: record.updatedAt,
+ syncMode: 'online',
+ savedAt: record.updatedAt,
+ }, 'latestActiveSnapshot');
tx.objectStore('workspaceState').put('1', 'legacyMigrated');
});
window.localStorage.setItem('phaserforge.startupMode.v1', 'new_empty_scene');
- }, { record: latestRecord });
+ }, { record: latestRecord, staleRecord: olderRecord });
await page.route('**/api/v1/auth/me', async (route) => {
await route.fulfill({
diff --git a/tests/editor/projectPersistence.test.ts b/tests/editor/projectPersistence.test.ts
index 9f033d3c..83a48d4a 100644
--- a/tests/editor/projectPersistence.test.ts
+++ b/tests/editor/projectPersistence.test.ts
@@ -972,14 +972,14 @@ describe('projectPersistence steady-state storage', () => {
expect(snapshot.localProjects[0]?.title).toBe('Recovered Title');
});
- it('does not let a newer snapshot for a different saved project override the explicit workspace active project', async () => {
+ it('restores the newer durable active snapshot when the workspace pointer still names an older cloud project', async () => {
const patternDemoProject = createEmptyProject();
patternDemoProject.id = 'project-pattern-demo';
- patternDemoProject.title = 'Pattern Demo';
+ patternDemoProject.title = 'Older Cloud Game';
const newerDraftProject = createEmptyProject();
newerDraftProject.id = 'project-newer-draft';
- newerDraftProject.title = 'Scratch Draft';
+ newerDraftProject.title = 'Pattern Demo';
const db = await openPersistenceDb();
try {
@@ -990,10 +990,16 @@ describe('projectPersistence steady-state storage', () => {
tx.objectStore('projects').put(buildStoredProjectRecord(patternDemoProject, {
id: patternDemoProject.id,
updatedAt: '2026-06-22T12:00:00.000Z',
+ origin: 'cloud-cache',
+ syncStatus: 'cloud',
+ cloudProjectId: 'game-older',
}));
tx.objectStore('projects').put(buildStoredProjectRecord(newerDraftProject, {
id: newerDraftProject.id,
updatedAt: '2026-06-22T12:05:00.000Z',
+ origin: 'cloud-cache',
+ syncStatus: 'cloud',
+ cloudProjectId: 'game-latest',
}));
tx.objectStore('workspaceState').put({
activeProjectId: patternDemoProject.id,
@@ -1013,8 +1019,8 @@ describe('projectPersistence steady-state storage', () => {
const snapshot = await projectPersistence.load();
- expect(snapshot.workspace.activeProjectId).toBe('project-pattern-demo');
- expect(snapshot.localProjects[0]?.id).toBe('project-pattern-demo');
+ expect(snapshot.workspace.activeProjectId).toBe('project-newer-draft');
+ expect(snapshot.localProjects[0]?.id).toBe('project-newer-draft');
expect(snapshot.localProjects[0]?.title).toBe('Pattern Demo');
});