MediaWiki:Gadget-livePricesMMG-core.js

After saving, you may need to bypass your browser's cache to see the changes. For further information, see Wikipedia:Bypass your cache.

  • In most Windows and Linux browsers: Hold down Ctrl and press F5.
  • In Safari: Hold down ⇧ Shift and click the Reload button.
  • In Chrome and Firefox for Mac: Hold down both ⌘ Cmd+⇧ Shift and press R.
"use strict";

//

// TODO: Do I just do something kind of dumb like use an object here for constant lookup?
EXEMPT_FROM_TAX = ['Old school bond', 'Chisel', 'Gardening trowel', 'Glassblowing pipe', 'Hammer', 'Needle', 'Pestle and mortar', 'Rake', 'Saw', 'Secateurs', 'Seed dibber', 'Shears', 'Spade', 'Watering can (0)'];

// Tax is never higher than 5m per item
MAX_TAX_AMOUNT = 5000000;
MMG_SMW_DATA_ENDPOINT = "https://oldschool.runescape.wiki/api.php?action=ask&query=[[MMG%20JSON::%2B]]%7C%3FMMG%20JSON%7Climit=10000&format=json";
MAPPING_ENDPOINT = 'https://prices.runescape.wiki/api/v1/osrs/mapping';
ALLOWED_GRANULARITIES = ['latest', '5m', '1h', '6h', '24h'];
SHOULD_TAX_ON_BUY = false;
SHOULD_TAX_ON_SELL = true;
mmgJs = {
  init: function init() {
    mmgJs.livePrices = {};
    mmgJs.officialPrices = {};
    mmgJs.buttonSelect = null;

    // Builds a map of MMG names to tr elements so we can manipulate them later
    mmgJs.indexTableRows();

    // Creates the buttons to select the price type
    mmgJs.initButtons();

    // Loads prices and bootstraps the table
    mmgJs.loadMMGData();
  },
  initButtons: function initButtons() {
    // Set up the ButtonSelectWidget that toggles between live and official prices
    var buttonDiv = document.querySelector('.mmg-list-table-buttons');
    var officialPricesButton = new OO.ui.ButtonOptionWidget({
      title: 'Official Prices',
      label: 'Official Prices',
      data: mmgJs.updateTableOfficialPrices
    });
    var livePricesButton = new OO.ui.ButtonOptionWidget({
      title: 'Live Prices',
      label: 'Live Prices',
      data: function data() {
        mmgJs.initUpdateTableLivePrices('24h');
      }
    });
    mmgJs.buttonSelect = new OO.ui.ButtonSelectWidget({
      items: [officialPricesButton, livePricesButton]
    });
    mmgJs.buttonSelect.on('select', function (item) {
      item.data();
    });
    $(buttonDiv).append(mmgJs.buttonSelect.$element);
  },
  // Build a map of MMG name -> tr for access later
  indexTableRows: function indexTableRows() {
    var table = document.querySelector('.mmg-list-table');
    var trs = table.getElementsByClassName('mmg-list-table-row');
    mmgJs.trsIndexedByName = {};
    for (var i = 0; i < trs.length; i++) {
      var tr = trs[i];
      mmgJs.trsIndexedByName[mmgJs.getTrMmgName(tr)] = tr;
    }
  },
  getTrMmgName: function getTrMmgName(thisTr) {
    return thisTr.querySelector('a').title;
  },
  loadMapping: function loadMapping(callback) {
    // Get the live prices mapping data since we will need it to match names and ids (is there a better place to look?)
    if (mmgJs.mapping) {
      callback();
    } else {
      $.ajax({
        type: "GET",
        url: MAPPING_ENDPOINT,
        dataType: "json",
        success: function success(msg) {
          mmgJs.mapping = {};
          for (var index in msg) {
            mmgJs.mapping[msg[index]['id']] = msg[index];
          }
        },
        error: function error(req) {
          console.log('ERROR: Mapping endpoint failed');
        }
      }).done(callback);
    }
  },
  loadLivePrices: function loadLivePrices(granularity) {
    // Check for mapping
    var endpoint = "https://prices.runescape.wiki/api/v1/osrs/" + granularity;
    // Get the live prices data
    $.ajax({
      type: "GET",
      url: endpoint,
      dataType: "json",
      indexValue: {
        granularity: granularity
      },
      success: function success(msg) {
        mmgJs.livePrices[this.indexValue.granularity] = {};
        for (var key in msg['data']) {
          if (!(key in mmgJs.mapping)) {
            console.log('WARNING: Key ' + key + ' not found in mapping. If the id is 2659 this is expected.');
            continue;
          }
          var itemName = mmgJs.mapping[key]['name'];
          mmgJs.livePrices[this.indexValue.granularity][itemName] = msg['data'][key];
        }
        mmgJs.updateTableLivePrices(granularity);
      },
      error: function error(req) {
        onFailure();
      }
    });
  },
  loadMMGData: function loadMMGData() {
    // Gets the MMG data via SMW. This should be called once per run.
    $.ajax({
      type: "GET",
      url: MMG_SMW_DATA_ENDPOINT,
      dataType: "json",
      success: function success(msg) {
        mmgJs.mmgData = msg;
        mmgJs.results = msg['query']['results'];
        mmgJs.storeOfficialPrices(msg);
        mmgJs.storeMMGIO(msg);

        // Set the default to Live Prices
        mmgJs.buttonSelect.selectItemByLabel('Live Prices');
      },
      error: function error(req) {
        console.log('ERROR: MMG SMW call failed...Aborting');
      }
    });
  },
  updateTableOfficialPrices: function updateTableOfficialPrices() {
    // We only get one price with the official prices so we have to assume buying and selling at that price
    mmgJs.buyingPriceMap = mmgJs.officialPrices;
    mmgJs.sellingPriceMap = mmgJs.officialPrices;
    mmgJs.createList();
  },
  storeOfficialPrices: function storeOfficialPrices(mmgData) {
    // TODO: There's probably a smarter way to get these values, but this is likely not our bottleneck anyway

    mmgJs.officialPrices = {};
    for (var mmg in mmgData['query']['results']) {
      var d = mmgData['query']['results'][mmg]['printouts']['MMG JSON'][0];
      var parsedData = JSON.parse($("<textarea/>").html(d).text());
      var inputs = parsedData['inputs'];
      var outputs = parsedData['outputs'];
      for (var index in inputs) {
        // TODO: Is there something idiomatic in old js? I think this is for-of now
        var item = inputs[index];
        if (item['pricetype'] == 'gemw') {
          mmgJs.officialPrices[item['name']] = item['value'];
        }
      }
      for (var index in outputs) {
        // TODO: Is there something idiomatic in old js? I think this is for-of now
        var item = outputs[index];
        if (item['pricetype'] == 'gemw') {
          mmgJs.officialPrices[item['name']] = item['value'];
        }
      }
    }
  },
  storeMMGIO: function storeMMGIO() {
    mmgJs.parsedResults = {};
    for (var mmg in mmgJs.results) {
      var d = mmgJs.results[mmg]['printouts']['MMG JSON'][0];
      mmgJs.parsedResults[mmg] = JSON.parse($("<textarea/>").html(d).text());
    }
  },
  initUpdateTableLivePrices: function initUpdateTableLivePrices(granularity) {
    // Build a price list based on what we asked for
    if (!ALLOWED_GRANULARITIES.includes(granularity)) {
      console.log('ERROR: ' + granularity + ' is not a supported granularity');
    }
    // We will call updateTableLivePrices via a callback in loadLivePrices if we don't already have the prices
    if (mmgJs.livePrices[granularity] === undefined) {
      mmgJs.loadMapping(function () {
        mmgJs.loadLivePrices(granularity);
      });
    } else {
      mmgJs.updateTableLivePrices(granularity);
    }
  },
  updateTableLivePrices: function updateTableLivePrices(granularity) {
    var granularityMap = mmgJs.livePrices[granularity];
    mmgJs.buyingPriceMap = {};
    mmgJs.sellingPriceMap = {};
    for (var itemName in granularityMap) {
      if (granularity === 'latest') {
        mmgJs.buyingPriceMap[itemName] = granularityMap[itemName]['high'];
        mmgJs.sellingPriceMap[itemName] = granularityMap[itemName]['low'];
      } else {
        mmgJs.buyingPriceMap[itemName] = granularityMap[itemName]['avgHighPrice'];
        mmgJs.sellingPriceMap[itemName] = granularityMap[itemName]['avgLowPrice'];
      }
    }
    mmgJs.createList();
  },
  createList: function createList() {
    for (var mmg in mmgJs.parsedResults) {
      var mmgName = mmgJs.results[mmg]['fulltext'];

      // Identify what row this is
      var thisTr = mmgJs.trsIndexedByName[mmgName];
      if (thisTr === undefined) {
        console.log('WARNING: MMG not found in the table: ' + mmgName);
        continue;
      }
      var numActions = mmgJs.getKphLocalStorage(mmgName) || mmgJs.parsedResults[mmg]['prices']['default_kph'];
      if (thisTr.querySelector('.mmg-kph-selector') === null) {
        mmgJs.addCellToggle(thisTr, numActions);
      }

      // Probably want to grab price info and pass it in here (or do we load then populate the rows?)
      mmgJs.updateTableRow(thisTr, numActions);
    }
  },
  updateTableRow: function updateTableRow(thisTr, numActions) {
    //console.log('Updating row with numActions = ' + numActions);

    // Update profit cell
    var profitCell = thisTr.querySelector('.mmg-list-table-profit-cell');
    // numActions should be value in the box or default
    var mmg = thisTr.querySelector('a').title;
    var profit = mmgJs.calculateProfit(mmg, mmgJs.buyingPriceMap, mmgJs.sellingPriceMap, numActions);

    // Add the correct class to the table cell
    if (profit > 0) {
      profitCell.classList.remove('coins-neg');
      profitCell.classList.add('coins-pos');
    } else if (profit < 0) {
      profitCell.classList.remove('coins-pos');
      profitCell.classList.add('coins-neg');
    } else {
      profitCell.classList.remove('coins-pos');
      profitCell.classList.remove('coins-neg');
    }
    profitCell.innerHTML = profit.toLocaleString();
  },
  addCellToggle: function addCellToggle(thisTr, valueToUse) {
    var mmgName = mmgJs.getTrMmgName(thisTr);
    var kphCell = thisTr.querySelector('.mmg-list-table-kph-cell');
    var kphField = new OO.ui.NumberInputWidget({
      min: 0,
      input: {
        value: valueToUse
      },
      classes: ['mmg-kph-selector'],
      showButtons: false,
      title: mmgJs.parsedResults[mmgName]['prices']['kph_text'] || 'Kills per hour'
    });
    function updateThisRow() {
      mmgJs.setKphLocalStorage(mmgName, kphField.getNumericValue());
      return mmgJs.updateTableRow(thisTr, kphField.getNumericValue());
    }
    kphField.on('change', updateThisRow);
    var resetButton = new OO.ui.ButtonWidget({
      icon: 'reload',
      label: 'Reset',
      invisibleLabel: true,
      classes: ['mmg-kph-refresh-field']
    });
    resetButton.on('click', function () {
      // Reset the key AFTER setValue since setValue would otherwise write default_kph to LS
      kphField.setValue(mmgJs.parsedResults[mmgName]['prices']['default_kph']);
      mmgJs.resetKphLocalStorage(mmgName);
    });
    var layout = new OO.ui.ActionFieldLayout(kphField, resetButton, {
      classes: ['mmg-kph-selector-field']
    });
    kphCell.innerHTML = '';
    $(kphCell).append(layout.$element);
  },
  calculateProfit: function calculateProfit(mmg, buyingPriceMap, sellingPriceMap, numActions) {
    var inputMap = mmgJs.parsedResults[mmg]['inputs'];
    var outputMap = mmgJs.parsedResults[mmg]['outputs'];
    var inputAmount = mmgJs.calculateValue(inputMap, buyingPriceMap, numActions, SHOULD_TAX_ON_BUY);
    var outputAmount = mmgJs.calculateValue(outputMap, sellingPriceMap, numActions, SHOULD_TAX_ON_SELL);
    return Math.floor(outputAmount - inputAmount);
  },
  getItemPrice: function getItemPrice(item, givenPrice) {
    // If the item does not use GE prices, always return the price as is
    if (item['pricetype'] != 'gemw') return item['value'];
    if (givenPrice === undefined || givenPrice === null) {
      console.log('WARNING: This item has no price in the price map you gave me! ' + item['name']);
      return item['value'];
    }
    return givenPrice;
  },
  calculateValue: function calculateValue(itemAmountMap, priceMap, numActions, useTax) {
    var sum = 0;
    for (var index in itemAmountMap) {
      // TODO: Is there something idiomatic in old js? I think this is for-of now
      var item = itemAmountMap[index];
      // For each item, determine the value
      // console.log("Item: " + JSON.stringify(item));

      // Stub this out to get the correct price
      var value = mmgJs.getItemPrice(item, priceMap[item['name']]);
      var quantity = item['qty'];
      // Might want to always use this value if pricetype is not gemw since that means it isnt on the ge
      var priceType = item['pricetype'];

      // Number used will not change based on numActions if isph is true
      var numberUsed;
      if (item['isph']) {
        numberUsed = quantity;
      } else {
        numberUsed = quantity * numActions;
      }
      if (useTax) {
        // Subtract tax since it is per item and not per txn
        sum += numberUsed * mmgJs.applyTax(item['name'], value);
      } else {
        sum += numberUsed * value;
      }
    }
    return sum;
  },
  applyTax: function applyTax(itemName, price) {
    if (EXEMPT_FROM_TAX.includes(itemName)) return price;
    return price - Math.min(Math.floor(price / 100), MAX_TAX_AMOUNT);
  },
  /**
   *  LocalStorage helper methods to retrieve and set values
   */

  getLSKeyNameForMmg: function getLSKeyNameForMmg(mmgName) {
    // mmgName should always have a "Money making guide/" prefix
    // This will work for anything that is a subpage and we could have some default for a top level page, but I don't want to pollute LS
    var mmg = mmgName.split('/')[1];
    if (mmg === undefined) return undefined;
    return mmg + '-mmg-kph';
  },
  getKphLocalStorage: function getKphLocalStorage(mmgName) {
    var lsKey = mmgJs.getLSKeyNameForMmg(mmgName);
    if (lsKey !== undefined) return localStorage.getItem(lsKey);
  },
  setKphLocalStorage: function setKphLocalStorage(mmgName, valueToUse) {
    var lsKey = mmgJs.getLSKeyNameForMmg(mmgName);
    if (lsKey !== undefined) localStorage.setItem(lsKey, valueToUse);
  },
  resetKphLocalStorage: function resetKphLocalStorage(mmgName) {
    var lsKey = mmgJs.getLSKeyNameForMmg(mmgName);
    if (lsKey !== undefined) localStorage.removeItem(lsKey);
  }
};
$(mmgJs.init);