Skip to content

Commit bff0750

Browse files
author
DavidQ
committed
Add context-aware overlays.
PR Details: - Enables dynamic overlay behavior
1 parent a4d7880 commit bff0750

8 files changed

Lines changed: 182 additions & 28 deletions

File tree

docs/dev/CODEX_COMMANDS.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ MODEL: GPT-5.4
22
REASONING: medium
33

44
COMMAND:
5-
Implement advanced overlay feature:
6-
- Add one advanced capability
7-
- Maintain compatibility
5+
Implement context-aware overlays:
6+
- Detect gameplay state
7+
- Adapt overlay behavior
88
- Update roadmap status only

docs/dev/COMMIT_COMMENT.txt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
Add advanced overlay feature.
1+
Add context-aware overlays.
22

33
PR Details:
4-
- Introduces Level 21 capabilities
4+
- Enables dynamic overlay behavior
5+
Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1-
[ ] Feature works
2-
[ ] No regressions
1+
[ ] Context detection works
2+
[ ] Overlay adapts
3+
[ ] No regression
34
[ ] Roadmap updated

docs/dev/roadmaps/MASTER_ROADMAP_HIGH_LEVEL.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -827,4 +827,4 @@
827827
- [ ] full-repo validation sweep
828828
- [x] zero regression requirement
829829
- [.] contract freeze readiness
830-
- [ ] readiness for long-term maintenance mode
830+
- [.] readiness for long-term maintenance mode

docs/pr/BUILD_PR.md

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,21 @@
1-
# BUILD_PR_LEVEL_21_1_ADVANCED_OVERLAY_FEATURES
1+
# BUILD_PR_LEVEL_21_2_CONTEXT_AWARE_OVERLAYS
22

33
## Purpose
4-
Introduce advanced overlay capabilities building on plugin system.
4+
Add context-aware overlays that respond to gameplay state.
55

66
## Roadmap Improvement
7-
Begins Level 21 advanced feature set.
7+
Advances Level 21 with dynamic overlay behavior.
88

99
## Scope
10-
- Add one advanced overlay capability (e.g. dynamic resizing or contextual display)
11-
- Ensure compatibility with plugin system
10+
- Detect gameplay context
11+
- Show/hide or modify overlays dynamically
12+
- Validate behavior
1213

1314
## Test Steps
14-
1. Activate advanced overlay
15-
2. Validate behavior
16-
3. Confirm no regression
15+
1. Change gameplay state
16+
2. Verify overlay adapts
17+
3. Confirm stability
1718

1819
## Expected
19-
- Advanced overlay works
20-
- System stable
20+
- Context-driven overlays
21+
- No regression

