Module:Mining Rate Calculator Module

From the RuneScape Wiki, the wiki for all things RuneScape
Jump to navigation Jump to search
Module documentation
This documentation is transcluded from Module:Mining Rate Calculator Module/doc. [edit] [history] [purge]
Module:Mining Rate Calculator Module's function main is invoked by Calculator:Mining/Rate/template.
Module:Mining Rate Calculator Module requires Module:Addcommas.
Module:Mining Rate Calculator Module requires Module:Arguments.
Module:Mining Rate Calculator Module requires Module:Currency.
Module:Mining Rate Calculator Module requires Module:ExchangeLite.
Module:Mining Rate Calculator Module requires Module:Experience.
Module:Mining Rate Calculator Module loads data from Module:MS Data.
Function list
L 16 — fnum
L 21 — shallow_copy
L 32 — p.main
L 774 — remainingExp
L 819 — geodePrice

The functionality for the Calculator:Mining/Rate and Calculator:Mining calculators.


local p = {}
local getArgs = require("Module:Arguments").getArgs;
local data = mw.loadData('Module:MS Data')
local gemw = require('Module:ExchangeLite')
local xp, level;

do
	local _module_experience = require('Module:Experience');
	xp = _module_experience._xp_at_level;
	level = _module_experience._level_at_xp;
end
local coins = require('Module:Currency')._amount
local commas = require('Module:Addcommas')._add

local lang = mw.getContentLanguage() -- Number format helper function.
local function fnum(x)
	if type(x) == 'number' then return lang:formatNum(x) end
	return x
end

local function shallow_copy(t)
    local copy = {}
    for k, v in pairs(t) do
        copy[k] = v
    end
    return copy
end


local remainingExp, geodePrice;

