RuneScape Wiki
mNo edit summary
mNo edit summary
 
(31 intermediate revisions by the same user not shown)
Line 45: Line 45:
 
local args = frame:getParent().args
 
local args = frame:getParent().args
 
local pctExpBoost = 0 -- Account for outfits, avatar, tools, etc
 
local pctExpBoost = 0 -- Account for outfits, avatar, tools, etc
local pctExpBoostSw = 0
 
 
local flatExpBoost = 0 -- Account for flat experience boosts
 
local flatExpBoost = 0 -- Account for flat experience boosts
 
local currLv, goalLv, currXP, goalXP, remaining
 
local currLv, goalLv, currXP, goalXP, remaining
Line 57: Line 56:
 
-- These sub-sections have different table elts than their parent skill
 
-- These sub-sections have different table elts than their parent skill
 
local exceptions =
 
local exceptions =
{"Agility-Other", "Conversion", "Firemaking-Barbarian", "Firemaking-Char",
+
{"Agility-Other", "Divination-Boons", "Divination-Dungeoneering", "Divination-Memory-storage bots", "Divination-Other",
"Firemaking-Other", "Fishing-Dungeoneering", "Flatpacks", "Forging", "Masters",
+
"Farming-Curing", "Farming-Gathering", "Farming-Manure", "Firemaking-Barbarian", "Firemaking-Char", "Firemaking-Other",
"Milestones", "Multiples", "Rooms", "Runespan - Free", "Runespan - Members", "Scrolls",
+
"Fishing-Dungeoneering", "Flatpacks", "Forging", "Masters", "Milestones", "Multiples", "Rooms", "Runespan - Free",
"Summoning-Dungeoneering - Pouches", "Slayer-Items", "Tiaras", "Woodcutting-Other"}
+
"Runespan - Members", "Scrolls", "Summoning-Pets", "Summoning-Dungeoneering - Pouches", "Slayer-Assignments", "Tiaras",
  +
"Thieving-Sorceress' Garden", "Thieving-Safes", "Woodcutting-Other"}
  +
local exceptionsAgility =
  +
{"Milestones", "Multiples", "Other"}
 
local exceptionsDiv =
  +
{"Conversion", "Conversion /w Energy", "Divine locations", "Milestones", "Signs and Portents", "Transmutation"}
  +
local exceptionsSlayer =
  +
{"Items", "Masters", "Milestones"}
 
local bonusExceptions =
 
local bonusExceptions =
 
{"Urns", "Dungeoneering", "Blast furnace"}
 
{"Urns", "Dungeoneering", "Blast furnace"}
Line 68: Line 74:
 
-- Hold bonuses and boosts
 
-- Hold bonuses and boosts
 
local bonusPct = pctExpBoost
 
local bonusPct = pctExpBoost
  +
local boostPctSw = pctExpBoostSw
 
 
 
-- Gather all relative experience boosts to find new base experience
 
-- Gather all relative experience boosts to find new base experience
 
if not (args.avatar == nil) and not (args.disp == "Urns") then
 
if not (args.avatar == nil) and not (args.disp == "Urns") then
Line 104: Line 109:
 
 
 
if not (args.extra == nil) and args.abyss == nil then
 
if not (args.extra == nil) and args.abyss == nil then
pctExpBoost = pctExpBoost +
+
if not (args.skill == "Divination" and args.disp == "Harvest") then
bonusGenerator
+
pctExpBoost = pctExpBoost +
{
+
bonusGenerator
skill = args.skill,
+
{
category = args.disp,
+
skill = args.skill,
object = "extra",
+
category = args.disp,
item = args.extra
+
object = "extra",
}
+
item = args.extra
 
}
 
end
 
end
 
end
 
 
Line 155: Line 162:
 
 
 
if not (args.wild == nil) then
 
if not (args.wild == nil) then
pctExpBoost, pctExpBoostSw = pctExpBoost +
+
pctExpBoost = pctExpBoost +
 
bonusGenerator
 
bonusGenerator
 
{
 
{
Line 182: Line 189:
 
-- Try to catch Silverhawk feathers early..
 
-- Try to catch Silverhawk feathers early..
 
if args.disp == "Silverhawk Feathers" then
 
if args.disp == "Silverhawk Feathers" then
 
 
local message = silverhawkFeathers(currLv, goalLv, currXP, goalXP, remaining, pctExpBoost)
 
local message = silverhawkFeathers(currLv, goalLv, currXP, goalXP, remaining, pctExpBoost)
 
local msgRet = mw.html.create('div'):css({['font-size'] = "16px", ['font-weight'] = "bold"}):wikitext(message)
 
local msgRet = mw.html.create('div'):css({['font-size'] = "16px", ['font-weight'] = "bold"}):wikitext(message)
 
return tostring(msgRet)
 
return tostring(msgRet)
 
end
 
end
  +
 
  +
-- Prevent the Wilderness Agility Course with no Demonic Skull bonus from displaying a table (same XP for every level)
  +
if args.disp == "Wilderness Agility Course" and (args.wild == "No" or args.wild == "Wilderness Sword 2+") then
  +
local message = wildernessAC(currLv, goalLv, currXP, goalXP, remaining, pctExpBoost)
  +
local msgRet = mw.html.create('div'):css({['font-size'] = "16px", ['font-weight'] = "normal"}):wikitext(message)
  +
return tostring(msgRet)
 
end
  +
 
-- Hold skill-related data
 
-- Hold skill-related data
 
local dataRet, data
 
local dataRet, data
Line 206: Line 219:
 
-- Find columns from pool
 
-- Find columns from pool
 
-- Some require specific parameters due to common phrases
 
-- Some require specific parameters due to common phrases
 
-- Is it possible to set a default value for "elts" as
 
-- '', 'Level', 'Name'
 
-- and then add any additional values to it from Module:Skill_calc/elts
 
-- eg (Construction):
 
-- 'XP', '# Needed', '(Base) Material', 'Cost per'
 
 
--]=]
 
--]=]
 
local eltsRet = require('Module:Skill calc/elts')
 
local eltsRet = require('Module:Skill calc/elts')
 
elts = eltsRet[args.skill]
 
elts = eltsRet[args.skill]
 
 
-- Filter out exceptions
+
-- Filter out exceptions (re-organized to allow skill-specific filters to catch some exceptions early)
  +
