Module:Sandbox/User:Gaz Lloyd/exherbs

Documentation for this module may be created at Module:Sandbox/User:Gaz Lloyd/exherbs/doc

--- this is a comment - from a -- to the end of a line
-- whatever is in a comment does nothing
--[=[
    this is a block comment, from --[(any number of =)[ to ](the same number of =)]
    you can use 0 but that isn't encouraged as it can be confused with wikilinks
    
    the -- when closing isn't required but it does make it line up nicely
--]=]

-- <nowiki>
-- a commented nowiki does nothing to the code but ensures there is no false whatlinkshere

--[======[
    STEP 1
    declare a main variable and define it as an empty table
    see also - step 3 at the end
--]======]
-- conventionally this is named p
local p = {}

--[==[
helper functions
these are up here as they generally need to be reasonably high up
--]==]
-- helper function that just gets GE prices
-- load in GE prices
-- loadData works slightly differently to require - if the relevant conditions are fulfilled, loadData should be used instead of require
local geps = mw.loadData('Module:GEPrices/data')
-- fetch the GE price out of the table or return 0 if it doesn't exist
function gep(item)
    return geps[item] or 0
end

-- another dependency loaded in here, we'll use this later
local curr = require('Module:Currency')._amount
-- a helper function to make coins images
function coins(x)
    return curr(string.format('%.2f',x), 'coins')
end

--[======[
    STEP 2
    assign functions into p
--]======]
-- `main` is a common name for the function that wikitext will call
-- the only passed input for the function called by wikitext is a Frame, often just called frame
function p.main(frame)
    -- it is common to strip out the arguments from the frame, then pass those into another function
    -- this is because we can then test the module in the console or without a template
    -- getParent() will error if there is no parent, which is the case in the console or without a template
    return p._main(frame:getParent().args)
end

