@@ -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+
73152export 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+
125222export 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
229344export 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