Skip to content

Commit 0929904

Browse files
author
DavidQ
committed
Implement real fullscreen bezel and gameplay background runtime delta
BUILD_PR_LEVEL_10_19_REAL_IMPLEMENTATION_DELTA_FULLSCREEN_BEZEL_AND_BACKGROUND
1 parent 8636c1b commit 0929904

12 files changed

Lines changed: 471 additions & 144 deletions

asteroids.txt

530 Bytes
Binary file not shown.

docs/dev/CODEX_COMMANDS.md

Lines changed: 38 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -2,43 +2,47 @@ MODEL: GPT-5.4
22
REASONING: high
33

44
COMMAND:
5-
Create `BUILD_PR_LEVEL_10_17_HTML_FULLSCREEN_BEZEL_AND_CANVAS_BACKGROUND_SPLIT` as one small testable PR.
6-
7-
Implement the correction exactly as follows:
8-
9-
1. Add a dedicated canvas class/module named `backgroundImage`
10-
- autodiscover: `games/<gameId>/assets/images/background.png`
11-
- render into the canvas
12-
- draw immediately after screen clear
13-
- draw before ALL other game/world content
14-
- keep logic independent from bezel logic
15-
16-
2. Add a dedicated HTML-overlay class/module named `fullscreenBezel`
17-
- autodiscover: `games/<gameId>/assets/images/bezel.png`
18-
- render at the HTML/container level above the canvas
19-
- only show when fullscreen is active
20-
- hide when not fullscreen
21-
- no canvas drawing for bezel
22-
- no Parallax coupling
23-
24-
3. Remove any incorrect coupling between background and bezel behavior
25-
- separate classes
26-
- separate render paths
27-
- separate responsibilities
28-
29-
4. Validation/tests
30-
- cover background draw order: after clear, before world render
31-
- cover bezel fullscreen-only visibility
32-
- cover bezel attachment to HTML/container layer rather than canvas
33-
- verify Asteroids uses:
34-
- `games/Asteroids/assets/images/background.png` when present
35-
- `games/Asteroids/assets/images/bezel.png` in fullscreen
5+
Create `BUILD_PR_LEVEL_10_19_REAL_IMPLEMENTATION_DELTA_FULLSCREEN_BEZEL_AND_BACKGROUND` as a real implementation PR, not a docs-only PR.
6+
7+
Assume these files already exist and are the correct conventions:
8+
- `games/Asteroids/assets/images/background.png`
9+
- `games/Asteroids/assets/images/bezel.png`
10+
11+
Implement all of the following with actual runtime code changes:
12+
13+
1. Add/use a dedicated `backgroundImage` class/module
14+
- autodiscover `games/<gameId>/assets/images/background.png`
15+
- canvas-rendered
16+
- draw immediately after clear
17+
- draw before all world/gameplay content
18+
- render ONLY during gameplay states
19+
- do NOT render in attract/title/select-player/menu/non-gameplay states
20+
21+
2. Add/use a dedicated `fullscreenBezel` class/module
22+
- autodiscover `games/<gameId>/assets/images/bezel.png`
23+
- HTML/container level overlay above canvas
24+
- only visible in fullscreen
25+
- must be visibly on screen, not DOM-only
26+
- verify/fix sizing, positioning, stacking context, host attachment, z-index, overflow, opacity, and fullscreen lifecycle wiring
27+
28+
3. Add focused tests/validation covering:
29+
- gameplay-only background gating
30+
- background draw order after clear and before world render
31+
- fullscreen bezel visibility on screen
32+
- bezel hidden outside fullscreen
33+
- no-op when files are missing
34+
35+
4. REQUIRED OUTPUT CONTENT
36+
The final ZIP MUST include actual changed implementation files.
37+
Docs-only output is not acceptable.
3638

3739
5. Final packaging step is REQUIRED
38-
- package ALL changed files into this exact repo-structured ZIP:
39-
`<project folder>/tmp/BUILD_PR_LEVEL_10_17_HTML_FULLSCREEN_BEZEL_AND_CANVAS_BACKGROUND_SPLIT.zip`
40+
Package ALL changed files into this exact repo-structured ZIP:
41+
`<project folder>/tmp/BUILD_PR_LEVEL_10_19_REAL_IMPLEMENTATION_DELTA_FULLSCREEN_BEZEL_AND_BACKGROUND.zip`
4042

