Skip to content

Commit 3c4967c

Browse files
author
DavidQ
committed
Add multi-layer overlay composition for gameplay-safe overlays.
PR Details: - Enables deterministic rendering of multiple active overlays - Preserves gameplay-first input and shared non-Tab input mapping
1 parent 3594bc4 commit 3c4967c

10 files changed

Lines changed: 432 additions & 51 deletions

docs/dev/CODEX_COMMANDS.md

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,15 @@ MODEL: GPT-5.4
22
REASONING: medium
33

44
COMMAND:
5-
Handle overlay input edge cases:
6-
- Prevent input flooding issues
7-
- Ensure no stuck key states
8-
- Maintain gameplay priority
9-
- Preserve behavior
5+
Create BUILD_PR_LEVEL_19_6_OVERLAY_MULTI_LAYER_COMPOSITION as the next smallest executable/testable PR.
106

11-
Package ZIP to <project folder>/tmp/
7+
Requirements:
8+
- Add multi-layer overlay composition for gameplay-safe overlays
9+
- Define and enforce deterministic render ordering for multiple active overlays
10+
- Prevent overlap/occlusion regressions in the composed state
11+
- Preserve existing shared non-Tab input mapping
12+
- Preserve gameplay-first input priority
13+
- Add or update focused tests validating composition order and non-interference
14+
- Update roadmap status for this PR using status markers only ([ ] [.] [x]); do not rewrite roadmap text
15+
- Do not modify start_of_day folders
16+
- Package the repo-structured ZIP to <project folder>/tmp/BUILD_PR_LEVEL_19_6_OVERLAY_MULTI_LAYER_COMPOSITION.zip

docs/dev/COMMIT_COMMENT.txt

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
Handle overlay input edge cases.
1+
Add multi-layer overlay composition for gameplay-safe overlays.
22

33
PR Details:
4-
- Stabilizes input under rapid and conflicting conditions
4+
- Enables deterministic rendering of multiple active overlays
5+
- Preserves gameplay-first input and shared non-Tab input mapping
6+
- Improves roadmap progress with a testable composition milestone
7+
- Roadmap update must be status-only
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# BUILD_PR_LEVEL_19_6_OVERLAY_MULTI_LAYER_COMPOSITION Report
2+
3+
## Purpose
4+
Add gameplay-safe multi-layer overlay composition with deterministic ordering while preserving existing input and gameplay-priority behavior.
5+
6+
## Scope Applied
7+
- Added composed overlay support in shared gameplay runtime:
8+
- active runtime overlay plus additional `compose: true` layers
9+
- deterministic ordering by `layerOrder`, then registration index
10+
- Added composition slot layout metadata to reduce overlap/occlusion regressions:
11+
- per-layer `overlayComposition.slot` (bottom-right anchored, stacked)
12+
- Preserved existing non-Tab mapping and gameplay-first input behavior.
13+
- Added focused runtime test coverage for composition order and non-interference.
14+
- Updated roadmap using status markers only.
15+
16+
## Files Changed
17+
- `samples/phase-17/shared/overlayGameplayRuntime.js`
18+
- `tests/runtime/Phase17OverlayMultiLayerComposition.test.mjs`
19+
- `docs/dev/roadmaps/MASTER_ROADMAP_HIGH_LEVEL.md`
20+
21+
## Validation
22+
- `tests/runtime/Phase17OverlayMultiLayerComposition.test.mjs` PASS
23+
- `tests/runtime/Phase17OverlayInputEdgeCases.test.mjs` PASS
24+
- `tests/runtime/Phase17OverlayGameplayRuntimeIntegration.test.mjs` PASS
25+
- `tests/runtime/Phase17RealGameplaySample.test.mjs` PASS
26+
- `tests/runtime/Phase17Sample1712GameplayMetricsTelemetry.test.mjs` PASS
27+
- `tests/runtime/Phase17Sample1713FinalReferenceGame.test.mjs` PASS
28+
- `tests/runtime/Phase17TabDebugOverlayCycle1707Plus.test.mjs` PASS
29+
- `tests/runtime/Phase17DebugOverlayBottomRightPosition.test.mjs` PASS
30+
- `tests/runtime/Phase18RuntimeLayerScaffold.test.mjs` PASS
31+
- `tests/runtime/Phase18IntegrationFlowPass.test.mjs` PASS
32+
- `tests/runtime/Phase18CoreServicesSkeleton.test.mjs` PASS
Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
Summary:
2-
This PR follows Level 19.2 by adding interaction controls for gameplay overlays.
3-
It keeps scope limited to a testable control layer so overlays can be shown, hidden, and cycled during gameplay without interfering with core player input.
2+
This PR adds a testable multi-layer composition step for Level 19 overlays.
3+
It allows multiple gameplay-safe overlays to render together with deterministic ordering while preserving input safety and gameplay visibility.
44

