This commit is contained in:
90
.gitea/workflows/deploy.yml
Normal file
90
.gitea/workflows/deploy.yml
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
name: Deploy to Production
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- master
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Deploy to server
|
||||||
|
run: |
|
||||||
|
echo "🚀 Deploying to /var/www/html/games"
|
||||||
|
echo "📁 Current directory: $(pwd)"
|
||||||
|
echo "📁 GITHUB_WORKSPACE: $GITHUB_WORKSPACE"
|
||||||
|
|
||||||
|
# Read game info from readme.md
|
||||||
|
if [ -f "$GITHUB_WORKSPACE/readme.md" ]; then
|
||||||
|
GAME_TITLE=$(sed -n '1p' "$GITHUB_WORKSPACE/readme.md")
|
||||||
|
GAME_DESC=$(sed -n '2p' "$GITHUB_WORKSPACE/readme.md")
|
||||||
|
GAME_TYPE=$(sed -n '3p' "$GITHUB_WORKSPACE/readme.md")
|
||||||
|
else
|
||||||
|
GAME_TITLE="Untitled Game"
|
||||||
|
GAME_DESC="No description"
|
||||||
|
GAME_TYPE="quiz"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create base directory
|
||||||
|
mkdir -p /var/www/html/games/
|
||||||
|
|
||||||
|
# Find and deploy all game folders (exclude source, .git, .gitea, etc.)
|
||||||
|
DEPLOYED_FOLDERS=""
|
||||||
|
DEPLOYED_URLS=""
|
||||||
|
for folder in $GITHUB_WORKSPACE/*/; do
|
||||||
|
folder_name=$(basename "$folder")
|
||||||
|
|
||||||
|
# Skip excluded folders
|
||||||
|
if [[ "$folder_name" == "source" ]] || \
|
||||||
|
[[ "$folder_name" == ".git" ]] || \
|
||||||
|
[[ "$folder_name" == ".gitea" ]] || \
|
||||||
|
[[ "$folder_name" == "node_modules" ]] || \
|
||||||
|
[[ "$folder_name" == "logs" ]] || \
|
||||||
|
[[ "$folder_name" == "uploads" ]]; then
|
||||||
|
echo "⏭️ Skipping: $folder_name"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Deploy the folder
|
||||||
|
echo "📦 Deploying: $folder_name"
|
||||||
|
rsync -av --delete "$folder" "/var/www/html/games/$folder_name/"
|
||||||
|
DEPLOYED_FOLDERS="$DEPLOYED_FOLDERS $folder_name"
|
||||||
|
GAME_URL="https://senaai.tech/games/$folder_name/"
|
||||||
|
DEPLOYED_URLS="$DEPLOYED_URLS\n 🔗 $GAME_URL"
|
||||||
|
|
||||||
|
# Get thumbnail (first image found or default)
|
||||||
|
THUMBNAIL=$(find "$folder" -type f \( -name "*.png" -o -name "*.jpg" -o -name "*.jpeg" \) 2>/dev/null | head -n 1 || true)
|
||||||
|
if [ -z "$THUMBNAIL" ]; then
|
||||||
|
THUMBNAIL="https://senaai.tech/games/$folder_name/images/default.png"
|
||||||
|
else
|
||||||
|
THUMBNAIL="https://senaai.tech/games/$folder_name/$(basename "$THUMBNAIL")"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Submit game info to API
|
||||||
|
echo "📤 Submitting game info to API..."
|
||||||
|
curl --location 'http://senaai.tech:10000/api/games/save-with-check' \
|
||||||
|
--header 'Content-Type: application/x-www-form-urlencoded' \
|
||||||
|
--data-urlencode "title=$GAME_TITLE" \
|
||||||
|
--data-urlencode "description=$GAME_DESC" \
|
||||||
|
--data-urlencode "url=$GAME_URL" \
|
||||||
|
--data-urlencode "thumbnail=$THUMBNAIL" \
|
||||||
|
--data-urlencode "type=$GAME_TYPE" || true
|
||||||
|
echo ""
|
||||||
|
done
|
||||||
|
|
||||||
|
# Show deployment summary
|
||||||
|
echo ""
|
||||||
|
echo "=========================================="
|
||||||
|
echo "🎉 Deployment Completed Successfully!"
|
||||||
|
echo "=========================================="
|
||||||
|
echo ""
|
||||||
|
echo "📍 Deployed URLs:"
|
||||||
|
echo -e "$DEPLOYED_URLS"
|
||||||
|
echo ""
|
||||||
|
echo "=========================================="
|
||||||
@@ -19347,6 +19347,15 @@ cr.plugins_.SenaPlugin = function (runtime) {
|
|||||||
this.bgMusicPlaying = false;
|
this.bgMusicPlaying = false;
|
||||||
this.bgMusicPaused = false;
|
this.bgMusicPaused = false;
|
||||||
this.calculatedPositions = [];
|
this.calculatedPositions = [];
|
||||||
|
this.customData = {
|
||||||
|
data1: "",
|
||||||
|
data2: "",
|
||||||
|
data3: "",
|
||||||
|
data4: "",
|
||||||
|
data5: ""
|
||||||
|
};
|
||||||
|
this.lastMessageData = null;
|
||||||
|
this.lastSenderUUID = "";
|
||||||
};
|
};
|
||||||
instanceProto.onDestroy = function () {
|
instanceProto.onDestroy = function () {
|
||||||
if (this.sdk) {
|
if (this.sdk) {
|
||||||
@@ -19376,6 +19385,15 @@ cr.plugins_.SenaPlugin = function (runtime) {
|
|||||||
Cnds.prototype.OnResume = function () {
|
Cnds.prototype.OnResume = function () {
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
Cnds.prototype.OnPairCorrect = function () {
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
Cnds.prototype.OnPairWrong = function () {
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
Cnds.prototype.OnMessage = function () {
|
||||||
|
return true;
|
||||||
|
};
|
||||||
pluginProto.cnds = new Cnds();
|
pluginProto.cnds = new Cnds();
|
||||||
function Acts() {}
|
function Acts() {}
|
||||||
Acts.prototype.Load = function () {
|
Acts.prototype.Load = function () {
|
||||||
@@ -19383,6 +19401,16 @@ cr.plugins_.SenaPlugin = function (runtime) {
|
|||||||
var gameCode = this.properties[0] || "G2510S1T30";
|
var gameCode = this.properties[0] || "G2510S1T30";
|
||||||
if (window["SenaSDK"]) {
|
if (window["SenaSDK"]) {
|
||||||
this.sdk = new window["SenaSDK"](gameCode);
|
this.sdk = new window["SenaSDK"](gameCode);
|
||||||
|
this.sdk.onCustomMessage = function (data, senderUuid) {
|
||||||
|
console.log('Runtime received custom message from:', senderUuid);
|
||||||
|
console.log('Data:', data);
|
||||||
|
self.lastMessageData = data;
|
||||||
|
self.lastSenderUUID = senderUuid;
|
||||||
|
window["SenaTrigger"].runtime.trigger(
|
||||||
|
cr.plugins_.SenaPlugin.prototype.cnds.OnMessage,
|
||||||
|
window["SenaTrigger"]
|
||||||
|
);
|
||||||
|
};
|
||||||
this.sdk.load(function (success) {
|
this.sdk.load(function (success) {
|
||||||
if (success) {
|
if (success) {
|
||||||
console.log("SDK loaded successfully");
|
console.log("SDK loaded successfully");
|
||||||
@@ -19506,16 +19534,92 @@ cr.plugins_.SenaPlugin = function (runtime) {
|
|||||||
objectWidth,
|
objectWidth,
|
||||||
margin,
|
margin,
|
||||||
maxWidth,
|
maxWidth,
|
||||||
|
rowBreak,
|
||||||
|
rowGap,
|
||||||
|
type,
|
||||||
|
groupGap
|
||||||
) {
|
) {
|
||||||
var self = this;
|
|
||||||
this.calculatedPositions = [];
|
this.calculatedPositions = [];
|
||||||
var totalWidth = count * objectWidth + (count - 1) * margin;
|
if (count <= 0) return;
|
||||||
var startX = (maxWidth - totalWidth) / 2;
|
var rows = [];
|
||||||
for (var i = 0; i < count; i++) {
|
if (rowBreak > 0) {
|
||||||
var posX = startX + i * (objectWidth + margin) + objectWidth / 2;
|
for (var i = 0; i < count; i += rowBreak) {
|
||||||
this.calculatedPositions.push(posX);
|
rows.push(Math.min(rowBreak, count - i));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (count <= 5) {
|
||||||
|
rows.push(count);
|
||||||
|
} else {
|
||||||
|
var top = Math.ceil((count + 1) / 2);
|
||||||
|
var bottom = count - top;
|
||||||
|
rows.push(top);
|
||||||
|
rows.push(bottom);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var baseY = 0;
|
||||||
|
if (type === "word") {
|
||||||
|
baseY = groupGap || (rowGap * rows.length); // word always below slot
|
||||||
|
}
|
||||||
|
var index = 0;
|
||||||
|
for (var r = 0; r < rows.length; r++) {
|
||||||
|
var itemsInRow = rows[r];
|
||||||
|
var rowWidth = itemsInRow * objectWidth + (itemsInRow - 1) * margin;
|
||||||
|
var startX = (maxWidth - rowWidth) / 2;
|
||||||
|
for (var i = 0; i < itemsInRow; i++) {
|
||||||
|
this.calculatedPositions.push({
|
||||||
|
x: startX + i * (objectWidth + margin) + objectWidth / 2,
|
||||||
|
y: baseY + r * rowGap
|
||||||
|
});
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log("Calculated positions (multi-row):", this.calculatedPositions);
|
||||||
|
};
|
||||||
|
Acts.prototype.LoadLevelG5 = function (levelIndex) {
|
||||||
|
if (this.sdk && this.sdk.loadLevelG5) {
|
||||||
|
this.sdk.loadLevelG5(levelIndex);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Acts.prototype.CheckPair = function (idx1, idx2) {
|
||||||
|
var self = this;
|
||||||
|
if (this.sdk && this.sdk.checkPair) {
|
||||||
|
this.sdk.checkPair(idx1, idx2, function (isMatch) {
|
||||||
|
if (isMatch) {
|
||||||
|
window["SenaTrigger"].runtime.trigger(
|
||||||
|
cr.plugins_.SenaPlugin.prototype.cnds.OnPairCorrect,
|
||||||
|
window["SenaTrigger"],
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
window["SenaTrigger"].runtime.trigger(
|
||||||
|
cr.plugins_.SenaPlugin.prototype.cnds.OnPairWrong,
|
||||||
|
window["SenaTrigger"],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Acts.prototype.SetData = function (data1, data2, data3, data4, data5) {
|
||||||
|
this.customData.data1 = data1 || "";
|
||||||
|
this.customData.data2 = data2 || "";
|
||||||
|
this.customData.data3 = data3 || "";
|
||||||
|
this.customData.data4 = data4 || "";
|
||||||
|
this.customData.data5 = data5 || "";
|
||||||
|
console.log('Custom data set:', this.customData);
|
||||||
|
};
|
||||||
|
Acts.prototype.PostMessage = function () {
|
||||||
|
if (this.sdk && this.sdk.sendMessageToParent) {
|
||||||
|
var dataToSend = {
|
||||||
|
data1: this.customData.data1,
|
||||||
|
data2: this.customData.data2,
|
||||||
|
data3: this.customData.data3,
|
||||||
|
data4: this.customData.data4,
|
||||||
|
data5: this.customData.data5
|
||||||
|
};
|
||||||
|
this.sdk.sendMessageToParent(dataToSend);
|
||||||
|
console.log('Posted message to parent:', dataToSend);
|
||||||
|
} else {
|
||||||
|
console.error('SDK not initialized or sendMessageToParent not available');
|
||||||
}
|
}
|
||||||
console.log("Calculated positions:", this.calculatedPositions);
|
|
||||||
};
|
};
|
||||||
pluginProto.acts = new Acts();
|
pluginProto.acts = new Acts();
|
||||||
function Exps() {}
|
function Exps() {}
|
||||||
@@ -19658,12 +19762,15 @@ cr.plugins_.SenaPlugin = function (runtime) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
Exps.prototype.getPosXbyIndex = function (ret, index) {
|
Exps.prototype.getPosXbyIndex = function (ret, index) {
|
||||||
if (
|
if (this.calculatedPositions[index]) {
|
||||||
this.calculatedPositions &&
|
ret.set_float(this.calculatedPositions[index].x);
|
||||||
index >= 0 &&
|
} else {
|
||||||
index < this.calculatedPositions.length
|
ret.set_float(0);
|
||||||
) {
|
}
|
||||||
ret.set_float(this.calculatedPositions[index]);
|
};
|
||||||
|
Exps.prototype.getPosYbyIndex = function (ret, index) {
|
||||||
|
if (this.calculatedPositions[index]) {
|
||||||
|
ret.set_float(this.calculatedPositions[index].y);
|
||||||
} else {
|
} else {
|
||||||
ret.set_float(0);
|
ret.set_float(0);
|
||||||
}
|
}
|
||||||
@@ -19703,6 +19810,59 @@ cr.plugins_.SenaPlugin = function (runtime) {
|
|||||||
ret.set_string("");
|
ret.set_string("");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
Exps.prototype.getTimePerCard = function (ret) {
|
||||||
|
if (this.sdk && this.sdk.getTimePerCard) {
|
||||||
|
ret.set_int(this.sdk.getTimePerCard());
|
||||||
|
} else {
|
||||||
|
ret.set_int(0);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Exps.prototype.getTotalLevels = function (ret) {
|
||||||
|
if (this.sdk && this.sdk.getTotalLevels) {
|
||||||
|
ret.set_int(this.sdk.getTotalLevels());
|
||||||
|
} else {
|
||||||
|
ret.set_int(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Exps.prototype.getCurrentLevel = function (ret) {
|
||||||
|
if (this.sdk) {
|
||||||
|
ret.set_int(this.sdk.currentLevel || 1);
|
||||||
|
} else {
|
||||||
|
ret.set_int(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Exps.prototype.GetCardType = function (ret, index) {
|
||||||
|
if (this.sdk && this.sdk.getCardType) {
|
||||||
|
ret.set_string(this.sdk.getCardType(index));
|
||||||
|
} else {
|
||||||
|
ret.set_string("");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Exps.prototype.getCardID = function (ret, index) {
|
||||||
|
if (this.sdk && this.sdk.getCardID) {
|
||||||
|
ret.set_string(this.sdk.getCardID(index) || "");
|
||||||
|
} else {
|
||||||
|
ret.set_string("");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Exps.prototype.getData = function (ret, dataIndex) {
|
||||||
|
var dataKey = "data" + dataIndex;
|
||||||
|
if (this.customData && this.customData[dataKey] !== undefined) {
|
||||||
|
ret.set_string(this.customData[dataKey]);
|
||||||
|
} else {
|
||||||
|
ret.set_string("");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Exps.prototype.getLastSenderUUID = function (ret) {
|
||||||
|
ret.set_string(this.lastSenderUUID || "");
|
||||||
|
};
|
||||||
|
Exps.prototype.getLastMessageJSON = function (ret) {
|
||||||
|
if (this.lastMessageData) {
|
||||||
|
ret.set_string(JSON.stringify(this.lastMessageData));
|
||||||
|
} else {
|
||||||
|
ret.set_string("{}");
|
||||||
|
}
|
||||||
|
};
|
||||||
pluginProto.exps = new Exps();
|
pluginProto.exps = new Exps();
|
||||||
})();
|
})();
|
||||||
;
|
;
|
||||||
@@ -25489,13 +25649,13 @@ cr.getObjectRefTable = function () { return [
|
|||||||
cr.plugins_.Function.prototype.cnds.OnFunction,
|
cr.plugins_.Function.prototype.cnds.OnFunction,
|
||||||
cr.plugins_.Sprite.prototype.acts.SetInstanceVar,
|
cr.plugins_.Sprite.prototype.acts.SetInstanceVar,
|
||||||
cr.plugins_.SenaPlugin.prototype.cnds.OnLoad,
|
cr.plugins_.SenaPlugin.prototype.cnds.OnLoad,
|
||||||
cr.plugins_.SenaPlugin.prototype.acts.Start,
|
|
||||||
cr.plugins_.Sprite.prototype.acts.SetAnimFrame,
|
cr.plugins_.Sprite.prototype.acts.SetAnimFrame,
|
||||||
|
cr.plugins_.SenaPlugin.prototype.acts.CalcObjectPositions,
|
||||||
cr.behaviors.Rex_MoveTo.prototype.acts.SetTargetPos,
|
cr.behaviors.Rex_MoveTo.prototype.acts.SetTargetPos,
|
||||||
cr.plugins_.Sprite.prototype.acts.SetVisible,
|
cr.plugins_.Sprite.prototype.acts.SetVisible,
|
||||||
cr.behaviors.Fade.prototype.acts.RestartFade,
|
cr.behaviors.Fade.prototype.acts.RestartFade,
|
||||||
|
cr.plugins_.SenaPlugin.prototype.acts.Start,
|
||||||
cr.plugins_.SenaPlugin.prototype.cnds.OnStart,
|
cr.plugins_.SenaPlugin.prototype.cnds.OnStart,
|
||||||
cr.plugins_.SenaPlugin.prototype.acts.CalcObjectPositions,
|
|
||||||
cr.system_object.prototype.cnds.Compare,
|
cr.system_object.prototype.cnds.Compare,
|
||||||
cr.plugins_.Browser.prototype.exps.ExecJS,
|
cr.plugins_.Browser.prototype.exps.ExecJS,
|
||||||
cr.plugins_.Function.prototype.exps.Param,
|
cr.plugins_.Function.prototype.exps.Param,
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -81,7 +81,7 @@
|
|||||||
|
|
||||||
<!-- Construct 2 exported games require jQuery. -->
|
<!-- Construct 2 exported games require jQuery. -->
|
||||||
<script src="jquery-3.4.1.min.js"></script>
|
<script src="jquery-3.4.1.min.js"></script>
|
||||||
<script src="tdv_sdk.js"></script>
|
|
||||||
<script src="sena_sdk.js"></script>
|
<script src="sena_sdk.js"></script>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"version": 1770267014,
|
"version": 1772180794,
|
||||||
"fileList": [
|
"fileList": [
|
||||||
"data.js",
|
"data.js",
|
||||||
"c2runtime.js",
|
"c2runtime.js",
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ function SenaSDK(gid = "G2510S1T30") {
|
|||||||
// 'preview' - Timeout 5s rồi fallback sample (testing với data thật)
|
// 'preview' - Timeout 5s rồi fallback sample (testing với data thật)
|
||||||
// 'dev' - Load sample ngay lập tức (development)
|
// 'dev' - Load sample ngay lập tức (development)
|
||||||
this.mode = "preview"; // Default mode
|
this.mode = "preview"; // Default mode
|
||||||
|
this.role = "student"; // Default role
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -118,6 +119,7 @@ SenaSDK.prototype.loadFromPostMessage = function (inputJson, callback) {
|
|||||||
console.warn("⚠️ Sena SDK: tdv_sdk not found, storing data locally");
|
console.warn("⚠️ Sena SDK: tdv_sdk not found, storing data locally");
|
||||||
self.data = inputJson.data;
|
self.data = inputJson.data;
|
||||||
self.correctAnswer = inputJson.answer;
|
self.correctAnswer = inputJson.answer;
|
||||||
|
self._parseGameCode(); // Thêm dòng này để SDK đọc đuôi thời gian T30
|
||||||
if (callback) callback(true);
|
if (callback) callback(true);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
@@ -147,6 +149,19 @@ SenaSDK.prototype.loadFromPostMessage = function (inputJson, callback) {
|
|||||||
audio: inputJson.data.audio || "",
|
audio: inputJson.data.audio || "",
|
||||||
hint: inputJson.data.hint || null,
|
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 / 6);
|
||||||
|
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;
|
self.correctAnswer = inputJson.answer;
|
||||||
|
|
||||||
// Cũng set vào list để hỗ trợ multi-question API
|
// Cũng set vào list để hỗ trợ multi-question API
|
||||||
@@ -166,6 +181,9 @@ SenaSDK.prototype.loadFromPostMessage = function (inputJson, callback) {
|
|||||||
self.currentQuestion = self.list[0];
|
self.currentQuestion = self.list[0];
|
||||||
self.userResults = [];
|
self.userResults = [];
|
||||||
|
|
||||||
|
// [UPDATE G4] Process G4 Data
|
||||||
|
if (self.gameType === 4) self._processG4Data();
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
"✅ Sena SDK: Single question loaded -",
|
"✅ Sena SDK: Single question loaded -",
|
||||||
inputJson.description || inputJson.data.question,
|
inputJson.description || inputJson.data.question,
|
||||||
@@ -234,9 +252,9 @@ SenaSDK.prototype.loadFromPostMessage = function (inputJson, callback) {
|
|||||||
*/
|
*/
|
||||||
SenaSDK.prototype._parseGameCode = function () {
|
SenaSDK.prototype._parseGameCode = function () {
|
||||||
let self = this;
|
let self = this;
|
||||||
const gameCode = self.gameCode || "G2510S1T30";
|
const gameCode = self.gameCode || "G4410S1T30"; // G4 mẫu
|
||||||
|
|
||||||
// FIX: Regex chấp nhận 2 chữ số cho Count (\d{1,2})
|
// Regex hỗ trợ G1-G9
|
||||||
const regex = /^G([1-9])(\d{1,2})([0-2])([0-2])(?:S([0-1]))?(?:T(\d+))?$/;
|
const regex = /^G([1-9])(\d{1,2})([0-2])([0-2])(?:S([0-1]))?(?:T(\d+))?$/;
|
||||||
let match = gameCode.match(regex);
|
let match = gameCode.match(regex);
|
||||||
|
|
||||||
@@ -248,7 +266,14 @@ SenaSDK.prototype._parseGameCode = function () {
|
|||||||
const shuffleFlag = match[5] !== undefined ? match[5] : "1";
|
const shuffleFlag = match[5] !== undefined ? match[5] : "1";
|
||||||
const timeStr = match[6] !== undefined ? match[6] : "0";
|
const timeStr = match[6] !== undefined ? match[6] : "0";
|
||||||
self.shuffle = shuffleFlag === "1";
|
self.shuffle = shuffleFlag === "1";
|
||||||
self.timeLimit = parseInt(timeStr, 10);
|
// --- [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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -350,9 +375,15 @@ SenaSDK.prototype.load = function (callback, template = "G2510S1T30") {
|
|||||||
self.mode = 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(
|
console.log(
|
||||||
"🎮 Sena SDK: Mode =",
|
"🎮 Sena SDK: Mode =",
|
||||||
self.mode.toUpperCase(),
|
self.mode.toUpperCase(),
|
||||||
|
"| Role =",
|
||||||
|
self.role || "student",
|
||||||
"| GameCode =",
|
"| GameCode =",
|
||||||
self.gameCode,
|
self.gameCode,
|
||||||
);
|
);
|
||||||
@@ -483,6 +514,21 @@ SenaSDK.prototype._loadFromServer = function (
|
|||||||
console.log("🎮 Sena SDK: Data shuffled immediately on load");
|
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 / 6);
|
||||||
|
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);
|
console.log("🎮 Sena SDK: Data loaded for", self.gameCode);
|
||||||
self._dataLoadedFromServer = true;
|
self._dataLoadedFromServer = true;
|
||||||
if (callback) callback(true);
|
if (callback) callback(true);
|
||||||
@@ -840,14 +886,12 @@ SenaSDK.prototype.guide = function () {
|
|||||||
return guide;
|
return guide;
|
||||||
};
|
};
|
||||||
/**
|
/**
|
||||||
* Get the question text
|
* Get the question text/url
|
||||||
* @returns {string} Question or request text
|
* @returns {string} Question, request text, or URL
|
||||||
*/
|
*/
|
||||||
SenaSDK.prototype.getQuestionValue = function () {
|
SenaSDK.prototype.getQuestionValue = function () {
|
||||||
var q = String(this.data.question || "").trim();
|
var q = String(this.data.question || "").trim();
|
||||||
if (q.toLowerCase().startsWith("http")) {
|
// Đã bỏ chặn URL để có thể lấy link ảnh/audio
|
||||||
return "";
|
|
||||||
}
|
|
||||||
return q;
|
return q;
|
||||||
};
|
};
|
||||||
/**
|
/**
|
||||||
@@ -905,7 +949,10 @@ SenaSDK.prototype.getRequestType = function () {
|
|||||||
* @returns {number} Number of options
|
* @returns {number} Number of options
|
||||||
*/
|
*/
|
||||||
SenaSDK.prototype.getOptionsCount = function () {
|
SenaSDK.prototype.getOptionsCount = function () {
|
||||||
return this.data.options.length;
|
if (this.data && this.data.options) {
|
||||||
|
return this.data.options.length;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1037,8 +1084,7 @@ SenaSDK.prototype.canReloadData = function () {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* End the game and check answer
|
* End the game and check answer
|
||||||
* @param {string} answer - User's answer (single text or multiple answers separated by |)
|
* [UPDATE] Support Unordered Answers & Auto-cleanup empty strings
|
||||||
* @returns {Object} Result object with isCorrect, duration, correctAnswer, and userAnswer
|
|
||||||
*/
|
*/
|
||||||
SenaSDK.prototype.end = function (answer, callback) {
|
SenaSDK.prototype.end = function (answer, callback) {
|
||||||
let self = this;
|
let self = this;
|
||||||
@@ -1071,88 +1117,98 @@ SenaSDK.prototype.end = function (answer, callback) {
|
|||||||
self.endTime = Date.now();
|
self.endTime = Date.now();
|
||||||
const duration = (self.endTime - self.startTime) / 1000;
|
const duration = (self.endTime - self.startTime) / 1000;
|
||||||
|
|
||||||
// Parse user answer - split by | for multiple answers
|
// 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("|")
|
const userAnswers = answer.includes("|")
|
||||||
? answer.split("|").map((a) => a.trim().toLowerCase())
|
? answer
|
||||||
: [answer.trim().toLowerCase()];
|
.split("|")
|
||||||
|
.map((a) => a.trim().toLowerCase())
|
||||||
|
.filter((a) => a)
|
||||||
|
: [answer.trim().toLowerCase()].filter((a) => a);
|
||||||
|
|
||||||
// Get correct answer(s) from data
|
// 2. GET CORRECT ANSWERS
|
||||||
let correctAnswers = [];
|
let correctAnswers = [];
|
||||||
if (self.correctAnswer) {
|
if (self.correctAnswer) {
|
||||||
// Check if answer is an array (multiple answers) or single answer
|
|
||||||
if (Array.isArray(self.correctAnswer)) {
|
if (Array.isArray(self.correctAnswer)) {
|
||||||
correctAnswers = self.correctAnswer.map((a) => {
|
correctAnswers = self.correctAnswer.map((a) =>
|
||||||
if (typeof a === "string") return a.toLowerCase();
|
(typeof a === "object" ? a.text || "" : String(a)).trim().toLowerCase(),
|
||||||
if (a.text) return a.text.toLowerCase();
|
);
|
||||||
return "";
|
} else {
|
||||||
});
|
let str =
|
||||||
} else if (typeof self.correctAnswer === "string") {
|
typeof self.correctAnswer === "object"
|
||||||
correctAnswers = [self.correctAnswer.toLowerCase()];
|
? self.correctAnswer.text
|
||||||
} else if (self.correctAnswer.text) {
|
: String(self.correctAnswer);
|
||||||
correctAnswers = [self.correctAnswer.text.toLowerCase()];
|
correctAnswers = str.includes("|")
|
||||||
|
? str.split("|").map((a) => a.trim().toLowerCase())
|
||||||
|
: [str.trim().toLowerCase()];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if answer is correct
|
// 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;
|
let isCorrect = false;
|
||||||
|
|
||||||
// Helper to normalize and strip proxy from URL
|
// Helper check file name for URL matching
|
||||||
const normalizeAnswer = (str) => {
|
|
||||||
let val = String(str || "")
|
|
||||||
.trim()
|
|
||||||
.toLowerCase();
|
|
||||||
if (val.includes("corsproxy.io/?")) {
|
|
||||||
try {
|
|
||||||
let decoded = decodeURIComponent(val.split("corsproxy.io/?")[1]);
|
|
||||||
if (decoded) val = decoded.toLowerCase().trim();
|
|
||||||
} catch (e) {}
|
|
||||||
}
|
|
||||||
return val;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Helper to get filename from URL
|
|
||||||
const getFileName = (url) => {
|
const getFileName = (url) => {
|
||||||
if (!url.startsWith("http")) return url;
|
if (!url.startsWith("http")) return url;
|
||||||
const parts = url.split("/");
|
try {
|
||||||
return parts[parts.length - 1].split("?")[0];
|
return url.split("/").pop().split("?")[0];
|
||||||
|
} catch (e) {
|
||||||
|
return url;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const normUser = userAnswers.map(normalizeAnswer);
|
if (finalUser.length === finalCorrect.length) {
|
||||||
const normCorrect = correctAnswers.map(normalizeAnswer);
|
isCorrect = finalUser.every((uVal, index) => {
|
||||||
|
let cVal = finalCorrect[index];
|
||||||
if (normUser.length === normCorrect.length) {
|
if (uVal === cVal) return true;
|
||||||
// For ordered multiple answers
|
// Fuzzy match cho URL (so sánh tên file ảnh)
|
||||||
isCorrect = normUser.every((ans, index) => {
|
if (uVal.startsWith("http") || cVal.startsWith("http")) {
|
||||||
if (ans === normCorrect[index]) return true;
|
return getFileName(uVal) === getFileName(cVal);
|
||||||
// Fuzzy match for URLs
|
|
||||||
if (ans.startsWith("http") || normCorrect[index].startsWith("http")) {
|
|
||||||
return getFileName(ans) === getFileName(normCorrect[index]);
|
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
} else if (normUser.length === 1 && normCorrect.length === 1) {
|
|
||||||
// For single answer
|
|
||||||
const u = normUser[0];
|
|
||||||
const c = normCorrect[0];
|
|
||||||
isCorrect = u === c;
|
|
||||||
if (!isCorrect && (u.startsWith("http") || c.startsWith("http"))) {
|
|
||||||
isCorrect = getFileName(u) === getFileName(c);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------
|
||||||
|
// [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 = {
|
const result = {
|
||||||
isCorrect: isCorrect,
|
isCorrect: isCorrect, // Lúc này isCorrect đã được xử lý xong xuôi
|
||||||
duration: duration,
|
duration: duration,
|
||||||
correctAnswer: correctAnswers.join(" | "),
|
correctAnswer: correctAnswers.join(" | "),
|
||||||
userAnswer: userAnswers.join(" | "),
|
userAnswer: userAnswers.join(" | "),
|
||||||
};
|
};
|
||||||
// if time spent more than time limit, mark as incorrect
|
|
||||||
if (self.timeLimit > 0 && duration > self.timeLimit) {
|
// -----------------------------------------------------------
|
||||||
result.isCorrect = false;
|
// [BƯỚC 3] Log và Return
|
||||||
}
|
// -----------------------------------------------------------
|
||||||
console.log(`Time spent in game: ${duration} seconds`);
|
console.log(`Time spent: ${duration}s`);
|
||||||
console.log(`Result: ${isCorrect ? "CORRECT" : "INCORRECT"}`);
|
console.log(
|
||||||
|
`Result: ${isCorrect ? "CORRECT" : "INCORRECT"} (User: ${result.userAnswer} vs Correct: ${result.correctAnswer})`,
|
||||||
|
);
|
||||||
|
|
||||||
if (callback) callback(result.isCorrect);
|
if (callback) callback(result.isCorrect);
|
||||||
|
return result; // Return full object for debug
|
||||||
};
|
};
|
||||||
|
|
||||||
SenaSDK.prototype.playVoice = function (type) {
|
SenaSDK.prototype.playVoice = function (type) {
|
||||||
@@ -1331,6 +1387,17 @@ SenaSDK.prototype.registerPostMessageListener = function () {
|
|||||||
}
|
}
|
||||||
break;
|
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":
|
case "SDK_ERROR":
|
||||||
// Server gửi error
|
// Server gửi error
|
||||||
console.error("❌ Sena SDK: Received SDK_ERROR", event.data.payload);
|
console.error("❌ Sena SDK: Received SDK_ERROR", event.data.payload);
|
||||||
@@ -1657,6 +1724,189 @@ SenaSDK.prototype.getCardAudio = function (index) {
|
|||||||
}
|
}
|
||||||
return "";
|
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 = 6; // Khóa cứng max 6 card/trang cho G5
|
||||||
|
|
||||||
|
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) {
|
if (typeof module !== "undefined" && module.exports) {
|
||||||
module.exports = SenaSDK;
|
module.exports = SenaSDK;
|
||||||
} else if (typeof define === "function" && define.amd) {
|
} else if (typeof define === "function" && define.amd) {
|
||||||
|
|||||||
Binary file not shown.
Reference in New Issue
Block a user