Difference between revisions of "Module:Skills/Artisan"
From Melvor Idle
ByteFoolish (talk | contribs) (getRunecraftingComboRunes) |
(_getRecipeTable: Support GP & SC costs) |
||
(27 intermediate revisions by 4 users not shown) | |||
Line 4: | Line 4: | ||
local SkillData = mw.loadData('Module:Skills/data') | local SkillData = mw.loadData('Module:Skills/data') | ||
− | |||
local Shared = require('Module:Shared') | local Shared = require('Module:Shared') | ||
local Items = require('Module:Items') | local Items = require('Module:Items') | ||
local Icons = require('Module:Icons') | local Icons = require('Module:Icons') | ||
− | |||
function p.getCookedItemsTable(frame) | 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 | end | ||
local tierSuffix = { 'I', 'II', 'III', 'IV' } | local tierSuffix = { 'I', 'II', 'III', 'IV' } | ||
function p._getHerblorePotionTable(category) | 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 | end | ||
function p.getHerblorePotionTable(frame) | function p.getHerblorePotionTable(frame) | ||
− | + | local category = frame.args ~= nil and frame.args[1] or frame | |
− | + | return p._getHerblorePotionTable(category) | |
end | end | ||
− | function p. | + | 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 | end | ||
− | function p. | + | 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 | end | ||
− | function p. | + | -- 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 | ||
+ | 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.gpCost)) | ||
+ | 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 | end | ||
return p | return p |
Latest revision as of 22:06, 17 March 2022
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 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.gpCost)) 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