-- Agility simplified
if findItem(exceptions, args.disp) or findItem(exceptions, args.skill .. '-' .. args.disp) then
 
elts = eltsRet[args.skill .. "-" .. args.disp]
+
if args.skill == "Agility" and findItem(exceptionsAgility, args.disp) then elts = eltsRet[args.skill .. "-Basic"]
  +
-- Divination simplified
  +
elseif args.skill == "Divination" and findItem(exceptionsDiv, args.disp) then elts = eltsRet[args.skill .. "-Profit"]
  +
-- Slayer simplified
  +
elseif args.skill == "Slayer" and findItem(exceptionsSlayer, args.disp) then elts = eltsRet[args.skill .. "-Basic"]
 
elseif findItem(exceptions, args.disp) or findItem(exceptions, args.skill .. '-' .. args.disp) then
  +
elts = eltsRet[args.skill .. "-" .. args.disp]
 
-- All urns share the same format; special exception
 
-- All urns share the same format; special exception
elseif args.disp == "Urns" then
+
elseif args.disp == "Urns" then elts = eltsRet[args.disp]
  +
-- Cooking in Daemonheim
elts = eltsRet[args.disp]
 
  +
elseif args.skill == "Cooking" and args.disp == "Dungeoneering" then elts = eltsRet["Cooking-Dungeoneering"]
 
-- Basic dungeon format
 
-- Basic dungeon format
elseif args.disp == "Dungeoneering" then
+
elseif args.disp == "Dungeoneering" then elts = eltsRet["Dungeons"]
elts = eltsRet["Dungeons"]
 
 
-- Dungeons with materials
 
-- Dungeons with materials
elseif string.find(args.disp, "Dungeoneering - ") then
+
elseif string.find(args.disp, "Dungeoneering - ") then elts = eltsRet["Dungeons-Materials"]
elts = eltsRet["Dungeons-Materials"]
 
 
-- Hunter methods with bait
 
-- Hunter methods with bait
 
elseif args.skill == "Hunter" and
 
elseif args.skill == "Hunter" and
 
(args.disp == "Nets and Sprites" or
 
(args.disp == "Nets and Sprites" or
 
args.disp == "Deadfall and Pitfall" or
 
args.disp == "Deadfall and Pitfall" or
args.disp == "Box Trapping") then
+
args.disp == "Box Trapping") then elts = eltsRet["Hunter-Bait"]
elts = eltsRet["Hunter-Bait"]
 
 
end
 
end
 
 
Line 239: Line 250:
 
 
 
for _, v in ipairs(data) do
 
for _, v in ipairs(data) do
 
 
--Leave common calculations outside of the function calls
 
--Leave common calculations outside of the function calls
 
 
--Material count
 
--Material count
 
local mcount = 1
 
local mcount = 1
Line 253: Line 262:
 
 
 
if v.material then
 
if v.material then
if ((v.mtrade ~= 0) or
+
if ((v.mtrade ~= 0) or (args.disp == "Urns") and not (v.currency)) then
(args.disp == "Urns") and
+
cost = gePrice(v.material, mcount) end
not (v.currency)) then
 
 
cost = gePrice(v.material, mcount)
 
end
 
 
elseif args.skill == "Prayer" then
 
elseif args.skill == "Prayer" then
 
if not (v.currency) then cost = gePrice({1, v.name}, 1)
 
if not (v.currency) then
+
else cost = v.value end
cost = gePrice({1, v.name}, 1)
+
elseif args.disp == "Scrolls" then cost = gePrice({1, v.familiarIcon}, 1)
else
 
cost = v.value
 
end
 
elseif args.disp == "Scrolls" then
 
cost = gePrice({1, v.familiarIcon}, 1)
 
 
end
 
end
  +
 
 
if v.trade ~= 0 and not v.currency and
 
if v.trade ~= 0 and not v.currency and
 
(args.skill ~= "Agility" and
 
(args.skill ~= "Agility" and
Line 275: Line 275:
 
args.skill ~= "Farming" and
 
args.skill ~= "Farming" and
 
args.skill ~= "Dungeoneering" and
 
args.skill ~= "Dungeoneering" and
args.skill ~= "Prayer") then
+
args.skill ~= "Prayer" and
+
args.skill ~= "Slayer") then
  +
 
if (args.skill == "Woodcutting" or
 
if (args.skill == "Woodcutting" or
 
args.skill == "Mining" or
 
args.skill == "Mining" or
Line 282: Line 283:
 
args.skill == "Divination") and not (v.icon == nil) then
 
args.skill == "Divination") and not (v.icon == nil) then
 
productCost = gePrice({1, v.icon}, 1)
 
productCost = gePrice({1, v.icon}, 1)
  +
 
 
elseif args.skill == "Hunter" then
 
elseif args.skill == "Hunter" then
if v.product then
+
if v.product then productCost = gePrice({1, v.product}, 1) end
productCost = gePrice({1, v.product}, 1) end
+
else productCost = gePrice({1, v.name}, 1) end
 
else
 
productCost = gePrice({1, v.name}, 1)
 
 
end
 
 
end
 
end
 
-- If a multiplier is set, it is applied to the product's value for profit calculations
 
-- If a multiplier is set, it is applied to the product's value for profit calculations
if v.multiplier then
+
if v.multiplier then productCost = productCost * v.multiplier end
  +
productCost = productCost * v.multiplier end
 
 
 
-- Check for other currencies
 
-- Check for other currencies
if (v.currency or v.currency2) and
+
if (v.currency or v.currency2) and args.skill ~= "Prayer" then
args.skill ~= "Prayer" then
+
productCost = v.value
productCost = v.value
+
if v.materialCost then cost = v.materialCost end
if v.materialCost then
 
cost = v.materialCost end
 
 
end
 
end
  +
 
 
-- Brewing makes two batches
 
-- Brewing makes two batches
if args.disp == "Brewing" then
+
if args.disp == "Brewing" then productCost = productCost * 2 end
  +
productCost = productCost * 2 end
 
 
 
--Establish any experience boosts
 
--Establish any experience boosts
 
local abyss = false
 
local abyss = false
if not (args.abyss == nil) and
+
if not (args.abyss == nil) and (args.abyss == "Yes" or args.abyss == "Demonic Skull") then
 
