Module:ZMICalculator
Module documentation
This documentation is transcluded from Template:No documentation/doc. [edit] [history] [purge]
This module does not have any documentation. Please consider adding documentation at Module:ZMICalculator/doc. [edit]
Module:ZMICalculator's function main is invoked by Calculator:ZMI/Module.
Module:ZMICalculator requires Module:Addcommas.
Module:ZMICalculator requires Module:Coins.
Module:ZMICalculator requires Module:Experience.
Module:ZMICalculator requires Module:Skill calc/Helpers.
Module:ZMICalculator requires Module:Yesno.
Module:ZMICalculator loads data from Module:GEPrices/data.json.
Module:ZMICalculator transcludes Template:Nocoins using frame:preprocess().
Module:ZMICalculator transcludes Template:Reflist using frame:preprocess().
local p = {}
-- For calculating experience
local helpers = require('Module:Skill_calc/Helpers')
local experience = require('Module:Experience')
-- For rendering the tables
local commas = require('Module:Addcommas')._add
local coins = require('Module:Coins')._amount
local gePrices = mw.loadJsonData('Module:GEPrices/data.json')
local yesno = require('Module:Yesno')
function nocoins(num)
frame = mw.getCurrentFrame()
return frame:preprocess('{{Nocoins|' .. num .. '}}')
end
local data = {
-- base xp, air, mind, water, earth, fire, body, cosmic, chaos, astral, nature, law, death, blood, soul
{9.37, 0.5, 0.25, 0.12, 0.06, 0.03, 0.015, 0.0085, 0.006, 0.0045, 0.003, 0.0015, 0.0008, 0.0005, 0.0002},
{10.5, 0.15, 0.18, 0.21, 0.24, 0.12, 0.06, 0.0175, 0.008, 0.006, 0.004, 0.0024, 0.0012, 0.0006, 0.0003},
{11.32, 0.12, 0.13, 0.135, 0.14, 0.15, 0.16, 0.08, 0.042, 0.021, 0.011, 0.0055, 0.0032, 0.0015, 0.0008},
{12.24, 0.07, 0.08, 0.09, 0.11, 0.12, 0.13, 0.2, 0.1, 0.05, 0.025, 0.013, 0.006, 0.004, 0.002},
{12.87, 0.06, 0.065, 0.07, 0.075, 0.08, 0.1, 0.15, 0.2, 0.1, 0.05, 0.026, 0.012, 0.008, 0.004},
{13.44, 0.05, 0.055, 0.06, 0.065, 0.07, 0.075, 0.1, 0.11, 0.15, 0.135, 0.07, 0.035, 0.017, 0.008},
{13.6, 0.045, 0.05, 0.055, 0.06, 0.07, 0.075, 0.095, 0.105, 0.14, 0.155, 0.08, 0.04, 0.02, 0.01},
{14.56, 0.03, 0.03, 0.03, 0.04, 0.04, 0.05, 0.07, 0.09, 0.12, 0.15, 0.18, 0.1, 0.05, 0.02},
{14.79, 0.02, 0.02, 0.03, 0.04, 0.05, 0.06, 0.07, 0.08, 0.105, 0.135, 0.145, 0.145, 0.06, 0.04},
{15.32, 0.01, 0.01, 0.02, 0.03, 0.04, 0.05, 0.06, 0.07, 0.1, 0.135, 0.145, 0.165, 0.1, 0.065},
{15.55, 0.01, 0.01, 0.02, 0.03, 0.03, 0.04, 0.05, 0.06, 0.095, 0.135, 0.145, 0.155, 0.13, 0.09},
}
-- Capacity using rune pouch and all possible runecraft pouches
local pouches = {
-- level, cumulative capacity, size
{1, 26 + 3, 'Small'},
{25, 25 + 9, 'Medium'},
{50, 24 + 18, 'Large'},
{75, 23 + 30, 'Giant'},
{85, 26 + 40, 'Colossal'},
{999, 1, 'na'},
}
local runes = {
-- rune, xp, diary boost in %
{ 'Air rune', 5, 25 },
{ 'Mind rune', 5.5, 25 },
{ 'Water rune', 6, 25 },
{ 'Earth rune', 6.5, 25 },
{ 'Fire rune', 7, 25 },
{ 'Body rune', 7.5, 25 },
{ 'Cosmic rune', 8, 25 },
{ 'Chaos rune', 8.5, 25 },
{ 'Astral rune', 8.7, 25 },
{ 'Nature rune', 9, 22.5 },
{ 'Law rune', 9.5, 20 },
{ 'Death rune', 10, 17.5 },
{ 'Blood rune', 23.8, 15 },
{ 'Soul rune', 29.7, 10 },
}
function p.main(frame)
-- Parse the args
local args = frame:getParent().args
local current = tonumber(args.current)
local currentType = args.currentType
local target = tonumber(args.target)
local targetType = args.targetType
local tripSeconds = tonumber(args.tripSeconds)
local blockedStorage = tonumber(args.blockedStorage)
local diary = yesno(args.diary)
local raiments = tonumber(args.raiments)
local colossal = yesno(args.colossal)
-- Not using a rune pouch means keeping the astral, law, and payment runes
-- for
if args.blockedStorage ~= 'true' then
runePouch = true
end
-- If we're targeting a level, we have a hard cap at 99
if targetType == 'Level' then
target = math.min(target, 99)
end
-- Update the pouch capacities if we have manually blocked storage (e.g., no rune pouch)
if blockedStorage > 0 then
for i=1, 5, 1 do
pouches[i][2] = pouches[i][2] - blockedStorage
end
end
-- Update the xp rates if we're using Daeyalt essence
local essenceType = 'Pure essence'
if args.daeyalt == 'true' then
essenceType = 'Daeyalt essence'
for i=1, 11, 1 do
data[i][1] = data[i][1] * 1.5
end
end
-- Get the starting and target XP
local bulkLevelInformation = helpers.calculateCurrentGoalInformation(current, currentType, target, targetType)
local currentXP = bulkLevelInformation.currentExperience
local goalXP = bulkLevelInformation.goalExperience
-- Get the total number of actions at each block to achieve the goal
local actions = getActions(currentXP, goalXP)
-- Get the total number of trips at each pouch block to achieve the goal
local trips = getTrips(currentXP, goalXP, colossal)
-- This calculates gp and renders the runes crafted table
local rcData = renderRunesCrafted(actions,diary,raiments)
-- Render the components
local summary = renderSummary(essenceType, actions, trips, tripSeconds, rcData['totalGP'])
local arTable = renderActionsRequired(actions,diary,raiments)
local ptTable = renderPouchTrips(trips, tripSeconds, essenceType)
local rcTable = rcData['hTable']
-- Combine the tables
return finalRender(summary, arTable, ptTable, rcTable)
end
-- Gets the total number of actions at each block level to achieve the goal
function getActions(currentXP, targetXP)
-- Track the total amount of XP gained
local totalXP = 0
-- The number of actions at each block to achieve our goal
local totalActions = 0
local actions = {
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
}
-- Make sure we're positive on XP
local blockXP = getBlockXP(currentXP, targetXP)
if blockXP['xp'] < 0 then
return {
xp = 0,
actions = actions,
totalActions = 0
}
end
-- Get the number of actions at each block to gain the goal XP
local currentBlock = blockXP['currentBlock']
local xpRate = 0
for i=currentBlock, 10, 1 do
-- Limit the amount of XP needed to the original goal
local xp = math.min(targetXP - currentXP, blockXP['xp'])
-- We have some XP left, so update the actions for this block
if xp > 0 then
xpRate = data[currentBlock][1]
local blockActions = math.ceil(xp / xpRate)
actions[currentBlock] = actions[currentBlock] + blockActions
totalActions = totalActions + blockActions
local xpGain = math.floor(blockActions * xpRate)
totalXP = totalXP + xpGain
currentXP = currentXP + xpGain
else
break
end
-- We have more XP to grab
if currentXP < targetXP then
blockXP = getBlockXP(currentXP, targetXP)
currentBlock = blockXP['currentBlock']
xpRate = data[currentBlock][1]
else
break
end
end
-- Get the actions remaining in the current block to achieve the goal (for 99+)
local remainingXP = math.max(targetXP - currentXP, 0)
if remainingXP > 0 then
xpRate = data[currentBlock][1]
local blockActions = math.ceil(remainingXP / xpRate)
local xpGain = math.floor(blockActions * xpRate)
totalXP = totalXP + xpGain
currentXP = currentXP + xpGain
actions[currentBlock] = actions[currentBlock] + blockActions
totalActions = totalActions + blockActions
end
return {
xp = totalXP,
actions = actions,
totalActions = totalActions
}
end
-- Gets the XP required to get to the next ZMI block (every 10 levels, or 99), or the target XP
function getBlockXP(startingXP, targetXP)
if targetXP <= startingXP then
return { xp = -1 }
end
local currentLevel = experience.level_at_xp_unr({args = {startingXP}})
local targetLevel = experience.level_at_xp_unr({args = {targetXP}})
-- Get the amount of XP to the end of the current block. Level 99 is last block
local currentBlock = 10 -- Level 99
local blockLevel = 0
if currentLevel < 99 then
currentBlock = math.floor(currentLevel / 10)
blockLevel = math.min((currentBlock + 1) * 10, 99)
end
-- There is still XP remaining to get to the next block
local xp = 0
if blockLevel > currentLevel then
xp = experience.xp_at_level_unr({args = {blockLevel}}) - startingXP
end
-- There is no amount of XP that will get us to the next block
return { currentBlock = currentBlock + 1, xp = xp }
end
-- Gets the total number of trips at each pouch level to get the target XP
function getTrips(startingXP, targetXP, colossal)
if targetXP <= startingXP then
return {
totalTrips = 0,
trips = {
0,
0,
0,
0,
0,
}
}
end
-- Track the trips
local totalTrips = 0
local trips = {
0,
0,
0,
0,
0,
}
-- Get the lowest level pouch that can be used
local minimumPouchLevel = experience.level_at_xp({args = {startingXP}})
minimumPouchLevel = math.floor(minimumPouchLevel / 25) + 1
if experience.level_at_xp({args = {startingXP}}) > 84 and colossal then
minimumPouchLevel = 5
end
-- 4 if the colossal pouch is turned of, 5 otherwise
local highestPouchLevel = 4 + (colossal and 1 or 0)
-- Run through each pouch level, excluding the colossal/giant pouch
local currentXP = startingXP
for i=minimumPouchLevel, highestPouchLevel, 1 do
local pouchTrips = getPouchTrips(currentXP, targetXP, colossal)
trips[i] = trips[i] + pouchTrips['trips']
totalTrips = totalTrips + pouchTrips['trips']
currentXP = pouchTrips['xp']
if currentXP == targetXP then
break
end
end
if currentXP < targetXP then
if 1 == 1 then
return 'ERORR'
end
local pouchTrips = getPouchTrips(currentXP, targetXP, colossal)
trips[highestPouchLevel] = trips[highestPouchLevel] + pouchTrips['trips']
totalTrips = totalTrips + pouchTrips['trips']
end
return {
trips = trips,
totalTrips = totalTrips,
}
end
-- Gets the trips required to get to the next pouch. This will slightly overestimate
-- the number of trips, due to the fact that it does not track XP on a per-essence
-- basis, but rather treats all actions within a given trip at the same XP rate.
-- In the worst case, this will miscalculate by 28 - 53 XP (depending on level).
-- However, this is minor (<1%) for levels 20+ and negligable (<0.1%) for 40+.
function getPouchTrips(startingXP, targetXP, colossal)
local currentLevel = experience.level_at_xp({args = {startingXP}})
-- Get the current and next pouch details
local pouchLevel = math.floor(currentLevel / 25) + 1
if experience.level_at_xp({args = {startingXP}}) > 84 and colossal then
pouchLevel = 5
end
local pouch = pouches[pouchLevel]
local currentCapacity = pouch[2]
local nextPouchLevel = pouches[pouchLevel + 1][1]
if pouchLevel == 4 and not colossal then
nextPouchLevel = pouches[6][1]
end
-- Track the trips and xp needed
local trips = 0
-- Calculate the number of actions for each level
local remainingXP = targetXP - startingXP
local currentXP = startingXP
while currentLevel < nextPouchLevel do
-- Get the block xp for the current level
local currentBlock = math.floor(currentLevel / 10) + 1
if currentLevel == 99 then
currentBlock = 11
end
local xpRate = data[currentBlock][1]
-- Get the xp needed to progress
local nextLevelXP = experience.xp_at_level_unr({args = {currentLevel + 1}})
-- For 99+, xp to next level no longer matters
if currentLevel == 99 then
nextLevelXP = targetXP
end
local nextXP = math.min(nextLevelXP, targetXP) - currentXP
-- Get the trips needed to progress
local currentTrips = math.ceil(nextXP / (xpRate * currentCapacity))
trips = trips + currentTrips
-- Update current and remaining XP
local xp = math.ceil(currentTrips * currentCapacity * xpRate)
remainingXP = remainingXP - xp
remainingXP = math.max(remainingXP, 0)
currentXP = currentXP + xp
currentXP = math.min(currentXP, targetXP)
-- Update current level
local nextCurrentLevel = experience.level_at_xp_unr({args = {currentXP}})
if nextCurrentLevel < currentLevel then
return 'ERROR: Broken trip level predicate'
elseif nextCurrentLevel == currentLevel then
break
end
currentLevel = nextCurrentLevel
if remainingXP == 0 then
break
end
end
return {
trips = trips,
xp = currentXP,
}
end
-- Formats the number of seconds into a useful time string
function formatTime(seconds, asText)
local hours = math.floor(seconds / 3600)
local minutes = math.floor(seconds / 60) % 60
local fSeconds = seconds % 60
if asText == true then
if hours > 1 then
return 'about ' .. commas(hours) .. ' hours'
end
minutes = minutes + (hours * 60)
if minutes > 1 then
return 'about ' .. commas(minutes) .. ' minutes'
end
if seconds == 1 then
return '1 second'
end
return commas(seconds) .. ' seconds'
end
return string.format('%02d:%02d:%02d', hours, minutes, fSeconds)
end
-- Renders the summary paragraph
function renderSummary(essenceType, actions, trips, tripSeconds, totalGP)
local totalDuration = tripSeconds * trips['totalTrips']
local formattedTime = formatTime(totalDuration, true)
-- To prevent NAN due to dividing by zero
if totalDuration == 0 then
totalDuration = 1
end
-- Calculate gp and xp per hour
local hours = totalDuration / 3600
local hourlyGP = math.floor(totalGP / hours)
local hourlyXP = math.floor(actions['xp'] / hours)
local summary = mw.html.create('p')
summary
:wikitext(
'It will take '
.. commas(actions['totalActions'])
.. ' '
.. essenceType:lower()
.. ' across '
.. commas(trips['totalTrips'])
.. ' trips to reach your goal. This will take '
.. formattedTime
.. '. This will earn an average of '
)
:wikitext(coins(round(hourlyGP)))
:wikitext(' per hour and ')
:wikitext(round(hourlyXP))
:wikitext(' XP per hour.')
:done()
return summary
end
-- Renders the Actions Required table
function renderActionsRequired(actions,diary,raiments)
-- Create the table
local hTable = mw.html.create('table')
:addClass('wikitable sortable sticky-header alternating-rows align-left-1')
:attr('style', 'text-align:right;float:left;margin-right:1.5em')
-- Add the headers
hTable
:tag('caption'):wikitext('Actions Required')
:tag('tr')
:tag('th'):wikitext('Level')
:tag('th'):wikitext('Actions')
:tag('th'):wikitext('XP')
:tag('th'):wikitext('GP')
:done()
-- Track the total gp
local totalGP = 0
-- Add the rows
for i=1, 11, 1 do
-- Calculate the block range
local range = '99'
if i < 11 then
range = ((i - 1) * 10) .. ' - ' .. ((i * 10) - 1)
end
-- Calculate the values
local blockActions = actions['actions'][i]
local xpRate = data[i][1]
local xp = math.floor(blockActions * xpRate)
-- Calculate the gp
local rowGP = 0
local rowDiaryGP = 0
for r=1, 14, 1 do
local crafted = blockActions * data[i][r + 1]
local gp = crafted * gePrices[runes[r][1]] * (1+(diary and runes[r][3]/100 or 0)+raiments/10+((raiments == 4) and 0.2 or 0))
rowGP = math.floor(rowGP + gp)
end
totalGP = totalGP + rowGP
-- Add the row
if blockActions > 0 then
hTable:tag('tr')
:tag('td'):wikitext(range)
:tag('td'):wikitext(commas(blockActions))
:tag('td'):wikitext(commas(xp))
:tag('td'):wikitext(nocoins(rowGP))
:done()
end
end
-- Add the footer
hTable:tag('tr')
:tag('th')
:attr('style', 'text-align:left')
:wikitext('Total')
:tag('th')
:attr('style', 'text-align:right')
:wikitext(commas(actions['totalActions']))
:tag('th')
:attr('style', 'text-align:right')
:wikitext(commas(actions['xp']))
:tag('th')
:attr('style', 'text-align:right')
:wikitext(coins(round(totalGP)))
:done()
return hTable
end
-- Renders the Pouch Trips table
function renderPouchTrips(trips, tripSeconds, essenceType)
frame = mw.getCurrentFrame()
-- Create the table
local hTable = mw.html.create('table')
:addClass('wikitable sortable sticky-header alternating-rows align-center-1 align-left-2')
:attr('style', 'text-align:right')
-- Add the headers
hTable
:tag('caption'):wikitext('Trips')
:tag('tr')
:tag('th'):attr('colspan', 2):wikitext('Pouch')
:tag('th'):wikitext('[[File:Runecraft_icon.png|link=Runecraft|x21px]]'):done()
:tag('th'):wikitext(frame:preprocess('[[File:' .. essenceType .. '.png|link=' .. essenceType .. '|x21px]] <ref>Total essence capacity for a single trip.</ref>'))
:tag('th'):wikitext('Trips')
:tag('th'):wikitext('Time')
:done()
-- Add the rows
for i=1, 5, 1 do
local pouch = pouches[i]
local rowTrips = trips['trips'][i]
local formattedTime = formatTime(tripSeconds * rowTrips, false)
-- Add the row
if rowTrips>0 then
hTable:tag('tr')
:tag('td'):wikitext('[[File:' .. pouch[3] .. ' pouch.png|link=' .. pouch[3] .. ' pouch|x21px]]'):done()
:tag('td'):wikitext(pouch[3])
:tag('td'):wikitext(pouch[1])
:tag('td'):wikitext(pouch[2])
:tag('td'):wikitext(round(rowTrips))
:tag('td'):wikitext(formattedTime)
:done()
end
end
local totalTime = formatTime(tripSeconds * trips['totalTrips'], false)
-- Add the footer
hTable:tag('tr')
:tag('th')
:attr('colspan', 4)
:attr('style', 'text-align:left')
:wikitext('Total')
:tag('th')
:attr('style', 'text-align:right')
:wikitext(round(trips['totalTrips']))
:tag('th')
:attr('style', 'text-align:right')
:wikitext(totalTime)
:done()
-- Wrap the table in a floated div so we can have the reflist underneath it
local div = mw.html.create('div'):attr('style', 'float:left')
:node(hTable)
:wikitext(frame:preprocess('{{Reflist}}'))
:done()
return div
end
-- Renders the Runes Crafted table
function renderRunesCrafted(actions,diary,raiments)
frame = mw.getCurrentFrame()
-- Create the table
local hTable = mw.html.create('table')
:addClass('wikitable sortable sticky-header alternating-rows align-center-1 align-left-2')
:attr('style', 'text-align:right')
-- Add the headers
hTable
:tag('caption'):wikitext('Runes Crafted')
:tag('tr')
:tag('th'):attr('colspan', 2):wikitext('Rune')
:tag('th'):wikitext('XP')
:tag('th'):wikitext('%')
:tag('th'):wikitext('Crafted')
:tag('th'):wikitext('GP')
:done()
-- We will be adding cells to each rune row showing the relative percent
-- of all runes crafted for that rune. As this relies on knowing the total
-- crafted, which can't be known at this time, we will be saving a reference
-- to them so we can update later.
local runeRatios = {}
-- Add the rows
local totalCrafted = 0
local totalGP = 0
for r=1, 14, 1 do
local rune = runes[r]
-- Calculate the crafted runes
local crafted = 0
for i=1, 11, 1 do
local blockActions = actions['actions'][i]
crafted = crafted + (blockActions * data[i][r + 1]) * (1+(diary and runes[r][3]/100 or 0)+raiments/10+((raiments == 4) and 0.2 or 0))
end
totalCrafted = totalCrafted + crafted
-- Get the prices
local gp = crafted * gePrices[rune[1]]
totalGP = totalGP + gp
-- Keep a reference to the ratio cells so we can update it later
local ratioCell = mw.html.create('td')
table.insert(runeRatios, { ratioCell, crafted })
-- Add the row
hTable:tag('tr')
:tag('td'):wikitext('[[File:' .. rune[1] .. '.png|link=' .. rune[1] .. '|x21px]]'):done()
:tag('td'):wikitext(rune[1])
:tag('td'):wikitext(round(crafted * rune[2]))
:node(ratioCell)
:tag('td'):wikitext(round(crafted))
:tag('td'):wikitext(nocoins(round(gp)))
:done()
end
-- Update the ratios
for i=1, 14, 1 do
local cell = runeRatios[i][1]
local ratio = (runeRatios[i][2] / totalCrafted) * 100
cell:wikitext(string.format('%0.2f%%', ratio))
end
-- Add the footer
hTable:tag('tr')
:tag('th')
:attr('colspan', 4)
:attr('style', 'text-align:left')
:wikitext('Total')
:tag('th')
:attr('style', 'text-align:right')
:wikitext(round(totalCrafted))
:tag('th')
:attr('style', 'text-align:right')
:wikitext(coins(round(totalGP)))
:done()
return {
hTable = hTable,
totalGP = totalGP,
}
end
function round(num)
return commas(string.format('%.2f', num))
end
function finalRender(summary, arTable, ptTable, rcTable)
local div = mw.html.create('div')
div
:node(summary)
:node(arTable)
:node(ptTable)
:node(mw.html.create('br'):attr('style', 'clear:both'):done())
:node(rcTable)
:done()
return tostring(div)
end
return p