Module:Skills/Artisan

From Melvor Idle
< Module:Skills
Revision as of 12:48, 5 March 2022 by Auron956 (talk | contribs) (Fix)
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 SkillData = mw.loadData('Module:Skills/data')

local Shared = require('Module:Shared')
local Items = require('Module:Items')
local Icons = require('Module:Icons')

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

	-- Find recipes for the relevant categories
	for i, recipe in ipairs(SkillData.Cooking.Recipes) do
		-- Note: Excludes Lemon cake
		if (categoryID == nil or recipe.category == categoryID) and recipe.masteryID >= 0 then
			table.insert(recipeArray, recipe)
		end
	end
	table.sort(recipeArray, function(a, b) return (a.level == b.level and a.masteryID < b.masteryID) or 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!!rowspan="2"|XP!!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.itemID)
		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|style="min-width:25px"|'..Icons.Icon({item.name, type='item', notext=true, size='50'}))
		table.insert(resultPart, '\r\n|style="min-width:25px"| ')
		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.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.timeString(recipe.baseInterval / 1000, true))
		table.insert(resultPart, '||style="text-align:right" data-sort-value="' .. recipe.baseXP .. '"|' .. Shared.formatnum(recipe.baseXP))
		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.qty}))
			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._getHerblorePotionTable(category)
	if string.upper(category) == 'COMBAT' then
		category = 0
	elseif string.upper(category) == 'SKILL' then
		category = 1
	elseif type(category) == 'string' then
		category = tonumber(category)
	end

	local potionArray = {}
	for i, potion in Shared.skpairs(SkillData.Herblore.Potions) do
		if potion.category == category then
			table.insert(potionArray, potion)
		end
	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')

	table.sort(potionArray, function(a, b) return a.level < b.level end)

	for i, potion in ipairs(potionArray) do
		table.insert(resultPart, '\r\n|-')
		if potion.name == 'Bird Nests Potion' then
			table.insert(resultPart, '\r\n|rowspan="4"|[[Bird Nest Potion]]')
		else
			table.insert(resultPart, '\r\n|rowspan="4"|[['..potion.name..']]')
		end
		table.insert(resultPart, '||rowspan="4" style="text-align:right"|'..potion.level)
		table.insert(resultPart, '||rowspan="4" style="text-align:right"|'..potion.baseXP)

		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.qty}))
		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.potionCharges..'||'..tierPot.description)
			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.getRunecraftingTable(frame)
	local category = frame.args ~= nil and frame.args[1] or frame
	return p._getRecipeTable('Runecrafting', category, {'Item', '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, {'Item', 'SkillLevel', 'SkillXP', 'GP', 'Ingredients'})
end

function p.getCraftingTable(frame)
	local category = frame.args ~= nil and frame.args[1] or frame
	return p._getRecipeTable('Crafting', category, {'Item', 'SkillLevel', 'SkillXP', 'GP', 'Ingredients'})
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 'ERROR: skillName must be a string'
	elseif not Shared.contains({'string', 'nil'}, type(categoryName)) then
		return 'ERROR: category must be a string or nil'
	elseif type(columnList) ~= 'table' or Shared.tableCount(columnList) == 0 then
		return 'ERROR: columnList must be a table with 1+ elements'
	end

	local categoryMap = {
		["Runecrafting"] = {
			["Runes"] = 0,
			["ComboRunes"] = 1,
			["Weapons"] = 2,
			["AirGear"] = 3,
			["WaterGear"] = 4,
			["EarthGear"] = 5,
			["FireGear"] = 6
		},
		["Fletching"] = {
			["Arrows"] = 0,
			["Shortbows"] = 1,
			["Longbows"] = 2,
			["Bolts"] = 3,
			["Crossbows"] = 4,
			["Javelins"] = 5
		},
		["Crafting"] = {
			["Leather"] = 0,
			["Dragonhide"] = 1,
			["Rings"] = 2,
			["Necklaces"] = 3,
			["Bags"] = 4
		}
	}
	local actionIntervals = {
		["Runecrafting"] = 2,
		["Fletching"]  = 2,
		["Crafting"] = 2
	}
	-- Validation: Category
	if categoryMap[skillName] == nil then
		return 'ERROR: The ' .. skillName .. ' skill is not supported by this function'
	elseif categoryName ~= '' and categoryMap[skillName][categoryName] == nil then
		local catNames = {}
		for catName, catID in pairs(categoryMap[skillName]) do
			table.insert(catNames, catName)
		end
		return 'ERROR: No such category ' .. categoryName .. ' for skill ' .. skillName .. ', the following are available: ' .. table.concat(catNames, ', ')
	end
	local categoryID = categoryMap[skillName][categoryName]
	local actionInterval = actionIntervals[skillName]

	-- Validation: Skill data
	local recipeKey = 'Recipes'
	if SkillData[skillName] == nil then
		return 'ERROR: Could not locate skill data for ' .. skillName
	elseif SkillData[skillName][recipeKey] == nil then
		return 'ERROR: Could not locate recipe data for ' .. skillName
	end

	-- Validation: Column list
	local columnDef = {
		["Item"] = {["header"] = 'Item\r\n!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}
	}
	-- Build the table header while we're here
	local resultPart = {}
	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 'ERROR: Invalid column ' .. colID .. ' requested'
		else
			table.insert(resultPart, '\r\n! ' .. columnDef[colID].header)
		end
	end

	-- Determine which recipes to include
	local recipeList = {}
	for i, recipe in ipairs(SkillData[skillName][recipeKey]) do
		if categoryID == nil or recipe.category == categoryID then
			table.insert(recipeList, recipe)
		end
	end
	if Shared.tableCount(recipeList) == 0 then
		return ''
	end
	table.sort(recipeList, function(a, b) return (a.level == b.level and a.masteryID < b.masteryID) or a.level < b.level end)

	-- Build rows based on recipes
	for i, recipe in ipairs(recipeList) do
		local item = Items.getItemByID(recipe.itemID)
		if item ~= nil then
			-- Some recipes have alternative costs, so the recipe may require multiple rows
			local costList = nil
			if recipe.alternativeCosts ~= nil and Shared.tableCount(recipe.alternativeCosts) > 0 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, '\r\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 == 'Item' then
							local namePrefix = spanStr
							if qty > 1 then
								namePrefix = namePrefix .. 'data-sort-value="' .. item.name .. '"'
							end
							table.insert(resultPart, '\r\n|' .. spanStr .. 'style="text-align:center;min-width:25px"| ' .. Icons.Icon({item.name, type='item', size='50', notext=true}))
							table.insert(resultPart, '\r\n|'.. (namePrefix ~= '' and namePrefix .. '| ' or ' ') .. (qty > 1 and '<b>' .. qty .. 'x</b> ' or '') .. Icons.Icon({item.name, type='item', noicon=true}))
						elseif colID == 'SkillLevel' then
							table.insert(resultPart, '\r\n|' .. spanStr .. 'style="text-align:right"| ' .. recipe.level)
						elseif colID == 'SkillXP' then
							table.insert(resultPart, '\r\n|' .. spanStr .. 'style="text-align:right"| ' .. recipe.baseXP)
						elseif colID == 'GP' then
							local val = math.floor(item.sellsFor)
							table.insert(resultPart, '\r\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.qty}))
								end
							end
							table.insert(resultPart, '\r\n|' .. (spanStr ~= '' and spanStr .. '| ' or ' ') .. table.concat(matArray, ', '))
						elseif colID == 'SkillXPSec' then
							table.insert(resultPart, '\r\n|' .. spanStr .. 'style="text-align:right"| ' .. string.format('%.2f', recipe.baseXP / actionInterval))
						elseif colID == 'GPSec' then
							local val = math.floor(item.sellsFor) * qty / actionInterval
							table.insert(resultPart, '\r\n|' .. spanStr .. 'data-sort-value="' .. val .. '"| ' .. Icons.GP(string.format('%.2f', val)))
						else
							table.insert(resultPart, '\r\n| ')
						end
					end
				end
			end
		end
	end
	table.insert(resultPart, '\r\n|}')
	return table.concat(resultPart)
end

return p