@mpgd/cli 0.19.0 → 0.20.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mpgd/cli",
3
- "version": "0.19.0",
3
+ "version": "0.20.0",
4
4
  "description": "Gunshi CLI for mpgd-kit starter generation and target workflow orchestration.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -53,16 +53,16 @@
53
53
  "@mpgd/adapter-ait": "0.8.0",
54
54
  "@mpgd/adapter-browser": "0.5.1",
55
55
  "@mpgd/adapter-devvit": "0.8.5",
56
- "@mpgd/adapter-capacitor": "0.4.5",
57
56
  "@mpgd/adapter-verse8": "0.2.2",
58
- "@mpgd/analytics": "0.3.8",
59
- "@mpgd/catalog": "0.5.1",
57
+ "@mpgd/adapter-capacitor": "0.4.5",
60
58
  "@mpgd/bridge": "0.7.0",
61
59
  "@mpgd/game-services": "0.11.0",
62
- "@mpgd/phaser-assets": "0.4.1",
63
60
  "@mpgd/i18n": "0.5.4",
61
+ "@mpgd/analytics": "0.3.8",
62
+ "@mpgd/phaser-assets": "0.4.1",
64
63
  "@mpgd/platform": "0.8.0",
65
- "@mpgd/target-config": "0.9.1"
64
+ "@mpgd/catalog": "0.5.1",
65
+ "@mpgd/target-config": "0.10.0"
66
66
  },
67
67
  "main": "./dist/index.js",
68
68
  "types": "./dist/index.d.ts",
@@ -127,13 +127,15 @@ preview, Apps in Toss, Devvit, Android, and iOS bundles.
127
127
 
128
128
  ## Viewport And Controls
129
129
 
130
- The starter computes an initial viewport plan with `@mpgd/target-config`.
130
+ The starter computes an initial viewport snapshot with `@mpgd/target-config`.
131
131
  Measure the game container first, then fall back to `visualViewport` or
132
132
  `window.innerWidth`:
133
133
 
134
134
  ```ts
135
+ import { resolveTargetViewportSnapshot } from '@mpgd/target-config';
136
+
135
137
  const measured = measureGameViewport();
136
- const viewport = resolveTargetViewportPlan({
138
+ const viewport = resolveTargetViewportSnapshot({
137
139
  width: measured.width,
138
140
  height: measured.height,
139
141
  source: measured.source,
@@ -146,7 +148,17 @@ const viewport = resolveTargetViewportPlan({
146
148
 
147
149
  Use this as a starting point for game-specific layout. The returned
148
150
  recommendation is not a hard target rule; override it when your playfield or
149
- input model needs a different UI arrangement.
151
+ input model needs a different UI arrangement. The snapshot also includes
152
+ `safeArea.contentBounds` for a full-bleed game that places its own persistent
153
+ HUD.
154
+
155
+ The generated HTML uses `viewport-fit=cover`, and the shared stylesheet exposes
156
+ the browser values as `--mpgd-safe-area-*`. The starter's `#game` mount is
157
+ already inside that CSS padding, so its default snapshot has zero local insets.
158
+ If a game instead measures the full browser viewport, pass
159
+ `readTargetViewportSafeAreaInsets(getComputedStyle(document.documentElement))`
160
+ to `resolveTargetViewportSnapshot`. Do not reserve the same inset in both the
161
+ outer CSS and the Phaser layout; that would shrink content twice.
150
162
 
151
163
  - `compact` is `<= 599px`: phone and narrow Devvit/card layouts. Prefer bottom
152
164
  controls and drawers.
@@ -60,3 +60,6 @@ game server or replace game-owned production evidence.
60
60
 
61
61
  Verify the first screen reports the viewport orientation policy, and treat
62
62
  locked orientation modes as soft prompts instead of unsafe WebView hard locks.
63
+ Verify safe-area geometry is reserved exactly once: CSS-padded containers must
64
+ not also subtract `safeArea.contentBounds`, while full-bleed game surfaces use
65
+ the snapshot bounds for persistent controls.
@@ -15,6 +15,8 @@ Boundaries:
15
15
  - Platform SDK imports belong in adapters or target wrappers.
16
16
  - Orientation policy should be chosen before adding resize behavior; treat
17
17
  locked modes as soft prompts unless a platform adapter supports hard locks.
