Skip to content

SDK ​

يوفّر هذا الـ SDK قناة اتصال آمنة ثنائية الاتجاه بين اللعبة والعميل الأصلي (iOS وAndroid)، لتمكين مطوري الطرف الثالث من استدعاء القدرات الأصلية.

اسم واجهة الـ API العامة: window.GameTokSDK

القدرات المتوفرة:

الإجراءالوصف
GET_PROFILEجلب ملف المستخدم الحالي
GET_PROFILESجلب ملفات مستخدمين متعددين دفعةً واحدة (بمصفوفة uid)
STORAGE_SETكتابة تخزين مفاتيح/قيم محلي (إعدادات، تقدّم، إلخ)
STORAGE_GETقراءة تخزين مفاتيح/قيم محلي (البيانات المحفوظة سابقًا)
ADD_SCOREإرسال نتيجة (لوائح المتصدرين، مشاركة، إلخ)
GET_PERMISSION_MICطلب إذن الميكروفون (أندرويد)
TOPUPشحن الرصيد عند نقص العملات داخل اللعبة؛ يفتح تدفق الشحن في العميل
ROUND_STARTإشعار بداية الجولة (مرة عند بدء كل جولة؛ مرتبط بسياسات المنصة للجولات/الطاقة)
ROUND_ENDإشعار نهاية الجولة (عند انتهاء الجولة؛ يجب أن يطابق round_id إشعار ROUND_START لنفس الجولة)
SHOW_ENERGY_INSUFFICIENT_DIALOGالطاقة غير كافية؛ إخطار المنصة لعرض نافذة نقص الطاقة (fire-and-forget)
onAudioSuspendالاستماع لحدث تعليق الصوت (يُطلق عندما تطلب المنصة من اللعبة الكتم)
offAudioSuspendإزالة مستمع حدث تعليق الصوت
onAudioResumeالاستماع لحدث استئناف الصوت (يُطلق عندما تسمح المنصة للعبة باستعادة الصوت)
offAudioResumeإزالة مستمع حدث استئناف الصوت
onAutoStartGameالاستماع لتعليمات العميل «بدء اللعبة» (نفس تأثير النقر على زر start داخل اللعبة)
offAutoStartGameإزالة مستمع تعليمات «بدء اللعبة»
onExitAndResetGameالاستماع لتعليمات العميل «إنهاء اللعبة وإعادة التعيين» (إنهاء اللعبة والعودة للحالة الأولية)
offExitAndResetGameإزالة مستمع تعليمات «إنهاء اللعبة وإعادة التعيين»
REQUEST_EXITنُقر زر «العودة إلى الرئيسية» المدمج في اللعبة؛ إخطار المنصة بالعودة إلى الرئيسية (fire-and-forget، ثم ترسل المنصة EXIT_AND_RESET_GAME)

أمثلة الدمج ​

يوضّح ما يلي كيفية تضمين الـ SDK في الصفحة واستدعاء كل قدرة.

1.1 تضمين السكربت ​

الطريقة الأولى: تحميل الحزمة عبر <script>

html
<script src="https://play.letskix.com/res/game/sdk-js/GameTokSDK.js"></script>
<script>
  console.log('GameTokSDK version loaded:', !!window.GameTokSDK);
</script>

التطوير المحلي (الصفحة غير مضمّنة في التطبيق، التصحيح في المتصفح): بعد تحميل الـ SDK استدعِ GameTokSDK.enableMock()؛ واجهات الـ Promise ستستخدم بيانات محاكاة مدمجة دون الحاجة للعميل الأصلي. لا تستدعِها في الإنتاج أو نسخ المتجر، وإلا سيرى المستخدمون بيانات وهمية.

html
<script src="https://play.letskix.com/res/game/sdk-js/GameTokSDK.js"></script>
<script>
  GameTokSDK.enableMock();
</script>

الاختبار على جهاز حقيقي (WebView داخل التطبيق): عند الحاجة للسجلات، بعد تحميل الـ SDK استدعِ GameTokSDK.enableDebug() (تُعيد Promise؛ تُحمّل vConsole في الصفحة وتُسجّل تبادلات الجسر مع الأصل). لا تستخدمها مع enableMock في آن واحد.

html
<script src="https://play.letskix.com/res/game/sdk-js/GameTokSDK.js"></script>
<script>
  GameTokSDK.enableDebug();
</script>

1.2 أمثلة الاستدعاء ​

تستخدم الأمثلة أسلوب then / catch مع الـ Promise. عند النجاح يُعاد كائن استجابة موحّد؛ عند الفشل يُرفض الـ Promise.

1.2.1 جلب الملف الشخصي ​

javascript
// مثال استدعاء
GameTokSDK.getProfile()
  .then(({ action, error, data }) => {
    console.log('تم جلب الملف الشخصي:', data);
  })
  .catch((e) => {
    console.error('فشل جلب الملف الشخصي:', e);
  });

