Module:Item/recipes: Difference between revisions
Jump to navigation
Jump to search
(The Aspirant only produces helmets, body armours, gloves and boots.) |
No edit summary |
||
(3 intermediate revisions by the same user not shown) | |||
Line 1: | Line 1: | ||
------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | ||
-- | -- | ||
-- Recipes for Module: | -- Recipes for Module:Item | ||
-- | -- | ||
------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | ||
Line 14: | Line 14: | ||
-- Should we use the sandbox version of our submodules? | -- Should we use the sandbox version of our submodules? | ||
local use_sandbox = m_util.misc.maybe_sandbox(' | local use_sandbox = m_util.misc.maybe_sandbox('Item') | ||
-- The cfg table contains all localisable strings and configuration, to make it | -- The cfg table contains all localisable strings and configuration, to make it | ||
-- easier to port this module to another wiki. | -- easier to port this module to another wiki. | ||
local cfg = use_sandbox and mw.loadData('Module: | local cfg = use_sandbox and mw.loadData('Module:Item/config/sandbox') or mw.loadData('Module:Item/config') | ||
local i18n = cfg.i18n.recipes | local i18n = cfg.i18n.recipes | ||
Line 249: | Line 249: | ||
}, | }, | ||
]] | ]] | ||
} | } | ||
Latest revision as of 23:36, 6 October 2024
This submodule of Module:Item contains configuration and functions for item recipes.
The above documentation is transcluded from Module:Item/recipes/doc.
Editors can experiment in this module's sandbox and testcases pages.
Subpages of this module.
Editors can experiment in this module's sandbox and testcases pages.
Subpages of this module.
-------------------------------------------------------------------------------
--
-- Recipes for Module:Item
--
-------------------------------------------------------------------------------
local m_util = require('Module:Util')
local m_cargo = require('Module:Cargo')
local m_game = mw.loadData('Module:Game')
-- Lazy loading
local f_modifier_link -- require('Module:Modifier link').modifier_link
-- Should we use the sandbox version of our submodules?
local use_sandbox = m_util.misc.maybe_sandbox('Item')
-- The cfg table contains all localisable strings and configuration, to make it
-- easier to port this module to another wiki.
local cfg = use_sandbox and mw.loadData('Module:Item/config/sandbox') or mw.loadData('Module:Item/config')
local i18n = cfg.i18n.recipes
-- ----------------------------------------------------------------------------
-- Helper functions
-- ----------------------------------------------------------------------------
local h = {}
-- Lazy loading for Module:Modifier link
function h.modifier_link(args)
if not f_modifier_link then
f_modifier_link = require('Module:Modifier link').main
end
return f_modifier_link(args)
end
h.conditions = {}
h.conditions.factory = {}
function h.conditions.factory.arg(args)
-- Required:
-- arg: The argument to check against
-- One must be specified
-- value: check whether the argument equals this value
-- values: check whether the argument is in this list of values
-- values_assoc: check whether the argument is in this associative table
--
-- Optional:
-- negate: negates the check against the value, i.e. whether the value is not equal or not in the list/table.
args = args or {}
-- Inner type of function depending on whether to check a single value, a list of values or an associative list of values
local inner
if args.value ~= nil then
inner = function (tpl)
return tpl == args.value
end
elseif args.values ~= nil then
inner = function (tpl)
for _, value in ipairs(args.values) do
if tpl == value then
return true
end
end
return false
end
elseif args.values_assoc ~= nil then
inner = function(tpl)
return args.values_assoc[tpl] ~= nil
end
else
error(string.format('Missing inner comparision function. Args: %s', mw.dumpObject(args)))
end
-- Outer type of function depending on whether to check a single value or against a table
return function (tpl_args)
local tpl_value = tpl_args[args.arg]
local rtr
if type(tpl_value) == 'table' then
rtr = false
for key, value in pairs(tpl_value) do
if type(key) == 'number' then
rtr = rtr or inner(value)
else
rtr = rtr or inner(key)
end
end
else
rtr = inner(tpl_value)
end
if args.negate then
rtr = not rtr
end
return rtr
end
end
function h.conditions.factory.not_arg(args)
args = args or {}
args.negate = true
return h.conditions.factory.arg(args)
end
function h.conditions.factory.flag_is_set(args)
return function (tpl_args)
return tpl_args._flags[args.flag] == true
end
end
function h.conditions.factory.acquisition_tag(args)
return function (tpl_args)
local negate = args.negate or false
for _, tag in ipairs(tpl_args.acquisition_tags or {}) do
if tag == args.tag then
return not negate
end
end
return negate
end
end
function h.conditions.factory.drop_monsters(args)
return function (tpl_args)
for _, monster in ipairs(tpl_args.drop_monsters or {}) do
if string.find(monster, args.monster, 1, true) then
return true
end
end
return false
end
end
function h.conditions.factory.drop_rarity(args)
return function (tpl_args)
for _, rarity in ipairs(tpl_args.drop_rarities_ids or {}) do
if rarity == args.rarity then
return true
end
end
return false
end
end
function h.conditions.factory.drop_level_not_greater_than(args)
return function (tpl_args)
if tpl_args.drop_level == nil then
return true
end
return tpl_args.drop_level <= args.level
end
end
function h.conditions.item_class_has_corrupted_implicits(tpl_args)
local groups = {
cfg.class_groups.weapons.keys,
cfg.class_groups.armor.keys,
cfg.class_groups.jewellery.keys,
{['Quiver'] = true, ['Jewel'] = true, ['AbyssJewel'] = true},
}
for _, g in ipairs(groups) do
if h.conditions.factory.arg{arg='class_id', values_assoc=g}(tpl_args) then
return true
end
end
return false
end
function h.conditions.item_class_has_influences(tpl_args)
local groups = {
cfg.class_groups.weapons.keys,
cfg.class_groups.armor.keys,
cfg.class_groups.jewellery.keys,
{['Quiver'] = true},
}
for _, g in ipairs(groups) do
if h.conditions.factory.arg{arg='class_id', values_assoc=g}(tpl_args) and h.conditions.factory.not_arg{arg='class_id', value='FishingRod'}(tpl_args) then
return true
end
end
return false
end
function h.conditions.item_class_has_synthesised_implicits(tpl_args)
local groups = {
cfg.class_groups.weapons.keys,
cfg.class_groups.armor.keys,
cfg.class_groups.jewellery.keys,
{['Quiver'] = true, ['Jewel'] = true, ['AbyssJewel'] = true},
}
for _, g in ipairs(groups) do
if h.conditions.factory.arg{arg='class_id', values_assoc=g}(tpl_args) and h.conditions.factory.not_arg{arg='class_id', value='FishingRod'}(tpl_args) then
return true
end
end
return false
end
function h.conditions.item_class_has_fractured_modifiers(tpl_args)
local groups = {
cfg.class_groups.weapons.keys,
cfg.class_groups.armor.keys,
cfg.class_groups.jewellery.keys,
{['Quiver'] = true, ['Jewel'] = true, ['Map'] = true},
}
for _, g in ipairs(groups) do
if h.conditions.factory.arg{arg='class_id', values_assoc=g}(tpl_args) and h.conditions.factory.not_arg{arg='class_id', value='FishingRod'}(tpl_args) then
return true
end
end
return false
end
-- ----------------------------------------------------------------------------
-- Additional configuration
-- ----------------------------------------------------------------------------
local c = {}
c.named_conditions = {
is_normal = h.conditions.factory.arg{arg='rarity_id', value='normal'},
is_unique = h.conditions.factory.arg{arg='rarity_id', value='unique'},
is_not_drop_restricted = h.conditions.factory.arg{arg='is_drop_restricted', value=false},
is_not_corrupted = h.conditions.factory.arg{arg='is_corrupted', value=false},
is_not_replica = h.conditions.factory.arg{arg='is_replica', value=false},
drop_level_ngt_divcard_default_max_ilvl = h.conditions.factory.drop_level_not_greater_than{level=cfg.divination_card_exchange_default_max_ilvl},
item_class_has_corrupted_implicits = h.conditions.item_class_has_corrupted_implicits,
item_class_has_influences = h.conditions.item_class_has_influences,
item_class_has_synthesised_implicits = h.conditions.item_class_has_synthesised_implicits,
item_class_has_fractured_modifiers = h.conditions.item_class_has_fractured_modifiers,
}
-- Order matters!
-- Put most specific outcome at the top and the least specific at the bottom.
c.automatic_recipes = {
--[[
{
conditions = {
function (tpl_args) end,
},
text = '',
parts = {
{
name = '',
item_id = '',
amount = 0,
notes = '',
},
},
},
]]
}
-- ----------------------------------------------------------------------------
-- Exported functions
-- ----------------------------------------------------------------------------
local p = {}
function p.process_recipes(tpl_args)
local query_data = {
id = {},
name = {},
page = {},
}
local recipes = {}
-- ------------------------------------------------------------------------
-- Manual data
-- ------------------------------------------------------------------------
local recipe_num = #recipes + 1
local recipe
repeat
local prefix = string.format('recipe%s_', recipe_num)
local part_num = 1
local part
recipe = {
parts = {},
result_amount = tonumber(tpl_args[prefix .. 'result_amount']) or 1,
text = m_util.cast.text(tpl_args[prefix .. 'description']),
automatic = false,
}
repeat
local part_prefix = string.format('%spart%s_', prefix, part_num)
part = {
item_name = tpl_args[part_prefix .. 'item_name'],
item_id = tpl_args[part_prefix .. 'item_id'],
item_page = tpl_args[part_prefix .. 'item_page'],
amount = tonumber(tpl_args[part_prefix .. 'amount']),
notes = m_util.cast.text(tpl_args[part_prefix .. 'notes']),
}
if part.item_name ~= nil or part.item_id ~= nil or part.item_page ~= nil then
if part.amount == nil then
error(string.format(i18n.errors.missing_amount, part_prefix .. 'amount'))
else
for key, array in pairs(query_data) do
local value = part['item_' .. key]
if value then
if array[value] then
table.insert(array[value], {recipe_num, part_num})
else
array[value] = {{recipe_num, part_num}, }
end
end
end
recipe.parts[#recipe.parts+1] = part
end
end
part_num = part_num + 1
until part.item_name == nil and part.item_id == nil and part.item_page == nil
-- recipe was empty, can terminate safely
if #recipe.parts == 0 then
recipe = nil
else
recipe_num = recipe_num + 1
recipes[#recipes+1] = recipe
end
until recipe == nil
-- ------------------------------------------------------------------------
-- Automatic
-- ------------------------------------------------------------------------
--
-- maps
--
local automatic_index = #recipes + 1
-- TODO: 3.9.0 Unsure how this works yet, so disabled for now
--[[if tpl_args.atlas_connections and tpl_args.rarity_id == 'normal' then
local results = m_cargo.query(
{'items', 'maps'},
{'items._pageName', 'items.name'},
{
join='items._pageID=maps._pageID',
where=string.format('items.class_id = "Map" AND items.rarity_id = "normal" AND maps.tier < %s AND items._pageName IN ("%s")', tpl_args.map_tier, table.concat(tpl_args.atlas_connections, '", "')),
}
)
for _, row in ipairs(results) do
recipes[#recipes+1] = {
text = i18n.misc.upgraded_from_map,
result_amount = 1,
parts = {
{
item_name = row['items.name'],
item_page = row['items._pageName'],
amount = 3,
notes = nil,
},
},
automatic = true,
}
end
end]]
--
-- oils
--
if tpl_args._flags.is_blight_item and tpl_args.blight_item_tier > 1 then
local results = m_cargo.query(
{'items', 'blight_items'},
{'items._pageName', 'items.name'},
{
join='items._pageID=blight_items._pageID',
where=string.format('blight_items.tier = %s', tpl_args.blight_item_tier - 1),
}
)
for _, row in ipairs(results) do
recipes[#recipes+1] = {
text = nil,
result_amount = 1,
parts = {
{
item_name = row['items.name'],
item_page = row['items._pageName'],
amount = 3,
notes = nil,
},
},
automatic = true,
}
end
end
--
-- essences
--
-- exclude remnant of corruption via type
if tpl_args._flags.is_essence and tpl_args.essence_type > 0 then
local results = m_cargo.query(
{'items', 'essences'},
{
'items._pageName',
'items.name',
'items.metadata_id',
'essences.category',
'essences.type',
},
{
join='items._pageID=essences._pageID',
where=string.format([[
(essences.category="%s" AND essences.level = %s)
OR (essences.type = %s AND essences.level = %s)
OR items.metadata_id = 'Metadata/Items/Currency/CurrencyCorruptMonolith'
OR (%s = 6 AND essences.type = 5 AND essences.level >= 5)
]],
tpl_args.essence_category, tpl_args.essence_level - 1,
tpl_args.essence_type - 1, tpl_args.essence_level,
-- special case for corruption only essences
tpl_args.essence_type
),
orderBy='essences.level ASC, essences.type ASC',
}
)
local remnant = results[1]
if remnant['items.metadata_id'] ~= 'Metadata/Items/Currency/CurrencyCorruptMonolith' then
error(string.format('Something went seriously wrong here. Got results: %s', mw.dumpObject(results)))
end
for i=2, #results do
local row = results[i]
if row['essences.category'] == tpl_args.essence_category then
-- 3 to 1 recipe
recipes[#recipes+1] = {
automatic = true,
result_amount = 1,
text = nil,
parts = {
{
item_id = row['items.metadata_id'],
item_page = row['items._pageName'],
item_name = row['items.name'],
amount = 3,
},
},
}
-- corruption +1
recipes[#recipes+1] = {
automatic = true,
result_amount = 1,
text = i18n.essence_plus_one_level,
parts = {
{
item_id = row['items.metadata_id'],
item_page = row['items._pageName'],
item_name = row['items.name'],
amount = 1,
},
{
item_id = remnant['items.metadata_id'],
item_page = remnant['items._pageName'],
item_name = remnant['items.name'],
amount = 1,
},
},
}
elseif tonumber(row['essences.type']) == tpl_args.essence_type - 1 then
-- corruption type change
recipes[#recipes+1] = {
automatic = true,
result_amount = 1,
text = i18n.essence_type_change,
parts = {
{
item_id = row['items.metadata_id'],
item_page = row['items._pageName'],
item_name = row['items.name'],
amount = 1,
},
{
item_id = remnant['items.metadata_id'],
item_page = remnant['items._pageName'],
item_name = remnant['items.name'],
amount = 1,
},
},
}
end
end
end
-- data based on mapping
if tpl_args.drop_enabled and not tpl_args.disable_automatic_recipes then
-- Test and cache results of all named conditions
for k, condition in pairs(c.named_conditions) do
if type(condition) == 'function' then
c.named_conditions[k] = condition(tpl_args)
end
end
for _, data in ipairs(c.automatic_recipes) do
local valid = true -- Can this recipe produce the item?
-- Check cached results for named conditions
for k, condition in pairs(c.named_conditions) do
if data.conditions[k] then
valid = condition
if not valid then
break
end
end
end
-- Test anonymous conditions
for _, condition in ipairs(data.conditions) do
valid = condition(tpl_args) and valid
if not valid then
break
end
end
if valid then
recipes[#recipes+1] = {
automatic = true,
result_amount = 1,
text = data.text(),
parts = data.parts,
}
for part_num, row in ipairs(data.parts) do
if query_data['id'][row.item_id] then
table.insert(query_data['id'][row.item_id], {#recipes, part_num})
else
query_data['id'][row.item_id] = {{#recipes, part_num}, }
end
end
end
end
end
if #recipes == 0 then
return
end
--
-- Fetch item data in a single query to sacrifice database load with a lot of references
--
local query_data_array = {
id = {},
name = {},
page = {},
}
local query_fields = {
id = 'items.metadata_id',
page = 'items._pageName',
name = 'items.name',
}
local where = {}
local expected_count = 0
for key, thing_array in pairs(query_data) do
for thing, _ in pairs(thing_array) do
table.insert(query_data_array[key], thing)
end
if #query_data_array[key] > 0 then
expected_count = expected_count + #query_data_array[key]
local q_data = table.concat(query_data_array[key], '", "')
table.insert(where, string.format('%s IN ("%s")', query_fields[key], q_data))
end
end
local results = m_cargo.query(
{'items'},
{'items._pageName', 'items.name', 'items.metadata_id'},
{
where=table.concat(where, ' OR '),
}
)
-- Now do The Void
for _, row in ipairs(results) do
if row[query_fields.id] and string.find(row[query_fields.id], 'Metadata/Items/DivinationCards/', 1, true) then
local part = {
item_id = 'Metadata/Items/DivinationCards/DivinationCardTheVoid',
amount = 1,
}
local result = m_cargo.query(
{'items'},
{'items._pageName', 'items.name', 'items.metadata_id'},
{
where=string.format('%s = "%s"', query_fields.id, part.item_id),
}
)
if #result > 0 then
recipes[#recipes+1] = {
automatic = true,
result_amount = 1,
text = i18n.the_void,
parts = {part},
}
if query_data['id'][part.item_id] then
table.insert(query_data['id'][part.item_id], {#recipes, 1})
else
query_data['id'][part.item_id] = {{#recipes, 1}, }
end
table.insert(results, result[1])
end
break
end
end
for _, row in ipairs(results) do
for key, thing_array in pairs(query_data) do
local recipe_parts = thing_array[row[query_fields[key]]]
if recipe_parts then
for _, recipe_part in ipairs(recipe_parts) do
local entry = recipes[recipe_part[1]].parts[recipe_part[2]]
for entry_key, data_key in pairs(query_fields) do
-- metadata_id may be nil, since we don't know them for unique items
if row[data_key] then
entry['item_' .. entry_key] = row[data_key]
end
end
end
-- set this to nil for error checking in later step
thing_array[row[query_fields[key]]] = nil
end
end
end
-- sbow the broken references if needed
if #results ~= expected_count then
-- query data was pruned of existing keys earlier, so only broken keys remain
for key, array in pairs(query_data) do
for thing, recipe_parts in pairs(array) do
for _, recipe_part in ipairs(recipe_parts) do
tpl_args._flags.invalid_recipe_parts = true
tpl_args._errors[#tpl_args._errors+1] = m_util.string.format(i18n.errors.invalid_recipe_parts, string.format('recipe%s_part%s_item_%s', recipe_part[1], recipe_part[2], key), thing)
end
end
end
end
--
-- Check for duplicates
--
local delete_recipes = {}
for i=automatic_index, #recipes do
for j=1, automatic_index-1 do
if #recipes[i].parts == #recipes[j].parts then
local match = true
for row_id, row in ipairs(recipes[i].parts) do
-- Only the fields from the database query are matched since we can be sure they're correct. Other fields may be subject to user error.
for _, key in ipairs({'item_id', 'item_name', 'item_page'}) do
match = match and (row[key] == recipes[j].parts[row_id][key])
end
end
if match then
tpl_args._flags.duplicate_recipes = true
tpl_args._errors[#tpl_args._errors+1] = string.format(i18n.errors.duplicate_recipes, j)
delete_recipes[#delete_recipes+1] = j
end
end
end
end
for offset, index in ipairs(delete_recipes) do
table.remove(recipes, index-(offset-1))
end
--
-- Set data
--
tpl_args.recipes = recipes
-- Set recipes data
for i, recipe in ipairs(recipes) do
table.insert(tpl_args._store_data, {
_table = 'acquisition_recipes',
recipe_id = i,
result_amount = recipe.result_amount,
description = recipe.text,
automatic = recipe.automatic,
})
for j, part in ipairs(recipe.parts) do
table.insert(tpl_args._store_data, {
_table = 'acquisition_recipe_parts',
part_id = j,
recipe_id = i,
item_name = part.item_name,
item_id = part.item_id,
item_page = part.item_page,
amount = part.amount,
notes = part.notes,
})
end
end
end
--
-- Debugging
--
function p.debug_validate_auto_upgraded_from()
local q = {}
local chk = {}
for _, data in ipairs(c.automatic_recipes) do
for _, part in ipairs(data.parts) do
q[#q+1] = part.item_id
chk[part.item_id] = {
amount=part.amount,
text=data.text(),
}
end
end
local results = m_cargo.array_query{
tables={'items', 'stackables'},
fields={'items.name', 'items.class_id', 'items.description', 'stackables.stack_size'},
id_field='items.metadata_id',
id_array=q,
query={
join='items._pageName=stackables._pageName',
},
}
for _, row in ipairs(results) do
if row['items.class_id'] == 'DivinationCard' and chk[row['items.metadata_id']].amount ~= tonumber(row['stackables.stack_size']) then
mw.logObject(string.format('Amount mismatch %s, expected %s', row['items.metadata_id'], row['stackables.stack_size']))
end
end
local tbl = mw.html.create('table')
tbl:attr('class', 'wikitable sortable')
for _, row in ipairs(results) do
tbl
:tag('tr')
:tag('td')
:wikitext(row['items.name'])
:done()
:tag('td')
:wikitext(chk[row['items.metadata_id']].text)
:done()
:tag('td')
:wikitext(row['items.description'])
:done()
:done()
end
return tostring(tbl)
end
return p