Module:Skills/Artisan

From Melvor Idle
The printable version is no longer supported and may have rendering errors. Please update your browser bookmarks and please use the default browser print function instead.

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

--Splitting some functions into here to avoid bloating a single file
--Contains function for skills that consume resources (ie smithing, cooking, herblore, etc.)
local p = {}

local Shared = require('Module:Shared')
local Constants = require('Module:Constants')
local GameData = require('Module:GameData')
local SkillData = GameData.skillData
local Items = require('Module:Items')
local Icons = require('Module:Icons')
local ItemSourceTables = require('Module:Items/SourceTables')

function p.getCookedItemsTable(frame)
	local category = frame.args ~= nil and frame.args[1] or frame
	local categoryMap = {
		["Cooking Fire"] = 'melvorD:Fire',
		["Furnace"] = 'melvorD:Furnace',
		["Pot"] = 'melvorD:Pot'
	}
	local categoryID = categoryMap[category]

	-- Find recipes for the relevant categories
	-- Note: Excludes Lemon cake
	local recipeArray = GameData.getEntities(SkillData.Cooking.recipes,
		function(recipe)
			return (categoryID == nil or recipe.categoryID == categoryID) and recipe.noMastery == nil
		end)
	table.sort(recipeArray, function(a, b) return a.level < b.level end)

	-- Logic for generating some cells of the table which are consistent for normal & perfect items
	local getHealingCell = function(item, qty)
		if item ~= nil then
			return 'data-sort-value="'..(math.floor(item.healsFor) * qty)..'"|'..Icons.Icon({"Hitpoints", type="skill", notext=true})..' '..math.floor(item.healsFor * 10)..(qty > 1 and ' (x'..qty..')' or '')
		else
			return ' '
		end
	end
	local getSaleValueCell = function(item, qty)
		if item ~= nil then
			return 'data-sort-value="'..math.floor(item.sellsFor * qty)..'"|'..Icons.GP(math.floor(item.sellsFor))..(qty > 1 and ' (x'..qty..')' or '')
		else
			return ' '
		end
	end

	local resultPart = {}
	table.insert(resultPart, '{| class="wikitable sortable stickyHeader"')
	table.insert(resultPart, '\r\n|- class="headerRow-0"')
	table.insert(resultPart, '\r\n!colspan="3" rowspan="2"|Cooked Item!!rowspan="2"|'..Icons.Icon({'Cooking', type='skill', notext=true})..' Level')
	table.insert(resultPart, '!!rowspan="2"|Cook Time (s)!!rowspan="2"|XP!!rowspan="2"|XP/s!!colspan="2"|Healing!!colspan="2"|Value!!rowspan="2"|Ingredients')
	table.insert(resultPart, '\r\n|- class="headerRow-1"')
	table.insert(resultPart, '\r\n!Normal!!' .. Icons.Icon({'Perfect', type='bonus', ext='png', notext=true, nolink=true}))
	table.insert(resultPart, '!!Normal!!' .. Icons.Icon({'Perfect', type='bonus', ext='png', notext=true, nolink=true}))

	for i, recipe in ipairs(recipeArray) do
		local item = Items.getItemByID(recipe.productID)
		local perfectItem = nil
		if recipe.perfectCookID ~= nil then
			perfectItem = Items.getItemByID(recipe.perfectCookID)
		end
		local qty = recipe.baseQuantity or 1

		table.insert(resultPart, '\r\n|-')
		table.insert(resultPart, '\r\n|class="table-img"|'..Icons.Icon({item.name, type='item', notext=true, size='50'}))
		table.insert(resultPart, '\r\n|class="table-img"| ')
		if perfectItem ~= nil then
			table.insert(resultPart, Icons.Icon({perfectItem.name, type='item', notext=true, size='50'}))
		end
		table.insert(resultPart, '||')
		if qty > 1 then
			table.insert(resultPart, qty..'x ')
		end
		table.insert(resultPart, Icons.getExpansionIcon(item.id))
		table.insert(resultPart, Icons.Icon({item.name, type='item', noicon = true}))
		table.insert(resultPart, '||style="text-align:right"|' .. recipe.level)
		table.insert(resultPart, '||style="text-align:right" data-sort-value="' .. recipe.baseInterval .. '"|' .. Shared.round(recipe.baseInterval / 1000, 2, 0))
		table.insert(resultPart, '||style="text-align:right" data-sort-value="' .. recipe.baseExperience .. '"|' .. Shared.formatnum(recipe.baseExperience))
		local xpRate = recipe.baseExperience / (recipe.baseInterval / 1000)
		table.insert(resultPart, '||style="text-align:right" data-sort-value="' .. xpRate .. '"|' .. Shared.round(xpRate, 2, 0))
		table.insert(resultPart, '||'..getHealingCell(item, qty)..'||'..getHealingCell(perfectItem, qty))
		table.insert(resultPart, '||'..getSaleValueCell(item, qty)..'||'..getSaleValueCell(perfectItem, qty))
		local matArray = {}
		for j, mat in ipairs(recipe.itemCosts) do
			local matItem = Items.getItemByID(mat.id)
			if matItem ~= nil then
				table.insert(matArray, Icons.Icon({matItem.name, type='item', notext=true, qty=mat.quantity}))
			end
		end
		table.insert(resultPart, '\r\n|'..table.concat(matArray, ' '))
	end

	table.insert(resultPart, '\r\n|}')
	return table.concat(resultPart)