مثال الاستجابة (JSON):

json
{
  "action": "GET_PROFILE",
  "error": false,
  "data": {
    "uid": 1024780,
    "avatar": "https://game-load-sa.lobah.net/avatar/1.jpg",
    "userName": "Guest",
    "userCoins": 0,
    "level": 0,
    "gameLevel": 0,
    "rankImg": "",
    "gender": 0,
    "testAccount": false,
    "guest": true
  }
}

الحقول الرئيسية:

الحقلالنوعالوصف
uidnumberالمعرّف الفريد للمستخدم
avatarstringرابط الصورة الرمزية
userNamestringاسم المستخدم / الاسم المعروض
userCoinsnumberرصيد العملات الحالي للمستخدم
levelnumberمستوى نمو المستخدم (نظام الحساب)
gameLevelnumberالرتبة (مبنية على مباريات اللعبة / التصنيف)؛ 0 تعني لا توجد رتبة (مثل لاعب جديد، لم يشارك في التصنيف)
rankImgstringرابط صورة الرتبة؛ سلسلة فارغة عندما لا تكون للمستخدم رتبة
gendernumberتعداد الجنس: 0 = غير محدد، 1 = ذكر، 2 = أنثى
testAccountbooleanما إذا كان حساب اختبار / حساب مراجعة المنصة
guestbooleanما إذا كان ضيفًا (لم يسجّل دخوله بحساب حقيقي)

1.2.2 جلب ملفات مستخدمين متعددة دفعةً واحدة ​

يُستخدم في لوائح المتصدرين، قوائم الأصدقاء، تسوية المباريات، أو أي سيناريو يحتاج لجلب معلومات أساسية لـعدة مستخدمين في آنٍ واحد، تجنبًا لاستدعاء getProfile بشكل متكرر.

javascript
/**
 * جلب ملفات مستخدمين متعددة
 * @param uids  مطلوب؛ مصفوفة uid للاستعلام (يُنصح بـ 1–50 في كل استدعاء)
 */
GameTokSDK.getProfiles({ uids: [1234567, 1234568, 1234569] })
  .then(({ action, error, data }) => {
    console.log('نجح الجلب المتعدد:', data);
    // data مصفوفة Profile؛ يمكن التكرار عليها أو تحويلها لـ Map حسب uid
    const map = new Map(data.map(p => [p.uid, p]));
    console.log('1234567 ->', map.get(1234567));
  })
  .catch((e) => {
    console.error('فشل الجلب المتعدد:', e);
  });

المعاملات:

الحقلالنوعمطلوبالوصف
uidsnumber[]نعمقائمة uid للاستعلام؛ الطول المُوصى به 1–50. إذا كانت مصفوفة فارغة، أو غير مصفوفة، أو تحتوي على عناصر غير صالحة (مثل "abc"، null، أعداد سالبة، كسور)، يُرفض الـ SDK فورًا بـ reject(SDKError)، رمز الخطأ INVALID_PARAMS = 1003، ولن يُرسل للأصل

مثال الاستجابة (JSON):

json
{
  "action": "GET_PROFILES",
  "error": false,
  "data": [
    {
      "uid": 1024780,
      "avatar": "https://game-load-sa.lobah.net/avatar/1.jpg",
      "userName": "Guest",
      "userCoins": 0,
      "level": 0,
      "gameLevel": 0,
      "rankImg": "",
      "gender": 0,
      "testAccount": false,
      "guest": true
    },
    {
      "uid": 1024781,
      "avatar": "https://game-load-sa.lobah.net/avatar/2.jpg",
      "userName": "Alice",
      "userCoins": 200,
      "level": 3,
      "gameLevel": 5,
      "rankImg": "https://game-load-sa.lobah.net/rank/5.png",
      "gender": 2,
      "testAccount": false,
      "guest": false
    }
  ]
}

1.2.3 تخزين مفتاح/قيمة ​

javascript
/**
 * تخزين مفتاح/قيمة
 * @param key   مفتاح مخصص
 * @param value قيمة؛ قد تكون نصًا أو كائنًا
 */
GameTokSDK.storageSet({ key: 'settings', value: { theme: 'dark', volume: 0.8 } })
  .then(() => {
    console.log('تم التخزين بنجاح');
  })
  .catch((e) => {
    console.error('فشل التخزين:', e);
  });

مثال الاستجابة (JSON):

json
{
  "action": "STORAGE_SET",
  "error": false,
  "data": null
}

1.2.4 قراءة مفتاح/قيمة ​

javascript
/**
 * قراءة مفتاح/قيمة
 * @param key المفتاح المعرّف مسبقًا
 */
GameTokSDK.storageGet({ key: 'settings' })
  .then((result) => {
    console.log('قراءة ناجحة:', result.data.value); // كائن أو نص
  })
  .catch((e) => {
    console.error('فشلت القراءة:', e);
  });

