@oyna360/game-sdk 0.5.1

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,112 @@
1
+ /** Public SDK contract — keep in sync with platform `@platform/types` when protocol changes. */
2
+ export declare const SDK_VERSION = "0.5.1";
3
+ export type AvatarPresetKind = 'procedural' | 'glb';
4
+ export interface SdkLobbyAvatar {
5
+ presetId: string;
6
+ presetKey: string;
7
+ presetKind: AvatarPresetKind;
8
+ customConfig: Record<string, unknown>;
9
+ }
10
+ export interface SdkUser {
11
+ id: string;
12
+ username: string;
13
+ displayName: string;
14
+ avatarUrl: string | null;
15
+ }
16
+ export interface SdkSession {
17
+ id: string;
18
+ token: string;
19
+ }
20
+ export interface SdkGameInfo {
21
+ slug: string;
22
+ name: string;
23
+ }
24
+ /** Same shape as platform `platform:init` / lobby SdkInitPayload. */
25
+ export interface SdkInitPayload {
26
+ session: SdkSession;
27
+ user: SdkUser;
28
+ game: SdkGameInfo;
29
+ avatar: SdkLobbyAvatar;
30
+ avatarBases?: Array<{
31
+ id: string;
32
+ glbUrl: string;
33
+ }>;
34
+ lobby?: {
35
+ wsUrl: string;
36
+ roomId: string;
37
+ strictRoom?: boolean;
38
+ };
39
+ }
40
+ export interface PlatformInitMessage extends SdkInitPayload {
41
+ type: 'platform:init';
42
+ version: string;
43
+ }
44
+ export interface PlatformSdkInitOptions {
45
+ /** Iframe wait timeout (ms). Direct mode uses a short probe then bootstrap. */
46
+ timeout?: number;
47
+ /**
48
+ * Platform API base including `/api`, e.g. `https://oyna360.ir/api`.
49
+ * Required for Direct Development Mode.
50
+ */
51
+ platformUrl?: string;
52
+ /**
53
+ * Platform web origin for the authorize page, e.g. `https://oyna360.ir`.
54
+ * Defaults to origin of `platformUrl` without `/api`.
55
+ */
56
+ platformWebUrl?: string;
57
+ /** Published game slug. Required for Direct Development Mode. */
58
+ gameSlug?: string;
59
+ }
60
+ export interface LeaderboardEntry {
61
+ rank: number;
62
+ score: number;
63
+ achievedAt: string;
64
+ user: {
65
+ displayName: string;
66
+ username: string;
67
+ avatarUrl: string | null;
68
+ };
69
+ }
70
+ export interface LeaderboardResponse {
71
+ gameSlug: string;
72
+ entries: LeaderboardEntry[];
73
+ }
74
+ export interface SubmitScoreResponse {
75
+ score: number;
76
+ previousBest: number | null;
77
+ isNewBest: boolean;
78
+ rank: number;
79
+ }
80
+ export interface AchievementItem {
81
+ key: string;
82
+ title: string;
83
+ description: string | null;
84
+ iconUrl: string | null;
85
+ sortOrder: number;
86
+ unlocked: boolean;
87
+ unlockedAt: string | null;
88
+ }
89
+ export interface AchievementsResponse {
90
+ gameSlug: string;
91
+ achievements: AchievementItem[];
92
+ }
93
+ export interface UnlockAchievementResponse {
94
+ key: string;
95
+ title: string;
96
+ unlocked: boolean;
97
+ unlockedAt: string;
98
+ alreadyUnlocked: boolean;
99
+ }
100
+ export interface WalletBalanceResponse {
101
+ gems: number;
102
+ coinsPerGem: number;
103
+ gameSlug: string;
104
+ }
105
+ export interface ConvertGemsResponse {
106
+ gemsSpent: number;
107
+ coinsReceived: number;
108
+ coinsPerGem: number;
109
+ gemsRemaining: number;
110
+ gameSlug: string;
111
+ }
112
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/dist/types.js ADDED
@@ -0,0 +1,6 @@
1
+ "use strict";
2
+ /** Public SDK contract — keep in sync with platform `@platform/types` when protocol changes. */
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.SDK_VERSION = void 0;
5
+ exports.SDK_VERSION = '0.5.1';
6
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":";AAAA,gGAAgG;;;AAEnF,QAAA,WAAW,GAAG,OAAO,CAAC"}
@@ -0,0 +1,71 @@
1
+ # نمای کلی — `@oyna360/game-sdk`
2
+
3
+ این سند برای **بازی‌سازی** است که می‌خواهد بازی‌اش را به oyna360 وصل کند.
4
+
5
+ ---
6
+
7
+ ## این SDK چه کار می‌کند؟
8
+
9
+ پل رسمی بین **بازی** و **پلتفرم oyna360**:
10
+
11
+ - گرفتن کاربر، سشن، اطلاعات بازی
12
+ - در صورت وجود: آواتار لابی و اطلاعات اتصال لابی (`lobby.wsUrl`, `roomId`)
13
+ - ثبت امتیاز، لیدربورد، دستاورد، کیف‌پول جِم (در حالت iframe / Production)
14
+
15
+ بازی را مجبور نمی‌کند موتور خاصی (Three، Babylon، Unity Web، …) داشته باشد.
16
+
17
+ ---
18
+
19
+ ## این SDK چه کار نمی‌کند؟
20
+
21
+ | کار | کجا |
22
+ |-----|-----|
23
+ | لابی ۳D، آواتار متحرک، چت لابی، WebSocket بازیکنان | `@oyna360/lobby-sdk` |
24
+ | سرور اختصاصی گیم‌پلی شما | سرور خودتان |
25
+ | ذخیره پسورد یا توکن دائمی داخل بازی | نباید انجام شود |
26
+
27
+ دو پکیج را قاطی یا fork-merge نکنید.
28
+
29
+ ---
30
+
31
+ ## معماری
32
+
33
+ ```text
34
+ oyna360 Platform
35
+
36
+ ┌───────────────┴───────────────┐
37
+ │ │
38
+ Production Direct Dev
39
+ /play/{slug} + iframe بازی روی origin شما
40
+ │ │
41
+ └───────────────┬───────────────┘
42
+
43
+ @oyna360/game-sdk
44
+
45
+ SdkInitPayload
46
+
47
+ ┌────────────┴────────────┐
48
+ ▼ ▼
49
+ گیم‌پلی / امتیاز @oyna360/lobby-sdk
50
+ (score, wallet, …) (لابی ۳D اختیاری)
51
+ ```
52
+
53
+ ---
54
+
55
+ ## چه زمانی فقط game-sdk؟ چه زمانی هر دو؟
56
+
57
+ | نوع بازی | game-sdk | lobby-sdk |
58
+ |----------|:--------:|:---------:|
59
+ | امتیاز / سشن / جِم بدون لابی ۳D | لازم | نه |
60
+ | لابی ۳D قبل از مسابقه + بعداً گیم‌پلی | لازم | لازم |
61
+ | فقط تست UI بدون شبکه | — | `createDev()` لابی (Fake) |
62
+
63
+ ---
64
+
65
+ ## نسخه و سازگاری
66
+
67
+ - نسخه پکیج با `PlatformSDK.version` خوانده می‌شود.
68
+ - در ادمین بازی فیلد `minSdkVersion` را طوری بگذارید که با SDK نصب‌شده شما سازگار باشد.
69
+ - Semantic Versioning: تغییر breaking در major.
70
+
71
+ بعدی: [01-shoro.md](./01-shoro.md)
@@ -0,0 +1,80 @@
1
+ # نصب و پیش‌نیاز
2
+
3
+ ## نصب
4
+
5
+ ```bash
6
+ npm install github:mamadjavadrasti/oyna360-game-sdk#v0.5.0
7
+ ```
8
+
9
+ Import:
10
+
11
+ ```ts
12
+ import { PlatformSDK } from '@oyna360/game-sdk';
13
+ // یا
14
+ import { init, getUser, submitScore } from '@oyna360/game-sdk';
15
+ ```
16
+
17
+ ---
18
+
19
+ ## پیش‌نیاز ثبت بازی در ادمین oyna360
20
+
21
+ | فیلد | توضیح |
22
+ |------|--------|
23
+ | `slug` | شناسه یکتای بازی در URL: `/play/{slug}` |
24
+ | `entryUrl` | آدرس باندل بازی (Production یا در Dev مثلاً `http://localhost:5180`) |
25
+ | `allowedOrigins` | لیست originهای مجاز بازی به‌صورت `scheme://host:port` |
26
+ | `minSdkVersion` | حداقل نسخه game-sdk (مثلاً `0.5.0`) |
27
+ | وضعیت | بازی باید Published باشد |
28
+
29
+ ### مثال Origin
30
+
31
+ درست:
32
+
33
+ - `https://games.example.com`
34
+ - `http://localhost:5180`
35
+ - `http://127.0.0.1:5173`
36
+ - `http://192.168.1.20:5180`
37
+
38
+ غلط:
39
+
40
+ - فقط `localhost` بدون scheme
41
+ - `*`
42
+ - path مثل `http://localhost:5180/game/` به‌جای origin (origin همان `http://localhost:5180` است)
43
+
44
+ ---
45
+
46
+ ## Production — جریان بازیکن
47
+
48
+ 1. بازیکن در oyna360 لاگین می‌کند.
49
+ 2. به `/play/{slug}` می‌رود.
50
+ 3. پلتفرم سشن بازی می‌سازد و بازی را در iframe از `entryUrl` لود می‌کند.
51
+ 4. game-sdk با `platform:ready` اعلام آمادگی می‌کند.
52
+ 5. پلتفرم `platform:init` می‌فرستد.
53
+ 6. `PlatformSDK.init()` resolve می‌شود.
54
+
55
+ بازی را **مستقیم** با باز کردن `entryUrl` در تب خالی تست Production نکنید؛ بدون parent، مسیر iframe کار نمی‌کند (مگر Direct Development را پیکربندی کرده باشید).
56
+
57
+ ---
58
+
59
+ ## Development — دو راه
60
+
61
+ ### الف) Direct Development (پیشنهادی اگر پلتفرم لوکال ندارید)
62
+
63
+ بازی روی Vite؛ اتصال به سرور واقعی oyna360.
64
+ راهنما: [09-direct-development.md](./09-direct-development.md)
65
+
66
+ ### ب) iframe با entryUrl لوکال
67
+
68
+ 1. در ادمین موقتاً `entryUrl` = `http://localhost:PORT`
69
+ 2. `allowedOrigins` شامل همان origin
70
+ 3. از `/play/{slug}` روی سرور (یا پلتفرم لوکال) وارد شوید
71
+
72
+ ---
73
+
74
+ ## امنیت برای بازی‌ساز
75
+
76
+ - `session.token` را در localStorage / Git نگذارید.
77
+ - USER/PASS پلتفرم را در `.env` بازی نگذارید.
78
+ - در Direct Mode از Authorize رسمی استفاده کنید؛ توکن یک‌بارمصرف است.
79
+
80
+ بعدی: [02-init.md](./02-init.md)
@@ -0,0 +1,136 @@
1
+ # `init()` و Platform Context
2
+
3
+ اولین فراخوانی تقریباً همیشه `PlatformSDK.init()` است.
4
+
5
+ ## امضا
6
+
7
+ ```ts
8
+ function init(options?: PlatformSdkInitOptions): Promise<SdkInitPayload>;
9
+
10
+ interface PlatformSdkInitOptions {
11
+ /** حداکثر انتظار (ms). در iframe پیش‌فرض ~10s؛ در Direct برای authorize می‌توانید بیشتر بدهید. */
12
+ timeout?: number;
13
+ /** پایه API با `/api` — الزامی در Direct Development */
14
+ platformUrl?: string;
15
+ /** origin وب پلتفرم برای صفحه Authorize — در Direct اگر API و Web جدا باشند الزامی */
16
+ platformWebUrl?: string;
17
+ /** slug بازی published — الزامی در Direct Development */
18
+ gameSlug?: string;
19
+ }
20
+ ```
21
+
22
+ جایگزین تنظیمات Direct:
23
+
24
+ ```ts
25
+ window.__OYNA360_DEV__ = {
26
+ platformUrl: 'https://oyna360.ir/api',
27
+ platformWebUrl: 'https://oyna360.ir',
28
+ gameSlug: 'my-game',
29
+ };
30
+ ```
31
+
32
+ ---
33
+
34
+ ## خروجی — `SdkInitPayload`
35
+
36
+ همان Context استاندارد Production (`platform:init`):
37
+
38
+ ```ts
39
+ interface SdkInitPayload {
40
+ user: SdkUser;
41
+ session: SdkSession;
42
+ game: SdkGameInfo;
43
+ avatar: SdkLobbyAvatar;
44
+ avatarBases?: Array<{ id: string; glbUrl: string }>;
45
+ lobby?: {
46
+ wsUrl: string; // مثلاً https://oyna360.ir/lobby
47
+ roomId: string; // مثلاً game:my-game
48
+ strictRoom?: boolean;
49
+ };
50
+ }
51
+ ```
52
+
53
+ | فیلد | کاربرد |
54
+ |------|--------|
55
+ | `user` | نمایش نام، id |
56
+ | `session` | `{ id, token }` — سشن بازی ephemeral |
57
+ | `game` | `slug` / `name` |
58
+ | `avatar` | ظاهر لابی برای lobby-sdk |
59
+ | `avatarBases` | کاتالوگ GLB مشترک (اختیاری) |
60
+ | `lobby` | آدرس WebSocket و اتاق — **هاردکد نکنید** |
61
+
62
+ ---
63
+
64
+ ## رفتار بر اساس محیط
65
+
66
+ ### 1) داخل iframe پلتفرم (Production)
67
+
68
+ 1. SDK در صورت embed بودن، `platform:ready` به parent می‌فرستد.
69
+ 2. منتظر `platform:init` می‌ماند.
70
+ 3. Context را ذخیره می‌کند و همان را روی `window` هم برای lobby-sdk منتشر می‌کند.
71
+
72
+ ```ts
73
+ const ctx = await PlatformSDK.init({ timeout: 15_000 });
74
+ ```
75
+
76
+ ### 2) تب مستقیم / بدون parent (Direct Development)
77
+
78
+ 1. اگر `platformUrl` + `gameSlug` نباشد → خطای واضح.
79
+ 2. صفحه `/dev/game-auth` (popup یا redirect) باز می‌شود.
80
+ 3. با اکانت واقعی لاگین می‌کنید.
81
+ 4. code یک‌بارمصرف → `exchange` → همان `SdkInitPayload`.
82
+
83
+ ```ts
84
+ const ctx = await PlatformSDK.init({
85
+ platformUrl: 'https://oyna360.ir/api',
86
+ platformWebUrl: 'https://oyna360.ir',
87
+ gameSlug: 'my-game',
88
+ timeout: 120_000,
89
+ });
90
+ ```
91
+
92
+ ### 3) Idempotent
93
+
94
+ اگر قبلاً init شده باشد، فراخوانی بعدی همان Promise/payload را می‌دهد.
95
+
96
+ ---
97
+
98
+ ## بعد از init
99
+
100
+ ```ts
101
+ PlatformSDK.isReady(); // true
102
+ PlatformSDK.getUser(); // SdkUser | null
103
+ PlatformSDK.getSession(); // SdkSession | null
104
+ PlatformSDK.getInitPayload(); // SdkInitPayload | null (کامل)
105
+ ```
106
+
107
+ ---
108
+
109
+ ## تحویل به lobby-sdk
110
+
111
+ روش توصیه‌شده:
112
+
113
+ ```ts
114
+ const init = await PlatformSDK.init({ /* … */ });
115
+ await PlatformLobby.create({
116
+ canvas,
117
+ platformInit: init,
118
+ roomId: init.lobby!.roomId,
119
+ wsUrl: init.lobby!.wsUrl,
120
+ });
121
+ ```
122
+
123
+ یا بعد از `init()` می‌توانید `PlatformLobby.createFromPlatform(canvas)` را بزنید (SDK لابی از Context منتشرشده روی window استفاده می‌کند).
124
+
125
+ ---
126
+
127
+ ## خطاهای رایج
128
+
129
+ | پیام / وضعیت | معنی |
130
+ |--------------|------|
131
+ | `init timeout — is the game running inside the platform?` | iframe هستید ولی parent `platform:init` نفرستاده (یا دیر) |
132
+ | `requires platformUrl + gameSlug` | Direct Mode بدون تنظیم |
133
+ | `Development origin not allowed` | origin بازی در allowlist سرور نیست |
134
+ | Authorize timeout | لاگین تمام نشد / popup بلاک شد |
135
+
136
+ بعدی: [03-user-session.md](./03-user-session.md) · Direct: [09-direct-development.md](./09-direct-development.md)
@@ -0,0 +1,33 @@
1
+ # User و Session
2
+
3
+ بعد از `init()` موفق.
4
+
5
+ ## خواندن sync
6
+
7
+ ```ts
8
+ const user = PlatformSDK.getUser();
9
+ // { id, username, displayName, avatarUrl }
10
+
11
+ const session = PlatformSDK.getSession();
12
+ // { id, token }
13
+ ```
14
+
15
+ قبل از init هر دو `null` هستند.
16
+
17
+ ## نکات session
18
+
19
+ - `token` برای هویت سشن بازی است (با JWT لاگین کاربر فرق دارد).
20
+ - عمر سشن محدود است و با activity تمدید می‌شود؛ آن را دائمی فرض نکنید.
21
+ - در Git / `.env` / localStorage ذخیره نکنید.
22
+ - برای lobby-sdk همان `session.token` از Context کافی است؛ خودتان به سوکت وصل نشوید مگر عمداً کلاینت سفارشی بنویسید.
23
+
24
+ ## نمایش در UI
25
+
26
+ ```ts
27
+ const { user, game } = await PlatformSDK.init();
28
+ title.textContent = `${user.displayName} — ${game.name}`;
29
+ ```
30
+
31
+ `avatarUrl` ممکن است `null` باشد؛ آواتار ۳D لابی از فیلد `avatar` در `getInitPayload()` می‌آید نه لزوماً از `avatarUrl`.
32
+
33
+ بعدی: [04-end-session.md](./04-end-session.md)
@@ -0,0 +1,22 @@
1
+ # `endSession()`
2
+
3
+ سشن بازی را می‌بندد تا در آمار/idle پلتفرم باز نماند.
4
+
5
+ ```ts
6
+ await PlatformSDK.endSession();
7
+ ```
8
+
9
+ ## رفتار
10
+
11
+ - **Production (iframe):** پیام `platform:session:end` به parent فرستاده می‌شود؛ پلتفرم سشن را می‌بندد.
12
+ - **Direct Development:** `POST {platformUrl}/sessions/end` با همان session token.
13
+
14
+ ## کی صدا بزنید؟
15
+
16
+ - وقتی بازیکن از بازی خارج می‌شود
17
+ - قبل از unload صفحه (در iframe)
18
+ - بعد از اتمام مسابقه اگر سشن دیگری نمی‌سازید
19
+
20
+ چندبار صدا زدن نباید بازی را خراب کند؛ اگر سشن از قبل بسته شده باشد parent نادیده می‌گیرد.
21
+
22
+ بعدی: [05-scores.md](./05-scores.md)
@@ -0,0 +1,30 @@
1
+ # امتیاز — `submitScore`
2
+
3
+ بهترین امتیاز بازیکن برای این بازی را ثبت می‌کند (keep-best).
4
+
5
+ ```ts
6
+ const result = await PlatformSDK.submitScore(4200);
7
+ // { score, previousBest, isNewBest, rank }
8
+ ```
9
+
10
+ ## پیش‌نیاز
11
+
12
+ - `init()` موفق
13
+ - سشن فعال
14
+ - **Production:** از طریق iframe parent · **Direct Dev:** REST مستقیم به `platformUrl` (همان session token)
15
+
16
+ ## قوانین
17
+
18
+ - `score` باید عدد نامنفی و finite باشد؛ اعشار truncate می‌شود.
19
+ - امتیاز بدتر از بهترین قبلی، best را عوض نمی‌کند (`isNewBest: false`).
20
+
21
+ ## مثال
22
+
23
+ ```ts
24
+ await PlatformSDK.init();
25
+ // … پایان مسابقه
26
+ const { isNewBest, rank } = await PlatformSDK.submitScore(finalScore);
27
+ if (isNewBest) showToast(`رکورد جدید! رتبه ${rank}`);
28
+ ```
29
+
30
+ بعدی: [06-leaderboard.md](./06-leaderboard.md)
@@ -0,0 +1,19 @@
1
+ # لیدربورد — `getLeaderboard`
2
+
3
+ ```ts
4
+ const board = await PlatformSDK.getLeaderboard(20);
5
+ // { gameSlug, entries: [{ rank, score, achievedAt, user: { displayName, username, avatarUrl } }] }
6
+ ```
7
+
8
+ - `limit` پیش‌فرض ۲۰
9
+ - نیاز به `init()` دارد
10
+ - **Production** و **Direct Dev** هر دو کار می‌کنند (iframe یا REST)
11
+
12
+ ```ts
13
+ const { entries } = await PlatformSDK.getLeaderboard(10);
14
+ for (const row of entries) {
15
+ console.log(row.rank, row.user.displayName, row.score);
16
+ }
17
+ ```
18
+
19
+ بعدی: [07-postmessage-protocol.md](./07-postmessage-protocol.md)
@@ -0,0 +1,45 @@
1
+ # پروتکل postMessage (Production)
2
+
3
+ در Production، بازی داخل iframe است و با **صفحهٔ parent پلتفرم** حرف می‌زند — نه مستقیم با REST برای این عملیات.
4
+
5
+ ## از بازی → پلتفرم
6
+
7
+ | `type` | نقش |
8
+ |--------|-----|
9
+ | `platform:ready` | SDK لود شد؛ لطفاً init بفرست |
10
+ | `platform:session:end` | بستن سشن |
11
+ | `platform:score:submit` | ثبت امتیاز (+ `requestId`, `sessionToken`, `score`) |
12
+ | `platform:leaderboard:get` | درخواست لیدربورد |
13
+ | `platform:achievement:unlock` / `platform:achievements:get` | دستاورد |
14
+ | `platform:wallet:get` / `platform:gems:exchange` | کیف‌پول |
15
+ | `platform:lobby:ready` | (از lobby-sdk) درخواست مجدد init |
16
+
17
+ ## از پلتفرم → بازی
18
+
19
+ | `type` | نقش |
20
+ |--------|-----|
21
+ | `platform:init` | Context کامل |
22
+ | `platform:score:result` و بقیه `*:result` | پاسخ RPC با همان `requestId` |
23
+
24
+ ## `platform:init` (شکل)
25
+
26
+ ```ts
27
+ {
28
+ type: 'platform:init',
29
+ version: string,
30
+ session: { id, token },
31
+ user: { id, username, displayName, avatarUrl },
32
+ game: { slug, name },
33
+ avatar: { presetId, presetKey, presetKind, customConfig },
34
+ avatarBases?: [{ id, glbUrl }],
35
+ lobby?: { wsUrl, roomId, strictRoom? }
36
+ }
37
+ ```
38
+
39
+ بازی‌ساز معمولاً لازم نیست این پیام‌ها را دستی بسازد؛ `PlatformSDK` و parent پلتفرم این کار را می‌کنند.
40
+
41
+ ## Direct Development
42
+
43
+ به‌جای parent، Authorize HTTP استفاده می‌شود؛ ولی **شکل نهایی Context همان `SdkInitPayload` است** تا lobby-sdk و کد بازی یکسان بمانند.
44
+
45
+ بعدی: [08-wallet.md](./08-wallet.md)
@@ -0,0 +1,32 @@
1
+ # کیف‌پول جِم — `getWallet` / `convertGems`
2
+
3
+ جِم ارز پلتفرم است. تبدیل به «سکه داخل بازی» با نرخ `coinsPerGem` همان بازی انجام می‌شود.
4
+
5
+ ```ts
6
+ const wallet = await PlatformSDK.getWallet();
7
+ // { gems, coinsPerGem, gameSlug }
8
+
9
+ const result = await PlatformSDK.convertGems(5);
10
+ // { gemsSpent, coinsReceived, coinsPerGem, gemsRemaining, gameSlug }
11
+ ```
12
+
13
+ ## مسئولیت بازی
14
+
15
+ پلتفرم جِم را کم می‌کند و `coinsReceived` را برمی‌گرداند.
16
+ **اعتبار سکه داخل اقتصاد خودتان** با شماست (در سرور/کلاینت بازی اعمال کنید).
17
+
18
+ ## نکات
19
+
20
+ - `gems` باید عدد صحیح مثبت باشد.
21
+ - در **Production** و **Direct Dev** هر دو کار می‌کند (iframe یا REST با session token).
22
+
23
+ ```ts
24
+ try {
25
+ const { coinsReceived } = await PlatformSDK.convertGems(amount);
26
+ await creditLocalCoins(coinsReceived);
27
+ } catch (e) {
28
+ showError(e);
29
+ }
30
+ ```
31
+
32
+ بعدی: [09-direct-development.md](./09-direct-development.md) · [10-achievements.md](./10-achievements.md)