@mpgd/target-config 0.9.1 → 0.10.1
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.d.ts +1 -1
- package/dist/effective.js +9 -37
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/releaseIdentity.d.ts +33 -0
- package/dist/releaseIdentity.js +80 -0
- package/dist/runtime.d.ts +1 -0
- package/dist/runtime.js +41 -0
- package/dist/viewport.d.ts +118 -0
- package/dist/viewport.js +107 -0
- package/package.json +2 -2
package/dist/effective.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type AdPlacementEntry, type AdPlacements, type ProductCatalog, type ProductCatalogEntry } from '@mpgd/catalog';
|
|
2
2
|
import { type FeatureAvailabilityReason, type TargetCapabilityConfig, type TargetConfig, type TargetConfigMatrix, type TargetFeatureConfig, type TargetIntegrationConfig, type TargetLocalizationConfig, type TargetPolicyRestrictions, type TargetReleaseConfig, type TargetRuntimeKind } from './runtime.js';
|
|
3
3
|
export type EffectiveAvailabilityReason = FeatureAvailabilityReason | 'missing-platform-id';
|
|
4
4
|
export interface EffectivePlatformTargetMetadata {
|
package/dist/effective.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { resolveAdPlacementPlatformId, resolveProductPlatformId, } from '@mpgd/catalog';
|
|
2
|
+
import { assertTargetIntegrationRuntimeBounds, } from './runtime.js';
|
|
2
3
|
export const defaultLeaderboardId = 'default';
|
|
3
4
|
const disabledAuthoritativeMonetization = {
|
|
4
5
|
iap: false,
|
|
@@ -10,10 +11,10 @@ export function createEffectiveTargetConfig(input) {
|
|
|
10
11
|
const products = input.catalog.products.map((product) => createEffectiveProductConfig(input.target, config, product));
|
|
11
12
|
const placements = input.adPlacements.placements.map((placement) => createEffectiveAdPlacementConfig(input.target, config, placement));
|
|
12
13
|
const leaderboardEnabled = config.features.leaderboard;
|
|
13
|
-
const integrations =
|
|
14
|
+
const integrations = assertTargetIntegrationRuntimeBounds(config.runtime, {
|
|
14
15
|
...config.integrations,
|
|
15
16
|
...input.platformTarget?.integrations,
|
|
16
|
-
});
|
|
17
|
+
}, `Effective target ${input.target}`);
|
|
17
18
|
return {
|
|
18
19
|
version: effectiveTargetConfigVersion({
|
|
19
20
|
targetConfig: input.targetConfigVersion,
|
|
@@ -107,8 +108,8 @@ export function getEffectiveAdPlacementConfig(config, placementId) {
|
|
|
107
108
|
return config.ads.placements.find((placement) => placement.id === placementId);
|
|
108
109
|
}
|
|
109
110
|
function createEffectiveProductConfig(target, config, product) {
|
|
110
|
-
const platformProductId =
|
|
111
|
-
const reason = effectiveProductReason(
|
|
111
|
+
const platformProductId = resolveProductPlatformId(product, target);
|
|
112
|
+
const reason = effectiveProductReason(config.runtime, config.features.iap, product, platformProductId);
|
|
112
113
|
return {
|
|
113
114
|
id: product.id,
|
|
114
115
|
type: product.type,
|
|
@@ -118,11 +119,11 @@ function createEffectiveProductConfig(target, config, product) {
|
|
|
118
119
|
...(platformProductId === undefined ? {} : { platformProductId }),
|
|
119
120
|
};
|
|
120
121
|
}
|
|
121
|
-
function effectiveProductReason(
|
|
122
|
+
function effectiveProductReason(runtime, targetEnabled, product, platformProductId) {
|
|
122
123
|
if (!targetEnabled) {
|
|
123
124
|
return 'target-disabled';
|
|
124
125
|
}
|
|
125
|
-
if (
|
|
126
|
+
if (runtime === 'verse8-web' && product.grant.type === 'resource') {
|
|
126
127
|
return 'capability-unsupported';
|
|
127
128
|
}
|
|
128
129
|
return effectiveItemReason(targetEnabled, platformProductId);
|
|
@@ -131,7 +132,7 @@ function createEffectiveAdPlacementConfig(target, config, placement) {
|
|
|
131
132
|
const featureEnabled = isRewardedPlacement(placement)
|
|
132
133
|
? config.features.rewardedAds
|
|
133
134
|
: config.features.interstitialAds;
|
|
134
|
-
const platformPlacementId =
|
|
135
|
+
const platformPlacementId = resolveAdPlacementPlatformId(placement, target);
|
|
135
136
|
const reason = effectiveItemReason(featureEnabled, platformPlacementId);
|
|
136
137
|
return {
|
|
137
138
|
id: placement.id,
|
|
@@ -152,35 +153,6 @@ function effectiveItemReason(targetEnabled, platformId) {
|
|
|
152
153
|
function effectiveTargetConfigVersion(input) {
|
|
153
154
|
return `${input.targetConfig}+catalog.${input.productCatalog}+ads.${input.adPlacements}`;
|
|
154
155
|
}
|
|
155
|
-
function productPlatformId(product, target) {
|
|
156
|
-
if (!isProductStoreTarget(target)) {
|
|
157
|
-
return undefined;
|
|
158
|
-
}
|
|
159
|
-
return normalizePlatformId(product.platformProductIds[target]);
|
|
160
|
-
}
|
|
161
|
-
function adPlacementPlatformId(placement, target) {
|
|
162
|
-
if (!isAdStoreTarget(target)) {
|
|
163
|
-
return undefined;
|
|
164
|
-
}
|
|
165
|
-
return normalizePlatformId(placement.platformPlacementIds[target]);
|
|
166
|
-
}
|
|
167
|
-
function normalizePlatformId(platformId) {
|
|
168
|
-
if (platformId === undefined) {
|
|
169
|
-
return undefined;
|
|
170
|
-
}
|
|
171
|
-
const trimmed = platformId.trim();
|
|
172
|
-
return trimmed.length === 0 ? undefined : trimmed;
|
|
173
|
-
}
|
|
174
|
-
function isProductStoreTarget(target) {
|
|
175
|
-
return target === 'android'
|
|
176
|
-
|| target === 'ios'
|
|
177
|
-
|| target === 'ait'
|
|
178
|
-
|| target === 'reddit'
|
|
179
|
-
|| target === 'verse8';
|
|
180
|
-
}
|
|
181
|
-
function isAdStoreTarget(target) {
|
|
182
|
-
return target === 'android' || target === 'ios' || target === 'ait' || target === 'verse8';
|
|
183
|
-
}
|
|
184
156
|
function isRewardedPlacement(placement) {
|
|
185
157
|
return placement.type === 'rewarded';
|
|
186
158
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { EffectiveTargetConfigMatrix } from './effective.js';
|
|
2
2
|
import type { TargetConfigMatrix } from './runtime.js';
|
|
3
3
|
export * from './effective.js';
|
|
4
|
+
export * from './releaseIdentity.js';
|
|
4
5
|
export * from './runtime.js';
|
|
5
6
|
export * from './viewport.js';
|
|
6
7
|
export declare const assertEffectiveTargetConfigMatrix: (input: unknown) => EffectiveTargetConfigMatrix;
|
package/dist/index.js
CHANGED
|
@@ -3,6 +3,7 @@ import * as _accessExpressionAsString_1 from "typia/lib/internal/_accessExpressi
|
|
|
3
3
|
const __typia_transform__accessExpressionAsString = _accessExpressionAsString_1._accessExpressionAsString;
|
|
4
4
|
import typia from 'typia';
|
|
5
5
|
export * from './effective.js';
|
|
6
|
+
export * from './releaseIdentity.js';
|
|
6
7
|
export * from './runtime.js';
|
|
7
8
|
export * from './viewport.js';
|
|
8
9
|
export const assertEffectiveTargetConfigMatrix = (() => {
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A human-facing release identity shared by every target artifact for one game release.
|
|
3
|
+
*
|
|
4
|
+
* `gameVersion` remains the game's SemVer value. `releaseRevision` is deliberately
|
|
5
|
+
* separate so a game can keep one semantic release while targets are promoted or
|
|
6
|
+
* rebuilt independently. For example, the common label for game `0.3.30` and
|
|
7
|
+
* revision `42` is `0.3.30-v42`.
|
|
8
|
+
*/
|
|
9
|
+
export interface MpgdReleaseIdentity {
|
|
10
|
+
readonly gameVersion: string;
|
|
11
|
+
readonly releaseRevision?: number;
|
|
12
|
+
readonly label: string;
|
|
13
|
+
}
|
|
14
|
+
export interface CreateMpgdReleaseIdentityInput {
|
|
15
|
+
readonly gameVersion: string;
|
|
16
|
+
readonly releaseRevision?: number;
|
|
17
|
+
/**
|
|
18
|
+
* Optional value from a build environment. It must equal the derived label so
|
|
19
|
+
* a stale or hand-edited label cannot be embedded into an artifact.
|
|
20
|
+
*/
|
|
21
|
+
readonly expectedLabel?: string;
|
|
22
|
+
}
|
|
23
|
+
export declare function createMpgdReleaseIdentity(input: CreateMpgdReleaseIdentityInput): MpgdReleaseIdentity;
|
|
24
|
+
export declare function formatMpgdReleaseLabel(gameVersion: string, releaseRevision: number | undefined): string;
|
|
25
|
+
/** Whether a normalized game version is eligible for an immutable production release. */
|
|
26
|
+
export declare function isMpgdFinalSemVer(value: string): boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Canonical release-manifest identifier derived from a common release label.
|
|
29
|
+
* Both components must be non-empty canonical tokens without the `+` delimiter.
|
|
30
|
+
*/
|
|
31
|
+
export declare function formatMpgdReleaseId(label: string, buildId: string): string;
|
|
32
|
+
/** Parse an optional release revision without making empty values meaningful. */
|
|
33
|
+
export declare function parseMpgdReleaseRevision(value: string | undefined): number | undefined;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
const semVerCore = '(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)';
|
|
2
|
+
const preReleaseNumericIdentifier = '(?:0|[1-9]\\d*)';
|
|
3
|
+
const preReleaseAlphanumericIdentifier = '(?:\\d*[A-Za-z-][0-9A-Za-z-]*)';
|
|
4
|
+
const preReleaseIdentifier = `(?:${preReleaseNumericIdentifier}|${preReleaseAlphanumericIdentifier})`;
|
|
5
|
+
const finalSemVerPattern = new RegExp(`^${semVerCore}$`, 'u');
|
|
6
|
+
const semVerPattern = new RegExp(`^${semVerCore}(?:-${preReleaseIdentifier}(?:\\.${preReleaseIdentifier})*)?$`, 'u');
|
|
7
|
+
export function createMpgdReleaseIdentity(input) {
|
|
8
|
+
const gameVersion = normalizeMpgdSemVer(input.gameVersion);
|
|
9
|
+
const releaseRevision = input.releaseRevision;
|
|
10
|
+
const label = formatNormalizedMpgdReleaseLabel(gameVersion, releaseRevision);
|
|
11
|
+
const expectedLabel = input.expectedLabel?.trim();
|
|
12
|
+
if (expectedLabel !== undefined && expectedLabel !== label) {
|
|
13
|
+
const detail = `${label}; received ${expectedLabel}.`;
|
|
14
|
+
throw new TypeError(`Release identity label must equal ${detail}`);
|
|
15
|
+
}
|
|
16
|
+
return releaseRevision === undefined
|
|
17
|
+
? { gameVersion, label }
|
|
18
|
+
: { gameVersion, releaseRevision, label };
|
|
19
|
+
}
|
|
20
|
+
export function formatMpgdReleaseLabel(gameVersion, releaseRevision) {
|
|
21
|
+
return formatNormalizedMpgdReleaseLabel(normalizeMpgdSemVer(gameVersion), releaseRevision);
|
|
22
|
+
}
|
|
23
|
+
function formatNormalizedMpgdReleaseLabel(gameVersion, releaseRevision) {
|
|
24
|
+
if (releaseRevision === undefined) {
|
|
25
|
+
return gameVersion;
|
|
26
|
+
}
|
|
27
|
+
if (!isMpgdFinalSemVer(gameVersion)) {
|
|
28
|
+
throw new TypeError('Release identity gameVersion must be a final SemVer when releaseRevision is set.');
|
|
29
|
+
}
|
|
30
|
+
if (!Number.isSafeInteger(releaseRevision) || releaseRevision < 1) {
|
|
31
|
+
throw new TypeError('Release identity releaseRevision must be a positive safe integer.');
|
|
32
|
+
}
|
|
33
|
+
return `${gameVersion}-v${releaseRevision}`;
|
|
34
|
+
}
|
|
35
|
+
/** Whether a normalized game version is eligible for an immutable production release. */
|
|
36
|
+
export function isMpgdFinalSemVer(value) {
|
|
37
|
+
return finalSemVerPattern.test(value);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Canonical release-manifest identifier derived from a common release label.
|
|
41
|
+
* Both components must be non-empty canonical tokens without the `+` delimiter.
|
|
42
|
+
*/
|
|
43
|
+
export function formatMpgdReleaseId(label, buildId) {
|
|
44
|
+
assertReleaseIdComponent(label, 'label');
|
|
45
|
+
assertReleaseIdComponent(buildId, 'buildId');
|
|
46
|
+
return `mpgd-${label}+${buildId}`;
|
|
47
|
+
}
|
|
48
|
+
/** Parse an optional release revision without making empty values meaningful. */
|
|
49
|
+
export function parseMpgdReleaseRevision(value) {
|
|
50
|
+
if (value === undefined || value.trim().length === 0) {
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
const normalized = value.trim();
|
|
54
|
+
if (!/^[1-9]\d*$/u.test(normalized)) {
|
|
55
|
+
throw new TypeError('Release revision must be a positive safe integer.');
|
|
56
|
+
}
|
|
57
|
+
const releaseRevision = Number.parseInt(normalized, 10);
|
|
58
|
+
if (!Number.isSafeInteger(releaseRevision)) {
|
|
59
|
+
throw new TypeError('Release revision must be a positive safe integer.');
|
|
60
|
+
}
|
|
61
|
+
return releaseRevision;
|
|
62
|
+
}
|
|
63
|
+
function normalizeMpgdSemVer(value) {
|
|
64
|
+
const normalized = value.trim();
|
|
65
|
+
if (normalized.includes('+')) {
|
|
66
|
+
throw new TypeError('Release identity gameVersion must not contain SemVer build metadata because "+" is reserved for release IDs.');
|
|
67
|
+
}
|
|
68
|
+
if (!semVerPattern.test(normalized)) {
|
|
69
|
+
throw new TypeError('Release identity gameVersion must be a SemVer value.');
|
|
70
|
+
}
|
|
71
|
+
return normalized;
|
|
72
|
+
}
|
|
73
|
+
function assertReleaseIdComponent(value, label) {
|
|
74
|
+
if (value.length === 0 || /\s/u.test(value)) {
|
|
75
|
+
throw new TypeError(`Release identifier ${label} must be a non-empty canonical token.`);
|
|
76
|
+
}
|
|
77
|
+
if (value.includes('+')) {
|
|
78
|
+
throw new TypeError(`Release identifier ${label} must not contain "+".`);
|
|
79
|
+
}
|
|
80
|
+
}
|
package/dist/runtime.d.ts
CHANGED
|
@@ -128,6 +128,7 @@ export declare const defaultTargetIntegrationConfig: {
|
|
|
128
128
|
readonly presentationMode: 'fullscreen';
|
|
129
129
|
};
|
|
130
130
|
export declare function normalizeTargetIntegrationConfig(config: Partial<TargetIntegrationConfig> | undefined): TargetIntegrationConfig;
|
|
131
|
+
export declare function assertTargetIntegrationRuntimeBounds(runtime: TargetRuntimeKind, config: Partial<TargetIntegrationConfig> | undefined, label: string): TargetIntegrationConfig;
|
|
131
132
|
export declare function targetConfigKeyForPlatform(target: PlatformConfigTarget): string;
|
|
132
133
|
export declare function getTargetConfig(matrix: TargetConfigMatrix, target: PlatformConfigTarget | string): TargetConfig;
|
|
133
134
|
export declare function isPlatformFeatureEnabled(config: TargetConfig, feature: PlatformFeature): boolean;
|
package/dist/runtime.js
CHANGED
|
@@ -12,6 +12,22 @@ export const targetIntegrations = [
|
|
|
12
12
|
'inboundShare',
|
|
13
13
|
'notifications',
|
|
14
14
|
];
|
|
15
|
+
const integrationUpperBoundsByRuntime = {
|
|
16
|
+
'verse8-web': {
|
|
17
|
+
identityUpgrade: 'unsupported',
|
|
18
|
+
presentation: 'available',
|
|
19
|
+
sharing: 'unsupported',
|
|
20
|
+
inboundShare: 'unsupported',
|
|
21
|
+
notifications: 'unsupported',
|
|
22
|
+
},
|
|
23
|
+
'web-preview': {
|
|
24
|
+
identityUpgrade: 'disabled',
|
|
25
|
+
presentation: 'available',
|
|
26
|
+
sharing: 'available',
|
|
27
|
+
inboundShare: 'available',
|
|
28
|
+
notifications: 'unsupported',
|
|
29
|
+
},
|
|
30
|
+
};
|
|
15
31
|
const integrationAvailabilityStateValues = [
|
|
16
32
|
'available',
|
|
17
33
|
'disabled',
|
|
@@ -48,6 +64,31 @@ export function normalizeTargetIntegrationConfig(config) {
|
|
|
48
64
|
presentationMode: normalizePresentationMode(config.presentationMode),
|
|
49
65
|
};
|
|
50
66
|
}
|
|
67
|
+
export function assertTargetIntegrationRuntimeBounds(runtime, config, label) {
|
|
68
|
+
const integrations = normalizeTargetIntegrationConfig(config);
|
|
69
|
+
if (runtime !== 'verse8-web' && runtime !== 'web-preview') {
|
|
70
|
+
return integrations;
|
|
71
|
+
}
|
|
72
|
+
const upperBounds = integrationUpperBoundsByRuntime[runtime];
|
|
73
|
+
for (const integration of targetIntegrations) {
|
|
74
|
+
const state = integrations[integration];
|
|
75
|
+
const upperBound = upperBounds[integration];
|
|
76
|
+
if (!isIntegrationStateWithinUpperBound(state, upperBound)) {
|
|
77
|
+
throw new Error(`${label} cannot configure ${integration} as ${state} for ${runtime} runtime; maximum supported state is ${upperBound}.`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return integrations;
|
|
81
|
+
}
|
|
82
|
+
function isIntegrationStateWithinUpperBound(state, upperBound) {
|
|
83
|
+
switch (upperBound) {
|
|
84
|
+
case 'available':
|
|
85
|
+
return true;
|
|
86
|
+
case 'disabled':
|
|
87
|
+
return state === 'disabled' || state === 'unsupported';
|
|
88
|
+
case 'unsupported':
|
|
89
|
+
return state === 'unsupported';
|
|
90
|
+
}
|
|
91
|
+
}
|
|
51
92
|
export function targetConfigKeyForPlatform(target) {
|
|
52
93
|
return target === 'browser' ? 'web-preview' : target;
|
|
53
94
|
}
|
package/dist/viewport.d.ts
CHANGED
|
@@ -1,16 +1,26 @@
|
|
|
1
1
|
import type { TargetConfig, TargetRuntimeKind } from './runtime.js';
|
|
2
|
+
/** The physical orientation derived from a viewport measurement. */
|
|
2
3
|
export type TargetViewportOrientation = 'portrait' | 'landscape';
|
|
4
|
+
/** How a game prefers or constrains viewport orientation. */
|
|
3
5
|
export type TargetViewportOrientationPolicyMode = 'responsive' | 'prefer-landscape' | 'prefer-portrait' | 'lock-landscape' | 'lock-portrait';
|
|
6
|
+
/** Presentation behavior when the measured orientation does not match policy. */
|
|
4
7
|
export type TargetViewportOrientationMismatchBehavior = 'continue' | 'letterbox' | 'show-rotate-prompt';
|
|
8
|
+
/** Responsive width class used by target-independent game layout. */
|
|
5
9
|
export type TargetViewportSizeClass = 'compact' | 'medium' | 'expanded';
|
|
10
|
+
/** Browser or embedded host shell that owns the game surface. */
|
|
6
11
|
export type TargetViewportShell = 'browser' | 'mobile-webview' | 'embedded-webview';
|
|
12
|
+
/** Browser measurement surface used to construct the viewport snapshot. */
|
|
7
13
|
export type TargetViewportMeasurementSource = 'container' | 'visual-viewport' | 'window' | 'unknown';
|
|
14
|
+
/** Recommended placement for primary game controls. */
|
|
8
15
|
export type TargetViewportControlPlacement = 'bottom' | 'side';
|
|
16
|
+
/** Recommended placement for secondary game panels. */
|
|
9
17
|
export type TargetViewportPanelPlacement = 'below' | 'side' | 'drawer';
|
|
18
|
+
/** Width thresholds used to classify target viewports. */
|
|
10
19
|
export interface TargetViewportBreakpoints {
|
|
11
20
|
readonly compactMaxWidth: number;
|
|
12
21
|
readonly expandedMinWidth: number;
|
|
13
22
|
}
|
|
23
|
+
/** Raw viewport measurement and optional target context. */
|
|
14
24
|
export interface TargetViewportInput {
|
|
15
25
|
readonly width: number;
|
|
16
26
|
readonly height: number;
|
|
@@ -18,6 +28,32 @@ export interface TargetViewportInput {
|
|
|
18
28
|
readonly source?: TargetViewportMeasurementSource;
|
|
19
29
|
readonly orientationPolicy?: TargetViewportOrientationPolicy;
|
|
20
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* Insets expressed in CSS pixels between a viewport edge and content that can
|
|
33
|
+
* safely receive persistent controls. A target adapter or browser host owns
|
|
34
|
+
* measuring these values; the viewport helpers only normalize their use.
|
|
35
|
+
*/
|
|
36
|
+
export interface TargetViewportSafeAreaInsets {
|
|
37
|
+
readonly top: number;
|
|
38
|
+
readonly right: number;
|
|
39
|
+
readonly bottom: number;
|
|
40
|
+
readonly left: number;
|
|
41
|
+
}
|
|
42
|
+
/** A rectangular region expressed in the coordinate space of a viewport. */
|
|
43
|
+
export interface TargetViewportBounds {
|
|
44
|
+
readonly x: number;
|
|
45
|
+
readonly y: number;
|
|
46
|
+
readonly width: number;
|
|
47
|
+
readonly height: number;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* An optional safe-area extension for an otherwise ordinary viewport
|
|
51
|
+
* measurement. Omitted values are treated as zero.
|
|
52
|
+
*/
|
|
53
|
+
export interface TargetViewportSnapshotInput extends TargetViewportInput {
|
|
54
|
+
readonly safeAreaInsets?: Partial<TargetViewportSafeAreaInsets>;
|
|
55
|
+
}
|
|
56
|
+
/** Normalized viewport geometry and responsive classification. */
|
|
21
57
|
export interface TargetViewportLayout {
|
|
22
58
|
readonly width: number;
|
|
23
59
|
readonly height: number;
|
|
@@ -29,15 +65,18 @@ export interface TargetViewportLayout {
|
|
|
29
65
|
readonly shell: TargetViewportShell;
|
|
30
66
|
readonly source: TargetViewportMeasurementSource;
|
|
31
67
|
}
|
|
68
|
+
/** Target-independent control and panel placement recommendations. */
|
|
32
69
|
export interface TargetViewportRecommendation {
|
|
33
70
|
readonly primaryControls: TargetViewportControlPlacement;
|
|
34
71
|
readonly secondaryPanels: TargetViewportPanelPlacement;
|
|
35
72
|
readonly safeAreaAware: boolean;
|
|
36
73
|
}
|
|
74
|
+
/** A game's orientation preference and mismatch behavior. */
|
|
37
75
|
export interface TargetViewportOrientationPolicy {
|
|
38
76
|
readonly mode: TargetViewportOrientationPolicyMode;
|
|
39
77
|
readonly mismatchBehavior?: TargetViewportOrientationMismatchBehavior;
|
|
40
78
|
}
|
|
79
|
+
/** Resolved orientation state for the current viewport and policy. */
|
|
41
80
|
export interface TargetViewportOrientationPlan {
|
|
42
81
|
readonly mode: TargetViewportOrientationPolicyMode;
|
|
43
82
|
readonly preferredOrientation?: TargetViewportOrientation;
|
|
@@ -47,23 +86,102 @@ export interface TargetViewportOrientationPlan {
|
|
|
47
86
|
readonly shouldLetterbox: boolean;
|
|
48
87
|
readonly shouldShowRotatePrompt: boolean;
|
|
49
88
|
}
|
|
89
|
+
/** Complete responsive layout plan without concrete safe-area geometry. */
|
|
50
90
|
export interface TargetViewportPlan {
|
|
51
91
|
readonly layout: TargetViewportLayout;
|
|
52
92
|
readonly recommendation: TargetViewportRecommendation;
|
|
53
93
|
readonly orientation: TargetViewportOrientationPlan;
|
|
54
94
|
}
|
|
95
|
+
/**
|
|
96
|
+
* The effective safe-area insets and the remaining rectangle for a game board
|
|
97
|
+
* or persistent HUD. Insets are clamped to the measured viewport so a malformed
|
|
98
|
+
* host measurement cannot produce a negative-sized content region.
|
|
99
|
+
*/
|
|
100
|
+
export interface TargetViewportSafeArea {
|
|
101
|
+
readonly insets: TargetViewportSafeAreaInsets;
|
|
102
|
+
readonly contentBounds: TargetViewportBounds;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* A viewport plan enriched with concrete safe-area geometry. It is assignable
|
|
106
|
+
* to `TargetViewportPlan`, so existing scene code can adopt it incrementally.
|
|
107
|
+
*/
|
|
108
|
+
export interface TargetViewportSnapshot extends TargetViewportPlan {
|
|
109
|
+
readonly safeArea: TargetViewportSafeArea;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* The minimal browser style surface needed to read CSS safe-area custom
|
|
113
|
+
* properties. Keeping this structural avoids a runtime DOM dependency in the
|
|
114
|
+
* target-config package.
|
|
115
|
+
*/
|
|
116
|
+
export interface TargetViewportComputedStyle {
|
|
117
|
+
getPropertyValue(property: string): string;
|
|
118
|
+
}
|
|
119
|
+
/** CSS custom-property names used to expose host safe-area insets. */
|
|
120
|
+
export interface TargetViewportSafeAreaCssVariables {
|
|
121
|
+
readonly top: string;
|
|
122
|
+
readonly right: string;
|
|
123
|
+
readonly bottom: string;
|
|
124
|
+
readonly left: string;
|
|
125
|
+
}
|
|
126
|
+
/** Default responsive breakpoints shared by generated game starters. */
|
|
55
127
|
export declare const defaultTargetViewportBreakpoints: {
|
|
56
128
|
readonly compactMaxWidth: 599;
|
|
57
129
|
readonly expandedMinWidth: 900;
|
|
58
130
|
};
|
|
131
|
+
/** Default policy that keeps games responsive in every orientation. */
|
|
59
132
|
export declare const defaultTargetViewportOrientationPolicy: {
|
|
60
133
|
readonly mode: 'responsive';
|
|
61
134
|
readonly mismatchBehavior: 'continue';
|
|
62
135
|
};
|
|
136
|
+
/** Zero-inset fallback for hosts without safe-area support. */
|
|
137
|
+
export declare const defaultTargetViewportSafeAreaInsets: {
|
|
138
|
+
readonly top: 0;
|
|
139
|
+
readonly right: 0;
|
|
140
|
+
readonly bottom: 0;
|
|
141
|
+
readonly left: 0;
|
|
142
|
+
};
|
|
143
|
+
/**
|
|
144
|
+
* CSS variables emitted by the Phaser starter. They deliberately mirror the
|
|
145
|
+
* platform `safe-area-inset-*` environment variables while remaining readable
|
|
146
|
+
* from a Phaser/TypeScript layout pass.
|
|
147
|
+
*/
|
|
148
|
+
export declare const defaultTargetViewportSafeAreaCssVariables: {
|
|
149
|
+
readonly top: '--mpgd-safe-area-top';
|
|
150
|
+
readonly right: '--mpgd-safe-area-right';
|
|
151
|
+
readonly bottom: '--mpgd-safe-area-bottom';
|
|
152
|
+
readonly left: '--mpgd-safe-area-left';
|
|
153
|
+
};
|
|
154
|
+
/** Map a configured target runtime to its viewport host shell. */
|
|
63
155
|
export declare function targetViewportShellForRuntime(runtime: TargetRuntimeKind): TargetViewportShell;
|
|
156
|
+
/** Resolve a viewport shell from the runtime field of a target config. */
|
|
64
157
|
export declare function targetViewportShellForConfig(config: Pick<TargetConfig, 'runtime'>): TargetViewportShell;
|
|
158
|
+
/** Normalize viewport input into responsive layout geometry. */
|
|
65
159
|
export declare function resolveTargetViewportLayout(input: TargetViewportInput, breakpoints?: TargetViewportBreakpoints): TargetViewportLayout;
|
|
160
|
+
/** Classify a viewport width against normalized responsive breakpoints. */
|
|
66
161
|
export declare function resolveTargetViewportSizeClass(width: number, breakpoints?: TargetViewportBreakpoints): TargetViewportSizeClass;
|
|
162
|
+
/** Recommend primary-control and secondary-panel placement for a layout. */
|
|
67
163
|
export declare function resolveTargetViewportRecommendation(layout: TargetViewportLayout): TargetViewportRecommendation;
|
|
164
|
+
/** Resolve orientation mismatch behavior for a measured layout. */
|
|
68
165
|
export declare function resolveTargetViewportOrientationPlan(layout: Pick<TargetViewportLayout, 'orientation'>, policy?: TargetViewportOrientationPolicy): TargetViewportOrientationPlan;
|
|
166
|
+
/** Build a responsive viewport plan from raw input and breakpoints. */
|
|
69
167
|
export declare function resolveTargetViewportPlan(input: TargetViewportInput, breakpoints?: TargetViewportBreakpoints): TargetViewportPlan;
|
|
168
|
+
/**
|
|
169
|
+
* Build a target viewport plan together with safe-area geometry for games that
|
|
170
|
+
* render persistent DOM controls or position Phaser HUD elements themselves.
|
|
171
|
+
*/
|
|
172
|
+
export declare function resolveTargetViewportSnapshot(input: TargetViewportSnapshotInput, breakpoints?: TargetViewportBreakpoints): TargetViewportSnapshot;
|
|
173
|
+
/**
|
|
174
|
+
* Resolve safe-area insets and content bounds for a given layout.
|
|
175
|
+
*
|
|
176
|
+
* Insets are clamped sequentially (top before bottom, left before right) so
|
|
177
|
+
* malformed host measurements cannot produce a negative-sized content region.
|
|
178
|
+
* When opposing insets exceed a viewport dimension, the first-processed side
|
|
179
|
+
* (top or left) takes priority.
|
|
180
|
+
*/
|
|
181
|
+
export declare function resolveTargetViewportSafeArea(layout: Pick<TargetViewportLayout, 'width' | 'height'>, insets?: Partial<TargetViewportSafeAreaInsets>): TargetViewportSafeArea;
|
|
182
|
+
/**
|
|
183
|
+
* Read the starter's computed CSS variables. Unknown or non-pixel values are
|
|
184
|
+
* intentionally treated as zero so browser shells without CSS env support keep
|
|
185
|
+
* the same full-viewport layout as before.
|
|
186
|
+
*/
|
|
187
|
+
export declare function readTargetViewportSafeAreaInsets(style: TargetViewportComputedStyle, variables?: TargetViewportSafeAreaCssVariables): TargetViewportSafeAreaInsets;
|
package/dist/viewport.js
CHANGED
|
@@ -1,11 +1,31 @@
|
|
|
1
|
+
/** Default responsive breakpoints shared by generated game starters. */
|
|
1
2
|
export const defaultTargetViewportBreakpoints = {
|
|
2
3
|
compactMaxWidth: 599,
|
|
3
4
|
expandedMinWidth: 900,
|
|
4
5
|
};
|
|
6
|
+
/** Default policy that keeps games responsive in every orientation. */
|
|
5
7
|
export const defaultTargetViewportOrientationPolicy = {
|
|
6
8
|
mode: 'responsive',
|
|
7
9
|
mismatchBehavior: 'continue',
|
|
8
10
|
};
|
|
11
|
+
/** Zero-inset fallback for hosts without safe-area support. */
|
|
12
|
+
export const defaultTargetViewportSafeAreaInsets = {
|
|
13
|
+
top: 0,
|
|
14
|
+
right: 0,
|
|
15
|
+
bottom: 0,
|
|
16
|
+
left: 0,
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* CSS variables emitted by the Phaser starter. They deliberately mirror the
|
|
20
|
+
* platform `safe-area-inset-*` environment variables while remaining readable
|
|
21
|
+
* from a Phaser/TypeScript layout pass.
|
|
22
|
+
*/
|
|
23
|
+
export const defaultTargetViewportSafeAreaCssVariables = {
|
|
24
|
+
top: '--mpgd-safe-area-top',
|
|
25
|
+
right: '--mpgd-safe-area-right',
|
|
26
|
+
bottom: '--mpgd-safe-area-bottom',
|
|
27
|
+
left: '--mpgd-safe-area-left',
|
|
28
|
+
};
|
|
9
29
|
const targetViewportOrientationPolicyModeDescriptors = {
|
|
10
30
|
responsive: {
|
|
11
31
|
preferredOrientation: undefined,
|
|
@@ -33,6 +53,7 @@ const targetViewportOrientationPolicyModeDescriptors = {
|
|
|
33
53
|
isLocked: true,
|
|
34
54
|
},
|
|
35
55
|
};
|
|
56
|
+
/** Map a configured target runtime to its viewport host shell. */
|
|
36
57
|
export function targetViewportShellForRuntime(runtime) {
|
|
37
58
|
switch (runtime) {
|
|
38
59
|
case 'capacitor-android':
|
|
@@ -49,9 +70,11 @@ export function targetViewportShellForRuntime(runtime) {
|
|
|
49
70
|
const exhaustive = runtime;
|
|
50
71
|
return exhaustive;
|
|
51
72
|
}
|
|
73
|
+
/** Resolve a viewport shell from the runtime field of a target config. */
|
|
52
74
|
export function targetViewportShellForConfig(config) {
|
|
53
75
|
return targetViewportShellForRuntime(config.runtime);
|
|
54
76
|
}
|
|
77
|
+
/** Normalize viewport input into responsive layout geometry. */
|
|
55
78
|
export function resolveTargetViewportLayout(input, breakpoints = defaultTargetViewportBreakpoints) {
|
|
56
79
|
const normalizedBreakpoints = normalizeTargetViewportBreakpoints(breakpoints);
|
|
57
80
|
const width = normalizeViewportDimension(input.width, 'width');
|
|
@@ -69,9 +92,11 @@ export function resolveTargetViewportLayout(input, breakpoints = defaultTargetVi
|
|
|
69
92
|
source: input.source ?? 'unknown',
|
|
70
93
|
};
|
|
71
94
|
}
|
|
95
|
+
/** Classify a viewport width against normalized responsive breakpoints. */
|
|
72
96
|
export function resolveTargetViewportSizeClass(width, breakpoints = defaultTargetViewportBreakpoints) {
|
|
73
97
|
return classifyTargetViewportSize(normalizeViewportDimension(width, 'width'), normalizeTargetViewportBreakpoints(breakpoints));
|
|
74
98
|
}
|
|
99
|
+
/** Recommend primary-control and secondary-panel placement for a layout. */
|
|
75
100
|
export function resolveTargetViewportRecommendation(layout) {
|
|
76
101
|
const narrowOrPortrait = layout.sizeClass === 'compact' || layout.orientation === 'portrait';
|
|
77
102
|
const browserSafeAreaRisk = layout.shell === 'browser' &&
|
|
@@ -83,6 +108,7 @@ export function resolveTargetViewportRecommendation(layout) {
|
|
|
83
108
|
safeAreaAware: layout.shell !== 'browser' || layout.sizeClass === 'compact' || browserSafeAreaRisk,
|
|
84
109
|
};
|
|
85
110
|
}
|
|
111
|
+
/** Resolve orientation mismatch behavior for a measured layout. */
|
|
86
112
|
export function resolveTargetViewportOrientationPlan(layout, policy = defaultTargetViewportOrientationPolicy) {
|
|
87
113
|
const normalizedPolicy = normalizeTargetViewportOrientationPolicy(policy);
|
|
88
114
|
const descriptor = targetViewportOrientationPolicyModeDescriptors[normalizedPolicy.mode];
|
|
@@ -100,6 +126,7 @@ export function resolveTargetViewportOrientationPlan(layout, policy = defaultTar
|
|
|
100
126
|
shouldShowRotatePrompt: isMismatch && mismatchBehavior === 'show-rotate-prompt',
|
|
101
127
|
};
|
|
102
128
|
}
|
|
129
|
+
/** Build a responsive viewport plan from raw input and breakpoints. */
|
|
103
130
|
export function resolveTargetViewportPlan(input, breakpoints = defaultTargetViewportBreakpoints) {
|
|
104
131
|
const layout = resolveTargetViewportLayout(input, breakpoints);
|
|
105
132
|
return {
|
|
@@ -108,6 +135,60 @@ export function resolveTargetViewportPlan(input, breakpoints = defaultTargetView
|
|
|
108
135
|
orientation: resolveTargetViewportOrientationPlan(layout, input.orientationPolicy),
|
|
109
136
|
};
|
|
110
137
|
}
|
|
138
|
+
/**
|
|
139
|
+
* Build a target viewport plan together with safe-area geometry for games that
|
|
140
|
+
* render persistent DOM controls or position Phaser HUD elements themselves.
|
|
141
|
+
*/
|
|
142
|
+
export function resolveTargetViewportSnapshot(input, breakpoints = defaultTargetViewportBreakpoints) {
|
|
143
|
+
const plan = resolveTargetViewportPlan(input, breakpoints);
|
|
144
|
+
return {
|
|
145
|
+
...plan,
|
|
146
|
+
safeArea: resolveTargetViewportSafeArea(plan.layout, input.safeAreaInsets),
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Resolve safe-area insets and content bounds for a given layout.
|
|
151
|
+
*
|
|
152
|
+
* Insets are clamped sequentially (top before bottom, left before right) so
|
|
153
|
+
* malformed host measurements cannot produce a negative-sized content region.
|
|
154
|
+
* When opposing insets exceed a viewport dimension, the first-processed side
|
|
155
|
+
* (top or left) takes priority.
|
|
156
|
+
*/
|
|
157
|
+
export function resolveTargetViewportSafeArea(layout, insets) {
|
|
158
|
+
const requested = normalizeTargetViewportSafeAreaInsets(insets);
|
|
159
|
+
const top = Math.min(requested.top, layout.height);
|
|
160
|
+
const bottom = Math.min(requested.bottom, layout.height - top);
|
|
161
|
+
const left = Math.min(requested.left, layout.width);
|
|
162
|
+
const right = Math.min(requested.right, layout.width - left);
|
|
163
|
+
return {
|
|
164
|
+
insets: {
|
|
165
|
+
top,
|
|
166
|
+
right,
|
|
167
|
+
bottom,
|
|
168
|
+
left,
|
|
169
|
+
},
|
|
170
|
+
contentBounds: {
|
|
171
|
+
x: left,
|
|
172
|
+
y: top,
|
|
173
|
+
width: layout.width - left - right,
|
|
174
|
+
height: layout.height - top - bottom,
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Read the starter's computed CSS variables. Unknown or non-pixel values are
|
|
180
|
+
* intentionally treated as zero so browser shells without CSS env support keep
|
|
181
|
+
* the same full-viewport layout as before.
|
|
182
|
+
*/
|
|
183
|
+
export function readTargetViewportSafeAreaInsets(style, variables = defaultTargetViewportSafeAreaCssVariables) {
|
|
184
|
+
return {
|
|
185
|
+
top: readTargetViewportCssPixels(style, variables.top),
|
|
186
|
+
right: readTargetViewportCssPixels(style, variables.right),
|
|
187
|
+
bottom: readTargetViewportCssPixels(style, variables.bottom),
|
|
188
|
+
left: readTargetViewportCssPixels(style, variables.left),
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
/** Classify a normalized width against normalized breakpoints. */
|
|
111
192
|
function classifyTargetViewportSize(width, breakpoints) {
|
|
112
193
|
if (width <= breakpoints.compactMaxWidth) {
|
|
113
194
|
return 'compact';
|
|
@@ -117,6 +198,7 @@ function classifyTargetViewportSize(width, breakpoints) {
|
|
|
117
198
|
}
|
|
118
199
|
return 'expanded';
|
|
119
200
|
}
|
|
201
|
+
/** Choose a secondary-panel placement for the resolved layout. */
|
|
120
202
|
function resolveTargetViewportPanelPlacement(layout) {
|
|
121
203
|
if (layout.sizeClass === 'compact') {
|
|
122
204
|
return 'drawer';
|
|
@@ -126,6 +208,7 @@ function resolveTargetViewportPanelPlacement(layout) {
|
|
|
126
208
|
}
|
|
127
209
|
return 'side';
|
|
128
210
|
}
|
|
211
|
+
/** Fill the policy mismatch behavior from its orientation mode. */
|
|
129
212
|
function normalizeTargetViewportOrientationPolicy(policy) {
|
|
130
213
|
const descriptor = targetViewportOrientationPolicyModeDescriptors[policy.mode];
|
|
131
214
|
return {
|
|
@@ -134,6 +217,7 @@ function normalizeTargetViewportOrientationPolicy(policy) {
|
|
|
134
217
|
(descriptor.isLocked ? 'show-rotate-prompt' : 'continue'),
|
|
135
218
|
};
|
|
136
219
|
}
|
|
220
|
+
/** Validate and normalize responsive breakpoint dimensions. */
|
|
137
221
|
function normalizeTargetViewportBreakpoints(breakpoints) {
|
|
138
222
|
const compactMaxWidth = normalizeViewportDimension(breakpoints.compactMaxWidth, 'compactMaxWidth');
|
|
139
223
|
const expandedMinWidth = normalizeViewportDimension(breakpoints.expandedMinWidth, 'expandedMinWidth');
|
|
@@ -145,6 +229,29 @@ function normalizeTargetViewportBreakpoints(breakpoints) {
|
|
|
145
229
|
expandedMinWidth,
|
|
146
230
|
};
|
|
147
231
|
}
|
|
232
|
+
/** Validate every supplied inset and fill omitted sides with zero. */
|
|
233
|
+
function normalizeTargetViewportSafeAreaInsets(insets) {
|
|
234
|
+
return {
|
|
235
|
+
top: normalizeViewportInset(insets?.top ?? 0, 'top'),
|
|
236
|
+
right: normalizeViewportInset(insets?.right ?? 0, 'right'),
|
|
237
|
+
bottom: normalizeViewportInset(insets?.bottom ?? 0, 'bottom'),
|
|
238
|
+
left: normalizeViewportInset(insets?.left ?? 0, 'left'),
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
/** Normalize a non-negative safe-area inset to whole CSS pixels. */
|
|
242
|
+
function normalizeViewportInset(value, name) {
|
|
243
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
244
|
+
throw new Error(`Viewport safe-area inset ${name} must be a non-negative finite number.`);
|
|
245
|
+
}
|
|
246
|
+
return Math.round(value);
|
|
247
|
+
}
|
|
248
|
+
/** Parse a non-negative CSS pixel custom-property value. */
|
|
249
|
+
function readTargetViewportCssPixels(style, property) {
|
|
250
|
+
const value = style.getPropertyValue(property).trim();
|
|
251
|
+
const match = /^(\d+(?:\.\d+)?)px$/u.exec(value);
|
|
252
|
+
return match === null ? 0 : Math.round(Number(match[1]));
|
|
253
|
+
}
|
|
254
|
+
/** Normalize a positive viewport dimension to at least one CSS pixel. */
|
|
148
255
|
function normalizeViewportDimension(value, name) {
|
|
149
256
|
if (!Number.isFinite(value) || value <= 0) {
|
|
150
257
|
throw new Error(`Viewport ${name} must be a positive finite number.`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mpgd/target-config",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.1",
|
|
4
4
|
"description": "Target-specific platform feature availability and effective config helpers for mpgd.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
],
|
|
38
38
|
"dependencies": {
|
|
39
39
|
"typia": "13.0.2",
|
|
40
|
-
"@mpgd/catalog": "0.5.
|
|
40
|
+
"@mpgd/catalog": "0.5.2",
|
|
41
41
|
"@mpgd/platform": "0.8.0",
|
|
42
42
|
"@mpgd/i18n": "0.5.4"
|
|
43
43
|
},
|