مثال الاستجابة (JSON):

json
{
  "action": "STORAGE_GET",
  "error": false,
  "data": {
    "value": {
        "theme": "dark",
        "volume": 0.8
    }
  }
}

1.2.5 إرسال النتيجة ​

javascript
/**
 * حسب اللعبة قد تُرسل نتيجة، مرحلة، أو مستوى.
 * للنتيجة: { score: 300, scoreType: 'score', remark: 'score' }
 * للمستوى: { score: 1, scoreType: 'level', remark: 'level' }
 * @param score     القيمة المطلوب إرسالها؛ عدد صحيح موجب
 * @param scoreType فئة البيانات (وصف يحدده العمل)
 * @param remark    ملاحظة اختيارية (قد تطابق scoreType)
 */

GameTokSDK.addScore({ score: 10, scoreType: 'score', remark: 'score' })
  .then(() => {
    console.log('تم إرسال النتيجة');
  })
  .catch((e) => {
    console.error('فشل الإرسال:', e);
  });

مثال الاستجابة (JSON):

json
{
  "action": "ADD_SCORE",
  "error": false,
  "data": null
}

1.2.6 إذن الميكروفون ​

javascript
/**
 * طلب إذن الميكروفون (أندرويد)
 * للاستخدام داخل اللعبة لطلب الإذن (مثل الميزات الصوتية).
 * بلا قيمة إرجاع؛ يُظهر فقط واجهة الأذونات الأصلية.
 */
GameTokSDK.getPermissionMic();

1.2.7 الشحن (TOPUP) ​

مهم: إلغاء المستخدم، فشل الدفع، وما شابه ما زالت تُحلّ عبر then. داخل then افحص data.code لمعرفة النجاح. catch مخصّص لأعطال الـ SDK، انتهاء المهلة، وما شابه.

javascript
/**
 * شحن الرصيد
 * @param amount المبلغ (الوحدات حسب الاتفاق مع العميل)
 */
GameTokSDK.topup({ amount: 100 })
  .then(({ action, error, data }) => {
    const code = data && data.code;
    if (code === 200) {
      console.log('نجح الشحن:', data);
    } else {
      console.warn('الشحن غير مكتمل أو فشل، الرمز:', code, data);
    }
  })
  .catch((e) => {
    console.error('خطأ في طلب الشحن (مثل عدم توفر الـ SDK):', e);
  });

مثال الاستجابة (JSON، نجاح):

json
{
  "action": "TOPUP",
  "error": false,
  "data": {
    "code": 200,
    "message": "ok"
  }
}

قيم code:
200 نجاح
401 فشل الشحن (خطأ)
402 فشل الشحن (إلغاء من المستخدم)

1.2.8 بداية الجولة (ROUND_START) ​

يُستدعى مرة واحدة بعد بدء الجولة فعليًا. round_id اختياري، ويُنصح بعدم تمريره: عند حذفه يولّد الـ SDK تلقائيًا معرّف جولة فريدًا عالميًا (بالبادئة round_) ويحتفظ به داخليًا، ويُرسل roundEnd() القيمة نفسها تلقائيًا. اختياريًا timestamp (بالميلي ثانية؛ إن حُذف يُستخدم الوقت الحالي). بلا إرجاع Promise — إرسال دون انتظار.

javascript
// الاستخدام الموصى به: بلا أي معاملات؛ يولّد الـ SDK round_id ويربطه بهذه الجولة تلقائيًا
GameTokSDK.roundStart();

// استخدام للتوافق: تمرير round_id بنفسك (يجب أن يكون فريدًا عالميًا لكل جولة ولا يُعاد استخدامه أبدًا عبر الجولات،
// ولا تستخدم أرقامًا تسلسلية متزايدة داخل الجلسة مثل 1، 2، 3 — فبعد تحديث الصفحة ستتعارض مع الجولات السابقة)
GameTokSDK.roundStart({ round_id: 'round-' + crypto.randomUUID() });

عند الحاجة لقراءة معرّف الجولة الحالية (مثل تمريره إلى showEnergyInsufficientDialog للإسناد):

javascript
const roundId = GameTokSDK.getCurrentRoundId(); // له قيمة بعد roundStart، ويصبح null بعد roundEnd

إذا استُدعي roundStart مرة أخرى دون roundEnd للجولة السابقة، يُعدّ ذلك بداية جولة جديدة ويستبدل الـ SDK معرّف الجولة الحالية مباشرة (تُعامل الجولة القديمة كجولة لم تنتهِ بشكل طبيعي، ولدى المنصة تسامح مع ذلك).

1.2.9 نهاية الجولة (ROUND_END) ​

