MediaWiki:SCalScript.js

From Istaria Lexica

Revision as of 22:41, 7 July 2022 by Maintenance script (talk | contribs) (Big script import)

Note: After saving, you may have to bypass your browser's cache to see the changes.

  • Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
  • Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
  • Internet Explorer / Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5
  • Opera: Go to Menu → Settings (Opera → Preferences on a Mac) and then to Privacy & security → Clear browsing data → Cached images and files.
/**
 * Skeleton Key Calculator
 * Elteria Shadowhand
 */
let SCalStateCheck = setInterval(() => {
  'use strict';
  console.log('ready state:' + document.readyState);
  if(document.readyState === 'complete') {
    clearInterval(SCalStateCheck);
    let mainDivId = 'SCalMain'; // ID of the main content box. Can be a <div> or whatever.
    if (document.getElementById(mainDivId)) {
      let version = '1.4'; // This script's version
      let baseResources; // Collection of the needed base resources. To be filled.
      let refinedResources; // Collection of the needed refined resources. To be filled.
      let productsDetails; // Detailed dynamic information for each product. To be filled.
      let currentProductIdCount; // Dirty little helper for adding dynamic IDs to productsDetails
  
      // defaults
      let maxKeyCount = 5000; // Skeleton Key counter maximum value
      let maxSkillCount = 2000; // Skill input fields maximum value
      let cookieParameters = '; path=/; SameSite=Strict';
      let jQueryUrlStandalone = 'jquery-3.6.0.js'; // url to the standalone jquery if mediawiki isn't present
      let cssUrlStandalone = 'styles.css'; // url to the stylesheet if mediawiki isn't present
      let cssFileMediaWiki = ' MediaWiki:SCalScript.css'; // name of the stylesheet article in mediawiki
      let hasMW; // Indicator if mediawiki is present
      let wikiUrl = 'https://www.istaria-lexica.de'; // used to link to the resource and product pages on a wiki
      let headline = 'The Calculator'; // Calculator's head line text
            
      // The dataset of resources
      // Note that the skill is NOT the actual skill you need to create the product! 
      // It's the skill you would need to create its parent product. E.g. if you created a Spell Shard which originally 
      // needs Spellcraft, the subcomponent (i.e. a Stone Brick, which would be Stoneworking) of it would be Spellcraft.
      // The min and max parameters behave the same.
      // the REAL needed skills are documented in the skillsOverview variable.
      let products = {
        'Skeleton Key': { skill: 'Tinkering', minCount: 1, maxCount: 1, minSkill: 1100, maxSkill: 1425, type: 'SCalProduct', subs:
          {
            'Enchanted Adamantium-Mithril Bar': { skill: 'Tinkering', minCount: 3, maxCount: 6, minSkill: 1100, maxSkill: 1425, type: 'SCalProduct', subs:
              {
                'Gozar\'s Blessing': { skill: 'Tinkering', minCount: 2, maxCount: 4, minSkill: 1100, maxSkill: 1425, type: 'SCalProduct', subs:
                  {
                    'Meltanis\' Prayer': { skill: 'Scribing', minCount: 1, maxCount: 3, minSkill: 1100, maxSkill: 1425, type: 'SCalProduct', subs:
                      {
                        'Travertine Spell Shard': { skill: 'Spellcraft', minCount: 1, maxCount: 5, minSkill: 1100, maxSkill: 1425, type: 'SCalRefinedResource', subs:
                          {
                            'Travertine Brick': { skill: 'Stoneworking', minCount: 2, maxCount: 5, minSkill: 1000, maxSkill: 1325, type: 'SCalRefinedResource', subs:
                              {
                                'Travertine Slab': { skill: 'Stoneworking', minCount: 2, maxCount: 5, minSkill: 1000, maxSkill: 1325 }
                              }
                            }
                          }
                        }, 'Radiant Essence Orb': { skill: 'Spellcraft', minCount: 4, maxCount: 10, minSkill: 1100, maxSkill: 1425, type: 'SCalRefinedResource', subs:
                          {
                            'Radiant Essence': { skill: 'Essence Shaping', minCount: 2, maxCount: 5, minSkill: 1000, maxSkill: 1325 }
                          }
                        }
                      }
                    }, 'Primal Burst III': { skill: 'Spellcraft', minCount: 1, maxCount: 3,minSkill: 930, maxSkill: 1130, type: 'SCalProduct', subs:
                      {
                        'Marble Spell Shard': { skill: 'Spellcraft', minCount: 5, maxCount: 5, minSkill: 930, maxSkill: 1130, type: 'SCalRefinedResource', subs:
                          {
                            'Marble Brick': { skill: 'Spellcraft', minCount: 2, maxCount: 5, minSkill: 800, maxSkill: 1100, type: 'SCalRefinedResource', subs: 
                              {
                                'Marble Slab': { skill: 'Stoneworking', minCount: 2, maxCount: 5, minSkill: 800, maxSkill: 1100 }
                              }
                            }
                          }
                        }, 'Shining Essence Orb': { skill: 'Spellcraft', minCount: 9, maxCount: 18, minSkill: 930, maxSkill: 1130, type: 'SCalRefinedResource', subs:
                          {
                            'Shining Essence': { skill: 'Essence Shaping', minCount: 2, maxCount: 5, minSkill: 800, maxSkill: 1100 }
                          }
                        }
                      }
                    }, 'Energy Strike V': { skill: 'Spellcraft', minCount: 1, maxCount: 3, minSkill: 850, maxSkill: 1050, type: 'SCalProduct', subs:
                      {
                        'Marble Spell Shard': { skill: 'Spellcraft', minCount: 4, maxCount: 4, minSkill: 850, maxSkill: 1050, type: 'SCalRefinedResource', subs:
                          {
                            'Marble Brick': { skill: 'Spellcraft', minCount: 2, maxCount: 5, minSkill: 800, maxSkill: 1100, type: 'SCalRefinedResource', subs:
                              {
                                'Marble Slab': { skill: 'Stoneworking', minCount: 2, maxCount: 5, minSkill: 800, maxSkill: 1100 }
                              }
                            }
                          }
                        }, 'Shining Essence Orb': { skill: 'Spellcraft', minCount: 7, maxCount: 14, minSkill: 800, maxSkill: 1100, type: 'SCalRefinedResource', subs:
                          {
                            'Shining Essence': { skill: 'Essence Shaping', minCount: 2, maxCount: 5, minSkill: 800, maxSkill: 1100 }
                          }
                        }
                      }
                    }, 'Ice Bomb V': { skill: 'Spellcraft', minCount: 1, maxCount: 3, minSkill: 920, maxSkill: 1120, type: 'SCalProduct', subs:
                      {
                        'Marble Spell Shard': { skill: 'Spellcraft', minCount: 5, maxCount: 5, minSkill: 800, maxSkill: 1100, type: 'SCalRefinedResource', subs:
                          {
                            'Marble Brick': { skill: 'Stoneworking', minCount: 2, maxCount: 5, minSkill: 800, maxSkill: 1100, type: 'SCalRefinedResource', subs:
                              {
                                'Marble Slab': { skill: 'Stoneworking', minCount: 2, maxCount: 5, minSkill: 800, maxSkill: 1100 }
                              }
                            }
                          }
                        }, 'Shining Essence Orb': { skill: 'Spellcraft', minCount: 9, maxCount: 18, minSkill: 920, maxSkill: 1120, type: 'SCalRefinedResource', subs:
                          {
                            'Shining Essence': { skill: 'Essence Shaping', minCount: 2, maxCount: 5, minSkill: 800, maxSkill: 1100 }
                          }
                        }
                      }
                    }, 'Fiery Strike V': { skill: 'Spellcraft', minCount: 1, maxCount: 3, minSkill: 980, maxSkill: 1180, type: 'SCalProduct', subs:
                      {
                        'Marble Spell Shard': { skill: 'Spellcraft', minCount: 6, maxCount: 6, minSkill: 800, maxSkill: 1100, type: 'SCalRefinedResource', subs:
                          {
                            'Marble Brick': { skill: 'Stoneworking', minCount: 2, maxCount: 5, minSkill: 800, maxSkill: 1100, type: 'SCalRefinedResource', subs:
                              {
                                'Marble Slab': { skill: 'Stoneworking', minCount: 2, maxCount: 5, minSkill: 800, maxSkill: 1100 }
                              }
                            }
                          }
                        }, 'Shining Essence Orb': { skill: 'Spellcraft', minCount: 11, maxCount: 22, minSkill: 980, maxSkill: 1180, type: 'SCalRefinedResource', subs:
                          {
                            'Shining Essence': { skill: 'Essence Shaping', minCount: 2, maxCount: 5, minSkill: 800, maxSkill: 1100 }
                          }
                        }
                      }
                    }, 'Gold Papyrus Sheet': { skill: 'Scribing', minCount: 2, maxCount: 5, minSkill: 1100, maxSkill: 1425, type: 'SCalRefinedResource', subs:
                      {
                        'Gold Papyrus Stem': { skill: 'Papermaking', minCount: 2, maxCount: 5, minSkill: 1000, maxSkill: 1325 }
                      }
                    }
                  }
                }, 'Hardened Adamantium-Mithril Bar': { skill: 'Tinkering', minCount: 2, maxCount: 5, minSkill: 1100, maxSkill: 1425, type: 'SCalRefinedResource', subs:
                  {
                    'Solution of Majorita': { skill: 'Tinkering', minCount: 2, maxCount: 5, minSkill: 1100, maxSkill: 1425, type: 'SCalRefinedResource', subs:
                      {
                        'Thornwood Bowl': { skill: 'Alchemy', minCount: 1, maxCount: 3, minSkill: 1100, maxSkill: 1425, type: 'SCalRefinedResource', subs:
                          {
                            'Thornwood Sap': { skill: 'Fletching', minCount: 3, maxCount: 6, minSkill: 1100, maxSkill: 1425 } ,
                            'Thornwood Board': { skill: 'Fletching', minCount: 5, maxCount: 10, minSkill: 1100, maxSkill: 1425, type: 'SCalRefinedResource', subs:
                              {
                                'Thornwood Log': { skill: 'Lumbering', minCount: 2, maxCount: 5, minSkill: 1000, maxSkill: 1325 }
                              }
                            }
                          }
                        }, 'Crystallized Travertine Brick': { skill: 'Alchemy', minCount: 5, maxCount: 10, minSkill: 1000, maxSkill: 1325, type: 'SCalRefinedResource', subs:
                          {
                            'Unfocused Violet Azulyte Crystal': { skill: 'Stoneworking', minCount: 1, maxCount: 5, minSkill: 1000, maxSkill: 1325 } ,
                            'Travertine Slab': { skill: 'Stoneworking', minCount: 1, maxCount: 5, minSkill: 1000, maxSkill: 1325 }
                          }
                        }, 'Purified Radiant Essence Orb': { skill: 'Alchemy', minCount: 1, maxCount: 4, minSkill: 1100, maxSkill: 1425, type: 'SCalRefinedResource', subs:
                          {
                            'Radiant Essence Orb': { skill: 'Alchemy', minCount: 1, maxCount: 4, minSkill: 1100, maxSkill: 1325, type: 'SCalRefinedResource', subs:
                              {
                                'Radiant Essence': { skill: 'Essence Shaping', minCount: 2, maxCount: 5, minSkill: 1000, maxSkill: 1325 }
                              }
                            }
                          }
                        }, 'Water': { skill: 'Alchemy', minCount: 15, maxCount: 30, minSkill: 1100, maxSkill: 1425 }
                      }
                    }, 'Adamantium-Mithril Bar': { skill: 'Tinkering', minCount: 1, maxCount: 1, minSkill: 1100, maxSkill: 1425, type: 'SCalRefinedResource', subs:
                      {
                        'Mithril Ore': { skill: 'Smelting', minCount: 3, maxCount: 6, minSkill: 1100, maxSkill: 1425 } ,
                        'Adamantium Ore': { skill: 'Smelting', minCount: 3, maxCount: 6, minSkill: 1100, maxSkill: 1425 }
                      }
                    }
                  }
                }
              }
            }, 'Skeleton Key Mold': { skill: 'Tinkering', minCount: 1, maxCount: 1, minSkill: 1100, maxSkill: 1425, type: 'SCalProduct', subs:
              {
                'Skeleton Key Pattern': { skill: 'Earthencraft', minCount: 1, maxCount: 1, minSkill: 1100, maxSkill: 1425 },
                'Porcelain Clay Chunk': { skill: 'Earthencraft', minCount: 2, maxCount: 5, minSkill: 1100, maxSkill: 1425 }
              }
            }
          }
        }
      };
  
      // A list of the skills and their limits you need to be able to craft everything
      let skillsOverview = {
        'Tinkering': { min: 1100, max: 1425 },
        'Scribing': { min: 1100, max: 1425 },
        'Spellcraft': { min: 1100, max: 1425 },
        'Essence Shaping': { min: 1000, max: 1325 },
        'Papermaking': { min: 1000, max: 1325 },
        'Alchemy': { min: 1100, max: 1425 },
        'Fletching': { min: 1100, max: 1425 },
        'Enchanting': { min: 1100, max: 1425 },
        'Stoneworking': { min: 1000, max: 1325 },
        'Lumbering': { min: 1000, max: 1325 },
        'Smelting': { min: 1100, max: 1425 },
        'Earthencraft': { min: 1100, max: 1425 }
      };
  
      ///////////////////////////////////////////////////////////////////
      //FUNCTIONS:
      var printKeyCounter = function printKeyCounter() {
        $('#' + mainDivId).append('<center><label for="SCalCountInput">How many Skeleton Keys would you want to create?</label></center>');
        $('#' + mainDivId).append('<center><input type="number" id="SCalCountInput" name="SCalCountInput" value="1"></center>');
        $('#SCalCountInput').change(function(event) {
          var value = event.target.value; // check for a correct entry
          if(!isRealInteger(value)) value = 1;
          if(value <= 0) value = 1;
          if(value > maxKeyCount) value = maxKeyCount;
          event.target.value = value;
  
          setCookieByInteger(event.target.id, value);
          updateLists();
        });
      };
      
      var printHeader = function printHeader() {
        var setSkillCookie = function(event) {
              setCookieByInteger(event.target.id, event.target.value);
              updateLists();
          };
        $('#' + mainDivId).append(`<h1 id="SCalH1">${headline}</h1>`);
        $('#' + mainDivId).append(`<center style="font-size: 0.8em; margin-bottom:30px">(v ${version})</center>`);
        $('#' + mainDivId).append('<center>Please enter your current skills in the fields below. Defaults to optimal skills.</center>');
  
        $('#' + mainDivId).append('<center><input type="button" class="SCal" id="SCalMaxSkillsBtn" value="Reset to optimal skills"></center>');
        $('#SCalMaxSkillsBtn').click(function() {
          if(confirm('This will reset all of the skills to optimal! Are you really sure? ')) {
            for (var v of Object.values(skillsOverview)) {
              $('#' + v.id).val(v.max);
              $('#' + v.id).change();
            }
          }
        });
  
        $('#' + mainDivId).append('<center><div id="SCalSkills" style="column-count:3; width:fit-content; text-align:left"></div></center>');
        for (var [skillName, values] of Object.entries(skillsOverview).sort()) {
          var id = values.id;
          var skillLevel = getCookie(id);
                  
          if(!isRealInteger(skillLevel)) skillLevel = values.max; // default to optimal value if cookie not correctly set
    
          $('#SCalSkills').append(`<input type="number" id="${id}" name="${id}" value="${skillLevel}"><label for="${id}">&nbsp;&nbsp;${skillName}</label><br />`);
          
          $('#' + id).change(setSkillCookie);
          $('#' + id).change();
        }
        
        $('#' + mainDivId).append('<br /><center>A <font style="color: red"><b>red</b></font> box means there is an error.</center>');
        $('#' + mainDivId).append('<center>A <font style="color: green"><b>green</b></font> box means you\'re optimal for creating the items.</center>');
        $('#' + mainDivId).append('<div id="SCalErrorText"></div>');
        $('#' + mainDivId).append('<br />');
        $('#' + mainDivId).append('<hr />');
      };
  
      var printExportLink = function printExportLink(text, buttontext, id, mainId, mimetype, filename) {
        $(`#${id}`).remove();
        $(`#${mainId}`).append(`<a id="${id}A"></a>`);
        $(`#${id}A`).append(`<input class="SCal" id="${id}" name="${id}" type="button" value="${buttontext}">`);
        $(`#${id}`).click(function() {
          var url = `data:${mimetype};charset=utf-8,${encodeURIComponent(text)}`;
          $(`#${id}A`).attr('href', url);
          $(`#${id}A`).attr('download', filename);
        });
      };
  
      // Creates a simple product-tree object, containing the amount of resources needed
      var recursiveCreateProductTree = function recursiveCreateProductTree(products) {
        var treeProducts = {};
        for(var [product, values] of Object.entries(products)) {
          treeProducts[product] = { needed: values.needed }; 
          
          if(values.hasOwnProperty('subs')) treeProducts[product].subs = recursiveCreateProductTree(values.subs);
        }
        return treeProducts;
      };
  
      var updateExportEverythingLinks = function updateExportEverythingLinks() {
        var JSONeverything = JSON.stringify({
          'Product Tree': recursiveCreateProductTree(productsDetails),
          'Base Resources': baseResources,
          'Refined Resources': refinedResources,
          'Skills': generateSkillsAsJSON()
        });
        printExportLink(JSONeverything, 'Export everything as JSON', 'SCalExportEverythingAsJSON', 'SCalExportEverything', 'application/json', 'Skeleton Key - All.json');
        
        var TEXTeverything = '--- Tree of needed Resources ---\n\n';
        TEXTeverything += recursiveGenerateTreeAsTEXT(productsDetails, 0);
        TEXTeverything += '\n\n';
        TEXTeverything += '--- Needed Base Resources ---\n\n';
        TEXTeverything += generateResourcesAsTEXT(baseResources);
        TEXTeverything += '\n\n';
        TEXTeverything += '--- Needed Refined Resources ---\n\n';
        TEXTeverything += generateResourcesAsTEXT(refinedResources);
        TEXTeverything += '\n\n';
        TEXTeverything += '--- Needed Skills ---\n\n';
        TEXTeverything += generateSkillsAsTEXT();
        printExportLink(TEXTeverything, 'Export everything as TEXT', 'SCalExportEverythingAsEXT', 'SCalExportEverything', 'text/plain', 'Skeleton Key - All.txt');
  
        var XMLeverything = '<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>\n';
        XMLeverything += '<Resources>\n';
        XMLeverything += '  <Tree>\n';
        XMLeverything += recursiveGenerateTreeAsXML(productsDetails, 4);
        XMLeverything += '  </Tree>\n';
        XMLeverything += generateResourcesAsXML(baseResources, 'BaseResources', 2);
        XMLeverything += generateResourcesAsXML(refinedResources, 'RefinedResources', 2);
        XMLeverything += generateSkillsAsXML(2);
        XMLeverything += '</Resources>\n';
        printExportLink(XMLeverything, 'Export everything as XML', 'SCalExportEverythingAsXML', 'SCalExportEverything', 'application/xml', 'Skeleton Key - All.xml');
      };
  
      var generateSkillsAsJSON = function generateSkillsAsJSON() {
        var json = {};
        for(var [skill, values] of Object.entries(skillsOverview).sort()) {
          json[skill] = {
            minimal: values.min,
            optimal: values.max
          };
        }
        return json;
      };
      
      var recursiveGenerateTreeAsTEXT = function recursiveGenerateTreeAsTEXT(productsDetails, indent) {
        var text = '';
        for(var [product, values] of Object.entries(productsDetails)) {
          for(var i=0; i<indent; i++) {
            text += ' ';
          }
          
          text += '|- ';
          text += product + ' ';
          text += values.needed;
          text += '\n';
  
          if(values.hasOwnProperty('subs')) {
            text += recursiveGenerateTreeAsTEXT(values.subs, indent + 3);
          }
        }
        return text;
      };
  
      var recursiveGenerateTreeAsXML = function recursiveGenerateTreeAsXML(productsDetails, indent) {
        var xml = '';
        for(var [product, value] of Object.entries(productsDetails)) {
          for(var i=0; i < indent; i++) {
            xml += ' ';
          }
          
          xml += `<item name="${product}" needed="${value.needed}">\n`;
  
          if(value.hasOwnProperty('subs')) xml += recursiveGenerateTreeAsXML(value.subs, indent + 2);
  
          for(var j=0; j < indent; j++) {
            xml += ' ';
          }
          
          xml +='</item>\n';
        }
        return xml;
      };
  
      var determineNeededDigitlengthResources = function determineNeededDigitlengthResources(resources) {
        var length = 0;
        for(var count of Object.values(resources)) {
          if(count.toString().length > length) length = count.toString().length;
        }
        return length;
      };
  
      var determineNeededDigitlengthSkills = function determineNeededDigitlengthSkills() {
        var length = [9, 10];
        for(var skill of Object.keys(skillsOverview)) {
          if(skillsOverview[skill].min.toString().length > length[0]) length[0] = skillsOverview[skill].min.toString().length;
          if(skillsOverview[skill].max.toString().length > length[1]) length[1] = skillsOverview[skill].max.toString().length;
        }
        return length;
      };
  
      // calculates the effective needed amount of a resource
      var calculateNeededResource = function calculateNeededResource(minSkill, maxSkill, minCount, maxCount, skill) {
          var currentSkill = $('#' + skillsOverview[skill].id).val();
          
          var percent = (currentSkill - minSkill) / (maxSkill - minSkill);
          var needEffective = maxCount - ((maxCount - minCount) * percent);
          needEffective = Math.ceil(needEffective);
    
          if (needEffective < minCount) needEffective = minCount;
          if (needEffective > maxCount) needEffective = maxCount;
  
          return needEffective;
      };
      
      var recursiveCleanHaveCookies = function recursiveCleanHaveCookies(details) {
        for(var values of Object.values(details)) {
          var id = values.id + 'Have';
          deleteCookie(id);
          
          if(values.hasOwnProperty('subs')) recursiveCleanHaveCookies(values.subs);
        }
      };
      
      var updateTree = function updateTree() {
        printExportLink(JSON.stringify(recursiveCreateProductTree(productsDetails)), 'Export as JSON', 'SCalExportTreeAsJSON', 'SCalResourcesExportLinks', 'application/json', 'Skeleton Key - Tree.json');
        printExportLink(recursiveGenerateTreeAsTEXT(productsDetails, 0), 'Export as TEXT', 'SCalExportTreeAsTEXT', 'SCalResourcesExportLinks', 'text/plain', 'Skeleton Key - Tree.txt');
        var treeAsXML = '<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>\n' + recursiveGenerateTreeAsXML(productsDetails, 0);
        printExportLink(treeAsXML, 'Export as XML', 'SCalExportTreeAsXML', 'SCalResourcesExportLinks', 'application/xml', 'Skeleton Key - Tree.xml');
  
        $('#SCalResetTree').remove();
        $('#SCalResourcesExportLinks').append('<input class="SCal" type="button" id="SCalResetTree" value="Reset all have-fields">');
        $('#SCalResetTree').click(function() {
          if(confirm('This will reset all have-fields to 0. Are you really sure?')) {
            recursiveCleanHaveCookies(productsDetails);
            updateLists();
          }
        });
  
        recursiveUpdateTreeProduct(productsDetails);
      };
      
      var printTree = function printTree() {
        $('#SCalResourcescol1').append('<h2>Tree of needed products</h2>');
        $('#SCalResourcescol1').append('<div id="SCalResourcesExportLinks"></div>');
        
        $('#SCalResourcescol1').append(recursivePrintTreeProduct(productsDetails));
        recursiveAddHaveChangeEvents(productsDetails);
      };
  
      var recursiveAddHaveChangeEvents = function recursiveAddHaveChangeEvents(products) {
        var setHaveCookie = function(event) {
            var have = event.target.value;
            if(!isRealInteger(have)) have = 0;
            if(have < 0) have = 0;
            setCookieByInteger(event.target.id, have);
            updateLists();
          };
        for(var values of Object.values(products)) {
          $(`#${values.id}Have`).change(setHaveCookie);
          
          if(values.hasOwnProperty('subs')) recursiveAddHaveChangeEvents(values.subs);
        }
      };
      
      //Calulates the needed data for the lists and puts them into objects
      //for further processing
      var recursiveCalculateResourceData = function recursiveCalculateResourceData(products, multiplikator) {
        var details = {};
        for(var [product, values] of Object.entries(products)) {
          currentProductIdCount += 1;
          var id = 'SCalItem' + currentProductIdCount;
          var needed = calculateNeededResource(values.minSkill, values.maxSkill, values.minCount, values.maxCount, values.skill);
          var needEffective = needed * multiplikator;
          var have = getCookie(id + 'Have');
          if(!isRealInteger(have)) have = 0;
          if(have < 0) have = 0;
          if(have > 0) needEffective = needEffective - have;
          if(needEffective < 0) needEffective = 0;
          
          //set the input field ID, needed amount and resource type
          details[product] = { 
            id: id,
            needed: needEffective,
            type: values.type
          };
          
          // Add needed amount to base resources
          // we assume that a product without subs is a base resource
          if(!values.hasOwnProperty('subs')) { 
            if(baseResources.hasOwnProperty(product)) {
              baseResources[product] = details[product].needed + baseResources[product];
            } else {
              baseResources[product] = details[product].needed;
            }
          }
  
          // Add needed amount to to refined resources
          if(values.type == 'SCalRefinedResource') { 
            if(refinedResources.hasOwnProperty(product)) {
              refinedResources[product] = details[product].needed + refinedResources[product];
            } else {
              refinedResources[product] = details[product].needed;
            }
          }
          
          // recurse through the sub products
          if(values.hasOwnProperty('subs')) details[product].subs = recursiveCalculateResourceData(values.subs, details[product].needed);
        }
        return details;
      };
      
      // Fills the lists with data
      var updateLists = function updateLists() {
        if(validateSkillInputs()) {
          initialize();
          updateExportEverythingLinks();
          updateTree();
          updateResources(baseResources, 'BaseResources');
          updateResources(refinedResources, 'RefinedResources');
        }      
      };
      
      //reinitializes everything and calculates needed data
      var initialize = function initialize() {
        baseResources = {};
        refinedResources = {};
        productsDetails = {};
        currentProductIdCount = 0;
        
        var keysWanted = getCookie('SCalCountInput'); // set the Skeleton Key count
        var keysNeeded = keysWanted;
        if(!isRealInteger(keysWanted)) keysNeeded = 1;
        if(keysWanted < 0) keysNeeded = 1;
        if(keysWanted > maxKeyCount) keysNeeded = maxKeyCount;
        if(keysWanted != keysNeeded) setCookie('SCalCountInput', keysNeeded); // cookie seems to be set wrong, we reset.
        $('#SCalCountInput').val(keysNeeded); 
        
        productsDetails = recursiveCalculateResourceData(products, keysWanted);
      };
  
      //creates the containers for the lists
      var printLists = function printLists() {
        initialize();
  
        // Container for the 'Export Everything' features
        $('#' + mainDivId).append('<div id="SCalExportEverything" style="text-align: center"></div>');
        
        // Containers for the lists
        $('#' + mainDivId).append('<center><table>' +
          '  <tr valign="top">' +
          '    <td id="SCalResourcescol1"></td>' +
          '    <td id="SCalResourcescol2"></td>' +
          '  </tr>' +
          '</table></center>'
        );
  
        printTree();
        printResources(baseResources, 'Overview of Base Resources', 'BaseResources');
        printResources(refinedResources, 'Overview of Refined Resources', 'RefinedResources');
        printNeededSkills();
  
        if(hasMW) {
          mw.loader.using('jquery.tablesorter', function() { // add the tablesorter to tables
            $('table.sortable').tablesorter({ sortList: [{ 0: 'asc' }] });
          });
        }
      };
  
      var getURL = function getURL(url) {
        if(hasMW) return mw.util.getUrl(url);
        return wikiUrl + '/' + url;
      };
  
      var printNeededSkills = function printNeededSkills() {
        $('#SCalResourcescol2').append('<h2>Required skills</h2>');
  
        printExportLink(JSON.stringify(generateSkillsAsJSON()), 'Export as JSON', 'SCalExportSkillsAsJSON', 'SCalResourcescol2', 'application/json', 'Skeleton Key - Skills.json');
        printExportLink(generateSkillsAsTEXT(), 'Export as TEXT', 'SCalExportSkillsAsTEXT', 'SCalResourcescol2', 'text/plain', 'Skeleton Key - Skills.txt');
        var skillsAsXML = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n' + generateSkillsAsXML();
        printExportLink(skillsAsXML, 'Export as XML', 'SCalExportSkillsAsXML', 'SCalResourcescol2', 'application/xml', 'Skeleton Key - Skills.xml');
  
        var tablecontent = '<table class="wikitable sortable">' + 
          '  <thead>' +
          '    <tr>' +
          '      <th align="left">Skill</th>' +
          '      <th align="left">Minimal</th>' +
          '      <th align="left">Optimal</th>' +
          '    </tr>' + 
          '  </thead>' +
          '  <tbody>';
  
        for(var [skill, values] of Object.entries(skillsOverview).sort()) {
          tablecontent += '    <tr>' + 
            `      <td><a href="${getURL(skill)}">${skill}</a></td>` +
            `      <td>${values.min}</td>` +
            `      <td>${values.max}</td>` +
            '    </tr>';
        }
  
        tablecontent += '  </tbody>';
        tablecontent += '</table>';
        $('#SCalResourcescol2').append(tablecontent);
      };
      
      // Tests if the provided value has a valid integer and sets a cookie for it
      var setCookieByInteger = function setCookieByInteger(id, value) {
        if(isRealInteger(value)) setCookie(id, value);
      };
  
      var recursivePrintTreeProduct = function recursivePrintTreeProduct(products) {
        var html = '';
        for(var [product, values] of Object.entries(products)) {
          var neededId = values.id + 'Needed'; 
          var haveId = values.id + 'Have';
          
          html += '  <ul>';
          html += '    <li>';
  
          if(product != 'Skeleton Key') {
            html += `      <span class="${values.type}"><a href="${getURL(product)}">${product}</a> (<span id="${neededId}"></span>)&nbsp;&nbsp;</span>` + 
                    `      <label for="${values.id}Have">have:</label><input class="SCalTreeHave" id="${haveId}" name="${haveId}" type="number">`;
          } else {
            html += `      <span class="${values.type}"><a href="${getURL(product)}">${product}</a> (<span id="${neededId}"></span>)</span>`;
          }
          if(values.hasOwnProperty('subs')) {
            html += recursivePrintTreeProduct(values.subs);
          }
          html += '    </li>';
          html += '  </ul>';
        }
        return html;
      };
  
      var recursiveUpdateTreeProduct = function recursiveUpdateTreeProduct(products) {
        var greenClass = 'SCalGreenInput';
  
        for(var [product, values] of Object.entries(products)) {
          var haveId = values.id + 'Have';
          var neededId = values.id + 'Needed';
  
          if(product != 'Skeleton Key') {
            $('#' + haveId).removeClass(greenClass);
  
            var have = getCookie(haveId);
            if(!isRealInteger(have)) have = 0;
            if(have < 0) have = 0;
            if(have > 0) $('#' + haveId).addClass(greenClass);
            $('#' + neededId).html(values.needed);
            $('#' + haveId).val(have);
          } else {
            $('#' + neededId).html(values.needed);
          }
          if(values.hasOwnProperty('subs')) {
            recursiveUpdateTreeProduct(values.subs);
          }
        }
      };
  
      var printResources = function printResources(resources, headline, type) {
        var allDoneId = 'SCal' + type + 'AllDone';
        var updateCookie = function() {
          if($(this).prop('checked')) {
            setCookie($(this).get(0).id, true);
          } else {
            deleteCookie($(this).get(0).id);
          }
          updateLists();
        };
  
        $('#SCalResourcescol2').append(`<h2>${headline}</h2>`);
        $('#SCalResourcescol2').append(`<div id="SCal${type}ExportLinks"></div>`);
        
        var tablecontent = '<table class="wikitable sortable">' + 
          '  <thead>' +
          '    <tr>' +
          '       <th style="vertical-align: top">Resource</th>' +
          '       <th style="vertical-align: top">Need</th>' +
          `       <th style="vertical-align: top" class="unsortable"><span><input type="checkbox" id="${allDoneId}"></span></th>` +
          '    </tr>' +
          '  </thead>' + 
          '  <tbody>';
  
        for (var resource of Object.keys(resources).sort()) {
          var id = resource.replaceAll(' ','') + 'Need';
          var doneId = id + 'Done';
          
          tablecontent += '<tr>' +
            `      <td><a href="${getURL(resource)}">${resource}</a></td>` +
            '      <td id="' + id + '"></td>' +
            `      <td style="text-align: center"><input type="checkbox" id="${doneId}"></td>` +
            '    </tr>';
        }
  
        tablecontent += '  </tbody>';
        tablecontent += '</table>';
        $('#SCalResourcescol2').append(tablecontent);
        
        $('#' + allDoneId).click(function() { // handle the 'all done' checkbox
            if($(this).prop('checked')) {
              setCookie($(this).get(0).id, true);
              
              for (var resource of Object.keys(resources)) {
                var doneId = resource.replaceAll(' ','') + 'NeedDone';
                setCookie(doneId, true);
              }
            } else {
              deleteCookie($(this).get(0).id);
              
              for (var resource2 of Object.keys(resources)) {
                var doneId2 = resource2.replaceAll(' ','') + 'NeedDone';
                deleteCookie(doneId2);
              }
            }
            updateLists();
        });
        
        for (var res of Object.keys(resources)) { // handle each resource 'done' checkbox
          var doneId2 = '#' + res.replaceAll(' ','') + 'NeedDone';
          $(doneId2).change(updateCookie);
        }
      };
      
      var updateResources = function updateResources(resources, type) {
        var allDoneId = `SCal${type}AllDone`;
        
        printExportLink(JSON.stringify(Object.fromEntries(Object.entries(resources).sort())), 'Export as JSON', `SCalExport${type}resourcesAsJSON`, `SCal${type}ExportLinks`, 'application/json', `Skeleton Key - ${type}.json`);
        printExportLink(generateResourcesAsTEXT(resources), 'Export as TEXT', `SCalExport${type}resourcesAsTEXT`, `SCal${type}ExportLinks`, 'text/plain', `Skeleton Key - ${type}.txt`);
        var resourceXML = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n' + generateResourcesAsXML(resources, type, 0);
        printExportLink(resourceXML, 'Export as XML', `SCalExport${type}resourcesAsXML`, `SCal${type}ExportLinks`, 'application/xml', `Skeleton Key - ${type}.xml`);
        
        var allDoneChecked = getCookie(allDoneId); // handle the 'All done' checkbox
        if(allDoneChecked === 'true') {
          $('#' + allDoneId).prop('checked', true);
        } else {
          $('#' + allDoneId).prop('checked', false);
        }
        
        for (var [resource, count] of Object.entries(resources)) { // handle the 'done' checkboxes and set the count accordingly
          var id = resource.replaceAll(' ', '') + 'Need';
          var doneId = id + 'Done';
          var checked = getCookie(doneId);
          
          if(checked === 'true') {
            $('#' + doneId).prop('checked', true);
            $('#' + id).html(0);
          } else {
            $('#' + doneId).prop('checked', false);
            $('#' + id).html(count);
          }
        }
      };
      
      var generateResourcesAsTEXT = function generateResourcesAsTEXT(resources) {
        var text = '';
        var length = determineNeededDigitlengthResources(resources);
        for(var [resource, count] of Object.entries(resources).sort()) {
          var indent = '';
          for(var i = 0;i < length - count.toString().length; i++) {
            indent += ' ';
          }
          
          text += count + indent + '   ' + resource + '\n';
        }
        return text;
      };
      
      var generateSkillsAsXML = function generateSkillsAsXML() {
        
        var xml = '<Skills>\n';
        for(var [skill, values] of Object.entries(skillsOverview).sort()) {
          xml += `  <skill minimal="${values.min}" optimal="${values.max}">${skill}</skill>\n`;
        }
        xml += '</Skills>';
        return xml;
      };
      
      var generateSkillsAsTEXT = function generateSkillsAsTEXT() {
        var length = determineNeededDigitlengthSkills();
        var text = 'Minimal  Optimal   Skillname\n';
        text += '--------------------------------------------------------\n';
  
        for(var [skill, values] of Object.entries(skillsOverview).sort()) {
          var needIndent = '';
          var optIndent = '';
          for(var i = 0;i < length[0] - values.min.toString().length; i++) {
            needIndent += ' ';
          }
  
          for(var j = 0;j < length[1] - values.max.toString().length; j++) {
            optIndent += ' ';
          }
          
          text += values.min + needIndent + values.max + optIndent + skill + '\n';
        }
        return text;
      };
  
      var generateResourcesAsXML = function generateResourcesAsXML(resources, tag) {
        var xml = `<${tag}>\n`;
        
        for(var [resource, count] of Object.entries(resources).sort()) {
          xml += `  <item needed="${count}">${resource}</item>\n`;
        }
        xml += `</${tag}>\n`;
        
        return xml;
      };
  
      var isRealInteger = function isRealInteger(number) {
        return (!isNaN(number) && parseInt(Number(number)) == number && !isNaN(parseInt(number, 10)));
      };
      
      // check if the skill's input fields contain valid entries
      var validateSkillInputs = function validateSkillInputs() {
        var ready = true;
        $('#SCalErrorText').html('');
  
        for (var [skill, values] of Object.entries(skillsOverview)) {
          var inputObj = $('#' + values.id);
          var skillCount = inputObj.val();
          
          inputObj.removeClass('SCalErroreousInput');
          inputObj.removeClass('SCalOptimalInput');
          
          if (!isRealInteger(skillCount)) {
            $('#SCalErrorText').append(`<center>${skill}: this is not a number!</center>`);
            inputObj.addClass('SCalErroreousInput');
            ready = false;
          } else {
            if (skillCount < 1 || skillCount > maxSkillCount) { 
              $('#SCalErrorText').append(`<center>${skill}: please provide a number between 1 and ${maxSkillCount}</center>`);
              inputObj.addClass('SCalErroreousInput');
              ready = false;
            }
            
            if(skillCount < values.min) {
              $('#SCalErrorText').append(`<center>Skill ${skill} is too low! (have:${skillCount}, need: ${values.min})</center>`);
              inputObj.addClass('SCalErroreousInput');
            }
            if(!inputObj.hasClass('SCalErroreousInput')) {
              if(skillCount >= values.max) inputObj.addClass('SCalOptimalInput');
            }
          }
        }
        return ready;
      };
      
      var setCookie = function setCookie(name, value) {
        var date = new Date(3030,1,4);
        var expires = '; expires=' + date.toGMTString();
        document.cookie = name + '=' + value + expires + cookieParameters;
      };
  
      var getCookie = function getCookie(name) {
        var nameEQ = name + '=';
        var ca = document.cookie.split(';');
        for(var i=0;i < ca.length;i++) {
          var c = ca[i];
          while (c.charAt(0)==' ') c = c.substring(1,c.length);
          if (c.indexOf(nameEQ) === 0) return c.substring(nameEQ.length,c.length);
        }
        return null;
      };
      
      var deleteCookie = function deleteCookie(name) {
        var expires = '; expires=Thu, 01 Jan 1970 00:00:00 UTC';
        document.cookie = name + '=' + expires + cookieParameters;
      };
      
      var loadJQuery = function loadJQuery() {
        return new Promise( function( resolve, reject ) {
          var jquery = document.createElement('script');
          
          
          jquery.src = jQueryUrlStandalone;
          jquery.type = 'text/javascript';
          document.head.appendChild(jquery);

          jquery.onload = function () {
            resolve();
          };
        });
      };

      var loadCSS = function loadCSS() {
        return new Promise( function( resolve, reject ) {
          var css = document.createElement('link');
          css.rel = 'stylesheet';
          css.href = cssUrlStandalone;
          document.head.appendChild(css);
          css.onload = function () {
            resolve();
          };
        });
      };
      
      var startSCal = function startSCal() {
        printHeader();
        printKeyCounter();
        printLists();
    
        updateLists();
      };
      // END OF FUNCTIONS
      ///////////////////////////////////////////////////////////////////

      // Let's start adding content
      // generate IDs for the tradeskills
      for(var [key, values] of Object.entries(skillsOverview)) {
        values.id = 'SCal' + key.replaceAll(/\s/g, '') + 'Input';
      }
  
      hasMW = true; // Check for the mediawiki api. We assume that jquery is available if the api exists.
      if(typeof mw === 'undefined') hasMW = false;

      if(hasMW) {
        var url = getURL(cssFileMediaWiki);
        if(url.includes('?')) { // workaround for mediawiki htaccess shortlinks
          url += '&';
        } else {
          url += '?';
        }
        url += 'action=raw&ctype=text/css';
        mw.loader.load(url, 'text/css');
        startSCal();
      } else {
        loadCSS().then(function() { // load standalone resources
          loadJQuery().then(function() {
            startSCal();
          });
        });
      }
    }
  }
}, 50);