@mpgd/cli 0.36.1 → 0.37.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 +1 -0
- package/dist/native-build-info.json +2 -2
- package/package.json +15 -14
- 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 +29 -2
- package/templates/phaser-game/package.json +2 -1
- package/templates/phaser-game/src/main.ts +43 -8
- package/templates/phaser-game/src/platform/gameServices.ts +12 -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/PlayScene.ts +37 -6
package/dist/index.js
CHANGED
|
@@ -72,6 +72,7 @@ const mpgdTemplateDependencyPackages = [
|
|
|
72
72
|
{ name: '@mpgd/catalog', packageDir: 'packages/catalog' },
|
|
73
73
|
{ name: '@mpgd/cli', packageDir: 'packages/cli' },
|
|
74
74
|
{ name: '@mpgd/game-services', packageDir: 'packages/game-services' },
|
|
75
|
+
{ name: '@mpgd/game-runtime', packageDir: 'packages/game-runtime' },
|
|
75
76
|
{ name: '@mpgd/i18n', packageDir: 'packages/i18n' },
|
|
76
77
|
{ name: '@mpgd/phaser-assets', packageDir: 'packages/phaser-assets' },
|
|
77
78
|
{ name: '@mpgd/platform', packageDir: 'packages/platform' },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mpgd/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.37.0",
|
|
4
4
|
"description": "Gunshi CLI for mpgd-kit starter generation and target workflow orchestration.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -54,25 +54,26 @@
|
|
|
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/
|
|
59
|
-
"@mpgd/
|
|
57
|
+
"@mpgd/catalog": "0.7.7",
|
|
58
|
+
"@mpgd/phaser-assets": "0.7.0",
|
|
59
|
+
"@mpgd/target-config": "0.17.1"
|
|
60
60
|
},
|
|
61
61
|
"devDependencies": {
|
|
62
62
|
"@types/node": "^24.0.0",
|
|
63
63
|
"@types/semver": "^7.8.0",
|
|
64
64
|
"ttsc": "0.30.4",
|
|
65
65
|
"typescript": "7.0.2",
|
|
66
|
-
"@mpgd/adapter-
|
|
67
|
-
"@mpgd/adapter-
|
|
68
|
-
"@mpgd/adapter-capacitor": "0.
|
|
69
|
-
"@mpgd/
|
|
70
|
-
"@mpgd/adapter-
|
|
71
|
-
"@mpgd/
|
|
72
|
-
"@mpgd/
|
|
73
|
-
"@mpgd/
|
|
74
|
-
"@mpgd/
|
|
75
|
-
"@mpgd/
|
|
66
|
+
"@mpgd/adapter-browser": "0.7.9",
|
|
67
|
+
"@mpgd/adapter-ait": "0.13.0",
|
|
68
|
+
"@mpgd/adapter-capacitor": "0.7.0",
|
|
69
|
+
"@mpgd/analytics": "0.4.1",
|
|
70
|
+
"@mpgd/adapter-verse8": "0.4.0",
|
|
71
|
+
"@mpgd/game-runtime": "0.3.0",
|
|
72
|
+
"@mpgd/i18n": "0.6.6",
|
|
73
|
+
"@mpgd/adapter-devvit": "0.9.15",
|
|
74
|
+
"@mpgd/platform": "0.15.0",
|
|
75
|
+
"@mpgd/bridge": "0.11.0",
|
|
76
|
+
"@mpgd/game-services": "0.18.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,19 @@
|
|
|
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
|
+
"android",
|
|
13
|
+
"ios",
|
|
14
|
+
"ait",
|
|
15
|
+
"reddit"
|
|
16
|
+
],
|
|
17
|
+
"futureTargets": [
|
|
18
|
+
"telegram",
|
|
19
|
+
"tauri"
|
|
20
|
+
],
|
|
10
21
|
"platformBoundary": "PlatformGateway",
|
|
11
22
|
"agentWorkflow": {
|
|
12
23
|
"guide": "docs/MPGD_KIT_WORKFLOWS.md",
|
|
@@ -67,6 +78,22 @@
|
|
|
67
78
|
{
|
|
68
79
|
"id": "telemetry.analytics.best-effort",
|
|
69
80
|
"owner": "src/main.ts"
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
"id": "runtime.game.owned-presentation",
|
|
84
|
+
"owner": "src/main.ts",
|
|
85
|
+
"capabilities": [
|
|
86
|
+
"game-owned-execution",
|
|
87
|
+
"shared-full-screen-presentation",
|
|
88
|
+
"late-claim-recovery",
|
|
89
|
+
"game-owned-audio"
|
|
90
|
+
],
|
|
91
|
+
"gotchas": [
|
|
92
|
+
"Create one game runtime above scene lifetimes; scene teardown must not release native presentation or global audio.",
|
|
93
|
+
"Rewarded v2 requires an application-owned durable encrypted operation journal; never substitute an in-memory map.",
|
|
94
|
+
"SDK earning and physical closure are separate from an authoritative backend ledger grant.",
|
|
95
|
+
"Recover existing claims on startup and resume without reopening SDK UI."
|
|
96
|
+
]
|
|
70
97
|
}
|
|
71
98
|
],
|
|
72
99
|
"acceptance": [
|
|
@@ -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);
|
|
@@ -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;
|
|
@@ -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,18 @@ 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
|
+
scene: this,
|
|
35
|
+
renderingPolicy: 'visibility',
|
|
36
|
+
audioOwner: 'game',
|
|
37
|
+
resetInput: () => {
|
|
38
|
+
this.input.keyboard?.resetKeys();
|
|
39
|
+
},
|
|
40
|
+
onUnsupportedState: (_snapshot, reason) => {
|
|
41
|
+
console.error('[game-execution]', reason);
|
|
42
|
+
},
|
|
43
|
+
});
|
|
29
44
|
|
|
30
45
|
this.marker = this.add.image(480, 250, starterAssetKeys.marker).setDisplaySize(72, 72);
|
|
31
46
|
this.scoreText = this.add
|
|
@@ -50,8 +65,16 @@ export class PlayScene extends Phaser.Scene {
|
|
|
50
65
|
})
|
|
51
66
|
.setOrigin(0.5);
|
|
52
67
|
|
|
53
|
-
this.
|
|
68
|
+
this.rewardedAction = this.context.gameRuntime.actions?.createRewardedAdController();
|
|
69
|
+
const action = this.rewardedAction;
|
|
70
|
+
const keyboard = this.input.keyboard;
|
|
71
|
+
const onReward = () => {
|
|
54
72
|
void this.requestRewardedAd();
|
|
73
|
+
};
|
|
74
|
+
keyboard?.on('keydown-R', onReward);
|
|
75
|
+
this.events.once('shutdown', () => {
|
|
76
|
+
action?.dispose();
|
|
77
|
+
keyboard?.off('keydown-R', onReward);
|
|
55
78
|
});
|
|
56
79
|
}
|
|
57
80
|
|
|
@@ -77,7 +100,7 @@ export class PlayScene extends Phaser.Scene {
|
|
|
77
100
|
private rewardHint(): string {
|
|
78
101
|
const rewardedAds = this.context.runtime.features.rewardedAds;
|
|
79
102
|
|
|
80
|
-
if (!rewardedAds.enabled || this.context.gameServices.client === undefined) {
|
|
103
|
+
if (!rewardedAds.enabled || this.context.gameServices.client === undefined || this.context.gameServices.monetizationRecovery === undefined) {
|
|
81
104
|
return t(this.context.locale, 'rewardUnavailable');
|
|
82
105
|
}
|
|
83
106
|
|
|
@@ -86,19 +109,25 @@ export class PlayScene extends Phaser.Scene {
|
|
|
86
109
|
|
|
87
110
|
private async requestRewardedAd(): Promise<void> {
|
|
88
111
|
const rewardedAds = this.context.runtime.features.rewardedAds;
|
|
89
|
-
const
|
|
112
|
+
const action = this.rewardedAction;
|
|
90
113
|
|
|
91
|
-
if (!rewardedAds.enabled ||
|
|
114
|
+
if (!rewardedAds.enabled || action === undefined || this.context.gameServices.monetizationRecovery === undefined) {
|
|
92
115
|
this.rewardText.setText(t(this.context.locale, 'rewardUnavailable'));
|
|
93
116
|
return;
|
|
94
117
|
}
|
|
95
118
|
|
|
119
|
+
if (this.context.gameRuntime.actions?.getAvailability() !== 'ready') {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
96
122
|
try {
|
|
97
|
-
const result = await
|
|
123
|
+
const result = await action.execute({
|
|
98
124
|
placementId: rewardedPlacementId,
|
|
99
125
|
idempotencyKey: createClientId('starter-reward'),
|
|
100
126
|
});
|
|
101
127
|
|
|
128
|
+
if (action.isDisposed() || this.rewardedAction !== action) {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
102
131
|
this.rewardText.setText(
|
|
103
132
|
result.claim?.granted === true
|
|
104
133
|
? t(this.context.locale, 'reward', { status: result.status })
|
|
@@ -112,7 +141,9 @@ export class PlayScene extends Phaser.Scene {
|
|
|
112
141
|
granted: false,
|
|
113
142
|
},
|
|
114
143
|
});
|
|
115
|
-
|
|
144
|
+
if (!action.isDisposed() && this.rewardedAction === action) {
|
|
145
|
+
this.rewardText.setText(t(this.context.locale, 'rewardError'));
|
|
146
|
+
}
|
|
116
147
|
console.error('[rewarded-ad]', error);
|
|
117
148
|
}
|
|
118
149
|
}
|