4143
Hard rules:
42-
- no commit-only result
44+
- do real implementation work
45+
- include changed source files in the ZIP
46+
- no docs-only completion
4347
- no missing ZIP
4448
- no unrelated repo changes

docs/dev/COMMIT_COMMENT.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
Split canvas background and HTML fullscreen bezel into separate classes
2-
BUILD_PR_LEVEL_10_17_HTML_FULLSCREEN_BEZEL_AND_CANVAS_BACKGROUND_SPLIT
1+
Implement real fullscreen bezel and gameplay background runtime delta
2+
BUILD_PR_LEVEL_10_19_REAL_IMPLEMENTATION_DELTA_FULLSCREEN_BEZEL_AND_BACKGROUND

docs/dev/NEXT_COMMAND.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
BUILD_PR_LEVEL_10_18_FULLSCREEN_BEZEL_VISIBILITY_VALIDATION
1+
BUILD_PR_LEVEL_10_20_VERIFY_ASTEROIDS_FULLSCREEN_BEZEL_ON_SCREEN
Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
- Split bezel and background into separate explicit systems
2-
- backgroundImage is canvas-rendered after clear and before all other content
3-
- fullscreenBezel is HTML-layer rendered above canvas in fullscreen only
4-
- Removed coupling between background and bezel behavior
5-
- Codex ZIP output explicitly required
1+
- Reframed the next PR to require actual implementation files in the ZIP
2+
- Locked asset assumptions to images/background.png and images/bezel.png
3+
- Kept separate classes: backgroundImage and fullscreenBezel
4+
- Enforced gameplay-only background and visible fullscreen bezel behavior
Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
- `backgroundImage` exists as separate class/module
2-
- `fullscreenBezel` exists as separate class/module
3-
- Background draws after clear and before world render
4-
- Bezel is HTML-level, not canvas-level
5-
- Bezel only appears in fullscreen
6-
- Missing files do not break runtime
7-
- Output ZIP created at `<project folder>/tmp/BUILD_PR_LEVEL_10_17_HTML_FULLSCREEN_BEZEL_AND_CANVAS_BACKGROUND_SPLIT.zip`
1+
- ZIP contains changed implementation files, not docs only
2+
- Background renders only during gameplay
3+
- Background draws after clear and before world/gameplay
4+
- Bezel is visibly on screen in fullscreen
5+
- Bezel is hidden when not fullscreen
6+
- Missing background/bezel files do not break runtime
7+
- Output ZIP created at `<project folder>/tmp/BUILD_PR_LEVEL_10_19_REAL_IMPLEMENTATION_DELTA_FULLSCREEN_BEZEL_AND_BACKGROUND.zip`
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# BUILD_PR_LEVEL_10_19_REAL_IMPLEMENTATION_DELTA_FULLSCREEN_BEZEL_AND_BACKGROUND
2+
3+
## Purpose
4+
Force a real implementation delta for Asteroids fullscreen bezel and gameplay-only background rendering.
5+
6+
## Why this PR exists
7+
Previous bundles did not produce a meaningful runtime delta in Git.
8+
This PR corrects that by requiring actual changed implementation files in the returned ZIP, not docs-only packaging.
9+
10+
Assumed asset paths are correct:
11+
12+
- `games/Asteroids/assets/images/background.png`
13+
- `games/Asteroids/assets/images/bezel.png`
14+
15+
No subfolders are involved.
16+
17+
## Required runtime behavior
18+
19+
### A. `backgroundImage`
20+
- Dedicated class/module name: `backgroundImage`
21+
- Uses `games/<gameId>/assets/images/background.png`
22+
- Drawn to canvas
23+
- Drawn only during gameplay states
24+
- Drawn immediately after clear and before all other world/gameplay content
25+
- Not drawn during attract/title/select-player/menu/non-gameplay screens
26+
27+
### B. `fullscreenBezel`
28+
- Dedicated class/module name: `fullscreenBezel`
29+
- Uses `games/<gameId>/assets/images/bezel.png`
30+
- Rendered at HTML/container level above the canvas
31+
- Only visible while fullscreen is active
32+
- Must be visually visible on screen, not merely present in the DOM
33+
34+
## Required implementation delta
35+
The returned ZIP must contain real changed source files if needed, such as:
36+
- runtime/game integration files
37+
- render pipeline files
38+
- DOM/fullscreen host files
39+
- focused tests/validation files
40+
41+
This PR is not complete if the ZIP only contains docs.
42+
43+
## Required validation evidence
44+
Implementation must prove:
45+
- background renders in gameplay only
46+
- bezel appears on screen in fullscreen
47+
- bezel is above the canvas
48+
- games without those files do not break
49+
50+
## Packaging rule
51+
Return a repo-structured ZIP at:
52+
`<project folder>/tmp/BUILD_PR_LEVEL_10_19_REAL_IMPLEMENTATION_DELTA_FULLSCREEN_BEZEL_AND_BACKGROUND.zip`
53+
54+
The ZIP must include:
55+
1. changed implementation files
56+
2. changed tests/validation files
57+
3. docs/pr/*
58+
4. docs/dev/codex_commands.md
59+
5. docs/dev/commit_comment.txt
60+
6. docs/dev/reports/*
61+
62+
If no implementation files are changed, this PR is considered failed.

src/engine/core/Engine.js

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import RuntimeMetrics from './RuntimeMetrics.js';
99
import FrameClock from './FrameClock.js';
1010
import FixedTicker from './FixedTicker.js';
1111
import EventBus from '../events/EventBus.js';
12-
import { backgroundImage, fullscreenBezel, FullscreenService } from '../runtime/index.js';
12+
import { backgroundImage, fullscreenBezel, FullscreenService, resolvePreferredFullscreenTarget } from '../runtime/index.js';
1313
import { AudioService } from '../audio/index.js';
1414
import { Logger } from '../logging/index.js';
1515
import { SettingsSystem } from '../release/index.js';
@@ -41,6 +41,11 @@ export default class Engine {
4141
this.canvas.height = height;
4242

4343
this.renderer = new CanvasRenderer(this.ctx);
44+
this.documentRef = globalThis.document ?? null;
45+
this.fullscreenTarget = resolvePreferredFullscreenTarget({
46+
canvas,
47+
documentRef: this.documentRef,
48+
}) || canvas;
4449
this.input = input;
4550
this.events = events || new EventBus();
4651
this.metrics = metrics || new RuntimeMetrics();
@@ -50,15 +55,16 @@ export default class Engine {
5055
maxCatchUpSteps: Number.POSITIVE_INFINITY,
5156
});
5257
this.fullscreen = fullscreen || FullscreenService.fromBrowser({
53-
documentRef: globalThis.document ?? null,
54-
target: canvas,
58+
documentRef: this.documentRef,
59+
target: this.fullscreenTarget,
5560
});
5661
this.backgroundImageLayer = backgroundImageLayer || new backgroundImage({
57-
documentRef: globalThis.document ?? null
62+
documentRef: this.documentRef
5863
});
5964
this.fullscreenBezelLayer = fullscreenBezelLayer || new fullscreenBezel({
6065
canvas,
61-
documentRef: globalThis.document ?? null
66+
host: this.fullscreenTarget,
67+
documentRef: this.documentRef
6268
});
6369
this.audio = audio || new AudioService();
6470
this.logger = logger || new Logger({ channel: 'engine' });
@@ -105,10 +111,15 @@ export default class Engine {
105111
this.audio.attach(this.canvas);
106112
}
107113
if (this.fullscreen && typeof this.fullscreen.attach === 'function') {
108-
this.fullscreen.attach(this.canvas);
114+
this.fullscreen.attach(this.fullscreenTarget);
109115
}
110116
if (this.fullscreenBezelLayer && typeof this.fullscreenBezelLayer.attach === 'function') {
111117
this.fullscreenBezelLayer.attach();
118+
const fullscreenActive = this.fullscreen?.getState?.().active === true;
119+
const fullscreenElement = this.fullscreen?.documentRef?.fullscreenElement
120+
|| this.documentRef?.fullscreenElement
121+
|| null;
122+
this.fullscreenBezelLayer.sync({ fullscreenActive, fullscreenElement });
112123
}
113124

114125
this.frameClock.reset();
@@ -161,12 +172,15 @@ export default class Engine {
161172

162173
const renderStart = performance.now();
163174
this.renderer.clear();
164-
this.backgroundImageLayer?.render?.(this.renderer);
175+
this.backgroundImageLayer?.render?.(this.renderer, { scene: this.scene, engine: this });
165176
if (this.scene && typeof this.scene.render === 'function') {
166177
this.scene.render(this.renderer, this);
167178
}
168179
const fullscreenActive = this.fullscreen?.getState?.().active === true;
169-
this.fullscreenBezelLayer?.sync?.({ fullscreenActive });
180+
const fullscreenElement = this.fullscreen?.documentRef?.fullscreenElement
181+
|| this.documentRef?.fullscreenElement
182+
|| null;
183+
this.fullscreenBezelLayer?.sync?.({ fullscreenActive, fullscreenElement });
170184
renderDurationMs = performance.now() - renderStart;
171185

172186
this.metrics.recordFrame({

src/engine/runtime/backgroundImage.js

Lines changed: 88 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,28 @@
11
import { resolveGameImageConventionPaths } from "./gameImageConvention.js";
22

3+
const NON_GAMEPLAY_MODE_TOKENS = Object.freeze([
4+
"menu",
5+
"title",
6+
"attract",
7+
"select-player",
8+
"player-select",
9+
"intro",
10+
"splash",
11+
"game-over",
12+
"credits",
13+
"pause"
14+
]);
15+
16+
const GAMEPLAY_MODE_TOKENS = Object.freeze([
17+
"playing",
18+
"gameplay",
19+
"in-game",
20+
"ingame",
21+
"combat",
22+
"runtime",
23+
"active"
24+
]);
25+
326
function createLayerState(path) {
427
return {
528
path,
@@ -27,6 +50,30 @@ function drawFullscreenImage(renderer, image) {
2750
return true;
2851
}
2952

53+
function toModeText(value) {
54+
return typeof value === "string" ? value.trim().toLowerCase() : "";
55+
}
56+
57+
function sceneModeCandidates(scene) {
58+
if (!scene || typeof scene !== "object") {
59+
return [];
60+
}
61+
62+
return [
63+
scene.mode,
64+
scene.state,
65+
scene.status,
66+
scene.screen,
67+
scene.view,
68+
scene.phase,
69+
scene.session?.mode,
70+
scene.session?.state,
71+
scene.session?.status
72+
]
73+
.map(toModeText)
74+
.filter(Boolean);
75+
}
76+
3077
export default class backgroundImage {
3178
constructor(options = {}) {
3279
const resolved = resolveGameImageConventionPaths({
@@ -48,6 +95,38 @@ export default class backgroundImage {
4895
};
4996
}
5097

98+
isGameplayState(scene) {
99+
if (!scene || typeof scene !== "object") {
100+
return true;
101+
}
102+
103+
if (typeof scene.isGameplayStateActive === "function") {
104+
const explicit = scene.isGameplayStateActive();
105+
if (typeof explicit === "boolean") {
106+
return explicit;
107+
}
108+
}
109+
if (typeof scene.isGameplayStateActive === "boolean") {
110+
return scene.isGameplayStateActive;
111+
}
112+
113+
const modes = sceneModeCandidates(scene);
114+
for (const mode of modes) {
115+
if (NON_GAMEPLAY_MODE_TOKENS.some((token) => mode.includes(token))) {
116+
return false;
117+
}
118+
if (GAMEPLAY_MODE_TOKENS.some((token) => mode.includes(token))) {
119+
return true;
120+
}
121+
}
122+
123+
if (scene.attractController?.active === true) {
124+
return false;
125+
}
126+
127+
return true;
128+
}
129+
51130
ensureLoaded() {
52131
if (!this.layer.path) {
53132
this.layer.status = "unavailable";
@@ -83,7 +162,15 @@ export default class backgroundImage {
83162
}
84163
}
85164

86-
render(renderer) {
165+
render(renderer, options = {}) {
166+
if (!this.isGameplayState(options.scene)) {
167+
return {
168+
drawn: false,
169+
reason: "non-gameplay-state",
170+
path: this.layer.path
171+
};
172+
}
173+
87174
this.ensureLoaded();
88175
if (this.layer.status !== "ready" || !this.layer.image) {
89176
return {

0 commit comments

Comments
 (0)