end

local tierSuffix = { 'I', 'II', 'III', 'IV' }
function p._getPotionDescription(potion)
	-- TODO: Temporary fix below for incorrect Traps Potion descriptions. To amend
	-- once corrected within game data
	if potion.customDescription and not Shared.contains(potion.id, 'melvorTotH:Traps_Potion_') then
		return potion.customDescription
	elseif type(potion.modifiers) == 'table' and not Shared.tableIsEmpty(potion.modifiers) then
		return Constants.getModifiersText(potion.modifiers, false, true)
	else
		return ''
	end
end

function p._getHerblorePotionTable(categoryName)
	local categoryID = nil
	if string.upper(categoryName) == 'COMBAT' then
		categoryID = 'melvorF:CombatPotions'
	elseif string.upper(categoryName) == 'SKILL' then
		categoryID = 'melvorF:SkillPotions'
	else
		return Shared.printError('No such potion category ' .. (categoryName or 'nil'))
	end

	local potionArray = GameData.getEntities(SkillData.Herblore.recipes, function(potion) return potion.categoryID == categoryID end)
	table.sort(potionArray, function(a, b) return a.level < b.level end)

	local resultPart = {}
	table.insert(resultPart, '{|class="wikitable sortable stickyHeader"')
	table.insert(resultPart, '\r\n|- class="headerRow-0"')
	table.insert(resultPart, '\r\n!Potion!!'..Icons.Icon({'Herblore', type='skill', notext=true})..' Level')
	table.insert(resultPart, '!!XP!!Ingredients!!style="width:30px;"|Tier!!Value!!Charges!!Effect')

	for i, potion in ipairs(potionArray) do
		table.insert(resultPart, '\r\n|-')
		local expIcon = Icons.getExpansionIcon(potion.potionIDs[1])
		table.insert(resultPart, '\r\n|rowspan="4"|'..expIcon..'[['..potion.name..']]')
		table.insert(resultPart, '||rowspan="4" style="text-align:right"|'..potion.level)
		table.insert(resultPart, '||rowspan="4" style="text-align:right"|'..potion.baseExperience)

		local matArray = {}
		for j, mat in ipairs(potion.itemCosts) do
			local matItem = Items.getItemByID(mat.id)
			table.insert(matArray, Icons.Icon({matItem.name, type='item', notext=true, qty=mat.quantity}))
		end
		table.insert(resultPart, '||rowspan="4"|'..table.concat(matArray, ', ')..'||')

		local tierRows = {}
		for j, potionID in ipairs(potion.potionIDs) do
			local rowTxt = {}
			local tierPot = Items.getItemByID(potionID)
			table.insert(rowTxt, Icons.Icon({tierPot.name, type='item', notext=true}))
			table.insert(rowTxt, Icons.Icon({tierPot.name, tierSuffix[j], type = 'item', noicon=true}))
			table.insert(rowTxt, '||style="text-align:right;" data-sort-value="'..tierPot.sellsFor..'"|'..Icons.GP(tierPot.sellsFor))
			table.insert(rowTxt, '||style="text-align:right;"|'..tierPot.charges..'|| '..p._getPotionDescription(tierPot))
			table.insert(tierRows, table.concat(rowTxt))
		end
		table.insert(resultPart, table.concat(tierRows, '\r\n|-\r\n|'))
	end

	table.insert(resultPart, '\r\n|}')
	return table.concat(resultPart)
