Files
sentence1/G102-sequence/tdv_sdk.js
lubukhu 6c3e93636e
All checks were successful
Deploy to Production / deploy (push) Successful in 8s
up
2026-01-24 13:32:25 +07:00

1196 lines
41 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
var tdv_sdk = {
// --- 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 () {
var self = this;
const urlParams = new URLSearchParams(window.location.search);
const bgParam = urlParams.get('bg');
if (bgParam) {
this.themeSettings.current_bg = bgParam.startsWith('bg') ? bgParam : "bg" + bgParam;
}
const id = urlParams.get('game_id') || this.gameID;
console.log("🔌 SDK: Sentence Game Initialized. Waiting for data...");
window.parent.postMessage({
type: "GAME_READY",
game_id: id,
available_bgs: this.themeSettings.bg_list,
selected_bg: this.themeSettings.current_bg
}, "*");
if (urlParams.has('offline') || urlParams.has('demo')) {
console.log("🔧 SDK: Offline/Demo mode - Loading default data");
this.loadDefaultData();
} else {
setTimeout(function () {
if (!self.serverDataLoaded && self.list.length === 0) {
console.warn("⚠️ SDK: No server data after 3s - Loading default data");
self.loadDefaultData();
}
}, 3000);
}
},
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" : "";
}
};
window.addEventListener("message", function (event) {
if (!event.data) return;
if (event.data.type === "SERVER_PUSH_DATA") tdv_sdk.load(event.data.jsonData);
if (event.data.type === "SERVER_PUSH_LEADERBOARD") tdv_sdk.loadLeaderboard(event.data.leaderboardData);
if (event.data.type === "SDK_ANSWER_RESULT") { tdv_sdk.handleAnswerResult(event.data); }
});