Files
sentence2/G102-sequence/tdv_sdk.js
lubukhu 2568d138ca up
2026-01-22 14:16:00 +07:00

1122 lines
39 KiB
JavaScript

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: [],
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, ...]
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);
},
// 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.");
}
}
},
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);
});