Module:MoneyMakingGuide: Difference between revisions

From Melvor Idle
(Add support for SC name and SC icon as item)
(Fix currency function calls)
 
(4 intermediate revisions by one other user not shown)
Line 27: Line 27:


local SpecialIcons = {
local SpecialIcons = {
['Gold Pieces'] = icons.GP(),
['Gold Pieces'] = icons._Currency('melvorD:GP'),
['Slayer Coins'] = icons.SC(),
['Slayer Coins'] = icons._Currency('melvorD:SlayerCoins'),
}
}


Line 89: Line 89:


local function parseProfitMargin(minProfit, maxProfit)
local function parseProfitMargin(minProfit, maxProfit)
-- Manually set maxProfit to numeric 0, only if it's nil.
if maxProfit == nil then maxProfit = 0 end
local min = tonumber(minProfit)
local min = tonumber(minProfit)
local max = tonumber(maxProfit)
local max = tonumber(maxProfit)
Line 99: Line 101:
if min then
if min then
sb  :append(icons.GP(num.round2(min / 1000)))
sb  :append(icons._Currency('melvorD:GP', num.round2(min)))
:append('k/hr Minimum ~ ')
:append(' to ')
end
end
sb  :append(icons.GP(num.round2(max / 1000)))
sb  :append(icons._Currency('melvorD:GP', num.round2(max)))
:append('k/hr Maximum')
:append(' per hour')
return sb:toString()
return sb:toString()
Line 238: Line 240:
valCell
valCell
:css('text-align','right')
:css('text-align','right')
:wikitext(icons.GP(num.round2(tot)))
:wikitext(icons._Currency('melvorD:GP', num.round2(tot)))
:attr("data-sort-value", tot)
:attr("data-sort-value", tot)
end
end
Line 354: Line 356:
         :tag("td")
         :tag("td")
         if args['profit'] then
         if args['profit'] then
         html:wikitext(icons.GP(num.round2(args['profit'])))
         html:wikitext(icons._Currency('melvorD:GP', num.round2(args['profit'])))
         else
         else
         html:wikitext(icons.GP(num.round2(maxProfit)))
         html:wikitext(icons._Currency('melvorD:GP', num.round2(maxProfit)))
         end
         end
         html = html
         html = html
Line 373: Line 375:
             :wikitext("Inputs")
             :wikitext("Inputs")
             if pInputs.TotalValue ~= 0 then
             if pInputs.TotalValue ~= 0 then
             html:wikitext(" (" .. icons.GP(num.round2(pInputs.TotalValue)) .. ")")
             html:wikitext(" (" .. icons._Currency('melvorD:GP', num.round2(pInputs.TotalValue)) .. ")")
             end
             end
             html = html
             html = html
Line 379: Line 381:
             :wikitext("Outputs")
             :wikitext("Outputs")
             if pOutputs.TotalValue ~= 0 then
             if pOutputs.TotalValue ~= 0 then
             html:wikitext(" (" .. icons.GP(num.round2(pOutputs.TotalValue)) .. ")")
             html:wikitext(" (" .. icons._Currency('melvorD:GP', num.round2(pOutputs.TotalValue)) .. ")")
             end
             end
             html = html
             html = html

Latest revision as of 14:52, 22 June 2024

Documentation for this module may be created at Module:MoneyMakingGuide/doc

local p = {}

local shared = require('Module:Shared')
local num = require('Module:Number')
local paramtest = require('Module:Shared/Paramtest')
local itemdb = require('Module:Items')
local icons = require('Module:Icons')
local StringBuilder = require('Module:StringBuilder')

-- Constants
local MaxDynamicArgs = 20
local AmountSuffix = 'amount'
local ValueSuffix = 'value'
local SkillPrefix = 'skillExp'

-- Determines the order of Icons
local DLCParams = { 
	toth  = icons.TotH(), 
	aod = icons.AoD(), 
	ita = icons.ItA()
}

local SpecialItems = {
	['GP'] = 'Gold Pieces',
	['SC'] = 'Slayer Coins',
}

local SpecialIcons = {
	['Gold Pieces'] = icons._Currency('melvorD:GP'),
	['Slayer Coins'] = icons._Currency('melvorD:SlayerCoins'),
}

--- Possible fix to allow wikicode (especially tables) to be passed to the 'explanation'
--- parameter, and get parsed by the wiki parser on return.
-- Not used currently, because it might not be necessary at all.
local function processMarkup(str)
	--return frame:preprocess(str)
end	

