Module:Sandbox/User:Robert571/RunningCost: Difference between revisions
Jump to navigation
Jump to search
No edit summary |
m (1 revision imported) |
(No difference)
|
Latest revision as of 21:23, 4 November 2021
Documentation for this module may be created at Module:Sandbox/User:Robert571/RunningCost/doc
local p = {}
local GEPRICES = mw.loadData('Module:GEPrices/data')
local backupgeprice = require('Module:Exchange')._price
local amount = require('Module:Currency_short')._amount
local frac = require('Module:GETotal').frac
local yesno = require('Module:Yesno')
local calcvalue = require('Module:Calcvalue')._get
local lang = mw.getContentLanguage()
-- Maximum number of methods of repair for an item
local MAX_NUM_METHODS = 4
-- Maximum number of unique items that a repair method can contain
local MAX_NUM_ITEM_TYPES = 20
-- references support
local USED_REFERENCES = false
local REFERENCES_GROUP_NAME = 'uc'
function ref(txt, name)
USED_REFERENCES = true
return mw.getCurrentFrame():extensionTag{
name = 'ref',
content = txt,
args = { name = name, group = REFERENCES_GROUP_NAME }
}
end
function add_reflist(t, colspan)
if USED_REFERENCES then
t = tostring(t)..mw.getCurrentFrame():extensionTag{name='references', args={group=REFERENCES_GROUP_NAME}}
--t:tag('tr'):tag('td'):attr('colspan', colspan):wikitext( mw.getCurrentFrame():extensionTag{name='references', args={group=REFERENCES_GROUP_NAME}} )
end
return t
end
function SMITHING_DISCOUNT_REF()
return ref("Cost can be reduced by 0.5% per [[Smithing]] level; 50% at 100 Smithing.", "smithing")
end
function DIVINE_CHARGE_REF()
return ref("Can be reduced by [[Charge pack#Charge drain reduction|research]], [[equipment level]], [[Efficient]]/[[Enhanced Efficient]] perk and the [[Invention cape]] perk.", "divine charge")
end
local COMBAT_RATES = {
[1] = {
["chargesPerHour"] = (30*60),
["name"] = "Average rate",
ref = function()
return ref(lang:formatNum(30*60).." charges per hour.", "rate1")
end
},
[2] = {
["chargesPerHour"] = (60*60),
["name"] = "High rate",
ref = function()
return ref(lang:formatNum(60*60).." charges per hour.", "rate2")
end
},
[3] = {
["chargesPerHour"] = (100*60),
["name"] = "Maximum rate",
ref = function()
return ref( lang:formatNum(100*60).." charges per hour.", "rate3")
end
},
}
-- Divine charge slot names and drain multipliers
-- Make all keys lowercase
local DIVINE_CHARGE_SLOTS = {
["2h"] = {
["name"] = "Two-handed",
["chargeDrainMult"] = 1.5
},
["main hand"] = {
["name"] = "Main hand",
["chargeDrainMult"] = 1.0
},
["weapon"] = {
["name"] = "Main hand",
["chargeDrainMult"] = 1.0
},
["torso"] = {
["name"] = "Torso",
["chargeDrainMult"] = 1.0
},
["body"] = {
["name"] = "Torso",
["chargeDrainMult"] = 1.0
},
["legs"] = {
["name"] = "Legs",
["chargeDrainMult"] = 1.0
},
["off-hand"] = {
["name"] = "Off-hand",
["chargeDrainMult"] = 0.5
},
["off-hand weapon"] = {
["name"] = "Off-hand",
["chargeDrainMult"] = 0.5
},
["shield"] = {
["name"] = "Off-hand",
["chargeDrainMult"] = 0.5
},
["tool"] = {
["name"] = "Tool",
["chargeDrainMult"] = 0.25
},
}
local CHARGES_PER_DIVINE_CHARGE = 3000
local INV_CAPE_FACTOR = 0.98
local ATTACK_CAPE_FACTOR = 0.98
function plink(itemname, qty)
return string.format("%s × [[File:%s.png|link=%s]] [[%s]]",lang:formatNum(qty),itemname,itemname,itemname)
end
function make_method_cell_wikitext(method)
local out = {}
local cost = calculateMethodCost(method)
-- For each item in repair method
for _,item in ipairs(method["items"]) do
-- Create "quantity × {{plink|item}}" line
local line = plink(item.name, item.qty)
if item["unGEable"] then
if item["calcvalue"] then
line = line .. ref("Item is not tradeable on the Grand Exchange. Using calculated value.","calcvalue")
else
line = line .. ref("Item is not tradeable on the Grand Exchange.","untradeable")
end
end
-- Add line to method string with line break
table.insert(out, line)
end
-- If smithing discount parameter
if method["smithing"] then
-- Add smithing discount ref
out[#out] = out[#out] .. SMITHING_DISCOUNT_REF()
end
return cost, table.concat(out, '<br>')
end
function p.fixedDuration(frame)
local args = frame:getParent().args
local methods = parseMethods(args)
if #methods <1 then error("No methods defined.") end
methods = parseDurations(args,methods)
local json = mw.text.killMarkers(mw.text.nowiki(mw.text.jsonEncode(methods)))
-- Determine if all durations are the same
local identicalDurations = true
local d = nil
for _,method in ipairs(methods) do
local duration = method["duration"]
if d == nil then
d = duration
elseif d ~= duration then
identicalDurations = false
break
end
end
--Determine if all descriptions are nil
local allDescNil = true
for _,method in ipairs(methods) do
local desc = method["description"]
allDescNil = allDescNil and desc == nil
end
local t = mw.html.create("table")
:addClass("wikitable")
if not allDescNil then
-- Start row
row = t:tag("tr")
row:tag("th") :wikitext("Description") :done()
-- Add description
for _,method in ipairs(methods) do
local desc = method["description"]
if desc == nil then
NA_cell(row)
else
row:tag("td") :wikitext(desc) :done()
end
end
row:done()
-- End row
end
-- Start row
row = t:tag("tr")
row:tag("th") :wikitext("Duration") :done()
-- If all durations are the same, make it one colspanned cell
if identicalDurations then
local minutes = methods[1]["duration"]
row:tag("td") :wikitext(minutesToString(minutes)) :attr("colspan",#methods) :done()
else
for _,method in ipairs(methods) do
row:tag("td") :wikitext(minutesToString(method["duration"])) :done()
end
end
row:done()
-- End row
-- Start row
row = t:tag("tr")
:tag("th") :wikitext("Cost") :done()
local costs = {}
-- For each method
for i,method in ipairs(methods) do
local cost, wikitext = make_method_cell_wikitext(method)
costs[i] = cost
-- Add repair method string to row
row:tag("td") :wikitext(wikitext) :attr("style","vertical-align: bottom;") :done()
end
t:done()
-- End row
row = t:tag("tr")
:tag("th") :wikitext("Total GE Price") :done()
-- For each repair method
for i,cost in ipairs(costs) do
-- Add repair method string to row
row:tag("td") :wikitext(coins(cost)) :done()
end
t:done()
-- End row
-- Start row
t:tag("tr")
:tag("th") :wikitext("Per hour") :attr("colspan",#methods+1) :done()
:done()
-- End row
-- Start row
t:tag("tr")
row = t:tag("th") :wikitext("Cost") :done()
-- For each method
for i, method in ipairs(methods) do
-- Retrieve calculated cost
local cost = costs[i]
local minutes = method["duration"]
local hours = minutes/60
-- Calculate cost per hour
local costPerHour = round(cost/hours)
-- Add cost per hour cell
row:tag("td") :wikitext(coins(costPerHour)) :done()
end
t:done()
-- End row
-- Properties to store
local dataStore = {
["Usage Cost Type"] = "FixedDuration",
["Usage Cost JSON"] = json
}
-- Store properties
local result = mw.smw.set( dataStore )
if result == true then
-- everything ok
else
error(result.error)
end
return add_reflist(t, #methods+1)
end
function p.combatCharge(frame)
local args = frame:getParent().args
local methods = parseMethods(args)
if #methods <1 then error("No methods defined.") end
local json = mw.text.killMarkers(mw.text.nowiki(mw.text.jsonEncode(methods)))
local charges = args["charges"]
charges = tonumber(charges)
-- Start table
local t = mw.html.create("table")
:addClass("wikitable")
-- Start row
:tag("tr")
:tag("th") :wikitext("[[Equipment degradation|Combat charges]]") :done()
-- Charges has colspan = number of methods
:tag("td") :wikitext(lang:formatNum(charges)) :attr("colspan",#methods) :done()
:done()
-- End row
--Determine if all descriptions are nil
local allDescNil = true
for _,method in ipairs(methods) do
local desc = method["description"]
allDescNil = allDescNil and desc == nil
end
if not allDescNil then
-- Start row
row = t:tag("tr")
row:tag("th") :wikitext("Description") :done()
-- Add description
for _,method in ipairs(methods) do
local desc = method["description"]
if desc == nil then
NA_cell(row)
else
row:tag("td") :wikitext(desc) :done()
end
end
row:done()
-- End row
end
-- Start row
row = t:tag("tr")
:tag("th") :wikitext("Full repair items") :done()
local costs = {}
-- For each repair method
for i,method in ipairs(methods) do
local cost, wikitext = make_method_cell_wikitext(method)
costs[i] = cost
-- Add repair method string to row
row:tag("td") :wikitext(wikitext) :attr("style","vertical-align: bottom;") :done()
end
t:done()
-- End row
row = t:tag("tr")
:tag("th") :wikitext("Total GE Price") :done()
-- For each repair method
for i,cost in ipairs(costs) do
-- Add repair method string to row
row:tag("td") :wikitext(coins(cost)) :done()
end
t:done()
-- End row
-- Start row
t:tag("tr")
:tag("th") :wikitext("Per hour") :attr("colspan",#methods+1) :done()
:done()
-- End row
-- Start row
row = t:tag("tr")
-- For each combat rate
for _,rate in ipairs(COMBAT_RATES) do
-- Add combat rate header cell with name and ref
row:tag("th") :wikitext(rate["name"]..rate.ref()) :done()
-- For each repair method
for i,cost in ipairs(costs) do
-- Calculate cost per hour
local costPerHour = round(cost/charges * rate["chargesPerHour"])
-- Add cost per hour cell
row:tag("td") :wikitext(coins(costPerHour)) :done()
end
-- End row
row:done()
-- Start row
row = t:tag("tr")
end
-- End row
t:done()
-- Properties to store
local dataStore = {
["Usage Cost Type"] = "CombatCharge",
["Usage Cost Charges"] = charges,
["Usage Cost JSON"] = json
}
-- Store properties
local result = mw.smw.set( dataStore )
if result == true then
-- everything ok
else
error(result.error)
end
return add_reflist(t, #methods+1)
end
function p.nonStandardDegrade(frame)
local args = frame:getParent().args
local methods = parseMethods(args)
if #methods <1 then error("No methods defined.") end
local json = mw.text.killMarkers(mw.text.nowiki(mw.text.jsonEncode(methods)))
local charges = args["charges"]
charges = tonumber(charges)
local degradeText = args["degradetext"]
-- Start table
local t = mw.html.create("table")
:addClass("wikitable")
-- Start row
:tag("tr")
:tag("th") :wikitext("[[Equipment degradation|Combat charges]]") :done()
-- Charges has colspan = number of methods
:tag("td") :wikitext(lang:formatNum(charges)) :attr("colspan",#methods) :done()
:done()
-- End row
--Determine if all descriptions are nil
local allDescNil = true
for _,method in ipairs(methods) do
local desc = method["description"]
allDescNil = allDescNil and desc == nil
end
if not allDescNil then
-- Start row
row = t:tag("tr")
row:tag("th") :wikitext("Description") :done()
-- Add description
for _,method in ipairs(methods) do
local desc = method["description"]
if desc == nil then
NA_cell(row)
else
row:tag("td") :wikitext(desc) :done()
end
end
row:done()
-- End row
end
-- Start row
row = t:tag("tr")
:tag("th") :wikitext("Full repair items") :done()
local costs = {}
-- For each repair method
for i,method in ipairs(methods) do
local cost, wikitext = make_method_cell_wikitext(method)
costs[i] = cost
-- Add repair method string to row
row:tag("td") :wikitext(wikitext) :attr("style","vertical-align: bottom;") :done()
end
t:done()
-- End row
row = t:tag("tr")
:tag("th") :wikitext("Total GE Price") :done()
-- For each repair method
for i,cost in ipairs(costs) do
-- Add repair method string to row
row:tag("td") :wikitext(coins(cost)) :done()
end
t:done()
-- End row
-- Start row
t:tag("tr")
:tag("th") :wikitext("Degrade mechanics") :done()
:tag("td") :wikitext(degradeText) :attr("colspan",#methods+1) :done()
t:done()
-- End row
-- Properties to store
local dataStore = {
["Usage Cost Type"] = "NonStandardDegrade",
["Usage Cost Charges"] = charges,
["Usage Cost JSON"] = json
}
-- Store properties
local result = mw.smw.set( dataStore )
if result == true then
-- everything ok
else
error(result.error)
end
return add_reflist(t, #methods+1)
end
function p.divineCharge(frame)
local args = frame:getParent().args
local tier = args["tier"]
tier = tonumber(tier)
local effectiveTier = tier
-- EffectiveTier set to 67 if less than 70
if tier < 70 then
effectiveTier = 67
end
local slot = args["slot"]
slot = string.lower(slot)
local slotName = DIVINE_CHARGE_SLOTS[slot]["name"]
if slotName == nil then error("Invalid slot: "..slot) end
local chargesPerHour = divineChargeCalc(effectiveTier,DIVINE_CHARGE_SLOTS[slot]["chargeDrainMult"])
local costPerHour = geprice("Divine charge") * chargesPerHour / CHARGES_PER_DIVINE_CHARGE
local t = mw.html.create("table")
:addClass("wikitable")
-- Start row
:tag("tr")
:tag("th") :wikitext("Tier") :done()
:tag("td") :wikitext(lang:formatNum(tier)) :done()
:done()
-- End row
-- Start row
:tag("tr")
:tag("th") :wikitext("Slot") :done()
:tag("td") :wikitext(slotName) :done()
:done()
-- End row
-- Start row
:tag("tr")
:tag("th") :wikitext("Per hour") :attr("colspan",2) :done()
:done()
-- End row
-- Start row
:tag("tr")
:tag("th") :wikitext("Charges") :done()
:tag("td") :wikitext(lang:formatNum(chargesPerHour)..DIVINE_CHARGE_REF()) :done()
:done()
-- End row
-- Start row
:tag("tr")
:tag("th") :wikitext("Cost") :done()
:tag("td") :wikitext(coins(costPerHour)) :done()
:done()
-- End row
-- Properties to store
local dataStore = {
["Usage Cost Type"] = "DivineCharge",
["Usage Cost Invention Tier"] = tier,
["Usage Cost Invention Slot"] = slot
}
-- Store properties
local result = mw.smw.set( dataStore )
if result == true then
-- everything ok
else
error(result.error)
end
return add_reflist(t, 2)
end
function p.getUsageCost(frame)
local args = frame:getParent().args
-- Page name to get properties from
local item = args[1]
-- Get Usage Cost Type property
local smw = mw.smw.ask({
'[['..item..']]',
'?Usage Cost Type#-',
'?Usage Cost Invention Tier#-',
'?Usage Cost Invention Slot#-',
'?Usage Cost Charges#-',
'?Usage Cost JSON#-'
})
-- If properties not found
if smw[1]["Usage Cost Type"] == nil then
error("Usage Cost not found for item: ".. item)
end
costType = string.lower(smw[1]["Usage Cost Type"])
local ret
-- Choose function based on costType
if costType == "fixedduration" then
ret = getFixedDuration(args, smw)
elseif costType == "combatcharge" then
ret = getCombatCharge(args, smw)
elseif costType == "divinecharge" then
ret = getDivineCharge(args,smw)
else
error("Invalid CostType: " .. costType)
end
return ret
end
function getFixedDuration(args, smw)
local item = args[1]
-- Operation to perform on multiple costs, defaults to minimum
local op = args["op"] or "min"
op = string.lower(op)
local smithing = args["smithing"] or 0
local smithingMult = smithingDiscountMult(smithing)
local encoded = smw[1]["Usage Cost JSON"]
local methods = mw.text.jsonDecode(mw.text.decode(encoded))
-- Track minimum, maximum and average cost
local minCost = nil
local maxCost = nil
local avgCost = 0
for _,method in ipairs(methods) do
-- Smithing multiplier is only applied if the method is affected by smithing
local smith = 1
if method["smithing"] then
smith = smithingMult
end
local minutes = method["duration"]
local hours = minutes/60
local methodCost = calculateMethodCost(method)
local costPerHour = methodCost * smith / hours
-- Update minimum, maximum and average cost
if minCost == nil or costPerHour < minCost then minCost = costPerHour end
if maxCost == nil or costPerHour > maxCost then maxCost = costPerHour end
avgCost = avgCost + costPerHour
end
avgCost = avgCost/#methods
local costUsed
-- Choose cost to use based on op
if op == "avg" then
costUsed = avgCost
elseif op == "highest" or op == "max" then
costUsed = maxCost
else
costUsed = minCost
end
return round(costUsed)
end
function getCombatCharge(args, smw)
local item = args[1]
-- Operation to perform on multiple costs, defaults to minimum
local op = args["op"] or "min"
op = string.lower(op)
local chargesPerHour = args["chargesperhour"] or COMBAT_RATES[1]["chargesPerHour"]
chargesPerHour = tonumber(chargesPerHour)
local cape = args["cape"] or false
cape = yesno(cape)
local capeFactor = 1.00
if cape then
capeFactor = ATTACK_CAPE_FACTOR
end
local smithing = args["smithing"] or 0
local smithingMult = smithingDiscountMult(smithing)
local charges = smw[1]["Usage Cost Charges"]
local encoded = smw[1]["Usage Cost JSON"]
local methods = mw.text.jsonDecode(mw.text.decode(encoded))
-- Track minimum, maximum and average cost
local minCost = nil
local maxCost = nil
local avgCost = 0
for _,method in ipairs(methods) do
-- Smithing multiplier is only applied if the method is affected by smithing
local smith = 1
if method["smithing"] then
smith = smithingMult
end
local methodCost = calculateMethodCost(method)
local costPerHour = methodCost * chargesPerHour * capeFactor * smith / charges
-- Update minimum, maximum and average cost
if minCost == nil or costPerHour < minCost then minCost = costPerHour end
if maxCost == nil or costPerHour > maxCost then maxCost = costPerHour end
avgCost = avgCost + costPerHour
end
avgCost = avgCost/#methods
local costUsed
-- Choose cost to use based on op
if op == "avg" then
costUsed = avgCost
elseif op == "highest" or op == "max" then
costUsed = maxCost
else
costUsed = minCost
end
return round(costUsed)
end
function getDivineCharge(args, smw)
-- Page name to get properties from
local item = args[1]
-- Invention level for charge drain reduction
local invlvl = args["invlvl"] or 1
local researchFactor = chargeDrainResearch(invlvl)
local cape = args["cape"] or false
cape = yesno(cape)
local capeFactor = 1.00
if cape then
capeFactor = INV_CAPE_FACTOR
end
local itemLvl = args["itemlvl"] or 1
itemLvlFactor = chargeDrainItemLvl(itemLvl)
local tier = smw[1]["Usage Cost Invention Tier"]
local slot = smw[1]["Usage Cost Invention Slot"]
slot = string.lower(slot)
local effectiveTier = tier
if tier < 70 then
effectiveTier = 67
end
local slotMult = DIVINE_CHARGE_SLOTS[slot]["chargeDrainMult"]
local chargesPerHour = divineChargeCalc(effectiveTier,slotMult)*researchFactor*capeFactor*itemLvlFactor
local costPerHour = geprice("Divine charge") * chargesPerHour / CHARGES_PER_DIVINE_CHARGE
return round(costPerHour)
end
function geprice(item)
return GEPRICES[item]
end
function round(n)
return n % 1 >= 0.5 and math.ceil(n) or math.floor(n)
end
function coins(n)
return amount(n,"coins")
end
function parseMethods(args)
local methods = {}
for i=1,MAX_NUM_METHODS,1 do
local mth = 'method'..i
local method = {
["items"] = {}
}
local itemTypeCount = 0
for j=1,MAX_NUM_ITEM_TYPES,1 do
local itemx = args[mth.."item"..j]
if itemx then
itemTypeCount = itemTypeCount+1
local qtyx = args[mth.."qty"..j] or 1
local qtyret = tonumber(qtyx) or frac(qtyx) or 1
local item = {
["name"] = itemx,
["qty"] = qtyret
}
table.insert(method["items"],item)
end
end
if #method["items"]>0 then
local description = args[mth.."desc"]
method["description"] = description
local smithing = args[mth.."smithing"] or false
smithing = yesno(smithing)
if smithing == nil then
smithing = false
end
method["smithing"] = smithing
table.insert(methods,method)
end
end
return methods
end
function NA_cell(row)
row:tag("td") :wikitext("N/A") :attr("style","text-align center") :attr("class","table-na") :done()
end
function parseDurations(args,methods)
for i,method in ipairs(methods) do
local duration = args["duration"]
if duration == nil then
duration = args["method"..i.."duration"]
end
if duration then
duration = tonumber(duration) or frac(duration) or 1
method["duration"] = duration
end
end
return methods
end
function minutesToString(minutes)
local out = {}
local working = minutes
local working2
if working > 60 then
working2 = math.floor(working/60)
table.insert(out, string.format('%d hour%s', working2, plural(working2, '', 's')))
working = working - working2 * 60
end
if working > 0 then
table.insert(out, string.format('%d minute%s', working, plural(working, '', 's')))
end
return table.concat(out, ' ')
end
function plural(x, sing, plrl)
if x == 1 then
return sing
end
return plrl
end
function onlyCoins(method)
return #method["items"] == 1 and method["items"][1]["name"] == "Coins"
end
function calculateMethodCost(method)
local cost = 0
for _,item in ipairs(method["items"]) do
local pricePerItem
-- Handle Coins since they have no GEP
if item["name"] == "Coins" then
pricePerItem = 1
else
pricePerItem = geprice(item["name"])
if pricePerItem == nil then
item["unGEable"] = true
success, pricePerItem = pcall(calcvalue,item["name"],false)
if success then
item["calcvalue"] = true
else
pricePerItem = 0
end
end
end
cost = cost + (pricePerItem*item["qty"])
end
return cost
end
function smithingDiscountMult(smithing)
return (1-(smithing/200))
end
function divineChargeCalc(effectiveTier,chargeDrainMult)
return (effectiveTier-60)/8 * chargeDrainMult * 60 * 60
end
function chargeDrainResearch(lvl)
lvl = tonumber(lvl)
if lvl>=1 and lvl<34 then
return 1.00
elseif lvl<49 then
return 0.99
elseif lvl<64 then
return 0.97
elseif lvl<69 then
return 0.95
elseif lvl<78 then
return 0.93
elseif lvl<83 then
return 0.91
elseif lvl<91 then
return 0.88
elseif lvl<95 then
return 0.86
elseif lvl<105 then
return 0.83
elseif lvl<=120 then
return 0.80
else
error("Unexpected Invention level: "..lvl)
end
end
function chargeDrainItemLvl(lvl)
lvl = tonumber(lvl)
if lvl>=1 and lvl<5 then
return 1.00
elseif lvl<14 then
return 0.9
elseif lvl<18 then
return 0.875
elseif lvl<=20 then
return 0.85
else
error("Unexpected item level: "..lvl)
end
end
return p