يُستدعى بعد انتهاء كل جولة (الفوز/الخسارة/انتهاء الوقت كلها تُعدّ نهاية). round_id اختياري: عند حذفه يُرسَل تلقائيًا نفس قيمة roundStart لهذه الجولة؛ وعند تمريره بنفسك يجب أن يطابق قيمة البداية. اختياريًا timestamp. بلا إرجاع Promise.

javascript
// الاستخدام الموصى به: بالاقتران مع roundStart() دون الاهتمام بـ round_id إطلاقًا
GameTokSDK.roundStart();
// ... منطق الجولة ...
GameTokSDK.roundEnd();

// استخدام للتوافق: تمرير صريح (يجب أن يطابق round_id الخاص بـ roundStart لهذه الجولة)
GameTokSDK.roundEnd({ round_id: myRoundId, timestamp: Date.now() });

إذا لم تكن هناك جولة جارية (لم يُستدعَ roundStart قط، أو سبق استدعاء roundEnd) ولم يُمرَّر round_id صريحًا، يُحذّر الـ SDK في وحدة التحكم ولا يُرسل إلى الأصل.

1.2.10 نافذة نقص الطاقة (SHOW_ENERGY_INSUFFICIENT_DIALOG) ​

عندما تعلم اللعبة بنقص الطاقة عبر getEnergy() / onEnergyUpdate() (انظر 1.2.11) (مثل balance < cost_per_round، فلا يمكن بدء جولة جديدة)، تستدعي هذه الواجهة لتطلب من المنصة عرض نافذة نقص الطاقة في طبقة حاوية اللعبة (تتضمن توجيهات مثل تسجيل الحضور للحصول على طاقة، والذهاب للحصول على طاقة). لا تحكم اللعبة على قواعد الطاقة ولا تتعامل مع واجهة النافذة؛ مسؤوليتها التفعيل فقط. بلا إرجاع Promise — إرسال دون انتظار.

ملاحظة: تعرض المنصة نفسها هذه النافذة أيضًا من تلقاء نفسها في مواضع مثل خصم بداية الجولة وفحص نهاية الجولة؛ واستدعاء اللعبة هو تفعيل تكميلي (السيناريو النموذجي: ينقر اللاعب زر «ابدأ» بينما تعلم اللعبة مسبقًا أن الطاقة غير كافية، فتطلب النافذة بدلًا من عدم الاستجابة بصمت).

javascript
/**
 * نافذة نقص الطاقة
 * @param scene    اختياري، سيناريو التفعيل (يُعرَّف حسب العمل، مثل round_start أو continue)
 * @param round_id اختياري، معرّف الجولة المرتبطة لتسهيل الإسناد لدى المنصة (يمكن أخذه من getCurrentRoundId())
 */
GameTokSDK.showEnergyInsufficientDialog({
  scene: 'round_start',
  round_id: GameTokSDK.getCurrentRoundId() || undefined,
});

// أو الاستدعاء بلا معاملات
GameTokSDK.showEnergyInsufficientDialog();

تدفق KC League: قبل بدء المباراة يتحقق خادم اللعبة لدى خادم المنصة عبر entry-check؛ وعند إرجاع canStart: false استدعِ هذه الواجهة نفسها مع إرفاق الحقول المُعادة من التحقق كما هي (قناة fire-and-forget تمرّر أي حقول JSON دون الحاجة إلى إصدار جديد من الـ SDK). بعد الاستدعاء تبقى اللعبة على شاشة الغلاف ويعود زر START قابلًا للنقر؛ وإذا أرادت المنصة بدء المباراة مباشرةً بعد شحن اللاعب/تسجيل حضوره، فسترسل AUTO_START_GAME ولا تحتاج اللعبة إلى إعادة المحاولة بنفسها.

javascript
GameTokSDK.showEnergyInsufficientDialog({
  scene: 'kc_entry_check',
  energyBalance: 0,          // الرصيد المُعاد من entry-check، يُرفَق كما هو
  blockReason: 'NO_ENERGY'   // القيمة التعدادية المُعادة من entry-check، تُرفَق كما هي
});

مثال الحمولة المُرسَلة (JSON):

json
{
  "action": "SHOW_ENERGY_INSUFFICIENT_DIALOG",
  "data": {
    "scene": "round_start",
    "round_id": "round-1732000000-abc123"
  }
}

1.2.11 الاستعلام عن الطاقة ودفع تحديثاتها (getEnergy / onEnergyUpdate) ​

خصم الطاقة ونوافذها وشحنها كلها تتم من جانب المنصة؛ ولا تحتاج اللعبة سوى أمرين: عرض HUD الطاقة، وتعطيل زر «ابدأ» عند نقص الطاقة. مصدرا البيانات:

  • getEnergy() (طلب-استجابة): استعلام واحد مبدئي عند التهيئة؛
  • onEnergyUpdate(handler) (دفع من المنصة): بعد كل تغيير في الطاقة (خصم بداية الجولة، تسجيل الحضور، الشحن، فحص نهاية الجولة، إلخ) تدفع المنصة أحدث قيمة، وتحدّث اللعبة الـ HUD بناءً عليها.

