1014 lines
37 KiB
JavaScript
1014 lines
37 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: 1400,
|
|
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); },
|
|
onLeaderboardLoaded: function (data) { console.log("SDK: Leaderboard Loaded"); },
|
|
|
|
// --- 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 ====================
|
|
// centerX: tâm màn hình (mặc định 960)
|
|
// spacing: khoảng cách giữa các ô (CỐ ĐỊNH, mặc định 200)
|
|
// ===== CẤU HÌNH CHỈNH TRONG CODE =====
|
|
centerX: 600, // Tâm màn hình X
|
|
startY: 840, // 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);
|
|
});
|