5-
Roadmap Improvement:
6-
- Advances Level 19 from gameplay overlay availability to gameplay-safe overlay usability
7-
- Keeps the change executable and validation-backed
5+
Roadmap Impact:
6+
- Improves Level 19 with a concrete, testable composition milestone
7+
- Keeps roadmap handling status-only as required by the updated instructions
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Roadmap Status Update Instruction:
2+
- Update the roadmap entry for BUILD_PR_LEVEL_19_6_OVERLAY_MULTI_LAYER_COMPOSITION using status markers only
3+
- Allowed changes: [ ] -> [.] -> [x]
4+
- Do not rewrite, move, or delete roadmap text
5+
- Do not perform cleanup edits outside the status change needed for this PR
Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
[ ] No stuck inputs
2-
[ ] Rapid cycling stable
3-
[ ] Gameplay unaffected
4-
[ ] No missed inputs
1+
[ ] Gameplay sample with overlay support loads
2+
[ ] Multiple overlays can be active together
3+
[ ] Composition/render order is deterministic
4+
[ ] No overlay occludes required gameplay information unexpectedly
5+
[ ] Gameplay controls remain responsive with multiple overlays active
6+
[ ] No Tab-based interaction is introduced
7+
[ ] Roadmap status was updated using status markers only

docs/dev/roadmaps/MASTER_ROADMAP_HIGH_LEVEL.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -800,7 +800,7 @@
800800
### Track D — Debug & Observability Maturity
801801
- [ ] ensure all systems expose debug data
802802
- [ ] ensure providers are complete and consistent
803-
- [ ] validate debug panels across systems
803+
- [.] validate debug panels across systems
804804
- [ ] confirm production-safe debug toggling
805805

806806
### Track E — Toolchain Validation

docs/pr/BUILD_PR.md

Lines changed: 39 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,45 @@
1-
# BUILD_PR_LEVEL_19_5_OVERLAY_INPUT_EDGE_CASES
1+
# BUILD_PR_LEVEL_19_6_OVERLAY_MULTI_LAYER_COMPOSITION
22

33
## Purpose
4-
Handle edge cases in overlay input interactions to ensure robust behavior under unusual or rapid input scenarios.
4+
Add testable multi-layer overlay composition so gameplay-safe overlays can render together in a predictable stack without visual conflicts.
5+
6+
## Roadmap Improvement
7+
This PR advances Level 19 from stable overlay input handling to stable multi-overlay composition during gameplay.
58

69
## Scope
7-
- Rapid key presses
8-
- Simultaneous input (gameplay + overlay)
9-
- Input buffering issues
10-
- Lost or stuck key states
10+
- Support composition of multiple active overlays in the same gameplay session
11+
- Define deterministic layer ordering for composed overlays
12+
- Prevent overlap and occlusion regressions caused by composed overlay rendering
13+
- Validate composition in at least one gameplay-active sample
14+
15+
## Included
16+
- Multi-layer composition rules for gameplay overlays
17+
- Deterministic render ordering for composed overlays
18+
- Focused validation for overlap, occlusion, and ordering behavior
19+
- Status-only roadmap update instruction for this PR
20+
21+
## Excluded
22+
- New overlay feature families
23+
- Mission-system expansion
24+
- Telemetry-system expansion
25+
- Visual redesign of existing overlays
26+
- Repo-wide render pipeline changes
27+
28+
## Execution Notes
29+
- Preserve existing non-Tab overlay input behavior
30+
- Preserve gameplay-first input priority
31+
- Keep scope to the smallest executable/testable change
32+
- Do not modify start_of_day folders
33+
- Update roadmap status only; do not rewrite roadmap text
1134

