Skip to content

Commit d136f50

Browse files
author
DavidQ
committed
Introduce overlay system expansion framework.
PR Details: - Enables future overlay extensibility - Maintains compatibility with Level 19 baseline
1 parent 73db963 commit d136f50

7 files changed

Lines changed: 271 additions & 17 deletions

File tree

docs/dev/CODEX_COMMANDS.md

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

44
COMMAND:
5-
Promote Level 19 overlay system to baseline.
6-
Update roadmap status only.
5+
Create overlay expansion framework:
6+
- Define extension points
7+
- Ensure compatibility with existing overlay system
8+
- Add minimal testable example
9+
- Update roadmap status only
10+
711
Package ZIP to <project folder>/tmp/

docs/dev/COMMIT_COMMENT.txt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
Promote Level 19 overlay system to baseline.
1+
Introduce overlay system expansion framework.
22

33
PR Details:
4-
- Marks completion of Level 19
5-
- Locks validated state
4+
- Enables future overlay extensibility
5+
- Maintains compatibility with Level 19 baseline
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1-
[ ] System stable
1+
[ ] Extension works
22
[ ] No regressions
3+
[ ] Gameplay unaffected
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
@@ -818,7 +818,7 @@
818818
- [ ] confirm no regression across phases
819819

820820
### Track G — Extensibility Readiness
821-
- [ ] validate plugin/extension patterns
821+
- [.] validate plugin/extension patterns
822822
- [ ] validate adding new systems is clean
823823
- [ ] validate external integration points
824824
- [ ] ensure future phases can build cleanly

docs/pr/BUILD_PR.md

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,21 @@
1-
# BUILD_PR_LEVEL_19_10_OVERLAY_PROMOTE_BASELINE
1+
# BUILD_PR_LEVEL_20_1_OVERLAY_SYSTEM_EXPANSION
22

33
## Purpose
4-
Promote validated Level 19 overlay system to baseline.
4+
Begin Level 20 by introducing a controlled expansion framework for new overlay types.
55

66
## Roadmap Improvement
7-
Marks Level 19 complete and production-ready.
7+
Transitions from stable baseline (Level 19) to extensible overlay architecture.
88

99
## Scope
10-
- Promote current implementation as baseline
11-
- Ensure no regression from validation state
10+
- Define extension points for new overlays
11+
- Ensure compatibility with existing systems
12+
- Validate one sample extension
1213

1314
## Test Steps
14-
1. Run gameplay with overlays
15-
2. Confirm all systems stable
15+
1. Load gameplay
16+
2. Register new overlay via extension
17+
3. Validate rendering + interaction
1618

