Skip to content

Commit 5b7ac64

Browse files
author
DavidQ
committed
Resolve Asteroids background image from Asset Manager manifest instead of hardcoded path - PR_26139_015-manifest-background-asset-resolution
1 parent c9eb34b commit 5b7ac64

5 files changed

Lines changed: 382 additions & 127 deletions

File tree

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# PR_26139_015-manifest-background-asset-resolution Report
2+
3+
## Summary
4+
5+
- Removed the Asteroids background image convention fallback so the runtime no longer guesses `assets/images/background.png`.
6+
- Background image resolution now selects only an `asset-manager-v2` image asset with `role: "background"`.
7+
- Background image loading waits for manifest resolution; when the optional background role is absent, the layer stays unavailable and does not create an image request.
8+
- Removed the stale gameplay-only background image gate so the shared background image layer renders behind menu, attract, and gameplay screens once ready.
9+
- Existing shared background render order remains: clear, background color, custom background callback, overlay, background image, game objects.
10+
11+
## Files Changed
12+
13+
- `src/engine/runtime/gameImageConvention.js`
14+
- `src/engine/runtime/backgroundImage.js`
15+
- `tests/core/BackgroundImageAndFullscreenBezel.test.mjs`
16+
- `tests/playwright/tools/AsteroidsBackgroundAssetResolution.spec.mjs`
17+
- `docs/dev/reports/PR_26139_015-manifest-background-asset-resolution_report.md`
18+
19+
## Background Asset Resolution
20+
21+
- Active background image source: `tools.asset-manager-v2.assets[*]` with `type: "image"` and `role: "background"`.
22+
- Current Asteroids manifest result: `/games/Asteroids/assets/images/deluxe.png`.
23+
- The deluxe filename is not hardcoded; it is discovered from the manifest asset entry.
24+
- If no Asset Manager background image role exists, `backgroundImageLayer.getState()` reports `path: ""` and `status: "unavailable"`.
25+
- No fallback/default background image path is produced.
26+
- The background image layer no longer suppresses rendering based on scene mode.
27+
28+
## Validation
29+
30+
- PASS: `node --check src/engine/runtime/gameImageConvention.js`
31+
- PASS: `node --check src/engine/runtime/backgroundImage.js`
32+
- PASS: `node --check tests/playwright/tools/AsteroidsBackgroundAssetResolution.spec.mjs`
33+
- PASS: targeted Node manifest/background absence validation
34+
- PASS: `npm run build:manifest`
35+
- PASS: `npx playwright test tests/playwright/tools/AsteroidsBackgroundAssetResolution.spec.mjs --project=playwright --workers=1 --reporter=list`
36+
- 2 passed.
37+
- Verified no request for `/games/Asteroids/assets/images/background.png`.
38+
- Verified `/games/Asteroids/assets/images/deluxe.png` loads from the Asset Manager `role: "background"` asset.
39+
- Verified the loaded background image changes the Asteroids menu canvas pixels from the manifest background color.
40+
- Verified removing the background image role causes no deluxe/background image request, no 404, and the game still boots.
41+
- PASS: `npx playwright test tests/playwright/tools/WorkspaceManagerV2.spec.mjs --project=playwright --workers=1 --reporter=list --grep "loads Object Vector Studio V2 runtime assets into Asteroids gameplay rendering"`
42+
- 1 passed.
43+
- Verified background render order remains clear, background color, custom background callback, overlay, background image, game objects.
44+
- WARN: `npm run test:workspace-v2`
45+
- 54 passed, 2 failed.
46+
- Existing unrelated failures:
47+
- `validates optional Text to Speech V2 schema contract through Workspace Manager V2 schema`
48+
- `tracks Object Vector Studio V2 dirty state through persisted edits and save outcomes`
49+
- PASS: `git diff --check`
50+
- Git emitted the existing line-ending normalization warning for `tests/core/BackgroundImageAndFullscreenBezel.test.mjs`; command exit code was 0.
51+
52+
## Manual Validation
53+
54+
1. Open `games/Asteroids/index.html`.
55+
2. Confirm the game boots without a request for `/games/Asteroids/assets/images/background.png`.
56+
3. Confirm the background image is visible on the initial Asteroids menu screen.
57+
4. Confirm the runtime background image path is `/games/Asteroids/assets/images/deluxe.png` only because the Asteroids manifest has an Asset Manager image asset with `role: "background"`.
58+
5. Temporarily remove that background image role from the manifest and reload.
59+
6. Confirm the game still boots, the background image layer is unavailable, and no missing background image request appears.
60+
61+
## Full Samples Smoke
62+
63+
- Skipped as requested; this PR changes the Asteroids/runtime background asset path only and does not broadly change sample loading.

