first commit

This commit is contained in:
silverpro89
2026-02-04 12:16:07 +07:00
commit f687007631
64 changed files with 28818 additions and 0 deletions

View File

@@ -0,0 +1,28 @@
{
"name": "QuizLucky",
"short_name": "QuizLucky",
"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"
}]
}

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 176 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 556 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 227 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 343 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1016 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 324 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 385 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

View File

@@ -0,0 +1,139 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<title>QuizLucky</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="1400" height="900">
<!-- 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>
</body>
</html>

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,64 @@
{
"version": 1770029488,
"fileList": [
"data.js",
"c2runtime.js",
"jquery-3.4.1.min.js",
"offlineClient.js",
"images/sprite-sheet0.png",
"images/main-sheet0.png",
"images/main-sheet1.png",
"images/spin-sheet0.png",
"images/spin-sheet1.png",
"images/backiconcowboy-sheet0.png",
"images/g3_point-sheet0.png",
"images/bg-default-000.jpg",
"images/l2_-sheet0.png",
"images/l1_-sheet0.png",
"images/l1_-sheet1.png",
"images/l2_2-sheet0.png",
"images/win_popup-sheet0.png",
"images/lose_popup-sheet0.png",
"images/l2_3-sheet0.png",
"images/replay_btn-sheet0.png",
"images/obj_-sheet0.png",
"images/obj_2-sheet0.png",
"images/obj_3-sheet0.png",
"images/particles.png",
"images/asset2_-sheet0.png",
"images/score_effect.png",
"images/fox_-sheet0.png",
"images/fox_-sheet1.png",
"images/txt_answer.png",
"images/answer-sheet0.png",
"images/wrong-sheet0.png",
"images/wrong2-sheet0.png",
"images/untitled4_6-sheet0.png",
"images/g3_point4-sheet0.png",
"images/huongdan-sheet0.png",
"images/batdau_btn-sheet0.png",
"images/bg2-sheet0.png",
"images/sound-sheet0.png",
"images/bg_intro-default-000.jpg",
"images/txt_answers.png",
"images/btn_speaker-sheet0.png",
"images/sprite2-sheet0.png",
"images/sprite3-sheet0.png",
"images/sprite4-sheet0.png",
"images/tag_abcd-sheet0.png",
"media/coin.ogg",
"media/freepik-jolly-vibes.ogg",
"media/correct.ogg",
"media/fail.ogg",
"media/click.ogg",
"media/dice-142528.ogg",
"media/clock-alarm-8761.ogg",
"icon-16.png",
"icon-32.png",
"icon-114.png",
"icon-128.png",
"icon-256.png",
"loading-logo.png",
"tdv_sdk.js"
]
}

View File

@@ -0,0 +1,53 @@
"use strict";
(function() {
class OfflineClient
{
constructor()
{
// Create a BroadcastChannel, if supported.
this._broadcastChannel = (typeof BroadcastChannel === "undefined" ? null : new BroadcastChannel("offline"));
// Queue of messages received before a message callback is set.
this._queuedMessages = [];
// The message callback.
this._onMessageCallback = null;
// If BroadcastChannel is supported, listen for messages.
if (this._broadcastChannel)
this._broadcastChannel.onmessage = (e => this._OnBroadcastChannelMessage(e));
}
_OnBroadcastChannelMessage(e)
{
// Have a message callback set: just forward the call.
if (this._onMessageCallback)
{
this._onMessageCallback(e);
return;
}
// Otherwise the app hasn't loaded far enough to set a message callback.
// Buffer the incoming messages to replay when the app sets a callback.
this._queuedMessages.push(e);
}
SetMessageCallback(f)
{
this._onMessageCallback = f;
// Replay any queued messages through the handler, then clear the queue.
for (let e of this._queuedMessages)
this._onMessageCallback(e);
this._queuedMessages.length = 0;
}
};
// Create the offline client ASAP so we receive and start queueing any messages the SW broadcasts.
window.OfflineClientInfo = new OfflineClient();
}());

View File

