90
.gitea/workflows/deploy.yml
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
name: Deploy to Production
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- master
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Deploy to server
|
||||||
|
run: |
|
||||||
|
echo "🚀 Deploying to /var/www/html/games"
|
||||||
|
echo "📁 Current directory: $(pwd)"
|
||||||
|
echo "📁 GITHUB_WORKSPACE: $GITHUB_WORKSPACE"
|
||||||
|
|
||||||
|
# Read game info from readme.md
|
||||||
|
if [ -f "$GITHUB_WORKSPACE/readme.md" ]; then
|
||||||
|
GAME_TITLE=$(sed -n '1p' "$GITHUB_WORKSPACE/readme.md")
|
||||||
|
GAME_DESC=$(sed -n '2p' "$GITHUB_WORKSPACE/readme.md")
|
||||||
|
GAME_TYPE=$(sed -n '3p' "$GITHUB_WORKSPACE/readme.md")
|
||||||
|
else
|
||||||
|
GAME_TITLE="Untitled Game"
|
||||||
|
GAME_DESC="No description"
|
||||||
|
GAME_TYPE="quiz"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create base directory
|
||||||
|
mkdir -p /var/www/html/games/
|
||||||
|
|
||||||
|
# Find and deploy all game folders (exclude source, .git, .gitea, etc.)
|
||||||
|
DEPLOYED_FOLDERS=""
|
||||||
|
DEPLOYED_URLS=""
|
||||||
|
for folder in $GITHUB_WORKSPACE/*/; do
|
||||||
|
folder_name=$(basename "$folder")
|
||||||
|
|
||||||
|
# Skip excluded folders
|
||||||
|
if [[ "$folder_name" == "source" ]] || \
|
||||||
|
[[ "$folder_name" == ".git" ]] || \
|
||||||
|
[[ "$folder_name" == ".gitea" ]] || \
|
||||||
|
[[ "$folder_name" == "node_modules" ]] || \
|
||||||
|
[[ "$folder_name" == "logs" ]] || \
|
||||||
|
[[ "$folder_name" == "uploads" ]]; then
|
||||||
|
echo "⏭️ Skipping: $folder_name"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Deploy the folder
|
||||||
|
echo "📦 Deploying: $folder_name"
|
||||||
|
rsync -av --delete "$folder" "/var/www/html/games/$folder_name/"
|
||||||
|
DEPLOYED_FOLDERS="$DEPLOYED_FOLDERS $folder_name"
|
||||||
|
GAME_URL="https://senaai.tech/games/$folder_name/"
|
||||||
|
DEPLOYED_URLS="$DEPLOYED_URLS\n 🔗 $GAME_URL"
|
||||||
|
|
||||||
|
# Get thumbnail (first image found or default)
|
||||||
|
THUMBNAIL=$(find "$folder" -type f \( -name "*.png" -o -name "*.jpg" -o -name "*.jpeg" \) 2>/dev/null | head -n 1 || true)
|
||||||
|
if [ -z "$THUMBNAIL" ]; then
|
||||||
|
THUMBNAIL="https://senaai.tech/games/$folder_name/images/default.png"
|
||||||
|
else
|
||||||
|
THUMBNAIL="https://senaai.tech/games/$folder_name/$(basename "$THUMBNAIL")"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Submit game info to API
|
||||||
|
echo "📤 Submitting game info to API..."
|
||||||
|
curl --location 'http://senaai.tech:10000/api/games/save-with-check' \
|
||||||
|
--header 'Content-Type: application/x-www-form-urlencoded' \
|
||||||
|
--data-urlencode "title=$GAME_TITLE" \
|
||||||
|
--data-urlencode "description=$GAME_DESC" \
|
||||||
|
--data-urlencode "url=$GAME_URL" \
|
||||||
|
--data-urlencode "thumbnail=$THUMBNAIL" \
|
||||||
|
--data-urlencode "type=$GAME_TYPE" || true
|
||||||
|
echo ""
|
||||||
|
done
|
||||||
|
|
||||||
|
# Show deployment summary
|
||||||
|
echo ""
|
||||||
|
echo "=========================================="
|
||||||
|
echo "🎉 Deployment Completed Successfully!"
|
||||||
|
echo "=========================================="
|
||||||
|
echo ""
|
||||||
|
echo "📍 Deployed URLs:"
|
||||||
|
echo -e "$DEPLOYED_URLS"
|
||||||
|
echo ""
|
||||||
|
echo "=========================================="
|
||||||
|
Before Width: | Height: | Size: 89 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: 49 KiB |
|
Before Width: | Height: | Size: 132 KiB |
|
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,67 +0,0 @@
|
|||||||
{
|
|
||||||
"version": 1769066087,
|
|
||||||
"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/human-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,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "G102-sequence",
|
"name": "G120-sequence",
|
||||||
"short_name": "G102-sequence",
|
"short_name": "G120-sequence",
|
||||||
"start_url": "index.html",
|
"start_url": "index.html",
|
||||||
"display": "fullscreen",
|
"display": "fullscreen",
|
||||||
"orientation": "any",
|
"orientation": "any",
|
||||||
1
audio_spelling_sequence_sentence/data.js
Normal file
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 96 KiB |
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 1.0 MiB After Width: | Height: | Size: 1.0 MiB |
BIN
audio_spelling_sequence_sentence/images/btn_check-sheet0.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
audio_spelling_sequence_sentence/images/btn_music-sheet0.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
audio_spelling_sequence_sentence/images/btn_music-sheet1.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
audio_spelling_sequence_sentence/images/btn_pause-sheet0.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
audio_spelling_sequence_sentence/images/btn_setting-sheet0.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 7.8 KiB |
BIN
audio_spelling_sequence_sentence/images/layer-sheet0.png
Normal file
|
After Width: | Height: | Size: 109 KiB |
BIN
audio_spelling_sequence_sentence/images/layer2-sheet0.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
audio_spelling_sequence_sentence/images/newwordpng-sheet0.png
Normal file
|
After Width: | Height: | Size: 162 KiB |
BIN
audio_spelling_sequence_sentence/images/panel-sheet0.png
Normal file
|
After Width: | Height: | Size: 332 KiB |
|
Before Width: | Height: | Size: 168 B After Width: | Height: | Size: 168 B |
BIN
audio_spelling_sequence_sentence/images/pause-sheet0.png
Normal file
|
After Width: | Height: | Size: 155 B |
BIN
audio_spelling_sequence_sentence/images/senaaikhoi-sheet0.png
Normal file
|
After Width: | Height: | Size: 225 KiB |
BIN
audio_spelling_sequence_sentence/images/slot-sheet0.png
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
audio_spelling_sequence_sentence/images/txt_texttimer.png
Normal file
|
After Width: | Height: | Size: 92 KiB |
BIN
audio_spelling_sequence_sentence/images/txt_worditem.png
Normal file
|
After Width: | Height: | Size: 93 KiB |
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
|
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
|
||||||
<title>G102-sequence</title>
|
<title>G120-sequence</title>
|
||||||
|
|
||||||
<!-- Standardised web app manifest -->
|
<!-- Standardised web app manifest -->
|
||||||
<link rel="manifest" href="appmanifest.json" />
|
<link rel="manifest" href="appmanifest.json" />
|
||||||
@@ -82,6 +82,7 @@
|
|||||||
<!-- Construct 2 exported games require jQuery. -->
|
<!-- Construct 2 exported games require jQuery. -->
|
||||||
<script src="jquery-3.4.1.min.js"></script>
|
<script src="jquery-3.4.1.min.js"></script>
|
||||||
<script src="tdv_sdk.js"></script>
|
<script src="tdv_sdk.js"></script>
|
||||||
|
<script src="sena_sdk.js"></script>
|
||||||
|
|
||||||
|
|
||||||
<!-- The runtime script. You can rename it, but don't forget to rename the reference here as well.
|
<!-- The runtime script. You can rename it, but don't forget to rename the reference here as well.
|
||||||
|
Before Width: | Height: | Size: 9.2 KiB After Width: | Height: | Size: 9.2 KiB |
BIN
audio_spelling_sequence_sentence/media/click.ogg
Normal file
BIN
audio_spelling_sequence_sentence/media/correct.ogg
Normal file
BIN
audio_spelling_sequence_sentence/media/error-010-206498.ogg
Normal file
39
audio_spelling_sequence_sentence/offline.js
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
{
|
||||||
|
"version": 1769516124,
|
||||||
|
"fileList": [
|
||||||
|
"data.js",
|
||||||
|
"c2runtime.js",
|
||||||
|
"jquery-3.4.1.min.js",
|
||||||
|
"offlineClient.js",
|
||||||
|
"images/btn_check-sheet0.png",
|
||||||
|
"images/pause-sheet0.png",
|
||||||
|
"images/slot-sheet0.png",
|
||||||
|
"images/txt_texttimer.png",
|
||||||
|
"images/txt_worditem.png",
|
||||||
|
"images/5sosarahtakesoff-sheet0.png",
|
||||||
|
"images/senaaikhoi-sheet0.png",
|
||||||
|
"images/checker_wrong_correct-sheet0.png",
|
||||||
|
"images/checker_wrong_correct-sheet1.png",
|
||||||
|
"images/btn_setting-sheet0.png",
|
||||||
|
"images/panel-sheet0.png",
|
||||||
|
"images/btn_pause-sheet0.png",
|
||||||
|
"images/btn_music-sheet0.png",
|
||||||
|
"images/btn_music-sheet1.png",
|
||||||
|
"images/panel_pause-sheet0.png",
|
||||||
|
"images/layer-sheet0.png",
|
||||||
|
"images/newwordpng-sheet0.png",
|
||||||
|
"images/layer2-sheet0.png",
|
||||||
|
"media/click.ogg",
|
||||||
|
"media/correct.ogg",
|
||||||
|
"media/error-010-206498.ogg",
|
||||||
|
"media/immersivecontrol-button-click-sound-463065.ogg",
|
||||||
|
"icon-16.png",
|
||||||
|
"icon-32.png",
|
||||||
|
"icon-114.png",
|
||||||
|
"icon-128.png",
|
||||||
|
"icon-256.png",
|
||||||
|
"loading-logo.png",
|
||||||
|
"tdv_sdk.js",
|
||||||
|
"sena_sdk.js"
|
||||||
|
]
|
||||||
|
}
|
||||||
600
audio_spelling_sequence_sentence/sena_sdk.js
Normal file
@@ -0,0 +1,600 @@
|
|||||||
|
/**
|
||||||
|
* Sena SDK Constructor
|
||||||
|
* @param {Object} config - Configuration object for the SDK
|
||||||
|
* @param {Object} config.data - Quiz data containing question, options, and answer
|
||||||
|
*/
|
||||||
|
function SenaSDK(gid = 'G2510S1T30') {
|
||||||
|
// Initialize data
|
||||||
|
this.data = null;
|
||||||
|
this.correctAnswer = null;
|
||||||
|
this.gameCode = gid;
|
||||||
|
// Initialize properties
|
||||||
|
this.timeLimit = 0;
|
||||||
|
this.shuffle = true;
|
||||||
|
// tracking time in game
|
||||||
|
this.startTime = 0;
|
||||||
|
this.endTime = 0;
|
||||||
|
// Initialize Web Speech API
|
||||||
|
this.speechSynthesis = window.speechSynthesis;
|
||||||
|
this.currentUtterance = null;
|
||||||
|
this.voiceSettings = {
|
||||||
|
lang: 'en-US',
|
||||||
|
rate: 1.0, // Speed (0.1 to 10)
|
||||||
|
pitch: 1.0, // Pitch (0 to 2)
|
||||||
|
volume: 1.0 // Volume (0 to 1)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shuffle array using Fisher-Yates algorithm
|
||||||
|
* @param {Array} array - Array to shuffle
|
||||||
|
*/
|
||||||
|
SenaSDK.prototype.shuffleArray = function(array) {
|
||||||
|
for (let i = array.length - 1; i > 0; i--) {
|
||||||
|
const j = Math.floor(Math.random() * (i + 1));
|
||||||
|
[array[i], array[j]] = [array[j], array[i]];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
SenaSDK.prototype.load = function(callback,template = 'G2510S1T30') {
|
||||||
|
let self = this;
|
||||||
|
// get parameter LID from URL
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
const LID = urlParams.get('LID');
|
||||||
|
if (LID) {
|
||||||
|
self.gameCode = LID;
|
||||||
|
};
|
||||||
|
fetch(`https://senaai.tech/sample/${self.gameCode}.json`)
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
self.data = data.data;
|
||||||
|
self.correctAnswer = data.answer || null;
|
||||||
|
// based on game code, set timeLimit and shuffle
|
||||||
|
const gameCode = self.gameCode || template;
|
||||||
|
const regex = /^G([1-5])([2-9])([0-2])([0-2])(?:S([0-1]))?(?:T(\d+))?$/;
|
||||||
|
const match = gameCode.match(regex);
|
||||||
|
if (match) {
|
||||||
|
const shuffleFlag = match[5] !== undefined ? match[5] : '1';
|
||||||
|
const timeStr = match[6] !== undefined ? match[6] : '0';
|
||||||
|
self.shuffle = (shuffleFlag === '1');
|
||||||
|
self.timeLimit = parseInt(timeStr, 10);
|
||||||
|
}
|
||||||
|
if (callback) callback(true);
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error loading LID data:', error);
|
||||||
|
if (callback) callback(false);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Generate comprehensive developer guide based on game code
|
||||||
|
* @returns {string} Developer guide with implementation instructions
|
||||||
|
*/
|
||||||
|
SenaSDK.prototype.guide = function() {
|
||||||
|
let self = this;
|
||||||
|
const gameCode = self.gameCode || 'G2510S1T30';
|
||||||
|
const data = self.data || {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Regex giải thích:
|
||||||
|
* ^G([1-5])([2-9])([0-2])([0-2]) : Bắt buộc (Loại, Số lượng, Q, O)
|
||||||
|
* (?:S([0-1]))? : Không bắt buộc, mặc định S1
|
||||||
|
* (?:T(\d+))? : Không bắt buộc, mặc định T0
|
||||||
|
*/
|
||||||
|
const regex = /^G([1-5])([2-9])([0-2])([0-2])(?:S([0-1]))?(?:T(\d+))?$/;
|
||||||
|
const match = gameCode.match(regex);
|
||||||
|
|
||||||
|
if (!match) return "Mã game không hợp lệ! Định dạng chuẩn: Gxxxx hoặc GxxxxSxTxx";
|
||||||
|
|
||||||
|
const category = match[1];
|
||||||
|
const count = match[2];
|
||||||
|
const qIdx = match[3];
|
||||||
|
const oIdx = match[4];
|
||||||
|
const shuffle = match[5] !== undefined ? match[5] : '1';
|
||||||
|
const time = match[6] !== undefined ? match[6] : '0';
|
||||||
|
|
||||||
|
const types = { '0': 'Text', '1': 'Image', '2': 'Audio' };
|
||||||
|
let guide = '';
|
||||||
|
|
||||||
|
// Header
|
||||||
|
guide += `╔════════════════════════════════════════════════════════════════════════════╗\n`;
|
||||||
|
guide += `║ SENA SDK - DEVELOPER GUIDE: ${gameCode.padEnd(37)}║\n`;
|
||||||
|
guide += `╚════════════════════════════════════════════════════════════════════════════╝\n\n`;
|
||||||
|
|
||||||
|
// Game Analysis
|
||||||
|
guide += `📊 GAME ANALYSIS\n`;
|
||||||
|
guide += `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`;
|
||||||
|
|
||||||
|
let gameName = '', instruction = '', displayMode = '';
|
||||||
|
|
||||||
|
switch(category) {
|
||||||
|
case '1':
|
||||||
|
gameName = "Quiz (Trắc nghiệm)";
|
||||||
|
instruction = `Người dùng chọn 1 đáp án đúng trong ${count} options`;
|
||||||
|
displayMode = `Question: ${types[qIdx]} → Options: ${types[oIdx]}`;
|
||||||
|
break;
|
||||||
|
case '2':
|
||||||
|
if (qIdx === '0' && oIdx === '0') {
|
||||||
|
gameName = "Sort Word (Sắp xếp từ)";
|
||||||
|
instruction = `Sắp xếp ${count} từ/ký tự thành chuỗi hoàn chỉnh`;
|
||||||
|
} else {
|
||||||
|
gameName = "Sequences (Sắp xếp chuỗi)";
|
||||||
|
instruction = `Sắp xếp ${count} items theo đúng thứ tự`;
|
||||||
|
}
|
||||||
|
displayMode = `Hint: ${types[qIdx]} → Items: ${types[oIdx]}`;
|
||||||
|
break;
|
||||||
|
case '3':
|
||||||
|
if (qIdx === oIdx) {
|
||||||
|
gameName = "Memory Card (Trí nhớ)";
|
||||||
|
instruction = `Lật và ghép ${count} cặp thẻ giống nhau`;
|
||||||
|
} else {
|
||||||
|
gameName = "Matching (Nối cặp)";
|
||||||
|
instruction = `Nối ${count} items từ 2 nhóm với nhau`;
|
||||||
|
}
|
||||||
|
displayMode = `Group A: ${types[qIdx]} ↔ Group B: ${types[oIdx]}`;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
guide += `Game Type : ${gameName}\n`;
|
||||||
|
guide += `Objective : ${instruction}\n`;
|
||||||
|
guide += `Display Mode : ${displayMode}\n`;
|
||||||
|
guide += `Items Count : ${count}\n`;
|
||||||
|
guide += `Shuffle : ${shuffle === '1' ? 'YES (call sdk.start() to shuffle)' : 'NO (S0)'}\n`;
|
||||||
|
guide += `Time Limit : ${time === '0' ? 'Unlimited' : time + ' seconds'}\n`;
|
||||||
|
|
||||||
|
if (data.hint && data.hint.type) {
|
||||||
|
guide += `Hint Type : ${data.hint.type}\n`;
|
||||||
|
guide += `Hint Count : ${Array.isArray(data.hint.value) ? data.hint.value.length : '1'}\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
guide += `\n🔧 IMPLEMENTATION STEPS\n`;
|
||||||
|
guide += `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n`;
|
||||||
|
|
||||||
|
// Step 1: Initialize
|
||||||
|
guide += `1️⃣ INITIALIZE SDK\n`;
|
||||||
|
guide += ` var sdk = new SenaSDK('${gameCode}');\n`;
|
||||||
|
guide += ` sdk.load(function(success) {\n`;
|
||||||
|
guide += ` if (success) sdk.start();\n`;
|
||||||
|
guide += ` });\n\n`;
|
||||||
|
|
||||||
|
// Step 2: Display based on game type
|
||||||
|
guide += `2️⃣ DISPLAY UI\n`;
|
||||||
|
|
||||||
|
if (category === '1') {
|
||||||
|
// Quiz
|
||||||
|
guide += ` // Display Question\n`;
|
||||||
|
if (qIdx === '0') {
|
||||||
|
guide += ` var questionText = sdk.getQuestionValue();\n`;
|
||||||
|
guide += ` displayText(questionText); // Show text\n`;
|
||||||
|
} else if (qIdx === '1') {
|
||||||
|
guide += ` var questionImg = sdk.getQuestionValue();\n`;
|
||||||
|
guide += ` displayImage(questionImg); // Show image URL\n`;
|
||||||
|
} else if (qIdx === '2') {
|
||||||
|
guide += ` sdk.playVoice('question'); // Auto play audio\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
guide += `\n // Display Options\n`;
|
||||||
|
guide += ` var optionsCount = sdk.getOptionsCount(); // ${count} items\n`;
|
||||||
|
guide += ` for (var i = 0; i < optionsCount; i++) {\n`;
|
||||||
|
if (oIdx === '0') {
|
||||||
|
guide += ` var optionText = sdk.getOptionsValue(i).text;\n`;
|
||||||
|
guide += ` createButton(optionText, i); // Show text button\n`;
|
||||||
|
} else if (oIdx === '1') {
|
||||||
|
guide += ` var optionImg = sdk.getOptionsValue(i).image;\n`;
|
||||||
|
guide += ` createImageButton(optionImg, i); // Show image button\n`;
|
||||||
|
} else if (oIdx === '2') {
|
||||||
|
guide += ` createAudioButton(i); // Button to play audio\n`;
|
||||||
|
guide += ` // onClick: sdk.playVoice('option' + (i+1));\n`;
|
||||||
|
}
|
||||||
|
guide += ` }\n`;
|
||||||
|
|
||||||
|
} else if (category === '2') {
|
||||||
|
// Sort/Sequences
|
||||||
|
guide += ` // Display Hint (if exists)\n`;
|
||||||
|
if (qIdx === '0') {
|
||||||
|
guide += ` var hintText = sdk.getQuestionValue() || sdk.getRequestValue();\n`;
|
||||||
|
guide += ` if (hintText) displayHint(hintText);\n`;
|
||||||
|
} else if (qIdx === '1') {
|
||||||
|
guide += ` var hintImg = sdk.getQuestionValue();\n`;
|
||||||
|
guide += ` if (hintImg) displayHintImage(hintImg);\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
guide += `\n // Display Draggable Items\n`;
|
||||||
|
guide += ` var itemsCount = sdk.getOptionsCount();\n`;
|
||||||
|
guide += ` for (var i = 0; i < itemsCount; i++) {\n`;
|
||||||
|
if (oIdx === '0') {
|
||||||
|
guide += ` var itemText = sdk.getOptionsValue(i).text;\n`;
|
||||||
|
guide += ` createDraggableText(itemText, i);\n`;
|
||||||
|
} else if (oIdx === '1') {
|
||||||
|
guide += ` var itemImg = sdk.getOptionsValue(i).image;\n`;
|
||||||
|
guide += ` createDraggableImage(itemImg, i);\n`;
|
||||||
|
}
|
||||||
|
guide += ` }\n`;
|
||||||
|
guide += ` // User drags to reorder items\n`;
|
||||||
|
|
||||||
|
} else if (category === '3') {
|
||||||
|
// Memory/Matching
|
||||||
|
guide += ` var itemsCount = sdk.getOptionsCount();\n`;
|
||||||
|
if (qIdx === oIdx) {
|
||||||
|
guide += ` // Memory Card - Create pairs\n`;
|
||||||
|
guide += ` var allCards = []; // Duplicate items for pairs\n`;
|
||||||
|
guide += ` for (var i = 0; i < itemsCount; i++) {\n`;
|
||||||
|
guide += ` allCards.push(sdk.getOptionsValue(i));\n`;
|
||||||
|
guide += ` allCards.push(sdk.getOptionsValue(i)); // Duplicate\n`;
|
||||||
|
guide += ` }\n`;
|
||||||
|
guide += ` shuffleArray(allCards);\n`;
|
||||||
|
guide += ` // Create face-down cards, flip on click\n`;
|
||||||
|
} else {
|
||||||
|
guide += ` // Matching - Create two groups\n`;
|
||||||
|
guide += ` for (var i = 0; i < itemsCount; i++) {\n`;
|
||||||
|
if (qIdx === '0') {
|
||||||
|
guide += ` var leftText = sdk.getOptionsValue(i).text;\n`;
|
||||||
|
guide += ` createLeftItem(leftText, i);\n`;
|
||||||
|
} else if (qIdx === '1') {
|
||||||
|
guide += ` var leftImg = sdk.getOptionsValue(i).image;\n`;
|
||||||
|
guide += ` createLeftItem(leftImg, i);\n`;
|
||||||
|
}
|
||||||
|
if (oIdx === '0') {
|
||||||
|
guide += ` var rightText = sdk.getOptionsValue(i).text; // Matching pair\n`;
|
||||||
|
guide += ` createRightItem(rightText, i);\n`;
|
||||||
|
} else if (oIdx === '1') {
|
||||||
|
guide += ` var rightImg = sdk.getOptionsValue(i).image;\n`;
|
||||||
|
guide += ` createRightItem(rightImg, i);\n`;
|
||||||
|
}
|
||||||
|
guide += ` }\n`;
|
||||||
|
guide += ` // User draws lines to match pairs\n`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 3: Handle Hint
|
||||||
|
if (data.hint && data.hint.type) {
|
||||||
|
guide += `\n3️⃣ HANDLE HINT (Optional)\n`;
|
||||||
|
guide += ` var hintType = sdk.getHintType(); // "${data.hint.type}"\n`;
|
||||||
|
if (data.hint.type === 'display') {
|
||||||
|
guide += ` var hintCount = sdk.getHintCount();\n`;
|
||||||
|
guide += ` for (var i = 0; i < hintCount; i++) {\n`;
|
||||||
|
guide += ` var hintItem = sdk.getHintValue(i);\n`;
|
||||||
|
guide += ` displayHintItem(hintItem, i); // Show each hint\n`;
|
||||||
|
guide += ` }\n`;
|
||||||
|
} else if (data.hint.type === 'audio') {
|
||||||
|
guide += ` var hintAudio = sdk.getHintValue();\n`;
|
||||||
|
guide += ` createHintButton(hintAudio); // Play audio hint\n`;
|
||||||
|
} else if (data.hint.type === 'text') {
|
||||||
|
guide += ` var hintText = sdk.getHintValue();\n`;
|
||||||
|
guide += ` displayHintText(hintText);\n`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 4: Check Answer
|
||||||
|
const stepNum = data.hint ? '4️⃣' : '3️⃣';
|
||||||
|
guide += `\n${stepNum} CHECK ANSWER\n`;
|
||||||
|
|
||||||
|
if (category === '1') {
|
||||||
|
guide += ` // User clicks an option\n`;
|
||||||
|
guide += ` function onOptionClick(selectedIndex) {\n`;
|
||||||
|
guide += ` var userAnswer = sdk.getOptionsValue(selectedIndex).text;\n`;
|
||||||
|
guide += ` var result = sdk.end(userAnswer, function(isCorrect) {\n`;
|
||||||
|
guide += ` if (isCorrect) showSuccess();\n`;
|
||||||
|
guide += ` else showError();\n`;
|
||||||
|
guide += ` });\n`;
|
||||||
|
guide += ` }\n`;
|
||||||
|
} else if (category === '2') {
|
||||||
|
guide += ` // User finishes sorting\n`;
|
||||||
|
guide += ` function onSubmitOrder(orderedArray) {\n`;
|
||||||
|
guide += ` var answerStr = orderedArray.map(item => item.text).join('|');\n`;
|
||||||
|
guide += ` sdk.end(answerStr, function(isCorrect) {\n`;
|
||||||
|
guide += ` if (isCorrect) showSuccess();\n`;
|
||||||
|
guide += ` else showCorrectOrder();\n`;
|
||||||
|
guide += ` });\n`;
|
||||||
|
guide += ` }\n`;
|
||||||
|
} else if (category === '3') {
|
||||||
|
guide += ` // User completes all matches/pairs\n`;
|
||||||
|
guide += ` function onAllMatched(matchedPairs) {\n`;
|
||||||
|
guide += ` var answerStr = matchedPairs.map(p => p.text).join('|');\n`;
|
||||||
|
guide += ` sdk.end(answerStr, function(isCorrect) {\n`;
|
||||||
|
guide += ` showResult(isCorrect);\n`;
|
||||||
|
guide += ` });\n`;
|
||||||
|
guide += ` }\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 5: Timer
|
||||||
|
if (time !== '0') {
|
||||||
|
const nextStep = data.hint ? '5️⃣' : '4️⃣';
|
||||||
|
guide += `\n${nextStep} TIMER COUNTDOWN\n`;
|
||||||
|
guide += ` var timeLimit = sdk.timeLimit; // ${time} seconds\n`;
|
||||||
|
guide += ` startCountdown(timeLimit);\n`;
|
||||||
|
guide += ` // If time runs out before sdk.end(), user fails\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// API Reference
|
||||||
|
guide += `\n\n📚 KEY API METHODS\n`;
|
||||||
|
guide += `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`;
|
||||||
|
guide += `sdk.load(callback, template) - Load data from server\n`;
|
||||||
|
guide += `sdk.start() - Start game, shuffle if needed\n`;
|
||||||
|
guide += `sdk.getQuestionValue() - Get question text/image/audio\n`;
|
||||||
|
guide += `sdk.getQuestionType() - Get question type (text/image/audio)\n`;
|
||||||
|
guide += `sdk.getOptionsCount() - Get number of options\n`;
|
||||||
|
guide += `sdk.getOptionsValue(index) - Get option object at index\n`;
|
||||||
|
guide += `sdk.getOptionsType() - Get options type\n`;
|
||||||
|
guide += `sdk.getHintType() - Get hint type\n`;
|
||||||
|
guide += `sdk.getHintValue(index) - Get hint value/array item\n`;
|
||||||
|
guide += `sdk.getHintCount() - Get hint items count\n`;
|
||||||
|
guide += `sdk.playVoice(type) - Play TTS (question/option1/hint)\n`;
|
||||||
|
guide += `sdk.end(answer, callback) - Check answer & return result\n`;
|
||||||
|
guide += `sdk.timeLimit - Time limit in seconds\n`;
|
||||||
|
guide += `sdk.shuffle - Whether to shuffle options\n`;
|
||||||
|
|
||||||
|
guide += `\n\n💡 TIPS\n`;
|
||||||
|
guide += `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`;
|
||||||
|
if (shuffle === '1') {
|
||||||
|
guide += `• Options are shuffled after sdk.start() - display in new order\n`;
|
||||||
|
}
|
||||||
|
if (time !== '0') {
|
||||||
|
guide += `• Implement timer UI and auto-submit when time expires\n`;
|
||||||
|
}
|
||||||
|
guide += `• Use Web Speech API: sdk.playVoice() for TTS in English\n`;
|
||||||
|
guide += `• Multiple answers format: "answer1|answer2|answer3"\n`;
|
||||||
|
guide += `• sdk.end() returns: {isCorrect, duration, correctAnswer, userAnswer}\n`;
|
||||||
|
|
||||||
|
guide += `\n${'═'.repeat(76)}\n`;
|
||||||
|
|
||||||
|
return guide;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Get the question text
|
||||||
|
* @returns {string} Question or request text
|
||||||
|
*/
|
||||||
|
SenaSDK.prototype.getQuestionValue = function() {
|
||||||
|
if (this.data.question && this.data.question !== "") {
|
||||||
|
return this.data.question;
|
||||||
|
} else {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Get the question type
|
||||||
|
* @returns {string} Question type (text, image, audio)
|
||||||
|
*/
|
||||||
|
SenaSDK.prototype.getQuestionType = function() {
|
||||||
|
let self = this;
|
||||||
|
// based on game code, determine question type
|
||||||
|
const gameCode = self.gameCode || 'G2510S1T30';
|
||||||
|
const regex = /^G([1-5])([2-9])([0-2])([0-2])(?:S([0-1]))?(?:T(\d+))?$/;
|
||||||
|
const match = gameCode.match(regex);
|
||||||
|
if (match) {
|
||||||
|
const qIdx = match[3];
|
||||||
|
const types = { '0': 'text', '1': 'image', '2': 'audio' };
|
||||||
|
return types[qIdx] || 'text';
|
||||||
|
}
|
||||||
|
return 'text';
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the request value
|
||||||
|
* @returns {string} Request text
|
||||||
|
*/
|
||||||
|
SenaSDK.prototype.getRequestValue = function() {
|
||||||
|
if (this.data && this.data.request && this.data.request !== "") {
|
||||||
|
return this.data.request;
|
||||||
|
} else {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the request type (same as question type)
|
||||||
|
* @returns {string} Request type (text, image, audio)
|
||||||
|
*/
|
||||||
|
SenaSDK.prototype.getRequestType = function() {
|
||||||
|
return this.getQuestionType();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get total number of options
|
||||||
|
* @returns {number} Number of options
|
||||||
|
*/
|
||||||
|
SenaSDK.prototype.getOptionsCount = function() {
|
||||||
|
return this.data.options.length;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get options type based on game code
|
||||||
|
* @returns {string} Options type (text, image, audio)
|
||||||
|
*/
|
||||||
|
SenaSDK.prototype.getOptionsType = function() {
|
||||||
|
let self = this;
|
||||||
|
// based on game code, determine options type
|
||||||
|
const gameCode = self.gameCode || 'G2510S1T30';
|
||||||
|
const regex = /^G([1-5])([2-9])([0-2])([0-2])(?:S([0-1]))?(?:T(\d+))?$/;
|
||||||
|
const match = gameCode.match(regex);
|
||||||
|
if (match) {
|
||||||
|
const oIdx = match[4];
|
||||||
|
const types = { '0': 'text', '1': 'image', '2': 'audio' };
|
||||||
|
return types[oIdx] || 'text';
|
||||||
|
}
|
||||||
|
return 'text';
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Get option value by index based on options type from game code
|
||||||
|
* @param {number} index - Option index
|
||||||
|
* @returns {string} Option value (text, image URL, or audio URL based on game code)
|
||||||
|
*/
|
||||||
|
SenaSDK.prototype.getOptionsValue = function(index) {
|
||||||
|
return this.data.options[index];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get hint type
|
||||||
|
* @returns {string} Hint type (e.g., 'display', 'audio', 'text', or empty string if no hint)
|
||||||
|
*/
|
||||||
|
SenaSDK.prototype.getHintType = function() {
|
||||||
|
if (this.data && this.data.hint && this.data.hint.type) {
|
||||||
|
return this.data.hint.type;
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get hint count (number of elements if hint is an array, particularly for display type)
|
||||||
|
* @returns {number} Number of elements in hint array, or 1 if not an array, or 0 if no hint
|
||||||
|
*/
|
||||||
|
SenaSDK.prototype.getHintCount = function() {
|
||||||
|
const hintValue = this.getHintValue();
|
||||||
|
if (hintValue === null) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (Array.isArray(hintValue)) {
|
||||||
|
return hintValue.length;
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get hint value
|
||||||
|
* @param {number} index - Optional index for array hints (display type)
|
||||||
|
* @returns {*} Hint value (string, array element, or null if no hint)
|
||||||
|
*/
|
||||||
|
SenaSDK.prototype.getHintValue = function(index) {
|
||||||
|
if (this.data && this.data.hint && this.data.hint.value !== undefined) {
|
||||||
|
const hintValue = this.data.hint.value;
|
||||||
|
const hintType = this.getHintType();
|
||||||
|
|
||||||
|
// If hint type is display and value is array, return specific index
|
||||||
|
if (hintType === 'display' && Array.isArray(hintValue)) {
|
||||||
|
if (index !== undefined && index >= 0 && index < hintValue.length) {
|
||||||
|
return hintValue[index];
|
||||||
|
}
|
||||||
|
// If no index provided or invalid, return the whole array
|
||||||
|
return hintValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For audio or text type, return the value directly
|
||||||
|
return hintValue;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start the quiz - resets index and shuffles options
|
||||||
|
*/
|
||||||
|
SenaSDK.prototype.start = function() {
|
||||||
|
let self = this;
|
||||||
|
self.curIndex = 0;
|
||||||
|
if (self.shuffle) {
|
||||||
|
self.shuffleArray(self.data.options);
|
||||||
|
}
|
||||||
|
self.startTime = Date.now();
|
||||||
|
// Additional logic for tracking can be added here if needed
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* End the game and check answer
|
||||||
|
* @param {string} answer - User's answer (single text or multiple answers separated by |)
|
||||||
|
* @returns {Object} Result object with isCorrect, duration, correctAnswer, and userAnswer
|
||||||
|
*/
|
||||||
|
SenaSDK.prototype.end = function(answer , callback) {
|
||||||
|
let self = this;
|
||||||
|
self.endTime = Date.now();
|
||||||
|
const duration = (self.endTime - self.startTime) / 1000;
|
||||||
|
|
||||||
|
// Parse user answer - split by | for multiple answers
|
||||||
|
const userAnswers = answer.includes('|') ? answer.split('|').map(a => a.trim().toLowerCase()) : [answer.trim().toLowerCase()];
|
||||||
|
|
||||||
|
// Get correct answer(s) from data
|
||||||
|
let correctAnswers = [];
|
||||||
|
if (self.correctAnswer) {
|
||||||
|
// Check if answer is an array (multiple answers) or single answer
|
||||||
|
if (Array.isArray(self.correctAnswer)) {
|
||||||
|
correctAnswers = self.correctAnswer.map(a => {
|
||||||
|
if (typeof a === 'string') return a.toLowerCase();
|
||||||
|
if (a.text) return a.text.toLowerCase();
|
||||||
|
return '';
|
||||||
|
});
|
||||||
|
} else if (typeof self.correctAnswer === 'string') {
|
||||||
|
correctAnswers = [self.correctAnswer.toLowerCase()];
|
||||||
|
} else if (self.correctAnswer.text) {
|
||||||
|
correctAnswers = [self.correctAnswer.text.toLowerCase()];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if answer is correct
|
||||||
|
let isCorrect = false;
|
||||||
|
if (userAnswers.length === correctAnswers.length) {
|
||||||
|
// For ordered multiple answers
|
||||||
|
isCorrect = userAnswers.every((ans, index) => ans === correctAnswers[index]);
|
||||||
|
} else if (userAnswers.length === 1 && correctAnswers.length === 1) {
|
||||||
|
// For single answer
|
||||||
|
isCorrect = userAnswers[0] === correctAnswers[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = {
|
||||||
|
isCorrect: isCorrect,
|
||||||
|
duration: duration,
|
||||||
|
correctAnswer: correctAnswers.join(' | '),
|
||||||
|
userAnswer: userAnswers.join(' | ')
|
||||||
|
};
|
||||||
|
// if time spent more than time limit, mark as incorrect
|
||||||
|
if (self.timeLimit > 0 && duration > self.timeLimit) {
|
||||||
|
result.isCorrect = false;
|
||||||
|
}
|
||||||
|
console.log(`Time spent in game: ${duration} seconds`);
|
||||||
|
console.log(`Result: ${isCorrect ? 'CORRECT' : 'INCORRECT'}`);
|
||||||
|
if (callback) callback(result.isCorrect);
|
||||||
|
};
|
||||||
|
|
||||||
|
SenaSDK.prototype.playVoice = function(type) {
|
||||||
|
let self = this;
|
||||||
|
// type: 'question', 'optionA', 'optionB', ...
|
||||||
|
// if type is options, get corresponding option text like option0 -> index 0
|
||||||
|
let textToSpeak = '';
|
||||||
|
if (type.startsWith('option')) {
|
||||||
|
const optionIndex = parseInt(type.slice(6)) - 1;
|
||||||
|
textToSpeak = self.getOptionsValue(optionIndex);
|
||||||
|
} else if (type === 'question') {
|
||||||
|
textToSpeak = self.getQuestionValue();
|
||||||
|
} else if (type === 'request') {
|
||||||
|
textToSpeak = self.getRequestValue();
|
||||||
|
} else if (type === 'hint') {
|
||||||
|
const hintValue = self.getHintValue();
|
||||||
|
if (typeof hintValue === 'string') {
|
||||||
|
textToSpeak = hintValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(textToSpeak == "") return;
|
||||||
|
if (window['audioPlayer']) {
|
||||||
|
window['audioPlayer'].pause();
|
||||||
|
window['audioPlayer'].src = textToSpeak;
|
||||||
|
window['audioPlayer'].play();
|
||||||
|
} else {
|
||||||
|
window['audioPlayer'] = new Audio(textToSpeak);
|
||||||
|
window['audioPlayer'].play();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
SenaSDK.prototype.helper = {};
|
||||||
|
|
||||||
|
SenaSDK.prototype.helper.CalcObjectPositions = function(n, objectWidth, margin, maxWidth) {
|
||||||
|
let self = this;
|
||||||
|
self.positions = [];
|
||||||
|
const totalWidth = n * objectWidth + (n - 1) * margin;
|
||||||
|
const startX = (maxWidth - totalWidth) / 2;
|
||||||
|
let positions = [];
|
||||||
|
for (let i = 0; i < n; i++) {
|
||||||
|
positions.push(startX + i * (objectWidth + margin));
|
||||||
|
}
|
||||||
|
positions.map(pos => pos + objectWidth / 2); // Adjusting to center the objects
|
||||||
|
self.positions = positions;
|
||||||
|
};
|
||||||
|
SenaSDK.prototype.helper.getPosXbyIndex = function(index) {
|
||||||
|
let self = this;
|
||||||
|
if (index < 0 || index >= self.positions.length) {
|
||||||
|
return null; // Return null if index is out of bounds
|
||||||
|
}
|
||||||
|
return self.positions[index];
|
||||||
|
};
|
||||||
|
// Export for different module systems
|
||||||
|
if (typeof module !== 'undefined' && module.exports) {
|
||||||
|
module.exports = SenaSDK;
|
||||||
|
} else if (typeof define === 'function' && define.amd) {
|
||||||
|
define([], function() { return SenaSDK; });
|
||||||
|
} else {
|
||||||
|
window.SenaSDK = SenaSDK;
|
||||||
|
}
|
||||||
1
audio_spelling_sequence_sentence/tdv_sdk.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
// hé lô quơ
|
||||||
3
readme.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
Sequence Word TextOnly
|
||||||
|
Mô tả của trò chơi
|
||||||
|
Sequence audio_spelling_sequence_sentence
|
||||||