end

function p.getHerblorePotionTable(frame)
	local category = frame.args ~= nil and frame.args[1] or frame
	return p._getHerblorePotionTable(category)
end

function p._getHerbloreHerbTable(args)
	local allHerbs = {}
	local allPotions = GameData.getEntities(SkillData.Herblore.recipes, function() return true end)
	
	-- Finds the herb from a potion along with the level required to make the potion.
	local function handlePotion(potion)
		local potionCosts = potion.itemCosts
		local level = potion.level
		
		if potionCosts == nil or level == nil then
			return
		end
		
		-- Find if this potion uses a herb, and which herb it is.
		for _, ingredient in pairs(potionCosts) do
			local ingredientID = ingredient.id
			if ingredientID == nil or string.sub(ingredientID, -5) ~= "_Herb" then
				return
			end
			
			-- Set the lowest level of potion this herb is used in.
			local currLevel = allHerbs[ingredientID] or 9999999
			if level < currLevel then
				allHerbs[ingredientID] = level
			end
		end
	end

	for _, potion in pairs(allPotions) do
		handlePotion(potion)
	end
	
	local sortedValues = Shared.sortDictionary(
		allHerbs, 
		function (a, b) return a.value < b.value end)

	local tbl = mw.html.create("table")
        :addClass("wikitable sortable stickyHeader")
        
    -- Add header
    tbl :tag("tr"):addClass("headerRow-0")
    	:tag("th"):wikitext(Icons.Icon({'Herblore', type='skill', notext=true})..' Level')
    	:tag("th"):wikitext("Herb")
    	:tag("th"):wikitext("Value")
    	:tag("th"):wikitext("Herb Sources")
    	:done()
    
    -- Fill wikitable.
    for _, v in pairs(sortedValues) do
    	local herbItem = Items.getItemByID(v['key'])
    	local herbLevel = v['value']
    	local dlcIcon = Icons.getExpansionIcon(herbItem.id)

    	-- Add rows
    	tbl :tag("tr")
    		:tag("td"):wikitext(herbLevel)
    		:tag("td"):wikitext(dlcIcon .. Icons.Icon({herbItem.name, type='item'}))
    		:tag("td"):wikitext(Icons.GP(herbItem.sellsFor))
    		:tag('td'):wikitext(ItemSourceTables._getItemSources(herbItem, false, nil, ' '))
    		:done()
    end

	return tostring(tbl)
end

function p.getHerbloreHerbTable(frame)
	local args = frame:getParent().args
	return p._getHerbloreHerbTable(args)
end

