Module:Skill calc: Difference between revisions

From Brighter Shores Wiki
Jump to navigation Jump to search
Content added Content deleted
(Just going to start out with the most simple version currently on the RSC wiki.)
 
No edit summary
 
Line 24: Line 24:
local p = {}
local p = {}


-- done
local commas = require('Module:Addcommas')._add
local commas = require('Module:Addcommas')._add
local tables = require('Module:Tables')
local tables = require('Module:Tables')
local range = require('Module:Enum').range

--todo
local coins = require('Module:Coins')._amount
local coins = require('Module:Coins')._amount
local gePrice = require('Module:GETotal')._quantity
local gePrice = require('Module:GETotal')._quantity
Line 31: Line 35:
local level = require('Module:Experience').level_at_xp
local level = require('Module:Experience').level_at_xp
local xp = require('Module:Experience').xp_at_level
local xp = require('Module:Experience').xp_at_level
local range = require('Module:Enum').range


-- This houses most of the processing power
-- This houses most of the processing power

Latest revision as of 09:23, 9 November 2024

Module documentation
This documentation is transcluded from Module:Skill calc/doc. [edit] [history] [purge]
This module does not have any documentation. Please consider adding documentation at Module:Skill calc/doc. [edit]
--[=[
    dependencies
    [[Module:Addcommas]]
    [[Module:Tables]]
    [[Module:Coins]]
    [[Module:GETotal]]
    [[Module:Number]]
    [[Module:Experience]]
    [[Module:Skill_calc/eltGenerator]]
    [[Module:Skill_calc/bonusGenerator]]
    TODO
        Get input on bonus stacking
        Making a module to handle bxp jericowrahl 11:25 am MST 11/02/18

        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 = {}

-- done
local commas        = require('Module:Addcommas')._add
local tables        = require('Module:Tables')
local range          = require('Module:Enum').range

--todo
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

-- 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 links,elts
    local prayerBoost -- This is needed to 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-Hall of Memories", "Divination-Hall of Memories (2 tick)", "Divination-Memory-storage bots", "Divination-Other", 
            "Farming-Curing Animals", "Farming-Raising Animals", "Farming-Manure", "Farming-Testing", "Firemaking-Barbarian", "Firemaking-Char", 
            "Firemaking-Other", "Firemaking-Incense Sticks (scratch + ashing)", "Prayer-Incense sticks", "Fishing-Dungeoneering", "Flatpacks", 
            "Forging", "Invention-Manufacturing", "Invention-Manufacturing - Ancient", "Masters", "Milestones", "Mining-Gems", "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 exceptionsSmithing = 
        {"Kethsian", "Masterwork (unfinished)", "Obsidian"}
    local bonusExceptions =
        {"Urns", "Dungeoneering", "Blast furnace"}
    -- These skills have no special considerations in Dungeoneering
    local basicDungeons =
        {"Mining", "Woodcutting"}
    -- elite skills
    local eliteSkills = {
    	['Invention'] = true
    }
    local isEliteSkill = eliteSkills[args.skill] or false
    -- 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
    
    -- Only elite that gives xp boost is dungeoneering
    if not (args.elite == nil) and not (args.disp == "Urns") and args.skill == "Dungeoneering" 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) and not (args.abyss == 'No') 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

        --[=[
        -- Anachronia spa
        if not (args.spa == nil) and args.disp == "Courses" then
        	pctExpBoost = pctExpBoost + 
                bonusGenerator
                    {
                        skill       = args.skill,
                        category    = args.disp,
                        object      = "spa", 
                        item        = args.spa
                    }
        end
        --]=]
		if not (tonumber(args.custom) == nil) then
			if args.abyss == nil or args.abyss == 'No' then
				pctExpBoost = pctExpBoost + 
					bonusGenerator
						{
							skill       = args.skill,
							category    = args.disp,
							object      = "custom", 
							item        = args.custom
						}
			end
        end
        
        if not (args.wild == nil) then
            pctExpBoost = pctExpBoost + 
                bonusGenerator
                    {
                        skill   = args.skill, 
                        object  = "wild", 
                        item    = args.wild
                    }
        end

        --[=[
        if not (args.age == nil) and args.disp == "Curing Animals" then
            pctExpBoost = bonusGenerator
                    {
                        skill       = args.skill,
                        category    = args.disp,
                        object      = "age", 
                        item        = args.age
                    }
        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, isEliteSkill)
    

    -- 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
    if (args.testing == "active" and args.disp == "Wilderness Agility Course") then
        if (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
    elseif args.disp == "Wilderness Agility Course" 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)
    elseif args.skill == "Smithing" and args.disp == "Equipment" then
        dataRet = require('Module:Skill calc/' .. args.skill .. '/data')
        data = dataRet(args.equipment)
    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 sticky-header')

    --[=[    
    -- 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)
    if args.skill == "Agility" and findItem(exceptionsAgility, args.disp) then      elts = eltsRet[args.skill .. "-Basic"]
    elseif args.skill == "Construction" and args.disp == "Other" then               elts = eltsRet[args.skill .. "-Other"]
    elseif args.skill == "Divination" and findItem(exceptionsDiv, args.disp) then   elts = eltsRet[args.skill .. "-Profit"]
    elseif args.skill == "Slayer" and findItem(exceptionsSlayer, args.disp) then    elts = eltsRet[args.skill .. "-Basic"]
    elseif args.skill == "Smithing" and args.disp == "Equipment" and findItem(exceptionsSmithing, args.equipment)
    																		then	elts = eltsRet["Smithing-Equipment"]
    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 == "Dungeons" 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"]
    elseif args.skill == "Hunter" and args.disp == "Big Game Hunting" then			elts = eltsRet["Hunter-BGH"]
    elseif args.testing == "active" and (args.skill == "Hunter" and 
    	    args.disp == "Box Trapping") then                                       elts = eltsRet["Hunter-Anachronia"]
    elseif args.skill == "Fishing" and args.disp == "Deep Sea" then			        elts = eltsRet["Fishing-Deep Sea"]
    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" and args.disp ~= 'Incense sticks') 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
             not ( args.skill == "Prayer" and args.disp ~= 'Incense sticks' ) and
             args.skill ~= "Invention" 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 not (args.skill == "Prayer" and args.disp ~= 'Incense sticks') 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
        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

        if ( args.skill == "Fletching" and string.find(args.disp, "Darts") ) then
        	if v.name == "Prototype Throwing Dart" then
        		needed = 0
        		local diffLv = goalLv - currLv
        		local remainingLevels = range(currLv, goalLv)
    			for j,w in ipairs( remainingLevels ) do
    				if w == currLv then
    					local lvlXPdiff
    					if diffLv > 0 then
    						lvlXPdiff = xp{ args = { w + 1 } } - currXP
    					else
    						lvlXPdiff = goalXP - currXP
    					end
    					needed = needed + lvlXPdiff / ( v.xp * w )
					elseif remainingLevels[j+1] then
						local lvlXPdiff = xp{ args = { w + 1 } } - xp{ args = { w } }
    					needed = needed + lvlXPdiff / ( v.xp * w )
					else
						local lvlXPdiff = goalXP - xp{ args = { w } }
    					needed = needed + lvlXPdiff / ( v.xp * w )
					end
				end
				needed = math.ceil(needed)
        	end
        end

        -- 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,
                                        currLv
                                    }
                            })

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

                elts = eltGenerator.generate_NoProfitNoLoss({args = {v,unitExp,needed,args,remaining,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,cost,args,productCost,remaining,currLv}})

            -- No Profit, Loss skills (Survival)
            elseif ( (args.skill == "Firemaking" and not string.find(args.disp, 'Incense') )
                or  (args.skill == "Prayer" and args.disp ~= 'Incense sticks')
                or  args.skill == "Construction"
                or  args.skill == "Magic") then

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

            -- Profit and Loss skills (Artisan)
            -- Fletching, Cooking, Farming, Smithing, Herblore, Summoning
            else
                elts = eltGenerator.generate_ProfitLoss({args = {v,unitExp,needed,cost,args,productCost,remaining,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
        levelRequired = math.floor(levelRequired)
        local class = 'table-bg-yellow sortbottom'
        if levelRequired > goalLv then                  class = 'table-bg-red sortbottom'
        elseif levelRequired <= currLv then             class = 'table-bg-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:0.9em;'>Levels refer to the minimum needed to use the associated workbench if otherwise lower.</div>"
        elseif display == "Boons" then
        msg = msg .. "<div style='color:red; font-size:0.9em;'>Each boon can only be made once.</div>"
    end
    local ret = mw.html.create('div'):css({['font-size'] = "1.1em", ['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, isElite)
    local goalLevel, currLevel, goalXP, currXP
    local elite = nil
    if isElite then
    	elite = '1'
    end
    
    if curr_intent == "Level" and tonumber(curr) <= 120 then
        currLevel = tonumber(curr)
        currXP = xp({args = {curr, elite = elite}})
    else
        currLevel = level({args = {curr, elite = elite}})
        currXP = tonumber(curr)
    end
    
    if goal_intent == "Level" and tonumber(goal) <= 120 then
        goalLevel = tonumber(goal)
        goalXP = xp({args = {goal, elite = elite}})
    else
        goalLevel = level({args = {goal, elite = elite}})
        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 settings.skill == "Agility" and item.name == "Anachronia Agility Course" then
    	--total = total * (1 + boost)
    elseif item.skill == "Firemaking" and item.bonus ~= nil then
        total = total + (item.bonus * (1 + boost))
    --elseif settings.skill == "Farming" and settings.disp == "Curing Animals" then
    	--total = math.floor((total * source.boost)*10)/10
    	--total = total * source.boost
    else
    	total = total * (1 + source.boost)
    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, aotSetting, altarSetting
        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
           item.name ~= "Cleansing crystal" then
			altarSetting = settings.altar
		else
			altarSetting = "None"
        end
        
        -- Check for JuJu potion modifier
        if potionSetting ~= nil then
            total = total * 
                    bonusGenerator 
                        {
                            skill       = settings.skill,
                            name        = item.name,
                            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
        
        -- Check for aura modifier
        if auraSetting ~= "No" then
            total = total * 
                    bonusGenerator 
                        {
                            skill   = settings.skill,
                            name    = item.name,
                            object  = "aura",
                            item    = auraSetting
                        }
        end
        
        -- Check for altar modifier
        if altarSetting ~= "None" then
            total = total * 
                    bonusGenerator 
                        {
                            skill   = settings.skill,
                            object  = "altar",
                            item    = altarSetting,
                            setting = settings.disp
                        }
    	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

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

    return "To train from '''" .. commas(currXP) .. " experience''' (level " .. currLv .. ") to '''" .. commas(goalXP) .. " experience''' (level " .. goalLv .. "), '''" .. commas(remaining) .. " experience''' is needed.<br/>This requires '''" .. commas(lapsToDo) .. " laps''' to be completed at '''" .. nXP .. " XP''' per lap."
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.level and b.level and a.level ~= b.level then
        value =  a.level < b.level
    elseif 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
    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