@mpgd/target-config 0.11.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/effective.js CHANGED
@@ -126,6 +126,9 @@ function effectiveProductReason(runtime, targetEnabled, product, platformProduct
126
126
  if (runtime === 'verse8-web' && product.grant.type === 'resource') {
127
127
  return 'capability-unsupported';
128
128
  }
129
+ if (runtime === 'microsoft-store-pwa' && product.type !== 'consumable') {
130
+ return 'capability-unsupported';
131
+ }
129
132
  return effectiveItemReason(targetEnabled, platformProductId);
130
133
  }
131
134
  function createEffectiveAdPlacementConfig(target, config, placement) {
package/dist/runtime.js CHANGED
@@ -111,6 +111,7 @@ export function applyTargetConfigToCapabilities(capabilities, config) {
111
111
  rewardedAds: capabilities.rewardedAds && config.features.rewardedAds,
112
112
  interstitialAds: capabilities.interstitialAds && config.features.interstitialAds,
113
113
  nativeLeaderboard: capabilities.nativeLeaderboard && config.features.leaderboard,
114
+ remoteLeaderboard: capabilities.remoteLeaderboard && config.features.leaderboard,
114
115
  localizedContent: capabilities.localizedContent && config.features.localization,
115
116
  };
116
117
  }
@@ -206,6 +207,13 @@ export function withTargetAvailability(gateway, config, options = {}) {
206
207
  };
207
208
  const notifications = notificationsAvailable ? gatewayNotifications : undefined;
208
209
  const isIapAvailable = async () => (availabilityConfig.features.iap && (await getGatewayCapabilities()).nativeIap);
210
+ const isLeaderboardAvailable = async () => {
211
+ if (!availabilityConfig.features.leaderboard) {
212
+ return false;
213
+ }
214
+ const capabilities = await getGatewayCapabilities();
215
+ return capabilities.nativeLeaderboard || capabilities.remoteLeaderboard;
216
+ };
209
217
  const isAdPlacementAllowed = (placementId, expectedType) => {
210
218
  const actualType = options.resolveAdPlacementType?.(placementId);
211
219
  if (actualType !== undefined && actualType !== expectedType) {
@@ -303,16 +311,21 @@ export function withTargetAvailability(gateway, config, options = {}) {
303
311
  return gateway.ads.showInterstitial(input);
304
312
  },
305
313
  },
306
- leaderboard: availabilityConfig.features.leaderboard
307
- ? gateway.leaderboard
308
- : {
309
- async submitScore() {
314
+ leaderboard: {
315
+ async submitScore(input) {
316
+ if (!await isLeaderboardAvailable()) {
310
317
  return {
311
318
  submitted: false,
312
319
  };
313
- },
314
- async open() { },
320
+ }
321
+ return gateway.leaderboard.submitScore(input);
315
322
  },
323
+ async open(input) {
324
+ if (await isLeaderboardAvailable()) {
325
+ await gateway.leaderboard.open(input);
326
+ }
327
+ },
328
+ },
316
329
  };
317
330
  }
