@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 +7 -7
- package/templates/phaser-game/README.md +19 -3
- 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/apps/target-ait/index.html +1 -1
- package/templates/phaser-game/apps/target-devvit/src/client/game.html +4 -1
- package/templates/phaser-game/apps/target-devvit/src/client/index.html +4 -1
- package/templates/phaser-game/game.html +4 -1
- package/templates/phaser-game/index.html +4 -1
- package/templates/phaser-game/src/main.ts +12 -3
- package/templates/phaser-game/src/platform/gameServices.ts +7 -0
- package/templates/phaser-game/src/runtime/gameContext.ts +2 -2
- package/templates/phaser-game/src/scenes/PlayScene.ts +9 -12
- package/templates/phaser-game/src/styles.css +12 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mpgd/cli",
|
|
3
|
-
"version": "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-
|
|
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/
|
|
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
|
|
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 =
|
|
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
|
|
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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
|
|
97
|
+
const result = await gameServicesClient.claimRewardedAd({
|
|
97
98
|
placementId: rewardedPlacementId,
|
|
98
99
|
idempotencyKey: createClientId('starter-reward'),
|
|
99
100
|
});
|
|
100
101
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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 -
|
|
34
|
+
100dvh - var(--mpgd-safe-area-top) - var(--mpgd-safe-area-bottom)
|
|
28
35
|
);
|
|
29
36
|
|
|
30
37
|
box-sizing: border-box;
|