src/engine/runtime/backgroundImage.js

Lines changed: 9 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -4,29 +4,6 @@ import {
44
resolveRuntimeAssetUrl
55
} from "./gameImageConvention.js";
66

7-
const NON_GAMEPLAY_MODE_TOKENS = Object.freeze([
8-
"menu",
9-
"title",
10-
"attract",
11-
"select-player",
12-
"player-select",
13-
"intro",
14-
"splash",
15-
"game-over",
16-
"credits",
17-
"pause"
18-
]);
19-
20-
const GAMEPLAY_MODE_TOKENS = Object.freeze([
21-
"playing",
22-
"gameplay",
23-
"in-game",
24-
"ingame",
25-
"combat",
26-
"runtime",
27-
"active"
28-
]);
29-
307
function createLayerState(path) {
318
return {
329
path,
@@ -54,30 +31,6 @@ function drawFullscreenImage(renderer, image) {
5431
return true;
5532
}
5633

57-
function toModeText(value) {
58-
return typeof value === "string" ? value.trim().toLowerCase() : "";
59-
}
60-
61-
function sceneModeCandidates(scene) {
62-
if (!scene || typeof scene !== "object") {
63-
return [];
64-
}
65-
66-
return [
67-
scene.mode,
68-
scene.state,
69-
scene.status,
70-
scene.screen,
71-
scene.view,
72-
scene.phase,
73-
scene.session?.mode,
74-
scene.session?.state,
75-
scene.session?.status
76-
]
77-
.map(toModeText)
78-
.filter(Boolean);
79-
}
80-
8134
export default class backgroundImage {
8235
constructor(options = {}) {
8336
this.documentRef = options.documentRef || globalThis.document || null;
@@ -88,6 +41,9 @@ export default class backgroundImage {
8841
this.gameId = resolved.gameId;
8942
this.manifestPath = resolved.manifestPath;
9043
this.layer = createLayerState(resolved.backgroundPath);
44+
this.manifestPayload = options.manifestPayload && typeof options.manifestPayload === "object" && !Array.isArray(options.manifestPayload)
45+
? options.manifestPayload
46+
: null;
9147
this.imageFactory = typeof options.imageFactory === "function"
9248
? options.imageFactory
9349
: (typeof Image === "function" ? () => new Image() : null);
@@ -116,7 +72,8 @@ export default class backgroundImage {
11672
this.manifestResolvePromise = resolveManifestChromeAssetPaths({
11773
gameId: this.gameId,
11874
manifestPath: this.manifestPath,
119-
documentRef: this.documentRef
75+
documentRef: this.documentRef,
76+
manifestPayload: this.manifestPayload
12077
})
12178
.then((resolved) => {
12279
this.gameId = resolved.gameId || this.gameId;
@@ -134,45 +91,14 @@ export default class backgroundImage {
13491
});
13592
}
13693

137-
isGameplayState(scene) {
138-
if (!scene || typeof scene !== "object") {
139-
return true;
140-
}
141-
142-
if (typeof scene.isGameplayStateActive === "function") {
143-
const explicit = scene.isGameplayStateActive();
144-
if (typeof explicit === "boolean") {
145-
return explicit;
146-
}
147-
}
148-
if (typeof scene.isGameplayStateActive === "boolean") {
149-
return scene.isGameplayStateActive;
150-
}
151-
152-
const modes = sceneModeCandidates(scene);
153-
for (const mode of modes) {
154-
if (NON_GAMEPLAY_MODE_TOKENS.some((token) => mode.includes(token))) {
155-
return false;
156-
}
157-
if (GAMEPLAY_MODE_TOKENS.some((token) => mode.includes(token))) {
158-
return true;
159-
}
160-
}
161-
162-
if (scene.attractController?.active === true) {
163-
return false;
164-
}
165-
166-
return true;
167-
}
168-
16994
ensureLoaded() {
17095
this.ensureManifestResolved();
17196

97+
if (!this.manifestResolved) {
98+
return;
99+
}
172100
if (!this.layer.path) {
173-
if (this.manifestResolved) {
174-
this.layer.status = "unavailable";
175-
}
101+
this.layer.status = "unavailable";
176102
return;
177103
}
178104
if (this.layer.status === "ready" || this.layer.status === "missing" || this.layer.status === "loading") {
@@ -213,13 +139,6 @@ export default class backgroundImage {
213139

214140
render(renderer, options = {}) {
215141
this.ensureLoaded();
216-
if (!this.isGameplayState(options.scene)) {
217-
return {
218-
drawn: false,
219-
reason: "non-gameplay-state",
220-
path: this.layer.path
221-
};
222-
}
223142
if (this.layer.status !== "ready" || !this.layer.image) {
224143
return {
225144
drawn: false,

src/engine/runtime/gameImageConvention.js

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ function resolveManifestRelativePath(pathValue, manifestPath) {
6666
return `${baseFolder}/${normalized.replace(/^\/+/, "")}`;
6767
}
6868

69-
function normalizeAssetEntry(rawEntry, fallbackId = "", manifestPath = "") {
69+
function normalizeAssetEntry(rawEntry, fallbackId = "", manifestPath = "", sourceToolId = "") {
7070
const entry = toObject(rawEntry);
7171
const path = resolveManifestRelativePath(entry.path || entry.runtimePath || entry.href, manifestPath);
7272
if (!path) {
@@ -77,6 +77,7 @@ function normalizeAssetEntry(rawEntry, fallbackId = "", manifestPath = "") {
7777
kind: safeText(entry.kind, "").toLowerCase(),
7878
path,
7979
role: safeText(entry.role, "").toLowerCase(),
80+
sourceToolId: safeText(sourceToolId, "").toLowerCase(),
8081
type: safeText(entry.type, "").toLowerCase()
8182
};
8283
}
@@ -127,12 +128,12 @@ function collectImageEntriesFromManifest(manifestPayload, { manifestPath = "" }
127128

128129
const assetBrowserAssets = toObject(payload?.tools?.["asset-browser"]?.assets);
129130
Object.entries(assetBrowserAssets).forEach(([assetId, rawEntry]) => {
130-
pushEntry(normalizeAssetEntry(rawEntry, assetId, manifestPath));
131+
pushEntry(normalizeAssetEntry(rawEntry, assetId, manifestPath, "asset-browser"));
131132
});
132133

133134
const assetManagerAssets = toObject(payload?.tools?.["asset-manager-v2"]?.assets);
134135
Object.entries(assetManagerAssets).forEach(([assetId, rawEntry]) => {
135-
pushEntry(normalizeAssetEntry(rawEntry, assetId, manifestPath));
136+
pushEntry(normalizeAssetEntry(rawEntry, assetId, manifestPath, "asset-manager-v2"));
136137
});
137138

138139
return entries;
@@ -186,6 +187,17 @@ function chooseGameBackgroundColor(entries) {
186187
|| null;
187188
}
188189

190+
function chooseAssetManagerBackgroundImagePath(entries) {
191+
const normalizedEntries = Array.isArray(entries) ? entries : [];
192+
const backgroundAsset = normalizedEntries.find((entry) => (
193+
entry?.sourceToolId === "asset-manager-v2"
194+
&& entry?.type === "image"
195+
&& entry?.role === "background"
196+
&& safeText(entry?.path, "")
197+
));
198+
return backgroundAsset?.path || "";
199+
}
200+
189201
const manifestCache = new Map();
190202

191203
async function readManifestPayload(manifestPath, documentRef = null) {
@@ -261,7 +273,7 @@ export function resolveGameImageConventionPaths(options = {}) {
261273
backgroundColorHex: "",
262274
backgroundColorName: "",
263275
backgroundColorPath: "",
264-
backgroundPath: gameId ? `games/${gameId}/assets/images/background.png` : "",
276+
backgroundPath: "",
265277
bezelPath: gameId ? `games/${gameId}/assets/images/bezel.png` : ""
266278
};
267279
}
@@ -288,7 +300,7 @@ export async function resolveManifestChromeAssetPaths(options = {}) {
288300
backgroundColorHex: backgroundColor?.hex || "",
289301
backgroundColorName: backgroundColor?.name || "",
290302
backgroundColorPath: backgroundColor?.path || "",
291-
backgroundPath: chooseSemanticImagePath(imageEntries, "background"),
303+
backgroundPath: chooseAssetManagerBackgroundImagePath(imageEntries),
292304
bezelPath: chooseSemanticImagePath(imageEntries, "bezel")
293305
};
294306
}

0 commit comments

Comments
 (0)