--- Formats a given string to TitleCase.
-- @param name (string) String to format.
-- @return (string) Formatted string, or the input if it's not a string.
local function formatItemName(name)
	-- Special case to correctly format GP name.
	if SpecialItems[name] then
        return SpecialItems[name]
    end

	-- Got to solve too many edge case swith this (like of and IV)
	-- Better just make the user use the correct name instead.
	--if type(name) == 'string' then
	--	return shared.titleCase(name)
	--end
	
	return name
end

local function formatSkillName(name)
	if type(name) == 'string' then
		return shared.titleCase(name)
	end
	
	return name
end

local function getErrorDiv(message)
	return mw.html.create('div')
		:css('color', 'red')
		:wikitext(message)
		:done()
end

local function getItemIcon(iconName)
	if SpecialIcons[iconName] then
		return SpecialIcons[iconName]
	end

	return icons.Icon({iconName, type='item', notext=true})
end

local function getSkillExpIcon(skillName, expValue)
	if expValue == nil then error("expValue is nil") end
	
	local icon = icons.Icon({skillName, type='skill', notext=true})
	expValue = num.formatnum(expValue)
	
	return icon .. ' ' .. expValue
end

local function parseProfitMargin(minProfit, maxProfit)
	-- Manually set maxProfit to numeric 0, only if it's nil.
	if maxProfit == nil then maxProfit = 0 end
	local min = tonumber(minProfit)
	local max = tonumber(maxProfit)
	
	if max == nil then
		error("maxProfit is not a valid number.") 
	end
	
	local sb = StringBuilder:new()
	
	if min then
		sb  :append(icons._Currency('melvorD:GP', num.round2(min)))
			:append(' to ')
	end
	
	sb  :append(icons._Currency('melvorD:GP', num.round2(max)))
		:append(' per hour')
		
	return sb:toString()
end

--- Parses the input and output items from the provided arguments.
-- @param args (table) Passed args table from the caller.
-- @param prefix (string) The prefix that determines input or output item parsing.
-- @return (table) A table containing parsed item information.
local function parseItemInOut(args, prefix)
	local items = {}
	local totalValue = 0
	
	for i = 1, MaxDynamicArgs do
		local numPrefix = prefix .. i

		-- Stop parsing. Could cause problems if user input skips indexes.
		if paramtest.is_empty(args[numPrefix]) then
			break
		end
			
		local pName = formatItemName(args[numPrefix])
		local pAmount = args[numPrefix .. AmountSuffix]
		local pValue = args[numPrefix .. ValueSuffix]

		-- Values *should* always exit this function with a non nil value.
		if paramtest.has_content(pAmount) then
			pAmount = tonumber(pAmount)
		end
		
		if not paramtest.has_content(pValue) or tonumber(pValue) == nil then
			pValue = itemdb.getItemValue(pName)
		else
			pValue = tonumber(pValue)
		end
		
		-- Default to 0 to add to totals.
		local totVal = pValue or 0
		local totAmt = pAmount or 0
		totalValue = totalValue + (totVal * totAmt)

		table.insert(items, {
			prmNumber = i,
			name = pName, 
			amount = pAmount, 
			value = pValue})
	end
	
	return {
		TotalValue = totalValue, 
		Items = items 
	}
end

--- Parses the skill experience from the provided arguments.
-- @param args (table) Passed args table from the caller.
-- @return (table) A table containing parsed skill experience information.
local function parseExp(args)
	local skills = {}
	for i = 1, MaxDynamicArgs do
		local skillPrefix = 'skillExp' .. i
		
		-- Stop parsing. Could cause problems if user input skips indexes.
		if paramtest.is_empty(args[skillPrefix]) then
			break
		end
			
		local pSkill = formatSkillName(args[skillPrefix])
		local pExp = tonumber(args[skillPrefix .. AmountSuffix])

		table.insert(skills, {
			prmNumber = i,
			name = pSkill, 
			exp = pExp})
	end
	
	-- Return nil if there is no experience earned.
	-- Lets the parent table know to enter 'None' instead
	if #skills == 0 then
        return nil
    end
	return skills
end

