Module:Sandbox/User:Gaz Lloyd/exherbs
Jump to navigation
Jump to search
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(' ')
: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>