1235
## Test Steps
13-
1. Spam overlay cycle key
14-
2. Hold gameplay + overlay inputs simultaneously
15-
3. Trigger rapid show/hide toggles
16-
4. Validate no stuck states
17-
18-
## Expected
19-
- No input lockups
20-
- No missed cycles
21-
- Stable behavior under stress
36+
1. Load a gameplay-active sample with overlay support
37+
2. Activate multiple overlays in the same session
38+
3. Verify render order matches the composition contract
39+
4. Verify overlays remain readable and do not hide required gameplay information
40+
5. Verify gameplay controls continue to work while multiple overlays are active
41+
42+
## Expected Result
43+
- Multiple overlays can render together predictably
44+
- Layer order remains stable
45+
- No visual conflict or gameplay-input regression is introduced

samples/phase-17/shared/overlayGameplayRuntime.js

Lines changed: 154 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,22 @@ function normalizeRuntimeExtensionEntry(entry) {
2222
return null;
2323
}
2424

25+
const layerOrderRaw = Number(entry.layerOrder);
26+
const layerOrder = Number.isFinite(layerOrderRaw) ? layerOrderRaw : 0;
27+
const compose = entry.compose === true;
28+
const panelWidthRaw = Number(entry.panelWidth);
29+
const panelHeightRaw = Number(entry.panelHeight);
30+
const panelWidth = Number.isFinite(panelWidthRaw) && panelWidthRaw > 0 ? panelWidthRaw : 260;
31+
const panelHeight = Number.isFinite(panelHeightRaw) && panelHeightRaw > 0 ? panelHeightRaw : 96;
32+
2533
return Object.freeze({
2634
overlayId,
2735
onStep,
2836
onRender,
37+
compose,
38+
layerOrder,
39+
panelWidth,
40+
panelHeight,
2941
});
3042
}
3143

@@ -70,6 +82,73 @@ function normalizeInteractionIndex(runtime) {
7082
return normalized;
7183
}
7284

