@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
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import Phaser from 'phaser';
|
|
2
|
+
|
|
3
|
+
import { BootScene } from '../scenes/BootScene';
|
|
4
|
+
import { LobbyScene } from '../scenes/LobbyScene';
|
|
5
|
+
import { PlayScene } from '../scenes/PlayScene';
|
|
6
|
+
import { starterContextKey, type StarterContext } from './gameContext';
|
|
7
|
+
|
|
8
|
+
export function createStarterGame(input: {
|
|
9
|
+
readonly mountId: string;
|
|
10
|
+
readonly context: StarterContext;
|
|
11
|
+
}): Phaser.Game {
|
|
12
|
+
const game = new Phaser.Game({
|
|
13
|
+
type: Phaser.AUTO,
|
|
14
|
+
parent: input.mountId,
|
|
15
|
+
width: 960,
|
|
16
|
+
height: 540,
|
|
17
|
+
backgroundColor: '#07111f',
|
|
18
|
+
scale: {
|
|
19
|
+
mode: Phaser.Scale.FIT,
|
|
20
|
+
autoCenter: Phaser.Scale.CENTER_BOTH,
|
|
21
|
+
},
|
|
22
|
+
scene: [BootScene, LobbyScene, PlayScene],
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
game.registry.set(starterContextKey, input.context);
|
|
26
|
+
|
|
27
|
+
return game;
|
|
28
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { AnalyticsReporter, BufferedAnalyticsSink } from '@mpgd/analytics';
|
|
2
|
+
import type { Locale } from '@mpgd/i18n';
|
|
3
|
+
import type { PlayerIdentity } from '@mpgd/platform';
|
|
4
|
+
import type { TargetConfiguredGateway, TargetRuntimeSnapshot } from '@mpgd/target-config';
|
|
5
|
+
|
|
6
|
+
import type { StarterGameServices } from '../platform/gameServices';
|
|
7
|
+
|
|
8
|
+
export const starterContextKey = 'starterContext';
|
|
9
|
+
|
|
10
|
+
export interface StarterContext {
|
|
11
|
+
readonly platform: TargetConfiguredGateway;
|
|
12
|
+
readonly runtime: TargetRuntimeSnapshot;
|
|
13
|
+
readonly player: PlayerIdentity;
|
|
14
|
+
readonly locale: Locale;
|
|
15
|
+
readonly gameServices: StarterGameServices;
|
|
16
|
+
readonly analytics: AnalyticsReporter;
|
|
17
|
+
readonly analyticsSink: BufferedAnalyticsSink;
|
|
18
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import Phaser from 'phaser';
|
|
2
|
+
|
|
3
|
+
import { t } from '../i18n/messages';
|
|
4
|
+
import { starterContextKey, type StarterContext } from '../runtime/gameContext';
|
|
5
|
+
|
|
6
|
+
export class LobbyScene extends Phaser.Scene {
|
|
7
|
+
constructor() {
|
|
8
|
+
super('LobbyScene');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
create(): void {
|
|
12
|
+
const rawContext = this.registry.get(starterContextKey);
|
|
13
|
+
|
|
14
|
+
if (rawContext === undefined || rawContext === null) {
|
|
15
|
+
throw new Error('Starter context is missing from Phaser registry.');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const context = rawContext as StarterContext;
|
|
19
|
+
const locale = context.locale;
|
|
20
|
+
const features = Object.values(context.runtime.features)
|
|
21
|
+
.filter((feature) => feature.enabled)
|
|
22
|
+
.map((feature) => feature.feature)
|
|
23
|
+
.join(', ') || t(locale, 'featuresNone');
|
|
24
|
+
|
|
25
|
+
this.add
|
|
26
|
+
.text(480, 106, t(locale, 'title'), {
|
|
27
|
+
color: '#ffffff',
|
|
28
|
+
fontFamily: 'Arial, sans-serif',
|
|
29
|
+
fontSize: '40px',
|
|
30
|
+
fontStyle: 'bold',
|
|
31
|
+
})
|
|
32
|
+
.setOrigin(0.5);
|
|
33
|
+
this.add
|
|
34
|
+
.text(480, 174, t(locale, 'target', { target: context.runtime.configTarget }), {
|
|
35
|
+
color: '#9fb3c8',
|
|
36
|
+
fontFamily: 'Arial, sans-serif',
|
|
37
|
+
fontSize: '20px',
|
|
38
|
+
})
|
|
39
|
+
.setOrigin(0.5);
|
|
40
|
+
this.add
|
|
41
|
+
.text(480, 216, t(locale, 'features', { features }), {
|
|
42
|
+
color: '#d6dee8',
|
|
43
|
+
fontFamily: 'Arial, sans-serif',
|
|
44
|
+
fontSize: '18px',
|
|
45
|
+
})
|
|
46
|
+
.setOrigin(0.5);
|
|
47
|
+
this.add
|
|
48
|
+
.text(480, 258, t(locale, 'backend', { mode: context.gameServices.mode }), {
|
|
49
|
+
color: context.gameServices.mode === 'disabled' ? '#f59e0b' : '#2dd4bf',
|
|
50
|
+
fontFamily: 'Arial, sans-serif',
|
|
51
|
+
fontSize: '18px',
|
|
52
|
+
})
|
|
53
|
+
.setOrigin(0.5);
|
|
54
|
+
this.add
|
|
55
|
+
.text(480, 336, t(locale, 'tapToStart'), {
|
|
56
|
+
color: '#ffffff',
|
|
57
|
+
fontFamily: 'Arial, sans-serif',
|
|
58
|
+
fontSize: '22px',
|
|
59
|
+
})
|
|
60
|
+
.setOrigin(0.5);
|
|
61
|
+
|
|
62
|
+
this.input.once('pointerdown', () => this.scene.start('PlayScene'));
|
|
63
|
+
this.input.keyboard?.once('keydown-ENTER', () => this.scene.start('PlayScene'));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import Phaser from 'phaser';
|
|
2
|
+
|
|
3
|
+
import type { LogicalAdPlacementId } from '@mpgd/platform';
|
|
4
|
+
|
|
5
|
+
import { createStarterRunState, stepStarterRunState, type StarterRunState } from '../game/state';
|
|
6
|
+
import { t } from '../i18n/messages';
|
|
7
|
+
import { starterContextKey, type StarterContext } from '../runtime/gameContext';
|
|
8
|
+
import { createClientId } from '../runtime/id';
|
|
9
|
+
|
|
10
|
+
const rewardedPlacementId = 'CONTINUE_AFTER_FAIL' satisfies LogicalAdPlacementId;
|
|
11
|
+
|
|
12
|
+
export class PlayScene extends Phaser.Scene {
|
|
13
|
+
private state: StarterRunState = createStarterRunState();
|
|
14
|
+
private marker!: Phaser.GameObjects.Arc;
|
|
15
|
+
private scoreText!: Phaser.GameObjects.Text;
|
|
16
|
+
private rewardText!: Phaser.GameObjects.Text;
|
|
17
|
+
private analyticsText!: Phaser.GameObjects.Text;
|
|
18
|
+
private context!: StarterContext;
|
|
19
|
+
private lastScore = -1;
|
|
20
|
+
private lastAnalyticsCount = -1;
|
|
21
|
+
|
|
22
|
+
constructor() {
|
|
23
|
+
super('PlayScene');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
create(): void {
|
|
27
|
+
this.context = this.registry.get(starterContextKey) as StarterContext;
|
|
28
|
+
|
|
29
|
+
this.marker = this.add.circle(480, 250, 34, 0x2dd4bf);
|
|
30
|
+
this.scoreText = this.add
|
|
31
|
+
.text(480, 86, '', {
|
|
32
|
+
color: '#ffffff',
|
|
33
|
+
fontFamily: 'Arial, sans-serif',
|
|
34
|
+
fontSize: '28px',
|
|
35
|
+
})
|
|
36
|
+
.setOrigin(0.5);
|
|
37
|
+
this.rewardText = this.add
|
|
38
|
+
.text(480, 412, this.rewardHint(), {
|
|
39
|
+
color: '#d6dee8',
|
|
40
|
+
fontFamily: 'Arial, sans-serif',
|
|
41
|
+
fontSize: '18px',
|
|
42
|
+
})
|
|
43
|
+
.setOrigin(0.5);
|
|
44
|
+
this.analyticsText = this.add
|
|
45
|
+
.text(480, 454, '', {
|
|
46
|
+
color: '#9fb3c8',
|
|
47
|
+
fontFamily: 'Arial, sans-serif',
|
|
48
|
+
fontSize: '16px',
|
|
49
|
+
})
|
|
50
|
+
.setOrigin(0.5);
|
|
51
|
+
|
|
52
|
+
this.input.keyboard?.on('keydown-R', () => {
|
|
53
|
+
void this.requestRewardedAd();
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
override update(_time: number, delta: number): void {
|
|
58
|
+
this.state = stepStarterRunState(this.state, delta);
|
|
59
|
+
|
|
60
|
+
const angle = this.state.phase * Math.PI * 2;
|
|
61
|
+
this.marker.setPosition(480 + Math.cos(angle) * 144, 250 + Math.sin(angle) * 54);
|
|
62
|
+
|
|
63
|
+
if (this.state.score !== this.lastScore) {
|
|
64
|
+
this.lastScore = this.state.score;
|
|
65
|
+
this.scoreText.setText(t(this.context.locale, 'score', { score: this.state.score }));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const analyticsCount = this.context.analyticsSink.events.length;
|
|
69
|
+
|
|
70
|
+
if (analyticsCount !== this.lastAnalyticsCount) {
|
|
71
|
+
this.lastAnalyticsCount = analyticsCount;
|
|
72
|
+
this.analyticsText.setText(t(this.context.locale, 'analytics', { count: analyticsCount }));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
private rewardHint(): string {
|
|
77
|
+
const rewardedAds = this.context.runtime.features.rewardedAds;
|
|
78
|
+
|
|
79
|
+
if (!rewardedAds.enabled) {
|
|
80
|
+
return t(this.context.locale, 'rewardUnavailable');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return t(this.context.locale, 'rewardPending');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
private async requestRewardedAd(): Promise<void> {
|
|
87
|
+
const rewardedAds = this.context.runtime.features.rewardedAds;
|
|
88
|
+
|
|
89
|
+
if (!rewardedAds.enabled) {
|
|
90
|
+
this.rewardText.setText(t(this.context.locale, 'rewardUnavailable'));
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
const result = await this.context.platform.ads.showRewarded({
|
|
96
|
+
placementId: rewardedPlacementId,
|
|
97
|
+
idempotencyKey: createClientId('starter-reward'),
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
await this.context.analytics.track({
|
|
101
|
+
name: result.status === 'completed' ? 'rewarded_ad_completed' : 'rewarded_ad_rejected',
|
|
102
|
+
properties: {
|
|
103
|
+
status: result.status,
|
|
104
|
+
granted: result.rewardGranted,
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
this.rewardText.setText(t(this.context.locale, 'reward', { status: result.status }));
|
|
109
|
+
} catch (error) {
|
|
110
|
+
await this.context.analytics.track({
|
|
111
|
+
name: 'rewarded_ad_rejected',
|
|
112
|
+
properties: {
|
|
113
|
+
status: 'failed',
|
|
114
|
+
granted: false,
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
this.rewardText.setText(t(this.context.locale, 'rewardError'));
|
|
118
|
+
console.error('[rewarded-ad]', error);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
html,
|
|
2
|
+
body,
|
|
3
|
+
#game {
|
|
4
|
+
width: 100%;
|
|
5
|
+
height: 100%;
|
|
6
|
+
margin: 0;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
body {
|
|
10
|
+
overflow: hidden;
|
|
11
|
+
background: #07111f;
|
|
12
|
+
color: #ffffff;
|
|
13
|
+
font-family: Arial, sans-serif;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
canvas {
|
|
17
|
+
display: block;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.boot-error {
|
|
21
|
+
box-sizing: border-box;
|
|
22
|
+
width: min(720px, calc(100vw - 32px));
|
|
23
|
+
margin: 20vh auto 0;
|
|
24
|
+
padding: 20px;
|
|
25
|
+
border: 1px solid #dc2626;
|
|
26
|
+
border-radius: 8px;
|
|
27
|
+
background: #1f1111;
|
|
28
|
+
color: #fecaca;
|
|
29
|
+
font-size: 16px;
|
|
30
|
+
line-height: 1.5;
|
|
31
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
__TSCONFIG_EXTENDS_LINE__
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"target": "ES2022",
|
|
5
|
+
"useDefineForClassFields": true,
|
|
6
|
+
"strict": true,
|
|
7
|
+
"strictNullChecks": true,
|
|
8
|
+
"noUncheckedIndexedAccess": true,
|
|
9
|
+
"exactOptionalPropertyTypes": true,
|
|
10
|
+
"noImplicitOverride": true,
|
|
11
|
+
"noFallthroughCasesInSwitch": true,
|
|
12
|
+
"skipLibCheck": true,
|
|
13
|
+
"module": "ESNext",
|
|
14
|
+
"moduleResolution": "Bundler",
|
|
15
|
+
"resolveJsonModule": true,
|
|
16
|
+
"isolatedModules": true,
|
|
17
|
+
"types": ["vite/client", "node"],
|
|
18
|
+
"noEmit": true
|
|
19
|
+
},
|
|
20
|
+
"include": [
|
|
21
|
+
"src/**/*.ts",
|
|
22
|
+
"vite.config.ts"__TSCONFIG_WORKSPACE_INCLUDES__
|
|
23
|
+
],
|
|
24
|
+
"exclude": [
|
|
25
|
+
"dist"__TSCONFIG_WORKSPACE_EXCLUDES__
|
|
26
|
+
]
|
|
27
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import ttsc from '@ttsc/unplugin/vite';
|
|
2
|
+
import { defineConfig } from 'vite';
|
|
3
|
+
|
|
4
|
+
export default defineConfig(({ mode }) => {
|
|
5
|
+
const isProduction = mode === 'production';
|
|
6
|
+
|
|
7
|
+
return {
|
|
8
|
+
plugins: [
|
|
9
|
+
ttsc({
|
|
10
|
+
project: 'tsconfig.json',
|
|
11
|
+
plugins: false,
|
|
12
|
+
}),
|
|
13
|
+
],
|
|
14
|
+
define: {
|
|
15
|
+
__APP_TARGET__: JSON.stringify(process.env.APP_TARGET ?? 'browser'),
|
|
16
|
+
__APP_VERSION__: JSON.stringify(process.env.APP_VERSION ?? '0.0.0-dev'),
|
|
17
|
+
__BUILD_ID__: JSON.stringify(process.env.BUILD_ID ?? 'local'),
|
|
18
|
+
__DEBUG_BUILD__: JSON.stringify(!isProduction),
|
|
19
|
+
},
|
|
20
|
+
build: {
|
|
21
|
+
target: 'es2022',
|
|
22
|
+
sourcemap: !isProduction,
|
|
23
|
+
outDir: 'dist',
|
|
24
|
+
assetsDir: 'assets',
|
|
25
|
+
emptyOutDir: true,
|
|
26
|
+
rolldownOptions: {
|
|
27
|
+
output: {
|
|
28
|
+
entryFileNames: 'assets/game.js',
|
|
29
|
+
chunkFileNames: 'assets/[name].js',
|
|
30
|
+
assetFileNames: 'assets/[name][extname]',
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
});
|