This commit is contained in:
@@ -26021,9 +26021,9 @@ cr.getObjectRefTable = function () { return [
|
|||||||
cr.plugins_.Browser,
|
cr.plugins_.Browser,
|
||||||
cr.plugins_.Function,
|
cr.plugins_.Function,
|
||||||
cr.plugins_.Sprite,
|
cr.plugins_.Sprite,
|
||||||
cr.plugins_.Text,
|
|
||||||
cr.plugins_.Touch,
|
|
||||||
cr.plugins_.SenaPlugin,
|
cr.plugins_.SenaPlugin,
|
||||||
|
cr.plugins_.Touch,
|
||||||
|
cr.plugins_.Text,
|
||||||
cr.behaviors.Rex_MoveTo,
|
cr.behaviors.Rex_MoveTo,
|
||||||
cr.behaviors.Fade,
|
cr.behaviors.Fade,
|
||||||
cr.behaviors.DragnDrop,
|
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.
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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"version": 1772435489,
|
"version": 1772438302,
|
||||||
"fileList": [
|
"fileList": [
|
||||||
"data.js",
|
"data.js",
|
||||||
"c2runtime.js",
|
"c2runtime.js",
|
||||||
@@ -23,7 +23,6 @@
|
|||||||
"images/hint-sheet0.png",
|
"images/hint-sheet0.png",
|
||||||
"images/sound_question-sheet0.png",
|
"images/sound_question-sheet0.png",
|
||||||
"images/rectangle1copy-sheet0.png",
|
"images/rectangle1copy-sheet0.png",
|
||||||
"images/newwords-sheet0.png",
|
|
||||||
"media/alert-234711.ogg",
|
"media/alert-234711.ogg",
|
||||||
"media/button-124476.ogg",
|
"media/button-124476.ogg",
|
||||||
"media/click-234708.ogg",
|
"media/click-234708.ogg",
|
||||||
|
|||||||
@@ -1087,7 +1087,7 @@ SenaSDK.prototype.canReloadData = function () {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* End the game and check answer
|
* 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) {
|
SenaSDK.prototype.end = function (answer, callback) {
|
||||||
let self = this;
|
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(`Time spent: ${duration}s`);
|
||||||
console.log(`Result: ${isCorrect ? "CORRECT" : "INCORRECT"}`);
|
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);
|
if (callback) callback(result.isCorrect);
|
||||||
return result; // Return full object for debug
|
return result; // Return full object for debug
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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.
BIN
source/SQ_Word_TextOnly.capx.backup1
Normal file
BIN
source/SQ_Word_TextOnly.capx.backup1
Normal file
Binary file not shown.
Reference in New Issue
Block a user