Initial commit
28
VoiceRush/appmanifest.json
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"name": "VoiceRush",
|
||||||
|
"short_name": "VoiceRush",
|
||||||
|
"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"
|
||||||
|
}]
|
||||||
|
}
|
||||||
BIN
VoiceRush/bg.mp4
Normal file
25735
VoiceRush/c2runtime.js
Normal file
1
VoiceRush/data.js
Normal file
BIN
VoiceRush/icon-114.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
VoiceRush/icon-128.png
Normal file
|
After Width: | Height: | Size: 33 KiB |
BIN
VoiceRush/icon-16.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
VoiceRush/icon-256.png
Normal file
|
After Width: | Height: | Size: 96 KiB |
BIN
VoiceRush/icon-32.png
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
VoiceRush/images/back_btn-sheet0.png
Normal file
|
After Width: | Height: | Size: 360 KiB |
BIN
VoiceRush/images/bg-sheet0.png
Normal file
|
After Width: | Height: | Size: 66 KiB |
BIN
VoiceRush/images/bg-sheet1.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
VoiceRush/images/bgm-sheet0.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
VoiceRush/images/bgm-sheet1.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
VoiceRush/images/blankcard-sheet0.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
VoiceRush/images/boardsetting-sheet0.png
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
VoiceRush/images/card-sheet0.png
Normal file
|
After Width: | Height: | Size: 149 KiB |
BIN
VoiceRush/images/close_btn-sheet0.png
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
VoiceRush/images/complete-sheet0.png
Normal file
|
After Width: | Height: | Size: 169 KiB |
BIN
VoiceRush/images/correct-sheet0.png
Normal file
|
After Width: | Height: | Size: 149 KiB |
BIN
VoiceRush/images/countdown-sheet0.png
Normal file
|
After Width: | Height: | Size: 50 KiB |
BIN
VoiceRush/images/countdown-sheet1.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
VoiceRush/images/countdown-sheet2.png
Normal file
|
After Width: | Height: | Size: 263 KiB |
BIN
VoiceRush/images/countdown-sheet3.png
Normal file
|
After Width: | Height: | Size: 178 KiB |
BIN
VoiceRush/images/countdown-sheet4.png
Normal file
|
After Width: | Height: | Size: 282 KiB |
BIN
VoiceRush/images/dim-sheet0.png
Normal file
|
After Width: | Height: | Size: 168 B |
BIN
VoiceRush/images/flashcircle-sheet0.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
VoiceRush/images/game_logo-sheet0.png
Normal file
|
After Width: | Height: | Size: 1005 KiB |
BIN
VoiceRush/images/game_logo-sheet1.png
Normal file
|
After Width: | Height: | Size: 1.7 MiB |
BIN
VoiceRush/images/game_logo-sheet2.png
Normal file
|
After Width: | Height: | Size: 2.6 MiB |
BIN
VoiceRush/images/greenborder-sheet0.png
Normal file
|
After Width: | Height: | Size: 5.9 KiB |
BIN
VoiceRush/images/khung-sheet0.png
Normal file
|
After Width: | Height: | Size: 5.9 KiB |
BIN
VoiceRush/images/khung-sheet1.png
Normal file
|
After Width: | Height: | Size: 5.9 KiB |
BIN
VoiceRush/images/khung-sheet2.png
Normal file
|
After Width: | Height: | Size: 5.9 KiB |
BIN
VoiceRush/images/khunganh-sheet0.png
Normal file
|
After Width: | Height: | Size: 3.6 KiB |
BIN
VoiceRush/images/khunganh-sheet1.png
Normal file
|
After Width: | Height: | Size: 3.6 KiB |
BIN
VoiceRush/images/koala-sheet0.png
Normal file
|
After Width: | Height: | Size: 241 KiB |
BIN
VoiceRush/images/logo-sheet0.png
Normal file
|
After Width: | Height: | Size: 37 KiB |
BIN
VoiceRush/images/next_btn-sheet0.png
Normal file
|
After Width: | Height: | Size: 359 KiB |
BIN
VoiceRush/images/pause-sheet0.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
VoiceRush/images/pause-sheet1.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
VoiceRush/images/pauseicon-sheet0.png
Normal file
|
After Width: | Height: | Size: 63 KiB |
BIN
VoiceRush/images/play-sheet0.png
Normal file
|
After Width: | Height: | Size: 432 KiB |
BIN
VoiceRush/images/replay-sheet0.png
Normal file
|
After Width: | Height: | Size: 146 KiB |
BIN
VoiceRush/images/score-sheet0.png
Normal file
|
After Width: | Height: | Size: 249 KiB |
BIN
VoiceRush/images/setting-sheet0.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
VoiceRush/images/sound-sheet0.png
Normal file
|
After Width: | Height: | Size: 629 KiB |
BIN
VoiceRush/images/sound_btn-sheet0.png
Normal file
|
After Width: | Height: | Size: 668 KiB |
BIN
VoiceRush/images/sprite-sheet0.png
Normal file
|
After Width: | Height: | Size: 224 KiB |
BIN
VoiceRush/images/sprite2-sheet0.png
Normal file
|
After Width: | Height: | Size: 433 KiB |
BIN
VoiceRush/images/sprite3-sheet0.png
Normal file
|
After Width: | Height: | Size: 229 KiB |
BIN
VoiceRush/images/timer-sheet0.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
VoiceRush/images/voicerush-sheet0.png
Normal file
|
After Width: | Height: | Size: 330 KiB |
BIN
VoiceRush/images/wrong-sheet0.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
VoiceRush/images/ằq-sheet0.png
Normal file
|
After Width: | Height: | Size: 65 KiB |
140
VoiceRush/index.html
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
|
||||||
|
<title>VoiceRush</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>
|
||||||
|
<script src="sena_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>
|
||||||
2
VoiceRush/jquery-3.4.1.min.js
vendored
Normal file
BIN
VoiceRush/loading-logo.png
Normal file
|
After Width: | Height: | Size: 376 KiB |
BIN
VoiceRush/media/8-bit-gaming-background-music-358443.ogg
Normal file
BIN
VoiceRush/media/alert-234711.ogg
Normal file
BIN
VoiceRush/media/bubble-pop-389501.ogg
Normal file
BIN
VoiceRush/media/button-124476.ogg
Normal file
BIN
VoiceRush/media/click-234708.ogg
Normal file
BIN
VoiceRush/media/click-sound-432501.ogg
Normal file
BIN
VoiceRush/media/click.m4a
Normal file
BIN
VoiceRush/media/click.ogg
Normal file
BIN
VoiceRush/media/collect-5930.ogg
Normal file
BIN
VoiceRush/media/correct-156911.ogg
Normal file
BIN
VoiceRush/media/correct.m4a
Normal file
BIN
VoiceRush/media/correct.ogg
Normal file
BIN
VoiceRush/media/countdown.ogg
Normal file
BIN
VoiceRush/media/error-010-206498.ogg
Normal file
BIN
VoiceRush/media/error-08-206492.ogg
Normal file
BIN
VoiceRush/media/fail-234710.ogg
Normal file
BIN
VoiceRush/media/fail.m4a
Normal file
BIN
VoiceRush/media/fail.ogg
Normal file
BIN
VoiceRush/media/interface-2-126517.ogg
Normal file
BIN
VoiceRush/media/material-buy-success-394517.ogg
Normal file
BIN
VoiceRush/media/pick-92276.ogg
Normal file
BIN
VoiceRush/media/pop-reverb-423718.ogg
Normal file
BIN
VoiceRush/media/retro-explode-1-236678.ogg
Normal file
41
VoiceRush/offline.js
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
{
|
||||||
|
"version": 1770361843,
|
||||||
|
"fileList": [
|
||||||
|
"data.js",
|
||||||
|
"c2runtime.js",
|
||||||
|
"jquery-3.4.1.min.js",
|
||||||
|
"offlineClient.js",
|
||||||
|
"images/card-sheet0.png",
|
||||||
|
"images/flashcircle-sheet0.png",
|
||||||
|
"images/dim-sheet0.png",
|
||||||
|
"images/countdown-sheet0.png",
|
||||||
|
"images/countdown-sheet1.png",
|
||||||
|
"images/khunganh-sheet0.png",
|
||||||
|
"images/khunganh-sheet1.png",
|
||||||
|
"images/logo-sheet0.png",
|
||||||
|
"images/timer-sheet0.png",
|
||||||
|
"images/boardsetting-sheet0.png",
|
||||||
|
"images/setting-sheet0.png",
|
||||||
|
"images/pause-sheet0.png",
|
||||||
|
"images/pause-sheet1.png",
|
||||||
|
"images/bgm-sheet0.png",
|
||||||
|
"images/bgm-sheet1.png",
|
||||||
|
"images/pauseicon-sheet0.png",
|
||||||
|
"images/blankcard-sheet0.png",
|
||||||
|
"media/correct-156911.ogg",
|
||||||
|
"media/alert-234711.ogg",
|
||||||
|
"media/bubble-pop-389501.ogg",
|
||||||
|
"media/button-124476.ogg",
|
||||||
|
"media/countdown.ogg",
|
||||||
|
"media/8-bit-gaming-background-music-358443.ogg",
|
||||||
|
"icon-16.png",
|
||||||
|
"icon-32.png",
|
||||||
|
"icon-114.png",
|
||||||
|
"icon-128.png",
|
||||||
|
"icon-256.png",
|
||||||
|
"loading-logo.png",
|
||||||
|
"tdv_sdk.js",
|
||||||
|
"bg.mp4",
|
||||||
|
"sena_sdk.js"
|
||||||
|
]
|
||||||
|
}
|
||||||
53
VoiceRush/offlineClient.js
Normal 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();
|
||||||
|
|
||||||
|
}());
|
||||||
|
|
||||||
1758
VoiceRush/sena_sdk.js
Normal file
403
VoiceRush/sw.js
Normal 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);
|
||||||
|
});
|
||||||
238
VoiceRush/tdv_sdk.js
Normal file
@@ -0,0 +1,238 @@
|
|||||||
|
var tdv_sdk = {};
|
||||||
|
|
||||||
|
// 1. DATA CONFIG (Dữ liệu nguồn)
|
||||||
|
tdv_sdk.list = [
|
||||||
|
{
|
||||||
|
id: "card_dog",
|
||||||
|
name: "dog",
|
||||||
|
image: "https://image.senaai.tech/images/dog7.png",
|
||||||
|
audio: "https://audio.senaai.tech/audio/Sena_Voice1_dog_06d80eb0.mp3",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "card_cat",
|
||||||
|
name: "cat",
|
||||||
|
image: "https://image.senaai.tech/images/Cat.png",
|
||||||
|
audio: "https://audio.senaai.tech/audio/Sena_Voice1_cat_d077f244.mp3",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "card_fish",
|
||||||
|
name: "fish",
|
||||||
|
image: "https://image.senaai.tech/images/Clownfish.png",
|
||||||
|
audio: "https://audio.senaai.tech/audio/Sena_Voice1_fish_83e4a96a.mp3",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "card_bird",
|
||||||
|
name: "bird",
|
||||||
|
image: "https://image.senaai.tech/images/Bird31.png",
|
||||||
|
audio: "https://audio.senaai.tech/audio/Sena_Voice1_bird_abaecf8c.mp3",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "card_mouse",
|
||||||
|
name: "mouse",
|
||||||
|
image: "https://image.senaai.tech/images/Gerbil3.png",
|
||||||
|
audio: "https://audio.senaai.tech/audio/Sena_Voice1_mouse_40203abe.mp3",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "card_cow",
|
||||||
|
name: "cow",
|
||||||
|
image: "https://image.senaai.tech/images/cow.png",
|
||||||
|
audio: "https://audio.senaai.tech/audio/Sena_Voice1_cow_81566e98.mp3",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// Cấu hình số lượng card cho từng Level (Ví dụ: Lv1=3 card, Lv2=8 card...)
|
||||||
|
tdv_sdk.pageCardConfig = [6, 6, 3, 4];
|
||||||
|
|
||||||
|
tdv_sdk.pageCurrent = 0;
|
||||||
|
tdv_sdk.currentPageList = [];
|
||||||
|
tdv_sdk.pageInfo = [];
|
||||||
|
|
||||||
|
// 2. CORE FUNCTIONS
|
||||||
|
tdv_sdk.start = function () {
|
||||||
|
tdv_sdk.pageInfo = [];
|
||||||
|
|
||||||
|
// Duyệt qua từng config level để sinh dữ liệu
|
||||||
|
for (let i = 0; i < tdv_sdk.pageCardConfig.length; i++) {
|
||||||
|
let count = tdv_sdk.pageCardConfig[i];
|
||||||
|
let cardList = [];
|
||||||
|
|
||||||
|
// Logic Random Kiểu A: Lấy ngẫu nhiên, cho phép lặp lại
|
||||||
|
for (let j = 0; j < count; j++) {
|
||||||
|
let randIndex = Math.floor(Math.random() * tdv_sdk.list.length);
|
||||||
|
// Copy đối tượng để đảm bảo tính riêng biệt (quan trọng cho game Beat)
|
||||||
|
let _obj = {
|
||||||
|
...tdv_sdk.list[randIndex], // ES6 spread operator
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
scale: 1,
|
||||||
|
index: j, // Lưu số thứ tự để dễ xử lý beat
|
||||||
|
};
|
||||||
|
cardList.push(_obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
tdv_sdk.pageInfo.push({
|
||||||
|
index: i,
|
||||||
|
totalCardInpage: cardList.length,
|
||||||
|
cardList: cardList,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
tdv_sdk.pageCurrent = 0;
|
||||||
|
tdv_sdk.getPostionInPage();
|
||||||
|
};
|
||||||
|
|
||||||
|
tdv_sdk.getPostionInPage = function () {
|
||||||
|
const _currentPage = tdv_sdk.pageInfo[tdv_sdk.pageCurrent];
|
||||||
|
|
||||||
|
const max_width = 1200,
|
||||||
|
max_height = 1200,
|
||||||
|
margin_x = 80,
|
||||||
|
margin_y = 80,
|
||||||
|
card_width = 300,
|
||||||
|
card_height = 300;
|
||||||
|
|
||||||
|
const totalCards = _currentPage.totalCardInpage;
|
||||||
|
|
||||||
|
// --- ĐIỂM CẦN SỬA ---
|
||||||
|
// Mặc định là 3 cột (cho trường hợp 3, 5, 6 card)
|
||||||
|
let max_col = 3;
|
||||||
|
|
||||||
|
// Nếu có đúng 4 card -> Ép xuống thành 2 cột để ra dạng Grid 2x2
|
||||||
|
if (totalCards === 4) {
|
||||||
|
max_col = 2;
|
||||||
|
}
|
||||||
|
// --------------------
|
||||||
|
|
||||||
|
const num_rows = Math.ceil(totalCards / max_col);
|
||||||
|
|
||||||
|
// Tính chiều cao tổng thể để căn giữa dọc
|
||||||
|
const total_block_height = num_rows * card_height + (num_rows - 1) * margin_y;
|
||||||
|
const start_y = (max_height - total_block_height) / 2 + card_height / 2;
|
||||||
|
|
||||||
|
_currentPage.cardList.forEach((card, index) => {
|
||||||
|
const row_idx = Math.floor(index / max_col);
|
||||||
|
const col_idx = index % max_col;
|
||||||
|
|
||||||
|
// Tính số item trong hàng hiện tại để căn giữa ngang
|
||||||
|
let items_in_current_row;
|
||||||
|
|
||||||
|
// Logic xác định số lượng card trong hàng hiện tại
|
||||||
|
// Nếu là hàng cuối cùng
|
||||||
|
if (row_idx === num_rows - 1) {
|
||||||
|
// Lấy số dư. Nếu chia hết (dư 0) thì tức là hàng đó đầy (bằng max_col)
|
||||||
|
let remainder = totalCards % max_col;
|
||||||
|
items_in_current_row = remainder === 0 ? max_col : remainder;
|
||||||
|
} else {
|
||||||
|
// Các hàng bên trên luôn đầy
|
||||||
|
items_in_current_row = max_col;
|
||||||
|
}
|
||||||
|
|
||||||
|
const current_row_width =
|
||||||
|
items_in_current_row * card_width + (items_in_current_row - 1) * margin_x;
|
||||||
|
const start_x = (max_width - current_row_width) / 2 + card_width / 2;
|
||||||
|
|
||||||
|
card.x = start_x + col_idx * (card_width + margin_x);
|
||||||
|
card.y = start_y + row_idx * (card_height + margin_y);
|
||||||
|
});
|
||||||
|
|
||||||
|
tdv_sdk.currentPageList = _currentPage.cardList;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 3. NAVIGATION & HELPER
|
||||||
|
tdv_sdk.nextPage = function () {
|
||||||
|
if (tdv_sdk.pageCurrent < tdv_sdk.pageInfo.length - 1) {
|
||||||
|
tdv_sdk.pageCurrent++;
|
||||||
|
tdv_sdk.getPostionInPage();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
tdv_sdk.prePage = function () {
|
||||||
|
if (tdv_sdk.pageCurrent > 0) {
|
||||||
|
tdv_sdk.pageCurrent--;
|
||||||
|
tdv_sdk.getPostionInPage();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
tdv_sdk.getCurrentPageListInfo_getTotal = function () {
|
||||||
|
return tdv_sdk.currentPageList.length; // Sửa lại: trả về độ dài thực (không -1)
|
||||||
|
};
|
||||||
|
|
||||||
|
tdv_sdk.getCurrentPageListInfo_getByIndex = function (index, attr) {
|
||||||
|
if (tdv_sdk.currentPageList[index])
|
||||||
|
return tdv_sdk.currentPageList[index][attr];
|
||||||
|
return "";
|
||||||
|
};
|
||||||
|
|
||||||
|
tdv_sdk.playSound = function (index) {
|
||||||
|
if (!tdv_sdk.currentPageList[index]) return;
|
||||||
|
const _audio = tdv_sdk.currentPageList[index].audio;
|
||||||
|
if (_audio == "") return;
|
||||||
|
|
||||||
|
// Logic play audio đơn giản
|
||||||
|
let audio = new Audio(_audio);
|
||||||
|
audio.play();
|
||||||
|
};
|
||||||
|
// Thêm vào cuối file tdv_sdk.js
|
||||||
|
tdv_sdk.getTotalPages = function () {
|
||||||
|
return tdv_sdk.pageInfo.length;
|
||||||
|
};
|
||||||
|
|
||||||
|
tdv_sdk.getCurrentPageIndex = function () {
|
||||||
|
return tdv_sdk.pageCurrent + 1; // +1 vì index bắt đầu từ 0
|
||||||
|
};
|
||||||
|
|
||||||
|
tdv_sdk.getTotalCardsOfGame = function () {
|
||||||
|
var total = 0;
|
||||||
|
if (tdv_sdk.pageCardConfig) {
|
||||||
|
for (var i = 0; i < tdv_sdk.pageCardConfig.length; i++) {
|
||||||
|
total += tdv_sdk.pageCardConfig[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return total;
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- CẤU HÌNH ĐỘ KHÓ (DIFFICULTY CONFIG) ---
|
||||||
|
// Options: "easy", "medium", "hard", "extreme"
|
||||||
|
tdv_sdk.difficulty = "hard";
|
||||||
|
|
||||||
|
tdv_sdk.getBeatSpeed = function () {
|
||||||
|
switch (tdv_sdk.difficulty) {
|
||||||
|
case "easy":
|
||||||
|
return 10.0;
|
||||||
|
case "medium":
|
||||||
|
return 6.0;
|
||||||
|
case "hard":
|
||||||
|
return 3.0;
|
||||||
|
case "extreme":
|
||||||
|
return 1.0;
|
||||||
|
default:
|
||||||
|
return 3.6;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- PHẦN Input Voice ---
|
||||||
|
|
||||||
|
window.addEventListener("message", function (event) {
|
||||||
|
var data = event.data;
|
||||||
|
|
||||||
|
// Log ra để kiểm tra xem nhận được gì (F12 Console)
|
||||||
|
console.log("[Game] Received data:", data);
|
||||||
|
|
||||||
|
if (data && data.type === "action") {
|
||||||
|
var signal = data.action; // Sẽ nhận được chuỗi "1" hoặc "0"
|
||||||
|
|
||||||
|
if (typeof c2_callFunction !== "undefined") {
|
||||||
|
var resultParam = parseInt(signal);
|
||||||
|
|
||||||
|
c2_callFunction("ShowFeedback", [resultParam]);
|
||||||
|
|
||||||
|
console.log("[Game] Called C2 Function ShowFeedback with:", resultParam);
|
||||||
|
} else {
|
||||||
|
console.warn(
|
||||||
|
"[Game] c2_callFunction not found! (Are you running inside C2 Preview?)",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
//I was here!
|
||||||