@mpgd/cli 0.18.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.18.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": {
@@ -50,19 +50,19 @@
50
50
  "@types/node": "^24.0.0",
51
51
  "ttsc": "0.18.4",
52
52
  "typescript": "7.0.2",
53
- "@mpgd/adapter-capacitor": "0.4.5",
54
53
  "@mpgd/adapter-ait": "0.8.0",
54
+ "@mpgd/adapter-browser": "0.5.1",
55
55
  "@mpgd/adapter-devvit": "0.8.5",
56
56
  "@mpgd/adapter-verse8": "0.2.2",
57
- "@mpgd/adapter-browser": "0.5.1",
58
- "@mpgd/analytics": "0.3.8",
57
+ "@mpgd/adapter-capacitor": "0.4.5",
59
58
  "@mpgd/bridge": "0.7.0",
60
- "@mpgd/catalog": "0.5.1",
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",
@@ -84,6 +84,10 @@ blocks localhost plus literal private or reserved IP addresses.
84
84
  this game directory, including non-production builds that use kit reference
85
85
  Capacitor shells.
86
86
 
87
+ For Sampo private-workspace releases, Devvit RC/final separation, and native
88
+ store-version provenance, see the
89
+ [multi-target release versioning guide](https://github.com/imjlk/mpgd-kit/blob/main/docs/MULTI_TARGET_RELEASE_VERSIONING.md).
90
+
87
91
  Optional targets can add their own config, scripts, runtime wiring, and release
88
92
  skill. To enable Microsoft Store after creation, run
89
93
  `pnpm exec mpgd target init microsoft-store --game . --kit-path ../mpgd-kit`.
@@ -123,13 +127,15 @@ preview, Apps in Toss, Devvit, Android, and iOS bundles.
123
127
 
124
128
  ## Viewport And Controls
125
129
 
126
- The starter computes an initial viewport plan with `@mpgd/target-config`.
130
+ The starter computes an initial viewport snapshot with `@mpgd/target-config`.
127
131
  Measure the game container first, then fall back to `visualViewport` or
128
132
  `window.innerWidth`:
129
133
 
130
134
  ```ts
135
+ import { resolveTargetViewportSnapshot } from '@mpgd/target-config';
136
+
131
137
  const measured = measureGameViewport();
132
- const viewport = resolveTargetViewportPlan({
138
+ const viewport = resolveTargetViewportSnapshot({
133
139
  width: measured.width,
134
140
  height: measured.height,
135
141
  source: measured.source,
@@ -142,7 +148,17 @@ const viewport = resolveTargetViewportPlan({
142
148
 
143
149
  Use this as a starting point for game-specific layout. The returned
144
150
  recommendation is not a hard target rule; override it when your playfield or
145
- 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.
146
162
 
147
163
  - `compact` is `<= 599px`: phone and narrow Devvit/card layouts. Prefer bottom
148
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;