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