Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 12 additions & 14 deletions docs/getting-started/pattern-demo.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand All @@ -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.

<img src="../assets/screenshots/playwright/canvas-selection-bar.png" alt="Selection bar close-up" style="display: block; margin: 0 auto; width: 498px; max-width: 100%; height: auto;" />

<p align="center"><em>Figure 5. On-canvas selection bar for multi-selection actions.</em></p>

<img src="../assets/screenshots/playwright/layout-popover.png" alt="Layout popover close-up" style="display: block; margin: 0 auto; width: 345px; max-width: 100%; height: auto;" />

<p align="center"><em>Figure 6. Layout popover for spacing and set-position operations.</em></p>

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.
Expand All @@ -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.

<img src="../assets/screenshots/playwright/titles-lined-up.png" alt="Titles lined up" style="display: block; margin: 0 auto; width: 910px; max-width: 100%; height: auto;" />

<p align="center"><em>Figure 5. Titles lined up - success check.</em></p>
<p align="center"><em>Figure 7. Titles lined up - success check.</em></p>

Success check:
- There is one top row with four labels
Expand Down Expand Up @@ -127,7 +133,7 @@ Success check:

<img src="../assets/screenshots/playwright/scene-graph-pattern-demo-sprites.png" alt="Scene graph Sprites list with the seven renamed ships" style="display: block; margin: 0 auto; width: 374px; max-width: 100%; height: auto;" />

<p align="center"><em>Figure 6. Scene graph Sprites list after renaming all seven ships.</em></p>
<p align="center"><em>Figure 8. Scene graph Sprites list after renaming all seven ships.</em></p>

Success check:
- You can see seven separate sprite entities in the scene graph.
Expand All @@ -136,15 +142,8 @@ Success check:

## Position the Ships with Selection Tools and Layout

<img src="../assets/screenshots/playwright/canvas-selection-bar.png" alt="Selection bar close-up" style="display: block; margin: 0 auto; width: 498px; max-width: 100%; height: auto;" />

<p align="center"><em>Figure 7. On-canvas selection bar for multi-selection actions.</em></p>

<img src="../assets/screenshots/playwright/layout-popover.png" alt="Layout popover close-up" style="display: block; margin: 0 auto; width: 345px; max-width: 100%; height: auto;" />

<p align="center"><em>Figure 8. Layout popover for spacing and set-position operations.</em></p>

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`
Expand Down Expand Up @@ -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.
Expand Down
18 changes: 3 additions & 15 deletions src/editor/projectPersistence.ts
Original file line number Diff line number Diff line change
Expand Up @@ -797,25 +797,13 @@ async function loadIndexedDbSnapshot(): Promise<PersistenceSnapshot> {
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);
Expand Down
26 changes: 23 additions & 3 deletions tests/e2e/cloud-reload-preserves-latest-head.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<IDBDatabase>((resolve, reject) => {
const request = window.indexedDB.open('phaserforge.persistence.v1', 1);
request.onerror = () => reject(request.error);
Expand All @@ -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({
Expand Down
16 changes: 11 additions & 5 deletions tests/editor/projectPersistence.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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,
Expand All @@ -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');
});

Expand Down
Loading