@mpgd/cli 0.4.0 → 0.5.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.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Gunshi CLI for mpgd-kit starter generation and target workflow orchestration.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -51,15 +51,15 @@
51
51
  "typescript": "7.0.1-rc",
52
52
  "@mpgd/adapter-ait": "0.3.3",
53
53
  "@mpgd/adapter-browser": "0.3.2",
54
+ "@mpgd/adapter-capacitor": "0.3.3",
54
55
  "@mpgd/adapter-devvit": "0.3.3",
55
56
  "@mpgd/analytics": "0.3.2",
56
- "@mpgd/adapter-capacitor": "0.3.3",
57
57
  "@mpgd/bridge": "0.4.0",
58
- "@mpgd/game-services": "0.3.3",
59
58
  "@mpgd/catalog": "0.3.2",
60
- "@mpgd/i18n": "0.3.2",
59
+ "@mpgd/game-services": "0.3.3",
61
60
  "@mpgd/phaser-assets": "0.4.0",
62
- "@mpgd/target-config": "0.3.3",
61
+ "@mpgd/i18n": "0.4.0",
62
+ "@mpgd/target-config": "0.4.0",
63
63
  "@mpgd/platform": "0.3.2"
64
64
  },
65
65
  "main": "./dist/index.js",
@@ -71,6 +71,51 @@ emitted URLs into the manifest with `?url` imports, or keep static
71
71
  starter keeps Vite `base: './'` so generated asset URLs stay portable across web
72
72
  preview, Apps in Toss, Devvit, Android, and iOS bundles.
73
73
 
74
+ ## Viewport And Controls
75
+
76
+ The starter computes an initial viewport plan with `@mpgd/target-config`.
77
+ Measure the game container first, then fall back to `visualViewport` or
78
+ `window.innerWidth`:
79
+
80
+ ```ts
81
+ const measured = measureGameViewport();
82
+ const viewport = resolveTargetViewportPlan({
83
+ width: measured.width,
84
+ height: measured.height,
85
+ source: measured.source,
86
+ runtime: runtime.config.runtime,
87
+ orientationPolicy: {
88
+ mode: 'prefer-landscape',
89
+ },
90
+ });
91
+ ```
92
+
93
+ Use this as a starting point for game-specific layout. The returned
94
+ recommendation is not a hard target rule; override it when your playfield or
95
+ input model needs a different UI arrangement.
96
+
97
+ - `compact` is `<= 599px`: phone and narrow Devvit/card layouts. Prefer bottom
98
+ controls and drawers.
99
+ - `medium` is `600px` through `899px`: larger phones, small tablets, and
100
+ moderate embeds. Keep the play surface centered and only add side controls
101
+ when touch targets remain comfortable.
102
+ - `expanded` is `>= 900px`: desktop-like layouts. Side controls and panels are
103
+ acceptable.
104
+
105
+ Orientation is measured from the container: height greater than width is
106
+ `portrait`; width greater than or equal to height is `landscape`. Devvit should
107
+ be treated as an embedded webview, so do not assume desktop Reddit always means
108
+ an expanded game surface.
109
+
110
+ Orientation policy is a runtime contract, not a guarantee that every shell can
111
+ hard-lock the device. Use `responsive` when both orientations are playable,
112
+ `prefer-landscape` or `prefer-portrait` when layout guidance should favor one
113
+ orientation, and `lock-landscape` or `lock-portrait` when the game should show a
114
+ soft rotate prompt on mismatch. The generated PWA manifest also declares
115
+ `"orientation": "landscape"` for installed web and Microsoft Store shells; keep
116
+ that manifest value aligned with the runtime policy if you make a portrait-first
117
+ game.
118
+
74
119
  ## Reddit Devvit
75
120
 
76
121
  This starter owns its Devvit app root in `apps/target-devvit`. That directory
@@ -21,3 +21,6 @@ after login before upload or playtest.
21
21
  For Microsoft Store changes, keep the first pass as a PWA/web target that uses
22
22
  the browser adapter. Add a dedicated Store commerce adapter only when wiring
23
23
  Digital Goods API and Payment Request through backend ledger verification.
24
+
25
+ Verify the first screen reports the viewport orientation policy, and treat
26
+ locked orientation modes as soft prompts instead of unsafe WebView hard locks.
@@ -9,6 +9,8 @@ Boundaries:
9
9
  - Purchases, rewarded ad grants, and leaderboard records should go through
10
10
  backend ledger APIs before mutating durable game save state.
11
11
  - Platform SDK imports belong in adapters or target wrappers.
12
+ - Orientation policy should be chosen before adding resize behavior; treat
13
+ locked modes as soft prompts unless a platform adapter supports hard locks.
12
14
 
13
15
  Useful commands:
14
16
 
@@ -25,6 +25,10 @@
25
25
  "id": "runtime.platform.gateway",
