@mpgd/cli 0.36.2 → 0.38.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/build-targets.d.ts +1 -1
- package/dist/build-targets.js +2 -0
- package/dist/index.js +3 -0
- package/dist/native-build-info.json +2 -2
- package/dist/native-build-target.js +214 -146
- package/package.json +14 -13
- package/templates/phaser-game/agent/acceptance.md +13 -0
- package/templates/phaser-game/agent/brief.md +9 -0
- package/templates/phaser-game/agent/game-manifest.json +30 -2
- package/templates/phaser-game/docs/MPGD_KIT_WORKFLOWS.md +16 -0
- package/templates/phaser-game/mpgd.targets.json +11 -0
- package/templates/phaser-game/package.json +2 -1
- package/templates/phaser-game/src/main.ts +43 -8
- package/templates/phaser-game/src/platform/buildGateways/crazygames.ts +14 -0
- package/templates/phaser-game/src/platform/gameServices.ts +12 -0
- package/templates/phaser-game/src/platform/runtimeDetector.ts +1 -0
- package/templates/phaser-game/src/runtime/createGame.ts +27 -0
- package/templates/phaser-game/src/runtime/gameContext.ts +2 -0
- package/templates/phaser-game/src/scenes/BootScene.ts +4 -1
- package/templates/phaser-game/src/scenes/PlayScene.ts +38 -6
- package/templates/phaser-game/vite.runtime-target-config.ts +2 -0
- package/templates/phaser-game/vite.shared.ts +7 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mpgd/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.38.0",
|
|
4
4
|
"description": "Gunshi CLI for mpgd-kit starter generation and target workflow orchestration.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -54,8 +54,8 @@
|
|
|
54
54
|
"typia": "13.0.2",
|
|
55
55
|
"undici": "^7.29.1",
|
|
56
56
|
"vite": "8.1.3",
|
|
57
|
-
"@mpgd/catalog": "0.7.
|
|
58
|
-
"@mpgd/target-config": "0.
|
|
57
|
+
"@mpgd/catalog": "0.7.8",
|
|
58
|
+
"@mpgd/target-config": "0.18.0",
|
|
59
59
|
"@mpgd/phaser-assets": "0.7.0"
|
|
60
60
|
},
|
|
61
61
|
"devDependencies": {
|
|
@@ -63,16 +63,17 @@
|
|
|
63
63
|
"@types/semver": "^7.8.0",
|
|
64
64
|
"ttsc": "0.30.4",
|
|
65
65
|
"typescript": "7.0.2",
|
|
66
|
-
"@mpgd/adapter-ait": "0.
|
|
67
|
-
"@mpgd/adapter-browser": "0.
|
|
68
|
-
"@mpgd/adapter-capacitor": "0.
|
|
69
|
-
"@mpgd/adapter-devvit": "0.9.
|
|
70
|
-
"@mpgd/adapter-verse8": "0.
|
|
71
|
-
"@mpgd/
|
|
72
|
-
"@mpgd/
|
|
73
|
-
"@mpgd/
|
|
74
|
-
"@mpgd/
|
|
75
|
-
"@mpgd/
|
|
66
|
+
"@mpgd/adapter-ait": "0.13.1",
|
|
67
|
+
"@mpgd/adapter-browser": "0.8.0",
|
|
68
|
+
"@mpgd/adapter-capacitor": "0.7.1",
|
|
69
|
+
"@mpgd/adapter-devvit": "0.9.16",
|
|
70
|
+
"@mpgd/adapter-verse8": "0.4.1",
|
|
71
|
+
"@mpgd/analytics": "0.4.2",
|
|
72
|
+
"@mpgd/game-runtime": "0.4.0",
|
|
73
|
+
"@mpgd/bridge": "0.11.0",
|
|
74
|
+
"@mpgd/game-services": "0.18.1",
|
|
75
|
+
"@mpgd/i18n": "0.6.7",
|
|
76
|
+
"@mpgd/platform": "0.16.0"
|
|
76
77
|
},
|
|
77
78
|
"main": "./dist/index.js",
|
|
78
79
|
"types": "./dist/index.d.ts",
|
|
@@ -63,3 +63,16 @@ locked orientation modes as soft prompts instead of unsafe WebView hard locks.
|
|
|
63
63
|
Verify safe-area geometry is reserved exactly once: CSS-padded containers must
|
|
64
64
|
not also subtract `safeArea.contentBounds`, while full-bleed game surfaces use
|
|
65
65
|
the snapshot bounds for persistent controls.
|
|
66
|
+
|
|
67
|
+
## Shared native presentation
|
|
68
|
+
|
|
69
|
+
- Purchase, rewarded, and interstitial requests use the same game-owned scope.
|
|
70
|
+
- Native closure resumes gameplay while backend verification can remain pending.
|
|
71
|
+
- Timeouts preserve unknown occupancy and late native/recovery observers.
|
|
72
|
+
- Scene shutdown removes its action view and keyboard handler; it never unmutes
|
|
73
|
+
a live ad or updates a replacement scene from an old Promise.
|
|
74
|
+
- Background and settings ownership remain after another native lease closes.
|
|
75
|
+
- Rewarded v2 is unavailable until a durable encrypted journal is supplied via
|
|
76
|
+
bootstrap monetization ports. A test memory store is not durable recovery.
|
|
77
|
+
- Use the real adapter's SDK-driven conformance tests and a game-owned platform
|
|
78
|
+
test environment; generic unit tests do not certify live ad delivery.
|
|
@@ -39,3 +39,12 @@ the game-owned PWA before package generation, then run package acceptance on
|
|
|
39
39
|
Windows before Partner Center submission. WACK remains an optional recommended
|
|
40
40
|
evidence source, not a universal prerequisite.
|
|
41
41
|
<!-- mpgd:microsoft-store:end -->
|
|
42
|
+
|
|
43
|
+
## Game-owned execution and advertising
|
|
44
|
+
|
|
45
|
+
Construct one `createGamePlatformRuntime` during bootstrap. Scenes bind gameplay
|
|
46
|
+
execution with `audioOwner: 'game'`; the Phaser game's sound manager owns audio
|
|
47
|
+
projection. Add SDKs only in target assembly or adapters. All rewarded views use
|
|
48
|
+
the game-owned action coordinator and authoritative services, with an
|
|
49
|
+
application-owned durable encrypted operation store. Keep recovery above scenes
|
|
50
|
+
and reconcile existing operations on startup and resume without opening SDK UI.
|
|
@@ -5,8 +5,20 @@
|
|
|
5
5
|
"title": "__GAME_TITLE__",
|
|
6
6
|
"engine": "phaser-4"
|
|
7
7
|
},
|
|
8
|
-
"targets": [
|
|
9
|
-
|
|
8
|
+
"targets": [
|
|
9
|
+
"web-preview",
|
|
10
|
+
"microsoft-store",
|
|
11
|
+
"verse8",
|
|
12
|
+
"crazygames",
|
|
13
|
+
"android",
|
|
14
|
+
"ios",
|
|
15
|
+
"ait",
|
|
16
|
+
"reddit"
|
|
17
|
+
],
|
|
18
|
+
"futureTargets": [
|
|
19
|
+
"telegram",
|
|
20
|
+
"tauri"
|
|
21
|
+
],
|
|
10
22
|
"platformBoundary": "PlatformGateway",
|
|
11
23
|
"agentWorkflow": {
|
|
12
24
|
"guide": "docs/MPGD_KIT_WORKFLOWS.md",
|
|
@@ -67,6 +79,22 @@
|
|
|
67
79
|
{
|
|
68
80
|
"id": "telemetry.analytics.best-effort",
|
|
69
81
|
"owner": "src/main.ts"
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
"id": "runtime.game.owned-presentation",
|
|
85
|
+
"owner": "src/main.ts",
|
|
86
|
+
"capabilities": [
|
|
87
|
+
"game-owned-execution",
|
|
88
|
+
"shared-full-screen-presentation",
|
|
89
|
+
"late-claim-recovery",
|
|
90
|
+
"game-owned-audio"
|
|
91
|
+
],
|
|
92
|
+
"gotchas": [
|
|
93
|
+
"Create one game runtime above scene lifetimes; scene teardown must not release native presentation or global audio.",
|
|
94
|
+
"Rewarded v2 requires an application-owned durable encrypted operation journal; never substitute an in-memory map.",
|
|
95
|
+
"SDK earning and physical closure are separate from an authoritative backend ledger grant.",
|
|
96
|
+
"Recover existing claims on startup and resume without reopening SDK UI."
|
|
97
|
+
]
|
|
70
98
|
}
|
|
71
99
|
],
|
|
72
100
|
"acceptance": [
|
|
@@ -104,6 +104,22 @@ state behind target services. Treat client commerce as pending; authoritative
|
|
|
104
104
|
grants belong in a game-owned Agent8 server purchase event with consume-once
|
|
105
105
|
catalog handling and verified completion logic for leaderboards.
|
|
106
106
|
|
|
107
|
+
### CrazyGames
|
|
108
|
+
|
|
109
|
+
Use the browser adapter's `/crazygames` entry point and the non-installable
|
|
110
|
+
`crazygames` web target. `crazyGamesLaunch: "basic"` disables monetization;
|
|
111
|
+
change it to `"full"` only after Full Launch approval. Configured midgame
|
|
112
|
+
interstitials use the shared advertising presentation contract. SDK callbacks
|
|
113
|
+
must never grant rewards or purchases: those capabilities remain disabled.
|
|
114
|
+
|
|
115
|
+
Playable scenes own gameplay scopes through the game runtime. Keep loading,
|
|
116
|
+
gameplay, and host `muteAudio` policy behind `PlatformGateway`; the host handles
|
|
117
|
+
focus reporting. Build and smoke with `pnpm exec mpgd target build crazygames`
|
|
118
|
+
and `pnpm exec mpgd target smoke crazygames`, providing `--kit-path` and the
|
|
119
|
+
project's `--targets-file`. See the kit's
|
|
120
|
+
[CrazyGames guide](https://github.com/imjlk/mpgd-kit/blob/main/docs/CRAZYGAMES.md)
|
|
121
|
+
and [official SDK documentation](https://docs.crazygames.com/sdk/intro/).
|
|
122
|
+
|
|
107
123
|
### Apps in Toss
|
|
108
124
|
|
|
109
125
|
Use official Apps in Toss documentation/MCP before SDK or review-flow changes.
|
|
@@ -22,6 +22,17 @@
|
|
|
22
22
|
"icon": { "profile": "web-preview" },
|
|
23
23
|
"output": "artifacts/verse8"
|
|
24
24
|
},
|
|
25
|
+
"crazygames": {
|
|
26
|
+
"kind": "web",
|
|
27
|
+
"gameApp": ".",
|
|
28
|
+
"adapter": "crazygames",
|
|
29
|
+
"icon": {
|
|
30
|
+
"profile": "web-preview"
|
|
31
|
+
},
|
|
32
|
+
"installable": false,
|
|
33
|
+
"crazyGamesLaunch": "basic",
|
|
34
|
+
"output": "artifacts/crazygames"
|
|
35
|
+
},
|
|
25
36
|
"android": {
|
|
26
37
|
"kind": "capacitor-android",
|
|
27
38
|
"gameApp": ".",
|
|
@@ -72,7 +72,8 @@
|
|
|
72
72
|
"@mpgd/platform": "__MPGD_DEPENDENCY_VERSION_PLATFORM__",
|
|
73
73
|
"@mpgd/phaser-assets": "__MPGD_DEPENDENCY_VERSION_PHASER_ASSETS__",
|
|
74
74
|
"@mpgd/target-config": "__MPGD_DEPENDENCY_VERSION_TARGET_CONFIG__",
|
|
75
|
-
"phaser": "4.2.0"
|
|
75
|
+
"phaser": "4.2.0",
|
|
76
|
+
"@mpgd/game-runtime": "__MPGD_DEPENDENCY_VERSION_GAME_RUNTIME__"
|
|
76
77
|
},
|
|
77
78
|
"devDependencies": {
|
|
78
79
|
"@ait-co/console-cli": "^0.1.49",
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
import { createGamePlatformRuntime, type GamePlatformRuntime } from '@mpgd/game-runtime/game';
|
|
1
2
|
import './styles.css';
|
|
2
3
|
|
|
3
4
|
import { createAnalyticsReporter, createBufferedAnalyticsSink } from '@mpgd/analytics';
|
|
4
5
|
import { resolveTargetMpgdLocale, type Locale } from '@mpgd/i18n';
|
|
5
6
|
import type { IdentitySession, LaunchIntent, PlatformGateway } from '@mpgd/platform';
|
|
6
7
|
import {
|
|
8
|
+
getEffectiveAdPlacementConfig,
|
|
7
9
|
measureTargetViewport,
|
|
8
10
|
resolveTargetViewportSnapshot,
|
|
9
11
|
waitForTargetViewportMeasurement,
|
|
@@ -15,14 +17,15 @@ import { t } from './i18n/messages';
|
|
|
15
17
|
import { createClientId } from './runtime/id';
|
|
16
18
|
import { createStarterGame } from './runtime/createGame';
|
|
17
19
|
import { detectRuntime } from './platform/runtimeDetector';
|
|
18
|
-
import { createStarterGameServices } from './platform/gameServices';
|
|
20
|
+
import { createStarterGameServices, type StarterMonetizationPorts } from './platform/gameServices';
|
|
19
21
|
import { installPlatform } from './platform/installPlatform';
|
|
20
22
|
import { installMicrosoftStorePwa } from './platform/microsoftStorePwa';
|
|
21
23
|
|
|
22
24
|
await bootstrap();
|
|
23
25
|
|
|
24
26
|
/** Install target services, analytics, and viewport state before starting the game. */
|
|
25
|
-
async function bootstrap(): Promise<void> {
|
|
27
|
+
async function bootstrap(options: { readonly monetization?: StarterMonetizationPorts } = {}): Promise<void> {
|
|
28
|
+
let gameRuntime: GamePlatformRuntime | undefined;
|
|
26
29
|
let locale: Locale = 'en';
|
|
27
30
|
let disposeMicrosoftStorePwa: (() => void) | undefined;
|
|
28
31
|
|
|
@@ -71,13 +74,43 @@ async function bootstrap(): Promise<void> {
|
|
|
71
74
|
sessionId: analyticsSessionId,
|
|
72
75
|
sink: analyticsSink,
|
|
73
76
|
});
|
|
74
|
-
const
|
|
77
|
+
const ownedRuntime = createGamePlatformRuntime({
|
|
75
78
|
gateway: platform,
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
79
|
+
initialLifecycleState: document.visibilityState === 'hidden' ? 'inactive' : 'active',
|
|
80
|
+
canShow: (placement) => {
|
|
81
|
+
const enabled = placement.format === 'rewarded'
|
|
82
|
+
? runtime.config.features.rewardedAds
|
|
83
|
+
: runtime.config.features.interstitialAds;
|
|
84
|
+
const configured = runtime.effectiveConfig === undefined
|
|
85
|
+
? undefined
|
|
86
|
+
: getEffectiveAdPlacementConfig(runtime.effectiveConfig, placement.placementId);
|
|
87
|
+
return enabled && (runtime.effectiveConfig === undefined || configured !== undefined && configured.reason !== 'target-disabled');
|
|
88
|
+
},
|
|
89
|
+
deadline: {
|
|
90
|
+
milliseconds: 30000,
|
|
91
|
+
schedule(callback, milliseconds) {
|
|
92
|
+
const timer = setTimeout(callback, milliseconds);
|
|
93
|
+
return () => clearTimeout(timer);
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
...(options.monetization?.reconciliation === undefined ? {} : { reconciliation: options.monetization.reconciliation }),
|
|
97
|
+
...(options.monetization?.purchasePresentation === undefined ? {} : { purchasePresentation: options.monetization.purchasePresentation }),
|
|
98
|
+
onObserverError: (error) => {
|
|
99
|
+
console.error('[game-runtime]', error);
|
|
100
|
+
},
|
|
101
|
+
createServices: (gateway) =>
|
|
102
|
+
createStarterGameServices({
|
|
103
|
+
gateway,
|
|
104
|
+
playerId: identitySession.playerId ?? player.playerId,
|
|
105
|
+
configTarget: runtime.configTarget,
|
|
106
|
+
...(options.monetization === undefined ? {} : { operationStore: options.monetization.operationStore }),
|
|
107
|
+
analytics: analyticsSink,
|
|
108
|
+
analyticsSessionId,
|
|
109
|
+
}),
|
|
80
110
|
});
|
|
111
|
+
gameRuntime = ownedRuntime;
|
|
112
|
+
const gameServices = ownedRuntime.services;
|
|
113
|
+
await ownedRuntime.reconcile();
|
|
81
114
|
|
|
82
115
|
await analytics.track({
|
|
83
116
|
name: 'game_started',
|
|
@@ -92,7 +125,8 @@ async function bootstrap(): Promise<void> {
|
|
|
92
125
|
preserveBrowserTouchGestures:
|
|
93
126
|
document.body.dataset.mpgdPreserveBrowserTouchGestures === 'true',
|
|
94
127
|
context: {
|
|
95
|
-
platform,
|
|
128
|
+
platform: ownedRuntime.gateway,
|
|
129
|
+
gameRuntime: ownedRuntime,
|
|
96
130
|
runtime,
|
|
97
131
|
viewport,
|
|
98
132
|
player,
|
|
@@ -105,6 +139,7 @@ async function bootstrap(): Promise<void> {
|
|
|
105
139
|
},
|
|
106
140
|
});
|
|
107
141
|
} catch (error) {
|
|
142
|
+
gameRuntime?.dispose();
|
|
108
143
|
disposeMicrosoftStorePwa?.();
|
|
109
144
|
renderBootstrapError(error, locale);
|
|
110
145
|
console.error('[bootstrap]', error);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { createCrazyGamesPlatformGateway } from '@mpgd/adapter-browser/crazygames';
|
|
2
|
+
import { resolveAdPlacementPlatformId, type AdPlacements } from '@mpgd/catalog';
|
|
3
|
+
import adPlacementsJson from '@mpgd/catalog/placements.json';
|
|
4
|
+
import type { PlatformGateway } from '@mpgd/platform';
|
|
5
|
+
import type { RuntimeConfig } from '../runtimeDetector';
|
|
6
|
+
|
|
7
|
+
export async function createBuildGateway(runtime: RuntimeConfig): Promise<PlatformGateway> {
|
|
8
|
+
const placements = (adPlacementsJson as AdPlacements).placements;
|
|
9
|
+
return createCrazyGamesPlatformGateway({
|
|
10
|
+
launch: __MPGD_PLATFORM_TARGET__?.crazyGamesLaunch ?? 'basic',
|
|
11
|
+
placementIds: placements.filter((placement) => placement.type === 'interstitial'
|
|
12
|
+
&& resolveAdPlacementPlatformId(placement, runtime.configTarget) !== undefined).map((placement) => placement.id),
|
|
13
|
+
});
|
|
14
|
+
}
|
|
@@ -5,8 +5,18 @@ import {
|
|
|
5
5
|
resolveGameServicesTransport,
|
|
6
6
|
type GameServicesRuntime,
|
|
7
7
|
type GameServicesRuntimeMode,
|
|
8
|
+
type MonetizationOperationStore,
|
|
8
9
|
} from '@mpgd/game-services';
|
|
9
10
|
import type { PlatformGateway } from '@mpgd/platform';
|
|
11
|
+
import type { GameActionReconciliationPort } from '@mpgd/game-runtime/actions';
|
|
12
|
+
import type { PurchasePresentationEvent } from '@mpgd/game-runtime/ads';
|
|
13
|
+
|
|
14
|
+
/** Supply a game-owned durable encrypted journal and trusted recovery/native facts. */
|
|
15
|
+
export interface StarterMonetizationPorts {
|
|
16
|
+
readonly operationStore: MonetizationOperationStore;
|
|
17
|
+
readonly reconciliation?: GameActionReconciliationPort;
|
|
18
|
+
readonly purchasePresentation?: { subscribe(listener: (event: PurchasePresentationEvent) => void): () => void };
|
|
19
|
+
}
|
|
10
20
|
|
|
11
21
|
export type StarterBackendMode = GameServicesRuntimeMode;
|
|
12
22
|
export type StarterGameServices = GameServicesRuntime;
|
|
@@ -15,12 +25,14 @@ export function createStarterGameServices(input: {
|
|
|
15
25
|
readonly gateway: PlatformGateway;
|
|
16
26
|
readonly playerId: string;
|
|
17
27
|
readonly configTarget: string;
|
|
28
|
+
readonly operationStore?: MonetizationOperationStore;
|
|
18
29
|
readonly analytics?: AnalyticsSink;
|
|
19
30
|
readonly analyticsSessionId?: string;
|
|
20
31
|
}): StarterGameServices {
|
|
21
32
|
return createGameServicesRuntime({
|
|
22
33
|
gateway: input.gateway,
|
|
23
34
|
playerId: input.playerId,
|
|
35
|
+
...(input.operationStore === undefined ? {} : { operationStore: input.operationStore }),
|
|
24
36
|
...(input.analytics === undefined ? {} : { analytics: input.analytics }),
|
|
25
37
|
...(input.analyticsSessionId === undefined
|
|
26
38
|
? {}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { bindGameAudio } from '@mpgd/game-runtime/audio';
|
|
1
2
|
import Phaser from 'phaser';
|
|
2
3
|
|
|
3
4
|
import { BootScene } from '../scenes/BootScene';
|
|
@@ -26,9 +27,35 @@ export function createStarterGame(input: {
|
|
|
26
27
|
autoCenter: Phaser.Scale.CENTER_BOTH,
|
|
27
28
|
},
|
|
28
29
|
scene: [BootScene, LobbyScene, PlayScene],
|
|
30
|
+
callbacks: {
|
|
31
|
+
postBoot(game: Phaser.Game) {
|
|
32
|
+
game.registry.set(starterContextKey, input.context);
|
|
33
|
+
ownGameAudio(game, input.context);
|
|
34
|
+
},
|
|
35
|
+
},
|
|
29
36
|
});
|
|
30
37
|
|
|
31
38
|
game.registry.set(starterContextKey, input.context);
|
|
32
39
|
|
|
33
40
|
return game;
|
|
34
41
|
}
|
|
42
|
+
|
|
43
|
+
/** The sound manager and execution projection belong to the game, not a scene. */
|
|
44
|
+
function ownGameAudio(game: Phaser.Game, context: StarterContext): void {
|
|
45
|
+
const audio = bindGameAudio({
|
|
46
|
+
execution: context.gameRuntime.execution,
|
|
47
|
+
sink: {
|
|
48
|
+
getMuted: () => game.sound.mute,
|
|
49
|
+
setMuted: (muted) => {
|
|
50
|
+
game.sound.mute = muted;
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
onError: (error) => {
|
|
54
|
+
console.error('[game-audio]', error);
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
game.events.once('destroy', () => {
|
|
58
|
+
context.gameRuntime.dispose();
|
|
59
|
+
audio.dispose();
|
|
60
|
+
});
|
|
61
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AnalyticsReporter, BufferedAnalyticsSink } from '@mpgd/analytics';
|
|
2
|
+
import type { GamePlatformRuntime } from '@mpgd/game-runtime/game';
|
|
2
3
|
import type { Locale } from '@mpgd/i18n';
|
|
3
4
|
import type { IdentitySession, LaunchIntent, PlayerIdentity } from '@mpgd/platform';
|
|
4
5
|
import type {
|
|
@@ -12,6 +13,7 @@ import type { StarterGameServices } from '../platform/gameServices';
|
|
|
12
13
|
export const starterContextKey = 'starterContext';
|
|
13
14
|
|
|
14
15
|
export interface StarterContext {
|
|
16
|
+
readonly gameRuntime: GamePlatformRuntime<TargetConfiguredGateway, StarterGameServices>;
|
|
15
17
|
readonly platform: TargetConfiguredGateway;
|
|
16
18
|
readonly runtime: TargetRuntimeSnapshot;
|
|
17
19
|
readonly viewport: TargetViewportSnapshot;
|
|
@@ -2,6 +2,8 @@ import Phaser from 'phaser';
|
|
|
2
2
|
|
|
3
3
|
import { loadPhaserAssets } from '@mpgd/phaser-assets';
|
|
4
4
|
|
|
5
|
+
import { starterContextKey, type StarterContext } from '../runtime/gameContext';
|
|
6
|
+
|
|
5
7
|
import { starterAssets } from '../assets/manifest';
|
|
6
8
|
|
|
7
9
|
export class BootScene extends Phaser.Scene {
|
|
@@ -14,6 +16,7 @@ export class BootScene extends Phaser.Scene {
|
|
|
14
16
|
}
|
|
15
17
|
|
|
16
18
|
create(): void {
|
|
17
|
-
this.
|
|
19
|
+
const context = this.registry.get(starterContextKey) as StarterContext;
|
|
20
|
+
this.scene.start(context.launchIntent.entry === 'free-play' ? 'PlayScene' : 'LobbyScene');
|
|
18
21
|
}
|
|
19
22
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { GameActionController } from '@mpgd/game-runtime/actions';
|
|
2
|
+
import { bindPhaserGameScene } from '@mpgd/game-runtime/phaser';
|
|
1
3
|
import Phaser from 'phaser';
|
|
2
4
|
|
|
3
5
|
import type { LogicalAdPlacementId } from '@mpgd/platform';
|
|
@@ -17,6 +19,7 @@ export class PlayScene extends Phaser.Scene {
|
|
|
17
19
|
private rewardText!: Phaser.GameObjects.Text;
|
|
18
20
|
private analyticsText!: Phaser.GameObjects.Text;
|
|
19
21
|
private context!: StarterContext;
|
|
22
|
+
private rewardedAction: GameActionController<'rewarded-ad'> | undefined;
|
|
20
23
|
private lastScore = -1;
|
|
21
24
|
private lastAnalyticsCount = -1;
|
|
22
25
|
|
|
@@ -26,6 +29,19 @@ export class PlayScene extends Phaser.Scene {
|
|
|
26
29
|
|
|
27
30
|
create(): void {
|
|
28
31
|
this.context = this.registry.get(starterContextKey) as StarterContext;
|
|
32
|
+
bindPhaserGameScene({
|
|
33
|
+
controller: this.context.gameRuntime.execution,
|
|
34
|
+
gameplayScope: this.context.gameRuntime.createGameplayScope(),
|
|
35
|
+
scene: this,
|
|
36
|
+
renderingPolicy: 'visibility',
|
|
37
|
+
audioOwner: 'game',
|
|
38
|
+
resetInput: () => {
|
|
39
|
+
this.input.keyboard?.resetKeys();
|
|
40
|
+
},
|
|
41
|
+
onUnsupportedState: (_snapshot, reason) => {
|
|
42
|
+
console.error('[game-execution]', reason);
|
|
43
|
+
},
|
|
44
|
+
});
|
|
29
45
|
|
|
30
46
|
this.marker = this.add.image(480, 250, starterAssetKeys.marker).setDisplaySize(72, 72);
|
|
31
47
|
this.scoreText = this.add
|
|
@@ -50,8 +66,16 @@ export class PlayScene extends Phaser.Scene {
|
|
|
50
66
|
})
|
|
51
67
|
.setOrigin(0.5);
|
|
52
68
|
|
|
53
|
-
this.
|
|
69
|
+
this.rewardedAction = this.context.gameRuntime.actions?.createRewardedAdController();
|
|
70
|
+
const action = this.rewardedAction;
|
|
71
|
+
const keyboard = this.input.keyboard;
|
|
72
|
+
const onReward = () => {
|
|
54
73
|
void this.requestRewardedAd();
|
|
74
|
+
};
|
|
75
|
+
keyboard?.on('keydown-R', onReward);
|
|
76
|
+
this.events.once('shutdown', () => {
|
|
77
|
+
action?.dispose();
|
|
78
|
+
keyboard?.off('keydown-R', onReward);
|
|
55
79
|
});
|
|
56
80
|
}
|
|
57
81
|
|
|
@@ -77,7 +101,7 @@ export class PlayScene extends Phaser.Scene {
|
|
|
77
101
|
private rewardHint(): string {
|
|
78
102
|
const rewardedAds = this.context.runtime.features.rewardedAds;
|
|
79
103
|
|
|
80
|
-
if (!rewardedAds.enabled || this.context.gameServices.client === undefined) {
|
|
104
|
+
if (!rewardedAds.enabled || this.context.gameServices.client === undefined || this.context.gameServices.monetizationRecovery === undefined) {
|
|
81
105
|
return t(this.context.locale, 'rewardUnavailable');
|
|
82
106
|
}
|
|
83
107
|
|
|
@@ -86,19 +110,25 @@ export class PlayScene extends Phaser.Scene {
|
|
|
86
110
|
|
|
87
111
|
private async requestRewardedAd(): Promise<void> {
|
|
88
112
|
const rewardedAds = this.context.runtime.features.rewardedAds;
|
|
89
|
-
const
|
|
113
|
+
const action = this.rewardedAction;
|
|
90
114
|
|
|
91
|
-
if (!rewardedAds.enabled ||
|
|
115
|
+
if (!rewardedAds.enabled || action === undefined || this.context.gameServices.monetizationRecovery === undefined) {
|
|
92
116
|
this.rewardText.setText(t(this.context.locale, 'rewardUnavailable'));
|
|
93
117
|
return;
|
|
94
118
|
}
|
|
95
119
|
|
|
120
|
+
if (this.context.gameRuntime.actions?.getAvailability() !== 'ready') {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
96
123
|
try {
|
|
97
|
-
const result = await
|
|
124
|
+
const result = await action.execute({
|
|
98
125
|
placementId: rewardedPlacementId,
|
|
99
126
|
idempotencyKey: createClientId('starter-reward'),
|
|
100
127
|
});
|
|
101
128
|
|
|
129
|
+
if (action.isDisposed() || this.rewardedAction !== action) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
102
132
|
this.rewardText.setText(
|
|
103
133
|
result.claim?.granted === true
|
|
104
134
|
? t(this.context.locale, 'reward', { status: result.status })
|
|
@@ -112,7 +142,9 @@ export class PlayScene extends Phaser.Scene {
|
|
|
112
142
|
granted: false,
|
|
113
143
|
},
|
|
114
144
|
});
|
|
115
|
-
|
|
145
|
+
if (!action.isDisposed() && this.rewardedAction === action) {
|
|
146
|
+
this.rewardText.setText(t(this.context.locale, 'rewardError'));
|
|
147
|
+
}
|
|
116
148
|
console.error('[rewarded-ad]', error);
|
|
117
149
|
}
|
|
118
150
|
}
|
|
@@ -20,6 +20,7 @@ const runtimeKinds = valueSet({
|
|
|
20
20
|
'apps-in-toss': true,
|
|
21
21
|
'devvit-web': true,
|
|
22
22
|
'verse8-web': true,
|
|
23
|
+
'crazygames-web': true,
|
|
23
24
|
'wechat-minigame': true,
|
|
24
25
|
'tiktok-minigame': true,
|
|
25
26
|
} satisfies Record<TargetRuntimeKind, true>);
|
|
@@ -32,6 +33,7 @@ const releaseProfiles = valueSet({
|
|
|
32
33
|
'apps-in-toss': true,
|
|
33
34
|
devvit: true,
|
|
34
35
|
verse8: true,
|
|
36
|
+
crazygames: true,
|
|
35
37
|
'wechat-minigame': true,
|
|
36
38
|
'tiktok-minigame': true,
|
|
37
39
|
} satisfies Record<ReleaseProfile, true>);
|
|
@@ -14,6 +14,7 @@ import { resolveTextSelectionMode } from './src/platform/textSelection';
|
|
|
14
14
|
interface RuntimePlatformTargetMetadata {
|
|
15
15
|
readonly kind: string;
|
|
16
16
|
readonly adapter: string;
|
|
17
|
+
readonly crazyGamesLaunch?: 'basic' | 'full';
|
|
17
18
|
readonly authoritativeGameServices?: boolean;
|
|
18
19
|
readonly integrations?: Record<string, unknown>;
|
|
19
20
|
}
|
|
@@ -151,6 +152,8 @@ export function resolveBuildGatewayModule(input: {
|
|
|
151
152
|
return input.debug && input.buildId === devvitSandboxBuildId
|
|
152
153
|
? 'src/platform/buildGateways/redditSandbox.ts'
|
|
153
154
|
: 'src/platform/buildGateways/reddit.ts';
|
|
155
|
+
case 'crazygames':
|
|
156
|
+
return 'src/platform/buildGateways/crazygames.ts';
|
|
154
157
|
case 'verse8':
|
|
155
158
|
return 'src/platform/buildGateways/verse8.ts';
|
|
156
159
|
case 'microsoft-store':
|
|
@@ -245,6 +248,9 @@ function readRuntimePlatformTarget(
|
|
|
245
248
|
throw new Error(`Platform target ${configTarget} must define kind and adapter.`);
|
|
246
249
|
}
|
|
247
250
|
|
|
251
|
+
if (target.crazyGamesLaunch !== undefined && (target.adapter !== 'crazygames' || !['basic', 'full'].includes(target.crazyGamesLaunch as string))) {
|
|
252
|
+
throw new Error(`Invalid CrazyGames launch setting for ${configTarget}.`);
|
|
253
|
+
}
|
|
248
254
|
if (target.integrations !== undefined && !isRecord(target.integrations)) {
|
|
249
255
|
throw new Error(`Platform target ${configTarget} integrations must be an object.`);
|
|
250
256
|
}
|
|
@@ -262,6 +268,7 @@ function readRuntimePlatformTarget(
|
|
|
262
268
|
...(target.authoritativeGameServices === undefined
|
|
263
269
|
? {}
|
|
264
270
|
: { authoritativeGameServices: target.authoritativeGameServices }),
|
|
271
|
+
...(target.crazyGamesLaunch === undefined ? {} : { crazyGamesLaunch: target.crazyGamesLaunch as 'basic' | 'full' }),
|
|
265
272
|
...(target.integrations === undefined ? {} : { integrations: target.integrations }),
|
|
266
273
|
};
|
|
267
274
|
}
|