abyss = true end
(args.abyss == "Yes" or args.abyss == "Demonic Skull") then
 
  +
abyss = true end
 
 
 
-- Specific exception for popular prayer screen
 
-- Specific exception for popular prayer screen
 
if v.name == "Cleansing crystal" then pctExpBoost = (pctExpBoost / (prayerBoost or 1)) - 1 end
 
if v.name == "Cleansing crystal" then pctExpBoost = (pctExpBoost / (prayerBoost or 1)) - 1 end
Line 330: Line 321:
 
-- Calculate needed iterations
 
-- Calculate needed iterations
 
local needed
 
local needed
if not (unitExp == 0 or unitExp == nil) then
+
if not (unitExp == 0 or unitExp == nil) then needed = tonumber(math.ceil(remaining / unitExp))
needed = tonumber(math.ceil(remaining / unitExp))
+
else needed = 0 end
else
 
needed = 0
 
end
 
   
 
-- Decide Label
 
-- Decide Label
Line 341: Line 329:
 
if v.page then fileName = v.page end
 
if v.page then fileName = v.page end
 
if v.alt then fileName = v.alt end
 
if v.alt then fileName = v.alt end
  +
 
 
-- Icon extension
 
-- Icon extension
 
-- If needed, can be obtained in eltGenerator
 
-- If needed, can be obtained in eltGenerator
Line 347: Line 335:
 
local ext = ".png"
 
local ext = ".png"
 
if v.ext then ext = v.ext end
 
if v.ext then ext = v.ext end
  +
 
 
-- File name of Icon
 
-- File name of Icon
 
-- ##Antiquated, can be phased out
 
-- ##Antiquated, can be phased out
 
local icon = v.icon
 
local icon = v.icon
  +
 
 
-- Keep this as the first check to prevent double generation
 
-- Keep this as the first check to prevent double generation
 
if (args.testing == "active") then
 
if (args.testing == "active") then
  +
 
 
-- Pass the current elts as a variable for elt generation
 
-- Pass the current elts as a variable for elt generation
 
generatedElts = eltGenerator.generate_elts(
 
generatedElts = eltGenerator.generate_elts(
Line 370: Line 358:
 
}
 
}
 
})
 
})
  +
 
 
-- No Profit, No Loss skills
 
-- No Profit, No Loss skills
 
else
 
else
Line 378: Line 366:
   
 
elts = eltGenerator.generate_NoProfitNoLoss({args = {v,unitExp,needed,fileName,args,icon,ext,currLv}})
 
elts = eltGenerator.generate_NoProfitNoLoss({args = {v,unitExp,needed,fileName,args,icon,ext,currLv}})
  +
--elts = eltGenerator.generate_NoProfitNoLoss({args = {v,unitExp,needed,args,currLv}})
 
  +
 
-- No Loss, Profit skills (Gathering)
 
-- No Loss, Profit skills (Gathering)
 
elseif (args.skill == "Mining"
 
elseif (args.skill == "Mining"
Line 386: Line 375:
 
or args.skill == "Divination"
 
or args.skill == "Divination"
 
or args.skill == "Hunter") then
 
or args.skill == "Hunter") then
  +
 
 
elts = eltGenerator.generate_ProfitNoLoss({args = {v,unitExp,needed,fileName,cost,args,icon,ext,productCost,remaining,currLv}})
 
elts = eltGenerator.generate_ProfitNoLoss({args = {v,unitExp,needed,fileName,cost,args,icon,ext,productCost,remaining,currLv}})
  +
--elts = eltGenerator.generate_ProfitNoLoss({args = {v,unitExp,needed,cost,args,productCost,remaining,currLv}})
 
  +
 
-- No Profit, Loss skills (Survival)
 
-- No Profit, Loss skills (Survival)
 
elseif (args.skill == "Firemaking"
 
elseif (args.skill == "Firemaking"
Line 394: Line 384:
 
or args.skill == "Construction"
 
or args.skill == "Construction"
 
or args.skill == "Magic") then
 
or args.skill == "Magic") then
  +
 
 
elts = eltGenerator.generate_NoProfitLoss({args = {v,unitExp,needed,fileName,cost,args,productCost,currLv}})
 
elts = eltGenerator.generate_NoProfitLoss({args = {v,unitExp,needed,fileName,cost,args,productCost,currLv}})
  +
--elts = eltGenerator.generate_NoProfitLoss({args = {v,unitExp,needed,cost,args,productCost,currLv}})
 
  +
 
-- Profit and Loss skills (Artisan)
 
-- Profit and Loss skills (Artisan)
 
-- Fletching, Cooking, Farming, Smithing, Herblore, Summoning
 
-- Fletching, Cooking, Farming, Smithing, Herblore, Summoning
else
+
else
  +
 
 
elts = eltGenerator.generate_ProfitLoss({args = {v,unitExp,needed,fileName,cost,args,productCost,currLv}})
 
elts = eltGenerator.generate_ProfitLoss({args = {v,unitExp,needed,fileName,cost,args,productCost,currLv}})
  +
--elts = eltGenerator.generate_ProfitLoss({args = {v,unitExp,needed,cost,args,productCost,currLv}})
 
 
end
 
end
 
 
end
 
end
  +
 
-- ##Antiquated - needs to be moved into eltGenerator
 
if args.skill == "Thieving" then table.insert(elts,v.location) end
 
 
 
-- Allow for items with no level requirement
 
-- Allow for items with no level requirement
 
local levelRequired = 1
 
local levelRequired = 1
if args.skill == "Slayer" and args.disp == "Assignments" then
+
if args.skill == "Slayer" and
 
(args.disp == "Assignments" or args.disp == "Monsters") then
 
if v.level2 then levelRequired = v.level2 end
 
if v.level2 then levelRequired = v.level2 end
 
else
 
else
 
if v.level then levelRequired = v.level end
 
if v.level then levelRequired = v.level end
 
end
 
end
  +
 
 
local class = 'sg-yellow'
 
local class = 'sg-yellow'
if levelRequired > goalLv then
+
if levelRequired > goalLv then class = 'sg-red'
class = 'sg-red'
+
elseif levelRequired <= currLv then class = 'sg-green' end
  +