--- Builds the section of the mmg table that shows the input and output items.
-- @param items (table) A table containing items
-- @return (string) The HTML representation of the item table section.
local function buildItemTable(itemSet)
	if itemSet == nil or itemSet.Items == nil or #itemSet.Items == 0 then
		return nil
	end

	-- Create table with header for items
	local tbl = mw.html.create('table')
	tbl:addClass('wikitable sortable text-align-center')
		:attr('style', 'font-size: 1em; width: calc(100% + 2px); margin: -1px')
		:tag('tr')
			:tag('th')
				:addClass('unsortable')
			:tag('th'):wikitext('Item')
			:tag('th'):wikitext('Quantity')
			:tag('th'):wikitext('Value')
			:done()
	
	-- Create individual rows for items
	for _, i in pairs(itemSet.Items) do
		local row = mw.html.create('tr')
		
		local imgCell = mw.html.create('td')
			:wikitext(getItemIcon(i.name))
			:css('height', '28px')
			:done()
			
		local itemCell = mw.html.create('td')
			:css('text-align','left')
			:wikitext(string.format('[[%s]]', i.name))
			:done()
			
		local qtyCell = mw.html.create('td')
		if i.amount == nil then
			qtyCell:node(getErrorDiv("Unable to parse quantity for item: " .. i.name))
		else
			local qty = num.autoround(i.amount)
			qtyCell:wikitext(num.formatnum(qty))
		end
		qtyCell:done()
		
		local valCell = mw.html.create('td')
		if i.value == nil then
			valCell:node(getErrorDiv("Unable to get value for item: " .. i.name))
		else
			local tot = i.value * (i.amount or 0)
			valCell
				:css('text-align','right')
				:wikitext(icons._Currency('melvorD:GP', num.round2(tot)))
				:attr("data-sort-value", tot)
		end
		valCell:done()
		
		-- Add all cells to row, and row to table.
		row:node(imgCell):node(itemCell):node(qtyCell):node(valCell):done()
		tbl:node(row)
	end
	
	return tbl:done()
end

--- Builds the section of the mmg table that shows the skill experience gained.
-- @param items (table) A table containing skills and experience values.
-- @return (string) The HTML representation of the item table section.
local function buildExpLayout(skills)
	local layoutLines = {}
	for _, skillLine in pairs(skills) do
		local hasInvalidExp = false
		local span = nil
	
		if skillLine.exp == nil or skillLine.exp <= 0 then
			hasInvalidExp = true
		end
		
		local skillExp = num.round2(skillLine.exp or 0)
		local skillIcon = getSkillExpIcon(skillLine.name, skillExp)

		if hasInvalidExp then
			span = getErrorDiv("Skill in parameter " .. skillLine.prmNumber .. " has an invalid experience value.")
		else
			span = mw.html.create('div')
				:wikitext(skillIcon)
				:done()
		end

		table.insert(layoutLines, span)
	end
	
	return layoutLines
end