يُعيد/يدفع كلاهما نفس بنية البيانات:

الحقلالنوعالوصف
balancenumber | nullرصيد الطاقة؛ يكون null عندما تكون ميزة الطاقة غير مفعّلة، وعندها يجب على اللعبة إخفاء واجهة الطاقة والعمل دون حدّ للطاقة
charge_enabledbooleanما إذا كان الخصم لكل جولة مفعّلًا لهذه اللعبة
cost_per_roundnumberعدد وحدات الطاقة المستهلكة في كل جولة
reasonstringسبب التغيير (في الدفع فقط): initial / round_charge / round_end_check / checkin / recharge_check
seqnumberرقم تسلسلي متزايد رتيبًا؛ يجب على المستهلك تجاهل القيم القديمة غير المرتبة حسب seq (لا يُضمن وصول الدفعات بالترتيب)
javascript
// التهيئة: استعلام واحد + تسجيل مستمع الدفع
const { data } = await GameTokSDK.getEnergy();
if (data.balance === null) {
  hideEnergyHud();           // ميزة الطاقة غير مفعّلة؛ التراجع إلى وضع دون حدّ للطاقة
} else {
  renderEnergyHud(data.balance, data.cost_per_round);
}

let lastSeq = -Infinity;
GameTokSDK.onEnergyUpdate((payload) => {
  if (payload.seq <= lastSeq) return; // تجاهل القيم القديمة غير المرتبة
  lastSeq = payload.seq;
  renderEnergyHud(payload.balance, payload.cost_per_round);
  setStartButtonEnabled(payload.balance === null || payload.balance >= payload.cost_per_round);
});

تعويض مزامنة آخر قيمة: كما في أحداث الصوت، القيمة الافتراضية لـ options.sync في onEnergyUpdate هي true — إذا كان الـ SDK قد خزّن قيمة طاقة مؤقتًا عند التسجيل (من دفعة سابقة أو من استجابة getEnergy())، يُعاد إرسالها مرة واحدة بشكل غير متزامن، فلا يفوّت التسجيل المتأخر الحالة الحالية؛ مرّر { sync: false } لتعطيل ذلك. تتوفر أيضًا offEnergyUpdate / onceEnergyUpdate (بنفس توقيع أحداث الصوت)، إضافة إلى ذاكرة مؤقتة للقراءة فقط:

javascript
const { last, seq } = GameTokSDK.getEnergyState(); // last أحدث بيانات الطاقة (رتيبة حسب seq)، وتكون null عند عدم وجود بيانات

التصحيح المحلي (Mock): بعد enableMock() تُعيد getEnergy() القيمة المحاكاة المدمجة { balance: 100, charge_enabled: true, cost_per_round: 8 }، ويمكن تجاوزها بمعالج مخصص:

javascript
GameTokSDK.enableMock({ GET_ENERGY: () => ({ balance: 3, charge_enabled: true, cost_per_round: 8, seq: Date.now() }) });

دفع ENERGY_UPDATE يأتي من المضيف ولا يتوفر في وضع Mock؛ أثناء التصحيح يمكن إطلاقه يدويًا من وحدة التحكم:

javascript
window.dispatchEvent(new MessageEvent('message', { data: JSON.stringify({
  action: 'ENERGY_UPDATE',
  data: { balance: 0, charge_enabled: true, cost_per_round: 8, reason: 'round_charge', seq: Date.now() }
})}));

1.2.12 مستمعو أحداث الصوت (onAudioSuspend / offAudioSuspend / onAudioResume / offAudioResume) ​

عندما تطلب المنصة من اللعبة الكتم (مثل دخول المستخدم غرفة بث مباشر، ورود مكالمة نظام، إلخ)، يُرسَل لها حدث تعليق الصوت؛ وعند السماح باستئناف الصوت، يُرسَل حدث الاستئناف. يجب على اللعبة إيقاف/استئناف جميع المؤثرات الصوتية والموسيقى الخلفية فور استقبال هذه الأحداث.

هذه واجهة مستمع أحداث، وليست واجهة Promise، ولا تُعيد أي قيمة.

javascript
// الاستماع لحدث تعليق الصوت
function handleAudioSuspend(payload) {
  console.log('تم استقبال إشعار الكتم:', payload);
  // كتم جميع المؤثرات الصوتية والموسيقى الخلفية في اللعبة
  myGame.muteAll();
}

GameTokSDK.onAudioSuspend(handleAudioSuspend);

// الاستماع لحدث استئناف الصوت
function handleAudioResume(payload) {
  console.log('تم استقبال إشعار استئناف الصوت:', payload);
  // استئناف جميع المؤثرات الصوتية والموسيقى الخلفية في اللعبة
  myGame.unmuteAll();
}