85+
function getComposedRuntimeFrames(runtime, activeOverlayId) {
86+
if (!runtime || !Array.isArray(runtime.runtimeExtensions) || runtime.runtimeExtensions.length === 0) {
87+
return [];
88+
}
89+
90+
const normalizedActiveOverlayId = String(activeOverlayId || '').trim();
91+
const activeIndex = normalizeInteractionIndex(runtime);
92+
const frames = [];
93+
94+
for (let i = 0; i < runtime.runtimeExtensions.length; i += 1) {
95+
const extension = runtime.runtimeExtensions[i];
96+
const isActive = i === activeIndex;
97+
if (!isActive && extension.compose !== true) {
98+
continue;
99+
}
100+
if (!shouldRunRuntimeExtension(extension, normalizedActiveOverlayId)) {
101+
continue;
102+
}
103+
104+
frames.push({
105+
extension,
106+
registrationIndex: i,
107+
isActive,
108+
});
109+
}
110+
111+
frames.sort((left, right) => {
112+
if (left.extension.layerOrder !== right.extension.layerOrder) {
113+
return left.extension.layerOrder - right.extension.layerOrder;
114+
}
115+
return left.registrationIndex - right.registrationIndex;
116+
});
117+
118+
return frames;
119+
}
120+
121+
function attachCompositionSlots(frames, renderer) {
122+
if (!Array.isArray(frames) || frames.length === 0) {
123+
return frames || [];
124+
}
125+
126+
const canvasSize = renderer?.getCanvasSize?.() || { width: 960, height: 540 };
127+
const width = Math.max(320, Number(canvasSize.width) || 960);
128+
const height = Math.max(180, Number(canvasSize.height) || 540);
129+
const margin = 16;
130+
const gap = 10;
131+
let cursorY = height - margin;
132+
133+
for (let i = 0; i < frames.length; i += 1) {
134+
const frame = frames[i];
135+
const slotWidth = Math.max(120, Number(frame.extension.panelWidth) || 260);
136+
const slotHeight = Math.max(32, Number(frame.extension.panelHeight) || 96);
137+
const slotX = Math.round(width - margin - slotWidth);
138+
const slotY = Math.round(cursorY - slotHeight);
139+
frame.slot = Object.freeze({
140+
x: slotX,
141+
y: slotY,
142+
width: slotWidth,
143+
height: slotHeight,
144+
anchor: 'bottom-right',
145+
});
146+
cursorY = slotY - gap;
147+
}
148+
149+
return frames;
150+
}
151+
73152
export function createOverlayGameplayRuntime({ runtimeExtensions = [] } = {}) {
74153
return {
75154
runtimeExtensions: normalizeRuntimeExtensions(runtimeExtensions),
@@ -122,6 +201,24 @@ export function getOverlayGameplayRuntimeInteractionSnapshot(runtime) {
122201
};
123202
}
124203

204+
export function getOverlayGameplayRuntimeCompositionSnapshot(runtime, context = {}) {
205+
const activeOverlayId = String(context?.activeOverlayId || '').trim();
206+
const frames = attachCompositionSlots(
207+
getComposedRuntimeFrames(runtime, activeOverlayId),
208+
context?.renderer
209+
);
210+
return frames.map((frame, index) => ({
211+
index,
212+
count: frames.length,
213+
registrationIndex: frame.registrationIndex,
214+
layerOrder: frame.extension.layerOrder,
215+
compose: frame.extension.compose === true,
216+
isActive: frame.isActive === true,
217+
overlayId: frame.extension.overlayId,
218+
slot: frame.slot,
219+
}));
220+
}
221+
125222
export function stepOverlayGameplayRuntimeControls(runtime, input, options = {}) {
126223
if (!runtime) {
127224
return false;
@@ -212,18 +309,36 @@ export function stepOverlayGameplayRuntime(runtime, context = {}) {
212309
}
213310

214311
const activeOverlayId = String(context.activeOverlayId || '').trim();
215-
const activeIndex = normalizeInteractionIndex(runtime);
216-
const extension = runtime.runtimeExtensions[activeIndex];
217-
if (!extension || !extension.onStep || !shouldRunRuntimeExtension(extension, activeOverlayId)) {
312+
const frames = getComposedRuntimeFrames(runtime, activeOverlayId);
313+
if (frames.length === 0) {
218314
return 0;
219315
}
220-
try {
221-
extension.onStep(context);
222-
return 1;
223-
} catch {
224-
// Runtime overlays must never break gameplay execution.
225-
return 0;
316+
317+
let invoked = 0;
318+
for (let i = 0; i < frames.length; i += 1) {
319+
const frame = frames[i];
320+
if (!frame.extension.onStep) {
321+
continue;
322+
}
323+
try {
324+
frame.extension.onStep({
325+
...context,
326+
overlayComposition: {
327+
index: i,
328+
count: frames.length,
329+
registrationIndex: frame.registrationIndex,
330+
layerOrder: frame.extension.layerOrder,
331+
compose: frame.extension.compose === true,
332+
isActive: frame.isActive === true,
333+
slot: frame.slot || null,
334+
},
335+
});
336+
invoked += 1;
337+
} catch {
338+
// Runtime overlays must never break gameplay execution.
339+
}
226340
}
341+
return invoked;
227342
}
228343

229344
export function renderOverlayGameplayRuntime(runtime, context = {}) {
@@ -237,16 +352,37 @@ export function renderOverlayGameplayRuntime(runtime, context = {}) {
237352
}
238353

239354
const activeOverlayId = String(context.activeOverlayId || '').trim();
240-
const activeIndex = normalizeInteractionIndex(runtime);
241-
const extension = runtime.runtimeExtensions[activeIndex];
242-
if (!extension || !extension.onRender || !shouldRunRuntimeExtension(extension, activeOverlayId)) {
355+
const frames = attachCompositionSlots(
356+
getComposedRuntimeFrames(runtime, activeOverlayId),
357+
context.renderer
358+
);
359+
if (frames.length === 0) {
243360
return 0;
244361
}
245-
try {
246-
extension.onRender(context);
247-
return 1;
248-
} catch {
249-
// Runtime overlays must never break gameplay rendering.
250-
return 0;
362+
363+
let invoked = 0;
364+
for (let i = 0; i < frames.length; i += 1) {
365+
const frame = frames[i];
366+
if (!frame.extension.onRender) {
367+
continue;
368+
}
369+
try {
370+
frame.extension.onRender({
371+
...context,
372+
overlayComposition: {
373+
index: i,
374+
count: frames.length,
375+
registrationIndex: frame.registrationIndex,
376+
layerOrder: frame.extension.layerOrder,
377+
compose: frame.extension.compose === true,
378+
isActive: frame.isActive === true,
379+
slot: frame.slot,
380+
},
381+
});
382+
invoked += 1;
383+
} catch {
384+
// Runtime overlays must never break gameplay rendering.
385+
}
251386
}
387+
return invoked;
252388
}

0 commit comments

Comments
 (0)