@@ -0,0 +1,403 @@
"use strict";
const OFFLINE_DATA_FILE = "offline.js";
const CACHE_NAME_PREFIX = "c2offline";
const BROADCASTCHANNEL_NAME = "offline";
const CONSOLE_PREFIX = "[SW] ";
const LAZYLOAD_KEYNAME = "";
// Create a BroadcastChannel if supported.
const broadcastChannel = (typeof BroadcastChannel === "undefined" ? null : new BroadcastChannel(BROADCASTCHANNEL_NAME));
//////////////////////////////////////
// Utility methods
function PostBroadcastMessage(o)
{
if (!broadcastChannel)
return; // not supported
// Impose artificial (and arbitrary!) delay of 3 seconds to make sure client is listening by the time the message is sent.
// Note we could remove the delay on some messages, but then we create a race condition where sometimes messages can arrive
// in the wrong order (e.g. "update ready" arrives before "started downloading update"). So to keep the consistent ordering,
// delay all messages by the same amount.
setTimeout(() => broadcastChannel.postMessage(o), 3000);
};
function Broadcast(type)
{
PostBroadcastMessage({
"type": type
});
};
function BroadcastDownloadingUpdate(version)
{
PostBroadcastMessage({
"type": "downloading-update",
"version": version
});
}
function BroadcastUpdateReady(version)
{
PostBroadcastMessage({
"type": "update-ready",
"version": version
});
}
function IsUrlInLazyLoadList(url, lazyLoadList)
{
if (!lazyLoadList)
return false; // presumably lazy load list failed to load
try {
for (const lazyLoadRegex of lazyLoadList)
{
if (new RegExp(lazyLoadRegex).test(url))
return true;
}
}
catch (err)
{
console.error(CONSOLE_PREFIX + "Error matching in lazy-load list: ", err);
}
return false;
};
function WriteLazyLoadListToStorage(lazyLoadList)
{
if (typeof localforage === "undefined")
return Promise.resolve(); // bypass if localforage not imported
else
return localforage.setItem(LAZYLOAD_KEYNAME, lazyLoadList)
};
function ReadLazyLoadListFromStorage()
{
if (typeof localforage === "undefined")
return Promise.resolve([]); // bypass if localforage not imported
else
return localforage.getItem(LAZYLOAD_KEYNAME);
};
function GetCacheBaseName()
{
// Include the scope to avoid name collisions with any other SWs on the same origin.
// e.g. "c2offline-https://example.com/foo/" (won't collide with anything under bar/)
return CACHE_NAME_PREFIX + "-" + self.registration.scope;
};
function GetCacheVersionName(version)
{
// Append the version number to the cache name.
// e.g. "c2offline-https://example.com/foo/-v2"
return GetCacheBaseName() + "-v" + version;
};
// Return caches.keys() filtered down to just caches we're interested in (with the right base name).
// This filters out caches from unrelated scopes.
async function GetAvailableCacheNames()
{
const cacheNames = await caches.keys();
const cacheBaseName = GetCacheBaseName();
return cacheNames.filter(n => n.startsWith(cacheBaseName));
};
// Identify if an update is pending, which is the case when we have 2 or more available caches.
// One must be an update that is waiting, since the next navigate that does an upgrade will
// delete all the old caches leaving just one currently-in-use cache.
async function IsUpdatePending()
{
const availableCacheNames = await GetAvailableCacheNames();
return (availableCacheNames.length >= 2);
};
// Automatically deduce the main page URL (e.g. index.html or main.aspx) from the available browser windows.
// This prevents having to hard-code an index page in the file list, implicitly caching it like AppCache did.
async function GetMainPageUrl()
{
const allClients = await clients.matchAll({
includeUncontrolled: true,
type: "window"
});
for (const c of allClients)
{
// Parse off the scope from the full client URL, e.g. https://example.com/index.html -> index.html
let url = c.url;
if (url.startsWith(self.registration.scope))
url = url.substring(self.registration.scope.length);
if (url && url !== "/") // ./ is also implicitly cached so don't bother returning that
{
// If the URL is solely a search string, prefix it with / to ensure it caches correctly.
// e.g. https://example.com/?foo=bar needs to cache as /?foo=bar, not just ?foo=bar.
if (url.startsWith("?"))
url = "/" + url;
return url;
}
}
return ""; // no main page URL could be identified
};
// Hack to fetch optionally bypassing HTTP cache until fetch cache options are supported in Chrome (crbug.com/453190)
function fetchWithBypass(request, bypassCache)
{
if (typeof request === "string")
request = new Request(request);
if (bypassCache)
{
// bypass enabled: add a random search parameter to avoid getting a stale HTTP cache result
const url = new URL(request.url);
url.search += Math.floor(Math.random() * 1000000);
return fetch(url, {
headers: request.headers,
mode: request.mode,
credentials: request.credentials,
redirect: request.redirect,
cache: "no-store"
});
}
else
{
// bypass disabled: perform normal fetch which is allowed to return from HTTP cache
return fetch(request);
}
};
// Effectively a cache.addAll() that only creates the cache on all requests being successful (as a weak attempt at making it atomic)
// and can optionally cache-bypass with fetchWithBypass in every request
async function CreateCacheFromFileList(cacheName, fileList, bypassCache)
{
// Kick off all requests and wait for them all to complete
const responses = await Promise.all(fileList.map(url => fetchWithBypass(url, bypassCache)));
// Check if any request failed. If so don't move on to opening the cache.
// This makes sure we only open a cache if all requests succeeded.
let allOk = true;
for (const response of responses)
{
if (!response.ok)
{
allOk = false;
console.error(CONSOLE_PREFIX + "Error fetching '" + response.url + "' (" + response.status + " " + response.statusText + ")");
}
}
if (!allOk)
throw new Error("not all resources were fetched successfully");
// Can now assume all responses are OK. Open a cache and write all responses there.
// TODO: ideally we can do this transactionally to ensure a complete cache is written as one atomic operation.
// This needs either new transactional features in the spec, or at the very least a way to rename a cache
// (so we can write to a temporary name that won't be returned by GetAvailableCacheNames() and then rename it when ready).
const cache = await caches.open(cacheName);
try {
return await Promise.all(responses.map(
(response, i) => cache.put(fileList[i], response)
));
}
catch (err)
{
// Not sure why cache.put() would fail (maybe if storage quota exceeded?) but in case it does,
// clean up the cache to try to avoid leaving behind an incomplete cache.
console.error(CONSOLE_PREFIX + "Error writing cache entries: ", err);
caches.delete(cacheName);
throw err;
}
};
async function UpdateCheck(isFirst)
{
try {
// Always bypass cache when requesting offline.js to make sure we find out about new versions.
const response = await fetchWithBypass(OFFLINE_DATA_FILE, true);
if (!response.ok)
throw new Error(OFFLINE_DATA_FILE + " responded with " + response.status + " " + response.statusText);
const data = await response.json();
const version = data.version;
const fileList = data.fileList;
const lazyLoadList = data.lazyLoad;
const currentCacheName = GetCacheVersionName(version);
const cacheExists = await caches.has(currentCacheName);
// Don't recache if there is already a cache that exists for this version. Assume it is complete.
if (cacheExists)
{
// Log whether we are up-to-date or pending an update.
const isUpdatePending = await IsUpdatePending();
if (isUpdatePending)
{
console.log(CONSOLE_PREFIX + "Update pending");
Broadcast("update-pending");
}
else
{
console.log(CONSOLE_PREFIX + "Up to date");
Broadcast("up-to-date");
}
return;
}
// Implicitly add the main page URL to the file list, e.g. "index.html", so we don't have to assume a specific name.
const mainPageUrl = await GetMainPageUrl();
// Prepend the main page URL to the file list if we found one and it is not already in the list.
// Also make sure we request the base / which should serve the main page.
fileList.unshift("./");
if (mainPageUrl && fileList.indexOf(mainPageUrl) === -1)
fileList.unshift(mainPageUrl);
console.log(CONSOLE_PREFIX + "Caching " + fileList.length + " files for offline use");
if (isFirst)
Broadcast("downloading");
else
BroadcastDownloadingUpdate(version);
// Note we don't bypass the cache on the first update check. This is because SW installation and the following
// update check caching will race with the normal page load requests. For any normal loading fetches that have already
// completed or are in-flight, it is pointless and wasteful to cache-bust the request for offline caching, since that
// forces a second network request to be issued when a response from the browser HTTP cache would be fine.
if (lazyLoadList)
await WriteLazyLoadListToStorage(lazyLoadList); // dump lazy load list to local storage#
await CreateCacheFromFileList(currentCacheName, fileList, !isFirst);
const isUpdatePending = await IsUpdatePending();
if (isUpdatePending)
{
console.log(CONSOLE_PREFIX + "All resources saved, update ready");
BroadcastUpdateReady(version);
}
else
{
console.log(CONSOLE_PREFIX + "All resources saved, offline support ready");
Broadcast("offline-ready");
}
}
catch (err)
{
// Update check fetches fail when we're offline, but in case there's any other kind of problem with it, log a warning.
console.warn(CONSOLE_PREFIX + "Update check failed: ", err);
}
};
self.addEventListener("install", event =>
{
// On install kick off an update check to cache files on first use.
// If it fails we can still complete the install event and leave the SW running, we'll just
// retry on the next navigate.
event.waitUntil(
UpdateCheck(true) // first update
.catch(() => null)
);
});
async function GetCacheNameToUse(availableCacheNames, doUpdateCheck)
{
// Prefer the oldest cache available. This avoids mixed-version responses by ensuring that if a new cache
// is created and filled due to an update check while the page is running, we keep returning resources
// from the original (oldest) cache only.
if (availableCacheNames.length === 1 || !doUpdateCheck)
return availableCacheNames[0];
// We are making a navigate request with more than one cache available. Check if we can expire any old ones.
const allClients = await clients.matchAll();
// If there are other clients open, don't expire anything yet. We don't want to delete any caches they
// might be using, which could cause mixed-version responses.
if (allClients.length > 1)
return availableCacheNames[0];
// Identify newest cache to use. Delete all the others.
const latestCacheName = availableCacheNames[availableCacheNames.length - 1];
console.log(CONSOLE_PREFIX + "Updating to new version");
await Promise.all(
availableCacheNames.slice(0, -1)
.map(c => caches.delete(c))
);
return latestCacheName;
};
async function HandleFetch(event, doUpdateCheck)
{
const availableCacheNames = await GetAvailableCacheNames();
// No caches available: go to network
if (!availableCacheNames.length)
return fetch(event.request);
const useCacheName = await GetCacheNameToUse(availableCacheNames, doUpdateCheck);
const cache = await caches.open(useCacheName);
const cachedResponse = await cache.match(event.request);
if (cachedResponse)
return cachedResponse; // use cached response
// We need to check if this request is to be lazy-cached. Send the request and load the lazy-load list
// from storage simultaneously.
const result = await Promise.all([fetch(event.request), ReadLazyLoadListFromStorage()]);
const fetchResponse = result[0];
const lazyLoadList = result[1];
if (IsUrlInLazyLoadList(event.request.url, lazyLoadList))
{
// Handle failure writing to the cache. This can happen if the storage quota is exceeded, which is particularly
// likely in Safari 11.1, which appears to have very tight storage limits. Make sure even in the event of an error
// we continue to return the response from the fetch.
try {
// Note clone response since we also respond with it
await cache.put(event.request, fetchResponse.clone());
}
catch (err)
{
console.warn(CONSOLE_PREFIX + "Error caching '" + event.request.url + "': ", err);
}
}
return fetchResponse;
};
self.addEventListener("fetch", event =>
{
/** NOTE (iain)
* This check is to prevent a bug with XMLHttpRequest where if its
* proxied with "FetchEvent.prototype.respondWith" no upload progress
* events are triggered. By returning we allow the default action to
* occur instead. Currently all cross-origin requests fall back to default.
*/
if (new URL(event.request.url).origin !== location.origin)
return;
// Check for an update on navigate requests
const doUpdateCheck = (event.request.mode === "navigate");
const responsePromise = HandleFetch(event, doUpdateCheck);
if (doUpdateCheck)
{
// allow the main request to complete, then check for updates
event.waitUntil(
responsePromise
.then(() => UpdateCheck(false)) // not first check
);
}
event.respondWith(responsePromise);
});