GameTokSDK.onAudioResume(handleAudioResume);

إزالة المستمعين:

javascript
// إزالة مستمع تعليق الصوت (مرّر نفس مرجع الدالة المستخدمة عند التسجيل)
GameTokSDK.offAudioSuspend(handleAudioSuspend);

// إزالة مستمع استئناف الصوت
GameTokSDK.offAudioResume(handleAudioResume);

معامل options.sync (مزامنة الحالة):

يدعم onAudioSuspend وonAudioResume معاملًا اختياريًا ثانيًا options، حيث يعالج sync (الافتراضي true) حالة تسجيل المستمع بعد انطلاق الحدث:

  • sync: true (الافتراضي) — إذا كان الصوت في حالة تعليق/استئناف عند تسجيل المستمع، سيُستدعى رد الاتصال فوريًا مرة واحدة في المهمة الدقيقة التالية، لضمان عدم إغفال اللعبة لتغييرات الحالة السابقة.
  • sync: false — يستمع فقط للأحداث الجديدة اللاحقة؛ بلا مزامنة للحالة عند التسجيل.
javascript
// الافتراضي sync: true — إذا كان الصوت معلقًا بالفعل، يُطلق رد الاتصال فورًا مرة واحدة
GameTokSDK.onAudioSuspend((payload) => {
  myGame.muteAll();
});

// تعطيل مزامنة الحالة، الاستماع للأحداث اللاحقة فقط
GameTokSDK.onAudioSuspend((payload) => {
  myGame.muteAll();
}, { sync: false });

الاستخدام الموصى به: سجّل المستمعين في أقرب وقت ممكن خلال مرحلة تهيئة اللعبة، مع إبقاء sync: true (الافتراضي)، بحيث تتمكن اللعبة من مزامنة الحالة بشكل صحيح حتى لو أرسلت المنصة تعليمات الكتم قبل التسجيل.

1.2.13 مستمعو أحداث التحكم في اللعبة (onAutoStartGame / offAutoStartGame / onExitAndResetGame / offExitAndResetGame) ​

يمكن للعميل إرسال نوعين من تعليمات التحكم إلى اللعبة (العميل => H5):

  • AUTO_START_GAME: يُخطر اللعبة بالبدء تلقائيًا، بنفس تأثير النقر على زر start داخل اللعبة. له سيناريوان: ① ينقر اللاعب «العب مرة أخرى» في طبقة لائحة المتصدرين بالمنصة، فترسله المنصة بعد التحقق من الطاقة؛ ② البدء التلقائي عند الدخول — عندما يدخل اللاعب من صفحة ملصق البرنامج الصغير بالنقر على «ابدأ الآن»، ترسله المنصة مباشرة بعد جاهزية اللعبة، وقد يصل فور انتهاء تحميل اللعبة دون أي إجراء من اللاعب داخلها.
  • EXIT_AND_RESET_GAME: يُخطر العميل اللعبة بالإنهاء والعودة للحالة الأولية (إنهاء الجولة الحالية فورًا، تنظيف العدّ التنازلي/الحركات، والعودة إلى الشاشة الأولية أو شاشة انتظار البدء).

⚠️ يجب على الألعاب المدمجة مع مسار الطاقة/لائحة المتصدرين تنفيذ هذين المستمعَين: تعتمد حماية المنصة من تسابق الطاقة (اكتشاف نقص الطاقة بعد بدء الجولة) على EXIT_AND_RESET_GAME لاستعادة الجولة، وتعتمد متابعة اللعب بعد تسجيل الحضور/الشحن والبدء التلقائي عند الدخول على AUTO_START_GAME لبدء الجولة؛ وعدم تنفيذهما يعني أن المنصة لن تستطيع إيقاف اللعبة عند نقص الطاقة.

هذه واجهة مستمع أحداث، وليست واجهة Promise، ولا تُعيد أي قيمة. يدعم AUTO_START_GAME إعادة إرسال لمرة واحدة عند التسجيل المتأخر: إذا وصلت التعليمات ولا يوجد مستمع بعد (مثل وصول البدء التلقائي عند الدخول قبل اكتمال تهيئة اللعبة)، يخزّن الـ SDK آخر تعليمات مؤقتًا، وعند تسجيل أول onAutoStartGame يُعيد إرسالها مرة واحدة بشكل غير متزامن ثم يمسحها — لذا يكفي تسجيل المستمع في مرحلة التهيئة كالمعتاد دون تفويت تعليمات الدخول. أما EXIT_AND_RESET_GAME فهي تعليمات لحظية خالصة بلا تعويض (تُرسَل أثناء الجولة فقط، حيث تكون اللعبة قد أكملت تهيئتها بالضرورة)؛ يُرجى تسجيل المستمع في أقرب وقت ممكن خلال مرحلة تهيئة اللعبة.

