Module:Sandbox/User:Fjara/Sandbox/Sandcastle
Documentation for this module may be created at Module:Sandbox/User:Fjara/Sandbox/Sandcastle/doc
local p = {}
-- Do we want to maintain warnings instead of erroring out
-- GE TAX setting
local prices = mw.loadJsonData('Module:GEPrices/data.json')
local volumes = mw.loadJsonData('Module:GEVolumes/data.json')
local commas = require('Module:Addcommas')._add
local hasContent = require('Module:Paramtest').has_content
local ucflc = require('Module:Paramtest').ucflc
local yesNo = require('Module:Yesno')
local round = require('Module:Number')._round
local coins = require('Module:Currency')._amount
local onMain = require('Module:Mainonly').on_main
local infobox = require('Module:Infobox')
local contains = require('Module:Array').contains
local scp = require('Module:SCP')._main
local varDef = mw.ext.VariablesLua.vardefine
local lang = mw.getContentLanguage()
local title = mw.title.getCurrentTitle()
local html = mw.html
local Globals = {
MAX_INPUT = 75,
MAX_OUTPUT = 75,
MAX_EXPERIENCE = 75,
}
local Intensities = {
'High',
'Medium',
'Low',
}
local Difficulties = {
'Easy',
'Medium',
'Hard',
'Very hard',
}
local Categories = {
'Collecting',
'Combat',
'Combat/Low',
'Combat/Mid',
'Combat/High',
'Processing',
'Recurring',
'Skilling',
}
local Skills = {
"Combat",
"Agility",
"Attack",
"Construction",
"Cooking",
"Crafting",
"Defence",
"Farming",
"Firemaking",
"Fishing",
"Fletching",
"Herblore",
"Hitpoints",
"Hunter",
"Magic",
"Mining",
"Prayer",
"Ranged",
"Runecraft",
"Slayer",
"Smithing",
"Strength",
"Thieving",
"Woodcutting",
}
function expr(x)
local good, answer = pcall(mw.ext.ParserFunctions.expr, x)
if(good) then
return answer
end
return nil
end
function sigfig(x, p)
local 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 sign * math.pow(10, n) * round(x / math.pow(10, n), 0)
end
function autoround(x, f)
x = tonumber(x) or 0
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
function createItemTable(tableType, args)
local isPerKill = args.kph and true or false
local ret = mw.html.create('table'):addClass('wikitable mmg-table align-center-1 align-center-2')
ret:tag('tr'):tag('th'):wikitext('Quantity'):done()
:tag('th'):attr('colspan', 2):wikitext(ucflc(tableType)):done()
:tag('th'):wikitext(tableType == 'input' and 'Cost' or 'Profit'):done()
for i,v in ipairs(args[tableType .. 's'].spans) do
ret:node(v)
end
if(args[tableType .. 's'].value ~= 0) then
ret:tag('tr'):tag('th'):attr('colspan', 4)
:tag('span')
:addClass('mmg-varieswithkph')
:attr({['data-mmg-cost-ph'] = args[tableType .. 's'].valueph, ['data-mmg-cost-pk'] = args[tableType .. 's'].valuepk})
:wikitext(coins(autoround(args[tableType .. 's'].value), 'coins'))
:done():done()
end
if(isPerKill and tableType == 'input') then -- Only want one kph incrementer
ret:addClass('mmg-isperkill')
:attr('data-default-kph', args.kph)
:attr('data-default-kph-name', args.kphName or 'Kills per hour')
end
return ret
end
function createOverviewtable(allArgs)
local ret = mw.html.create('table'):addClass('wikitable mmg-table align-center-1 align-center-2')
ret:tag('tr'):tag('th'):wikitext(allArgs.kphName):css( 'text-align', 'right' ):done()
:tag('td'):wikitext('butts'):done()
:tag('tr'):tag('th'):wikitext('Input cost'):css( 'text-align', 'right' ):done()
:tag('td'):wikitext(coins(autoround(allArgs.inputs.value), 'coins')):done()
:tag('tr'):tag('th'):wikitext('Gross output'):css( 'text-align', 'right' ):done()
:tag('td'):wikitext(coins(autoround(allArgs.outputs.value), 'coins')):done()
:tag('tr'):tag('th'):wikitext('Net profit'):css( 'text-align', 'right' ):done()
:tag('td'):wikitext(coins(autoround(allArgs.profit), 'coins')):done()
return ret
end
function defineVars(allArgs)
--[[
vdf('kph', string.format('<span class="mmg-variable mmg-kph">%s</span>', args.kph))
vdf('default_kph', args.kph)
vdf('inputPH', string.format('<span class="mmg-variable mmg-input-ph" data-mmg-cost-ph="%s">%s</span>', parsedInput.valueph, _coins(autoround(parsedInput.valueph), 'nocoins')))
vdf('inputPK', string.format('<span class="mmg-variable mmg-input-pk" data-mmg-cost-pk="%s">%s</span>', parsedInput.valuepk, _coins(autoround(parsedInput.valuepk), 'nocoins')))
vdf('input', string.format('<span class="mmg-variable mmg-input" data-mmg-cost-ph="%s", data-mmg-cost-pk="%s">%s</span>', parsedInput.valueph, parsedInput.valuepk, _coins(autoround(parsedInput.value), 'nocoins')))
vdf('outputPH', string.format('<span class="mmg-variable mmg-output-ph" data-mmg-cost-ph="%s">%s</span>', parsedOutput.valueph, _coins(autoround(parsedOutput.valueph), 'nocoins')))
vdf('outputPK', string.format('<span class="mmg-variable mmg-output-pk" data-mmg-cost-pk="%s">%s</span>', parsedOutput.valuepk, _coins(autoround(parsedOutput.valuepk), 'nocoins')))
vdf('output', string.format('<span class="mmg-variable mmg-varieswithkph mmg-output" data-mmg-cost-ph="%s", data-mmg-cost-pk="%s">%s</span>', parsedOutput.valueph, parsedOutput.valuepk, _coins(autoround(parsedOutput.value), 'nocoins')))
vdf('profitPH', string.format('<span class="mmg-variable mmg-profit-ph" data-mmg-cost-ph="%s">%s</span>', parsedOutput.valueph-parsedInput.valueph, _coins(autoround(parsedOutput.valueph-parsedInput.valueph), 'nocoins')))
vdf('profitPK', string.format('<span class="mmg-variable mmg-profit-pk" data-mmg-cost-pk="%s">%s</span>', parsedOutput.valuepk-parsedInput.valuepk, _coins(autoround(parsedOutput.valuepk-parsedInput.valuepk), 'nocoins')))
vdf('profit', string.format('<span class="mmg-variable mmg-varieswithkph mmg-profit" data-mmg-cost-ph="%s", data-mmg-cost-pk="%s">%s</span>', parsedOutput.valueph-parsedInput.valueph, parsedOutput.valuepk-parsedInput.valuepk, _coins(autoround(parsedOutput.value-parsedInput.value), 'nocoins')))
else
vdf('input', string.format('<span class="mmg-input">%s</span>', parsedInput.value, _coins(autoround(parsedInput.value), 'nocoins')))
vdf('output', string.format('<span class="mmg-input">%s</span>', parsedOutput.value, _coins(autoround(parsedOutput.value), 'nocoins')))
vdf('profit', string.format('<span class="mmg-input">%s</span>', parsedOutput.value-parsedInput.value, _coins(autoround(parsedOutput.value-parsedInput.value), 'nocoins')))
vdf('input_raw', parsedInput.value)
vdf('output_raw', parsedOutput.value)
vdf('profit_raw', parsedOutput.value-parsedInput.value)
--]]
end
function createInfobox(args, profit)
local ret = infobox.new(args)
ret:defineParams{
{ name = 'activity', func = 'activity' },
{ name = 'image', func = 'image' },
{ name = 'members', 'has_content' },
{ name = 'isRecurring', 'has_content' },
{ name = 'recurrance', 'has_content' },
{ name = 'duration', 'has_content' },
{ name = 'GP/hr', func = 'profit' },
{ name = 'XP/hr', func = 'experience.spans' },
{ name = 'intensity', func = 'intensity' },
{ name = 'location', func = 'location' },
{ name = 'category', func = 'category' },
{ name = 'skillCategory', func = 'has_content' },
}
ret:create()
ret:cleanParams()
ret:customButtonPlacement(true)
ret:setDefaultVersionSMW(true)
ret:defineLinks({
colspan = 5,
links = {
{ tostring(title), 'Edit' },
{ 'Talk:Money making guide?action=edit§ion=new&preloadtitle=[[' .. tostring(title) .. '|' .. title.subpageText .. ']]', 'Discuss' },
}})
ret:defineName('Infobox MMG')
ret:addClass('infobox-mmg')
ret:addRow{
{ tag = 'argh', content = 'activity', class='infobox-header', colspan = '5' }
}
:addRow{
{ tag = 'argd', content = 'image', class = 'infobox-image infobox-full-width-content', colspan = '5' }
}
:addRow{
{ tag = 'th', content = 'Members', colspan = '2' },
{ tag = 'argd', content = 'members', colspan = '3' }
}
if(ret:paramDefined('isRecurring')) then
ret:addRow{
{ tag = 'th', content = 'Recurrance interval', colspan = '2' },
{ tag = 'argd', content = 'recurrance', colspan = '3' }
}
ret:addRow{
{ tag = 'th', content = 'Activity duration', colspan = '2' },
{ tag = 'argd', content = 'dration', colspan = '3' }
}
--addRow for gp/instance or effective gp/hr
end
ret:addRow{
{ tag = 'th', content = 'GP/hr', colspan = '2' },
{ tag = 'argd', content = 'profit', colspan = '3' }
}
:addRow{
{ tag = 'th', content = 'XP/hr', colspan = '2' },
{ tag = 'argd', content = 'experience', colspan = '3' }
}
:addRow{
{ tag = 'th', content = 'Intensity', colspan = '2' },
{ tag = 'argd', content = 'intensity', colspan = '3' }
}
:addRow{
{ tag = 'th', content = 'Location', colspan = '2' },
{ tag = 'argd', content = 'location', colspan = '3' }
}
:addRow{
{ tag = 'th', content = 'Category', colspan = '2' },
{ tag = 'argd', content = 'category', colspan = '3' }
}
if(not ret:paramGrep('category', 'combat')) then
ret:addRow{
{ tag = 'th', content = 'Skill Category', colspan = '2' },
{ tag = 'argd', content = 'skillCategory', colspan = '3' }
}
end
return ret:tostring()
end
-- Feel free to remove/change unused data
function setSMW(allArgs)
--[[local smwData = {
activity = allArgs.activity,
members = yesNo(allArgs.members, true),
location = allArgs.location,
category = allArgs.category,
skillcategory = allArgs.skillCategory,
intensity = allArgs.intensity,
difficulty = allArgs.difficulty,
skillList = allArgs.skillList,
questList = allArgs.questList,
itemList = allArgs.itemList,
otherList = allArgs.otherList,
isperkill = allArgs.kph and true or false,
prices = {
input = allArgs.inputs.value,
output = allArgs.outputs.value,
value = allArgs.outputs.profit,
},
inputs = allArgs.inputs.items,
outputs = allArgs.outputs.items,
experience = allArgs.experience,
version = allArgs.version,
}
if(allArgs.isRecurring) then
smwData.duration = allArgs.duration,
smwData.duration_text = allArgs.durationString,
smwData.recurrence = allArgs.recurrance,
end
if(allArgs.kph) then
smwData.default_kph = allArgs.kph,
smwData.kph_text = allArgs.kphName,
smwData.prices = {
input_perhour = allArgs.inputs.valueph,
input_perkill = allArgs.inputs.valuepk,
output_perhour = allArgs.outputs.valueph,
output_perkill = allArgs.outputs.valuepk,
default_value = allArgs.outputs.value - allArgs.inputs.value,
}
else
smwData.prices = {
input = allArgs.inputs.value,
output = allArgs.outputs.value,
value = allArgs.outputs.value - allArgs.inputs.value,
}
end
smwData = mw.text.killMarkers(mw.text.nowiki(mw.text.jsonEncode(smwData)))
if(allArgs.isRecurring) then
mw.smw.set({
['MMG value'] = args.profit,
['MMG recurring JSON'] = smwData --?
})
else
mw.smw.set({
['MMG value'] = args.profit,
['MMG JSON'] = smwData
})
end--]]
end
function categories(allArgs)
local smw = true
local cats = '[[Category:Money making guides]]'
if(allArgs.isRecurring) then
cats = cats .. '[[Category:MMG/Recurring]]'
end
if(allArgs.members == nil) then
cats = cats .. '[[Category:Money making guides without a membership status]]'
elseif(not yesNo(args.members)) then
cats = cats .. '[[Category:MMG/F2P]]'
end
if(allArgs.exclude) then
smw = false
cats = cats .. '[[Category:Obsolete money making guides]]'
elseif(args.isRecurring) then -- Recurring only
if(args.profit <= 0) then
smw = false
cats = cats .. '[[Category:Obsolete money making guides]]'
end
elseif(yesNo(args.members)) then -- Members specific conditions
if(args.profit <= 100000) then
smw = false
cats = cats .. '[[Category:Obsolete money making guides]]'
end
else -- F2P specific conditions
if((args.profit <= 20000) and (args.inputs.value > 0)) then
smw = false
cats = cats .. '[[Category:Obsolete money making guides]]'
elseif(args.profit <= 15000) then
smw = false
cats = cats .. '[[Category:Obsolete money making guides]]'
end
end
if(args.category == nil) then
cats = cats .. '[[Category:Money making guides with an invalid category]]'
else
local firstSlash, _ = string.find(args.category, '/')
cats = cats .. '[[Category:MMG/' .. string.sub(args.category, 1, (firstSlash ~= nil) and firstSlash - 1 or string.len(args.category)) .. ']]'
end
return cats, smw
end
-- args is either the entire frame's args or a defined subset of those contining all input/output/experience arguments
--- argPrefix of 'input' or 'output' will return a table with the following keys:
---- 'value' contains the total value specified by args
---- 'valuepk' contains the total value per kill specified by args
---- 'valueph' contains the total value per hour specified by args
---- 'spans' contains a formatted string of items and quantities specified by args. This can be directly plugged into the HTML table.
---- 'items' contains all the args as a Lua list. Each element in the table has the following keys:
------ 'name' contains the name of the item
------ 'qty' contains the numeric quantity specified for the item
------ 'value' contains the value of the item specified by the value parameter or GE price if available and the value parameter isn't used
------ 'isph' contains the boolean for if the item is calculated per hour, if false it is per kill
------ 'pricetype' contains where the value is coming from, if it is set by the parameter it has a value of 'value' and if set by the GE lookup it will have a value of 'gemw'
--- argPrefix of 'experience' will return a table with the following keys:
---- 'spans' contains a formatted string of skill and experience quantities specified by args. This can be directly plugged into the HTML table.
---- 'skills' contains all the args as a Lua list. Each element in the table has the following keys:
----- 'skill' contains the name of the skill
----- 'xp' contains the quantity of experience
----- 'isph' contains the boolean for if the experience is calculated per hour, if false it is per kill
function parseMultiArg(argPrefix, args)
local elements = {}
local textElements = {}
local isPerKill = args.kph and true or false
local defaultKPH = args.kph or 1
local totalValue = 0
local valuePerKill = 0
local valuePerHour = 0
local isExperience = false
local spanClass
local attrPrefix
if(argPrefix == 'experience') then
isExperience = true
spanClass = 'mmg-xpline'
attrPrefix = 'data-mmg-xp-'
else
spanClass = 'mmg-' .. argPrefix:lower()
attrPrefix = 'data-mmg-cost-'
end
for i = 1, Globals['MAX_' .. argPrefix:upper()], 1 do
if(not hasContent(args[argPrefix .. i])) then break end
local argIterated = argPrefix .. i
local argName = args[argIterated]
local argQuantity = 1
if(args[argIterated .. 'num']) then
argQuantity = tonumber(args[argIterated .. 'num']) or expr(args[argIterated .. 'num'])
if(argQuantity == nil) then
error(argIterated .. ' with value ' .. args[argIterated .. 'num'] .. ' is not a valid number or expression')
end
end
local isArgPerHour = not isPerKill
if(isPerKill and yesNo(args[argIterated .. 'isph'])) then
isArgPerHour = true
end
local argValue
local argValueType
if(args[argIterated .. 'value']) then
argValue = tonumber(args[argIterated .. 'value']) or expr(args[argIterated .. 'value'])
if(argQuantity == nil) then
error(argIterated .. ' with value ' .. args[argIterated .. 'value'] .. ' is not a valid number or expression')
end
argValueType = 'value'
end
if((argValue == nil) and (isExperience == false)) then
argValue = prices[argName]
if(argValue == nil) then
error('Could not find exchange price for item: ' .. argName .. ', in arg: ' .. argIterated .. '. Double-check the spelling[[Category:Money making guides with a failed GE lookup]]' )
end
argValueType = 'gemw'
end
local argTotalValue, argTotalQuantity, attrName
local attrValue = argQuantity * (argValue or 1) -- Experience is 1:1
if(isPerKill and not isArgPerHour) then
argTotalQuantity = argQuantity * defaultKPH
argTotalValue = attrValue * defaultKPH
valuePerKill = valuePerKill + attrValue
attrName = attrPrefix .. 'pk'
else
argTotalQuantity = argQuantity
argTotalValue = attrValue
valuePerHour = valuePerHour + attrValue
attrName = attrPrefix .. 'ph'
end
totalValue = totalValue + argTotalValue
local variesWithKphClass = (isPerKill and not isArgPerHour) and 'mmg-varieswithkph' or ''
if(isExperience) then
local span = html.create('span'):addClass(spanClass .. variesWithKphClass)
span:attr(attrName, argQuantity):wikitext(scp(argName, autoround(argTotalQuantity, true))):done()
table.insert(textElements, spam)
table.insert(elements, { skill = argName, xp = argQuantity, isph = isPerHour })
else
local row = html.create('tr'):addClass(spanClass .. variesWithKphClass)
row:tag('td'):addClass('mmg-quantity'):attr('data-mmg-qty', argQuantity):wikitext(autoround(argTotalQuantity, true)):done()
:tag('td'):wikitext('[[File:' .. argName .. '.png|link=' .. argName .. ']]'):done()
:tag('td'):wikitext('[[' .. argName .. ']]'):done()
:tag('td'):addClass('mmg-cost'):attr(attrName, attrValue):wikitext(coins(autoround(argTotalValue), 'nocoins')):done()
table.insert(textElements, row)
table.insert(elements, { name = argName, qty = argQuantity, value = argValue, isph = isPerHour, pricetype = pricetype })
end
end
--rework this?
if(isExperience) then
return { spans = textElements, skills = elements }
else
return { value = totalValue, valuepk = valuePerKill, valueph = valuePerHour, spans = textElements, items = elements }
end
end
-- Custom obsolete trigger support, either by compare two variables, or switch checks for profit amount or volume checks
function p._main(args, isRecurring)
local details = args.details or ''
local allArgs = {
activity = args.activity or title.subpageText,
image = args.image,
kph = tonumber(args.kph) or 1,
kphName = args.kphname or 'Kills per hour',
members = yesNo(args.members or ''),
location = args.location or 'Anywhere',
category = contains(Categories, args.category) and args.category or nil,
skillCategory = contains(Skills, args.skillcategory) and args.skillcategory or nil,
intensity = contains(Intensities, args.intensity) and args.intensity or nil,
difficulty = contains(Difficulties, args.difficulty) and args.difficulty or nil,
skillList = args.skill,
questList = args.quest,
itemList = args.item,
otherList = args.other,
inputs = parseMultiArg('input', args),
outputs = parseMultiArg('output', args),
experience = parseMultiArg('experience', args),
version = args.version,
exclude = yesNo(args.exclude or '', false),
}
allArgs.profit = allArgs.outputs.value - allArgs.inputs.value
allArgs.roi = (allArgs.outputs.value - allArgs.inputs.value) / allArgs.inputs.value * 100
if(hasContent(args.durationMinutes) and hasContent(args.recurrance)) then
allArgs.isRecurring = true
local timeAsString = ''
local good, minutes = expr(args.durationMinutes)
minutes = tonumber(minutes)
if(not good or not minutes) then
minutes = 1
end
if(minutes < 1) then
local seconds = minutes * 60
timeAsString = seconds .. ' ' .. lang:plural(seconds, 'second', 'seconds')
else
timeAsString = minutes .. ' ' .. lang:plural(minutes, 'minute', 'minutes')
end
allArgs.durationString = timeAsString
allArgs.duration = minutes
allArgs.recurrance = args.recurrance or ''
allArgs.instanceProfit = round((allArgs.inputs.value - allArgs.outputs.value), 0)
allArgs.effectiveProfit = round((allArgs.inputs.value - allArgs.outputs.value) * 60 / minutes, 0)
end
defineVars(allArgs)
-- This does not work how I want it to
local infobox = createInfobox(allArgs)
local cats = ''
local smw = false
if(onMain()) then
cats, smw = categories(allArgs)
if(smw) then
setSMW(allArgs)
end
end
local ret = '__NOTOC__' .. infobox .. tostring(details)
if((allArgs.skillList ~= nil) or (allArgs.questList ~= nil) or (allArgs.itemList ~= nil) or (allArgs.otherList ~= nil)) then
ret = ret .. '\n==Requirements==\n'
end
if(allArgs.skillList ~= nil) then
ret = ret .. '\n===Skills===\n' .. tostring(allArgs.skillList)
end
if(allArgs.questList ~= nil) then
ret = ret .. '\n===Quests===\n' .. tostring(allArgs.questList)
end
if(allArgs.itemList ~= nil) then
ret = ret .. '\n===Items===\n' .. tostring(allArgs.itemList)
end
if(allArgs.otherList ~= nil) then
ret = ret .. '\n===Other===\n' .. tostring(allArgs.otherList)
end
return ret .. '\n==Overview==\n' .. tostring(createOverviewtable(allArgs)) .. '\n===Inputs===\n' .. tostring(createItemTable('input', allArgs)) .. '\n===Outputs===\n' .. tostring(createItemTable('output', allArgs)) .. cats
end
-- Is there a way to vardefine kph without js/span/classes so that it returns a raw number that can be used for calculations?
--Could a template accept the input and strip it without losing the ability for js to modifiy it?
function p.main(frame)
local args = frame:getParent().args
--mw.logObject(args)
-- Set KPH value before anything else goes on
if(hasContent(args.kph)) then
varDef('kph', string.format('<span class="mmg-variable mmg-kph">%s</span>', args.kph))
--varDef.var( name, default )
end
frame:callParserFunction('DISPLAYTITLE', title.subpageText)
frame:callParserFunction('DEFAULTSORT', title.subpageText)
return p._main(args)
end
-- Calculate the profit only
function p.profit(frame)
local frame = frame or mw.getCurrentFrame()
local args = frame:getParent().args
return parseMultiArg('input', args).value - parseMultiArg('output', args).value
end
return p