@mpgd/game-runtime 0.2.6 → 0.4.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.
@@ -0,0 +1,47 @@
1
+ import type { GameServicesOperationClient, GameServicesRewardedAdResult } from '@mpgd/game-services/operations';
2
+ import type { PlatformGateway, RewardedAdResult } from '@mpgd/platform';
3
+ import type { AdPlacementInput } from '@mpgd/platform/ads';
4
+ import { type GameExecutionController } from '../index.js';
5
+ import { type GameLifecycleState } from '../platform/index.js';
6
+ import { type FullScreenPresentationScope } from '../presentation/index.js';
7
+ import { type AdWaitDeadline, type PurchasePresentationEvent } from '../ads/index.js';
8
+ import { type GameActionCoordinator, type GameActionReconciliationPort } from '../actions/index.js';
9
+ import { type ObserverErrorHandler } from '../observers.js';
10
+ /** Structural service ports keep the core usable without DOM, HTTP, or schema libraries. */
11
+ export interface GameServicePorts {
12
+ readonly client?: GameServicesOperationClient;
13
+ readonly monetizationRecovery?: {
14
+ recoverRewardResult(idempotencyKey: string, platform: RewardedAdResult): Promise<GameServicesRewardedAdResult>;
15
+ reconcile(): Promise<unknown>;
16
+ };
17
+ }
18
+ /** One scene's logical play state; execution blocks remain owned by the game runtime. */
19
+ export interface GameGameplayScope {
20
+ setActive(active: boolean): void;
21
+ dispose(): void;
22
+ }
23
+ export interface GamePlatformRuntime<T extends PlatformGateway = PlatformGateway, S extends GameServicePorts = GameServicePorts> {
24
+ readonly gateway: T;
25
+ readonly execution: GameExecutionController;
26
+ readonly presentation: FullScreenPresentationScope;
27
+ readonly services: S;
28
+ readonly actions?: GameActionCoordinator;
29
+ createGameplayScope(): GameGameplayScope;
30
+ /** Reconcile existing journal entries only; this method never opens platform UI. */
31
+ reconcile(): Promise<void>;
32
+ /** Game teardown; native observers and application-owned late recovery may continue. */
33
+ dispose(): void;
34
+ }
35
+ /** Construct once above every scene, preserving target gateway extensions and receiver binding. */
36
+ export declare function createGamePlatformRuntime<T extends PlatformGateway, S extends GameServicePorts>(input: {
37
+ readonly gateway: T;
38
+ readonly createServices: (gateway: T) => S;
39
+ readonly initialLifecycleState: GameLifecycleState;
40
+ readonly canShow?: (placement: AdPlacementInput) => boolean;
41
+ readonly deadline?: AdWaitDeadline;
42
+ readonly reconciliation?: GameActionReconciliationPort;
43
+ readonly purchasePresentation?: {
44
+ subscribe(listener: (event: PurchasePresentationEvent) => void): () => void;
45
+ };
46
+ readonly onObserverError?: ObserverErrorHandler;
47
+ }): GamePlatformRuntime<T, S>;
@@ -0,0 +1,194 @@
1
+ import { createGameExecutionController, } from '../index.js';
2
+ import { bindGameLifecycle } from '../platform/index.js';
3
+ import { createFullScreenPresentationScope, } from '../presentation/index.js';
4
+ import { createAdClaimEvidenceRecoveryObserver, createCoordinatedPlatformGateway, } from '../ads/index.js';
5
+ import { createGameActionCoordinator, } from '../actions/index.js';
6
+ import { observe } from '../observers.js';
7
+ /** Construct once above every scene, preserving target gateway extensions and receiver binding. */
8
+ export function createGamePlatformRuntime(input) {
9
+ const execution = createGameExecutionController({
10
+ ...(input.onObserverError === undefined ? {} : { onListenerError: input.onObserverError }),
11
+ });
12
+ const presentation = createFullScreenPresentationScope({ execution });
13
+ let services;
14
+ let lateEvidence;
15
+ const source = input.gateway.ads.provider;
16
+ const coordinated = source === undefined ? undefined : createCoordinatedPlatformGateway({
17
+ gateway: input.gateway, provider: source, presentation,
18
+ canShow: (placement) => input.canShow?.(placement) !== false && (placement.format !== 'rewarded' || services?.monetizationRecovery !== undefined),
19
+ ...(input.deadline === undefined ? {} : { deadline: input.deadline }),
20
+ ...(input.purchasePresentation === undefined ? {} : { purchasePresentation: input.purchasePresentation }),
21
+ ...(input.onObserverError === undefined ? {} : { onObserverError: input.onObserverError }),
22
+ onClaimEvidence: (observation) => lateEvidence?.(observation),
23
+ onLateResult: async ({ input: request, result }) => {
24
+ if (request.format !== 'rewarded' || result.eligibility === 'eligible' || result.presentation !== 'not-started'
25
+ || result.evidence !== undefined || result.claimEvidence !== undefined || services?.monetizationRecovery === undefined) {
26
+ return;
27
+ }
28
+ if (result.outcome !== 'failed' && result.outcome !== 'unavailable' && result.outcome !== 'skipped') {
29
+ return;
30
+ }
31
+ const recovered = await services.monetizationRecovery.recoverRewardResult(request.idempotencyKey, { status: result.outcome, rewardGranted: false });
32
+ if (!disposed) {
33
+ actions?.confirmRecoveredRewardResult(request.idempotencyKey, recovered);
34
+ }
35
+ },
36
+ });
37
+ const gateway = (coordinated ?? input.gateway);
38
+ let lifecycle;
39
+ let actions;
40
+ let unsubscribeResume;
41
+ let disposed = false;
42
+ let hostAudio;
43
+ let unsubscribeHostAudio;
44
+ const gameplayScopes = new Map();
45
+ const lifecycleReason = 'game-runtime:lifecycle';
46
+ let gameplayActive = false;
47
+ let hasPlayed = false;
48
+ let loading = true;
49
+ let unsubscribeActivity;
50
+ const updateActivity = () => {
51
+ const snapshot = execution.getSnapshot();
52
+ const activity = gateway.gameActivity;
53
+ const playable = [...gameplayScopes.values()].some(Boolean);
54
+ const blocked = snapshot.blocks.some((block) => block.channels.includes('simulation')
55
+ && !(activity?.handlesFocusChanges === true && hasPlayed && block.reason === lifecycleReason));
56
+ const next = !disposed && snapshot.status === 'active' && playable && !blocked;
57
+ if (next && loading) {
58
+ observe(() => {
59
+ activity?.setLoading(false);
60
+ loading = false;
61
+ }, input.onObserverError);
62
+ if (loading) {
63
+ return;
64
+ }
65
+ }
66
+ if (next !== gameplayActive) {
67
+ observe(() => {
68
+ activity?.setGameplayActive(next);
69
+ gameplayActive = next;
70
+ if (next) {
71
+ hasPlayed = true;
72
+ }
73
+ }, input.onObserverError);
74
+ }
75
+ };
76
+ const createGameplayScope = () => {
77
+ const owner = {};
78
+ let ended = disposed;
79
+ if (!ended) {
80
+ gameplayScopes.set(owner, false);
81
+ }
82
+ return Object.freeze({
83
+ setActive(active) {
84
+ if (!ended && !disposed) {
85
+ gameplayScopes.set(owner, active);
86
+ updateActivity();
87
+ }
88
+ },
89
+ dispose() {
90
+ if (!ended) {
91
+ ended = true;
92
+ gameplayScopes.delete(owner);
93
+ updateActivity();
94
+ }
95
+ },
96
+ });
97
+ };
98
+ const dispose = () => {
99
+ if (disposed) {
100
+ return;
101
+ }
102
+ disposed = true;
103
+ updateActivity();
104
+ if (loading) {
105
+ observe(() => {
106
+ gateway.gameActivity?.setLoading(false);
107
+ loading = false;
108
+ }, input.onObserverError);
109
+ }
110
+ unsubscribeActivity?.();
111
+ gameplayScopes.clear();
112
+ execution.destroy();
113
+ if (unsubscribeHostAudio !== undefined) {
114
+ observe(unsubscribeHostAudio, input.onObserverError);
115
+ }
116
+ hostAudio?.release();
117
+ actions?.dispose();
118
+ coordinated?.dispose();
119
+ presentation.dispose();
120
+ lifecycle?.dispose();
121
+ if (unsubscribeResume !== undefined) {
122
+ observe(unsubscribeResume, input.onObserverError);
123
+ }
124
+ };
125
+ const reconcile = async () => {
126
+ if (disposed) {
127
+ return;
128
+ }
129
+ await services?.monetizationRecovery?.reconcile();
130
+ if (!disposed && input.reconciliation !== undefined) {
131
+ await actions?.reconcile();
132
+ }
133
+ };
134
+ try {
135
+ services = input.createServices(gateway);
136
+ const recovery = services.monetizationRecovery;
137
+ if (recovery !== undefined) {
138
+ lateEvidence = createAdClaimEvidenceRecoveryObserver({ recovery, onResult: async (request, result) => {
139
+ if (!disposed && result.status === 'rejected') {
140
+ actions?.confirmRecoveredRewardResult(request.idempotencyKey, result);
141
+ }
142
+ if (!disposed && input.reconciliation !== undefined) {
143
+ await actions?.reconcile();
144
+ }
145
+ } });
146
+ }
147
+ if (services.client !== undefined) {
148
+ actions = createGameActionCoordinator({ execution, client: services.client,
149
+ ...(coordinated === undefined ? {} : { presentation }),
150
+ ...(input.reconciliation === undefined ? {} : { reconciliation: input.reconciliation }),
151
+ ...(input.onObserverError === undefined ? {} : { onObserverError: input.onObserverError }),
152
+ });
153
+ }
154
+ lifecycle = bindGameLifecycle({ controller: execution, source: gateway.lifecycle, initialState: input.initialLifecycleState,
155
+ reason: lifecycleReason,
156
+ ...(input.onObserverError === undefined ? {} : { onError: input.onObserverError }),
157
+ });
158
+ unsubscribeResume = gateway.lifecycle.onResume(() => { observe(reconcile, input.onObserverError); });
159
+ const settings = gateway.gameSettings;
160
+ if (settings !== undefined) {
161
+ const updateMute = (muted) => {
162
+ if (disposed) {
163
+ return;
164
+ }
165
+ if (muted && hostAudio === undefined) {
166
+ hostAudio = execution.acquireBlock({ reason: 'game-runtime:host-audio', channels: ['audio'] });
167
+ }
168
+ else if (!muted && hostAudio !== undefined) {
169
+ const owned = hostAudio;
170
+ hostAudio = undefined;
171
+ owned.release();
172
+ }
173
+ };
174
+ unsubscribeHostAudio = settings.onAudioMuteChange(updateMute);
175
+ updateMute(settings.getAudioMuted());
176
+ }
177
+ observe(() => gateway.gameActivity?.setLoading(true), input.onObserverError);
178
+ unsubscribeActivity = execution.subscribe(updateActivity);
179
+ return Object.freeze({
180
+ gateway,
181
+ execution,
182
+ presentation,
183
+ services,
184
+ createGameplayScope,
185
+ ...(actions === undefined ? {} : { actions }),
186
+ reconcile,
187
+ dispose,
188
+ });
189
+ }
190
+ catch (error) {
191
+ dispose();
192
+ throw error;
193
+ }
194
+ }
@@ -1,4 +1,5 @@
1
1
  import type Phaser from 'phaser';