javascript
// الاستماع لتعليمات «بدء اللعبة»
function handleAutoStart(payload) {
  console.log('تم استقبال تعليمات بدء اللعبة:', payload);
  myGame.start(); // نفس النقر على زر start داخل اللعبة
}
GameTokSDK.onAutoStartGame(handleAutoStart);

// الاستماع لتعليمات «إنهاء اللعبة وإعادة التعيين»
function handleExitReset(payload) {
  console.log('تم استقبال تعليمات الإنهاء وإعادة التعيين:', payload);
  myGame.exitAndReset(); // إنهاء الجولة الحالية والعودة للحالة الأولية
}
GameTokSDK.onExitAndResetGame(handleExitReset);

إزالة المستمعين:

javascript
// مرّر نفس مرجع الدالة المستخدمة عند التسجيل
GameTokSDK.offAutoStartGame(handleAutoStart);
GameTokSDK.offExitAndResetGame(handleExitReset);

الاستماع مرة واحدة:

javascript
GameTokSDK.onceAutoStartGame((payload) => {
  myGame.start();
});
GameTokSDK.onceExitAndResetGame((payload) => {
  myGame.exitAndReset();
});

صيغة الرسائل المُرسَلة من العميل:

json
{ "action": "AUTO_START_GAME" }
json
{ "action": "EXIT_AND_RESET_GAME" }

1.2.14 زر الرجوع المدمج في اللعبة (requestExit / REQUEST_EXIT) ​

إذا كانت واجهة اللعبة تحتوي على زر «العودة إلى الرئيسية / الخروج» خاص بها، فعند نقر اللاعب استدعِ requestExit() لإخطار المنصة «أريد العودة إلى الرئيسية». عند الاستقبال ترسل المنصة EXIT_AND_RESET_GAME، وتُنهي اللعبة الأمر في مستمع onExitAndResetGame القائم (إنهاء الجولة، تنظيف الحركات، العودة إلى شاشة انتظار البدء) — لا يغيّر requestExit() نفسه أي حالة للجولة، ولا يمسح round_id الحالي.

هذه واجهة fire-and-forget: بلا Promise، بلا قيمة إرجاع، وبلا إزالة للتكرار (تتولى المنصة إزالة تكرار النقرات المتعددة). جميع المعاملات اختيارية، وهي لأغراض ربط التشخيص فقط:

javascript
/**
 * نقر اللاعب على زر «العودة إلى الرئيسية» داخل اللعبة
 * @param reason    اختياري، سبب التفعيل، الافتراضي 'player_button'
 * @param state     اختياري، حالة اللعبة عند النقر (مثل 'IDLE' / 'PLAY' / 'RESULT')، للتشخيص فقط
 * @param timestamp اختياري، طابع زمني بالميلي ثانية؛ إن حُذف يُستخدم الوقت الحالي
 */
GameTokSDK.requestExit();

// مع معلومات التشخيص
GameTokSDK.requestExit({ reason: 'player_button', state: 'PLAY' });

// الإنهاء الفعلي ما زال يتم هنا (يجب تسجيله قبل الدمج، انظر 1.2.13)
GameTokSDK.onExitAndResetGame(() => myGame.exitAndReset());

إذا كانت هناك جولة جارية (بعد roundStart وقبل roundEnd)، يُرفق الـ SDK تلقائيًا round_id لتسهيل ربط المنصة بهذه الجولة؛ ولا حاجة لتمريره يدويًا.

مثال الحمولة المُرسَلة (JSON):

json
{
  "action": "REQUEST_EXIT",
  "data": {
    "reason": "player_button",
    "state": "PLAY",
    "round_id": "round_xxx",
    "timestamp": 1717000000000
  }
}

شرح الحقول:

الحقلالنوعالوصف
reasonstringسبب التفعيل، الافتراضي player_button
statestringاختياري، حالة اللعبة عند النقر، للتشخيص فقط
round_idstringاختياري، يُرفقه الـ SDK تلقائيًا عند النقر أثناء الجولة
timestampnumberطابع زمني بالميلي ثانية، الافتراضي الوقت الحالي

⚠️ لا تُعِد تعيين اللعبة بنفسك بعد requestExit() — انتظر دائمًا وصول EXIT_AND_RESET_GAME ثم أنهِ الأمر، لتجنب عدم التزامن بين نافذة/تسوية المنصة وشاشة اللعبة.

