Module:Skills/Artisan: Difference between revisions
From Melvor Idle
Falterfire (talk | contribs) (added GP icon to item price column in crafting tables) |
m (Add perfect sell value to cooking table) |
||
(83 intermediate revisions by 6 users not shown) | |||
Line 2: | Line 2: | ||
--Contains function for skills that consume resources (ie smithing, cooking, herblore, etc.) | --Contains function for skills that consume resources (ie smithing, cooking, herblore, etc.) | ||
local p = {} | local p = {} | ||
local Shared = require('Module:Shared') | local Shared = require('Module:Shared') | ||
local Common = require('Module:Common') | |||
local GameData = require('Module:GameData') | |||
local SkillData = GameData.skillData | |||
local Modifiers = require('Module:Modifiers') | |||
local Skills = require('Module:Skills') | |||
local Items = require('Module:Items') | local Items = require('Module:Items') | ||
local Icons = require('Module:Icons') | local Icons = require('Module:Icons') | ||
local ItemSourceTables = require('Module:Items/SourceTables') | |||
local Num = require('Module:Number') | |||
function p.getCookedItemsTable(frame) | function p.getCookedItemsTable(frame) | ||
local | local args = frame.args ~= nil and frame.args or frame | ||
local | local category = args[1] | ||
local realmName = args.realm | |||
local realm = Skills.getRealmFromName(realmName) | |||
if realm == nil then | |||
return Shared.printError('Failed to find a realm with name ' .. (realmName or 'nil')) | |||
end | end | ||
local skillID = 'Cooking' | |||
-- | local categoryMap = { | ||
local | ["Cooking Fire"] = 'melvorD:Fire', | ||
["Furnace"] = 'melvorD:Furnace', | |||
return | ["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 | |||
and Skills.getRecipeRealm(recipe) == realm.id | |||
) | |||
end | end | ||
) | |||
table.sort(recipeArray, function(a, b) return Skills.standardRecipeSort(skillID, a, b) end) | |||
local cookIcon = Icons._SkillRealmIcon('Cooking', realm.id) | |||
local perfectIcon = Icons.Icon({'Perfect', type='bonus', ext='png', notext=true, nolink=true, size=20}) | |||
local healIcon = Icons.Icon({"Hitpoints", type="skill", notext=true, nolink=true, size=20}) | |||
-- Only include time for cooking fire, as the other options have a fixed interval. | |||
local includeTime = category == "Cooking Fire" | |||
local | local html = mw.html.create('table') | ||
:addClass('wikitable sortable stickyHeader') | |||
for i, item | local header0 = html:tag('tr'):addClass("headerRow-0") | ||
header0 :tag('th'):wikitext('Cooked Item') | |||
:attr('colspan', 3) | |||
:attr('rowspan', 2) | |||
header0 :tag('th'):wikitext(cookIcon .. '<br>Level') | |||
:attr('rowspan', 2) | |||
header0 :tag('th'):wikitext('[[DLC]]') | |||
:attr('rowspan', 2) | |||
if includeTime == true then | |||
header0 :tag('th'):wikitext('Cook<br>Time (s)') | |||
:attr('rowspan', 2) | |||
end | |||
header0 :tag('th'):wikitext(cookIcon .. '<br>XP') | |||
:attr('rowspan', 2) | |||
header0 :tag('th'):wikitext('XP/s') | |||
:attr('rowspan', 2) | |||
header0 :tag('th'):wikitext('Total Healing') | |||
:attr('colspan', 2) | |||
header0 :tag('th'):wikitext('Ingredients') | |||
:attr('rowspan', 2) | |||
header0 :tag('th'):wikitext('Value') | |||
:attr('colspan', 2) | |||
html:tag('tr'):addClass("headerRow-1") | |||
:tag('th'):wikitext('Normal') | |||
:tag('th'):wikitext('Perfect') | |||
:tag('th'):wikitext('Normal') | |||
:tag('th'):wikitext('Perfect') | |||
for i, recipe in ipairs(recipeArray) do | |||
local level = Skills.getRecipeLevel(skillID, recipe) | |||
local baseXP = recipe.baseAbyssalExperience or recipe.baseExperience | |||
local baseInt = recipe.baseInterval / 1000 | |||
local xpRate = baseXP / baseInt | |||
local item = Items.getItemByID(recipe.productID) | |||
local currency = item.sellsForCurrency or 'melvorD:GP' | |||
local perfectItem = nil | local perfectItem = nil | ||
if | if recipe.perfectCookID ~= nil then | ||
perfectItem = Items.getItemByID( | perfectItem = Items.getItemByID(recipe.perfectCookID) | ||
end | end | ||
local qty = item. | local qty = recipe.baseQuantity or 1 | ||
local normalHeal = math.floor(item.healsFor * 10) * qty | |||
local perfectHeal = math.floor(perfectItem.healsFor * 10) * qty | |||
local row = html:tag('tr') | |||
row:tag('td'):wikitext(Icons.Icon({item.name, type='item', notext=true})) | |||
:addClass("table-img") | |||
if perfectItem ~= nil then | if perfectItem ~= nil then | ||
row:tag('td'):wikitext(Icons.Icon({perfectItem.name, type='item', notext=true})) | |||
:addClass("table-img") | |||
end | end | ||
local nameCell = row:tag('td') | |||
nameCell:wikitext('[[' .. item.name .. ']]') | |||
if qty > 1 then | if qty > 1 then | ||
nameCell:wikitext(' x' .. qty) | |||
end | end | ||
row:tag('td'):wikitext(level) | |||
:css('text-align', 'center') | |||
row:tag('td'):wikitext(Icons.getDLCColumnIcon(item.id)) | |||
:css('text-align', 'center') | |||
:attr('data-sort-value', Icons.getExpansionID(item.id)) | |||
if includeTime == true then | |||
row:tag('td'):wikitext(Num.round(baseInt, 2, 0) .. 's') | |||
:css('text-align', 'right') | |||
:attr('data-sort-value', baseInt) | |||
end | |||
row:tag('td'):wikitext(Num.formatnum(baseXP)) | |||
:css('text-align', 'right') | |||
:attr('data-sort-value', baseXP) | |||
row:tag('td'):wikitext(Num.formatnum(Num.round(xpRate, 0, 0))) | |||
:css('text-align', 'right') | |||
:attr('data-sort-value', xpRate) | |||
row:tag('td'):wikitext(healIcon .. ' ') | |||
:wikitext(Num.formatnum(normalHeal)) | |||
:attr('data-sort-value', normalHeal) | |||
row:tag('td'):wikitext(perfectIcon .. ' ') | |||
:wikitext(Num.formatnum(perfectHeal)) | |||
:attr('data-sort-value', perfectHeal) | |||
local ingrCell = row:tag('td') | |||
local matArray = {} | |||
for j, mat in ipairs(recipe.itemCosts) do | |||
local matItem = Items.getItemByID(mat.id) | |||
if matItem ~= nil then | |||
local sub = mw.html.create('sub') | |||
:wikitext(mat.quantity .. 'x') | |||
:addClass('item-qty') | |||
:done() | |||
ingrCell:node(sub) | |||
ingrCell:wikitext(Icons.Icon({matItem.name, type='item', notext=true})) | |||
end | |||
end | end | ||
row:tag('td'):wikitext(Icons._Currency(currency, item.sellsFor)) | |||
:css('text-align', 'right') | |||
row:tag('td'):wikitext(Icons._Currency(currency, perfectItem.sellsFor)) | |||
:css('text-align', 'right') | |||
end | end | ||
return tostring(html) | |||
return | |||
end | end | ||
local tierSuffix = { 'I', 'II', 'III', 'IV' } | local tierSuffix = { 'I', 'II', 'III', 'IV' } | ||
function p. | 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( | elseif type(potion.modifiers) == 'table' and not Shared.tableIsEmpty(potion.modifiers) then | ||
return Modifiers.getModifiersText(potion.modifiers, false, true) | |||
else | |||
return '' | |||
end | end | ||
end | |||
local | function p._getHerblorePotionTable(categoryName) | ||
local skillID = 'Herblore' | |||
local category = GameData.getEntityByName(SkillData.Herblore.categories, categoryName) | |||
table.insert( | if category == nil then | ||
local catNames = {} | |||
for i, cat in pairs(SkillData.Herblore.categories) do | |||
table.insert(catNames, cat.name) | |||
end | end | ||
return Shared.printError('No such category ' .. categoryName .. ', the following are available: ' .. table.concat(catNames, ', ')) | |||
end | end | ||
local categoryID = category.id | |||
local | local potionArray = GameData.getEntities(SkillData.Herblore.recipes, | ||
function(potion) | |||
-- Category implies a realm selection, so no ned to check this separately | |||
return potion.categoryID == categoryID | |||
end | |||
) | |||
table.sort(potionArray, function(a, b) return a | table.sort(potionArray, function(a, b) return Skills.standardRecipeSort(skillID, a, b) end) | ||
local html = mw.html.create('table') | |||
:addClass('wikitable sortable stickyHeader mw-collapsible') | |||
html:tag('tr'):addClass('headerRow-0') | |||
:tag('th'):wikitext('Potion') | |||
:tag("th"):wikitext(Icons.Icon({'Herblore', type='skill', notext=true}) .. '<br>Level') | |||
:tag('th'):wikitext('XP') | |||
:tag('th'):wikitext('Ingredients') | |||
:tag('th'):wikitext('[[DLC]]') | |||
:tag('th'):wikitext('Tier') | |||
:css('width', '30px') | |||
--:tag('th'):wikitext('Value') | |||
:tag('th'):wikitext('Charges') | |||
:tag('th'):wikitext('Effect') | |||
for i, potion in ipairs(potionArray) do | |||
local level = Skills.getRecipeLevel(skillID, potion) | |||
local baseXP = potion.baseAbyssalExperience or potion.baseExperience | |||
local reqText = Skills.getRecipeRequirementText(SkillData.Herblore.name, potion) | |||
local costText = Common.getCostString({ | |||
["items"] = potion.itemCosts, | |||
["currencies"] = potion.currencyCosts | |||
}, 'N/A', nil, '<br>') | |||
-- Prefetch potion items, as these are required for the overall row | |||
-- as well as the tier rows | |||
local potionItems, lastPotionIdx = {}, 1 | |||
for j, potionID in ipairs(potion.potionIDs) do | |||
potionItems[j] = Items.getItemByID(potionID) | |||
lastPotionIdx = j | |||
end | end | ||
local | local row = html:tag('tr') | ||
row:tag('td'):wikitext(Icons.Icon({potionItems[lastPotionIdx].name, type='item', notext=true, nolink=true})) | |||
:attr('rowspan', 4):wikitext('[['..potion.name..']]') | |||
row:tag('td'):attr('rowspan', 4):wikitext(level) | |||
:css('text-align', 'center') | |||
row:tag('td'):attr('rowspan', 4):wikitext(Num.formatnum(baseXP)) | |||
:attr('data-sort-value', baseXP) | |||
:css('text-align', 'right') | |||
row:tag('td'):attr('rowspan', 4):wikitext(costText) | |||
row:tag('td'):attr('rowspan', 4):wikitext(Icons.getDLCColumnIcon(potion.potionIDs[1])) | |||
:css('text-align', 'center') | |||
:attr('data-sort-value', Icons.getExpansionID(potion.potionIDs[1])) | |||
for j, potionID in ipairs(potion.potionIDs) do | |||
local tierPot = potionItems[j] | |||
-- First row needs to be added to the main row of the rowspan row. | |||
-- The other 3 need to be added to the main table as new rows. | |||
local tierRow = j == 1 and row or html:tag('tr') | |||
tierRow:tag('td'):wikitext(Icons.Icon({tierPot.name, type='item', notext=true})) | |||
:wikitext('[[' .. tierPot.name .. '|' .. tierSuffix[j] .. ']]') | |||
--tierRow:tag('td'):wikitext(Items.getValueText(tierPot)) | |||
-- :css('text-align', 'right') | |||
-- :attr('data-sort-value', tierPot.sellsFor) | |||
tierRow:tag('td'):wikitext(tierPot.charges) | |||
:css('text-align', 'right') | |||
tierRow:tag('td'):wikitext(p._getPotionDescription(tierPot)) | |||
end | end | ||
end | end | ||
return tostring(html) | |||
return | |||
end | end | ||
function p.getHerblorePotionTable(frame) | function p.getHerblorePotionTable(frame) | ||
local | local args = frame.args ~= nil and frame.args or frame | ||
local category = args[1] | |||
return p._getHerblorePotionTable(category) | return p._getHerblorePotionTable(category) | ||
end | end | ||
function p. | -- Returns the icon of the past potion found in potion.PotiodIDs | ||
local | -- This is usually the IV potion. | ||
function p._getHerblorePotionIcon(potion) | |||
local lastPot = nil | |||
for k, v in ipairs(potion.potionIDs) do | |||
lastPot = v | |||
end | |||
local pot = Items.getItemByID(lastPot) | |||
return Icons.Icon({pot.name, type='item', notext=true, size='25'}) | |||
end | |||
function p._getHerblorePotionSlimTable(realmID) | |||
local skillID = 'Herblore' | |||
local realmIcon = Icons._SkillRealmIcon('Herblore', realmID) | |||
local potions = GameData.getEntities(SkillData.Herblore.recipes, | |||
function(x) | |||
return Skills.getRecipeRealm(x) == realmID | |||
end | |||
) | |||
table.sort(potions, function(a, b) return Skills.standardRecipeSort(skillID, a, b) end) | |||
local function getIngredients(potion) | |||
local herbs = {} | |||
local secondaries = {} | |||
-- Grab secondaries and split herb. | |||
if type(potion.itemCosts) == 'table' then | |||
for k, v in pairs(potion.itemCosts) do | |||
local ingredientID = v.id | |||
local item = GameData.getEntityByID('items', v.id) | |||
local icon = Icons.Icon({item.name, type='item', qty=v.quantity}) | |||
if ingredientID ~= nil and string.sub(ingredientID, -5) == "_Herb" then | |||
table.insert(herbs, icon) | |||
else | |||
table.insert(secondaries, icon) | |||
end | |||
end | |||
end | |||
-- Format currencies as normal | |||
if type(potion.currencies) == 'table' then | |||
for k, v in pairs(potion.currencies) do | |||
local currID = v.id or v.currencyID | |||
table.insert(secondaries, Icons._Currency(currID, v.quantity)) | |||
end | |||
end | |||
return { | |||
herbs = table.concat(herbs, '<br>'), | |||
secondaries = table.concat(secondaries, '<br>') | |||
} | |||
end | end | ||
local tbl = mw.html.create("table") | |||
:addClass("wikitable sortable stickyHeader mw-collapsible mw-collapsed") | |||
-- Add header | |||
tbl :tag("tr"):addClass("headerRow-0") | |||
:tag('th'):wikitext("Potion") | |||
:attr('colspan', 2) | |||
:tag('th'):wikitext('[[DLC]]') | |||
:tag('th'):wikitext(realmIcon .. '<br>Level') | |||
:tag('th'):wikitext(realmIcon .. '<br>XP') | |||
:tag('th'):wikitext('Herb') | |||
:tag('th'):wikitext('Secondary') | |||
--:tag('th'):wikitext('Effect') | |||
:done() | |||
for i, potion in ipairs(potions) do | |||
local level = Skills.getRecipeLevel(skillID, potion) | |||
local xp = potion.baseAbyssalExperience or potion.baseExperience | |||
local ingreds = getIngredients(potion) | |||
local row = tbl:tag('tr') | |||
row:tag('td'):wikitext(p._getHerblorePotionIcon(potion)) | |||
:css('text-align', 'center') | |||
row:tag('td'):wikitext('[['..potion.name..']]') | |||
row:tag('td'):wikitext(Icons.getDLCColumnIcon(potion.potionIDs[1])) | |||
:css('text-align', 'center') | |||
:attr('data-sort-value', Icons.getExpansionID(potion.potionIDs[1])) | |||
row:tag('td'):wikitext(level) | |||
:css('text-align', 'center') | |||
row:tag('td'):wikitext(Num.formatnum(xp)) | |||
:css('text-align', 'right') | |||
row:tag('td'):wikitext(ingreds.herbs) | |||
row:tag('td'):wikitext(ingreds.secondaries) | |||
end | |||
return tostring(tbl) | |||
end | |||
function p.getHerblorePotionSlimTable(frame) | |||
local args = frame.args ~= nil and frame.args or frame | |||
local realmName = args.realm or 'Melvor Realm' | |||
local realm = Skills.getRealmFromName(realmName) | |||
if realm == nil then | |||
return Shared.printError('Failed to find a realm with name ' .. (realmName or 'nil')) | |||
end | end | ||
return p._getHerblorePotionSlimTable(realm.id) | |||
end | |||
function p._getHerbloreHerbTable(realmID) | |||
local allHerbs = {} | |||
-- 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 = Skills.getRecipeLevel('Herblore', potion) | |||
if potionCosts == nil or level == nil then | |||
for | return | ||
local | end | ||
-- Find if this potion uses a herb, and which herb it is. | |||
for _, ingredient in ipairs(potionCosts) do | |||
local ingredientID = ingredient.id | |||
if ingredientID ~= nil and string.sub(ingredientID, -5) == "_Herb" then | |||
-- 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 | end | ||
end | |||
local recipes = GameData.getEntities(SkillData.Herblore.recipes, | |||
function(obj) | |||
return Skills.getRecipeRealm(obj) == realmID | |||
end | |||
) | |||
for _, potion in ipairs(recipes) do | |||
handlePotion(potion) | |||
end | 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}) .. '<br>Level') | |||
:tag("th"):wikitext("Herb") | |||
:tag("th"):wikitext("[[DLC]]") | |||
: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'] | |||
-- Add rows | |||
tbl :tag("tr") | |||
:tag("td"):wikitext(herbLevel) | |||
:css('text-align', 'center') | |||
:tag("td"):wikitext(Icons.Icon({herbItem.name, type='item'})) | |||
:tag("td"):wikitext(Icons.getDLCColumnIcon(herbItem.id)) | |||
:css('text-align', 'center') | |||
:tag("td"):wikitext(Items.getValueText(herbItem)) | |||
:tag('td'):wikitext(ItemSourceTables._getItemSources(herbItem, false, nil, ' ')) | |||
:done() | |||
end | |||
return tostring(tbl) | |||
end | |||
if | function p.getHerbloreHerbTable(frame) | ||
return | local args = frame:getParent().args | ||
local realmName = args.realm | |||
local realm = Skills.getRealmFromName(realmName) | |||
if realm == nil then | |||
return Shared.printError('Failed to find a realm with name ' .. (realmName or 'nil')) | |||
end | end | ||
return p._getHerbloreHerbTable(realm.id) | |||
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 html = mw.html.create('table') | |||
:addClass('wikitable') | |||
html:tag('tr') | |||
:tag('th'):attr('colspan', 4) | |||
:wikitext('[[' .. potionName ..']]') | |||
:tag('tr') | |||
:tag('th'):wikitext('Potion') | |||
:tag('th'):wikitext('Tier') | |||
:tag('th'):wikitext('Charges') | |||
:tag('th'):wikitext('Effect') | |||
for i, potionID in ipairs(recipe.potionIDs) do | |||
local tier = tierSuffix[i] | |||
local potion = Items.getItemByID(potionID) | |||
if potion ~= nil then | |||
html:tag('tr') | |||
:tag('td'):wikitext(Icons.getExpansionIcon(potion.id)) | |||
:wikitext(Icons.Icon({potion.name, type='item', notext=true, size='25'})) | |||
:css('text-wrap', 'nowrap') | |||
:tag('td'):wikitext(Icons.Icon({potion.name, tier, type='item', noicon=true})) | |||
:tag('td'):wikitext(potion.charges) | |||
:tag('td'):wikitext(p._getPotionDescription(potion)) | |||
end | end | ||
end | end | ||
return tostring(html) | |||
return | end | ||
function p.getRunecraftingTable(frame) | |||
local category = frame.args ~= nil and frame.args[1] or frame | |||
return p._getRecipeTable('Runecrafting', category, {'Item', 'DLC', 'SkillLevel', 'SkillXP', 'Materials', 'Currency'}) | |||
end | |||
function p.getFletchingTable(frame) | |||
local category = frame.args ~= nil and frame.args[1] or frame | |||
return p._getRecipeTable('Fletching', category, {'Item', 'DLC', 'SkillLevel', 'SkillXP', 'Materials', 'Currency'}) | |||
end | end | ||
function p.getCraftingTable(frame) | function p.getCraftingTable(frame) | ||
local category = frame.args ~= nil and frame.args[1] or frame | local category = frame.args ~= nil and frame.args[1] or frame | ||
local | local columns = {'Item', 'DLC', 'SkillLevel', 'SkillXP', 'Materials', 'Currency'} | ||
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 = {'Item', 'DLC', 'SkillLevel', 'SkillXP', 'Materials', 'Currency'} | |||
if category ~= 'Bars' and category ~= 'Abyssal Bars' then | |||
table.insert(columns, 'CurrencyBar') | |||
end | end | ||
return p._getRecipeTable('Smithing', category, columns) | |||
end | |||
if | -- Given a skill name, category, and column list, produces a table of recipes for that | ||
return | -- 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, Currency, Materials, SkillXPSec, CurrencySec, CurrencyBar | |||
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 | 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 | |||
local | -- 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 categoryRealm = category.realm or 'melvorD:Melvor' | |||
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 = { | |||
["Item"] = {["header"] = 'Item', ["altRepeat"] = false}, | |||
["SkillLevel"] = {["header"] = Icons._SkillRealmIcon(skillName, categoryRealm) .. '<br>Level', ["altRepeat"] = false}, | |||
["DLC"] = {["header"] = '[[DLC]]', ["altRepeat"] = true}, | |||
["SkillXP"] = {["header"] = 'XP', ["altRepeat"] = false}, | |||
["Materials"] = {["header"] = 'Materials', ["altRepeat"] = true}, | |||
["Currency"] = {["header"] = 'Value', ["altRepeat"] = true}, | |||
["SkillXPSec"] = {["header"] = 'XP/s', ["altRepeat"] = false}, | |||
["CurrencySec"] = {["header"] = 'Value/s', ["altRepeat"] = true}, | |||
["CurrencyBar"] = {["header"] = 'Value/Bar', ["altRepeat"] = true}, | |||
["Description"] = {["header"] = "Description", ["altRepeat"] = true} | |||
} | |||
-- Build the table header while we're here | |||
local html = mw.html.create('table') | |||
:addClass('wikitable sortable stickyHeader') | |||
local header = html:tag('tr') | |||
:addClass('headerRow-0') | |||
local barIDList = {} | |||
if | for i, colID in ipairs(columnList) do | ||
if columnDef[colID] == nil then | |||
return Shared.printError('Invalid column ' .. colID .. ' requested') | |||
else | |||
if colID == 'Item' then | |||
header:tag('th'):wikitext('Item') | |||
:attr('colspan', 2) | |||
else | |||
header:tag('th'):wikitext(columnDef[colID].header) | |||
if colID == 'CurrencyBar' then | |||
-- For Smithing, a currency value per bar column is included. If this | |||
-- is requested, then obtain a list of bar item IDs | |||
barIDList = p.getBarItemIDs() | |||
end | |||
end | |||
end | end | ||
for j, mat in Shared. | 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 Skills.standardRecipeSort(skillName, a, b) 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) | |||
local level = Skills.getRecipeLevel(skillName, recipe) | |||
local baseXP = recipe.baseAbyssalExperience or recipe.baseExperience | |||
if item ~= nil then | |||
-- Some recipes have alternative costs, so the recipe may require multiple rows | |||
local hasAltCosts = recipe.alternativeCosts ~= nil and not Shared.tableIsEmpty(recipe.alternativeCosts) | |||
local costList = (hasAltCosts and recipe.alternativeCosts) or {{["itemCosts"] = recipe.itemCosts}} | |||
local costCount = Shared.tableCount(costList) | |||
-- Build one row per element within costList | |||
for recipeRow, costDef in ipairs(costList) do | |||
local rowspanAmt = nil | |||
if recipeRow == 1 and costCount > 1 then | |||
rowspanAmt = costCount | |||
end | |||
local rowspanStr = (recipeRow == 1 and costCount > 1 and 'rowspan="' .. costCount .. '" ') or '' | |||
local qty = (costDef.quantityMultiplier or 1) * (recipe.baseQuantity or 1) | |||
local row = html:tag('tr') | |||
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 spanAmt = (not altRepeat and rowspanStr) or '' | |||
local cell = row:tag('td') | |||
if not altRepeat and rowspanAmt ~= nil then | |||
cell:attr('rowspan', tostring(rowspanAmt)) | |||
end | |||
if colID == 'Item' then | |||
cell:wikitext(Icons.Icon({item.name, type='item', notext=true})) | |||
:addClass('table-img') | |||
cell = row:tag('td') | |||
if not altRepeat and rowspanAmt ~= nil then | |||
cell:attr('rowspan', tostring(rowspanAmt)) | |||
end | |||
cell:wikitext((qty > 1 and '<b>' .. qty .. 'x</b> ' or '') .. Icons.Icon({item.name, type='item', noicon=true})) | |||
if qty > 1 then | |||
cell:attr('data-sort-value', item.name) | |||
end | |||
elseif colID == 'SkillLevel' then | |||
cell:wikitext(level) | |||
:css('text-align', 'center') | |||
elseif colID == 'DLC' then | |||
local dlcID = recipe.id | |||
if hasAltCosts then | |||
-- For alt costs, look at the namespace of the required materials, | |||
-- as the recipe could be from base game but using expansion materials | |||
local recNS, recLocalID = Shared.getLocalID(recipe.id) | |||
if Shared.contains({'melvorD', 'melvorF'}, recNS) then | |||
for k, mat in ipairs(costDef.itemCosts) do | |||
local matNS, matLocalID = Shared.getLocalID(mat.id) | |||
if matNS ~= recNS then | |||
dlcID = mat.id | |||
break | |||
end | |||
end | |||
end | |||
end | |||
cell:wikitext(Icons.getDLCColumnIcon(dlcID)) | |||
:attr('data-sort-value', Icons.getExpansionID(dlcID)) | |||
:css('text-align', 'center') | |||
elseif colID == 'SkillXP' then | |||
cell:wikitext(Num.formatnum(baseXP)) | |||
:css('text-align', 'right') | |||
:attr('data-sort-value', tostring(baseXP)) | |||
elseif colID == 'Currency' then | |||
local val = math.floor(item.sellsFor) | |||
cell:wikitext(Items.getValueText(item) .. (qty > 1 and ' (x' .. qty .. ')' or '')) | |||
:attr('data-sort-value', tostring(val * qty)) | |||
elseif colID == 'Materials' then | |||
local currCost = { ["items"] = {}, ["currencies"] = recipe.currencyCosts } | |||
local currText = Common.getCostString(currCost, nil, nil, ' ') | |||
if currText ~= nil then | |||
cell:wikitext(currText) | |||
end | |||
for k, mat in ipairs(costDef.itemCosts) do | |||
local matItem = Items.getItemByID(mat.id) | |||
if matItem ~= nil then | |||
local sub = mw.html.create('sub') | |||
:wikitext(Num.formatnum(mat.quantity) .. 'x') | |||
:addClass('item-qty') | |||
:done() | |||
cell:node(sub) | |||
cell:wikitext(Icons.Icon({matItem.name, type='item', notext=true})) | |||
end | |||
end | |||
elseif colID == 'SkillXPSec' then | |||
local XPRate = baseXP / actionInterval | |||
cell:wikitext(Num.formatnum(string.format('%.2f', XPRate))) | |||
:css('text-align', 'right') | |||
:attr('data-sort-value', tostring(XPRate)) | |||
elseif colID == 'CurrencySec' then | |||
local saleCurrency = item.sellsForCurrency or 'melvorD:GP' | |||
local val = math.floor(item.sellsFor) * qty / actionInterval | |||
cell:wikitext(Icons._Currency(saleCurrency, string.format('%.2f', val))) | |||
:attr('data-sort-value', tostring(val)) | |||
elseif colID == 'CurrencyBar' 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 saleCurrency = item.sellsForCurrency or 'melvorD:GP' | |||
local barVal = Num.round(math.floor(item.sellsFor) * qty / barQty, 1, 1) | |||
cell:wikitext(Icons._Currency(saleCurrency, barVal)) | |||
:attr('data-sort-value', tostring(barVal)) | |||
else | |||
cell:wikitext('N/A') | |||
:addClass('table-na') | |||
:attr('data-sort-value', '0') | |||
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 = Modifiers.getModifiersText(item.modifiers, false) | |||
end | |||
cell:wikitext(descrip) | |||
end | |||
end | |||
end | |||
end | |||
end | end | ||
end | end | ||
return tostring(html) | |||
end | |||
function p.getBarItemIDs() | |||
return | local barCategories = { | ||
['melvorD:Bars'] = true, | |||
['melvorItA:AbyssalBars'] = true | |||
} | |||
local barIDList = {} | |||
for i, recipe in ipairs(SkillData.Smithing.recipes) do | |||
if barCategories[recipe.categoryID] then | |||
table.insert(barIDList, recipe.productID) | |||
end | |||
end | |||
return barIDList | |||
end | end | ||
return p | return p |
Latest revision as of 20:58, 27 September 2024
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 Common = require('Module:Common')
local GameData = require('Module:GameData')
local SkillData = GameData.skillData
local Modifiers = require('Module:Modifiers')
local Skills = require('Module:Skills')
local Items = require('Module:Items')
local Icons = require('Module:Icons')
local ItemSourceTables = require('Module:Items/SourceTables')
local Num = require('Module:Number')
function p.getCookedItemsTable(frame)
local args = frame.args ~= nil and frame.args or frame
local category = args[1]
local realmName = args.realm
local realm = Skills.getRealmFromName(realmName)
if realm == nil then
return Shared.printError('Failed to find a realm with name ' .. (realmName or 'nil'))
end
local skillID = 'Cooking'
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
and Skills.getRecipeRealm(recipe) == realm.id
)
end
)
table.sort(recipeArray, function(a, b) return Skills.standardRecipeSort(skillID, a, b) end)
local cookIcon = Icons._SkillRealmIcon('Cooking', realm.id)
local perfectIcon = Icons.Icon({'Perfect', type='bonus', ext='png', notext=true, nolink=true, size=20})
local healIcon = Icons.Icon({"Hitpoints", type="skill", notext=true, nolink=true, size=20})
-- Only include time for cooking fire, as the other options have a fixed interval.
local includeTime = category == "Cooking Fire"
local html = mw.html.create('table')
:addClass('wikitable sortable stickyHeader')
local header0 = html:tag('tr'):addClass("headerRow-0")
header0 :tag('th'):wikitext('Cooked Item')
:attr('colspan', 3)
:attr('rowspan', 2)
header0 :tag('th'):wikitext(cookIcon .. '<br>Level')
:attr('rowspan', 2)
header0 :tag('th'):wikitext('[[DLC]]')
:attr('rowspan', 2)
if includeTime == true then
header0 :tag('th'):wikitext('Cook<br>Time (s)')
:attr('rowspan', 2)
end
header0 :tag('th'):wikitext(cookIcon .. '<br>XP')
:attr('rowspan', 2)
header0 :tag('th'):wikitext('XP/s')
:attr('rowspan', 2)
header0 :tag('th'):wikitext('Total Healing')
:attr('colspan', 2)
header0 :tag('th'):wikitext('Ingredients')
:attr('rowspan', 2)
header0 :tag('th'):wikitext('Value')
:attr('colspan', 2)
html:tag('tr'):addClass("headerRow-1")
:tag('th'):wikitext('Normal')
:tag('th'):wikitext('Perfect')
:tag('th'):wikitext('Normal')
:tag('th'):wikitext('Perfect')
for i, recipe in ipairs(recipeArray) do
local level = Skills.getRecipeLevel(skillID, recipe)
local baseXP = recipe.baseAbyssalExperience or recipe.baseExperience
local baseInt = recipe.baseInterval / 1000
local xpRate = baseXP / baseInt
local item = Items.getItemByID(recipe.productID)
local currency = item.sellsForCurrency or 'melvorD:GP'
local perfectItem = nil
if recipe.perfectCookID ~= nil then
perfectItem = Items.getItemByID(recipe.perfectCookID)
end
local qty = recipe.baseQuantity or 1
local normalHeal = math.floor(item.healsFor * 10) * qty
local perfectHeal = math.floor(perfectItem.healsFor * 10) * qty
local row = html:tag('tr')
row:tag('td'):wikitext(Icons.Icon({item.name, type='item', notext=true}))
:addClass("table-img")
if perfectItem ~= nil then
row:tag('td'):wikitext(Icons.Icon({perfectItem.name, type='item', notext=true}))
:addClass("table-img")
end
local nameCell = row:tag('td')
nameCell:wikitext('[[' .. item.name .. ']]')
if qty > 1 then
nameCell:wikitext(' x' .. qty)
end
row:tag('td'):wikitext(level)
:css('text-align', 'center')
row:tag('td'):wikitext(Icons.getDLCColumnIcon(item.id))
:css('text-align', 'center')
:attr('data-sort-value', Icons.getExpansionID(item.id))
if includeTime == true then
row:tag('td'):wikitext(Num.round(baseInt, 2, 0) .. 's')
:css('text-align', 'right')
:attr('data-sort-value', baseInt)
end
row:tag('td'):wikitext(Num.formatnum(baseXP))
:css('text-align', 'right')
:attr('data-sort-value', baseXP)
row:tag('td'):wikitext(Num.formatnum(Num.round(xpRate, 0, 0)))
:css('text-align', 'right')
:attr('data-sort-value', xpRate)
row:tag('td'):wikitext(healIcon .. ' ')
:wikitext(Num.formatnum(normalHeal))
:attr('data-sort-value', normalHeal)
row:tag('td'):wikitext(perfectIcon .. ' ')
:wikitext(Num.formatnum(perfectHeal))
:attr('data-sort-value', perfectHeal)
local ingrCell = row:tag('td')
local matArray = {}
for j, mat in ipairs(recipe.itemCosts) do
local matItem = Items.getItemByID(mat.id)
if matItem ~= nil then
local sub = mw.html.create('sub')
:wikitext(mat.quantity .. 'x')
:addClass('item-qty')
:done()
ingrCell:node(sub)
ingrCell:wikitext(Icons.Icon({matItem.name, type='item', notext=true}))
end
end
row:tag('td'):wikitext(Icons._Currency(currency, item.sellsFor))
:css('text-align', 'right')
row:tag('td'):wikitext(Icons._Currency(currency, perfectItem.sellsFor))
:css('text-align', 'right')
end
return tostring(html)
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 Modifiers.getModifiersText(potion.modifiers, false, true)
else
return ''
end
end
function p._getHerblorePotionTable(categoryName)
local skillID = 'Herblore'
local category = GameData.getEntityByName(SkillData.Herblore.categories, categoryName)
if category == nil then
local catNames = {}
for i, cat in pairs(SkillData.Herblore.categories) do
table.insert(catNames, cat.name)
end
return Shared.printError('No such category ' .. categoryName .. ', the following are available: ' .. table.concat(catNames, ', '))
end
local categoryID = category.id
local potionArray = GameData.getEntities(SkillData.Herblore.recipes,
function(potion)
-- Category implies a realm selection, so no ned to check this separately
return potion.categoryID == categoryID
end
)
table.sort(potionArray, function(a, b) return Skills.standardRecipeSort(skillID, a, b) end)
local html = mw.html.create('table')
:addClass('wikitable sortable stickyHeader mw-collapsible')
html:tag('tr'):addClass('headerRow-0')
:tag('th'):wikitext('Potion')
:tag("th"):wikitext(Icons.Icon({'Herblore', type='skill', notext=true}) .. '<br>Level')
:tag('th'):wikitext('XP')
:tag('th'):wikitext('Ingredients')
:tag('th'):wikitext('[[DLC]]')
:tag('th'):wikitext('Tier')
:css('width', '30px')
--:tag('th'):wikitext('Value')
:tag('th'):wikitext('Charges')
:tag('th'):wikitext('Effect')
for i, potion in ipairs(potionArray) do
local level = Skills.getRecipeLevel(skillID, potion)
local baseXP = potion.baseAbyssalExperience or potion.baseExperience
local reqText = Skills.getRecipeRequirementText(SkillData.Herblore.name, potion)
local costText = Common.getCostString({
["items"] = potion.itemCosts,
["currencies"] = potion.currencyCosts
}, 'N/A', nil, '<br>')
-- Prefetch potion items, as these are required for the overall row
-- as well as the tier rows
local potionItems, lastPotionIdx = {}, 1
for j, potionID in ipairs(potion.potionIDs) do
potionItems[j] = Items.getItemByID(potionID)
lastPotionIdx = j
end
local row = html:tag('tr')
row:tag('td'):wikitext(Icons.Icon({potionItems[lastPotionIdx].name, type='item', notext=true, nolink=true}))
:attr('rowspan', 4):wikitext('[['..potion.name..']]')
row:tag('td'):attr('rowspan', 4):wikitext(level)
:css('text-align', 'center')
row:tag('td'):attr('rowspan', 4):wikitext(Num.formatnum(baseXP))
:attr('data-sort-value', baseXP)
:css('text-align', 'right')
row:tag('td'):attr('rowspan', 4):wikitext(costText)
row:tag('td'):attr('rowspan', 4):wikitext(Icons.getDLCColumnIcon(potion.potionIDs[1]))
:css('text-align', 'center')
:attr('data-sort-value', Icons.getExpansionID(potion.potionIDs[1]))
for j, potionID in ipairs(potion.potionIDs) do
local tierPot = potionItems[j]
-- First row needs to be added to the main row of the rowspan row.
-- The other 3 need to be added to the main table as new rows.
local tierRow = j == 1 and row or html:tag('tr')
tierRow:tag('td'):wikitext(Icons.Icon({tierPot.name, type='item', notext=true}))
:wikitext('[[' .. tierPot.name .. '|' .. tierSuffix[j] .. ']]')
--tierRow:tag('td'):wikitext(Items.getValueText(tierPot))
-- :css('text-align', 'right')
-- :attr('data-sort-value', tierPot.sellsFor)
tierRow:tag('td'):wikitext(tierPot.charges)
:css('text-align', 'right')
tierRow:tag('td'):wikitext(p._getPotionDescription(tierPot))
end
end
return tostring(html)
end
function p.getHerblorePotionTable(frame)
local args = frame.args ~= nil and frame.args or frame
local category = args[1]
return p._getHerblorePotionTable(category)
end
-- Returns the icon of the past potion found in potion.PotiodIDs
-- This is usually the IV potion.
function p._getHerblorePotionIcon(potion)
local lastPot = nil
for k, v in ipairs(potion.potionIDs) do
lastPot = v
end
local pot = Items.getItemByID(lastPot)
return Icons.Icon({pot.name, type='item', notext=true, size='25'})
end
function p._getHerblorePotionSlimTable(realmID)
local skillID = 'Herblore'
local realmIcon = Icons._SkillRealmIcon('Herblore', realmID)
local potions = GameData.getEntities(SkillData.Herblore.recipes,
function(x)
return Skills.getRecipeRealm(x) == realmID
end
)
table.sort(potions, function(a, b) return Skills.standardRecipeSort(skillID, a, b) end)
local function getIngredients(potion)
local herbs = {}
local secondaries = {}
-- Grab secondaries and split herb.
if type(potion.itemCosts) == 'table' then
for k, v in pairs(potion.itemCosts) do
local ingredientID = v.id
local item = GameData.getEntityByID('items', v.id)
local icon = Icons.Icon({item.name, type='item', qty=v.quantity})
if ingredientID ~= nil and string.sub(ingredientID, -5) == "_Herb" then
table.insert(herbs, icon)
else
table.insert(secondaries, icon)
end
end
end
-- Format currencies as normal
if type(potion.currencies) == 'table' then
for k, v in pairs(potion.currencies) do
local currID = v.id or v.currencyID
table.insert(secondaries, Icons._Currency(currID, v.quantity))
end
end
return {
herbs = table.concat(herbs, '<br>'),
secondaries = table.concat(secondaries, '<br>')
}
end
local tbl = mw.html.create("table")
:addClass("wikitable sortable stickyHeader mw-collapsible mw-collapsed")
-- Add header
tbl :tag("tr"):addClass("headerRow-0")
:tag('th'):wikitext("Potion")
:attr('colspan', 2)
:tag('th'):wikitext('[[DLC]]')
:tag('th'):wikitext(realmIcon .. '<br>Level')
:tag('th'):wikitext(realmIcon .. '<br>XP')
:tag('th'):wikitext('Herb')
:tag('th'):wikitext('Secondary')
--:tag('th'):wikitext('Effect')
:done()
for i, potion in ipairs(potions) do
local level = Skills.getRecipeLevel(skillID, potion)
local xp = potion.baseAbyssalExperience or potion.baseExperience
local ingreds = getIngredients(potion)
local row = tbl:tag('tr')
row:tag('td'):wikitext(p._getHerblorePotionIcon(potion))
:css('text-align', 'center')
row:tag('td'):wikitext('[['..potion.name..']]')
row:tag('td'):wikitext(Icons.getDLCColumnIcon(potion.potionIDs[1]))
:css('text-align', 'center')
:attr('data-sort-value', Icons.getExpansionID(potion.potionIDs[1]))
row:tag('td'):wikitext(level)
:css('text-align', 'center')
row:tag('td'):wikitext(Num.formatnum(xp))
:css('text-align', 'right')
row:tag('td'):wikitext(ingreds.herbs)
row:tag('td'):wikitext(ingreds.secondaries)
end
return tostring(tbl)
end
function p.getHerblorePotionSlimTable(frame)
local args = frame.args ~= nil and frame.args or frame
local realmName = args.realm or 'Melvor Realm'
local realm = Skills.getRealmFromName(realmName)
if realm == nil then
return Shared.printError('Failed to find a realm with name ' .. (realmName or 'nil'))
end
return p._getHerblorePotionSlimTable(realm.id)
end
function p._getHerbloreHerbTable(realmID)
local allHerbs = {}
-- 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 = Skills.getRecipeLevel('Herblore', potion)
if potionCosts == nil or level == nil then
return
end
-- Find if this potion uses a herb, and which herb it is.
for _, ingredient in ipairs(potionCosts) do
local ingredientID = ingredient.id
if ingredientID ~= nil and string.sub(ingredientID, -5) == "_Herb" then
-- 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
end
local recipes = GameData.getEntities(SkillData.Herblore.recipes,
function(obj)
return Skills.getRecipeRealm(obj) == realmID
end
)
for _, potion in ipairs(recipes) 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}) .. '<br>Level')
:tag("th"):wikitext("Herb")
:tag("th"):wikitext("[[DLC]]")
: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']
-- Add rows
tbl :tag("tr")
:tag("td"):wikitext(herbLevel)
:css('text-align', 'center')
:tag("td"):wikitext(Icons.Icon({herbItem.name, type='item'}))
:tag("td"):wikitext(Icons.getDLCColumnIcon(herbItem.id))
:css('text-align', 'center')
:tag("td"):wikitext(Items.getValueText(herbItem))
:tag('td'):wikitext(ItemSourceTables._getItemSources(herbItem, false, nil, ' '))
:done()
end
return tostring(tbl)
end
function p.getHerbloreHerbTable(frame)
local args = frame:getParent().args
local realmName = args.realm
local realm = Skills.getRealmFromName(realmName)
if realm == nil then
return Shared.printError('Failed to find a realm with name ' .. (realmName or 'nil'))
end
return p._getHerbloreHerbTable(realm.id)
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 html = mw.html.create('table')
:addClass('wikitable')
html:tag('tr')
:tag('th'):attr('colspan', 4)
:wikitext('[[' .. potionName ..']]')
:tag('tr')
:tag('th'):wikitext('Potion')
:tag('th'):wikitext('Tier')
:tag('th'):wikitext('Charges')
:tag('th'):wikitext('Effect')
for i, potionID in ipairs(recipe.potionIDs) do
local tier = tierSuffix[i]
local potion = Items.getItemByID(potionID)
if potion ~= nil then
html:tag('tr')
:tag('td'):wikitext(Icons.getExpansionIcon(potion.id))
:wikitext(Icons.Icon({potion.name, type='item', notext=true, size='25'}))
:css('text-wrap', 'nowrap')
:tag('td'):wikitext(Icons.Icon({potion.name, tier, type='item', noicon=true}))
:tag('td'):wikitext(potion.charges)
:tag('td'):wikitext(p._getPotionDescription(potion))
end
end
return tostring(html)
end
function p.getRunecraftingTable(frame)
local category = frame.args ~= nil and frame.args[1] or frame
return p._getRecipeTable('Runecrafting', category, {'Item', 'DLC', 'SkillLevel', 'SkillXP', 'Materials', 'Currency'})
end
function p.getFletchingTable(frame)
local category = frame.args ~= nil and frame.args[1] or frame
return p._getRecipeTable('Fletching', category, {'Item', 'DLC', 'SkillLevel', 'SkillXP', 'Materials', 'Currency'})
end
function p.getCraftingTable(frame)
local category = frame.args ~= nil and frame.args[1] or frame
local columns = {'Item', 'DLC', 'SkillLevel', 'SkillXP', 'Materials', 'Currency'}
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 = {'Item', 'DLC', 'SkillLevel', 'SkillXP', 'Materials', 'Currency'}
if category ~= 'Bars' and category ~= 'Abyssal Bars' then
table.insert(columns, 'CurrencyBar')
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, Currency, Materials, SkillXPSec, CurrencySec, CurrencyBar
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 categoryRealm = category.realm or 'melvorD:Melvor'
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 = {
["Item"] = {["header"] = 'Item', ["altRepeat"] = false},
["SkillLevel"] = {["header"] = Icons._SkillRealmIcon(skillName, categoryRealm) .. '<br>Level', ["altRepeat"] = false},
["DLC"] = {["header"] = '[[DLC]]', ["altRepeat"] = true},
["SkillXP"] = {["header"] = 'XP', ["altRepeat"] = false},
["Materials"] = {["header"] = 'Materials', ["altRepeat"] = true},
["Currency"] = {["header"] = 'Value', ["altRepeat"] = true},
["SkillXPSec"] = {["header"] = 'XP/s', ["altRepeat"] = false},
["CurrencySec"] = {["header"] = 'Value/s', ["altRepeat"] = true},
["CurrencyBar"] = {["header"] = 'Value/Bar', ["altRepeat"] = true},
["Description"] = {["header"] = "Description", ["altRepeat"] = true}
}
-- Build the table header while we're here
local html = mw.html.create('table')
:addClass('wikitable sortable stickyHeader')
local header = html:tag('tr')
:addClass('headerRow-0')
local barIDList = {}
for i, colID in ipairs(columnList) do
if columnDef[colID] == nil then
return Shared.printError('Invalid column ' .. colID .. ' requested')
else
if colID == 'Item' then
header:tag('th'):wikitext('Item')
:attr('colspan', 2)
else
header:tag('th'):wikitext(columnDef[colID].header)
if colID == 'CurrencyBar' then
-- For Smithing, a currency value per bar column is included. If this
-- is requested, then obtain a list of bar item IDs
barIDList = p.getBarItemIDs()
end
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 Skills.standardRecipeSort(skillName, a, b) 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)
local level = Skills.getRecipeLevel(skillName, recipe)
local baseXP = recipe.baseAbyssalExperience or recipe.baseExperience
if item ~= nil then
-- Some recipes have alternative costs, so the recipe may require multiple rows
local hasAltCosts = recipe.alternativeCosts ~= nil and not Shared.tableIsEmpty(recipe.alternativeCosts)
local costList = (hasAltCosts and recipe.alternativeCosts) or {{["itemCosts"] = recipe.itemCosts}}
local costCount = Shared.tableCount(costList)
-- Build one row per element within costList
for recipeRow, costDef in ipairs(costList) do
local rowspanAmt = nil
if recipeRow == 1 and costCount > 1 then
rowspanAmt = costCount
end
local rowspanStr = (recipeRow == 1 and costCount > 1 and 'rowspan="' .. costCount .. '" ') or ''
local qty = (costDef.quantityMultiplier or 1) * (recipe.baseQuantity or 1)
local row = html:tag('tr')
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 spanAmt = (not altRepeat and rowspanStr) or ''
local cell = row:tag('td')
if not altRepeat and rowspanAmt ~= nil then
cell:attr('rowspan', tostring(rowspanAmt))
end
if colID == 'Item' then
cell:wikitext(Icons.Icon({item.name, type='item', notext=true}))
:addClass('table-img')
cell = row:tag('td')
if not altRepeat and rowspanAmt ~= nil then
cell:attr('rowspan', tostring(rowspanAmt))
end
cell:wikitext((qty > 1 and '<b>' .. qty .. 'x</b> ' or '') .. Icons.Icon({item.name, type='item', noicon=true}))
if qty > 1 then
cell:attr('data-sort-value', item.name)
end
elseif colID == 'SkillLevel' then
cell:wikitext(level)
:css('text-align', 'center')
elseif colID == 'DLC' then
local dlcID = recipe.id
if hasAltCosts then
-- For alt costs, look at the namespace of the required materials,
-- as the recipe could be from base game but using expansion materials
local recNS, recLocalID = Shared.getLocalID(recipe.id)
if Shared.contains({'melvorD', 'melvorF'}, recNS) then
for k, mat in ipairs(costDef.itemCosts) do
local matNS, matLocalID = Shared.getLocalID(mat.id)
if matNS ~= recNS then
dlcID = mat.id
break
end
end
end
end
cell:wikitext(Icons.getDLCColumnIcon(dlcID))
:attr('data-sort-value', Icons.getExpansionID(dlcID))
:css('text-align', 'center')
elseif colID == 'SkillXP' then
cell:wikitext(Num.formatnum(baseXP))
:css('text-align', 'right')
:attr('data-sort-value', tostring(baseXP))
elseif colID == 'Currency' then
local val = math.floor(item.sellsFor)
cell:wikitext(Items.getValueText(item) .. (qty > 1 and ' (x' .. qty .. ')' or ''))
:attr('data-sort-value', tostring(val * qty))
elseif colID == 'Materials' then
local currCost = { ["items"] = {}, ["currencies"] = recipe.currencyCosts }
local currText = Common.getCostString(currCost, nil, nil, ' ')
if currText ~= nil then
cell:wikitext(currText)
end
for k, mat in ipairs(costDef.itemCosts) do
local matItem = Items.getItemByID(mat.id)
if matItem ~= nil then
local sub = mw.html.create('sub')
:wikitext(Num.formatnum(mat.quantity) .. 'x')
:addClass('item-qty')
:done()
cell:node(sub)
cell:wikitext(Icons.Icon({matItem.name, type='item', notext=true}))
end
end
elseif colID == 'SkillXPSec' then
local XPRate = baseXP / actionInterval
cell:wikitext(Num.formatnum(string.format('%.2f', XPRate)))
:css('text-align', 'right')
:attr('data-sort-value', tostring(XPRate))
elseif colID == 'CurrencySec' then
local saleCurrency = item.sellsForCurrency or 'melvorD:GP'
local val = math.floor(item.sellsFor) * qty / actionInterval
cell:wikitext(Icons._Currency(saleCurrency, string.format('%.2f', val)))
:attr('data-sort-value', tostring(val))
elseif colID == 'CurrencyBar' 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 saleCurrency = item.sellsForCurrency or 'melvorD:GP'
local barVal = Num.round(math.floor(item.sellsFor) * qty / barQty, 1, 1)
cell:wikitext(Icons._Currency(saleCurrency, barVal))
:attr('data-sort-value', tostring(barVal))
else
cell:wikitext('N/A')
:addClass('table-na')
:attr('data-sort-value', '0')
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 = Modifiers.getModifiersText(item.modifiers, false)
end
cell:wikitext(descrip)
end
end
end
end
end
end
return tostring(html)
end
function p.getBarItemIDs()
local barCategories = {
['melvorD:Bars'] = true,
['melvorItA:AbyssalBars'] = true
}
local barIDList = {}
for i, recipe in ipairs(SkillData.Smithing.recipes) do
if barCategories[recipe.categoryID] then
table.insert(barIDList, recipe.productID)
end
end
return barIDList
end
return p