1196 lines
41 KiB
JavaScript
1196 lines
41 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: [],
|
||
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); }
|
||
});
|