function p._buildMMGTable(args)
	if args['guideName'] == nil then
		error("Money Making Guide must have a valid guideName parameter.")
	end
	
	-- Parse arguments.
	local pSkills = parseExp(args)
	local pInputs = parseItemInOut(args, 'input')
	local pOutputs = parseItemInOut(args, 'output')
	local dlcIcons = p.getDLCIcons(args['dlc'], ' ')
	local minProfit = args['minimumProfit']
	local maxProfit = pOutputs.TotalValue - pInputs.TotalValue

	local tbl = mw.html.create()

	local html = tbl:tag("table")
    :addClass("wikitable")
    :attr('style', 'width: 100%; text-align: center;')
    :tag("tr")
        :tag("td")
            :attr("colspan", 2)
            :css('font-weight', 'bold')
            :wikitext(table.concat(dlcIcons) .. ' ')
            :wikitext(args['guideName'] or '{{{guideName}}}')
    :tag("tr")
        :tag("td")
            :attr("colspan", 2)
            :wikitext(parseProfitMargin(minProfit, maxProfit))
    :tag("tr")
        :tag("th")
            :attr("colspan", 2)
            :wikitext("Requirements")
    :tag('tr')
        :tag('th')
            :wikitext('Skills')
        :tag('th')
            :wikitext('Other')
        :done()
    :tag("tr")
        :tag("td")
        	:addClass('mmg-no-list')
        	:newline()
        	:wikitext(paramtest.default_to(args['skills'], 'None')) 
    :tag("td")
            :addClass('mmg-no-list')
        	:newline()
        	:wikitext(paramtest.default_to(args['other'], 'None')) 
    :tag('tr')
        :tag('th')
            :wikitext('Items')
        :tag('th')
            :wikitext('Recommended')
    :tag('tr')
        :tag('td')
        	:addClass('mmg-no-list')
        	:newline()
            :wikitext(paramtest.default_to(args['items'], 'None')) 
        :tag('td')
            :addClass('mmg-no-list')
        	:newline()
            :wikitext(paramtest.default_to(args['recommended'], 'None')) 
    :tag("tr")
        :tag("th")
            :attr("colspan", 2)
            :wikitext("Results")
    :tag("tr")
        :tag("th")
            :wikitext("Profit")
        :tag("th")
            :wikitext("Experience gained")
    :tag("tr")
        :tag("td")
        	if args['profit'] then
        		html:wikitext(icons._Currency('melvorD:GP', num.round2(args['profit'])))
        	else
        		html:wikitext(icons._Currency('melvorD:GP', num.round2(maxProfit)))
        	end
        	html = html
        :done()
        :tag("td")
        	if pSkills ~= nil then
        		for _, v in ipairs(buildExpLayout(pSkills)) do
        			html:node(v)
        		end
        	else
        		html:wikitext("None")
        	end
            html = html
    :tag("tr")
        :tag("th")
            :wikitext("Inputs")
            if pInputs.TotalValue ~= 0 then
            	html:wikitext(" (" .. icons._Currency('melvorD:GP', num.round2(pInputs.TotalValue)) .. ")")
            end
            html = html
        :tag("th")
            :wikitext("Outputs")
            if pOutputs.TotalValue ~= 0 then
            	html:wikitext(" (" .. icons._Currency('melvorD:GP', num.round2(pOutputs.TotalValue)) .. ")")
            end
            html = html
    :tag("tr")
        :tag("td")
        	local inputsTable = buildItemTable(pInputs)
        	if inputsTable ~= nil then
        		html:css('padding', '0')
        			:css('vertical-align', 'top')
        			:node(inputsTable)
        	else
        		html:wikitext("None")
        	end
        	html = html
        :tag("td")
        	local outputsTable = buildItemTable(pOutputs)
        	if outputsTable ~= nil then
        		html:css('padding', '0')
        			:css('vertical-align', 'top')
        			:node(outputsTable)
        	else
        		html:wikitext("None")
        	end
        	html = html
		:done()
	:done()
	
	return tbl:done()
		:newline()
end

function p.buildMMGTable(frame)
	local args = frame:getParent().args
	return p._buildMMGTable(args)
end

--- Returns the parsed result of the dlcParam.
-- @param dlcParam (string) A string separated by , listing the DLCs required for the MMG
-- @return (table) A table containing the items of provided DLCs
function p.getDLCIcons(dlcParam)
	if type(dlcParam) ~= 'string' then return {} end

	local input = shared.splitString(dlcParam, ',', true)
	local dlcs = {}
	for _, arg in pairs(input) do
		local argLower = arg:lower()
		table.insert(dlcs, DLCParams[argLower])
	end

	return dlcs
end

function p.main(frame)
	error("Call a specific function rather than main.")
end

function p.test()
	local args = {
		guideName        ='Mining Pure Crystal',
category         ='Non-combat',
dlc              ='aod, toth',
skills           =[[
*{{SkillReq|Mining|85}}
*{{SkillReq|Herblore|53}}]],
items            =[=[
*{{ItemIcon|Mining Gloves}}
*{{ItemIcon|Perfect Swing Potion IV}}]=],
other            =[=[
*{{ItemIcon|Pure Crystal|notext=true}}{{Icon|Mastery|notext=true}} Level 99
*{{UpgradeIcon|Dragon Pickaxe}}
*[[Mining#Mastery Pool Checkpoints|95% Mining Mastery Pool Checkpoint]]]=],
recommended      =[=[
[[Money_Making/Mining_Pure_Crystal#Improves_GP_Rate|Bonusses that improve profit]]]=],
	}
	
	local t = p._buildMMGTable(args)
	
	mw.log(t)
end

-- function p.test()
-- 	local args = {
-- 		guideName = "",
-- 		interval = "",
-- 		skills = "",
-- 		items = "",
-- 		other = "",
-- 		recommended = "",
-- 		skillExp1 = "Maagic",
-- 		skillExp1amount = "-1000",
-- 		skillExp2 = "Mining",
-- 		skillExp2amount = "420",
-- 		input1 = "nature rune",
-- 		input1amount = "5",
-- 		input1value = "69",
-- 		input2 = "Fire Rune",
-- 		input2amount = "20",
-- 		output1 = "gp",
-- 		output1amount = "1050",
-- 		category = "",
-- 		dlc = "aod, toth",
-- 		intensity = "",
-- 		explanation = ""
-- 	}
-- 	
-- 	mw.log(p._buildMMGTable(args))
-- end

return p