@@ -1,28 +0,0 @@
|
||||
{
|
||||
"name": "G102-sequence",
|
||||
"short_name": "G102-sequence",
|
||||
"start_url": "index.html",
|
||||
"display": "fullscreen",
|
||||
"orientation": "any",
|
||||
"icons": [{
|
||||
"src": "icon-16.png",
|
||||
"sizes": "16x16",
|
||||
"type": "image/png"
|
||||
}, {
|
||||
"src": "icon-32.png",
|
||||
"sizes": "32x32",
|
||||
"type": "image/png"
|
||||
}, {
|
||||
"src": "icon-128.png",
|
||||
"sizes": "128x128",
|
||||
"type": "image/png"
|
||||
}, {
|
||||
"src": "icon-256.png",
|
||||
"sizes": "144x144",
|
||||
"type": "image/png"
|
||||
}, {
|
||||
"src": "icon-256.png",
|
||||
"sizes": "256x256",
|
||||
"type": "image/png"
|
||||
}]
|
||||
}
|
||||
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 1016 KiB |
|
Before Width: | Height: | Size: 7.1 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 1.4 MiB |
|
Before Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 132 KiB |
|
Before Width: | Height: | Size: 168 B |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 183 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 1.7 MiB |
|
Before Width: | Height: | Size: 527 KiB |
|
Before Width: | Height: | Size: 530 KiB |
|
Before Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 69 KiB |
|
Before Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 630 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 588 KiB |
|
Before Width: | Height: | Size: 88 KiB |
|
Before Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 155 B |
|
Before Width: | Height: | Size: 800 KiB |
|
Before Width: | Height: | Size: 683 KiB |
|
Before Width: | Height: | Size: 189 KiB |
|
Before Width: | Height: | Size: 138 KiB |
|
Before Width: | Height: | Size: 9.4 KiB |
|
Before Width: | Height: | Size: 7.9 KiB |
|
Before Width: | Height: | Size: 9.1 KiB |
|
Before Width: | Height: | Size: 168 B |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 92 KiB |
|
Before Width: | Height: | Size: 145 KiB |
@@ -1,235 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
|
||||
<title>G102-sequence</title>
|
||||
|
||||
<!-- Standardised web app manifest -->
|
||||
<link rel="manifest" href="appmanifest.json" />
|
||||
|
||||
<!-- Allow fullscreen mode on iOS devices. (These are Apple specific meta tags.) -->
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no, minimal-ui" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
|
||||
<link rel="apple-touch-icon" sizes="256x256" href="icon-256.png" />
|
||||
<meta name="HandheldFriendly" content="true" />
|
||||
|
||||
<!-- Chrome for Android web app tags -->
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<link rel="shortcut icon" sizes="256x256" href="icon-256.png" />
|
||||
|
||||
<!-- All margins and padding must be zero for the canvas to fill the screen. -->
|
||||
<style type="text/css">
|
||||
* {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
html, body {
|
||||
color: #fff;
|
||||
overflow: hidden;
|
||||
touch-action: none;
|
||||
-ms-touch-action: none;
|
||||
}
|
||||
canvas {
|
||||
touch-action-delay: none;
|
||||
touch-action: none;
|
||||
-ms-touch-action: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="fb-root"></div>
|
||||
<div style="width: 100%; height: 100%; position: absolute; top: 0; left: 0; z-index: -1;">
|
||||
<!--<img src = "./bg.jpg" style="width:100%; height: 100%; object-fit: cover;"/> -->
|
||||
<video autoplay="" loop="" muted="" style="width : 100%; height: 100%;object-fit: cover;" src="bg.mp4">
|
||||
</video>
|
||||
</div>
|
||||
<script>
|
||||
// Issue a warning if trying to preview an exported project on disk.
|
||||
(function(){
|
||||
// Check for running exported on file protocol
|
||||
if (window.location.protocol.substr(0, 4) === "file")
|
||||
{
|
||||
alert("Exported games won't work until you upload them. (When running on the file:/// protocol, browsers block many features from working for security reasons.)");
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
|
||||
<!-- The canvas must be inside a div called c2canvasdiv -->
|
||||
<div id="c2canvasdiv">
|
||||
|
||||
<!-- The canvas the project will render to. If you change its ID, don't forget to change the
|
||||
ID the runtime looks for in the jQuery events above (ready() and cr_sizeCanvas()). -->
|
||||
<canvas id="c2canvas" width="1200" height="1200">
|
||||
<!-- This text is displayed if the visitor's browser does not support HTML5.
|
||||
You can change it, but it is a good idea to link to a description of a browser
|
||||
and provide some links to download some popular HTML5-compatible browsers. -->
|
||||
<h1>Your browser does not appear to support HTML5. Try upgrading your browser to the latest version. <a href="http://www.whatbrowser.org">What is a browser?</a>
|
||||
<br/><br/><a href="http://www.microsoft.com/windows/internet-explorer/default.aspx">Microsoft Internet Explorer</a><br/>
|
||||
<a href="http://www.mozilla.com/firefox/">Mozilla Firefox</a><br/>
|
||||
<a href="http://www.google.com/chrome/">Google Chrome</a><br/>
|
||||
<a href="http://www.apple.com/safari/download/">Apple Safari</a></h1>
|
||||
</canvas>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Pages load faster with scripts at the bottom -->
|
||||
|
||||
<!-- Construct 2 exported games require jQuery. -->
|
||||
<script src="jquery-3.4.1.min.js"></script>
|
||||
<script src="tdv_sdk.js"></script>
|
||||
|
||||
|
||||
<!-- The runtime script. You can rename it, but don't forget to rename the reference here as well.
|
||||
This file will have been minified and obfuscated if you enabled "Minify script" during export. -->
|
||||
<script src="c2runtime.js"></script>
|
||||
|
||||
<script>
|
||||
// Start the Construct 2 project running on window load.
|
||||
jQuery(document).ready(function ()
|
||||
{
|
||||
// Create new runtime using the c2canvas
|
||||
cr_createRuntime("c2canvas");
|
||||
});
|
||||
|
||||
// Pause and resume on page becoming visible/invisible
|
||||
function onVisibilityChanged() {
|
||||
if (document.hidden || document.mozHidden || document.webkitHidden || document.msHidden)
|
||||
cr_setSuspended(true);
|
||||
else
|
||||
cr_setSuspended(false);
|
||||
};
|
||||
|
||||
document.addEventListener("visibilitychange", onVisibilityChanged, false);
|
||||
document.addEventListener("mozvisibilitychange", onVisibilityChanged, false);
|
||||
document.addEventListener("webkitvisibilitychange", onVisibilityChanged, false);
|
||||
document.addEventListener("msvisibilitychange", onVisibilityChanged, false);
|
||||
|
||||
function OnRegisterSWError(e)
|
||||
{
|
||||
console.warn("Failed to register service worker: ", e);
|
||||
};
|
||||
|
||||
// Runtime calls this global method when ready to start caching (i.e. after startup).
|
||||
// This registers the service worker which caches resources for offline support.
|
||||
window.C2_RegisterSW = function C2_RegisterSW()
|
||||
{
|
||||
if (!navigator.serviceWorker)
|
||||
return; // no SW support, ignore call
|
||||
|
||||
try {
|
||||
navigator.serviceWorker.register("sw.js", { scope: "./" })
|
||||
.then(function (reg)
|
||||
{
|
||||
console.log("Registered service worker on " + reg.scope);
|
||||
})
|
||||
.catch(OnRegisterSWError);
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
OnRegisterSWError(e);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<script src="./sdk/package/dist/sena-game-sdk.js"></script>
|
||||
|
||||
<script>
|
||||
/**
|
||||
* ==============================
|
||||
* SDK BRIDGE – G120 (SEQUENCE)
|
||||
* ==============================
|
||||
* - Chỉ làm cầu nối
|
||||
* - Không xử lý game logic
|
||||
* - Không chấm điểm
|
||||
*/
|
||||
|
||||
window.SDK_BRIDGE = {
|
||||
sdk: null,
|
||||
gameData: [],
|
||||
currentIndex: 0,
|
||||
gameCode: "G120",
|
||||
mode: "preview"
|
||||
};
|
||||
|
||||
// ===== INIT SDK =====
|
||||
function initSDKBridge() {
|
||||
var urlParams = new URLSearchParams(window.location.search);
|
||||
SDK_BRIDGE.mode = urlParams.get("mode") || "preview";
|
||||
|
||||
SDK_BRIDGE.sdk = new SenaGameSDK({
|
||||
iframePath: "./sdk/package/dist/sdk-iframe/index.html",
|
||||
gameCode: SDK_BRIDGE.gameCode,
|
||||
mode: SDK_BRIDGE.mode,
|
||||
debug: true,
|
||||
|
||||
onReady: function (sdk) {
|
||||
console.log("✅ [SDK BRIDGE] SDK Ready");
|
||||
},
|
||||
|
||||
onDataReady: function (data) {
|
||||
console.log("📥 [SDK BRIDGE] Data Ready:", data);
|
||||
|
||||
SDK_BRIDGE.gameData = data.items || [];
|
||||
SDK_BRIDGE.currentIndex = 0;
|
||||
|
||||
// Đẩy data cho Construct 2
|
||||
if (window.tdv_sdk) {
|
||||
window.tdv_sdk.gameData = SDK_BRIDGE.gameData;
|
||||
window.tdv_sdk.currentQuestionIndex = 0;
|
||||
window.tdv_sdk.loadQuestions();
|
||||
}
|
||||
|
||||
if (window.c2_callFunction) {
|
||||
window.c2_callFunction("OnSDKDataReady", []);
|
||||
}
|
||||
},
|
||||
|
||||
onAnswerResult: function (result) {
|
||||
console.log("📝 [SDK BRIDGE] Answer Result:", result);
|
||||
window.answerResult = result.correct ? 1 : 0;
|
||||
|
||||
if (window.TDVTriger) {
|
||||
window.TDVTriger.runtime.trigger(
|
||||
cr.plugins_.TDVplugin.prototype.cnds.OnAnswerChecked,
|
||||
window.TDVTriger
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
onGameComplete: function (result) {
|
||||
console.log("🏁 [SDK BRIDGE] Game Complete:", result);
|
||||
},
|
||||
|
||||
onError: function (err) {
|
||||
console.error("❌ [SDK BRIDGE] SDK Error:", err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ===== HELPERS CHO CONSTRUCT 2 =====
|
||||
function submitSequenceAnswer(sequenceArray) {
|
||||
if (!SDK_BRIDGE.sdk) return;
|
||||
SDK_BRIDGE.sdk.submitAnswer({
|
||||
selectedAnswer: sequenceArray
|
||||
});
|
||||
}
|
||||
|
||||
function nextQuestion() {
|
||||
SDK_BRIDGE.currentIndex++;
|
||||
if (window.tdv_sdk) {
|
||||
window.tdv_sdk.currentQuestionIndex = SDK_BRIDGE.currentIndex;
|
||||
window.tdv_sdk.loadQuestions();
|
||||
}
|
||||
}
|
||||
|
||||
// Auto init
|
||||
window.addEventListener("load", initSDKBridge);
|
||||
</script>
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,66 +0,0 @@
|
||||
{
|
||||
"version": 1769236279,
|
||||
"fileList": [
|
||||
"data.js",
|
||||
"c2runtime.js",
|
||||
"jquery-3.4.1.min.js",
|
||||
"offlineClient.js",
|
||||
"images/sena_ui_frame_result-sheet0.png",
|
||||
"images/sena_btn_replay-sheet0.png",
|
||||
"images/asset3-sheet0.png",
|
||||
"images/asset3-sheet1.png",
|
||||
"images/answers-sheet0.png",
|
||||
"images/txt_answerss.png",
|
||||
"images/checker_frame-sheet0.png",
|
||||
"images/btn_submit-sheet0.png",
|
||||
"images/txt_instructions.png",
|
||||
"images/btn_submit2-sheet0.png",
|
||||
"images/btn_next-sheet0.png",
|
||||
"images/btn_play-sheet0.png",
|
||||
"images/btn_play-sheet1.png",
|
||||
"images/bg-sheet0.png",
|
||||
"images/frame_door-sheet0.png",
|
||||
"images/frame_door_left-sheet0.png",
|
||||
"images/frame_door_right-sheet0.png",
|
||||
"images/hand_right-sheet0.png",
|
||||
"images/hand_left-sheet0.png",
|
||||
"images/ao_vang_quan_xanh-sheet0.png",
|
||||
"images/sprite-sheet0.png",
|
||||
"images/khung_thoai-sheet0.png",
|
||||
"images/khung_thoai2-sheet0.png",
|
||||
"images/khung_diem-sheet0.png",
|
||||
"images/avatar-sheet0.png",
|
||||
"images/star_-sheet0.png",
|
||||
"images/frame_score-sheet0.png",
|
||||
"images/dong_ho-sheet0.png",
|
||||
"images/coin-sheet0.png",
|
||||
"images/sena_ui_frame_intro-sheet0.png",
|
||||
"images/sena_title_leaderboard-sheet0.png",
|
||||
"images/sena_ui_frame_leaderboard-sheet0.png",
|
||||
"images/sena_btn_exit-sheet0.png",
|
||||
"images/sena_ui_item_bg-sheet0.png",
|
||||
"images/sena_ui_item_bg_user_rank-sheet0.png",
|
||||
"images/sena_btn_leaderboard-sheet0.png",
|
||||
"images/sena_ui_number_rank-sheet0.png",
|
||||
"images/sena_ui_number_rank-sheet1.png",
|
||||
"images/sena_ui_number_rank-sheet2.png",
|
||||
"images/sena_fx_wrong_mark-sheet0.png",
|
||||
"images/sena_fx_correct_mark-sheet0.png",
|
||||
"media/bg.m4a",
|
||||
"media/bg.ogg",
|
||||
"media/click.m4a",
|
||||
"media/click.ogg",
|
||||
"media/correct.m4a",
|
||||
"media/correct.ogg",
|
||||
"media/fail.m4a",
|
||||
"media/fail.ogg",
|
||||
"icon-16.png",
|
||||
"icon-32.png",
|
||||
"icon-114.png",
|
||||
"icon-128.png",
|
||||
"icon-256.png",
|
||||
"loading-logo.png",
|
||||
"tdv_sdk.js",
|
||||
"bg.mp4"
|
||||
]
|
||||
}
|
||||
@@ -1,506 +0,0 @@
|
||||
# SDK Message Protocol
|
||||
|
||||
Game giao tiếp với SDK thông qua **postMessage** trong hidden iframe. Game chỉ cần biết các message types, payloads, và khi nào gửi/nhận.
|
||||
|
||||
---
|
||||
|
||||
## 🔌 Architecture
|
||||
|
||||
```
|
||||
┌──────────────────────────────┐
|
||||
│ Game (React/Vue/etc) │
|
||||
│ │
|
||||
│ window.parent.postMessage() │
|
||||
│ window.addEventListener() │
|
||||
└──────────────┬───────────────┘
|
||||
│ postMessage
|
||||
│ (JSON)
|
||||
↓
|
||||
┌──────────────────────┐
|
||||
│ Hidden Iframe │
|
||||
│ (sdk-iframe.html) │
|
||||
│ │
|
||||
│ - Sanitize data │
|
||||
│ - Verify answers │
|
||||
│ - Call API │
|
||||
│ - Send responses │
|
||||
└──────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📨 Message Types
|
||||
|
||||
### 1️⃣ **SDK_INIT** (Game → SDK)
|
||||
|
||||
Game khởi tạo SDK với mode và game_code.
|
||||
|
||||
**Gửi:**
|
||||
```javascript
|
||||
window.parent.postMessage({
|
||||
type: 'SDK_INIT',
|
||||
payload: {
|
||||
mode: 'dev' | 'preview' | 'live',
|
||||
game_code: 'G001' | 'G002' | ... | 'G123',
|
||||
|
||||
// LIVE mode only:
|
||||
assignment_id?: string,
|
||||
student_id?: string,
|
||||
api_base_url?: string,
|
||||
auth_token?: string
|
||||
}
|
||||
}, '*');
|
||||
```
|
||||
|
||||
**Ví dụ DEV mode:**
|
||||
```javascript
|
||||
window.parent.postMessage({
|
||||
type: 'SDK_INIT',
|
||||
payload: {
|
||||
mode: 'dev',
|
||||
game_code: 'G001'
|
||||
}
|
||||
}, '*');
|
||||
```
|
||||
|
||||
**Ví dụ LIVE mode:**
|
||||
```javascript
|
||||
window.parent.postMessage({
|
||||
type: 'SDK_INIT',
|
||||
payload: {
|
||||
mode: 'live',
|
||||
game_code: 'G001',
|
||||
assignment_id: 'ASSIGN_123',
|
||||
student_id: 'STU_456',
|
||||
api_base_url: 'https://api.sena.tech',
|
||||
auth_token: 'token_xyz'
|
||||
}
|
||||
}, '*');
|
||||
```
|
||||
|
||||
**Nhận (SDK gửi lại khi ready):**
|
||||
```javascript
|
||||
{
|
||||
type: 'SDK_READY',
|
||||
payload: {
|
||||
mode: 'dev' | 'preview' | 'live',
|
||||
game_code: 'G001'
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2️⃣ **SDK_DATA_READY** (SDK → Game)
|
||||
|
||||
SDK gửi sanitized data cho game render. Game phải listen sự kiện này.
|
||||
|
||||
**Nhận:**
|
||||
```javascript
|
||||
window.addEventListener('message', (event) => {
|
||||
if (event.data.type === 'SDK_DATA_READY') {
|
||||
const {
|
||||
items, // Sanitized items (NO answers!)
|
||||
total_questions,
|
||||
completed_count,
|
||||
resume_data // Optional: previous results
|
||||
} = event.data.payload;
|
||||
|
||||
// Render game
|
||||
renderGame(items);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
**Payload items (tùy game_code):**
|
||||
|
||||
#### Quiz Games (G001-G004):
|
||||
```json
|
||||
{
|
||||
"id": "q1",
|
||||
"question": "What is 2+2?",
|
||||
"options": [
|
||||
{"text": "5"},
|
||||
{"text": "4"},
|
||||
{"text": "3"}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### Sequence Games (G110-G123):
|
||||
```json
|
||||
{
|
||||
"id": "seq1",
|
||||
"question": ["H", "", "L", "", "O"],
|
||||
"options": ["L", "E"],
|
||||
"audio_url": "https://..." // optional
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3️⃣ **SDK_CHECK_ANSWER** (Game → SDK)
|
||||
|
||||
Game gửi user's answer để SDK verify.
|
||||
|
||||
**Gửi:**
|
||||
```javascript
|
||||
window.parent.postMessage({
|
||||
type: 'SDK_CHECK_ANSWER',
|
||||
payload: {
|
||||
question_id: 'q1',
|
||||
choice: any, // Index (quiz) hoặc Array (sequence)
|
||||
time_spent?: number // Milliseconds
|
||||
}
|
||||
}, '*');
|
||||
```
|
||||
|
||||
**Quiz example (choice = index):**
|
||||
```javascript
|
||||
window.parent.postMessage({
|
||||
type: 'SDK_CHECK_ANSWER',
|
||||
payload: {
|
||||
question_id: 'q1',
|
||||
choice: 1, // User clicked option index 1
|
||||
time_spent: 5000
|
||||
}
|
||||
}, '*');
|
||||
```
|
||||
|
||||
**Sequence example (choice = reordered array):**
|
||||
```javascript
|
||||
window.parent.postMessage({
|
||||
type: 'SDK_CHECK_ANSWER',
|
||||
payload: {
|
||||
question_id: 'seq1',
|
||||
choice: ["H", "e", "l", "l", "o"], // Reordered
|
||||
time_spent: 8000
|
||||
}
|
||||
}, '*');
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4️⃣ **SDK_ANSWER_RESULT** (SDK → Game)
|
||||
|
||||
SDK gửi kết quả verify.
|
||||
|
||||
**Nhận:**
|
||||
```javascript
|
||||
window.addEventListener('message', (event) => {
|
||||
if (event.data.type === 'SDK_ANSWER_RESULT') {
|
||||
const {
|
||||
question_id,
|
||||
correct, // true/false
|
||||
score, // 0-1 hoặc custom
|
||||
synced, // true = already synced to server
|
||||
feedback // Optional: "✅ Correct!" or "❌ Wrong"
|
||||
} = event.data.payload;
|
||||
|
||||
if (correct) {
|
||||
showCorrectFeedback(question_id);
|
||||
} else {
|
||||
showWrongFeedback(question_id);
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5️⃣ **SDK_PUSH_DATA** (Game → SDK, PREVIEW only)
|
||||
|
||||
Nếu PREVIEW mode, game có thể push data thay vì SDK fetch.
|
||||
|
||||
**Gửi:**
|
||||
```javascript
|
||||
window.parent.postMessage({
|
||||
type: 'SDK_PUSH_DATA',
|
||||
payload: {
|
||||
items: [
|
||||
{
|
||||
id: 'q1',
|
||||
question: 'What is 2+2?',
|
||||
options: [
|
||||
{text: '3'},
|
||||
{text: '4'},
|
||||
{text: '5'}
|
||||
],
|
||||
answer: '4' // Server data (with answer)
|
||||
},
|
||||
// ... more items
|
||||
]
|
||||
}
|
||||
}, '*');
|
||||
```
|
||||
|
||||
SDK sẽ sanitize (remove answer, shuffle options) rồi gửi SDK_DATA_READY.
|
||||
|
||||
---
|
||||
|
||||
### 6️⃣ **SDK_ERROR** (SDK → Game)
|
||||
|
||||
SDK gửi error notification.
|
||||
|
||||
**Nhận:**
|
||||
```javascript
|
||||
window.addEventListener('message', (event) => {
|
||||
if (event.data.type === 'SDK_ERROR') {
|
||||
const {
|
||||
code, // Error code
|
||||
message, // Error message
|
||||
details // Optional: more info
|
||||
} = event.data.payload;
|
||||
|
||||
console.error(`[SDK Error] ${code}: ${message}`);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎮 Data Structures by Game Type
|
||||
|
||||
### Quiz Games (G001-G004)
|
||||
|
||||
#### Sanitized (Game receives):
|
||||
```json
|
||||
{
|
||||
"id": "q1",
|
||||
"question": "Audio URL or text",
|
||||
"image_url": "Image URL (optional)",
|
||||
"options": [
|
||||
{"text": "Option A"} or {"audio": "Audio URL"}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### User Answer Format:
|
||||
```javascript
|
||||
choice = 0 // Index of selected option
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Sequence Word (G110-G113)
|
||||
|
||||
#### Sanitized (Game receives):
|
||||
```json
|
||||
{
|
||||
"id": "seq_word_1",
|
||||
"question": ["H", "", "L", "", "O"],
|
||||
"options": ["L", "E"],
|
||||
"audio_url": "URL (optional)"
|
||||
}
|
||||
```
|
||||
|
||||
#### User Answer Format:
|
||||
```javascript
|
||||
choice = ["H", "e", "l", "l", "o"] // Reordered array
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Sequence Sentence (G120-G123)
|
||||
|
||||
#### Sanitized (Game receives):
|
||||
```json
|
||||
{
|
||||
"id": "seq_sent_1",
|
||||
"question": ["I", "", "reading", ""],
|
||||
"options": ["love", "books"],
|
||||
"audio_url": "URL (optional)"
|
||||
}
|
||||
```
|
||||
|
||||
#### User Answer Format:
|
||||
```javascript
|
||||
choice = ["I", "love", "reading", "books"] // Reordered
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 💻 Complete Game Implementation Example
|
||||
|
||||
```javascript
|
||||
// ============ INITIALIZE ============
|
||||
|
||||
// Listen for SDK messages
|
||||
window.addEventListener('message', (event) => {
|
||||
handleSdkMessage(event.data);
|
||||
});
|
||||
|
||||
// Initialize SDK
|
||||
function initGame() {
|
||||
const mode = 'live';
|
||||
const gameCode = 'G001';
|
||||
|
||||
window.parent.postMessage({
|
||||
type: 'SDK_INIT',
|
||||
payload: {
|
||||
mode,
|
||||
game_code: gameCode,
|
||||
assignment_id: 'ASSIGN_123',
|
||||
student_id: 'STU_456',
|
||||
api_base_url: 'https://api.sena.tech'
|
||||
}
|
||||
}, '*');
|
||||
}
|
||||
|
||||
// Handle all SDK messages
|
||||
function handleSdkMessage(data) {
|
||||
switch (data.type) {
|
||||
case 'SDK_READY':
|
||||
console.log('SDK initialized:', data.payload);
|
||||
break;
|
||||
|
||||
case 'SDK_DATA_READY':
|
||||
const items = data.payload.items;
|
||||
renderGameItems(items);
|
||||
break;
|
||||
|
||||
case 'SDK_ANSWER_RESULT':
|
||||
const result = data.payload;
|
||||
if (result.correct) {
|
||||
showCorrectFeedback(result.question_id);
|
||||
} else {
|
||||
showWrongFeedback(result.question_id);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'SDK_ERROR':
|
||||
console.error('SDK Error:', data.payload);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// ============ RENDER GAME ============
|
||||
|
||||
function renderGameItems(items) {
|
||||
items.forEach(item => {
|
||||
if (item.options) {
|
||||
// Quiz game
|
||||
renderQuizQuestion(item);
|
||||
} else if (Array.isArray(item.question)) {
|
||||
// Sequence game
|
||||
renderSequenceGame(item);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ============ HANDLE USER ANSWER ============
|
||||
|
||||
function submitAnswer(questionId, userChoice) {
|
||||
window.parent.postMessage({
|
||||
type: 'SDK_CHECK_ANSWER',
|
||||
payload: {
|
||||
question_id: questionId,
|
||||
choice: userChoice, // Index for quiz, array for sequence
|
||||
time_spent: 5000
|
||||
}
|
||||
}, '*');
|
||||
}
|
||||
|
||||
// ============ GAME START ============
|
||||
|
||||
// Start when page loads
|
||||
window.addEventListener('load', () => {
|
||||
initGame();
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Message Flow Examples
|
||||
|
||||
### DEV Mode (Local Verify)
|
||||
```
|
||||
Game: SDK_INIT
|
||||
↓
|
||||
SDK: SDK_READY
|
||||
↓
|
||||
SDK: SDK_DATA_READY (mock items)
|
||||
↓
|
||||
Game: SDK_CHECK_ANSWER
|
||||
↓
|
||||
SDK: SDK_ANSWER_RESULT (instantly)
|
||||
```
|
||||
|
||||
### LIVE Mode (Server Verify)
|
||||
```
|
||||
Game: SDK_INIT
|
||||
↓
|
||||
SDK: SDK_READY
|
||||
↓
|
||||
SDK: fetch from API → SDK_DATA_READY
|
||||
↓
|
||||
Game: SDK_CHECK_ANSWER
|
||||
↓
|
||||
SDK: POST to server
|
||||
↓
|
||||
SDK: SDK_ANSWER_RESULT (wait for server)
|
||||
```
|
||||
|
||||
### PREVIEW Mode (Push Data)
|
||||
```
|
||||
Game: SDK_INIT (mode='preview')
|
||||
↓
|
||||
SDK: SDK_READY
|
||||
↓
|
||||
Game: SDK_PUSH_DATA (items with answers)
|
||||
↓
|
||||
SDK: sanitize → SDK_DATA_READY
|
||||
↓
|
||||
Game: SDK_CHECK_ANSWER
|
||||
↓
|
||||
SDK: SDK_ANSWER_RESULT (local verify)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ Important Notes
|
||||
|
||||
✅ **SDK Always Sanitizes**
|
||||
- Removes `answer` field
|
||||
- Removes `word`, `sentence`, `parts`, `missing_letter_count`
|
||||
- Shuffles options (for quiz)
|
||||
|
||||
✅ **Game Never Gets Answer**
|
||||
- Game receives only question + options
|
||||
- Answer is stored server-side
|
||||
|
||||
✅ **Shuffled Options**
|
||||
- Game receives shuffled options array
|
||||
- Game user clicks index
|
||||
- SDK internally resolves index → text
|
||||
|
||||
✅ **Sequence Games**
|
||||
- Random positions are blanked (based on `missing_letter_count`)
|
||||
- Game receives question with blanks + missing items
|
||||
- User reorders missing items
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Testing
|
||||
|
||||
Test locally without SDK:
|
||||
```javascript
|
||||
// Simulate SDK messages for testing
|
||||
function simulateSdkMessage(type, payload) {
|
||||
const event = new MessageEvent('message', {
|
||||
data: { type, payload }
|
||||
});
|
||||
window.dispatchEvent(event);
|
||||
}
|
||||
|
||||
// Simulate SDK_DATA_READY
|
||||
simulateSdkMessage('SDK_DATA_READY', {
|
||||
items: [
|
||||
{
|
||||
id: 'q1',
|
||||
question: 'What is 2+2?',
|
||||
options: [{text: '4'}, {text: '5'}, {text: '3'}]
|
||||
}
|
||||
]
|
||||
});
|
||||
```
|
||||
29
G102-sequence/sdk/package/dist/EventEmitter.d.ts
vendored
@@ -1,29 +0,0 @@
|
||||
/**
|
||||
* Game Iframe SDK - Event Emitter
|
||||
* Simple typed event emitter for SDK
|
||||
*/
|
||||
export type EventHandler<T = any> = (data: T) => void;
|
||||
export declare class EventEmitter<Events extends Record<string, any>> {
|
||||
private handlers;
|
||||
/**
|
||||
* Subscribe to an event
|
||||
*/
|
||||
on<K extends keyof Events>(event: K, handler: EventHandler<Events[K]>): () => void;
|
||||
/**
|
||||
* Subscribe to an event (once)
|
||||
*/
|
||||
once<K extends keyof Events>(event: K, handler: EventHandler<Events[K]>): () => void;
|
||||
/**
|
||||
* Unsubscribe from an event
|
||||
*/
|
||||
off<K extends keyof Events>(event: K, handler: EventHandler<Events[K]>): void;
|
||||
/**
|
||||
* Emit an event
|
||||
*/
|
||||
emit<K extends keyof Events>(event: K, data: Events[K]): void;
|
||||
/**
|
||||
* Remove all handlers for an event (or all events)
|
||||
*/
|
||||
removeAllListeners(event?: keyof Events): void;
|
||||
}
|
||||
//# sourceMappingURL=EventEmitter.d.ts.map
|
||||
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"EventEmitter.d.ts","sourceRoot":"","sources":["../src/EventEmitter.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,MAAM,YAAY,CAAC,CAAC,GAAG,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC,KAAK,IAAI,CAAC;AAEtD,qBAAa,YAAY,CAAC,MAAM,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC;IACxD,OAAO,CAAC,QAAQ,CAAmD;IAEnE;;OAEG;IACH,EAAE,CAAC,CAAC,SAAS,MAAM,MAAM,EAAE,KAAK,EAAE,CAAC,EAAE,OAAO,EAAE,YAAY,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,GAAG,MAAM,IAAI;IAUlF;;OAEG;IACH,IAAI,CAAC,CAAC,SAAS,MAAM,MAAM,EAAE,KAAK,EAAE,CAAC,EAAE,OAAO,EAAE,YAAY,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,GAAG,MAAM,IAAI;IAQpF;;OAEG;IACH,GAAG,CAAC,CAAC,SAAS,MAAM,MAAM,EAAE,KAAK,EAAE,CAAC,EAAE,OAAO,EAAE,YAAY,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,GAAG,IAAI;IAI7E;;OAEG;IACH,IAAI,CAAC,CAAC,SAAS,MAAM,MAAM,EAAE,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,GAAG,IAAI;IAU7D;;OAEG;IACH,kBAAkB,CAAC,KAAK,CAAC,EAAE,MAAM,MAAM,GAAG,IAAI;CAOjD"}
|
||||
65
G102-sequence/sdk/package/dist/EventEmitter.js
vendored
@@ -1,65 +0,0 @@
|
||||
"use strict";
|
||||
/**
|
||||
* Game Iframe SDK - Event Emitter
|
||||
* Simple typed event emitter for SDK
|
||||
*/
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.EventEmitter = void 0;
|
||||
class EventEmitter {
|
||||
constructor() {
|
||||
this.handlers = new Map();
|
||||
}
|
||||
/**
|
||||
* Subscribe to an event
|
||||
*/
|
||||
on(event, handler) {
|
||||
if (!this.handlers.has(event)) {
|
||||
this.handlers.set(event, new Set());
|
||||
}
|
||||
this.handlers.get(event).add(handler);
|
||||
// Return unsubscribe function
|
||||
return () => this.off(event, handler);
|
||||
}
|
||||
/**
|
||||
* Subscribe to an event (once)
|
||||
*/
|
||||
once(event, handler) {
|
||||
const wrappedHandler = (data) => {
|
||||
this.off(event, wrappedHandler);
|
||||
handler(data);
|
||||
};
|
||||
return this.on(event, wrappedHandler);
|
||||
}
|
||||
/**
|
||||
* Unsubscribe from an event
|
||||
*/
|
||||
off(event, handler) {
|
||||
this.handlers.get(event)?.delete(handler);
|
||||
}
|
||||
/**
|
||||
* Emit an event
|
||||
*/
|
||||
emit(event, data) {
|
||||
this.handlers.get(event)?.forEach(handler => {
|
||||
try {
|
||||
handler(data);
|
||||
}
|
||||
catch (err) {
|
||||
console.error(`[EventEmitter] Error in handler for "${String(event)}":`, err);
|
||||
}
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Remove all handlers for an event (or all events)
|
||||
*/
|
||||
removeAllListeners(event) {
|
||||
if (event) {
|
||||
this.handlers.delete(event);
|
||||
}
|
||||
else {
|
||||
this.handlers.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
exports.EventEmitter = EventEmitter;
|
||||
//# sourceMappingURL=EventEmitter.js.map
|
||||
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"EventEmitter.js","sourceRoot":"","sources":["../src/EventEmitter.ts"],"names":[],"mappings":";AAAA;;;GAGG;;;AAIH,MAAa,YAAY;IAAzB;QACY,aAAQ,GAAyC,IAAI,GAAG,EAAE,CAAC;IAwDvE,CAAC;IAtDG;;OAEG;IACH,EAAE,CAAyB,KAAQ,EAAE,OAAgC;QACjE,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC;YAC5B,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,KAAK,EAAE,IAAI,GAAG,EAAE,CAAC,CAAC;QACxC,CAAC;QACD,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAE,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QAEvC,8BAA8B;QAC9B,OAAO,GAAG,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;IAC1C,CAAC;IAED;;OAEG;IACH,IAAI,CAAyB,KAAQ,EAAE,OAAgC;QACnE,MAAM,cAAc,GAAG,CAAC,IAAe,EAAE,EAAE;YACvC,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,cAAc,CAAC,CAAC;YAChC,OAAO,CAAC,IAAI,CAAC,CAAC;QAClB,CAAC,CAAC;QACF,OAAO,IAAI,CAAC,EAAE,CAAC,KAAK,EAAE,cAAc,CAAC,CAAC;IAC1C,CAAC;IAED;;OAEG;IACH,GAAG,CAAyB,KAAQ,EAAE,OAAgC;QAClE,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC;IAC9C,CAAC;IAED;;OAEG;IACH,IAAI,CAAyB,KAAQ,EAAE,IAAe;QAClD,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC,OAAO,CAAC,EAAE;YACxC,IAAI,CAAC;gBACD,OAAO,CAAC,IAAI,CAAC,CAAC;YAClB,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACX,OAAO,CAAC,KAAK,CAAC,wCAAwC,MAAM,CAAC,KAAK,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;YAClF,CAAC;QACL,CAAC,CAAC,CAAC;IACP,CAAC;IAED;;OAEG;IACH,kBAAkB,CAAC,KAAoB;QACnC,IAAI,KAAK,EAAE,CAAC;YACR,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QAChC,CAAC;aAAM,CAAC;YACJ,IAAI,CAAC,QAAQ,CAAC,KAAK,EAAE,CAAC;QAC1B,CAAC;IACL,CAAC;CACJ;AAzDD,oCAyDC"}
|
||||
@@ -1,93 +0,0 @@
|
||||
/**
|
||||
* Game Iframe SDK - Core
|
||||
* SDK chính - compose các layers: MessageHandler, MessageSender
|
||||
*/
|
||||
import { EventEmitter } from './EventEmitter';
|
||||
import { MessageHandler } from './MessageHandler';
|
||||
import { MessageSender } from './MessageSender';
|
||||
import { GameIframeSDKConfig, SDKEvents, PushDataPayload, LeaderboardData } from './types';
|
||||
/**
|
||||
* GameIframeSDK - Main SDK class
|
||||
* Composes MessageHandler và MessageSender
|
||||
*/
|
||||
export declare class GameIframeSDK extends EventEmitter<SDKEvents> {
|
||||
private config;
|
||||
private messageHandler;
|
||||
private messageSender;
|
||||
private pendingData;
|
||||
private isReady;
|
||||
constructor(config: GameIframeSDKConfig);
|
||||
/**
|
||||
* Set iframe element reference
|
||||
*/
|
||||
setIframe(iframe: HTMLIFrameElement | null): this;
|
||||
/**
|
||||
* Get current iframe
|
||||
*/
|
||||
getIframe(): HTMLIFrameElement | null;
|
||||
/**
|
||||
* Check if game is ready
|
||||
*/
|
||||
isGameReady(): boolean;
|
||||
/**
|
||||
* Check if sender is ready (iframe available)
|
||||
*/
|
||||
isSenderReady(): boolean;
|
||||
/**
|
||||
* Send game data to iframe
|
||||
*/
|
||||
sendGameData(data: PushDataPayload): boolean;
|
||||
/**
|
||||
* Send leaderboard data to iframe
|
||||
*/
|
||||
sendLeaderboard(data: LeaderboardData): boolean;
|
||||
/**
|
||||
* Queue data to be sent when game is ready
|
||||
*/
|
||||
queueGameData(data: PushDataPayload): this;
|
||||
/**
|
||||
* Clear queued data
|
||||
*/
|
||||
clearQueuedData(): this;
|
||||
/**
|
||||
* Force reload iframe
|
||||
*/
|
||||
reloadIframe(): boolean;
|
||||
/**
|
||||
* Cleanup and destroy SDK
|
||||
*/
|
||||
destroy(): void;
|
||||
/**
|
||||
* Get MessageHandler instance for advanced usage
|
||||
*/
|
||||
getMessageHandler(): MessageHandler;
|
||||
/**
|
||||
* Get MessageSender instance for advanced usage
|
||||
*/
|
||||
getMessageSender(): MessageSender;
|
||||
/**
|
||||
* Setup event forwarding from MessageHandler to SDK events
|
||||
*/
|
||||
private setupEventForwarding;
|
||||
/**
|
||||
* Send queued data
|
||||
*/
|
||||
private sendQueuedData;
|
||||
/**
|
||||
* Internal logging
|
||||
*/
|
||||
private log;
|
||||
}
|
||||
/**
|
||||
* Create SDK instance
|
||||
*/
|
||||
export declare function createGameIframeSDK(config: GameIframeSDKConfig): GameIframeSDK;
|
||||
/**
|
||||
* Get or create default SDK instance
|
||||
*/
|
||||
export declare function getGameIframeSDK(config?: GameIframeSDKConfig): GameIframeSDK;
|
||||
/**
|
||||
* Destroy default instance
|
||||
*/
|
||||
export declare function destroyGameIframeSDK(): void;
|
||||
//# sourceMappingURL=GameIframeSDK.d.ts.map
|
||||
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"GameIframeSDK.d.ts","sourceRoot":"","sources":["../src/GameIframeSDK.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAClD,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,OAAO,EACH,mBAAmB,EAEnB,SAAS,EACT,eAAe,EACf,eAAe,EAClB,MAAM,SAAS,CAAC;AAEjB;;;GAGG;AACH,qBAAa,aAAc,SAAQ,YAAY,CAAC,SAAS,CAAC;IACtD,OAAO,CAAC,MAAM,CAAgC;IAC9C,OAAO,CAAC,cAAc,CAAiB;IACvC,OAAO,CAAC,aAAa,CAAgB;IACrC,OAAO,CAAC,WAAW,CAAgC;IACnD,OAAO,CAAC,OAAO,CAAkB;gBAErB,MAAM,EAAE,mBAAmB;IA4BvC;;OAEG;IACH,SAAS,CAAC,MAAM,EAAE,iBAAiB,GAAG,IAAI,GAAG,IAAI;IAOjD;;OAEG;IACH,SAAS,IAAI,iBAAiB,GAAG,IAAI;IAIrC;;OAEG;IACH,WAAW,IAAI,OAAO;IAItB;;OAEG;IACH,aAAa,IAAI,OAAO;IAQxB;;OAEG;IACH,YAAY,CAAC,IAAI,EAAE,eAAe,GAAG,OAAO;IAa5C;;OAEG;IACH,eAAe,CAAC,IAAI,EAAE,eAAe,GAAG,OAAO;IAiB/C;;OAEG;IACH,aAAa,CAAC,IAAI,EAAE,eAAe,GAAG,IAAI;IAY1C;;OAEG;IACH,eAAe,IAAI,IAAI;IASvB;;OAEG;IACH,YAAY,IAAI,OAAO;IASvB;;OAEG;IACH,OAAO,IAAI,IAAI;IAYf;;OAEG;IACH,iBAAiB,IAAI,cAAc;IAInC;;OAEG;IACH,gBAAgB,IAAI,aAAa;IAQjC;;OAEG;IACH,OAAO,CAAC,oBAAoB;IAmC5B;;OAEG;IACH,OAAO,CAAC,cAAc;IAOtB;;OAEG;IACH,OAAO,CAAC,GAAG;CAkBd;AAQD;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,mBAAmB,GAAG,aAAa,CAE9E;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,CAAC,EAAE,mBAAmB,GAAG,aAAa,CAQ5E;AAED;;GAEG;AACH,wBAAgB,oBAAoB,IAAI,IAAI,CAG3C"}
|
||||
254
G102-sequence/sdk/package/dist/GameIframeSDK.js
vendored
@@ -1,254 +0,0 @@
|
||||
"use strict";
|
||||
/**
|
||||
* Game Iframe SDK - Core
|
||||
* SDK chính - compose các layers: MessageHandler, MessageSender
|
||||
*/
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.GameIframeSDK = void 0;
|
||||
exports.createGameIframeSDK = createGameIframeSDK;
|
||||
exports.getGameIframeSDK = getGameIframeSDK;
|
||||
exports.destroyGameIframeSDK = destroyGameIframeSDK;
|
||||
const EventEmitter_1 = require("./EventEmitter");
|
||||
const MessageHandler_1 = require("./MessageHandler");
|
||||
const MessageSender_1 = require("./MessageSender");
|
||||
const types_1 = require("./types");
|
||||
/**
|
||||
* GameIframeSDK - Main SDK class
|
||||
* Composes MessageHandler và MessageSender
|
||||
*/
|
||||
class GameIframeSDK extends EventEmitter_1.EventEmitter {
|
||||
constructor(config) {
|
||||
super();
|
||||
this.pendingData = null;
|
||||
this.isReady = false;
|
||||
this.config = { ...types_1.DEFAULT_CONFIG, ...config };
|
||||
// Initialize layers
|
||||
this.messageHandler = new MessageHandler_1.MessageHandler({
|
||||
acceptedOrigin: this.config.iframeOrigin,
|
||||
debug: this.config.debug,
|
||||
});
|
||||
this.messageSender = new MessageSender_1.MessageSender({
|
||||
targetOrigin: this.config.iframeOrigin,
|
||||
debug: this.config.debug,
|
||||
});
|
||||
// Setup event forwarding
|
||||
this.setupEventForwarding();
|
||||
// Start listening
|
||||
this.messageHandler.start();
|
||||
this.log('info', 'SDK initialized', { config: this.config });
|
||||
}
|
||||
// ==========================================================================
|
||||
// PUBLIC API - Iframe Management
|
||||
// ==========================================================================
|
||||
/**
|
||||
* Set iframe element reference
|
||||
*/
|
||||
setIframe(iframe) {
|
||||
this.messageSender.setIframe(iframe);
|
||||
this.isReady = false;
|
||||
this.log('info', 'Iframe set', { hasIframe: !!iframe });
|
||||
return this;
|
||||
}
|
||||
/**
|
||||
* Get current iframe
|
||||
*/
|
||||
getIframe() {
|
||||
return this.messageSender.getIframe();
|
||||
}
|
||||
/**
|
||||
* Check if game is ready
|
||||
*/
|
||||
isGameReady() {
|
||||
return this.isReady;
|
||||
}
|
||||
/**
|
||||
* Check if sender is ready (iframe available)
|
||||
*/
|
||||
isSenderReady() {
|
||||
return this.messageSender.isReady();
|
||||
}
|
||||
// ==========================================================================
|
||||
// PUBLIC API - Send Data
|
||||
// ==========================================================================
|
||||
/**
|
||||
* Send game data to iframe
|
||||
*/
|
||||
sendGameData(data) {
|
||||
const result = this.messageSender.sendGameData(data);
|
||||
if (!result.success) {
|
||||
this.emit('error', {
|
||||
message: 'Failed to send game data',
|
||||
error: result.error,
|
||||
});
|
||||
}
|
||||
return result.success;
|
||||
}
|
||||
/**
|
||||
* Send leaderboard data to iframe
|
||||
*/
|
||||
sendLeaderboard(data) {
|
||||
const result = this.messageSender.sendLeaderboard(data);
|
||||
if (!result.success) {
|
||||
this.emit('error', {
|
||||
message: 'Failed to send leaderboard',
|
||||
error: result.error,
|
||||
});
|
||||
}
|
||||
return result.success;
|
||||
}
|
||||
// ==========================================================================
|
||||
// PUBLIC API - Queue & Auto-send
|
||||
// ==========================================================================
|
||||
/**
|
||||
* Queue data to be sent when game is ready
|
||||
*/
|
||||
queueGameData(data) {
|
||||
this.pendingData = data;
|
||||
this.log('info', 'Data queued for when game is ready');
|
||||
// If already ready, send immediately
|
||||
if (this.isReady) {
|
||||
this.sendQueuedData();
|
||||
}
|
||||
return this;
|
||||
}
|
||||
/**
|
||||
* Clear queued data
|
||||
*/
|
||||
clearQueuedData() {
|
||||
this.pendingData = null;
|
||||
return this;
|
||||
}
|
||||
// ==========================================================================
|
||||
// PUBLIC API - Iframe Control
|
||||
// ==========================================================================
|
||||
/**
|
||||
* Force reload iframe
|
||||
*/
|
||||
reloadIframe() {
|
||||
this.isReady = false;
|
||||
return this.messageSender.reloadIframe();
|
||||
}
|
||||
// ==========================================================================
|
||||
// PUBLIC API - Lifecycle
|
||||
// ==========================================================================
|
||||
/**
|
||||
* Cleanup and destroy SDK
|
||||
*/
|
||||
destroy() {
|
||||
this.messageHandler.destroy();
|
||||
this.removeAllListeners();
|
||||
this.pendingData = null;
|
||||
this.isReady = false;
|
||||
this.log('info', 'SDK destroyed');
|
||||
}
|
||||
// ==========================================================================
|
||||
// PUBLIC API - Direct Layer Access (Advanced)
|
||||
// ==========================================================================
|
||||
/**
|
||||
* Get MessageHandler instance for advanced usage
|
||||
*/
|
||||
getMessageHandler() {
|
||||
return this.messageHandler;
|
||||
}
|
||||
/**
|
||||
* Get MessageSender instance for advanced usage
|
||||
*/
|
||||
getMessageSender() {
|
||||
return this.messageSender;
|
||||
}
|
||||
// ==========================================================================
|
||||
// PRIVATE METHODS
|
||||
// ==========================================================================
|
||||
/**
|
||||
* Setup event forwarding from MessageHandler to SDK events
|
||||
*/
|
||||
setupEventForwarding() {
|
||||
// Forward gameReady
|
||||
this.messageHandler.on('gameReady', () => {
|
||||
this.isReady = true;
|
||||
this.emit('gameReady', undefined);
|
||||
// Auto-send queued data if enabled
|
||||
if (this.config.autoSendOnReady && this.pendingData) {
|
||||
setTimeout(() => {
|
||||
this.sendQueuedData();
|
||||
}, this.config.readyDelay);
|
||||
}
|
||||
});
|
||||
// Forward answerReport
|
||||
this.messageHandler.on('answerReport', (data) => {
|
||||
this.emit('answerReport', data);
|
||||
});
|
||||
// Forward finalResult
|
||||
this.messageHandler.on('finalResult', (data) => {
|
||||
this.emit('finalResult', data);
|
||||
});
|
||||
// Forward leaderboardRequest
|
||||
this.messageHandler.on('leaderboardRequest', (data) => {
|
||||
this.emit('leaderboardRequest', data);
|
||||
});
|
||||
// Forward errors
|
||||
this.messageHandler.on('error', (error) => {
|
||||
this.emit('error', error);
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Send queued data
|
||||
*/
|
||||
sendQueuedData() {
|
||||
if (this.pendingData) {
|
||||
this.sendGameData(this.pendingData);
|
||||
this.pendingData = null;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Internal logging
|
||||
*/
|
||||
log(level, message, data) {
|
||||
if (this.config.debug) {
|
||||
const prefix = '[GameIframeSDK]';
|
||||
switch (level) {
|
||||
case 'info':
|
||||
console.log(prefix, message, data ?? '');
|
||||
break;
|
||||
case 'warn':
|
||||
console.warn(prefix, message, data ?? '');
|
||||
break;
|
||||
case 'error':
|
||||
console.error(prefix, message, data ?? '');
|
||||
break;
|
||||
}
|
||||
}
|
||||
this.emit('log', { level, message, data });
|
||||
}
|
||||
}
|
||||
exports.GameIframeSDK = GameIframeSDK;
|
||||
// ==========================================================================
|
||||
// FACTORY / SINGLETON HELPERS
|
||||
// ==========================================================================
|
||||
let defaultInstance = null;
|
||||
/**
|
||||
* Create SDK instance
|
||||
*/
|
||||
function createGameIframeSDK(config) {
|
||||
return new GameIframeSDK(config);
|
||||
}
|
||||
/**
|
||||
* Get or create default SDK instance
|
||||
*/
|
||||
function getGameIframeSDK(config) {
|
||||
if (!defaultInstance && config) {
|
||||
defaultInstance = new GameIframeSDK(config);
|
||||
}
|
||||
if (!defaultInstance) {
|
||||
throw new Error('GameIframeSDK not initialized. Call with config first.');
|
||||
}
|
||||
return defaultInstance;
|
||||
}
|
||||
/**
|
||||
* Destroy default instance
|
||||
*/
|
||||
function destroyGameIframeSDK() {
|
||||
defaultInstance?.destroy();
|
||||
defaultInstance = null;
|
||||
}
|
||||
//# sourceMappingURL=GameIframeSDK.js.map
|
||||
@@ -1,70 +0,0 @@
|
||||
/**
|
||||
* Game Iframe SDK - Message Handler
|
||||
* Xử lý message từ iframe
|
||||
*/
|
||||
import { AnswerReportData, FinalResultData } from './types';
|
||||
import { EventEmitter } from './EventEmitter';
|
||||
export interface MessageHandlerEvents {
|
||||
gameReady: void;
|
||||
answerReport: AnswerReportData;
|
||||
finalResult: FinalResultData;
|
||||
leaderboardRequest: {
|
||||
top: number;
|
||||
};
|
||||
unknownMessage: {
|
||||
type: string;
|
||||
data: any;
|
||||
};
|
||||
error: {
|
||||
message: string;
|
||||
error?: any;
|
||||
};
|
||||
}
|
||||
export interface MessageHandlerConfig {
|
||||
/**
|
||||
* Accepted origin (use '*' to accept all - not recommended for production)
|
||||
*/
|
||||
acceptedOrigin: string;
|
||||
/**
|
||||
* Enable debug logging
|
||||
*/
|
||||
debug?: boolean;
|
||||
}
|
||||
/**
|
||||
* MessageHandler - Xử lý incoming messages từ iframe
|
||||
*/
|
||||
export declare class MessageHandler extends EventEmitter<MessageHandlerEvents> {
|
||||
private config;
|
||||
private boundHandler;
|
||||
private isListening;
|
||||
constructor(config: MessageHandlerConfig);
|
||||
/**
|
||||
* Start listening for messages
|
||||
*/
|
||||
start(): this;
|
||||
/**
|
||||
* Stop listening for messages
|
||||
*/
|
||||
stop(): this;
|
||||
/**
|
||||
* Check if handler is listening
|
||||
*/
|
||||
isActive(): boolean;
|
||||
/**
|
||||
* Handle incoming message
|
||||
*/
|
||||
private handleMessage;
|
||||
/**
|
||||
* Check if origin is allowed
|
||||
*/
|
||||
private isOriginAllowed;
|
||||
/**
|
||||
* Debug log
|
||||
*/
|
||||
private log;
|
||||
/**
|
||||
* Cleanup
|
||||
*/
|
||||
destroy(): void;
|
||||
}
|
||||
//# sourceMappingURL=MessageHandler.d.ts.map
|
||||
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"MessageHandler.d.ts","sourceRoot":"","sources":["../src/MessageHandler.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAiB,gBAAgB,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAC3E,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAE9C,MAAM,WAAW,oBAAoB;IACjC,SAAS,EAAE,IAAI,CAAC;IAChB,YAAY,EAAE,gBAAgB,CAAC;IAC/B,WAAW,EAAE,eAAe,CAAC;IAC7B,kBAAkB,EAAE;QAAE,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC;IACpC,cAAc,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,GAAG,CAAA;KAAE,CAAC;IAC5C,KAAK,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,GAAG,CAAA;KAAE,CAAC;CAC3C;AAED,MAAM,WAAW,oBAAoB;IACjC;;OAEG;IACH,cAAc,EAAE,MAAM,CAAC;IAEvB;;OAEG;IACH,KAAK,CAAC,EAAE,OAAO,CAAC;CACnB;AAED;;GAEG;AACH,qBAAa,cAAe,SAAQ,YAAY,CAAC,oBAAoB,CAAC;IAClE,OAAO,CAAC,MAAM,CAAuB;IACrC,OAAO,CAAC,YAAY,CAAgD;IACpE,OAAO,CAAC,WAAW,CAAS;gBAEhB,MAAM,EAAE,oBAAoB;IAKxC;;OAEG;IACH,KAAK,IAAI,IAAI;IAab;;OAEG;IACH,IAAI,IAAI,IAAI;IAWZ;;OAEG;IACH,QAAQ,IAAI,OAAO;IAInB;;OAEG;IACH,OAAO,CAAC,aAAa;IAyCrB;;OAEG;IACH,OAAO,CAAC,eAAe;IAOvB;;OAEG;IACH,OAAO,CAAC,GAAG;IAMX;;OAEG;IACH,OAAO,IAAI,IAAI;CAIlB"}
|
||||
115
G102-sequence/sdk/package/dist/MessageHandler.js
vendored
@@ -1,115 +0,0 @@
|
||||
"use strict";
|
||||
/**
|
||||
* Game Iframe SDK - Message Handler
|
||||
* Xử lý message từ iframe
|
||||
*/
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.MessageHandler = void 0;
|
||||
const types_1 = require("./types");
|
||||
const EventEmitter_1 = require("./EventEmitter");
|
||||
/**
|
||||
* MessageHandler - Xử lý incoming messages từ iframe
|
||||
*/
|
||||
class MessageHandler extends EventEmitter_1.EventEmitter {
|
||||
constructor(config) {
|
||||
super();
|
||||
this.boundHandler = null;
|
||||
this.isListening = false;
|
||||
this.config = config;
|
||||
}
|
||||
/**
|
||||
* Start listening for messages
|
||||
*/
|
||||
start() {
|
||||
if (this.isListening) {
|
||||
return this;
|
||||
}
|
||||
this.boundHandler = this.handleMessage.bind(this);
|
||||
window.addEventListener('message', this.boundHandler);
|
||||
this.isListening = true;
|
||||
this.log('MessageHandler started');
|
||||
return this;
|
||||
}
|
||||
/**
|
||||
* Stop listening for messages
|
||||
*/
|
||||
stop() {
|
||||
if (this.boundHandler) {
|
||||
window.removeEventListener('message', this.boundHandler);
|
||||
this.boundHandler = null;
|
||||
}
|
||||
this.isListening = false;
|
||||
this.log('MessageHandler stopped');
|
||||
return this;
|
||||
}
|
||||
/**
|
||||
* Check if handler is listening
|
||||
*/
|
||||
isActive() {
|
||||
return this.isListening;
|
||||
}
|
||||
/**
|
||||
* Handle incoming message
|
||||
*/
|
||||
handleMessage(event) {
|
||||
// Origin check
|
||||
if (!this.isOriginAllowed(event.origin)) {
|
||||
return;
|
||||
}
|
||||
const { type, data } = event.data || {};
|
||||
if (!type)
|
||||
return;
|
||||
this.log(`Received: ${type}`, data);
|
||||
try {
|
||||
switch (type) {
|
||||
case types_1.MESSAGE_TYPES.GAME_READY:
|
||||
this.emit('gameReady', undefined);
|
||||
break;
|
||||
case types_1.MESSAGE_TYPES.ANSWER_REPORT:
|
||||
// Raw data pass-through
|
||||
this.emit('answerReport', data);
|
||||
break;
|
||||
case types_1.MESSAGE_TYPES.FINAL_RESULT:
|
||||
// Raw data pass-through
|
||||
this.emit('finalResult', data);
|
||||
break;
|
||||
case types_1.MESSAGE_TYPES.GET_LEADERBOARD:
|
||||
this.emit('leaderboardRequest', { top: data?.top || 10 });
|
||||
break;
|
||||
default:
|
||||
this.emit('unknownMessage', { type, data });
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
const err = error;
|
||||
this.emit('error', { message: `Error handling ${type}`, error: err });
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Check if origin is allowed
|
||||
*/
|
||||
isOriginAllowed(origin) {
|
||||
if (this.config.acceptedOrigin === '*') {
|
||||
return true;
|
||||
}
|
||||
return origin === this.config.acceptedOrigin;
|
||||
}
|
||||
/**
|
||||
* Debug log
|
||||
*/
|
||||
log(message, data) {
|
||||
if (this.config.debug) {
|
||||
console.log('[MessageHandler]', message, data ?? '');
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Cleanup
|
||||
*/
|
||||
destroy() {
|
||||
this.stop();
|
||||
this.removeAllListeners();
|
||||
}
|
||||
}
|
||||
exports.MessageHandler = MessageHandler;
|
||||
//# sourceMappingURL=MessageHandler.js.map
|
||||
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"MessageHandler.js","sourceRoot":"","sources":["../src/MessageHandler.ts"],"names":[],"mappings":"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"}
|
||||
@@ -1,60 +0,0 @@
|
||||
/**
|
||||
* Game Iframe SDK - Message Sender
|
||||
* Gửi message đến iframe
|
||||
*/
|
||||
import { PushDataPayload, LeaderboardData } from './types';
|
||||
export interface MessageSenderConfig {
|
||||
/**
|
||||
* Target origin for postMessage
|
||||
*/
|
||||
targetOrigin: string;
|
||||
/**
|
||||
* Enable debug logging
|
||||
*/
|
||||
debug?: boolean;
|
||||
}
|
||||
export interface SendResult {
|
||||
success: boolean;
|
||||
error?: Error;
|
||||
}
|
||||
/**
|
||||
* MessageSender - Gửi messages đến iframe
|
||||
*/
|
||||
export declare class MessageSender {
|
||||
private config;
|
||||
private iframe;
|
||||
constructor(config: MessageSenderConfig);
|
||||
/**
|
||||
* Set iframe element
|
||||
*/
|
||||
setIframe(iframe: HTMLIFrameElement | null): this;
|
||||
/**
|
||||
* Get current iframe
|
||||
*/
|
||||
getIframe(): HTMLIFrameElement | null;
|
||||
/**
|
||||
* Check if iframe is available
|
||||
*/
|
||||
isReady(): boolean;
|
||||
/**
|
||||
* Send raw message to iframe
|
||||
*/
|
||||
sendRaw(message: any): SendResult;
|
||||
/**
|
||||
* Send game data (SERVER_PUSH_DATA)
|
||||
*/
|
||||
sendGameData(payload: PushDataPayload): SendResult;
|
||||
/**
|
||||
* Send leaderboard (SERVER_PUSH_LEADERBOARD)
|
||||
*/
|
||||
sendLeaderboard(data: LeaderboardData): SendResult;
|
||||
/**
|
||||
* Reload iframe
|
||||
*/
|
||||
reloadIframe(): boolean;
|
||||
/**
|
||||
* Debug log
|
||||
*/
|
||||
private log;
|
||||
}
|
||||
//# sourceMappingURL=MessageSender.d.ts.map
|
||||
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"MessageSender.d.ts","sourceRoot":"","sources":["../src/MessageSender.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,eAAe,EAAE,eAAe,EAAiB,MAAM,SAAS,CAAC;AAE1E,MAAM,WAAW,mBAAmB;IAChC;;OAEG;IACH,YAAY,EAAE,MAAM,CAAC;IAErB;;OAEG;IACH,KAAK,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,UAAU;IACvB,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,KAAK,CAAC;CACjB;AAED;;GAEG;AACH,qBAAa,aAAa;IACtB,OAAO,CAAC,MAAM,CAAsB;IACpC,OAAO,CAAC,MAAM,CAAkC;gBAEpC,MAAM,EAAE,mBAAmB;IAIvC;;OAEG;IACH,SAAS,CAAC,MAAM,EAAE,iBAAiB,GAAG,IAAI,GAAG,IAAI;IAKjD;;OAEG;IACH,SAAS,IAAI,iBAAiB,GAAG,IAAI;IAIrC;;OAEG;IACH,OAAO,IAAI,OAAO;IAIlB;;OAEG;IACH,OAAO,CAAC,OAAO,EAAE,GAAG,GAAG,UAAU;IAmBjC;;OAEG;IACH,YAAY,CAAC,OAAO,EAAE,eAAe,GAAG,UAAU;IAoBlD;;OAEG;IACH,eAAe,CAAC,IAAI,EAAE,eAAe,GAAG,UAAU;IAmBlD;;OAEG;IACH,YAAY,IAAI,OAAO;IAqBvB;;OAEG;IACH,OAAO,CAAC,GAAG;CAYd"}
|
||||
132
G102-sequence/sdk/package/dist/MessageSender.js
vendored
@@ -1,132 +0,0 @@
|
||||
"use strict";
|
||||
/**
|
||||
* Game Iframe SDK - Message Sender
|
||||
* Gửi message đến iframe
|
||||
*/
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.MessageSender = void 0;
|
||||
const types_1 = require("./types");
|
||||
/**
|
||||
* MessageSender - Gửi messages đến iframe
|
||||
*/
|
||||
class MessageSender {
|
||||
constructor(config) {
|
||||
this.iframe = null;
|
||||
this.config = config;
|
||||
}
|
||||
/**
|
||||
* Set iframe element
|
||||
*/
|
||||
setIframe(iframe) {
|
||||
this.iframe = iframe;
|
||||
return this;
|
||||
}
|
||||
/**
|
||||
* Get current iframe
|
||||
*/
|
||||
getIframe() {
|
||||
return this.iframe;
|
||||
}
|
||||
/**
|
||||
* Check if iframe is available
|
||||
*/
|
||||
isReady() {
|
||||
return !!this.iframe?.contentWindow;
|
||||
}
|
||||
/**
|
||||
* Send raw message to iframe
|
||||
*/
|
||||
sendRaw(message) {
|
||||
if (!this.iframe?.contentWindow) {
|
||||
return {
|
||||
success: false,
|
||||
error: new Error('Iframe not available'),
|
||||
};
|
||||
}
|
||||
try {
|
||||
this.iframe.contentWindow.postMessage(message, this.config.targetOrigin);
|
||||
this.log('Sent message', { type: message.type });
|
||||
return { success: true };
|
||||
}
|
||||
catch (error) {
|
||||
const err = error;
|
||||
this.log('Send failed', { error: err.message });
|
||||
return { success: false, error: err };
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Send game data (SERVER_PUSH_DATA)
|
||||
*/
|
||||
sendGameData(payload) {
|
||||
// Inline message creation
|
||||
const message = {
|
||||
type: types_1.MESSAGE_TYPES.SERVER_PUSH_DATA,
|
||||
jsonData: payload,
|
||||
};
|
||||
const result = this.sendRaw(message);
|
||||
if (result.success) {
|
||||
const dataLength = payload.data?.length || 0;
|
||||
this.log('Sent game data', {
|
||||
game_id: payload.game_id,
|
||||
items: dataLength,
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
/**
|
||||
* Send leaderboard (SERVER_PUSH_LEADERBOARD)
|
||||
*/
|
||||
sendLeaderboard(data) {
|
||||
// Inline message creation
|
||||
const message = {
|
||||
type: types_1.MESSAGE_TYPES.SERVER_PUSH_LEADERBOARD,
|
||||
leaderboardData: data,
|
||||
};
|
||||
const result = this.sendRaw(message);
|
||||
if (result.success) {
|
||||
this.log('Sent leaderboard', {
|
||||
players: data.top_players?.length || 0,
|
||||
hasUserRank: !!data.user_rank,
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
/**
|
||||
* Reload iframe
|
||||
*/
|
||||
reloadIframe() {
|
||||
if (!this.iframe) {
|
||||
return false;
|
||||
}
|
||||
const currentSrc = this.iframe.src;
|
||||
if (!currentSrc || currentSrc === 'about:blank') {
|
||||
return false;
|
||||
}
|
||||
this.iframe.src = '';
|
||||
setTimeout(() => {
|
||||
if (this.iframe) {
|
||||
this.iframe.src = currentSrc;
|
||||
this.log('Iframe reloaded');
|
||||
}
|
||||
}, 100);
|
||||
return true;
|
||||
}
|
||||
/**
|
||||
* Debug log
|
||||
*/
|
||||
log(message, data) {
|
||||
if (this.config.debug) {
|
||||
console.log('[MessageSender]', message);
|
||||
if (data) {
|
||||
try {
|
||||
console.log(JSON.stringify(data, null, 2));
|
||||
}
|
||||
catch (e) {
|
||||
console.log(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
exports.MessageSender = MessageSender;
|
||||
//# sourceMappingURL=MessageSender.js.map
|
||||
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"MessageSender.js","sourceRoot":"","sources":["../src/MessageSender.ts"],"names":[],"mappings":"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"}
|
||||
@@ -1,49 +0,0 @@
|
||||
/**
|
||||
* Data Validator
|
||||
* Verify data structure cho từng game code
|
||||
*
|
||||
* Usage:
|
||||
* ```typescript
|
||||
* import { validateGameData, DataValidator } from 'game-iframe-sdk/client';
|
||||
*
|
||||
* const result = validateGameData('G001', receivedData);
|
||||
* if (!result.valid) {
|
||||
* console.error('Invalid data:', result.errors);
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
import { GameCode } from '../kit/GameDataHandler';
|
||||
export interface ValidationResult {
|
||||
valid: boolean;
|
||||
errors: string[];
|
||||
warnings: string[];
|
||||
}
|
||||
export interface FieldSchema {
|
||||
type: 'string' | 'number' | 'boolean' | 'array' | 'object' | 'any';
|
||||
required: boolean;
|
||||
arrayItemType?: 'string' | 'number' | 'object' | 'any';
|
||||
description?: string;
|
||||
}
|
||||
export interface ItemSchema {
|
||||
[field: string]: FieldSchema;
|
||||
}
|
||||
/**
|
||||
* Validate game data payload
|
||||
*/
|
||||
export declare function validateGameData(gameCode: GameCode, payload: any): ValidationResult;
|
||||
/**
|
||||
* Get schema for a game code
|
||||
*/
|
||||
export declare function getSchema(gameCode: GameCode): ItemSchema | null;
|
||||
/**
|
||||
* Get schema documentation for a game code
|
||||
*/
|
||||
export declare function getSchemaDoc(gameCode: GameCode): string;
|
||||
export declare class DataValidator {
|
||||
private gameCode;
|
||||
constructor(gameCode: GameCode);
|
||||
validate(payload: any): ValidationResult;
|
||||
getSchema(): ItemSchema | null;
|
||||
getSchemaDoc(): string;
|
||||
}
|
||||
//# sourceMappingURL=DataValidator.d.ts.map
|
||||
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"DataValidator.d.ts","sourceRoot":"","sources":["../../src/client/DataValidator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAE,QAAQ,EAAc,MAAM,wBAAwB,CAAC;AAM9D,MAAM,WAAW,gBAAgB;IAC7B,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,QAAQ,EAAE,MAAM,EAAE,CAAC;CACtB;AAED,MAAM,WAAW,WAAW;IACxB,IAAI,EAAE,QAAQ,GAAG,QAAQ,GAAG,SAAS,GAAG,OAAO,GAAG,QAAQ,GAAG,KAAK,CAAC;IACnE,QAAQ,EAAE,OAAO,CAAC;IAClB,aAAa,CAAC,EAAE,QAAQ,GAAG,QAAQ,GAAG,QAAQ,GAAG,KAAK,CAAC;IACvD,WAAW,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,UAAU;IACvB,CAAC,KAAK,EAAE,MAAM,GAAG,WAAW,CAAC;CAChC;AAoJD;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,QAAQ,EAAE,OAAO,EAAE,GAAG,GAAG,gBAAgB,CAmDnF;AAED;;GAEG;AACH,wBAAgB,SAAS,CAAC,QAAQ,EAAE,QAAQ,GAAG,UAAU,GAAG,IAAI,CAE/D;AAED;;GAEG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,QAAQ,GAAG,MAAM,CAyBvD;AAMD,qBAAa,aAAa;IACtB,OAAO,CAAC,QAAQ,CAAW;gBAEf,QAAQ,EAAE,QAAQ;IAI9B,QAAQ,CAAC,OAAO,EAAE,GAAG,GAAG,gBAAgB;IAIxC,SAAS,IAAI,UAAU,GAAG,IAAI;IAI9B,YAAY,IAAI,MAAM;CAGzB"}
|
||||
@@ -1,252 +0,0 @@
|
||||
"use strict";
|
||||
/**
|
||||
* Data Validator
|
||||
* Verify data structure cho từng game code
|
||||
*
|
||||
* Usage:
|
||||
* ```typescript
|
||||
* import { validateGameData, DataValidator } from 'game-iframe-sdk/client';
|
||||
*
|
||||
* const result = validateGameData('G001', receivedData);
|
||||
* if (!result.valid) {
|
||||
* console.error('Invalid data:', result.errors);
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.DataValidator = void 0;
|
||||
exports.validateGameData = validateGameData;
|
||||
exports.getSchema = getSchema;
|
||||
exports.getSchemaDoc = getSchemaDoc;
|
||||
const GameDataHandler_1 = require("../kit/GameDataHandler");
|
||||
// =============================================================================
|
||||
// SCHEMAS FOR EACH GAME CODE
|
||||
// =============================================================================
|
||||
const QUIZ_BASE_SCHEMA = {
|
||||
id: { type: 'string', required: true, description: 'Unique question ID' },
|
||||
options: { type: 'array', required: true, arrayItemType: 'string', description: 'Answer options' },
|
||||
answer: { type: 'number', required: true, description: 'Correct answer index (0-based)' },
|
||||
};
|
||||
const SCHEMAS = {
|
||||
// Quiz variants
|
||||
G001: {
|
||||
...QUIZ_BASE_SCHEMA,
|
||||
question: { type: 'string', required: true, description: 'Text question' },
|
||||
},
|
||||
G002: {
|
||||
...QUIZ_BASE_SCHEMA,
|
||||
question_audio: { type: 'string', required: true, description: 'Audio URL for question' },
|
||||
},
|
||||
G003: {
|
||||
...QUIZ_BASE_SCHEMA,
|
||||
question: { type: 'string', required: true, description: 'Text question' },
|
||||
// options are audio URLs
|
||||
},
|
||||
G004: {
|
||||
...QUIZ_BASE_SCHEMA,
|
||||
question_image: { type: 'string', required: true, description: 'Image URL for question' },
|
||||
question: { type: 'string', required: false, description: 'Optional text hint' },
|
||||
},
|
||||
// G005: Quiz Text-Image (options are image URLs, client picks index)
|
||||
G005: {
|
||||
...QUIZ_BASE_SCHEMA,
|
||||
question: { type: 'string', required: true, description: 'Text question' },
|
||||
// options are image URLs, answer is index pointing to correct image
|
||||
},
|
||||
// Sequence Word variants
|
||||
G110: {
|
||||
id: { type: 'string', required: true },
|
||||
word: { type: 'string', required: true, description: 'The word to arrange' },
|
||||
parts: { type: 'array', required: true, arrayItemType: 'string', description: 'Letters/parts to arrange' },
|
||||
answer: { type: 'array', required: true, arrayItemType: 'string', description: 'Correct order' },
|
||||
},
|
||||
G111: {
|
||||
id: { type: 'string', required: true },
|
||||
word: { type: 'string', required: true },
|
||||
parts: { type: 'array', required: true, arrayItemType: 'string' },
|
||||
answer: { type: 'array', required: true, arrayItemType: 'string' },
|
||||
audio_url: { type: 'string', required: true, description: 'Audio hint URL' },
|
||||
},
|
||||
G112: {
|
||||
id: { type: 'string', required: true },
|
||||
word: { type: 'string', required: true },
|
||||
parts: { type: 'array', required: true, arrayItemType: 'string' },
|
||||
answer: { type: 'array', required: true, arrayItemType: 'string' },
|
||||
audio_url: { type: 'string', required: true },
|
||||
},
|
||||
G113: {
|
||||
id: { type: 'string', required: true },
|
||||
word: { type: 'string', required: true },
|
||||
parts: { type: 'array', required: true, arrayItemType: 'string' },
|
||||
answer: { type: 'array', required: true, arrayItemType: 'string' },
|
||||
audio_url: { type: 'string', required: true },
|
||||
},
|
||||
// Sequence Sentence variants
|
||||
G120: {
|
||||
id: { type: 'string', required: true },
|
||||
sentence: { type: 'string', required: false, description: 'Full sentence (hint)' },
|
||||
parts: { type: 'array', required: true, arrayItemType: 'string', description: 'Words to arrange' },
|
||||
answer: { type: 'array', required: true, arrayItemType: 'string', description: 'Correct word order' },
|
||||
},
|
||||
G121: {
|
||||
id: { type: 'string', required: true },
|
||||
sentence: { type: 'string', required: false },
|
||||
parts: { type: 'array', required: true, arrayItemType: 'string' },
|
||||
answer: { type: 'array', required: true, arrayItemType: 'string' },
|
||||
audio_url: { type: 'string', required: true },
|
||||
},
|
||||
G122: {
|
||||
id: { type: 'string', required: true },
|
||||
sentence: { type: 'string', required: false },
|
||||
parts: { type: 'array', required: true, arrayItemType: 'string' },
|
||||
answer: { type: 'array', required: true, arrayItemType: 'string' },
|
||||
audio_url: { type: 'string', required: true },
|
||||
},
|
||||
G123: {
|
||||
id: { type: 'string', required: true },
|
||||
sentence: { type: 'string', required: false },
|
||||
parts: { type: 'array', required: true, arrayItemType: 'string' },
|
||||
answer: { type: 'array', required: true, arrayItemType: 'string' },
|
||||
audio_url: { type: 'string', required: true },
|
||||
}
|
||||
};
|
||||
// =============================================================================
|
||||
// VALIDATOR
|
||||
// =============================================================================
|
||||
/**
|
||||
* Validate a single item against schema
|
||||
*/
|
||||
function validateItem(item, schema, itemIndex) {
|
||||
const errors = [];
|
||||
if (!item || typeof item !== 'object') {
|
||||
errors.push(`Item [${itemIndex}]: Must be an object`);
|
||||
return errors;
|
||||
}
|
||||
for (const [field, fieldSchema] of Object.entries(schema)) {
|
||||
const value = item[field];
|
||||
// Check required
|
||||
if (fieldSchema.required && (value === undefined || value === null)) {
|
||||
errors.push(`Item [${itemIndex}].${field}: Required field is missing`);
|
||||
continue;
|
||||
}
|
||||
// Skip validation if optional and not present
|
||||
if (!fieldSchema.required && (value === undefined || value === null)) {
|
||||
continue;
|
||||
}
|
||||
// Check type
|
||||
const actualType = Array.isArray(value) ? 'array' : typeof value;
|
||||
if (fieldSchema.type !== 'any' && actualType !== fieldSchema.type) {
|
||||
errors.push(`Item [${itemIndex}].${field}: Expected ${fieldSchema.type}, got ${actualType}`);
|
||||
continue;
|
||||
}
|
||||
// Check array items
|
||||
if (fieldSchema.type === 'array' && fieldSchema.arrayItemType && fieldSchema.arrayItemType !== 'any') {
|
||||
for (let i = 0; i < value.length; i++) {
|
||||
const itemType = typeof value[i];
|
||||
if (itemType !== fieldSchema.arrayItemType) {
|
||||
errors.push(`Item [${itemIndex}].${field}[${i}]: Expected ${fieldSchema.arrayItemType}, got ${itemType}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return errors;
|
||||
}
|
||||
/**
|
||||
* Validate game data payload
|
||||
*/
|
||||
function validateGameData(gameCode, payload) {
|
||||
const errors = [];
|
||||
const warnings = [];
|
||||
// Check game code
|
||||
if (!GameDataHandler_1.GAME_CODES[gameCode]) {
|
||||
errors.push(`Unknown game code: ${gameCode}`);
|
||||
return { valid: false, errors, warnings };
|
||||
}
|
||||
// Check payload structure
|
||||
if (!payload || typeof payload !== 'object') {
|
||||
errors.push('Payload must be an object');
|
||||
return { valid: false, errors, warnings };
|
||||
}
|
||||
// Check data array
|
||||
const items = payload.data || payload.items || payload.questions;
|
||||
if (!items) {
|
||||
errors.push('Missing data array (expected "data", "items", or "questions")');
|
||||
return { valid: false, errors, warnings };
|
||||
}
|
||||
if (!Array.isArray(items)) {
|
||||
errors.push('"data" must be an array');
|
||||
return { valid: false, errors, warnings };
|
||||
}
|
||||
if (items.length === 0) {
|
||||
warnings.push('Data array is empty');
|
||||
}
|
||||
// Validate each item
|
||||
const schema = SCHEMAS[gameCode];
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const itemErrors = validateItem(items[i], schema, i);
|
||||
errors.push(...itemErrors);
|
||||
}
|
||||
// Check for duplicate IDs
|
||||
const ids = items.map((item) => item.id).filter(Boolean);
|
||||
const duplicates = ids.filter((id, index) => ids.indexOf(id) !== index);
|
||||
if (duplicates.length > 0) {
|
||||
warnings.push(`Duplicate IDs found: ${[...new Set(duplicates)].join(', ')}`);
|
||||
}
|
||||
return {
|
||||
valid: errors.length === 0,
|
||||
errors,
|
||||
warnings,
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Get schema for a game code
|
||||
*/
|
||||
function getSchema(gameCode) {
|
||||
return SCHEMAS[gameCode] ?? null;
|
||||
}
|
||||
/**
|
||||
* Get schema documentation for a game code
|
||||
*/
|
||||
function getSchemaDoc(gameCode) {
|
||||
const schema = SCHEMAS[gameCode];
|
||||
if (!schema)
|
||||
return `Unknown game code: ${gameCode}`;
|
||||
const gameInfo = GameDataHandler_1.GAME_CODES[gameCode];
|
||||
const lines = [
|
||||
`## ${gameCode}: ${gameInfo.name}`,
|
||||
`Category: ${gameInfo.category}`,
|
||||
'',
|
||||
'### Fields:',
|
||||
];
|
||||
for (const [field, fieldSchema] of Object.entries(schema)) {
|
||||
const required = fieldSchema.required ? '(required)' : '(optional)';
|
||||
let type = fieldSchema.type;
|
||||
if (fieldSchema.arrayItemType) {
|
||||
type = `${fieldSchema.type}<${fieldSchema.arrayItemType}>`;
|
||||
}
|
||||
lines.push(`- **${field}**: ${type} ${required}`);
|
||||
if (fieldSchema.description) {
|
||||
lines.push(` - ${fieldSchema.description}`);
|
||||
}
|
||||
}
|
||||
return lines.join('\n');
|
||||
}
|
||||
// =============================================================================
|
||||
// DATA VALIDATOR CLASS
|
||||
// =============================================================================
|
||||
class DataValidator {
|
||||
constructor(gameCode) {
|
||||
this.gameCode = gameCode;
|
||||
}
|
||||
validate(payload) {
|
||||
return validateGameData(this.gameCode, payload);
|
||||
}
|
||||
getSchema() {
|
||||
return getSchema(this.gameCode);
|
||||
}
|
||||
getSchemaDoc() {
|
||||
return getSchemaDoc(this.gameCode);
|
||||
}
|
||||
}
|
||||
exports.DataValidator = DataValidator;
|
||||
//# sourceMappingURL=DataValidator.js.map
|
||||
@@ -1,146 +0,0 @@
|
||||
/**
|
||||
* GameClientSDK - SDK dành cho Game Iframe
|
||||
*
|
||||
* Sử dụng trong game để:
|
||||
* - Tự động xác định mode (preview/live) từ URL
|
||||
* - Nhận data từ parent (preview) hoặc fetch API (live)
|
||||
* - Verify answers locally
|
||||
* - Report results về parent
|
||||
*/
|
||||
import { GameCode } from '../kit/GameDataHandler';
|
||||
import { ValidationResult } from './DataValidator';
|
||||
export type ClientMode = 'preview' | 'live' | 'dev';
|
||||
export interface ClientSDKConfig {
|
||||
debug?: boolean;
|
||||
apiBaseUrl?: string;
|
||||
getAuthHeaders?: () => Record<string, string>;
|
||||
}
|
||||
export interface URLParams {
|
||||
mode: ClientMode;
|
||||
gameCode: GameCode;
|
||||
gameId?: string;
|
||||
lid?: string;
|
||||
studentId?: string;
|
||||
}
|
||||
export interface GameDataPayload {
|
||||
game_id: string;
|
||||
game_code: GameCode;
|
||||
data: any[];
|
||||
completed_question_ids?: Array<{
|
||||
id: string;
|
||||
result: 0 | 1;
|
||||
}>;
|
||||
}
|
||||
export interface AnswerResult {
|
||||
isCorrect: boolean;
|
||||
score: number;
|
||||
feedback?: string;
|
||||
}
|
||||
export interface FinalResult {
|
||||
score: number;
|
||||
total: number;
|
||||
correct: number;
|
||||
wrong: number;
|
||||
details: Array<{
|
||||
question_id: string;
|
||||
choice: any;
|
||||
result: 0 | 1;
|
||||
time_spent: number;
|
||||
}>;
|
||||
}
|
||||
export interface ClientSDKEvents {
|
||||
ready: void;
|
||||
dataReceived: {
|
||||
items: any[];
|
||||
resumeData?: any[];
|
||||
validation?: ValidationResult;
|
||||
};
|
||||
error: {
|
||||
message: string;
|
||||
error?: any;
|
||||
};
|
||||
modeDetected: {
|
||||
mode: ClientMode;
|
||||
params: URLParams;
|
||||
};
|
||||
validationError: {
|
||||
validation: ValidationResult;
|
||||
};
|
||||
}
|
||||
type EventHandler<T> = (data: T) => void;
|
||||
declare class SimpleEventEmitter<Events extends Record<string, any>> {
|
||||
private handlers;
|
||||
on<K extends keyof Events>(event: K, handler: EventHandler<Events[K]>): () => void;
|
||||
off<K extends keyof Events>(event: K, handler: EventHandler<Events[K]>): void;
|
||||
protected emit<K extends keyof Events>(event: K, data: Events[K]): void;
|
||||
}
|
||||
export declare class GameClientSDK extends SimpleEventEmitter<ClientSDKEvents> {
|
||||
private config;
|
||||
private params;
|
||||
private mode;
|
||||
private originalItems;
|
||||
private sanitizedItems;
|
||||
private userAnswers;
|
||||
private isInitialized;
|
||||
private startTime;
|
||||
constructor(config?: ClientSDKConfig);
|
||||
/**
|
||||
* Get current mode
|
||||
*/
|
||||
getMode(): ClientMode;
|
||||
/**
|
||||
* Get URL params
|
||||
*/
|
||||
getParams(): URLParams;
|
||||
/**
|
||||
* Get game code
|
||||
*/
|
||||
getGameCode(): GameCode;
|
||||
/**
|
||||
* Get sanitized items (safe for rendering)
|
||||
*/
|
||||
getItems(): any[];
|
||||
/**
|
||||
* Submit an answer and get verification result
|
||||
*/
|
||||
submitAnswer(questionId: string, choice: any): AnswerResult;
|
||||
/**
|
||||
* Get final result
|
||||
*/
|
||||
getFinalResult(): FinalResult;
|
||||
/**
|
||||
* Report final result to parent
|
||||
*/
|
||||
reportFinalResult(result?: FinalResult): void;
|
||||
/**
|
||||
* Request leaderboard from parent
|
||||
*/
|
||||
requestLeaderboard(top?: number): void;
|
||||
/**
|
||||
* Cleanup
|
||||
*/
|
||||
destroy(): void;
|
||||
private parseURLParams;
|
||||
private setupMessageListener;
|
||||
private handleMessage;
|
||||
private initialize;
|
||||
/**
|
||||
* Load mock data for dev mode
|
||||
*/
|
||||
private loadMockData;
|
||||
private sendGameReady;
|
||||
private fetchLiveData;
|
||||
private handleDataReceived;
|
||||
private sendAnswerReport;
|
||||
private log;
|
||||
}
|
||||
/**
|
||||
* Get or create GameClientSDK instance
|
||||
*/
|
||||
export declare function getGameClientSDK(config?: ClientSDKConfig): GameClientSDK;
|
||||
/**
|
||||
* Destroy client instance
|
||||
*/
|
||||
export declare function destroyGameClientSDK(): void;
|
||||
export {};
|
||||
//# sourceMappingURL=GameClientSDK.d.ts.map
|
||||
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"GameClientSDK.d.ts","sourceRoot":"","sources":["../../src/client/GameClientSDK.ts"],"names":[],"mappings":"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"}
|
||||
@@ -1,365 +0,0 @@
|
||||
"use strict";
|
||||
/**
|
||||
* GameClientSDK - SDK dành cho Game Iframe
|
||||
*
|
||||
* Sử dụng trong game để:
|
||||
* - Tự động xác định mode (preview/live) từ URL
|
||||
* - Nhận data từ parent (preview) hoặc fetch API (live)
|
||||
* - Verify answers locally
|
||||
* - Report results về parent
|
||||
*/
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.GameClientSDK = void 0;
|
||||
exports.getGameClientSDK = getGameClientSDK;
|
||||
exports.destroyGameClientSDK = destroyGameClientSDK;
|
||||
const GameDataHandler_1 = require("../kit/GameDataHandler");
|
||||
const MockData_1 = require("./MockData");
|
||||
const DataValidator_1 = require("./DataValidator");
|
||||
class SimpleEventEmitter {
|
||||
constructor() {
|
||||
this.handlers = new Map();
|
||||
}
|
||||
on(event, handler) {
|
||||
if (!this.handlers.has(event)) {
|
||||
this.handlers.set(event, new Set());
|
||||
}
|
||||
this.handlers.get(event).add(handler);
|
||||
return () => this.off(event, handler);
|
||||
}
|
||||
off(event, handler) {
|
||||
this.handlers.get(event)?.delete(handler);
|
||||
}
|
||||
emit(event, data) {
|
||||
this.handlers.get(event)?.forEach(handler => {
|
||||
try {
|
||||
handler(data);
|
||||
}
|
||||
catch (err) {
|
||||
console.error(`[GameClientSDK] Error in ${String(event)} handler:`, err);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
// =============================================================================
|
||||
// GAME CLIENT SDK
|
||||
// =============================================================================
|
||||
class GameClientSDK extends SimpleEventEmitter {
|
||||
constructor(config = {}) {
|
||||
super();
|
||||
// Data storage
|
||||
this.originalItems = new Map(); // Có đáp án
|
||||
this.sanitizedItems = []; // Không có đáp án
|
||||
this.userAnswers = new Map();
|
||||
this.isInitialized = false;
|
||||
this.startTime = 0;
|
||||
this.config = {
|
||||
debug: config.debug ?? false,
|
||||
apiBaseUrl: config.apiBaseUrl ?? '',
|
||||
getAuthHeaders: config.getAuthHeaders ?? (() => ({})),
|
||||
};
|
||||
// Parse URL params
|
||||
this.params = this.parseURLParams();
|
||||
this.mode = this.params.mode;
|
||||
this.log('info', 'SDK created', { mode: this.mode, params: this.params });
|
||||
// Emit mode detected
|
||||
this.emit('modeDetected', { mode: this.mode, params: this.params });
|
||||
// Setup message listener
|
||||
this.setupMessageListener();
|
||||
// Auto-initialize based on mode
|
||||
this.initialize();
|
||||
}
|
||||
// ==========================================================================
|
||||
// PUBLIC API
|
||||
// ==========================================================================
|
||||
/**
|
||||
* Get current mode
|
||||
*/
|
||||
getMode() {
|
||||
return this.mode;
|
||||
}
|
||||
/**
|
||||
* Get URL params
|
||||
*/
|
||||
getParams() {
|
||||
return { ...this.params };
|
||||
}
|
||||
/**
|
||||
* Get game code
|
||||
*/
|
||||
getGameCode() {
|
||||
return this.params.gameCode;
|
||||
}
|
||||
/**
|
||||
* Get sanitized items (safe for rendering)
|
||||
*/
|
||||
getItems() {
|
||||
return this.sanitizedItems;
|
||||
}
|
||||
/**
|
||||
* Submit an answer and get verification result
|
||||
*/
|
||||
submitAnswer(questionId, choice) {
|
||||
const originalItem = this.originalItems.get(questionId);
|
||||
if (!originalItem) {
|
||||
this.log('warn', `Item not found: ${questionId}`);
|
||||
return { isCorrect: false, score: 0, feedback: 'Question not found' };
|
||||
}
|
||||
// Verify using GameDataHandler
|
||||
const result = (0, GameDataHandler_1.checkAnswer)(this.params.gameCode, originalItem, choice);
|
||||
// Store user answer
|
||||
const timeSpent = Date.now() - (this.userAnswers.size === 0 ? this.startTime : Date.now());
|
||||
this.userAnswers.set(questionId, {
|
||||
choice,
|
||||
result: result.isCorrect ? 1 : 0,
|
||||
time: timeSpent,
|
||||
});
|
||||
// Report to parent
|
||||
this.sendAnswerReport(questionId, choice, result.isCorrect ? 1 : 0, timeSpent);
|
||||
this.log('info', `Answer submitted: ${questionId}`, { choice, result });
|
||||
return result;
|
||||
}
|
||||
/**
|
||||
* Get final result
|
||||
*/
|
||||
getFinalResult() {
|
||||
const details = Array.from(this.userAnswers.entries()).map(([id, data]) => ({
|
||||
question_id: id,
|
||||
choice: data.choice,
|
||||
result: data.result,
|
||||
time_spent: data.time,
|
||||
}));
|
||||
const correct = details.filter(d => d.result === 1).length;
|
||||
const total = this.originalItems.size;
|
||||
return {
|
||||
score: total > 0 ? Math.round((correct / total) * 100) : 0,
|
||||
total,
|
||||
correct,
|
||||
wrong: total - correct,
|
||||
details,
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Report final result to parent
|
||||
*/
|
||||
reportFinalResult(result) {
|
||||
const finalResult = result ?? this.getFinalResult();
|
||||
window.parent.postMessage({
|
||||
type: 'FINAL_RESULT',
|
||||
data: finalResult,
|
||||
}, '*');
|
||||
this.log('info', 'Final result reported', finalResult);
|
||||
}
|
||||
/**
|
||||
* Request leaderboard from parent
|
||||
*/
|
||||
requestLeaderboard(top = 10) {
|
||||
window.parent.postMessage({
|
||||
type: 'GET_LEADERBOARD',
|
||||
data: { top },
|
||||
}, '*');
|
||||
}
|
||||
/**
|
||||
* Cleanup
|
||||
*/
|
||||
destroy() {
|
||||
window.removeEventListener('message', this.handleMessage);
|
||||
this.originalItems.clear();
|
||||
this.sanitizedItems = [];
|
||||
this.userAnswers.clear();
|
||||
this.log('info', 'SDK destroyed');
|
||||
}
|
||||
// ==========================================================================
|
||||
// PRIVATE METHODS
|
||||
// ==========================================================================
|
||||
parseURLParams() {
|
||||
const searchParams = new URLSearchParams(window.location.search);
|
||||
const mode = (searchParams.get('mode') || 'preview');
|
||||
const gameCode = (searchParams.get('game_code') || 'G001');
|
||||
const gameId = searchParams.get('game_id') || undefined;
|
||||
const lid = searchParams.get('lid') || undefined;
|
||||
const studentId = searchParams.get('student_id') || undefined;
|
||||
// Validate mode
|
||||
if (mode !== 'preview' && mode !== 'live' && mode !== 'dev') {
|
||||
this.log('warn', `Invalid mode: ${mode}, defaulting to preview`);
|
||||
}
|
||||
// Validate game code
|
||||
if (!GameDataHandler_1.GAME_CODES[gameCode]) {
|
||||
this.log('warn', `Unknown game code: ${gameCode}`);
|
||||
}
|
||||
return { mode, gameCode, gameId, lid, studentId };
|
||||
}
|
||||
setupMessageListener() {
|
||||
this.handleMessage = this.handleMessage.bind(this);
|
||||
window.addEventListener('message', this.handleMessage);
|
||||
}
|
||||
handleMessage(event) {
|
||||
const { type, jsonData, leaderboardData } = event.data || {};
|
||||
this.log('debug', 'Message received', { type, hasData: !!jsonData });
|
||||
switch (type) {
|
||||
case 'SERVER_PUSH_DATA':
|
||||
if (jsonData) {
|
||||
this.handleDataReceived(jsonData);
|
||||
}
|
||||
break;
|
||||
case 'SERVER_PUSH_LEADERBOARD':
|
||||
if (leaderboardData) {
|
||||
this.log('info', 'Leaderboard received', leaderboardData);
|
||||
// Could emit event here
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
async initialize() {
|
||||
// Send GAME_READY immediately
|
||||
this.sendGameReady();
|
||||
if (this.mode === 'dev') {
|
||||
// Dev mode: load mock data immediately
|
||||
this.log('info', 'DEV MODE: Loading mock data...');
|
||||
this.loadMockData();
|
||||
}
|
||||
else if (this.mode === 'live') {
|
||||
// Live mode: fetch data from API
|
||||
await this.fetchLiveData();
|
||||
}
|
||||
else {
|
||||
// Preview mode: wait for postMessage
|
||||
this.log('info', 'Preview mode: waiting for SERVER_PUSH_DATA...');
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Load mock data for dev mode
|
||||
*/
|
||||
loadMockData() {
|
||||
const mockData = (0, MockData_1.getMockData)(this.params.gameCode);
|
||||
if (!mockData) {
|
||||
this.emit('error', {
|
||||
message: `No mock data available for game code: ${this.params.gameCode}`
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.log('info', `Loaded mock data for ${this.params.gameCode}`);
|
||||
this.handleDataReceived(mockData);
|
||||
}
|
||||
sendGameReady() {
|
||||
window.parent.postMessage({ type: 'GAME_READY' }, '*');
|
||||
this.emit('ready', undefined);
|
||||
this.log('info', 'GAME_READY sent');
|
||||
}
|
||||
async fetchLiveData() {
|
||||
const { gameId, lid } = this.params;
|
||||
if (!gameId || !lid) {
|
||||
this.emit('error', { message: 'Live mode requires game_id and lid' });
|
||||
return;
|
||||
}
|
||||
if (!this.config.apiBaseUrl) {
|
||||
this.emit('error', { message: 'Live mode requires apiBaseUrl' });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const url = `${this.config.apiBaseUrl}/games/${gameId}?lid=${lid}`;
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
...this.config.getAuthHeaders(),
|
||||
};
|
||||
this.log('info', `Fetching live data: ${url}`);
|
||||
const response = await fetch(url, { headers });
|
||||
if (!response.ok) {
|
||||
throw new Error(`API Error: ${response.status}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
this.handleDataReceived(data);
|
||||
}
|
||||
catch (error) {
|
||||
this.log('error', 'Failed to fetch live data', error);
|
||||
this.emit('error', { message: 'Failed to fetch game data', error });
|
||||
}
|
||||
}
|
||||
handleDataReceived(payload) {
|
||||
this.startTime = Date.now();
|
||||
// Update game code if provided
|
||||
if (payload.game_code && GameDataHandler_1.GAME_CODES[payload.game_code]) {
|
||||
this.params.gameCode = payload.game_code;
|
||||
}
|
||||
// Validate data structure
|
||||
const validation = (0, DataValidator_1.validateGameData)(this.params.gameCode, payload);
|
||||
if (!validation.valid) {
|
||||
this.log('error', 'Data validation failed', validation.errors);
|
||||
this.emit('validationError', { validation });
|
||||
// Continue anyway to allow partial rendering
|
||||
}
|
||||
if (validation.warnings.length > 0) {
|
||||
this.log('warn', 'Data validation warnings', validation.warnings);
|
||||
}
|
||||
// Extract items from various payload formats
|
||||
const items = payload.data || payload.items || payload.questions || [];
|
||||
const resumeData = payload.completed_question_ids || [];
|
||||
// Store original items (with answers)
|
||||
this.originalItems.clear();
|
||||
items.forEach((item) => {
|
||||
if (item.id) {
|
||||
this.originalItems.set(item.id, item);
|
||||
}
|
||||
});
|
||||
// Sanitize for client (remove answers)
|
||||
this.sanitizedItems = (0, GameDataHandler_1.sanitizeForClient)(this.params.gameCode, items);
|
||||
this.isInitialized = true;
|
||||
this.log('info', `Data received: ${items.length} items, ${resumeData.length} completed`);
|
||||
// Emit event with validation result
|
||||
this.emit('dataReceived', {
|
||||
items: this.sanitizedItems,
|
||||
resumeData,
|
||||
validation,
|
||||
});
|
||||
}
|
||||
sendAnswerReport(questionId, choice, result, timeSpent) {
|
||||
window.parent.postMessage({
|
||||
type: 'ANSWER_REPORT',
|
||||
data: {
|
||||
question_id: questionId,
|
||||
question_index: Array.from(this.originalItems.keys()).indexOf(questionId),
|
||||
choice,
|
||||
result,
|
||||
time_spent: timeSpent,
|
||||
},
|
||||
}, '*');
|
||||
}
|
||||
log(level, message, data) {
|
||||
if (!this.config.debug && level === 'debug')
|
||||
return;
|
||||
const prefix = `[GameClientSDK:${this.mode}]`;
|
||||
switch (level) {
|
||||
case 'debug':
|
||||
case 'info':
|
||||
console.log(prefix, message, data ?? '');
|
||||
break;
|
||||
case 'warn':
|
||||
console.warn(prefix, message, data ?? '');
|
||||
break;
|
||||
case 'error':
|
||||
console.error(prefix, message, data ?? '');
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
exports.GameClientSDK = GameClientSDK;
|
||||
// =============================================================================
|
||||
// FACTORY
|
||||
// =============================================================================
|
||||
let clientInstance = null;
|
||||
/**
|
||||
* Get or create GameClientSDK instance
|
||||
*/
|
||||
function getGameClientSDK(config) {
|
||||
if (!clientInstance) {
|
||||
clientInstance = new GameClientSDK(config);
|
||||
}
|
||||
return clientInstance;
|
||||
}
|
||||
/**
|
||||
* Destroy client instance
|
||||
*/
|
||||
function destroyGameClientSDK() {
|
||||
clientInstance?.destroy();
|
||||
clientInstance = null;
|
||||
}
|
||||
//# sourceMappingURL=GameClientSDK.js.map
|
||||
182
G102-sequence/sdk/package/dist/client/MockData.d.ts
vendored
@@ -1,182 +0,0 @@
|
||||
/**
|
||||
* Mock Data cho từng Game Code
|
||||
* Game developers dùng để test game standalone
|
||||
*
|
||||
* Usage:
|
||||
* ```typescript
|
||||
* import { MockData } from 'game-iframe-sdk/client';
|
||||
*
|
||||
* // Lấy sample data cho Quiz Text-Text
|
||||
* const quizData = MockData.G001;
|
||||
*
|
||||
* // Lấy sample data cho Sequence Word
|
||||
* const seqData = MockData.G110;
|
||||
* ```
|
||||
*/
|
||||
import { GameCode } from '../kit/GameDataHandler';
|
||||
/** G001: Quiz Text-Text */
|
||||
export declare const MOCK_G001: {
|
||||
game_code: GameCode;
|
||||
game_id: string;
|
||||
data: {
|
||||
id: string;
|
||||
question: string;
|
||||
options: string[];
|
||||
answer: number;
|
||||
}[];
|
||||
};
|
||||
/** G002: Quiz Audio-Text */
|
||||
export declare const MOCK_G002: {
|
||||
game_code: GameCode;
|
||||
game_id: string;
|
||||
data: {
|
||||
id: string;
|
||||
question_audio: string;
|
||||
options: string[];
|
||||
answer: number;
|
||||
}[];
|
||||
};
|
||||
/** G003: Quiz Text-Audio */
|
||||
export declare const MOCK_G003: {
|
||||
game_code: GameCode;
|
||||
game_id: string;
|
||||
data: {
|
||||
id: string;
|
||||
question: string;
|
||||
options: string[];
|
||||
answer: number;
|
||||
}[];
|
||||
};
|
||||
/** G004: Quiz Image-Text */
|
||||
export declare const MOCK_G004: {
|
||||
game_code: GameCode;
|
||||
game_id: string;
|
||||
data: ({
|
||||
id: string;
|
||||
question_image: string;
|
||||
question: string;
|
||||
options: string[];
|
||||
answer: number;
|
||||
} | {
|
||||
id: string;
|
||||
question_image: string;
|
||||
options: string[];
|
||||
answer: number;
|
||||
question?: undefined;
|
||||
})[];
|
||||
};
|
||||
/** G005: Quiz Text-Image */
|
||||
export declare const MOCK_G005: {
|
||||
game_code: GameCode;
|
||||
game_id: string;
|
||||
data: {
|
||||
id: string;
|
||||
question: string;
|
||||
options: string[];
|
||||
answer: number;
|
||||
}[];
|
||||
};
|
||||
/** G110: Sequence Word - no audio */
|
||||
export declare const MOCK_G110: {
|
||||
game_code: GameCode;
|
||||
game_id: string;
|
||||
data: {
|
||||
id: string;
|
||||
word: string;
|
||||
parts: string[];
|
||||
answer: string[];
|
||||
}[];
|
||||
};
|
||||
/** G111: Sequence Word - audio, hide 2 */
|
||||
export declare const MOCK_G111: {
|
||||
game_code: GameCode;
|
||||
game_id: string;
|
||||
data: {
|
||||
id: string;
|
||||
word: string;
|
||||
parts: string[];
|
||||
answer: string[];
|
||||
audio_url: string;
|
||||
}[];
|
||||
};
|
||||
/** G112: Sequence Word - audio, hide 4 */
|
||||
export declare const MOCK_G112: {
|
||||
game_code: GameCode;
|
||||
game_id: string;
|
||||
data: {
|
||||
id: string;
|
||||
word: string;
|
||||
parts: string[];
|
||||
answer: string[];
|
||||
audio_url: string;
|
||||
}[];
|
||||
};
|
||||
/** G113: Sequence Word - audio, hide all */
|
||||
export declare const MOCK_G113: {
|
||||
game_code: GameCode;
|
||||
game_id: string;
|
||||
data: {
|
||||
id: string;
|
||||
word: string;
|
||||
parts: string[];
|
||||
answer: string[];
|
||||
audio_url: string;
|
||||
}[];
|
||||
};
|
||||
/** G120: Sequence Sentence - no audio */
|
||||
export declare const MOCK_G120: {
|
||||
game_code: GameCode;
|
||||
game_id: string;
|
||||
data: {
|
||||
id: string;
|
||||
sentence: string;
|
||||
parts: string[];
|
||||
answer: string[];
|
||||
}[];
|
||||
};
|
||||
/** G121: Sequence Sentence - audio, hide 2 */
|
||||
export declare const MOCK_G121: {
|
||||
game_code: GameCode;
|
||||
game_id: string;
|
||||
data: {
|
||||
id: string;
|
||||
sentence: string;
|
||||
parts: string[];
|
||||
answer: string[];
|
||||
audio_url: string;
|
||||
}[];
|
||||
};
|
||||
/** G122: Sequence Sentence - audio, hide 4 */
|
||||
export declare const MOCK_G122: {
|
||||
game_code: GameCode;
|
||||
game_id: string;
|
||||
data: {
|
||||
id: string;
|
||||
sentence: string;
|
||||
parts: string[];
|
||||
answer: string[];
|
||||
audio_url: string;
|
||||
}[];
|
||||
};
|
||||
/** G123: Sequence Sentence - audio, hide all */
|
||||
export declare const MOCK_G123: {
|
||||
game_code: GameCode;
|
||||
game_id: string;
|
||||
data: {
|
||||
id: string;
|
||||
sentence: string;
|
||||
parts: string[];
|
||||
answer: string[];
|
||||
audio_url: string;
|
||||
}[];
|
||||
};
|
||||
export declare const MockData: Record<GameCode, any>;
|
||||
/**
|
||||
* Get mock data for a game code
|
||||
*/
|
||||
export declare function getMockData(code: GameCode): any;
|
||||
/**
|
||||
* Get all available game codes
|
||||
*/
|
||||
export declare function getAvailableGameCodes(): GameCode[];
|
||||
//# sourceMappingURL=MockData.d.ts.map
|
||||
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"MockData.d.ts","sourceRoot":"","sources":["../../src/client/MockData.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AAMlD,2BAA2B;AAC3B,eAAO,MAAM,SAAS;eACG,QAAQ;;;;;;;;CAsBhC,CAAC;AAEF,4BAA4B;AAC5B,eAAO,MAAM,SAAS;eACG,QAAQ;;;;;;;;CAgBhC,CAAC;AAEF,4BAA4B;AAC5B,eAAO,MAAM,SAAS;eACG,QAAQ;;;;;;;;CAchC,CAAC;AAEF,4BAA4B;AAC5B,eAAO,MAAM,SAAS;eACG,QAAQ;;;;;;;;;;;;;;;CAiBhC,CAAC;AAEF,4BAA4B;AAC5B,eAAO,MAAM,SAAS;eACG,QAAQ;;;;;;;;CAchC,CAAC;AAMF,qCAAqC;AACrC,eAAO,MAAM,SAAS;eACG,QAAQ;;;;;;;;CAsBhC,CAAC;AAEF,0CAA0C;AAC1C,eAAO,MAAM,SAAS;eACG,QAAQ;;;;;;;;;CAWhC,CAAC;AAEF,0CAA0C;AAC1C,eAAO,MAAM,SAAS;eACG,QAAQ;;;;;;;;;CAWhC,CAAC;AAEF,4CAA4C;AAC5C,eAAO,MAAM,SAAS;eACG,QAAQ;;;;;;;;;CAWhC,CAAC;AAMF,yCAAyC;AACzC,eAAO,MAAM,SAAS;eACG,QAAQ;;;;;;;;CAgBhC,CAAC;AAEF,8CAA8C;AAC9C,eAAO,MAAM,SAAS;eACG,QAAQ;;;;;;;;;CAWhC,CAAC;AAEF,8CAA8C;AAC9C,eAAO,MAAM,SAAS;eACG,QAAQ;;;;;;;;;CAWhC,CAAC;AAEF,gDAAgD;AAChD,eAAO,MAAM,SAAS;eACG,QAAQ;;;;;;;;;CAWhC,CAAC;AAMF,eAAO,MAAM,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE,GAAG,CAiB1C,CAAC;AAEF;;GAEG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,QAAQ,GAAG,GAAG,CAE/C;AAED;;GAEG;AACH,wBAAgB,qBAAqB,IAAI,QAAQ,EAAE,CAElD"}
|
||||
289
G102-sequence/sdk/package/dist/client/MockData.js
vendored
@@ -1,289 +0,0 @@
|
||||
"use strict";
|
||||
/**
|
||||
* Mock Data cho từng Game Code
|
||||
* Game developers dùng để test game standalone
|
||||
*
|
||||
* Usage:
|
||||
* ```typescript
|
||||
* import { MockData } from 'game-iframe-sdk/client';
|
||||
*
|
||||
* // Lấy sample data cho Quiz Text-Text
|
||||
* const quizData = MockData.G001;
|
||||
*
|
||||
* // Lấy sample data cho Sequence Word
|
||||
* const seqData = MockData.G110;
|
||||
* ```
|
||||
*/
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.MockData = exports.MOCK_G123 = exports.MOCK_G122 = exports.MOCK_G121 = exports.MOCK_G120 = exports.MOCK_G113 = exports.MOCK_G112 = exports.MOCK_G111 = exports.MOCK_G110 = exports.MOCK_G005 = exports.MOCK_G004 = exports.MOCK_G003 = exports.MOCK_G002 = exports.MOCK_G001 = void 0;
|
||||
exports.getMockData = getMockData;
|
||||
exports.getAvailableGameCodes = getAvailableGameCodes;
|
||||
// =============================================================================
|
||||
// QUIZ MOCK DATA
|
||||
// =============================================================================
|
||||
/** G001: Quiz Text-Text */
|
||||
exports.MOCK_G001 = {
|
||||
game_code: 'G001',
|
||||
game_id: 'mock-quiz-text-text',
|
||||
data: [
|
||||
{
|
||||
id: 'q1',
|
||||
question: 'Thủ đô của Việt Nam là gì?',
|
||||
options: ['Hà Nội', 'Hồ Chí Minh', 'Đà Nẵng', 'Huế'],
|
||||
answer: 0, // Index của đáp án đúng
|
||||
},
|
||||
{
|
||||
id: 'q2',
|
||||
question: '2 + 2 = ?',
|
||||
options: ['3', '4', '5', '6'],
|
||||
answer: 1,
|
||||
},
|
||||
{
|
||||
id: 'q3',
|
||||
question: 'Con vật nào biết bay?',
|
||||
options: ['Chó', 'Mèo', 'Chim', 'Cá'],
|
||||
answer: 2,
|
||||
},
|
||||
],
|
||||
};
|
||||
/** G002: Quiz Audio-Text */
|
||||
exports.MOCK_G002 = {
|
||||
game_code: 'G002',
|
||||
game_id: 'mock-quiz-audio-text',
|
||||
data: [
|
||||
{
|
||||
id: 'q1',
|
||||
question_audio: 'https://example.com/audio/question1.mp3',
|
||||
options: ['Apple', 'Banana', 'Orange', 'Grape'],
|
||||
answer: 0,
|
||||
},
|
||||
{
|
||||
id: 'q2',
|
||||
question_audio: 'https://example.com/audio/question2.mp3',
|
||||
options: ['Dog', 'Cat', 'Bird', 'Fish'],
|
||||
answer: 2,
|
||||
},
|
||||
],
|
||||
};
|
||||
/** G003: Quiz Text-Audio */
|
||||
exports.MOCK_G003 = {
|
||||
game_code: 'G003',
|
||||
game_id: 'mock-quiz-text-audio',
|
||||
data: [
|
||||
{
|
||||
id: 'q1',
|
||||
question: 'Chọn phát âm đúng của từ "Hello"',
|
||||
options: [
|
||||
'https://example.com/audio/hello1.mp3',
|
||||
'https://example.com/audio/hello2.mp3',
|
||||
'https://example.com/audio/hello3.mp3',
|
||||
],
|
||||
answer: 0,
|
||||
},
|
||||
],
|
||||
};
|
||||
/** G004: Quiz Image-Text */
|
||||
exports.MOCK_G004 = {
|
||||
game_code: 'G004',
|
||||
game_id: 'mock-quiz-image-text',
|
||||
data: [
|
||||
{
|
||||
id: 'q1',
|
||||
question_image: 'https://example.com/images/apple.jpg',
|
||||
question: 'Đây là quả gì?', // Optional hint
|
||||
options: ['Táo', 'Cam', 'Chuối', 'Nho'],
|
||||
answer: 0,
|
||||
},
|
||||
{
|
||||
id: 'q2',
|
||||
question_image: 'https://example.com/images/cat.jpg',
|
||||
options: ['Chó', 'Mèo', 'Thỏ', 'Chuột'],
|
||||
answer: 1,
|
||||
},
|
||||
],
|
||||
};
|
||||
/** G005: Quiz Text-Image */
|
||||
exports.MOCK_G005 = {
|
||||
game_code: 'G005',
|
||||
game_id: 'mock-quiz-text-image',
|
||||
data: [
|
||||
{
|
||||
id: 'q1',
|
||||
question: 'Chọn hình ảnh con mèo',
|
||||
options: [
|
||||
'https://example.com/images/dog.jpg',
|
||||
'https://example.com/images/cat.jpg',
|
||||
'https://example.com/images/bird.jpg',
|
||||
],
|
||||
answer: 1,
|
||||
},
|
||||
],
|
||||
};
|
||||
// =============================================================================
|
||||
// SEQUENCE WORD MOCK DATA
|
||||
// =============================================================================
|
||||
/** G110: Sequence Word - no audio */
|
||||
exports.MOCK_G110 = {
|
||||
game_code: 'G110',
|
||||
game_id: 'mock-sequence-word',
|
||||
data: [
|
||||
{
|
||||
id: 'sw1',
|
||||
word: 'APPLE',
|
||||
parts: ['A', 'P', 'P', 'L', 'E'], // Đáp án đúng theo thứ tự
|
||||
answer: ['A', 'P', 'P', 'L', 'E'], // SDK sẽ shuffle parts, giữ answer để verify
|
||||
},
|
||||
{
|
||||
id: 'sw2',
|
||||
word: 'HELLO',
|
||||
parts: ['H', 'E', 'L', 'L', 'O'],
|
||||
answer: ['H', 'E', 'L', 'L', 'O'],
|
||||
},
|
||||
{
|
||||
id: 'sw3',
|
||||
word: 'WORLD',
|
||||
parts: ['W', 'O', 'R', 'L', 'D'],
|
||||
answer: ['W', 'O', 'R', 'L', 'D'],
|
||||
},
|
||||
],
|
||||
};
|
||||
/** G111: Sequence Word - audio, hide 2 */
|
||||
exports.MOCK_G111 = {
|
||||
game_code: 'G111',
|
||||
game_id: 'mock-sequence-word-audio-2',
|
||||
data: [
|
||||
{
|
||||
id: 'sw1',
|
||||
word: 'BANANA',
|
||||
parts: ['B', 'A', 'N', 'A', 'N', 'A'],
|
||||
answer: ['B', 'A', 'N', 'A', 'N', 'A'],
|
||||
audio_url: 'https://example.com/audio/banana.mp3',
|
||||
},
|
||||
],
|
||||
};
|
||||
/** G112: Sequence Word - audio, hide 4 */
|
||||
exports.MOCK_G112 = {
|
||||
game_code: 'G112',
|
||||
game_id: 'mock-sequence-word-audio-4',
|
||||
data: [
|
||||
{
|
||||
id: 'sw1',
|
||||
word: 'COMPUTER',
|
||||
parts: ['C', 'O', 'M', 'P', 'U', 'T', 'E', 'R'],
|
||||
answer: ['C', 'O', 'M', 'P', 'U', 'T', 'E', 'R'],
|
||||
audio_url: 'https://example.com/audio/computer.mp3',
|
||||
},
|
||||
],
|
||||
};
|
||||
/** G113: Sequence Word - audio, hide all */
|
||||
exports.MOCK_G113 = {
|
||||
game_code: 'G113',
|
||||
game_id: 'mock-sequence-word-audio-all',
|
||||
data: [
|
||||
{
|
||||
id: 'sw1',
|
||||
word: 'ELEPHANT',
|
||||
parts: ['E', 'L', 'E', 'P', 'H', 'A', 'N', 'T'],
|
||||
answer: ['E', 'L', 'E', 'P', 'H', 'A', 'N', 'T'],
|
||||
audio_url: 'https://example.com/audio/elephant.mp3',
|
||||
},
|
||||
],
|
||||
};
|
||||
// =============================================================================
|
||||
// SEQUENCE SENTENCE MOCK DATA
|
||||
// =============================================================================
|
||||
/** G120: Sequence Sentence - no audio */
|
||||
exports.MOCK_G120 = {
|
||||
game_code: 'G120',
|
||||
game_id: 'mock-sequence-sentence',
|
||||
data: [
|
||||
{
|
||||
id: 'ss1',
|
||||
sentence: 'I love learning English.',
|
||||
parts: ['I', 'love', 'learning', 'English.'],
|
||||
answer: ['I', 'love', 'learning', 'English.'],
|
||||
},
|
||||
{
|
||||
id: 'ss2',
|
||||
sentence: 'The cat is sleeping.',
|
||||
parts: ['The', 'cat', 'is', 'sleeping.'],
|
||||
answer: ['The', 'cat', 'is', 'sleeping.'],
|
||||
},
|
||||
],
|
||||
};
|
||||
/** G121: Sequence Sentence - audio, hide 2 */
|
||||
exports.MOCK_G121 = {
|
||||
game_code: 'G121',
|
||||
game_id: 'mock-sequence-sentence-audio-2',
|
||||
data: [
|
||||
{
|
||||
id: 'ss1',
|
||||
sentence: 'She goes to school every day.',
|
||||
parts: ['She', 'goes', 'to', 'school', 'every', 'day.'],
|
||||
answer: ['She', 'goes', 'to', 'school', 'every', 'day.'],
|
||||
audio_url: 'https://example.com/audio/sentence1.mp3',
|
||||
},
|
||||
],
|
||||
};
|
||||
/** G122: Sequence Sentence - audio, hide 4 */
|
||||
exports.MOCK_G122 = {
|
||||
game_code: 'G122',
|
||||
game_id: 'mock-sequence-sentence-audio-4',
|
||||
data: [
|
||||
{
|
||||
id: 'ss1',
|
||||
sentence: 'My brother plays football in the park.',
|
||||
parts: ['My', 'brother', 'plays', 'football', 'in', 'the', 'park.'],
|
||||
answer: ['My', 'brother', 'plays', 'football', 'in', 'the', 'park.'],
|
||||
audio_url: 'https://example.com/audio/sentence2.mp3',
|
||||
},
|
||||
],
|
||||
};
|
||||
/** G123: Sequence Sentence - audio, hide all */
|
||||
exports.MOCK_G123 = {
|
||||
game_code: 'G123',
|
||||
game_id: 'mock-sequence-sentence-audio-all',
|
||||
data: [
|
||||
{
|
||||
id: 'ss1',
|
||||
sentence: 'The quick brown fox jumps over the lazy dog.',
|
||||
parts: ['The', 'quick', 'brown', 'fox', 'jumps', 'over', 'the', 'lazy', 'dog.'],
|
||||
answer: ['The', 'quick', 'brown', 'fox', 'jumps', 'over', 'the', 'lazy', 'dog.'],
|
||||
audio_url: 'https://example.com/audio/sentence3.mp3',
|
||||
},
|
||||
],
|
||||
};
|
||||
// =============================================================================
|
||||
// MOCK DATA MAP
|
||||
// =============================================================================
|
||||
exports.MockData = {
|
||||
// Quiz
|
||||
G001: exports.MOCK_G001,
|
||||
G002: exports.MOCK_G002,
|
||||
G003: exports.MOCK_G003,
|
||||
G004: exports.MOCK_G004,
|
||||
// Sequence Word
|
||||
G110: exports.MOCK_G110,
|
||||
G111: exports.MOCK_G111,
|
||||
G112: exports.MOCK_G112,
|
||||
G113: exports.MOCK_G113,
|
||||
// Sequence Sentence
|
||||
G120: exports.MOCK_G120,
|
||||
G121: exports.MOCK_G121,
|
||||
G122: exports.MOCK_G122,
|
||||
G123: exports.MOCK_G123,
|
||||
G005: exports.MOCK_G005,
|
||||
};
|
||||
/**
|
||||
* Get mock data for a game code
|
||||
*/
|
||||
function getMockData(code) {
|
||||
return exports.MockData[code] ?? null;
|
||||
}
|
||||
/**
|
||||
* Get all available game codes
|
||||
*/
|
||||
function getAvailableGameCodes() {
|
||||
return Object.keys(exports.MockData);
|
||||
}
|
||||
//# sourceMappingURL=MockData.js.map
|
||||
@@ -1,8 +0,0 @@
|
||||
/**
|
||||
* Client SDK exports
|
||||
* SDK dành cho game developers sử dụng trong game iframe
|
||||
*/
|
||||
export { GameClientSDK, getGameClientSDK, destroyGameClientSDK, type ClientMode, type ClientSDKConfig, type URLParams, type GameDataPayload, type AnswerResult, type FinalResult, type ClientSDKEvents, } from './GameClientSDK';
|
||||
export { MockData, getMockData, getAvailableGameCodes, MOCK_G001, MOCK_G002, MOCK_G003, MOCK_G004, MOCK_G110, MOCK_G111, MOCK_G112, MOCK_G113, MOCK_G120, MOCK_G121, MOCK_G122, MOCK_G123, } from './MockData';
|
||||
export { validateGameData, getSchema, getSchemaDoc, DataValidator, type ValidationResult, type FieldSchema, type ItemSchema, } from './DataValidator';
|
||||
//# sourceMappingURL=index.d.ts.map
|
||||
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EACH,aAAa,EACb,gBAAgB,EAChB,oBAAoB,EACpB,KAAK,UAAU,EACf,KAAK,eAAe,EACpB,KAAK,SAAS,EACd,KAAK,eAAe,EACpB,KAAK,YAAY,EACjB,KAAK,WAAW,EAChB,KAAK,eAAe,GACvB,MAAM,iBAAiB,CAAC;AAGzB,OAAO,EACH,QAAQ,EACR,WAAW,EACX,qBAAqB,EACrB,SAAS,EACT,SAAS,EACT,SAAS,EACT,SAAS,EACT,SAAS,EACT,SAAS,EACT,SAAS,EACT,SAAS,EACT,SAAS,EACT,SAAS,EACT,SAAS,EACT,SAAS,GACZ,MAAM,YAAY,CAAC;AAGpB,OAAO,EACH,gBAAgB,EAChB,SAAS,EACT,YAAY,EACZ,aAAa,EACb,KAAK,gBAAgB,EACrB,KAAK,WAAW,EAChB,KAAK,UAAU,GAClB,MAAM,iBAAiB,CAAC"}
|
||||
35
G102-sequence/sdk/package/dist/client/index.js
vendored
@@ -1,35 +0,0 @@
|
||||
"use strict";
|
||||
/**
|
||||
* Client SDK exports
|
||||
* SDK dành cho game developers sử dụng trong game iframe
|
||||
*/
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.DataValidator = exports.getSchemaDoc = exports.getSchema = exports.validateGameData = exports.MOCK_G123 = exports.MOCK_G122 = exports.MOCK_G121 = exports.MOCK_G120 = exports.MOCK_G113 = exports.MOCK_G112 = exports.MOCK_G111 = exports.MOCK_G110 = exports.MOCK_G004 = exports.MOCK_G003 = exports.MOCK_G002 = exports.MOCK_G001 = exports.getAvailableGameCodes = exports.getMockData = exports.MockData = exports.destroyGameClientSDK = exports.getGameClientSDK = exports.GameClientSDK = void 0;
|
||||
var GameClientSDK_1 = require("./GameClientSDK");
|
||||
Object.defineProperty(exports, "GameClientSDK", { enumerable: true, get: function () { return GameClientSDK_1.GameClientSDK; } });
|
||||
Object.defineProperty(exports, "getGameClientSDK", { enumerable: true, get: function () { return GameClientSDK_1.getGameClientSDK; } });
|
||||
Object.defineProperty(exports, "destroyGameClientSDK", { enumerable: true, get: function () { return GameClientSDK_1.destroyGameClientSDK; } });
|
||||
// Mock Data - sample data cho từng game code
|
||||
var MockData_1 = require("./MockData");
|
||||
Object.defineProperty(exports, "MockData", { enumerable: true, get: function () { return MockData_1.MockData; } });
|
||||
Object.defineProperty(exports, "getMockData", { enumerable: true, get: function () { return MockData_1.getMockData; } });
|
||||
Object.defineProperty(exports, "getAvailableGameCodes", { enumerable: true, get: function () { return MockData_1.getAvailableGameCodes; } });
|
||||
Object.defineProperty(exports, "MOCK_G001", { enumerable: true, get: function () { return MockData_1.MOCK_G001; } });
|
||||
Object.defineProperty(exports, "MOCK_G002", { enumerable: true, get: function () { return MockData_1.MOCK_G002; } });
|
||||
Object.defineProperty(exports, "MOCK_G003", { enumerable: true, get: function () { return MockData_1.MOCK_G003; } });
|
||||
Object.defineProperty(exports, "MOCK_G004", { enumerable: true, get: function () { return MockData_1.MOCK_G004; } });
|
||||
Object.defineProperty(exports, "MOCK_G110", { enumerable: true, get: function () { return MockData_1.MOCK_G110; } });
|
||||
Object.defineProperty(exports, "MOCK_G111", { enumerable: true, get: function () { return MockData_1.MOCK_G111; } });
|
||||
Object.defineProperty(exports, "MOCK_G112", { enumerable: true, get: function () { return MockData_1.MOCK_G112; } });
|
||||
Object.defineProperty(exports, "MOCK_G113", { enumerable: true, get: function () { return MockData_1.MOCK_G113; } });
|
||||
Object.defineProperty(exports, "MOCK_G120", { enumerable: true, get: function () { return MockData_1.MOCK_G120; } });
|
||||
Object.defineProperty(exports, "MOCK_G121", { enumerable: true, get: function () { return MockData_1.MOCK_G121; } });
|
||||
Object.defineProperty(exports, "MOCK_G122", { enumerable: true, get: function () { return MockData_1.MOCK_G122; } });
|
||||
Object.defineProperty(exports, "MOCK_G123", { enumerable: true, get: function () { return MockData_1.MOCK_G123; } });
|
||||
// Data Validator - verify data structure
|
||||
var DataValidator_1 = require("./DataValidator");
|
||||
Object.defineProperty(exports, "validateGameData", { enumerable: true, get: function () { return DataValidator_1.validateGameData; } });
|
||||
Object.defineProperty(exports, "getSchema", { enumerable: true, get: function () { return DataValidator_1.getSchema; } });
|
||||
Object.defineProperty(exports, "getSchemaDoc", { enumerable: true, get: function () { return DataValidator_1.getSchemaDoc; } });
|
||||
Object.defineProperty(exports, "DataValidator", { enumerable: true, get: function () { return DataValidator_1.DataValidator; } });
|
||||
//# sourceMappingURL=index.js.map
|
||||
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":";AAAA;;;GAGG;;;AAEH,iDAWyB;AAVrB,8GAAA,aAAa,OAAA;AACb,iHAAA,gBAAgB,OAAA;AAChB,qHAAA,oBAAoB,OAAA;AAUxB,6CAA6C;AAC7C,uCAgBoB;AAfhB,oGAAA,QAAQ,OAAA;AACR,uGAAA,WAAW,OAAA;AACX,iHAAA,qBAAqB,OAAA;AACrB,qGAAA,SAAS,OAAA;AACT,qGAAA,SAAS,OAAA;AACT,qGAAA,SAAS,OAAA;AACT,qGAAA,SAAS,OAAA;AACT,qGAAA,SAAS,OAAA;AACT,qGAAA,SAAS,OAAA;AACT,qGAAA,SAAS,OAAA;AACT,qGAAA,SAAS,OAAA;AACT,qGAAA,SAAS,OAAA;AACT,qGAAA,SAAS,OAAA;AACT,qGAAA,SAAS,OAAA;AACT,qGAAA,SAAS,OAAA;AAGb,yCAAyC;AACzC,iDAQyB;AAPrB,iHAAA,gBAAgB,OAAA;AAChB,0GAAA,SAAS,OAAA;AACT,6GAAA,YAAY,OAAA;AACZ,8GAAA,aAAa,OAAA"}
|
||||
@@ -1,61 +0,0 @@
|
||||
/**
|
||||
* Game Iframe SDK - Event Emitter
|
||||
* Simple typed event emitter for SDK
|
||||
*/
|
||||
export class EventEmitter {
|
||||
constructor() {
|
||||
this.handlers = new Map();
|
||||
}
|
||||
/**
|
||||
* Subscribe to an event
|
||||
*/
|
||||
on(event, handler) {
|
||||
if (!this.handlers.has(event)) {
|
||||
this.handlers.set(event, new Set());
|
||||
}
|
||||
this.handlers.get(event).add(handler);
|
||||
// Return unsubscribe function
|
||||
return () => this.off(event, handler);
|
||||
}
|
||||
/**
|
||||
* Subscribe to an event (once)
|
||||
*/
|
||||
once(event, handler) {
|
||||
const wrappedHandler = (data) => {
|
||||
this.off(event, wrappedHandler);
|
||||
handler(data);
|
||||
};
|
||||
return this.on(event, wrappedHandler);
|
||||
}
|
||||
/**
|
||||
* Unsubscribe from an event
|
||||
*/
|
||||
off(event, handler) {
|
||||
this.handlers.get(event)?.delete(handler);
|
||||
}
|
||||
/**
|
||||
* Emit an event
|
||||
*/
|
||||
emit(event, data) {
|
||||
this.handlers.get(event)?.forEach(handler => {
|
||||
try {
|
||||
handler(data);
|
||||
}
|
||||
catch (err) {
|
||||
console.error(`[EventEmitter] Error in handler for "${String(event)}":`, err);
|
||||
}
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Remove all handlers for an event (or all events)
|
||||
*/
|
||||
removeAllListeners(event) {
|
||||
if (event) {
|
||||
this.handlers.delete(event);
|
||||
}
|
||||
else {
|
||||
this.handlers.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
//# sourceMappingURL=EventEmitter.js.map
|
||||
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"EventEmitter.js","sourceRoot":"","sources":["../../src/EventEmitter.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,MAAM,OAAO,YAAY;IAAzB;QACY,aAAQ,GAAyC,IAAI,GAAG,EAAE,CAAC;IAwDvE,CAAC;IAtDG;;OAEG;IACH,EAAE,CAAyB,KAAQ,EAAE,OAAgC;QACjE,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC;YAC5B,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,KAAK,EAAE,IAAI,GAAG,EAAE,CAAC,CAAC;QACxC,CAAC;QACD,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAE,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QAEvC,8BAA8B;QAC9B,OAAO,GAAG,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;IAC1C,CAAC;IAED;;OAEG;IACH,IAAI,CAAyB,KAAQ,EAAE,OAAgC;QACnE,MAAM,cAAc,GAAG,CAAC,IAAe,EAAE,EAAE;YACvC,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,cAAc,CAAC,CAAC;YAChC,OAAO,CAAC,IAAI,CAAC,CAAC;QAClB,CAAC,CAAC;QACF,OAAO,IAAI,CAAC,EAAE,CAAC,KAAK,EAAE,cAAc,CAAC,CAAC;IAC1C,CAAC;IAED;;OAEG;IACH,GAAG,CAAyB,KAAQ,EAAE,OAAgC;QAClE,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC;IAC9C,CAAC;IAED;;OAEG;IACH,IAAI,CAAyB,KAAQ,EAAE,IAAe;QAClD,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC,OAAO,CAAC,EAAE;YACxC,IAAI,CAAC;gBACD,OAAO,CAAC,IAAI,CAAC,CAAC;YAClB,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACX,OAAO,CAAC,KAAK,CAAC,wCAAwC,MAAM,CAAC,KAAK,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;YAClF,CAAC;QACL,CAAC,CAAC,CAAC;IACP,CAAC;IAED;;OAEG;IACH,kBAAkB,CAAC,KAAoB;QACnC,IAAI,KAAK,EAAE,CAAC;YACR,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QAChC,CAAC;aAAM,CAAC;YACJ,IAAI,CAAC,QAAQ,CAAC,KAAK,EAAE,CAAC;QAC1B,CAAC;IACL,CAAC;CACJ"}
|
||||
247
G102-sequence/sdk/package/dist/esm/GameIframeSDK.js
vendored
@@ -1,247 +0,0 @@
|
||||
/**
|
||||
* Game Iframe SDK - Core
|
||||
* SDK chính - compose các layers: MessageHandler, MessageSender
|
||||
*/
|
||||
import { EventEmitter } from './EventEmitter';
|
||||
import { MessageHandler } from './MessageHandler';
|
||||
import { MessageSender } from './MessageSender';
|
||||
import { DEFAULT_CONFIG, } from './types';
|
||||
/**
|
||||
* GameIframeSDK - Main SDK class
|
||||
* Composes MessageHandler và MessageSender
|
||||
*/
|
||||
export class GameIframeSDK extends EventEmitter {
|
||||
constructor(config) {
|
||||
super();
|
||||
this.pendingData = null;
|
||||
this.isReady = false;
|
||||
this.config = { ...DEFAULT_CONFIG, ...config };
|
||||
// Initialize layers
|
||||
this.messageHandler = new MessageHandler({
|
||||
acceptedOrigin: this.config.iframeOrigin,
|
||||
debug: this.config.debug,
|
||||
});
|
||||
this.messageSender = new MessageSender({
|
||||
targetOrigin: this.config.iframeOrigin,
|
||||
debug: this.config.debug,
|
||||
});
|
||||
// Setup event forwarding
|
||||
this.setupEventForwarding();
|
||||
// Start listening
|
||||
this.messageHandler.start();
|
||||
this.log('info', 'SDK initialized', { config: this.config });
|
||||
}
|
||||
// ==========================================================================
|
||||
// PUBLIC API - Iframe Management
|
||||
// ==========================================================================
|
||||
/**
|
||||
* Set iframe element reference
|
||||
*/
|
||||
setIframe(iframe) {
|
||||
this.messageSender.setIframe(iframe);
|
||||
this.isReady = false;
|
||||
this.log('info', 'Iframe set', { hasIframe: !!iframe });
|
||||
return this;
|
||||
}
|
||||
/**
|
||||
* Get current iframe
|
||||
*/
|
||||
getIframe() {
|
||||
return this.messageSender.getIframe();
|
||||
}
|
||||
/**
|
||||
* Check if game is ready
|
||||
*/
|
||||
isGameReady() {
|
||||
return this.isReady;
|
||||
}
|
||||
/**
|
||||
* Check if sender is ready (iframe available)
|
||||
*/
|
||||
isSenderReady() {
|
||||
return this.messageSender.isReady();
|
||||
}
|
||||
// ==========================================================================
|
||||
// PUBLIC API - Send Data
|
||||
// ==========================================================================
|
||||
/**
|
||||
* Send game data to iframe
|
||||
*/
|
||||
sendGameData(data) {
|
||||
const result = this.messageSender.sendGameData(data);
|
||||
if (!result.success) {
|
||||
this.emit('error', {
|
||||
message: 'Failed to send game data',
|
||||
error: result.error,
|
||||
});
|
||||
}
|
||||
return result.success;
|
||||
}
|
||||
/**
|
||||
* Send leaderboard data to iframe
|
||||
*/
|
||||
sendLeaderboard(data) {
|
||||
const result = this.messageSender.sendLeaderboard(data);
|
||||
if (!result.success) {
|
||||
this.emit('error', {
|
||||
message: 'Failed to send leaderboard',
|
||||
error: result.error,
|
||||
});
|
||||
}
|
||||
return result.success;
|
||||
}
|
||||
// ==========================================================================
|
||||
// PUBLIC API - Queue & Auto-send
|
||||
// ==========================================================================
|
||||
/**
|
||||
* Queue data to be sent when game is ready
|
||||
*/
|
||||
queueGameData(data) {
|
||||
this.pendingData = data;
|
||||
this.log('info', 'Data queued for when game is ready');
|
||||
// If already ready, send immediately
|
||||
if (this.isReady) {
|
||||
this.sendQueuedData();
|
||||
}
|
||||
return this;
|
||||
}
|
||||
/**
|
||||
* Clear queued data
|
||||
*/
|
||||
clearQueuedData() {
|
||||
this.pendingData = null;
|
||||
return this;
|
||||
}
|
||||
// ==========================================================================
|
||||
// PUBLIC API - Iframe Control
|
||||
// ==========================================================================
|
||||
/**
|
||||
* Force reload iframe
|
||||
*/
|
||||
reloadIframe() {
|
||||
this.isReady = false;
|
||||
return this.messageSender.reloadIframe();
|
||||
}
|
||||
// ==========================================================================
|
||||
// PUBLIC API - Lifecycle
|
||||
// ==========================================================================
|
||||
/**
|
||||
* Cleanup and destroy SDK
|
||||
*/
|
||||
destroy() {
|
||||
this.messageHandler.destroy();
|
||||
this.removeAllListeners();
|
||||
this.pendingData = null;
|
||||
this.isReady = false;
|
||||
this.log('info', 'SDK destroyed');
|
||||
}
|
||||
// ==========================================================================
|
||||
// PUBLIC API - Direct Layer Access (Advanced)
|
||||
// ==========================================================================
|
||||
/**
|
||||
* Get MessageHandler instance for advanced usage
|
||||
*/
|
||||
getMessageHandler() {
|
||||
return this.messageHandler;
|
||||
}
|
||||
/**
|
||||
* Get MessageSender instance for advanced usage
|
||||
*/
|
||||
getMessageSender() {
|
||||
return this.messageSender;
|
||||
}
|
||||
// ==========================================================================
|
||||
// PRIVATE METHODS
|
||||
// ==========================================================================
|
||||
/**
|
||||
* Setup event forwarding from MessageHandler to SDK events
|
||||
*/
|
||||
setupEventForwarding() {
|
||||
// Forward gameReady
|
||||
this.messageHandler.on('gameReady', () => {
|
||||
this.isReady = true;
|
||||
this.emit('gameReady', undefined);
|
||||
// Auto-send queued data if enabled
|
||||
if (this.config.autoSendOnReady && this.pendingData) {
|
||||
setTimeout(() => {
|
||||
this.sendQueuedData();
|
||||
}, this.config.readyDelay);
|
||||
}
|
||||
});
|
||||
// Forward answerReport
|
||||
this.messageHandler.on('answerReport', (data) => {
|
||||
this.emit('answerReport', data);
|
||||
});
|
||||
// Forward finalResult
|
||||
this.messageHandler.on('finalResult', (data) => {
|
||||
this.emit('finalResult', data);
|
||||
});
|
||||
// Forward leaderboardRequest
|
||||
this.messageHandler.on('leaderboardRequest', (data) => {
|
||||
this.emit('leaderboardRequest', data);
|
||||
});
|
||||
// Forward errors
|
||||
this.messageHandler.on('error', (error) => {
|
||||
this.emit('error', error);
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Send queued data
|
||||
*/
|
||||
sendQueuedData() {
|
||||
if (this.pendingData) {
|
||||
this.sendGameData(this.pendingData);
|
||||
this.pendingData = null;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Internal logging
|
||||
*/
|
||||
log(level, message, data) {
|
||||
if (this.config.debug) {
|
||||
const prefix = '[GameIframeSDK]';
|
||||
switch (level) {
|
||||
case 'info':
|
||||
console.log(prefix, message, data ?? '');
|
||||
break;
|
||||
case 'warn':
|
||||
console.warn(prefix, message, data ?? '');
|
||||
break;
|
||||
case 'error':
|
||||
console.error(prefix, message, data ?? '');
|
||||
break;
|
||||
}
|
||||
}
|
||||
this.emit('log', { level, message, data });
|
||||
}
|
||||
}
|
||||
// ==========================================================================
|
||||
// FACTORY / SINGLETON HELPERS
|
||||
// ==========================================================================
|
||||
let defaultInstance = null;
|
||||
/**
|
||||
* Create SDK instance
|
||||
*/
|
||||
export function createGameIframeSDK(config) {
|
||||
return new GameIframeSDK(config);
|
||||
}
|
||||
/**
|
||||
* Get or create default SDK instance
|
||||
*/
|
||||
export function getGameIframeSDK(config) {
|
||||
if (!defaultInstance && config) {
|
||||
defaultInstance = new GameIframeSDK(config);
|
||||
}
|
||||
if (!defaultInstance) {
|
||||
throw new Error('GameIframeSDK not initialized. Call with config first.');
|
||||
}
|
||||
return defaultInstance;
|
||||
}
|
||||
/**
|
||||
* Destroy default instance
|
||||
*/
|
||||
export function destroyGameIframeSDK() {
|
||||
defaultInstance?.destroy();
|
||||
defaultInstance = null;
|
||||
}
|
||||
//# sourceMappingURL=GameIframeSDK.js.map
|
||||
111
G102-sequence/sdk/package/dist/esm/MessageHandler.js
vendored
@@ -1,111 +0,0 @@
|
||||
/**
|
||||
* Game Iframe SDK - Message Handler
|
||||
* Xử lý message từ iframe
|
||||
*/
|
||||
import { MESSAGE_TYPES } from './types';
|
||||
import { EventEmitter } from './EventEmitter';
|
||||
/**
|
||||
* MessageHandler - Xử lý incoming messages từ iframe
|
||||
*/
|
||||
export class MessageHandler extends EventEmitter {
|
||||
constructor(config) {
|
||||
super();
|
||||
this.boundHandler = null;
|
||||
this.isListening = false;
|
||||
this.config = config;
|
||||
}
|
||||
/**
|
||||
* Start listening for messages
|
||||
*/
|
||||
start() {
|
||||
if (this.isListening) {
|
||||
return this;
|
||||
}
|
||||
this.boundHandler = this.handleMessage.bind(this);
|
||||
window.addEventListener('message', this.boundHandler);
|
||||
this.isListening = true;
|
||||
this.log('MessageHandler started');
|
||||
return this;
|
||||
}
|
||||
/**
|
||||
* Stop listening for messages
|
||||
*/
|
||||
stop() {
|
||||
if (this.boundHandler) {
|
||||
window.removeEventListener('message', this.boundHandler);
|
||||
this.boundHandler = null;
|
||||
}
|
||||
this.isListening = false;
|
||||
this.log('MessageHandler stopped');
|
||||
return this;
|
||||
}
|
||||
/**
|
||||
* Check if handler is listening
|
||||
*/
|
||||
isActive() {
|
||||
return this.isListening;
|
||||
}
|
||||
/**
|
||||
* Handle incoming message
|
||||
*/
|
||||
handleMessage(event) {
|
||||
// Origin check
|
||||
if (!this.isOriginAllowed(event.origin)) {
|
||||
return;
|
||||
}
|
||||
const { type, data } = event.data || {};
|
||||
if (!type)
|
||||
return;
|
||||
this.log(`Received: ${type}`, data);
|
||||
try {
|
||||
switch (type) {
|
||||
case MESSAGE_TYPES.GAME_READY:
|
||||
this.emit('gameReady', undefined);
|
||||
break;
|
||||
case MESSAGE_TYPES.ANSWER_REPORT:
|
||||
// Raw data pass-through
|
||||
this.emit('answerReport', data);
|
||||
break;
|
||||
case MESSAGE_TYPES.FINAL_RESULT:
|
||||
// Raw data pass-through
|
||||
this.emit('finalResult', data);
|
||||
break;
|
||||
case MESSAGE_TYPES.GET_LEADERBOARD:
|
||||
this.emit('leaderboardRequest', { top: data?.top || 10 });
|
||||
break;
|
||||
default:
|
||||
this.emit('unknownMessage', { type, data });
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
const err = error;
|
||||
this.emit('error', { message: `Error handling ${type}`, error: err });
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Check if origin is allowed
|
||||
*/
|
||||
isOriginAllowed(origin) {
|
||||
if (this.config.acceptedOrigin === '*') {
|
||||
return true;
|
||||
}
|
||||
return origin === this.config.acceptedOrigin;
|
||||
}
|
||||
/**
|
||||
* Debug log
|
||||
*/
|
||||
log(message, data) {
|
||||
if (this.config.debug) {
|
||||
console.log('[MessageHandler]', message, data ?? '');
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Cleanup
|
||||
*/
|
||||
destroy() {
|
||||
this.stop();
|
||||
this.removeAllListeners();
|
||||
}
|
||||
}
|
||||
//# sourceMappingURL=MessageHandler.js.map
|
||||
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"MessageHandler.js","sourceRoot":"","sources":["../../src/MessageHandler.ts"],"names":[],"mappings":"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"}
|
||||
128
G102-sequence/sdk/package/dist/esm/MessageSender.js
vendored
@@ -1,128 +0,0 @@
|
||||
/**
|
||||
* Game Iframe SDK - Message Sender
|
||||
* Gửi message đến iframe
|
||||
*/
|
||||
import { MESSAGE_TYPES } from './types';
|
||||
/**
|
||||
* MessageSender - Gửi messages đến iframe
|
||||
*/
|
||||
export class MessageSender {
|
||||
constructor(config) {
|
||||
this.iframe = null;
|
||||
this.config = config;
|
||||
}
|
||||
/**
|
||||
* Set iframe element
|
||||
*/
|
||||
setIframe(iframe) {
|
||||
this.iframe = iframe;
|
||||
return this;
|
||||
}
|
||||
/**
|
||||
* Get current iframe
|
||||
*/
|
||||
getIframe() {
|
||||
return this.iframe;
|
||||
}
|
||||
/**
|
||||
* Check if iframe is available
|
||||
*/
|
||||
isReady() {
|
||||
return !!this.iframe?.contentWindow;
|
||||
}
|
||||
/**
|
||||
* Send raw message to iframe
|
||||
*/
|
||||
sendRaw(message) {
|
||||
if (!this.iframe?.contentWindow) {
|
||||
return {
|
||||
success: false,
|
||||
error: new Error('Iframe not available'),
|
||||
};
|
||||
}
|
||||
try {
|
||||
this.iframe.contentWindow.postMessage(message, this.config.targetOrigin);
|
||||
this.log('Sent message', { type: message.type });
|
||||
return { success: true };
|
||||
}
|
||||
catch (error) {
|
||||
const err = error;
|
||||
this.log('Send failed', { error: err.message });
|
||||
return { success: false, error: err };
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Send game data (SERVER_PUSH_DATA)
|
||||
*/
|
||||
sendGameData(payload) {
|
||||
// Inline message creation
|
||||
const message = {
|
||||
type: MESSAGE_TYPES.SERVER_PUSH_DATA,
|
||||
jsonData: payload,
|
||||
};
|
||||
const result = this.sendRaw(message);
|
||||
if (result.success) {
|
||||
const dataLength = payload.data?.length || 0;
|
||||
this.log('Sent game data', {
|
||||
game_id: payload.game_id,
|
||||
items: dataLength,
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
/**
|
||||
* Send leaderboard (SERVER_PUSH_LEADERBOARD)
|
||||
*/
|
||||
sendLeaderboard(data) {
|
||||
// Inline message creation
|
||||
const message = {
|
||||
type: MESSAGE_TYPES.SERVER_PUSH_LEADERBOARD,
|
||||
leaderboardData: data,
|
||||
};
|
||||
const result = this.sendRaw(message);
|
||||
if (result.success) {
|
||||
this.log('Sent leaderboard', {
|
||||
players: data.top_players?.length || 0,
|
||||
hasUserRank: !!data.user_rank,
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
/**
|
||||
* Reload iframe
|
||||
*/
|
||||
reloadIframe() {
|
||||
if (!this.iframe) {
|
||||
return false;
|
||||
}
|
||||
const currentSrc = this.iframe.src;
|
||||
if (!currentSrc || currentSrc === 'about:blank') {
|
||||
return false;
|
||||
}
|
||||
this.iframe.src = '';
|
||||
setTimeout(() => {
|
||||
if (this.iframe) {
|
||||
this.iframe.src = currentSrc;
|
||||
this.log('Iframe reloaded');
|
||||
}
|
||||
}, 100);
|
||||
return true;
|
||||
}
|
||||
/**
|
||||
* Debug log
|
||||
*/
|
||||
log(message, data) {
|
||||
if (this.config.debug) {
|
||||
console.log('[MessageSender]', message);
|
||||
if (data) {
|
||||
try {
|
||||
console.log(JSON.stringify(data, null, 2));
|
||||
}
|
||||
catch (e) {
|
||||
console.log(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
//# sourceMappingURL=MessageSender.js.map
|
||||
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"MessageSender.js","sourceRoot":"","sources":["../../src/MessageSender.ts"],"names":[],"mappings":"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"}
|
||||
@@ -1,245 +0,0 @@
|
||||
/**
|
||||
* Data Validator
|
||||
* Verify data structure cho từng game code
|
||||
*
|
||||
* Usage:
|
||||
* ```typescript
|
||||
* import { validateGameData, DataValidator } from 'game-iframe-sdk/client';
|
||||
*
|
||||
* const result = validateGameData('G001', receivedData);
|
||||
* if (!result.valid) {
|
||||
* console.error('Invalid data:', result.errors);
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
import { GAME_CODES } from '../kit/GameDataHandler';
|
||||
// =============================================================================
|
||||
// SCHEMAS FOR EACH GAME CODE
|
||||
// =============================================================================
|
||||
const QUIZ_BASE_SCHEMA = {
|
||||
id: { type: 'string', required: true, description: 'Unique question ID' },
|
||||
options: { type: 'array', required: true, arrayItemType: 'string', description: 'Answer options' },
|
||||
answer: { type: 'number', required: true, description: 'Correct answer index (0-based)' },
|
||||
};
|
||||
const SCHEMAS = {
|
||||
// Quiz variants
|
||||
G001: {
|
||||
...QUIZ_BASE_SCHEMA,
|
||||
question: { type: 'string', required: true, description: 'Text question' },
|
||||
},
|
||||
G002: {
|
||||
...QUIZ_BASE_SCHEMA,
|
||||
question_audio: { type: 'string', required: true, description: 'Audio URL for question' },
|
||||
},
|
||||
G003: {
|
||||
...QUIZ_BASE_SCHEMA,
|
||||
question: { type: 'string', required: true, description: 'Text question' },
|
||||
// options are audio URLs
|
||||
},
|
||||
G004: {
|
||||
...QUIZ_BASE_SCHEMA,
|
||||
question_image: { type: 'string', required: true, description: 'Image URL for question' },
|
||||
question: { type: 'string', required: false, description: 'Optional text hint' },
|
||||
},
|
||||
// G005: Quiz Text-Image (options are image URLs, client picks index)
|
||||
G005: {
|
||||
...QUIZ_BASE_SCHEMA,
|
||||
question: { type: 'string', required: true, description: 'Text question' },
|
||||
// options are image URLs, answer is index pointing to correct image
|
||||
},
|
||||
// Sequence Word variants
|
||||
G110: {
|
||||
id: { type: 'string', required: true },
|
||||
word: { type: 'string', required: true, description: 'The word to arrange' },
|
||||
parts: { type: 'array', required: true, arrayItemType: 'string', description: 'Letters/parts to arrange' },
|
||||
answer: { type: 'array', required: true, arrayItemType: 'string', description: 'Correct order' },
|
||||
},
|
||||
G111: {
|
||||
id: { type: 'string', required: true },
|
||||
word: { type: 'string', required: true },
|
||||
parts: { type: 'array', required: true, arrayItemType: 'string' },
|
||||
answer: { type: 'array', required: true, arrayItemType: 'string' },
|
||||
audio_url: { type: 'string', required: true, description: 'Audio hint URL' },
|
||||
},
|
||||
G112: {
|
||||
id: { type: 'string', required: true },
|
||||
word: { type: 'string', required: true },
|
||||
parts: { type: 'array', required: true, arrayItemType: 'string' },
|
||||
answer: { type: 'array', required: true, arrayItemType: 'string' },
|
||||
audio_url: { type: 'string', required: true },
|
||||
},
|
||||
G113: {
|
||||
id: { type: 'string', required: true },
|
||||
word: { type: 'string', required: true },
|
||||
parts: { type: 'array', required: true, arrayItemType: 'string' },
|
||||
answer: { type: 'array', required: true, arrayItemType: 'string' },
|
||||
audio_url: { type: 'string', required: true },
|
||||
},
|
||||
// Sequence Sentence variants
|
||||
G120: {
|
||||
id: { type: 'string', required: true },
|
||||
sentence: { type: 'string', required: false, description: 'Full sentence (hint)' },
|
||||
parts: { type: 'array', required: true, arrayItemType: 'string', description: 'Words to arrange' },
|
||||
answer: { type: 'array', required: true, arrayItemType: 'string', description: 'Correct word order' },
|
||||
},
|
||||
G121: {
|
||||
id: { type: 'string', required: true },
|
||||
sentence: { type: 'string', required: false },
|
||||
parts: { type: 'array', required: true, arrayItemType: 'string' },
|
||||
answer: { type: 'array', required: true, arrayItemType: 'string' },
|
||||
audio_url: { type: 'string', required: true },
|
||||
},
|
||||
G122: {
|
||||
id: { type: 'string', required: true },
|
||||
sentence: { type: 'string', required: false },
|
||||
parts: { type: 'array', required: true, arrayItemType: 'string' },
|
||||
answer: { type: 'array', required: true, arrayItemType: 'string' },
|
||||
audio_url: { type: 'string', required: true },
|
||||
},
|
||||
G123: {
|
||||
id: { type: 'string', required: true },
|
||||
sentence: { type: 'string', required: false },
|
||||
parts: { type: 'array', required: true, arrayItemType: 'string' },
|
||||
answer: { type: 'array', required: true, arrayItemType: 'string' },
|
||||
audio_url: { type: 'string', required: true },
|
||||
}
|
||||
};
|
||||
// =============================================================================
|
||||
// VALIDATOR
|
||||
// =============================================================================
|
||||
/**
|
||||
* Validate a single item against schema
|
||||
*/
|
||||
function validateItem(item, schema, itemIndex) {
|
||||
const errors = [];
|
||||
if (!item || typeof item !== 'object') {
|
||||
errors.push(`Item [${itemIndex}]: Must be an object`);
|
||||
return errors;
|
||||
}
|
||||
for (const [field, fieldSchema] of Object.entries(schema)) {
|
||||
const value = item[field];
|
||||
// Check required
|
||||
if (fieldSchema.required && (value === undefined || value === null)) {
|
||||
errors.push(`Item [${itemIndex}].${field}: Required field is missing`);
|
||||
continue;
|
||||
}
|
||||
// Skip validation if optional and not present
|
||||
if (!fieldSchema.required && (value === undefined || value === null)) {
|
||||
continue;
|
||||
}
|
||||
// Check type
|
||||
const actualType = Array.isArray(value) ? 'array' : typeof value;
|
||||
if (fieldSchema.type !== 'any' && actualType !== fieldSchema.type) {
|
||||
errors.push(`Item [${itemIndex}].${field}: Expected ${fieldSchema.type}, got ${actualType}`);
|
||||
continue;
|
||||
}
|
||||
// Check array items
|
||||
if (fieldSchema.type === 'array' && fieldSchema.arrayItemType && fieldSchema.arrayItemType !== 'any') {
|
||||
for (let i = 0; i < value.length; i++) {
|
||||
const itemType = typeof value[i];
|
||||
if (itemType !== fieldSchema.arrayItemType) {
|
||||
errors.push(`Item [${itemIndex}].${field}[${i}]: Expected ${fieldSchema.arrayItemType}, got ${itemType}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return errors;
|
||||
}
|
||||
/**
|
||||
* Validate game data payload
|
||||
*/
|
||||
export function validateGameData(gameCode, payload) {
|
||||
const errors = [];
|
||||
const warnings = [];
|
||||
// Check game code
|
||||
if (!GAME_CODES[gameCode]) {
|
||||
errors.push(`Unknown game code: ${gameCode}`);
|
||||
return { valid: false, errors, warnings };
|
||||
}
|
||||
// Check payload structure
|
||||
if (!payload || typeof payload !== 'object') {
|
||||
errors.push('Payload must be an object');
|
||||
return { valid: false, errors, warnings };
|
||||
}
|
||||
// Check data array
|
||||
const items = payload.data || payload.items || payload.questions;
|
||||
if (!items) {
|
||||
errors.push('Missing data array (expected "data", "items", or "questions")');
|
||||
return { valid: false, errors, warnings };
|
||||
}
|
||||
if (!Array.isArray(items)) {
|
||||
errors.push('"data" must be an array');
|
||||
return { valid: false, errors, warnings };
|
||||
}
|
||||
if (items.length === 0) {
|
||||
warnings.push('Data array is empty');
|
||||
}
|
||||
// Validate each item
|
||||
const schema = SCHEMAS[gameCode];
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const itemErrors = validateItem(items[i], schema, i);
|
||||
errors.push(...itemErrors);
|
||||
}
|
||||
// Check for duplicate IDs
|
||||
const ids = items.map((item) => item.id).filter(Boolean);
|
||||
const duplicates = ids.filter((id, index) => ids.indexOf(id) !== index);
|
||||
if (duplicates.length > 0) {
|
||||
warnings.push(`Duplicate IDs found: ${[...new Set(duplicates)].join(', ')}`);
|
||||
}
|
||||
return {
|
||||
valid: errors.length === 0,
|
||||
errors,
|
||||
warnings,
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Get schema for a game code
|
||||
*/
|
||||
export function getSchema(gameCode) {
|
||||
return SCHEMAS[gameCode] ?? null;
|
||||
}
|
||||
/**
|
||||
* Get schema documentation for a game code
|
||||
*/
|
||||
export function getSchemaDoc(gameCode) {
|
||||
const schema = SCHEMAS[gameCode];
|
||||
if (!schema)
|
||||
return `Unknown game code: ${gameCode}`;
|
||||
const gameInfo = GAME_CODES[gameCode];
|
||||
const lines = [
|
||||
`## ${gameCode}: ${gameInfo.name}`,
|
||||
`Category: ${gameInfo.category}`,
|
||||
'',
|
||||
'### Fields:',
|
||||
];
|
||||
for (const [field, fieldSchema] of Object.entries(schema)) {
|
||||
const required = fieldSchema.required ? '(required)' : '(optional)';
|
||||
let type = fieldSchema.type;
|
||||
if (fieldSchema.arrayItemType) {
|
||||
type = `${fieldSchema.type}<${fieldSchema.arrayItemType}>`;
|
||||
}
|
||||
lines.push(`- **${field}**: ${type} ${required}`);
|
||||
if (fieldSchema.description) {
|
||||
lines.push(` - ${fieldSchema.description}`);
|
||||
}
|
||||
}
|
||||
return lines.join('\n');
|
||||
}
|
||||
// =============================================================================
|
||||
// DATA VALIDATOR CLASS
|
||||
// =============================================================================
|
||||
export class DataValidator {
|
||||
constructor(gameCode) {
|
||||
this.gameCode = gameCode;
|
||||
}
|
||||
validate(payload) {
|
||||
return validateGameData(this.gameCode, payload);
|
||||
}
|
||||
getSchema() {
|
||||
return getSchema(this.gameCode);
|
||||
}
|
||||
getSchemaDoc() {
|
||||
return getSchemaDoc(this.gameCode);
|
||||
}
|
||||
}
|
||||
//# sourceMappingURL=DataValidator.js.map
|
||||
@@ -1,359 +0,0 @@
|
||||
/**
|
||||
* GameClientSDK - SDK dành cho Game Iframe
|
||||
*
|
||||
* Sử dụng trong game để:
|
||||
* - Tự động xác định mode (preview/live) từ URL
|
||||
* - Nhận data từ parent (preview) hoặc fetch API (live)
|
||||
* - Verify answers locally
|
||||
* - Report results về parent
|
||||
*/
|
||||
import { checkAnswer, sanitizeForClient, GAME_CODES } from '../kit/GameDataHandler';
|
||||
import { getMockData } from './MockData';
|
||||
import { validateGameData } from './DataValidator';
|
||||
class SimpleEventEmitter {
|
||||
constructor() {
|
||||
this.handlers = new Map();
|
||||
}
|
||||
on(event, handler) {
|
||||
if (!this.handlers.has(event)) {
|
||||
this.handlers.set(event, new Set());
|
||||
}
|
||||
this.handlers.get(event).add(handler);
|
||||
return () => this.off(event, handler);
|
||||
}
|
||||
off(event, handler) {
|
||||
this.handlers.get(event)?.delete(handler);
|
||||
}
|
||||
emit(event, data) {
|
||||
this.handlers.get(event)?.forEach(handler => {
|
||||
try {
|
||||
handler(data);
|
||||
}
|
||||
catch (err) {
|
||||
console.error(`[GameClientSDK] Error in ${String(event)} handler:`, err);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
// =============================================================================
|
||||
// GAME CLIENT SDK
|
||||
// =============================================================================
|
||||
export class GameClientSDK extends SimpleEventEmitter {
|
||||
constructor(config = {}) {
|
||||
super();
|
||||
// Data storage
|
||||
this.originalItems = new Map(); // Có đáp án
|
||||
this.sanitizedItems = []; // Không có đáp án
|
||||
this.userAnswers = new Map();
|
||||
this.isInitialized = false;
|
||||
this.startTime = 0;
|
||||
this.config = {
|
||||
debug: config.debug ?? false,
|
||||
apiBaseUrl: config.apiBaseUrl ?? '',
|
||||
getAuthHeaders: config.getAuthHeaders ?? (() => ({})),
|
||||
};
|
||||
// Parse URL params
|
||||
this.params = this.parseURLParams();
|
||||
this.mode = this.params.mode;
|
||||
this.log('info', 'SDK created', { mode: this.mode, params: this.params });
|
||||
// Emit mode detected
|
||||
this.emit('modeDetected', { mode: this.mode, params: this.params });
|
||||
// Setup message listener
|
||||
this.setupMessageListener();
|
||||
// Auto-initialize based on mode
|
||||
this.initialize();
|
||||
}
|
||||
// ==========================================================================
|
||||
// PUBLIC API
|
||||
// ==========================================================================
|
||||
/**
|
||||
* Get current mode
|
||||
*/
|
||||
getMode() {
|
||||
return this.mode;
|
||||
}
|
||||
/**
|
||||
* Get URL params
|
||||
*/
|
||||
getParams() {
|
||||
return { ...this.params };
|
||||
}
|
||||
/**
|
||||
* Get game code
|
||||
*/
|
||||
getGameCode() {
|
||||
return this.params.gameCode;
|
||||
}
|
||||
/**
|
||||
* Get sanitized items (safe for rendering)
|
||||
*/
|
||||
getItems() {
|
||||
return this.sanitizedItems;
|
||||
}
|
||||
/**
|
||||
* Submit an answer and get verification result
|
||||
*/
|
||||
submitAnswer(questionId, choice) {
|
||||
const originalItem = this.originalItems.get(questionId);
|
||||
if (!originalItem) {
|
||||
this.log('warn', `Item not found: ${questionId}`);
|
||||
return { isCorrect: false, score: 0, feedback: 'Question not found' };
|
||||
}
|
||||
// Verify using GameDataHandler
|
||||
const result = checkAnswer(this.params.gameCode, originalItem, choice);
|
||||
// Store user answer
|
||||
const timeSpent = Date.now() - (this.userAnswers.size === 0 ? this.startTime : Date.now());
|
||||
this.userAnswers.set(questionId, {
|
||||
choice,
|
||||
result: result.isCorrect ? 1 : 0,
|
||||
time: timeSpent,
|
||||
});
|
||||
// Report to parent
|
||||
this.sendAnswerReport(questionId, choice, result.isCorrect ? 1 : 0, timeSpent);
|
||||
this.log('info', `Answer submitted: ${questionId}`, { choice, result });
|
||||
return result;
|
||||
}
|
||||
/**
|
||||
* Get final result
|
||||
*/
|
||||
getFinalResult() {
|
||||
const details = Array.from(this.userAnswers.entries()).map(([id, data]) => ({
|
||||
question_id: id,
|
||||
choice: data.choice,
|
||||
result: data.result,
|
||||
time_spent: data.time,
|
||||
}));
|
||||
const correct = details.filter(d => d.result === 1).length;
|
||||
const total = this.originalItems.size;
|
||||
return {
|
||||
score: total > 0 ? Math.round((correct / total) * 100) : 0,
|
||||
total,
|
||||
correct,
|
||||
wrong: total - correct,
|
||||
details,
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Report final result to parent
|
||||
*/
|
||||
reportFinalResult(result) {
|
||||
const finalResult = result ?? this.getFinalResult();
|
||||
window.parent.postMessage({
|
||||
type: 'FINAL_RESULT',
|
||||
data: finalResult,
|
||||
}, '*');
|
||||
this.log('info', 'Final result reported', finalResult);
|
||||
}
|
||||
/**
|
||||
* Request leaderboard from parent
|
||||
*/
|
||||
requestLeaderboard(top = 10) {
|
||||
window.parent.postMessage({
|
||||
type: 'GET_LEADERBOARD',
|
||||
data: { top },
|
||||
}, '*');
|
||||
}
|
||||
/**
|
||||
* Cleanup
|
||||
*/
|
||||
destroy() {
|
||||
window.removeEventListener('message', this.handleMessage);
|
||||
this.originalItems.clear();
|
||||
this.sanitizedItems = [];
|
||||
this.userAnswers.clear();
|
||||
this.log('info', 'SDK destroyed');
|
||||
}
|
||||
// ==========================================================================
|
||||
// PRIVATE METHODS
|
||||
// ==========================================================================
|
||||
parseURLParams() {
|
||||
const searchParams = new URLSearchParams(window.location.search);
|
||||
const mode = (searchParams.get('mode') || 'preview');
|
||||
const gameCode = (searchParams.get('game_code') || 'G001');
|
||||
const gameId = searchParams.get('game_id') || undefined;
|
||||
const lid = searchParams.get('lid') || undefined;
|
||||
const studentId = searchParams.get('student_id') || undefined;
|
||||
// Validate mode
|
||||
if (mode !== 'preview' && mode !== 'live' && mode !== 'dev') {
|
||||
this.log('warn', `Invalid mode: ${mode}, defaulting to preview`);
|
||||
}
|
||||
// Validate game code
|
||||
if (!GAME_CODES[gameCode]) {
|
||||
this.log('warn', `Unknown game code: ${gameCode}`);
|
||||
}
|
||||
return { mode, gameCode, gameId, lid, studentId };
|
||||
}
|
||||
setupMessageListener() {
|
||||
this.handleMessage = this.handleMessage.bind(this);
|
||||
window.addEventListener('message', this.handleMessage);
|
||||
}
|
||||
handleMessage(event) {
|
||||
const { type, jsonData, leaderboardData } = event.data || {};
|
||||
this.log('debug', 'Message received', { type, hasData: !!jsonData });
|
||||
switch (type) {
|
||||
case 'SERVER_PUSH_DATA':
|
||||
if (jsonData) {
|
||||
this.handleDataReceived(jsonData);
|
||||
}
|
||||
break;
|
||||
case 'SERVER_PUSH_LEADERBOARD':
|
||||
if (leaderboardData) {
|
||||
this.log('info', 'Leaderboard received', leaderboardData);
|
||||
// Could emit event here
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
async initialize() {
|
||||
// Send GAME_READY immediately
|
||||
this.sendGameReady();
|
||||
if (this.mode === 'dev') {
|
||||
// Dev mode: load mock data immediately
|
||||
this.log('info', 'DEV MODE: Loading mock data...');
|
||||
this.loadMockData();
|
||||
}
|
||||
else if (this.mode === 'live') {
|
||||
// Live mode: fetch data from API
|
||||
await this.fetchLiveData();
|
||||
}
|
||||
else {
|
||||
// Preview mode: wait for postMessage
|
||||
this.log('info', 'Preview mode: waiting for SERVER_PUSH_DATA...');
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Load mock data for dev mode
|
||||
*/
|
||||
loadMockData() {
|
||||
const mockData = getMockData(this.params.gameCode);
|
||||
if (!mockData) {
|
||||
this.emit('error', {
|
||||
message: `No mock data available for game code: ${this.params.gameCode}`
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.log('info', `Loaded mock data for ${this.params.gameCode}`);
|
||||
this.handleDataReceived(mockData);
|
||||
}
|
||||
sendGameReady() {
|
||||
window.parent.postMessage({ type: 'GAME_READY' }, '*');
|
||||
this.emit('ready', undefined);
|
||||
this.log('info', 'GAME_READY sent');
|
||||
}
|
||||
async fetchLiveData() {
|
||||
const { gameId, lid } = this.params;
|
||||
if (!gameId || !lid) {
|
||||
this.emit('error', { message: 'Live mode requires game_id and lid' });
|
||||
return;
|
||||
}
|
||||
if (!this.config.apiBaseUrl) {
|
||||
this.emit('error', { message: 'Live mode requires apiBaseUrl' });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const url = `${this.config.apiBaseUrl}/games/${gameId}?lid=${lid}`;
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
...this.config.getAuthHeaders(),
|
||||
};
|
||||
this.log('info', `Fetching live data: ${url}`);
|
||||
const response = await fetch(url, { headers });
|
||||
if (!response.ok) {
|
||||
throw new Error(`API Error: ${response.status}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
this.handleDataReceived(data);
|
||||
}
|
||||
catch (error) {
|
||||
this.log('error', 'Failed to fetch live data', error);
|
||||
this.emit('error', { message: 'Failed to fetch game data', error });
|
||||
}
|
||||
}
|
||||
handleDataReceived(payload) {
|
||||
this.startTime = Date.now();
|
||||
// Update game code if provided
|
||||
if (payload.game_code && GAME_CODES[payload.game_code]) {
|
||||
this.params.gameCode = payload.game_code;
|
||||
}
|
||||
// Validate data structure
|
||||
const validation = validateGameData(this.params.gameCode, payload);
|
||||
if (!validation.valid) {
|
||||
this.log('error', 'Data validation failed', validation.errors);
|
||||
this.emit('validationError', { validation });
|
||||
// Continue anyway to allow partial rendering
|
||||
}
|
||||
if (validation.warnings.length > 0) {
|
||||
this.log('warn', 'Data validation warnings', validation.warnings);
|
||||
}
|
||||
// Extract items from various payload formats
|
||||
const items = payload.data || payload.items || payload.questions || [];
|
||||
const resumeData = payload.completed_question_ids || [];
|
||||
// Store original items (with answers)
|
||||
this.originalItems.clear();
|
||||
items.forEach((item) => {
|
||||
if (item.id) {
|
||||
this.originalItems.set(item.id, item);
|
||||
}
|
||||
});
|
||||
// Sanitize for client (remove answers)
|
||||
this.sanitizedItems = sanitizeForClient(this.params.gameCode, items);
|
||||
this.isInitialized = true;
|
||||
this.log('info', `Data received: ${items.length} items, ${resumeData.length} completed`);
|
||||
// Emit event with validation result
|
||||
this.emit('dataReceived', {
|
||||
items: this.sanitizedItems,
|
||||
resumeData,
|
||||
validation,
|
||||
});
|
||||
}
|
||||
sendAnswerReport(questionId, choice, result, timeSpent) {
|
||||
window.parent.postMessage({
|
||||
type: 'ANSWER_REPORT',
|
||||
data: {
|
||||
question_id: questionId,
|
||||
question_index: Array.from(this.originalItems.keys()).indexOf(questionId),
|
||||
choice,
|
||||
result,
|
||||
time_spent: timeSpent,
|
||||
},
|
||||
}, '*');
|
||||
}
|
||||
log(level, message, data) {
|
||||
if (!this.config.debug && level === 'debug')
|
||||
return;
|
||||
const prefix = `[GameClientSDK:${this.mode}]`;
|
||||
switch (level) {
|
||||
case 'debug':
|
||||
case 'info':
|
||||
console.log(prefix, message, data ?? '');
|
||||
break;
|
||||
case 'warn':
|
||||
console.warn(prefix, message, data ?? '');
|
||||
break;
|
||||
case 'error':
|
||||
console.error(prefix, message, data ?? '');
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// =============================================================================
|
||||
// FACTORY
|
||||
// =============================================================================
|
||||
let clientInstance = null;
|
||||
/**
|
||||
* Get or create GameClientSDK instance
|
||||
*/
|
||||
export function getGameClientSDK(config) {
|
||||
if (!clientInstance) {
|
||||
clientInstance = new GameClientSDK(config);
|
||||
}
|
||||
return clientInstance;
|
||||
}
|
||||
/**
|
||||
* Destroy client instance
|
||||
*/
|
||||
export function destroyGameClientSDK() {
|
||||
clientInstance?.destroy();
|
||||
clientInstance = null;
|
||||
}
|
||||
//# sourceMappingURL=GameClientSDK.js.map
|
||||