/** * ========== SENA SDK - MERGED COMPLETE VERSION ========== * Combines all features from root, update_1, and update_2 * - Advanced game type support (G1-G9, G26) * - PostMessage handling with late arrival support * - Multi-question and single-question modes * - G4, G5, G9 specific implementations * - Matching, WordSearch, GroupSort, Crossword, Puzzle support */ /** * ========== GAME TYPES CONFIG ========== */ var SENA_GAME_TYPES = { 1: { name: 'Quiz', description: 'Trắc nghiệm' }, 2: { name: 'Sort', description: 'Sắp xếp' }, 3: { name: 'Match', description: 'Nối cặp', sdk: 'tdv_sdk' }, 4: { name: 'Fill', description: 'Điền từ' }, 5: { name: 'Custom', description: 'Tùy chỉnh' }, 6: { name: 'OddOneOut', description: 'Chọn khác loại' }, 7: { name: 'WordSearch', description: 'Tìm từ ẩn', sdk: 'tdv_wordsearch' }, 8: { name: 'GroupSort', description: 'Phân loại nhóm', sdk: 'tdv_groupsort' }, 9: { name: 'Crossword', description: 'Ô chữ', sdk: 'tdv_crossword' }, 26: { name: 'Puzzle', description: 'Ghép hình', sdk: 'tdv_puzzle' } }; // Helper: Get valid game type range function getSenaGameTypeRange() { var types = Object.keys(SENA_GAME_TYPES).map(Number); return { min: Math.min.apply(null, types), max: Math.max.apply(null, types) }; } // ========== STUBS FOR LAZY LOADING ========== window.tdv_sdk = window.tdv_sdk || { isStub: true, init: function (mode) { console.log('⚠️ tdv_sdk.init() called on Stub. Mode saved:', mode); this._pendingMode = mode; }, prepareIDs: function () { }, getItemCount: function () { return 0; }, loadFromPostMessage: function () { console.warn('Stub loadFromPostMessage called'); }, isCorrect: function () { return 0; }, getCorrectCount: function () { return 0; }, getTimeSpent: function () { return 0; } }; // Helper: Regex for game code function getSenaGameCodeRegex(strict) { var range = getSenaGameTypeRange(); var typePattern = '[' + range.min + '-' + range.max + ']'; var qoPattern = strict ? '[0-2]' : '[0-9]'; return new RegExp('^G(' + typePattern + ')([2-9])(' + qoPattern + ')(' + qoPattern + ')(?:S([0-1]))?(?:T(\\d+))?$'); } /** * Sena SDK Constructor */ function SenaSDK(gid = 'G2510S1T30') { // Core data this.data = null; this.correctAnswer = null; this.gameCode = gid; this.timeLimit = 0; this.shuffle = true; this.uuid = Date.now() + '_' + Math.floor(Math.random() * 100000); // Time tracking this.startTime = 0; this.endTime = 0; // TTS (Web Speech API) this.speechSynthesis = window.speechSynthesis; this.currentUtterance = null; this.voiceSettings = { lang: 'en-US', rate: 1.0, pitch: 1.0, volume: 1.0 }; // Multi-question support this.list = []; this.currentQuestion = null; this.level = 0; this.totalQuestions = 0; this.userResults = []; this.gameID = null; this.userId = null; this.postMessageDataLoaded = false; // Game type flags this.isMatchingGame = false; this.isOddOneOutGame = false; this.isWordSearchGame = false; this.isGroupSortGame = false; // PostMessage tracking this._postMessageListenerRegistered = false; this._waitingForPostMessage = false; this._postMessageTimeout = null; this._loadCallback = null; // Interaction tracking this._gameStartedByUser = false; this._dataLoadedFromServer = false; // G5 specific this.masterList = []; this.currentLevel = 0; this.totalLevels = 1; this.timePerCard = 0; // Mode: 'live' | 'preview' | 'dev' this.mode = 'preview'; } /** * Shuffle array using Fisher-Yates algorithm */ SenaSDK.prototype.shuffleArray = function (array) { for (let i = array.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [array[i], array[j]] = [array[j], array[i]]; } }; /** * Load data from postMessage (SERVER_PUSH_DATA) */ SenaSDK.prototype.loadFromPostMessage = function (inputJson, callback) { let self = this; if (!inputJson) { console.error('🎮 Sena SDK: No data in postMessage'); if (callback) callback(false); return false; } console.log('📦 Sena SDK: Loading from PostMessage:', inputJson); // Update gameCode if provided if (inputJson.gameCode) { self.gameCode = inputJson.gameCode; } let gameCode = self.gameCode; let gameCategory = gameCode.charAt(1); console.log('🎮 Sena SDK: GameCode:', gameCode, '| Category:', gameCategory); // ========== FORWARD TO SPECIALIZED SDKs ========== // Matching (G3xxx) if (gameCategory === '3') { console.log('🎯 Sena SDK: Detected MATCHING GAME (G3xxx), forwarding to tdv_sdk...'); if (window.tdv_sdk && typeof window.tdv_sdk.loadFromPostMessage === 'function') { self.isMatchingGame = true; return window.tdv_sdk.loadFromPostMessage(inputJson, callback); } else { console.error('❌ tdv_sdk not loaded!'); if (callback) callback(false); return false; } } // WordSearch (G7xxx) if (gameCategory === '7') { console.log('🎯 Sena SDK: Detected WORDSEARCH GAME (G7xxx), forwarding to tdv_wordsearch...'); if (window.tdv_wordsearch && typeof window.tdv_wordsearch.loadFromPostMessage === 'function') { self.isWordSearchGame = true; return window.tdv_wordsearch.loadFromPostMessage(inputJson, callback); } else { console.error('❌ tdv_wordsearch not loaded!'); if (callback) callback(false); return false; } } // GroupSort (G8xxx) if (gameCategory === '8') { console.log('🎯 Sena SDK: Detected GROUPSORT GAME (G8xxx), forwarding to tdv_groupsort...'); if (window.tdv_groupsort && typeof window.tdv_groupsort.loadFromPostMessage === 'function') { self.isGroupSortGame = true; return window.tdv_groupsort.loadFromPostMessage(inputJson, callback); } else { console.error('❌ tdv_groupsort not loaded!'); if (callback) callback(false); return false; } } // Crossword (G9xxx) if (gameCategory === '9') { console.log('🎯 Sena SDK: Detected CROSSWORD GAME (G9xxx), forwarding to tdv_crossword...'); if (window.tdv_crossword && typeof window.tdv_crossword.loadFromPostMessage === 'function') { return window.tdv_crossword.loadFromPostMessage(inputJson, callback); } else { console.error('❌ tdv_crossword not loaded!'); if (callback) callback(false); return false; } } // Puzzle (G26xxx) if (gameCategory === '2' && gameCode.charAt(2) === '6') { console.log('🎯 Sena SDK: Detected PUZZLE GAME (G26xxx), forwarding to tdv_puzzle...'); if (window.tdv_puzzle && typeof window.tdv_puzzle.loadFromPostMessage === 'function') { return window.tdv_puzzle.loadFromPostMessage(inputJson, callback); } else { console.error('❌ tdv_puzzle not loaded!'); if (callback) callback(false); return false; } } // ========== BASIC GAMES (Quiz, Sort, OddOneOut, Fill, Custom) ========== let items = inputJson.list || [inputJson]; // Cancel timeout if waiting if (self._postMessageTimeout) { clearTimeout(self._postMessageTimeout); self._postMessageTimeout = null; } self._waitingForPostMessage = false; // Parse game code self._parseGameCode(); if (items.length === 1) { // Single question let item = items[0]; self.data = { question: item.question || item.data?.question || '', request: item.request || item.data?.request || '', options: item.options || item.data?.options || [], image: item.image || item.data?.image || '', audio: item.audio || item.data?.audio || '', hint: item.hint || item.data?.hint || null, reason: item.reason || item.data?.reason || '' }; self.correctAnswer = item.answer; self.gameID = item.id || null; // G5 specific: Initialize master list if (self.gameType === 5 && self.data?.options) { self.masterList = [...self.data.options]; self.totalLevels = Math.ceil(self.masterList.length / self.itemCount); self.currentLevel = 0; self.loadLevelG5(1); } // G4 specific: Process data if (self.gameType === 4) { self._processG4Data(); } } else { // Multi-question self.list = items.map((item, idx) => ({ id: item.id || idx, question: item.question || '', request: item.request || '', options: item.options || [], answer: item.answer, image: item.image || '', audio: item.audio || '', reason: item.reason || '' })); self.totalQuestions = items.length; self.level = 0; self._loadCurrentQuestionToData(); } self.postMessageDataLoaded = true; console.log('✅ Sena SDK: Data processed -', items.length, 'item(s)'); if (self._loadCallback) { self._loadCallback(true); self._loadCallback = null; } if (callback) callback(true); return true; }; /** * Parse game code to get settings */ SenaSDK.prototype._parseGameCode = function () { let self = this; const gameCode = self.gameCode || 'G2510S1T30'; const regex = getSenaGameCodeRegex(true); let match = gameCode.match(regex); if (match) { self.gameType = parseInt(match[1], 10); self.itemCount = parseInt(match[2], 10); self.questionType = parseInt(match[3], 10); self.optionType = parseInt(match[4], 10); const shuffleFlag = match[5] !== undefined ? match[5] : '1'; const timeStr = match[6] !== undefined ? match[6] : '0'; self.shuffle = (shuffleFlag === '1'); // G5 uses T for time per card if (self.gameType === 5) { self.timePerCard = parseInt(timeStr, 10); self.timeLimit = 0; } else { self.timeLimit = parseInt(timeStr, 10); self.timePerCard = 0; } } }; /** * Load current question to this.data */ SenaSDK.prototype._loadCurrentQuestionToData = function () { let self = this; if (self.list.length > 0 && self.level < self.list.length) { self.currentQuestion = self.list[self.level]; self.data = { question: self.currentQuestion.question || '', request: self.currentQuestion.request || '', options: self.currentQuestion.options || [], image: self.currentQuestion.image || '', audio: self.currentQuestion.audio || '', reason: self.currentQuestion.reason || '' }; self.correctAnswer = self.currentQuestion.answer; } }; /** * Move to next question */ SenaSDK.prototype.nextQuestion = function () { let self = this; if (self.level < self.totalQuestions - 1) { self.level++; self._loadCurrentQuestionToData(); console.log('🎮 Sena SDK: Next Question:', self.level + 1, '/', self.totalQuestions); return true; } console.log('🎮 Sena SDK: No more questions'); return false; }; /** * Get current question number (1-indexed) */ SenaSDK.prototype.getCurrentNumber = function () { return this.level + 1; }; /** * Get total questions */ SenaSDK.prototype.getTotalQuestions = function () { return this.totalQuestions || 1; }; // PostMessage timeout setting SenaSDK.prototype.POSTMESSAGE_TIMEOUT_MS = 5000; // 5 seconds /** * Load data (with mode support) */ SenaSDK.prototype.load = function (callback, template = 'G2510S1T30') { let self = this; // Auto-register postMessage listener if (!self._postMessageListenerRegistered) { self.registerPostMessageListener(); self._postMessageListenerRegistered = true; } // Already loaded from postMessage if (self.postMessageDataLoaded && self.list.length > 0) { console.log('🎮 Sena SDK: Data already loaded from postMessage'); if (callback) callback(true); return; } // Already have data if (self.data && self.data.options) { console.log('🎮 Sena SDK: Data already available'); if (callback) callback(true); return; } // Get URL parameters const urlParams = new URLSearchParams(window.location.search); const LID = urlParams.get('LID'); if (LID) { self.gameCode = LID; } const urlMode = urlParams.get('mode'); if (urlMode && ['live', 'preview', 'dev'].includes(urlMode.toLowerCase())) { self.mode = urlMode.toLowerCase(); } console.log('🎮 Sena SDK: Mode =', self.mode.toUpperCase(), '| GameCode =', self.gameCode); self._loadCallback = callback; // Mode-based loading switch (self.mode) { case 'live': self._waitingForPostMessage = true; console.log('⏳ Sena SDK: [LIVE MODE] Waiting for server data (no timeout)...'); self._sendGameReady(); break; case 'dev': console.log('🔧 Sena SDK: [DEV MODE] Loading sample data immediately...'); self._waitingForPostMessage = false; self._loadFromServer(callback, template); break; case 'preview': default: self._waitingForPostMessage = true; console.log('⏳ Sena SDK: [PREVIEW MODE] Waiting for postMessage (5s timeout)...'); self._postMessageTimeout = setTimeout(function () { if (self._waitingForPostMessage && !self.postMessageDataLoaded) { console.warn('⚠️ Sena SDK: No postMessage received, fallback to sample data...'); self._loadFromServer(self._loadCallback, template); } }, self.POSTMESSAGE_TIMEOUT_MS); break; } }; /** * Send GAME_READY message */ SenaSDK.prototype._sendGameReady = function () { let self = this; window.parent.postMessage({ type: "GAME_READY", uuid: self.uuid, payload: { game_id: self.gameID || self.gameCode, game_code: self.gameCode, mode: self.mode } }, "*"); console.log('📤 Sena SDK: Sent GAME_READY to parent'); console.log('🔑 UUID:', self.uuid); }; /** * Load from server (fallback) */ SenaSDK.prototype._loadFromServer = function (callback, template = 'G2510S1T30') { let self = this; self._waitingForPostMessage = false; let url = `https://senaai.tech/sample/${self.gameCode}.json`; console.log('📡 Sena SDK: Fetching sample from:', url); fetch(url) .then(response => { if (!response.ok) throw new Error("HTTP " + response.status); return response.json(); }) .then(jsonData => { console.log('✅ Sena SDK: Fetched data success'); self.loadFromPostMessage(jsonData); self._dataLoadedFromServer = true; }) .catch(error => { console.error('❌ Sena SDK: Error loading data:', error); if (self.mode === 'dev') { self._loadHardcodedFallback(callback); } else { if (callback) callback(false); } }); }; /** * Hardcoded fallback (for dev mode when fetch fails) */ SenaSDK.prototype._loadHardcodedFallback = function (callback) { let self = this; console.warn('⚠️ Sena SDK: Using HARDCODED fallback data'); let fallbackJson = { gameCode: self.gameCode, data: { question: "What is 2 + 2?", options: ["3", "4", "5", "6"] }, answer: "4" }; self.loadFromPostMessage(fallbackJson, callback); }; /** * Validate game code */ SenaSDK.prototype.validateGameCode = function (code) { code = code || this.gameCode; const regex = getSenaGameCodeRegex(true); const match = code.match(regex); const range = getSenaGameTypeRange(); if (!match) { return { valid: false, error: 'Invalid game code format', code: code }; } return { valid: true, code: code, type: parseInt(match[1], 10), count: parseInt(match[2], 10), qType: parseInt(match[3], 10), oType: parseInt(match[4], 10), shuffle: match[5] !== '0', time: match[6] ? parseInt(match[6], 10) : 0, description: this.getGameCodeDescription(match) }; }; /** * Get game code description */ SenaSDK.prototype.getGameCodeDescription = function (match) { if (!match) return ''; const gameType = SENA_GAME_TYPES[match[1]]?.name || 'Unknown'; const contentTypes = ['Text', 'Image', 'Audio']; const qType = contentTypes[parseInt(match[3])] || 'Unknown'; const oType = contentTypes[parseInt(match[4])] || 'Unknown'; return `${gameType}: ${match[2]} items, ${qType} → ${oType}`; }; /** * Generate developer guide */ SenaSDK.prototype.guide = function () { let self = this; const gameCode = self.gameCode || 'G2510S1T30'; return `╔════════════════════════════════════════════════════════════════════════════╗ ║ SENA SDK - DEVELOPER GUIDE: ${gameCode.padEnd(37)}║ ╚════════════════════════════════════════════════════════════════════════════╝ 📚 MERGED VERSION This SDK combines all game types (G1-G9, G26) with full postMessage support. 🎮 SUPPORTED GAME TYPES: ${Object.entries(SENA_GAME_TYPES).map(([key, val]) => ` G${key}: ${val.name} - ${val.description}`).join('\n')} 📖 For detailed documentation, visit: https://senaai.tech/docs `; }; // ========== GETTERS ========== SenaSDK.prototype.getQuestionValue = function () { var q = String(this.data?.question || '').trim(); if (q.toLowerCase().startsWith('http')) return ''; return q; }; SenaSDK.prototype.getQuestionType = function () { let self = this; const gameCode = self.gameCode || 'G2510S1T30'; const regex = getSenaGameCodeRegex(true); const match = gameCode.match(regex); if (match) { const qIdx = match[3]; const types = { '0': 'text', '1': 'image', '2': 'audio' }; return types[qIdx] || 'text'; } return 'text'; }; SenaSDK.prototype.getQuestionImage = function () { return String(this.data?.question || '').trim(); }; SenaSDK.prototype.getRequestValue = function () { return String(this.data?.request || '').trim(); }; SenaSDK.prototype.getRequestType = function () { return this.getQuestionType(); }; SenaSDK.prototype.getOptionsCount = function () { return this.data?.options?.length || 0; }; SenaSDK.prototype.getOptionsType = function () { let self = this; const gameCode = self.gameCode || 'G2510S1T30'; const regex = getSenaGameCodeRegex(true); const match = gameCode.match(regex); if (match) { const oIdx = match[4]; const types = { '0': 'text', '1': 'image', '2': 'audio' }; return types[oIdx] || 'text'; } return 'text'; }; SenaSDK.prototype.getOptionsValue = function (index) { return this.data?.options?.[index]; }; SenaSDK.prototype.getHintType = function () { return this.data?.hint?.type || ''; }; SenaSDK.prototype.getHintCount = function () { const hintValue = this.getHintValue(); if (hintValue === null) return 0; if (Array.isArray(hintValue)) return hintValue.length; return 1; }; SenaSDK.prototype.getHintValue = function (index) { if (this.data?.hint?.value !== undefined) { const hintValue = this.data.hint.value; const hintType = this.getHintType(); if (hintType === 'display' && Array.isArray(hintValue)) { if (index !== undefined && index >= 0 && index < hintValue.length) { return hintValue[index]; } return hintValue; } return hintValue; } return null; }; SenaSDK.prototype.getReason = function () { return this.data?.reason || this.currentQuestion?.reason || ''; }; SenaSDK.prototype.hasReason = function () { let reason = this.getReason(); return (reason && reason.length > 0) ? 1 : 0; }; // ========== G9 MEMORY FUNCTIONS ========== SenaSDK.prototype.getTargetName = function () { if (this.correctAnswer) { if (typeof this.correctAnswer === 'object' && this.correctAnswer.text) { return this.correctAnswer.text; } return String(this.correctAnswer); } return ''; }; SenaSDK.prototype.getCardName = function (index) { if (this.data?.options?.[index]) { let opt = this.data.options[index]; return opt.name || opt.text || ''; } return ''; }; SenaSDK.prototype.getCardImage = function (index) { return this.data?.options?.[index]?.image || ''; }; SenaSDK.prototype.getCardAudio = function (index) { return this.data?.options?.[index]?.audio || ''; }; SenaSDK.prototype.getCardID = function (index) { return this.data?.options?.[index]?.id || ''; }; SenaSDK.prototype.getCardType = function (index) { if (this.data?.options?.[index]) { return this.data.options[index].type || 'text'; } if (this.masterList?.[index]) { return this.masterList[index].type || 'text'; } return 'text'; }; // ========== G5 VOICE RUSH FUNCTIONS ========== SenaSDK.prototype.loadLevelG5 = function (levelIndex) { let self = this; if (self.gameType !== 5 || !self.masterList) return false; self.currentLevel = levelIndex; let count = self.itemCount; window.Sena_TotalLevels = Math.ceil(self.masterList.length / count); let startIndex = (levelIndex - 1) * count; let endIndex = Math.min(startIndex + count, self.masterList.length); let levelOptions = self.masterList.slice(startIndex, endIndex); self.data.options = levelOptions; console.log(`🎮 Sena SDK: Loaded G5 Level ${levelIndex} with ${levelOptions.length} cards`); return true; }; SenaSDK.prototype.getTotalLevels = function () { return this.totalLevels || 1; }; SenaSDK.prototype.getTimePerCard = function () { if (this.timePerCard === undefined) { this._parseGameCode(); } return this.timePerCard > 0 ? this.timePerCard : 5; }; // ========== G4 MEMORY CARD FUNCTIONS ========== SenaSDK.prototype._processG4Data = function () { let self = this; if (!self.data.options) self.data.options = []; // Count occurrences let counts = {}; self.data.options.forEach(item => { if (item.type !== 'blank' && item.name) { counts[item.name] = (counts[item.name] || 0) + 1; } }); // Remove orphans (items that appear less than 2 times) self.data.options.forEach(item => { if (item.type !== 'blank' && item.name && counts[item.name] < 2) { console.log('🎮 Sena SDK: Orphan card detected:', item.name); item.type = 'blank'; item.name = 'blank'; item.image = ''; item.id = 'blank_sanitized'; } }); // Fill with blank cards to make 9 total while (self.data.options.length < 9) { self.data.options.push({ id: 'blank_' + self.data.options.length, type: 'blank', name: 'blank', value: -1, image: '' }); } // Shuffle if (self.shuffle) { self.shuffleArray(self.data.options); } }; SenaSDK.prototype.checkPair = function (idx1, idx2, callback) { let self = this; let card1 = self.data?.options?.[idx1]; let card2 = self.data?.options?.[idx2]; if (!card1 || !card2) { if (callback) callback(false); return; } let isMatch = false; if (card1.type !== 'blank' && card2.type !== 'blank') { if (card1.id !== card2.id) { if (card1.name && card2.name && card1.name.toLowerCase() === card2.name.toLowerCase()) { isMatch = true; } } } console.log(`🎮 Sena SDK: Check Pair [${idx1}] vs [${idx2}] -> ${isMatch ? 'MATCH' : 'WRONG'}`); if (callback) callback(isMatch); }; // ========== GAME ACTIONS ========== SenaSDK.prototype.start = function () { let self = this; // Forward to specialized SDKs if (self.isMatchingGame && window.tdv_sdk?.start) { console.log('🎮 Sena SDK: Forwarding start() to tdv_sdk'); window.tdv_sdk.start(); return; } // Standard games self.curIndex = 0; if (self.shuffle && self.data?.options) { self.shuffleArray(self.data.options); } self.startTime = Date.now(); }; SenaSDK.prototype.markUserInteraction = function () { if (!this._gameStartedByUser) { this._gameStartedByUser = true; console.log('🎮 Sena SDK: User interaction detected'); if (this.isMatchingGame && window.tdv_sdk) { window.tdv_sdk._gameStartedByUser = true; } } }; SenaSDK.prototype.canReloadData = function () { return !this._gameStartedByUser; }; SenaSDK.prototype.end = function (answer, callback) { let self = this; const category = self.gameCode ? self.gameCode.charAt(1) : ''; // ========== FORWARD TO SPECIALIZED SDKs ========== // Crossword (G9) if (category === '9' && window.tdv_crossword) { let isCorrect = (window.tdv_crossword.placedCount >= window.tdv_crossword.totalCells); let duration = window.tdv_crossword.getTimeSpent?.() || 0; console.log(`🎮 Sena SDK: Crossword Result: ${isCorrect ? 'WIN' : 'LOSE'}`); if (callback) callback(isCorrect); return { isCorrect, duration }; } // Matching/GroupSort (G3/G8) if ((category === '8' || category === '3' || self.isMatchingGame) && window.tdv_sdk) { console.log('🎮 Sena SDK: Forwarding end() to tdv_sdk'); let isCorrect = false; let duration = 0; if (String(answer || '').includes(':') && category === '8') { isCorrect = self.validateCheck(answer); } else { isCorrect = window.tdv_sdk.isCorrect?.() === 1; } duration = window.tdv_sdk.getTimeSpent?.() || 0; if (callback) callback(isCorrect); return { isCorrect, duration }; } // ========== STANDARD GAMES (Quiz/Sort/Fill) ========== self.endTime = Date.now(); const duration = (self.endTime - self.startTime) / 1000; const answerStr = String(answer || ''); const userAnswers = answerStr.includes('|') ? answerStr.split('|').map(a => a.trim().toLowerCase()).filter(a => a) : [answerStr.trim().toLowerCase()].filter(a => a); let correctAnswers = []; if (self.correctAnswer) { if (Array.isArray(self.correctAnswer)) { correctAnswers = self.correctAnswer.map(a => { if (typeof a === 'string') return a.toLowerCase(); if (a.text) return a.text.toLowerCase(); return ''; }); } else if (typeof self.correctAnswer === 'string') { correctAnswers = self.correctAnswer.includes('|') ? self.correctAnswer.split('|').map(a => a.trim().toLowerCase()) : [self.correctAnswer.toLowerCase()]; } else if (self.correctAnswer.text) { correctAnswers = [self.correctAnswer.text.toLowerCase()]; } } // Compare answers const isStrictOrder = self.gameType === 2; // Sort game const finalUser = isStrictOrder ? [...userAnswers] : [...userAnswers].sort(); const finalCorrect = isStrictOrder ? [...correctAnswers] : [...correctAnswers].sort(); let isCorrect = false; const getFileName = (url) => { if (!url.startsWith('http')) return url; return url.split('/').pop().split('?')[0]; }; if (finalUser.length === finalCorrect.length) { isCorrect = finalUser.every((uVal, index) => { let cVal = finalCorrect[index]; if (uVal === cVal) return true; if (uVal.startsWith('http') || cVal.startsWith('http')) { return getFileName(uVal) === getFileName(cVal); } return false; }); } // Check time limit if (self.timeLimit > 0 && duration > self.timeLimit) { isCorrect = false; console.log('🎮 Sena SDK: Time limit exceeded'); } const result = { isCorrect: isCorrect, duration: duration, correctAnswer: correctAnswers.join(' | '), userAnswer: userAnswers.join(' | ') }; console.log(`🎮 Sena SDK: Result: ${isCorrect ? 'CORRECT' : 'INCORRECT'} (${duration}s)`); if (callback) callback(result.isCorrect); return result; }; SenaSDK.prototype.validateCheck = function (answer) { let self = this; const answerStr = String(answer || ''); const userAnswers = answerStr.includes('|') ? answerStr.split('|').map(a => a.trim().toLowerCase()) : [answerStr.trim().toLowerCase()]; let correctAnswers = []; if (self.correctAnswer) { if (Array.isArray(self.correctAnswer)) { correctAnswers = self.correctAnswer.map(a => { if (typeof a === 'string') return a.toLowerCase(); if (a.hasOwnProperty('left') && a.hasOwnProperty('right')) { return (String(a.left) + ':' + String(a.right)).toLowerCase(); } return ''; }); } } // Partial check for GroupSort if (userAnswers.length === 1 && correctAnswers.length > 1) { return correctAnswers.includes(userAnswers[0]); } // Full check if (userAnswers.length === correctAnswers.length) { return userAnswers.every((ans, index) => ans === correctAnswers[index]); } return false; }; // ========== AUDIO ========== SenaSDK.prototype.playVoice = function (type) { let self = this; let textToSpeak = ''; if (type.startsWith('option')) { const optionIndex = parseInt(type.slice(6)) - 1; textToSpeak = self.getOptionsValue(optionIndex); } else if (type === 'question') { textToSpeak = self.getQuestionValue(); } else if (type === 'request') { textToSpeak = self.getRequestValue(); } else if (type === 'hint') { const hintValue = self.getHintValue(); if (typeof hintValue === 'string') { textToSpeak = hintValue; } } if (textToSpeak === '') return; if (window['audioPlayer']) { window['audioPlayer'].pause(); window['audioPlayer'].src = textToSpeak; window['audioPlayer'].play(); } else { window['audioPlayer'] = new Audio(textToSpeak); window['audioPlayer'].play(); } }; // ========== POSTMESSAGE ========== SenaSDK.prototype.registerPostMessageListener = function () { let self = this; window.addEventListener("message", function (event) { if (!event.data || !event.data.type) return; switch (event.data.type) { case "SERVER_PUSH_DATA": console.log('📥 Sena SDK: Received SERVER_PUSH_DATA'); if (self._postMessageTimeout) { clearTimeout(self._postMessageTimeout); self._postMessageTimeout = null; } self._waitingForPostMessage = false; // Nếu trước đó load bằng server rồi if (self._dataLoadedFromServer && !self._gameStartedByUser) { console.log('🔄 PostMessage late, reloading data'); self.postMessageDataLoaded = false; self._dataLoadedFromServer = false; } if (self._gameStartedByUser) { console.log('⚠️ Sena SDK: PostMessage ignored (user already playing)'); return; } self.loadFromPostMessage(event.data.jsonData); if (self._loadCallback) { let cb = self._loadCallback; self._loadCallback = null; cb(true); } break; case "SYNC_TIME": if (event.data.end_time_iso) { let endTimeDate = new Date(event.data.end_time_iso); let now = new Date(); self.timeLimit = Math.max(0, Math.floor((endTimeDate - now) / 1000)); console.log('🎮 Sena SDK: Time synced:', self.timeLimit, 's'); } break; case "SDK_DATA_READY": console.log('📥 Sena SDK: Received SDK_DATA_READY'); self._handleSdkDataReady(event.data.payload); break; case "SDK_PUSH_DATA": console.log('📥 Sena SDK: Received SDK_PUSH_DATA'); if (event.data.payload?.items) { self._handleSdkPushData(event.data.payload.items); } break; case "SEQUENCE_SYNC": // Đổi ở đây console.log("📥 Sena SDK: Received SEQUENCE_SYNC", event.data); if (event.data.uuid === self.uuid) { console.log("🔄 Sena SDK: Own message echoed back, processing..."); } if (typeof self.onCustomMessage === "function") { self.onCustomMessage(event.data.data, event.data.uuid); } break; case "SDK_ERROR": console.error('❌ Sena SDK: Received SDK_ERROR', event.data.payload); break; } }); console.log('🎮 Sena SDK: PostMessage listener registered'); }; /** * Send custom message to parent window with UUID * @param {Object} data - Custom JSON data to send */ SenaSDK.prototype.sendMessageToParent = function (data) { let self = this; if (!data || typeof data !== 'object') { console.error('❌ Sena SDK: sendMessageToParent requires data object'); return false; } let payload = { type: "Sequence_Sync", uuid: self.uuid, data: data, timestamp: Date.now() }; window.parent.postMessage(payload, "*"); console.log('📤 Sena SDK: Sent Sequence_Sync to parent'); console.log('🔑 UUID:', self.uuid); console.log('📦 Data:', data); return true; }; SenaSDK.prototype._handleSdkDataReady = function (payload) { let self = this; if (!payload?.items) { console.error('🎮 Sena SDK: SDK_DATA_READY missing items'); return; } if (self._postMessageTimeout) { clearTimeout(self._postMessageTimeout); self._postMessageTimeout = null; } self._waitingForPostMessage = false; let items = payload.items; if (items.length === 1) { let item = items[0]; self.data = { question: item.question || '', request: item.question || '', options: item.options?.map(o => o.text || o.audio || o) || [] }; self.gameID = item.id; } else { self.list = items.map((item, idx) => ({ id: item.id || idx, question: item.question || '', options: item.options?.map(o => o.text || o) || [], answer: null })); self.totalQuestions = items.length; self.level = payload.completed_count || 0; self._loadCurrentQuestionToData(); } self.postMessageDataLoaded = true; console.log('✅ Sena SDK: SDK_DATA_READY processed'); if (self._loadCallback) { self._loadCallback(true); self._loadCallback = null; } }; SenaSDK.prototype._handleSdkPushData = function (items) { let self = this; if (!items || items.length === 0) { console.error('🎮 Sena SDK: SDK_PUSH_DATA missing items'); return; } if (self._postMessageTimeout) { clearTimeout(self._postMessageTimeout); self._postMessageTimeout = null; } self._waitingForPostMessage = false; if (items.length === 1) { let item = items[0]; self.data = { question: item.question || '', options: item.options || [] }; self.correctAnswer = item.answer; self.gameID = item.id; } else { self.list = items.map((item, idx) => ({ id: item.id || idx, question: item.question || '', options: item.options || [], answer: item.answer })); self.totalQuestions = items.length; self.level = 0; self._loadCurrentQuestionToData(); } self.postMessageDataLoaded = true; console.log('✅ Sena SDK: SDK_PUSH_DATA processed'); if (self._loadCallback) { self._loadCallback(true); self._loadCallback = null; } }; SenaSDK.prototype.getTimeSpent = function () { if (!this.startTime) return 0; return Math.floor((Date.now() - this.startTime) / 1000); }; // ========== MULTI-QUESTION SUPPORT ========== SenaSDK.prototype.play = function (selectedText, isTimeout) { let self = this; if (!self.currentQuestion && self.data) { let result = { isCorrect: false, result: 0 }; self.end(selectedText, function (isCorrect) { result.isCorrect = isCorrect; result.result = isCorrect ? 1 : 0; }); return result.result; } if (!self.currentQuestion) return 0; let alreadyAnswered = self.userResults.find(r => r.id === self.currentQuestion.id); if (alreadyAnswered) return alreadyAnswered.result; let isCorrect = false; let userChoice = null; if (isTimeout === true || String(isTimeout) === 'true') { isCorrect = false; userChoice = null; } else { userChoice = String(selectedText).trim(); let correctAnswer = String(self.currentQuestion.answer).trim(); isCorrect = (userChoice.toLowerCase() === correctAnswer.toLowerCase()); } let resultValue = isCorrect ? 1 : 0; self.userResults.push({ id: self.currentQuestion.id, result: resultValue }); console.log('🎮 Sena SDK: Answer Result:', resultValue); return resultValue; }; SenaSDK.prototype.submitResults = function () { let self = this; let uniqueResults = []; let seenIds = {}; for (let i = self.userResults.length - 1; i >= 0; i--) { if (!seenIds[self.userResults[i].id]) { uniqueResults.unshift(self.userResults[i]); seenIds[self.userResults[i].id] = true; } } let correctCount = uniqueResults.filter(r => r.result === 1).length; let totalScore = self.totalQuestions > 0 ? Math.round((correctCount / self.totalQuestions) * 100) / 10 : 0; let timeSpent = Math.floor((Date.now() - self.startTime) / 1000); let finalData = { game_id: self.gameID, score: totalScore, time_spent: timeSpent, correct_count: correctCount, total_questions: self.totalQuestions, details: uniqueResults }; console.log('🎮 Sena SDK: Final Score:', totalScore); window.parent.postMessage({ type: "FINAL_RESULT", uuid: self.uuid, data: finalData }, "*"); return finalData; }; // ========== HELPER ========== SenaSDK.prototype.helper = {}; SenaSDK.prototype.helper.CalcObjectPositions = function (n, objectWidth, margin, maxWidth) { let self = this; self.positions = []; const totalWidth = n * objectWidth + (n - 1) * margin; const startX = (maxWidth - totalWidth) / 2; for (let i = 0; i < n; i++) { self.positions.push(startX + i * (objectWidth + margin) + objectWidth / 2); } }; SenaSDK.prototype.helper.getPosXbyIndex = function (index) { if (index < 0 || index >= this.positions.length) return null; return this.positions[index]; }; // ========== EXPORT ========== if (typeof module !== 'undefined' && module.exports) { module.exports = SenaSDK; } else if (typeof define === 'function' && define.amd) { define([], function () { return SenaSDK; }); } else { window.SenaSDK = SenaSDK; }