@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 +5 -5
- package/templates/phaser-game/README.md +45 -0
- package/templates/phaser-game/agent/acceptance.md +3 -0
- package/templates/phaser-game/agent/brief.md +2 -0
- package/templates/phaser-game/agent/game-manifest.json +4 -0
- package/templates/phaser-game/src/i18n/messages.ts +9 -0
- package/templates/phaser-game/src/main.ts +51 -0
- package/templates/phaser-game/src/runtime/gameContext.ts +6 -1
- package/templates/phaser-game/src/scenes/LobbyScene.ts +35 -2
- package/templates/phaser-game/src/styles.css +25 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mpgd/cli",
|
|
3
|
-
"version": "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/
|
|
59
|
+
"@mpgd/game-services": "0.3.3",
|
|
61
60
|
"@mpgd/phaser-assets": "0.4.0",
|
|
62
|
-
"@mpgd/
|
|
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
|
|
|
@@ -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 {
|
|
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(
|
|
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,
|
|
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 {
|