المعاملات وشكل الاستجابة ​

  • أمثلة معاملات شائعة (مرجعية؛ التفاصيل لكل واجهة):
    • getProfile: `` (غالبًا بلا معاملات إضافية)
    • getProfiles: { uids: number[] } (مطلوب، مصفوفة غير فارغة؛ تُعيد Profile[]، نفس بنية getProfile.data على شكل مصفوفة)
    • storageSet: { key: string, value: any }
    • storageGet: { key: string }
    • addScore: { score: number, scoreType: string }
    • getPermissionMic: `` (غالبًا بلا معاملات إضافية)
    • topup: { amount: number, ... } (حقول أخرى حسب بروتوكول العميل)
    • roundStart / roundEnd: { round_id: string, timestamp?: number }
    • showEnergyInsufficientDialog: { scene?: string, round_id?: string } (اختياري؛ إرسال دون انتظار، بلا قيمة إرجاع)
    • onAudioSuspend / onAudioResume: (handler: Function, options?: { sync?: boolean })
    • offAudioSuspend / offAudioResume: (handler: Function)
    • onAutoStartGame / onExitAndResetGame: (handler: Function)
    • offAutoStartGame / offExitAndResetGame: (handler: Function)
    • requestExit: { reason?: string, state?: string, timestamp?: number } (كلها اختيارية؛ إرسال دون انتظار، بلا قيمة إرجاع؛ يُرفَق round_id تلقائيًا أثناء الجولة)
  • عند النجاح، الاستجابة الموحّدة:
typescript
{
  action: string;      // اسم الإجراء (مثل 'GET_PROFILE')
  error: false;        // false عند النجاح (عند الفشل يُرفض الـ Promise)
  data: any;           // حمولة الأصل؛ الحقول حسب الإجراء
}
  • سلوك الفشل:
    • topup: معظم نتائج العمل (نجاح/فشل/إلغاء) تُحلّ في then عبر data.code؛ catch مخصّص لأخطاء الـ SDK وانتهاء المهلة.
    • واجهات Promise الأخرى: عند الفشل يُرفض الـ Promise بكائن Error؛ يُفضّل التعامل في .catch (تسجيل، إعادة محاولة، أو تنبيه).

ملاحظات مهمة ​

لا تستبدل ولا تحذف الكائنات العامة التالية وإلا يتعطّل الـ SDK:

  • window.GameTokSDK (نقطة الدخول الرئيسية)

مثال (لا تفعل هذا):

javascript
// خطر — يعطل الجسر مع iOS / Android
window.GameTokSDK = {};
window.GameTokSDK = null;
delete window.GameTokSDK;

مثال كامل ​

javascript
// جلب الملف الشخصي
GameTokSDK.getProfile({})
  .then((resp) => {
    console.log('Profile:', resp.data);
  })
  .catch((e) => {
    console.error('فشل جلب الملف الشخصي:', e);
  });

// جلب ملفات مستخدمين متعددة (للوائح المتصدرين، قوائم الأصدقاء، إلخ)
GameTokSDK.getProfiles({ uids: [10001, 10002, 10003] })
  .then(({ data }) => {
    const map = new Map(data.map(p => [p.uid, p]));
    console.log('ملف 10001:', map.get(10001));
  })
  .catch((e) => {
    console.error('فشل الجلب المتعدد:', e);
  });

// تخزين
GameTokSDK.storageSet({ key: 'config', value: { lang: 'ar' } })
  .then(() => {
    console.log('تم التخزين بنجاح');
  })
  .catch((e) => {
    console.error('فشل التخزين:', e);
  });

// قراءة
GameTokSDK.storageGet({ key: 'config' })
  .then((resp) => {
    console.log('Config:', resp.data.value);
  })
  .catch((e) => {
    console.error('فشلت القراءة:', e);
  });

// نتيجة
GameTokSDK.addScore({ score: 5, scoreType: 'score' })
  .then(() => {
    console.log('Score added');
  })
  .catch((e) => {
    console.error('فشل إرسال النتيجة:', e);
  });

// الميكروفون (أندرويد، بلا إرجاع)
GameTokSDK.getPermissionMic();

// الشحن: افحص data.code في then (200 = نجاح)
GameTokSDK.topup({ amount: 100 })
  .then(({ data }) => {
    if (data && data.code === 200) {
      console.log('نجح الشحن', data);
    } else {
      console.log('لم ينجح الشحن', data);
    }
  })
  .catch((e) => console.error('خطأ في الشحن', e));

// إشعارات الجولة (نفس round_id)
const rid = 'r-' + Date.now();
GameTokSDK.roundStart({ round_id: rid });
GameTokSDK.roundEnd({ round_id: rid });

// مستمعو أحداث الصوت (يُسجَّلان خلال تهيئة اللعبة)
GameTokSDK.onAudioSuspend((payload) => {
  console.log('كتم', payload);
  myGame.muteAll();
});

GameTokSDK.onAudioResume((payload) => {
  console.log('استئناف الصوت', payload);
  myGame.unmuteAll();
});

// زر «العودة إلى الرئيسية» المدمج في اللعبة: إخطار المنصة فقط، والإنهاء ينتظر EXIT_AND_RESET_GAME
document.getElementById('btn-home').onclick = () => GameTokSDK.requestExit({ state: 'PLAY' });

Swipe & Play Endless Game Together