318
331
  function resolveAvailabilityConfig(config, effectiveConfig) {
@@ -335,7 +348,7 @@ function isFeatureCapabilitySupported(feature, capabilities) {
335
348
  case 'interstitialAds':
336
349
  return capabilities.interstitialAds;
337
350
  case 'leaderboard':
338
- return capabilities.nativeLeaderboard;
351
+ return capabilities.nativeLeaderboard || capabilities.remoteLeaderboard;
339
352
  case 'localization':
340
353
  return capabilities.localizedContent;
341
354
  }
@@ -15,6 +15,10 @@ export type TargetViewportMeasurementSource = 'container' | 'visual-viewport' |
15
15
  export type TargetViewportControlPlacement = 'bottom' | 'side';
16
16
  /** Recommended placement for secondary game panels. */
17
17
  export type TargetViewportPanelPlacement = 'below' | 'side' | 'drawer';
18
+ /** Adaptive shell arrangement for a game surface and its surrounding UI. */
19
+ export type TargetViewportCompositionMode = 'side-rails' | 'bottom-controls' | 'compact-portrait';
20
+ /** Optional expanded-screen arrangement requested by a game. */
21
+ export type TargetViewportExpandedLayout = 'none' | 'side-rails';
18
22
  /** Width thresholds used to classify target viewports. */
19
23
  export interface TargetViewportBreakpoints {
20
24
  readonly compactMaxWidth: number;
@@ -109,6 +113,36 @@ export interface TargetViewportSnapshot extends TargetViewportPlan {
109
113
  readonly safeArea: TargetViewportSafeArea;
110
114
  }
111
115
  /**
116
+ * Input for resolving a concrete adaptive game shell from an existing viewport
117
+ * snapshot. The aspect ratio is expressed as width divided by height.
118
+ */
119
+ export interface TargetViewportCompositionInput {
120
+ readonly viewport: Pick<TargetViewportSnapshot, 'layout' | 'safeArea'>;
121
+ readonly gameAspectRatio: number;
122
+ readonly expandedLayout?: TargetViewportExpandedLayout;
123
+ readonly minRailWidth?: number;
124
+ }
125
+ interface TargetViewportCompositionBounds {
126
+ readonly contentBounds: TargetViewportBounds;
127
+ readonly gameBounds: TargetViewportBounds;
128
+ }
129
+ /** Concrete safe-area-relative geometry for an adaptive game shell. */
130
+ export type TargetViewportComposition = TargetViewportCompositionBounds & ({
131
+ readonly mode: 'side-rails';
132
+ readonly leftRailBounds: TargetViewportBounds;
133
+ readonly rightRailBounds: TargetViewportBounds;
134
+ readonly primaryControls: 'side';
135
+ readonly secondaryPanels: 'side';
136
+ } | {
137
+ readonly mode: 'bottom-controls';
138
+ readonly primaryControls: 'bottom';
139
+ readonly secondaryPanels: 'below';
140
+ } | {
141
+ readonly mode: 'compact-portrait';
142
+ readonly primaryControls: 'bottom';
143
+ readonly secondaryPanels: 'drawer';
144
+ });
145
+ /**
112
146
  * The minimal browser style surface needed to read CSS safe-area custom
113
147
  * properties. Keeping this structural avoids a runtime DOM dependency in the
114
148
  * target-config package.
@@ -151,6 +185,8 @@ export declare const defaultTargetViewportSafeAreaCssVariables: {
151
185
  readonly bottom: '--mpgd-safe-area-bottom';
152
186
  readonly left: '--mpgd-safe-area-left';
153
187
  };
188
+ /** Default minimum width for each expanded-layout side rail. */
189
+ export declare const defaultTargetViewportCompositionMinRailWidth = 160;
154
190
  /** Map a configured target runtime to its viewport host shell. */
155
191
  export declare function targetViewportShellForRuntime(runtime: TargetRuntimeKind): TargetViewportShell;
156
192
  /** Resolve a viewport shell from the runtime field of a target config. */
@@ -171,6 +207,15 @@ export declare function resolveTargetViewportPlan(input: TargetViewportInput, br
171
207
  */
172
208
  export declare function resolveTargetViewportSnapshot(input: TargetViewportSnapshotInput, breakpoints?: TargetViewportBreakpoints): TargetViewportSnapshot;
173
209
  /**
210
+ * Resolve an adaptive game shell inside the snapshot's safe content bounds.
211
+ *
212
+ * A landscape viewport uses side rails only when the requested game aspect and
213
+ * both minimum rails fit at full content height. Portrait viewports remain
214
+ * full-bleed so phone shells do not inherit desktop letterboxing. Intermediate
215
+ * layouts keep the full game bounds and move persistent controls to the bottom.
216
+ */
217
+ export declare function resolveTargetViewportComposition(input: TargetViewportCompositionInput): TargetViewportComposition;
218
+ /**
174
219
  * Resolve safe-area insets and content bounds for a given layout.
175
220
  *
176
221
  * Insets are clamped sequentially (top before bottom, left before right) so
@@ -185,3 +230,4 @@ export declare function resolveTargetViewportSafeArea(layout: Pick<TargetViewpor
185
230
  * the same full-viewport layout as before.
186
231
  */
187
232
  export declare function readTargetViewportSafeAreaInsets(style: TargetViewportComputedStyle, variables?: TargetViewportSafeAreaCssVariables): TargetViewportSafeAreaInsets;
233
+ export {};
package/dist/viewport.js CHANGED
@@ -26,6 +26,8 @@ export const defaultTargetViewportSafeAreaCssVariables = {
26
26
  bottom: '--mpgd-safe-area-bottom',
27
27
  left: '--mpgd-safe-area-left',
28
28
  };
29
+ /** Default minimum width for each expanded-layout side rail. */
30
+ export const defaultTargetViewportCompositionMinRailWidth = 160;
29
31
  const targetViewportOrientationPolicyModeDescriptors = {
30
32
  responsive: {
31
33
  preferredOrientation: undefined,
@@ -147,6 +149,80 @@ export function resolveTargetViewportSnapshot(input, breakpoints = defaultTarget
147
149
  safeArea: resolveTargetViewportSafeArea(plan.layout, input.safeAreaInsets),
148
150
  };
149
151
  }
152
+ /**
153
+ * Resolve an adaptive game shell inside the snapshot's safe content bounds.
154
+ *
155
+ * A landscape viewport uses side rails only when the requested game aspect and
156
+ * both minimum rails fit at full content height. Portrait viewports remain
157
+ * full-bleed so phone shells do not inherit desktop letterboxing. Intermediate
158
+ * layouts keep the full game bounds and move persistent controls to the bottom.
159
+ */
160
+ export function resolveTargetViewportComposition(input) {
161
+ const gameAspectRatio = normalizeViewportAspectRatio(input.gameAspectRatio);
162
+ const minRailWidth = normalizeViewportMinimum(input.minRailWidth ?? defaultTargetViewportCompositionMinRailWidth, 'minRailWidth');
163
+ const contentBounds = { ...input.viewport.safeArea.contentBounds };
164
+ const portrait = input.viewport.layout.orientation === 'portrait';
165
+ if (portrait) {
166
+ if (input.viewport.layout.sizeClass === 'compact') {
167
+ return {
168
+ mode: 'compact-portrait',
169
+ contentBounds,
170
+ gameBounds: contentBounds,
171
+ primaryControls: 'bottom',
172
+ secondaryPanels: 'drawer',
173
+ };
174
+ }
175
+ return {
176
+ mode: 'bottom-controls',
177
+ contentBounds,
178
+ gameBounds: contentBounds,
179
+ primaryControls: 'bottom',
180
+ secondaryPanels: 'below',
181
+ };
182
+ }
183
+ const requestedSideRails = input.expandedLayout === 'side-rails';
184
+ const minimumGameWidth = contentBounds.width > 0 && contentBounds.height > 0 ? 1 : 0;
185
+ const desiredGameWidth = Math.max(minimumGameWidth, Math.round(contentBounds.height * gameAspectRatio));
186
+ const fittedGameWidth = Math.min(contentBounds.width, desiredGameWidth);
187
+ const availableRailWidth = (contentBounds.width - fittedGameWidth) / 2;
188
+ if (requestedSideRails &&
189
+ input.viewport.layout.sizeClass === 'expanded' &&
190
+ desiredGameWidth <= contentBounds.width &&
191
+ availableRailWidth >= minRailWidth) {
192
+ const gameBounds = {
193
+ x: contentBounds.x + availableRailWidth,
194
+ y: contentBounds.y,
195
+ width: fittedGameWidth,
196
+ height: contentBounds.height,
197
+ };
198
+ return {
199
+ mode: 'side-rails',
200
+ contentBounds,
201
+ gameBounds,
202
+ leftRailBounds: {
203
+ x: contentBounds.x,
204
+ y: contentBounds.y,
205
+ width: availableRailWidth,
206
+ height: contentBounds.height,
207
+ },
208
+ rightRailBounds: {
209
+ x: gameBounds.x + gameBounds.width,
210
+ y: contentBounds.y,
211
+ width: availableRailWidth,
212
+ height: contentBounds.height,
213
+ },
214
+ primaryControls: 'side',
215
+ secondaryPanels: 'side',
216
+ };
217
+ }
218
+ return {
219
+ mode: 'bottom-controls',
220
+ contentBounds,
221
+ gameBounds: contentBounds,
222
+ primaryControls: 'bottom',
223
+ secondaryPanels: 'below',
224
+ };
225
+ }
150
226
  /**
151
227
  * Resolve safe-area insets and content bounds for a given layout.
152
228
  *
@@ -246,6 +322,20 @@ function normalizeViewportInset(value, name) {
246
322
  }
247
323
  return Math.round(value);
248
324
  }
325
+ /** Validate a non-negative CSS-pixel minimum without discarding fractions. */
326
+ function normalizeViewportMinimum(value, name) {
327
+ if (!Number.isFinite(value) || value < 0) {
328
+ throw new Error(`Viewport ${name} must be a non-negative finite number.`);
329
+ }
330
+ return value;
331
+ }
332
+ /** Validate a finite positive width-to-height aspect ratio. */
333
+ function normalizeViewportAspectRatio(value) {
334
+ if (!Number.isFinite(value) || value <= 0) {
335
+ throw new Error('Viewport gameAspectRatio must be a positive finite number.');
336
+ }
337
+ return value;
338
+ }
249
339
  /** Parse a non-negative CSS pixel custom-property value. */
250
340
  function readTargetViewportCssPixels(style, property) {
251
341
  const value = style.getPropertyValue(property).trim();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mpgd/target-config",
3
- "version": "0.11.0",
3
+ "version": "0.12.0",
4
4
  "description": "Target-specific platform feature availability and effective config helpers for mpgd.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -37,9 +37,9 @@
37
37
  ],
38
38
  "dependencies": {
39
39
  "typia": "13.0.2",
40
- "@mpgd/catalog": "0.5.2",
41
- "@mpgd/i18n": "0.5.4",
42
- "@mpgd/platform": "0.8.0"
40
+ "@mpgd/catalog": "0.6.0",
41
+ "@mpgd/i18n": "0.5.5",
42
+ "@mpgd/platform": "0.9.0"
43
43
  },
44
44
  "devDependencies": {
45
45
  "ttsc": "0.18.4",
package/targets.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "2026-07-19.1",
2
+ "version": "2026-08-12",
3
3
  "targets": {
4
4
  "web-preview": {
5
5
  "runtime": "web-preview",
@@ -47,10 +47,10 @@
47
47
  "microsoft-store": {
48
48
  "runtime": "microsoft-store-pwa",
49
49
  "features": {
50
- "iap": false,
50
+ "iap": true,
51
51
  "rewardedAds": false,
52
52
  "interstitialAds": false,
53
- "leaderboard": false,
53
+ "leaderboard": true,
54
54
  "localization": true
55
55
  },
56
56
  "capabilities": {
@@ -61,7 +61,7 @@
61
61
  "fallbackLocale": "en"
62
62
  },
63
63
  "monetization": {
64
- "iap": false,
64
+ "iap": true,
65
65
  "rewardedAds": false,
66
66
  "interstitialAds": false
67
67
  },