diff --git a/.gitignore b/.gitignore index bba8027..0f6f9cf 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,2 @@ .cache/ -*.min.js *.map diff --git a/dist/howler.core.min.js b/dist/howler.core.min.js new file mode 100644 index 0000000..c47759a --- /dev/null +++ b/dist/howler.core.min.js @@ -0,0 +1,2 @@ +/*! howler.js v2.0.9 | (c) 2013-2018, James Simpson of GoldFire Studios | MIT License | howlerjs.com */ +!function(){"use strict";var e=function(){this.init()};e.prototype={init:function(){var e=this||n;return e._counter=1e3,e._codecs={},e._howls=[],e._muted=!1,e._volume=1,e._canPlayEvent="canplaythrough",e._navigator="undefined"!=typeof window&&window.navigator?window.navigator:null,e.masterGain=null,e.noAudio=!1,e.usingWebAudio=!0,e.autoSuspend=!0,e.ctx=null,e.mobileAutoEnable=!0,e._setup(),e},volume:function(e){var t=this||n;if(e=parseFloat(e),t.ctx||_(),void 0!==e&&e>=0&&e<=1){if(t._volume=e,t._muted)return t;t.usingWebAudio&&t.masterGain.gain.setValueAtTime(e,n.ctx.currentTime);for(var o=0;o=0;t--)e._howls[t].unload();return e.usingWebAudio&&e.ctx&&void 0!==e.ctx.close&&(e.ctx.close(),e.ctx=null,_()),e},codecs:function(e){return(this||n)._codecs[e.replace(/^x-/,"")]},_setup:function(){var e=this||n;if(e.state=e.ctx?e.ctx.state||"running":"running",e._autoSuspend(),!e.usingWebAudio)if("undefined"!=typeof Audio)try{var t=new Audio;void 0===t.oncanplaythrough&&(e._canPlayEvent="canplay")}catch(n){e.noAudio=!0}else e.noAudio=!0;try{var t=new Audio;t.muted&&(e.noAudio=!0)}catch(e){}return e.noAudio||e._setupCodecs(),e},_setupCodecs:function(){var e=this||n,t=null;try{t="undefined"!=typeof Audio?new Audio:null}catch(n){return e}if(!t||"function"!=typeof t.canPlayType)return e;var o=t.canPlayType("audio/mpeg;").replace(/^no$/,""),r=e._navigator&&e._navigator.userAgent.match(/OPR\/([0-6].)/g),a=r&&parseInt(r[0].split("/")[1],10)<33;return e._codecs={mp3:!(a||!o&&!t.canPlayType("audio/mp3;").replace(/^no$/,"")),mpeg:!!o,opus:!!t.canPlayType('audio/ogg; codecs="opus"').replace(/^no$/,""),ogg:!!t.canPlayType('audio/ogg; codecs="vorbis"').replace(/^no$/,""),oga:!!t.canPlayType('audio/ogg; codecs="vorbis"').replace(/^no$/,""),wav:!!t.canPlayType('audio/wav; codecs="1"').replace(/^no$/,""),aac:!!t.canPlayType("audio/aac;").replace(/^no$/,""),caf:!!t.canPlayType("audio/x-caf;").replace(/^no$/,""),m4a:!!(t.canPlayType("audio/x-m4a;")||t.canPlayType("audio/m4a;")||t.canPlayType("audio/aac;")).replace(/^no$/,""),mp4:!!(t.canPlayType("audio/x-mp4;")||t.canPlayType("audio/mp4;")||t.canPlayType("audio/aac;")).replace(/^no$/,""),weba:!!t.canPlayType('audio/webm; codecs="vorbis"').replace(/^no$/,""),webm:!!t.canPlayType('audio/webm; codecs="vorbis"').replace(/^no$/,""),dolby:!!t.canPlayType('audio/mp4; codecs="ec-3"').replace(/^no$/,""),flac:!!(t.canPlayType("audio/x-flac;")||t.canPlayType("audio/flac;")).replace(/^no$/,"")},e},_enableMobileAudio:function(){var e=this||n,t=/iPhone|iPad|iPod|Android|BlackBerry|BB10|Silk|Mobi/i.test(e._navigator&&e._navigator.userAgent),o=!!("ontouchend"in window||e._navigator&&e._navigator.maxTouchPoints>0||e._navigator&&e._navigator.msMaxTouchPoints>0);if(!e._mobileEnabled&&e.ctx&&(t||o)){e._mobileEnabled=!1,e._mobileUnloaded||44100===e.ctx.sampleRate||(e._mobileUnloaded=!0,e.unload()),e._scratchBuffer=e.ctx.createBuffer(1,1,22050);var r=function(){n._autoResume();var t=e.ctx.createBufferSource();t.buffer=e._scratchBuffer,t.connect(e.ctx.destination),void 0===t.start?t.noteOn(0):t.start(0),"function"==typeof e.ctx.resume&&e.ctx.resume(),t.onended=function(){t.disconnect(0),e._mobileEnabled=!0,e.mobileAutoEnable=!1,document.removeEventListener("touchstart",r,!0),document.removeEventListener("touchend",r,!0)}};return document.addEventListener("touchstart",r,!0),document.addEventListener("touchend",r,!0),e}},_autoSuspend:function(){var e=this;if(e.autoSuspend&&e.ctx&&void 0!==e.ctx.suspend&&n.usingWebAudio){for(var t=0;t0?i._seek:o._sprite[e][0]/1e3),s=Math.max(0,(o._sprite[e][0]+o._sprite[e][1])/1e3-_),l=1e3*s/Math.abs(i._rate);i._paused=!1,i._ended=!1,i._sprite=e,i._seek=_,i._start=o._sprite[e][0]/1e3,i._stop=(o._sprite[e][0]+o._sprite[e][1])/1e3,i._loop=!(!i._loop&&!o._sprite[e][2]);var c=i._node;if(o._webAudio){var f=function(){o._refreshBuffer(i);var e=i._muted||o._muted?0:i._volume;c.gain.setValueAtTime(e,n.ctx.currentTime),i._playStart=n.ctx.currentTime,void 0===c.bufferSource.start?i._loop?c.bufferSource.noteGrainOn(0,_,86400):c.bufferSource.noteGrainOn(0,_,s):i._loop?c.bufferSource.start(0,_,86400):c.bufferSource.start(0,_,s),l!==1/0&&(o._endTimers[i._id]=setTimeout(o._ended.bind(o,i),l)),t||setTimeout(function(){o._emit("play",i._id)},0)};"running"===n.state?f():(o.once("resume",f),o._clearTimer(i._id))}else{var p=function(){c.currentTime=_,c.muted=i._muted||o._muted||n._muted||c.muted,c.volume=i._volume*n.volume(),c.playbackRate=i._rate;try{var r=c.play();if("undefined"!=typeof Promise&&r instanceof Promise){o._playLock=!0;var a=function(){o._playLock=!1,t||o._emit("play",i._id)};r.then(a,a)}else t||o._emit("play",i._id);if(c.paused)return void o._emit("playerror",i._id,"Playback was unable to start. This is most commonly an issue on mobile devices where playback was not within a user interaction.");"__default"!==e?o._endTimers[i._id]=setTimeout(o._ended.bind(o,i),l):(o._endTimers[i._id]=function(){o._ended(i),c.removeEventListener("ended",o._endTimers[i._id],!1)},c.addEventListener("ended",o._endTimers[i._id],!1))}catch(e){o._emit("playerror",i._id,e)}},m=window&&window.ejecta||!c.readyState&&n._navigator.isCocoonJS;if(c.readyState>=3||m)p();else{var v=function(){p(),c.removeEventListener(n._canPlayEvent,v,!1)};c.addEventListener(n._canPlayEvent,v,!1),o._clearTimer(i._id)}}return i._id},pause:function(e){var n=this;if("loaded"!==n._state||n._playLock)return n._queue.push({event:"pause",action:function(){n.pause(e)}}),n;for(var t=n._getSoundIds(e),o=0;o=0?t=parseInt(r[0],10):e=parseFloat(r[0])}else r.length>=2&&(e=parseFloat(r[0]),t=parseInt(r[1],10));var a;if(!(void 0!==e&&e>=0&&e<=1))return a=t?o._soundById(t):o._sounds[0],a?a._volume:0;if("loaded"!==o._state)return o._queue.push({event:"volume",action:function(){o.volume.apply(o,r)}}),o;void 0===t&&(o._volume=e),t=o._getSoundIds(t);for(var u=0;u0?o/_:o),l=Date.now();e._fadeTo=t,e._interval=setInterval(function(){var r=(Date.now()-l)/o;l=Date.now(),i+=d*r,i=Math.max(0,i),i=Math.min(1,i),i=Math.round(100*i)/100,u._webAudio?e._volume=i:u.volume(i,e._id,!0),a&&(u._volume=i),(tn&&i>=t)&&(clearInterval(e._interval),e._interval=null,e._fadeTo=null,u.volume(t,e._id),u._emit("fade",e._id))},s)},_stopFade:function(e){var t=this,o=t._soundById(e);return o&&o._interval&&(t._webAudio&&o._node.gain.cancelScheduledValues(n.ctx.currentTime),clearInterval(o._interval),o._interval=null,t.volume(o._fadeTo,e),o._fadeTo=null,t._emit("fade",e)),t},loop:function(){var e,n,t,o=this,r=arguments;if(0===r.length)return o._loop;if(1===r.length){if("boolean"!=typeof r[0])return!!(t=o._soundById(parseInt(r[0],10)))&&t._loop;e=r[0],o._loop=e}else 2===r.length&&(e=r[0],n=parseInt(r[1],10));for(var a=o._getSoundIds(n),u=0;u=0?t=parseInt(r[0],10):e=parseFloat(r[0])}else 2===r.length&&(e=parseFloat(r[0]),t=parseInt(r[1],10));var i;if("number"!=typeof e)return i=o._soundById(t),i?i._rate:o._rate;if("loaded"!==o._state)return o._queue.push({event:"rate",action:function(){o.rate.apply(o,r)}}),o;void 0===t&&(o._rate=e),t=o._getSoundIds(t);for(var d=0;d=0?t=parseInt(r[0],10):o._sounds.length&&(t=o._sounds[0]._id,e=parseFloat(r[0]))}else 2===r.length&&(e=parseFloat(r[0]),t=parseInt(r[1],10));if(void 0===t)return o;if("loaded"!==o._state)return o._queue.push({event:"seek",action:function(){o.seek.apply(o,r)}}),o;var i=o._soundById(t);if(i){if(!("number"==typeof e&&e>=0)){if(o._webAudio){var d=o.playing(t)?n.ctx.currentTime-i._playStart:0,_=i._rateSeek?i._rateSeek-i._seek:0;return i._seek+(_+d*Math.abs(i._rate))}return i._node.currentTime}var s=o.playing(t);if(s&&o.pause(t,!0),i._seek=e,i._ended=!1,o._clearTimer(t),s&&o.play(t,!0),!o._webAudio&&i._node&&(i._node.currentTime=e),s&&!o._webAudio){var l=function(){o._playLock?setTimeout(l,0):o._emit("seek",t)};setTimeout(l,0)}else o._emit("seek",t)}return o},playing:function(e){var n=this;if("number"==typeof e){var t=n._soundById(e);return!!t&&!t._paused}for(var o=0;o=0&&n._howls.splice(a,1)}var u=!0;for(o=0;o=0;a--)r[a].id&&r[a].id!==n&&"load"!==e||(setTimeout(function(e){e.call(this,n,t)}.bind(o,r[a].fn),0),r[a].once&&o.off(e,r[a].fn,r[a].id));return o._loadQueue(e),o},_loadQueue:function(e){var n=this;if(n._queue.length>0){var t=n._queue[0];t.event===e&&(n._queue.shift(),n._loadQueue()),e||t.action()}return n},_ended:function(e){var t=this,o=e._sprite;if(!t._webAudio&&e._node&&!e._node.paused&&!e._node.ended&&e._node.currentTime=0;o--){if(t<=n)return;e._sounds[o]._ended&&(e._webAudio&&e._sounds[o]._node&&e._sounds[o]._node.disconnect(0),e._sounds.splice(o,1),t--)}}},_getSoundIds:function(e){var n=this;if(void 0===e){for(var t=[],o=0;o0&&(r[t._src]=e,d(t,e))},function(){t._emit("loaderror",null,"Decoding audio data failed.")})},d=function(e,n){n&&!e._duration&&(e._duration=n.duration),0===Object.keys(e._sprite).length&&(e._sprite={__default:[0,1e3*e._duration]}),"loaded"!==e._state&&(e._state="loaded",e._emit("load"),e._loadQueue())},_=function(){try{"undefined"!=typeof AudioContext?n.ctx=new AudioContext:"undefined"!=typeof webkitAudioContext?n.ctx=new webkitAudioContext:n.usingWebAudio=!1}catch(e){n.usingWebAudio=!1}var e=/iP(hone|od|ad)/.test(n._navigator&&n._navigator.platform),t=n._navigator&&n._navigator.appVersion.match(/OS (\d+)_(\d+)_?(\d+)?/),o=t?parseInt(t[1],10):null;if(e&&o&&o<9){var r=/safari/.test(n._navigator&&n._navigator.userAgent.toLowerCase());(n._navigator&&n._navigator.standalone&&!r||n._navigator&&!n._navigator.standalone&&!r)&&(n.usingWebAudio=!1)}n.usingWebAudio&&(n.masterGain=void 0===n.ctx.createGain?n.ctx.createGainNode():n.ctx.createGain(),n.masterGain.gain.setValueAtTime(n._muted?0:1,n.ctx.currentTime),n.masterGain.connect(n.ctx.destination)),n._setup()};"function"==typeof define&&define.amd&&define([],function(){return{Howler:n,Howl:t}}),"undefined"!=typeof exports&&(exports.Howler=n,exports.Howl=t),"undefined"!=typeof window?(window.HowlerGlobal=e,window.Howler=n,window.Howl=t,window.Sound=o):"undefined"!=typeof global&&(global.HowlerGlobal=e,global.Howler=n,global.Howl=t,global.Sound=o)}(); \ No newline at end of file diff --git a/dist/howler.min.js b/dist/howler.min.js new file mode 100644 index 0000000..68102b3 --- /dev/null +++ b/dist/howler.min.js @@ -0,0 +1,4 @@ +/*! howler.js v2.0.9 | (c) 2013-2018, James Simpson of GoldFire Studios | MIT License | howlerjs.com */ +!function(){"use strict";var e=function(){this.init()};e.prototype={init:function(){var e=this||n;return e._counter=1e3,e._codecs={},e._howls=[],e._muted=!1,e._volume=1,e._canPlayEvent="canplaythrough",e._navigator="undefined"!=typeof window&&window.navigator?window.navigator:null,e.masterGain=null,e.noAudio=!1,e.usingWebAudio=!0,e.autoSuspend=!0,e.ctx=null,e.mobileAutoEnable=!0,e._setup(),e},volume:function(e){var t=this||n;if(e=parseFloat(e),t.ctx||_(),void 0!==e&&e>=0&&e<=1){if(t._volume=e,t._muted)return t;t.usingWebAudio&&t.masterGain.gain.setValueAtTime(e,n.ctx.currentTime);for(var o=0;o=0;t--)e._howls[t].unload();return e.usingWebAudio&&e.ctx&&void 0!==e.ctx.close&&(e.ctx.close(),e.ctx=null,_()),e},codecs:function(e){return(this||n)._codecs[e.replace(/^x-/,"")]},_setup:function(){var e=this||n;if(e.state=e.ctx?e.ctx.state||"running":"running",e._autoSuspend(),!e.usingWebAudio)if("undefined"!=typeof Audio)try{var t=new Audio;void 0===t.oncanplaythrough&&(e._canPlayEvent="canplay")}catch(n){e.noAudio=!0}else e.noAudio=!0;try{var t=new Audio;t.muted&&(e.noAudio=!0)}catch(e){}return e.noAudio||e._setupCodecs(),e},_setupCodecs:function(){var e=this||n,t=null;try{t="undefined"!=typeof Audio?new Audio:null}catch(n){return e}if(!t||"function"!=typeof t.canPlayType)return e;var o=t.canPlayType("audio/mpeg;").replace(/^no$/,""),r=e._navigator&&e._navigator.userAgent.match(/OPR\/([0-6].)/g),a=r&&parseInt(r[0].split("/")[1],10)<33;return e._codecs={mp3:!(a||!o&&!t.canPlayType("audio/mp3;").replace(/^no$/,"")),mpeg:!!o,opus:!!t.canPlayType('audio/ogg; codecs="opus"').replace(/^no$/,""),ogg:!!t.canPlayType('audio/ogg; codecs="vorbis"').replace(/^no$/,""),oga:!!t.canPlayType('audio/ogg; codecs="vorbis"').replace(/^no$/,""),wav:!!t.canPlayType('audio/wav; codecs="1"').replace(/^no$/,""),aac:!!t.canPlayType("audio/aac;").replace(/^no$/,""),caf:!!t.canPlayType("audio/x-caf;").replace(/^no$/,""),m4a:!!(t.canPlayType("audio/x-m4a;")||t.canPlayType("audio/m4a;")||t.canPlayType("audio/aac;")).replace(/^no$/,""),mp4:!!(t.canPlayType("audio/x-mp4;")||t.canPlayType("audio/mp4;")||t.canPlayType("audio/aac;")).replace(/^no$/,""),weba:!!t.canPlayType('audio/webm; codecs="vorbis"').replace(/^no$/,""),webm:!!t.canPlayType('audio/webm; codecs="vorbis"').replace(/^no$/,""),dolby:!!t.canPlayType('audio/mp4; codecs="ec-3"').replace(/^no$/,""),flac:!!(t.canPlayType("audio/x-flac;")||t.canPlayType("audio/flac;")).replace(/^no$/,"")},e},_enableMobileAudio:function(){var e=this||n,t=/iPhone|iPad|iPod|Android|BlackBerry|BB10|Silk|Mobi/i.test(e._navigator&&e._navigator.userAgent),o=!!("ontouchend"in window||e._navigator&&e._navigator.maxTouchPoints>0||e._navigator&&e._navigator.msMaxTouchPoints>0);if(!e._mobileEnabled&&e.ctx&&(t||o)){e._mobileEnabled=!1,e._mobileUnloaded||44100===e.ctx.sampleRate||(e._mobileUnloaded=!0,e.unload()),e._scratchBuffer=e.ctx.createBuffer(1,1,22050);var r=function(){n._autoResume();var t=e.ctx.createBufferSource();t.buffer=e._scratchBuffer,t.connect(e.ctx.destination),void 0===t.start?t.noteOn(0):t.start(0),"function"==typeof e.ctx.resume&&e.ctx.resume(),t.onended=function(){t.disconnect(0),e._mobileEnabled=!0,e.mobileAutoEnable=!1,document.removeEventListener("touchstart",r,!0),document.removeEventListener("touchend",r,!0)}};return document.addEventListener("touchstart",r,!0),document.addEventListener("touchend",r,!0),e}},_autoSuspend:function(){var e=this;if(e.autoSuspend&&e.ctx&&void 0!==e.ctx.suspend&&n.usingWebAudio){for(var t=0;t0?i._seek:o._sprite[e][0]/1e3),s=Math.max(0,(o._sprite[e][0]+o._sprite[e][1])/1e3-_),l=1e3*s/Math.abs(i._rate);i._paused=!1,i._ended=!1,i._sprite=e,i._seek=_,i._start=o._sprite[e][0]/1e3,i._stop=(o._sprite[e][0]+o._sprite[e][1])/1e3,i._loop=!(!i._loop&&!o._sprite[e][2]);var c=i._node;if(o._webAudio){var f=function(){o._refreshBuffer(i);var e=i._muted||o._muted?0:i._volume;c.gain.setValueAtTime(e,n.ctx.currentTime),i._playStart=n.ctx.currentTime,void 0===c.bufferSource.start?i._loop?c.bufferSource.noteGrainOn(0,_,86400):c.bufferSource.noteGrainOn(0,_,s):i._loop?c.bufferSource.start(0,_,86400):c.bufferSource.start(0,_,s),l!==1/0&&(o._endTimers[i._id]=setTimeout(o._ended.bind(o,i),l)),t||setTimeout(function(){o._emit("play",i._id)},0)};"running"===n.state?f():(o.once("resume",f),o._clearTimer(i._id))}else{var p=function(){c.currentTime=_,c.muted=i._muted||o._muted||n._muted||c.muted,c.volume=i._volume*n.volume(),c.playbackRate=i._rate;try{var r=c.play();if("undefined"!=typeof Promise&&r instanceof Promise){o._playLock=!0;var a=function(){o._playLock=!1,t||o._emit("play",i._id)};r.then(a,a)}else t||o._emit("play",i._id);if(c.paused)return void o._emit("playerror",i._id,"Playback was unable to start. This is most commonly an issue on mobile devices where playback was not within a user interaction.");"__default"!==e?o._endTimers[i._id]=setTimeout(o._ended.bind(o,i),l):(o._endTimers[i._id]=function(){o._ended(i),c.removeEventListener("ended",o._endTimers[i._id],!1)},c.addEventListener("ended",o._endTimers[i._id],!1))}catch(e){o._emit("playerror",i._id,e)}},m=window&&window.ejecta||!c.readyState&&n._navigator.isCocoonJS;if(c.readyState>=3||m)p();else{var v=function(){p(),c.removeEventListener(n._canPlayEvent,v,!1)};c.addEventListener(n._canPlayEvent,v,!1),o._clearTimer(i._id)}}return i._id},pause:function(e){var n=this;if("loaded"!==n._state||n._playLock)return n._queue.push({event:"pause",action:function(){n.pause(e)}}),n;for(var t=n._getSoundIds(e),o=0;o=0?t=parseInt(r[0],10):e=parseFloat(r[0])}else r.length>=2&&(e=parseFloat(r[0]),t=parseInt(r[1],10));var a;if(!(void 0!==e&&e>=0&&e<=1))return a=t?o._soundById(t):o._sounds[0],a?a._volume:0;if("loaded"!==o._state)return o._queue.push({event:"volume",action:function(){o.volume.apply(o,r)}}),o;void 0===t&&(o._volume=e),t=o._getSoundIds(t);for(var u=0;u0?o/_:o),l=Date.now();e._fadeTo=t,e._interval=setInterval(function(){var r=(Date.now()-l)/o;l=Date.now(),i+=d*r,i=Math.max(0,i),i=Math.min(1,i),i=Math.round(100*i)/100,u._webAudio?e._volume=i:u.volume(i,e._id,!0),a&&(u._volume=i),(tn&&i>=t)&&(clearInterval(e._interval),e._interval=null,e._fadeTo=null,u.volume(t,e._id),u._emit("fade",e._id))},s)},_stopFade:function(e){var t=this,o=t._soundById(e);return o&&o._interval&&(t._webAudio&&o._node.gain.cancelScheduledValues(n.ctx.currentTime),clearInterval(o._interval),o._interval=null,t.volume(o._fadeTo,e),o._fadeTo=null,t._emit("fade",e)),t},loop:function(){var e,n,t,o=this,r=arguments;if(0===r.length)return o._loop;if(1===r.length){if("boolean"!=typeof r[0])return!!(t=o._soundById(parseInt(r[0],10)))&&t._loop;e=r[0],o._loop=e}else 2===r.length&&(e=r[0],n=parseInt(r[1],10));for(var a=o._getSoundIds(n),u=0;u=0?t=parseInt(r[0],10):e=parseFloat(r[0])}else 2===r.length&&(e=parseFloat(r[0]),t=parseInt(r[1],10));var i;if("number"!=typeof e)return i=o._soundById(t),i?i._rate:o._rate;if("loaded"!==o._state)return o._queue.push({event:"rate",action:function(){o.rate.apply(o,r)}}),o;void 0===t&&(o._rate=e),t=o._getSoundIds(t);for(var d=0;d=0?t=parseInt(r[0],10):o._sounds.length&&(t=o._sounds[0]._id,e=parseFloat(r[0]))}else 2===r.length&&(e=parseFloat(r[0]),t=parseInt(r[1],10));if(void 0===t)return o;if("loaded"!==o._state)return o._queue.push({event:"seek",action:function(){o.seek.apply(o,r)}}),o;var i=o._soundById(t);if(i){if(!("number"==typeof e&&e>=0)){if(o._webAudio){var d=o.playing(t)?n.ctx.currentTime-i._playStart:0,_=i._rateSeek?i._rateSeek-i._seek:0;return i._seek+(_+d*Math.abs(i._rate))}return i._node.currentTime}var s=o.playing(t);if(s&&o.pause(t,!0),i._seek=e,i._ended=!1,o._clearTimer(t),s&&o.play(t,!0),!o._webAudio&&i._node&&(i._node.currentTime=e),s&&!o._webAudio){var l=function(){o._playLock?setTimeout(l,0):o._emit("seek",t)};setTimeout(l,0)}else o._emit("seek",t)}return o},playing:function(e){var n=this;if("number"==typeof e){var t=n._soundById(e);return!!t&&!t._paused}for(var o=0;o=0&&n._howls.splice(a,1)}var u=!0;for(o=0;o=0;a--)r[a].id&&r[a].id!==n&&"load"!==e||(setTimeout(function(e){e.call(this,n,t)}.bind(o,r[a].fn),0),r[a].once&&o.off(e,r[a].fn,r[a].id));return o._loadQueue(e),o},_loadQueue:function(e){var n=this;if(n._queue.length>0){var t=n._queue[0];t.event===e&&(n._queue.shift(),n._loadQueue()),e||t.action()}return n},_ended:function(e){var t=this,o=e._sprite;if(!t._webAudio&&e._node&&!e._node.paused&&!e._node.ended&&e._node.currentTime=0;o--){if(t<=n)return;e._sounds[o]._ended&&(e._webAudio&&e._sounds[o]._node&&e._sounds[o]._node.disconnect(0),e._sounds.splice(o,1),t--)}}},_getSoundIds:function(e){var n=this;if(void 0===e){for(var t=[],o=0;o0&&(r[t._src]=e,d(t,e))},function(){t._emit("loaderror",null,"Decoding audio data failed.")})},d=function(e,n){n&&!e._duration&&(e._duration=n.duration),0===Object.keys(e._sprite).length&&(e._sprite={__default:[0,1e3*e._duration]}),"loaded"!==e._state&&(e._state="loaded",e._emit("load"),e._loadQueue())},_=function(){try{"undefined"!=typeof AudioContext?n.ctx=new AudioContext:"undefined"!=typeof webkitAudioContext?n.ctx=new webkitAudioContext:n.usingWebAudio=!1}catch(e){n.usingWebAudio=!1}var e=/iP(hone|od|ad)/.test(n._navigator&&n._navigator.platform),t=n._navigator&&n._navigator.appVersion.match(/OS (\d+)_(\d+)_?(\d+)?/),o=t?parseInt(t[1],10):null;if(e&&o&&o<9){var r=/safari/.test(n._navigator&&n._navigator.userAgent.toLowerCase());(n._navigator&&n._navigator.standalone&&!r||n._navigator&&!n._navigator.standalone&&!r)&&(n.usingWebAudio=!1)}n.usingWebAudio&&(n.masterGain=void 0===n.ctx.createGain?n.ctx.createGainNode():n.ctx.createGain(),n.masterGain.gain.setValueAtTime(n._muted?0:1,n.ctx.currentTime),n.masterGain.connect(n.ctx.destination)),n._setup()};"function"==typeof define&&define.amd&&define([],function(){return{Howler:n,Howl:t}}),"undefined"!=typeof exports&&(exports.Howler=n,exports.Howl=t),"undefined"!=typeof window?(window.HowlerGlobal=e,window.Howler=n,window.Howl=t,window.Sound=o):"undefined"!=typeof global&&(global.HowlerGlobal=e,global.Howler=n,global.Howl=t,global.Sound=o)}(); +/*! Spatial Plugin */ +!function(){"use strict";HowlerGlobal.prototype._pos=[0,0,0],HowlerGlobal.prototype._orientation=[0,0,-1,0,1,0],HowlerGlobal.prototype.stereo=function(n){var e=this;if(!e.ctx||!e.ctx.listener)return e;for(var t=e._howls.length-1;t>=0;t--)e._howls[t].stereo(n);return e},HowlerGlobal.prototype.pos=function(n,e,t){var o=this;return o.ctx&&o.ctx.listener?(e="number"!=typeof e?o._pos[1]:e,t="number"!=typeof t?o._pos[2]:t,"number"!=typeof n?o._pos:(o._pos=[n,e,t],o.ctx.listener.setPosition(o._pos[0],o._pos[1],o._pos[2]),o)):o},HowlerGlobal.prototype.orientation=function(n,e,t,o,r,a){var i=this;if(!i.ctx||!i.ctx.listener)return i;var p=i._orientation;return e="number"!=typeof e?p[1]:e,t="number"!=typeof t?p[2]:t,o="number"!=typeof o?p[3]:o,r="number"!=typeof r?p[4]:r,a="number"!=typeof a?p[5]:a,"number"!=typeof n?p:(i._orientation=[n,e,t,o,r,a],i.ctx.listener.setOrientation(n,e,t,o,r,a),i)},Howl.prototype.init=function(n){return function(e){var t=this;return t._orientation=e.orientation||[1,0,0],t._stereo=e.stereo||null,t._pos=e.pos||null,t._pannerAttr={coneInnerAngle:void 0!==e.coneInnerAngle?e.coneInnerAngle:360,coneOuterAngle:void 0!==e.coneOuterAngle?e.coneOuterAngle:360,coneOuterGain:void 0!==e.coneOuterGain?e.coneOuterGain:0,distanceModel:void 0!==e.distanceModel?e.distanceModel:"inverse",maxDistance:void 0!==e.maxDistance?e.maxDistance:1e4,panningModel:void 0!==e.panningModel?e.panningModel:"HRTF",refDistance:void 0!==e.refDistance?e.refDistance:1,rolloffFactor:void 0!==e.rolloffFactor?e.rolloffFactor:1},t._onstereo=e.onstereo?[{fn:e.onstereo}]:[],t._onpos=e.onpos?[{fn:e.onpos}]:[],t._onorientation=e.onorientation?[{fn:e.onorientation}]:[],n.call(this,e)}}(Howl.prototype.init),Howl.prototype.stereo=function(e,t){var o=this;if(!o._webAudio)return o;if("loaded"!==o._state)return o._queue.push({event:"stereo",action:function(){o.stereo(e,t)}}),o;var r=void 0===Howler.ctx.createStereoPanner?"spatial":"stereo";if(void 0===t){if("number"!=typeof e)return o._stereo;o._stereo=e,o._pos=[e,0,0]}for(var a=o._getSoundIds(t),i=0;i Object.entries(obj).forEach(([k, v]) => { + typeof v == 'object' ? parse(v, convert) : obj[k] = convert(v); + }); + + let promises = []; + parse(images, str => { + let img = new Image(); + img.src = 'img/' + str; + promises.push(new Promise((res) => { + img.addEventListener('load', res); + })); + return img; + }); + parse(audio, str => { + let audio = new Howl({ + src: ['audio/' + str] + }); + promises.push(new Promise((res) => { + audio.once('load', res); + })); + return audio; + }); + + await Promise.all(promises); +} + +class Module { + constructor(x, y, ship, { + name = 'Unnamed Module', + type = 'block', + id = 'unknown', + mass = 1, + fuelCapacity = 0, + ...properties + }) { + this.x = x; + this.y = y; + this.name = name; + this.type = type; + this.mass = mass; + this.ship = ship; + this.id = id; + this.images = images.modules[this.type][this.id]; + this.data = modules[this.type][this.id]; + + if (this.type == 'thruster') { + this.power = 0; + } + } + + reset() { + if (this.type == 'thruster') { + this.power = 0; + } + } + + get com() { + return this.ship.getWorldPoint(...this.localCom); + } + + get currentImage() { + if (this.type == 'thruster') { + return this.power > 0 ? this.images.on : this.images.off; + } else { + return this.images; + } + } + + get localCom() { + return [this.x + 0.5, this.y + 0.5]; + } +} + +/* + * Constants that do not change during gameplay. + * This can kind of be treated like a configuration file, I guess. + * + * All length units are relative to the size of a small ship module, which + * is always 1x1. + */ + +// For fixing floating point rounding errors. +const EPSILON = 1e-8; +// Don't change these. +const TAU = Math.PI * 2; +// Unit length of sector. May affect spawning a bit. +const SECTOR_SIZE = 512; +// G, G-boy, The big G, Mr. G, g's big brother, G-dog +const GRAVITATIONAL_CONSTANT = 0.002; +// Perspective constraints. Higher zoom value = closer. +const MIN_ZOOM = 1; +const MAX_ZOOM = 100; +const DEFAULT_ZOOM = 10; +const ZOOM_SPEED = 0.01; +// Ship landing. Angle in radians. +const TIP_ANGLE = 0.25; +const TIP_SPEED = 0.03; +const CRASH_SPEED = 0.7; +// Ship flight mechanics. Speed measured in units per tick. +const FUEL_BURN_RATE = 0.5; +const THRUST_POWER = 0.004; +const TURN_POWER = 0.07; +// Distance at which an orbited planet will not be considered a parent body. +const MAX_PARENT_CELESTIAL_DISTANCE = 120; +// Ship editing. +const EDIT_MARGIN = 2; +// Floating items. +const ENTITY_ROTATION_RATE = 0.01; +// World generation. +const PLANET_SPAWN_RATE = 100; +const ENTITY_SPAWN_RATE = 8; +const MIN_CELESTIAL_SPACING = 15; +const FUEL_CAN_AMOUNT = 4; + +class Body { + constructor(x, y, mass) { + this.x = x; + this.y = y; + this.r = 0; + this.xvel = 0; + this.yvel = 0; + this.rvel = 0; + this.rfriction = 0.9; + this.mass = mass; + } + + get com() { + return [this.x, this.y]; + } + + get pos() { + return [this.x, this.y]; + } + + get speed() { + return Math.sqrt(this.xvel ** 2 + this.yvel ** 2); + } + + angleDifference(a, b) { + return Math.atan2(Math.sin(a - b), Math.cos(a - b)); + } + + normalizeAngle(a = this.r) { + return ((a % TAU) + TAU) % TAU; + } + + getCelestialCollision(celestials) { + let result = false; + celestials.forEach(c => { + let dis = this.distanceTo(c); + if (dis < c.radius) result = c; + }); + return result; + } + + getWorldPoint(lx, ly, test) { + let [cx, cy] = this.localCom; + let [nx, ny] = this.rotateVector(lx - cx, ly - cy, this.r); + return [nx + this.x + cx, ny + this.y + cy]; + } + + getLocalPoint(wx, wy) { + let [lx, ly] = [wx - this.x, wy - this.y]; + let [cx, cy] = this.localCom; + let [nx, ny] = this.rotateVector(lx, ly, -this.r); + return [nx - cx, ny - cy]; + } + + rotateVector(x, y, r = this.r) { + return [(x * Math.cos(-r) + y * Math.sin(-r)), + -(-y * Math.cos(-r) + x * Math.sin(-r))]; + } + + // TODO: Remove and replace uses with `rotateVector`. + relativeVector(x, y) { + return this.rotateVector(x, y, this.r); + } + + tickMotion(speed = 1) { + this.x += this.xvel * speed; + this.y += this.yvel * speed; + this.r += this.rvel * speed; + this.rvel *= this.rfriction * speed; + } + + tickGravity(bodies, speed = 1) { + bodies.forEach(b => { + let force = b.mass / (this.distanceTo(b) ** 2) * GRAVITATIONAL_CONSTANT; + let [[ax, ay], [bx, by]] = [this.com, b.com]; + let angle = Math.atan2(by - ay, bx - ax); + this.xvel += Math.cos(angle) * force * speed; + this.yvel += Math.sin(angle) * force * speed; + }); + } + + distanceTo(body) { + let [[ax, ay], [bx, by]] = [this.com, body.com]; + return Math.max(Math.sqrt(((bx - ax) ** 2) + + ((by - ay) ** 2)), 1); + } + + angleTo(ax, ay, bx, by) { + return Math.atan2(by - ay, bx - ax); + } + + approach(body, distance) { + let [[ax, ay], [bx, by]] = [this.com, body.com]; + let angle = Math.atan2(by - ay, bx - ax); + this.x += Math.cos(angle) * distance; + this.y += Math.sin(angle) * distance; + } + + halt() { + this.xvel = 0; + this.yvel = 0; + } + + applyDirectionalForce(x, y, r) { + let [vx, vy] = this.rotateVector(x, y); + this.xvel += vx / this.mass; + this.yvel += vy / this.mass; + this.rvel += r / this.mass; + } + + orbit(cel, altitude, angle = 0) { + this.gravity = true; + let speed = Math.sqrt(GRAVITATIONAL_CONSTANT * cel.mass / (altitude + cel.radius)); + let [cx, cy] = cel.com; + let [comX, comY] = this.localCom; + let [dx, dy] = this.rotateVector(0, -(altitude + cel.radius), angle); + [this.xvel, this.yvel] = this.rotateVector(speed, 0, angle); + this.x = cx + dx - comX; + this.y = cy + dy - comY; + } +} + +function createThrustExhaust(thruster) { + let ship = thruster.ship; + let [fx, fy] = ship.relativeVector(0, 0.2); + let [dx, dy] = ship.relativeVector((Math.random() - 0.5) * 0.5, 0.5); + let [cx, cy] = thruster.com; + particles.add(new Particle(cx + dx, cy + dy, { + xvel: ship.xvel + fx, + yvel: ship.yvel + fy, + color: '#f4c542', + lifetime: 5, + size: 0.07 + })); +} + +function createEndEditBurst(ship) { + for (let i = 0; i < 20; i++) { + particles.add(new Particle(...ship.poc, { + color: '#ccc', + lifetime: Math.random() * 30 + 25, + size: Math.random() * 0.3 + 0.05, + spray: 0.3, + gravity: true + })); + } +} + +function createCrash(ship) { + for (let i = 0; i < ship.mass + 3; i++) { + particles.add(new Particle(...ship.com, { + color: '#f2e860', + lifetime: Math.random() * 50 + 40, + size: Math.random() * 0.2 + 0.2, + spray: 0.9, + gravity: true + })); + } + for (let i = 0; i < ship.mass + 3; i++) { + particles.add(new Particle(...ship.com, { + color: '#f75722', + lifetime: Math.random() * 50 + 40, + size: Math.random() * 0.2 + 0.2, + spray: 0.5, + gravity: true + })); + } + for (let i = 0; i < ship.mass * 2 + 3; i++) { + particles.add(new Particle(...ship.com, { + color: '#888', + lifetime: Math.random() * 30 + 55, + size: Math.random() * 0.5 + 0.4, + spray: 2, + friction: 0.9, + gravity: false + })); + } +} + +function createPickupBurst(ship, point) { + for (let i = 0; i < 20; i++) { + particles.add(new Particle(...point, { + xvel: ship.xvel, + yvel: ship.yvel, + color: '#eae55d', + lifetime: Math.random() * 20 + 15, + friction: 0.95, + size: Math.random() * 0.2 + 0.05, + spray: 0.3 + })); + } +} + +function createItemToss(ship) { + particles.add(new Particle(...ship.com, { + xvel: ship.xvel, + yvel: ship.yvel, + color: '#a87234', + lifetime: 50, + size: 0.6, + spray: 0.4 + })); +} + +class Particle extends Body { + constructor(x, y, { + xvel = 0, + yvel = 0, + spray = 0.1, + fizzle = 0, + maxFizzle = fizzle * 2, + color = '#fff', + gravity = false, + lifetime = 50, + size = 0.1, + friction = 0.99 + }) { + super(x, y, 0.1); + + this.size = size; + this.xvel = xvel + (Math.random() - 0.5) * spray; + this.yvel = yvel + (Math.random() - 0.5) * spray; + this.friction = friction; + this.fizzle = fizzle; + this.maxFizzle = maxFizzle; + this.color = color; + this.gravity = gravity; + this.life = lifetime; + } + + get com() { + return [this.x - this.size / 2, this.y - this.size / 2]; + } + + tick() { + if (this.life-- <= 0) { + particles.delete(this); + return; + } + + this.tickMotion(); + if (this.gravity) this.tickGravity(celestials); + + this.xvel *= this.friction; + this.yvel *= this.friction; + this.x += (Math.random() - 0.5) * this.fizzle; + this.y += (Math.random() - 0.5) * this.fizzle; + if (this.fizzle < this.mazFizzle) this.fizzle *= 1.05; + } +} + +const items = new Map(); +let currentItem = null; +let capacity = 0; +let usedSpace = 0; + +let onupdate = () => {}; + +function init$1() { + items.clear(); +} + +function canToss() { + return !state.editing || message === 'inventory too full' + || message === ''; +} + +function getTiles() { + let list = Array.from(items.values()); + list.sort((a, b) => toId(...a.ident) < toId(...b.ident)); + usedSpace = list.reduce((a, b) => a + b.quantity, 0); + return list; +} + +function addItem(type, id) { + let mapId = toId(type, id); + if (!items.has(mapId)) items.set(mapId, new Tile(type, id)); + let tile = items.get(mapId); + tile.increase(); + update(); +} + +function removeItem(type, id) { + let mapId = toId(type, id); + if (!items.has(mapId)) return; + let tile = items.get(mapId); + tile.decrease(); + if (tile.quantity == 0) { + items.delete(mapId); + currentItem = null; + } + if (canToss()) + tossItem(); + update(); + validate(); +} + +function selectItem(type, id) { + currentItem = items.get(toId(type, id)); + update(); +} + +function setOnupdate(func) { + onupdate = func; +} + +function update() { + capacity = playerShip.cargoCapacity; + onupdate(); +} + +function toId(type, id) { + return `${type}.${id}`; +} + +class Tile { + constructor(type, id, q = 0) { + this.type = type; + this.id = id; + this.mapId = toId(type, id); + this.quantity = q; + this.image = images.modules[type][id]; + this.data = modules[type][id]; + if (type === 'thruster') this.image = this.image.off; + } + + get textInfo() { + let text = this.data.name + '\n\n' + this.data.tooltip + '\n\n'; + text += 'Mass: ' + this.data.mass + '\n'; + + if (this.type === 'thruster') + text += 'Power: ' + this.data.thrust + '\n'; + if (this.type === 'fuel') + text += 'Fuel capacity: ' + this.data.fuelCapacity + '\n'; + if (this.type === 'capsule') { + text += 'Rotational power: ' + this.data.rotation + '\n'; + text += 'Cargo space: ' + this.data.capacity + '\n'; + } + + return text; + } + + get ident() { + return [this.type, this.id]; + } + + increase() { + this.quantity++; + } + + decrease() { + this.quantity = Math.max(0, this.quantity - 1); + } +} + +const playing = new Map(); + +function play(name) { + audio[name].play(); +} + +function start(name) { + if (!playing.has(name)) + playing.set(name, audio[name]); + + let howl = playing.get(name); + howl.loop(true); + howl.play(); +} + +function stop(name) { + if (!playing.has(name)) return false; + let howl = playing.get(name); + if (howl.playing()) { + howl.stop(); + return true; + } + return false; +} + +function toggle(name) { + if (!stop(name)) start(name); +} + +function volume(name, level) { + if (!playing.has(name)) return; + playing.get(name).volume(level); +} + +let shipLanded = false; +let score = 0; +let gameOverReason = ''; +let scoreText = ''; + +let notification = null; +let notLife = 0; + +let landedPlanets = new Set(); + +function init$2() { + score = 0; + shipLanded = false; +} + +function outOfFuel() { + gameOver('You ran out of fuel'); +} + +function playMusic() { + start('music'); + volume('music', 0.4); +} + +function notify(message$$1, time = 80) { + if (notification === null) return; + notification.text = message$$1; + notLife = time; +} + +function tick() { + if (notification === null) return; + if ((notLife-- <= 0 || state.gameOver) && !state.paused) + notification.text = ''; +} + +function setNotificationElement(el) { + notification = el; +} + +function startGame() { + init$2(); + state.gameOver = false; + changeView$1('game'); + perspective.reset(); + perspective.focusPlayer(); +} + +function toMenu() { + changeView$1('menu'); +} + +function togglePause() { + console.log(state.paused); + state.paused = !state.paused; + play('pause'); + if (state.paused) { + notify('Paused', 0); + } +} + +function landShip(planet) { + shipLanded = true; + if (!landedPlanets.has(planet)) { + newPlanet(planet); + } + state.landed = true; +} + +function howToPlay() { + changeView$1('instructions'); +} + +function newPlanet(planet) { + let value = (planet.radius * 2 + 50) | 0; + landedPlanets.add(planet); + play('newPlanet'); + score += value; + notify('Landed on new planet: +' + value); +} + +function launchShip() { + shipLanded = false; + state.landed = false; +} + +function crash() { + gameOver('You crashed'); + play('crash'); + createCrash(playerShip); +} + +function gameOver(reason) { + if (state.editing) + endEditing(); + gameOverReason = reason; + state.gameOver = true; + state.inventory = false; + state.editing = false; + perspective.changeZoom(MIN_ZOOM, 0.99); + let massScore = playerShip.mass * 100; + let fuelScore = playerShip.fuel * 50 | 0; + let finalScore = massScore + fuelScore + score; + scoreText = 'Ship mass: ' + + ' '.repeat(5 - ('' + massScore).length) + massScore + '\n' + + 'Remaining fuel: ' + + ' '.repeat(5 - ('' + fuelScore).length) + fuelScore + '\n' + + 'Score: ' + + ' '.repeat(5 - ('' + score).length) + score + '\n\n' + + 'Final score: ' + + ' '.repeat(5 - ('' + finalScore).length) + finalScore; +} + +function toggleEdit() { + if (state.editing) { + endEditing(); + return; + } + state.editing = true; + state.inventory = true; + init$4(); +} + +function toggleTrace() { + let trace$$1 = toggleTrace$1(); + notify('Path prediction: ' + (trace$$1 ? 'on' : 'off')); +} + +function toggleMarkers() { + let markers$$1 = toggleMarkers$1(); + notify('Item markers: ' + (markers$$1 ? 'on' : 'off')); +} + +function cycleRotationMode() { + let message$$1 = { + parent: 'planet', + local: 'ship', + universe: 'universe' + }[cycleRotationMode$1()]; + + notify('Rotation view: ' + message$$1); +} + +function endEditing() { + let {valid, reason} = end(); + + if (valid) { + play('endEdit'); + createEndEditBurst(playerShip); + changePerspective('universe'); + state.editing = false; + state.inventory = false; + } +} + +function tossItem() { + createItemToss(playerShip); + play('toss'); +} + +function collectItem(type, id, name) { + if (type === 'fuelcan') { + playerShip.addFuel(FUEL_CAN_AMOUNT); + play('fuelPickup'); + score += 10; + notify('Collected fuel: +10'); + return true; + } else { + if (usedSpace > capacity) { + notify('No space left in inventory', 60); + return false; + } + addItem(type, id); + play('itemPickup'); + score += 20; + notify(`Collected "${name}" module: +20`, 150); + return true; + } +} + +class Tracer extends Body { + constructor(ship) { + super(...ship.pos, 0.1); + + this.ship = ship; + this.path = []; + } + + run(distance) { + this.path = []; + [this.x, this.y] = this.ship.com; + [this.xvel, this.yvel] = [this.ship.xvel, this.ship.yvel]; + let dis = 0; + let last = this.pos; + let factor = 5; + + for (let i = 0; dis < distance; i++) { + if (this.tickPath(factor)) break; + this.path.push(this.pos); + + if (i % 10 === 0) { + let [lx, ly] = last; + dis += Math.sqrt((this.x - lx) ** 2 + (this.y - ly) ** 2); + last = this.pos; + } + + if (i > distance * 5) break; + } + + [this.x, this.y] = this.ship.com; + } + + tick() { + this.run(100); + } + + tickPath(speed) { + this.tickMotion(speed); + this.tickGravity(celestials, speed); + return !!this.getCelestialCollision(celestials); + } +} + +class Ship extends Body { + constructor(x, y) { + super(x, y, 0); + + this.localCom = [0, 0]; + this.modules = new Set(); + this.maxRadius = 0; + this.landed = false; + this.lastContactModule = null; + this.poc = this.com; + this.maxFuel = 0; + this.fuel = 0; + this.rotationPower = 0; + this.cargoCapacity = 0; + this.thrust = 0; + this.crashed = false; + this.timeWithoutFuel = 0; + } + + get com() { + let [lx, ly] = this.localCom; + return [this.x + lx, this.y + ly]; + } + + get parentCelestial() { + let closest = null; + let closestDistance = 0; + + celestials.forEach(c => { + let dis = this.distanceTo(c); + if (closest === null || dis < closestDistance) { + closest = c; + closestDistance = dis; + } + }); + + if (closestDistance > MAX_PARENT_CELESTIAL_DISTANCE) + return null; + + return closest; + } + + tick() { + if (this.crashed) return; + if (!state.editing) this.tickMotion(); + if (!this.landed) this.tickGravity(celestials); + if (!state.editing) this.resolveCollisions(); + + this.modules.forEach(m => { + if (m.type == 'thruster' && m.power !== 0) { + for (let i = 0; i < 2; i++) createThrustExhaust(m); + } + }); + + this.modules.forEach(m => m.reset()); + + if (shipLanded != this.landed) { + if (this.landed) { + landShip(this.parentCelestial); + } else { + launchShip(); + } + } + + if (this.fuel === 0 && !state.gameOver) { + if (this.timeWithoutFuel++ > 300) + outOfFuel(); + } else { + this.timeWithoutFuel = 0; + } + } + + clearModules() { + this.modules.clear(); + } + + addFuel(amount) { + this.fuel = Math.min(this.fuel + amount, this.maxFuel); + } + + addModule(x, y, properties, options) { + let module = new Module(x, y, this, {...properties, ...options}); + this.modules.add(module); + this.refreshShape(); + } + + deleteModule(module) { + this.modules.delete(module); + this.refreshShape(); + } + + refreshShape() { + let points = []; + this.modules.forEach(m => points.push([...m.localCom, m.mass])); + this.mass = points.reduce((a, [,,b]) => a + b, 0); + this.localCom = points.reduce(([ax, ay], [bx, by, bm]) => + [ax + bx * bm, ay + by * bm], [0, 0]) + .map(x => x / this.mass); + let [lx, ly] = this.localCom; + this.maxRadius = points.reduce((a, [bx, by]) => + Math.max(Math.sqrt((bx - lx) ** 2 + (by - ly) ** 2), a), 0) + 1; + + this.maxFuel = 0; + this.rotationPower = 0; + this.cargoCapacity = 0; + this.thrust = 0; + + this.modules.forEach(m => { + if (m.type === 'fuel') { + this.maxFuel += m.data.fuelCapacity; + } else if (m.type === 'capsule') { + this.rotationPower += m.data.rotation; + this.cargoCapacity += m.data.capacity; + } else if (m.type === 'thruster') { + this.thrust += m.data.thrust; + } else if (m.type === 'gyroscope') { + this.rotationPower += m.data.rotation; + } else if (m.type === 'cargo') { + this.cargoCapacity += m.data.capacity; + } + }); + } + + colliding(point, radius) { + let [px, py] = point; + let result = false; + + this.modules.forEach(m => { + let [mx, my] = this.getWorldPoint(...m.localCom); + let dis = Math.sqrt((py - my) ** 2 + (px - mx) ** 2); + if (dis < radius) result = true; + }); + + return result; + } + + resolveCollisions() { + this.landed = false; + + celestials.forEach(c => { + let dis = this.distanceTo(c); + + if (dis < c.radius + this.maxRadius) { + this.modules.forEach(m => { + this.checkModuleCollision(m, c); + }); + } + }); + } + + checkModuleCollision(module, body) { + let p = this.getWorldPoint(...module.localCom); + let dis = body.distanceTo({ com: p }); + if (dis < body.radius + 0.5 + EPSILON) { + this.approach(body, dis - (body.radius + 0.5)); + this.moduleCollided(module); + this.halt(); + this.resolveCelestialCollision(p, body, module); + this.poc = p; + this.lastContactModule = module; + } + } + + moduleCollided(module) { + if (this.landed) return; + let speed = Math.sqrt(this.xvel ** 2 + this.yvel ** 2); + if (module.type !== 'thruster' || speed > CRASH_SPEED) { + crash(); + this.crashed = true; + } + } + + resolveCelestialCollision(pos, cel, module) { + let celToCom = this.angleTo(...this.com, ...cel.com); + let celToPoc = this.angleTo(...pos, ...cel.com); + let pocToCom = this.angleTo(...this.com, ...pos); + let shipAngle = this.normalizeAngle(this.r + Math.PI / 2); + + let turnAngle = this.angleDifference(celToPoc, pocToCom); + let checkAngle = this.angleDifference(celToPoc, shipAngle); + let correctionAngle = this.angleDifference(shipAngle, celToCom); + + let [force] = this.rotateVector(0, 1, turnAngle); + + if (Math.abs(checkAngle) < TIP_ANGLE) { + [force] = this.rotateVector(0, 1, correctionAngle); + force *= 0.2; + } + + let canLand = Math.abs(checkAngle) < 0.03 + && Math.abs(this.rvel) < 0.001; + + if (canLand) { + this.landed = true; + this.rvel = 0; + this.r = celToCom - Math.PI / 2; + } + + this.rvel += force * TIP_SPEED; + } + + applyThrust({ forward = 0, left = 0, right = 0, back = 0, + turnLeft = 0, turnRight = 0}) { + + let thrustForce = -forward * THRUST_POWER * this.thrust; + let turnForce = (turnRight - turnLeft) * TURN_POWER; + if (this.fuel <= 0) { + this.fuel = 0; + thrustForce = 0; + } else { + this.fuel -= Math.abs(thrustForce) * FUEL_BURN_RATE; + } + turnForce *= this.rotationPower; + + this.applyDirectionalForce(0, thrustForce, turnForce); + + if (Math.abs(this.rvel) > 0.1) { + this.rvel *= 0.7; + } + + this.modules.forEach(m => { + if (m.type !== 'thruster' || thrustForce == 0) return; + m.power += forward; + }); + } +} + +class Celestial extends Body { + constructor(x, y, radius, { + density = 1, + type = 'rock' + }) { + let mass = (radius ** 2) * density; + super(x, y, mass); + this.radius = radius; + + this.type = type; + let imageArr = Object.values(images.celestials[this.type]); + this.image = imageArr[Math.random() * imageArr.length | 0]; + } + + get com() { + return [this.x + this.radius, this.y + this.radius]; + } + + get escapeVelocity() { + + } + + tick() { + + } + + get diameter() { + return this.radius * 2; + } +} + +class Entity extends Body { + constructor(x, y, type = 'fuel', id = 'small', { + xvel = 0, + yvel = 0, + gravity = false + } = {}) { + super(x, y, 0.1); + this.xvel = xvel; + this.yvel = yvel; + this.width = 1; + this.height = 1; + this.radius = (this.width + this.height) / 2; + this.type = type; + this.id = id; + if (this.type === 'fuelcan') { + this.image = images.modules.fuelcan; + this.name = 'Fuel Can'; + } else { + this.image = images.modules[type][id]; + this.name = modules[type][id].name; + if (this.type === 'thruster') + this.image = this.image.off; + } + this.gravity = gravity; + this.touched = false; + } + + get com() { + return [this.x + this.width / 2, this.y + this.height / 2]; + } + + get localCom() { + return [this.width / 2, this.height / 2]; + } + + remove() { + entities.delete(this); + } + + tick() { + if (Math.abs(playerShip.x - this.x) > 500 || + Math.abs(playerShip.y - this.y) > 500) return; + this.r += ENTITY_ROTATION_RATE; + this.tickMotion(); + if (this.gravity) this.tickGravity(celestials); + let col = this.getCelestialCollision(celestials); + + if (col !== false) { + this.remove(); + } + + if (playerShip.colliding(this.com, this.radius)) { + if (this.touched) return; + this.touched = true; + let success = collectItem(this.type, this.id, this.name); + if (!success) return; + createPickupBurst(playerShip, this.com); + this.remove(); + } else { + this.touched = false; + } + } +} + +let spawnedSectors = new Map(); + +const visibleRadius = (400 / MIN_ZOOM) + SECTOR_SIZE; + +function tick$1() { + let [px, py] = playerShip.com; + + for (let x = px - visibleRadius; x < px + visibleRadius; x += SECTOR_SIZE) + for (let y = py - visibleRadius; y < py + visibleRadius; y += SECTOR_SIZE) { + let [sx, sy] = [x / SECTOR_SIZE | 0, y / SECTOR_SIZE | 0]; + let id = `${sx}.${sy}`; + if (!spawnedSectors.has(id)) spawnSector(sx, sy); + } + + spawnedSectors.forEach((objects, key) => { + let [sx, sy] = key.split('.'); + let [wx, wy] = [sx * SECTOR_SIZE, sy * SECTOR_SIZE]; + let dis = (wx - px) ** 2 + (wy - py) ** 2; + if (dis > (SECTOR_SIZE * 4) ** 2) { + spawnedSectors.delete(key); + objects.forEach(remove); + } + }); +} + +function nearest(x, y, set) { + let closest = null; + let closestDis = 0; + + set.forEach(e => { + let dis = e.distanceTo({ com: [x, y] }); + if (closest === null || dis < closestDis) { + closest = e; + closestDis = dis; + } + }); + + return [closest, closestDis]; +} + +function spawnSector(x, y) { + let area = SECTOR_SIZE ** 2; + let spawned = new Set(); + + for (let i = 0; i < area / 1000; i++) { + let [px, py] = [(x + Math.random()) * SECTOR_SIZE, (y + Math.random()) * SECTOR_SIZE]; + if (Math.random() < PLANET_SPAWN_RATE / 1000) { + spawned.add(randomPlanet(px, py)); + } else if (Math.random() < ENTITY_SPAWN_RATE / 1000){ + spawned.add(randomEntity(px, py)); + } + } + + spawnedSectors.set(`${x}.${y}`, spawned); +} + +function randomPlanet(x, y, { + radius = Math.random() * 60 + 30, + type = 'green', + density = 3 + } = {}) { + + let [cel, dis] = nearest(x, y, celestials); + let mcs = MIN_CELESTIAL_SPACING; + + if (cel !== null && dis < Math.max(radius, cel.radius) * mcs) return; + + let planet = celestial(x, y, radius, { + density: density, + type: type + }); + + for (let i = 0.1; i < 4; i += 1) { + if (Math.random() > ENTITY_SPAWN_RATE) { + let e = randomEntity(); + e.orbit(planet, i * radius, Math.random() * Math.PI * 2); + } + } + + for (let i = 0; i < 5; i++) { + if (Math.random() > ENTITY_SPAWN_RATE || true) { + let e = randomEntity(); + e.orbit(planet, 1.5, Math.random() * Math.PI * 2); + e.gravity = false; + e.halt(); + } + } + + return planet; +} + +function randomEntity(x, y) { + let entity; + + if (Math.random() > 0.3) { + entity = new Entity(x, y, 'fuelcan'); + } else { + let type, id; + while (true) { + let arr = Object.entries(modules); + [type, arr] = arr[Math.random() * arr.length | 0]; + arr = Object.keys(arr); + id = arr[Math.random() * arr.length | 0]; + let value = modules[type][id].value; + if (Math.random() < (1 / value)) break; + } + entity = new Entity(x, y, type, id); + } + + entities.add(entity); + return entity; +} + +function player() { + let ship = new Ship(0, -45); + ship.addModule(0, 0, modules.capsule.small); + ship.addModule(0, 1, modules.fuel.small); + ship.addModule(0, 2, modules.thruster.light); + //ship.addModule(1, 2, modules.thruster.light); + //ship.addModule(-1, 2, modules.thruster.light); + ships.add(ship); + setPlayerShip(ship); + ship.addFuel(ship.maxFuel); + + let tracer = new Tracer(ship); + tracers.add(tracer); + + return ship; +} + +function startPlanet() { + let planet = randomPlanet(0, 0, { + radius: 40, + density: 3, + type: 'green' + }); + let fuel = new Entity(0, 0, 'fuelcan'); + entities.add(fuel); + fuel.orbit(planet, 10, -0.5); + return planet; +} + +function celestial(x, y, radius, params) { + let celestial = new Celestial(x - radius, y - radius, radius, params); + celestials.add(celestial); + return celestial; +} + +const entities = new Set(); +const celestials = new Set(); +const ships = new Set(); +const particles = new Set(); +const tracers = new Set(); + +let playerShip = null; + +function setPlayerShip(ship) { + playerShip = ship; +} + +function init$3() { + clear(); + player(); + let p = startPlanet(); + tick$1(); +} + +function clear() { + entities.clear(); + celestials.clear(); + ships.clear(); + particles.clear(); + tracers.clear(); +} + +function remove(object) { + entities.delete(object); + celestials.delete(object); +} + +function tick$2() { + particles.forEach(p => p.tick()); + celestials.forEach(c => c.tick()); + entities.forEach(e => e.tick()); + ships.forEach(s => s.tick()); + if (trace) tracers.forEach(t => t.tick()); + tick$1(); +} + +let tiles = new Map(); +let width = 0; +let height = 0; +let position = [0, 0]; +let message = ''; +let info = ''; + +function init$4() { + let ship = playerShip; + let modules$$1 = ship.modules; + + tiles.clear(); + + modules$$1.forEach(m => { + let pos = [m.x, m.y]; + tiles.set(posId(...pos), new Tile$1(...pos, m)); + }); + + message = ''; + adjustSize(); + + let neededZoom = canvas.width / (Math.max(width, height) + 10); + changePerspective('planet', 0, -5); + setZoom(neededZoom); +} + +function adjustSize() { + let margin = EDIT_MARGIN; + let [sx, ex, sy, ey] = getBoundaries(); + [width, height] = [ex - sx + margin * 2 + 1, ey - sy + margin * 2 + 1]; + position = [sx - margin, sy - margin]; + getAttributes(); +} + +function end() { + let result = validate(); + + result = { + valid: result === false, + reason: result + }; + + if (result.valid) { + let ship = playerShip; + let [ox, oy] = ship.com; + ship.clearModules(); + tiles.forEach(t => { + if (t.type === null) return; + ship.addModule(t.x, t.y, modules[t.type][t.id]); + }); + let [nx, ny] = ship.com; + let [dx, dy] = [nx - ox, ny - oy]; + ship.x -= dx; + ship.y -= dy; + } + + return result; +} + +function getAttributes() { + let cargo = 0; + let fuel = 0; + let rotation = 0; + let mass = 0; + let thrust = 0; + + tiles.forEach(t => { + if (t.type === null) return; + if (t.type === 'fuel') { + fuel += t.module.fuelCapacity; + } else if (t.type === 'capsule') { + rotation += t.module.rotation; + cargo += t.module.capacity; + } else if (t.type === 'thruster') { + thrust += t.module.thrust; + } else if (t.type === 'gyroscope') { + rotation += t.module.rotation; + } else if (t.type === 'cargo') { + cargo += t.module.capacity; + } + mass += t.module.mass; + }); + + info = 'Mass: ' + mass + '\n' + + 'Fuel capacity: ' + fuel + '\n' + + 'Thrust/mass ratio: ' + (thrust / Math.max(mass, 1)).toFixed(1) + '\n' + + 'Rotation speed: ' + (rotation / Math.max(mass, 1) * 100).toFixed(1) + + '\n' + + 'Cargo capacity: ' + cargo; +} + +function validate() { + let capsulesFound = 0; + let thrustersFound = 0; + let fuelFound = 0; + let unvisited = new Set(); + + tiles.forEach(t => { + if (t.type !== null) unvisited.add(t); + }); + + let reason = ''; + + if (unvisited.size == 0) { + reason = 'no capsule'; + } + + let visit = (tile) => { + unvisited.delete(tile); + if (tile.type == 'capsule') capsulesFound++; + if (tile.type == 'thruster') thrustersFound++; + if (tile.type == 'fuel') fuelFound++; + tile.neighbours.forEach(n => { + if (unvisited.has(n) && n.neighbours.indexOf(tile) > -1) { + visit(n); + } + }); + }; + + if (unvisited.size > 0) + visit(unvisited.values().next().value); + + if (unvisited.size > 0) { + reason = 'not connected'; + } else if (capsulesFound === 0) { + reason = 'no capsule'; + } else if (thrustersFound === 0) { + reason = 'no thruster'; + } else if (fuelFound === 0) { + reason = 'no fuel tank'; + } else if (usedSpace > capacity) { + reason = 'inventory too full'; + } else { + reason = false; + } + + if (reason === false) { + message = ''; + } else { + message = reason; + } + + return reason; +} + +function positionAdjust(x, y) { + let [px, py] = position; + return [x + px, y + py]; +} + +function clickTile(x, y) { + if (currentItem === null) return; + let current = getTile(x, y).source; + if (current.type !== null) { + return; + } else { + } + + let pos = positionAdjust(x, y); + let id = posId(...pos); + tiles.set(id, new Tile$1(...pos, currentItem)); + removeItem(...currentItem.ident); + adjustSize(); + validate(); +} + +function rightClickTile(x, y) { + let current = getTile(x, y).source; + if (current.type === null) return; + let { x: tx, y: ty } = current; + let id = posId(tx, ty); + tiles.set(id, new Tile$1(tx, ty, null)); + addItem(current.type, current.id); + adjustSize(); + validate(); +} + +function getTile(x, y) { + let [px, py] = position; + return getRawTile(px + x, py + y); +} + +function getRawTile(x, y) { + let id = posId(x, y); + if (!tiles.has(id)) + tiles.set(id, new Tile$1(x, y, null)); + return tiles.get(id); + // TODO: Get linked tiles. +} + +function posId(x, y) { + return `${x}.${y}`; +} + +function getBoundaries() { + let sx = sy = ex = ey = null; + tiles.forEach(t => { + if (t.type === null) return; + if (sx === null || t.x < sx) sx = t.x; + if (ex === null || t.x > ex) ex = t.x; + if (sy === null || t.y < sy) sy = t.y; + if (ey === null || t.y > ey) ey = t.y; + }); + return [sx, ex, sy, ey]; +} + +class Tile$1 { + constructor(x, y, module) { + if (module === null) { + this.module = null; + this.image = null; + this.type = null; + this.id = null; + } else { + ({type: this.type, id: this.id} = module); + this.module = modules[this.type][this.id]; + this.image = images.modules[this.type][this.id]; + if (module.type === 'thruster') this.image = this.image.off; + } + this.x = x; + this.y = y; + } + + get valid() { + return true; + } + + get neighbours() { + return [[0, -1], [1, 0], [0, 1], [-1, 0]].filter((_, i) => { + return this.module.connectivity[i]; + }).map(([dx, dy]) => getRawTile(this.x + dx, this.y + dy)); + } + + get source() { + return this; + } + + get drawPos() { + let [px, py] = pos; + return [this.x + px, this.y + py]; + } +} + +const mouse = { pressed: {}, held: {}, x: 0, y: 0, scroll: 0 }; +const keyCode = { pressed: {}, held: {} }; +const key = { pressed: {}, held: {} }; + +function tick$3() { + mouse.pressed = {}; + keyCode.pressed = {}; + key.pressed = {}; + mouse.scroll = 0; +} + +function init$5() { + window.addEventListener('keydown', event => { + keyCode.pressed[event.code] = !keyCode.held[event.code]; + keyCode.held[event.code] = true; + key.pressed[event.key] = !keyCode.held[event.key]; + key.held[event.key] = true; + }); + + window.addEventListener('keyup', event => { + keyCode.held[event.code] = false; + key.held[event.key] = false; + }); + // Ṕ͕͖ẖ̨’̖̺͓̪̹n̼͇͔̯̝̖g̙̩̭͕ͅͅl̻̰͘u͎̥͍̗ͅi̼̞̪̩͚̜͖ ̫̝̻͚͟m͎̳̙̭̩̩̕g̟̤̬̮l̫̕w̶͚͈͚̟͔’͖n͏̝͖̞̺ͅa͏̹͓̬̺f̗̬̬̬̖̫͜h͙̘̝̱̬̗͜ ̼͎͖C̱͔̱͖ṭ̬̱͖h̵̰̼̘̩ùlh̙́u̪̫ ̪̺̹̙̯R̞͓̹̞’͍͎͉͎̦͙ͅl͇̠̮y̙̪̰̪͙̖e̢̩͉͙̼h̗͔̹̳ ̶w̨̼͍̝̭̣̣ͅg̶̳̦̳a̴͉̹͙̭̟ͅh͈͎̞̜͉́’̼̜̠͞n̲a̯g̮͚͓̝l̠ ̹̹̱͙̝f̧̝͖̱h̪̟̻͖̖t͎͘aͅg̤̘͜n̶͈̻̻̝̳ + window.addEventListener('mousedown', event => { + mouse.pressed[event.button] = !mouse.held[event.button]; + mouse.held[event.button] = true; + tickAfterMouse = false; + }); + + window.addEventListener('mouseup', event => { + mouse.held[event.button] = false; + }); + + window.addEventListener('mousemove', event => { + let rect = canvas.getBoundingClientRect(); + mouse.x = event.clientX - rect.left; + mouse.y = event.clientY - rect.top; + }); + + window.addEventListener('wheel', event => { + mouse.scroll = event.deltaY; + }); + + window.addEventListener('contextmenu', event => { + event.preventDefault(); + }); +} + +class Rect { + constructor(x = 0, y = 0, w = 0, h = 0) { + this.x = x; + this.y = y; + this.w = w; + this.h = h; + + this.onclick = null; + this.mouseHeld = false; + + this.rightMouseHeld = false; + this.onRightClick = null; + } + + click() {} + + rightClick() {} + + tickMouse() { + if (this.mouseHeld == true && !mouse.held[0] && this.mouseOver) + this.click(); + if (!this.mouseHeld && mouse.pressed[0] && this.mouseOver) + this.mouseHeld = true; + if (!mouse.held[0]) + this.mouseHeld = false; + + if (this.rightMouseHeld == true && !mouse.held[2] + &&this.mouseOver) + this.rightClick(); + if (!this.rightMouseHeld && mouse.pressed[2] && this.mouseOver) + this.rightMouseHeld = true; + if (!mouse.held[2]) + this.rightMouseHeld = false; + } + + get shape() { + return [this.x, this.y, this.w, this.h]; + } + + get end() { + return [this.x + this.w, this.y + this.h]; + } + + get center() { + return [this.x + this.w / 2, this.y + this.h / 2]; + } + + get mouseOver() { + return this.containsPoint(mouse.x, mouse.y); + } + + get mouseClicked() { + return this.mouseOver() && mouse.pressed[0]; + } + + containsPoint(x, y) { + return x > this.x && x < this.x + this.w + && y > this.y && y < this.y + this.h; + } +} + +const defaultOptions = { + draw: true, // Whether the element itself will be rendered. + drawChildren: true // Whether children will be rendered. +}; + +class GuiElement extends Rect { + constructor(x, y, w, h, options = {}) { + super(x, y, w, h); + this.children = new Set(); + this.parent = null; + + this.type = 'element'; + + this.options = Object.assign({}, defaultOptions, options); + } + + tickElement() { + this.tickMouse(); + this.tick(); + this.children.forEach(c => c.tickElement()); + } + + tick() { + + } + + get drawn() { + if (!this.options.drawChildren) return false; + if (!this.parent) return true; + return this.parent.drawn; + } + + append(element) { + this.children.add(element); + element.parent = this; + element.x += this.x; + element.y += this.y; + } + + clear() { + this.children.clear(); + } + // Code should be self-describing, comments are for fucking about. + // - Albert Einstein + + posRelative({x = null, xc = 0, y = null, yc = 0, w = null, h = null}) { + if (x !== null) + this.x = (this.parent.w * x) - (this.w * xc) + this.parent.x; + if (y !== null) + this.y = (this.parent.h * y) - (this.h * yc) + this.parent.y; + if (w !== null) + this.w = this.parent.w * w; + if (h !== null) + this.h = this.parent.h * h; + } +} + +class GuiFrame extends GuiElement { + constructor(x, y, w, h, options) { + super(x, y, w, h, options); + this.type = 'frame'; + } +} + +class GuiImage extends GuiElement { + constructor(src, x, y, w, h) { + w = w || src.width; + h = h || src.height; + super(x, y, w, h); + this.type = 'image'; + this.image = src; + this.imgRatio = src.width / src.height; + } + + scaleImage({ w = null, h = null }) { + if (w !== null && h === null) { + this.w = w; + this.h = w / this.imgRatio; + } else if (h !== null && w === null) { + this.h = h; + this.w = h / this.imgRatio; + } else if ( h !== null && w !== null) { + this.w = w; + this.h = h; + } + } +} + +class GuiButton extends GuiElement { + constructor(text, onclick, x, y, w = 100, h = 30) { + super(x, y, w, h); + this.type = 'button'; + this.text = text; + this.onclick = onclick; + } + + click() { + if (this.drawn && !this.options.disabled) + this.onclick(); + } +} + +class GuiItemButton extends GuiButton { + constructor(tile, onclick, x, y, w = 50, h = 50, { + padding = 0, + selected = false, + quantity = 1, + } = {}) { + super(null, onclick, x, y, w, h); + this.module = tile.module; + this.image = tile.image; + this.type = 'itemButton'; + this.padding = padding; + this.selected = selected; + this.quantity = quantity; + } + + click() { + if (this.drawn) + this.onclick('left'); + } + + rightClick() { + if (this.drawn) + this.onclick('right'); + } +} + +class GuiInventory extends GuiElement { + constructor(x, y, w = 100, h = 30) { + super(x, y, w, h); + this.type = 'inventory'; + this.tileWidth = 4; + this.tileHeight = 5; + this.currentPage = 0; + setOnupdate(this.updateTiles.bind(this)); + this.guiInfo = null; + } + + updateTiles() { + this.children.clear(); + + let tileRatio = this.tileWidth / this.tileHeight; + let rectRatio = this.w / this.h; + let tileSize; + let [ox, oy] = [0, 0]; + + if (tileRatio < rectRatio) { + tileSize = this.h / this.tileHeight; + ox = (this.w - (tileSize * this.tileWidth)) / 2; + } else { + tileSize = this.w / this.tileWidth; + oy = (this.h - (tileSize * this.tileHeight)) / 2; + } + + let spacing = 0.15 * tileSize; + let pageSize = this.tileWidth * this.tileHeight; + let offset = pageSize * this.currentPage; + let tiles = getTiles().slice(offset); + let tile; + let cur = currentItem; + + for (let y = 0; y < this.tileHeight; y++) + for (let x = 0; x < this.tileWidth && tiles.length; x++) { + let i = y * this.tileWidth + (x % this.tileWidth) + offset; + tile = tiles.shift(); + + let ex = x * tileSize + spacing / 2 + ox; + let ey = y * tileSize + spacing / 2 + oy; + let [ew, eh] = [tileSize - spacing, tileSize - spacing]; + + let ident = tile.ident; + + let onclick = (button) => { + this.tileClicked(...ident, button); + }; + + let selected = cur !== null && tile.type === cur.type + && tile.id === cur.id; + + let el = new GuiItemButton(tile, onclick, ex, ey, ew, eh, { + padding: 0.1, + selected: selected, + quantity: tile.quantity + }); + + this.append(el); + } + + this.guiInfo.text = cur === null ? '' : cur.textInfo; + this.guiInfo.splitLines(); + } + + tick() { + if (state.inventory && !this.active) this.updateTiles(); + this.active = state.inventory; + this.parent.options.drawChildren = this.active; + if (!this.active) return; + + this.children; + } + + getTile(x, y) { + return this.getTile(x + px, y + py); + } + + tileClicked(type, id, button) { + if (button == 'left') selectItem(type, id); + + if (button == 'right') { + if (canToss()) { + removeItem(type, id); + } + } + + this.updateTiles(); + } +} + +class GuiEdit extends GuiElement { + constructor(x, y, w = 100, h = 30) { + super(x, y, w, h); + this.type = 'edit'; + this.tileWidth = 0; + this.tileHeight = 0; + this.active = false; + this.guiInventory = null; + } + + updateTiles() { + this.children.clear(); + + [this.tileWidth, this.tileHeight] = [width, height]; + + let tileRatio = this.tileWidth / this.tileHeight; + let rectRatio = this.w / this.h; + let tileSize; + let [ox, oy] = [0, 0]; + + if (tileRatio < rectRatio) { + tileSize = this.h / this.tileHeight; + ox = (this.w - (tileSize * this.tileWidth)) / 2; + } else { + tileSize = this.w / this.tileWidth; + oy = (this.h - (tileSize * this.tileHeight)) / 2; + } + + let spacing = 0.1 * tileSize; + + for (let x = 0; x < this.tileWidth; x++) + for (let y = 0; y < this.tileHeight; y++) { + let tile = getTile(x, y); + let ex = x * tileSize + spacing / 2 + ox; + let ey = y * tileSize + spacing / 2 + oy; + let [ew, eh] = [tileSize - spacing, tileSize - spacing]; + + let onclick = (button) => { + this.tileClicked(x, y, button); + }; + + let el = new GuiItemButton(tile, onclick, ex, ey, ew, eh); + this.append(el); + } + } + + tick() { + if (state.editing && !this.active) this.updateTiles(); + this.active = state.editing; + this.parent.options.drawChildren = this.active; + if (!this.active) return; + + this.textElements.info.text = info; + + [this.tileWidth, this.tileHeight] = [width, height]; + } + + getTile(x, y) { + let [px, py] = position; + return getTile(x + px, y + py); + } + + tileClicked(x, y, button) { + if (button == 'left') { + clickTile(x, y); + } else if (button == 'right') { + rightClickTile(x, y); + } + + this.updateTiles(); + this.guiInventory.updateTiles(); + } +} + +class GuiText extends GuiElement { + constructor(text = '', x, y, w, h, { + size = 10, + align = 'left', + valign = 'top', + color = '#fff' + } = {}) { + w = w; + h = h; + super(x, y, w, h); + this.type = 'text'; + this.color = color; + this.text = text; + this.spacing = size * 1.2; + this.font = size + 'px Consolas'; + this.align = align; + this.valign = valign; + } + + splitLines() { + // Not very robust, but good enough for now. Mebe fix later? + context.font = this.font; + let maxLineLength = (this.w / context.measureText('A').width) | 0; + maxLineLength = Math.max(maxLineLength, 1); + + let lines = []; + let currentLine = ''; + + this.text.split('\n').forEach(l => { + currentLine = ''; + l.split(' ').forEach(word => { + if (word.length + currentLine.length > maxLineLength) { + lines.push(currentLine.slice(0, -1)); + currentLine = ''; + } + currentLine += word + ' '; + }); + lines.push(currentLine.slice(0, -1)); + }); + + this.text = lines.join('\n'); + } +} + +function root() { + return new GuiFrame(0, 0, canvas.width, canvas.height, { + draw: false + }); +} + +function title() { + let shadow = root(); + let logo = new GuiImage(images.title.logo); + shadow.append(logo); + logo.scaleImage({ w: shadow.w * 0.7 }); + logo.posRelative({ x: 0.5, xc: 0.5, y: 0.2 }); + let start = new GuiButton('Start game', startGame, 0, 0, 200); + shadow.append(start); + start.posRelative({ x: 0.5, xc: 0.5, y: 1 }); + start.y -= 160; + + let secondFunction = () => {}; + let second = new GuiButton('Don\'t start game', secondFunction, 0, 0, 200); + shadow.append(second); + second.posRelative({ x: 0.5, xc: 0.5, y: 1 }); + second.y -= 110; + + let thirdFunction = howToPlay; + let third = new GuiButton('How to play', thirdFunction, 0, 0, 200); + shadow.append(third); + third.posRelative({ x: 0.5, xc: 0.5, y: 1 }); + third.y -= 60; + + return shadow; +} + +const instructionText = `\ +Flight controls + +WAD: Movement +Shift + WAD: Fine movement +E: Open/close inventory +R: Toggle item markers +T: Toggle path prediction +P: Pause/unpause +M: Toggle music + + +Ship editing and inventory controls + +Left click: Select module in inventory +Right click: Toss away module in inventory +Left click: Place module on ship +Right click: Remove module from ship +Escape: Exit ship editing screen + + +Fly around collecting modules and fuel, and land to build your ship using \ +those collected modules. Get the highest score possible without crashing or \ +running out of fuel. +`; + +function instructions() { + let shadow = root(); + + let frame = new GuiFrame(); + shadow.append(frame); + frame.posRelative({x: 0.1, y: 0.1, w: 0.8, h: 0.8}); + + let back = new GuiButton('Return to menu', toMenu, 0, 0, 200); + frame.append(back); + back.posRelative({ x: 0.5, xc: 0.5, y: 1 }); + back.y -= 60; + + let text = new GuiText(instructionText, 0, 0, 0, 0, { + size: 12, + align: 'left', + valign: 'top' + }); + frame.append(text); + text.posRelative({x: 0.05, y: 0.05, w: 0.9, h: 0.9}); + text.splitLines(); + + return shadow; +} + +function game() { + let shadow = root(); + + let editButton = new GuiButton('Edit rocket', toggleEdit, 0, 0, 200); + shadow.append(editButton); + editButton.posRelative({ x: 0.5, xc: 0.5, y: 1 }); + editButton.y -= 45; + editButton.tick = () => { + let usable = state.landed && !state.gameOver; + editButton.options.draw = usable; + editButton.options.disabled = usable && message !== ''; + if (state.editing) { + editButton.text = 'Finish'; + if (message !== '') editButton.text = '(' + message + ')'; + } else { + editButton.text = 'Edit rocket'; + } + }; + + let fuel = new GuiText('', 0, 0, 0, 0, { + size: 14, + align: 'right', + valign: 'bottom' + }); + shadow.append(fuel); + fuel.posRelative({x: 1, y: 1}); + fuel.y -= 10; + fuel.x -= 10; + fuel.tick = () => { + let ship = playerShip; + fuel.text = 'Fuel: ' + ship.fuel.toFixed(1) + '/' + + ship.maxFuel.toFixed(1); + }; + + let score$$1 = new GuiText('', 0, 0, 0, 0, { + size: 14, + align: 'left', + valign: 'bottom' + }); + shadow.append(score$$1); + score$$1.posRelative({x: 0, y: 1}); + score$$1.y -= 10; + score$$1.x += 10; + score$$1.tick = () => { + score$$1.text = 'Score: ' + score; + }; + + + let editShadow = root(); + shadow.append(editShadow); + editShadow.posRelative({x: 0.45, y: 0, w: 0.55, h: 0.6}); + editShadow.x -= 10; + editShadow.y += 10; + + let edit = new GuiEdit(0, 0, 0, 0); + editShadow.append(edit); + edit.posRelative({w: 1, h: 1}); + + let editInfoText = new GuiText('', 0, 0, 0, 0, { + size: 12, + align: 'right' + }); + editShadow.append(editInfoText); + editInfoText.posRelative({x: 1, y: 1}); + editInfoText.y += 5; + editInfoText.x -= 20; + + let editWarnText = new GuiText('', 0, 0, 0, 0, { + size: 12, + align: 'center' + }); + editShadow.append(editWarnText); + editWarnText.posRelative({x: 0.5, y: 1}); + editWarnText.y += 20; + + edit.textElements = { + info: editInfoText, + warn: editWarnText + }; + + + let invShadow = root(); + shadow.append(invShadow); + invShadow.posRelative({x: 0, w: 0.4, h: 0.6}); + invShadow.x += 10; + invShadow.y += 10; + + let invElement = new GuiInventory(0, 0, 0, 0); + invShadow.append(invElement); + invElement.posRelative({w: 1, h: 0.8}); + + let capacityInfo = new GuiText('', 0, 0, 0, 0, { + size: 12, + align: 'left', + valign: 'bottom' + }); + invShadow.append(capacityInfo); + capacityInfo.posRelative({x: 0, y: 1}); + capacityInfo.y -= 5; + capacityInfo.x += 5; + capacityInfo.tick = () => { + capacityInfo.text = 'Space used: ' + usedSpace + ' / ' + + capacity; + }; + + let moduleInfo = new GuiText('', 0, 0, 0, 0, { + size: 12, + align: 'left', + valign: 'top' + }); + invShadow.append(moduleInfo); + moduleInfo.posRelative({x: 0, y: 1, w: 1}); + moduleInfo.splitLines(); + moduleInfo.y += 5; + invElement.guiInfo = moduleInfo; + + edit.guiInventory = invElement; + + + let notification = new GuiText('', 0, 0, 0, 0, { + size: 14, + align: 'center', + valign: 'top' + }); + shadow.append(notification); + notification.posRelative({x: 0.5}); + notification.y += 10; + setNotificationElement(notification); + + + let gameOver$$1 = root(); + shadow.append(gameOver$$1); + gameOver$$1.posRelative({x: 0.2, y: 0.2, w: 0.6, h: 0.6}); + + let gameOverMain = new GuiText('Game over', 0, 0, 0, 0, { + size: 48, + align: 'center', + valign: 'top' + }); + gameOver$$1.append(gameOverMain); + gameOverMain.posRelative({x: 0.5}); + gameOverMain.y += 10; + gameOver$$1.tick = () => { + gameOver$$1.options.drawChildren = state.gameOver; + }; + + let gameOverReason$$1 = new GuiText('', 0, 0, 0, 0, { + size: 14, + align: 'center', + valign: 'top' + }); + gameOver$$1.append(gameOverReason$$1); + gameOverReason$$1.posRelative({x: 0.5}); + gameOverReason$$1.y += 100; + gameOverReason$$1.tick = () => { + gameOverReason$$1.text = gameOverReason; + }; + + let gameOverScore = new GuiText('', 0, 0, 0, 0, { + size: 14, + align: 'center', + valign: 'top' + }); + gameOver$$1.append(gameOverScore); + gameOverScore.posRelative({x: 0.5}); + gameOverScore.y += 200; + gameOverScore.tick = () => { + gameOverScore.text = scoreText; + }; + + let gameOverExit = new GuiButton('Main menu', toMenu, 0, 0, 200); + gameOver$$1.append(gameOverExit); + gameOverExit.posRelative({ x: 0.5, xc: 0.5, y: 1 }); + gameOverExit.y -= 10; + + return shadow; +} + +const elements = new Set(); +let root$1; + +function init$6() { + elements.clear(); + root$1 = root(); + changeView('menu'); +} + +function tick$4() { + root$1.tickElement(); +} + +function changeView(view) { + root$1.clear(); + + if (view === 'menu') { + root$1.append(title()); + } + + if (view === 'game') { + root$1.append(game()); + } + + if (view === 'instructions') { + root$1.append(instructions()); + } +} + +function render() { + renderElement(root$1); +} + +function renderElement(element) { + if (element.options.draw) { + if (element.type === 'frame') renderFrame(element); + if (element.type === 'image') renderImage(element); + if (element.type === 'button') renderButton(element); + if (element.type === 'edit') renderEdit(element); + if (element.type === 'itemButton') renderItemButton(element); + if (element.type === 'inventory') renderInventory(element); + if (element.type === 'text') renderText(element); + } + + if (element.options.drawChildren) + element.children.forEach(renderElement); +} + +function renderFrame(element) { + context.fillStyle = '#a3977c'; + context.fillRect(...element.shape); + context.lineWidth = 3; + context.strokeStyle = '#6d634b'; + context.strokeRect(...element.shape); +} + +function renderImage(element) { + context.drawImage(element.image, ...element.shape); +} + +function renderText(element) { + context.font = element.font; + context.textAlign = element.align; + context.textBaseline = element.valign; + context.fillStyle = element.color; + element.text.split('\n').forEach((line, i) => + context.fillText(line, element.x, element.y + i * element.spacing) + ); +} + +function renderButton(element) { + if (element.mouseHeld && !element.options.disabled) { + context.fillStyle = '#706244'; + } else if (element.mouseOver && !element.options.disabled) { + context.fillStyle = '#ad9869'; + } else { + context.fillStyle = '#917f58'; + } + + if (element.options.disabled) { + context.globalAlpha = 0.5; + } + + let [sx, sy, w, h] = element.shape; + let [ex, ey] = [sx + w, sy + h]; + let rad = 5; + + context.beginPath(); + context.moveTo(sx + rad, sy); + context.lineTo(ex - rad, sy); + context.quadraticCurveTo(ex, sy, ex, sy + rad); + context.lineTo(ex, ey - rad); + context.quadraticCurveTo(ex, ey, ex - rad, ey); + context.lineTo(sx + rad, ey); + context.quadraticCurveTo(sx, ey, sx, ey - rad); + context.lineTo(sx, sy + rad); + context.quadraticCurveTo(sx, sy, sx + rad, sy); + context.closePath(); + + context.fill(); + context.strokeStyle = '#541'; + context.lineWidth = 2; + context.stroke(); + + context.textAlign = 'center'; + context.textBaseline = 'middle'; + context.fillStyle = '#fff'; + context.font = '12pt Consolas'; + context.fillText(element.text, ...element.center); + + context.globalAlpha = 1; +} + +function renderItemButton(element) { + context.globalAlpha = 0.5; + if (element.mouseHeld || element.rightMouseHeld) { + context.fillStyle = '#080808'; + } else { + context.fillStyle = element.mouseOver ? '#222' : '#0e0e0e'; + } + + context.fillRect(...element.shape); + if (element.selected) { + context.strokeStyle = '#fff'; + context.lineWidth = 2; + } else { + context.strokeStyle = '#333'; + context.lineWidth = 1; + } + context.strokeRect(...element.shape); + context.globalAlpha = 1; + + if (element.image) { + let p = element.padding; + let ox = element.x + (p / 2 * element.w); + let oy = element.y + (p / 2 * element.h); + let [dw, dh] = [element.w * (1 - p), element.h * (1 - p)]; + context.drawImage(element.image, ox, oy, dw, dh); + } + + if (element.quantity > 1) { + context.textAlign = 'right'; + context.textBaseline = 'bottom'; + context.fillStyle = '#fff'; + context.font = 'bold 10pt Consolas'; + let [ex, ey] = element.end; + context.fillText('x' + element.quantity, ex - 2, ey - 2); + } +} + +function renderEdit(element) { + +} + +function renderInventory(element) { + context.globalAlpha = 0.1; + context.fillStyle = '#541'; + context.fillRect(...element.parent.shape); + context.globalAlpha = 1; +} + +function render$1() { + particles.forEach(renderParticle); + celestials.forEach(renderCelestial); + if (trace) tracers.forEach(renderTracer); + ships.forEach(renderShip); + entities.forEach(renderEntity); + + /* + if (typeof window.q === 'undefined') window.q = []; + q.forEach(p => { + context.fillStyle = p[2]; + context.fillRect(p[0] - 0.05, p[1] - 0.05, 0.1, 0.1); + }); + */ +} + +function renderParticle(particle) { + context.fillStyle = particle.color; + context.fillRect(...particle.com, particle.size, particle.size); +} + +function renderEntity(entity) { + context.save(); + context.translate(...entity.com); + let alpha = Math.max(1 - ((perspective.zoom - 1) / 2), 0) ** 2; + if (alpha > 0 && markers) { + context.globalAlpha = alpha; + context.beginPath(); + context.arc(0, 0, 4, 0, 2 * Math.PI); + context.lineWidth = 1; + context.strokeStyle = '#31911b'; + if (entity.type === 'fuelcan') + context.strokeStyle = '#af4021'; + context.stroke(); + context.globalAlpha = 1; + } + context.rotate(entity.r); + context.drawImage(entity.image, -0.5, -0.5, 1, 1); + context.restore(); +} + +function renderShip(ship) { + if (ship.crashed) return; + context.save(); + context.translate(...ship.com); + context.rotate(ship.r); + let [cx, cy] = ship.localCom; + context.translate(-cx, -cy); + ship.modules.forEach(m => { + let [mx, my] = [m.x, m.y]; + if (state.editing) { + + } + context.drawImage(m.currentImage, m.x, m.y, 1, 1); + }); + context.restore(); +} + +const celestialImages = { + green: Object.values(images.celestials.green) +}; + +function renderCelestial(cel) { + context.drawImage(cel.image, cel.x, cel.y, + cel.diameter, cel.diameter); +} + +function renderTracer(tracer) { + context.lineWidth = 0.1; + context.strokeStyle = '#fff'; + context.beginPath(); + context.moveTo(...tracer.pos); + let path = tracer.path; + + for (let i = 0; i < path.length; i++) { + context.lineTo(...path[i]); + if (i % 5 === 0 || i == path.length - 1) { + context.stroke(); + context.globalAlpha = (1 - (i / path.length)) * 0.5; + } + } + + context.globalAlpha = 1; +} + +let patterns = null; + +function init$7() { + patterns = { + back: context.createPattern(images.background.back, 'repeat'), + middle: context.createPattern(images.background.middle, 'repeat'), + front: context.createPattern(images.background.front, 'repeat') + }; +} + +function render$2(angle) { + if (patterns === null) init$7(); + + renderLayer(patterns.back, 0.3, 1, angle); + renderLayer(patterns.middle, 0.5, 0.3, angle); + //renderLayer(patterns.front, 0.7, 0.3, angle); +} + +function renderLayer(pattern, speed = 1, scale = 1, angle = 0) { + context.save(); + let outset = (Math.abs(Math.cos(angle)) + Math.abs(Math.sin(angle))); + outset = ((outset - 1) * canvas.width) / scale; + let [px, py] = [perspective.x * speed, perspective.y * speed]; + context.translate(-px, -py); + context.scale(scale, scale); + context.fillStyle = pattern; + context.fillRect(px / scale - outset / 2, py / scale - outset / 2, + canvas.width / scale + outset, canvas.height / scale + outset); + context.restore(); +} + +const TAU$1 = TAU; + +let canvas, context, tempCanvas, tempContext; +let perspective; +let trace = false; +let markers = false; + +function init$8() { + canvas = document.querySelector('#main'); + context = canvas.getContext('2d'); + tempCanvas = document.querySelector('#temp'); + tempContext = tempCanvas.getContext('2d'); + + canvas.width = 600; + canvas.height = 600; + + perspective = new Perspective(); + + context.fillStyle = '#000'; + context.fillRect(0, 0, canvas.width, canvas.height); + context.font = '36px Consolas'; + context.textAlign = 'center'; + context.textBaseline = 'middle'; + context.fillStyle = '#fff'; + context.fillText('Loading...', canvas.width / 2, canvas.height / 2); +} + +function render$3() { + context.clearRect(0, 0, canvas.width, canvas.height); + context.fillStyle = '#000'; + context.fillRect(0, 0, canvas.width, canvas.height); + + context.beginPath(); + context.rect(0, 0, canvas.width, canvas.height); + context.clip(); + + context.save(); + perspective.tick(); + perspective.transformRotate(); + render$2(perspective.rotation); + perspective.transformCanvas(); + render$1(); + context.restore(); + + render(); +} + +function changePerspective(rotationMode, shiftX = 0, shiftY = 0) { + perspective.changeRotationMode(rotationMode); + perspective.changeShift(shiftX, shiftY); + perspective.transition = 1; +} + +function cycleRotationMode$1() { + if (perspective.rotationMode === 'parent') { + perspective.changeRotationMode('local'); + } else if (perspective.rotationMode === 'local') { + perspective.changeRotationMode('universe'); + } else { + perspective.changeRotationMode('parent'); + } + perspective.transition = 1; + return perspective.rotationMode; +} + +function toggleTrace$1() { + trace = !trace; + return trace; +} + +function toggleMarkers$1() { + markers = !markers; + return markers; +} + +function changeZoom(delta) { + perspective.zoomDelta(delta); +} + +function setZoom(target) { + perspective.changeZoom(target); +} + +class Perspective { + constructor() { + this.x = 0; + this.y = 0; + this.shiftX = 0; + this.shiftY = 0; + this.zoom = 0; + this.bounds = [0, 0, canvas.width, canvas.height]; + this.reset(); + } + + reset() { + this.rotationMode = 'universe'; + this.targetZoom = DEFAULT_ZOOM; + this.oldTarget = 0; + this.oldShift = [0, 0]; + this.oldZoom = 0; + this.transition = 0; + this.zoomTransition = 0; + this.zoomTransitionSpeed = 0.9; + this.rotation = 0; + this.targetRotation = 0; + this.zoom = DEFAULT_ZOOM; + this.targetZoom = this.zoom; + this.focus = null; + this.rotationFocus = null; + } + + changeRotationMode(mode) { + this.oldShift = this.currentShift; + this.oldTarget = this.currentRotation; + this.rotationMode = mode; + } + + changeShift(x, y) { + this.oldShift = this.currentShift; + [this.shiftX, this.shiftY] = [x, y]; + } + + changeZoom(zoom, speed = 0.9) { + this.oldZoom = this.currentZoom; + this.targetZoom = zoom; + this.zoomTransition = 1; + this.zoomTransitionSpeed = speed; + } + + get currentShift() { + let [ox, oy] = this.oldShift; + return [this.interpolate(this.shiftX, ox), + this.interpolate(this.shiftY, oy)]; + } + + get currentRotation() { + return this.interpolateAngles(this.targetRotation, this.oldTarget); + } + + get currentZoom() { + let t = this.zoomTransition; + return (this.oldZoom * t + this.targetZoom * (1 - t)); + } + + interpolate(cur, old, x = this.transition) { + return (old * x + cur * (1 - x)); + } + + interpolateAngles(cur, old, x = this.transition) { + return old + this.angleDifference(old, cur) * (1 - x); + } + + angleDifference(a, b) { + return Math.atan2(Math.sin(b - a), Math.cos(b - a)); + } + + tick() { + if (this.focus !== null) + [this.x, this.y] = this.focus.com; + + if (this.focus === null || this.rotationMode === 'universe') { + this.targetRotation = 0; + } else if (this.rotationMode === 'parent') { + let parent = this.focus.parentCelestial; + if (parent === null) { + this.targetRotation = 0; + } else { + let a = this.focus.angleTo(...this.focus.com, ...parent.com); + this.targetRotation = a - Math.PI / 2; + } + } else { + this.targetRotation = this.focus.r; + } + + this.normalize(); + + let dif = Math.abs(this.targetRotation - this.rotation); + this.rotationMet = dif < (this.rotationMet ? 0.3 : 0.05); + + this.rotation = this.currentRotation; + this.zoom = this.currentZoom; + + this.transition *= 0.9; + this.zoomTransition *= this.zoomTransitionSpeed; + } + + focusPlayer() { + this.focus = playerShip; + this.rotationFocus = playerShip; + } + + zoomDelta(delta) { + let factor = 1 + (ZOOM_SPEED * Math.abs(delta)); + let target = this.targetZoom * (delta > 0 ? factor : 1 / factor); + this.changeZoom(target, 0.7); + this.normalize(); + } + + normalize() { + this.targetZoom = Math.max(MIN_ZOOM, + Math.min(MAX_ZOOM, this.targetZoom)); + this.targetRotation %= TAU$1; + } + + transformRotate() { + let [,,bw, bh] = this.bounds; + context.translate(bw / 2, bh / 2); + context.rotate(-this.rotation); + context.translate(-bw / 2, -bh / 2); + } + + rotateVector(x, y, r = this.rotation) { + return [(x * Math.cos(r) - y * Math.sin(r)), + (y * Math.cos(r) - x * Math.sin(r))]; + } + + transformCanvas() { + let [,,bw, bh] = this.bounds; + let [sx, sy] = this.rotateVector(...this.currentShift, this.rotation); + let tx = -(this.x + sx) * this.zoom; + let ty = -(this.y + sy) * this.zoom; + context.translate(tx + bw / 2, ty + bh / 2); + context.scale(this.zoom, this.zoom); + } + + normalizeAngle(a = this.r) { + return ((a % TAU$1) + TAU$1) % TAU$1; + } +} + +const mapping$1 = { + thrust: 'KeyW', + left: 'KeyA', + right: 'KeyD', + reduce: 'ShiftLeft', + exitEdit: 'Escape', + inventory: 'KeyE', + cycleRotation: 'KeyC', + toggleTrace: 'KeyT', + toggleMarkers: 'KeyR', + toggleMusic: 'KeyM', + togglePause: 'KeyP' +}; + +let held, pressed; + +function tick$5() { + held = keyCode.held; + pressed = keyCode.pressed; + + if (state.editing) { + tickEditing(); + } else if (state.playing && !state.gameOver && !state.paused) { + tickPlaying(); + } + + if (!state.editing) { + if (mouse.scroll !== 0) { + changeZoom(-mouse.scroll); + } + + if (pressed[mapping$1.togglePause] && !state.gameOver) { + togglePause(); + } + } + + if (state.gameOver) { + stop('engine'); + } + + if (pressed[mapping$1.toggleMusic]) { + toggle('music'); + } +} + +function tickPlaying() { + let power = held[mapping$1.reduce] ? 0.3 : 1; + + if (held[mapping$1.thrust] && playerShip.fuel !== 0) { + playerShip.applyThrust({ forward: power }); + let vol = Math.min(0.7, perspective.zoom / 10); + volume('engine', vol); + } else { + stop('engine'); + } + + if (pressed[mapping$1.thrust]) { + if (playerShip.fuel !== 0) { + start('engine'); + } else { + stop('engine'); + } + } + + if (held[mapping$1.left]) { + playerShip.applyThrust({ turnLeft: power }); + } + + if (held[mapping$1.right]) { + playerShip.applyThrust({ turnRight: power }); + } + + if (pressed[mapping$1.inventory]) { + state.inventory = !state.inventory; + } + + if (pressed[mapping$1.cycleRotation]) { + cycleRotationMode(); + } + + if (pressed[mapping$1.toggleTrace]) { + toggleTrace(); + } + + if (pressed[mapping$1.toggleMarkers]) { + toggleMarkers(); + } +} + +function tickEditing() { + if (pressed[mapping$1.exitEdit]) { + endEditing(); + } +} + +let state; + +async function init$9() { + state = { + view: 'menu', + playing: false, + editing: false, + paused: false, + inventory: false, + gameOver: false + }; + + init$8(); + await init(); + init$6(); + init$5(); + + playMusic(); + //events.startGame(); + + loop(tick$6); +} + +function changeView$1(view) { + state.view = view; + changeView(view); + + if (view === 'game') { + state.playing = true; + state.editing = false; + state.paused = false; + init$3(); + init$1(); + } else if (view === 'instructions') { + state.playing = false; + changeView('instructions'); + } else if (view === 'menu') { + changeView('menu'); + clear(); + } +} + +function loop(fn, fps = 60) { + let then = Date.now(); + let interval = 1000 / fps; + + (function loop(time) { + requestAnimationFrame(loop); + + // again, Date.now() if it's available + let now = Date.now(); + let delta = now - then; + + if (delta > interval) { + then = now - (delta % interval); + fn(); + } + })(0); +} +function tick$6() { + tick(); + + if (state.view == 'game' && !state.paused) { + tick$2(); + } + + tick$5(); + + tick$4(); + render$3(); + tick$3(); +} + +window.addEventListener('load', init$9); +//# sourceMappingURL=improcket.min.js.map