diff --git a/docs/assets/js/crypto/core.min.js b/docs/assets/js/crypto/core.min.js new file mode 100644 index 0000000..1fbb437 --- /dev/null +++ b/docs/assets/js/crypto/core.min.js @@ -0,0 +1 @@ +!function(t,n){"object"==typeof exports?module.exports=exports=n():"function"==typeof define&&define.amd?define([],n):t.CryptoJS=n()}(this,function(){var t=t||function(f){var t;if("undefined"!=typeof window&&window.crypto&&(t=window.crypto),!t&&"undefined"!=typeof window&&window.msCrypto&&(t=window.msCrypto),!t&&"undefined"!=typeof global&&global.crypto&&(t=global.crypto),!t&&"function"==typeof require)try{t=require("crypto")}catch(t){}function i(){if(t){if("function"==typeof t.getRandomValues)try{return t.getRandomValues(new Uint32Array(1))[0]}catch(t){}if("function"==typeof t.randomBytes)try{return t.randomBytes(4).readInt32LE()}catch(t){}}throw new Error("Native crypto module could not be used to get secure random number.")}var e=Object.create||function(t){var n;return r.prototype=t,n=new r,r.prototype=null,n};function r(){}var n={},o=n.lib={},s=o.Base={extend:function(t){var n=e(this);return t&&n.mixIn(t),n.hasOwnProperty("init")&&this.init!==n.init||(n.init=function(){n.$super.init.apply(this,arguments)}),(n.init.prototype=n).$super=this,n},create:function(){var t=this.extend();return t.init.apply(t,arguments),t},init:function(){},mixIn:function(t){for(var n in t)t.hasOwnProperty(n)&&(this[n]=t[n]);t.hasOwnProperty("toString")&&(this.toString=t.toString)},clone:function(){return this.init.prototype.extend(this)}},p=o.WordArray=s.extend({init:function(t,n){t=this.words=t||[],this.sigBytes=null!=n?n:4*t.length},toString:function(t){return(t||c).stringify(this)},concat:function(t){var n=this.words,e=t.words,i=this.sigBytes,r=t.sigBytes;if(this.clamp(),i%4)for(var o=0;o>>2]>>>24-o%4*8&255;n[i+o>>>2]|=s<<24-(i+o)%4*8}else for(o=0;o>>2]=e[o>>>2];return this.sigBytes+=r,this},clamp:function(){var t=this.words,n=this.sigBytes;t[n>>>2]&=4294967295<<32-n%4*8,t.length=f.ceil(n/4)},clone:function(){var t=s.clone.call(this);return t.words=this.words.slice(0),t},random:function(t){for(var n=[],e=0;e>>2]>>>24-r%4*8&255;i.push((o>>>4).toString(16)),i.push((15&o).toString(16))}return i.join("")},parse:function(t){for(var n=t.length,e=[],i=0;i>>3]|=parseInt(t.substr(i,2),16)<<24-i%8*4;return new p.init(e,n/2)}},u=a.Latin1={stringify:function(t){for(var n=t.words,e=t.sigBytes,i=[],r=0;r>>2]>>>24-r%4*8&255;i.push(String.fromCharCode(o))}return i.join("")},parse:function(t){for(var n=t.length,e=[],i=0;i>>2]|=(255&t.charCodeAt(i))<<24-i%4*8;return new p.init(e,n)}},d=a.Utf8={stringify:function(t){try{return decodeURIComponent(escape(u.stringify(t)))}catch(t){throw new Error("Malformed UTF-8 data")}},parse:function(t){return u.parse(unescape(encodeURIComponent(t)))}},h=o.BufferedBlockAlgorithm=s.extend({reset:function(){this._data=new p.init,this._nDataBytes=0},_append:function(t){"string"==typeof t&&(t=d.parse(t)),this._data.concat(t),this._nDataBytes+=t.sigBytes},_process:function(t){var n,e=this._data,i=e.words,r=e.sigBytes,o=this.blockSize,s=r/(4*o),a=(s=t?f.ceil(s):f.max((0|s)-this._minBufferSize,0))*o,c=f.min(4*a,r);if(a){for(var u=0;u>> 24)) & 0x00ff00ff) | + (((M_offset_i << 24) | (M_offset_i >>> 8)) & 0xff00ff00) + ); + } + + // Shortcuts + var H = this._hash.words; + + var M_offset_0 = M[offset + 0]; + var M_offset_1 = M[offset + 1]; + var M_offset_2 = M[offset + 2]; + var M_offset_3 = M[offset + 3]; + var M_offset_4 = M[offset + 4]; + var M_offset_5 = M[offset + 5]; + var M_offset_6 = M[offset + 6]; + var M_offset_7 = M[offset + 7]; + var M_offset_8 = M[offset + 8]; + var M_offset_9 = M[offset + 9]; + var M_offset_10 = M[offset + 10]; + var M_offset_11 = M[offset + 11]; + var M_offset_12 = M[offset + 12]; + var M_offset_13 = M[offset + 13]; + var M_offset_14 = M[offset + 14]; + var M_offset_15 = M[offset + 15]; + + // Working varialbes + var a = H[0]; + var b = H[1]; + var c = H[2]; + var d = H[3]; + + // Computation + a = FF(a, b, c, d, M_offset_0, 7, T[0]); + d = FF(d, a, b, c, M_offset_1, 12, T[1]); + c = FF(c, d, a, b, M_offset_2, 17, T[2]); + b = FF(b, c, d, a, M_offset_3, 22, T[3]); + a = FF(a, b, c, d, M_offset_4, 7, T[4]); + d = FF(d, a, b, c, M_offset_5, 12, T[5]); + c = FF(c, d, a, b, M_offset_6, 17, T[6]); + b = FF(b, c, d, a, M_offset_7, 22, T[7]); + a = FF(a, b, c, d, M_offset_8, 7, T[8]); + d = FF(d, a, b, c, M_offset_9, 12, T[9]); + c = FF(c, d, a, b, M_offset_10, 17, T[10]); + b = FF(b, c, d, a, M_offset_11, 22, T[11]); + a = FF(a, b, c, d, M_offset_12, 7, T[12]); + d = FF(d, a, b, c, M_offset_13, 12, T[13]); + c = FF(c, d, a, b, M_offset_14, 17, T[14]); + b = FF(b, c, d, a, M_offset_15, 22, T[15]); + + a = GG(a, b, c, d, M_offset_1, 5, T[16]); + d = GG(d, a, b, c, M_offset_6, 9, T[17]); + c = GG(c, d, a, b, M_offset_11, 14, T[18]); + b = GG(b, c, d, a, M_offset_0, 20, T[19]); + a = GG(a, b, c, d, M_offset_5, 5, T[20]); + d = GG(d, a, b, c, M_offset_10, 9, T[21]); + c = GG(c, d, a, b, M_offset_15, 14, T[22]); + b = GG(b, c, d, a, M_offset_4, 20, T[23]); + a = GG(a, b, c, d, M_offset_9, 5, T[24]); + d = GG(d, a, b, c, M_offset_14, 9, T[25]); + c = GG(c, d, a, b, M_offset_3, 14, T[26]); + b = GG(b, c, d, a, M_offset_8, 20, T[27]); + a = GG(a, b, c, d, M_offset_13, 5, T[28]); + d = GG(d, a, b, c, M_offset_2, 9, T[29]); + c = GG(c, d, a, b, M_offset_7, 14, T[30]); + b = GG(b, c, d, a, M_offset_12, 20, T[31]); + + a = HH(a, b, c, d, M_offset_5, 4, T[32]); + d = HH(d, a, b, c, M_offset_8, 11, T[33]); + c = HH(c, d, a, b, M_offset_11, 16, T[34]); + b = HH(b, c, d, a, M_offset_14, 23, T[35]); + a = HH(a, b, c, d, M_offset_1, 4, T[36]); + d = HH(d, a, b, c, M_offset_4, 11, T[37]); + c = HH(c, d, a, b, M_offset_7, 16, T[38]); + b = HH(b, c, d, a, M_offset_10, 23, T[39]); + a = HH(a, b, c, d, M_offset_13, 4, T[40]); + d = HH(d, a, b, c, M_offset_0, 11, T[41]); + c = HH(c, d, a, b, M_offset_3, 16, T[42]); + b = HH(b, c, d, a, M_offset_6, 23, T[43]); + a = HH(a, b, c, d, M_offset_9, 4, T[44]); + d = HH(d, a, b, c, M_offset_12, 11, T[45]); + c = HH(c, d, a, b, M_offset_15, 16, T[46]); + b = HH(b, c, d, a, M_offset_2, 23, T[47]); + + a = II(a, b, c, d, M_offset_0, 6, T[48]); + d = II(d, a, b, c, M_offset_7, 10, T[49]); + c = II(c, d, a, b, M_offset_14, 15, T[50]); + b = II(b, c, d, a, M_offset_5, 21, T[51]); + a = II(a, b, c, d, M_offset_12, 6, T[52]); + d = II(d, a, b, c, M_offset_3, 10, T[53]); + c = II(c, d, a, b, M_offset_10, 15, T[54]); + b = II(b, c, d, a, M_offset_1, 21, T[55]); + a = II(a, b, c, d, M_offset_8, 6, T[56]); + d = II(d, a, b, c, M_offset_15, 10, T[57]); + c = II(c, d, a, b, M_offset_6, 15, T[58]); + b = II(b, c, d, a, M_offset_13, 21, T[59]); + a = II(a, b, c, d, M_offset_4, 6, T[60]); + d = II(d, a, b, c, M_offset_11, 10, T[61]); + c = II(c, d, a, b, M_offset_2, 15, T[62]); + b = II(b, c, d, a, M_offset_9, 21, T[63]); + + // Intermediate hash value + H[0] = (H[0] + a) | 0; + H[1] = (H[1] + b) | 0; + H[2] = (H[2] + c) | 0; + H[3] = (H[3] + d) | 0; + }, + + _doFinalize: function () { + // Shortcuts + var data = this._data; + var dataWords = data.words; + + var nBitsTotal = this._nDataBytes * 8; + var nBitsLeft = data.sigBytes * 8; + + // Add padding + dataWords[nBitsLeft >>> 5] |= 0x80 << (24 - nBitsLeft % 32); + + var nBitsTotalH = Math.floor(nBitsTotal / 0x100000000); + var nBitsTotalL = nBitsTotal; + dataWords[(((nBitsLeft + 64) >>> 9) << 4) + 15] = ( + (((nBitsTotalH << 8) | (nBitsTotalH >>> 24)) & 0x00ff00ff) | + (((nBitsTotalH << 24) | (nBitsTotalH >>> 8)) & 0xff00ff00) + ); + dataWords[(((nBitsLeft + 64) >>> 9) << 4) + 14] = ( + (((nBitsTotalL << 8) | (nBitsTotalL >>> 24)) & 0x00ff00ff) | + (((nBitsTotalL << 24) | (nBitsTotalL >>> 8)) & 0xff00ff00) + ); + + data.sigBytes = (dataWords.length + 1) * 4; + + // Hash final blocks + this._process(); + + // Shortcuts + var hash = this._hash; + var H = hash.words; + + // Swap endian + for (var i = 0; i < 4; i++) { + // Shortcut + var H_i = H[i]; + + H[i] = (((H_i << 8) | (H_i >>> 24)) & 0x00ff00ff) | + (((H_i << 24) | (H_i >>> 8)) & 0xff00ff00); + } + + // Return final computed hash + return hash; + }, + + clone: function () { + var clone = Hasher.clone.call(this); + clone._hash = this._hash.clone(); + + return clone; + } + }); + + function FF(a, b, c, d, x, s, t) { + var n = a + ((b & c) | (~b & d)) + x + t; + return ((n << s) | (n >>> (32 - s))) + b; + } + + function GG(a, b, c, d, x, s, t) { + var n = a + ((b & d) | (c & ~d)) + x + t; + return ((n << s) | (n >>> (32 - s))) + b; + } + + function HH(a, b, c, d, x, s, t) { + var n = a + (b ^ c ^ d) + x + t; + return ((n << s) | (n >>> (32 - s))) + b; + } + + function II(a, b, c, d, x, s, t) { + var n = a + (c ^ (b | ~d)) + x + t; + return ((n << s) | (n >>> (32 - s))) + b; + } + + /** + * Shortcut function to the hasher's object interface. + * + * @param {WordArray|string} message The message to hash. + * + * @return {WordArray} The hash. + * + * @static + * + * @example + * + * var hash = CryptoJS.MD5('message'); + * var hash = CryptoJS.MD5(wordArray); + */ + C.MD5 = Hasher._createHelper(MD5); + + /** + * Shortcut function to the HMAC's object interface. + * + * @param {WordArray|string} message The message to hash. + * @param {WordArray|string} key The secret key. + * + * @return {WordArray} The HMAC. + * + * @static + * + * @example + * + * var hmac = CryptoJS.HmacMD5(message, key); + */ + C.HmacMD5 = Hasher._createHmacHelper(MD5); + }(Math)); + + + return CryptoJS.MD5; + +})); \ No newline at end of file diff --git a/docs/assets/js/index.js b/docs/assets/js/index.js index df0eaaa..23661b1 100644 --- a/docs/assets/js/index.js +++ b/docs/assets/js/index.js @@ -50,6 +50,12 @@ function update() { } function draw() { + + // If draw is being called, the user has interacted with the page at least once. + // This signal can be used to notify the audio permission handler + unlockAudioPermission(); + + // Handle game state switch (globalState) { // title screen case globalStates.titleScreen: @@ -110,6 +116,25 @@ function onAssetsLoaded() { setup(60); -// Hide the preloader -// This should actually run after all assets have been downloaded -page_preloader.hide(false); \ No newline at end of file +/* Preload actions */ + +// To make something happen before the preloader is hidden, add it to this list +// The function must take a callback that will be run when the function finishes +let actions = [preCacheSounds]; +let actionsCompleted = 0; + +// Loop through every action, and load it +actions.forEach((action) => { + + // Run the action & handle loading + action(() => { + + // Incr the number of successfully loaded actions + actionsCompleted += 1; + + // If this is the last aciton, hide the loader + if (actionsCompleted == actions.length) { + page_preloader.hide(false); + } + }) +}); diff --git a/docs/assets/js/sounds/permissionhandler.js b/docs/assets/js/sounds/permissionhandler.js new file mode 100644 index 0000000..1a5f2fb --- /dev/null +++ b/docs/assets/js/sounds/permissionhandler.js @@ -0,0 +1,22 @@ +/** + * This file just exists to keep track of weather the user has interacted with the + * webpage. Most browsers will block autoplay if no interaction has been made. + */ + + +// Tracker for permission unlock +let _audioPermUnlock = false; + +/** + * Call this when the user interacts with the page + */ +function unlockAudioPermission() { + _audioPermUnlock = true; +} + +/** + * Check if autoplay is enabled + */ +function canPlayAudio() { + return _audioPermUnlock; +} \ No newline at end of file diff --git a/docs/assets/js/sounds/soundcontext.js b/docs/assets/js/sounds/soundcontext.js new file mode 100644 index 0000000..9804973 --- /dev/null +++ b/docs/assets/js/sounds/soundcontext.js @@ -0,0 +1,152 @@ +/** + * This file contains classes for playing sounds. + * The SoundContext works by providing multiple audio channels, + * just like an old ATARI, or even GameBoy sound system. Each + * channel can be controlled individually. + * + * ---- Usage ---- + * + * // Load all sounds + * preCacheSounds(()=>{ + * // Code can be run here as soon as all sounds are loaded + * // ... + * }); + * + * // Play a sound + * globalSoundContext.playSound(globalSoundContext.channels., sounds.); + * + * // Stop a channel + * globalSoundContext.mute(globalSoundContext.channels.); + */ + +/** + * A sound channel can play 1 sound at a time, and supports sound queueing + */ +class _SoundChannel { + + /** + * Create a sound channel + * @param {number} max_queue_size Maximum number of sounds that can be queued before sounds are skipped + */ + constructor(max_queue_size) { + this.max_size = max_queue_size + this.sound_queue = [] + } + + /** + * Add a snippet to the queue + * @param {SoundSnippet} snippet + */ + enqueue(snippet) { + + // If the queue is full, cut out the next sound in the queue to make room + if (this.sound_queue.length >= this.max_size) { + this.sound_queue.splice(1, 1); + } + + // Append the sound to the queue + this.sound_queue.push(snippet); + + // If this is the first sound in the queue, spawn a watcher job, and play it + if (this.sound_queue.length == 1) { + this.sound_queue[0].play(); + this._spawnWatcher(this.sound_queue[0]); + } + } + + /** + * Start a job to run when the sound finishes to remove the sound from the queue + * @param {SoundSnippet} snippet + */ + _spawnWatcher(snippet) { + + // Read the snippet length + let length = snippet.getLengthSeconds(); + + // Spawn a clean action for that time in the future + setTimeout(() => { + this._cleanQueue(snippet); + }, length); + } + + /** + * This should be run when every sound finishes. This will remove that + * sound from from the queue, start the next sound, and spawn a + * new watcher for that sound. + * @param {SoundSnippet} snippet + */ + _cleanQueue(snippet) { + + // Get the snippet hash + let hash = snippet.getHash(); + + // Make sure there are actually sounds playing + if (this.sound_queue.length > 0) { + + // If the first snippet in the queue matches this hash, remove it. + // If not, something must have happened. Just ignore it, and move on + if (this.sound_queue[0].getHash() == hash) { + + // Popoff the snippet + this.sound_queue.shift(); + + } + + + // Spawn a watcher for the next sound & play it + if (this.sound_queue.length > 0) { + this.sound_queue[0].play(); + this._spawnWatcher(this.sound_queue[0]); + } + } + } + + /** + * Clear the entire queue, and stop all sounds + */ + clear() { + + // Stop every sound + this.sound_queue.forEach((sound) => { + sound.stop(); + }) + + // Clear the queue + this.sound_queue = []; + + } +} + + +class _SoundContext { + + constructor() { + + // Define all sound channels + this.channels = { + bgm: new _SoundChannel(2) + } + } + + /** + * Play a sound in a channel + * @param {*} channel + * @param {SoundSnippet} snippet + */ + playSound(channel, snippet) { + console.log(`[SoundContext] Playing snippet: ${snippet.getName()}`); + channel.enqueue(snippet); + } + + /** + * Stop all sounds in a channel + * @param {*} channel + */ + mute(channel) { + channel.clear(); + } + +} + +// The global context for sounds +let globalSoundContext = new _SoundContext(); \ No newline at end of file diff --git a/docs/assets/js/sounds/sounds.js b/docs/assets/js/sounds/sounds.js new file mode 100644 index 0000000..935dc6a --- /dev/null +++ b/docs/assets/js/sounds/sounds.js @@ -0,0 +1,63 @@ +/** + * This file handles all sound assets, and loading them + * To add a new sound asset: + * 1) Make a mapping from the asset name to it's filename in soundAssetMap + * 2) Define the SoundSnippet in soundAssets + * + * The preloader will handle asset loading for you. + * Make sure to check the console for any errors with loading your file + */ + + +// A mapping of asset names to their files +// This exists to give nicer names to files +let soundAssetMap = { + "debug-ding":"/assets/sounds/debug-ding.mp3" +} + +// All available sounds +let soundAssets = { + debug_ding: new SoundSnippet("debug-ding") +} + +/** + * Cache all sounds in browser, then notify a callback of success + * @param {function} callback Callback for completion + */ +function preCacheSounds(callback) { + + // Counter for number of sounds cached + let cachedCount = 0; + + Object.keys(soundAssets).forEach((key) => { + + // Get the SoundSnippet + let sound = soundAssets[key]; + + // Cache the sound + sound.cache(() => { + + // Incr the cache count + cachedCount += 1; + + // If this is the last sound, fire off the callback + if (cachedCount == Object.keys(soundAssets).length) { + callback(); + } + + }); + + }); + + + // Spawn a notifier for loading issues + setTimeout(() => { + + // If not all sounds have been cached by the time this is called, send a warning + if (cachedCount < Object.keys(soundAssets).length) { + console.warn(`[preCacheSounds] Only ${cachedCount} of ${Object.keys(soundAssets).length} sounds have been cached after 2 seconds. Is there a missing asset? or is the user on a slow connection?`); + } + + }, 2000); + +} \ No newline at end of file diff --git a/docs/assets/js/sounds/soundsnippet.js b/docs/assets/js/sounds/soundsnippet.js new file mode 100644 index 0000000..af03267 --- /dev/null +++ b/docs/assets/js/sounds/soundsnippet.js @@ -0,0 +1,81 @@ + +// Counter for hash generation +let _hashCounter = 0; + +class SoundSnippet { + + /** + * Load a sound asset to a snipper + * @param {string} asset_name Asset name as defined in soundassetmap.js + */ + constructor(asset_name) { + + // Store asset name + this.asset_name = asset_name; + + // Compute an asset hash + this.asset_hash = CryptoJS.MD5(`ASSET:${asset_name}::${_hashCounter}`); + _hashCounter += 1; + + // Read actual asset path + this.assetPath = soundAssetMap[asset_name]; + + // Set up the audio object + this.audio = new Audio(); + + } + + /** + * Cache this sound, then notify a callback of completion + * @param {function} callback callback to notify + */ + cache(callback) { + + // Set the audio SRC + this.audio.src = this.assetPath; + + // Create a callback for loading finished + this.audio.addEventListener("loadeddata", callback, true); + } + + + play() { + + // If autoplay is disabled, we notify the console + if (canPlayAudio()) { + // Play the snippet + this.audio.play(); + } else { + console.warn("[SoundSnippet] Tried to play audio with autoplay disabled. The user must press the play button before you can play audio"); + } + + } + + stop() { + + // Stop the snippet + this.audio.stop(); + + } + + /** + * Get the sound length in seconds + */ + getLengthSeconds() { + return 0; + } + + /** + * Get the asset name + */ + getName() { + return this.asset_name; + } + + /** + * Get this asset's hash. This can be used for comparing objects efficiently. + */ + getHash() { + return this.asset_hash; + } +} \ No newline at end of file diff --git a/docs/assets/sounds/debug-ding.mp3 b/docs/assets/sounds/debug-ding.mp3 new file mode 100644 index 0000000..ff9ebbd Binary files /dev/null and b/docs/assets/sounds/debug-ding.mp3 differ diff --git a/docs/index.html b/docs/index.html index ce9f65c..e89a7e5 100644 --- a/docs/index.html +++ b/docs/index.html @@ -24,6 +24,10 @@ + + + + @@ -45,6 +49,12 @@ + + + + + +