@mpgd/cli 0.1.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/LICENSE +21 -0
- package/dist/bin.d.ts +2 -0
- package/dist/bin.js +3 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +930 -0
- package/package.json +64 -0
- package/templates/phaser-game/README.md +66 -0
- package/templates/phaser-game/agent/acceptance.md +16 -0
- package/templates/phaser-game/agent/brief.md +20 -0
- package/templates/phaser-game/agent/game-manifest.json +41 -0
- package/templates/phaser-game/index.html +12 -0
- package/templates/phaser-game/mpgd.targets.json +42 -0
- package/templates/phaser-game/package.json +35 -0
- package/templates/phaser-game/src/env.d.ts +12 -0
- package/templates/phaser-game/src/game/state.ts +29 -0
- package/templates/phaser-game/src/i18n/messages.ts +64 -0
- package/templates/phaser-game/src/main.ts +78 -0
- package/templates/phaser-game/src/platform/gameServices.ts +70 -0
- package/templates/phaser-game/src/platform/installPlatform.ts +96 -0
- package/templates/phaser-game/src/platform/runtimeDetector.ts +30 -0
- package/templates/phaser-game/src/runtime/createGame.ts +28 -0
- package/templates/phaser-game/src/runtime/gameContext.ts +18 -0
- package/templates/phaser-game/src/runtime/id.ts +6 -0
- package/templates/phaser-game/src/scenes/BootScene.ts +11 -0
- package/templates/phaser-game/src/scenes/LobbyScene.ts +65 -0
- package/templates/phaser-game/src/scenes/PlayScene.ts +121 -0
- package/templates/phaser-game/src/styles.css +31 -0
- package/templates/phaser-game/tsconfig.json +27 -0
- package/templates/phaser-game/vite.config.ts +35 -0
package/package.json
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mpgd/cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Gunshi CLI for mpgd-kit starter generation and target workflow orchestration.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/imjlk/mpgd-kit.git",
|
|
9
|
+
"directory": "packages/cli"
|
|
10
|
+
},
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/imjlk/mpgd-kit/issues"
|
|
13
|
+
},
|
|
14
|
+
"homepage": "https://github.com/imjlk/mpgd-kit#readme",
|
|
15
|
+
"keywords": [
|
|
16
|
+
"mpgd",
|
|
17
|
+
"phaser",
|
|
18
|
+
"vite",
|
|
19
|
+
"typescript",
|
|
20
|
+
"capacitor",
|
|
21
|
+
"apps-in-toss",
|
|
22
|
+
"devvit",
|
|
23
|
+
"game-development",
|
|
24
|
+
"game-distribution",
|
|
25
|
+
"cli"
|
|
26
|
+
],
|
|
27
|
+
"type": "module",
|
|
28
|
+
"sideEffects": false,
|
|
29
|
+
"exports": {
|
|
30
|
+
".": {
|
|
31
|
+
"types": "./dist/index.d.ts",
|
|
32
|
+
"default": "./dist/index.js"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"files": [
|
|
36
|
+
"dist",
|
|
37
|
+
"templates"
|
|
38
|
+
],
|
|
39
|
+
"bin": {
|
|
40
|
+
"mpgd": "./dist/bin.js"
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"@gunshi/plugin-completion": "^0.35.1",
|
|
44
|
+
"@gunshi/plugin-i18n": "^0.35.1",
|
|
45
|
+
"@gunshi/resources": "^0.35.1",
|
|
46
|
+
"gunshi": "^0.35.1"
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"@types/node": "^24.0.0",
|
|
50
|
+
"ttsc": "0.16.9",
|
|
51
|
+
"typescript": "7.0.1-rc"
|
|
52
|
+
},
|
|
53
|
+
"main": "./dist/index.js",
|
|
54
|
+
"types": "./dist/index.d.ts",
|
|
55
|
+
"publishConfig": {
|
|
56
|
+
"access": "public"
|
|
57
|
+
},
|
|
58
|
+
"scripts": {
|
|
59
|
+
"check": "ttsc --noEmit -p tsconfig.json",
|
|
60
|
+
"lint": "ttsc --noEmit -p tsconfig.json",
|
|
61
|
+
"format": "ttsc format",
|
|
62
|
+
"fix": "ttsc fix"
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# __GAME_TITLE__
|
|
2
|
+
|
|
3
|
+
Standalone Phaser 4 game starter generated by `@mpgd/create-game`.
|
|
4
|
+
|
|
5
|
+
## Local Loop
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
pnpm install
|
|
9
|
+
pnpm dev
|
|
10
|
+
pnpm check
|
|
11
|
+
pnpm build
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
The starter keeps game code behind `PlatformGateway` and demonstrates:
|
|
15
|
+
|
|
16
|
+
- target-aware adapter selection through `APP_TARGET`
|
|
17
|
+
- target-config/effective-config runtime snapshots
|
|
18
|
+
- local translation keys
|
|
19
|
+
- optional `@mpgd/game-services` client wiring
|
|
20
|
+
- best-effort analytics events
|
|
21
|
+
- rewarded ad smoke action
|
|
22
|
+
|
|
23
|
+
## Target Builds
|
|
24
|
+
|
|
25
|
+
Target builds require an `mpgd-kit` checkout because the target wrappers live in
|
|
26
|
+
the kit repository. Pass that checkout with `--kit-path`:
|
|
27
|
+
|
|
28
|
+
```sh
|
|
29
|
+
pnpm dlx @mpgd/cli target build-all \
|
|
30
|
+
--targets-file ./mpgd.targets.json \
|
|
31
|
+
--kit-path ../mpgd-kit \
|
|
32
|
+
--targets web,ait \
|
|
33
|
+
--ait-variant wrapper
|
|
34
|
+
pnpm dlx @mpgd/cli target smoke-all \
|
|
35
|
+
--targets-file ./mpgd.targets.json \
|
|
36
|
+
--kit-path ../mpgd-kit \
|
|
37
|
+
--targets web,ait
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
When developing the kit itself, the same commands work through the repository
|
|
41
|
+
script:
|
|
42
|
+
|
|
43
|
+
```sh
|
|
44
|
+
pnpm --dir ../mpgd-kit mpgd target build-all \
|
|
45
|
+
--targets-file "$PWD/mpgd.targets.json" \
|
|
46
|
+
--kit-path ../mpgd-kit \
|
|
47
|
+
--targets web,ait \
|
|
48
|
+
--ait-variant wrapper
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
`mpgd.targets.json` contains `${MPGD_KIT_PATH}` tokens. The CLI resolves them
|
|
52
|
+
into `.mpgd.targets.generated.json` before calling the kit's existing target
|
|
53
|
+
build and smoke scripts.
|
|
54
|
+
|
|
55
|
+
## Game Services
|
|
56
|
+
|
|
57
|
+
The backend client is disabled unless a backend URL is configured:
|
|
58
|
+
|
|
59
|
+
```sh
|
|
60
|
+
VITE_MPGD_GAME_SERVICES_URL=http://localhost:5173
|
|
61
|
+
VITE_MPGD_GAME_SERVICES_TARGET=android
|
|
62
|
+
VITE_MPGD_GAME_SERVICES_TRANSPORT=http
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Use `VITE_MPGD_GAME_SERVICES_TRANSPORT=orpc` with a `/rpc` URL to test the oRPC
|
|
66
|
+
client path.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Acceptance
|
|
2
|
+
|
|
3
|
+
Run these checks before handing off a generated game starter:
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
pnpm check
|
|
7
|
+
pnpm build
|
|
8
|
+
pnpm --dir ../mpgd-kit mpgd target build-all --targets-file "$PWD/mpgd.targets.json" --targets web,ait --ait-variant wrapper
|
|
9
|
+
pnpm --dir ../mpgd-kit mpgd target smoke-all --targets-file "$PWD/mpgd.targets.json" --targets web,ait
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
For Apps in Toss changes, use the apps-in-toss MCP before implementation and
|
|
13
|
+
keep SDK calls inside adapters or target wrappers.
|
|
14
|
+
|
|
15
|
+
For Reddit Devvit changes, keep Devvit SDK calls inside the target wrapper and
|
|
16
|
+
continue to expose game-facing behavior through `PlatformGateway`.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# __GAME_TITLE__ Brief
|
|
2
|
+
|
|
3
|
+
Build a Phaser 4 game on top of the mpgd platform boundary.
|
|
4
|
+
|
|
5
|
+
Boundaries:
|
|
6
|
+
|
|
7
|
+
- Phaser scenes render and collect input only.
|
|
8
|
+
- Target services go through `PlatformGateway`.
|
|
9
|
+
- Purchases, rewarded ad grants, and leaderboard records should go through
|
|
10
|
+
backend ledger APIs before mutating durable game save state.
|
|
11
|
+
- Platform SDK imports belong in adapters or target wrappers.
|
|
12
|
+
|
|
13
|
+
Useful commands:
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
pnpm dev
|
|
17
|
+
pnpm check
|
|
18
|
+
pnpm build
|
|
19
|
+
pnpm --dir ../mpgd-kit mpgd target build-all --targets-file "$PWD/mpgd.targets.json" --targets web,ait --ait-variant wrapper
|
|
20
|
+
```
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 1,
|
|
3
|
+
"game": {
|
|
4
|
+
"id": "__GAME_NAME__",
|
|
5
|
+
"title": "__GAME_TITLE__",
|
|
6
|
+
"engine": "phaser-4"
|
|
7
|
+
},
|
|
8
|
+
"targets": ["web-preview", "android", "ios", "ait", "reddit"],
|
|
9
|
+
"futureTargets": ["telegram", "tauri"],
|
|
10
|
+
"platformBoundary": "PlatformGateway",
|
|
11
|
+
"mcpRequirements": {
|
|
12
|
+
"ait": "Use apps-in-toss MCP before Apps in Toss SDK or review-flow changes.",
|
|
13
|
+
"reddit": "Use Devvit documentation/MCP before Devvit SDK or publish-flow changes."
|
|
14
|
+
},
|
|
15
|
+
"blocks": [
|
|
16
|
+
{
|
|
17
|
+
"id": "simulation.loop.phase",
|
|
18
|
+
"owner": "src/game/state.ts"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"id": "runtime.platform.gateway",
|
|
22
|
+
"owner": "src/platform/installPlatform.ts"
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"id": "services.backend.game-services",
|
|
26
|
+
"owner": "src/platform/gameServices.ts"
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"id": "localization.messages",
|
|
30
|
+
"owner": "src/i18n/messages.ts"
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"id": "telemetry.analytics.best-effort",
|
|
34
|
+
"owner": "src/main.ts"
|
|
35
|
+
}
|
|
36
|
+
],
|
|
37
|
+
"acceptance": [
|
|
38
|
+
"pnpm check",
|
|
39
|
+
"pnpm build"
|
|
40
|
+
]
|
|
41
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>__GAME_TITLE__</title>
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<div id="game"></div>
|
|
10
|
+
<script type="module" src="/src/main.ts"></script>
|
|
11
|
+
</body>
|
|
12
|
+
</html>
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"targets": {
|
|
3
|
+
"web-preview": {
|
|
4
|
+
"kind": "web",
|
|
5
|
+
"gameApp": ".",
|
|
6
|
+
"adapter": "browser",
|
|
7
|
+
"output": "artifacts/web-preview"
|
|
8
|
+
},
|
|
9
|
+
"android": {
|
|
10
|
+
"kind": "capacitor-android",
|
|
11
|
+
"gameApp": ".",
|
|
12
|
+
"shellApp": "${MPGD_KIT_PATH}/apps/mobile-capacitor",
|
|
13
|
+
"adapter": "capacitor",
|
|
14
|
+
"webDir": "${MPGD_KIT_PATH}/apps/mobile-capacitor/www",
|
|
15
|
+
"artifact": "aab"
|
|
16
|
+
},
|
|
17
|
+
"ios": {
|
|
18
|
+
"kind": "capacitor-ios",
|
|
19
|
+
"gameApp": ".",
|
|
20
|
+
"shellApp": "${MPGD_KIT_PATH}/apps/mobile-capacitor",
|
|
21
|
+
"adapter": "capacitor",
|
|
22
|
+
"webDir": "${MPGD_KIT_PATH}/apps/mobile-capacitor/www",
|
|
23
|
+
"artifact": "ipa"
|
|
24
|
+
},
|
|
25
|
+
"ait": {
|
|
26
|
+
"kind": "apps-in-toss",
|
|
27
|
+
"gameApp": ".",
|
|
28
|
+
"wrapperApp": "${MPGD_KIT_PATH}/apps/target-ait",
|
|
29
|
+
"adapter": "ait",
|
|
30
|
+
"webDir": "${MPGD_KIT_PATH}/apps/target-ait/public/game",
|
|
31
|
+
"artifact": ".ait"
|
|
32
|
+
},
|
|
33
|
+
"reddit": {
|
|
34
|
+
"kind": "devvit-web",
|
|
35
|
+
"gameApp": ".",
|
|
36
|
+
"wrapperApp": "${MPGD_KIT_PATH}/apps/target-devvit",
|
|
37
|
+
"adapter": "devvit",
|
|
38
|
+
"webDir": "${MPGD_KIT_PATH}/apps/target-devvit/dist/client",
|
|
39
|
+
"artifact": "devvit"
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "__PACKAGE_NAME__",
|
|
3
|
+
"private": true,
|
|
4
|
+
"version": "0.0.0",
|
|
5
|
+
"description": "__GAME_TITLE__ mpgd Phaser game.",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"dev": "__WORKSPACE_I18N_BUILD_PREFIX__vite --host 0.0.0.0",
|
|
9
|
+
"build": "__WORKSPACE_I18N_BUILD_PREFIX__vite build",
|
|
10
|
+
"check": "ttsc --noEmit -p tsconfig.json",
|
|
11
|
+
"lint": "ttsc --noEmit -p tsconfig.json",
|
|
12
|
+
"format": "ttsc format",
|
|
13
|
+
"fix": "ttsc fix"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"@mpgd/adapter-ait": "__MPGD_DEPENDENCY_VERSION__",
|
|
17
|
+
"@mpgd/adapter-browser": "__MPGD_DEPENDENCY_VERSION__",
|
|
18
|
+
"@mpgd/adapter-capacitor": "__MPGD_DEPENDENCY_VERSION__",
|
|
19
|
+
"@mpgd/adapter-devvit": "__MPGD_DEPENDENCY_VERSION__",
|
|
20
|
+
"@mpgd/analytics": "__MPGD_DEPENDENCY_VERSION__",
|
|
21
|
+
"@mpgd/catalog": "__MPGD_DEPENDENCY_VERSION__",
|
|
22
|
+
"@mpgd/game-services": "__MPGD_DEPENDENCY_VERSION__",
|
|
23
|
+
"@mpgd/i18n": "__MPGD_DEPENDENCY_VERSION__",
|
|
24
|
+
"@mpgd/platform": "__MPGD_DEPENDENCY_VERSION__",
|
|
25
|
+
"@mpgd/target-config": "__MPGD_DEPENDENCY_VERSION__",
|
|
26
|
+
"phaser": "4.2.0"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@ttsc/unplugin": "0.16.9",
|
|
30
|
+
"@types/node": "^24.0.0",
|
|
31
|
+
"ttsc": "0.16.9",
|
|
32
|
+
"typescript": "7.0.1-rc",
|
|
33
|
+
"vite": "^8.1.3"
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/// <reference types="vite/client" />
|
|
2
|
+
|
|
3
|
+
declare const __APP_TARGET__: string;
|
|
4
|
+
declare const __APP_VERSION__: string;
|
|
5
|
+
declare const __BUILD_ID__: string;
|
|
6
|
+
declare const __DEBUG_BUILD__: boolean;
|
|
7
|
+
|
|
8
|
+
interface ImportMetaEnv {
|
|
9
|
+
readonly VITE_MPGD_GAME_SERVICES_URL?: string;
|
|
10
|
+
readonly VITE_MPGD_GAME_SERVICES_TARGET?: string;
|
|
11
|
+
readonly VITE_MPGD_GAME_SERVICES_TRANSPORT?: string;
|
|
12
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
const scoreIntervalMs = 100;
|
|
2
|
+
const phasePeriodMs = 2400;
|
|
3
|
+
|
|
4
|
+
export interface StarterRunState {
|
|
5
|
+
readonly elapsedMs: number;
|
|
6
|
+
readonly score: number;
|
|
7
|
+
readonly phase: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function createStarterRunState(): StarterRunState {
|
|
11
|
+
return {
|
|
12
|
+
elapsedMs: 0,
|
|
13
|
+
score: 0,
|
|
14
|
+
phase: 0,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function stepStarterRunState(
|
|
19
|
+
state: StarterRunState,
|
|
20
|
+
deltaMs: number,
|
|
21
|
+
): StarterRunState {
|
|
22
|
+
const elapsedMs = state.elapsedMs + deltaMs;
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
elapsedMs,
|
|
26
|
+
score: Math.floor(elapsedMs / scoreIntervalMs),
|
|
27
|
+
phase: (elapsedMs % phasePeriodMs) / phasePeriodMs,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { Locale } from '@mpgd/i18n';
|
|
2
|
+
|
|
3
|
+
export type GameMessageKey =
|
|
4
|
+
| 'title'
|
|
5
|
+
| 'target'
|
|
6
|
+
| 'backend'
|
|
7
|
+
| 'features'
|
|
8
|
+
| 'featuresNone'
|
|
9
|
+
| 'tapToStart'
|
|
10
|
+
| 'score'
|
|
11
|
+
| 'reward'
|
|
12
|
+
| 'rewardError'
|
|
13
|
+
| 'rewardPending'
|
|
14
|
+
| 'rewardUnavailable'
|
|
15
|
+
| 'analytics'
|
|
16
|
+
| 'bootError';
|
|
17
|
+
|
|
18
|
+
const messages = {
|
|
19
|
+
en: {
|
|
20
|
+
title: '__GAME_TITLE__',
|
|
21
|
+
target: 'Target: {target}',
|
|
22
|
+
backend: 'Game Services: {mode}',
|
|
23
|
+
features: 'Features: {features}',
|
|
24
|
+
featuresNone: 'none',
|
|
25
|
+
tapToStart: 'Tap or press Enter to start',
|
|
26
|
+
score: 'Score {score}',
|
|
27
|
+
reward: 'Rewarded ad smoke: {status}',
|
|
28
|
+
rewardError: 'Rewarded ad smoke failed',
|
|
29
|
+
rewardPending: 'Press R to request a rewarded ad smoke',
|
|
30
|
+
rewardUnavailable: 'Rewarded ads unavailable on this target',
|
|
31
|
+
analytics: 'Analytics events: {count}',
|
|
32
|
+
bootError: 'Failed to start game',
|
|
33
|
+
},
|
|
34
|
+
ko: {
|
|
35
|
+
title: '__GAME_TITLE__',
|
|
36
|
+
target: '타깃: {target}',
|
|
37
|
+
backend: 'Game Services: {mode}',
|
|
38
|
+
features: '기능: {features}',
|
|
39
|
+
featuresNone: '없음',
|
|
40
|
+
tapToStart: '탭하거나 Enter를 눌러 시작',
|
|
41
|
+
score: '점수 {score}',
|
|
42
|
+
reward: '리워드 광고 스모크: {status}',
|
|
43
|
+
rewardError: '리워드 광고 스모크 실패',
|
|
44
|
+
rewardPending: 'R 키로 리워드 광고 스모크 요청',
|
|
45
|
+
rewardUnavailable: '이 타깃에서는 리워드 광고를 사용할 수 없음',
|
|
46
|
+
analytics: '애널리틱스 이벤트: {count}',
|
|
47
|
+
bootError: '게임 시작 실패',
|
|
48
|
+
},
|
|
49
|
+
} satisfies Record<Locale, Record<GameMessageKey, string>>;
|
|
50
|
+
|
|
51
|
+
export function t(
|
|
52
|
+
locale: Locale,
|
|
53
|
+
key: GameMessageKey,
|
|
54
|
+
values: Record<string, string | number> = {},
|
|
55
|
+
): string {
|
|
56
|
+
const language = locale === 'ko' ? 'ko' : 'en';
|
|
57
|
+
let text: string = messages[language][key] ?? messages.en[key];
|
|
58
|
+
|
|
59
|
+
for (const [name, value] of Object.entries(values)) {
|
|
60
|
+
text = text.replaceAll(`{${name}}`, String(value));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return text;
|
|
64
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import './styles.css';
|
|
2
|
+
|
|
3
|
+
import { createAnalyticsReporter, createBufferedAnalyticsSink } from '@mpgd/analytics';
|
|
4
|
+
import { resolveMpgdLocale, type Locale } from '@mpgd/i18n';
|
|
5
|
+
|
|
6
|
+
import { t } from './i18n/messages';
|
|
7
|
+
import { createClientId } from './runtime/id';
|
|
8
|
+
import { createStarterGame } from './runtime/createGame';
|
|
9
|
+
import { detectRuntime } from './platform/runtimeDetector';
|
|
10
|
+
import { createStarterGameServices } from './platform/gameServices';
|
|
11
|
+
import { installPlatform } from './platform/installPlatform';
|
|
12
|
+
|
|
13
|
+
await bootstrap();
|
|
14
|
+
|
|
15
|
+
async function bootstrap(): Promise<void> {
|
|
16
|
+
let locale: Locale = 'en';
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
const runtimeConfig = detectRuntime();
|
|
20
|
+
const platform = await installPlatform(runtimeConfig);
|
|
21
|
+
const runtime = await platform.getTargetRuntime();
|
|
22
|
+
const player =
|
|
23
|
+
(await platform.identity.getPlayer()) ?? {
|
|
24
|
+
playerId: 'local-player',
|
|
25
|
+
displayName: 'Local Player',
|
|
26
|
+
};
|
|
27
|
+
locale = resolveMpgdLocale(runtime.capabilities);
|
|
28
|
+
const analyticsSink = createBufferedAnalyticsSink();
|
|
29
|
+
const analytics = createAnalyticsReporter({
|
|
30
|
+
target: platform.target,
|
|
31
|
+
sessionId: createClientId('session'),
|
|
32
|
+
sink: analyticsSink,
|
|
33
|
+
});
|
|
34
|
+
const gameServices = createStarterGameServices({
|
|
35
|
+
gateway: platform,
|
|
36
|
+
playerId: player.playerId,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
await analytics.track({
|
|
40
|
+
name: 'game_started',
|
|
41
|
+
properties: {
|
|
42
|
+
target: platform.target,
|
|
43
|
+
configTarget: runtime.configTarget,
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
createStarterGame({
|
|
48
|
+
mountId: 'game',
|
|
49
|
+
context: {
|
|
50
|
+
platform,
|
|
51
|
+
runtime,
|
|
52
|
+
player,
|
|
53
|
+
locale,
|
|
54
|
+
gameServices,
|
|
55
|
+
analytics,
|
|
56
|
+
analyticsSink,
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
} catch (error) {
|
|
60
|
+
renderBootstrapError(error, locale);
|
|
61
|
+
console.error('[bootstrap]', error);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function renderBootstrapError(error: unknown, locale: Locale): void {
|
|
66
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
67
|
+
const root = document.querySelector<HTMLDivElement>('#game');
|
|
68
|
+
|
|
69
|
+
if (root === null) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
root.replaceChildren();
|
|
74
|
+
const panel = document.createElement('div');
|
|
75
|
+
panel.className = 'boot-error';
|
|
76
|
+
panel.textContent = `${t(locale, 'bootError')}: ${message}`;
|
|
77
|
+
root.append(panel);
|
|
78
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createGameServicesClient,
|
|
3
|
+
createGameServicesFetchBackendTransport,
|
|
4
|
+
createGameServicesHttpBackendApi,
|
|
5
|
+
createGameServicesOrpcBackendApi,
|
|
6
|
+
createGameServicesOrpcClient,
|
|
7
|
+
type GameServicesClient,
|
|
8
|
+
type GameServicesStoreTarget,
|
|
9
|
+
} from '@mpgd/game-services';
|
|
10
|
+
import type { PlatformGateway, PlatformTarget } from '@mpgd/platform';
|
|
11
|
+
|
|
12
|
+
export type StarterBackendMode = 'disabled' | 'http' | 'orpc';
|
|
13
|
+
|
|
14
|
+
export interface StarterGameServices {
|
|
15
|
+
readonly mode: StarterBackendMode;
|
|
16
|
+
readonly baseUrl?: string;
|
|
17
|
+
readonly target?: GameServicesStoreTarget;
|
|
18
|
+
readonly client?: GameServicesClient;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function createStarterGameServices(input: {
|
|
22
|
+
readonly gateway: PlatformGateway;
|
|
23
|
+
readonly playerId: string;
|
|
24
|
+
}): StarterGameServices {
|
|
25
|
+
const baseUrl = import.meta.env.VITE_MPGD_GAME_SERVICES_URL;
|
|
26
|
+
const requestedMode = import.meta.env.VITE_MPGD_GAME_SERVICES_TRANSPORT;
|
|
27
|
+
const storeTarget = readStoreTarget(
|
|
28
|
+
import.meta.env.VITE_MPGD_GAME_SERVICES_TARGET ?? input.gateway.target,
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
if (baseUrl === undefined || baseUrl.length === 0 || storeTarget === null) {
|
|
32
|
+
return {
|
|
33
|
+
mode: 'disabled',
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const mode = requestedMode === 'orpc' ? 'orpc' : 'http';
|
|
38
|
+
const backend =
|
|
39
|
+
mode === 'orpc'
|
|
40
|
+
? createGameServicesOrpcBackendApi(
|
|
41
|
+
createGameServicesOrpcClient({
|
|
42
|
+
url: baseUrl,
|
|
43
|
+
}),
|
|
44
|
+
)
|
|
45
|
+
: createGameServicesHttpBackendApi({
|
|
46
|
+
transport: createGameServicesFetchBackendTransport({
|
|
47
|
+
baseUrl,
|
|
48
|
+
}),
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
mode,
|
|
53
|
+
baseUrl,
|
|
54
|
+
target: storeTarget,
|
|
55
|
+
client: createGameServicesClient({
|
|
56
|
+
gateway: input.gateway,
|
|
57
|
+
backend,
|
|
58
|
+
playerId: input.playerId,
|
|
59
|
+
target: storeTarget,
|
|
60
|
+
}),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function readStoreTarget(target: PlatformTarget | string): GameServicesStoreTarget | null {
|
|
65
|
+
if (target === 'android' || target === 'ios' || target === 'ait') {
|
|
66
|
+
return target;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import type { AdPlacements, ProductCatalog } from '@mpgd/catalog';
|
|
2
|
+
import adPlacementsJson from '@mpgd/catalog/placements.json';
|
|
3
|
+
import productCatalogJson from '@mpgd/catalog/catalog.json';
|
|
4
|
+
import type { PlatformGateway } from '@mpgd/platform';
|
|
5
|
+
import {
|
|
6
|
+
createEffectiveTargetConfig,
|
|
7
|
+
getTargetConfig,
|
|
8
|
+
targetConfigKeyForPlatform,
|
|
9
|
+
withTargetAvailability,
|
|
10
|
+
type TargetConfigMatrix,
|
|
11
|
+
type TargetConfiguredGateway,
|
|
12
|
+
} from '@mpgd/target-config';
|
|
13
|
+
import targetConfigMatrixJson from '@mpgd/target-config/targets.json';
|
|
14
|
+
|
|
15
|
+
import type { RuntimeConfig } from './runtimeDetector';
|
|
16
|
+
|
|
17
|
+
const devvitSandboxBuildId = 'devvit-sandbox';
|
|
18
|
+
const targetConfigMatrix = targetConfigMatrixJson as TargetConfigMatrix;
|
|
19
|
+
const productCatalog = productCatalogJson as ProductCatalog;
|
|
20
|
+
const adPlacements = adPlacementsJson as AdPlacements;
|
|
21
|
+
const targetAdPlacements = adPlacements.placements.map((placement) => ({
|
|
22
|
+
id: placement.id,
|
|
23
|
+
type: placement.type,
|
|
24
|
+
}));
|
|
25
|
+
const adPlacementTypes = new Map<string, 'rewarded' | 'interstitial'>(
|
|
26
|
+
targetAdPlacements.map((placement) => [placement.id, placement.type]),
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
export async function installPlatform(runtime: RuntimeConfig): Promise<TargetConfiguredGateway> {
|
|
30
|
+
let gateway: PlatformGateway;
|
|
31
|
+
|
|
32
|
+
switch (runtime.target) {
|
|
33
|
+
case 'android':
|
|
34
|
+
case 'ios': {
|
|
35
|
+
const { createCapacitorPlatformGateway } = await import('@mpgd/adapter-capacitor');
|
|
36
|
+
gateway = createCapacitorPlatformGateway({
|
|
37
|
+
target: runtime.target,
|
|
38
|
+
appVersion: runtime.appVersion,
|
|
39
|
+
buildId: runtime.buildId,
|
|
40
|
+
});
|
|
41
|
+
break;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
case 'ait': {
|
|
45
|
+
const { createAitPlatformGateway, createAitSandboxBridge } = await import(
|
|
46
|
+
'@mpgd/adapter-ait'
|
|
47
|
+
);
|
|
48
|
+
gateway = createAitPlatformGateway({
|
|
49
|
+
appVersion: runtime.appVersion,
|
|
50
|
+
buildId: runtime.buildId,
|
|
51
|
+
...(runtime.debug ? { fallbackBridge: createAitSandboxBridge() } : {}),
|
|
52
|
+
});
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
case 'reddit': {
|
|
57
|
+
const { createDevvitPlatformGateway, createDevvitSandboxBridge } = await import(
|
|
58
|
+
'@mpgd/adapter-devvit'
|
|
59
|
+
);
|
|
60
|
+
gateway = createDevvitPlatformGateway({
|
|
61
|
+
appVersion: runtime.appVersion,
|
|
62
|
+
buildId: runtime.buildId,
|
|
63
|
+
...(shouldUseDevvitSandbox(runtime) ? { fallbackBridge: createDevvitSandboxBridge() } : {}),
|
|
64
|
+
});
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
default: {
|
|
69
|
+
const { createBrowserPlatformGateway } = await import('@mpgd/adapter-browser');
|
|
70
|
+
gateway = createBrowserPlatformGateway();
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const configTarget = targetConfigKeyForPlatform(runtime.target);
|
|
75
|
+
const targetConfig = getTargetConfig(targetConfigMatrix, configTarget);
|
|
76
|
+
const effectiveConfig = createEffectiveTargetConfig({
|
|
77
|
+
target: configTarget,
|
|
78
|
+
targetConfigVersion: targetConfigMatrix.version,
|
|
79
|
+
config: targetConfig,
|
|
80
|
+
catalog: productCatalog,
|
|
81
|
+
adPlacements,
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
return withTargetAvailability(gateway, targetConfig, {
|
|
85
|
+
configTarget,
|
|
86
|
+
effectiveConfig,
|
|
87
|
+
adPlacements: targetAdPlacements,
|
|
88
|
+
resolveAdPlacementType(placementId) {
|
|
89
|
+
return adPlacementTypes.get(placementId);
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function shouldUseDevvitSandbox(runtime: RuntimeConfig): boolean {
|
|
95
|
+
return runtime.debug && runtime.buildId === devvitSandboxBuildId;
|
|
96
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { PlatformTarget } from '@mpgd/platform';
|
|
2
|
+
|
|
3
|
+
const validTargets = new Set<string>([
|
|
4
|
+
'android',
|
|
5
|
+
'ios',
|
|
6
|
+
'ait',
|
|
7
|
+
'reddit',
|
|
8
|
+
'telegram',
|
|
9
|
+
'tauri',
|
|
10
|
+
] satisfies readonly PlatformTarget[]);
|
|
11
|
+
|
|
12
|
+
export interface RuntimeConfig {
|
|
13
|
+
readonly target: PlatformTarget;
|
|
14
|
+
readonly appVersion: string;
|
|
15
|
+
readonly buildId: string;
|
|
16
|
+
readonly debug: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function detectRuntime(): RuntimeConfig {
|
|
20
|
+
return {
|
|
21
|
+
target: normalizeTarget(__APP_TARGET__),
|
|
22
|
+
appVersion: __APP_VERSION__,
|
|
23
|
+
buildId: __BUILD_ID__,
|
|
24
|
+
debug: __DEBUG_BUILD__,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function normalizeTarget(value: string): PlatformTarget {
|
|
29
|
+
return validTargets.has(value) ? (value as PlatformTarget) : 'browser';
|
|
30
|
+
}
|