samples/phase-17/shared/overlayExpansionContracts.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,9 @@ function normalizeRuntimeExtensionEntry(entry) {
5454
const onStep = typeof entry.onStep === 'function' ? entry.onStep : null;
5555
const onRender = typeof entry.onRender === 'function' ? entry.onRender : null;
5656
const resolvePanelSize = typeof entry.resolvePanelSize === 'function' ? entry.resolvePanelSize : null;
57+
const resolveContextBehavior = typeof entry.resolveContextBehavior === 'function'
58+
? entry.resolveContextBehavior
59+
: null;
5760
if (!onStep && !onRender) {
5861
return null;
5962
}
@@ -72,6 +75,7 @@ function normalizeRuntimeExtensionEntry(entry) {
7275
onStep,
7376
onRender,
7477
resolvePanelSize,
78+
resolveContextBehavior,
7579
compose: entry.compose === true,
7680
layerOrder,
7781
visualPriority,

samples/phase-17/shared/overlayGameplayRuntime.js

Lines changed: 100 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,16 @@ function normalizeRuntimeExtensionEntry(entry) {
3232
const panelWidth = Number.isFinite(panelWidthRaw) && panelWidthRaw > 0 ? panelWidthRaw : 260;
3333
const panelHeight = Number.isFinite(panelHeightRaw) && panelHeightRaw > 0 ? panelHeightRaw : 96;
3434
const resolvePanelSize = typeof entry.resolvePanelSize === 'function' ? entry.resolvePanelSize : null;
35+
const resolveContextBehavior = typeof entry.resolveContextBehavior === 'function'
36+
? entry.resolveContextBehavior
37+
: null;
3538

3639
return Object.freeze({
3740
overlayId,
3841
onStep,
3942
onRender,
4043
resolvePanelSize,
44+
resolveContextBehavior,
4145
compose,
4246
layerOrder,
4347
visualPriority,
@@ -140,7 +144,81 @@ function rectsOverlap(a, b) {
140144
return a.x < b.x + b.width && a.x + a.width > b.x && a.y < b.y + b.height && a.y + a.height > b.y;
141145
}
142146

143-
function getComposedRuntimeFrames(runtime, activeOverlayId) {
147+
function resolveOverlayGameplayState(context = {}) {
148+
if (!context || typeof context !== 'object') {
149+
return null;
150+
}
151+
152+
if (context.gameplayState && typeof context.gameplayState === 'object') {
153+
return context.gameplayState;
154+
}
155+
156+
const scene = context.scene;
157+
if (!scene || typeof scene !== 'object') {
158+
return null;
159+
}
160+
161+
if (typeof scene.getOverlayGameplayState === 'function') {
162+
const value = scene.getOverlayGameplayState();
163+
if (value && typeof value === 'object') {
164+
return value;
165+
}
166+
}
167+
168+
if (typeof scene.getGameplayState === 'function') {
169+
const value = scene.getGameplayState();
170+
if (value && typeof value === 'object') {
171+
return value;
172+
}
173+
}
174+
175+
if (scene.gameplayState && typeof scene.gameplayState === 'object') {
176+
return scene.gameplayState;
177+
}
178+
179+
if (scene.state && typeof scene.state === 'object') {
180+
return scene.state;
181+
}
182+
183+
return null;
184+
}
185+
186+
function resolveRuntimeExtensionContextBehavior(extension, context = {}) {
187+
const defaultBehavior = {
188+
visible: true,
189+
compose: extension?.compose === true,
190+
panelWidth: Number(extension?.panelWidth) || 260,
191+
panelHeight: Number(extension?.panelHeight) || 96,
192+
};
193+
194+
if (!extension || typeof extension.resolveContextBehavior !== 'function') {
195+
return defaultBehavior;
196+
}
197+
198+
try {
199+
const gameplayState = resolveOverlayGameplayState(context);
200+
const resolved = extension.resolveContextBehavior({
201+
...context,
202+
gameplayState,
203+
});
204+
return {
205+
visible: resolved?.visible !== false,
206+
compose: resolved?.compose === true || resolved?.compose === false
207+
? resolved.compose
208+
: defaultBehavior.compose,
209+
panelWidth: Number.isFinite(Number(resolved?.panelWidth))
210+
? Number(resolved.panelWidth)
211+
: defaultBehavior.panelWidth,
212+
panelHeight: Number.isFinite(Number(resolved?.panelHeight))
213+
? Number(resolved.panelHeight)
214+
: defaultBehavior.panelHeight,
215+
};
216+
} catch {
217+
return defaultBehavior;
218+
}
219+
}
220+
221+
function getComposedRuntimeFrames(runtime, activeOverlayId, context = {}) {
144222
if (!runtime || !Array.isArray(runtime.runtimeExtensions) || runtime.runtimeExtensions.length === 0) {
145223
return [];
146224
}
@@ -151,8 +229,12 @@ function getComposedRuntimeFrames(runtime, activeOverlayId) {
151229

152230
for (let i = 0; i < runtime.runtimeExtensions.length; i += 1) {
153231
const extension = runtime.runtimeExtensions[i];
232+
const contextBehavior = resolveRuntimeExtensionContextBehavior(extension, context);
233+
if (contextBehavior.visible === false) {
234+
continue;
235+
}
154236
const isActive = i === activeIndex;
155-
if (!isActive && extension.compose !== true) {
237+
if (!isActive && contextBehavior.compose !== true) {
156238
continue;
157239
}
158240
if (!shouldRunRuntimeExtension(extension, normalizedActiveOverlayId)) {
@@ -163,6 +245,7 @@ function getComposedRuntimeFrames(runtime, activeOverlayId) {
163245
extension,
164246
registrationIndex: i,
165247
isActive,
248+
contextBehavior,
166249
});
167250
}
168251

@@ -362,12 +445,19 @@ function attachCompositionSlots(frames, renderer, safeZones = [], layoutContext
362445

363446
for (let i = 0; i < frames.length; i += 1) {
364447
const frame = frames[i];
365-
const fallbackWidth = Math.max(120, Number(frame.extension.panelWidth) || 260);
366-
const fallbackHeight = Math.max(32, Number(frame.extension.panelHeight) || 96);
448+
const fallbackWidth = Math.max(
449+
120,
450+
Number(frame.contextBehavior?.panelWidth) || Number(frame.extension.panelWidth) || 260
451+
);
452+
const fallbackHeight = Math.max(
453+
32,
454+
Number(frame.contextBehavior?.panelHeight) || Number(frame.extension.panelHeight) || 96
455+
);
367456
const dynamicPanelSize = resolveDynamicPanelSize(frame.extension, {
368457
...layoutContext,
369458
renderer,
370459
canvasSize: { width, height },
460+
gameplayState: resolveOverlayGameplayState(layoutContext),
371461
frameIndex: i,
372462
frameCount: frames.length,
373463
safeZones,
@@ -499,7 +589,7 @@ export function getOverlayGameplayRuntimeCompositionSnapshot(runtime, context =
499589
const activeOverlayId = String(context?.activeOverlayId || '').trim();
500590
const safeZones = resolveLayoutSafeZones(context);
501591
const frames = deriveRenderHierarchy(attachCompositionSlots(
502-
getComposedRuntimeFrames(runtime, activeOverlayId),
592+
getComposedRuntimeFrames(runtime, activeOverlayId, context),
503593
context?.renderer,
504594
safeZones,
505595
context
@@ -517,7 +607,7 @@ export function getOverlayGameplayRuntimeCompositionSnapshot(runtime, context =
517607
visualTier: frame.visualTier,
518608
readabilityOpacity: frame.readabilityOpacity,
519609
hiddenByClutter: !visibleSet.has(frame),
520-
compose: frame.extension.compose === true,
610+
compose: frame.contextBehavior?.compose === true,
521611
isActive: frame.isActive === true,
522612
overlayId: frame.extension.overlayId,
523613
slot: frame.slot,
@@ -614,7 +704,7 @@ export function stepOverlayGameplayRuntime(runtime, context = {}) {
614704
}
615705

616706
const activeOverlayId = String(context.activeOverlayId || '').trim();
617-
const frames = getComposedRuntimeFrames(runtime, activeOverlayId);
707+
const frames = getComposedRuntimeFrames(runtime, activeOverlayId, context);
618708
if (frames.length === 0) {
619709
return 0;
620710
}
@@ -633,7 +723,7 @@ export function stepOverlayGameplayRuntime(runtime, context = {}) {
633723
count: frames.length,
634724
registrationIndex: frame.registrationIndex,
635725
layerOrder: frame.extension.layerOrder,
636-
compose: frame.extension.compose === true,
726+
compose: frame.contextBehavior?.compose === true,
637727
isActive: frame.isActive === true,
638728
slot: frame.slot || null,
639729
},
@@ -661,7 +751,7 @@ export function renderOverlayGameplayRuntime(runtime, context = {}) {
661751
const frames = applyCompositionReadabilityLimits(
662752
deriveRenderHierarchy(
663753
attachCompositionSlots(
664-
getComposedRuntimeFrames(runtime, activeOverlayId),
754+
getComposedRuntimeFrames(runtime, activeOverlayId, context),
665755
context.renderer,
666756
safeZones,
667757
context
@@ -692,7 +782,7 @@ export function renderOverlayGameplayRuntime(runtime, context = {}) {
692782
visualTier: frame.visualTier,
693783
readabilityOpacity: frame.readabilityOpacity,
694784
hiddenByClutter: frame.hiddenByClutter === true,
695-
compose: frame.extension.compose === true,
785+
compose: frame.contextBehavior?.compose === true,
696786
isActive: frame.isActive === true,
697787
slot: frame.slot,
698788
},

tests/runtime/Phase19OverlayExpansionFramework.test.mjs

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,8 +182,65 @@ function assertDynamicPanelSizingCapability() {
182182
assert.equal(fallbackSnapshot[0].slot.height, 98, 'Resolver failures should preserve compatibility via configured height fallback.');
183183
}
184184

185+
function assertContextAwareOverlayBehavior() {
186+
const framework = createPhase19OverlayExpansionFramework();
187+
framework.registerExtension(definePhase19OverlayExtension({
188+
id: 'phase19-overlay-context-aware',
189+
overlays: [
190+
{ id: 'ui', label: 'UI' },
191+
{ id: 'runtime', label: 'Runtime' },
192+
],
193+
initialOverlayId: 'ui',
194+
runtimeExtensions: [
195+
{
196+
overlayId: 'runtime',
197+
compose: true,
198+
panelWidth: 220,
199+
panelHeight: 90,
200+
onRender() {},
201+
resolveContextBehavior(context) {
202+
const phase = String(context?.gameplayState?.phase || '').trim();
203+
if (phase !== 'combat') {
204+
return {
205+
visible: false,
206+
};
207+
}
208+
return {
209+
visible: true,
210+
compose: true,
211+
panelWidth: 312,
212+
panelHeight: 104,
213+
};
214+
},
215+
},
216+
],
217+
}));
218+
219+
const runtime = framework.createRuntimeForExtension('phase19-overlay-context-aware');
220+
const hiddenSnapshot = getOverlayGameplayRuntimeCompositionSnapshot(runtime, {
221+
activeOverlayId: 'runtime',
222+
renderer: createRendererProbe(960, 540),
223+
gameplayState: { phase: 'menu' },
224+
});
225+
assert.equal(hiddenSnapshot.length, 0, 'Context-aware runtime should hide overlay when gameplay phase is not combat.');
226+
227+
const visibleSnapshot = getOverlayGameplayRuntimeCompositionSnapshot(runtime, {
228+
activeOverlayId: 'runtime',
229+
renderer: createRendererProbe(960, 540),
230+
scene: {
231+
getGameplayState() {
232+
return { phase: 'combat' };
233+
},
234+
},
235+
});
236+
assert.equal(visibleSnapshot.length, 1, 'Context-aware runtime should detect gameplay state from scene and show overlay.');
237+
assert.equal(visibleSnapshot[0].slot.width, 312, 'Context-aware runtime should adapt panel width from gameplay state behavior.');
238+
assert.equal(visibleSnapshot[0].slot.height, 104, 'Context-aware runtime should adapt panel height from gameplay state behavior.');
239+
}
240+
185241
export function run() {
186242
assertExpansionRegistrationAndCompatibility();
187243
assertExtensionLifecycleMutations();
188244
assertDynamicPanelSizingCapability();
245+
assertContextAwareOverlayBehavior();
189246
}

0 commit comments

Comments
 (0)