@mpgd/cli 0.4.1 → 0.6.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/dist/index.js +66 -0
- package/package.json +12 -12
- package/templates/phaser-game/README.md +12 -0
- package/templates/phaser-game/agent/acceptance.md +5 -0
- package/templates/phaser-game/agent/brief.md +2 -0
- package/templates/phaser-game/agent/game-manifest.json +8 -0
- package/templates/phaser-game/apps/target-devvit/src/server/index.ts +46 -1
- package/templates/phaser-game/apps/target-devvit/vite.server.config.ts +2 -1
- package/templates/phaser-game/src/env.d.ts +1 -0
- package/templates/phaser-game/src/i18n/messages.ts +6 -0
- package/templates/phaser-game/src/main.ts +45 -2
- package/templates/phaser-game/src/runtime/gameContext.ts +3 -1
- package/templates/phaser-game/src/scenes/LobbyScene.ts +19 -2
- package/templates/phaser-game/vite.config.ts +57 -0
package/dist/index.js
CHANGED
|
@@ -949,6 +949,7 @@ function toTemplatePath(value) {
|
|
|
949
949
|
}
|
|
950
950
|
function createTargetCommandEnv(values) {
|
|
951
951
|
const targetsFile = path.resolve(readOptionalString(values['targets-file']) ?? 'mpgd.targets.json');
|
|
952
|
+
const gameRoot = path.dirname(targetsFile);
|
|
952
953
|
const kitPath = resolveKitPathForTarget(values);
|
|
953
954
|
const resolvedTargetsFile = readOptionalString(values['resolved-targets-file']);
|
|
954
955
|
const preparedTargetsFile = preparePlatformTargetsFile({
|
|
@@ -960,10 +961,75 @@ function createTargetCommandEnv(values) {
|
|
|
960
961
|
});
|
|
961
962
|
return {
|
|
962
963
|
...process.env,
|
|
964
|
+
...createGameOwnedReleaseEnv(gameRoot, process.env),
|
|
963
965
|
MPGD_KIT_PATH: kitPath,
|
|
964
966
|
MPGD_PLATFORM_TARGETS_FILE: preparedTargetsFile,
|
|
965
967
|
};
|
|
966
968
|
}
|
|
969
|
+
function createGameOwnedReleaseEnv(gameRoot, baseEnv) {
|
|
970
|
+
const monetizationFiles = resolveGameOwnedMonetizationFiles(gameRoot, baseEnv);
|
|
971
|
+
const sourceGitSha = readOptionalString(baseEnv.MPGD_SOURCE_GIT_SHA)
|
|
972
|
+
?? readSourceGitSha(gameRoot);
|
|
973
|
+
const appVersion = readOptionalString(baseEnv.APP_VERSION)
|
|
974
|
+
?? readGamePackageVersion(gameRoot);
|
|
975
|
+
return {
|
|
976
|
+
...monetizationFiles,
|
|
977
|
+
MPGD_SOURCE_GIT_SHA: sourceGitSha,
|
|
978
|
+
...(appVersion === undefined ? {} : { APP_VERSION: appVersion }),
|
|
979
|
+
};
|
|
980
|
+
}
|
|
981
|
+
function resolveGameOwnedMonetizationFiles(gameRoot, baseEnv) {
|
|
982
|
+
const configuredCatalog = readConfiguredPath(baseEnv.MPGD_PRODUCT_CATALOG_FILE);
|
|
983
|
+
const configuredPlacements = readConfiguredPath(baseEnv.MPGD_AD_PLACEMENTS_FILE);
|
|
984
|
+
if ((configuredCatalog === undefined) !== (configuredPlacements === undefined)) {
|
|
985
|
+
throw new Error('MPGD_PRODUCT_CATALOG_FILE and MPGD_AD_PLACEMENTS_FILE must be configured together.');
|
|
986
|
+
}
|
|
987
|
+
if (configuredCatalog !== undefined && configuredPlacements !== undefined) {
|
|
988
|
+
return {
|
|
989
|
+
MPGD_PRODUCT_CATALOG_FILE: path.resolve(gameRoot, configuredCatalog),
|
|
990
|
+
MPGD_AD_PLACEMENTS_FILE: path.resolve(gameRoot, configuredPlacements),
|
|
991
|
+
};
|
|
992
|
+
}
|
|
993
|
+
const catalogFile = path.join(gameRoot, 'mpgd.catalog.json');
|
|
994
|
+
const placementsFile = path.join(gameRoot, 'mpgd.ad-placements.json');
|
|
995
|
+
const hasCatalog = existsSync(catalogFile);
|
|
996
|
+
const hasPlacements = existsSync(placementsFile);
|
|
997
|
+
if (hasCatalog !== hasPlacements) {
|
|
998
|
+
throw new Error(`Game target builds must provide both mpgd.catalog.json and mpgd.ad-placements.json: ${gameRoot}`);
|
|
999
|
+
}
|
|
1000
|
+
if (!hasCatalog) {
|
|
1001
|
+
return {};
|
|
1002
|
+
}
|
|
1003
|
+
return {
|
|
1004
|
+
MPGD_PRODUCT_CATALOG_FILE: catalogFile,
|
|
1005
|
+
MPGD_AD_PLACEMENTS_FILE: placementsFile,
|
|
1006
|
+
};
|
|
1007
|
+
}
|
|
1008
|
+
function readConfiguredPath(value) {
|
|
1009
|
+
const normalized = value?.trim();
|
|
1010
|
+
return normalized === undefined || normalized.length === 0 ? undefined : normalized;
|
|
1011
|
+
}
|
|
1012
|
+
function readSourceGitSha(gameRoot) {
|
|
1013
|
+
const result = spawnSync('git', ['-C', gameRoot, 'rev-parse', '--short', 'HEAD'], {
|
|
1014
|
+
encoding: 'utf8',
|
|
1015
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
1016
|
+
});
|
|
1017
|
+
if (result.error !== undefined || result.status !== 0) {
|
|
1018
|
+
return 'uncommitted';
|
|
1019
|
+
}
|
|
1020
|
+
return result.stdout.trim() || 'uncommitted';
|
|
1021
|
+
}
|
|
1022
|
+
function readGamePackageVersion(gameRoot) {
|
|
1023
|
+
const packageFile = path.join(gameRoot, 'package.json');
|
|
1024
|
+
if (!existsSync(packageFile)) {
|
|
1025
|
+
return undefined;
|
|
1026
|
+
}
|
|
1027
|
+
const packageJson = readJsonForCli(packageFile);
|
|
1028
|
+
if (typeof packageJson !== 'object' || packageJson === null || Array.isArray(packageJson)) {
|
|
1029
|
+
return undefined;
|
|
1030
|
+
}
|
|
1031
|
+
return readOptionalString(packageJson.version);
|
|
1032
|
+
}
|
|
967
1033
|
function preparePlatformTargetsFile(input) {
|
|
968
1034
|
const parsed = readJsonForCli(input.targetsFile);
|
|
969
1035
|
const gameRoot = path.dirname(input.targetsFile);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mpgd/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Gunshi CLI for mpgd-kit starter generation and target workflow orchestration.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -49,18 +49,18 @@
|
|
|
49
49
|
"@types/node": "^24.0.0",
|
|
50
50
|
"ttsc": "0.16.9",
|
|
51
51
|
"typescript": "7.0.1-rc",
|
|
52
|
-
"@mpgd/adapter-
|
|
53
|
-
"@mpgd/adapter-
|
|
54
|
-
"@mpgd/adapter-capacitor": "0.
|
|
55
|
-
"@mpgd/analytics": "0.3.
|
|
56
|
-
"@mpgd/adapter-
|
|
57
|
-
"@mpgd/bridge": "0.
|
|
58
|
-
"@mpgd/catalog": "0.3.
|
|
52
|
+
"@mpgd/adapter-browser": "0.4.0",
|
|
53
|
+
"@mpgd/adapter-devvit": "0.4.0",
|
|
54
|
+
"@mpgd/adapter-capacitor": "0.4.0",
|
|
55
|
+
"@mpgd/analytics": "0.3.3",
|
|
56
|
+
"@mpgd/adapter-ait": "0.4.0",
|
|
57
|
+
"@mpgd/bridge": "0.5.0",
|
|
58
|
+
"@mpgd/catalog": "0.3.3",
|
|
59
|
+
"@mpgd/i18n": "0.4.1",
|
|
60
|
+
"@mpgd/game-services": "0.4.0",
|
|
59
61
|
"@mpgd/phaser-assets": "0.4.0",
|
|
60
|
-
"@mpgd/platform": "0.
|
|
61
|
-
"@mpgd/target-config": "0.
|
|
62
|
-
"@mpgd/i18n": "0.3.3",
|
|
63
|
-
"@mpgd/game-services": "0.3.3"
|
|
62
|
+
"@mpgd/platform": "0.4.0",
|
|
63
|
+
"@mpgd/target-config": "0.5.0"
|
|
64
64
|
},
|
|
65
65
|
"main": "./dist/index.js",
|
|
66
66
|
"types": "./dist/index.d.ts",
|
|
@@ -84,6 +84,9 @@ const viewport = resolveTargetViewportPlan({
|
|
|
84
84
|
height: measured.height,
|
|
85
85
|
source: measured.source,
|
|
86
86
|
runtime: runtime.config.runtime,
|
|
87
|
+
orientationPolicy: {
|
|
88
|
+
mode: 'prefer-landscape',
|
|
89
|
+
},
|
|
87
90
|
});
|
|
88
91
|
```
|
|
89
92
|
|
|
@@ -104,6 +107,15 @@ Orientation is measured from the container: height greater than width is
|
|
|
104
107
|
be treated as an embedded webview, so do not assume desktop Reddit always means
|
|
105
108
|
an expanded game surface.
|
|
106
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
|
+
|
|
107
119
|
## Reddit Devvit
|
|
108
120
|
|
|
109
121
|
This starter owns its Devvit app root in `apps/target-devvit`. That directory
|
|
@@ -12,6 +12,8 @@ pnpm --dir ../mpgd-kit mpgd target doctor --targets-file "$PWD/mpgd.targets.json
|
|
|
12
12
|
|
|
13
13
|
For Apps in Toss changes, use the apps-in-toss MCP before implementation and
|
|
14
14
|
keep SDK calls inside adapters or target wrappers.
|
|
15
|
+
Resolve identity and launch intents during bootstrap, treat inbound share data as
|
|
16
|
+
untrusted, and keep notification delivery on the server.
|
|
15
17
|
|
|
16
18
|
For Reddit Devvit changes, keep Devvit SDK calls inside the target wrapper and
|
|
17
19
|
continue to expose game-facing behavior through `PlatformGateway`. The Devvit
|
|
@@ -21,3 +23,6 @@ after login before upload or playtest.
|
|
|
21
23
|
For Microsoft Store changes, keep the first pass as a PWA/web target that uses
|
|
22
24
|
the browser adapter. Add a dedicated Store commerce adapter only when wiring
|
|
23
25
|
Digital Goods API and Payment Request through backend ledger verification.
|
|
26
|
+
|
|
27
|
+
Verify the first screen reports the viewport orientation policy, and treat
|
|
28
|
+
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,14 @@
|
|
|
25
25
|
"id": "runtime.platform.gateway",
|
|
26
26
|
"owner": "src/platform/installPlatform.ts"
|
|
27
27
|
},
|
|
28
|
+
{
|
|
29
|
+
"id": "platform.gateway.bootstrap-intents",
|
|
30
|
+
"owner": "src/main.ts"
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"id": "runtime.viewport.orientation-policy",
|
|
34
|
+
"owner": "src/main.ts"
|
|
35
|
+
},
|
|
28
36
|
{
|
|
29
37
|
"id": "platform.devvit.app-root",
|
|
30
38
|
"owner": "apps/target-devvit"
|
|
@@ -168,7 +168,7 @@ async function handleBridgeRequest(input: BridgeRequest): Promise<BridgeResponse
|
|
|
168
168
|
nativeLeaderboard: true,
|
|
169
169
|
achievements: false,
|
|
170
170
|
cloudSave: true,
|
|
171
|
-
socialShare:
|
|
171
|
+
socialShare: false,
|
|
172
172
|
haptics: false,
|
|
173
173
|
localizedContent: true,
|
|
174
174
|
});
|
|
@@ -186,6 +186,51 @@ async function handleBridgeRequest(input: BridgeRequest): Promise<BridgeResponse
|
|
|
186
186
|
});
|
|
187
187
|
}
|
|
188
188
|
|
|
189
|
+
case 'identity.getSession': {
|
|
190
|
+
const playerId = currentPlayerId();
|
|
191
|
+
|
|
192
|
+
return ok(
|
|
193
|
+
input,
|
|
194
|
+
playerId === undefined
|
|
195
|
+
? {
|
|
196
|
+
identityLevel: 'guest',
|
|
197
|
+
trustLevel: 'local',
|
|
198
|
+
}
|
|
199
|
+
: {
|
|
200
|
+
identityLevel: 'authenticated',
|
|
201
|
+
playerId,
|
|
202
|
+
trustLevel: 'server-verified',
|
|
203
|
+
},
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
case 'identity.requestUpgrade': {
|
|
208
|
+
const authenticated = currentPlayerId() !== undefined;
|
|
209
|
+
|
|
210
|
+
return ok(input, {
|
|
211
|
+
status: authenticated ? 'completed' : 'unavailable',
|
|
212
|
+
reloadExpected: false,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
case 'presentation.getLaunchIntent':
|
|
217
|
+
return ok(input, { entry: 'home' });
|
|
218
|
+
|
|
219
|
+
case 'presentation.requestGameSurface':
|
|
220
|
+
return ok(input, 'unavailable');
|
|
221
|
+
|
|
222
|
+
case 'share.share':
|
|
223
|
+
return ok(input, { status: 'unavailable' });
|
|
224
|
+
|
|
225
|
+
case 'share.readInboundShare':
|
|
226
|
+
return ok(input, null);
|
|
227
|
+
|
|
228
|
+
case 'notifications.getStatus':
|
|
229
|
+
return ok(input, 'approval-required');
|
|
230
|
+
|
|
231
|
+
case 'notifications.requestSubscription':
|
|
232
|
+
return ok(input, 'unavailable');
|
|
233
|
+
|
|
189
234
|
case 'commerce.getProducts':
|
|
190
235
|
case 'commerce.getEntitlements':
|
|
191
236
|
return ok(input, []);
|
|
@@ -22,6 +22,7 @@ export default defineConfig({
|
|
|
22
22
|
ssr: 'src/server/index.ts',
|
|
23
23
|
outDir: 'dist/server',
|
|
24
24
|
target: 'node22',
|
|
25
|
+
minify: 'esbuild',
|
|
25
26
|
sourcemap: false,
|
|
26
27
|
emptyOutDir: true,
|
|
27
28
|
rollupOptions: {
|
|
@@ -29,7 +30,7 @@ export default defineConfig({
|
|
|
29
30
|
output: {
|
|
30
31
|
format: 'cjs',
|
|
31
32
|
entryFileNames: 'index.cjs',
|
|
32
|
-
|
|
33
|
+
codeSplitting: false,
|
|
33
34
|
},
|
|
34
35
|
},
|
|
35
36
|
},
|
|
@@ -4,6 +4,7 @@ declare const __APP_TARGET__: string;
|
|
|
4
4
|
declare const __MPGD_CONFIG_TARGET__: string;
|
|
5
5
|
declare const __APP_VERSION__: string;
|
|
6
6
|
declare const __BUILD_ID__: string;
|
|
7
|
+
declare const __SOURCE_GIT_SHA__: string;
|
|
7
8
|
declare const __DEBUG_BUILD__: boolean;
|
|
8
9
|
|
|
9
10
|
interface ImportMetaEnv {
|
|
@@ -4,6 +4,8 @@ export type GameMessageKey =
|
|
|
4
4
|
| 'title'
|
|
5
5
|
| 'target'
|
|
6
6
|
| 'viewport'
|
|
7
|
+
| 'orientationPolicy'
|
|
8
|
+
| 'orientationMismatch'
|
|
7
9
|
| 'backend'
|
|
8
10
|
| 'features'
|
|
9
11
|
| 'featuresNone'
|
|
@@ -21,6 +23,8 @@ const messages = {
|
|
|
21
23
|
title: '__GAME_TITLE__',
|
|
22
24
|
target: 'Target: {target}',
|
|
23
25
|
viewport: 'Viewport: {sizeClass} {orientation} - controls {controls}',
|
|
26
|
+
orientationPolicy: 'Orientation policy: {mode}',
|
|
27
|
+
orientationMismatch: 'Rotate to {orientation} for {mode}',
|
|
24
28
|
backend: 'Game Services: {mode}',
|
|
25
29
|
features: 'Features: {features}',
|
|
26
30
|
featuresNone: 'none',
|
|
@@ -37,6 +41,8 @@ const messages = {
|
|
|
37
41
|
title: '__GAME_TITLE__',
|
|
38
42
|
target: '타깃: {target}',
|
|
39
43
|
viewport: '뷰포트: {sizeClass} {orientation} - 컨트롤 {controls}',
|
|
44
|
+
orientationPolicy: '방향 정책: {mode}',
|
|
45
|
+
orientationMismatch: '{mode}: {orientation} 방향으로 회전',
|
|
40
46
|
backend: 'Game Services: {mode}',
|
|
41
47
|
features: '기능: {features}',
|
|
42
48
|
featuresNone: '없음',
|
|
@@ -2,7 +2,11 @@ import './styles.css';
|
|
|
2
2
|
|
|
3
3
|
import { createAnalyticsReporter, createBufferedAnalyticsSink } from '@mpgd/analytics';
|
|
4
4
|
import { resolveMpgdLocale, type Locale } from '@mpgd/i18n';
|
|
5
|
-
import {
|
|
5
|
+
import type { IdentitySession, LaunchIntent, PlatformGateway } from '@mpgd/platform';
|
|
6
|
+
import {
|
|
7
|
+
resolveTargetViewportPlan,
|
|
8
|
+
type TargetViewportOrientationPolicy,
|
|
9
|
+
} from '@mpgd/target-config';
|
|
6
10
|
|
|
7
11
|
import { t } from './i18n/messages';
|
|
8
12
|
import { createClientId } from './runtime/id';
|
|
@@ -25,15 +29,23 @@ async function bootstrap(): Promise<void> {
|
|
|
25
29
|
const runtimeConfig = detectRuntime();
|
|
26
30
|
const platform = await installPlatform(runtimeConfig);
|
|
27
31
|
const runtime = await platform.getTargetRuntime();
|
|
32
|
+
const orientationPolicy = {
|
|
33
|
+
mode: 'prefer-landscape',
|
|
34
|
+
} as const satisfies TargetViewportOrientationPolicy;
|
|
28
35
|
const viewport = resolveTargetViewportPlan({
|
|
29
36
|
...measureGameViewport(),
|
|
30
37
|
runtime: runtime.config.runtime,
|
|
38
|
+
orientationPolicy,
|
|
31
39
|
});
|
|
32
40
|
const player =
|
|
33
41
|
(await platform.identity.getPlayer()) ?? {
|
|
34
42
|
playerId: 'local-player',
|
|
35
43
|
displayName: 'Local Player',
|
|
36
44
|
};
|
|
45
|
+
const [identitySession, launchIntent] = await Promise.all([
|
|
46
|
+
resolveIdentitySession(platform, player.playerId),
|
|
47
|
+
resolveLaunchIntent(platform),
|
|
48
|
+
]);
|
|
37
49
|
locale = resolveMpgdLocale(runtime.capabilities);
|
|
38
50
|
const analyticsSink = createBufferedAnalyticsSink();
|
|
39
51
|
const analytics = createAnalyticsReporter({
|
|
@@ -43,7 +55,7 @@ async function bootstrap(): Promise<void> {
|
|
|
43
55
|
});
|
|
44
56
|
const gameServices = createStarterGameServices({
|
|
45
57
|
gateway: platform,
|
|
46
|
-
playerId: player.playerId,
|
|
58
|
+
playerId: identitySession.playerId ?? player.playerId,
|
|
47
59
|
});
|
|
48
60
|
|
|
49
61
|
await analytics.track({
|
|
@@ -61,6 +73,8 @@ async function bootstrap(): Promise<void> {
|
|
|
61
73
|
runtime,
|
|
62
74
|
viewport,
|
|
63
75
|
player,
|
|
76
|
+
identitySession,
|
|
77
|
+
launchIntent,
|
|
64
78
|
locale,
|
|
65
79
|
gameServices,
|
|
66
80
|
analytics,
|
|
@@ -73,6 +87,35 @@ async function bootstrap(): Promise<void> {
|
|
|
73
87
|
}
|
|
74
88
|
}
|
|
75
89
|
|
|
90
|
+
async function resolveIdentitySession(
|
|
91
|
+
platform: PlatformGateway,
|
|
92
|
+
fallbackPlayerId: string,
|
|
93
|
+
): Promise<IdentitySession> {
|
|
94
|
+
try {
|
|
95
|
+
return (await platform.identity.getSession?.()) ?? createGuestSession(fallbackPlayerId);
|
|
96
|
+
} catch (error) {
|
|
97
|
+
console.warn('[platform] identity session unavailable; using guest fallback.', error);
|
|
98
|
+
return createGuestSession(fallbackPlayerId);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function resolveLaunchIntent(platform: PlatformGateway): Promise<LaunchIntent> {
|
|
103
|
+
try {
|
|
104
|
+
return (await platform.presentation?.getLaunchIntent()) ?? { entry: 'home' };
|
|
105
|
+
} catch (error) {
|
|
106
|
+
console.warn('[platform] launch intent unavailable; using home fallback.', error);
|
|
107
|
+
return { entry: 'home' };
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function createGuestSession(playerId: string): IdentitySession {
|
|
112
|
+
return {
|
|
113
|
+
identityLevel: 'guest',
|
|
114
|
+
playerId,
|
|
115
|
+
trustLevel: 'local',
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
76
119
|
function renderBootstrapError(error: unknown, locale: Locale): void {
|
|
77
120
|
const message = error instanceof Error ? error.message : String(error);
|
|
78
121
|
const root = document.querySelector<HTMLDivElement>('#game');
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { AnalyticsReporter, BufferedAnalyticsSink } from '@mpgd/analytics';
|
|
2
2
|
import type { Locale } from '@mpgd/i18n';
|
|
3
|
-
import type { PlayerIdentity } from '@mpgd/platform';
|
|
3
|
+
import type { IdentitySession, LaunchIntent, PlayerIdentity } from '@mpgd/platform';
|
|
4
4
|
import type {
|
|
5
5
|
TargetConfiguredGateway,
|
|
6
6
|
TargetRuntimeSnapshot,
|
|
@@ -16,6 +16,8 @@ export interface StarterContext {
|
|
|
16
16
|
readonly runtime: TargetRuntimeSnapshot;
|
|
17
17
|
readonly viewport: TargetViewportPlan;
|
|
18
18
|
readonly player: PlayerIdentity;
|
|
19
|
+
readonly identitySession: IdentitySession;
|
|
20
|
+
readonly launchIntent: LaunchIntent;
|
|
19
21
|
readonly locale: Locale;
|
|
20
22
|
readonly gameServices: StarterGameServices;
|
|
21
23
|
readonly analytics: AnalyticsReporter;
|
|
@@ -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'), {
|
|
@@ -61,14 +71,21 @@ export class LobbyScene extends Phaser.Scene {
|
|
|
61
71
|
)
|
|
62
72
|
.setOrigin(0.5);
|
|
63
73
|
this.add
|
|
64
|
-
.text(480, 300,
|
|
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 }), {
|
|
65
82
|
color: context.gameServices.mode === 'disabled' ? '#f59e0b' : '#2dd4bf',
|
|
66
83
|
fontFamily: 'Arial, sans-serif',
|
|
67
84
|
fontSize: '18px',
|
|
68
85
|
})
|
|
69
86
|
.setOrigin(0.5);
|
|
70
87
|
this.add
|
|
71
|
-
.text(480,
|
|
88
|
+
.text(480, 384, t(locale, 'tapToStart'), {
|
|
72
89
|
color: '#ffffff',
|
|
73
90
|
fontFamily: 'Arial, sans-serif',
|
|
74
91
|
fontSize: '22px',
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
|
|
1
4
|
import ttsc from '@ttsc/unplugin/vite';
|
|
2
5
|
import { defineConfig } from 'vite';
|
|
3
6
|
|
|
@@ -12,11 +15,15 @@ export default defineConfig(({ mode }) => {
|
|
|
12
15
|
plugins: false,
|
|
13
16
|
}),
|
|
14
17
|
],
|
|
18
|
+
resolve: {
|
|
19
|
+
alias: createCatalogAliases(),
|
|
20
|
+
},
|
|
15
21
|
define: {
|
|
16
22
|
__APP_TARGET__: JSON.stringify(process.env.APP_TARGET ?? 'browser'),
|
|
17
23
|
__MPGD_CONFIG_TARGET__: JSON.stringify(process.env.MPGD_CONFIG_TARGET ?? ''),
|
|
18
24
|
__APP_VERSION__: JSON.stringify(process.env.APP_VERSION ?? '0.0.0-dev'),
|
|
19
25
|
__BUILD_ID__: JSON.stringify(process.env.BUILD_ID ?? 'local'),
|
|
26
|
+
__SOURCE_GIT_SHA__: JSON.stringify(process.env.MPGD_SOURCE_GIT_SHA ?? 'uncommitted'),
|
|
20
27
|
__DEBUG_BUILD__: JSON.stringify(!isProduction),
|
|
21
28
|
},
|
|
22
29
|
build: {
|
|
@@ -35,3 +42,53 @@ export default defineConfig(({ mode }) => {
|
|
|
35
42
|
},
|
|
36
43
|
};
|
|
37
44
|
});
|
|
45
|
+
|
|
46
|
+
function createCatalogAliases(): Record<string, string> {
|
|
47
|
+
const productCatalogFile = readConfiguredPath(process.env.MPGD_PRODUCT_CATALOG_FILE);
|
|
48
|
+
const adPlacementsFile = readConfiguredPath(process.env.MPGD_AD_PLACEMENTS_FILE);
|
|
49
|
+
|
|
50
|
+
if ((productCatalogFile === undefined) !== (adPlacementsFile === undefined)) {
|
|
51
|
+
throw new Error(
|
|
52
|
+
'MPGD_PRODUCT_CATALOG_FILE and MPGD_AD_PLACEMENTS_FILE must be configured together.',
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (productCatalogFile === undefined || adPlacementsFile === undefined) {
|
|
57
|
+
return {};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
'@mpgd/catalog/catalog.json': resolveCatalogPath(productCatalogFile, adPlacementsFile),
|
|
62
|
+
'@mpgd/catalog/placements.json': resolveCatalogPath(adPlacementsFile, productCatalogFile),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function readConfiguredPath(value: string | undefined): string | undefined {
|
|
67
|
+
const normalized = value?.trim();
|
|
68
|
+
return normalized === undefined || normalized.length === 0 ? undefined : normalized;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function resolveCatalogPath(path: string, pairedPath: string): string {
|
|
72
|
+
return resolve(resolveCatalogBaseDir(path, pairedPath), path);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function resolveCatalogBaseDir(path: string, pairedPath: string): string {
|
|
76
|
+
const fallbackBaseDir = process.cwd();
|
|
77
|
+
const candidates = [
|
|
78
|
+
fallbackBaseDir,
|
|
79
|
+
readConfiguredPath(process.env.INIT_CWD),
|
|
80
|
+
readConfiguredPath(process.env.PWD),
|
|
81
|
+
];
|
|
82
|
+
|
|
83
|
+
for (const candidate of candidates) {
|
|
84
|
+
if (
|
|
85
|
+
candidate !== undefined
|
|
86
|
+
&& existsSync(resolve(candidate, path))
|
|
87
|
+
&& existsSync(resolve(candidate, pairedPath))
|
|
88
|
+
) {
|
|
89
|
+
return candidate;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return fallbackBaseDir;
|
|
94
|
+
}
|