function p.getPotionTable(frame)
	local potionName = frame.args ~= nil and frame.args[1] or frame

	local recipe = GameData.getEntityByName(SkillData.Herblore.recipes, potionName)
	if recipe == nil then
		return Shared.printError('No potion named "' .. potionName .. '" was found')
	end

	local resultPart = {}
	table.insert(resultPart, '{| class="wikitable"')
	table.insert(resultPart, '\r\n!colspan=4|[['..potionName..']]')
	table.insert(resultPart, '\r\n|-\r\n!Potion!!Tier!!Charges!!Effect')

	for i, potionID in ipairs(recipe.potionIDs) do
		local tier = tierSuffix[i]
		local potion = Items.getItemByID(potionID)
		if potion ~= nil then
			table.insert(resultPart, '\r\n|-')
			table.insert(resultPart, '\r\n| ' .. Icons.Icon({potion.name, type='item', notext=true, size='60'}))
			table.insert(resultPart, '|| ' .. Icons.getExpansionIcon(potion.id) .. Icons.Icon({potion.name, tier, type='item', noicon=true}))
			table.insert(resultPart, '|| ' .. potion.charges .. '|| ' .. p._getPotionDescription(potion))
		end
	end

	table.insert(resultPart, '\r\n|}')
	return table.concat(resultPart)
end

function p.getRunecraftingTable(frame)
	local category = frame.args ~= nil and frame.args[1] or frame
	return p._getRecipeTable('Runecrafting', category, {'ItemImage', 'ItemName', 'SkillLevel', 'SkillXP', 'GP', 'Ingredients', 'SkillXPSec', 'GPSec'})
end

function p.getFletchingTable(frame)
	local category = frame.args ~= nil and frame.args[1] or frame
	return p._getRecipeTable('Fletching', category, {'ItemImage', 'ItemName', 'SkillLevel', 'SkillXP', 'GP', 'Ingredients'})
end

function p.getCraftingTable(frame)
	local category = frame.args ~= nil and frame.args[1] or frame
	local columns = {'ItemImage', 'ItemName', 'SkillLevel', 'SkillXP', 'GP', 'Ingredients'}
	if category == 'Rings' or category == 'Necklaces' then
		table.insert(columns, "Description")
	end
	return p._getRecipeTable('Crafting', category, columns)
end

function p.getSmithingTable(frame)
	local category = frame.args ~= nil and frame.args[1] or frame
	local columns = {'ItemImage', 'ItemName', 'SkillLevel', 'SkillXP', 'GP', 'Ingredients'}
	if category ~= 'Bars' then
		table.insert(columns, 'GPBar')
	end
	return p._getRecipeTable('Smithing', category, columns)
end