View File

@@ -0,0 +1,843 @@
var tdv_sdk = {};
tdv_sdk.list = [
// ==================== PART 1: MULTIPLE CHOICE (Q1-Q10) - DỄ → TRUNG BÌNH ====================
{
"id": 1,
"question": "Choose the correct greeting.",
"options": ["My name Rosy", "Hello, my name is Tim.", "Name is hello", "I class Tim"],
"answer": "Hello, my name is Tim.",
"audio": ""
},
{
"id": 2,
"question": "Which sentence is correct?",
"options": ["My name Tim.", "I name is Tim.", "My name is Tim.", "My Tim name."],
"answer": "My name is Tim.",
"audio": ""
},
{
"id": 3,
"question": "Which number comes after seven?",
"options": ["six", "eight", "nine", "ten"],
"answer": "eight",
"audio": ""
},
{
"id": 4,
"question": "Which word is NOT a toy?",
"options": ["doll", "teddy bear", "car", "pencil"],
"answer": "pencil",
"audio": ""
},
{
"id": 5,
"question": "Which word starts with the /b/ sound?",
"options": ["apple", "ball", "annie", "egg"],
"answer": "ball",
"audio": ""
},
{
"id": 6,
"question": "Which thing is usually in a pencil case?",
"options": ["door", "window", "eraser", "bag"],
"answer": "eraser",
"audio": ""
},
{
"id": 7,
"question": "Who is your dad's sister?",
"options": ["uncle", "cousin", "aunt", "brother"],
"answer": "aunt",
"audio": ""
},
{
"id": 8,
"question": "Which sentence is correct?",
"options": ["I has a sister.", "I have a sister.", "I am a sister.", "I sister have."],
"answer": "I have a sister.",
"audio": ""
},
{
"id": 9,
"question": "Which animal is NOT big?",
"options": ["elephant", "giraffe", "hippo", "fish"],
"answer": "fish",
"audio": ""
},
{
"id": 10,
"question": "Which word means 'not long'?",
"options": ["tall", "short", "thin", "little"],
"answer": "short",
"audio": ""
},
// ==================== PART 2: FILL IN THE BLANKS (Q11-Q20) - TRUNG BÌNH → KHÓ ====================
{
"id": 11,
"question": "Hello. My ___ is Billy.",
"options": ["class", "name", "my", "hello"],
"answer": "name",
"audio": ""
},
{
"id": 12,
"question": "I have a pen and a ___.",
"options": ["doors", "windows", "pencil", "bags"],
"answer": "pencil",
"audio": ""
},
{
"id": 13,
"question": "The giraffe is tall. The bird is ___.",
"options": ["big", "tall", "little", "long"],
"answer": "little",
"audio": ""
},
{
"id": 14,
"question": "Choose the correct sentence.",
"options": ["He have black hair.", "He has black hair.", "He is black hair.", "He black hair."],
"answer": "He has black hair.",
"audio": ""
},
{
"id": 15,
"question": "Which word has the /c/ sound?",
"options": ["dog", "desk", "cat", "fig"],
"answer": "cat",
"audio": ""
},
{
"id": 16,
"question": "Which word has the /d/ sound?",
"options": ["goat", "chicken", "dog", "cookie"],
"answer": "dog",
"audio": ""
},
{
"id": 17,
"question": "I eat rice and ___.",
"options": ["milk", "juice", "meat", "water"],
"answer": "meat",
"audio": ""
},
{
"id": 18,
"question": "Which word has the /i/ sound?",
"options": ["egg", "ink", "apple", "goat"],
"answer": "ink",
"audio": ""
},
{
"id": 19,
"question": "She has ___ hair. (not straight)",
"options": ["long", "short", "curly", "black"],
"answer": "curly",
"audio": ""
},
{
"id": 20,
"question": "Choose the correct sentence.",
"options": ["I have brown eye.", "I has brown eyes.", "I have brown eyes.", "I am brown eyes."],
"answer": "I have brown eyes.",
"audio": ""
},
// ==================== PART 3: LISTENING (Q21-Q30) - TRICKY VERSION ====================
{
"id": 21,
"question": "Who is Rosy's teacher?",
"options": ["Tim", "Billy", "Miss Jones", "Annie"],
"answer": "Miss Jones",
"audio": "https://audio.senaai.tech/audio/Sena_Voice1_hello_my_name_is_rosy_i_am_not_in_tims_class_i_am__26b5db3e.mp3"
},
{
"id": 22,
"question": "Which number does the speaker NOT count?",
"options": ["three", "four", "five", "six"],
"answer": "six",
"audio": "https://audio.senaai.tech/audio/Sena_Voice1_i_can_count_numbers_i_count_one_to_five_i_stop_at__11cfd4d6.mp3"
},
{
"id": 23,
"question": "Which toy does the speaker have?",
"options": ["doll", "teddy bear", "ball", "kite"],
"answer": "ball",
"audio": "https://audio.senaai.tech/audio/Sena_Voice1_i_have_many_toys_i_have_a_ball_and_a_car_i_dont_ha_d45aa04e.mp3"
},
{
"id": 24,
"question": "Where is the pencil?",
"options": ["in the bag", "on the desk", "in the pencil case", "under the book"],
"answer": "in the pencil case",
"audio": "https://audio.senaai.tech/audio/Sena_Voice1_look_in_my_bag_there_is_a_pencil_case_the_pencil_i_0b7bb024.mp3"
},
{
"id": 25,
"question": "Who does the speaker have?",
"options": ["sister", "cousin", "brother", "aunt"],
"answer": "brother",
"audio": "https://audio.senaai.tech/audio/Sena_Voice1_this_is_my_family_i_have_one_brother_i_dont_have_a_699c2d45.mp3"
},
{
"id": 26,
"question": "Which animal is big?",
"options": ["giraffe", "bird", "elephant", "fish"],
"answer": "elephant",
"audio": "https://audio.senaai.tech/audio/Sena_Voice1_i_see_animals_the_elephant_is_big_the_giraffe_is_t_2bd3f1eb.mp3"
},
{
"id": 27,
"question": "What does the speaker drink?",
"options": ["juice", "water", "milk", "yogurt"],
"answer": "milk",
"audio": "https://audio.senaai.tech/audio/Sena_Voice1_i_eat_rice_and_meat_i_drink_milk_not_juice_juice_i_3b913853.mp3"
},
{
"id": 28,
"question": "What is correct about his hair?",
"options": ["long brown hair", "short brown hair", "short hair", "long black hair"],
"answer": "short hair",
"audio": "https://audio.senaai.tech/audio/Sena_Voice1_my_brother_is_tall_he_has_short_hair_his_hair_is_n_8601e646.mp3"
},
{
"id": 29,
"question": "Where is the fish?",
"options": ["on the tree", "in the water", "on the desk", "in the bag"],
"answer": "in the water",
"audio": "https://audio.senaai.tech/audio/Sena_Voice1_i_see_a_cat_and_a_dog_the_bird_is_on_the_tree_the__722df16d.mp3"
},
{
"id": 30,
"question": "What does the speaker do first?",
"options": ["look out the window", "close the door", "open the door", "open the window"],
"answer": "open the door",
"audio": "https://audio.senaai.tech/audio/Sena_Voice1_i_open_the_door_i_look_out_the_window_i_close_the__9d3fa0c9.mp3"
},
// ==================== PART 4: TET BACK-UP (Q31-Q40) ====================
{
"id": 31,
"question": "We see many apricot blossoms at Tet in the South of Vietnam.\n\nWhat color are apricot blossoms usually?",
"options": ["Blue", "Yellow", "Purple", "Black"],
"answer": "Yellow",
"audio": ""
},
{
"id": 32,
"question": "At Tet, children get lucky money in a ____.",
"options": ["white box", "school bag", "red envelope", "paper bag"],
"answer": "red envelope",
"audio": ""
},
{
"id": 33,
"question": "What is its Vietnamese name?",
"options": ["Bánh mì", "Bánh tét", "Bánh chưng", "Bánh bao"],
"answer": "Bánh chưng",
"audio": ""
},
{
"id": 34,
"question": "People go to the flower market at Tet to ____.",
"options": ["buy books", "buy flowers", "buy clothes", "buy toys"],
"answer": "buy flowers",
"audio": ""
},
{
"id": 35,
"question": "At Tet, families often visit ____.",
"options": ["teachers", "classmates", "relatives and friends", "neighbors only"],
"answer": "relatives and friends",
"audio": ""
},
{
"id": 36,
"question": "What does 'fireworks' mean?",
"options": ["flowers", "bright lights in the sky", "thunders on the sky", "clothes"],
"answer": "bright lights in the sky",
"audio": ""
},
{
"id": 37,
"question": "People go to the pagoda at Tet to ____.",
"options": ["play games", "pray for good luck", "buy food", "watch TV"],
"answer": "pray for good luck",
"audio": ""
},
{
"id": 38,
"question": "Which sentence is correct?",
"options": ["I get lucky money in red envelope.", "I get lucky money on red envelope.", "I get lucky money from red envelope.", "I get lucky money in a red envelope."],
"answer": "I get lucky money in a red envelope.",
"audio": ""
},
{
"id": 39,
"question": "Which activity is NOT for Tet?",
"options": ["Getting lucky money", "Watching fireworks", "Visiting relatives", "Taking an English exam"],
"answer": "Taking an English exam",
"audio": ""
},
{
"id": 40,
"question": "What do they do at night?",
"options": ["Eat cake", "Visit friends", "Watch fireworks", "Go to school"],
"answer": "Watch fireworks",
"audio": [
"https://audio.senaai.tech/audio/Sena_Voice1_at_tet_my_family_eats_square_glutinous_rice_cake_w_9c54c1b1.mp3",
"https://audio.senaai.tech/audio/Sena_Voice1_at_night_we_watch_fireworks_5c7495f0.mp3"
]
}
];
tdv_sdk.currentIndex = 0;
tdv_sdk.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]];
}
};
tdv_sdk.start = function () {
tdv_sdk.currentIndex = 0;
tdv_sdk.listquestion = [];
tdv_sdk.totalquestion = 40;
for (let i = 0; i < tdv_sdk.totalquestion; i++) {
tdv_sdk.listquestion.push(tdv_sdk.list[i]);
}
};
tdv_sdk.loadquestion = function () {
// Kiểm tra xem còn câu hỏi không
if (tdv_sdk.currentIndex >= tdv_sdk.totalquestion) {
console.log("🎉 Game Over! No more questions.");
tdv_sdk.curQuestion = null;
tdv_sdk.questionState = -1; // State đặc biệt: Game Over
return 0; // Trả về 0 để Construct biết game đã kết thúc
}
tdv_sdk.curQuestion = tdv_sdk.listquestion[tdv_sdk.currentIndex];
tdv_sdk.correctAnswer = tdv_sdk.curQuestion.answer;
tdv_sdk.shuffleArray(tdv_sdk.curQuestion.options);
tdv_sdk.questionState = 0; // Reset state khi load câu hỏi mới
return 1; // Trả về 1 nếu load thành công
};
// ==================== QUESTION STATE MACHINE ====================
// State: 0 = Roll dice, 1 = Chờ start timer, 2 = Đang đếm, 3 = Chờ hiện đáp án, 4 = Chờ đóng layer
tdv_sdk.questionState = 0;
// Lấy state hiện tại (dùng để check trong Construct)
tdv_sdk.getQuestionState = function () {
return tdv_sdk.questionState;
};
// Set state (dùng khi cần set thủ công)
tdv_sdk.setQuestionState = function (state) {
tdv_sdk.questionState = state;
console.log("📍 Question State set to:", state);
return state;
};
// Xử lý khi bấm Space - trả về state mới
// questionState: 0 = Roll dice, 1 = Start timer, 2 = Đang đếm, 3 = Show answer, 4 = Close layer
tdv_sdk.handleSpacePress = function () {
switch (tdv_sdk.questionState) {
case 0:
// Space: Roll dice
console.log("🎲 Space 0: Roll dice!");
return 0;
case 1:
// Space: Bắt đầu đếm giờ (chờ hết giờ mới được bấm tiếp)
tdv_sdk.questionState = 2;
gameTimer.resetQuestionTimer();
console.log("▶️ Space 1: Timer started!");
return 1;
case 2:
// Đang đếm giờ - Bấm Space sẽ dừng timer và hiện đáp án ngay
gameTimer.stopTimer();
tdv_sdk.questionState = 3;
console.log("⏹️ Space 2: Timer stopped! Show answer.");
return 2;
case 3:
// Space: Hiện đáp án đúng
tdv_sdk.questionState = 4;
console.log("✅ Space 3: Show correct answer!");
return 3;
case 4:
// Space: Đóng layer câu hỏi
// LƯU Ý: KHÔNG gọi nextQuestion() ở đây vì Construct đã gọi riêng
tdv_sdk.questionState = 0;
console.log("🚪 Space 4: Close question layer!");
return 4;
default:
return tdv_sdk.questionState;
}
};
// Gọi khi bắt đầu hiện câu hỏi (sau khi thỏ di chuyển xong)
tdv_sdk.startQuestion = function () {
tdv_sdk.questionState = 1;
console.log("❓ Question ready! Press Space to start timer.");
return 1;
};
// Gọi khi hết giờ (timer = 0)
tdv_sdk.onTimeUp = function () {
if (tdv_sdk.questionState === 2) {
gameTimer.currentTime = 0; // Đảm bảo hiển thị 0
gameTimer.stopTimer();
tdv_sdk.questionState = 3;
console.log("⏰ Time up! Press Space to see answer.");
}
return 3;
};
// Gọi khi người chơi trả lời xong (chỉ gọi 1 lần khi đang đếm giờ)
tdv_sdk.onAnswered = function () {
// Chỉ xử lý nếu đang ở state 2 (đang đếm giờ)
if (tdv_sdk.questionState !== 2) {
console.log("⚠️ onAnswered skipped - already processed");
return tdv_sdk.questionState;
}
gameTimer.stopTimer();
tdv_sdk.questionState = 3;
console.log("📝 Answered! Press Space to see answer.");
return 3;
};
// Check xem câu hỏi hiện tại có phải dạng NGHE không (audio là mảng)
// Trả về 1 nếu là Listening Question (Q21-Q30), 0 nếu là câu hỏi thường
tdv_sdk.isListeningQuestion = function () {
if (!tdv_sdk.curQuestion) return 0;
return Array.isArray(tdv_sdk.curQuestion.audio) ? 1 : 0;
};
tdv_sdk.getCurQuestion = function () {
return tdv_sdk.curQuestion.question;
};
tdv_sdk.getCurOptions = function (index) {
return tdv_sdk.curQuestion.options[index];
};
// Tính scale phù hợp dựa trên độ dài text đáp án
// Trả về giá trị scale từ 0.5 đến 1.0
// maxLength: Độ dài tối đa trước khi bắt đầu thu nhỏ (mặc định 12)
tdv_sdk.getOptionScale = function (index, maxLength) {
var text = tdv_sdk.curQuestion.options[index] || "";
var maxLen = maxLength || 12; // Bắt đầu thu nhỏ khi > 12 ký tự
var minScale = 0.5; // Scale nhỏ nhất
var maxScale = 0.8; // Scale lớn nhất
var len = text.length;
if (len <= maxLen) {
return maxScale; // Giữ nguyên scale = 1 nếu text ngắn
}
// Tính toán: giảm dần từ 1.0 đến 0.5 theo độ dài
var ratio = Math.max(0, 1 - (len - maxLen) / 25);
var newScale = minScale + (maxScale - minScale) * ratio;
// Làm tròn 2 chữ số thập phân
return Math.round(Math.max(minScale, newScale) * 100) / 100;
};
// Phiên bản linh hoạt hơn - tính scale tuyến tính
// Điều chỉnh scale mạnh hơn cho text dài
tdv_sdk.getAnswerScale = function (index, maxLen) {
// Sử dụng getCurOptions thay vì getAnswerByIndex
var answer = tdv_sdk.getCurOptions(index);
if (!answer) return 1;
var standardLen = maxLen || 8; // Độ dài chuẩn (giảm từ 10 xuống 8)
var len = String(answer).length;
// Scale mạnh hơn cho text dài
if (len <= standardLen) return 1; // <= 8 ký tự: scale 1.0
if (len <= 10) return 0.85; // 9-10 ký tự
if (len <= 12) return 0.75; // 11-12 ký tự
if (len <= 14) return 0.65; // 13-14 ký tự (Lunar New Year)
if (len <= 18) return 0.55; // 15-18 ký tự
if (len <= 22) return 0.5; // 19-22 ký tự
return 0.45; // > 22 ký tự: scale tối thiểu
};
// HÀM MỚI: Nhận TEXT trực tiếp thay vì index (tránh lỗi LoopIndex)
// Sử dụng: tdv_sdk.getTextScale("Lunar New Year")
tdv_sdk.getTextScale = function (text, maxLen) {
if (!text) return 1;
var standardLen = maxLen || 8;
var len = String(text).length;
// Scale mạnh hơn cho text dài
if (len <= standardLen) return 1; // <= 8 ký tự: scale 1.0
if (len <= 10) return 0.95; // 9-10 ký tự
if (len <= 12) return 0.9; // 11-12 ký tự
if (len <= 14) return 0.85; // 13-14 ký tự
if (len <= 18) return 0.8; // 15-18 ký tự
if (len <= 22) return 0.75; // 19-22 ký tự
return 0.7; // > 22 ký tự: scale tối thiểu
};
tdv_sdk.getCurCorrectOption = function () {
return tdv_sdk.correctAnswer;
};
tdv_sdk.getCorrectCount = function () {
return tdv_sdk.answers;
};
tdv_sdk.getCurImageUrl = function () {
return tdv_sdk.curQuestion.url;
};
tdv_sdk.nextQuestion = function () {
tdv_sdk.currentIndex++;
};
tdv_sdk.getCurIndex = function () {
return tdv_sdk.currentIndex;
};
// Alias cho getCurIndex (tương thích với các game khác)
tdv_sdk.getCurrentLevel = function () {
return tdv_sdk.currentIndex;
};
tdv_sdk.getTotalQuestion = function () {
return tdv_sdk.totalquestion;
};
// Check xem còn câu hỏi không (trả về 1 nếu còn, 0 nếu hết)
tdv_sdk.hasMoreQuestions = function () {
return tdv_sdk.currentIndex < tdv_sdk.totalquestion ? 1 : 0;
};
// Check xem đây có phải câu hỏi cuối cùng không (trả về 1 nếu đúng)
tdv_sdk.isLastQuestion = function () {
return tdv_sdk.currentIndex >= tdv_sdk.totalquestion - 1 ? 1 : 0;
};
// Lấy số câu hỏi còn lại
tdv_sdk.getRemainingQuestions = function () {
return Math.max(0, tdv_sdk.totalquestion - tdv_sdk.currentIndex);
};
// Reset lại từ đầu (shuffle lại câu hỏi)
tdv_sdk.reloadQuestions = function () {
tdv_sdk.currentIndex = 0;
tdv_sdk.listquestion = [];
for (let i = 0; i < tdv_sdk.totalquestion; i++) {
tdv_sdk.listquestion.push(tdv_sdk.list[i]);
}
console.log("🔄 Questions reloaded and shuffled!");
return 1;
};
tdv_sdk.getId = function (index) {
return tdv_sdk.list[index].id;
};
tdv_sdk.getAnswers = function (index) {
return tdv_sdk.answers[index];
};
// --- AUDIO: Hỗ trợ cả string đơn và mảng audio ---
tdv_sdk.currentAudioQueue = [];
tdv_sdk.currentAudioIndex = 0;
tdv_sdk.playSound = function (name) {
console.log(`Play sound: ${name}`);
const audioSrc = `https://audio.senaai.vn/audio/en_female_1_${name.toLowerCase()}.mp3`;
const audio = new Audio(audioSrc);
audio.play();
};
// Phát audio câu hỏi (hỗ trợ cả string đơn và mảng)
tdv_sdk.playSoundQuestion = function (rate) {
// Dừng audio cũ nếu đang chạy
if (window.audio && !window.audio.paused) {
window.audio.pause();
window.audio.currentTime = 0;
}
tdv_sdk.currentAudioQueue = [];
tdv_sdk.currentAudioIndex = 0;
var audioSource = tdv_sdk.curQuestion.audio;
var playbackRate = rate || 1;
if (!audioSource) {
console.log("Không có audio cho câu hỏi này.");
return;
}
if (Array.isArray(audioSource)) {
// Mảng audio (Hội thoại: context + question)
tdv_sdk.currentAudioQueue = audioSource;
tdv_sdk.playNextInQueue(playbackRate);
} else {
// Audio đơn
tdv_sdk.playSingleAudio(audioSource, playbackRate);
}
};
tdv_sdk.playSingleAudio = function (src, rate) {
var audio = new Audio(src);
audio.playbackRate = rate;
window.audio = audio;
audio.play().catch(e => console.error("Lỗi phát audio:", e));
console.log(`Đang phát: ${src}`);
};
tdv_sdk.playNextInQueue = function (rate) {
if (tdv_sdk.currentAudioIndex >= tdv_sdk.currentAudioQueue.length) {
console.log("Đã phát hết đoạn hội thoại.");
return;
}
var src = tdv_sdk.currentAudioQueue[tdv_sdk.currentAudioIndex];
var audio = new Audio(src);
audio.playbackRate = rate;
window.audio = audio;
audio.onended = function () {
tdv_sdk.currentAudioIndex++;
setTimeout(function () {
tdv_sdk.playNextInQueue(rate);
}, 300);
};
audio.play().catch(e => console.error("Lỗi phát audio:", e));
console.log(`Đang phát đoạn ${tdv_sdk.currentAudioIndex + 1}: ${src}`);
};
tdv_sdk.stopSound = function () {
if (window.audio && !window.audio.paused) {
window.audio.pause();
window.audio.currentTime = 0;
}
tdv_sdk.currentAudioQueue = [];
tdv_sdk.currentAudioIndex = 0;
};
tdv_sdk.goHome = function () {
window.location.href = "../";
};
// ==================== QUESTION TIMER (Mỗi câu hỏi 10 giây) ====================
var gameTimer = {};
gameTimer.questionTime = 15; // Thời gian mỗi câu hỏi (giây)
gameTimer.currentTime = gameTimer.questionTime;
gameTimer.isRunning = false;
gameTimer.intervalId = null;
// Bắt đầu đếm ngược cho câu hỏi hiện tại
gameTimer.startTimer = function () {
if (gameTimer.isRunning) {
console.warn("Timer is already running.");
return 0;
}
gameTimer.isRunning = true;
gameTimer.intervalId = setInterval(function () {
if (gameTimer.currentTime > 0) {
gameTimer.currentTime--;
} else {
gameTimer.stopTimer();
console.log("⏰ Question time is up!");
}
}, 1000);
console.log("⏱️ Question timer started:", gameTimer.questionTime, "seconds.");
return 1;
};
gameTimer.stopTimer = function () {
if (gameTimer.intervalId) {
clearInterval(gameTimer.intervalId);
gameTimer.intervalId = null;
gameTimer.isRunning = false;
console.log("Timer stopped.");
}
};
gameTimer.resetTimer = function () {
gameTimer.stopTimer();
gameTimer.currentTime = gameTimer.questionTime;
console.log("⏱️ Timer reset to:", gameTimer.questionTime, "seconds.");
};
// Reset timer cho câu hỏi mới (gọi khi loadquestion) - CHỈ RESET, KHÔNG START
gameTimer.prepareQuestionTimer = function () {
gameTimer.stopTimer();
gameTimer.currentTime = gameTimer.questionTime;
console.log("⏱️ Timer prepared:", gameTimer.questionTime, "seconds. Press Space to start.");
return 1;
};
// Reset VÀ start timer (gọi khi bấm Space)
gameTimer.resetQuestionTimer = function () {
gameTimer.stopTimer();
gameTimer.currentTime = gameTimer.questionTime;
gameTimer.startTimer();
console.log("🔄 Timer started:", gameTimer.questionTime, "seconds.");
return 1;
};
// Set thời gian mỗi câu hỏi (mặc định 10 giây)
gameTimer.setQuestionTime = function (seconds) {
gameTimer.questionTime = seconds || 10;
console.log("⚙️ Question time set to:", gameTimer.questionTime, "seconds.");
return gameTimer.questionTime;
};
gameTimer.getFormattedTime = function () {
if (gameTimer.currentTime < 0) return "0:00";
const minutes = Math.floor(gameTimer.currentTime / 60);
const seconds = gameTimer.currentTime % 60;
const formattedSeconds = seconds < 10 ? '0' + seconds : seconds;
return minutes + ':' + formattedSeconds;
};
gameTimer.getCurrentTimeValue = function () {
return gameTimer.currentTime;
};
// Trả về thời gian hiển thị (luôn = 0 nếu đã hết giờ)
gameTimer.getDisplayTime = function () {
// Nếu đã hết giờ (state >= 3), luôn trả về 0
if (tdv_sdk.questionState >= 3) {
return 0;
}
return Math.max(0, gameTimer.currentTime);
};
gameTimer.pauseTimer = function () {
if (gameTimer.isRunning) {
clearInterval(gameTimer.intervalId);
gameTimer.intervalId = null;
gameTimer.isRunning = false;
console.log("Timer paused at:", gameTimer.getFormattedTime());
return 1;
}
return 0;
};
gameTimer.resumeTimer = function () {
if (!gameTimer.isRunning && gameTimer.currentTime > 0) {
gameTimer.isRunning = true;
gameTimer.intervalId = setInterval(function () {
if (gameTimer.currentTime > 0) {
gameTimer.currentTime--;
} else {
gameTimer.stopTimer();
console.log("Time is up!");
}
}, 1000);
console.log("Timer resumed from:", gameTimer.getFormattedTime());
return 1;
}
return 0;
};
// ==================== GAME SCORE ====================
var gameScore = {};
gameScore.currentScore = 0;
gameScore.addScore = function (points) {
const pts = parseInt(points) || 0;
if (pts > 0) {
gameScore.currentScore += pts;
console.log(`Score added: ${pts}. New score: ${gameScore.currentScore}`);
}
return gameScore.currentScore;
};
gameScore.resetScore = function () {
gameScore.currentScore = 0;
console.log("Score reset to 0.");
return 0;
};
gameScore.getScore = function () {
return gameScore.currentScore;
};
gameScore.deductScore = function (points) {
const pts = parseInt(points) || 0;
if (gameScore.currentScore >= pts) {
gameScore.currentScore -= pts;
console.log(`Score deducted: ${pts}. New score: ${gameScore.currentScore}`);
} else {
gameScore.currentScore = 0;
console.log(`Score deducted. New score is 0.`);
}
return gameScore.currentScore;
};
// ==================== GAME LIVES ====================
var gameLives = {};
gameLives.maxLives = 3;
gameLives.currentLives = gameLives.maxLives;
gameLives.resetLives = function () {
gameLives.currentLives = gameLives.maxLives;
console.log("Lives reset to:", gameLives.maxLives);
return gameLives.currentLives;
};
gameLives.loseLife = function () {
if (gameLives.currentLives > 0) {
gameLives.currentLives--;
console.log("Lost 1 life. Remaining:", gameLives.currentLives);
} else {
console.log("No lives left. Game Over condition.");
}
return gameLives.currentLives;
};
gameLives.getLives = function () {
return gameLives.currentLives;
};
// ==================== INSTRUCTIONS ====================
tdv_sdk.getQuizInstructions = function () {
const instructions =
" 🌟 HƯỚNG DẪN GAME: HÀNH TRÌNH CỦA THỎ 🌟\n\n" +
"🎯 MỤC TIÊU:\n" +
" Giúp chú thỏ đi hết hành trình và đến đích! Trả lời đúng các câu hỏi để ghi thật nhiều điểm trên đường đi.\n\n" +
"🌀 CÁCH CHƠI:\n" +
" 1. Lắc xúc xắc: Chạm vào xúc xắc để quay.\n" +
" 2. Di chuyển: Chú thỏ sẽ đi theo số chấm xuất hiện trên xúc xắc.\n" +
" 3. Trả lời câu hỏi: Khi chú thỏ dừng lại, chọn đáp án đúng hiển thị trên màn hình.\n" +
" 4. Tiếp tục hành trình: Sau khi trả lời, bạn có thể quay lại xúc xắc ngay để đi tiếp.\n\n" +
"💡 ĐIỂM SỐ VÀ THỬ THÁCH:\n" +
" ✅ Đúng: +100 điểm.\n" +
" ❌ Sai: Không bị trừ điểm hay phạt.\n" +
" 🐺 Nguy hiểm: Nếu dừng ở ô **'SÓI'** (Wolf), bạn sẽ bị trừ 100 điểm ngay lập tức.\n\n" +
"🍀 Cùng chú thỏ phiêu lưu, trả lời câu hỏi và ghi thật nhiều điểm trên hành trình nhé!";
return instructions;
};
tdv_sdk.hasAudio = function () {
if (!tdv_sdk.curQuestion) return 0;
var audio = tdv_sdk.curQuestion.audio;
// Check nếu là mảng và có item
if (Array.isArray(audio) && audio.length > 0) return 1;
// Check nếu là string và không rỗng
if (typeof audio === 'string' && audio.trim() !== '') return 1;
return 0;
};

Binary file not shown.