@clocktone/game-sdk 1.0.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.
Files changed (34) hide show
  1. package/README.md +402 -0
  2. package/android/build.gradle +69 -0
  3. package/android/src/main/kotlin/com/playloop/plugins/applovinmax/PlayLoopAppLovinMaxPlugin.kt +316 -0
  4. package/android/src/main/kotlin/com/playloop/plugins/deviceid/PlayLoopDeviceIdPlugin.kt +30 -0
  5. package/dist/loader/cjs/index.js +72 -0
  6. package/dist/loader/esm/index.js +69 -0
  7. package/dist/types/CashableSDK.d.ts +46 -0
  8. package/dist/types/adapters/CapacitorAdsAdapter.d.ts +25 -0
  9. package/dist/types/adapters/WebViewAdsAdapter.d.ts +13 -0
  10. package/dist/types/babylon/BabylonPlugin.d.ts +17 -0
  11. package/dist/types/babylon/index.d.ts +17 -0
  12. package/dist/types/index.d.ts +481 -0
  13. package/dist/types/loader/index.d.ts +300 -0
  14. package/dist/types/modules/AnalyticsModule.d.ts +17 -0
  15. package/dist/types/modules/EmbeddedAdsModule.d.ts +30 -0
  16. package/dist/types/modules/FeatureFlagModule.d.ts +36 -0
  17. package/dist/types/modules/RewardsModule.d.ts +19 -0
  18. package/dist/types/modules/SessionModule.d.ts +16 -0
  19. package/dist/types/modules/StandaloneAdsModule.d.ts +51 -0
  20. package/dist/types/transport/HttpTransport.d.ts +19 -0
  21. package/dist/types/transport/WebViewTransport.d.ts +17 -0
  22. package/dist/types/types.d.ts +147 -0
  23. package/dist/types/ui/UIModule.d.ts +64 -0
  24. package/docs/ads.md +210 -0
  25. package/docs/analytics.md +45 -0
  26. package/docs/babylon.md +88 -0
  27. package/docs/feature-flags.md +109 -0
  28. package/docs/game-integration-guide.md +449 -0
  29. package/docs/loader.md +113 -0
  30. package/docs/rewards.md +57 -0
  31. package/docs/session.md +43 -0
  32. package/docs/ui.md +248 -0
  33. package/docs/wire-protocol.md +194 -0
  34. package/package.json +81 -0