elseif levelRequired <= currLv then
 
 
if args.testing == "active" then tables._row(ret:tag('tr'):addClass(class), generatedElts, false)
class = 'sg-green'
 
 
else tables._row(ret:tag('tr'):addClass(class), elts, false)
end
 
if args.testing == "active" then
 
tables._row(ret:tag('tr'):addClass(class), generatedElts, false)
 
else
 
tables._row(ret:tag('tr'):addClass(class), elts, false)
 
 
end
 
end
 
end
 
end
  +
 
 
message = displayExp{display=args.disp, skill=args.skill, remaining=remaining,
 
message = displayExp{display=args.disp, skill=args.skill, remaining=remaining,
goalLv=goalLv, goalXP=goalXP, currLv = currLv, currXP = currXP,
+
goalLv=goalLv, goalXP=goalXP, currLv = currLv, currXP = currXP}
  +
bonusPct = bonusPct, boostPctSw = boostPctSw}
 
 
 
if (args.testing == "active") then
 
if (args.testing == "active") then
 
local testNotice = mw.html.create('div'):css({['font-size'] = "16px", ['font-weight'] = "bold"}):wikitext(testMessage)
 
local testNotice = mw.html.create('div'):css({['font-size'] = "16px", ['font-weight'] = "bold"}):wikitext(testMessage)
Line 467: Line 450:
 
local currXP = params.currXP
 
local currXP = params.currXP
 
local currLv = params.currLv
 
local currLv = params.currLv
local bonusPct = params.bonusPct
 
local boostPctSw = params.boostPctSw
 
   
msg = "To train " .. skill .. " from " .. commas(currXP) .. " experience (level " .. currLv .. ") to " .. commas(goalXP) .. " experience (level " .. goalLv .. "), " .. commas(remaining) .. " experience is required"
+
msg = "To train " .. skill .. " from " .. commas(currXP) .. " experience (level " .. currLv .. ") to " .. commas(goalXP) .. " experience (level " .. goalLv .. "), " .. commas(remaining) .. " experience is required."
  +
if skill == "Agility" then -- if bonusPct ~= 0 then
 
msg = msg .. " with a " .. (bonusPct*100)
 
--if not (boostPctSw == nil) then
 
--msg = msg .. "+" .. (boostPctSw/100)
 
--end
 
msg = msg .. "% bonus"
 
end
 
msg = msg .. "."
 
 
 
if display == "Flatpacks" then
 
if display == "Flatpacks" then
 
msg = msg .. "<div style='color:red; font-size:12px;'>Levels refer to the minimum needed to use the associated workbench if otherwise lower.</div>"
 
msg = msg .. "<div style='color:red; font-size:12px;'>Levels refer to the minimum needed to use the associated workbench if otherwise lower.</div>"
 
elseif display == "Char" then
 
elseif display == "Char" then
 
msg = msg .. "<div style='color:red; font-size:12px;'>[[Char's training cave]] can only be done once every week, for 10 minutes at a time.</div>"
 
msg = msg .. "<div style='color:red; font-size:12px;'>[[Char's training cave]] can only be done once every week, for 10 minutes at a time.</div>"
 
elseif display == "Boons" then
  +
msg = msg .. "<div style='color:red; font-size:12px;'>Each boon can only be made once.</div>"
 
end
 
end
 
local ret = mw.html.create('div'):css({['font-size'] = "16px", ['font-weight'] = "bold"}):wikitext(msg)
 
local ret = mw.html.create('div'):css({['font-size'] = "16px", ['font-weight'] = "bold"}):wikitext(msg)
Line 672: Line 647:
 
end
 
end
 
return 'To train from ' .. commas(currXP) .. ' experience (level ' .. currLv .. ') to ' .. commas(goalXP) .. ' experience (level ' .. goalLv .. '), ' .. commas(remaining) .. " experience is needed. This requires '''" .. commas(feathers) .. " [[Silverhawk feathers|feathers]]''' and, at current GE Prices, will cost " .. coins(gePrice({feathers,'Silverhawk feathers'},1)) .. ' coins.'
 
return 'To train from ' .. commas(currXP) .. ' experience (level ' .. currLv .. ') to ' .. commas(goalXP) .. ' experience (level ' .. goalLv .. '), ' .. commas(remaining) .. " experience is needed. This requires '''" .. commas(feathers) .. " [[Silverhawk feathers|feathers]]''' and, at current GE Prices, will cost " .. coins(gePrice({feathers,'Silverhawk feathers'},1)) .. ' coins.'
 
end
  +
  +
function wildernessAC(currLv, goalLv, currXP, goalXP, remaining, pctExpBoost)
  +
local lapsToDo = numbers(remaining / (571.4 * (1 + pctExpBoost)))
  +
  +
return 'To train from ' .. commas(currXP) .. ' experience (level ' .. currLv .. ') to ' .. commas(goalXP) .. ' experience (level ' .. goalLv .. '), ' .. commas(remaining) .. " experience is needed. This requires '''" .. commas(lapsToDo) .. " laps''' to be completed."
 
end
 
end
   

Latest revision as of 18:10, 13 September 2018

Documentation for this module may be created at Module:Skill calc/doc

--[=[
    dependencies
    [[Module:Addcommas]]
    [[Module:Tables]]
    [[Module:Coins]]
    [[Module:GETotal]]
    [[Module:Number]]
    [[Module:Experience]]
    [[Module:Silverhawks/data]]
    [[Module:Skill_calc/eltGenerator]]
    [[Module:Skill_calc/bonusGenerator]]
    TODO
        Get input on bonus stacking
--]=]

--[=[ 
        Note: Once all existing calculators have been converted, this Module will be cleaned
              up to remove any spaghetti code.
            
        To change data in this calculator, navigate to its appropriate page:
            e.g. Module:Skill calc/SKILLNAME/data
        Follow the guides on that page and try to emulate the items around it.
--]=]


-- <nowiki>

local p = {}

local commas        = require('Module:Addcommas')._add
local tables        = require('Module:Tables')
local coins         = require('Module:Coins')._amount
local gePrice       = require('Module:GETotal')._quantity
local numbers       = require('Module:Number')._round
local level         = require('Module:Experience').level_at_xp
local xp            = require('Module:Experience').xp_at_level
local featherExp    = require('Module:Silverhawks/data')