18
+ - Safe-area geometry must be reserved exactly once: use outer CSS safe-area
19
+ padding or the viewport snapshot `safeArea.contentBounds`, never both.
18
20
 
19
21
  Useful commands:
20
22
 
@@ -40,6 +40,10 @@
40
40
  "id": "runtime.viewport.orientation-policy",
41
41
  "owner": "src/main.ts"
42
42
  },
43
+ {
44
+ "id": "runtime.viewport.safe-area-contract",
45
+ "owner": "src/main.ts"
46
+ },
43
47
  {
44
48
  "id": "platform.devvit.app-root",
45
49
  "owner": "apps/target-devvit"
@@ -4,7 +4,7 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta
6
6
  name="viewport"
7
- content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"
7
+ content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover"
8
8
  />
9
9
  <title>__GAME_TITLE__</title>
10
10
  </head>
@@ -2,7 +2,10 @@
2
2
  <html lang="en">
3
3
  <head>
4
4
  <meta charset="UTF-8" />
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
5
+ <meta
6
+ name="viewport"
7
+ content="width=device-width, initial-scale=1.0, viewport-fit=cover"
8
+ />
6
9
  <meta name="theme-color" content="#0f172a" />
7
10
  <title>__GAME_TITLE__</title>
8
11
  </head>
@@ -2,7 +2,10 @@
2
2
  <html lang="en">
3
3
  <head>
4
4
  <meta charset="UTF-8" />
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
5
+ <meta
6
+ name="viewport"
7
+ content="width=device-width, initial-scale=1.0, viewport-fit=cover"
8
+ />
6
9
  <meta name="theme-color" content="#0f172a" />
7
10
  <title>__GAME_TITLE__</title>
8
11
  </head>
@@ -2,7 +2,10 @@
2
2
  <html lang="en">
3
3
  <head>
4
4
  <meta charset="UTF-8" />
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
5
+ <meta
6
+ name="viewport"
7
+ content="width=device-width, initial-scale=1.0, viewport-fit=cover"
8
+ />
6
9
  <meta name="theme-color" content="#0f172a" />
7
10
  <title>__GAME_TITLE__</title>
8
11
  </head>
@@ -2,7 +2,10 @@
2
2
  <html lang="en">
3
3
  <head>
4
4
  <meta charset="UTF-8" />
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
5
+ <meta
6
+ name="viewport"
7
+ content="width=device-width, initial-scale=1.0, viewport-fit=cover"
8
+ />
6
9
  <meta name="theme-color" content="#0f172a" />
7
10
  <link rel="manifest" href="./manifest.webmanifest" />
8
11
  <title>__GAME_TITLE__</title>
@@ -4,7 +4,7 @@ import { createAnalyticsReporter, createBufferedAnalyticsSink } from '@mpgd/anal
4
4
  import { resolveTargetMpgdLocale, type Locale } from '@mpgd/i18n';
5
5
  import type { IdentitySession, LaunchIntent, PlatformGateway } from '@mpgd/platform';
6
6
  import {
7
- resolveTargetViewportPlan,
7
+ resolveTargetViewportSnapshot,
8
8
  type TargetViewportOrientationPolicy,
9
9
  } from '@mpgd/target-config';
10
10
 
@@ -18,6 +18,7 @@ import { installMicrosoftStorePwa } from './platform/microsoftStorePwa';
18
18
 
19
19
  await bootstrap();
20
20
 
21
+ /** Install target services, analytics, and viewport state before starting the game. */
21
22
  async function bootstrap(): Promise<void> {
22
23
  let locale: Locale = 'en';
23
24
  let disposeMicrosoftStorePwa: (() => void) | undefined;
@@ -35,7 +36,7 @@ async function bootstrap(): Promise<void> {
35
36
  const orientationPolicy = {
36
37
  mode: 'prefer-landscape',
37
38
  } as const satisfies TargetViewportOrientationPolicy;
38
- const viewport = resolveTargetViewportPlan({
39
+ const viewport = resolveTargetViewportSnapshot({
39
40
  ...measureGameViewport(),
40
41
  runtime: runtime.config.runtime,
41
42
  orientationPolicy,
@@ -56,14 +57,17 @@ async function bootstrap(): Promise<void> {
56
57
  ?? runtime.config.localization.fallbackLocale,
57
58
  });
58
59
  const analyticsSink = createBufferedAnalyticsSink();
60
+ const analyticsSessionId = createClientId('session');
59
61
  const analytics = createAnalyticsReporter({
60
62
  target: platform.target,
61
- sessionId: createClientId('session'),
63
+ sessionId: analyticsSessionId,
62
64
  sink: analyticsSink,
63
65
  });
64
66
  const gameServices = createStarterGameServices({
65
67
  gateway: platform,
66
68
  playerId: identitySession.playerId ?? player.playerId,
69
+ analytics: analyticsSink,
70
+ analyticsSessionId,
67
71
  });
68
72
 
69
73
  await analytics.track({
@@ -98,6 +102,7 @@ async function bootstrap(): Promise<void> {
98
102
  }
99
103
  }
100
104
 
105
+ /** Resolve a verified platform session or fall back to a local guest. */
101
106
  async function resolveIdentitySession(
102
107
  platform: PlatformGateway,
103
108
  fallbackPlayerId: string,
@@ -110,6 +115,7 @@ async function resolveIdentitySession(
110
115
  }
111
116
  }
112
117
 
118
+ /** Resolve the host launch intent while preserving a home-entry fallback. */
113
119
  async function resolveLaunchIntent(platform: PlatformGateway): Promise<LaunchIntent> {
114
120
  try {
115
121
  return (await platform.presentation?.getLaunchIntent()) ?? { entry: 'home' };
@@ -119,6 +125,7 @@ async function resolveLaunchIntent(platform: PlatformGateway): Promise<LaunchInt
119
125
  }
120
126
  }
121
127
 
128
+ /** Create the minimal local identity used when a host session is unavailable. */
122
129
  function createGuestSession(playerId: string): IdentitySession {
123
130
  return {
124
131
  identityLevel: 'guest',
@@ -127,6 +134,7 @@ function createGuestSession(playerId: string): IdentitySession {
127
134
  };
128
135
  }
129
136
 
137
+ /** Render a localized bootstrap failure inside the game mount. */
130
138
  function renderBootstrapError(error: unknown, locale: Locale): void {
131
139
  const message = error instanceof Error ? error.message : String(error);
132
140
  const root = document.querySelector<HTMLDivElement>('#game');
@@ -142,6 +150,7 @@ function renderBootstrapError(error: unknown, locale: Locale): void {
142
150
  root.append(panel);
143
151
  }
144
152
 
153
+ /** Measure the game surface before falling back to browser viewport geometry. */
145
154
  function measureGameViewport(): {
146
155
  readonly width: number;
147
156
  readonly height: number;
@@ -1,3 +1,4 @@
1
+ import type { AnalyticsSink } from '@mpgd/analytics';
1
2
  import {
2
3
  createGameServicesRuntime,
3
4
  resolveGameServicesAuthorityMode,
@@ -13,10 +14,16 @@ export type StarterGameServices = GameServicesRuntime;
13
14
  export function createStarterGameServices(input: {
14
15
  readonly gateway: PlatformGateway;
15
16
  readonly playerId: string;
17
+ readonly analytics?: AnalyticsSink;
18
+ readonly analyticsSessionId?: string;
16
19
  }): StarterGameServices {
17
20
  return createGameServicesRuntime({
18
21
  gateway: input.gateway,
19
22
  playerId: input.playerId,
23
+ ...(input.analytics === undefined ? {} : { analytics: input.analytics }),
24
+ ...(input.analyticsSessionId === undefined
25
+ ? {}
26
+ : { analyticsSessionId: input.analyticsSessionId }),
20
27
  authorityMode: resolveGameServicesAuthorityMode(import.meta.env.MODE),
21
28
  target: import.meta.env.VITE_MPGD_GAME_SERVICES_TARGET ?? input.gateway.target,
22
29
  ...(import.meta.env.VITE_MPGD_GAME_SERVICES_URL === undefined
@@ -4,7 +4,7 @@ import type { IdentitySession, LaunchIntent, PlayerIdentity } from '@mpgd/platfo
4
4
  import type {
5
5
  TargetConfiguredGateway,
6
6
  TargetRuntimeSnapshot,
7
- TargetViewportPlan,
7
+ TargetViewportSnapshot,
8
8
  } from '@mpgd/target-config';
9
9
 
10
10
  import type { StarterGameServices } from '../platform/gameServices';
@@ -14,7 +14,7 @@ export const starterContextKey = 'starterContext';
14
14
  export interface StarterContext {
15
15
  readonly platform: TargetConfiguredGateway;
16
16
  readonly runtime: TargetRuntimeSnapshot;
17
- readonly viewport: TargetViewportPlan;
17
+ readonly viewport: TargetViewportSnapshot;
18
18
  readonly player: PlayerIdentity;
19
19
  readonly identitySession: IdentitySession;
20
20
  readonly launchIntent: LaunchIntent;
@@ -77,7 +77,7 @@ export class PlayScene extends Phaser.Scene {
77
77
  private rewardHint(): string {
78
78
  const rewardedAds = this.context.runtime.features.rewardedAds;
79
79
 
80
- if (!rewardedAds.enabled) {
80
+ if (!rewardedAds.enabled || this.context.gameServices.client === undefined) {
81
81
  return t(this.context.locale, 'rewardUnavailable');
82
82
  }
83
83
 
@@ -86,27 +86,24 @@ export class PlayScene extends Phaser.Scene {
86
86
 
87
87
  private async requestRewardedAd(): Promise<void> {
88
88
  const rewardedAds = this.context.runtime.features.rewardedAds;
89
+ const gameServicesClient = this.context.gameServices.client;
89
90
 
90
- if (!rewardedAds.enabled) {
91
+ if (!rewardedAds.enabled || gameServicesClient === undefined) {
91
92
  this.rewardText.setText(t(this.context.locale, 'rewardUnavailable'));
92
93
  return;
93
94
  }
94
95
 
95
96
  try {
96
- const result = await this.context.platform.ads.showRewarded({
97
+ const result = await gameServicesClient.claimRewardedAd({
97
98
  placementId: rewardedPlacementId,
98
99
  idempotencyKey: createClientId('starter-reward'),
99
100
  });
100
101
 
101
- await this.context.analytics.track({
102
- name: result.status === 'completed' ? 'rewarded_ad_completed' : 'rewarded_ad_rejected',
103
- properties: {
104
- status: result.status,
105
- granted: result.rewardGranted,
106
- },
107
- });
108
-
109
- this.rewardText.setText(t(this.context.locale, 'reward', { status: result.status }));
102
+ this.rewardText.setText(
103
+ result.claim?.granted === true
104
+ ? t(this.context.locale, 'reward', { status: result.status })
105
+ : t(this.context.locale, 'rewardError'),
106
+ );
110
107
  } catch (error) {
111
108
  await this.context.analytics.track({
112
109
  name: 'rewarded_ad_rejected',
@@ -1,3 +1,10 @@
1
+ :root {
2
+ --mpgd-safe-area-top: env(safe-area-inset-top, 0px);
3
+ --mpgd-safe-area-right: env(safe-area-inset-right, 0px);
4
+ --mpgd-safe-area-bottom: env(safe-area-inset-bottom, 0px);
5
+ --mpgd-safe-area-left: env(safe-area-inset-left, 0px);
6
+ }
7
+
1
8
  html,
2
9
  body {
3
10
  width: 100%;
@@ -13,10 +20,10 @@ body {
13
20
  min-height: 100dvh;
14
21
  overflow: hidden;
15
22
  padding:
16
- env(safe-area-inset-top)
17
- env(safe-area-inset-right)
18
- env(safe-area-inset-bottom)
19
- env(safe-area-inset-left);
23
+ var(--mpgd-safe-area-top)
24
+ var(--mpgd-safe-area-right)
25
+ var(--mpgd-safe-area-bottom)
26
+ var(--mpgd-safe-area-left);
20
27
  background: #07111f;
21
28
  color: #ffffff;
22
29
  font-family: Arial, sans-serif;
@@ -24,7 +31,7 @@ body {
24
31
 
25
32
  #game {
26
33
  --mpgd-game-max-height: calc(
27
- 100dvh - env(safe-area-inset-top, 0px) - env(safe-area-inset-bottom, 0px)
34
+ 100dvh - var(--mpgd-safe-area-top) - var(--mpgd-safe-area-bottom)
28
35
  );
29
36
 
30
37
  box-sizing: border-box;