diff --git a/rlbot_gui/gui.py b/rlbot_gui/gui.py
index 93064ead..fd121950 100644
--- a/rlbot_gui/gui.py
+++ b/rlbot_gui/gui.py
@@ -247,12 +247,14 @@ def pick_location(is_folder, filter="Config files (*.cfg)"):
def read_info(bundle: RunnableConfigBundle):
details_header = 'Details'
if bundle.base_agent_config.has_section(details_header):
+ raw_tags = bundle.base_agent_config.get(details_header, 'tags')
return {
'developer': bundle.base_agent_config.get(details_header, 'developer'),
'description': bundle.base_agent_config.get(details_header, 'description'),
'fun_fact': bundle.base_agent_config.get(details_header, 'fun_fact'),
'github': bundle.base_agent_config.get(details_header, 'github'),
'language': bundle.base_agent_config.get(details_header, 'language'),
+ 'tags': [tag.strip() for tag in raw_tags.split(',')] if raw_tags else [],
}
return None
diff --git a/rlbot_gui/gui/css/style.css b/rlbot_gui/gui/css/style.css
index f3df3cf6..d70423e2 100644
--- a/rlbot_gui/gui/css/style.css
+++ b/rlbot_gui/gui/css/style.css
@@ -70,6 +70,10 @@ body, html {
position: relative;
}
+.team-card .bot-card {
+ display: flex;
+}
+
.team-label {
position: absolute;
bottom: 3px;
@@ -84,6 +88,7 @@ body, html {
.bot-card {
margin: 2px;
box-shadow: 1px 1px 7px #0000002e;
+ display: inline-flex;
}
.bot-card .card-body {
@@ -98,7 +103,12 @@ body, html {
}
.script-card {
- background: linear-gradient(90deg, rgb(255, 255, 255) 0%, rgba(255, 255, 255, 0.34) 50%)
+ background: linear-gradient(90deg, rgb(255, 255, 255) 0%, rgba(255, 255, 255, 0.644) 50%)
+}
+
+.bot-card.disabled {
+ opacity: 50%;
+ cursor: default;
}
.center-flex {
@@ -162,8 +172,16 @@ body, html {
padding: 2px 5px;
}
-.bot-pool .bot-card {
- display: inline-flex;
+.bot-pool .categories-radio-group {
+ margin-right: 10px;
+ background-color: white;
+}
+
+.bot-pool .scripts-header {
+ margin-top: 5px;
+ margin-bottom: 0px;
+ margin-left: 3px;
+ font-weight: bold;
}
.bot-card img {
@@ -175,8 +193,8 @@ body, html {
margin: 0 3px 0 0;
}
-.filtered {
- opacity: 0.2;
+.script-card:not(.disabled) .script-switch * {
+ cursor: pointer;
}
.bot-card img.darkened {
diff --git a/rlbot_gui/gui/js/bot-card-vue.js b/rlbot_gui/gui/js/bot-card-vue.js
index 0850af2f..4c8dc6fb 100644
--- a/rlbot_gui/gui/js/bot-card-vue.js
+++ b/rlbot_gui/gui/js/bot-card-vue.js
@@ -1,25 +1,46 @@
+import RunnableCard from './runnable-card-vue.js'
+
export default {
name: 'bot-card',
- props: ['bot'],
- template: `
-
-
-
-
-
- {{ bot.name }} ({{ bot.uniquePathSegment }})
-
-
-
-
-
-
-
-
-
-
-
+ components: {
+ 'runnable-card': RunnableCard,
+ },
+ props: {
+ bot: Object,
+ disabled: Boolean,
+ draggable: {
+ type: Boolean,
+ default: true,
+ },
+ },
+ template: /*html*/`
+
+
+
+
+
+ {{ bot.name }}
+
+ ({{ bot.uniquePathSegment }})
+
+
+
+
`,
+ computed: {
+ draggableModel: function() {
+ return [this.bot];
+ },
+ draggableOptions: function() {
+ return {
+ group: {
+ name: 'bots',
+ pull: 'clone',
+ put: false,
+ },
+ sort: false,
+ disabled: !this.draggable || this.disabled,
+ };
+ },
+ },
}
diff --git a/rlbot_gui/gui/js/categories.js b/rlbot_gui/gui/js/categories.js
new file mode 100644
index 00000000..5274c501
--- /dev/null
+++ b/rlbot_gui/gui/js/categories.js
@@ -0,0 +1,73 @@
+export default {
+ all: {
+ name: "All",
+ categories: [
+ {
+ bots: "*",
+ scripts: "*",
+ includePsyonixBots: true,
+ },
+ ],
+ },
+ standard: {
+ name: "Standard",
+ categories: [
+ {
+ name: "Bots for 1v1",
+ bots: "1v1",
+ },
+ {
+ name: "Bots with teamplay",
+ bots: "teamplay",
+ },
+ {
+ name: "Goalie bots",
+ bots: "goalie",
+ },
+ ],
+ },
+ extra: {
+ name: "Extra modes",
+ categories: [
+ {
+ name: "Hoops",
+ bots: "hoops",
+ scripts: "hoops",
+ },
+ {
+ name: "Dropshot",
+ bots: "dropshot",
+ scripts: "dropshot",
+ },
+ {
+ name: "Snow Day",
+ bots: "snow-day",
+ scripts: "snow-day",
+ },
+ {
+ name: "Rumble",
+ bots: "rumble",
+ scripts: "rumble",
+ },
+ {
+ name: "Spike Rush",
+ bots: "spike-rush",
+ scripts: "spike-rush",
+ },
+ {
+ name: "Heatseeker",
+ bots: "heatseeker",
+ scripts: "heatseeker",
+ },
+ ],
+ },
+ special: {
+ name: "Special bots/scripts",
+ categories: [
+ {
+ bots: "memebot",
+ displayScriptDependencies: true,
+ },
+ ],
+ },
+};
\ No newline at end of file
diff --git a/rlbot_gui/gui/js/main-vue.js b/rlbot_gui/gui/js/main-vue.js
index 0ca23dc2..ee749c43 100644
--- a/rlbot_gui/gui/js/main-vue.js
+++ b/rlbot_gui/gui/js/main-vue.js
@@ -1,8 +1,11 @@
import AppearanceEditor from './appearance-editor-vue.js'
import MutatorField from './mutator-field-vue.js'
import BotCard from './bot-card-vue.js'
+import ScriptCard from './script-card-vue.js'
+import ScriptDependencies from './script-dependencies-vue.js'
import TeamCard from './team-card-vue.js'
import LauncherPreferenceModal from './launcher-preference-vue.js'
+import categories from './categories.js';
const HUMAN = {'name': 'Human', 'type': 'human', 'image': 'imgs/human.png'};
const STARTING_BOT_POOL = [
@@ -81,7 +84,7 @@ export default {
-
+
Add
@@ -108,35 +111,46 @@ export default {
Manage bot folders
-
-
-
-
-
-
Recommendations
-
-
-
-
-
-
-
-
-
- {{script.name}}
-
-
-
-
+
+
+
+
+
+
+
+
+
+ No bots available.
+ Try updating your botpack.
+
+
+
+
+
+
+
@@ -254,6 +268,10 @@ export default {
@@ -360,7 +378,7 @@ export default {
Not sure which bots to play against? Try our recommended picks:
-
+
Select
@@ -393,6 +411,8 @@ export default {
'appearance-editor': AppearanceEditor,
'mutator-field': MutatorField,
'bot-card': BotCard,
+ 'script-card': ScriptCard,
+ 'script-dependencies': ScriptDependencies,
'team-card': TeamCard,
'launcher-preference-modal': LauncherPreferenceModal,
},
@@ -400,9 +420,9 @@ export default {
return {
botPool: STARTING_BOT_POOL,
scriptPool: [],
- blueTeam: [],
+ blueTeam: [HUMAN],
orangeTeam: [],
- teamSelection: "blue",
+ teamSelection: 'orange',
matchOptions: null,
matchSettings: {
map: null,
@@ -441,20 +461,27 @@ export default {
snackbarContent: null,
showProgressSpinner: false,
languageSupport: null,
- activeBot: null,
newBotName: '',
newBotLanguageChoice: 'python',
folderSettings: {
files: {},
folders: {}
},
+ isBotpackUpToDate: true,
downloadProgressPercent: 0,
downloadStatus: '',
showBotpackUpdateSnackbar: false,
botNameFilter: '',
appearancePath: '',
recommendations: null,
- downloadModalTitle: "Downloading Bot Pack"
+ downloadModalTitle: "Downloading Bot Pack",
+ categories: categories,
+ primaryCategoryOptions: Object.values(categories).map(ctg => {
+ ctg.options = ctg.categories.map(sc => ({text: sc.name, value: sc}));
+ ctg.selected = ctg.categories[0];
+ return {text: ctg.name, value: ctg};
+ }),
+ primaryCategorySelected: categories.all,
}
},
@@ -594,8 +621,27 @@ export default {
eel.scan_for_bots()(this.botsReceived);
eel.scan_for_scripts()(this.scriptsReceived);
},
- passesFilter: function(botName) {
- return botName.toLowerCase().includes(this.botNameFilter.toLowerCase());
+ passesFilter: function(runnable) {
+ let category = this.secondaryCategorySelected;
+
+ if (!runnable.name.toLowerCase().includes(this.botNameFilter.toLowerCase()))
+ return false;
+
+ // only display Human when it's not on any of the teams
+ if (runnable.type === 'human')
+ return !this.blueTeam.concat(this.orangeTeam).includes(HUMAN);
+
+ if (runnable.type === 'psyonix')
+ return category.includePsyonixBots;
+
+ let allowedTags = runnable.type === 'script' ? category.scripts : category.bots;
+ if (allowedTags) {
+ if (allowedTags === '*') {
+ return true;
+ }
+ return runnable.info.tags.some(tag => allowedTags.includes(tag));
+ }
+ return false;
},
botLoadHandler: function (response) {
this.$bvModal.hide('new-bot-modal');
@@ -613,8 +659,9 @@ export default {
!this.botPool.find( (element) => element.path === bot.path ));
freshBots.forEach((bot) => bot.warn = false);
+ freshBots.sort((a, b) => a.name.localeCompare(b.name));
- this.botPool = this.botPool.concat(freshBots).sort((a, b) => a.name.localeCompare(b.name));
+ this.botPool = this.botPool.concat(freshBots);
this.applyLanguageWarnings();
this.distinguishDuplicateBots();
this.showProgressSpinner = false;
@@ -624,8 +671,9 @@ export default {
const freshScripts = scripts.filter( (script) =>
!this.scriptPool.find( (element) => element.path === script.path ));
freshScripts.forEach((script) => {script.enabled = !!this.matchSettings.scripts.find( (element) => element.path === script.path )});
+ freshScripts.sort((a, b) => a.name.localeCompare(b.name));
- this.scriptPool = this.scriptPool.concat(freshScripts).sort((a, b) => a.name.localeCompare(b.name));
+ this.scriptPool = this.scriptPool.concat(freshScripts);
this.applyLanguageWarnings();
this.showProgressSpinner = false;
},
@@ -706,6 +754,7 @@ export default {
botpackUpdateChecked: function (isBotpackUpToDate) {
this.showBotpackUpdateSnackbar = !isBotpackUpToDate;
+ this.isBotpackUpToDate = isBotpackUpToDate;
},
botPackUpdated: function (message) {
@@ -715,6 +764,7 @@ export default {
eel.get_folder_settings()(this.folderSettingsReceived);
eel.get_recommendations()(recommendations => this.recommendations = recommendations);
eel.get_match_options()(this.matchOptionsReceived)
+ this.primaryCategorySelected = this.categories.standard;
},
onInstallationComplete: function (result) {
@@ -774,6 +824,18 @@ export default {
this.matchSettings.mutators[key] != this.matchOptions.mutators[key + "_types"][0]
).filter(Boolean).length;
},
+ activeBot: function() {
+ return this.$store.state.activeBot;
+ },
+ secondaryCategorySelected: function() {
+ return this.primaryCategorySelected.selected;
+ },
+ displayedBotsCount: function() {
+ return this.botPool.filter(this.passesFilter).length;
+ },
+ displayedScriptsCount: function() {
+ return this.scriptPool.filter(this.passesFilter).length;
+ },
},
created: function () {
this.startup()
diff --git a/rlbot_gui/gui/js/main.js b/rlbot_gui/gui/js/main.js
index 432ced8a..001eb162 100644
--- a/rlbot_gui/gui/js/main.js
+++ b/rlbot_gui/gui/js/main.js
@@ -20,8 +20,20 @@ const router = new VueRouter({
routes: routes
});
+const store = new Vuex.Store({
+ state: {
+ activeBot: null,
+ },
+ mutations: {
+ setActiveBot(state, bot) {
+ state.activeBot = bot;
+ },
+ },
+});
+
const app = new Vue({
router: router,
+ store: store,
el: '#app',
data: {
bodyStyle: null
diff --git a/rlbot_gui/gui/js/runnable-card-vue.js b/rlbot_gui/gui/js/runnable-card-vue.js
new file mode 100644
index 00000000..fa7b0e3d
--- /dev/null
+++ b/rlbot_gui/gui/js/runnable-card-vue.js
@@ -0,0 +1,21 @@
+export default {
+ name: 'runnable-card',
+ props: ['runnable', 'disabled'],
+ template: /*html*/`
+
+
+ {{ runnable.name }}
+
+
+
+
+
+
+
+
+
+
+ `,
+}
diff --git a/rlbot_gui/gui/js/script-card-vue.js b/rlbot_gui/gui/js/script-card-vue.js
new file mode 100644
index 00000000..d117e4f0
--- /dev/null
+++ b/rlbot_gui/gui/js/script-card-vue.js
@@ -0,0 +1,19 @@
+import RunnableCard from './runnable-card-vue.js'
+
+export default {
+ name: 'script-card',
+ props: ['script', 'disabled'],
+ components: {
+ 'runnable-card': RunnableCard,
+ },
+ template: `
+
+
+
+
+ {{ script.name }}
+
+
+
+ `,
+}
diff --git a/rlbot_gui/gui/js/script-dependencies-vue.js b/rlbot_gui/gui/js/script-dependencies-vue.js
new file mode 100644
index 00000000..58b2fa03
--- /dev/null
+++ b/rlbot_gui/gui/js/script-dependencies-vue.js
@@ -0,0 +1,83 @@
+import BotCard from './bot-card-vue.js'
+import ScriptCard from './script-card-vue.js'
+
+function prefixFilter(arr, prefix) {
+ // cut prefix from strings and remove those which don't have the prefix
+ return arr.filter(str => str.startsWith(prefix)).map(str => str.substring(prefix.length));
+}
+
+export default {
+ name: 'script-dependencies',
+ components: {
+ 'bot-card': BotCard,
+ 'script-card': ScriptCard,
+ },
+ props: ['bots', 'scripts', 'nameFilter'],
+ template: /*html*/`
+
+ `,
+ methods: {
+ passesFilter: function(runnable) {
+ return runnable.name.toLowerCase().includes(this.nameFilter.toLowerCase());
+ }
+ },
+ computed: {
+ dependencies: function() {
+ // array of objects, which contain a script and bots/scripts that support/require it
+ return this.scripts.map(script => {
+ let enableTags = prefixFilter(script.info.tags, "enables-");
+ let enableTagFilter = runnable => runnable.info && enableTags.some(tag =>
+ prefixFilter(runnable.info.tags, "supports-").includes(tag) ||
+ prefixFilter(runnable.info.tags, "requires-").includes(tag)
+ );
+
+ let supportedBots = this.bots.filter(enableTagFilter);
+ let supportedScripts = this.scripts.filter(enableTagFilter);
+ let visible = [script, ...supportedBots, ...supportedScripts].some(this.passesFilter);
+
+ return {script, supportedBots, supportedScripts, visible};
+
+ }).filter(d => d.supportedScripts.length + d.supportedBots.length > 0);
+ },
+ uninvolvedScripts: function() {
+ // scripts that don't require another script and arent supported/required by anything else
+ return this.scripts.filter(script => this.dependencies.every(
+ d => d.script != script && !script.info.tags.some(tag => tag.startsWith("requires-"))
+ ));
+ },
+ },
+}
diff --git a/rlbot_gui/gui/js/vuex.min.js b/rlbot_gui/gui/js/vuex.min.js
new file mode 100644
index 00000000..ac693efb
--- /dev/null
+++ b/rlbot_gui/gui/js/vuex.min.js
@@ -0,0 +1,6 @@
+/**
+ * vuex v2.1.1
+ * (c) 2016 Evan You
+ * @license MIT
+ */
+!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):t.Vuex=e()}(this,function(){"use strict";function t(t){x&&(t._devtoolHook=x,x.emit("vuex:init",t),x.on("vuex:travel-to-state",function(e){t.replaceState(e)}),t.subscribe(function(t,e){x.emit("vuex:mutation",t,e)}))}function e(t){function e(){var t=this.$options;t.store?this.$store=t.store:t.parent&&t.parent.$store&&(this.$store=t.parent.$store)}var n=Number(t.version.split(".")[0]);if(n>=2){var o=t.config._lifecycleHooks.indexOf("init")>-1;t.mixin(o?{init:e}:{beforeCreate:e})}else{var r=t.prototype._init;t.prototype._init=function(t){void 0===t&&(t={}),t.init=t.init?[e].concat(t.init):e,r.call(this,t)}}}function n(t){return Array.isArray(t)?t.map(function(t){return{key:t,val:t}}):Object.keys(t).map(function(e){return{key:e,val:t[e]}})}function o(t){return function(e,n){return"string"!=typeof e?(n=e,e=""):"/"!==e.charAt(e.length-1)&&(e+="/"),t(e,n)}}function r(t,e){console.error("[vuex] module namespace not found in "+t+"(): "+e)}function i(t,e){Object.keys(t).forEach(function(n){return e(t[n],n)})}function s(t){return null!==t&&"object"==typeof t}function a(t){return t&&"function"==typeof t.then}function u(t,e){if(!t)throw new Error("[vuex] "+e)}function c(t,e){if(t.update(e),e.modules)for(var n in e.modules){if(!t.getChild(n))return void console.warn("[vuex] trying to add a new module '"+n+"' on hot reloading, manual reload is needed");c(t.getChild(n),e.modules[n])}}function l(t){t._actions=Object.create(null),t._mutations=Object.create(null),t._wrappedGetters=Object.create(null),t._modulesNamespaceMap=Object.create(null);var e=t.state;p(t,e,[],t._modules.root,!0),f(t,e)}function f(t,e){var n=t._vm;t.getters={};var o=t._wrappedGetters,r={};i(o,function(e,n){r[n]=function(){return e(t)},Object.defineProperty(t.getters,n,{get:function(){return t._vm[n]},enumerable:!0})});var s=A.config.silent;A.config.silent=!0,t._vm=new A({data:{state:e},computed:r}),A.config.silent=s,t.strict&&_(t),n&&(t._withCommit(function(){n.state=null}),A.nextTick(function(){return n.$destroy()}))}function p(t,e,n,o,r){var i=!n.length,s=t._modules.getNamespace(n);if(s&&(t._modulesNamespaceMap[s]=o),!i&&!r){var a=g(e,n.slice(0,-1)),u=n[n.length-1];t._withCommit(function(){A.set(a,u,o.state)})}var c=o.context=h(t,s);o.forEachMutation(function(e,o){var r=s+o;m(t,r,e,n)}),o.forEachAction(function(e,o){var r=s+o;v(t,r,e,c,n)}),o.forEachGetter(function(e,o){var r=s+o;y(t,r,e,c,n)}),o.forEachChild(function(o,i){p(t,e,n.concat(i),o,r)})}function h(t,e){var n=""===e,o={dispatch:n?t.dispatch:function(n,o,r){var i=w(n,o,r),s=i.payload,a=i.options,u=i.type;return a&&a.root||(u=e+u,t._actions[u])?t.dispatch(u,s):void console.error("[vuex] unknown local action type: "+i.type+", global type: "+u)},commit:n?t.commit:function(n,o,r){var i=w(n,o,r),s=i.payload,a=i.options,u=i.type;return a&&a.root||(u=e+u,t._mutations[u])?void t.commit(u,s,a):void console.error("[vuex] unknown local mutation type: "+i.type+", global type: "+u)}};return Object.defineProperty(o,"getters",{get:n?function(){return t.getters}:function(){return d(t,e)}}),o}function d(t,e){var n={},o=e.length;return Object.keys(t.getters).forEach(function(r){if(r.slice(0,o)===e){var i=r.slice(o);Object.defineProperty(n,i,{get:function(){return t.getters[r]},enumerable:!0})}}),n}function m(t,e,n,o){var r=t._mutations[e]||(t._mutations[e]=[]);r.push(function(e){n(g(t.state,o),e)})}function v(t,e,n,o,r){var i=t._actions[e]||(t._actions[e]=[]);i.push(function(e,i){var s=n({dispatch:o.dispatch,commit:o.commit,getters:o.getters,state:g(t.state,r),rootGetters:t.getters,rootState:t.state},e,i);return a(s)||(s=Promise.resolve(s)),t._devtoolHook?s.catch(function(e){throw t._devtoolHook.emit("vuex:error",e),e}):s})}function y(t,e,n,o,r){return t._wrappedGetters[e]?void console.error("[vuex] duplicate getter key: "+e):void(t._wrappedGetters[e]=function(t){return n(g(t.state,r),o.getters,t.state,t.getters)})}function _(t){t._vm.$watch("state",function(){u(t._committing,"Do not mutate vuex store state outside mutation handlers.")},{deep:!0,sync:!0})}function g(t,e){return e.length?e.reduce(function(t,e){return t[e]},t):t}function w(t,e,n){return s(t)&&t.type&&(n=e,e=t,t=t.type),{type:t,payload:e,options:n}}function b(t){return A?void console.error("[vuex] already installed. Vue.use(Vuex) should be called only once."):(A=t,void e(A))}var x="undefined"!=typeof window&&window.__VUE_DEVTOOLS_GLOBAL_HOOK__,O=o(function(t,e){var o={};return n(e).forEach(function(e){var n=e.key,i=e.val;o[n]=function(){var e=this.$store.state,n=this.$store.getters;if(t){var o=this.$store._modulesNamespaceMap[t];if(!o)return void r("mapState",t);e=o.state,n=o.context.getters}return"function"==typeof i?i.call(this,e,n):e[i]}}),o}),M=o(function(t,e){var o={};return n(e).forEach(function(e){var n=e.key,r=e.val;r=t+r,o[n]=function(){for(var t=[],e=arguments.length;e--;)t[e]=arguments[e];return this.$store.commit.apply(this.$store,[r].concat(t))}}),o}),k=o(function(t,e){var o={};return n(e).forEach(function(e){var n=e.key,r=e.val;r=t+r,o[n]=function(){return r in this.$store.getters||console.error("[vuex] unknown getter: "+r),this.$store.getters[r]}}),o}),E=o(function(t,e){var o={};return n(e).forEach(function(e){var n=e.key,r=e.val;r=t+r,o[n]=function(){for(var t=[],e=arguments.length;e--;)t[e]=arguments[e];return this.$store.dispatch.apply(this.$store,[r].concat(t))}}),o}),j=function(t,e){this.runtime=e,this._children=Object.create(null),this._rawModule=t},C={state:{},namespaced:{}};C.state.get=function(){return this._rawModule.state||{}},C.namespaced.get=function(){return!!this._rawModule.namespaced},j.prototype.addChild=function(t,e){this._children[t]=e},j.prototype.removeChild=function(t){delete this._children[t]},j.prototype.getChild=function(t){return this._children[t]},j.prototype.update=function(t){this._rawModule.namespaced=t.namespaced,t.actions&&(this._rawModule.actions=t.actions),t.mutations&&(this._rawModule.mutations=t.mutations),t.getters&&(this._rawModule.getters=t.getters)},j.prototype.forEachChild=function(t){i(this._children,t)},j.prototype.forEachGetter=function(t){this._rawModule.getters&&i(this._rawModule.getters,t)},j.prototype.forEachAction=function(t){this._rawModule.actions&&i(this._rawModule.actions,t)},j.prototype.forEachMutation=function(t){this._rawModule.mutations&&i(this._rawModule.mutations,t)},Object.defineProperties(j.prototype,C);var $=function(t){var e=this;this.root=new j(t,!1),t.modules&&i(t.modules,function(t,n){e.register([n],t,!1)})};$.prototype.get=function(t){return t.reduce(function(t,e){return t.getChild(e)},this.root)},$.prototype.getNamespace=function(t){var e=this.root;return t.reduce(function(t,n){return e=e.getChild(n),t+(e.namespaced?n+"/":"")},"")},$.prototype.update=function(t){c(this.root,t)},$.prototype.register=function(t,e,n){var o=this;void 0===n&&(n=!0);var r=this.get(t.slice(0,-1)),s=new j(e,n);r.addChild(t[t.length-1],s),e.modules&&i(e.modules,function(e,r){o.register(t.concat(r),e,n)})},$.prototype.unregister=function(t){var e=this.get(t.slice(0,-1)),n=t[t.length-1];e.getChild(n).runtime&&e.removeChild(n)};var A,V=function(e){var n=this;void 0===e&&(e={}),u(A,"must call Vue.use(Vuex) before creating a store instance."),u("undefined"!=typeof Promise,"vuex requires a Promise polyfill in this browser.");var o=e.state;void 0===o&&(o={});var r=e.plugins;void 0===r&&(r=[]);var i=e.strict;void 0===i&&(i=!1),this._committing=!1,this._actions=Object.create(null),this._mutations=Object.create(null),this._wrappedGetters=Object.create(null),this._modules=new $(e),this._modulesNamespaceMap=Object.create(null),this._subscribers=[],this._watcherVM=new A;var s=this,a=this,c=a.dispatch,l=a.commit;this.dispatch=function(t,e){return c.call(s,t,e)},this.commit=function(t,e,n){return l.call(s,t,e,n)},this.strict=i,p(this,o,[],this._modules.root),f(this,o),r.concat(t).forEach(function(t){return t(n)})},G={state:{}};G.state.get=function(){return this._vm.$data.state},G.state.set=function(t){u(!1,"Use store.replaceState() to explicit replace store state.")},V.prototype.commit=function(t,e,n){var o=this,r=w(t,e,n),i=r.type,s=r.payload,a=r.options,u={type:i,payload:s},c=this._mutations[i];return c?(this._withCommit(function(){c.forEach(function(t){t(s)})}),this._subscribers.forEach(function(t){return t(u,o.state)}),void(a&&a.silent&&console.warn("[vuex] mutation type: "+i+". Silent option has been removed. Use the filter functionality in the vue-devtools"))):void console.error("[vuex] unknown mutation type: "+i)},V.prototype.dispatch=function(t,e){var n=w(t,e),o=n.type,r=n.payload,i=this._actions[o];return i?i.length>1?Promise.all(i.map(function(t){return t(r)})):i[0](r):void console.error("[vuex] unknown action type: "+o)},V.prototype.subscribe=function(t){var e=this._subscribers;return e.indexOf(t)<0&&e.push(t),function(){var n=e.indexOf(t);n>-1&&e.splice(n,1)}},V.prototype.watch=function(t,e,n){var o=this;return u("function"==typeof t,"store.watch only accepts a function."),this._watcherVM.$watch(function(){return t(o.state,o.getters)},e,n)},V.prototype.replaceState=function(t){var e=this;this._withCommit(function(){e._vm.state=t})},V.prototype.registerModule=function(t,e){"string"==typeof t&&(t=[t]),u(Array.isArray(t),"module path must be a string or an Array."),this._modules.register(t,e),p(this,this.state,t,this._modules.get(t)),f(this,this.state)},V.prototype.unregisterModule=function(t){var e=this;"string"==typeof t&&(t=[t]),u(Array.isArray(t),"module path must be a string or an Array."),this._modules.unregister(t),this._withCommit(function(){var n=g(e.state,t.slice(0,-1));A.delete(n,t[t.length-1])}),l(this)},V.prototype.hotUpdate=function(t){this._modules.update(t),l(this)},V.prototype._withCommit=function(t){var e=this._committing;this._committing=!0,t(),this._committing=e},Object.defineProperties(V.prototype,G),"undefined"!=typeof window&&window.Vue&&b(window.Vue);var P={Store:V,install:b,version:"2.1.1",mapState:O,mapMutations:M,mapGetters:k,mapActions:E};return P});
\ No newline at end of file
diff --git a/rlbot_gui/gui/main.html b/rlbot_gui/gui/main.html
index bc053291..28cdf3ba 100644
--- a/rlbot_gui/gui/main.html
+++ b/rlbot_gui/gui/main.html
@@ -52,6 +52,7 @@
+
diff --git a/screenshots/RLBotGUI-Home.PNG b/screenshots/RLBotGUI-Home.PNG
index 2662ea77..99646b93 100644
Binary files a/screenshots/RLBotGUI-Home.PNG and b/screenshots/RLBotGUI-Home.PNG differ
diff --git a/setup.py b/setup.py
index 6d452bfe..8b4d82cd 100644
--- a/setup.py
+++ b/setup.py
@@ -1,6 +1,6 @@
import setuptools
-__version__ = '0.0.113'
+__version__ = '0.0.114'
with open("README.md", "r") as readme_file:
long_description = readme_file.read()