-- This houses most of the processing power
local eltGenerator  = require('Module:Skill_calc/eltGenerator')
local bonusGenerator = require('Module:Skill_calc/bonusGenerator')


function p.noValue(frame)
    local args = frame:getParent().args
    local pctExpBoost = 0               -- Account for outfits, avatar, tools, etc
    local flatExpBoost = 0              -- Account for flat experience boosts
    local currLv, goalLv, currXP, goalXP, remaining
    local icon,links,elts
    local prayerBoost -- This is needed for account for popular methods
    
    local message, testMessage
    if args.testing == "active" then 
        testMessage = "This calculator is being used to test new features." end
    
    -- These sub-sections have different table elts than their parent skill
    local exceptions = 
        {"Agility-Other", "Divination-Boons", "Divination-Dungeoneering", "Divination-Memory-storage bots", "Divination-Other", 
            "Farming-Curing", "Farming-Gathering", "Farming-Manure", "Firemaking-Barbarian", "Firemaking-Char", "Firemaking-Other", 
            "Fishing-Dungeoneering", "Flatpacks", "Forging", "Masters", "Milestones", "Multiples", "Rooms", "Runespan - Free", 
            "Runespan - Members", "Scrolls", "Summoning-Pets", "Summoning-Dungeoneering - Pouches", "Slayer-Assignments", "Tiaras", 
            "Thieving-Sorceress' Garden", "Thieving-Safes", "Woodcutting-Other"}
    local exceptionsAgility = 
        {"Milestones", "Multiples", "Other"}
    local exceptionsDiv = 
        {"Conversion", "Conversion /w Energy", "Divine locations", "Milestones", "Signs and Portents", "Transmutation"}
    local exceptionsSlayer = 
        {"Items", "Masters", "Milestones"}
    local bonusExceptions =
        {"Urns", "Dungeoneering", "Blast furnace"}
    -- These skills have no special considerations in Dungeoneering
    local basicDungeons =
        {"Mining", "Woodcutting"}
    -- Hold bonuses and boosts
    local bonusPct = pctExpBoost

    -- Gather all relative experience boosts to find new base experience
    if not (args.avatar == nil) and not (args.disp == "Urns") then 
        pctExpBoost = pctExpBoost + 
            bonusGenerator
                {
                    skill   = args.skill, 
                    object  = "avatar", 
                    pieces  = tonumber(args.avatar)
                }
    end
    
    if not (args.elite == nil) and not (args.disp == "Urns") then
        pctExpBoost = pctExpBoost + 
            bonusGenerator
                {
                    skill   = args.skill, 
                    object  = "elite", 
                    pieces  = tonumber(args.elite)
                }
    end
    
    -- Some bonuses can not be used on some sub-sections (E.G. Dungeoneering)
    if not findItem(bonusExceptions, args.disp) then
        if not (args.abyss == nil) then
            pctExpBoost = pctExpBoost + 
                bonusGenerator
                    {
                        skill   = args.skill, 
                        object  = "abyss", 
                        item    = args.abyss
                    }
        end
    
        if not (args.extra == nil) and args.abyss == nil then
            if not (args.skill == "Divination" and args.disp == "Harvest") then
                pctExpBoost = pctExpBoost + 
                    bonusGenerator
                        {
                            skill       = args.skill,
                            category    = args.disp,
                            object      = "extra", 
                            item        = args.extra
                        }
            end
        end
    
        if not (args.outfit == nil) then
            pctExpBoost = pctExpBoost + 
                bonusGenerator
                    {
                        skill   = args.skill, 
                        object  = "outfit", 
                        pieces  = tonumber(args.outfit)
                    }
        end
        
        if not (args.portable == nil) and (args.portable == "Yes") then
            pctExpBoost = pctExpBoost + 
                bonusGenerator
                    {
                        skill   = args.skill, 
                        object  = "portable", 
                        pieces  = 1
                    }
        end
        
        if not (args.tool == nil) then 
            flatExpBoost = flatExpBoost + 
                bonusGenerator
                    {
                        skill   = args.skill, 
                        object  = "tool", 
                        item    = args.tool
                    }
        end
        
        if not (args.altar == nil) then
            prayerBoost = bonusGenerator
                    {
                        skill   = args.skill,
                        object  = "altar",
                        item    = args.altar
                    }
            pctExpBoost = (pctExpBoost + 1) * prayerBoost
        end
        
        if not (args.wild == nil) then
            pctExpBoost = pctExpBoost + 
                bonusGenerator
                    {
                        skill   = args.skill, 
                        object  = "wild", 
                        item    = args.wild
                    }
        end

        if not (args.custom == nil) and args.abyss == nil then
            pctExpBoost = pctExpBoost + 
                bonusGenerator
                    {
                        skill       = args.skill,
                        category    = args.disp,
                        object      = "custom", 
                        item        = args.custom
                    }
        end
    end

    -- Translate goals into experience comparisons
    -- Calculate iterations to goal
    currLv, currXP, goalLv, goalXP, remaining = remainingExp(args.current, args.goal, args.currToggle, args.goalToggle)
    
    -- Try to catch Silverhawk feathers early..
    if args.disp == "Silverhawk Feathers" then
        local message = silverhawkFeathers(currLv, goalLv, currXP, goalXP, remaining, pctExpBoost)
        local msgRet = mw.html.create('div'):css({['font-size'] = "16px", ['font-weight'] = "bold"}):wikitext(message)
        return tostring(msgRet)
    end

    -- Prevent the Wilderness Agility Course with no Demonic Skull bonus from displaying a table (same XP for every level)
    if args.disp == "Wilderness Agility Course" and (args.wild == "No" or args.wild == "Wilderness Sword 2+") then
        local message = wildernessAC(currLv, goalLv, currXP, goalXP, remaining, pctExpBoost)
        local msgRet = mw.html.create('div'):css({['font-size'] = "16px", ['font-weight'] = "normal"}):wikitext(message)
        return tostring(msgRet)
    end

    -- Hold skill-related data
    local dataRet, data
    -- Grab Sub-Category Table Data
    if args.skill == "Slayer" and args.disp == "Assignments" then
        dataRet = require('Module:Skill calc/Slayer/Assignments/data')
        data = dataRet(args.disp,args.creature)
    else
        dataRet = require('Module:Skill calc/' .. args.skill .. '/data')
        data = dataRet(args.disp)
    end

    table.sort(data, function(a,b) return sortTable(a,b) end )
    
    local ret = mw.html.create('table'):addClass('wikitable sortable')

    --[=[    
    -- Find columns from pool
    -- Some require specific parameters due to common phrases
    --]=]
    local eltsRet = require('Module:Skill calc/elts')
    elts = eltsRet[args.skill]
    
    -- Filter out exceptions (re-organized to allow skill-specific filters to catch some exceptions early)
    -- Agility simplified
    if args.skill == "Agility" and findItem(exceptionsAgility, args.disp) then      elts = eltsRet[args.skill .. "-Basic"]
    -- Divination simplified
    elseif args.skill == "Divination" and findItem(exceptionsDiv, args.disp) then   elts = eltsRet[args.skill .. "-Profit"]
    -- Slayer simplified
    elseif args.skill == "Slayer" and findItem(exceptionsSlayer, args.disp) then    elts = eltsRet[args.skill .. "-Basic"]
    elseif findItem(exceptions, args.disp) or findItem(exceptions, args.skill .. '-' .. args.disp) then
                                                                                    elts = eltsRet[args.skill .. "-" .. args.disp]
    -- All urns share the same format; special exception
    elseif args.disp == "Urns" then                                                 elts = eltsRet[args.disp]
    -- Cooking in Daemonheim
    elseif args.skill == "Cooking" and args.disp == "Dungeoneering" then            elts = eltsRet["Cooking-Dungeoneering"]
    -- Basic dungeon format
    elseif args.disp == "Dungeoneering" then                                        elts = eltsRet["Dungeons"]
    -- Dungeons with materials
    elseif string.find(args.disp, "Dungeoneering - ") then                          elts = eltsRet["Dungeons-Materials"]
    -- Hunter methods with bait
    elseif args.skill == "Hunter" and
            (args.disp == "Nets and Sprites" or
             args.disp == "Deadfall and Pitfall" or
             args.disp == "Box Trapping") then                                      elts = eltsRet["Hunter-Bait"]
    end
    
    tables._row(ret:tag('tr'), elts, true)
    
    for _, v in ipairs(data) do
        --Leave common calculations outside of the function calls
        --Material count
        local mcount = 1
        if v.mcount then
            mcount = v.mcount
        end
        
        --Get total cost of materials
        local cost = 0
        local productCost = 0
        
        if v.material then
            if ((v.mtrade ~= 0) or (args.disp == "Urns") and not (v.currency)) then
                                                        cost = gePrice(v.material, mcount) end
        elseif args.skill == "Prayer" then
            if not (v.currency) then                    cost = gePrice({1, v.name}, 1)
            else                                        cost = v.value end
        elseif args.disp == "Scrolls" then              cost = gePrice({1, v.familiarIcon}, 1)
        end

        if v.trade ~= 0 and not v.currency and
            (args.skill ~= "Agility" and
             args.skill ~= "Construction" and
             args.skill ~= "Farming" and
             args.skill ~= "Dungeoneering" and
             args.skill ~= "Prayer" and
             args.skill ~= "Slayer") then

            if (args.skill == "Woodcutting" or 
                args.skill == "Mining" or 
                args.skill == "Runecrafting" or
                args.skill == "Divination") and not (v.icon == nil) then
                productCost = gePrice({1, v.icon}, 1)

            elseif args.skill == "Hunter" then
                if v.product then                       productCost = gePrice({1, v.product}, 1) end
            else                                        productCost = gePrice({1, v.name}, 1) end
        end
         -- If a multiplier is set, it is applied to the product's value for profit calculations
        if v.multiplier then                            productCost = productCost * v.multiplier end

        -- Check for other currencies
        if (v.currency or v.currency2) and args.skill ~= "Prayer" then
            productCost = v.value
            if v.materialCost then                      cost = v.materialCost end
        end

        -- Brewing makes two batches
        if args.disp == "Brewing" then                  productCost = productCost * 2 end

        --Establish any experience boosts
        local abyss = false
        if not (args.abyss == nil) and (args.abyss == "Yes" or args.abyss == "Demonic Skull") then 
                                                        abyss = true end

        -- Specific exception for popular prayer screen
        if v.name == "Cleansing crystal" then pctExpBoost = (pctExpBoost / (prayerBoost or 1)) - 1 end
        local unitExp = calculateBonus
            {
                base        = v.xp,
                currLv      = currLv,
                boost       = pctExpBoost,
                boostSw     = pctExpBoostSw,
                flatBoost   = flatExpBoost,
                abyss       = abyss,
                settings    = args,
                item        = v
            }

        -- Calculate needed iterations
        local needed
        if not (unitExp == 0 or unitExp == nil) then    needed = tonumber(math.ceil(remaining / unitExp))
        else                                            needed = 0 end

        -- Decide Label
        -- ##Antiquated, can be phased out
        local fileName = v.name
        if v.page then fileName = v.page end
        if v.alt then fileName = v.alt end

        -- Icon extension
        -- If needed, can be obtained in eltGenerator
        -- ##Antiquated. Phase out
        local ext   = ".png"
        if v.ext then ext = v.ext end

        -- File name of Icon
        -- ##Antiquated, can be phased out
        local icon  = v.icon

        -- Keep this as the first check to prevent double generation
        if (args.testing == "active") then

            -- Pass the current elts as a variable for elt generation
            generatedElts = eltGenerator.generate_elts( 
                            {   
                                args = 
                                    {
                                        v,
                                        args,
                                        unitExp,
                                        needed,
                                        remaining,
                                        cost,
                                        productCost,
                                        elts
                                    }
                            })

        -- No Profit, No Loss skills 
        else 
            if  (args.skill == "Agility"
              or args.skill == "Thieving"
              or args.skill == "Slayer") then

            elts = eltGenerator.generate_NoProfitNoLoss({args = {v,unitExp,needed,fileName,args,icon,ext,currLv}})
            --elts = eltGenerator.generate_NoProfitNoLoss({args = {v,unitExp,needed,args,currLv}})

            -- No Loss, Profit skills (Gathering)
            elseif (args.skill == "Mining"
                or  args.skill == "Fishing"
                or  args.skill == "Woodcutting"
                or  args.skill == "Runecrafting"
                or  args.skill == "Divination"
                or  args.skill == "Hunter") then

                elts = eltGenerator.generate_ProfitNoLoss({args = {v,unitExp,needed,fileName,cost,args,icon,ext,productCost,remaining,currLv}})
                --elts = eltGenerator.generate_ProfitNoLoss({args = {v,unitExp,needed,cost,args,productCost,remaining,currLv}})

            -- No Profit, Loss skills (Survival)
            elseif (args.skill == "Firemaking" 
                or  args.skill == "Prayer"
                or  args.skill == "Construction"
                or  args.skill == "Magic") then

                elts = eltGenerator.generate_NoProfitLoss({args = {v,unitExp,needed,fileName,cost,args,productCost,currLv}})
                --elts = eltGenerator.generate_NoProfitLoss({args = {v,unitExp,needed,cost,args,productCost,currLv}})

            -- Profit and Loss skills (Artisan)
            -- Fletching, Cooking, Farming, Smithing, Herblore, Summoning
            else

                elts = eltGenerator.generate_ProfitLoss({args = {v,unitExp,needed,fileName,cost,args,productCost,currLv}})
                --elts = eltGenerator.generate_ProfitLoss({args = {v,unitExp,needed,cost,args,productCost,currLv}})
            end
        end

        -- Allow for items with no level requirement
        local levelRequired = 1
        if args.skill == "Slayer" and 
            (args.disp == "Assignments" or args.disp == "Monsters") then
            if v.level2 then levelRequired = v.level2 end
        else
            if v.level then levelRequired = v.level end
        end

        local class = 'sg-yellow'
        if levelRequired > goalLv then                  class = 'sg-red'
        elseif levelRequired <= currLv then             class = 'sg-green' end

        if args.testing == "active" then                tables._row(ret:tag('tr'):addClass(class), generatedElts, false)
        else                                            tables._row(ret:tag('tr'):addClass(class), elts, false)
        end
    end

    message = displayExp{display=args.disp, skill=args.skill, remaining=remaining, 
        goalLv=goalLv, goalXP=goalXP, currLv = currLv, currXP = currXP}

    if (args.testing == "active") then
        local testNotice = mw.html.create('div'):css({['font-size'] = "16px", ['font-weight'] = "bold"}):wikitext(testMessage)
        return tostring(testNotice) .. tostring(message) .. tostring(ret)
    else 
        return tostring(message) .. tostring(ret)
    end
