MediaWiki:Gadget-livePricesMMG-core.js

This is an old revision of this page, as edited by Alex (talk | contribs) at 01:38, 13 October 2024 (Created page with "// // 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?acti..."). The present address (URL) is a permanent link to this revision, which may differ significantly from the current revision.

(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)

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.
//

// 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 () {
    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 () {
    // 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() { 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 () {
    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 (thisTr) {
    return thisTr.querySelector('a').title;
  },

  loadMapping: function (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 (msg) {
          mmgJs.mapping = {};
          for (var index in msg) {
            mmgJs.mapping[msg[index]['id']] = msg[index];
          }
        },
        error: function (req) {
          console.log('ERROR: Mapping endpoint failed')
        }
      }).done(callback);
    }
  },

  loadLivePrices: function (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 (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 (req) {
        onFailure()
      }
    });
  },

  loadMMGData: function () {
    // 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 (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 (req) {
        console.log('ERROR: MMG SMW call failed...Aborting')
      }
    });
  },

  updateTableOfficialPrices: function () {
    // 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 (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 () {
    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 (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 (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 () {

    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 (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 (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 (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 (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 (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 (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 (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 (mmgName) {
    var lsKey = mmgJs.getLSKeyNameForMmg(mmgName);
    if (lsKey !== undefined)
      return localStorage.getItem(lsKey);
  },

  setKphLocalStorage: function (mmgName, valueToUse) {
    var lsKey = mmgJs.getLSKeyNameForMmg(mmgName);
    if (lsKey !== undefined)
      localStorage.setItem(lsKey, valueToUse);
  },

  resetKphLocalStorage: function (mmgName) {
    var lsKey = mmgJs.getLSKeyNameForMmg(mmgName);
    if (lsKey !== undefined)
      localStorage.removeItem(lsKey);
  }

}

$(mmgJs.init);