|
|
(5 intermediate revisions by the same user not shown) |
Line 1: |
Line 1: |
| local p = {} | | local p = {} |
|
| |
|
| -- imports
| | local function getItemValue(name) |
| local gePrice = require('Module:Exchange')._price
| | -- Use mw.loadData if needed, or fetch values another way |
| local yn = require('Module:Yesno')
| | return tonumber(mw.ext.VariablesLua.var{name}) or 0 |
| local round = require('Module:Number')._round
| |
| local _coins = require('Module:Currency')._amount
| |
| local lang = mw.getContentLanguage()
| |
| local title = mw.title.getCurrentTitle()
| |
| local onmain = require('Module:Mainonly').on_main | |
| | |
| function expr(x) | |
| local e_g, e = pcall(mw.ext.ParserFunctions.expr, x)
| |
| if e_g then
| |
| return e
| |
| end
| |
| return nil
| |
| end
| |
| | |
| function sigfig(x, p)
| |
| local x_sign = x < 0 and '-1' or '1'
| |
| local x = math.abs(x)
| |
| if x == 0 then
| |
| return 0
| |
| end
| |
| local n = math.floor(math.log10(x)) + 1 - p
| |
| return tonumber(x_sign) * math.pow(10,n) * round(x/math.pow(10, n), 0)
| |
| end
| |
| | |
| function autoround(x, f)
| |
| x = tonumber(x) or 0
| |
| local _x
| |
| if x < 0.1 and x > -0.1 then
| |
| _x = sigfig(x,2)
| |
| elseif x >= 100 or x <= -100 then
| |
| _x = round(x, 0)
| |
| else
| |
| _x = round(x, 2)
| |
| end
| |
| if f then
| |
| return lang:formatNum(_x)
| |
| end
| |
| return _x
| |
| end
| |
| | |
| -- Config constants, change as needed
| |
| MAX_INPUTS = 75
| |
| MAX_OUTPUTS = 75
| |
| MAX_XP = 75
| |
| | |
| function p.testmmgtable(args)
| |
| return p._mmgtable(mw.getCurrentFrame(), args)
| |
| end
| |
| | |
| function p.mmgtable(frame)
| |
| local args = frame:getParent().args
| |
| return p._mmgtable(frame, args)
| |
| end
| |
| | |
| -- Create an MMG table.
| |
| -- Frame is the frame the module was invoked from.
| |
| -- Args are the template arguments used when creating the table.
| |
| function p._mmgtable(frame, args)
| |
| local parsedInput = handleInputs(args)
| |
| local parsedOutput = handleOutputs(args)
| |
| local parsedXP = handleXP(args)
| |
| local is_per_kill = yn(args.isperkill)
| |
| local ret = mw.html.create('')
| |
|
| |
| mw.logObject(parsedInput)
| |
| mw.logObject(parsedOutput)
| |
| mw.logObject(parsedXP)
| |
|
| |
| local tbl = ret:tag('table'):addClass('wikitable mmg-table')
| |
| :cssText('width:100%;text-align:center;')
| |
| | |
|
| |
| local final_profit = parsedOutput.value - parsedInput.value
| |
|
| |
| | |
|
| |
| | |
|
| |
| if not onmain() then
| |
| set_smw = false
| |
| cats = ''
| |
| end
| |
|
| |
| if set_smw then
| |
| local smw_data = {
| |
| members = yn(args.Members or 'yes', true),
| |
| skill = args.Skill,
| |
| quest = args.Quest,
| |
| activity = args.Activity,
| |
| category = args.Category,
| |
| skillcategory = args.SkillCategory,
| |
| intensity = args.Intensity,
| |
| isperkill = is_per_kill,
| |
| version = args.Version,
| |
| inputs = parsedInput.list,
| |
| outputs = parsedOutput.list
| |
| }
| |
| if is_per_kill then
| |
| smw_data.prices = {
| |
| input_perhour=parsedInput.valueph,
| |
| input_perkill=parsedInput.valuepk,
| |
| output_perhour=parsedOutput.valueph,
| |
| output_perkill=parsedOutput.valuepk,
| |
| default_kph=tonumber(args.kph) or 1,
| |
| kph_text=kph_text,
| |
| default_value=parsedOutput.value - parsedInput.value,
| |
| }
| |
| else
| |
| smw_data.prices = {
| |
| input=parsedInput.value,
| |
| output=parsedOutput.value,
| |
| value=parsedOutput.value - parsedInput.value
| |
| }
| |
| end
| |
| end
| |
| return ret, cats
| |
| end
| |
| | |
| -- Calculate the profit and do nothing else.
| |
| function p.profit(frame)
| |
| local frame = frame or mw.getCurrentFrame()
| |
| local args = frame:getParent().args -- Template args, NOT #invoke args
| |
|
| |
| return handleOutputs(args).value - handleInputs(args).value
| |
| end
| |
| | |
| -- Implements handleInputs and handleOutputs
| |
| -- See those functions for further details
| |
| function handleIteratedArgs(args, prefix, max_iters)
| |
| local frame = mw.getCurrentFrame()
| |
| local items = {}
| |
| local total_item_value = 0
| |
| local textlines = {}
| |
| local is_per_kill = yn(args.isperkill)
| |
| local defaultKPH = tonumber(args.kph) or 1
| |
| local value_per_kill = 0
| |
| local value_per_hour = 0
| |
|
| |
| for i=1,max_iters,1 do
| |
| if not args[prefix..i] then break end
| |
| local pri = prefix..i
| |
| local span = mw.html.create('span')
| |
| span:addClass('mmg-itemline mmg-'..prefix:lower())
| |
|
| |
| local name = args[pri]
| |
| local qty_param = args[pri..'num']
| |
| local actual_qty = nil
| |
| local value_param = args[pri..'value']
| |
| local actual_value = nil
| |
| local is_per_hour = not is_per_kill
| |
| if is_per_kill and yn(args[pri..'isph']) then
| |
| is_per_hour = true
| |
| end
| |
|
| |
| -- Keep track of sanity check states - we want to handle them gracefully later.
| |
| local invalid_qty_present = false
| |
| local invalid_value_present = false
| |
| local failed_ge_lookup = false
| |
| local pricetype = ''
| |
|
| |
| if qty_param then
| |
| actual_qty = tonumber(qty_param) or expr(qty_param)
| |
| invalid_qty_present = not actual_qty
| |
| actual_qty = actual_qty or 1
| |
| -- If the given quantity doesn't look like a number, we'll default to 1
| |
| -- but we should probably alert the user
| |
| -- since they might want to fix that
| |
| else
| |
| -- Default value of 1
| |
| actual_qty = 1
| |
| end
| |
|
| |
| if value_param then
| |
| -- Again, if it was specified, it should be a number
| |
| -- If it isn't, we pretend it wasn't specified
| |
| -- but we alert the user because it's probably not what they want
| |
| actual_value = tonumber(value_param) or expr(value_param)
| |
| invalid_value_present = not actual_value
| |
| pricetype = 'value'
| |
| end
| |
|
| |
| -- If we got the value earlier, skip this part
| |
| if not actual_value then
| |
| -- Here we try to find an exchange price
| |
| -- If we get here, and we can't get an exchange price
| |
| -- we default to 0.
| |
| -- This is almost certainly not what the user wants,
| |
| -- so we warn them about it.
| |
| local success, price = pcall(gePrice, name)
| |
| actual_value = success and tonumber(price) -- This is awful but still pleasant somehow
| |
| failed_ge_lookup = not actual_value
| |
| actual_value = actual_value or 0
| |
| pricetype = 'gemw'
| |
| end
| |
| local this_item_value, this_item_qty, attrName
| |
| local attrVal = actual_qty * actual_value
| |
| if is_per_kill and not is_per_hour then
| |
| span:addClass('mmg-varieswithkph')
| |
| this_item_qty = actual_qty * defaultKPH
| |
| this_item_value = attrVal * defaultKPH
| |
| value_per_kill = value_per_kill + attrVal
| |
| attrName = 'data-mmg-cost-pk'
| |
| else
| |
| this_item_qty = actual_qty
| |
| this_item_value = attrVal
| |
| value_per_hour = value_per_hour + attrVal
| |
| attrName = 'data-mmg-cost-ph'
| |
| end
| |
| total_item_value = total_item_value + this_item_value
| |
|
| |
| span:tag('span'):addClass('mmg-quantity'):attr('data-mmg-qty', actual_qty):wikitext(autoround(this_item_qty, true))
| |
| if invalid_qty_present then
| |
| span:node(warning('Could not interpret \''..qty_param..'\' as a number, defaulting to 1'))
| |
| end
| |
| span:wikitext(string.format(' × [[File:%s.png|link=]] %s (', name, name, name))
| |
| span:tag('span'):addClass('mmg-cost'):attr(attrName, attrVal):wikitext(_coins(autoround(this_item_value), 'nocoins'))
| |
| span:wikitext(')')
| |
| if invalid_value_present then
| |
| span:node(warning('Could not interpret \''..value_param..'\' as a number, ignoring.'))
| |
| end
| |
| if failed_ge_lookup then
| |
| span:node(warning('Could not find exchange price for item \''..name..'\', please double-check the spelling'))
| |
| span:wikitext('[[Category:Money making guides with a failed GE lookup]]')
| |
| end
| |
| if args[pri..'note'] then
| |
| span:tag('span'):addClass('mmg-note'):wikitext(' ',args[pri..'note'])
| |
| end
| |
|
| |
| table.insert(textlines, span)
| |
|
| |
| table.insert(items, {name = name, qty = actual_qty, value = actual_value, isph = is_per_hour, pricetype=pricetype})
| |
| end
| |
|
| |
| return {value = total_item_value, valuepk = value_per_kill, valueph = value_per_hour, spans = textlines, list = items}
| |
| end
| |
| | |
| -- args are the args supplied to the template, (or a subset of them contining all input arguments)
| |
| -- Returns a table. The table has three keys: 'value', 'text', and 'as_table'
| |
| ---- 'value' contains the total value of the inputs specified by args
| |
| ---- 'text' contains a formatted string based on the inputs specified by args. This can be directly plugged into the HTML table.
| |
| ---- 'list' contains all the inputs as a Lua list. Each input is represented by a table with the following keys
| |
| ------ 'name' being the name of the item
| |
| ------ 'value' being the value of the item in question
| |
| ------ 'qty' being the quantity specified for the item
| |
| function handleInputs(args)
| |
| return handleIteratedArgs(args, 'Input', MAX_INPUTS)
| |
| end
| |
| | |
| -- args are the args supplied to the template, (or a subset of them contining all output arguments)
| |
| -- Returns a table. The table has two keys: 'value', and 'text'
| |
| ---- 'value' contains the total value of the outputs specified by args
| |
| ---- 'text' contains a formatted string based on the outputs specified by args. This can be directly plugged into the HTML table.
| |
| ---- 'list' contains all the outputs as a Lua list. Each output is represented by a table with the following keys
| |
| ------ 'name' being the name of the item
| |
| ------ 'value' being the value of the item in question
| |
| ------ 'qty' being the quantity specified for the item
| |
| function handleOutputs(args)
| |
| return handleIteratedArgs(args, 'Output', MAX_OUTPUTS)
| |
| end | | end |
|
| |
|
| | function p.main(frame) |
| | local args = frame:getParent().args |
| | local totalInput = 0 |
| | local totalOutput = 0 |
|
| |
|
| function handleXP(args)
| | -- Loop inputs |
| local frame = mw.getCurrentFrame()
| | for i = 1, 20 do |
| local items = {}
| | local name = args["Input" .. i] |
| local textlines = {}
| | local num = tonumber(args["Input" .. i .. "num"]) or 0 |
| local is_per_kill = yn(args.isperkill)
| | local isPerHour = args["Input" .. i .. "isph"] == "y" |
| local defaultKPH = tonumber(args.kph) or 1
| | if name then |
|
| | local value = getItemValue(name) |
| for i=1,MAX_XP,1 do
| | if isPerHour then |
| if not args['Experience'..i] then break end
| | num = num / (tonumber(args.kph) or 1) |
| local pri = 'Experience'..i
| | end |
| local span = mw.html.create('span')
| | totalInput = totalInput + (value * num) |
| span:addClass('mmg-xpline')
| | end |
|
| | end |
| local skill = args[pri]
| |
| local qty_param = args[pri..'num']
| |
| local actual_qty = tonumber(qty_param) or expr(qty_param) or 0
| |
| local is_per_hour = not is_per_kill
| |
| if is_per_kill and yn(args[pri..'isph']) then
| |
| is_per_hour = true
| |
| end
| |
| local this_item_value, attrName
| |
| if is_per_kill and not is_per_hour then
| |
| span:addClass('mmg-varieswithkph')
| |
| this_item_value = actual_qty * defaultKPH
| |
| attrName = 'data-mmg-xp-pk'
| |
| else
| |
| this_item_value = actual_qty
| |
| attrName = 'data-mmg-xp-ph'
| |
| end
| |
|
| |
| -- TODO modulise SCP
| |
| span:attr(attrName, actual_qty)
| |
| :wikitext(frame:expandTemplate{title='SCP', args = { skill, autoround(this_item_value, true) }})
| |
|
| |
| table.insert(textlines, span)
| |
| table.insert(items, { skill = skill, xp = actual_qty, isph = is_per_hour })
| |
| end
| |
|
| |
| return { spans = textlines, items = items }
| |
| end
| |
| | |
| -- Creates a neat little warning message
| |
| function warning(msg)
| |
| return mw.html.create('span')
| |
| :addClass('mmg-warning')
| |
| :css({
| |
| ['border-bottom'] = '1px red dotted',
| |
| color = 'red',
| |
| cursor = 'help'
| |
| })
| |
| :attr('title', msg)
| |
| :wikitext('*')
| |
| end | |
| | |
| function p.recurringTable(frame)
| |
| return p._recurringTable(frame, frame:getParent().args)
| |
| end | |
|
| |
|
| function p._recurringTable(frame, args)
| | -- Loop outputs |
| local parsedInput = handleInputs(args)
| | for i = 1, 30 do |
| local parsedOutput = handleOutputs(args)
| | local name = args["Output" .. i] |
| local timeAsString = nil
| | local num = tonumber(args["Output" .. i .. "num"]) or 0 |
| local num_good, numMinutes = pcall(mw.ext.ParserFunctions.expr, args['Activity Time'])
| | if name then |
| numMinutes = tonumber(numMinutes)
| | local value = getItemValue(name) |
| if not num_good or not numMinutes then
| | totalOutput = totalOutput + (value * num) |
| numMinutes = 1
| | end |
| end
| | end |
| if numMinutes < 1 then
| |
| local seconds = numMinutes * 60
| |
| timeAsString = seconds..' '..lang:plural(seconds, 'second', 'seconds')
| |
| else
| |
| timeAsString = numMinutes..' '..lang:plural(numMinutes, 'minute', 'minutes')
| |
| end
| |
| local tbl = mw.html.create('table')
| |
| local inputTR = mw.html.create('tr')
| |
| local inputTD = inputTR:tag('td')
| |
| local outputTD = inputTR:tag('td')
| |
| for i,v in ipairs(parsedInput.spans) do
| |
| inputTD:node(v)
| |
| end
| |
| for i,v in ipairs(parsedOutput.spans) do
| |
| outputTD:node(v)
| |
| end
| |
|
| |
|
|
| | local profit = totalOutput - totalInput |
| -- This is one statement, defining a single variable. It goes on for 120 lines. I don't know how I feel about this tbh.
| | return math.floor(profit + 0.5) -- Rounded profit |
| -- I mean how the heck would you even document this, for a start?
| |
| tbl:addClass('wikitable') -- I guess it's pretty self-documenting if you know HTML
| |
| :cssText('width:100%;text-align:center;') -- But like...
| |
| :tag('tr')
| |
| :tag('caption')
| |
| :wikitext(args['Activity'])
| |
| :done()
| |
| :done()
| |
| :tag('tr')
| |
| :tag('th')
| |
| :wikitext('Profit per instance')
| |
| :done()
| |
| :tag('td')
| |
| :attr('rowspan', '8')
| |
| :wikitext(args['Image'])
| |
| :done()
| |
| :done()
| |
| :tag('tr')
| |
| :tag('td')
| |
| :wikitext(_coins(round(parsedOutput.value - parsedInput.value, 0), 'coins'), ' per instance')
| |
| :done()
| |
| :done()
| |
| :tag('tr')
| |
| :tag('th')
| |
| :wikitext('Activity time')
| |
| :done()
| |
| :done()
| |
| :tag('tr')
| |
| :tag('td')
| |
| :wikitext(timeAsString)
| |
| :done()
| |
| :done()
| |
| :tag('tr')
| |
| :tag('th')
| |
| :wikitext('Minimum recurrence time')
| |
| :done()
| |
| :done()
| |
| :tag('tr')
| |
| :tag('td')
| |
| :wikitext(args['Recurrence Time'])
| |
| :done()
| |
| :done()
| |
| :tag('tr')
| |
| :tag('th')
| |
| :wikitext('Effective profit')
| |
| :done()
| |
| :done()
| |
| :tag('tr')
| |
| :tag('td')
| |
| :wikitext(_coins(tostring(parsedOutput.value - parsedInput.value)..' * 60 / ('..args['Activity Time']..') round 0', 'coins') .. ' per hour') -- A bit messy but it should do the job. Assuming Activity Time is a well-behaved number, of course.
| |
| :done()
| |
| :done()
| |
| :tag('tr')
| |
| :tag('th')
| |
| :wikitext('Skill requirements')
| |
| :done()
| |
| :tag('th')
| |
| :wikitext('Quest requirements')
| |
| :done()
| |
| :done()
| |
| :tag('tr')
| |
| :tag('td')
| |
| :addClass('plainlist')
| |
| :newline()
| |
| :wikitext(args['Skill'] or 'None')
| |
| :done()
| |
| :tag('td')
| |
| :addClass('plainlist')
| |
| :newline()
| |
| :wikitext(args['Quest'] or 'None')
| |
| :done()
| |
| :done()
| |
| :tag('tr')
| |
| :tag('th')
| |
| :wikitext('Item requirements')
| |
| :done()
| |
| :tag('th')
| |
| :wikitext('Other requirements')
| |
| :done()
| |
| :done()
| |
| :tag('tr')
| |
| :tag('td')
| |
| :addClass('plainlist')
| |
| :newline()
| |
| :wikitext(args['Item'] or 'None')
| |
| :done()
| |
| :tag('td')
| |
| :addClass('plainlist')
| |
| :newline()
| |
| :wikitext(args['Other'] or 'None')
| |
| :done()
| |
| :done()
| |
| :tag('tr')
| |
| :tag('th')
| |
| :wikitext('Experience gained')
| |
| :done()
| |
| :tag('th')
| |
| :wikitext('Location')
| |
| :done()
| |
| :done()
| |
| :tag('tr')
| |
| :tag('td')
| |
| :wikitext(args['Other Benefits'] or 'None')
| |
| :done()
| |
| :tag('td')
| |
| :wikitext(args['Location'] or 'Anywhere') -- Sensible enough as a default
| |
| :done()
| |
| :done()
| |
| :tag('tr')
| |
| :tag('th')
| |
| :wikitext('Inputs')
| |
| :wikitext(' (', _coins(round(parsedInput.value, 0), 'coins'), ')')
| |
| :done()
| |
| :tag('th')
| |
| :wikitext('Outputs')
| |
| :wikitext(' (', _coins(round(parsedOutput.value, 0), 'coins'), ')')
| |
| :done()
| |
| :done()
| |
| :node(inputTR)
| |
| :done()
| |
|
| |
| frame:callParserFunction('DISPLAYTITLE', title.subpageText)
| |
| frame:callParserFunction('DEFAULTSORT', title.subpageText)
| |
|
| |
| local cats = '[[Category:Money making guides]][[Category:MMG/Recurring]]'
| |
|
| |
| local final_profit = parsedOutput.value - parsedInput.value
| |
|
| |
| local set_smw = true
| |
| if final_profit <= 0 then
| |
| cats = cats .. '[[Category:Obsolete money making guides]]'
| |
| else
| |
| cats = cats .. '[[Category:Worthwhile money making guides]]'
| |
| end
| |
|
| |
| if not onmain() then
| |
| set_smw = false
| |
| cats = ''
| |
| end
| |
|
| |
| if set_smw then
| |
| local smw_data = {
| |
| members = yn(args.Members or 'yes', true),
| |
| skill = args.Skill,
| |
| quest = args.Quest,
| |
| other = args.Other,
| |
| activity = args.Activity,
| |
| category = args.Category,
| |
| version = args.Version,
| |
| time = numMinutes,
| |
| recurrence = args['Recurrence Time'],
| |
| prices = {
| |
| input=parsedInput.value,
| |
| output=parsedOutput.value,
| |
| value=parsedOutput.value - parsedInput.value
| |
| },
| |
| inputs = parsedInput.list,
| |
| outputs = parsedOutput.list
| |
| }
| |
| smw_data = mw.text.killMarkers(mw.text.nowiki(mw.text.jsonEncode(smw_data)))
| |
| mw.smw.set({
| |
| ['MMG value']=parsedOutput.value - parsedInput.value,
| |
| ['MMG recurring JSON']=smw_data
| |
| })
| |
| end
| |
|
| |
| return tbl, cats
| |
| end | | end |
|
| |
|
| return p | | return p |