NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript==
// @namespace https://openuserjs.org/users/SB100
// @name GGn Equipment Loadout Manager
// @description Set loadouts for your equipment so you can mass equip and unequip items
// @updateURL https://openuserjs.org/meta/SB100/GGn_Equipment_Loadout_Manager.meta.js
// @version 1.4.1
// @author SB100
// @copyright 2021, SB100 (https://openuserjs.org/users/SB100)
// @license MIT
// @include https://gazellegames.net/user.php?action=equipment
// @grant GM_xmlhttpRequest
// @grant GM_getValue
// @grant GM_setValue
// ==/UserScript==
// ==OpenUserJS==
// @author SB100
// ==/OpenUserJS==
/* jshint esversion: 6 */
/**
* =============================
* ADVANCED OPTIONS
* =============================
*/
// If you want the Equipment Loadout Manager to be opened by default when you visit the equipment page
const DEFAULT_OPENED = true;
// If you want to bypass the confirm dialog when clicking Equip Items
const BYPASS_CONFIRM = false;
// If you want to show the quick equip buttons when the equipment loadout manager form is closed
const SHOW_QUICK_BUTTONS = true;
/**
* =============================
* END ADVANCED OPTIONS
* DO NOT MODIFY BELOW THIS LINE
* =============================
*/
/**
* A cache of all the items we've analyzed on the page
*/
const ITEM_CACHE = {};
/**
* Wait a specific amount of time.
* Use this in an async function via: `await wait(ms)`
*/
function wait(ms) {
return new Promise((resolve) => setTimeout(resolve, ms))
}
/**
* Shallow compare 2 objects for equality
*/
function objShallowEqual(obj1, obj2) {
return Object.keys(obj1).length === Object.keys(obj2).length &&
Object.keys(obj1).every(key => obj1[key] === obj2[key]);
}
/**
* Get the current user id
*/
function getUserId() {
return unsafeWindow.userid;
}
/**
* Get the users auth key
*/
function getAuthKey() {
return unsafeWindow.authkey;
}
/**
* Turn a slot id into a slot name
*/
function getSlotName(slotId) {
switch (slotId) {
case 1:
return 'Helmet';
case 2:
return 'Upper Body';
case 3:
return 'Arms';
case 4:
return 'Legs';
case 5:
return 'Hands';
case 6:
return 'Feet';
case 7:
return 'Left Hand';
case 8:
return 'Right Hand';
case 9:
return 'Necklace';
case 10:
return 'Left Ring'
case 11:
return 'Right Ring'
case 12:
return 'Backpack';
case 13:
return 'Clothes';
case 14:
return 'Pet 1';
case 15:
return 'Pet 2';
default:
return 'Unknown';
}
}
/**
* Turn a slot name into a slot id
*/
function getSlotId(slotName) {
switch (slotName) {
case 'Helmet':
return 1;
case 'Upper Body':
return 2;
case 'Arms':
return 3;
case 'Legs':
return 4;
case 'Hands':
return 5;
case 'Feet':
return 6;
case 'Left Hand':
return 7;
case 'Right Hand':
return 8;
case 'Necklace':
return 9;
case 'Left Ring':
return 10;
case 'Right Ring':
return 11;
case 'Backpack':
return 12;
case 'Clothes':
return 13;
case 'Pet 1':
return 14;
case 'Pet 2':
return 15;
default:
return 0;
}
};
/**
* Get all items currently available to the user.
* If wantOnlyEquip is true, this will only return items that are currently equipped
*/
function getItems(wantOnlyEquip = false) {
const selector = wantOnlyEquip ? '.itemslot .item' : '.itemslot .item, #items-wrapper .item';
return Array.from(document.querySelectorAll(selector)).reduce((result, elem) => {
const slots = elem.dataset.slots.split(',');
const conjunction = Array.isArray(slots) && slots.pop();
slots.forEach(s => {
const slot = parseInt(s, 10);
const slotName = getSlotName(slot);
const id = parseInt(elem.dataset.id, 10);
if (!result[slotName]) {
result[slotName] = [];
}
ITEM_CACHE[id] = {
id,
name: decodeURI(elem.dataset.itemName),
slots: slots.map(slot => parseInt(slot, 10)),
conjunction: conjunction
}
result[slotName].push(ITEM_CACHE[id]);
});
return result;
}, {});
}
/**
* Get an item's details by an item id
*/
function getItem(itemId) {
if (ITEM_CACHE[itemId]) {
return ITEM_CACHE[itemId];
}
const elems = Array.from(document.querySelectorAll('.itemslot .item, #items-wrapper .item'));
for (let i = 0, len = elems.length; i < len; i += 1) {
const elem = elems[i];
const id = parseInt(elem.dataset.id, 10)
if (id === itemId) {
const slots = elem.dataset.slots.split(',');
const conjunction = Array.isArray(slots) && slots.pop();
ITEM_CACHE[id] = {
id,
name: decodeURI(elem.dataset.itemName),
slots: slots.map(slot => parseInt(slot, 10)),
conjunction: conjunction
}
return ITEM_CACHE[id];
}
}
return {}
}
/**
* Group items by their item slots and conjunction.
* We need this so that we can [un]equip rings and pets one at a time to avoid a race condition
*/
function groupItems(itemIds) {
const seen = {
0: []
};
const results = {
0: []
};
for (let i = 0, iLen = itemIds.length; i < iLen; i += 1) {
const item = getItem(itemIds[i]);
const key = `${item.slots.sort().join(':')}:${item.conjunction}`;
let pushed = false;
for (let j = 0, jLen = Object.keys(seen).length; j < jLen; j += 1) {
if (seen[j].includes(key) === false) {
results[j].push(item.id);
seen[j].push(key);
pushed = true;
break;
}
}
if (pushed === false) {
results[Object.keys(results).length] = [item.id];
seen[Object.keys(seen).length] = [key];
}
}
return results;
}
/**
* Get all saved loadouts
*/
function getLoadouts() {
const existing = GM_getValue('loadouts', '{}');
return JSON.parse(existing);
}
/**
* Send a request to the server to equip or unequip an item
*/
function sendEquipRequest(isEquip, itemId, userId, authKey) {
let resolver;
let rejecter;
const p = new Promise((resolveFn, rejectFn) => {
resolver = resolveFn;
rejecter = rejectFn;
});
const url = `/user.php?action=ajax_equip_item&auth=${authKey}&itemid=${itemId}&userid=${userId}&equiptype=${isEquip ? 'equip' : 'unequip'}`;
GM_xmlhttpRequest({
method: 'get',
url: url,
timeout: 5000,
onloadstart: function () {
},
onload: function () {
resolver();
},
onerror: function () {
rejecter('Error')
},
ontimeout: function () {
rejecter('Timeout')
},
});
return p;
}
/**
* Convenience method for bulk equipping or unequipping items
*/
function equipAllItemIds(itemIds, userId, authKey, isEquip = true) {
const promises = itemIds.map(itemId => sendEquipRequest(isEquip, itemId, userId, authKey));
return Promise.all(promises);
}
/**
* Turn a form into a JSON object, without the loadout name
*/
function formToJson(formData) {
const result = {};
for (const [key, val] of formData.entries()) {
if (key === 'loadout-name') continue;
result[key] = parseInt(val, 10);
}
return result;
}
// --------------------------------------------------------------------------------------------- Handlers
/**
* Handler for when the "Equip Items" button is clicked. This will:
* - Check that the current loadout is up to date (i.e. all changes have been saved)
* - Calculates what needs to be unequipped, and what needs to be equipped
* - Confirms the changes with the user
* - Sends requests to the server to equip and unequip items
* - Reloads the page
*/
async function handleEquip() {
// currently equip
const currentlyEquipObj = getItems(true);
const currentlyEquipIds = Object.keys(currentlyEquipObj).reduce((result, key) => {
const values = currentlyEquipObj[key];
values.forEach(value => result.push(value.id));
return result;
}, []);
const uniqueEquipIds = [...new Set(currentlyEquipIds)]
// loadout items
const uniqueLoadoutIds = [];
const formData = new FormData(document.querySelector('.loadout-manager__form'));
for (const [key, val] of formData.entries()) {
if (key === 'loadout-name') continue;
uniqueLoadoutIds.push(parseInt(val, 10));
}
// check if there have been any changes in the loadout
const loadouts = getLoadouts();
const loadout = loadouts[formData.get('loadout-name')];
const form = formToJson(formData);
if (!loadout || !objShallowEqual(loadout, form)) {
alert(`Changes detected in this loadout\rPlease verify your loadout, save it, and then try again`);
return;
}
// calculate difference for what we have to unequip
const unequip = uniqueEquipIds.filter(x => !uniqueLoadoutIds.includes(x));
// calculate difference for what we have to equip
const equip = uniqueLoadoutIds.filter(x => !uniqueEquipIds.includes(x));
// check if we acutally have to make changes
if (unequip.length === 0 && equip.length === 0) {
window.alert('Nothing to do!');
return;
}
if (!BYPASS_CONFIRM) {
// get names of equipment we will equip and unequip
const unequipNames = unequip.map(itemId => getItem(itemId).name);
const equipNames = equip.map(itemId => getItem(itemId).name);
// confirm the changes we're about to make with the user
const message = `About to run "${formData.get('loadout-name')}" loadout. This will result in the following changes:\r\r${unequipNames.length > 0 ? `To Unequip:\r${unequipNames.map(s => ` - ${s}`).join("\r")}\r\r` : ''}${equip.length > 0 ? `To Equip:\r${equipNames.map(s => ` - ${s}`).join("\r")}\r\r` : ''}Is this ok?`;
if (!window.confirm(message)) {
return;
}
}
// user has confirmed changes - let's make them!
const loading = document.querySelector('.loadout-manager__loading');
loading && loading.classList.add('loadout-manager__loading--showing');
try {
// group items so that we can avoid race conditions
const unequipGroups = groupItems(unequip);
const equipGroups = groupItems(equip);
// first unequip what we need to
loading.innerHTML = 'Unequipping unneeded items';
for (let i = 0, len = Object.keys(unequipGroups).length; i < len; i += 1) {
await equipAllItemIds(unequipGroups[i], getUserId(), getAuthKey(), false);
await wait(250);
}
// then equip what we need to
loading.innerHTML = 'Equipping missing items';
for (let i = 0, len = Object.keys(equipGroups).length; i < len; i += 1) {
await equipAllItemIds(equipGroups[i], getUserId(), getAuthKey(), true);
await wait(250);
}
// then reload the page to see new changes
loading.innerHTML = 'Reloading page';
window.location.reload(true);
return true;
}
catch (e) {
// if there was an error, show the user, and do nothing more
loading.innerHTML = 'An error occurred whilst processing the loadout';
console.error(e);
return false;
}
}
/**
* Handler for when the "Save" button is clicked.
* This saves a loadout for future selection
*/
function handleSave(event, loadoutManager) {
const formData = new FormData(document.querySelector('.loadout-manager__form'));
const name = formData.get('loadout-name');
if (name === '') {
window.alert('You must enter a loadout name');
return;
}
const loadouts = getLoadouts();
loadouts[name] = formToJson(formData);
GM_setValue('loadouts', JSON.stringify(loadouts));
// recreate the dropdown, and have this value now selected
createLoadoutDropdown(loadoutManager, name);
}
/**
* Handler for when the "Remove" button is clicked
* This removes a currently stored loadout
*/
function handleRemove(name, loadoutManager) {
const loadouts = getLoadouts();
delete loadouts[name];
GM_setValue('loadouts', JSON.stringify(loadouts));
// remove the form
createLoadoutForm('-1', loadoutManager);
// recreate the dropdown with the deleted value now gone
createLoadoutDropdown(loadoutManager);
}
/**
* Handler for when an item is selected.
* This enables / disables other checkboxes in the form for a better user experience
*/
function handleItemSelect(target, items) {
const isChecked = target.checked;
const itemId = parseInt(target.dataset.itemId, 10);
const currentSlotId = parseInt(target.parentNode.parentNode.dataset.slotId, 10);
const item = items[getSlotName(currentSlotId)].find(item => item.id === itemId);
const itemSlots = item && item.slots;
const itemConjunction = item.conjunction;
if (!currentSlotId || !itemSlots) {
console.log('Couldnt find item slot data - script needs updating!');
return;
}
// disable/enable all other checkboxes in the current fieldset
Array.from(document.querySelectorAll(`.loadout-manager__form fieldset[data-slot-id="${currentSlotId}"] input[type="checkbox"]`)).forEach(option => {
// don't act on the checkbox we just ticked
if (parseInt(option.dataset.itemId, 10) === itemId) {
return;
}
if (isChecked) {
option.disabled = true;
option.dataset[`disabled${itemId}`] = 1;
return;
}
delete option.dataset[`disabled${itemId}`];
if (Object.keys(option.dataset).filter(key => key.startsWith('disabled')).length === 0) {
option.disabled = false;
}
});
// disable/enable the same item that might exist in other fieldsets - it can't be equip twice
itemSlots
.filter(slot => slot !== currentSlotId)
.forEach(slot => {
Array.from(document.querySelectorAll(`.loadout-manager__form fieldset[data-slot-id="${slot}"] input[type="checkbox"]`)).forEach(option => {
// don't act on anything which isn't the same item in another slot
if (parseInt(option.dataset.itemId, 10) !== itemId) {
return;
}
if (isChecked) {
option.disabled = true;
option.dataset[`disabled${itemId}`] = 1;
return;
}
delete option.dataset[`disabled${itemId}`];
if (Object.keys(option.dataset).filter(key => key.startsWith('disabled')).length === 0) {
option.disabled = false;
}
});
});
// If the conjunctive is AND, uncheck and disable all checkboxes from this slot, and other slots in the AND
if (itemConjunction === 'AND') {
itemSlots
.forEach(slot => {
Array.from(document.querySelectorAll(`.loadout-manager__form fieldset[data-slot-id="${slot}"] input[type="checkbox"]`)).forEach(option => {
// don't act on the checkbox we just ticked
if (parseInt(option.dataset.itemId, 10) === itemId && slot === currentSlotId) {
return;
}
if (isChecked) {
option.checked = false;
option.disabled = true;
// remove all disabled entries so we can start again
Object.keys(option.dataset).filter(key => key.startsWith('disabled')).forEach(key => delete option.dataset[key]);
option.dataset[`disabled${itemId}`] = 1;
return;
}
delete option.dataset[`disabled${itemId}`];
if (Object.keys(option.dataset).filter(key => key.startsWith('disabled')).length === 0) {
option.disabled = false;
}
});
});
}
}
/**
* Handler for when you hover over a checkbox in the form.
* This highlights the item in your inventory, or on your avatars loadout, for a better user experience
*/
function handleLabelHover(event, isHover) {
const itemId = parseInt(event.target.dataset.itemId || event.target.querySelector('input[type="checkbox"]').dataset.itemId, 10);
const itemNode = document.querySelector(`.item[data-id="${itemId}"]`);
isHover ? itemNode.classList.add('highlight') : itemNode.classList.remove('highlight');
}
// --------------------------------------------------------------------------------------------- UI
/**
* All the custom styling that powers the loadout. BEM FTW.
*/
function createStyleTag() {
const css = `.loadout-manager {
position: absolute;
top: 320px;
right: 0;
min-width: 250px;
padding: 10px;
background-color: rgba(0, 0, 0, 0.7);
transform: translateX(100%);
}
.loadout-manager.loadout-manager--opened {
transform: translateX(0);
}
.loadout-manager__arrow {
display: inline-block;
position: absolute;
left: -25px;
top: 0;
padding: 10px;
background-color: rgba(0, 0, 0, 0.85);
cursor: pointer;
font-size: 16px;
font-weight: bold;
}
.loadout-manager__select {
display: block;
margin: 15px 0;
padding: 5px;
height: auto;
}
.loadout-manager__form {
overflow-y: scroll;
max-height: 700px;
}
.loadout-manager__form input[type='text'] {
display: block;
}
.loadout-manager__form .loadout-manager__buttons input:first-child {
margin-left: 0;
}
.loadout-manager__form .loadout-manager__buttons input {
cursor: pointer;
}
.loadout-manager__form .loadout-manager__buttons input:disabled {
background-color: rgba(53.0, 43.0, 7.0, 0.5);
cursor: not-allowed;
}
.loadout-manager__form fieldset {
margin-bottom: 5px;
}
.loadout-manager__form label {
display: block;
margin-top: 2px;
}
.loadout-manager__form input[type='checkbox'] {
margin: 0 5px;
}
.loadout-manager__loading {
display: none;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.7);
align-items: center;
justify-content: center;
padding: 15px;
text-align: center;
}
.loadout-manager__loading.loadout-manager__loading--showing {
display: flex;
}
@keyframes marquee {
0% { transform: translate(0, 0); }
50% { transform: translate(calc(50px - 100%), 0); }
100% { transform: translate(0, 0); }
}
.loadout-quick {
position: absolute;
right: 0;
top: 350px;
width: 240px;
padding: 15px;
display: flex;
flex-direction: row-reverse;
flex-wrap: wrap;
}
.loadout-manager--opened + .loadout-quick {
top: 410px;
}
.loadout-quick__item {
display: block;
position: relative;
overflow: hidden;
padding: 10px;
width: 70px;
height: 50px;
margin: 5px;
border-radius: 10px;
cursor: pointer;
}
.loadout-quick__item::after {
display: block;
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
content: '';
overflow: hidden;
background: linear-gradient(to right, var(--mediumBlue) 0%, transparent 15%, transparent 85%, var(--mediumBlue) 100%);
}
.loadout-quick__marquee {
display: inline-block;
white-space: nowrap;
min-width: 100%;
}
.loadout-quick__item:hover .loadout-quick__marquee {
animation: marquee 2s linear infinite;
}`;
const style = document.createElement('style');
style.type = 'text/css';
style.appendChild(document.createTextNode(css));
document.head.appendChild(style);
}
/**
* Create the Quick Equip Buttons
*/
function createQuickLoadout(selectedValue, loadoutManager) {
// remove any existing buttons
const existing = document.querySelector('.loadout-quick');
existing && existing.remove();
// only create these buttons when nothing is selected.
if (selectedValue !== '-1' || SHOW_QUICK_BUTTONS === false) {
return;
}
// create all the options from the saved values
const loadouts = getLoadouts();
const fragment = document.createDocumentFragment();
Object.keys(loadouts).forEach(loadout => {
const btn = document.createElement('button');
btn.classList.add('loadout-quick__item');
btn.innerHTML = `<span class='loadout-quick__marquee'>${loadout}</span>`;
// when we click a button, create the form, equip whats in the form, and then destroy the form. Winner!
btn.onclick = () => {
createLoadoutDropdown(loadoutManager, loadout);
handleEquip();
createLoadoutDropdown(loadoutManager);
}
fragment.appendChild(btn);
});
const loadoutQuick = document.createElement('div');
loadoutQuick.classList.add('loadout-quick');
loadoutQuick.appendChild(fragment);
const wrapper = document.getElementById('wrapper');
wrapper.appendChild(loadoutQuick);
}
/**
* This creates a loadout form and prepopulates the checkboxes from a saved loadout
*/
function createLoadoutForm(selectId, loadoutManager) {
// remove an existing form
const existingForm = loadoutManager.querySelector('.loadout-manager__form');
existingForm && existingForm.remove();
// create the quick loadout buttons
createQuickLoadout(selectId, loadoutManager);
// "select an option"
if (selectId === '-1') {
return;
}
// find data on loadout that has been selected
const loadouts = getLoadouts();
const loadout = loadouts[selectId] || {};
// turn all items into fieldsets containing checkboxes
const items = getItems();
const fieldsets = Object
.keys(items)
.sort((a, b) => getSlotId(a) > getSlotId(b) ? 1 : -1)
.map(sectionName => {
const sectionItems = items[sectionName];
const fragment = document.createDocumentFragment();
sectionItems.forEach(item => {
const section = `loadout-items-${sectionName.toLowerCase().replace(/\s/, '')}`;
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.name = section;
checkbox.value = item.id;
checkbox.dataset.itemId = item.id;
checkbox.dataset.conjunction = item.conjunction;
checkbox.onchange = (event) => handleItemSelect(event.target, items);
if (loadout[section] === item.id) {
delete loadout[section];
checkbox.checked = true;
}
const text = document.createTextNode(item.name);
const label = document.createElement('label');
label.onmouseover = (event) => handleLabelHover(event, true);
label.onmouseout = (event) => handleLabelHover(event, false);
label.appendChild(checkbox);
label.appendChild(text);
fragment.appendChild(label);
});
const fieldset = document.createElement('fieldset');
fieldset.dataset.slotId = getSlotId(sectionName);
const legend = document.createElement('legend');
legend.innerHTML = sectionName;
fieldset.appendChild(legend);
fieldset.appendChild(fragment);
return fieldset;
});
// create the form
const form = document.createElement('form');
form.classList.add('loadout-manager__form');
form.action = '#!loadout';
form.method = 'get';
form.onsubmit = (e) => {
e.preventDefault();
return false;
}
// loadout name input
const input = document.createElement('input');
input.type = 'text';
input.name = 'loadout-name';
input.placeholder = 'Loadout Name';
if (selectId !== '-1' && selectId !== '0') input.value = selectId;
// buttons
const equip = document.createElement('input');
equip.type = 'button';
equip.value = 'Equip Items';
if (selectId === '-1' || selectId === '0') equip.disabled = true;
equip.onclick = () => handleEquip();
const save = document.createElement('input');
save.type = 'button';
save.value = 'Save';
save.onclick = (event) => handleSave(event, loadoutManager);
const remove = document.createElement('input');
remove.type = 'button';
remove.value = 'Delete';
if (selectId === '-1' || selectId === '0') remove.disabled = true;
remove.onclick = () => handleRemove(selectId, loadoutManager);
const buttonContainer = document.createElement('div');
buttonContainer.classList.add('loadout-manager__buttons');
buttonContainer.appendChild(equip);
buttonContainer.appendChild(save);
buttonContainer.appendChild(remove);
// construct loadout manager form
form.appendChild(input);
form.appendChild(buttonContainer);
fieldsets.forEach(fieldset => form.appendChild(fieldset));
// append to the loadout manager div
loadoutManager.appendChild(form);
// check if we need to disable any options, that are not NOT conjunctions
Array.from(document.querySelectorAll(`.loadout-manager__form input[type="checkbox"]:not([data-conjunction="AND"])`)).forEach(option => {
handleItemSelect(option, items);
});
// now run through the AND conjunctions and disable any options that shouldn't be available
Array.from(document.querySelectorAll(`.loadout-manager__form input[type="checkbox"][data-conjunction="AND"]:checked`)).forEach(option => {
handleItemSelect(option, items);
});
// verify all parts of the loadout were used. We've been deleting from this object as keys have been used up, so any left means that items are missing
const missingKeys = Object.keys(loadout);
if (missingKeys.length > 0) {
alert(`This loadout contains items which are now missing from your inventory for the following:
${missingKeys.map(missing => ` - ${missing}`).join("\r")}
Please verify the loadout and resave it to remove this message`);
}
}
/**
* This creates the loadout dropdown with the saved loadouts in it
*/
function createLoadoutDropdown(loadoutManager, selectedValue = '-1') {
// remove any existing dropdown
const existing = document.querySelector('.loadout-manager__select');
existing && existing.remove();
// create all the options from the saved values
const loadouts = getLoadouts();
const fragment = document.createDocumentFragment();
Object.keys(loadouts).forEach(loadout => {
const option = document.createElement('option');
option.value = loadout;
option.innerText = loadout;
if (loadout === selectedValue) option.selected = true;
fragment.appendChild(option);
});
// create the select and add some default options
const loadoutManagerSelect = document.createElement('select');
loadoutManagerSelect.classList.add('loadout-manager__select');
loadoutManagerSelect.innerHTML = `
<option value="-1"> -- Select an option</option>
<option value="0"> -- Create new loadout</option>
`;
loadoutManagerSelect.onchange = (event) => {
const selectId = event.target.value;
createLoadoutForm(selectId, loadoutManager);
};
loadoutManagerSelect.appendChild(fragment);
// append to the loadout manager itself
loadoutManager.appendChild(loadoutManagerSelect);
// create the loadout form
createLoadoutForm(selectedValue, loadoutManager);
}
/**
* This creates the main loadout manager
*/
function createLoadoutManager() {
const wrapper = document.getElementById('wrapper');
// create the loadout manager wrapper
const loadoutManager = document.createElement('div');
loadoutManager.classList.add('loadout-manager');
if (DEFAULT_OPENED) loadoutManager.classList.add('loadout-manager--opened');
loadoutManager.innerHTML = `<strong>Equipment Loadout Manager</strong>`;
// arrow to expand and collapse the manager
const loadoutManagerArrow = document.createElement('div');
loadoutManagerArrow.classList.add('loadout-manager__arrow');
loadoutManagerArrow.innerHTML = `‹`;
loadoutManagerArrow.onclick = () => {
loadoutManager.classList.toggle('loadout-manager--opened');
}
// create the loading overload. The handleEquip function will populate the innerHTML
const loadoutManagerLoading = document.createElement('div');
loadoutManagerLoading.classList.add('loadout-manager__loading');
// DOM manipulation
loadoutManager.appendChild(loadoutManagerArrow);
loadoutManager.appendChild(loadoutManagerLoading);
wrapper.appendChild(loadoutManager);
// this should always be loaded last in the loadoutManager
createLoadoutDropdown(loadoutManager);
}
// --------------------------------------------------------------------------------------------- The main function runner
(function () {
'use strict';
createStyleTag();
createLoadoutManager();
})();