end

--[=[ displayExp
-- Creates a text string output for the goal calculations
-- Inputs:
--      params      Incoming parameters to generate string
--          - display   Current set of sub-categorical data
--          - skill     Current skill
--          - remaining Experience needed for goal
--          - goalXP    Expected experience
--          - goalLv    Expected level
--          - currXP    Current experience
--          - currLv    Current level
-- Returns:
--      msg     String created from params
--          -   this may be appended with a warning specifically for flatpacks
--]=]
function displayExp(params)
    local msg -- Converted from message to avoid conflict
    local display    = params.display
    local skill      = params.skill
    local remaining  = params.remaining
    local goalXP     = params.goalXP
    local goalLv     = params.goalLv
    local currXP     = params.currXP
    local currLv     = params.currLv

    msg = "To train " .. skill .. " from " .. commas(currXP) .. " experience (level " .. currLv .. ") to " .. commas(goalXP) .. " experience (level " .. goalLv .. "), " .. commas(remaining) .. " experience is required."

    if display == "Flatpacks" then
        msg = msg .. "<div style='color:red; font-size:12px;'>Levels refer to the minimum needed to use the associated workbench if otherwise lower.</div>"
    elseif display == "Char" then
        msg = msg .. "<div style='color:red; font-size:12px;'>[[Char's training cave]] can only be done once every week, for 10 minutes at a time.</div>"
    elseif display == "Boons" then
        msg = msg .. "<div style='color:red; font-size:12px;'>Each boon can only be made once.</div>"
    end
    local ret = mw.html.create('div'):css({['font-size'] = "16px", ['font-weight'] = "bold"}):wikitext(msg)
    return tostring(ret)
