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.bgMusicPaused = false;
|
||||
this.calculatedPositions = [];
|
||||
this.customData = {
|
||||
data1: "",
|
||||
data2: "",
|
||||
data3: "",
|
||||
data4: "",
|
||||
data5: ""
|
||||
};
|
||||
this.lastMessageData = null;
|
||||
this.lastSenderUUID = "";
|
||||
};
|
||||
instanceProto.onDestroy = function () {
|
||||
if (this.sdk) {
|
||||
@@ -19382,6 +19391,9 @@ cr.plugins_.SenaPlugin = function (runtime) {
|
||||
Cnds.prototype.OnPairWrong = function () {
|
||||
return true;
|
||||
};
|
||||
Cnds.prototype.OnMessage = function () {
|
||||
return true;
|
||||
};
|
||||
pluginProto.cnds = new Cnds();
|
||||
function Acts() {}
|
||||
Acts.prototype.Load = function () {
|
||||
@@ -19389,6 +19401,16 @@ cr.plugins_.SenaPlugin = function (runtime) {
|
||||
var gameCode = this.properties[0] || "G2510S1T30";
|
||||
if (window["SenaSDK"]) {
|
||||
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) {
|
||||
if (success) {
|
||||
console.log("SDK loaded successfully");
|
||||
@@ -19512,16 +19534,46 @@ cr.plugins_.SenaPlugin = function (runtime) {
|
||||
objectWidth,
|
||||
margin,
|
||||
maxWidth,
|
||||
rowBreak,
|
||||
rowGap,
|
||||
type,
|
||||
groupGap
|
||||
) {
|
||||
var self = this;
|
||||
this.calculatedPositions = [];
|
||||
var totalWidth = count * objectWidth + (count - 1) * margin;
|
||||
var startX = (maxWidth - totalWidth) / 2;
|
||||
for (var i = 0; i < count; i++) {
|
||||
var posX = startX + i * (objectWidth + margin) + objectWidth / 2;
|
||||
this.calculatedPositions.push(posX);
|
||||
if (count <= 0) return;
|
||||
var rows = [];
|
||||
if (rowBreak > 0) {
|
||||
for (var i = 0; i < count; i += rowBreak) {
|
||||
rows.push(Math.min(rowBreak, count - i));
|
||||
}
|
||||
console.log("Calculated positions:", this.calculatedPositions);
|
||||
} 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) {
|
||||
@@ -19546,6 +19598,29 @@ cr.plugins_.SenaPlugin = function (runtime) {
|
||||
});
|
||||
}
|
||||
};
|
||||
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');
|
||||
}
|
||||
};
|
||||
pluginProto.acts = new Acts();
|
||||
function Exps() {}
|
||||
Exps.prototype.getQuestionValue = function (ret) {
|
||||
@@ -19687,12 +19762,15 @@ cr.plugins_.SenaPlugin = function (runtime) {
|
||||
}
|
||||
};
|
||||
Exps.prototype.getPosXbyIndex = function (ret, index) {
|
||||
if (
|
||||
this.calculatedPositions &&
|
||||
index >= 0 &&
|
||||
index < this.calculatedPositions.length
|
||||
) {
|
||||
ret.set_float(this.calculatedPositions[index]);
|
||||
if (this.calculatedPositions[index]) {
|
||||
ret.set_float(this.calculatedPositions[index].x);
|
||||
} else {
|
||||
ret.set_float(0);
|
||||
}
|
||||
};
|
||||
Exps.prototype.getPosYbyIndex = function (ret, index) {
|
||||
if (this.calculatedPositions[index]) {
|
||||
ret.set_float(this.calculatedPositions[index].y);
|
||||
} else {
|
||||
ret.set_float(0);
|
||||
}
|
||||
@@ -19754,7 +19832,11 @@ cr.plugins_.SenaPlugin = function (runtime) {
|
||||
}
|
||||
};
|
||||
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) {
|
||||
@@ -19763,6 +19845,24 @@ cr.plugins_.SenaPlugin = function (runtime) {
|
||||
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();
|
||||
})();
|
||||
;
|
||||
|
||||
File diff suppressed because one or more lines are too long
Binary file not shown.
|
Before Width: | Height: | Size: 386 KiB After Width: | Height: | Size: 349 B |
@@ -81,7 +81,7 @@
|
||||
|
||||
<!-- Construct 2 exported games require jQuery. -->
|
||||
<script src="jquery-3.4.1.min.js"></script>
|
||||
<script src="tdv_sdk.js"></script>
|
||||
|
||||
<script src="sena_sdk.js"></script>
|
||||
|
||||
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 376 KiB After Width: | Height: | Size: 120 KiB |
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": 1770529725,
|
||||
"version": 1772168306,
|
||||
"fileList": [
|
||||
"data.js",
|
||||
"c2runtime.js",
|
||||
@@ -21,7 +21,6 @@
|
||||
"icon-128.png",
|
||||
"icon-256.png",
|
||||
"loading-logo.png",
|
||||
"bg.mp4",
|
||||
"sena_sdk.js"
|
||||
]
|
||||
}
|
||||
@@ -50,6 +50,7 @@ function SenaSDK(gid = "G2510S1T30") {
|
||||
// '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
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -118,6 +119,7 @@ SenaSDK.prototype.loadFromPostMessage = function (inputJson, callback) {
|
||||
console.warn("⚠️ Sena SDK: tdv_sdk not found, storing data locally");
|
||||
self.data = inputJson.data;
|
||||
self.correctAnswer = inputJson.answer;
|
||||
self._parseGameCode(); // Thêm dòng này để SDK đọc đuôi thời gian T30
|
||||
if (callback) callback(true);
|
||||
}
|
||||
return true;
|
||||
@@ -373,9 +375,15 @@ SenaSDK.prototype.load = function (callback, template = "G2510S1T30") {
|
||||
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,
|
||||
);
|
||||
@@ -878,14 +886,12 @@ SenaSDK.prototype.guide = function () {
|
||||
return guide;
|
||||
};
|
||||
/**
|
||||
* Get the question text
|
||||
* @returns {string} Question or request text
|
||||
* Get the question text/url
|
||||
* @returns {string} Question, request text, or URL
|
||||
*/
|
||||
SenaSDK.prototype.getQuestionValue = function () {
|
||||
var q = String(this.data.question || "").trim();
|
||||
if (q.toLowerCase().startsWith("http")) {
|
||||
return "";
|
||||
}
|
||||
// Đã bỏ chặn URL để có thể lấy link ảnh/audio
|
||||
return q;
|
||||
};
|
||||
/**
|
||||
@@ -1138,10 +1144,13 @@ SenaSDK.prototype.end = function (answer, callback) {
|
||||
}
|
||||
}
|
||||
|
||||
// 3. COMPARE (UNORDERED - So sánh không cần thứ tự)
|
||||
// Sort cả 2 mảng để so sánh tập hợp
|
||||
const sortedUser = [...userAnswers].sort();
|
||||
const sortedCorrect = [...correctAnswers].sort();
|
||||
// 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;
|
||||
|
||||
@@ -1155,9 +1164,9 @@ SenaSDK.prototype.end = function (answer, callback) {
|
||||
}
|
||||
};
|
||||
|
||||
if (sortedUser.length === sortedCorrect.length) {
|
||||
isCorrect = sortedUser.every((uVal, index) => {
|
||||
let cVal = sortedCorrect[index];
|
||||
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")) {
|
||||
@@ -1170,7 +1179,12 @@ SenaSDK.prototype.end = function (answer, callback) {
|
||||
// -----------------------------------------------------------
|
||||
// [BƯỚC 1] Kiểm tra Time Limit TRƯỚC (Sửa biến isCorrect)
|
||||
// -----------------------------------------------------------
|
||||
if (self.timeLimit > 0 && duration > self.timeLimit) {
|
||||
// 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");
|
||||
}
|
||||
@@ -1373,6 +1387,17 @@ SenaSDK.prototype.registerPostMessageListener = function () {
|
||||
}
|
||||
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);
|
||||
@@ -1855,6 +1880,33 @@ SenaSDK.prototype.checkPair = function (idx1, idx2, callback) {
|
||||
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) {
|
||||
|
||||
3
readme.md
Normal file
3
readme.md
Normal file
@@ -0,0 +1,3 @@
|
||||
Sequence Word TextOnly
|
||||
Mô tả của trò chơi
|
||||
Sequence
|
||||
Binary file not shown.
Reference in New Issue
Block a user