var tdv_sdk = { mode: 'live', game_code: 'G120', sdk: null, // Instance of external SenaGameSDK // --- KHO DỮ LIỆU NỘI BỘ --- list: [], currentQuestion: null, level: 0, totalQuestions: 0, userResults: [], gameID: null, userId: null, leaderboardData: null, gameStartTime: null, endTime: null, instructions: "Sắp xếp các từ thành câu đúng!", serverDataLoaded: false, // ===== GAME MODE ===== gameMode: "fill_blank", // "full_order" | "fill_blank" missingIndices: [], // lưu index các slot bị thiếu blankIndexes: [], // danh sách index bị trống missingWords: [], lastAnswerResult: null, // { correct: true/false, raw: data } blankCount: 1, // số ô trống (server control) currentWords: [], // Mảng các từ đã shuffle correctSentence: "", // Câu đúng (gốc) correctWords: [], // Mảng các từ đúng (theo thứ tự) listPosX: [], newWidth: 150, maxWWidth: 1200, margin: 40, // --- DỮ LIỆU MẶC ĐỊNH (FALLBACK) --- defaultData: { game_id: "SQ_SENTENCE_001", user_id: "guest_user", blank_count: 2, end_time_iso: null, instructions: "Arrange the words to form correct sentences!", data: [ { "sentence": "There are some toys in the box.", "parts": [ "There", "are", "some", "toys", "in", "the", "box." ], "is_scrambled": true, "original_quote": "1. some toys are There in the box.", "id": "SE-1768646900503-0", "game_type": "sequence" }, { "sentence": "She is eating a cake.", "parts": [ "She", "is", "eating", "a", "cake." ], "is_scrambled": true, "original_quote": "2. is She a cake . eating", "id": "SE-1768646900503-1", "game_type": "sequence" }, { "sentence": "May I visit the gift shop?", "parts": [ "May", "I", "visit", "the", "gift", "shop?" ], "is_scrambled": true, "original_quote": "3. visit May I gift shop ? the", "id": "SE-1768646900503-2", "game_type": "sequence" } ] }, // Leaderboard mặc định defaultLeaderboard: { top_players: [ { rank: 1, name: "Player 1", score: 10, time_spent: 45 }, { rank: 2, name: "You", score: 9, time_spent: 52 }, { rank: 3, name: "Player 3", score: 8, time_spent: 60 } ], user_rank: { rank: "2", name: "You", score: 9, time_spent: 52 } }, themeSettings: { current_bg: "bg", bg_list: ["bg1", "bg2", "bg3", "bg4", "bg5", "bg6", "bg7", "bg8", "bg9", "bg10"] }, // --- CÁC HÀM CALLBACK --- onGameStart: function () { console.log("SDK: Sentence Game Started"); }, onAnswerChecked: function (isCorrect, data) { console.log("SDK: Answer Checked", isCorrect); }, onGameFinished: function (finalData) { console.log("SDK: Game Finished", finalData,data); }, onLeaderboardLoaded: function (data) { console.log("SDK: Leaderboard Loaded",data); }, // --- KHỞI TẠO & LOAD DỮ LIỆU --- init: function (config) { var self = this; config = config || {}; var urlParams = new URLSearchParams(window.location.search); this.mode = config.mode || urlParams.get('mode') || 'live'; this.game_code = config.game_code || urlParams.get('game_code') || 'SQ_SENTENCE'; // Auto preview if (window.self === window.parent && this.mode === 'live') { console.log("⚠️ Standalone detected - Switching to PREVIEW"); this.mode = 'preview'; } // Globals cho Construct 2 (GIỮ) window.answerResult = -1; window.gameState = 0; // BG var bgParam = urlParams.get('bg'); if (bgParam) { this.themeSettings.current_bg = bgParam.startsWith('bg') ? bgParam : "bg" + bgParam; } console.log(`🚀 TDV SDK Init | Mode: ${this.mode} | Code: ${this.game_code}`); // ===== CHECK SDK ===== if (typeof SenaGameSDK === 'undefined') { console.error("❌ SenaGameSDK not found! Fallback to local default data."); setTimeout(function () { self.load(self.defaultData); }, 500); return; } // ===== INIT SDK ===== this.sdk = new SenaGameSDK({ iframePath: './sdk/package/dist/sdk-iframe/index.html', mode: this.mode, gameCode: this.game_code, debug: true, // SDK sẵn sàng → push data onReady: function (sdk) { console.log("✅ SDK Ready → pushing defaultData"); sdk.pushData({ items: self.defaultData.data // QUAN TRỌNG: map đúng field }); }, // SDK trả data đã xử lý onDataReady: function (data) { var items = data && data.items ? data.items : []; console.log("📥 SDK DataReady:", items.length); if (items.length > 0) { self.serverDataLoaded = true; self.load({ data: items, blank_count: data.blank_count || self.defaultData.blank_count }); } else { console.warn("⚠️ SDK returned empty items"); } }, // Kết quả từng câu onAnswerResult: function (result) { console.log("📝 Answer Result:", result); self.handleAnswerResult(result); }, // Game complete onGameComplete: function (result) { console.log("🏁 Game Complete:", result); self.onGameFinished(result); }, onSessionStart: function (session) { console.log("🎮 Session:", session); self.gameID = session.gameId; self.userId = session.userId; }, onError: function (error) { console.error("❌ SDK Error:", error); } }); // ===== TIMEOUT FALLBACK ===== setTimeout(function () { if (!self.serverDataLoaded) { console.warn("⚠️ SDK timeout → fallback defaultData"); self.load(self.defaultData); } }, 5000); }, loadDefaultData: function () { if (this.list && this.list.length > 0) { console.log("📦 SDK: Data already loaded, skipping default"); return; } var now = new Date(); var endTime = new Date(now.getTime() + (180 * 1000)); // 3 phút var defaultJson = JSON.parse(JSON.stringify(this.defaultData)); defaultJson.end_time_iso = endTime.toISOString(); console.log("📦 SDK: Loading default data with", defaultJson.data.length, "sentences"); this.load(defaultJson); }, load: function (inputJson) { if (!inputJson) return; this.serverDataLoaded = true; this.list = inputJson.data || inputJson.questions || []; this.gameID = inputJson.game_id; this.userId = inputJson.user_id; this.totalQuestions = this.list.length; this.blankCount = inputJson.blank_count || 1; if (inputJson.end_time_iso) { this.endTime = new Date(inputJson.end_time_iso); } else { let fallbackSeconds = inputJson.total_time || 180; this.endTime = new Date(Date.now() + fallbackSeconds * 1000); } if (inputJson.metadata && inputJson.metadata.description) { this.instructions = inputJson.metadata.description; } const completedData = inputJson.completed_question_ids || []; this.userResults = []; let resumeLevel = 0; for (let i = 0; i < this.list.length; i++) { const done = completedData.find(item => (item.id || item) === this.list[i].id); if (done) { this.userResults.push({ id: this.list[i].id, result: done.result !== undefined ? done.result : 0 }); resumeLevel = i + 1; } else { resumeLevel = i; break; } } this.level = Math.min(resumeLevel, this.totalQuestions - 1); this.gameStartTime = new Date(); this.loadQuestions(); this.onGameStart(); if (window['TDVTriger']) window['TDVTriger'].runtime.trigger(cr.plugins_.TDVplugin.prototype.cnds.OnLoad, window['TDVTriger']); }, // --- LOGIC THỜI GIAN --- getRemainingTime: function () { if (!this.endTime) return 0; let diff = Math.floor((this.endTime - new Date()) / 1000); return diff > 0 ? diff : 0; }, getTimeSpent: function () { if (!this.gameStartTime) return 0; const now = new Date(); return Math.floor((now - this.gameStartTime) / 1000); }, formatTime: function (seconds) { if (seconds === undefined || seconds === null) return "00:00"; const mins = Math.floor(seconds / 60); const secs = seconds % 60; return String(mins).padStart(2, '0') + ":" + String(secs).padStart(2, '0'); }, // ==================== LOGIC CHÍNH: LOAD CÂU HỎI ==================== lastLoadedLevel: -1, loadQuestions: function () { if (this.lastLoadedLevel === this.level && this.currentWords.length > 0) { console.log("⏭️ Sentence", this.level + 1, "already loaded, skipping..."); return; } this.currentQuestion = this.list[this.level]; if (this.currentQuestion) { // Lấy câu đúng từ field "sentence" this.correctSentence = String(this.currentQuestion.sentence || "").trim(); // PHƯƠNG THỨC 1: Nhận parts đã chia sẵn từ server if (this.currentQuestion.parts && Array.isArray(this.currentQuestion.parts) && this.currentQuestion.parts.length > 0) { // Server đã gửi sẵn parts (có thể đã shuffle hoặc chưa) this.currentWords = [...this.currentQuestion.parts]; // Tách correctWords từ sentence để so sánh this.correctWords = this.correctSentence.split(/\s+/).filter(w => w.length > 0); console.log("📦 Using server-provided parts:", this.currentWords); } // PHƯƠNG THỨC 2: Tự tách câu và shuffle (fallback) else { // TÁCH CÂU THÀNH CÁC TỪ (by space) this.correctWords = this.correctSentence.split(/\s+/).filter(w => w.length > 0); // Shuffle các từ this.currentWords = this.shuffleArray([...this.correctWords]); console.log("🔀 Auto-split and shuffled words"); } // ===== FILL BLANK MODE ===== this.missingIndices = []; if (this.gameMode === "fill_blank") { var total = this.correctWords.length; // random 1 slot bị thiếu var missing = Math.floor(Math.random() * total); this.missingIndices.push(missing); console.log("🕳️ Missing slot index:", missing); } // Tính width dựa trên số từ và độ dài từ this.newWidth = this.calculateWordWidth(this.currentWords); // Tính vị trí X cho từng từ this.listPosX = this.getObjectPositions(this.currentWords.length, this.newWidth, this.margin, this.maxWWidth); // Đánh dấu đã load level này this.lastLoadedLevel = this.level; console.log("📝 Correct sentence:", this.correctSentence); console.log("📝 Display words:", this.currentWords); console.log("📝 Word count:", this.currentWords.length); this.generateBlankIndexes(); this.generateMissingWords(); } }, // Tính width dựa trên số từ và độ dài từ dài nhất calculateWordWidth: function (words) { var count = words.length; var maxLen = Math.max(...words.map(w => w.length)); // Base width theo độ dài từ var baseWidth = Math.max(80, maxLen * 15 + 20); // Điều chỉnh theo số từ để vừa màn hình if (count <= 3) return Math.min(200, baseWidth); if (count <= 5) return Math.min(180, baseWidth); if (count <= 7) return Math.min(150, baseWidth); return Math.min(120, baseWidth); }, generateBlankIndexes: function () { this.blankIndexes = []; var total = this.correctWords.length; var need = Math.min(this.blankCount, total); var pool = []; for (var i = 0; i < total; i++) { pool.push(i); } // shuffle pool for (var i = pool.length - 1; i > 0; i--) { var j = Math.floor(Math.random() * (i + 1)); var tmp = pool[i]; pool[i] = pool[j]; pool[j] = tmp; } // lấy need phần tử đầu this.blankIndexes = pool.slice(0, need); console.log("🕳️ Blank indexes:", this.blankIndexes); }, generateMissingWords: function () { this.missingWords = []; for (var i = 0; i < this.blankIndexes.length; i++) { var idx = this.blankIndexes[i]; this.missingWords.push(this.correctWords[idx]); } // shuffle this.shuffleArray(this.missingWords); console.log("🧩 Missing words:", this.missingWords); }, // ==================== TÍNH SCALE CHO TỪ ==================== getWordScale: function () { var wordCount = this.currentWords.length; if (wordCount <= 3) return 1.2; if (wordCount <= 4) return 1.1; if (wordCount <= 5) return 1.0; if (wordCount <= 6) return 0.9; if (wordCount <= 7) return 0.85; return 0.8; }, getTextScale: function () { return this.getWordScale(); }, getFontSize: function (baseSize) { var base = baseSize || 40; return Math.round(base * this.getWordScale()); }, // ==================== KIỂM TRA ĐÁP ÁN ==================== play: function (userAnswer, isTimeout = false) { if (!this.currentQuestion) return 0; var isActuallyTimeout = (isTimeout === true || String(isTimeout) === "true"); if (this.userResults.some(r => r.id === this.currentQuestion.id)) { return this.userResults.find(r => r.id === this.currentQuestion.id).result; } let isCorrect = false; let finalChoice = isActuallyTimeout ? "TIMEOUT" : String(userAnswer).trim(); if (!isActuallyTimeout) { // So sánh câu user sắp xếp với câu đúng (case-insensitive) let cleanCorrect = this.correctSentence.toLowerCase().replace(/\s+/g, ' ').trim(); let cleanUser = finalChoice.toLowerCase().replace(/\s+/g, ' ').trim(); isCorrect = (cleanUser === cleanCorrect); } const report = { question_id: this.currentQuestion.id, result: isCorrect ? 1 : 0, choice: finalChoice, is_timeout: isActuallyTimeout }; console.log("📤 Report Sent:", report); this.userResults.push({ id: report.question_id, result: report.result }); window.parent.postMessage({ type: "ANSWER_REPORT", data: report }, "*"); return report.result; }, // ==================== TRACKING TỪ THEO VỊ TRÍ SLOT ==================== placedWords: [], // Mảng lưu từ theo vị trí slot [0, 1, 2, 3, ...] userSequence: [], // sequence cuối cùng gửi cho SDK iframe canSubmit: 0, // 0 = chưa đủ từ, 1 = đủ từ có thể submit // Reset mảng placedWords khi bắt đầu câu hỏi mới resetPlacedWords: function () { var count = this.currentWords.length; this.placedWords = new Array(count).fill(""); this.canSubmit = 0; console.log("🔄 Reset placedWords:", this.placedWords); }, buildUserSequence: function () { var result = []; for (var i = 0; i < this.correctWords.length; i++) { // blank slot → lấy user đặt if (this.isBlankIndex(i)) { result.push(this.placedWords[i] || ""); } // preset slot → lấy từ đúng else { result.push(this.correctWords[i]); } } this.userSequence = result; console.log("📦 User sequence built:", result); return result; }, submitSequenceAnswer: function () { if (this.canSubmit !== 1) { console.warn("❌ Cannot submit – sequence incomplete"); return; } var sequence = this.buildUserSequence(); window.parent.postMessage({ type: "SDK_CHECK_ANSWER", game_id: this.gameID, question_id: this.currentQuestion.id, choice: sequence }, "*"); console.log("📤 Sent sequence to SDK iframe:", sequence); }, // Alias cho tương thích resetPlacedLetters: function () { return this.resetPlacedWords(); }, // Kiểm tra slot có trống không isSlotEmpty: function (slotIndex) { if (slotIndex < 0 || slotIndex >= this.placedWords.length) { return 0; } return this.placedWords[slotIndex] === "" ? 1 : 0; }, // Đặt từ vào slot (vị trí từ 0) // Trả về 1 = thành công, 0 = thất bại (slot đã có từ) setWordAtSlot: function (word, slotIndex) { if (slotIndex >= 0 && slotIndex < this.placedWords.length) { if (this.placedWords[slotIndex] !== "") { console.log("⚠️ Slot " + (slotIndex + 1) + " already has word '" + this.placedWords[slotIndex] + "'"); return 0; } this.placedWords[slotIndex] = word; console.log("📍 Word '" + word + "' placed at Slot " + (slotIndex + 1)); this.updateSubmitState(); return 1; } return 0; }, // Alias cho tương thích với SQ Word setLetterAtSlot: function (word, slotIndex) { return this.setWordAtSlot(word, slotIndex); }, // Xóa từ khỏi slot clearWordAtSlot: function (slotIndex) { if (slotIndex >= 0 && slotIndex < this.placedWords.length) { this.placedWords[slotIndex] = ""; console.log("🗑️ Cleared Slot " + (slotIndex + 1)); this.updateSubmitState(); } }, // Alias cho tương thích clearLetterAtSlot: function (slotIndex) { return this.clearWordAtSlot(slotIndex); }, // Cập nhật trạng thái Submit updateSubmitState: function () { var required = (this.gameMode === "fill_blank") ? this.blankIndexes.length : this.currentWords.length; var allPlaced = this.getPlacedCount() === required; var oldState = this.canSubmit; this.canSubmit = allPlaced ? 1 : 0; if (this.canSubmit !== oldState) { if (this.canSubmit === 1) { console.log("✅ All words placed! Submit button should appear."); } else { console.log("⏳ Not all words placed. Submit hidden."); } } }, handleAnswerResult: function (data) { // data ví dụ: // { // type: "SDK_ANSWER_RESULT", // question_id, // correct: true/false, // score_delta, // extra // } this.lastAnswerResult = { correct: !!data.correct, raw: data }; console.log( data.correct ? "✅ Answer correct (SDK)" : "❌ Answer wrong (SDK)", data ); // Bắn trigger cho game (chưa xử lý ở C2 bước này) if (window['TDVTriger']) { window['TDVTriger'].runtime.trigger( cr.plugins_.TDVplugin.prototype.cnds.OnAnswerChecked, window['TDVTriger'] ); } }, isLastAnswerCorrect: function () { return this.lastAnswerResult ? (this.lastAnswerResult.correct ? 1 : 0) : -1; }, getLastAnswerRaw: function () { return this.lastAnswerResult ? this.lastAnswerResult.raw : null; }, getMissingCount: function () { return this.missingWords.length; }, getMissingWordByIndex: function (index) { if (this.missingIndices.indexOf(index) === -1) return ""; console.log("🧩 Missing words:", this.missingWords); console.log("🧩 Missing words:", this.index); return this.correctWords[index] || ""; }, isBlankIndex: function (index) { return this.blankIndexes.indexOf(index) !== -1 ? 1 : 0; }, getBlankCount: function () { return this.blankIndexes.length; }, // Lấy câu đã ghép (nối các từ theo thứ tự slot) getPlacedSentence: function () { var result = []; for (var i = 0; i < this.correctWords.length; i++) { // Nếu là blank slot → lấy từ user đặt if (this.isBlankIndex(i)) { result.push(this.placedWords[i] || ""); } // Nếu là preset slot → lấy từ đúng else { result.push(this.correctWords[i]); } } return result.join(" ").replace(/\s+/g, " ").trim(); }, // Alias getPlacedWord: function () { return this.getPlacedSentence(); }, // Đếm số slot đã có từ getPlacedCount: function () { return this.placedWords.filter(w => w !== "").length; }, // Kiểm tra đã đặt đủ từ chưa isAllPlaced: function () { return this.canSubmit; }, canSubmitAnswer: function () { return this.canSubmit; }, // ==================== SUBMIT ĐÁP ÁN ==================== submitAnswer: function () { if (this.canSubmit !== 1) { console.log("❌ Cannot submit - not all words placed!"); return -1; } var userSentence = this.getPlacedSentence(); console.log("📝 SUBMIT:", userSentence, "vs", this.correctSentence); return this.play(userSentence, false); }, checkAnswer: function () { return this.submitAnswer(); }, logPlacedWord: function (wordValue, positionIndex) { console.log(`📍 Word Placed: "${wordValue}" at Slot ${positionIndex + 1}`); }, // Alias cho tương thích logPlacedLetter: function (wordValue, positionIndex) { return this.logPlacedWord(wordValue, positionIndex); }, // ===== FILL BLANK GETTERS ===== isPresetSlot: function (index) { if (this.gameMode !== "fill_blank") return 0; return this.missingIndices.indexOf(index) === -1 ? 1 : 0; }, isBlankSlot: function (index) { if (this.gameMode !== "fill_blank") return 0; return this.missingIndices.indexOf(index) !== -1 ? 1 : 0; }, // --- CÁC HÀM GETTER & TIỆN ÍCH --- getWordsCount: function () { return this.currentWords.length; }, getLettersCount: function () { return this.currentWords.length; }, // Alias // Lấy từ theo index (đã shuffle) getWordByIndex: function (i) { return this.currentWords[i] || ""; }, getLetterByIndex: function (i) { return this.currentWords[i] || ""; }, // Alias // Lấy từ đúng theo index (chưa shuffle) getCorrectWordByIndex: function (i) { return this.correctWords[i] || ""; }, getCorrectLetterByIndex: function (i) { return this.getCorrectWordByIndex(i); }, // Alias // Lấy câu đúng getCorrectSentence: function () { return this.correctSentence; }, getCorrectWord: function () { return this.correctSentence; }, // Alias getWidth: function () { return this.newWidth; }, getPosXbyIndex: function (i) { return this.listPosX[i] || 0; }, // ==================== TÍNH WIDTH/SIZE CHO TỪNG TỪ RIÊNG ==================== // Tính width cho từng từ dựa trên độ dài // Sử dụng: Browser.ExecJS("tdv_sdk.getWidthByIndex(" & loopindex & ")") getWidthByIndex: function (index) { var word = this.currentWords[index] || ""; return this.getWidthByWord(word); }, // Tính width dựa trên độ dài từ // charWidth: pixel/ký tự (mặc định 30) // minWidth: width tối thiểu (mặc định 100) // maxWidth: width tối đa (mặc định 350) getWidthByWord: function (word, charWidth, minWidth, maxWidth) { var len = String(word || "").length; var cw = charWidth || 30; // Tăng từ 25 lên 30 var min = minWidth || 100; // Tăng từ 80 lên 100 var max = maxWidth || 350; // Tăng từ 250 lên 350 var w = len * cw + 50; // +50 padding (tăng từ 40) return Math.max(min, Math.min(max, w)); }, // Tính font size cho từng từ dựa trên độ dài // baseSize: font size cơ bản (mặc định 40) // Sử dụng: Browser.ExecJS("tdv_sdk.getFontSizeByIndex(" & loopindex & ", 40)") getFontSizeByIndex: function (index, baseSize, boxWidth) { var word = this.currentWords[index] || ""; return this.getFontSizeByWord(word, baseSize, boxWidth); }, // Tính font size để text VỪA VỚI BOX // Đơn giản: từ dài → font nhỏ hơn getFontSizeByWord: function (word, baseSize, boxWidth) { var len = String(word || "").length; var base = baseSize || 50; // Quy tắc đơn giản: cứ mỗi ký tự > 5 thì giảm font if (len <= 2) return base; // "I", "He" if (len <= 3) return base; // "The", "cat" if (len <= 4) return base; // "love", "goes" if (len <= 5) return base; // "plays", "happy" if (len <= 6) return Math.round(base * 0.95); // "school" → 42 if (len <= 7) return Math.round(base * 0.85); // "English" → 37 if (len <= 8) return Math.round(base * 0.75); // "football", "everyday" → 32 if (len <= 9) return Math.round(base * 0.6); // → 27 if (len <= 10) return Math.round(base * 0.6); // "basketball" → 25 return Math.round(base * 0.6); // > 10 → 20 }, // Tính font size ĐỒNG ĐỀU cho tất cả từ (dựa trên từ dài nhất) // Sử dụng: Browser.ExecJS("tdv_sdk.getUniformFontSize(40, 200)") getUniformFontSize: function (baseSize, boxWidth) { var base = baseSize || 40; var box = boxWidth || 200; // Tìm từ dài nhất var maxLen = 0; for (var i = 0; i < this.currentWords.length; i++) { if (this.currentWords[i].length > maxLen) { maxLen = this.currentWords[i].length; } } if (maxLen === 0) return base; var neededWidth = maxLen * base * 0.6; if (neededWidth <= (box - 20)) return base; var fitSize = Math.floor((box - 20) / (maxLen * 0.6)); var minSize = Math.max(16, base * 0.4); return Math.max(minSize, fitSize); }, // ==================== TÍNH SCALE ĐỂ TEXT VỪA BOX ==================== // Tính scale cho text/sprite để vừa với box cố định // boxWidth: width của box (mặc định 200) // charPixel: số pixel trung bình mỗi ký tự ở scale 1 (mặc định 25) // Sử dụng: Browser.ExecJS("tdv_sdk.getScaleByIndex(" & loopindex & ", 200)") getScaleByIndex: function (index, boxWidth, charPixel) { var word = this.currentWords[index] || ""; return this.getScaleByWord(word, boxWidth, charPixel); }, // Tính scale dựa trên từ và boxWidth getScaleByWord: function (word, boxWidth, charPixel) { var len = String(word || "").length; var box = boxWidth || 200; var charW = charPixel || 25; // Pixel/ký tự ở scale 1 // Tính width cần thiết cho text ở scale 1 var neededWidth = len * charW; // Nếu vừa box → scale 1, nếu không → scale nhỏ lại if (neededWidth <= box) { return 1.0; } // Scale = boxWidth / neededWidth (với padding) var scale = (box - 20) / neededWidth; // -20 padding // Giới hạn scale tối thiểu 0.4 để text không quá nhỏ return Math.max(0.4, Math.round(scale * 100) / 100); }, // Tính scale đồng đều cho TẤT CẢ từ (dựa trên từ dài nhất) // Sử dụng: Browser.ExecJS("tdv_sdk.getUniformScale(200)") getUniformScale: function (boxWidth, charPixel) { var box = boxWidth || 200; var charW = charPixel || 25; // Tìm từ dài nhất var maxLen = 0; for (var i = 0; i < this.currentWords.length; i++) { var len = this.currentWords[i].length; if (len > maxLen) maxLen = len; } var neededWidth = maxLen * charW; if (neededWidth <= box) return 1.0; var scale = (box - 20) / neededWidth; return Math.max(0.4, Math.round(scale * 100) / 100); }, getCurrentImage: function () { return this.currentQuestion ? (this.currentQuestion.image || "") : ""; }, getCurrentAudio: function () { return this.currentQuestion ? (this.currentQuestion.audio || "") : ""; }, getCurrentNumber: function () { return this.level + 1; }, getTotalQuestions: function () { return this.totalQuestions; }, getInstructions: function () { return this.instructions; }, getCurrentScore: function () { if (this.totalQuestions === 0) return 0; const seenIds = new Set(); let correctCount = 0; for (let i = this.userResults.length - 1; i >= 0; i--) { const item = this.userResults[i]; if (!seenIds.has(item.id)) { if (item.result === 1) correctCount++; seenIds.add(item.id); } } let score = (correctCount / this.totalQuestions) * 10; return Math.round(score * 10) / 10; }, getFinalScore: function () { let correct = this.userResults.filter(r => r.result === 1).length; return correct * 100; }, getCorrectCountText: function () { const seenIds = new Set(); let correctCount = 0; for (let i = this.userResults.length - 1; i >= 0; i--) { const item = this.userResults[i]; if (!seenIds.has(item.id)) { if (item.result === 1) correctCount++; seenIds.add(item.id); } } return correctCount + " / " + this.totalQuestions; }, getCorrectResultText: function () { return this.getCorrectCountText(); }, getObjectPositions: function (n, objectWidth, margin, maxWidth) { const totalWidth = n * objectWidth + (n - 1) * margin; const startX = (maxWidth - totalWidth) / 2; let positions = []; for (let i = 0; i < n; i++) { positions.push(startX + i * (objectWidth + margin)); } return positions.map(pos => pos + objectWidth / 2); }, // ==================== VỊ TRÍ VỚI MARGIN CỐ ĐỊNH ==================== // leftMargin: khoảng cách bên trái (mặc định 200) // rightMargin: khoảng cách bên phải (mặc định 200) // screenWidth: width màn hình (mặc định 1920) // Tính vị trí tất cả các ô với margin cố định getFixedMarginPositions: function (leftMargin, rightMargin, screenWidth) { var left = leftMargin || 200; var right = rightMargin || 200; var screen = screenWidth || 1920; var n = this.currentWords.length; if (n <= 0) return []; // Vùng khả dụng var availableWidth = screen - left - right; // Tính khoảng cách giữa các ô var spacing = availableWidth / n; // Tính vị trí tâm của mỗi ô var positions = []; for (var i = 0; i < n; i++) { positions.push(left + spacing * i + spacing / 2); } // Lưu lại để dùng this.listPosX = positions; console.log("📍 Fixed margin positions:", positions); return positions; }, // Lấy vị trí X của ô theo index (với margin cố định) // Sử dụng: Browser.ExecJS("tdv_sdk.getFixedPosX(" & loopindex & ", 200, 200, 1920)") getFixedPosX: function (index, leftMargin, rightMargin, screenWidth) { var left = leftMargin || 200; var right = rightMargin || 200; var screen = screenWidth || 1920; var n = this.currentWords.length; if (n <= 0 || index < 0 || index >= n) return 0; var availableWidth = screen - left - right; var spacing = availableWidth / n; return left + spacing * index + spacing / 2; }, // Tính width mỗi ô (với margin cố định, các ô cùng size) // Sử dụng: Browser.ExecJS("tdv_sdk.getFixedWidth(200, 200, 1920, 20)") getFixedWidth: function (leftMargin, rightMargin, screenWidth, gap) { var left = leftMargin || 200; var right = rightMargin || 200; var screen = screenWidth || 1920; var g = gap || 20; // Khoảng cách giữa các ô var n = this.currentWords.length; if (n <= 0) return 0; var availableWidth = screen - left - right; var totalGap = (n - 1) * g; var boxWidth = (availableWidth - totalGap) / n; return Math.floor(boxWidth); }, // ==================== VỊ TRÍ CĂN GIỮA VỚI SPACING CỐ ĐỊNH ==================== // ===== LAYOUT CONFIG ===== startY: 840, spacingX: 210, spacingY: 95, // ===== HELPER: chia số ô theo số dòng ===== getRowConfig: function () { var n = this.currentWords.length; // 1 dòng if (n <= 5) { return [n]; } // 2 dòng (dòng trên nặng hơn) if (n <= 10) { var top = Math.ceil(n * 0.6); var bottom = n - top; return [top, bottom]; } // 3 dòng (giảm dần) var rows = []; var remain = n; // dòng 1 ~ 40% var r1 = Math.ceil(n * 0.4); rows.push(r1); remain -= r1; // dòng 2 ~ 35% của phần còn lại var r2 = Math.ceil(remain * 0.5); rows.push(r2); remain -= r2; // dòng 3: còn lại rows.push(remain); return rows; }, // ===== X POSITION ===== getCenteredPosX: function (index) { var rows = this.getRowConfig(); var centerX = this.maxWWidth / 2; var acc = 0; var row = 0; var col = 0; for (var i = 0; i < rows.length; i++) { if (index < acc + rows[i]) { row = i; col = index - acc; break; } acc += rows[i]; } var itemsInRow = rows[row]; var totalWidth = (itemsInRow - 1) * this.spacingX; var startX = centerX - totalWidth / 2; return startX + col * this.spacingX; }, // ===== Y POSITION ===== getCenteredPosY: function (index) { var rows = this.getRowConfig(); var acc = 0; var row = 0; for (var i = 0; i < rows.length; i++) { if (index < acc + rows[i]) { row = i; break; } acc += rows[i]; } return this.startY + row * this.spacingY; }, // ===== CHECKER Y (luôn dưới answers) ===== getCheckerBaseY: function (index) { var rows = this.getRowConfig(); // Tổng chiều cao answers var answerBlockHeight = rows.length * this.spacingY; // Xác định checker đang ở dòng nào var acc = 0; var row = 0; for (var i = 0; i < rows.length; i++) { if (index < acc + rows[i]) { row = i; break; } acc += rows[i]; } // Checker nằm dưới answers + theo dòng tương ứng return this.startY + this.spacingY // khoảng cách giữa answers & checker + row * this.spacingY // dòng checker + 10; // padding nhỏ }, // centerX: 600, // Tâm màn hình X // startY: 800, // Y của dòng đầu tiên // spacingX: 210, // Khoảng cách giữa các ô (X & Y) // spacingY: 95, // maxPerRow: 5, // Số ô mỗi dòng // // Lấy vị trí X của ô theo index (căn giữa + spacing cố định) // // Sử dụng: Browser.ExecJS("tdv_sdk.getCenteredPosX(" & loopindex & ", 960, 200)") // getCenteredPosX: function (index) { // var n = this.currentWords.length; // if (n <= 0) return this.centerX; // var row = Math.floor(index / this.maxPerRow); // // Số ô trong dòng hiện tại // var itemsInRow = Math.min( // this.maxPerRow, // n - row * this.maxPerRow // ); // var totalWidth = (itemsInRow - 1) * this.spacingX; // var startX = this.centerX - totalWidth / 2; // var col = index % this.maxPerRow; // return startX + col * this.spacingX; // }, // // ===== TÍNH Y (XUỐNG DÒNG) ===== // getCenteredPosY: function () { // // var row = Math.floor(index / this.maxPerRow); // return this.startY; // }, // getCheckerBaseY: function (index) { // var n = this.currentWords.length; // if (n <= 0) return this.startY; // var row = Math.floor(index / this.maxPerRow); // // checker nằm dưới dòng answers cuối // return this.startY + 1 * this.spacingY + row * this.spacingY + 10; // }, 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]]; } return array; }, nextQuestion: function () { if (this.level < this.totalQuestions - 1) { this.level++; this.loadQuestions(); return 1; } return 0; }, // --- AUDIO --- playAudio: function () { var audioSrc = this.getCurrentAudio(); if (!audioSrc) { console.log("No audio for this sentence"); return; } if (window.audio && !window.audio.paused) { window.audio.pause(); window.audio.currentTime = 0; } var audio = new Audio(audioSrc); window.audio = audio; audio.play().catch(e => console.error("Audio error:", e)); console.log("🔊 Playing audio:", audioSrc); }, // --- KẾT THÚC & LEADERBOARD --- forceFinishGame: function () { this.result(); }, result: function () { const uniqueResults = []; const seenIds = new Set(); for (let i = this.userResults.length - 1; i >= 0; i--) { if (!seenIds.has(this.userResults[i].id)) { uniqueResults.unshift(this.userResults[i]); seenIds.add(this.userResults[i].id); } } const totalScore = this.getCurrentScore(); const timeSpent = this.getTimeSpent(); const finalData = { game_id: this.gameID, user_id: this.userId, score: totalScore, time_spent: timeSpent, details: uniqueResults }; console.log("📊 Final Result - Score:", totalScore, "Time Spent:", timeSpent, "s"); this.onGameFinished(finalData); window.parent.postMessage({ type: "FINAL_RESULT", data: finalData }, "*"); this.leaderboard(); }, // --- LEADERBOARD --- leaderboard: function () { var self = this; window.parent.postMessage({ type: "GET_LEADERBOARD", game_id: this.gameID }, "*"); setTimeout(function () { if (!self.leaderboardData) { console.warn("⚠️ SDK: No leaderboard from server - Generating with user score"); self.loadLeaderboard(self.generateUserLeaderboard()); } }, 2000); }, generateUserLeaderboard: function () { var userScore = this.getCurrentScore(); var userName = this.userId || "You"; var userTimeSpent = this.getTimeSpent(); var topPlayers = JSON.parse(JSON.stringify(this.defaultLeaderboard.top_players)); var userPlayer = { rank: 0, name: userName, score: userScore, time_spent: userTimeSpent }; var userRankIndex = -1; for (var i = 0; i < topPlayers.length; i++) { if (userScore > topPlayers[i].score || (userScore === topPlayers[i].score && userTimeSpent < topPlayers[i].time_spent)) { userRankIndex = i; break; } } var result = { top_players: [], user_rank: null }; if (userRankIndex !== -1) { topPlayers.splice(userRankIndex, 0, userPlayer); topPlayers = topPlayers.slice(0, 3); for (var j = 0; j < topPlayers.length; j++) { topPlayers[j].rank = j + 1; } result.top_players = topPlayers; result.user_rank = topPlayers[userRankIndex]; } else { result.top_players = topPlayers; result.user_rank = { rank: 4, name: userName, score: userScore, time_spent: userTimeSpent }; } console.log("📊 Generated leaderboard - User rank:", result.user_rank.rank); return result; }, loadLeaderboard: function (data) { this.leaderboardData = data; if (window['TDVTriger']) window['TDVTriger'].runtime.trigger(cr.plugins_.TDVplugin.prototype.cnds.OnLeaderboardLoaded, window['TDVTriger']); }, getLbLength: function () { return (this.leaderboardData && this.leaderboardData.top_players) ? this.leaderboardData.top_players.length : 0; }, getLbItemY: function (index, startY, spacing) { return startY + (index * spacing); }, getLbAttr: function (index, attr) { if (!this.leaderboardData || !this.leaderboardData.top_players) return ""; var i = parseInt(index); if (!isNaN(i) && this.leaderboardData.top_players[i]) { var value = this.leaderboardData.top_players[i][attr]; if (attr === 'rank' && value !== undefined) return value; if (attr === 'time_spent' && value !== undefined) return value + "s"; return value !== undefined ? value : ""; } return ""; }, getLbUserAttr: function (attr) { if (this.leaderboardData && this.leaderboardData.user_rank) { var value = this.leaderboardData.user_rank[attr]; if (attr === 'rank' && value) return value; if (attr === 'time_spent' && value !== undefined) return value + "s"; return value !== undefined ? value : ""; } return (attr === 'score') ? "0" : (attr === 'time_spent') ? "0s" : ""; } };