1719
## Expected
18-
- Stable baseline
19-
- Ready for next level
20+
- New overlays plug in cleanly
21+
- No regression
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
/*
2+
Toolbox Aid
3+
David Quesenberry
4+
04/16/2026
5+
createPhase19OverlayExpansionFramework.js
6+
*/
7+
import { LEVEL17_OVERLAY_CYCLE_KEY } from '/samples/phase-17/shared/overlayCycleInput.js';
8+
import {
9+
defineOverlayExtensionContract,
10+
getOverlayControllerConfigFromContract,
11+
} from '/samples/phase-17/shared/overlayExpansionContracts.js';
12+
import { createOverlayGameplayRuntime } from '/samples/phase-17/shared/overlayGameplayRuntime.js';
13+
14+
const DEFAULT_CHANNEL = 'gameplay';
15+
16+
function normalizeContractId(contractId) {
17+
return String(contractId || '').trim();
18+
}
19+
20+
export function definePhase19OverlayExtension({
21+
id = '',
22+
overlays = [],
23+
initialOverlayId = '',
24+
cycleKey = LEVEL17_OVERLAY_CYCLE_KEY,
25+
persistenceKey = '',
26+
channel = DEFAULT_CHANNEL,
27+
runtimeExtensions = [],
28+
metadata = {},
29+
} = {}) {
30+
return defineOverlayExtensionContract({
31+
id,
32+
overlays,
33+
initialOverlayId,
34+
cycleKey: String(cycleKey || LEVEL17_OVERLAY_CYCLE_KEY).trim() || LEVEL17_OVERLAY_CYCLE_KEY,
35+
persistenceKey,
36+
channel: String(channel || DEFAULT_CHANNEL).trim() || DEFAULT_CHANNEL,
37+
runtimeExtensions,
38+
metadata: {
39+
phase: '19',
40+
...(metadata || {}),
41+
},
42+
});
43+
}
44+
45+
function normalizeExtensionContract(candidate) {
46+
if (!candidate || typeof candidate !== 'object') {
47+
throw new Error('Overlay extension registration requires a contract-like object.');
48+
}
49+
50+
const id = normalizeContractId(candidate.id);
51+
if (!id || !Array.isArray(candidate.overlays)) {
52+
throw new Error('Overlay extension registration requires contract id and overlays.');
53+
}
54+
55+
if (Object.isFrozen(candidate) && typeof candidate.initialOverlayId === 'string') {
56+
return candidate;
57+
}
58+
59+
return definePhase19OverlayExtension(candidate);
60+
}
61+
62+
export default function createPhase19OverlayExpansionFramework({ extensions = [] } = {}) {
63+
const contractMap = new Map();
64+
65+
function registerExtension(extension) {
66+
const contract = normalizeExtensionContract(extension);
67+
contractMap.set(contract.id, contract);
68+
return contract;
69+
}
70+
71+
function unregisterExtension(id) {
72+
const normalizedId = normalizeContractId(id);
73+
if (!normalizedId) {
74+
return false;
75+
}
76+
return contractMap.delete(normalizedId);
77+
}
78+
79+
function getExtension(id) {
80+
const normalizedId = normalizeContractId(id);
81+
if (!normalizedId) {
82+
return null;
83+
}
84+
return contractMap.get(normalizedId) ?? null;
85+
}
86+
87+
function listExtensions() {
88+
return Object.freeze(Array.from(contractMap.values()));
89+
}
90+
91+
function listExtensionIds() {
92+
return Object.freeze(Array.from(contractMap.keys()));
93+
}
94+
95+
function getControllerConfig(id) {
96+
const extension = getExtension(id);
97+
if (!extension) {
98+
return null;
99+
}
100+
return getOverlayControllerConfigFromContract(extension);
101+
}
102+
103+
function createRuntimeForExtension(id) {
104+
const extension = getExtension(id);
105+
if (!extension) {
106+
return null;
107+
}
108+
const runtime = createOverlayGameplayRuntime({
109+
runtimeExtensions: extension.runtimeExtensions,
110+
});
111+
runtime.interactionCycleKey = String(extension.cycleKey || LEVEL17_OVERLAY_CYCLE_KEY);
112+
return runtime;
113+
}
114+
115+
if (Array.isArray(extensions)) {
116+
for (let i = 0; i < extensions.length; i += 1) {
117+
registerExtension(extensions[i]);
118+
}
119+
}
120+
121+
return {
122+
registerExtension,
123+
unregisterExtension,
124+
getExtension,
125+
listExtensions,
126+
listExtensionIds,
127+
getControllerConfig,
128+
createRuntimeForExtension,
129+
};
130+
}
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
/*
2+
Toolbox Aid
3+
David Quesenberry
4+
04/16/2026
5+
Phase19OverlayExpansionFramework.test.mjs
6+
*/
7+
import assert from 'node:assert/strict';
8+
import { LEVEL17_OVERLAY_CYCLE_KEY } from '../../samples/phase-17/shared/overlayCycleInput.js';
9+
import {
10+
renderOverlayGameplayRuntime,
11+
stepOverlayGameplayRuntime,
12+
} from '../../samples/phase-17/shared/overlayGameplayRuntime.js';
13+
import createPhase19OverlayExpansionFramework, {
14+
definePhase19OverlayExtension,
15+
} from '../../samples/phase-19/shared/overlay/createPhase19OverlayExpansionFramework.js';
16+
17+
function createRendererProbe(width = 960, height = 540) {
18+
return {
19+
getCanvasSize() {
20+
return { width, height };
21+
},
22+
clear() {},
23+
drawRect() {},
24+
strokeRect() {},
25+
drawText() {},
26+
drawLine() {},
27+
drawPolygon() {},
28+
drawImageFrame() {},
29+
};
30+
}
31+
32+
function assertExpansionRegistrationAndCompatibility() {
33+
const counters = { step: 0, render: 0 };
34+
const framework = createPhase19OverlayExpansionFramework();
35+
const extension = framework.registerExtension(definePhase19OverlayExtension({
36+
id: 'phase19-overlay-example',
37+
overlays: [
38+
{ id: 'hud', label: 'HUD' },
39+
{ id: 'runtime', label: 'Runtime Diagnostics' },
40+
],
41+
initialOverlayId: 'hud',
42+
persistenceKey: 'phase19:example:overlay-index',
43+
runtimeExtensions: [
44+
{
45+
overlayId: 'runtime',
46+
compose: true,
47+
layerOrder: 10,
48+
panelWidth: 220,
49+
panelHeight: 88,
50+
onStep() {
51+
counters.step += 1;
52+
},
53+
onRender() {
54+
counters.render += 1;
55+
},
56+
},
57+
],
58+
metadata: {
59+
source: 'runtime-test',
60+
},
61+
}));
62+
63+
assert.equal(extension.channel, 'gameplay', 'Phase 19 overlay extension should default to gameplay channel.');
64+
assert.equal(extension.cycleKey, LEVEL17_OVERLAY_CYCLE_KEY, 'Phase 19 extension should stay compatible with shared cycle key.');
65+
assert.equal(extension.metadata.phase, '19', 'Phase 19 extension metadata should include phase identifier.');
66+
assert.deepEqual(framework.listExtensionIds(), ['phase19-overlay-example'], 'Extension registration should expose exact extension id.');
67+
68+
const controllerConfig = framework.getControllerConfig('phase19-overlay-example');
69+
assert.notEqual(controllerConfig, null, 'Controller config should resolve for registered extension.');
70+
assert.equal(controllerConfig.initialOverlayId, 'hud', 'Controller config should preserve initial overlay id.');
71+
assert.deepEqual(
72+
controllerConfig.overlays.map((entry) => entry.id),
73+
['hud', 'runtime'],
74+
'Controller config should expose expected overlay map.'
75+
);
76+
77+
const runtime = framework.createRuntimeForExtension('phase19-overlay-example');
78+
assert.notEqual(runtime, null, 'Runtime should be creatable from registered overlay extension.');
79+
assert.equal(runtime.interactionCycleKey, LEVEL17_OVERLAY_CYCLE_KEY, 'Runtime cycle key should remain shared-key compatible.');
80+
assert.equal(
81+
stepOverlayGameplayRuntime(runtime, { activeOverlayId: 'runtime' }),
82+
1,
83+
'Runtime should execute registered step extension hook.'
84+
);
85+
assert.equal(
86+
renderOverlayGameplayRuntime(runtime, {
87+
activeOverlayId: 'runtime',
88+
renderer: createRendererProbe(),
89+
}),
90+
1,
91+
'Runtime should execute registered render extension hook.'
92+
);
93+
assert.equal(counters.step, 1, 'Runtime step hook should update test counter.');
94+
assert.equal(counters.render, 1, 'Runtime render hook should update test counter.');
95+
}
96+
97+
function assertExtensionLifecycleMutations() {
98+
const framework = createPhase19OverlayExpansionFramework({
99+
extensions: [
100+
definePhase19OverlayExtension({
101+
id: 'phase19-overlay-seed',
102+
overlays: [{ id: 'seed', label: 'Seed' }],
103+
}),
104+
],
105+
});
106+
107+
assert.equal(framework.listExtensions().length, 1, 'Seed extension should be registered from constructor input.');
108+
assert.equal(framework.unregisterExtension('phase19-overlay-seed'), true, 'Registered extension should be removable.');
109+
assert.equal(framework.unregisterExtension('phase19-overlay-seed'), false, 'Removing missing extension should return false.');
110+
assert.equal(framework.getControllerConfig('phase19-overlay-seed'), null, 'Removed extension should no longer resolve controller config.');
111+
assert.equal(framework.createRuntimeForExtension('phase19-overlay-seed'), null, 'Removed extension should no longer create runtime.');
112+
}
113+
114+
export function run() {
115+
assertExpansionRegistrationAndCompatibility();
116+
assertExtensionLifecycleMutations();
117+
}

0 commit comments

Comments
 (0)