function p.main(frame)
	local args = getArgs(frame)
	local mining_level = tonumber(args.mining_level) or 1		-- Mining level
	local str_level = tonumber(args.str_level) or 1				-- Strength level
	local mining_xp = tonumber(args.mining_xp) or 1				-- Mining xp
	local target_lvl = tonumber(args.target_level) or 0			-- Target mining level
	local target_xp = tonumber(args.target_xp) or 0				-- Target mining xp
	local pick = data.picks[args.pickaxe]						-- Pickaxe
	local methodtmp = args.method or 'Semi-AFK'
	local method = data.method_m[methodtmp]						-- Method chosen
	local corehammer = tonumber(args.corehammer) or 0			-- Tagga's corehammer
	local pockettmp = args.pocket or 'None'
	local pocket = data.pocket[pockettmp]					-- Pocket
	local tmpvarrock = args.varrock or 'None'
	local varrock = data.varrock[tmpvarrock]					-- Varrock armour
	local newvar = tonumber(args.newvar) or 1					-- New varrock tasks
	local familiar = data.familiar_m[args.familiar] or 0		-- Familiar
	local gmo = data.outfit_s[args.gmo] or 0					-- Golden mining outfit pieces unlocked
	local outfittmp = args.outfit or 'None'
	local outfit = data.outfit_m[outfittmp]						-- Outfit selected
	local cape = tonumber(args.cape) or 0						-- Mining skillcape
	local amulet = tonumber(args.amulet) or 0					-- Amulet of glory
	local ringtmp = args.ring or 'None'
	local ring = data.rings[ringtmp]							-- Ring
	local lucktmp = args.luck or 'None'
	local luck = data.luck[lucktmp]								-- Luck items and potions
	local pof = args.pofperk or 'None'							-- POF animal perks.
	local avatmp = args.avatar or 'None'
	local avatar = data.avatar[avatmp] or 0						-- Avatar
	local juju = args.juju or 'None'							-- Juju mining potions
	local pottmp = args.str_potion or args.potion or 'None'
	local str_potion = data.potion[pottmp]						-- Strength bonus potions
	pottmp = args.mining_potion or 'None'
	local mining_potion = data.mining_potion[pottmp]			-- Mining bonus potions
	local spelltmp = args.spell or 'None'
	local spell = data.spells_m[spelltmp]						-- Crystallise / Light Form
	local trah = tonumber(args.trah) or 0						-- Trahaearn Hour
	local sprite = tonumber(args.starsprite) or 0				-- Star sprite ore bonus for first 15 minutes
	local spirits = tonumber(args.spirits) or 0					-- Stone spirits
	local lvl20 = tonumber(args.lvl20) or 1						-- Pickaxe item level 20
	local honed = data.honed[args.honed] or 0					-- Honed perk
	honed = honed * lvl20
	local wise = data.wise[args.wise] or 0						-- Wise perk
	local furnace = data.furnace[args.furnace] or 0				-- Furnace perk
	furnace = furnace * lvl20
	local fortune = data.fortune[args.fortune] or 0				-- Fortune perk
	fortune = fortune * lvl20
	local impsouled = data.impsouled[args.impsouled] or 0		-- Imp Souled perk
	impsouled = impsouled * lvl20

	-- NEW PERKS ADDED HERE
	local careless = 0
	if args.careless and args.careless ~= '' and args.careless ~= 'None' then
		-- Try data table lookup first (for consistency with other perks)
		if data.careless and data.careless[args.careless] then
			careless = data.careless[args.careless]
		else
			-- Otherwise treat as direct number input (1-5)
			careless = tonumber(args.careless) or 0
		end
	end
	local explosive = tonumber(args.explosive) or 0				-- Explosive perk (1=Yes, 0=No)

	local urntmp = args.urns or 'None'
	local urns = data.urns[urntmp]								-- Urns
	local urntypes = shallow_copy(data.urn_types)
	if urntmp:find("Exquisite") then
		urntypes[#urntypes] = data.urn_exquisite -- Replace decorated with exquisite urn
	end
	local ramtmp = args.ramhammer or 'None'
	local ramhammer = data.ramhammer[ramtmp]					-- Dwarven Ramhammer
	local refined = data.refined[args.refined] or 0				-- Refined
	refined = refined * lvl20
	local raf = tonumber(args.raf) or 0							-- Refer a friend bonus
	local bxp = tonumber(args.bxp) or 0							-- Bonus XP
	local bxpnum = tonumber(args.bxpnum) or 0					-- Amount of bonus XP
	local dxp = tonumber(args.dxp) or 0							-- Double XP Weekend
	local pcore = tonumber(args.pulsecore) or 0					-- Advanced pulse core (only xp boost)
	local skillctmp = args.skillchompa or 'None'
	local skillchompa = data.skillchompa[skillctmp]				-- Skillchompas
	local customtmp = tonumber(args.custom) or 0
	local custom = customtmp / 100								-- Custom XP boost
	local custom_mine = tonumber(args.custommine) or 0			-- Custom mining level boost (flat #)
	local custom_str = tonumber(args.customstr) or 0			-- Custom strength level boost (flat #)
	local porter = data.porter[args.porter] or 0				-- Cost of one porter charge
	local critenhancer = tonumber(args.critenhancer) or 0		-- Enhancer (Crit)
	local geodeenhancer = tonumber(args.geodeenhancer) or 0		-- Enhancer (Geode)

	-- No ramhammer on dxp
	if dxp ~= 0 then
		ramhammer = data.ramhammer['None']
	end

	-- Combine luck items bonuses, non-stacking
	local meta_bonus = 0
	if  ring.meta > luck.meta then
		meta_bonus = ring.meta
	else
		meta_bonus = luck.meta
	end
	local meta = data.metamorphic_chance + meta_bonus			-- Chance for metamorphic geode

	-- Pof perks
	local pofgeode = 0
	local pofcrit = 0
	if pof == 'Scimitops' then
		pofgeode = 0.01
	elseif pof == 'Scimitops x2' then
		pofgeode = 0.02
	elseif pof == 'Scimitops x2 + Asciatops' then
		pofgeode = 0.02
		pofcrit = 0.03
	elseif pof == 'Scimitops + Asciatops' then
		pofgeode = 0.01
		pofcrit = 0.03
	elseif pof == 'Scimitops + Asciatops x2' then
		pofgeode = 0.01
		pofcrit = 0.06
	elseif pof == 'Asciatops x2' then
		pofcrit = 0.06
	elseif pof == 'Asciatops' then
		pofcrit = 0.03
	end

	-- Offhands
	local offhandcritdamage = 0
	local offhandcritchance = 0
	if corehammer == 1 then
		offhandcritdamage = 35
		offhandcritchance = 0.1
	end

	--Enhancer (Geode)
	local enhancergeode = 0
	if geodeenhancer == 1 then
		enhancergeode = 0.3
	end

	--Enhancer (Crit)
	local enhancercrit = 0
	if critenhancer == 1 then
		enhancercrit = 0.1
	end

	-- Levels and targets
	local remaining = 0
	local virtual_mining_level;
	virtual_mining_level, mining_xp, target_lvl, target_xp, remaining = remainingExp(mining_level, mining_xp, target_lvl, target_xp)

		-- deal with no bxp but claiming to have bxp
	if bxpnum == 0 then
		bxp = 0
	end

	local real_mining_level = math.min(virtual_mining_level, 110);
	mining_level = real_mining_level;

	-- Experience boosts (direct XP modifiers)
	local xp_boosts = 1 * (1 + dxp) * (1 + raf) * (1 + pcore) + avatar + wise + bxp + furnace + ramhammer.xp + custom
	if outfit.gmo == 1 then xp_boosts = xp_boosts + gmo end -- If the selected outfit applies the golden mining outfit bonus then add it to xp boosts

	-- Bonus xp
	if bxpnum > 0 then
		local div = 2 * (xp_boosts -1) -- remove boosts then half of xp can come from bxp
		if bxpnum < (remaining / div) then
			xp_boosts = xp_boosts - 1
			remaining = remaining - bxpnum
		end
	end

	local has_target = false									-- Has a target
	if remaining > 0 then has_target = true end					-- Has target true if remaining xp > 0

	local t = mw.html.create('table')	-- Create the headers of the table
	t:addClass('wikitable sticky-header sortable')

	if not has_target then
		local r = t:tag('tr')
		r:tag('th')  :wikitext('Level')             :attr('rowspan', 2) :done()
		:tag('th')  :wikitext('Rock')               :attr('rowspan', 2) :done()
		:tag('th')  :wikitext('Hardness Penalty')   :attr('rowspan', 2) :done()
		:tag('th')  :wikitext('Crit Chance')        :attr('rowspan', 2) :done()
		:tag('th')  :wikitext('Double ore chance')  :attr('rowspan', 2) :done()
		:tag('th')  :wikitext('XP Per Hour')        :attr('colspan', 3) :done()
		if spirits > 0 then
			r:tag('th')  :wikitext('Stone Spirits Used')  :attr('rowspan', 2)  :done()
		end
		if urns > 0 then
			r:tag('th')  :wikitext('Urns Used')     :attr('rowspan', 2) :done()
		end
		r:tag('th')  :wikitext('Ore Per Hour')      :attr('rowspan', 2) :done()
		:tag('th')  :wikitext('Ore Value')			:attr('rowspan', 2) :done()
		:tag('th')  :wikitext('Geode Chance')       :attr('rowspan', 2) :done()
		:tag('th')  :wikitext('Geodes Per Hour')    :attr('rowspan', 2) :done()
		:tag('th')  :wikitext('GP Per Hour')		:attr('rowspan', 2) :done()
		:done()
		:tag('tr')
		:tag('th')  :wikitext('Min')                                     :done()
		:tag('th')  :wikitext('Avg')                                    :done()
		:tag('th')  :wikitext('Max')                                     :done()
		:done()
	else
		local r = t:tag('tr')
		r:tag('th')  :wikitext('Rock')              :attr('rowspan', 2) :done()
		:tag('th')	:wikitext('Level')				:attr('rowspan', 2) :done()
		:tag('th')	:wikitext('Time Required (hr)')		:attr('colspan', 3)	:done()
		if spirits > 0 then
			r:tag('th')  :wikitext('Stone Spirits Used')  :attr('rowspan', 2)  :done()
		end
		if urns > 0 then
			r:tag('th')  :wikitext('Urns Used')     :attr('rowspan', 2) :done()
		end
		r:tag('th')	:wikitext('Ore mined')			:attr('rowspan', 2)	:done()
		:tag('th')	:wikitext('Geodes found')		:attr('rowspan', 2)	:done()
		:tag('th')	:wikitext('Ore cost')			:attr('rowspan', 2)	:done()
		:tag('th')	:wikitext('Profit/Loss')		:attr('rowspan', 2)	:done()
		:tag('th')	:wikitext('Profit/XP')			:attr('rowspan', 2)	:done()
		:done()
		:tag('tr')
		:tag('th')	:wikitext('Min')									:done()
		:tag('th')  :wikitext('Avg')                                     :done()
		:tag('th')  :wikitext('Max')                                    :done()
		:done()
	end

	-- Mining level boosts
	if (mining_potion.type == "normal") then
		mining_level = real_mining_level * mining_potion.per + mining_potion.flat;
	elseif (mining_potion.type == "extreme") then
		-- TODO: Extreme mining potion (once that gets added)
	else
		error("Unknown potion type: " .. mining_potion.type);
	end
	mining_level = mining_level + custom_mine
	if real_mining_level < 99 then cape = 0 end			-- Turn off skillcape if selected without 99 mining
	if cape > 0 then
		mining_level = math.max(mining_level, real_mining_level + 1); -- Level boosts don't stack
	end
	local damage_bonus = familiar + ring.damage		-- Damage bonus
	-- Strength level boosts
	str_level = str_level * str_potion.per + str_potion.flat + custom_str

	-- Check that pickaxe is usable at level
	local higher_pick = false
	local orig_pick;
	-- It's not possible to use level boosts to use higher pickaxe tiers
	if pick.level > real_mining_level then
		higher_pick = true
		orig_pick = pick
		local picklvl = 0
		local pickdmg = 0
		for p,pdata in pairs(data.picks) do
			if pdata.level > picklvl and pdata.max > pickdmg and pdata.level <= real_mining_level then
				pick = pdata
				picklvl = pdata.level
				pickdmg = pdata.max
			end
		end
	end

	-- Check number of perks
	local perk_cnt = 0
	if wise > 0 then
		perk_cnt = perk_cnt + 1
	end
	if furnace > 0.1 then
		perk_cnt = perk_cnt + 1
	end
	if honed > 0 then
		perk_cnt = perk_cnt + 1
	end
	if fortune > 0 then
		perk_cnt = perk_cnt + 1
	end
	if refined > 0 then
		perk_cnt = perk_cnt + 1
	end
	if impsouled > 0 then
		perk_cnt = perk_cnt + 1
	end

	-- Critical hit bonus
	local crit_bonus = 0
	for i = 1,20,1 do
		if mining_level >= data.crit_bonus_level[i]
		then crit_bonus = data.crit_bonus[i]
		else break end
	end
	crit_bonus = crit_bonus + skillchompa.bonus + offhandcritdamage	-- Skillchompas critical hit bonus

	if args.method == 'AFK' then spell = data.spells_m['None'] end -- Turn off crystallise if AFK

	for index,r in ipairs(data.rocks) do
		if mining_level >= r.level or has_target then	-- Remove rocks that cannot be mined at the selected mining level

			-- No urns with lava flow
			local xp_boosts_rock = xp_boosts
			if urns > 0 and r.urns then
					xp_boosts_rock = xp_boosts_rock + urns
			end

			-- Net hardness
			local net_hardness = pick.pen - r.hardness
			if net_hardness > 0 then net_hardness = 0 end

			-- Critical hit chance
			local crit_chance = 0
			if		r.len_crit_chance_level == 2 then
				if		mining_level >= r.crit_chance_level[2] then crit_chance = data.crit_chances[2]
				elseif	mining_level >= r.crit_chance_level[1] then crit_chance = data.crit_chances[1]
				else												crit_chance = data.crit_chance_base end
			elseif	r.len_crit_chance_level == 1 then
				if		mining_level >= r.crit_chance_level[1] then crit_chance = data.crit_chances[1]
				else												crit_chance = data.crit_chance_base end
			else													crit_chance = data.crit_chance_base
			end
			crit_chance = crit_chance + pocket.crit + pofcrit + offhandcritchance + enhancercrit
			if args.outfit == 'Starfury Outfit' then -- Starfury's crit chance only applies to Seren stones
				if r.name == '[[Seren stones]]' then crit_chance = crit_chance + outfit.crit	end
			else
				crit_chance = crit_chance + outfit.crit
			end

			--Base damage calculations
			local net_crit_bonus = crit_bonus + net_hardness
			if net_crit_bonus < 1 then net_crit_bonus = 1 end
			local net_crit = net_crit_bonus * crit_chance

			-- !!! START DAMAGE CALCULATION UPDATE FOR PERKS !!!
			local damage_base = (mining_level + math.floor(str_level/10) + net_hardness + net_crit + damage_bonus) * ramhammer.damage

			-- Careless Perk: 2% damage increase per rank
			if careless > 0 then
				damage_base = damage_base * (1 + (0.02 * careless))
			end

			-- Explosive Perk: 20% damage increase on core rocks
			if explosive == 1 and r.core == 1 then
				damage_base = damage_base * 1.20
			end
			-- !!! END DAMAGE CALCULATION UPDATE !!!

			local damage_min = damage_base + pick.min
			local damage_avg = damage_base + pick.avg
			local damage_max = damage_base + pick.max

			if damage_min < 1 then damage_min = 1 end -- If negative damage due to hardness then swings do 1 damage.
			if damage_avg < 1 then damage_avg = 1 end
			if damage_max < 1 then damage_max = 1 end

			-- Perfect juju mining potion
			local method_xp = method.xp
			if juju == 'Perfect juju mining potion' and r.spirits ~= nil and r.spirits > 0 and method_xp < (0.9) then
				method_xp = 0.9
			end

			-- Separate method_ore for ore calculations
			-- Careless: Damage increase directly benefits ore generation (fewer swings per ore)
			-- so stamina penalty should not apply to ores, only to XP
			local method_ore = method_xp
			if careless > 0 then
    			-- Reduces stamina by an additional 10% per rank (Careless 5 = 50% penalty)
				method_xp = method_xp * (1 - (0.10 * careless))
    			-- Note: method_xp is capped at 1 later by r.drain == 1 logic.
				-- For ore calculations, do not apply stamina penalty - damage increase already benefits ores
				-- The 2% per rank damage increase reduces swings per ore, which increases ore per swing
				-- This directly compensates for any stamina loss in terms of ore generation
			end

			-- Some rocks do not use stamina drain
			if r.drain == 1 then
				method_xp = 1
				method_ore = 1
			end

			-- XP calculations
			local xp_swing_min = math.floor(10 * damage_min * r.xp * data.base_xp)/10
			local xp_swing_avg = math.floor(10 * damage_avg * r.xp * data.base_xp)/10
			local xp_swing_max = math.floor(10 * damage_max * r.xp * data.base_xp)/10
			local xp_hr_min = xp_swing_min * data.swing_hr * method_xp
			local xp_hr_avg = xp_swing_avg * data.swing_hr * method_xp
			local xp_hr_max = xp_swing_max * data.swing_hr * method_xp

			-- Ore calculations (average only)
			local swings_ore = r.diff / damage_avg -- Average swings per ore
			local uncapped_swings_ore = swings_ore
			if swings_ore < 1 then swings_ore = 1 end -- Apply damage cap
			local ore_swing = 1 / swings_ore -- Average ore per swing
			local base_ore = ore_swing * (data.swing_hr * (1 - furnace)) * math.min(method_ore * (swings_ore/uncapped_swings_ore),1) -- Average ore per hour, adjusted by theoretical uncapped swings per hour to correct afk method calculation

			-- Double ore chance
			local double_ore_chance = 0
			if		r.len_double_ore == 2 then
				if		mining_level >= r.double_ore_chance[2] then double_ore_chance = data.double_ore_chances[2]
				elseif	mining_level >= r.double_ore_chance[1] then double_ore_chance = data.double_ore_chances[1]
				else												double_ore_chance = data.double_ore_chance_base end
			elseif	r.len_double_ore == 1 then
				if		mining_level >= r.double_ore_chance[1] then double_ore_chance = data.double_ore_chances[1]
				else												double_ore_chance = data.double_ore_chance_base end
			else													double_ore_chance = data.double_ore_chance_base
			end
			double_ore_chance = double_ore_chance + honed + fortune -- Apply honed & fortune perk and mining cape if applicable
			if r.core == 1 then double_ore_chance = double_ore_chance + cape end -- apply mining cape only on core rocks

			-- Double ore chance (Varrock armour)
			if varrock.no > 0 then					-- Skips if no varrock armour is selected
				if r.core == 1 then					-- Skips if the rock is not a core rock
					if r.varrock[varrock.no] == 1 then	-- Checks if the armour selected applies to the rock
						double_ore_chance = double_ore_chance + (data.varrock[r.varrock_no]['effect'] * newvar)
					end
				end
			end
			-- Double ore chance (Trah hour)
			if trah == 1 then
				if r.trah == 1 then double_ore_chance = double_ore_chance + data.trah_bonus end
			end
			-- Double ore chance (Star sprite), fist 15 minutes
			if sprite == 1 then
				if r.sprite == 1 then double_ore_chance = double_ore_chance + data.sprite_bonus * 0.25 end
			end

			local ore_boosts = r.ore + double_ore_chance	-- Ore multiplier
			local spirits_multiplier = 1.0  -- Multiplier for stone spirits
			if r.spirits ~= nil and r.spirits > 0 then
				if spirits == 1 then
					-- Stone spirits double the ore yield - each ore mined gives 2 ores instead of 1
					-- This is a true 2x multiplier on the final ore count
					spirits_multiplier = 2.0
				elseif juju ~= 'None' then
					ore_boosts = ore_boosts + 0.1								-- Stone spirits from juju mining potions
				end
			end
			local total_ore = base_ore * ore_boosts * spirits_multiplier

			-- Geode chance
			local geode_chance = 0
			if		r.len_geode_chance == 3 then
				if		mining_level >= r.geode_chance_level[3] then geode_chance = data.geode_chances[3]
				elseif	mining_level >= r.geode_chance_level[2] then geode_chance = data.geode_chances[2]
				elseif	mining_level >= r.geode_chance_level[1] then geode_chance = data.geode_chances[1]
				else											geode_chance = data.geode_chance_base end
			elseif		r.len_geode_chance == 2 then
				if		mining_level >= r.geode_chance_level[2] then geode_chance = data.geode_chances[2]
				elseif	mining_level >= r.geode_chance_level[1] then geode_chance = data.geode_chances[1]
				else												 geode_chance = data.geode_chance_base end
			elseif	r.len_geode_chance == 1 then
				if		mining_level >= r.geode_chance_level[1] then geode_chance = data.geode_chances[1]
				else												 geode_chance = data.geode_chance_base end
			else													 geode_chance = data.geode_chance_base end
			geode_chance = geode_chance + pocket.geode + refined + amulet + pofgeode + enhancergeode

			-- Rockertunities
			-- Rockertunity multiplier
			local rck_mult = 0
			if r.len_rockertunity_level == 2 then
				if mining_level >= r.rockertunity_level[2] then rck_mult = data.rockertunity_mult[2]
				elseif mining_level >= r.rockertunity_level[1] then rck_mult = data.rockertunity_mult[1]
				else rck_mult = data.rockertunity_mult_base end
			elseif r.len_rockertunity_level == 1 then
				if mining_level >= r.rockertunity_level[1] then rck_mult = data.rockertunity_mult[1]
				else rck_mult = data.rockertunity_mult_base end
			else
				rck_mult = data.rockertunity_mult_base
			end
			rck_mult = rck_mult + outfit.rck + pocket.rck -- Add the magic golem and pocket rockertunity multiplier

			-- Rockertunity Ore
			local rck_ore_loss = (rck_mult * ore_swing) - (data.rck_loss * ore_swing)
			if rck_ore_loss > 1 then rck_ore_loss = 1 end
			local rck_ore = data.rck_hr * rck_ore_loss

			-- Rockertunity XP
			local rck_xp_min = data.rck_hr * ((rck_mult * xp_swing_min) - (data.rck_loss * xp_swing_min))
			local rck_xp_avg = data.rck_hr * ((rck_mult * xp_swing_avg) - (data.rck_loss * xp_swing_avg))
			local rck_xp_max = data.rck_hr * ((rck_mult * xp_swing_max) - (data.rck_loss * xp_swing_max))

			if method.rck == 1 then
				if r.core == 1 then
					-- !!! ROCKERTUNITY UPDATE: DISABLE IF EXPLOSIVE IS ACTIVE !!!
					if explosive == 0 then
						xp_hr_min = math.floor(xp_hr_min + rck_xp_min)
						xp_hr_avg = math.floor(xp_hr_avg + rck_xp_avg)
						xp_hr_max = math.floor(xp_hr_max + rck_xp_max)
						base_ore = base_ore + rck_ore
						total_ore = total_ore + rck_ore * ore_boosts
					end
					-- !!! END ROCKERTUNITY UPDATE !!!
				end
			end

			if r.diff == 1 then total_ore = 0 end -- Remove ore for unknown HP (seren stones / arc) temporary until their HP has been decided

			-- Stone spirits used, and cost
			local spirits_used = 0
			local spirit_cost = 0
			if spirits == 1 and r.spirits ~= nil and r.spirits > 0 then
				spirits_used = base_ore * r.spirits
				if juju ~= 'None' then
					spirits_used = spirits_used - base_ore * 0.1
				end
				spirits_used = math.ceil(spirits_used)
				spirit_cost = gemw.price(r.spirit_name)
			end

			-- Urns used, and cost
			local urns_used = 0
			local urn_cost = 0
			local urn_img = ''
			if xp_boosts_rock > xp_boosts then
				for _, urn in ipairs(urntypes) do
					if urn.level >= r.level then
						urn_img = urn.img..' '
						urns_used = math.ceil(xp_hr_avg / urn.xp)
						urn_cost = urn.ge
						break
					end
				end
			end

			-- Apply experience boosts and crystallise
			if r.core == 1 then
				total_ore = math.floor(10 * total_ore * spell.ore)/10
				xp_hr_min = math.floor(xp_hr_min * (xp_boosts_rock + spell.xp))
				xp_hr_avg = math.floor(xp_hr_avg * (xp_boosts_rock + spell.xp))
				xp_hr_max = math.floor(xp_hr_max * (xp_boosts_rock + spell.xp))
			else
				total_ore = math.floor(10 * total_ore)/10
				xp_hr_min = math.floor(xp_hr_min * xp_boosts_rock)
				xp_hr_avg = math.floor(xp_hr_avg * xp_boosts_rock)
				xp_hr_max = math.floor(xp_hr_max * xp_boosts_rock)
			end

			-- Do not calculate XP if using a dwarven ramhammer
			if ramtmp == 'Damage Mode' then
				xp_hr_min = 0
				xp_hr_avg = 0
				xp_hr_max = 0
			elseif ramtmp == 'XP Mode' then
				total_ore = 0
			end

			-- Calculate GPH for best ore in the category
			local gold_per_hour = -1
			if (nil == r.avg_value) == false then
				gold_per_hour = total_ore * r.avg_value
			elseif (nil == r.name_GrandExchange) == false then
				local total_price = 0
				local count = 0
				for index,name in pairs(r.name_GrandExchange) do
					total_price = total_price + gemw.price(name)
					count = count + 1
				end
				gold_per_hour = total_ore * total_price / count
			else
				gold_per_hour = 0
			end

			-- Add a row to the table
			if not has_target then
				-- Total Geodes
				local geodes = ""
				local geode_price = 0
				local total_geode = 0
				if r.core == 1 then												-- Geodes only work with core rocks
					if method.rck == 1 then geodes = (base_ore + rck_ore) * geode_chance
					else					geodes = base_ore * geode_chance end
					if r.geode == 'Sedimentary' then
						local sednum = math.floor(100*geodes)/100
						geode_price = sednum * geodePrice('sedimentary')
						geodes = '[[File:Sedimentary geode.png|Sedimentary geode|link=Sedimentary geode]] '..sednum
						total_geode = sednum
					else
						local ignnum = math.floor(100*(geodes*(1-meta)))/100
						local metnum = math.floor(100*(geodes*meta))/100
						geode_price = ignnum * geodePrice('igneous') + metnum * geodePrice('metamorphic')
						geodes = '[[File:Igneous geode.png|Igneous geode|link=Igneous geode]] '..ignnum..'<br>[[File:Metamorphic geode.png|Metamorphic geode|link=Metamorphic geode]] '..metnum
						total_geode = ignnum + metnum
					end
				end

				-- Urn cost
				urn_cost = urns_used * urn_cost

				-- Spirits cost
				spirit_cost = spirits_used * spirit_cost

				-- Porter cost
				local porter_cost = base_ore * math.min(r.ore, 1) * porter * (1 - impsouled)

				-- GP per hour
				local total_profit = gold_per_hour + math.floor(geode_price) - urn_cost - spirit_cost - porter_cost

				-- Clean up values and ignore zero values for a cleaner looking calculator
				if net_hardness == 0 then net_hardness = "" end
				if total_ore == 0 then total_ore = "" end
				if r.core == 0 then geode_chance = ""
				else geode_chance = (geode_chance*100)..'%'
				end

				local row = t:tag('tr')
				row:tag('td')  :wikitext(r.level)                  			:css('text-align', 'center')    :done()
				:tag('td')  :wikitext(r.img..' '..r.name)           		                    	        :done()
				:tag('td')  :wikitext(net_hardness)             			:css('text-align', 'center')    :done()
				:tag('td')  :wikitext((crit_chance*100)..'%')   			:css('text-align', 'center')    :done()
				:tag('td')  :wikitext((double_ore_chance*100)..'%') 		:css('text-align', 'center')    :done()
				:tag('td')  :wikitext(fnum(xp_hr_min))          			:css('text-align', 'center')    :done()
				:tag('td')  :wikitext("'''"..fnum(xp_hr_avg).."'''")		:css('text-align', 'center')	:done()
				:tag('td')  :wikitext(fnum(xp_hr_max))						:css('text-align', 'center')    :done()
				if spirits > 0 then
					if r.spirits ~= nil and r.spirits > 0 then
						row:tag('td')	:wikitext(string.format('[[File:%s.png|%s|link=%s]]', r.spirit_name, r.spirit_name, r.spirit_name)..fnum(spirits_used))
						:css('text-align', 'center')	:attr('data-sort-value', urns_used)    :done()
					else
						row:tag('td')	:wikitext(fnum(spirits_used) or "")	:css('text-align', 'center')	:attr('data-sort-value', urns_used)    :done()
					end
				end
				if urns > 0 then
					row:tag('td')	:wikitext(urn_img..fnum(urns_used))		:css('text-align', 'center')	:attr('data-sort-value', urns_used)    :done()
				end
				row:tag('td')  :wikitext(fnum(total_ore) or "")       		:css('text-align', 'center')    :done()
				:tag('td')  :wikitext(coins(gold_per_hour,'coins'))			:css('text-align', 'center')    :done()
				:tag('td')  :wikitext(geode_chance)							:css('text-align', 'center')    :done()
				:tag('td')  :wikitext(geodes)								:css('text-align', 'center')    :attr('data-sort-value', total_geode)	:done()
				:tag('td')  :wikitext(coins(total_profit,'coins'))			:css('text-align', 'center')    :done()
				:done()
			else
				-- Color rows based on current/target levels
				local row_classes = 'table-bg-yellow sortbottom'
				if target_lvl < r.level then 			row_classes = 'table-bg-red sortbottom'
				elseif mining_level >= r.level then		row_classes = 'table-bg-green' end

				-- Calculate time needed
				local min_time = math.ceil(remaining / xp_hr_max)
				local avg_time = math.ceil(remaining / xp_hr_avg)
				local max_time = math.ceil(remaining / xp_hr_min)

				-- Spirits used, and cost
				spirits_used = math.ceil(avg_time * spirits_used)
				spirit_cost = spirits_used * spirit_cost

				-- Urns used, and cost
				urns_used = math.ceil(avg_time * urns_used)
				urn_cost = urns_used * urn_cost

				-- Porters cost
				local porter_cost = avg_time * base_ore * math.min(r.ore, 1) * porter * (1 - impsouled)

				-- Total ore
				total_ore = math.floor(total_ore * avg_time)
				-- Total ore cost
				local ore_cost = math.floor(avg_time * gold_per_hour)

				-- Total geodes and price
				local geodes = ""
				local geode_price = 0
				local total_geode = 0
				if r.core == 1 then												-- Geodes only work with core rocks
					if method.rck == 1 then geodes = (base_ore + rck_ore) * geode_chance
					else					geodes = base_ore * geode_chance end
					if r.geode == 'Sedimentary' then
						local sednum = math.floor(avg_time * (100*geodes)/100)
						geode_price = sednum * geodePrice('sedimentary')
						geodes = '[[File:Sedimentary geode.png|Sedimentary geode|link=Sedimentary geode]] '..sednum
						total_geode = sednum
					else
						local ignnum = math.floor(avg_time * 100*(geodes*(1-meta))/100)
						local metnum = math.floor(avg_time * 100*(geodes*meta)/100)
						geode_price = ignnum * geodePrice('igneous') + metnum * geodePrice('metamorphic')
						geodes = '[[File:Igneous geode.png|Igneous geode|link=Igneous geode]] '..ignnum..'<br>[[File:Metamorphic geode.png|Metamorphic geode|link=Metamorphic geode]] '..metnum
						total_geode = ignnum + metnum
					end
				end

				-- Total profit and profit per xp
				local total_profit = ore_cost + geode_price - urn_cost - spirit_cost - porter_cost
				local profit_xp = math.floor((total_profit / remaining)*100) / 100

				-- Clean up values and ignore zero values for a cleaner looking calculator
				if net_hardness == 0 then net_hardness = "" end
				if total_ore == 0 then total_ore = "" end
				if r.core == 0 then geode_chance = ""
				else geode_chance = (geode_chance*100)..'%'
				end

				local row = t:tag('tr')	:addClass(row_classes)
				row:tag('td')	:wikitext(r.img..' '..r.name)												:done()
				:tag('td')  :wikitext(r.level)                  			:css('text-align', 'center')    :done()
				:tag('td')  :wikitext(fnum(min_time))          				:css('text-align', 'center')    :done()
				:tag('td')  :wikitext("'''"..fnum(avg_time).."'''")			:css('text-align', 'center')	:done()
				:tag('td')  :wikitext(fnum(max_time))						:css('text-align', 'center')    :done()
				if spirits > 0 then
					if r.spirits ~= nil and r.spirits > 0 then
						row:tag('td')	:wikitext(string.format('[[File:%s.png|%s|link=%s]]', r.spirit_name, r.spirit_name, r.spirit_name)..fnum(spirits_used))
						:css('text-align', 'center')	:attr('data-sort-value', urns_used)    :done()
					else
						row:tag('td')	:wikitext(fnum(spirits_used) or "")	:css('text-align', 'center')	:attr('data-sort-value', urns_used)    :done()
					end
				end
				if urns > 0 then
					row:tag('td')	:wikitext(urn_img..fnum(urns_used))		:css('text-align', 'center')	:attr('data-sort-value', urns_used)    :done()
				end
				row:tag('td') :wikitext(fnum(total_ore) or "")       		:css('text-align', 'center') 	:done()
				:tag('td')  :wikitext(geodes)								:css('text-align', 'center')	:attr('data-sort-value', total_geode)   :done()
				:tag('td')  :wikitext(coins(ore_cost,'coins'))				:css('text-align', 'center')    :done()
				:tag('td')  :wikitext(coins(total_profit,'coins'))			:css('text-align', 'center')    :done()
				:tag('td')  :wikitext(coins(profit_xp,'coins'))				:css('text-align', 'center')    :done()
				:done()
			end
		end
	end

	local msg = mw.html.create('div')

	-- Add message of xp needed to reach target
	if has_target then
		local msgtxt = 'To train mining from '..commas(mining_xp)..' experience (level '..virtual_mining_level..') to '..commas(target_xp)..' experience (level '..target_lvl..'), '..commas(remaining)..' experience is required.'
		msg:tag('p'):css({['font-size'] = "1.1em", ['font-weight'] = "bold"}):wikitext(msgtxt):done()
	end
	-- Add message for pickaxe level
	if higher_pick then
		local msgtxt = orig_pick.name..' pickaxe requires mining level '..orig_pick.level..' to use. Your pickaxe will act as a '..pick.name..' pickaxe.'
		msg:tag('p'):css({['font-size'] = "1em", ['font-weight'] = "bold", ['color'] = "red",}):wikitext(msgtxt):done()
	end

	return tostring(msg)..tostring(t)
end


--[=[ remainingExp
-- Finds and returns experiences and levels based on inputs
-- Inputs:
--      curLvl		The current level or 1
--		curXP		The current xp or 1
--      tgtLvl		The target level or 0
--		tgtXP		The target xp or 0
-- Returns:
--      current level,
--      current experience,
--      goal level,
--      goal experience,
--      experience remaining
--]=]
function remainingExp(curLvl, curXP, tgtLvl, tgtXP)
	local remaining

	if curLvl > 120 and curXP == 1 then
		curXP = curLvl
	elseif curXP <= 120 and curLvl == 1 then
		curLvl = curXP
		curXP = 1
	end
	if tgtLvl > 120 and tgtXP == 0 then
		tgtXP = tgtLvl
	elseif tgtXP <= 120 and tgtLvl == 0 then
		tgtLvl = tgtXP
		tgtXP = 0
	end

	if curXP == 1 then
		curXP = xp(curLvl)
	else
		curLvl = level(curXP)
	end

	if tgtLvl > 0 or tgtXP > 0 then
		if tgtXP == 0 then
			tgtXP = xp(tgtLvl)
		else
			tgtLvl = level(tgtXP)
		end
	end

    -- Prevent negative values
    local remaining = math.ceil(tgtXP - curXP)
    if remaining < 0 then
        remaining = 0
    end
    return curLvl, curXP, tgtLvl, tgtXP, remaining
end

--[=[ geodePrice
-- Calculates and returns average price for a geode based on its possible rewards
-- Inputs:
--      gtype		Geode type
-- Returns:
--      average price
--]=]
function geodePrice(gtype)
	local total = 0
	local items = 0

	-- Loop over geode and get prices
	for index,i in ipairs(data[gtype]) do
		if gtype == 'metamorphic' then
			if i.name == 'Dragon equipment' then
				-- Dragon drops based on Mod Breezy on Discord 22-Jan-19
				local dragonequip = {
					gemw.price('Dragon battleaxe'),
					gemw.price('Off-hand dragon battleaxe'),
					gemw.price('Dragon claw'),
					gemw.price('Off-hand dragon claw'),
					gemw.price('Dragon longsword'),
					gemw.price('Off-hand dragon longsword'),
					gemw.price('Dragon mace'),
					gemw.price('Off-hand dragon mace'),
					gemw.price('Dragon scimitar'),
					gemw.price('Off-hand dragon scimitar'),
					gemw.price('Dragon warhammer'),
					gemw.price('Off-hand dragon warhammer'),
					gemw.price('Dragon hasta'),
					gemw.price('Dragon 2h sword'),
					gemw.price('Dragon halberd'),
					gemw.price('Dragon spear'),
					gemw.price('Dragon full helm'),
					gemw.price('Dragon helm'),
					gemw.price('Dragon chainbody'),
					gemw.price('Dragon kiteshield'),
					gemw.price('Dragon sq shield'),
					gemw.price('Dragon platelegs'),
					gemw.price('Dragon plateskirt'),
					gemw.price('Dragon boots'),
				}
				local sum = 0
				for i,v in ipairs(dragonequip) do
					sum = sum + v
				end
				total = total + sum/table.getn(dragonequip)
				items = items + 1
			elseif i.ge == 'no' then
				if i.alch == 'yes' then
					total = total + i.alchvalue
					items = items + 1
				elseif i.merch == 'yes' then
					total = total + i.merchvalue
					items = items + 1
				else
					items = items + 1
				end
			else
				if i.number == 1 then
					if i.name == 'Uncut onyx' then
						total = total + gemw.price(i.name) * (1/10)
						--skip adding to count here, adding below for onyx dust since both are 1/14 chance like the rest
					else
						total = total + gemw.price(i.name)
						items = items + 1
					end
				else
					if i.name == 'Onyx dust' then
						local minprice = gemw.price(i.name) * i.low
						local maxprice = gemw.price(i.name) * i.high
						total = total + ((minprice+maxprice)/2)*(9/10)
						items = items + 1
					else
						local minprice = gemw.price(i.name) * i.low
						local maxprice = gemw.price(i.name) * i.high
						total = total + (minprice+maxprice)/2
						items = items + 1
					end
				end
			end
		else
			total = total + gemw.price(i.name) * i.prob
		end
	end

	if gtype == 'metamorphic' then
		total = total / items
	end

	return math.floor(total)
end

return p