Module:Herb Farming calculator
Documentation for this module may be created at Module:Herb Farming calculator/doc
local p = {}
local coins = require('Module:Coins')._amount
local gePrices = mw.loadJsonData('Module:GEPrices/data.json')
local farmingData = mw.loadData('Module:Skill calc/Farming')
local paramTest = require('Module:Paramtest')
local yesNo = require('Module:Yesno')
local scp = require('Module:SCP')._main
local listToText = mw.text.listToText
local warningTag = ' <span style="color:red;"><b>*</b></span>'
local warningNote = '<span style="color:red;"><b>*</b></span> It costs more to use [[Resurrect Crops]] instead of replanting with a new seed. This is based on the cost of the seed with your chance to resurrect it.\n'
local herbs = {
{ ['name'] = 'Guam leaf', ['lowCTS'] = 25, ['highCTS'] = 80 },
{ ['name'] = 'Marrentill', ['lowCTS'] = 28, ['highCTS'] = 80 },
{ ['name'] = 'Tarromin', ['lowCTS'] = 31, ['highCTS'] = 80 },
{ ['name'] = 'Harralander', ['lowCTS'] = 36, ['highCTS'] = 80 },
{ ['name'] = 'Goutweed', ['lowCTS'] = 39, ['highCTS'] = 80 },
{ ['name'] = 'Ranarr weed', ['lowCTS'] = 39, ['highCTS'] = 80 },
{ ['name'] = 'Toadflax', ['lowCTS'] = 43, ['highCTS'] = 80 },
{ ['name'] = 'Irit leaf', ['lowCTS'] = 46, ['highCTS'] = 80 },
{ ['name'] = 'Avantoe', ['lowCTS'] = 50, ['highCTS'] = 80 },
{ ['name'] = 'Kwuarm', ['lowCTS'] = 54, ['highCTS'] = 80 },
{ ['name'] = 'Snapdragon', ['lowCTS'] = 57, ['highCTS'] = 80 },
{ ['name'] = 'Huasca', ['lowCTS'] = 58, ['highCTS'] = 80 },
{ ['name'] = 'Cadantine', ['lowCTS'] = 60, ['highCTS'] = 80 },
{ ['name'] = 'Lantadyme', ['lowCTS'] = 64, ['highCTS'] = 80 },
{ ['name'] = 'Dwarf weed', ['lowCTS'] = 67, ['highCTS'] = 80 },
{ ['name'] = 'Torstol', ['lowCTS'] = 71, ['highCTS'] = 80 },
}
local UnfinishedPotions = {
['Guam leaf'] = { ['herblore'] = 3, ['potion'] = 'Guam potion (unf)' },
['Marrentill'] = { ['herblore'] = 5, ['potion'] = 'Marrentill potion (unf)' },
['Tarromin'] = { ['herblore'] = 12, ['potion'] = 'Tarromin potion (unf)' },
['Harralander'] = { ['herblore'] = 22, ['potion'] = 'Harralander potion (unf)' },
['Goutweed'] = { ['herblore'] = 0, ['potion'] = 'Vial of water' },
['Ranarr weed'] = { ['herblore'] = 30, ['potion'] = 'Ranarr potion (unf)' },
['Toadflax'] = { ['herblore'] = 34, ['potion'] = 'Toadflax potion (unf)' },
['Irit leaf'] = { ['herblore'] = 45, ['potion'] = 'Irit potion (unf)' },
['Avantoe'] = { ['herblore'] = 50, ['potion'] = 'Avantoe potion (unf)' },
['Kwuarm'] = { ['herblore'] = 55, ['potion'] = 'Kwuarm potion (unf)' },
['Huasca'] = { ['herblore'] = 58, ['potion'] = 'Huasca potion (unf)' },
['Snapdragon'] = { ['herblore'] = 63, ['potion'] = 'Snapdragon potion (unf)' },
['Cadantine'] = { ['herblore'] = 66, ['potion'] = 'Cadantine potion (unf)' },
['Lantadyme'] = { ['herblore'] = 69, ['potion'] = 'Lantadyme potion (unf)' },
['Dwarf weed'] = { ['herblore'] = 72, ['potion'] = 'Dwarf weed potion (unf)' },
['Torstol'] = { ['herblore'] = 78, ['potion'] = 'Torstol potion (unf)' },
['Cadantineblood'] = { ['herblore'] = 80, ['potion'] = 'Cadantine blood potion (unf)' },
}
local compostChanceToLoseLifeReduction = {
['None'] = 1,
['Compost'] = 2,
['Supercompost'] = 5,
['Ultracompost'] = 10,
}
local compostLifeValue = {
['None'] = 0,
['Compost'] = 1,
['Supercompost'] = 2,
['Ultracompost'] = 3,
}
-- Yield increases are actually a flat increase in chance to save calculation
-- 5% is +10, 10% is +17, 15% is +25
local percentToValueIncreaseCTS = {
['0'] = 0,
['0.05'] = 10,
['0.1'] = 17,
['0.15'] = 25,
}
local goutweedSanfewExchange = gePrices['Grimy guam leaf'] * 32/128 + gePrices['Grimy marrentill'] * 24/128 + gePrices['Grimy tarromin'] * 18/128 + gePrices['Grimy harralander'] * 14/128 + gePrices['Grimy ranarr weed'] * 11/128 + gePrices['Grimy irit leaf'] * 8/128 + gePrices['Grimy avantoe'] * 6/128 + gePrices['Grimy kwuarm'] * 5/128 + gePrices['Grimy cadantine'] * 4/128 + gePrices['Grimy lantadyme'] * 3/128 + gePrices['Grimy dwarf weed'] * 3/128
function createHeader(farmingLevel, sell, tableCaption)
local header = mw.html.create('table'):addClass('wikitable sortable sticky-header align-center-2 align-right-3 align-right-4 align-center-5 align-center-6 align-right-7 align-center-8 align-right-9')
-- tableCaption is used only for patches
local additionalCaption = ''
local outputType = 'Run'
if(tableCaption ~= nil) then
outputType = 'Patch'
additionalCaption = tableCaption .. ' at '
header:addClass('mw-collapsible mw-collapsed')
end
header:tag('caption'):wikitext('Herb yield at ' .. additionalCaption .. scp('Farming', farmingLevel, true))
row = header:tag('tr')
row:tag('th'):wikitext('Herb'):done()
:tag('th'):wikitext(scp('Farming') .. 'Level'):done()
:tag('th'):wikitext('Seed cost'):done()
if(sell == 'Clean' or sell == 'Grimy') then
row:tag('th'):wikitext('Herb price'):done()
else
row:tag('th'):wikitext(scp('Herblore') .. 'Level'):done()
:tag('th'):wikitext('Unfinished potion'):done()
:tag('th'):wikitext('Unf potion price'):done()
end
row:tag('th'):wikitext('Chance of Death'):done()
:tag('th'):wikitext('Expected Herbs'):done()
:tag('th'):wikitext('Profit per ' .. outputType):done()
:tag('th'):wikitext('XP per ' .. outputType):done()
:tag('th'):wikitext('GP/XP'):done()
return header
end
function createRow(herbName, herbData, deathChance, isResurrectWorth, sell, yield, profit, xp, gpPerXp)
local row = mw.html.create('tr')
local herbType, herbPrice, potionPrice
if(herbName == 'Goutweed') then
herbType = 'Goutweed'
herbPrice = math.ceil(goutweedSanfewExchange)
elseif(sell == 'Clean') then
herbType = herbName
herbPrice = gePrices[herbType]
elseif(sell == 'Grimy') then
herbType = 'Grimy ' .. herbName:lower()
herbPrice = gePrices[herbType]
elseif(sell == 'Unfinished potion') then
herbType = herbName
potionPrice = gePrices[UnfinishedPotions[herbType].potion]
end
row:tag('td'):wikitext('[[File:' .. herbType .. '.png]] [[' .. herbType .. '|' .. herbName .. ']]'):done()
:tag('td'):wikitext(herbData.level):done()
:tag('td'):wikitext(coins(gePrices[herbData.seed])):done()
if(herbName == 'Goutweed' and sell == 'Unfinished potion') then
row:tag('td'):wikitext():addClass('table-na nohighlight'):done()
:tag('td'):wikitext():addClass('table-na nohighlight'):done()
:tag('td'):wikitext():addClass('table-na nohighlight'):css('border-right-width', 'medium'):done()
elseif(sell == 'Clean' or sell == 'Grimy') then
row:tag('td'):wikitext(coins(herbPrice)):css('border-right-width', 'medium'):done()
elseif(sell == 'Unfinished potion') then
row:tag('td'):wikitext(UnfinishedPotions[herbType].herblore):done()
:tag('td'):wikitext('[[File:' .. UnfinishedPotions[herbType].potion .. '.png]] [[' .. UnfinishedPotions[herbType].potion .. ']]'):done()
:tag('td'):wikitext(coins(potionPrice)):css('border-right-width', 'medium'):done()
end
row:tag('td'):wikitext(tonumber(string.format("%.3f", deathChance * 100)) .. '%' .. (not isResurrectWorth and warningTag or '')):done()
:tag('td'):wikitext(tonumber(string.format("%.3f", yield))):done()
:tag('td'):wikitext(coins(profit)):done()
:tag('td'):wikitext(tonumber(string.format("%.1f", xp))):done()
:tag('td'):wikitext(coins(gpPerXp)):done()
return row
end
-- From [[Farming#Variable crop yield]] and [[Talk:Farming#Yield rates of various crops]]
function generateEstimatedYield(farmingLevel, lowCTS, highCTS, harvestLives, itemBonus, diaryBonus, attasBonus)
local lowCTSFinal = math.floor(lowCTS * (1 + itemBonus))
lowCTSFinal = lowCTSFinal + diaryBonus
lowCTSFinal = math.floor(lowCTSFinal * (1 + attasBonus))
local highCTSFinal = math.floor(highCTS * (1 + itemBonus))
highCTSFinal = highCTSFinal + diaryBonus
highCTSFinal = math.floor(highCTSFinal * (1 + attasBonus))
local chanceToSave = skillInterp(lowCTSFinal, highCTSFinal, farmingLevel)
return harvestLives / (1 - chanceToSave)
end
-- From [[Module:Skilling success chart]]
function skillInterp(low, high, level)
local value = math.modf(low * (99 - level) / 98) + math.modf(high * (level - 1) / 98) + 1
return math.min(math.max(value / 256, 0), 1)
end
function p._main(args)
local farmingLevel = paramTest.default_to(tonumber(args.farmingLevel), 1)
local patchCount = paramTest.default_to(tonumber(args.patchCount), 1)
local weissDiseaseFreePatch = yesNo(args.weissDiseaseFreePatch) or false
local trollheimDiseaseFreePatch = yesNo(args.trollheimDiseaseFreePatch) or false
local hosidiusDiseaseFreePatch = yesNo(args.hosidiusDiseaseFreePatch) or false
local fortisDiseaseFreePatch = yesNo(args.fortisDiseaseFreePatch) or false
local compostType = paramTest.default_to(paramTest.ucflc(args.compostType), 'None')
local useBottomlessCompostBucket = yesNo(args.useBottomlessCompostBucket) or false
local useMagicSecateurs = yesNo(args.useMagicSecateurs) or false
local useFarmingCape = yesNo(args.useFarmingCape) or false
local animaType = paramTest.default_to(paramTest.ucflc(args.animaType), 'None')
local kandarinDiary = paramTest.default_to(args.kandarinDiary, 'None')
local kourendHardDiary = yesNo(args.kourendHardDiary) or false
local useResurrectCrops = yesNo(args.useResurrectCrops) or false
local magicLevel = paramTest.default_to(tonumber(args.magicLevel), 1)
local sell = paramTest.default_to(args.sell, 'Grimy') -- Grimy, Clean, Unfinished potion
local herbDataTable = {}
for _, data in ipairs(farmingData) do
if(data['type'] == 'Herb') then
herbDataTable[data.name] = {
['level'] = data.level,
['xp'] = data.xp,
['plantXp'] = data.plantXp,
['seed'] = data.materials[1].name,
}
end
end
local compostCostPerPatch = 0
if(compostType ~= 'None') then
compostCostPerPatch = useBottomlessCompostBucket and gePrices[compostType] * 0.5 or gePrices[compostType]
end
local costToResurrect = (gePrices['Soul rune'] * 8) + (gePrices['Nature rune'] * 12) + (gePrices['Blood rune'] * 8) + (gePrices['Earth rune'] * 25)
-- Resurrect Crops spell, level 78 = 50% and level 99 = 75%
local chanceToResurrect = skillInterp(-111, 191, magicLevel)
local itemBonus = 0
if(useMagicSecateurs) then
itemBonus = itemBonus + .1
end
if(useFarmingCape) then
itemBonus = itemBonus + .05
end
local kandarinDiaryBonus = 0
if(kandarinDiary ~= 'None') then
kandarinDiaryBonus = tonumber(kandarinDiary:match("%d+")) / 100
end
local kourendDiaryBonus = kourendHardDiary and 0.05 or 0
local attasBonus = animaType == 'Attas' and 0.05 or 0
-- Iasor lowers death rates by 80%
local deathRateModifier = animaType == 'Iasor' and 0.2 or 1
-- All herbs default have 26/128 chance to die each stage. There are only 3 stages which can be diseased or die.
---- This is actually 27, see [[Talk:Disease (Farming)]]
local deathRatePerStage = (math.floor(math.floor(27 / compostChanceToLoseLifeReduction[compostType]) * deathRateModifier) + 1) / 128
local patchSurvivalRate = (1 - deathRatePerStage)^3
local patchDeathRate = 1 - patchSurvivalRate
if(useResurrectCrops) then
--\left (\sum_{i=2}^{n-1}\binom{n-1}{i}p^i \left(1-P\right)^{\left(n-1)-i\right )} \right )R
patchDeathRate = ((1-(1-deathRatePerStage)^4)*(1-chanceToResurrect)) + (6 * deathRatePerStage^2 * (1 - deathRatePerStage)^2) + (4 * deathRatePerStage^3 * (1-deathRatePerStage)) + deathRatePerStage^4
patchSurvivalRate = 1 - patchDeathRate
end
local patches = {}
-- Hard Kourend Diary completion
if(kourendHardDiary and hosidiusDiseaseFreePatch and patchCount > 0) then
table.insert(patches, {
createHeader(farmingLevel, sell, 'Hosidius (Protected, +' .. kourendDiaryBonus * 100 .. '% Diary bonus)'),
patchCount = 1,
yieldBonus = percentToValueIncreaseCTS[tostring(kourendDiaryBonus)],
protected = true,
})
patchCount = patchCount - 1
end
-- Protected patches, includes Kourend if hard diary is not completed.
local protectedNames = {}
if(weissDiseaseFreePatch and patchCount > 0) then
table.insert(protectedNames, 'Weiss')
patchCount = patchCount - 1
end
if(trollheimDiseaseFreePatch and patchCount > 0) then
table.insert(protectedNames, 'Trollheim')
patchCount = patchCount - 1
end
if(fortisDiseaseFreePatch and patchCount > 0) then
table.insert(protectedNames, 'Civitas illa Fortis')
patchCount = patchCount - 1
end
-- Only include if it is not already included above
if(not kourendHardDiary and hosidiusDiseaseFreePatch and patchCount > 0) then
table.insert(protectedNames, 'Hosidius')
patchCount = patchCount - 1
end
if(#protectedNames > 0) then
table.insert(patches, {
createHeader(farmingLevel, sell, listToText(protectedNames, ', ', ' and ') .. ' (Protected)'),
patchCount = #protectedNames,
yieldBonus = 0,
protected = true,
})
end
-- Kandarin patch with Kandarin Diary completion
if(kandarinDiaryBonus > 0 and patchCount > 0) then
table.insert(patches, {
createHeader(farmingLevel, sell, 'Kandarin (+' .. kandarinDiaryBonus * 100 .. '% Diary bonus)'),
patchCount = 1,
yieldBonus = percentToValueIncreaseCTS[tostring(kandarinDiaryBonus)],
protected = false,
})
patchCount = patchCount - 1
end
-- Farming Guild patch with diary
if(kourendHardDiary and patchCount > 0) then
table.insert(patches, {
createHeader(farmingLevel, sell, 'Farming Guild (+' .. kourendDiaryBonus * 100 .. '% Diary bonus)'),
patchCount = 1,
yieldBonus = percentToValueIncreaseCTS[tostring(kourendDiaryBonus)],
protected = false,
})
patchCount = patchCount - 1
end
-- The rest of the patches, no additional yield and no protection
if(patchCount > 0) then
table.insert(patches, {
createHeader(farmingLevel, sell, patchCount .. ' standard patch' .. (patchCount > 1 and 'es' or '')), -- If more than 1 patch, pluralise
patchCount = patchCount,
yieldBonus = 0,
protected = false,
})
end
local runRet = createHeader(farmingLevel, sell)
local patchRet = ''
for _, herb in ipairs(herbs) do
local seedCost = gePrices[herbDataTable[herb.name].seed]
local isRessurectWorth = true
if(useResurrectCrops) then
isRessurectWorth = seedCost * chanceToResurrect > costToResurrect
end
local totalYield, totalProfit, totalXp = 0, 0, 0
local additiveDeathRate, patchCounter = 0, 0
for _, patch in ipairs(patches) do
local deathRate = patch.protected == true and 0 or patchDeathRate
local patchYield = generateEstimatedYield(farmingLevel, herb.lowCTS, herb.highCTS, 3 + compostLifeValue[compostType], itemBonus, patch.yieldBonus, attasBonus)
local truePatchYield = patch.protected == true and patchYield or patchYield * patchSurvivalRate
local patchCost = compostCostPerPatch + gePrices[herbDataTable[herb.name].seed] + ((patch.protected ~= true and useResurrectCrops == true) and costToResurrect * deathRate or 0)
local waterVialCost = sell == 'Unfinished potion' and gePrices['Vial of water'] or 0
local profitItemName = ''
if(sell == 'Grimy') then
profitItemName = 'Grimy ' .. herb.name:lower()
elseif(sell == 'Clean' or herb.name == 'Goutweed') then
profitItemName = herb.name
elseif(sell == 'Unfinished potion') then
profitItemName = UnfinishedPotions[herb.name].potion
end
local grossProfit = herb.name == 'Goutweed' and goutweedSanfewExchange or (gePrices[profitItemName]) * truePatchYield
local patchProfit = grossProfit - patchCost - (truePatchYield * waterVialCost)
local patchXp = (truePatchYield * herbDataTable[herb.name].xp) + (herbDataTable[herb.name].plantXp * patchSurvivalRate)
local gpPerXp = patchProfit / patchXp
patch[1]:node(createRow(herb.name, herbDataTable[herb.name], deathRate, isRessurectWorth, sell, truePatchYield, patchProfit, patchXp, gpPerXp))
totalYield = totalYield + (truePatchYield * patch.patchCount)
totalProfit = totalProfit + (patchProfit * patch.patchCount)
totalXp = totalXp + (patchXp * patch.patchCount)
additiveDeathRate = additiveDeathRate + (deathRate * patch.patchCount)
patchCounter = patchCounter + patch.patchCount
end
runRet:node(createRow(herb.name, herbDataTable[herb.name], additiveDeathRate/patchCounter, isRessurectWorth, sell, totalYield, totalProfit, totalXp, totalProfit/totalXp))
end
for _, v in ipairs(patches) do
patchRet = patchRet .. tostring(v[1])
end
local patchCountWarning = ''
-- This is to catch weird situations caused by inability to affect calculator buttons. E.g. It is not possible to have 9 patches calculated correctly with one or both of these disabled.
if(8 + (trollheimDiseaseFreePatch and 1 or 0) + (weissDiseaseFreePatch and 1 or 0) + (fortisDiseaseFreePatch and 1 or 0) < tonumber(args.patchCount)) then
patchCountWarning = '<b>Warning:</b> Your currently disabled disease-free patches make the \'\'Total herb patches\'\' option inaccurate.'
end
return patchCountWarning .. tostring(runRet) .. (useResurrectCrops and warningNote or '') .. 'Click \'Show\' on the tables to view patch breakdowns:\n' .. tostring(patchRet)
end
-- DEBUG
-- args = {['farmingLevel'] = 99, ['patchCount'] = 10, ['weissDiseaseFreePatch'] = true, ['trollheimDiseaseFreePatch'] = true, ['hosidiusDiseaseFreePatch'] = true, ['compostType'] = 'Ultracompost', ['useBottomlessCompostBucket'] = true, ['useMagicSecateurs'] = true, ['useFarmingCape'] = true, ['animaType'] = 'Iasor', ['kandarinDiary'] = '15% — Elite Kandarin Diary', ['kourendHardDiary'] = true, ['useResurrectCrops'] = true, ['magicLevel'] = 99 }
function p.main(frame)
local args = frame.args
--mw.logObject(args)
return p._main(args)
end
return p