-- this function is also part of p but can be called with just the arguments, i.e. in the console
function p._main(args)
    -- local is the keyword to declare a variable in the current scope
    local level
    -- level will be undefined if called outside of this function
    
    -- assigning to a variable just uses =
    level = args.FarmingLevel_Input

    -- declaring and assiging a variable at the same time
    local compost = args['Compost_Input']

    -- args is a table - which is like a map, associative array, or JSON object, if you are aware of those data structures
    -- tables have KEYS which are names, and VALUES assigned to those keys
    -- the above uses two equivalent methods to access the value with a key
    --     level uses a shortcut method - dot notation - that is only available if the key is a string, doesn't start with a number, and doesn't contain special characters like spaces or * etc
    --     compost uses the main way of accessing a value with a key - this will work for all data types and keys

    -- lets set up the rest of the variables we want
    -- we can assign multiple variables in one local by having a comma-separated list on each side of the =
    local secateurs, potion, aura, outfit = args.Secateurs_Input, args.Potion_Input, args.Aura_Input, args.Outfit_Input
    -- this isn't often used in this way, but can be useful in other situations

    -- now lets do some basic 'fixing' of our variables, making them more normalised for the rest of the code
    -- everything passed in from wikitext is a string, so if we want to do arithmetic we need to turn it into a number
    -- tonumber turns the input into a number, or returns nil if it isn't a number
    -- nil is a false value, so if it does return nil, the `or 1` will default it to 1
    level = tonumber(level) or 1

	-- now that level is a number we can enforce bounds
	-- first make sure it is an integer
	level = math.floor(level) -- flooring rounds down to the nearest integer - floor(3.1) = floor(3.9) = 3
	-- now bounds
	-- for this we use an if statement
	if level < 1 then
		-- if the level is less than 1 then make level 1
		level = 1
	elseif level > 99 then
		-- this if statement will only be triggered if level is not less than 1
		level = 99
	end
	-- this could be split into
	--[=[
	if level < 1 then
		level = 1
	end
	if level > 99 then
		level = 99
	end
	--]=]
	-- however this is slightly less efficient, as every time you check both conditions
	-- in the above, you only check the second condition if the first condition is false
	-- which makes it very slightly more efficient in that case
	-- its also clearer in general
	
    -- string.lower converts the input string to lowercase
    -- if compost is nil, it will default to 'none'
    compost = string.lower(compost or 'none')
    -- this also demonstrates a string datatype, which is marked with apostrophes ' or quotes "
    -- they are both equivalent, other than inside a '-string you don't need to escape ", and vice versa

    -- do basically the same to other variables
    aura = string.lower(aura or 'none')
    outfit = string.lower(outfit or 'none')

    -- lets do something else for secateurs and potion
    -- since these are checkboxes, they can be represented with booleans (true or false)
    -- lets use an if statement to check
    -- first lowercase
    secateurs = string.lower(secateurs or 'no')
    -- now we can check
    --[==[
    if secateurs == 'yes' then
    	secateurs = true
    else
    	secateurs = false
    end
    --]==]
    --HOWEVER this is entirely unnecessary use of an if statement
    -- for this we should really just use
    secateurs = secateurs == 'yes'
    -- `secateurs == 'yes'` is results in a boolean, we can assign it to a variable like anything else
    
    -- in this calculator the yes and no are the only options, but it is usually good to check outside of that for other ways of saying yes and no
    -- eg the strings y/true/1 and n/false/0 might be used instead
    -- while we can set up our own if statements to test this, we already have functions that convert strings to booleans based on yes and no
    -- this is at [[Module:Yesno]] and we can load it in using require
    -- (note that this is usually done at the top of the module, just before or just after step 1, but for demonstration I'm doing it here)
    local yesno = require('Module:Yesno')
    -- module:yesno just returns a function, so we can use it like this
    potion = yesno(potion or '', false)
    -- this call ensures that if potion is not defined, it has a definition
    --     and that if potion is an invalid value for yes/no, it is just defaulted to false

    -- for demo purposes, i'm going to now call a new function to actually build the table
    local ret = makeTable(level, compost, aura, outfit, secateurs, potion)

    -- make sure to return the relevant thing at the end
    return ret
end


--[=[
    data tables
--]=]
-- before moving on to the function, we're going to setup some data tables
-- again, this is usually at the top, between step 1 and 2
-- but for demonstration they're here

-- these data tables are for mapping from the values passed from the frame into the actual values we care about
local compostInfo = {
    none = 3,
    compost = 4,
    supercompost = 5,
    ['ultracompost'] = 6
    -- much like above, the full way uses [] but we can omit them if the key is a simple string
}

local potionInfo = {
    [true] = 1/3,
    [false] = 0
    -- since we're using the boolean types true and false and not strings
    -- we have to use bracket notation
}

local auraInfo = {
    none = 0,
    ['basic (3%)'] = 0.03,
    ['greater (5%)'] = 0.05,
    ['master (7%)'] = 0.07,
    ['supreme (10%)'] = 0.1,
    ['legendary (15%)'] = 0.15
    -- these contain spaces and % which are special, so []
}

local outfitInfo = {
    none = 0,
    ['crop farmer outfit'] = 0.05,
    ['master farmer outfit'] = 0.07
    -- spaces so []
}

-- this is the main data table containing all the information about the herbs
-- the outer table is using another alternative notation, providing no keys at all
-- this makes it behave like a standard array in other langs - the keys will begin at 1 (the number 1 not the string '1' - these are different keys!) and increment by 1 for each un-keyed item in it
local herbsInfo = {
    {
        name = 'Guam', -- herb name
        level = 1, -- planting level
        plant = 11, -- planting xp
        harvest = 12.5, -- harvesting level
        chance = { -- chances
            [true] = { -- WITH secateurs
                [1] = 0.1059, -- level 1
            },
            [false] = { -- WITHOUT secateurs
                [1] = 0.0980, -- level 1
            },
        }
    },
    {
        name = 'Goutweed',
        level = 29,
        plant = 105,
        harvest = 45,
        chance = {
            [true] = {
                [1] = 0.1647,
            },
            [false] = {
                [1] = 0.1529,
            }
        },
        herbname = 'Goutweed', -- name override
        seed = 'Gout tuber', -- seed name override
        herbprice = gep('Grimy guam') * 0.262 + gep('Grimy marrentill')*0.156 + gep('Grimy tarromin')*0.122 + gep('Grimy harralander')*0.118 + gep('Grimy ranarr')*0.060 + gep('Grimy irit')*0.066 + gep('Grimy wergali')*0.084 + gep('Grimy avantoe')*0.046 + gep('Grimy kwuarm')*0.022 + gep('Grimy cadantine')*0.026 + gep('Grimy lantadyme')*0.022 + gep('Grimy dwarf weed')*0.016 -- price override
    },
    {
        name = 'Bloodweed',
        level = 57,
        plant = 72,
        harvest = 81.4,
        chance = {
            [true] = {
                [1] = 0.2431,
                [99] = 0.3765 -- level 99 override
            },
            [false] = {
                [1] = 0.2235,
                [99] = 0.3451 -- level 99
            },
        }
    },
    -- fill in with more herbs
}

local defaultChances = {
	[true] = 0.3451,
	[false] = 0.3137
}

function makeTable(level, compost, aura, outfit, secateurs, potion)
    -- declare t as a mw.html object
    -- mw.html is used to make html tag creation generally easier
    local t = mw.html.create('table')

    -- this is a method of the mw.html object
    -- this is actually syntactic sugar for mw.html.addClass(t, 'wikitable'), but using that is dumb if you don't have to
    t:addClass('wikitable')
    
    -- add more classes
    -- I like to have numbers aligned right except for levels, which are aligned center
    -- since only 3 columns aren't aligned right, use style to align right then override with classes
    t:css('text-align', 'right')
    t:addClass('align-left-1 align-center-2 align-center-3 ')

    -- you can chain this syntax, if the object returns a value that is an object (which all of mw.html does)
    -- whitespace doesn't matter so it is good to indent to make the structure clear
    t:tag('tr') -- create a table row (tr)
        :tag('th') -- create a table header cell (th)
            :wikitext('Herb type') -- add some content to the th
            :attr('colspan', 2) -- add an attribute
            :addClass('unsortable') -- add a class
        :done() -- mark that we are done with the th, which means that the next command is on the tr
        :tag('th') -- create another th
            :wikitext('[[File:Farming.png|20px|frameless|link=Farming]] level') -- we can't call a template inside lua so just put the file in manually (e.g. we can't do {{scm|farming}})
        :done()
        -- and now the rest of the headers
        :tag('th')
            :wikitext('Planting XP')
        :done()
        :tag('th')
            :wikitext('Harvest XP')
        :done()
        :tag('th')
            :wikitext('Seed value')
        :done()
        :tag('th')
            :wikitext('Herb value')
        :done()
        :tag('th')
            :wikitext('&nbsp;')
            :addClass('unsortable')
            :attr('rowspan', #herbsInfo+1) -- make this as long as the herbsInfo table, plus 1 for the header
        :done()
        :tag('th')
        -- we're not going to put the references here for now, since they are complicated
        -- if you are interested I can add them later
            :wikitext('Actions')
        :done()
        :tag('th')
            :wikitext('Yield')
        :done()
        :tag('th')
            :wikitext('Gross profit')
        :done()
        :tag('th')
            :wikitext('Net profit')
        :done()
        :tag('th')
            :wikitext('Expected XP')
        :done()
        :tag('th')
            :wikitext('Expected GP/XP')
        :done()

    -- header is now done!
    -- we can call :done() or :addDone() here but it isn't necessary since we are now done chaining, starting a new statement wil work just fine

    -- now we're going to iterate the entire herbs table
    -- but before that, there are a few things we'll be using over the entire iteration, so setup a few variables
    -- get the compost number from the info, default to none if an invalid value was provided
    -- for the calculator you shouldn't ever have an invalid value, but it is good practice to check anyway (unless you intentionally want it to script error)
    local compostVal = compostInfo[compost] or compostInfo.none
    local potionVal = potionInfo[potion] or potionInfo[false]
    local auraVal = auraInfo[aura] or auraInfo.none
    local outfitVal = outfitInfo[outfit] or outfitInfo.none

    -- since these are always just added together anyway
    local yieldMultiplier = 1 + potionVal + auraVal + outfitVal

    -- ok now we can begin the iteration
    -- this uses a common form of the for statement
    -- you can read this as "declare the variables i and v; for every item in the herbsInfo table in order, assign the key to i and the value to v, then execute the code inside"
    -- if that doesn't make sense, don't worry - if you've never encountered loops before, they can be a brainbender
    for i,v in ipairs(herbsInfo) do
        -- its usually good to imagine how this will run over a typical entry in the table (e.g. guam)
        -- and then imagine it running over a less typical entry (e.g. goutweed)

        -- create a tr inside the return table, and store it
        local tr = t:tag('tr')
        
        -- there's a number of things happening right here
        -- 1) get the herbname from the current herb table we're looking at, and use that if it exists
        -- 2) if herbname doesn't exist it will be nil and thus we trigger the other side of the or
        -- 3) we take the string 'Grimy ' (with a space) and concatenate it (the .. operator) to
        -- 4) the name from the current herb table, lowercased - if we know that the variable is a string, we can do the : syntactic sugar like on mw.html objects (this is equivalent to string.lower(v.name))
        local gherb = v.herbname or ('Grimy '..v.name:lower())

        -- we'll use these a few times, so lets set up some variables for prices
        -- seed price: 2 steps of overrides - an explicit seedprice
        --      if not set, find the gep of the seed name
        --      if not set, find the gep of the herb name + seed
        local seedprice = v.seedprice or gep(v.seed or (v.name .. ' seed'))
        -- same for herb, but we've already calculated the 'grimy herb' name
        local herbprice = v.herbprice or gep(v.herb or gherb)

        -- calculate the various values
        -- just define these important variables here, fill in later - these go into the table
        local actions, yield, grossprofit, netprofit, xp, gpxp

        -- workings, though could reasonably be reduced into fewer lines if wanted
        local chances, gradient, constant, ymxc
        chances = v.chance[secateurs]
        -- apply default chances
        -- `not chances[99]` checks that `chances[99]` is not a false value
        -- in lua a false value is anything that is or evaluates to `false` or `nil`
        -- notably, the number 0, the string 0, an empty string, an empty table are all true values (and thus all numbers, strings, tables, and functions are considered true)
        -- so this effectively checks if `chances[99]` is not defined
        -- if it isn't defined, it returns nil which is a false value, so we then apply the default
        if not chances[99] then
        	chances[99] = defaultChances[secateurs]
        end
        -- this form of if statement is very common, checking for definition
        -- for a wider form of defined checking (which additionally checks for the string to contain some non-whitespace characters, like the parser function #if), use [[Module:Paramtest]]'s has_content/is_empty functions
        -- this if statement is used for demonstration, as you could also do it with `chances[99] = chances[99] or defaultChances[secateurs]`
        
        gradient = (chances[99] - chances[1]) / (99-1)
        constant = chances[1] - gradient
        ymxc = gradient * level + constant
        -- actions
        actions = compostVal / (1 - ymxc)
        -- yield
        yield = actions * yieldMultiplier
        
        -- profit
        grossprofit = herbprice * yield
        netprofit = grossprofit - seedprice
        -- TODO - compost prices, scroll of life, death chance

        -- xp
        xp = v.plant + yield * v.harvest
        gpxp = netprofit / xp

        -- fill in the row
        tr  :tag('td') -- td tag is a normal cell
                :wikitext(string.format('[[File:%s.png|link=%s]]', gherb, gherb)) -- file cell
            -- this uses string.format, which is a very useful function to prevent repeatedly concatenating strings (which can be a slow operation)
            -- essentially, each time you see %s in the string, that will be replaced by the corresponding value from the rest of the arguments (in this case we have 2 %s's and they're replaced by the value of gherb both times)
            -- string.format has a lot more complexities but that is a task for the full documentation to explain
            :done()
            :tag('td')
                :wikitext('[['..gherb..']]') -- could use format but for 2 concats/1 subst, probably doesn't matter enough
            :done()
            -- nothing special about these
            :tag('td')
                :wikitext(v.level)
            :done()
            :tag('td')
                :wikitext(v.plant)
            :done()
            :tag('td')
                :wikitext(v.harvest)
            :done()
            :tag('td')
            	:wikitext(coins(seedprice))
                -- pass this in to the coins helper function defined below, so that we get the value formatted with the coins image
            :done()
            :tag('td')
            	:wikitext(coins(herbprice))
            :done()
            :tag('td')
                :wikitext(string.format('%.2f', actions))
                -- this format uses %f, which is a floating point number, with the precision modifier .2
                -- this means there can be any number of digits to the left of the decimal point, and 2 digits to the right of the decimal point
                -- i.e. this rounds to 2 decimal places
            :done()
            :tag('td')
                :wikitext(string.format('%.2f', yield))
            :done()
            :tag('td')
                :wikitext(coins(grossprofit))
            :done()
            :tag('td')
                :wikitext(coins(netprofit))
            :done()
            :tag('td')
                :wikitext(string.format('%.2f', xp))
            :done()
            :tag('td')
                :wikitext(coins(gpxp))
            :done()

    end
    -- NB ipairs is used to loop array-tables where the keys are integers from 1 to n with no gaps
    -- to loop a map-table, use pairs
    -- be aware that ipairs guarantees the order is preserved, while pairs does not guarantee an order

    -- make sure to return the table we created
    return t
    -- we can call tostring on t if we wanted to, but this will be done automatically when going back from lua to wikitext
end

--[======[
    STEP 3
    return the main variable
--]======]
return p
-- in modern mediawiki, nowiki needs to be closed to be effective
-- </nowiki>