26
26
  "owner": "src/platform/installPlatform.ts"
27
27
  },
28
+ {
29
+ "id": "runtime.viewport.orientation-policy",
30
+ "owner": "src/main.ts"
31
+ },
28
32
  {
29
33
  "id": "platform.devvit.app-root",
30
34
  "owner": "apps/target-devvit"
@@ -3,6 +3,9 @@ import type { Locale } from '@mpgd/i18n';
3
3
  export type GameMessageKey =
4
4
  | 'title'
5
5
  | 'target'
6
+ | 'viewport'
7
+ | 'orientationPolicy'
8
+ | 'orientationMismatch'
6
9
  | 'backend'
7
10
  | 'features'
8
11
  | 'featuresNone'
@@ -19,6 +22,9 @@ const messages = {
19
22
  en: {
20
23
  title: '__GAME_TITLE__',
21
24
  target: 'Target: {target}',
25
+ viewport: 'Viewport: {sizeClass} {orientation} - controls {controls}',
26
+ orientationPolicy: 'Orientation policy: {mode}',
27
+ orientationMismatch: 'Rotate to {orientation} for {mode}',
22
28
  backend: 'Game Services: {mode}',
23
29
  features: 'Features: {features}',
24
30
  featuresNone: 'none',
@@ -34,6 +40,9 @@ const messages = {
34
40
  ko: {
35
41
  title: '__GAME_TITLE__',
36
42
  target: '타깃: {target}',
43
+ viewport: '뷰포트: {sizeClass} {orientation} - 컨트롤 {controls}',
44
+ orientationPolicy: '방향 정책: {mode}',
45
+ orientationMismatch: '{mode}: {orientation} 방향으로 회전',
37
46
  backend: 'Game Services: {mode}',
38
47
  features: '기능: {features}',
39
48
  featuresNone: '없음',
@@ -2,6 +2,10 @@ import './styles.css';
2
2
 
3
3
  import { createAnalyticsReporter, createBufferedAnalyticsSink } from '@mpgd/analytics';
4
4
  import { resolveMpgdLocale, type Locale } from '@mpgd/i18n';
5
+ import {
6
+ resolveTargetViewportPlan,
7
+ type TargetViewportOrientationPolicy,
8
+ } from '@mpgd/target-config';
5
9
 
6
10
  import { t } from './i18n/messages';
7
11
  import { createClientId } from './runtime/id';
@@ -24,6 +28,14 @@ async function bootstrap(): Promise<void> {
24
28
  const runtimeConfig = detectRuntime();
25
29
  const platform = await installPlatform(runtimeConfig);
26
30
  const runtime = await platform.getTargetRuntime();
31
+ const orientationPolicy = {
32
+ mode: 'prefer-landscape',
33
+ } as const satisfies TargetViewportOrientationPolicy;
34
+ const viewport = resolveTargetViewportPlan({
35
+ ...measureGameViewport(),
36
+ runtime: runtime.config.runtime,
37
+ orientationPolicy,
38
+ });
27
39
  const player =
28
40
  (await platform.identity.getPlayer()) ?? {
29
41
  playerId: 'local-player',
@@ -54,6 +66,7 @@ async function bootstrap(): Promise<void> {
54
66
  context: {
55
67
  platform,
56
68
  runtime,
69
+ viewport,
57
70
  player,
58
71
  locale,
59
72
  gameServices,
@@ -81,3 +94,41 @@ function renderBootstrapError(error: unknown, locale: Locale): void {
81
94
  panel.textContent = `${t(locale, 'bootError')}: ${message}`;
82
95
  root.append(panel);
83
96
  }
97
+
98
+ function measureGameViewport(): {
99
+ readonly width: number;
100
+ readonly height: number;
101
+ readonly source: 'container' | 'visual-viewport' | 'window';
102
+ } {
103
+ const container = document.querySelector<HTMLElement>('#game');
104
+ const rect = container?.getBoundingClientRect();
105
+
106
+ if (rect !== undefined && rect.width > 0 && rect.height > 0) {
107
+ return {
108
+ width: rect.width,
109
+ height: rect.height,
110
+ source: 'container',
111
+ };
112
+ }
113
+
114
+ const visualViewport = window.visualViewport;
115
+
116
+ if (
117
+ visualViewport !== undefined &&
118
+ visualViewport !== null &&
119
+ visualViewport.width > 0 &&
120
+ visualViewport.height > 0
121
+ ) {
122
+ return {
123
+ width: visualViewport.width,
124
+ height: visualViewport.height,
125
+ source: 'visual-viewport',
126
+ };
127
+ }
128
+
129
+ return {
130
+ width: window.innerWidth,
131
+ height: window.innerHeight,
132
+ source: 'window',
133
+ };
134
+ }
@@ -1,7 +1,11 @@
1
1
  import type { AnalyticsReporter, BufferedAnalyticsSink } from '@mpgd/analytics';
2
2
  import type { Locale } from '@mpgd/i18n';
3
3
  import type { PlayerIdentity } from '@mpgd/platform';
4
- import type { TargetConfiguredGateway, TargetRuntimeSnapshot } from '@mpgd/target-config';
4
+ import type {
5
+ TargetConfiguredGateway,
6
+ TargetRuntimeSnapshot,
7
+ TargetViewportPlan,
8
+ } from '@mpgd/target-config';
5
9
 
6
10
  import type { StarterGameServices } from '../platform/gameServices';
7
11
 
@@ -10,6 +14,7 @@ export const starterContextKey = 'starterContext';
10
14
  export interface StarterContext {
11
15
  readonly platform: TargetConfiguredGateway;
12
16
  readonly runtime: TargetRuntimeSnapshot;
17
+ readonly viewport: TargetViewportPlan;
13
18
  readonly player: PlayerIdentity;
14
19
  readonly locale: Locale;
15
20
  readonly gameServices: StarterGameServices;
@@ -21,6 +21,16 @@ export class LobbyScene extends Phaser.Scene {
21
21
  .filter((feature) => feature.enabled)
22
22
  .map((feature) => feature.feature)
23
23
  .join(', ') || t(locale, 'featuresNone');
24
+ const orientation = context.viewport.orientation;
25
+ const orientationText =
26
+ orientation.shouldShowRotatePrompt && orientation.preferredOrientation !== undefined
27
+ ? t(locale, 'orientationMismatch', {
28
+ mode: orientation.mode,
29
+ orientation: orientation.preferredOrientation,
30
+ })
31
+ : t(locale, 'orientationPolicy', {
32
+ mode: orientation.mode,
33
+ });
24
34
 
25
35
  this.add
26
36
  .text(480, 106, t(locale, 'title'), {
@@ -45,14 +55,37 @@ export class LobbyScene extends Phaser.Scene {
45
55
  })
46
56
  .setOrigin(0.5);
47
57
  this.add
48
- .text(480, 258, t(locale, 'backend', { mode: context.gameServices.mode }), {
58
+ .text(
59
+ 480,
60
+ 258,
61
+ t(locale, 'viewport', {
62
+ sizeClass: context.viewport.layout.sizeClass,
63
+ orientation: context.viewport.layout.orientation,
64
+ controls: context.viewport.recommendation.primaryControls,
65
+ }),
66
+ {
67
+ color: '#d6dee8',
68
+ fontFamily: 'Arial, sans-serif',
69
+ fontSize: '18px',
70
+ },
71
+ )
72
+ .setOrigin(0.5);
73
+ this.add
74
+ .text(480, 300, orientationText, {
75
+ color: orientation.shouldShowRotatePrompt ? '#f59e0b' : '#d6dee8',
76
+ fontFamily: 'Arial, sans-serif',
77
+ fontSize: '18px',
78
+ })
79
+ .setOrigin(0.5);
80
+ this.add
81
+ .text(480, 342, t(locale, 'backend', { mode: context.gameServices.mode }), {
49
82
  color: context.gameServices.mode === 'disabled' ? '#f59e0b' : '#2dd4bf',
50
83
  fontFamily: 'Arial, sans-serif',
51
84
  fontSize: '18px',
52
85
  })
53
86
  .setOrigin(0.5);
54
87
  this.add
55
- .text(480, 336, t(locale, 'tapToStart'), {
88
+ .text(480, 384, t(locale, 'tapToStart'), {
56
89
  color: '#ffffff',
57
90
  fontFamily: 'Arial, sans-serif',
58
91
  fontSize: '22px',
@@ -1,20 +1,43 @@
1
1
  html,
2
- body,
3
- #game {
2
+ body {
4
3
  width: 100%;
5
4
  height: 100%;
6
5
  margin: 0;
7
6
  }
8
7
 
9
8
  body {
9
+ display: grid;
10
+ place-items: center;
11
+ box-sizing: border-box;
12
+ min-width: 0;
13
+ min-height: 100dvh;
10
14
  overflow: hidden;
15
+ 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);
11
20
  background: #07111f;
12
21
  color: #ffffff;
13
22
  font-family: Arial, sans-serif;
14
23
  }
15
24
 
25
+ #game {
26
+ --mpgd-game-max-height: calc(
27
+ 100dvh - env(safe-area-inset-top, 0px) - env(safe-area-inset-bottom, 0px)
28
+ );
29
+
30
+ box-sizing: border-box;
31
+ width: min(100%, 960px, calc(var(--mpgd-game-max-height) * 16 / 9));
32
+ aspect-ratio: 16 / 9;
33
+ min-width: 0;
34
+ min-height: 0;
35
+ touch-action: none;
36
+ }
37
+
16
38
  canvas {
17
39
  display: block;
40
+ touch-action: none;
18
41
  }
19
42
 
20
43
  .boot-error {