-- Given a skill name, category, and column list, produces a table of recipes for that
-- skill & category combination. If categoryName is '', all recipes are returned.
-- Note: This only supports a number of skills with consistent recipe data structures, being:
-- Fletching, Crafting, and Runecrafting
-- Valid column list options are: Item, SkillLevel, SkillXP, GP, Ingredients, SkillXPSec, GPSec
function p._getRecipeTable(skillName, categoryName, columnList)
	-- Validation: Parameters
	if type(skillName) ~= 'string' then
		return Shared.printError('skillName must be a string')
	elseif not Shared.contains({'string', 'nil'}, type(categoryName)) then
		return Shared.printError('category must be a string or nil')
	elseif type(columnList) ~= 'table' or Shared.tableIsEmpty(columnList) then
		return Shared.printError('columnList must be a table with 1+ elements')
	end

	local supportedSkills = {
		'Smithing',
		'Crafting',
		'Fletching',
		'Runecrafting'
	}
	if not Shared.contains(supportedSkills, skillName) then
		return Shared.printError('The ' .. skillName .. ' skill is not supported by this function')
	end

	-- Validation: Category
	local category = GameData.getEntityByName(SkillData[skillName].categories, categoryName)
	if category == nil then
		local catNames = {}
		for i, cat in pairs(SkillData[skillName].categories) do
			table.insert(catNames, cat.name)
		end
		return Shared.printError('No such category ' .. categoryName .. ' for skill ' .. skillName .. ', the following are available: ' .. table.concat(catNames, ', '))
	end
	local actionInterval = SkillData[skillName].baseInterval / 1000

	-- Validation: Skill data
	local recipeKey = 'recipes'
	if SkillData[skillName] == nil then
		return Shared.printError('Could not locate skill data for ' .. skillName)
	elseif SkillData[skillName][recipeKey] == nil then
		return Shared.printError('Could not locate recipe data for ' .. skillName)
	end

	-- Validation: Column list
	local columnDef = {
		["ItemImage"] = {["header"] = 'Item', ["altRepeat"] = false},
		["ItemName"] = {["header"] = 'Name', ["altRepeat"] = true},
		["SkillLevel"] = {["header"] = Icons.Icon({skillName, type='skill', notext=true}) .. ' Level', ["altRepeat"] = false},
		["SkillXP"] = {["header"] = 'XP', ["altRepeat"] = false},
		["GP"] = {["header"] = 'Value', ["altRepeat"] = true},
		["Ingredients"] = {["header"] = 'Ingredients', ["altRepeat"] = true},
		["SkillXPSec"] = {["header"] = 'XP/s', ["altRepeat"] = false},
		["GPSec"] = {["header"] = 'GP/s', ["altRepeat"] = true},
		["GPBar"] = {["header"] = 'Value/Bar', ["altRepeat"] = true },
		["Description"] = {["header"] = "Description", ["altRepeat"] = true}
	}
	-- Build the table header while we're here
	local resultPart, barIDList = {}, {}
	table.insert(resultPart, '{| class="wikitable sortable stickyHeader"\r\n|- class="headerRow-0"')
	for i, colID in ipairs(columnList) do
		if columnDef[colID] == nil then
			return Shared.printError('Invalid column ' .. colID .. ' requested')
		else
			table.insert(resultPart, '\r\n! ' .. columnDef[colID].header)
			if colID == 'GPBar' then
				-- For Smithing, a GP value per bar column is included. If this
				-- is requested, then obtain a list of bar item IDs
				barIDList = p.getBarItemIDs()
			end
		end
	end

	-- Determine which recipes to include
	local recipeList = GameData.getEntities(SkillData[skillName][recipeKey],
		function(recipe)
			return category.id == nil or recipe.categoryID == category.id
		end)
	if Shared.tableIsEmpty(recipeList) then
		return ''
	end
	table.sort(recipeList, function(a, b) return a.level < b.level end)

	-- Build rows based on recipes
	for i, recipe in ipairs(recipeList) do
		local ns, _ = GameData.getLocalID(recipe.id)
		local item = Items.getItemByID(recipe.productID)
		if item ~= nil then
			-- Some recipes have alternative costs, so the recipe may require multiple rows
			local costList = nil
			if recipe.alternativeCosts ~= nil and not Shared.tableIsEmpty(recipe.alternativeCosts) then
				costList = recipe.alternativeCosts
			else
				costList = {{["itemCosts"] = recipe.itemCosts}}
			end
			local costCount = Shared.tableCount(costList)
			-- Build one row per element within costList
			for recipeRow, costDef in ipairs(costList) do
				local rowspanStr = (recipeRow == 1 and costCount > 1 and 'rowspan="' .. costCount .. '" ') or ''
				local qty = (costDef.quantityMultiplier or 1) * (recipe.baseQuantity or 1)
				table.insert(resultPart, '\n|-')
				for j, colID in ipairs(columnList) do
					local altRepeat = columnDef[colID].altRepeat
					-- All columns must be generated if this is the very first row for a recipe,
					-- for subsequent rows only columns marked as altRepeat = true are generated
					if recipeRow == 1 or altRepeat then
						local spanStr = (not altRepeat and rowspanStr) or ''
						if colID == 'ItemImage' then
							table.insert(resultPart, '\n|' .. spanStr .. 'class="table-img"| ' .. Icons.Icon({item.name, type='item', size='50', notext=true}))
						elseif colID == "ItemName" then
							local namePrefix = spanStr
							if qty > 1 then
								namePrefix = namePrefix .. 'data-sort-value="' .. item.name .. '"'
							end
							table.insert(resultPart, '\n|'.. (namePrefix ~= '' and namePrefix .. '| ' or ' ') .. Icons.getExpansionIcon(item.id) .. (qty > 1 and '<b>' .. qty .. 'x</b> ' or '') .. Icons.Icon({item.name, type='item', noicon=true}))
						elseif colID == 'SkillLevel' then
							table.insert(resultPart, '\n|' .. spanStr .. 'style="text-align:right"| ' .. recipe.level)
						elseif colID == 'SkillXP' then
							table.insert(resultPart, '\n|' .. spanStr .. 'data-sort-value="' .. recipe.baseExperience ..'" style="text-align:right"| ' .. Shared.formatnum(recipe.baseExperience))
						elseif colID == 'GP' then
							local val = math.floor(item.sellsFor)
							table.insert(resultPart, '\n|' .. spanStr .. 'data-sort-value="' .. (val * qty) .. '"| ' .. Icons.GP(val) .. (qty > 1 and ' (x' .. qty .. ')' or ''))
						elseif colID == 'Ingredients' then
							local matArray = {}
							for k, mat in ipairs(costDef.itemCosts) do
								local matItem = Items.getItemByID(mat.id)
								if matItem ~= nil then
									table.insert(matArray, Icons.Icon({matItem.name, type='item', notext=true, qty=mat.quantity}))
								end
							end
							if recipe.gpCost ~= nil and recipe.gpCost > 0 then
								table.insert(matArray, Icons.GP(recipe.gpCost))
							end
							if recipe.scCost ~= nil and recipe.scCost > 0 then
								table.insert(matArray, Icons.SC(recipe.scCost))
							end
							table.insert(resultPart, '\n|' .. (spanStr ~= '' and spanStr .. '| ' or ' ') .. table.concat(matArray, ', '))
						elseif colID == 'SkillXPSec' then
							table.insert(resultPart, '\n|' .. spanStr .. 'style="text-align:right"| ' .. string.format('%.2f', recipe.baseExperience / actionInterval))
						elseif colID == 'GPSec' then
							local val = math.floor(item.sellsFor) * qty / actionInterval
							table.insert(resultPart, '\n|' .. spanStr .. 'data-sort-value="' .. val .. '"| ' .. Icons.GP(string.format('%.2f', val)))
						elseif colID == 'GPBar' then
							local barQty = 0
							for k, mat in ipairs(costDef.itemCosts) do
								if Shared.contains(barIDList, mat.id) then
									barQty = barQty + mat.quantity
								end
							end
							if barQty > 0 then
								local barVal = Shared.round(math.floor(item.sellsFor) * qty / barQty, 1, 1)
								table.insert(resultPart, '\n|' .. spanStr .. 'data-sort-value="' .. barVal .. '"| ' .. Icons.GP(barVal))
							else
								table.insert(resultPart, '\n|' .. spanStr .. 'data-sort-value="0" class="table-na"| N/A')
							end
						elseif colID == 'Description' then
							local descrip = Items._getItemStat(item, 'description')
							if descrip == 'No Description' and item.modifiers ~= nil and not Shared.tableIsEmpty(item.modifiers) then
								descrip = Constants.getModifiersText(item.modifiers, false)
							end
							table.insert(resultPart, '\n| '..spanStr..'|'..descrip)
						else
							table.insert(resultPart, '\n| ')
						end
					end
				end
			end
		end
	end
	table.insert(resultPart, '\n|}')
	return table.concat(resultPart)
end

function p.getBarItemIDs()
	local barIDList = {}
	for i, recipe in ipairs(SkillData.Smithing.recipes) do
		if recipe.categoryID == 'melvorD:Bars' then
			table.insert(barIDList, recipe.productID)
		end
	end
	return barIDList
end

return p