This commit is contained in:
361
G102-sequence/sdk/package/dist/loader/SenaGameSDK.js
vendored
Normal file
361
G102-sequence/sdk/package/dist/loader/SenaGameSDK.js
vendored
Normal file
@@ -0,0 +1,361 @@
|
||||
"use strict";
|
||||
/**
|
||||
* SenaGame SDK Loader
|
||||
*
|
||||
* Ready-to-use interface for game developers.
|
||||
* Handles SDK iframe creation, communication, and lifecycle.
|
||||
*
|
||||
* Usage:
|
||||
* ```html
|
||||
* <script src="sena-game-sdk.js"></script>
|
||||
* <script>
|
||||
* const game = new SenaGameSDK({
|
||||
* iframePath: '/path/to/sdk-iframe/index.html',
|
||||
* mode: 'live',
|
||||
* gameCode: 'G001',
|
||||
* onReady: (sdk) => {
|
||||
* sdk.pushData({ items: [...] });
|
||||
* },
|
||||
* onAnswerResult: (result) => {
|
||||
* console.log('Answer:', result);
|
||||
* }
|
||||
* });
|
||||
* </script>
|
||||
* ```
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.SenaGameSDK = void 0;
|
||||
// ========================================
|
||||
// MESSAGE TYPES (matching SDK)
|
||||
// ========================================
|
||||
const MSG = {
|
||||
// Outgoing (Game → SDK)
|
||||
INIT: 'SDK_INIT',
|
||||
PUSH_DATA: 'SDK_PUSH_DATA',
|
||||
SUBMIT_ANSWER: 'SDK_CHECK_ANSWER',
|
||||
COMPLETE_GAME: 'SDK_COMPLETE_GAME',
|
||||
GET_STATUS: 'SDK_GET_STATUS',
|
||||
// Incoming (SDK → Game)
|
||||
READY: 'SDK_READY',
|
||||
DATA_READY: 'SDK_DATA_READY',
|
||||
ANSWER_RESULT: 'SDK_ANSWER_RESULT',
|
||||
GAME_COMPLETE: 'SDK_GAME_COMPLETE',
|
||||
SESSION_STARTED: 'SDK_SESSION_STARTED',
|
||||
STATUS: 'SDK_STATUS',
|
||||
ERROR: 'SDK_ERROR',
|
||||
};
|
||||
// ========================================
|
||||
// DEFAULT CONFIG
|
||||
// ========================================
|
||||
const DEFAULT_CONFIG = {
|
||||
iframePath: '../sdk-iframe/index.html',
|
||||
mode: 'preview',
|
||||
gameCode: '',
|
||||
debug: false,
|
||||
timeout: 10000,
|
||||
iframeStyle: 'position:fixed;width:1px;height:1px;left:-9999px;border:none;',
|
||||
};
|
||||
// ========================================
|
||||
// SENA GAME SDK CLASS
|
||||
// ========================================
|
||||
class SenaGameSDK {
|
||||
constructor(config) {
|
||||
this.iframe = null;
|
||||
this.isReady = false;
|
||||
this.isDataReady = false;
|
||||
this.pendingMessages = [];
|
||||
this.initResolver = null;
|
||||
this.timeoutId = null;
|
||||
this.config = { ...DEFAULT_CONFIG, ...config };
|
||||
// --- CLEANUP OLD LISTENERS ---
|
||||
// Tránh trường hợp init SDK nhiều lần bị trùng listener cũ
|
||||
const oldSDK = window._sena_game_sdk_instance;
|
||||
if (oldSDK && typeof oldSDK.destroy === 'function') {
|
||||
oldSDK.destroy();
|
||||
}
|
||||
window._sena_game_sdk_instance = this;
|
||||
// Create promise for ready state
|
||||
this.initPromise = new Promise((resolve, reject) => {
|
||||
this.initResolver = { resolve, reject };
|
||||
});
|
||||
// Bind methods
|
||||
this._handleMessage = this._handleMessage.bind(this);
|
||||
// Auto-initialize
|
||||
this._init();
|
||||
}
|
||||
// ----------------------------------------
|
||||
// PUBLIC API
|
||||
// ----------------------------------------
|
||||
/**
|
||||
* Push game data to SDK
|
||||
*/
|
||||
pushData(data) {
|
||||
// Safe extraction of the array
|
||||
let itemsArray = [];
|
||||
if (Array.isArray(data)) {
|
||||
itemsArray = data;
|
||||
}
|
||||
else if (data && Array.isArray(data.items)) {
|
||||
itemsArray = data.items;
|
||||
}
|
||||
else if (data && Array.isArray(data.data)) {
|
||||
itemsArray = data.data;
|
||||
}
|
||||
else if (data && data.items && typeof data.items === 'object') {
|
||||
itemsArray = [data.items];
|
||||
}
|
||||
else if (data && data.data && typeof data.data === 'object') {
|
||||
itemsArray = [data.data];
|
||||
}
|
||||
// Transform to SDK iframe internal format
|
||||
const payload = {
|
||||
data: itemsArray,
|
||||
completed_question_ids: (data && data.completed_question_ids) || []
|
||||
};
|
||||
this._send(MSG.PUSH_DATA, payload);
|
||||
}
|
||||
/**
|
||||
* Submit an answer
|
||||
*/
|
||||
submitAnswer(answer) {
|
||||
// Transform to SDK iframe format
|
||||
const payload = {
|
||||
question_id: answer.questionId,
|
||||
choice: answer.selectedAnswer,
|
||||
time_spent: answer.timeSpent ?? 0
|
||||
};
|
||||
this._send(MSG.SUBMIT_ANSWER, payload);
|
||||
}
|
||||
/**
|
||||
* Complete the game
|
||||
*/
|
||||
completeGame() {
|
||||
this._send(MSG.COMPLETE_GAME, {});
|
||||
}
|
||||
/**
|
||||
* Get current status
|
||||
*/
|
||||
getStatus() {
|
||||
this._send(MSG.GET_STATUS, {});
|
||||
}
|
||||
/**
|
||||
* Wait for SDK to be ready
|
||||
*/
|
||||
async ready() {
|
||||
if (this.isReady)
|
||||
return this;
|
||||
return this.initPromise;
|
||||
}
|
||||
/**
|
||||
* Check if SDK is ready
|
||||
*/
|
||||
get sdkReady() {
|
||||
return this.isReady;
|
||||
}
|
||||
/**
|
||||
* Check if data is ready
|
||||
*/
|
||||
get dataReady() {
|
||||
return this.isDataReady;
|
||||
}
|
||||
/**
|
||||
* Destroy the SDK instance
|
||||
*/
|
||||
destroy() {
|
||||
window.removeEventListener('message', this._handleMessage);
|
||||
if (this.timeoutId) {
|
||||
clearTimeout(this.timeoutId);
|
||||
}
|
||||
if (this.iframe && this.iframe.parentNode) {
|
||||
this.iframe.parentNode.removeChild(this.iframe);
|
||||
}
|
||||
window.removeEventListener('message', this._handleMessage);
|
||||
this.isReady = false;
|
||||
this._log('SDK destroyed');
|
||||
}
|
||||
// ----------------------------------------
|
||||
// PRIVATE METHODS
|
||||
// ----------------------------------------
|
||||
_init() {
|
||||
this._log('Initializing SenaGameSDK...');
|
||||
// Setup message listener
|
||||
window.addEventListener('message', this._handleMessage);
|
||||
// Create iframe
|
||||
this._createIframe();
|
||||
// Setup timeout
|
||||
this.timeoutId = setTimeout(() => {
|
||||
if (!this.isReady) {
|
||||
const error = new Error('SDK initialization timeout');
|
||||
this._error(error);
|
||||
if (this.initResolver) {
|
||||
this.initResolver.reject(error);
|
||||
}
|
||||
}
|
||||
}, this.config.timeout);
|
||||
}
|
||||
_createIframe() {
|
||||
this.iframe = document.createElement('iframe');
|
||||
this.iframe.id = 'sena-game-sdk-iframe';
|
||||
this.iframe.src = this.config.iframePath;
|
||||
this.iframe.style.cssText = this.config.iframeStyle || '';
|
||||
this.iframe.onload = () => {
|
||||
this._log('Iframe loaded, sending INIT...');
|
||||
setTimeout(() => {
|
||||
this._send(MSG.INIT, {
|
||||
mode: this.config.mode,
|
||||
game_code: this.config.gameCode,
|
||||
});
|
||||
}, 100);
|
||||
};
|
||||
this.iframe.onerror = () => {
|
||||
this._error(new Error('Failed to load SDK iframe'));
|
||||
};
|
||||
document.body.appendChild(this.iframe);
|
||||
this._log(`Iframe created: ${this.config.iframePath}`);
|
||||
}
|
||||
_send(type, payload) {
|
||||
if (!this.iframe || !this.iframe.contentWindow) {
|
||||
this._log(`Queuing message: ${type}`, 'warn');
|
||||
this.pendingMessages.push({ type, payload });
|
||||
return;
|
||||
}
|
||||
const message = { type, payload, timestamp: Date.now() };
|
||||
this._log(`→ ${type}`, 'send');
|
||||
this.iframe.contentWindow.postMessage(message, '*');
|
||||
}
|
||||
_handleMessage(event) {
|
||||
const data = event.data;
|
||||
if (!data || !data.type)
|
||||
return;
|
||||
// Only process SDK messages
|
||||
if (!data.type.startsWith('SDK_'))
|
||||
return;
|
||||
this._log(`← ${data.type}`, 'recv');
|
||||
switch (data.type) {
|
||||
case MSG.READY:
|
||||
this._onSDKReady();
|
||||
break;
|
||||
case MSG.DATA_READY:
|
||||
this._onDataReady(data.payload);
|
||||
break;
|
||||
case MSG.ANSWER_RESULT:
|
||||
this._onAnswerResult(data.payload);
|
||||
break;
|
||||
case MSG.GAME_COMPLETE:
|
||||
this._onGameComplete(data.payload);
|
||||
break;
|
||||
case MSG.SESSION_STARTED:
|
||||
this._onSessionStart(data.payload);
|
||||
break;
|
||||
case MSG.ERROR:
|
||||
this._error(new Error(data.payload?.message || 'SDK Error'));
|
||||
break;
|
||||
}
|
||||
}
|
||||
_onSDKReady() {
|
||||
this.isReady = true;
|
||||
if (this.timeoutId) {
|
||||
clearTimeout(this.timeoutId);
|
||||
this.timeoutId = null;
|
||||
}
|
||||
// Flush pending messages
|
||||
while (this.pendingMessages.length > 0) {
|
||||
const msg = this.pendingMessages.shift();
|
||||
if (msg) {
|
||||
this._send(msg.type, msg.payload);
|
||||
}
|
||||
}
|
||||
// Callback
|
||||
if (this.config.onReady) {
|
||||
this.config.onReady(this);
|
||||
}
|
||||
// Resolve promise
|
||||
if (this.initResolver) {
|
||||
this.initResolver.resolve(this);
|
||||
}
|
||||
this._log('✅ SDK Ready!');
|
||||
}
|
||||
_onDataReady(payload) {
|
||||
this.isDataReady = true;
|
||||
// Normalize payload to camelCase
|
||||
const normalized = {
|
||||
items: payload.items || [],
|
||||
totalQuestions: payload.total_questions || payload.totalQuestions || 0,
|
||||
completedCount: payload.completed_count || payload.completedCount || 0,
|
||||
resumeData: payload.resume_data || payload.resumeData || []
|
||||
};
|
||||
if (this.config.onDataReady) {
|
||||
this.config.onDataReady(normalized);
|
||||
}
|
||||
this._log(`✅ Data Ready: ${normalized.items.length} items`);
|
||||
}
|
||||
_onAnswerResult(payload) {
|
||||
const normalized = {
|
||||
questionId: payload.question_id || payload.questionId,
|
||||
isCorrect: payload.correct !== undefined ? payload.correct : payload.isCorrect,
|
||||
correctAnswer: payload.correct_answer || payload.correctAnswer || '',
|
||||
score: payload.score || 0,
|
||||
currentScore: payload.current_score || payload.currentScore || 0,
|
||||
totalAnswered: payload.total_answered || payload.totalAnswered || 0
|
||||
};
|
||||
if (this.config.onAnswerResult) {
|
||||
this.config.onAnswerResult(normalized);
|
||||
}
|
||||
}
|
||||
_onGameComplete(payload) {
|
||||
const normalized = {
|
||||
success: payload.success !== undefined ? payload.success : true,
|
||||
finalScore: payload.score !== undefined ? payload.score : (payload.finalScore || 0),
|
||||
correctCount: payload.correct !== undefined ? payload.correct : (payload.correctCount || 0),
|
||||
totalQuestions: payload.total !== undefined ? payload.total : (payload.totalQuestions || 0),
|
||||
wrongCount: payload.wrong !== undefined ? payload.wrong : (payload.wrongCount || 0),
|
||||
total: payload.total || 0
|
||||
};
|
||||
if (this.config.onGameComplete) {
|
||||
this.config.onGameComplete(normalized);
|
||||
}
|
||||
}
|
||||
_onSessionStart(payload) {
|
||||
const normalized = {
|
||||
assignmentId: payload.assignment_id || payload.assignmentId,
|
||||
userId: payload.student_id || payload.userId,
|
||||
gameId: payload.game_code || payload.gameId,
|
||||
startedAt: payload.started_at || payload.startedAt || new Date().toISOString()
|
||||
};
|
||||
if (this.config.onSessionStart) {
|
||||
this.config.onSessionStart(normalized);
|
||||
}
|
||||
}
|
||||
_error(error) {
|
||||
this._log(`❌ Error: ${error.message}`, 'error');
|
||||
if (this.config.onError) {
|
||||
this.config.onError(error);
|
||||
}
|
||||
}
|
||||
_log(message, type = 'info') {
|
||||
if (!this.config.debug)
|
||||
return;
|
||||
const prefix = '[SenaGameSDK]';
|
||||
const styles = {
|
||||
info: 'color: #888',
|
||||
send: 'color: #ff0',
|
||||
recv: 'color: #0f0',
|
||||
warn: 'color: #fa0',
|
||||
error: 'color: #f00',
|
||||
};
|
||||
console.log(`%c${prefix} ${message}`, styles[type] || styles.info);
|
||||
}
|
||||
}
|
||||
exports.SenaGameSDK = SenaGameSDK;
|
||||
/** SDK version */
|
||||
SenaGameSDK.VERSION = '1.0.0';
|
||||
// ========================================
|
||||
// EXPORT FOR BROWSER (UMD)
|
||||
// ========================================
|
||||
if (typeof window !== 'undefined') {
|
||||
window.SenaGameSDK = SenaGameSDK;
|
||||
}
|
||||
exports.default = SenaGameSDK;
|
||||
//# sourceMappingURL=SenaGameSDK.js.map
|
||||
Reference in New Issue
Block a user