@@ -0,0 +1,481 @@
1
+ interface PlayLoopRequest {
2
+ type: 'jp:request';
3
+ action: PlayLoopAction;
4
+ requestId: string;
5
+ payload?: Record<string, unknown>;
6
+ }
7
+ interface PlayLoopResponse {
8
+ type: 'jp:response';
9
+ requestId: string;
10
+ data?: unknown;
11
+ error?: {
12
+ code: PlayLoopErrorCode;
13
+ message: string;
14
+ };
15
+ }
16
+ interface PlayLoopEvent {
17
+ type: 'jp:event';
18
+ event: PlayLoopEventName;
19
+ data?: unknown;
20
+ }
21
+ interface PlayLoopReady {
22
+ type: 'jp:ready';
23
+ version: string;
24
+ }
25
+ type PlayLoopMessage = PlayLoopRequest | PlayLoopResponse | PlayLoopEvent | PlayLoopReady;
26
+ type PlayLoopAction = 'ads.showRewarded' | 'ads.showInterstitial' | 'ads.isInterstitialAvailable' | 'ads.showBanner' | 'ads.hideBanner' | 'ads.destroyBanner' | 'rewards.awardCoins' | 'rewards.reserveSponsoredVideo' | 'rewards.claimSponsoredVideo' | 'rewards.bonusOffer' | 'rewards.awardBonus' | 'session.getBalance' | 'session.getWallet' | 'games.init' | 'analytics.playTick' | 'analytics.themeColor' | 'lifecycle.ready' | 'lifecycle.progress' | 'tier.claimCelebration' | 'games.list' | 'ui.closeGame' | 'ui.openMenu';
27
+ type PlayLoopEventName = 'lifecycle.freeze' | 'lifecycle.resume' | 'rewards.coinsAwarded';
28
+ type PlayLoopErrorCode = 'TIMEOUT' | 'TRANSPORT_ERROR' | 'AD_NOT_AVAILABLE' | 'NETWORK_ERROR' | 'USER_NOT_LINKED';
29
+ interface PlayLoopSDKConfig {
30
+ gameId: string;
31
+ mode?: 'embedded' | 'standalone';
32
+ apiBaseUrl?: string;
33
+ requestTimeoutMs?: number;
34
+ debug?: boolean;
35
+ /** Client ID for embedded mode (host passes it for direct HTTP auth) */
36
+ clientId?: string;
37
+ /** AppLovin MAX config (required for standalone mode ads) */
38
+ appLovin?: {
39
+ /** SDK key from AppLovin dashboard (Account → General → Keys) */
40
+ sdkKey: string;
41
+ rewardedAdUnitId: string;
42
+ interstitialAdUnitId: string;
43
+ /** Ad unit ID for banner ads (optional - banners disabled if omitted) */
44
+ bannerAdUnitId?: string;
45
+ };
46
+ /** Auto-show a banner ad at this position after init completes. */
47
+ banner?: BannerPosition;
48
+ /** Called when SDK needs the game to pause (e.g. ad overlay). */
49
+ onPause?: () => void;
50
+ /** Called when SDK signals the game can resume. */
51
+ onResume?: () => void;
52
+ /** UI config (top bar, sponsored modal, close/menu buttons) */
53
+ ui?: UIConfig;
54
+ }
55
+ interface UIConfig {
56
+ /** Called when close button is tapped */
57
+ onClose?: () => void;
58
+ /** Called when menu button is tapped (optional) */
59
+ onMenuOpen?: () => void;
60
+ /** Show the floating PlayLoop button (default: false) */
61
+ showPlayLoopButton?: boolean;
62
+ /** Show close button on the PlayLoopButton trigger (default: true in embedded) */
63
+ showClose?: boolean;
64
+ /** Show menu button on the PlayLoopButton trigger (default: true in embedded) */
65
+ showMenu?: boolean;
66
+ /** Position and style config for the PlayLoop button */
67
+ PlayLoopButton?: {
68
+ position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
69
+ };
70
+ }
71
+ interface TierInfo {
72
+ tier: number;
73
+ name: string;
74
+ target: number;
75
+ }
76
+ interface WalletData {
77
+ coins: number;
78
+ tier: {
79
+ currentTier: number;
80
+ currentTierName: string;
81
+ nextTier?: {
82
+ name: string;
83
+ target: number;
84
+ };
85
+ };
86
+ tierDefinitions: TierInfo[];
87
+ coinGoal: number;
88
+ coinsEarnedThisPeriod: number;
89
+ earnings: number;
90
+ cashoutPeriodEndsAt: number;
91
+ }
92
+ interface ActiveOffer {
93
+ offerId: string;
94
+ amount?: number;
95
+ type: 'bonus' | 'booster';
96
+ }
97
+ interface InitResponse {
98
+ userId: string;
99
+ featureFlags?: {
100
+ clientKey: string;
101
+ bootstrapValues?: string;
102
+ };
103
+ }
104
+ interface AdConfig {
105
+ fullScreenIntervalMs: number;
106
+ initialFullScreenMs: number;
107
+ optToFullGapMs: number;
108
+ fullToOptGapMs: number;
109
+ actionFullScreenMs: number;
110
+ rewardMediaDelayMs: number;
111
+ arcadeBonusIntervalMs: number;
112
+ arcadeBoostIntervalMs: number;
113
+ }
114
+ /** Default ad config values (matches PlayLoop app defaults). */
115
+ declare const DEFAULT_AD_CONFIG: AdConfig;
116
+ type EventHandler = (event: PlayLoopEventName, data?: unknown) => void;
117
+ interface SendOptions {
118
+ timeoutMs?: number;
119
+ }
120
+ interface ITransport {
121
+ send<T = unknown>(action: PlayLoopAction, payload?: Record<string, unknown>, opts?: SendOptions): Promise<T>;
122
+ onEvent(handler: EventHandler): () => void;
123
+ dispose(): void;
124
+ }
125
+ type BannerPosition = 'top' | 'bottom';
126
+ interface IAdsAdapter {
127
+ showRewardedVideo(): Promise<boolean>;
128
+ showInterstitial(): Promise<boolean>;
129
+ showBanner(position?: BannerPosition): Promise<boolean>;
130
+ hideBanner(): Promise<void>;
131
+ destroyBanner(): Promise<void>;
132
+ }
133
+ interface IAds {
134
+ showRewardedVideo(): Promise<boolean>;
135
+ showInterstitial(): Promise<boolean>;
136
+ showBanner(position?: BannerPosition): Promise<boolean>;
137
+ hideBanner(): Promise<void>;
138
+ destroyBanner(): Promise<void>;
139
+ onBeforeAd(handler: () => void): () => void;
140
+ onAfterAd(handler: (result: {
141
+ success: boolean;
142
+ }) => void): () => void;
143
+ dispose(): void;
144
+ /** Whether an interstitial can be shown (cooldown check). Always true without cooldowns. */
145
+ isInterstitialAvailable(): boolean;
146
+ /** Whether a rewarded ad can be shown (cooldown check). Always true without cooldowns. */
147
+ isRewardedAvailable(): boolean;
148
+ /** Update ad cooldown config at runtime (standalone mode only, no-op in embedded). */
149
+ updateAdConfig?(overrides: Partial<AdConfig>): void;
150
+ /** Reset all cooldown timers so ads can be shown immediately (standalone only, no-op in embedded). */
151
+ resetCooldowns?(): void;
152
+ /** Current ad config snapshot (standalone only). */
153
+ getAdConfig?(): Readonly<AdConfig>;
154
+ }
155
+
156
+ declare class UIModule {
157
+ private transport;
158
+ private nativeTransport;
159
+ private ads;
160
+ private config;
161
+ private adConfig;
162
+ private debug;
163
+ private mode;
164
+ private rootEl;
165
+ private playLoopButton;
166
+ private wallet;
167
+ private scheduler;
168
+ private modals;
169
+ constructor(transport: ITransport, ads: IAds, config: UIConfig, debug: boolean, adConfig: AdConfig, mode?: 'embedded' | 'standalone', nativeTransport?: ITransport);
170
+ /**
171
+ * Mount the UI shell (styles + root + PlayLoopButton in loading state).
172
+ * Called early before auth completes so the pill is visible immediately.
173
+ */
174
+ mountShell(): void;
175
+ updateAds(ads: IAds, adConfig?: AdConfig): void;
176
+ initialize(): Promise<void>;
177
+ getTopBarHeight(): number;
178
+ showSponsoredInterstitial(): Promise<boolean>;
179
+ showCoinAward(newTotalCoins: number): void;
180
+ showCoinToast(amount: number, note?: string): void;
181
+ updateCoins(coins: number): void;
182
+ refreshWallet(): Promise<void>;
183
+ triggerBonusOffer(): Promise<void>;
184
+ triggerBoosterOffer(): Promise<void>;
185
+ setVisible(visible: boolean): void;
186
+ dispose(): void;
187
+ private initPlayLoopButton;
188
+ private handleCloseGame;
189
+ private handleOpenMenu;
190
+ private log;
191
+ }
192
+
193
+ declare class SessionModule {
194
+ private transport;
195
+ private mode;
196
+ private appSetId;
197
+ private clientId;
198
+ constructor(transport: ITransport | null, mode: 'embedded' | 'standalone', clientId?: string);
199
+ setTransport(transport: ITransport): void;
200
+ initialize(): Promise<void>;
201
+ getAuthHeaders(): Record<string, string>;
202
+ getBalance(): Promise<{
203
+ coins: number;
204
+ }>;
205
+ }
206
+
207
+ declare class RewardsModule {
208
+ private transport;
209
+ private emitter;
210
+ private getMultiplier;
211
+ private lastAwardTime;
212
+ private static readonly AWARD_COOLDOWN_MS;
213
+ constructor(transport: ITransport, getMultiplier?: () => number);
214
+ awardCoins(opts?: {
215
+ multiplier?: number;
216
+ }): Promise<{
217
+ coins: number;
218
+ }>;
219
+ onCoinsAwarded(handler: (data: {
220
+ coins: number;
221
+ }) => void): () => void;
222
+ dispose(): void;
223
+ }
224
+
225
+ declare class AnalyticsModule {
226
+ private transport;
227
+ private gameId;
228
+ private tickTimer;
229
+ private secondsAccumulated;
230
+ constructor(transport: ITransport, gameId: string);
231
+ startAutoTracking(): void;
232
+ stopAutoTracking(): void;
233
+ reportThemeColor(hex: string): void;
234
+ dispose(): void;
235
+ private flush;
236
+ }
237
+
238
+ declare class FeatureFlagModule {
239
+ private client;
240
+ private _adConfig;
241
+ private initialized;
242
+ private debug;
243
+ constructor(debug: boolean);
244
+ /**
245
+ * Initialize Statsig with bootstrap values from the backend init endpoint.
246
+ * Mirrors the frontend StatsigAdapter.initialize() pattern.
247
+ * Returns the userId from the init response.
248
+ */
249
+ initialize(transport: ITransport, appSetId: string | null, gameId: string, mode: 'embedded' | 'standalone'): Promise<string | null>;
250
+ /** Get the current ad configuration (Statsig overrides merged with defaults). */
251
+ get adConfig(): Readonly<AdConfig>;
252
+ /** Check if the adapter is initialized. */
253
+ isInitialized(): boolean;
254
+ /**
255
+ * Read a dynamic config value by name.
256
+ * Mirrors frontend StatsigAdapter.getConfig().
257
+ */
258
+ getConfig(configName: string): {
259
+ get: <V>(key: string, defaultValue: V) => V;
260
+ value: Record<string, unknown>;
261
+ };
262
+ /** Check a feature gate. Returns false if Statsig is not initialized. */
263
+ checkGate(name: string): boolean;
264
+ /** Log an event to Statsig. */
265
+ logEvent(eventName: string, value?: number, metadata?: Record<string, string>): void;
266
+ dispose(): void;
267
+ /** Read `media_settings` dynamic config and merge with defaults. */
268
+ private readAdConfig;
269
+ }
270
+
271
+ type BabylonEngine = {
272
+ stopRenderLoop(renderFunction?: () => void): void;
273
+ runRenderLoop(renderFunction: () => void): void;
274
+ };
275
+ type BabylonScene = {
276
+ render(): void;
277
+ };
278
+ declare class BabylonPlugin {
279
+ private engine;
280
+ private scene;
281
+ bindScene(scene: BabylonScene, engine?: BabylonEngine): void;
282
+ freeze(): void;
283
+ resume(): void;
284
+ dispose(): void;
285
+ }
286
+
287
+ declare class PlayLoopSDK {
288
+ private config;
289
+ private transport;
290
+ private nativeTransport;
291
+ private emitter;
292
+ private unsubTransportEvents;
293
+ private unsubNativeEvents;
294
+ private unsubRewardsCoins;
295
+ private _session;
296
+ private _rewards;
297
+ private _ads;
298
+ private _analytics;
299
+ private _featureFlags;
300
+ private _babylon;
301
+ private _ui;
302
+ private _isAuthenticated;
303
+ readonly mode: 'embedded' | 'standalone';
304
+ static getHostConfig(): {
305
+ clientId?: string;
306
+ apiBaseUrl?: string;
307
+ debug?: boolean;
308
+ } | null;
309
+ static getWidgetHeight(): number;
310
+ constructor(config: PlayLoopSDKConfig);
311
+ init(): Promise<void>;
312
+ private initEmbeddedMode;
313
+ private initStandaloneMode;
314
+ private initRewards;
315
+ private finalizeUI;
316
+ private subscribeTransportEvents;
317
+ private autoDetectBabylon;
318
+ private handleTransportEvent;
319
+ get session(): SessionModule;
320
+ get rewards(): RewardsModule;
321
+ get ads(): IAds;
322
+ get analytics(): AnalyticsModule;
323
+ get isAuthenticated(): boolean;
324
+ get featureFlags(): FeatureFlagModule | null;
325
+ getTopBarHeight(): number;
326
+ getTopBarHeightAsync(): Promise<number>;
327
+ get adConfig(): Readonly<AdConfig>;
328
+ get ui(): UIModule | null;
329
+ get babylon(): BabylonPlugin;
330
+ onFreeze(handler: () => void): () => void;
331
+ onResume(handler: () => void): () => void;
332
+ onWidgetReady(handler: (data: {
333
+ height: number;
334
+ }) => void): () => void;
335
+ dispose(): void;
336
+ private detectMode;
337
+ }
338
+
339
+ /**
340
+ * Ads module for embedded mode (game hosted in PlayLoop WebView).
341
+ * Thin pass-through — the host app manages cooldowns.
342
+ */
343
+ declare class EmbeddedAdsModule implements IAds {
344
+ private adapter;
345
+ private emitter;
346
+ constructor(adapter: IAdsAdapter);
347
+ /** Always available — host app manages cooldowns. */
348
+ isInterstitialAvailable(): boolean;
349
+ /** Always available — host app manages cooldowns. */
350
+ isRewardedAvailable(): boolean;
351
+ showRewardedVideo(): Promise<boolean>;
352
+ showInterstitial(): Promise<boolean>;
353
+ /** Show a banner ad at the given screen position (default: bottom). */
354
+ showBanner(position?: BannerPosition): Promise<boolean>;
355
+ /** Hide the banner ad (keeps it loaded for fast re-show). */
356
+ hideBanner(): Promise<void>;
357
+ /** Destroy the banner ad and free resources. */
358
+ destroyBanner(): Promise<void>;
359
+ onBeforeAd(handler: () => void): () => void;
360
+ onAfterAd(handler: (result: {
361
+ success: boolean;
362
+ }) => void): () => void;
363
+ dispose(): void;
364
+ }
365
+
366
+ /**
367
+ * Ads module for standalone mode (Capacitor game running independently).
368
+ * Wraps EmbeddedAdsModule with cooldown enforcement since there is no host app
369
+ * to manage ad pacing.
370
+ *
371
+ * Cooldown rules (matching frontend/src/lib/ads.ts):
372
+ * - First interstitial delayed by `initialFullScreenMs`
373
+ * - After interstitial: next interstitial in `fullScreenIntervalMs`,
374
+ * next rewarded in `fullToOptGapMs`
375
+ * - After rewarded: next interstitial in `optToFullGapMs`
376
+ * - Rewarded ads are never blocked (user-initiated), but record cross-cooldowns
377
+ */
378
+ declare class StandaloneAdsModule implements IAds {
379
+ private inner;
380
+ private config;
381
+ private _nextInterstitialAt;
382
+ private _nextRewardedAt;
383
+ private _hasShownFullScreenAd;
384
+ constructor(inner: EmbeddedAdsModule, config: AdConfig);
385
+ /** Whether an interstitial can be shown right now (cooldown elapsed). */
386
+ isInterstitialAvailable(): boolean;
387
+ /** Whether a rewarded ad can be shown right now (cooldown elapsed). */
388
+ isRewardedAvailable(): boolean;
389
+ /** Whether any full-screen ad has been shown this session. */
390
+ get hasShownFullScreenAd(): boolean;
391
+ showRewardedVideo(): Promise<boolean>;
392
+ showInterstitial(): Promise<boolean>;
393
+ showBanner(position?: BannerPosition): Promise<boolean>;
394
+ hideBanner(): Promise<void>;
395
+ destroyBanner(): Promise<void>;
396
+ onBeforeAd(handler: () => void): () => void;
397
+ onAfterAd(handler: (result: {
398
+ success: boolean;
399
+ }) => void): () => void;
400
+ /** Update ad cooldown config at runtime (partial merge). */
401
+ updateAdConfig(overrides: Partial<AdConfig>): void;
402
+ /** Reset all cooldown timers so ads can be shown immediately. */
403
+ resetCooldowns(): void;
404
+ /** Current ad config (read-only snapshot). */
405
+ getAdConfig(): Readonly<AdConfig>;
406
+ dispose(): void;
407
+ /** Lazy-init: first access starts the first-interstitial cooldown. */
408
+ private getNextInterstitialAt;
409
+ private recordInterstitialShown;
410
+ private recordRewardedShown;
411
+ }
412
+
413
+ declare class WebViewTransport implements ITransport {
414
+ private pending;
415
+ private eventHandlers;
416
+ private messageListener;
417
+ private defaultTimeoutMs;
418
+ constructor(opts?: {
419
+ defaultTimeoutMs?: number;
420
+ });
421
+ send<T = unknown>(action: PlayLoopAction, payload?: Record<string, unknown>, opts?: SendOptions): Promise<T>;
422
+ onEvent(handler: EventHandler): () => void;
423
+ dispose(): void;
424
+ private handleMessage;
425
+ }
426
+
427
+ interface HttpTransportConfig {
428
+ apiBaseUrl: string;
429
+ gameId: string;
430
+ getAuthHeaders: () => Record<string, string>;
431
+ defaultTimeoutMs?: number;
432
+ /** SDK version for X-Client-Build header */
433
+ sdkVersion?: string;
434
+ }
435
+ declare class HttpTransport implements ITransport {
436
+ private config;
437
+ private activeControllers;
438
+ private disposed;
439
+ constructor(config: HttpTransportConfig);
440
+ send<T = unknown>(action: PlayLoopAction, payload?: Record<string, unknown>, opts?: SendOptions): Promise<T>;
441
+ onEvent(_handler: EventHandler): () => void;
442
+ dispose(): void;
443
+ }
444
+
445
+ declare class WebViewAdsAdapter implements IAdsAdapter {
446
+ private transport;
447
+ constructor(transport: ITransport);
448
+ showRewardedVideo(): Promise<boolean>;
449
+ showInterstitial(): Promise<boolean>;
450
+ showBanner(position?: BannerPosition): Promise<boolean>;
451
+ hideBanner(): Promise<void>;
452
+ destroyBanner(): Promise<void>;
453
+ }
454
+
455
+ interface AppLovinConfig {
456
+ sdkKey: string;
457
+ rewardedAdUnitId: string;
458
+ interstitialAdUnitId: string;
459
+ bannerAdUnitId?: string;
460
+ debug?: boolean;
461
+ }
462
+ declare class CapacitorAdsAdapter implements IAdsAdapter {
463
+ private plugin;
464
+ private config;
465
+ private initialized;
466
+ private initPromise;
467
+ private debug;
468
+ constructor(config: AppLovinConfig);
469
+ initialize(): Promise<void>;
470
+ showRewardedVideo(): Promise<boolean>;
471
+ showInterstitial(): Promise<boolean>;
472
+ showBanner(position?: BannerPosition): Promise<boolean>;
473
+ hideBanner(): Promise<void>;
474
+ destroyBanner(): Promise<void>;
475
+ }
476
+
477
+ /** Compute widget top-bar height from window.innerWidth — no init required. */
478
+ declare function getWidgetHeight(): number;
479
+
480
+ export { AnalyticsModule, BabylonPlugin, CapacitorAdsAdapter, DEFAULT_AD_CONFIG, EmbeddedAdsModule, FeatureFlagModule, HttpTransport, PlayLoopSDK, RewardsModule, SessionModule, StandaloneAdsModule, UIModule, WebViewAdsAdapter, WebViewTransport, getWidgetHeight };
481
+ export type { ActiveOffer, AdConfig, BannerPosition, EventHandler, IAds, IAdsAdapter, ITransport, InitResponse, PlayLoopAction, PlayLoopErrorCode, PlayLoopEvent, PlayLoopEventName, PlayLoopMessage, PlayLoopReady, PlayLoopRequest, PlayLoopResponse, PlayLoopSDKConfig, SendOptions, TierInfo, UIConfig, WalletData };