Module:Sandbox/User:Hypermice/Module:Skill calc

From the RuneScape Wiki, the wiki for all things RuneScape
Jump to: navigation, search
Module documentation
This documentation is transcluded from Template:Module sandbox/doc. [edit] [purge]

This module is a sandbox for Hypermice. It can be used to test changes to existing modules, prototype new modules, or just experimenting with lua features.

Invocations of this sandbox should be kept in userspace; if the module is intended for use in other namespaces, it should be moved out of the sandbox into a normal module and template.

This default documentation can be overridden by creating the /doc subpage of this module, as normal.

--[=[
    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

-- 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-Other", 
            "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-Items", "Slayer-Assignments", "Tiaras", "Woodcutting-Other"}
    local exceptionsDiv = 
        {"Conversion", "Divine locations", "Milestones", "Signs and Portents", "Transmutation"}
    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
    
    -- 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:Sandbox/User:Hypermice/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
    if findItem(exceptions, args.disp) or findItem(exceptions, args.skill .. '-' .. args.disp) then
                                                                                        elts = eltsRet[args.skill .. "-" .. args.disp]
    -- Divination simplified
    elseif args.skill == "Divination" and findItem(exceptionsDiv, args.disp) then       elts = eltsRet[args.skill .. "-Profit"]
    -- All urns share the same format; special exception
    elseif args.disp == "Urns" then                                                     elts = eltsRet[args.disp]
    -- 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") 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}})

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

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

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

            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
        local levelRequired = 1
        if args.skill == "Slayer" and args.disp == "Assignments" 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, bonusPct = bonusPct}

    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
    local bonusPct   = params.bonusPct

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

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

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