end

--[=[ remainingExp
-- Finds and returns experiences and levels based on inputs
-- Inputs:
--      curr	current value
--      goal	goal value
--      curr_intent		what the current is (level/experience)
--      goal_intent		what the goal is (level/experience)
-- Returns:
--      current level,
--      current experience,
--      goal level,
--      goal experience,
--      experience remaining
--]=]
function remainingExp(curr, goal, curr_intent, goal_intent)
    local goalLevel, currLevel, goalXP, currXP
    
    if curr_intent == "Level" and tonumber(curr) <= 120 then
        currLevel = tonumber(curr)
        currXP = xp({args = {curr}})
    else
        currLevel = level({args = {curr}})
        currXP = tonumber(curr)
    end
    
    if goal_intent == "Level" and tonumber(goal) <= 120 then
        goalLevel = tonumber(goal)
        goalXP = xp({args = {goal}})
    else
        goalLevel = level({args = {goal}})
        goalXP = tonumber(goal)
    end
    
    -- Prevent negative values
    local remaining = math.ceil(goalXP - currXP)
    if remaining < 0 then
        remaining = 0
    end
    return currLevel, currXP, goalLevel, goalXP, remaining
end

--[=[ calculateBonus
-- Inputs:
--      source        Incoming data
--          - base      Base experience for item
--          - boost     Percent experience boost, expressed as a decimal percentage
--              - ava       Avatar bonus
--              - outfit    Outfit bonus
--              - tools     Extra bonuses
--          - flatBoost Flat experience boost
-- Returns:
--  Numeric value of new base experience including bonuses
--]=]
function calculateBonus(source) 
    local total     = source.base        -- base experience
    local currLv    = source.currLv
    local boost     = source.boost       -- bonus percentage
    local boostSw   = source.boostSw     -- value not being set?
    local flatBoost = source.flatBoost
    local abyss     = source.abyss
    local settings  = source.settings    -- calculator
    local item      = source.item        -- ../data
    local cLv                           -- holder for current level

    if settings.disp == "Wilderness Agility Course" or 
        (item.page == "Wilderness Agility Course" and settings.disp ~= "Milestones") then
        if (settings.disp == "Wilderness Agility Course") then cLv = item.level
        else cLv = currLv end
        if settings.wild == "Demonic Skull" then
            total = total + 498.9
            if (cLv > 50) then total = (((cLv-21)*boost)*(total)) end
        elseif settings.wild == "Both" then
            total = ((total*((cLv-21)*boost))+((((cLv-21)*boost)+0.05)*498.9))
            --total = ((total*((cLv-21)*boost))+((((cLv-21)*boost)+boostSw)*498.9))
        elseif settings.wild == "Wilderness Sword 2+" then
            --total = total + (498.9*(1+boostSw))
            total = total + (498.9*1.05) -- 1+0.05
        else
            total = total + 498.9
        end
    elseif item.skill == "Firemaking" and item.bonus ~= nil then
        total = total + (item.bonus * (1 + boost))
    else
        if source.boost < 1 then 
            total = total * (1 + source.boost)
        else
            total = total * source.boost
        end
    end
    if not (settings == nil) then
        -- Check for additional modifiers. These must be done on an item to item basis
        --  to filter out items that may not be affected by certain boosts
        local potionSetting, vosSetting, auraSetting = ""
        local aotSetting, itemName = ""
        if settings.potion then potionSetting = settings.potion end
        if settings.vos then 
            if settings.vos == "Yes" then vosSetting = "VoiceOfSeren"
            else vosSetting = "No" end
        end
        if settings.aura then auraSetting = settings.aura end
        if settings.aot then 
            if settings.aot == "Yes" then aotSetting = "AvgOverTime"
            else aotSetting = "No" end
        end
        if  settings.altar  and
            settings.altar ~= "None" and
            item.name ~= "Cleansing crystal" then
             itemName = settings.altar 
        else itemName = item.name
        end
        
        if potionSetting ~= nil then
            -- Check for JuJu potion modifier
            total = total * 
                    bonusGenerator 
                        {
                            skill       = settings.skill,
                            name        = itemName,
                            object      = "potion",
                            item        = potionSetting,
                            setting     = vosSetting,
                            subSetting  = aotSetting
                        }
        end
        
        -- Check for VoS modifier   
        if vosSetting ~= "No" then
            total = total * 
                    bonusGenerator 
                        {
                            skill       = settings.skill,
                            name        = item.name,
                            object      = "VoiceOfSeren",
                            setting     = potionSetting,
                            subSetting  = aotSetting
                        }                    
        end
        
        if auraSetting ~= "No" then
            -- Check for aura modifier
            total = total * 
                    bonusGenerator 
                        {
                            skill   = settings.skill,
                            name    = item.name,
                            object  = "aura",
                            item    = auraSetting
                        }
        end
    end
    -- If the abyss is active, do not add 1
    --   Not sure how it all stacks :: NEED INPUT
    if source.abyss == true then total = source.base * source.boost end
    total = total + source.flatBoost
    
    return numbers(total,1)
