@mpgd/cli 0.4.0 → 0.4.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mpgd/cli",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "Gunshi CLI for mpgd-kit starter generation and target workflow orchestration.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -51,16 +51,16 @@
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-devvit": "0.3.3",
55
- "@mpgd/analytics": "0.3.2",
56
54
  "@mpgd/adapter-capacitor": "0.3.3",
55
+ "@mpgd/analytics": "0.3.2",
56
+ "@mpgd/adapter-devvit": "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",
61
59
  "@mpgd/phaser-assets": "0.4.0",
62
- "@mpgd/target-config": "0.3.3",
63
- "@mpgd/platform": "0.3.2"
60
+ "@mpgd/platform": "0.3.2",
61
+ "@mpgd/target-config": "0.3.4",
62
+ "@mpgd/i18n": "0.3.3",
63
+ "@mpgd/game-services": "0.3.3"
64
64
  },
65
65
  "main": "./dist/index.js",
66
66
  "types": "./dist/index.d.ts",
@@ -71,6 +71,39 @@ 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
+ });
88
+ ```
89
+
90
+ Use this as a starting point for game-specific layout. The returned
91
+ recommendation is not a hard target rule; override it when your playfield or
92
+ input model needs a different UI arrangement.
93
+
94
+ - `compact` is `<= 599px`: phone and narrow Devvit/card layouts. Prefer bottom
95
+ controls and drawers.
96
+ - `medium` is `600px` through `899px`: larger phones, small tablets, and
97
+ moderate embeds. Keep the play surface centered and only add side controls
98
+ when touch targets remain comfortable.
99
+ - `expanded` is `>= 900px`: desktop-like layouts. Side controls and panels are
100
+ acceptable.
101
+
102
+ Orientation is measured from the container: height greater than width is
103
+ `portrait`; width greater than or equal to height is `landscape`. Devvit should
104
+ be treated as an embedded webview, so do not assume desktop Reddit always means
105
+ an expanded game surface.
106
+
74
107
  ## Reddit Devvit
75
108
 
76
109
  This starter owns its Devvit app root in `apps/target-devvit`. That directory
@@ -3,6 +3,7 @@ import type { Locale } from '@mpgd/i18n';
3
3
  export type GameMessageKey =
4
4
  | 'title'
5
5
  | 'target'
6
+ | 'viewport'
6
7
  | 'backend'
7
8
  | 'features'
8
9
  | 'featuresNone'
@@ -19,6 +20,7 @@ const messages = {
19
20
  en: {
20
21
  title: '__GAME_TITLE__',
21
22
  target: 'Target: {target}',
23
+ viewport: 'Viewport: {sizeClass} {orientation} - controls {controls}',
22
24
  backend: 'Game Services: {mode}',
23
25
  features: 'Features: {features}',
24
26
  featuresNone: 'none',
@@ -34,6 +36,7 @@ const messages = {
34
36
  ko: {
35
37
  title: '__GAME_TITLE__',
36
38
  target: '타깃: {target}',
39
+ viewport: '뷰포트: {sizeClass} {orientation} - 컨트롤 {controls}',
37
40
  backend: 'Game Services: {mode}',
38
41
  features: '기능: {features}',
39
42
  featuresNone: '없음',
@@ -2,6 +2,7 @@ import './styles.css';
2
2
 
3
3
  import { createAnalyticsReporter, createBufferedAnalyticsSink } from '@mpgd/analytics';
4
4
  import { resolveMpgdLocale, type Locale } from '@mpgd/i18n';
5
+ import { resolveTargetViewportPlan } from '@mpgd/target-config';
5
6
 
6
7
  import { t } from './i18n/messages';
7
8
  import { createClientId } from './runtime/id';
@@ -24,6 +25,10 @@ async function bootstrap(): Promise<void> {
24
25
  const runtimeConfig = detectRuntime();
25
26
  const platform = await installPlatform(runtimeConfig);
26
27
  const runtime = await platform.getTargetRuntime();
28
+ const viewport = resolveTargetViewportPlan({
29
+ ...measureGameViewport(),
30
+ runtime: runtime.config.runtime,
31
+ });
27
32
  const player =
28
33
  (await platform.identity.getPlayer()) ?? {
29
34
  playerId: 'local-player',
@@ -54,6 +59,7 @@ async function bootstrap(): Promise<void> {
54
59
  context: {
55
60
  platform,
56
61
  runtime,
62
+ viewport,
57
63
  player,
58
64
  locale,
59
65
  gameServices,
@@ -81,3 +87,41 @@ function renderBootstrapError(error: unknown, locale: Locale): void {
81
87
  panel.textContent = `${t(locale, 'bootError')}: ${message}`;
82
88
  root.append(panel);
83
89
  }
90
+
91
+ function measureGameViewport(): {
92
+ readonly width: number;
93
+ readonly height: number;
94
+ readonly source: 'container' | 'visual-viewport' | 'window';
95
+ } {
96
+ const container = document.querySelector<HTMLElement>('#game');
97
+ const rect = container?.getBoundingClientRect();
98
+
99
+ if (rect !== undefined && rect.width > 0 && rect.height > 0) {
100
+ return {
101
+ width: rect.width,
102
+ height: rect.height,
103
+ source: 'container',
104
+ };
105
+ }
106
+
107
+ const visualViewport = window.visualViewport;
108
+
109
+ if (
110
+ visualViewport !== undefined &&
111
+ visualViewport !== null &&
112
+ visualViewport.width > 0 &&
113
+ visualViewport.height > 0
114
+ ) {
115
+ return {
116
+ width: visualViewport.width,
117
+ height: visualViewport.height,
118
+ source: 'visual-viewport',
119
+ };
120
+ }
121
+
122
+ return {
123
+ width: window.innerWidth,
124
+ height: window.innerHeight,
125
+ source: 'window',
126
+ };
127
+ }
@@ -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;
@@ -45,14 +45,30 @@ export class LobbyScene extends Phaser.Scene {
45
45
  })
46
46
  .setOrigin(0.5);
47
47
  this.add
48
- .text(480, 258, t(locale, 'backend', { mode: context.gameServices.mode }), {
48
+ .text(
49
+ 480,
50
+ 258,
51
+ t(locale, 'viewport', {
52
+ sizeClass: context.viewport.layout.sizeClass,
53
+ orientation: context.viewport.layout.orientation,
54
+ controls: context.viewport.recommendation.primaryControls,
55
+ }),
56
+ {
57
+ color: '#d6dee8',
58
+ fontFamily: 'Arial, sans-serif',
59
+ fontSize: '18px',
60
+ },
61
+ )
62
+ .setOrigin(0.5);
63
+ this.add
64
+ .text(480, 300, t(locale, 'backend', { mode: context.gameServices.mode }), {
49
65
  color: context.gameServices.mode === 'disabled' ? '#f59e0b' : '#2dd4bf',
50
66
  fontFamily: 'Arial, sans-serif',
51
67
  fontSize: '18px',
52
68
  })
53
69
  .setOrigin(0.5);
54
70
  this.add
55
- .text(480, 336, t(locale, 'tapToStart'), {
71
+ .text(480, 342, t(locale, 'tapToStart'), {
56
72
  color: '#ffffff',
57
73
  fontFamily: 'Arial, sans-serif',
58
74
  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 {