2
+ import type { GameGameplayScope } from '../game/index.js';
2
3
  import type { GameExecutionController, GameExecutionSnapshot } from '../index.js';
3
4
  export interface GameplayAudioSink {
4
5
  getMuted(): boolean;
@@ -16,9 +17,12 @@ export interface BindPhaserGameSceneInput {
16
17
  readonly resetInput: () => void;
17
18
  readonly renderingPolicy: 'visibility';
18
19
  readonly audio?: GameplayAudioSink;
20
+ /** A game-owned bindGameAudio projection survives this scene's disposal. */
21
+ readonly audioOwner?: 'scene' | 'game';
19
22
  readonly uiScope?: {
20
23
  dispose(): void;
21
24
  };
25
+ readonly gameplayScope?: GameGameplayScope;
22
26
  /** Phaser pause also suspends input: simulation-only blocking cannot preserve active input. */
23
27
  readonly onUnsupportedState: (snapshot: GameExecutionSnapshot, reason: UnsupportedPhaserExecutionState) => void | Promise<void>;
24
28
  readonly onError?: (error: unknown) => void | Promise<void>;
@@ -9,6 +9,9 @@ export function bindPhaserGameScene(input) {
9
9
  if (controller.getSnapshot().status === 'destroyed') {
10
10
  throw new Error('Cannot bind a destroyed game execution controller.');
11
11
  }
12
+ if (input.audioOwner === 'game' && audio !== undefined) {
13
+ throw new TypeError('Game-owned audio cannot also be controlled by a scene sink.');
14
+ }
12
15
  const events = scene.sys.events;
13
16
  let disposed = false;
14
17
  let sceneEnded = false;
@@ -20,6 +23,7 @@ export function bindPhaserGameScene(input) {
20
23
  let ownedVisibility = false;
21
24
  let ownedAudio = false;
22
25
  let inputBlocked = false;
26
+ let externallyInactive = scene.sys.isPaused();
23
27
  const ownedInput = new Set();
24
28
  let unsubscribe = () => { };
25
29
  function report(error) {
@@ -96,6 +100,7 @@ export function bindPhaserGameScene(input) {
96
100
  }
97
101
  disposed = true;
98
102
  attempt(unsubscribe);
103
+ attempt(() => input.gameplayScope?.dispose());
99
104
  // Keep shutdown observation installed while restoration calls into consumer/engine code.
100
105
  if (mode !== 'terminal') {
101
106
  attempt(() => {
@@ -138,11 +143,14 @@ export function bindPhaserGameScene(input) {
138
143
  function onPause() {
139
144
  if (!disposed && !applying) {
140
145
  ownedPause = false;
146
+ externallyInactive = true;
147
+ attempt(() => input.gameplayScope?.setActive(false));
141
148
  }
142
149
  }
143
150
  function onResume() {
144
151
  if (!disposed && !applying) {
145
152
  ownedPause = false;
153
+ externallyInactive = false;
146
154
  apply();
147
155
  }
148
156
  }
@@ -150,8 +158,11 @@ export function bindPhaserGameScene(input) {
150
158
  // External sleep owns activity/visibility until an external wake. Never wake it ourselves.
151
159
  ownedPause = false;
152
160
  ownedVisibility = false;
161
+ externallyInactive = true;
162
+ attempt(() => input.gameplayScope?.setActive(false));
153
163
  }
154
164
  function onWake() {
165
+ externallyInactive = false;
155
166
  if (controller.getSnapshot().blocked['gameplay-input']) {
156
167
  inputBlocked = false;
157
168
  }
@@ -172,9 +183,10 @@ export function bindPhaserGameScene(input) {
172
183
  dispose('terminal');
173
184
  break;
174
185
  }
186
+ attempt(() => input.gameplayScope?.setActive(!externallyInactive && (scene.sys.isActive() || scene.sys.isPaused())));
175
187
  const blocked = snapshot.blocked;
176
188
  const simulationUnsupported = blocked.simulation && !blocked['gameplay-input'];
177
- const audioUnsupported = blocked.audio && audio === undefined;
189
+ const audioUnsupported = blocked.audio && audio === undefined && input.audioOwner !== 'game';
178
190
  const notifySimulation = simulationUnsupported && !reportedSimulation;
179
191
  const notifyAudio = audioUnsupported && !reportedAudio;
180
192
  reportedSimulation = simulationUnsupported;
@@ -0,0 +1,41 @@
1
+ import type { GameExecutionController } from '../index.js';
2
+ import { type UiListener } from '../ui/index.js';
3
+ import type { ObserverErrorHandler } from '../observers.js';
4
+ export type FullScreenPresentationKind = 'purchase' | 'rewarded' | 'interstitial';
5
+ export interface FullScreenPresentationOwner {
6
+ readonly kind: FullScreenPresentationKind;
7
+ readonly invocationId: string;
8
+ readonly state: 'requested' | 'open' | 'unknown';
9
+ }
10
+ export interface FullScreenPresentationSnapshot {
11
+ readonly version: number;
12
+ readonly status: 'ready' | 'busy' | 'unknown' | 'disposed';
13
+ readonly owner?: FullScreenPresentationOwner;
14
+ }
15
+ export interface FullScreenPresentationLease {
16
+ /** Native facts only. A caller deadline or a backend grant cannot confirm closure. */
17
+ markStarted(): void;
18
+ markUnknown(): void;
19
+ confirmClosed(): void;
20
+ }
21
+ export interface FullScreenPresentationScope {
22
+ readonly execution: GameExecutionController;
23
+ acquire(input: {
24
+ readonly kind: FullScreenPresentationKind;
25
+ readonly invocationId: string;
26
+ readonly audioStart?: 'requested' | 'started';
27
+ }): FullScreenPresentationLease;
28
+ getSnapshot(): FullScreenPresentationSnapshot;
29
+ subscribe(listener: UiListener<FullScreenPresentationSnapshot>): () => void;
30
+ /** Stop new presentations and detach projections; retain any live native ownership. */
31
+ dispose(): void;
32
+ }
33
+ export declare class PresentationExecutionError extends Error {
34
+ readonly code: 'busy' | 'disposed' | 'key-conflict' | 'history-full' | 'already-completed';
35
+ constructor(code: 'busy' | 'disposed' | 'key-conflict' | 'history-full' | 'already-completed');
36
+ }
37
+ /** Share one scope above all screens, providers, and purchase/ad entrypoints. */
38
+ export declare function createFullScreenPresentationScope(input: {
39
+ readonly execution: GameExecutionController;
40
+ readonly onObserverError?: ObserverErrorHandler;
41
+ }): FullScreenPresentationScope;
@@ -0,0 +1,153 @@
1
+ import { createGameUiBridge } from '../ui/index.js';
2
+ export class PresentationExecutionError extends Error {
3
+ code;
4
+ constructor(code) {
5
+ super(`Presentation cannot start: ${code}`);
6
+ this.code = code;
7
+ this.name = 'PresentationExecutionError';
8
+ }
9
+ }
10
+ /** Share one scope above all screens, providers, and purchase/ad entrypoints. */
11
+ export function createFullScreenPresentationScope(input) {
12
+ let disposed = false;
13
+ let version = 0;
14
+ let owner;
15
+ let snapshot = Object.freeze({ version, status: 'ready' });
16
+ const bridge = createGameUiBridge({
17
+ initialSnapshot: snapshot,
18
+ ...(input.onObserverError === undefined ? {} : { onListenerError: input.onObserverError }),
19
+ });
20
+ const isDisposed = () => disposed || input.execution.getSnapshot().status === 'destroyed';
21
+ function publish() {
22
+ let status = 'ready';
23
+ if (isDisposed()) {
24
+ status = 'disposed';
25
+ }
26
+ else if (owner?.info.state === 'unknown') {
27
+ status = 'unknown';
28
+ }
29
+ else if (owner !== undefined) {
30
+ status = 'busy';
31
+ }
32
+ snapshot = Object.freeze({
33
+ version: ++version,
34
+ status,
35
+ ...(owner === undefined ? {} : { owner: owner.info }),
36
+ });
37
+ if (!disposed) {
38
+ bridge.setSnapshot(snapshot);
39
+ }
40
+ }
41
+ return Object.freeze({
42
+ execution: input.execution,
43
+ acquire(request) {
44
+ if (isDisposed()) {
45
+ throw new PresentationExecutionError('disposed');
46
+ }
47
+ if (owner !== undefined) {
48
+ throw new PresentationExecutionError('busy');
49
+ }
50
+ const kind = request.kind;
51
+ const invocationId = request.invocationId;
52
+ const audioStart = request.audioStart ?? 'requested';
53
+ if (!['purchase', 'rewarded', 'interstitial'].includes(kind)
54
+ || typeof invocationId !== 'string' || invocationId.trim() === '' || invocationId.length > 512
55
+ || (audioStart !== 'requested' && audioStart !== 'started')) {
56
+ throw new TypeError('Invalid presentation identity.');
57
+ }
58
+ if (isDisposed()) {
59
+ throw new PresentationExecutionError('disposed');
60
+ }
61
+ if (owner !== undefined) {
62
+ throw new PresentationExecutionError('busy');
63
+ }
64
+ const owned = {
65
+ info: Object.freeze({ kind, invocationId, state: 'requested' }),
66
+ };
67
+ // Install ownership before execution listeners can reenter during acquisition.
68
+ owner = owned;
69
+ try {
70
+ owned.block = input.execution.acquireBlock({
71
+ reason: `presentation:${kind}`, channels: audioStart === 'started' ? ['simulation', 'gameplay-input'] : ['simulation', 'gameplay-input', 'audio'],
72
+ });
73
+ }
74
+ catch (error) {
75
+ if (owner === owned) {
76
+ owner = undefined;
77
+ }
78
+ throw error;
79
+ }
80
+ let acquiringAudio = false;
81
+ const ensureAudio = () => {
82
+ if (audioStart !== 'started' || acquiringAudio || owned.audioBlock !== undefined || input.execution.getSnapshot().status === 'destroyed') {
83
+ return;
84
+ }
85
+ acquiringAudio = true;
86
+ try {
87
+ const block = input.execution.acquireBlock({ reason: `presentation:${kind}:audio`, channels: ['audio'] });
88
+ if (owner === owned) {
89
+ owned.audioBlock = block;
90
+ }
91
+ else {
92
+ block.release();
93
+ }
94
+ }
95
+ finally {
96
+ acquiringAudio = false;
97
+ }
98
+ };
99
+ publish();
100
+ return Object.freeze({
101
+ markStarted() {
102
+ if (owner === owned && owned.info.state !== 'open') {
103
+ const previousInfo = owned.info;
104
+ ensureAudio();
105
+ if (owner !== owned || owned.info !== previousInfo) {
106
+ return;
107
+ }
108
+ owned.info = Object.freeze({ ...owned.info, state: 'open' });
109
+ publish();
110
+ }
111
+ },
112
+ markUnknown() {
113
+ if (owner === owned && owned.info.state !== 'unknown') {
114
+ const previousInfo = owned.info;
115
+ // Lost native observations quarantine audio as well as execution.
116
+ ensureAudio();
117
+ if (owner !== owned || owned.info !== previousInfo) {
118
+ return;
119
+ }
120
+ owned.info = Object.freeze({ ...owned.info, state: 'unknown' });
121
+ publish();
122
+ }
123
+ },
124
+ confirmClosed() {
125
+ if (owner !== owned) {
126
+ return;
127
+ }
128
+ owner = undefined;
129
+ publish();
130
+ owned.audioBlock?.release();
131
+ owned.block?.release();
132
+ },
133
+ });
134
+ },
135
+ getSnapshot() {
136
+ if (isDisposed() && snapshot.status !== 'disposed') {
137
+ publish();
138
+ }
139
+ return snapshot;
140
+ },
141
+ subscribe: (listener) => bridge.subscribeSnapshot(listener),
142
+ dispose() {
143
+ if (disposed) {
144
+ return;
145
+ }
146
+ // Publish disposal before detaching view projections, without proving native closure.
147
+ snapshot = Object.freeze({ version: ++version, status: 'disposed', ...(owner === undefined ? {} : { owner: owner.info }) });
148
+ disposed = true;
149
+ bridge.setSnapshot(snapshot);
150
+ bridge.destroy();
151
+ },
152
+ });
153
+ }
package/docs/phaser.md CHANGED
@@ -104,3 +104,14 @@ subsequent explicit `dispose()` retries it while the runtime is active. Successf
104
104
  cleanup stays idempotent; terminal runtime destruction never retries an unmute.
105
105
  Complete this explicit cleanup before reusing the same sink for another binding,
106
106
  as required by the single-writer ownership contract. No retry timer is installed.
107
+
108
+ ## Game-owned sound manager
109
+
110
+ Use `bindGameAudio` from `@mpgd/game-runtime/audio` above every scene and select
111
+ `audioOwner: 'game'` in scene bindings. That option prevents scene-level audio
112
+ projection and suppresses the missing-scene-audio diagnostic; providing both
113
+ it and a scene audio sink is rejected. Connect the game sound manager's mute
114
+ property to the sink. Scene shutdown only detaches its input/render binding.
115
+ Destroy the shared runtime on the Phaser game's destroy event, then detach the
116
+ audio projection. Unknown native presentation and application-owned claim
117
+ recovery survive scene disposal and must not be canceled by SDK cleanup.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mpgd/game-runtime",
3
- "version": "0.2.6",
3
+ "version": "0.4.0",
4
4
  "description": "Gameplay execution, scoped UI and action coordination with optional Phaser scene bindings.",
5
5
  "license": "MIT",
6
6
  "keywords": [
@@ -34,6 +34,22 @@
34
34
  "./phaser": {
35
35
  "types": "./dist/phaser/index.d.ts",
36
36
  "default": "./dist/phaser/index.js"
37
+ },
38
+ "./presentation": {
39
+ "types": "./dist/presentation/index.d.ts",
40
+ "default": "./dist/presentation/index.js"
41
+ },
42
+ "./ads": {
43
+ "types": "./dist/ads/index.d.ts",
44
+ "default": "./dist/ads/index.js"
45
+ },
46
+ "./audio": {
47
+ "types": "./dist/audio/index.d.ts",
48
+ "default": "./dist/audio/index.js"
49
+ },
50
+ "./game": {
51
+ "types": "./dist/game/index.d.ts",
52
+ "default": "./dist/game/index.js"
37
53
  }
38
54
  },
39
55
  "files": [
@@ -48,7 +64,8 @@
48
64
  "@types/node": "^24.0.0"
49
65
  },
50
66
  "dependencies": {
51
- "@mpgd/game-services": "0.17.2"
67
+ "@mpgd/game-services": "0.18.1",
68
+ "@mpgd/platform": "0.16.0"
52
69
  },
53
70
  "repository": {
54
71
  "type": "git",