1918 lines
61 KiB
JavaScript
1918 lines
61 KiB
JavaScript
/**
|
||
* Sena SDK Constructor
|
||
* @param {Object} config - Configuration object for the SDK
|
||
* @param {Object} config.data - Quiz data containing question, options, and answer
|
||
*/
|
||
function SenaSDK(gid = "G2510S1T30") {
|
||
// Initialize data
|
||
this.data = null;
|
||
this.correctAnswer = null;
|
||
this.gameCode = gid;
|
||
// Initialize properties
|
||
this.timeLimit = 0;
|
||
this.shuffle = true;
|
||
// tracking time in game
|
||
this.startTime = 0;
|
||
this.endTime = 0;
|
||
// Initialize Web Speech API
|
||
this.speechSynthesis = window.speechSynthesis;
|
||
this.currentUtterance = null;
|
||
this.voiceSettings = {
|
||
lang: "en-US",
|
||
rate: 1.0, // Speed (0.1 to 10)
|
||
pitch: 1.0, // Pitch (0 to 2)
|
||
volume: 1.0, // Volume (0 to 1)
|
||
};
|
||
|
||
// Multi-question support (from postMessage)
|
||
this.list = [];
|
||
this.currentQuestion = null;
|
||
this.level = 0;
|
||
this.totalQuestions = 0;
|
||
this.userResults = [];
|
||
this.gameID = null;
|
||
this.userId = null;
|
||
this.postMessageDataLoaded = false;
|
||
this.isMatchingGame = false; // true nếu là game matching (G3xxx)
|
||
|
||
// PostMessage listener tracking
|
||
this._postMessageListenerRegistered = false;
|
||
this._waitingForPostMessage = false;
|
||
this._postMessageTimeout = null;
|
||
this._loadCallback = null;
|
||
|
||
// Game interaction tracking - để biết có thể reload data không
|
||
this._gameStartedByUser = false; // true khi user đã tương tác (click, drag, etc.)
|
||
this._dataLoadedFromServer = false; // true nếu data từ server (không phải postMessage)
|
||
|
||
// ========== MODE SUPPORT ==========
|
||
// 'live' - Chờ vô hạn cho postMessage từ server (production)
|
||
// 'preview' - Timeout 5s rồi fallback sample (testing với data thật)
|
||
// 'dev' - Load sample ngay lập tức (development)
|
||
this.mode = "preview"; // Default mode
|
||
this.role = "student"; // Default role
|
||
}
|
||
|
||
/**
|
||
* Shuffle array using Fisher-Yates algorithm
|
||
* @param {Array} array - Array to shuffle
|
||
*/
|
||
SenaSDK.prototype.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]];
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Load data từ postMessage (SERVER_PUSH_DATA)
|
||
* Game type được xác định từ gameCode:
|
||
* - G1xxx = Quiz (trắc nghiệm)
|
||
* - G2xxx = Sort (sắp xếp)
|
||
* - G3xxx = Match (nối cặp) -> forward to tdv_sdk
|
||
* - G4xxx = Fill (điền từ)
|
||
* - G5xxx = Custom
|
||
*
|
||
* Data format:
|
||
* {
|
||
* gameCode: "G1400S1T30", // Bắt buộc - xác định loại game
|
||
* data: { request, question, options: [] },
|
||
* answer: "..."
|
||
* }
|
||
*/
|
||
SenaSDK.prototype.loadFromPostMessage = function (inputJson, callback) {
|
||
let self = this;
|
||
|
||
if (!inputJson) {
|
||
console.error("🎮 Sena SDK: No data in postMessage");
|
||
if (callback) callback(false);
|
||
return false;
|
||
}
|
||
|
||
console.log("📦 Sena SDK: Loading from PostMessage:", inputJson);
|
||
|
||
// ========== DETECT GAME TYPE TỪ GAMECODE ==========
|
||
let gameCode = inputJson.gameCode || self.gameCode;
|
||
let gameCategory = gameCode.charAt(1); // G[category]xxx - 1=Quiz, 2=Sort, 3=Match, 4=Fill, 5=Custom
|
||
|
||
console.log("🎮 Sena SDK: GameCode:", gameCode, "| Category:", gameCategory);
|
||
|
||
// ========== MATCHING GAME (G3xxx) ==========
|
||
// Chuyển tiếp cho tdv_sdk xử lý
|
||
if (gameCategory === "3") {
|
||
console.log(
|
||
"🎯 Sena SDK: Detected MATCHING GAME (G3xxx), forwarding to tdv_sdk...",
|
||
);
|
||
|
||
// Lưu thông tin cơ bản
|
||
self.gameCode = gameCode;
|
||
self.gameID = gameCode;
|
||
self.postMessageDataLoaded = true;
|
||
self.isMatchingGame = true;
|
||
self._dataLoadedFromServer = false; // Data từ postMessage, không phải server
|
||
|
||
// Forward data cho tdv_sdk nếu có
|
||
if (window.tdv_sdk) {
|
||
window.tdv_sdk.loadFromPostMessage(inputJson, callback);
|
||
console.log("✅ Sena SDK: Forwarded to tdv_sdk");
|
||
} else {
|
||
console.warn("⚠️ Sena SDK: tdv_sdk not found, storing data locally");
|
||
self.data = inputJson.data;
|
||
self.correctAnswer = inputJson.answer;
|
||
if (callback) callback(true);
|
||
}
|
||
return true;
|
||
}
|
||
|
||
// ========== QUIZ / SORT / FILL / CUSTOM (G1, G2, G4, G5) ==========
|
||
// Single question format: có gameCode và data (không phải array)
|
||
let isSingleQuestion =
|
||
inputJson.gameCode && inputJson.data && !Array.isArray(inputJson.data);
|
||
|
||
if (isSingleQuestion) {
|
||
// ========== SINGLE QUESTION (SenaSDK format) ==========
|
||
console.log("🎯 Sena SDK: Detected SINGLE QUESTION format");
|
||
|
||
self.gameCode = inputJson.gameCode || self.gameCode;
|
||
self.gameID = inputJson.gameCode;
|
||
|
||
// Parse game code để lấy settings
|
||
self._parseGameCode();
|
||
|
||
// Set data trực tiếp
|
||
self.data = {
|
||
question: inputJson.data.question || "",
|
||
request: inputJson.data.request || "",
|
||
options: inputJson.data.options || [],
|
||
image: inputJson.data.image || "",
|
||
audio: inputJson.data.audio || "",
|
||
hint: inputJson.data.hint || null,
|
||
};
|
||
|
||
// --- [UPDATE G5] Khởi tạo Master List cho G5 ---
|
||
if (self.gameType === 5 && self.data && self.data.options) {
|
||
// Lưu trữ danh sách gốc
|
||
self.masterList = [...self.data.options];
|
||
// Tính tổng số level
|
||
self.totalLevels = Math.ceil(self.masterList.length / self.itemCount);
|
||
self.currentLevel = 0;
|
||
|
||
// Load Level 1 ngay lập tức để self.data.options chỉ chứa 6 card đầu
|
||
self.loadLevelG5(1);
|
||
}
|
||
|
||
self.correctAnswer = inputJson.answer;
|
||
|
||
// Cũng set vào list để hỗ trợ multi-question API
|
||
self.list = [
|
||
{
|
||
id: "Q1",
|
||
question: inputJson.data.question,
|
||
request: inputJson.data.request,
|
||
options: inputJson.data.options,
|
||
answer: inputJson.answer,
|
||
image: inputJson.data.image,
|
||
audio: inputJson.data.audio,
|
||
},
|
||
];
|
||
self.totalQuestions = 1;
|
||
self.level = 0;
|
||
self.currentQuestion = self.list[0];
|
||
self.userResults = [];
|
||
|
||
// [UPDATE G4] Process G4 Data
|
||
if (self.gameType === 4) self._processG4Data();
|
||
|
||
console.log(
|
||
"✅ Sena SDK: Single question loaded -",
|
||
inputJson.description || inputJson.data.question,
|
||
);
|
||
} else {
|
||
// ========== MULTI-QUESTION (Exam mode) ==========
|
||
console.log("📋 Sena SDK: Detected MULTI-QUESTION format");
|
||
|
||
// Lưu thông tin game
|
||
self.gameID = inputJson.game_id || "quiz_game";
|
||
self.userId = inputJson.user_id || "guest";
|
||
|
||
// Lưu danh sách câu hỏi
|
||
self.list = inputJson.data || inputJson.questions || [];
|
||
self.totalQuestions = self.list.length;
|
||
|
||
// Xử lý thời gian
|
||
if (inputJson.total_time) {
|
||
self.timeLimit = parseInt(inputJson.total_time, 10);
|
||
}
|
||
|
||
if (inputJson.end_time_iso) {
|
||
let endTimeDate = new Date(inputJson.end_time_iso);
|
||
let now = new Date();
|
||
self.timeLimit = Math.max(0, Math.floor((endTimeDate - now) / 1000));
|
||
}
|
||
|
||
// Xử lý resume (câu hỏi đã hoàn thành)
|
||
let completedData = inputJson.completed_question_ids || [];
|
||
self.userResults = [];
|
||
let resumeLevel = 0;
|
||
|
||
for (let i = 0; i < self.list.length; i++) {
|
||
let done = completedData.find(
|
||
(item) => (item.id || item) === self.list[i].id,
|
||
);
|
||
if (done) {
|
||
self.userResults.push({
|
||
id: self.list[i].id,
|
||
result: done.result !== undefined ? done.result : 0,
|
||
});
|
||
resumeLevel = i + 1;
|
||
} else {
|
||
resumeLevel = i;
|
||
break;
|
||
}
|
||
}
|
||
|
||
self.level = Math.min(resumeLevel, self.totalQuestions - 1);
|
||
|
||
// Load câu hỏi đầu tiên và convert sang định dạng SenaSDK
|
||
self._loadCurrentQuestionToData();
|
||
|
||
console.log("✅ Sena SDK: Loaded", self.totalQuestions, "questions");
|
||
}
|
||
|
||
self.postMessageDataLoaded = true;
|
||
|
||
if (callback) callback(true);
|
||
return true;
|
||
};
|
||
|
||
/**
|
||
* [UPDATED] Parse game code để lấy settings
|
||
* Cho phép G1-G9 (thay vì G1-G5)
|
||
*/
|
||
SenaSDK.prototype._parseGameCode = function () {
|
||
let self = this;
|
||
const gameCode = self.gameCode || "G4410S1T30"; // G4 mẫu
|
||
|
||
// Regex hỗ trợ G1-G9
|
||
const regex = /^G([1-9])(\d{1,2})([0-2])([0-2])(?:S([0-1]))?(?:T(\d+))?$/;
|
||
let match = gameCode.match(regex);
|
||
|
||
if (match) {
|
||
self.gameType = parseInt(match[1], 10);
|
||
self.itemCount = parseInt(match[2], 10);
|
||
self.questionType = parseInt(match[3], 10);
|
||
self.optionType = parseInt(match[4], 10);
|
||
const shuffleFlag = match[5] !== undefined ? match[5] : "1";
|
||
const timeStr = match[6] !== undefined ? match[6] : "0";
|
||
self.shuffle = shuffleFlag === "1";
|
||
// --- [UPDATE G5] Logic Time Per Card ---
|
||
if (self.gameType === 5) {
|
||
self.timePerCard = parseInt(timeStr, 10); // T5 = 5s mỗi card
|
||
self.timeLimit = 0; // G5 không giới hạn tổng thời gian
|
||
} else {
|
||
self.timeLimit = parseInt(timeStr, 10);
|
||
self.timePerCard = 0;
|
||
}
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Load câu hỏi hiện tại vào this.data
|
||
*/
|
||
SenaSDK.prototype._loadCurrentQuestionToData = function () {
|
||
let self = this;
|
||
|
||
if (self.list.length > 0 && self.level < self.list.length) {
|
||
self.currentQuestion = self.list[self.level];
|
||
|
||
// Convert sang định dạng SenaSDK chuẩn
|
||
self.data = {
|
||
question: self.currentQuestion.question || "",
|
||
request: self.currentQuestion.request || "",
|
||
options: self.currentQuestion.options || [],
|
||
image: self.currentQuestion.image || "",
|
||
audio: self.currentQuestion.audio || "",
|
||
};
|
||
self.correctAnswer = self.currentQuestion.answer;
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Chuyển sang câu hỏi tiếp theo (multi-question mode)
|
||
*/
|
||
SenaSDK.prototype.nextQuestion = function () {
|
||
let self = this;
|
||
|
||
if (self.level < self.totalQuestions - 1) {
|
||
self.level++;
|
||
self._loadCurrentQuestionToData();
|
||
console.log(
|
||
"🎮 Sena SDK: Next Question:",
|
||
self.level + 1,
|
||
"/",
|
||
self.totalQuestions,
|
||
);
|
||
return true;
|
||
}
|
||
|
||
console.log("🎮 Sena SDK: No more questions");
|
||
return false;
|
||
};
|
||
|
||
/**
|
||
* Lấy số câu hỏi hiện tại (1-indexed)
|
||
*/
|
||
SenaSDK.prototype.getCurrentNumber = function () {
|
||
return this.level + 1;
|
||
};
|
||
|
||
/**
|
||
* Lấy tổng số câu hỏi
|
||
*/
|
||
SenaSDK.prototype.getTotalQuestions = function () {
|
||
return this.totalQuestions || 1;
|
||
};
|
||
|
||
// Settings cho postMessage waiting
|
||
SenaSDK.prototype.POSTMESSAGE_TIMEOUT_MS = 1000; // 5 giây (chỉ dùng cho preview mode)
|
||
|
||
SenaSDK.prototype.load = function (callback, template = "G2510S1T30") {
|
||
let self = this;
|
||
|
||
// *** AUTO-REGISTER postMessage listener (chỉ 1 lần) ***
|
||
if (!self._postMessageListenerRegistered) {
|
||
self.registerPostMessageListener();
|
||
self._postMessageListenerRegistered = true;
|
||
}
|
||
|
||
// Nếu đã có data từ postMessage, không cần load lại
|
||
if (self.postMessageDataLoaded && self.list.length > 0) {
|
||
console.log("🎮 Sena SDK: Data already loaded from postMessage");
|
||
if (callback) callback(true);
|
||
return;
|
||
}
|
||
|
||
// Nếu đã có data (từ bất kỳ nguồn nào), không cần load lại
|
||
if (self.data && self.data.options) {
|
||
console.log("🎮 Sena SDK: Data already available");
|
||
if (callback) callback(true);
|
||
return;
|
||
}
|
||
|
||
// ========== GET URL PARAMETERS ==========
|
||
const urlParams = new URLSearchParams(window.location.search);
|
||
|
||
// Get LID (game code) from URL
|
||
const LID = urlParams.get("LID");
|
||
if (LID) {
|
||
self.gameCode = LID;
|
||
}
|
||
|
||
// Get MODE from URL: ?mode=live | ?mode=preview | ?mode=dev
|
||
const urlMode = urlParams.get("mode");
|
||
if (urlMode && ["live", "preview", "dev"].includes(urlMode.toLowerCase())) {
|
||
self.mode = urlMode.toLowerCase();
|
||
}
|
||
|
||
// THÊM 2 DÒNG NÀY: Lấy role từ URL
|
||
const urlRole = urlParams.get("role");
|
||
if (urlRole) self.role = urlRole.toLowerCase();
|
||
|
||
console.log(
|
||
"🎮 Sena SDK: Mode =",
|
||
self.mode.toUpperCase(),
|
||
"| Role =",
|
||
self.role || "student",
|
||
"| GameCode =",
|
||
self.gameCode,
|
||
);
|
||
|
||
// Lưu callback để gọi sau
|
||
self._loadCallback = callback;
|
||
|
||
// ========== MODE-BASED LOADING ==========
|
||
switch (self.mode) {
|
||
case "live":
|
||
// LIVE MODE: Chờ vô hạn cho postMessage từ server
|
||
self._waitingForPostMessage = true;
|
||
console.log(
|
||
"⏳ Sena SDK: [LIVE MODE] Waiting for server data via postMessage (no timeout)...",
|
||
);
|
||
// Không set timeout - chờ vô hạn
|
||
// Gửi GAME_READY để server biết game sẵn sàng nhận data
|
||
self._sendGameReady();
|
||
break;
|
||
|
||
case "dev":
|
||
// DEV MODE: Load sample data ngay lập tức
|
||
console.log("🔧 Sena SDK: [DEV MODE] Loading sample data immediately...");
|
||
self._waitingForPostMessage = false;
|
||
self._loadFromServer(callback, template);
|
||
break;
|
||
|
||
case "preview":
|
||
default:
|
||
// PREVIEW MODE: Timeout 5s rồi fallback sample
|
||
self._waitingForPostMessage = true;
|
||
console.log(
|
||
"⏳ Sena SDK: [PREVIEW MODE] Waiting for postMessage (" +
|
||
self.POSTMESSAGE_TIMEOUT_MS / 1000 +
|
||
"s timeout)...",
|
||
);
|
||
|
||
// Set timeout để fallback load từ server
|
||
self._postMessageTimeout = setTimeout(function () {
|
||
if (self._waitingForPostMessage && !self.postMessageDataLoaded) {
|
||
console.warn(
|
||
"⚠️ Sena SDK: No postMessage received, fallback to sample data...",
|
||
);
|
||
self._loadFromServer(self._loadCallback, template);
|
||
}
|
||
}, self.POSTMESSAGE_TIMEOUT_MS);
|
||
break;
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Gửi GAME_READY message cho parent window
|
||
* Để server biết game đã sẵn sàng nhận data
|
||
*/
|
||
SenaSDK.prototype._sendGameReady = function () {
|
||
let self = this;
|
||
window.parent.postMessage(
|
||
{
|
||
type: "GAME_READY",
|
||
payload: {
|
||
game_id: self.gameID || self.gameCode,
|
||
game_code: self.gameCode,
|
||
mode: self.mode,
|
||
},
|
||
},
|
||
"*",
|
||
);
|
||
console.log("📤 Sena SDK: Sent GAME_READY to parent");
|
||
};
|
||
|
||
/**
|
||
* Fallback: Load data từ server khi không có postMessage
|
||
*/
|
||
SenaSDK.prototype._loadFromServer = function (
|
||
callback,
|
||
template = "G2510S1T30",
|
||
) {
|
||
let self = this;
|
||
self._waitingForPostMessage = false;
|
||
|
||
const validation = self.validateGameCode(self.gameCode);
|
||
if (!validation.valid) {
|
||
console.warn("🎮 Sena SDK: " + validation.error);
|
||
}
|
||
|
||
console.log("📡 Sena SDK: Fallback loading from server...");
|
||
|
||
fetch(`https://senaai.tech/sample/${self.gameCode}.json`)
|
||
.then((response) => response.json())
|
||
.then((data) => {
|
||
self.data = data.data;
|
||
self.correctAnswer = data.answer || null;
|
||
const gameCode = self.gameCode || template;
|
||
|
||
// FIX: Regex chấp nhận 2 chữ số cho Count
|
||
const strictRegex =
|
||
/^G([1-9])(\d{1,2})([0-2])([0-2])(?:S([0-1]))?(?:T(\d+))?$/;
|
||
let match = gameCode.match(strictRegex);
|
||
|
||
if (match) {
|
||
self.gameType = parseInt(match[1], 10);
|
||
self.itemCount = parseInt(match[2], 10);
|
||
self.questionType = parseInt(match[3], 10);
|
||
self.optionType = parseInt(match[4], 10);
|
||
const shuffleFlag = match[5] !== undefined ? match[5] : "1";
|
||
const timeStr = match[6] !== undefined ? match[6] : "0";
|
||
self.shuffle = shuffleFlag === "1";
|
||
self.timeLimit = parseInt(timeStr, 10);
|
||
|
||
console.log("🎮 Sena SDK: ✓ Valid game code parsed:", {
|
||
type: self.gameType,
|
||
count: self.itemCount,
|
||
time: self.timeLimit,
|
||
});
|
||
} else {
|
||
console.error(
|
||
"🎮 Sena SDK: ✗ Cannot parse game code (Strict match failed):",
|
||
gameCode,
|
||
);
|
||
// Fallback safe mode
|
||
self.shuffle = true;
|
||
// Nếu load được file json thì ta cứ tin tưởng lấy time trong đó nếu có, hoặc default 30s
|
||
self.timeLimit = 30;
|
||
}
|
||
// Mục đích: Tráo vị trí data ngay lập tức sau khi load xong
|
||
if (self.shuffle && self.data && self.data.options) {
|
||
self.shuffleArray(self.data.options);
|
||
console.log("🎮 Sena SDK: Data shuffled immediately on load");
|
||
}
|
||
|
||
// --- [UPDATE G5] Khởi tạo Master List cho G5 ---
|
||
if (self.gameType === 5 && self.data && self.data.options) {
|
||
// Lưu trữ danh sách gốc
|
||
self.masterList = [...self.data.options];
|
||
// Tính tổng số level
|
||
self.totalLevels = Math.ceil(self.masterList.length / self.itemCount);
|
||
self.currentLevel = 0;
|
||
|
||
// Load Level 1 ngay lập tức để self.data.options chỉ chứa 6 card đầu
|
||
self.loadLevelG5(1);
|
||
}
|
||
|
||
// [UPDATE G4] Process G4 Data
|
||
if (self.gameType === 4) self._processG4Data();
|
||
|
||
console.log("🎮 Sena SDK: Data loaded for", self.gameCode);
|
||
self._dataLoadedFromServer = true;
|
||
if (callback) callback(true);
|
||
})
|
||
.catch((error) => {
|
||
console.error("🎮 Sena SDK: Error loading data:", error);
|
||
if (callback) callback(false);
|
||
});
|
||
};
|
||
|
||
/**
|
||
* Validate game code format
|
||
* @param {string} code - Game code to validate
|
||
* @returns {Object} Validation result with valid flag and parsed values
|
||
*/
|
||
SenaSDK.prototype.validateGameCode = function (code) {
|
||
code = code || this.gameCode;
|
||
|
||
// Format: G[type][count][qType][oType]S[shuffle]T[time]
|
||
// type: 1=Quiz, 2=Sort, 3=Match/Memory, 4=Fill, 5=Custom
|
||
// count: 2-9
|
||
// qType/oType: 0=Text, 1=Image, 2=Audio
|
||
const regex = /^G([1-9])(\d{1,2})([0-2])([0-2])(?:S([0-1]))?(?:T(\d+))?$/;
|
||
const match = code.match(regex);
|
||
|
||
if (!match) {
|
||
// Check what's wrong
|
||
let error = "Invalid game code format";
|
||
const looseMatch = code.match(/^G([0-9])(\d{1,2})([0-9])([0-9])/);
|
||
|
||
if (looseMatch) {
|
||
const issues = [];
|
||
// QUAN TRỌNG: Sửa '5' thành '9' ở dòng dưới đây
|
||
if (looseMatch[1] < "1" || looseMatch[1] > "9")
|
||
issues.push("type must be 1-9");
|
||
if (looseMatch[2] < "2" || looseMatch[2] > "9")
|
||
issues.push("count must be 2-9");
|
||
if (looseMatch[3] > "2")
|
||
issues.push("qType must be 0-2 (0=Text, 1=Image, 2=Audio)");
|
||
if (looseMatch[4] > "2")
|
||
issues.push("oType must be 0-2 (0=Text, 1=Image, 2=Audio)");
|
||
if (issues.length > 0) error = issues.join(", ");
|
||
}
|
||
|
||
return { valid: false, error: error, code: code };
|
||
}
|
||
|
||
return {
|
||
valid: true,
|
||
code: code,
|
||
type: parseInt(match[1], 10),
|
||
count: parseInt(match[2], 10),
|
||
qType: parseInt(match[3], 10),
|
||
oType: parseInt(match[4], 10),
|
||
shuffle: match[5] !== "0",
|
||
time: match[6] ? parseInt(match[6], 10) : 0,
|
||
description: this.getGameCodeDescription(match),
|
||
};
|
||
};
|
||
|
||
/**
|
||
* Get human-readable description of game code
|
||
*/
|
||
SenaSDK.prototype.getGameCodeDescription = function (match) {
|
||
if (!match) return "";
|
||
|
||
const types = {
|
||
1: "Quiz",
|
||
2: "Sort/Sequence",
|
||
3: "Match/Memory",
|
||
4: "Fill",
|
||
5: "Custom",
|
||
};
|
||
const contentTypes = ["Text", "Image", "Audio"];
|
||
|
||
const gameType = types[match[1]] || "Unknown";
|
||
const qType = contentTypes[parseInt(match[3])] || "Unknown";
|
||
const oType = contentTypes[parseInt(match[4])] || "Unknown";
|
||
|
||
return `${gameType}: ${match[2]} items, ${qType} → ${oType}`;
|
||
};
|
||
/**
|
||
* Generate comprehensive developer guide based on game code
|
||
* @returns {string} Developer guide with implementation instructions
|
||
*/
|
||
SenaSDK.prototype.guide = function () {
|
||
let self = this;
|
||
const gameCode = self.gameCode || "G2510S1T30";
|
||
const data = self.data || {};
|
||
|
||
/**
|
||
* Regex giải thích:
|
||
* ^G([1-5])([2-9])([0-2])([0-2]) : Bắt buộc (Loại, Số lượng, Q, O)
|
||
* (?:S([0-1]))? : Không bắt buộc, mặc định S1
|
||
* (?:T(\d+))? : Không bắt buộc, mặc định T0
|
||
*/
|
||
const regex = /^G([1-9])([2-9])([0-2])([0-2])(?:S([0-1]))?(?:T(\d+))?$/;
|
||
const match = gameCode.match(regex);
|
||
|
||
if (!match)
|
||
return "Mã game không hợp lệ! Định dạng chuẩn: Gxxxx hoặc GxxxxSxTxx";
|
||
|
||
const category = match[1];
|
||
const count = match[2];
|
||
const qIdx = match[3];
|
||
const oIdx = match[4];
|
||
const shuffle = match[5] !== undefined ? match[5] : "1";
|
||
const time = match[6] !== undefined ? match[6] : "0";
|
||
|
||
const types = { 0: "Text", 1: "Image", 2: "Audio" };
|
||
let guide = "";
|
||
|
||
// Header
|
||
guide += `╔════════════════════════════════════════════════════════════════════════════╗\n`;
|
||
guide += `║ SENA SDK - DEVELOPER GUIDE: ${gameCode.padEnd(37)}║\n`;
|
||
guide += `╚════════════════════════════════════════════════════════════════════════════╝\n\n`;
|
||
|
||
// Game Analysis
|
||
guide += `📊 GAME ANALYSIS\n`;
|
||
guide += `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`;
|
||
|
||
let gameName = "",
|
||
instruction = "",
|
||
displayMode = "";
|
||
|
||
switch (category) {
|
||
case "1":
|
||
gameName = "Quiz (Trắc nghiệm)";
|
||
instruction = `Người dùng chọn 1 đáp án đúng trong ${count} options`;
|
||
displayMode = `Question: ${types[qIdx]} → Options: ${types[oIdx]}`;
|
||
break;
|
||
case "2":
|
||
if (qIdx === "0" && oIdx === "0") {
|
||
gameName = "Sort Word (Sắp xếp từ)";
|
||
instruction = `Sắp xếp ${count} từ/ký tự thành chuỗi hoàn chỉnh`;
|
||
} else {
|
||
gameName = "Sequences (Sắp xếp chuỗi)";
|
||
instruction = `Sắp xếp ${count} items theo đúng thứ tự`;
|
||
}
|
||
displayMode = `Hint: ${types[qIdx]} → Items: ${types[oIdx]}`;
|
||
break;
|
||
case "3":
|
||
if (qIdx === oIdx) {
|
||
gameName = "Memory Card (Trí nhớ)";
|
||
instruction = `Lật và ghép ${count} cặp thẻ giống nhau`;
|
||
} else {
|
||
gameName = "Matching (Nối cặp)";
|
||
instruction = `Nối ${count} items từ 2 nhóm với nhau`;
|
||
}
|
||
displayMode = `Group A: ${types[qIdx]} ↔ Group B: ${types[oIdx]}`;
|
||
break;
|
||
}
|
||
|
||
guide += `Game Type : ${gameName}\n`;
|
||
guide += `Objective : ${instruction}\n`;
|
||
guide += `Display Mode : ${displayMode}\n`;
|
||
guide += `Items Count : ${count}\n`;
|
||
guide += `Shuffle : ${shuffle === "1" ? "YES (call sdk.start() to shuffle)" : "NO (S0)"}\n`;
|
||
guide += `Time Limit : ${time === "0" ? "Unlimited" : time + " seconds"}\n`;
|
||
|
||
if (data.hint && data.hint.type) {
|
||
guide += `Hint Type : ${data.hint.type}\n`;
|
||
guide += `Hint Count : ${Array.isArray(data.hint.value) ? data.hint.value.length : "1"}\n`;
|
||
}
|
||
|
||
guide += `\n🔧 IMPLEMENTATION STEPS\n`;
|
||
guide += `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n`;
|
||
|
||
// Step 1: Initialize
|
||
guide += `1️⃣ INITIALIZE SDK\n`;
|
||
guide += ` var sdk = new SenaSDK('${gameCode}');\n`;
|
||
guide += ` sdk.load(function(success) {\n`;
|
||
guide += ` if (success) sdk.start();\n`;
|
||
guide += ` });\n\n`;
|
||
|
||
// Step 2: Display based on game type
|
||
guide += `2️⃣ DISPLAY UI\n`;
|
||
|
||
if (category === "1") {
|
||
// Quiz
|
||
guide += ` // Display Question\n`;
|
||
if (qIdx === "0") {
|
||
guide += ` var questionText = sdk.getQuestionValue();\n`;
|
||
guide += ` displayText(questionText); // Show text\n`;
|
||
} else if (qIdx === "1") {
|
||
guide += ` var questionImg = sdk.getQuestionValue();\n`;
|
||
guide += ` displayImage(questionImg); // Show image URL\n`;
|
||
} else if (qIdx === "2") {
|
||
guide += ` sdk.playVoice('question'); // Auto play audio\n`;
|
||
}
|
||
|
||
guide += `\n // Display Options\n`;
|
||
guide += ` var optionsCount = sdk.getOptionsCount(); // ${count} items\n`;
|
||
guide += ` for (var i = 0; i < optionsCount; i++) {\n`;
|
||
if (oIdx === "0") {
|
||
guide += ` var optionText = sdk.getOptionsValue(i).text;\n`;
|
||
guide += ` createButton(optionText, i); // Show text button\n`;
|
||
} else if (oIdx === "1") {
|
||
guide += ` var optionImg = sdk.getOptionsValue(i).image;\n`;
|
||
guide += ` createImageButton(optionImg, i); // Show image button\n`;
|
||
} else if (oIdx === "2") {
|
||
guide += ` createAudioButton(i); // Button to play audio\n`;
|
||
guide += ` // onClick: sdk.playVoice('option' + (i+1));\n`;
|
||
}
|
||
guide += ` }\n`;
|
||
} else if (category === "2") {
|
||
// Sort/Sequences
|
||
guide += ` // Display Hint (if exists)\n`;
|
||
if (qIdx === "0") {
|
||
guide += ` var hintText = sdk.getQuestionValue() || sdk.getRequestValue();\n`;
|
||
guide += ` if (hintText) displayHint(hintText);\n`;
|
||
} else if (qIdx === "1") {
|
||
guide += ` var hintImg = sdk.getQuestionValue();\n`;
|
||
guide += ` if (hintImg) displayHintImage(hintImg);\n`;
|
||
}
|
||
|
||
guide += `\n // Display Draggable Items\n`;
|
||
guide += ` var itemsCount = sdk.getOptionsCount();\n`;
|
||
guide += ` for (var i = 0; i < itemsCount; i++) {\n`;
|
||
if (oIdx === "0") {
|
||
guide += ` var itemText = sdk.getOptionsValue(i).text;\n`;
|
||
guide += ` createDraggableText(itemText, i);\n`;
|
||
} else if (oIdx === "1") {
|
||
guide += ` var itemImg = sdk.getOptionsValue(i).image;\n`;
|
||
guide += ` createDraggableImage(itemImg, i);\n`;
|
||
}
|
||
guide += ` }\n`;
|
||
guide += ` // User drags to reorder items\n`;
|
||
} else if (category === "3") {
|
||
// Memory/Matching
|
||
guide += ` var itemsCount = sdk.getOptionsCount();\n`;
|
||
if (qIdx === oIdx) {
|
||
guide += ` // Memory Card - Create pairs\n`;
|
||
guide += ` var allCards = []; // Duplicate items for pairs\n`;
|
||
guide += ` for (var i = 0; i < itemsCount; i++) {\n`;
|
||
guide += ` allCards.push(sdk.getOptionsValue(i));\n`;
|
||
guide += ` allCards.push(sdk.getOptionsValue(i)); // Duplicate\n`;
|
||
guide += ` }\n`;
|
||
guide += ` shuffleArray(allCards);\n`;
|
||
guide += ` // Create face-down cards, flip on click\n`;
|
||
} else {
|
||
guide += ` // Matching - Create two groups\n`;
|
||
guide += ` for (var i = 0; i < itemsCount; i++) {\n`;
|
||
if (qIdx === "0") {
|
||
guide += ` var leftText = sdk.getOptionsValue(i).text;\n`;
|
||
guide += ` createLeftItem(leftText, i);\n`;
|
||
} else if (qIdx === "1") {
|
||
guide += ` var leftImg = sdk.getOptionsValue(i).image;\n`;
|
||
guide += ` createLeftItem(leftImg, i);\n`;
|
||
}
|
||
if (oIdx === "0") {
|
||
guide += ` var rightText = sdk.getOptionsValue(i).text; // Matching pair\n`;
|
||
guide += ` createRightItem(rightText, i);\n`;
|
||
} else if (oIdx === "1") {
|
||
guide += ` var rightImg = sdk.getOptionsValue(i).image;\n`;
|
||
guide += ` createRightItem(rightImg, i);\n`;
|
||
}
|
||
guide += ` }\n`;
|
||
guide += ` // User draws lines to match pairs\n`;
|
||
}
|
||
}
|
||
|
||
// Step 3: Handle Hint
|
||
if (data.hint && data.hint.type) {
|
||
guide += `\n3️⃣ HANDLE HINT (Optional)\n`;
|
||
guide += ` var hintType = sdk.getHintType(); // "${data.hint.type}"\n`;
|
||
if (data.hint.type === "display") {
|
||
guide += ` var hintCount = sdk.getHintCount();\n`;
|
||
guide += ` for (var i = 0; i < hintCount; i++) {\n`;
|
||
guide += ` var hintItem = sdk.getHintValue(i);\n`;
|
||
guide += ` displayHintItem(hintItem, i); // Show each hint\n`;
|
||
guide += ` }\n`;
|
||
} else if (data.hint.type === "audio") {
|
||
guide += ` var hintAudio = sdk.getHintValue();\n`;
|
||
guide += ` createHintButton(hintAudio); // Play audio hint\n`;
|
||
} else if (data.hint.type === "text") {
|
||
guide += ` var hintText = sdk.getHintValue();\n`;
|
||
guide += ` displayHintText(hintText);\n`;
|
||
}
|
||
}
|
||
|
||
// Step 4: Check Answer
|
||
const stepNum = data.hint ? "4️⃣" : "3️⃣";
|
||
guide += `\n${stepNum} CHECK ANSWER\n`;
|
||
|
||
if (category === "1") {
|
||
guide += ` // User clicks an option\n`;
|
||
guide += ` function onOptionClick(selectedIndex) {\n`;
|
||
guide += ` var userAnswer = sdk.getOptionsValue(selectedIndex).text;\n`;
|
||
guide += ` var result = sdk.end(userAnswer, function(isCorrect) {\n`;
|
||
guide += ` if (isCorrect) showSuccess();\n`;
|
||
guide += ` else showError();\n`;
|
||
guide += ` });\n`;
|
||
guide += ` }\n`;
|
||
} else if (category === "2") {
|
||
guide += ` // User finishes sorting\n`;
|
||
guide += ` function onSubmitOrder(orderedArray) {\n`;
|
||
guide += ` var answerStr = orderedArray.map(item => item.text).join('|');\n`;
|
||
guide += ` sdk.end(answerStr, function(isCorrect) {\n`;
|
||
guide += ` if (isCorrect) showSuccess();\n`;
|
||
guide += ` else showCorrectOrder();\n`;
|
||
guide += ` });\n`;
|
||
guide += ` }\n`;
|
||
} else if (category === "3") {
|
||
guide += ` // User completes all matches/pairs\n`;
|
||
guide += ` function onAllMatched(matchedPairs) {\n`;
|
||
guide += ` var answerStr = matchedPairs.map(p => p.text).join('|');\n`;
|
||
guide += ` sdk.end(answerStr, function(isCorrect) {\n`;
|
||
guide += ` showResult(isCorrect);\n`;
|
||
guide += ` });\n`;
|
||
guide += ` }\n`;
|
||
}
|
||
|
||
// Step 5: Timer
|
||
if (time !== "0") {
|
||
const nextStep = data.hint ? "5️⃣" : "4️⃣";
|
||
guide += `\n${nextStep} TIMER COUNTDOWN\n`;
|
||
guide += ` var timeLimit = sdk.timeLimit; // ${time} seconds\n`;
|
||
guide += ` startCountdown(timeLimit);\n`;
|
||
guide += ` // If time runs out before sdk.end(), user fails\n`;
|
||
}
|
||
|
||
// API Reference
|
||
guide += `\n\n📚 KEY API METHODS\n`;
|
||
guide += `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`;
|
||
guide += `sdk.load(callback, template) - Load data from server\n`;
|
||
guide += `sdk.start() - Start game, shuffle if needed\n`;
|
||
guide += `sdk.getQuestionValue() - Get question text/image/audio\n`;
|
||
guide += `sdk.getQuestionType() - Get question type (text/image/audio)\n`;
|
||
guide += `sdk.getOptionsCount() - Get number of options\n`;
|
||
guide += `sdk.getOptionsValue(index) - Get option object at index\n`;
|
||
guide += `sdk.getOptionsType() - Get options type\n`;
|
||
guide += `sdk.getHintType() - Get hint type\n`;
|
||
guide += `sdk.getHintValue(index) - Get hint value/array item\n`;
|
||
guide += `sdk.getHintCount() - Get hint items count\n`;
|
||
guide += `sdk.playVoice(type) - Play TTS (question/option1/hint)\n`;
|
||
guide += `sdk.end(answer, callback) - Check answer & return result\n`;
|
||
guide += `sdk.timeLimit - Time limit in seconds\n`;
|
||
guide += `sdk.shuffle - Whether to shuffle options\n`;
|
||
|
||
guide += `\n\n💡 TIPS\n`;
|
||
guide += `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`;
|
||
if (shuffle === "1") {
|
||
guide += `• Options are shuffled after sdk.start() - display in new order\n`;
|
||
}
|
||
if (time !== "0") {
|
||
guide += `• Implement timer UI and auto-submit when time expires\n`;
|
||
}
|
||
guide += `• Use Web Speech API: sdk.playVoice() for TTS in English\n`;
|
||
guide += `• Multiple answers format: "answer1|answer2|answer3"\n`;
|
||
guide += `• sdk.end() returns: {isCorrect, duration, correctAnswer, userAnswer}\n`;
|
||
|
||
guide += `\n${"═".repeat(76)}\n`;
|
||
|
||
return guide;
|
||
};
|
||
/**
|
||
* Get the question text/url
|
||
* @returns {string} Question, request text, or URL
|
||
*/
|
||
SenaSDK.prototype.getQuestionValue = function () {
|
||
var q = String(this.data.question || "").trim();
|
||
// Đã bỏ chặn URL để có thể lấy link ảnh/audio
|
||
return q;
|
||
};
|
||
/**
|
||
* Get the question type
|
||
* @returns {string} Question type (text, image, audio)
|
||
*/
|
||
SenaSDK.prototype.getQuestionType = function () {
|
||
let self = this;
|
||
// based on game code, determine question type
|
||
const gameCode = self.gameCode || "G2510S1T30";
|
||
const regex = /^G([1-9])([2-9])([0-2])([0-2])(?:S([0-1]))?(?:T(\d+))?$/;
|
||
const match = gameCode.match(regex);
|
||
if (match) {
|
||
const qIdx = match[3];
|
||
const types = { 0: "text", 1: "image", 2: "audio" };
|
||
return types[qIdx] || "text";
|
||
}
|
||
return "text";
|
||
};
|
||
|
||
/**
|
||
* [UPDATE] Get the question image URL
|
||
* Dùng cho G4 khi cần hiển thị song song cả Text (getQuestionValue) và Image
|
||
* @returns {string} Image URL
|
||
*/
|
||
SenaSDK.prototype.getQuestionImage = function () {
|
||
if (this.data && this.data.image) {
|
||
return String(this.data.image).trim();
|
||
}
|
||
return "";
|
||
};
|
||
|
||
/**
|
||
* Get the request value
|
||
* @returns {string} Request text
|
||
*/
|
||
SenaSDK.prototype.getRequestValue = function () {
|
||
if (this.data && this.data.request && this.data.request !== "") {
|
||
return this.data.request;
|
||
} else {
|
||
return "";
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Get the request type (same as question type)
|
||
* @returns {string} Request type (text, image, audio)
|
||
*/
|
||
SenaSDK.prototype.getRequestType = function () {
|
||
return this.getQuestionType();
|
||
};
|
||
|
||
/**
|
||
* Get total number of options
|
||
* @returns {number} Number of options
|
||
*/
|
||
SenaSDK.prototype.getOptionsCount = function () {
|
||
if (this.data && this.data.options) {
|
||
return this.data.options.length;
|
||
}
|
||
return 0;
|
||
};
|
||
|
||
/**
|
||
* Get options type based on game code
|
||
* @returns {string} Options type (text, image, audio)
|
||
*/
|
||
SenaSDK.prototype.getOptionsType = function () {
|
||
let self = this;
|
||
// based on game code, determine options type
|
||
const gameCode = self.gameCode || "G2510S1T30";
|
||
const regex = /^G([1-9])([2-9])([0-2])([0-2])(?:S([0-1]))?(?:T(\d+))?$/;
|
||
const match = gameCode.match(regex);
|
||
if (match) {
|
||
const oIdx = match[4];
|
||
const types = { 0: "text", 1: "image", 2: "audio" };
|
||
return types[oIdx] || "text";
|
||
}
|
||
return "text";
|
||
};
|
||
/**
|
||
* Get option value by index based on options type from game code
|
||
* @param {number} index - Option index
|
||
* @returns {string} Option value (text, image URL, or audio URL based on game code)
|
||
*/
|
||
SenaSDK.prototype.getOptionsValue = function (index) {
|
||
return this.data.options[index];
|
||
};
|
||
|
||
/**
|
||
* Get hint type
|
||
* @returns {string} Hint type (e.g., 'display', 'audio', 'text', or empty string if no hint)
|
||
*/
|
||
SenaSDK.prototype.getHintType = function () {
|
||
if (this.data && this.data.hint && this.data.hint.type) {
|
||
return this.data.hint.type;
|
||
}
|
||
return "";
|
||
};
|
||
|
||
/**
|
||
* Get hint count (number of elements if hint is an array, particularly for display type)
|
||
* @returns {number} Number of elements in hint array, or 1 if not an array, or 0 if no hint
|
||
*/
|
||
SenaSDK.prototype.getHintCount = function () {
|
||
const hintValue = this.getHintValue();
|
||
if (hintValue === null) {
|
||
return 0;
|
||
}
|
||
if (Array.isArray(hintValue)) {
|
||
return hintValue.length;
|
||
}
|
||
return 1;
|
||
};
|
||
|
||
/**
|
||
* Get hint value
|
||
* @param {number} index - Optional index for array hints (display type)
|
||
* @returns {*} Hint value (string, array element, or null if no hint)
|
||
*/
|
||
SenaSDK.prototype.getHintValue = function (index) {
|
||
if (this.data && this.data.hint && this.data.hint.value !== undefined) {
|
||
const hintValue = this.data.hint.value;
|
||
const hintType = this.getHintType();
|
||
|
||
// If hint type is display and value is array, return specific index
|
||
if (hintType === "display" && Array.isArray(hintValue)) {
|
||
if (index !== undefined && index >= 0 && index < hintValue.length) {
|
||
return hintValue[index];
|
||
}
|
||
// If no index provided or invalid, return the whole array
|
||
return hintValue;
|
||
}
|
||
|
||
// For audio or text type, return the value directly
|
||
return hintValue;
|
||
}
|
||
return null;
|
||
};
|
||
|
||
/**
|
||
* Start the quiz - resets index and shuffles options
|
||
*/
|
||
SenaSDK.prototype.start = function () {
|
||
let self = this;
|
||
|
||
// Nếu là matching game, forward sang tdv_sdk.start()
|
||
if (self.isMatchingGame) {
|
||
console.log("🎮 Sena SDK: Matching game - forwarding start() to tdv_sdk");
|
||
if (window.tdv_sdk && typeof window.tdv_sdk.start === "function") {
|
||
window.tdv_sdk.start();
|
||
}
|
||
return;
|
||
}
|
||
|
||
// Quiz/Sort/Fill games
|
||
self.curIndex = 0;
|
||
if (self.shuffle && self.data && self.data.options) {
|
||
self.shuffleArray(self.data.options);
|
||
}
|
||
self.startTime = Date.now();
|
||
// Additional logic for tracking can be added here if needed
|
||
};
|
||
|
||
/**
|
||
* Đánh dấu user đã tương tác với game
|
||
* Gọi hàm này từ Construct khi user click/drag lần đầu
|
||
* Sau khi gọi, postMessage muộn sẽ bị bỏ qua
|
||
*/
|
||
SenaSDK.prototype.markUserInteraction = function () {
|
||
if (!this._gameStartedByUser) {
|
||
this._gameStartedByUser = true;
|
||
console.log(
|
||
"🎮 Sena SDK: User interaction detected - late postMessage will be ignored",
|
||
);
|
||
|
||
// Cũng đánh dấu cho tdv_sdk nếu là matching game
|
||
if (this.isMatchingGame && window.tdv_sdk) {
|
||
window.tdv_sdk._gameStartedByUser = true;
|
||
}
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Kiểm tra xem có thể reload data không (user chưa tương tác)
|
||
*/
|
||
SenaSDK.prototype.canReloadData = function () {
|
||
return !this._gameStartedByUser;
|
||
};
|
||
|
||
/**
|
||
* End the game and check answer
|
||
* [UPDATE] Support Unordered Answers & Auto-cleanup empty strings
|
||
*/
|
||
SenaSDK.prototype.end = function (answer, callback) {
|
||
let self = this;
|
||
|
||
// ========== MATCHING GAME: Forward to tdv_sdk ==========
|
||
if (self.isMatchingGame && window.tdv_sdk) {
|
||
console.log("🎮 Sena SDK: Matching game - forwarding end() to tdv_sdk");
|
||
|
||
// Lấy kết quả từ tdv_sdk
|
||
let isCorrect = window.tdv_sdk.isCorrect() === 1;
|
||
let duration = window.tdv_sdk.getTimeSpent();
|
||
let correctCount = window.tdv_sdk.getCorrectCount();
|
||
let totalPairs = window.tdv_sdk.pairCount;
|
||
|
||
console.log(`Time spent in game: ${duration} seconds`);
|
||
console.log(
|
||
`Result: ${correctCount}/${totalPairs} correct - ${isCorrect ? "CORRECT" : "INCORRECT"}`,
|
||
);
|
||
|
||
if (callback) callback(isCorrect);
|
||
return {
|
||
isCorrect: isCorrect,
|
||
duration: duration,
|
||
correctCount: correctCount,
|
||
totalPairs: totalPairs,
|
||
};
|
||
}
|
||
|
||
// ========== QUIZ/SORT/FILL GAMES ==========
|
||
self.endTime = Date.now();
|
||
const duration = (self.endTime - self.startTime) / 1000;
|
||
|
||
// 1. CLEANUP INPUT: Tách chuỗi, Xóa khoảng trắng, Chuyển thường, LỌC BỎ RỖNG
|
||
// .filter(a => a) sẽ loại bỏ ngay cái đuôi "" do dấu | thừa tạo ra
|
||
const userAnswers = answer.includes("|")
|
||
? answer
|
||
.split("|")
|
||
.map((a) => a.trim().toLowerCase())
|
||
.filter((a) => a)
|
||
: [answer.trim().toLowerCase()].filter((a) => a);
|
||
|
||
// 2. GET CORRECT ANSWERS
|
||
let correctAnswers = [];
|
||
if (self.correctAnswer) {
|
||
if (Array.isArray(self.correctAnswer)) {
|
||
correctAnswers = self.correctAnswer.map((a) =>
|
||
(typeof a === "object" ? a.text || "" : String(a)).trim().toLowerCase(),
|
||
);
|
||
} else {
|
||
let str =
|
||
typeof self.correctAnswer === "object"
|
||
? self.correctAnswer.text
|
||
: String(self.correctAnswer);
|
||
correctAnswers = str.includes("|")
|
||
? str.split("|").map((a) => a.trim().toLowerCase())
|
||
: [str.trim().toLowerCase()];
|
||
}
|
||
}
|
||
|
||
// 3. COMPARE
|
||
// Nếu là Game Type 2 (Sort) thì giữ nguyên thứ tự, nếu không thì sort (unordered)
|
||
const isStrictOrder = self.gameType === 2;
|
||
const finalUser = isStrictOrder ? [...userAnswers] : [...userAnswers].sort();
|
||
const finalCorrect = isStrictOrder
|
||
? [...correctAnswers]
|
||
: [...correctAnswers].sort();
|
||
|
||
let isCorrect = false;
|
||
|
||
// Helper check file name for URL matching
|
||
const getFileName = (url) => {
|
||
if (!url.startsWith("http")) return url;
|
||
try {
|
||
return url.split("/").pop().split("?")[0];
|
||
} catch (e) {
|
||
return url;
|
||
}
|
||
};
|
||
|
||
if (finalUser.length === finalCorrect.length) {
|
||
isCorrect = finalUser.every((uVal, index) => {
|
||
let cVal = finalCorrect[index];
|
||
if (uVal === cVal) return true;
|
||
// Fuzzy match cho URL (so sánh tên file ảnh)
|
||
if (uVal.startsWith("http") || cVal.startsWith("http")) {
|
||
return getFileName(uVal) === getFileName(cVal);
|
||
}
|
||
return false;
|
||
});
|
||
}
|
||
|
||
// -----------------------------------------------------------
|
||
// [BƯỚC 1] Kiểm tra Time Limit TRƯỚC (Sửa biến isCorrect)
|
||
// -----------------------------------------------------------
|
||
// THÊM ĐIỀU KIỆN: Nếu là teacher thì bỏ qua kiểm tra thời gian
|
||
if (
|
||
self.role !== "teacher" &&
|
||
self.timeLimit > 0 &&
|
||
duration > self.timeLimit
|
||
) {
|
||
isCorrect = false; // CHỈ sửa biến boolean, KHÔNG gọi result.isCorrect
|
||
console.log("🎮 Sena SDK: Time Limit Exceeded -> Result set to False");
|
||
}
|
||
|
||
// -----------------------------------------------------------
|
||
// [BƯỚC 2] Sau đó mới tạo biến result (Dùng isCorrect đã chốt)
|
||
// -----------------------------------------------------------
|
||
const result = {
|
||
isCorrect: isCorrect, // Lúc này isCorrect đã được xử lý xong xuôi
|
||
duration: duration,
|
||
correctAnswer: correctAnswers.join(" | "),
|
||
userAnswer: userAnswers.join(" | "),
|
||
};
|
||
|
||
// -----------------------------------------------------------
|
||
// [BƯỚC 3] Log và Return
|
||
// -----------------------------------------------------------
|
||
console.log(`Time spent: ${duration}s`);
|
||
console.log(
|
||
`Result: ${isCorrect ? "CORRECT" : "INCORRECT"} (User: ${result.userAnswer} vs Correct: ${result.correctAnswer})`,
|
||
);
|
||
|
||
if (callback) callback(result.isCorrect);
|
||
return result; // Return full object for debug
|
||
};
|
||
|
||
SenaSDK.prototype.playVoice = function (type) {
|
||
let self = this;
|
||
// type: 'question', 'optionA', 'optionB', ...
|
||
// if type is options, get corresponding option text like option0 -> index 0
|
||
let textToSpeak = "";
|
||
if (type.startsWith("option")) {
|
||
const optionIndex = parseInt(type.slice(6)) - 1;
|
||
textToSpeak = self.getOptionsValue(optionIndex);
|
||
} else if (type === "question") {
|
||
textToSpeak = self.getQuestionValue();
|
||
} else if (type === "request") {
|
||
textToSpeak = self.getRequestValue();
|
||
} else if (type === "hint") {
|
||
const hintValue = self.getHintValue();
|
||
if (typeof hintValue === "string") {
|
||
textToSpeak = hintValue;
|
||
}
|
||
}
|
||
if (textToSpeak == "") return;
|
||
if (window["audioPlayer"]) {
|
||
window["audioPlayer"].pause();
|
||
window["audioPlayer"].src = textToSpeak;
|
||
window["audioPlayer"].play();
|
||
} else {
|
||
window["audioPlayer"] = new Audio(textToSpeak);
|
||
window["audioPlayer"].play();
|
||
}
|
||
};
|
||
|
||
SenaSDK.prototype.helper = {};
|
||
|
||
SenaSDK.prototype.helper.CalcObjectPositions = function (
|
||
n,
|
||
objectWidth,
|
||
margin,
|
||
maxWidth,
|
||
) {
|
||
let self = this;
|
||
self.positions = [];
|
||
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));
|
||
}
|
||
positions.map((pos) => pos + objectWidth / 2); // Adjusting to center the objects
|
||
self.positions = positions;
|
||
};
|
||
SenaSDK.prototype.helper.getPosXbyIndex = function (index) {
|
||
let self = this;
|
||
if (index < 0 || index >= self.positions.length) {
|
||
return null; // Return null if index is out of bounds
|
||
}
|
||
return self.positions[index];
|
||
};
|
||
|
||
/**
|
||
* Đăng ký listener nhận data qua postMessage
|
||
* Gọi hàm này sau khi tạo instance SenaSDK
|
||
*/
|
||
SenaSDK.prototype.registerPostMessageListener = function () {
|
||
let self = this;
|
||
|
||
window.addEventListener("message", function (event) {
|
||
if (!event.data || !event.data.type) return;
|
||
|
||
switch (event.data.type) {
|
||
case "SERVER_PUSH_DATA":
|
||
console.log(
|
||
"📥 Sena SDK: Received SERVER_PUSH_DATA",
|
||
event.data.jsonData,
|
||
);
|
||
|
||
// Hủy timeout fallback nếu đang chờ
|
||
if (self._postMessageTimeout) {
|
||
clearTimeout(self._postMessageTimeout);
|
||
self._postMessageTimeout = null;
|
||
}
|
||
self._waitingForPostMessage = false;
|
||
|
||
// ========== LATE ARRIVAL HANDLING ==========
|
||
// Nếu đã load từ server NHƯNG user chưa tương tác → cho phép override
|
||
if (self._dataLoadedFromServer && !self._gameStartedByUser) {
|
||
console.log(
|
||
"🔄 Sena SDK: PostMessage arrived late but user hasn't started - RELOADING DATA",
|
||
);
|
||
// Reset state trước khi load mới
|
||
self.postMessageDataLoaded = false;
|
||
self._dataLoadedFromServer = false;
|
||
}
|
||
|
||
// Nếu user đã tương tác → bỏ qua postMessage muộn
|
||
if (self._gameStartedByUser) {
|
||
console.log(
|
||
"⚠️ Sena SDK: PostMessage arrived late but user already playing - IGNORED",
|
||
);
|
||
return;
|
||
}
|
||
|
||
// Load data từ postMessage
|
||
self.loadFromPostMessage(event.data.jsonData);
|
||
|
||
// Nếu là matching game, cũng forward cho tdv_sdk listener
|
||
let gameCode = event.data.jsonData && event.data.jsonData.gameCode;
|
||
if (gameCode && gameCode.charAt(1) === "3" && window.tdv_sdk) {
|
||
// Hủy timeout của tdv_sdk nếu có
|
||
if (window.tdv_sdk._postMessageTimeout) {
|
||
clearTimeout(window.tdv_sdk._postMessageTimeout);
|
||
window.tdv_sdk._postMessageTimeout = null;
|
||
}
|
||
window.tdv_sdk._waitingForPostMessage = false;
|
||
|
||
// Gọi callback của tdv_sdk nếu có
|
||
if (window.tdv_sdk._loadCallback) {
|
||
window.tdv_sdk._loadCallback(true);
|
||
window.tdv_sdk._loadCallback = null;
|
||
}
|
||
}
|
||
|
||
// Gọi callback nếu có (từ hàm load())
|
||
if (self._loadCallback) {
|
||
self._loadCallback(true);
|
||
self._loadCallback = null;
|
||
}
|
||
break;
|
||
|
||
case "SYNC_TIME":
|
||
if (event.data.end_time_iso) {
|
||
let endTimeDate = new Date(event.data.end_time_iso);
|
||
let now = new Date();
|
||
self.timeLimit = Math.max(0, Math.floor((endTimeDate - now) / 1000));
|
||
console.log(
|
||
"🎮 Sena SDK: Time synced, remaining:",
|
||
self.timeLimit,
|
||
"s",
|
||
);
|
||
|
||
// Sync cho tdv_sdk nếu có
|
||
if (window.tdv_sdk) {
|
||
window.tdv_sdk.timeLimit = self.timeLimit;
|
||
window.tdv_sdk.endTime = endTimeDate;
|
||
}
|
||
}
|
||
break;
|
||
|
||
// ========== NEW MESSAGE TYPES (theo README protocol) ==========
|
||
|
||
case "SDK_DATA_READY":
|
||
// Server gửi sanitized data cho game render (LIVE mode)
|
||
console.log("📥 Sena SDK: Received SDK_DATA_READY", event.data.payload);
|
||
self._handleSdkDataReady(event.data.payload);
|
||
break;
|
||
|
||
case "SDK_PUSH_DATA":
|
||
// Preview mode: Game push data để SDK sanitize
|
||
console.log("📥 Sena SDK: Received SDK_PUSH_DATA", event.data.payload);
|
||
if (event.data.payload && event.data.payload.items) {
|
||
// Convert items to our format và load
|
||
self._handleSdkPushData(event.data.payload.items);
|
||
}
|
||
break;
|
||
|
||
case "SDK_ANSWER_RESULT":
|
||
// Server trả về kết quả verify answer
|
||
console.log(
|
||
"📥 Sena SDK: Received SDK_ANSWER_RESULT",
|
||
event.data.payload,
|
||
);
|
||
self._lastAnswerResult = event.data.payload;
|
||
// Trigger callback nếu có
|
||
if (self._answerResultCallback) {
|
||
self._answerResultCallback(event.data.payload);
|
||
self._answerResultCallback = null;
|
||
}
|
||
break;
|
||
|
||
case "SEQUENCE_SYNC":
|
||
console.log("📥 Sena SDK: Received SEQUENCE_SYNC", event.data);
|
||
if (event.data.uuid === self.uuid) {
|
||
console.log("🔄 Sena SDK: Own message echoed back, processing...");
|
||
}
|
||
|
||
if (typeof self.onCustomMessage === "function") {
|
||
self.onCustomMessage(event.data.data, event.data.uuid);
|
||
}
|
||
break;
|
||
|
||
case "SDK_ERROR":
|
||
// Server gửi error
|
||
console.error("❌ Sena SDK: Received SDK_ERROR", event.data.payload);
|
||
break;
|
||
}
|
||
});
|
||
|
||
console.log("🎮 Sena SDK: PostMessage listener registered");
|
||
console.log(
|
||
" Supported types: SERVER_PUSH_DATA, SDK_DATA_READY, SDK_PUSH_DATA, SYNC_TIME",
|
||
);
|
||
|
||
// Đăng ký tdv_sdk listener nếu có (không cần gọi registerPostMessageListener riêng)
|
||
if (window.tdv_sdk && !window.tdv_sdk._postMessageListenerRegistered) {
|
||
window.tdv_sdk._postMessageListenerRegistered = true;
|
||
console.log("🎮 Sena SDK: tdv_sdk will receive forwarded data");
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Handle SDK_DATA_READY message (LIVE mode - server gửi sanitized data)
|
||
*/
|
||
SenaSDK.prototype._handleSdkDataReady = function (payload) {
|
||
let self = this;
|
||
|
||
if (!payload || !payload.items) {
|
||
console.error("🎮 Sena SDK: SDK_DATA_READY missing items");
|
||
return;
|
||
}
|
||
|
||
// Hủy timeout nếu có
|
||
if (self._postMessageTimeout) {
|
||
clearTimeout(self._postMessageTimeout);
|
||
self._postMessageTimeout = null;
|
||
}
|
||
self._waitingForPostMessage = false;
|
||
|
||
// Convert SDK format to SenaSDK format
|
||
let items = payload.items;
|
||
|
||
if (items.length === 1) {
|
||
// Single question
|
||
let item = items[0];
|
||
self.data = {
|
||
question: item.question || "",
|
||
request: item.question || "",
|
||
options: item.options
|
||
? item.options.map((o) => o.text || o.audio || o)
|
||
: [],
|
||
};
|
||
self.gameID = item.id;
|
||
} else {
|
||
// Multi-question
|
||
self.list = items.map((item, idx) => ({
|
||
id: item.id || idx,
|
||
question: item.question || "",
|
||
request: item.question || "",
|
||
options: item.options
|
||
? item.options.map((o) => o.text || o.audio || o)
|
||
: [],
|
||
answer: null, // Server keeps answer
|
||
}));
|
||
self.totalQuestions = items.length;
|
||
self.level = payload.completed_count || 0;
|
||
self._loadCurrentQuestionToData();
|
||
}
|
||
|
||
self.postMessageDataLoaded = true;
|
||
console.log("✅ Sena SDK: SDK_DATA_READY processed -", items.length, "items");
|
||
|
||
// Gọi callback
|
||
if (self._loadCallback) {
|
||
self._loadCallback(true);
|
||
self._loadCallback = null;
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Handle SDK_PUSH_DATA message (PREVIEW mode - game push data với answer)
|
||
*/
|
||
SenaSDK.prototype._handleSdkPushData = function (items) {
|
||
let self = this;
|
||
|
||
if (!items || items.length === 0) {
|
||
console.error("🎮 Sena SDK: SDK_PUSH_DATA missing items");
|
||
return;
|
||
}
|
||
|
||
// Hủy timeout nếu có
|
||
if (self._postMessageTimeout) {
|
||
clearTimeout(self._postMessageTimeout);
|
||
self._postMessageTimeout = null;
|
||
}
|
||
self._waitingForPostMessage = false;
|
||
|
||
if (items.length === 1) {
|
||
// Single question
|
||
let item = items[0];
|
||
self.data = {
|
||
question: item.question || "",
|
||
request: item.question || "",
|
||
options: item.options
|
||
? item.options.map((o) => o.text || o.audio || o)
|
||
: [],
|
||
};
|
||
self.correctAnswer = item.answer;
|
||
self.gameID = item.id;
|
||
} else {
|
||
// Multi-question với answer
|
||
self.list = items.map((item, idx) => ({
|
||
id: item.id || idx,
|
||
question: item.question || "",
|
||
request: item.question || "",
|
||
options: item.options
|
||
? item.options.map((o) => o.text || o.audio || o)
|
||
: [],
|
||
answer: item.answer,
|
||
}));
|
||
self.totalQuestions = items.length;
|
||
self.level = 0;
|
||
self._loadCurrentQuestionToData();
|
||
}
|
||
|
||
self.postMessageDataLoaded = true;
|
||
console.log("✅ Sena SDK: SDK_PUSH_DATA processed -", items.length, "items");
|
||
|
||
// Gọi callback
|
||
if (self._loadCallback) {
|
||
self._loadCallback(true);
|
||
self._loadCallback = null;
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Gửi SDK_CHECK_ANSWER cho server verify (LIVE mode)
|
||
*/
|
||
SenaSDK.prototype.checkAnswerViaServer = function (
|
||
questionId,
|
||
choice,
|
||
callback,
|
||
) {
|
||
let self = this;
|
||
|
||
self._answerResultCallback = callback;
|
||
|
||
window.parent.postMessage(
|
||
{
|
||
type: "SDK_CHECK_ANSWER",
|
||
payload: {
|
||
question_id: questionId,
|
||
choice: choice,
|
||
time_spent: self.getTimeSpent ? self.getTimeSpent() * 1000 : 0,
|
||
},
|
||
},
|
||
"*",
|
||
);
|
||
|
||
console.log("📤 Sena SDK: Sent SDK_CHECK_ANSWER for", questionId);
|
||
};
|
||
|
||
/**
|
||
* Get time spent in seconds
|
||
*/
|
||
SenaSDK.prototype.getTimeSpent = function () {
|
||
if (!this.startTime) return 0;
|
||
return Math.floor((Date.now() - this.startTime) / 1000);
|
||
};
|
||
|
||
/**
|
||
* Submit đáp án cho multi-question mode
|
||
* @param {string} selectedText - Đáp án người dùng chọn
|
||
* @param {boolean} isTimeout - True nếu hết giờ
|
||
* @returns {Object} Result object {isCorrect, result}
|
||
*/
|
||
SenaSDK.prototype.play = function (selectedText, isTimeout) {
|
||
let self = this;
|
||
|
||
// Nếu không có list (single question mode), dùng end()
|
||
if (!self.currentQuestion && self.data) {
|
||
let result = { isCorrect: false, result: 0 };
|
||
self.end(selectedText, function (isCorrect) {
|
||
result.isCorrect = isCorrect;
|
||
result.result = isCorrect ? 1 : 0;
|
||
});
|
||
return result.result;
|
||
}
|
||
|
||
if (!self.currentQuestion) return 0;
|
||
|
||
let isActuallyTimeout = isTimeout === true || String(isTimeout) === "true";
|
||
|
||
// Kiểm tra đã trả lời câu này chưa
|
||
let alreadyAnswered = self.userResults.find(
|
||
(r) => r.id === self.currentQuestion.id,
|
||
);
|
||
if (alreadyAnswered) {
|
||
return alreadyAnswered.result;
|
||
}
|
||
|
||
let isCorrect = false;
|
||
let userChoice = null;
|
||
|
||
if (isActuallyTimeout) {
|
||
isCorrect = false;
|
||
userChoice = null;
|
||
} else {
|
||
userChoice = String(selectedText).trim();
|
||
let correctAnswer = String(self.currentQuestion.answer).trim();
|
||
isCorrect = userChoice.toLowerCase() === correctAnswer.toLowerCase();
|
||
}
|
||
|
||
let resultValue = isCorrect ? 1 : 0;
|
||
|
||
let report = {
|
||
question_id: self.currentQuestion.id,
|
||
result: resultValue,
|
||
choice: userChoice,
|
||
is_timeout: isActuallyTimeout,
|
||
};
|
||
|
||
self.userResults.push({ id: report.question_id, result: report.result });
|
||
|
||
console.log("🎮 Sena SDK: Answer Report:", report);
|
||
|
||
// Gửi kết quả cho parent window
|
||
window.parent.postMessage({ type: "ANSWER_REPORT", data: report }, "*");
|
||
|
||
return resultValue;
|
||
};
|
||
|
||
/**
|
||
* Kết thúc bài thi và gửi kết quả (multi-question mode)
|
||
*/
|
||
SenaSDK.prototype.submitResults = function () {
|
||
let self = this;
|
||
|
||
// Loại bỏ duplicate results
|
||
let uniqueResults = [];
|
||
let seenIds = {};
|
||
for (let i = self.userResults.length - 1; i >= 0; i--) {
|
||
if (!seenIds[self.userResults[i].id]) {
|
||
uniqueResults.unshift(self.userResults[i]);
|
||
seenIds[self.userResults[i].id] = true;
|
||
}
|
||
}
|
||
|
||
let correctCount = uniqueResults.filter((r) => r.result === 1).length;
|
||
let totalScore =
|
||
self.totalQuestions > 0
|
||
? Math.round((correctCount / self.totalQuestions) * 100) / 10
|
||
: 0;
|
||
let timeSpent = Math.floor((Date.now() - self.startTime) / 1000);
|
||
|
||
let finalData = {
|
||
game_id: self.gameID,
|
||
user_id: self.userId,
|
||
score: totalScore,
|
||
time_spent: timeSpent,
|
||
correct_count: correctCount,
|
||
total_questions: self.totalQuestions,
|
||
details: uniqueResults,
|
||
};
|
||
|
||
console.log(
|
||
"🎮 Sena SDK: Final Result - Score:",
|
||
totalScore,
|
||
"Time Spent:",
|
||
timeSpent,
|
||
"s",
|
||
);
|
||
|
||
// Gửi kết quả cho parent window
|
||
window.parent.postMessage({ type: "FINAL_RESULT", data: finalData }, "*");
|
||
|
||
return finalData;
|
||
};
|
||
|
||
// Export for different module systems
|
||
if (typeof module !== "undefined" && module.exports) {
|
||
module.exports = SenaSDK;
|
||
} else if (typeof define === "function" && define.amd) {
|
||
define([], function () {
|
||
return SenaSDK;
|
||
});
|
||
} else {
|
||
window.SenaSDK = SenaSDK;
|
||
}
|
||
/**
|
||
* [NEW] Get Target Name for G9 (Memory Shuffle)
|
||
* Lấy tên đáp án đúng (Ví dụ: "cat")
|
||
*/
|
||
SenaSDK.prototype.getTargetName = function () {
|
||
// Với G9, correctAnswer sẽ lưu target name
|
||
if (this.correctAnswer) {
|
||
// Nếu correctAnswer là object (trường hợp hiếm), lấy text
|
||
if (typeof this.correctAnswer === "object" && this.correctAnswer.text) {
|
||
return this.correctAnswer.text;
|
||
}
|
||
return String(this.correctAnswer);
|
||
}
|
||
return "";
|
||
};
|
||
/**
|
||
* [NEW] Helpers for Card Object (G9)
|
||
*/
|
||
SenaSDK.prototype.getCardName = function (index) {
|
||
if (this.data && this.data.options && this.data.options[index]) {
|
||
let opt = this.data.options[index];
|
||
return opt.name || opt.text || "";
|
||
}
|
||
return "";
|
||
};
|
||
SenaSDK.prototype.getCardImage = function (index) {
|
||
if (this.data && this.data.options && this.data.options[index]) {
|
||
let opt = this.data.options[index];
|
||
return opt.image || "";
|
||
}
|
||
return "";
|
||
};
|
||
SenaSDK.prototype.getCardAudio = function (index) {
|
||
if (this.data && this.data.options && this.data.options[index]) {
|
||
let opt = this.data.options[index];
|
||
return opt.audio || "";
|
||
}
|
||
return "";
|
||
};
|
||
|
||
/**
|
||
* [UPDATE G5] Load data cho level cụ thể (Phân trang tự nhiên)
|
||
* Logic mới: Lấy vừa đủ số lượng còn lại, không lặp lại (wrap-around) data cũ.
|
||
*/
|
||
SenaSDK.prototype.loadLevelG5 = function (levelIndex) {
|
||
let self = this;
|
||
if (self.gameType !== 5 || !self.masterList) return false;
|
||
|
||
self.currentLevel = levelIndex;
|
||
let count = self.itemCount; // Số card tối đa mỗi trang (VD: 6)
|
||
|
||
window.Sena_TotalLevels = Math.ceil(self.masterList.length / count);
|
||
|
||
let startIndex = (levelIndex - 1) * count;
|
||
|
||
// --- LOGIC MỚI: CẮT DATA (SLICING) ---
|
||
// Tính điểm kết thúc: Nếu vượt quá độ dài list thì lấy độ dài list (không wrap)
|
||
let endIndex = Math.min(startIndex + count, self.masterList.length);
|
||
|
||
// Cắt danh sách card cho level hiện tại
|
||
let levelOptions = self.masterList.slice(startIndex, endIndex);
|
||
|
||
// Gán vào data.options để C2 render
|
||
self.data.options = levelOptions;
|
||
|
||
console.log(
|
||
`🎮 Sena SDK: Loaded Level ${levelIndex} (G5) with ${levelOptions.length} cards`,
|
||
);
|
||
return true;
|
||
};
|
||
|
||
/**
|
||
* [NEW G5] Lấy thông tin Level
|
||
*/
|
||
SenaSDK.prototype.getTotalLevels = function () {
|
||
return this.totalLevels || 1;
|
||
};
|
||
|
||
SenaSDK.prototype.getTimePerCard = function () {
|
||
if (this.timePerCard === undefined) {
|
||
this._parseGameCode();
|
||
}
|
||
|
||
if (this.timePerCard && this.timePerCard > 0) {
|
||
return this.timePerCard;
|
||
}
|
||
|
||
return 5;
|
||
};
|
||
|
||
SenaSDK.prototype.getCardType = function (index) {
|
||
// Ưu tiên 1: Lấy từ data.options (G4, G1, G2 đang chạy trên grid hiện tại)
|
||
if (this.data && this.data.options && this.data.options[index]) {
|
||
return this.data.options[index].type || "text";
|
||
}
|
||
|
||
// Ưu tiên 2: Fallback cho G5 (Master List)
|
||
if (this.masterList && this.masterList[index]) {
|
||
return this.masterList[index].type || "text";
|
||
}
|
||
|
||
// Mặc định
|
||
return "text";
|
||
};
|
||
|
||
// [UPDATE G4] Xử lý data đặc thù cho Memory Card: Fill blank và Xử lý thẻ lẻ (Orphan)
|
||
SenaSDK.prototype._processG4Data = function () {
|
||
let self = this;
|
||
if (!self.data.options) self.data.options = [];
|
||
|
||
// BƯỚC 1: Xử lý thẻ lẻ (Sanitize Data) ngay tại nguồn
|
||
// Đếm số lượng xuất hiện của từng cặp tên
|
||
let counts = {};
|
||
self.data.options.forEach((item) => {
|
||
if (item.type !== "blank" && item.name) {
|
||
counts[item.name] = (counts[item.name] || 0) + 1;
|
||
}
|
||
});
|
||
|
||
// Duyệt lại và biến những thẻ có số lượng < 2 thành blank
|
||
self.data.options.forEach((item) => {
|
||
if (item.type !== "blank" && item.name) {
|
||
if (counts[item.name] < 2) {
|
||
console.log("🎮 Sena SDK: Orphan card detected & removed:", item.name);
|
||
item.type = "blank";
|
||
item.name = "blank"; // Xóa tên để tránh logic game bắt nhầm
|
||
item.image = ""; // Xóa ảnh
|
||
item.id = "blank_sanitized";
|
||
}
|
||
}
|
||
});
|
||
|
||
// BƯỚC 2: Fill thêm thẻ blank cho đủ 9 ô (Logic cũ)
|
||
while (self.data.options.length < 9) {
|
||
self.data.options.push({
|
||
id: "blank_" + self.data.options.length,
|
||
type: "blank",
|
||
name: "blank",
|
||
value: -1,
|
||
image: "",
|
||
});
|
||
}
|
||
|
||
// BƯỚC 3: Shuffle (Trộn bài)
|
||
if (self.shuffle) {
|
||
self.shuffleArray(self.data.options);
|
||
}
|
||
};
|
||
|
||
// [UPDATE G4] Hàm lấy ID
|
||
SenaSDK.prototype.getCardID = function (index) {
|
||
if (this.data && this.data.options && this.data.options[index]) {
|
||
return this.data.options[index].id || "";
|
||
}
|
||
return "";
|
||
};
|
||
|
||
// [UPDATE G4] Hàm Check Pair (Logic tạm thời ở Client cho Mock)
|
||
SenaSDK.prototype.checkPair = function (idx1, idx2, callback) {
|
||
let self = this;
|
||
|
||
// Validate index
|
||
let card1 = self.data.options[idx1];
|
||
let card2 = self.data.options[idx2];
|
||
|
||
if (!card1 || !card2) {
|
||
if (callback) callback(false);
|
||
return;
|
||
}
|
||
|
||
// Logic so sánh: Name giống nhau VÀ ID khác nhau (tránh click 2 lần 1 thẻ) VÀ không phải blank
|
||
let isMatch = false;
|
||
|
||
if (card1.type !== "blank" && card2.type !== "blank") {
|
||
if (card1.id !== card2.id) {
|
||
// Check ko phải chính nó
|
||
// So sánh name (ví dụ: "dog" == "dog")
|
||
if (
|
||
card1.name &&
|
||
card2.name &&
|
||
card1.name.toLowerCase() === card2.name.toLowerCase()
|
||
) {
|
||
isMatch = true;
|
||
}
|
||
}
|
||
}
|
||
|
||
console.log(
|
||
`🎮 Sena SDK: Check Pair [${idx1}] vs [${idx2}] -> ${isMatch ? "MATCH" : "WRONG"}`,
|
||
);
|
||
|
||
// [TODO] Sau này sẽ thay đoạn này bằng postMessage lên Server verify
|
||
if (callback) callback(isMatch);
|
||
};
|
||
|
||
/**
|
||
* [NEW v2.2] Gửi Custom Data lên Parent Window
|
||
* @param {Object} data - Object chứa 5 trường data1 -> data5
|
||
*/
|
||
SenaSDK.prototype.sendMessageToParent = function (data) {
|
||
let self = this;
|
||
|
||
// Tự động tạo UUID cho session nếu chưa có
|
||
if (!self.uuid) {
|
||
self.uuid =
|
||
"session-" + Date.now() + "-" + Math.floor(Math.random() * 10000);
|
||
}
|
||
|
||
// Đóng gói payload đúng chuẩn tài liệu v2.2
|
||
let payload = {
|
||
type: "SEQUENCE_SYNC",
|
||
uuid: self.uuid,
|
||
data: data,
|
||
timestamp: Date.now(),
|
||
};
|
||
|
||
console.log("📤 Sena SDK: Sending SEQUENCE_SYNC to parent:", payload);
|
||
|
||
// Gửi lên Parent Window (Backend/Iframe parent)
|
||
window.parent.postMessage(payload, "*");
|
||
};
|
||
|
||
if (typeof module !== "undefined" && module.exports) {
|
||
module.exports = SenaSDK;
|
||
} else if (typeof define === "function" && define.amd) {
|
||
define([], function () {
|
||
return SenaSDK;
|
||
});
|
||
} else {
|
||
window.SenaSDK = SenaSDK;
|
||
}
|