@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mpgd/cli",
3
- "version": "0.36.2",
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.6",
58
- "@mpgd/target-config": "0.17.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.12.7",
67
- "@mpgd/adapter-browser": "0.7.8",
68
- "@mpgd/adapter-capacitor": "0.6.2",
69
- "@mpgd/adapter-devvit": "0.9.14",
70
- "@mpgd/adapter-verse8": "0.3.12",
71
- "@mpgd/bridge": "0.10.0",
72
- "@mpgd/analytics": "0.4.0",
73
- "@mpgd/game-services": "0.17.2",
74
- "@mpgd/i18n": "0.6.5",
75
- "@mpgd/platform": "0.14.0"
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": ["web-preview", "microsoft-store", "verse8", "android", "ios", "ait", "reddit"],
9
- "futureTargets": ["telegram", "tauri"],
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 gameServices = createStarterGameServices({
77
+ const ownedRuntime = createGamePlatformRuntime({
75
78
  gateway: platform,
76
- playerId: identitySession.playerId ?? player.playerId,
77
- configTarget: runtime.configTarget,
78
- analytics: analyticsSink,
79
- analyticsSessionId,
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
  ? {}
@@ -7,6 +7,7 @@ const validTargets = new Set<string>([
7
7
  'microsoft-store',
8
8
  'reddit',
9
9
  'verse8',
10
+ 'crazygames',
10
11
  'telegram',
11
12
  'tauri',
12
13
  'wechat',
@@ -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.scene.start('LobbyScene');
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.input.keyboard?.on('keydown-R', () => {
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 gameServicesClient = this.context.gameServices.client;
113
+ const action = this.rewardedAction;
90
114
 
91
- if (!rewardedAds.enabled || gameServicesClient === undefined) {
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 gameServicesClient.claimRewardedAd({
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
- this.rewardText.setText(t(this.context.locale, 'rewardError'));
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
  }