diff --git a/G102-sequence/index.html b/G102-sequence/index.html index 5e28211..8a8b894 100644 --- a/G102-sequence/index.html +++ b/G102-sequence/index.html @@ -137,5 +137,99 @@ + + + \ No newline at end of file diff --git a/G102-sequence/tdv_sdk.js b/G102-sequence/tdv_sdk.js index 3de99ba..90767ff 100644 --- a/G102-sequence/tdv_sdk.js +++ b/G102-sequence/tdv_sdk.js @@ -1,1214 +1,122 @@ +/** + * ========================================= + * TDV_SDK – GAME HELPER (G120) + * ========================================= + * - KHÔNG làm SDK + * - KHÔNG load server + * - CHỈ quản lý UI + slot + drag + */ + var tdv_sdk = { - mode: 'live', - game_code: 'G120', - sdk: null, // Instance of external SenaGameSDK - // --- KHO DỮ LIỆU NỘI BỘ --- - list: [], + // ===== DATA FROM SDK BRIDGE ===== + gameData: [], + currentQuestionIndex: 0, currentQuestion: null, - level: 0, - totalQuestions: 0, - userResults: [], - gameID: null, - userId: null, - leaderboardData: null, - gameStartTime: null, - questionStartTime: null, - lastAnswerResult: -1, - endTime: null, - instructions: "Sắp xếp các từ thành câu đúng!", - serverDataLoaded: false, - waitingForServerVerify: false, + // ===== QUESTION DATA ===== + correctSentence: "", + currentWords: [], + correctWords: [], + blankCount: 1, - // ===== 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 + // ===== SLOT DATA ===== + placedWords: [], + blankIndexes: [], 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: [ - { - "id": "sent_test_001", - "sentence": "I love playing football", - "missing_letter_count": 2, - "audio_url": "https://api.tuandv.com/sentence.mp3" - } - ] - }, - - // 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') || 'G120'; - - // 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 len = (data && data.items) ? data.items.length : 0; - console.log("📥 SDK CALLBACK onDataReady received:", len, "items"); - if (len > 0) { - self.load(data); - } else { - console.warn("⚠️ SDK CALLBACK onDataReady had 0 items, ignoring..."); - } - }, - - // 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 (data) { - if (!data) return; - - var rawItems = data.items || data.data || []; - var itemsCount = Array.isArray(rawItems) ? rawItems.length : 0; - - // CHỐNG LOAD ĐÈ: Nếu đã có dữ liệu thật thì không load lại nữa - if (this.serverDataLoaded && this.list && this.list.length > 0) { - return; - } - - console.log("⭐ [FORCE-UPDATED] SDK Processing Data. Items found:", itemsCount); - - if (itemsCount > 0) { - this.serverDataLoaded = true; - this.list = JSON.parse(JSON.stringify(rawItems)); - this.totalQuestions = data.totalQuestions || data.total_questions || this.list.length; - this.completedCount = data.completedCount || data.completed_count || 0; - this.level = 0; - - this.gameStartTime = new Date(); - this.loadQuestions(); - - // Trigger Construct 2 - if (window['TDVTriger']) { - window['TDVTriger'].runtime.trigger(cr.plugins_.TDVplugin.prototype.cnds.OnLoad, window['TDVTriger']); - } - console.log(`✅ SUCCESS: Game loaded with ${this.list.length} questions.`); - } else { - console.warn("⏳ Received 0 items. Game will wait for real data message."); - } - }, - - // --- 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, + canSubmit: 0, + // ===== LOAD QUESTION ===== loadQuestions: function () { - if (this.lastLoadedLevel === this.level && this.currentWords.length > 0) { - console.log("⏭️ Sentence", this.level + 1, "already loaded, skipping..."); + var q = this.gameData[this.currentQuestionIndex]; + if (!q) { + console.warn("No question data"); return; } - this.currentQuestion = this.list[this.level]; - if (this.currentQuestion) { - // ===== NORMALIZE DATA (COMPANY STANDARD) ===== - var q = this.currentQuestion; + this.currentQuestion = q; - // Chuẩn công ty - if (q.question && !q.sentence) { - q.sentence = q.question; - } + // G120 chuẩn + this.correctSentence = q.question || ""; + this.currentWords = q.options || []; + this.correctWords = q.correctSequence || this.currentWords.slice(); + this.blankCount = q.blank_count || 1; - // Options / words - if (Array.isArray(q.options) && !q.parts) { - q.parts = q.options; - } + this.resetPlacedWords(); + this.generateBlankIndexes(); + this.generateMissingWords(); - // Fallback cuối - if (!q.parts && q.sentence) { - q.parts = q.sentence.split(/\s+/); - } - // ===== LOGIC GỐC (BẮT BUỘC PHẢI CÓ) ===== - this.correctSentence = String(q.sentence || "").trim(); - - if (q.parts && Array.isArray(q.parts) && q.parts.length > 0) { - this.currentWords = [...q.parts]; - this.correctWords = this.correctSentence.split(/\s+/).filter(w => w.length > 0); - console.log("📦 Using server-provided parts:", this.currentWords); - } else { - this.correctWords = this.correctSentence.split(/\s+/).filter(w => w.length > 0); - this.currentWords = this.shuffleArray([...this.correctWords]); - console.log("🔀 Auto-split and shuffled words"); - } - - // reset + blank logic - this.resetPlacedWords(); - this.generateBlankIndexes(); - this.generateMissingWords(); - - this.newWidth = this.calculateWordWidth(this.currentWords); - this.listPosX = this.getObjectPositions( - this.currentWords.length, - this.newWidth, - this.margin, - this.maxWWidth - ); - - this.lastLoadedLevel = this.level; - - console.log("📝 Correct sentence:", this.correctSentence); - console.log("📝 Display words:", this.currentWords); - console.log("📝 Word count:", this.currentWords.length); - - } - + console.log("🧩 Question loaded:", q); + console.log("📝 Sentence:", this.correctSentence); + console.log("🧩 Words:", this.currentWords); }, - // 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 + // ===== SLOT LOGIC ===== resetPlacedWords: function () { - var count = this.currentWords.length; - this.placedWords = new Array(count).fill(""); + this.placedWords = new Array(this.correctWords.length).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); + setWordAtSlot: function (word, index) { + if (this.placedWords[index] !== "") return 0; + this.placedWords[index] = word; + this.updateSubmitState(); + return 1; }, - // Alias cho tương thích - resetPlacedLetters: function () { - return this.resetPlacedWords(); + clearWordAtSlot: function (index) { + this.placedWords[index] = ""; + this.updateSubmitState(); }, - // 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 filled = this.placedWords.filter(w => w !== "").length; + this.canSubmit = (filled === this.blankIndexes.length) ? 1 : 0; + }, - var allPlaced = this.getPlacedCount() === required; + // ===== BLANK LOGIC ===== + generateBlankIndexes: function () { + this.blankIndexes = []; + var pool = [...Array(this.correctWords.length).keys()]; - var oldState = this.canSubmit; - this.canSubmit = allPlaced ? 1 : 0; + for (var i = pool.length - 1; i > 0; i--) { + var j = Math.floor(Math.random() * (i + 1)); + [pool[i], pool[j]] = [pool[j], pool[i]]; + } - if (this.canSubmit !== oldState) { - if (this.canSubmit === 1) { - console.log("✅ All words placed! Submit button should appear."); + this.blankIndexes = pool.slice(0, this.blankCount); + console.log("🕳️ Blank indexes:", this.blankIndexes); + }, + + generateMissingWords: function () { + this.missingWords = []; + for (var i = 0; i < this.blankIndexes.length; i++) { + this.missingWords.push(this.correctWords[this.blankIndexes[i]]); + } + console.log("🧩 Missing words:", this.missingWords); + }, + + // ===== BUILD SEQUENCE ===== + buildUserSequence: function () { + var result = []; + for (var i = 0; i < this.correctWords.length; i++) { + if (this.blankIndexes.indexOf(i) !== -1) { + result.push(this.placedWords[i] || ""); } else { - console.log("⏳ Not all words placed. Submit hidden."); + result.push(this.correctWords[i]); } } - }, - 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']); - }, + // ===== SUBMIT ===== + submitAnswer: function () { + if (this.canSubmit !== 1) return; - getLbLength: function () { - return (this.leaderboardData && this.leaderboardData.top_players) ? this.leaderboardData.top_players.length : 0; - }, + var sequence = this.buildUserSequence(); + console.log("📤 Submit sequence:", sequence); - 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" : ""; + window.submitSequenceAnswer(sequence); } -}; \ No newline at end of file +};