xoa logo
All checks were successful
Deploy to Production / deploy (push) Successful in 7s

This commit is contained in:
Đặng Minh Quang
2026-03-02 15:01:11 +07:00
parent 5979d198e2
commit ee65ffdbe4
33 changed files with 20 additions and 740 deletions

View File

@@ -26021,9 +26021,9 @@ cr.getObjectRefTable = function () { return [
cr.plugins_.Browser,
cr.plugins_.Function,
cr.plugins_.Sprite,
cr.plugins_.Text,
cr.plugins_.Touch,
cr.plugins_.SenaPlugin,
cr.plugins_.Touch,
cr.plugins_.Text,
cr.behaviors.Rex_MoveTo,
cr.behaviors.Fade,
cr.behaviors.DragnDrop,

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 469 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 155 B

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,5 +1,5 @@
{
"version": 1772435489,
"version": 1772438302,
"fileList": [
"data.js",
"c2runtime.js",
@@ -23,7 +23,6 @@
"images/hint-sheet0.png",
"images/sound_question-sheet0.png",
"images/rectangle1copy-sheet0.png",
"images/newwords-sheet0.png",
"media/alert-234711.ogg",
"media/button-124476.ogg",
"media/click-234708.ogg",

View File

@@ -1087,7 +1087,7 @@ SenaSDK.prototype.canReloadData = function () {
/**
* End the game and check answer
* [UPDATE] Support Unordered Answers & Auto-cleanup empty strings
* [UPDATE] Support Unordered Answers, Auto-cleanup empty strings & Post GAME_RESULT to FE
*/
SenaSDK.prototype.end = function (answer, callback) {
let self = this;
@@ -1217,11 +1217,25 @@ SenaSDK.prototype.end = function (answer, callback) {
};
// -----------------------------------------------------------
// [BƯỚC 3] Log và Return
// [BƯỚC 3] Log và Return (KÈM BẮN POST MESSAGE CHO FE)
// -----------------------------------------------------------
console.log(`Time spent: ${duration}s`);
console.log(`Result: ${isCorrect ? "CORRECT" : "INCORRECT"}`);
// THÊM MỚI: Bắn tín hiệu GAME_RESULT lên cho hệ thống FE tính điểm
window.parent.postMessage(
{
type: "GAME_RESULT",
payload: {
game_id: self.gameID || self.gameCode,
result: isCorrect ? "CORRECT" : "INCORRECT",
time_spent: duration,
},
},
"*",
);
console.log("📤 Sena SDK: GAME_RESULT sent successfully for Quiz/Sort/Fill");
if (callback) callback(result.isCorrect);
return result; // Return full object for debug
};

View File

@@ -1,733 +0,0 @@
/**
* TDV SDK v8.2 - SENA PLUGIN SYNC VERSION
* - Sync dữ liệu từ SenaAI Construct 2 Plugin (window.SenaTrigger.sdk)
* - Hoặc tự load từ: https://senaai.tech/sample/{GameCode}.json
* - Hỗ trợ Quiz dạng: Câu hỏi Text, Đáp án Text/Image
**/
var tdv_sdk = {
game_code: 'G1400S1T30',
activeSdk: null,
serverDataLoaded: false,
gameStartTime: null,
currentQuestionIndex: 0,
totalScore: 0,
timeLimit: 0,
_timerStarted: false,
_lastLogTime: -1,
_isPaused: false,
_pausedElapsed: 0,
// ==================== SYNC FROM SENA PLUGIN ====================
/**
* Sync data từ SenaAI Plugin
* Gọi hàm này sau khi SenaAI.Load hoàn tất (On LOAD Complete)
*/
syncFromPlugin: function () {
var self = this;
// Lấy SDK instance từ SenaAI Plugin
if (window.SenaTrigger && window.SenaTrigger.sdk) {
var pluginSdk = window.SenaTrigger.sdk;
// Nếu plugin chưa có data, trả về false để SDK tự load
if (!pluginSdk.data) {
console.warn('⚠️ TDV SDK: Plugin has no data yet, will fallback to self-load');
return false;
}
self.activeSdk = {
data: pluginSdk.data,
correctAnswer: pluginSdk.correctAnswer,
shuffle: pluginSdk.shuffle,
gameCode: pluginSdk.gameCode,
startTime: pluginSdk.startTime,
timeLimit: pluginSdk.timeLimit
};
self.game_code = pluginSdk.gameCode || self.game_code;
self.timeLimit = pluginSdk.timeLimit || 0;
self._parseGameCode(); // Fallback parse if plugin doesn't have it
// SyncStartTime: Chỉ đồng bộ nếu tdv_sdk chưa bắt đầu đếm ngược
if (pluginSdk.startTime > 0 && !this._timerStarted) {
this.gameStartTime = pluginSdk.startTime;
this._timerStarted = true;
console.log('🔗 Timer synced from plugin:', this.gameStartTime);
}
self.serverDataLoaded = true;
console.log('✅ TDV SDK: Synced from SenaAI Plugin (StartTime:', self.gameStartTime, ')');
return true;
}
console.warn('⚠️ TDV SDK: SenaAI Plugin not found');
return false;
},
/**
* Initialize - tự động sync từ plugin nếu có
*/
init: function (config) {
var self = this;
if (config && config.gameCode) {
self.game_code = config.gameCode;
}
// Override từ URL param LID
var urlParams = new URLSearchParams(window.location.search);
var LID = urlParams.get('LID');
if (LID) {
self.game_code = LID;
}
console.log('🎮 TDV SDK v8.2 - SenaAI Plugin Sync');
console.log('📦 Game Code:', self.game_code);
// Tự động sync nếu plugin đã load
self.syncFromPlugin();
self._parseGameCode();
},
/**
* Helper: Parse game code để lấy cấu hình timeLimit, shuffle
*/
_parseGameCode: function () {
var self = this;
var regex = /^G([1-5])([2-9])([0-2])([0-2])(?:S([0-1]))?(?:T(\d+))?$/;
var match = String(self.game_code).match(regex);
if (match) {
var timeStr = match[6] !== undefined ? match[6] : '0';
self.timeLimit = parseInt(timeStr, 10);
}
},
/**
* Load data - ưu tiên sync từ plugin, nếu không thì tự load
*/
load: function (callback) {
var self = this;
// Thử sync từ plugin trước
if (self.syncFromPlugin()) {
if (callback) callback(true);
return;
}
// Nếu không có plugin, tự load từ server
var url = 'https://senaai.tech/sample/' + self.game_code + '.json';
console.log('📡 TDV SDK: Self-loading from:', url);
fetch(url)
.then(function (response) {
if (!response.ok) throw new Error('HTTP ' + response.status);
return response.json();
})
.then(function (json) {
console.log('✅ TDV SDK: Data loaded:', json);
self.activeSdk = {
data: json.data,
correctAnswer: json.answer,
shuffle: false
};
self.serverDataLoaded = true;
// self.gameStartTime = Date.now(); // REMOVED: Chỉ bắt đầu khi Start
if (callback) callback(true);
})
.catch(function (error) {
console.error('❌ TDV SDK: Load Error:', error);
self.serverDataLoaded = false;
if (callback) callback(false);
});
},
/**
* Start game
*/
start: function () {
// Sync lại từ plugin mỗi khi start (đảm bảo data mới nhất)
this.syncFromPlugin();
this._parseGameCode();
this.gameStartTime = Date.now();
this._timerStarted = true;
this._isPaused = false;
this._pausedElapsed = 0;
this.currentQuestionIndex = 0;
console.log('🎮 Game Started! Timer set to:', this.timeLimit);
},
// ==================== DATA GETTERS - Tự động sync nếu chưa có data ====================
/**
* Lấy SDK data, sync từ plugin nếu chưa có
*/
_getSdk: function () {
if (!this.activeSdk || !this.activeSdk.data) {
this.syncFromPlugin();
}
return this.activeSdk;
},
/**
* Lấy instruction/request text
*/
getInstructions: function () {
var sdk = this._getSdk();
if (!sdk || !sdk.data) return "";
return sdk.data.request || sdk.data.question || "";
},
/**
* Lấy request text
* Sử dụng: Browser.ExecJS("tdv_sdk.getRequest()")
*/
getRequest: function () {
var sdk = this._getSdk();
if (!sdk || !sdk.data) return "";
return sdk.data.request || "";
},
/**
* Lấy question text
* Sử dụng: Browser.ExecJS("tdv_sdk.getQuestion()")
*/
getQuestion: function () {
var sdk = this._getSdk();
if (!sdk || !sdk.data) return "";
var q = String(sdk.data.question || "").trim();
// Nếu question là một URL (bắt đầu bằng http hoặc là link ảnh/âm thanh), trả về rỗng để tránh hiện text
if (q.toLowerCase().startsWith('http') || this._isAudioUrl(q) || this._isImageUrl(q)) {
return "";
}
return q;
},
/**
* Lấy loại câu hỏi: 'text', 'image', 'audio'
*/
getQuestionType: function () {
var sdk = this._getSdk();
if (!sdk || !sdk.data) return 'text';
var q = String(sdk.data.question || "");
var isUrl = q.toLowerCase().startsWith('http');
// Check audio trước
if (sdk.data.audio || (isUrl && this._isAudioUrl(q))) return 'audio';
// Check image
if (sdk.data.image || sdk.data.image_url || (isUrl && this._isImageUrl(q))) return 'image';
// Fallback theo game code (số thứ 3)
if (this.game_code && this.game_code.length >= 4) {
var qTypeChar = this.game_code.charAt(3);
if (qTypeChar === '1') return 'image';
if (qTypeChar === '2') return 'audio';
}
return 'text';
},
/**
* Helper: Kiểm tra chuỗi có phải URL âm thanh không
*/
_isAudioUrl: function (url) {
var str = String(url).toLowerCase();
if (!str.startsWith('http')) return false;
var exts = ['.mp3', '.wav', '.ogg', '.m4a', '.aac'];
for (var i = 0; i < exts.length; i++) {
if (str.endsWith(exts[i])) return true;
}
return str.includes('/audio/') || str.includes('audio.');
},
/**
* Helper: Kiểm tra chuỗi có phải URL hình ảnh không
*/
_isImageUrl: function (url) {
var str = String(url).toLowerCase();
if (!str.startsWith('http')) return false;
var exts = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp'];
for (var i = 0; i < exts.length; i++) {
if (str.endsWith(exts[i])) return true;
}
return str.includes('/img/') || str.includes('/image/') || str.includes('image.');
},
/**
* Kiểm tra có hình ảnh câu hỏi không
* Sử dụng: Browser.ExecJS("tdv_sdk.hasImage()") = 1
*/
hasImage: function () {
var sdk = this._getSdk();
if (!sdk || !sdk.data) return 0;
var url = sdk.data.image_url || sdk.data.image || "";
if (url && String(url).toLowerCase().startsWith('http')) return 1;
// Nếu không có field image riêng, check question có phải là link ảnh không
var q = sdk.data.question || "";
return this._isImageUrl(q) ? 1 : 0;
},
/**
* Lấy URL hình ảnh câu hỏi
*/
getImageUrl: function () {
var sdk = this._getSdk();
if (!sdk || !sdk.data) return "";
var url = sdk.data.image_url || sdk.data.image || "";
if (!url && this._isImageUrl(sdk.data.question)) {
url = sdk.data.question;
}
return this.getCorsUrl(url);
},
/**
* Kiểm tra có audio câu hỏi không
* Sử dụng: Browser.ExecJS("tdv_sdk.hasAudio()") = 1
*/
hasAudio: function () {
var sdk = this._getSdk();
if (!sdk || !sdk.data) return 0;
if (sdk.data.audio && sdk.data.audio.length > 0) return 1;
// Check nếu question là link audio
return this._isAudioUrl(sdk.data.question) ? 1 : 0;
},
/**
* Lấy URL audio câu hỏi
* Sử dụng: Browser.ExecJS("tdv_sdk.getAudioUrl()")
*/
getAudioUrl: function () {
var sdk = this._getSdk();
if (!sdk || !sdk.data) return "";
var url = sdk.data.audio || "";
if (!url && this._isAudioUrl(sdk.data.question)) {
url = sdk.data.question;
}
return this.getCorsUrl(url);
},
// ==================== OPTIONS GETTERS ====================
/**
* Lấy số lượng options
* Sử dụng: Browser.ExecJS("tdv_sdk.getOptionsCount()")
*/
getOptionsCount: function () {
var sdk = this._getSdk();
if (!sdk || !sdk.data || !sdk.data.options) return 0;
return sdk.data.options.length;
},
/**
* Lấy giá trị gốc của option (Object hoặc String)
*/
_getRawOptionValue: function (index) {
var sdk = this._getSdk();
if (!sdk || !sdk.data || !sdk.data.options) return null;
return sdk.data.options[index];
},
/**
* Helper: Parse JSON string if needed
*/
_parseData: function (val) {
if (typeof val === 'string' && val.trim().startsWith('{')) {
try {
var obj = JSON.parse(val);
return obj.text || val;
} catch (e) { return val; }
}
return val;
},
/**
* Lấy text của option theo index
* Sử dụng: Browser.ExecJS("tdv_sdk.getAnswerByIndex(0)")
*/
getAnswerByIndex: function (index) {
var opt = this._getRawOptionValue(index);
if (!opt) return "";
// Trường hợp là Object {text: "...", image: "..."}
if (typeof opt === 'object') {
return opt.text || "";
}
// Trường hợp là chuỗi (có thể là JSON hoặc URL)
var text = this._parseData(String(opt));
// Nếu text là một URL (bắt đầu bằng http), trả về rỗng để tránh hiện link trên nút
if (typeof text === 'string' && text.toLowerCase().startsWith('http')) {
return "";
}
return text;
},
/**
* Alias - getOptionText
*/
getOptionText: function (index) {
return this.getAnswerByIndex(index);
},
/**
* Kiểm tra option có hình ảnh không
* Sử dụng: Browser.ExecJS("tdv_sdk.hasOptionImage(0)") = 1
*/
hasOptionImage: function (index) {
var opt = this._getRawOptionValue(index);
if (!opt) return 0;
// Trường hợp là Object
if (typeof opt === 'object') {
var url = opt.image || opt.image_url || "";
return (url && url.length > 0) ? 1 : 0;
}
// Trường hợp là chuỗi (check link ảnh)
return this._isImageUrl(String(opt)) ? 1 : 0;
},
/**
* Lấy URL hình ảnh của option
* Sử dụng: Browser.ExecJS("tdv_sdk.getOptionImageUrl(0)")
*/
getOptionImageUrl: function (index) {
var opt = this._getRawOptionValue(index);
if (!opt) return "";
if (typeof opt === 'object') {
var url = opt.image || opt.image_url || "";
return this.getCorsUrl(url);
}
// Nếu là link ảnh trực tiếp
var str = String(opt);
if (this._isImageUrl(str)) {
return this.getCorsUrl(str);
}
return "";
},
/**
* Kiểm tra option có audio không
* Trả về 1 nếu là object có property audio HOẶC là chuỗi URL dẫn đến file âm thanh
*/
hasOptionAudio: function (index) {
var opt = this._getRawOptionValue(index);
if (!opt) return 0;
// Trường hợp là Object
if (typeof opt === 'object') {
return (opt.audio && opt.audio.length > 0) ? 1 : 0;
}
// Trường hợp là chuỗi (kiểm tra xem có phải link audio không)
var str = String(opt).toLowerCase();
if (str.startsWith('http')) {
// Check các định dạng âm thanh phổ biến
var extensions = ['.mp3', '.wav', '.ogg', '.m4a', '.aac'];
for (var i = 0; i < extensions.length; i++) {
if (str.endsWith(extensions[i])) return 1;
}
if (str.includes('/audio/') || str.includes('audio.')) return 1;
}
return 0;
},
/**
* Lấy URL audio của option
*/
getOptionAudio: function (index) {
var opt = this._getRawOptionValue(index);
if (!opt) return "";
if (typeof opt === 'object') {
return this.getCorsUrl(opt.audio || "");
}
// Nếu là chuỗi bắt đầu bằng http, coi như đó là URL audio trực tiếp
var str = String(opt);
if (str.toLowerCase().startsWith('http')) {
return this.getCorsUrl(str);
}
return "";
},
// ==================== ANSWER CHECKING (Trigger Plugin Events) ====================
/**
* Lấy đáp án đúng
* Sử dụng: Browser.ExecJS("tdv_sdk.getCorrectResultText()")
*/
getCorrectResultText: function () {
var sdk = this._getSdk();
if (!sdk || !sdk.correctAnswer) return "";
return String(sdk.correctAnswer);
},
/**
* Kiểm tra đáp án - QUAN TRỌNG cho SenaAI Plugin
* Sử dụng: Browser.ExecJS("tdv_sdk.checkAnswer('apple')")
* Returns: 1 (đúng) hoặc 0 (sai)
*
* Plugin sẽ trigger:
* - "On Correct Answer" nếu đúng
* - "On Wrong Answer" nếu sai
*/
checkAnswer: function (userAnswer) {
var self = this;
var isCorrect = 0;
// Ưu tiên dùng hàm end() của Plugin SDK chính để tính toán duration/score
if (window.SenaTrigger && window.SenaTrigger.sdk) {
console.log('🏁 Calling official sena_sdk.end()...');
window.SenaTrigger.sdk.end(userAnswer, function (result) {
isCorrect = result ? 1 : 0;
});
} else {
// Fallback nếu không có plugin (logic cũ)
var correct = this.getCorrectResultText().toLowerCase().trim();
var user = String(userAnswer).toLowerCase().trim();
if (user.includes('corsproxy.io/?')) {
try {
var decoded = decodeURIComponent(user.split('corsproxy.io/?')[1]);
if (decoded) user = decoded.toLowerCase().trim();
} catch (e) { }
}
isCorrect = (user === correct) ? 1 : 0;
if (!isCorrect && (user.startsWith('http') || correct.startsWith('http'))) {
var getFileName = function (url) {
var parts = url.split('/');
return parts[parts.length - 1].split('?')[0];
};
if (getFileName(user) === getFileName(correct)) isCorrect = 1;
}
}
// Đồng bộ kết quả ra Bridge cho Construct 2
window.tdv_bridge_result = isCorrect;
if (isCorrect) this.totalScore++;
console.log('📝 Result Bridge:', isCorrect === 1 ? '✅ CORRECT' : '❌ WRONG');
return isCorrect;
},
/**
* Kiểm tra đáp án theo index của option đã chọn
* Đã cải tiến để lấy đúng giá trị (text hoặc URL) để so sánh
*/
checkAnswerByIndex: function (index) {
var opt = this._getRawOptionValue(index);
var val = "";
if (typeof opt === 'object') {
val = opt.text || "";
} else {
val = String(opt);
}
return this.checkAnswer(val);
},
/**
* Submit đáp án - alias cho checkAnswer
* Sử dụng: Browser.ExecJS("tdv_sdk.play('apple')")
*/
play: function (userAnswer) {
return this.checkAnswer(userAnswer);
},
// ==================== AUDIO PLAYBACK ====================
/**
* Phát audio câu hỏi
* Sử dụng: Browser.ExecJS("tdv_sdk.playQuestionAudio()")
*/
playQuestionAudio: function () {
var url = this.getAudioUrl();
if (url) {
console.log('🔊 Playing question audio');
new Audio(url).play().catch(function (e) { console.error(e); });
}
},
/**
* Phát audio option
* Sử dụng: Browser.ExecJS("tdv_sdk.playOptionAudio(0)")
*/
playOptionAudio: function (index) {
var url = this.getOptionAudio(index);
if (url) {
console.log('🔊 Playing option', index, 'audio');
new Audio(url).play().catch(function (e) { console.error(e); });
}
},
/**
* Phát audio từ URL
* Sử dụng: Browser.ExecJS("tdv_sdk.playSpecificAudio('url')")
*/
playSpecificAudio: function (url) {
if (url && url !== "" && url !== "NaN") {
new Audio(url).play().catch(function (e) { console.error(e); });
}
},
// ==================== SCORE & GAME STATE ====================
getCurrentScore: function () { return this.totalScore; },
getScore: function () { return this.totalScore; },
getCurrentNumber: function () { return this.currentQuestionIndex + 1; },
getTotalQuestions: function () { return 1; },
/**
* Tính toán font size linh hoạt dựa trên độ dài văn bản
* Sử dụng: Browser.ExecJS("tdv_sdk.getFontSizeForText('văn bản', 36, 20)")
*/
/**
* Chèn thêm Proxy để vượt rào CORS cho các link ảnh/audio từ server lạ
* Sử dụng: Browser.ExecJS("tdv_sdk.getCorsUrl('link_anh')")
*/
getCorsUrl: function (url) {
url = String(url || "");
if (!url || !url.startsWith('http')) return url;
// Nếu đã là link từ senaai.tech thì không cần proxy
if (url.includes('senaai.tech')) return url;
// Sử dụng một trong các public proxy (có thể thay đổi nếu proxy này die)
return "https://corsproxy.io/?" + encodeURIComponent(url);
},
getFontSizeForText: function (text, defaultSize, minSize) {
text = String(text || "");
defaultSize = Number(defaultSize) || 36;
minSize = Number(minSize) || 20;
var len = text.length;
if (len <= 12) return defaultSize;
if (len >= 40) return minSize;
// Giảm dần font size tuyến tính dựa trên độ dài
var ratio = (len - 12) / (40 - 12);
var size = defaultSize - (ratio * (defaultSize - minSize));
return Math.floor(size);
},
getFontSizeForOption: function (index, defaultSize, minSize) {
var text = this.getAnswerByIndex(index);
return this.getFontSizeForText(text, defaultSize, minSize);
},
getRemainingTime: function () {
// Ưu tiên dùng trực tiếp từ Core SDK nếu có
if (window.SenaTrigger && window.SenaTrigger.sdk && typeof window.SenaTrigger.sdk.getRemainingTime === 'function') {
return window.SenaTrigger.sdk.getRemainingTime();
}
// Fallback cho tdv_sdk tự đếm (nếu plugin chưa load xong hoặc plugin cũ)
var limit = this.timeLimit > 0 ? this.timeLimit : 30;
// Chỉ sync từ plugin nếu chưa bắt đầu
if (!this._timerStarted) {
this.syncFromPlugin();
}
// Nếu vẫn chưa có mốc thời gian, tự khởi tạo (chỉ làm 1 lần)
if (!this._timerStarted) {
this.gameStartTime = Date.now();
this._timerStarted = true;
this._isPaused = false;
this._pausedElapsed = 0;
console.log('⏱️ SDK Auto-start timer:', limit, 'seconds');
}
var elapsed = 0;
if (this._isPaused) {
elapsed = this._pausedElapsed;
} else {
elapsed = (Date.now() - this.gameStartTime) / 1000;
}
var remaining = limit - elapsed;
var finalTime = Math.max(0, Math.floor(remaining));
// Log trạng thái để debug
if (finalTime % 5 === 0 && finalTime !== this._lastLogTime) {
console.log('⏳ Time Left:', finalTime, (this._isPaused ? '[PAUSED]' : ''), '(Elapsed:', Math.floor(elapsed), ')');
this._lastLogTime = finalTime;
}
return finalTime;
},
isDataLoaded: function () { return this.serverDataLoaded; },
// ==================== LEGACY COMPATIBILITY ====================
getAttr: function (attr) {
var sdk = this._getSdk();
return (sdk && sdk.data) ? (sdk.data[attr] || "") : "";
},
recordResult: function (res) { window.tdv_bridge_result = res ? 1 : 0; },
finish: function () { console.log('🏁 Game Finished!'); },
resumeTime: function () {
if (!this._isPaused) return;
// Cập nhật lại gameStartTime để bù đắp cho khoảng thời gian đã trôi qua
this.gameStartTime = Date.now() - (this._pausedElapsed * 1000);
this._isPaused = false;
// Đồng bộ với Core SDK mới nhất
if (window.SenaTrigger && window.SenaTrigger.sdk && typeof window.SenaTrigger.sdk.resume === 'function') {
window.SenaTrigger.sdk.resume();
} else if (window.SenaTrigger && window.SenaTrigger.sdk) {
window.SenaTrigger.sdk.startTime = this.gameStartTime;
}
console.log('▶️ Timer Resumed via tdv_sdk');
},
pauseTime: function () {
if (this._isPaused || !this._timerStarted) return;
this._pausedElapsed = (Date.now() - this.gameStartTime) / 1000;
this._isPaused = true;
// Đồng bộ với Core SDK mới nhất
if (window.SenaTrigger && window.SenaTrigger.sdk && typeof window.SenaTrigger.sdk.pause === 'function') {
window.SenaTrigger.sdk.pause();
}
console.log('⏸️ Timer Paused at:', Math.floor(this._pausedElapsed), 's via tdv_sdk');
},
stopTime: function () { this.pauseTime(); },
submitAllResults: function () { console.log('📤 Submitting results...'); },
forceFinishGame: function () {
console.log('🚫 Force Finish Game (Time Up)');
// Khi hết giờ, nộp một đáp án rỗng "" để tính là SAI và kết thúc game
this.checkAnswer("");
this.finish();
},
nextQuestion: function () { this.currentQuestionIndex++; },
leaderboard: function () { },
result: function () { }
};
// Auto-init
window.tdv_sdk = tdv_sdk;
tdv_sdk.init();
console.log('✅ TDV SDK v8.2 Ready - Use with SenaAI Plugin');

Binary file not shown.

Binary file not shown.