end

--[=[ silverhawkFeathers
--  Inputs:
--      currlv      Current level
--      goalLv      Goal Level
--      currXP      Current Experience
--      goalXP      Goal Experience
--      remaining   Difference from goal
--      pctExpBoost Calculated boost to base exp
--  Returns:
--      String including needed feathers, remaining experience, and current GE cost for feathers
--]=]
function silverhawkFeathers(currLv, goalLv, currXP, goalXP, remaining, pctExpBoost)
    local feathers = 0
    local featherXP
    local desc = ""
    local workingXP = currXP
    local workingLv = currLv
    while workingXP < goalXP do
		feathers = feathers + 1
		workingLv = level({args = {workingXP}})
		featherXP = featherExp[workingLv] + (featherExp[workingLv] * pctExpBoost)

		workingXP = workingXP + featherXP
	end
	return 'To train from ' .. commas(currXP) .. ' experience (level ' .. currLv .. ') to ' .. commas(goalXP) .. ' experience (level ' .. goalLv .. '), ' .. commas(remaining) .. " experience is needed. This requires '''" .. commas(feathers) .. " [[Silverhawk feathers|feathers]]''' and, at current GE Prices, will cost " .. coins(gePrice({feathers,'Silverhawk feathers'},1)) .. ' coins.'
end

function wildernessAC(currLv, goalLv, currXP, goalXP, remaining, pctExpBoost)
    local lapsToDo = numbers(remaining / (571.4 * (1 + pctExpBoost)))

    return 'To train from ' .. commas(currXP) .. ' experience (level ' .. currLv .. ') to ' .. commas(goalXP) .. ' experience (level ' .. goalLv .. '), ' .. commas(remaining) .. " experience is needed. This requires '''" .. commas(lapsToDo) .. " laps''' to be completed."
end

-- Make it easier to find items in a set
-- A little heavy : if someone can find a better way, replace.
function findItem (list, item)
    local status = false
    for _,v in pairs(list) do
        if v == item then
            status = true
            break
        end
    end
    return status
end

function sortTable(a, b)
    local value = false
    if a.name and b.name and
            a.name == b.name and
            (a.title or b.title) then
            if a.title and not b.title then
                value = a.title < b.name
            elseif not a.title and b.title then
                value = a.name < b.title
            else 
                value = a.title < b.title end
    elseif a.level and b.level then
        value =  a.level < b.level
    else
        value =  a.xp < b.xp
    end
    
    return value
end

--[[
Modified version of GETotal.
Will add together the price of a * 4 and b / 4
Can recognize and parse vulgar fractions
All fractions and decimals will be truncated off the final price
Technically unlimited

To use:
variable 'a' = array of {quantity, value, "item name", etc...}
variable 'b' = number of unique items to be included
--]]
function getPrice(a,b)
	local values = a or {}
	local count  = b or 0
	local price  = 0
	local prices = {}
 
	for i=1 , (count*3) , 3 do
	    local valuex = a[i+1]
		local itemx  = a[i+2]
		if itemx then
			local qtyx = a[i] or 1
			local qtyret = tonumber(qtyx) or frac(qtyx) or 1
			table.insert(prices,math.floor(valuex * qtyret))
		end
    end

	for _, v in ipairs(prices) do
		price = price + v
	end
	return price
end

return p