Module:Item/recipes: Difference between revisions
Jump to navigation
Jump to search
(Removed automatic upgrade path for The Breach – Breach uniques are not league-specific items, so this upgrade path must be specified manually.) |
No edit summary |
||
(132 intermediate revisions by 6 users not shown) | |||
Line 1: | Line 1: | ||
------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | ||
-- | -- | ||
-- | -- Recipes for Module:Item | ||
-- | -- | ||
------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | ||
local m_util = require('Module:Util') | |||
local m_cargo = require('Module:Cargo') | local m_cargo = require('Module:Cargo') | ||
local m_game = mw.loadData('Module:Game') | local m_game = mw.loadData('Module:Game') | ||
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. | local i18n = cfg.i18n.recipes | ||
-- ---------------------------------------------------------------------------- | -- ---------------------------------------------------------------------------- | ||
Line 49: | Line 49: | ||
-- Optional: | -- Optional: | ||
-- negate: negates the check against the value, i.e. whether the value is not equal or not in the list/table. | -- 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 | -- Inner type of function depending on whether to check a single value, a list of values or an associative list of values | ||
Line 77: | Line 75: | ||
-- Outer type of function depending on whether to check a single value or against a table | -- Outer type of function depending on whether to check a single value or against a table | ||
return function (tpl_args | return function (tpl_args) | ||
local tpl_value = tpl_args[args.arg] | local tpl_value = tpl_args[args.arg] | ||
local rtr | local rtr | ||
Line 99: | Line 97: | ||
end | end | ||
function h.conditions.factory. | function h.conditions.factory.not_arg(args) | ||
return function (tpl_args, | args = args or {} | ||
for _, | args.negate = true | ||
if | 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 | return true | ||
end | end | ||
Line 110: | Line 143: | ||
end | end | ||
h.conditions. | 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 | function h.conditions.item_class_has_corrupted_implicits(tpl_args) | ||
local groups = { | local groups = { | ||
cfg.class_groups.weapons.keys, | cfg.class_groups.weapons.keys, | ||
Line 121: | Line 160: | ||
} | } | ||
for _, g in ipairs(groups) do | for _, g in ipairs(groups) do | ||
if h.conditions.factory.arg{arg='class_id', values_assoc=g}(tpl_args | if h.conditions.factory.arg{arg='class_id', values_assoc=g}(tpl_args) then | ||
return true | return true | ||
end | end | ||
Line 128: | Line 167: | ||
end | end | ||
function h.conditions.item_class_has_influences(tpl_args | function h.conditions.item_class_has_influences(tpl_args) | ||
local groups = { | local groups = { | ||
cfg.class_groups.weapons.keys, | cfg.class_groups.weapons.keys, | ||
Line 136: | Line 175: | ||
} | } | ||
for _, g in ipairs(groups) do | for _, g in ipairs(groups) do | ||
if h.conditions.factory.arg{arg='class_id', values_assoc=g}(tpl_args, | 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 | return true | ||
end | end | ||
Line 149: | Line 218: | ||
local c = {} | 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! | -- Order matters! | ||
-- Put most specific outcome at the top and the least specific at the bottom. | -- Put most specific outcome at the top and the least specific at the bottom. | ||
c. | c.automatic_recipes = { | ||
--[[ | --[[ | ||
{ | { | ||
conditions = { | |||
function (tpl_args) end, | |||
function (tpl_args | |||
}, | }, | ||
text = '', | text = '', | ||
parts = { | |||
{ | { | ||
name = '', | name = '', | ||
Line 176: | Line 248: | ||
}, | }, | ||
}, | }, | ||
]] | |||
} | } | ||
Line 2,566: | Line 258: | ||
local p = {} | local p = {} | ||
function p. | function p.process_recipes(tpl_args) | ||
local query_data = { | local query_data = { | ||
id = {}, | id = {}, | ||
Line 2,572: | Line 264: | ||
page = {}, | page = {}, | ||
} | } | ||
local | local recipes = {} | ||
-- ------------------------------------------------------------------------ | -- ------------------------------------------------------------------------ | ||
-- Manual data | -- Manual data | ||
-- ------------------------------------------------------------------------ | -- ------------------------------------------------------------------------ | ||
local | local recipe_num = #recipes + 1 | ||
local | local recipe | ||
repeat | repeat | ||
local prefix = string.format(' | local prefix = string.format('recipe%s_', recipe_num) | ||
local | local part_num = 1 | ||
local | local part | ||
recipe = { | |||
parts = {}, | |||
text = m_util.cast.text(tpl_args[prefix .. ' | result_amount = tonumber(tpl_args[prefix .. 'result_amount']) or 1, | ||
text = m_util.cast.text(tpl_args[prefix .. 'description']), | |||
automatic = false, | automatic = false, | ||
} | } | ||
repeat | repeat | ||
local | local part_prefix = string.format('%spart%s_', prefix, part_num) | ||
part = { | |||
item_name = tpl_args[ | item_name = tpl_args[part_prefix .. 'item_name'], | ||
item_id = tpl_args[ | item_id = tpl_args[part_prefix .. 'item_id'], | ||
item_page = tpl_args[ | item_page = tpl_args[part_prefix .. 'item_page'], | ||
amount = tonumber(tpl_args[ | amount = tonumber(tpl_args[part_prefix .. 'amount']), | ||
notes = m_util.cast.text(tpl_args[ | notes = m_util.cast.text(tpl_args[part_prefix .. 'notes']), | ||
} | } | ||
if | if part.item_name ~= nil or part.item_id ~= nil or part.item_page ~= nil then | ||
if | if part.amount == nil then | ||
error(string.format(i18n.errors.missing_amount, | error(string.format(i18n.errors.missing_amount, part_prefix .. 'amount')) | ||
else | else | ||
for key, array in pairs(query_data) do | for key, array in pairs(query_data) do | ||
local value = | local value = part['item_' .. key] | ||
if value then | if value then | ||
if array[value] then | if array[value] then | ||
table.insert(array[value], { | table.insert(array[value], {recipe_num, part_num}) | ||
else | else | ||
array[value] = {{ | array[value] = {{recipe_num, part_num}, } | ||
end | end | ||
end | end | ||
end | end | ||
recipe.parts[#recipe.parts+1] = part | |||
end | end | ||
end | end | ||
part_num = part_num + 1 | |||
until | until part.item_name == nil and part.item_id == nil and part.item_page == nil | ||
-- | -- recipe was empty, can terminate safely | ||
if # | if #recipe.parts == 0 then | ||
recipe = nil | |||
else | else | ||
recipe_num = recipe_num + 1 | |||
recipes[#recipes+1] = recipe | |||
end | end | ||
until | until recipe == nil | ||
-- ------------------------------------------------------------------------ | -- ------------------------------------------------------------------------ | ||
-- Automatic | -- Automatic | ||
Line 2,634: | Line 328: | ||
-- maps | -- maps | ||
-- | -- | ||
local automatic_index = # | local automatic_index = #recipes + 1 | ||
-- TODO: 3.9.0 Unsure how this works yet, so disabled for now | -- TODO: 3.9.0 Unsure how this works yet, so disabled for now | ||
--[[if tpl_args.atlas_connections and tpl_args.rarity_id == | --[[if tpl_args.atlas_connections and tpl_args.rarity_id == 'normal' then | ||
local results = m_cargo.query( | local results = m_cargo.query( | ||
{'items', 'maps'}, | {'items', 'maps'}, | ||
Line 2,646: | Line 340: | ||
) | ) | ||
for _, row in ipairs(results) do | for _, row in ipairs(results) do | ||
recipes[#recipes+1] = { | |||
text = i18n.misc.upgraded_from_map, | text = i18n.misc.upgraded_from_map, | ||
result_amount = 1, | |||
parts = { | |||
{ | { | ||
item_name = row['items.name'], | item_name = row['items.name'], | ||
Line 2,674: | Line 369: | ||
) | ) | ||
for _, row in ipairs(results) do | for _, row in ipairs(results) do | ||
recipes[#recipes+1] = { | |||
text = nil, | text = nil, | ||
result_amount = 1, | |||
parts = { | |||
{ | { | ||
item_name = row['items.name'], | item_name = row['items.name'], | ||
Line 2,695: | Line 391: | ||
-- exclude remnant of corruption via type | -- exclude remnant of corruption via type | ||
if tpl_args.is_essence and tpl_args.essence_type > 0 then | if tpl_args._flags.is_essence and tpl_args.essence_type > 0 then | ||
local results = m_cargo.query( | local results = m_cargo.query( | ||
{'items', 'essences'}, | {'items', 'essences'}, | ||
Line 2,730: | Line 426: | ||
if row['essences.category'] == tpl_args.essence_category then | if row['essences.category'] == tpl_args.essence_category then | ||
-- 3 to 1 recipe | -- 3 to 1 recipe | ||
recipes[#recipes+1] = { | |||
automatic = true, | automatic = true, | ||
result_amount = 1, | |||
text = nil, | text = nil, | ||
parts = { | |||
{ | { | ||
item_id = row['items.metadata_id'], | item_id = row['items.metadata_id'], | ||
Line 2,743: | Line 440: | ||
} | } | ||
-- corruption +1 | -- corruption +1 | ||
recipes[#recipes+1] = { | |||
automatic = true, | automatic = true, | ||
result_amount = 1, | |||
text = i18n.essence_plus_one_level, | text = i18n.essence_plus_one_level, | ||
parts = { | |||
{ | { | ||
item_id = row['items.metadata_id'], | item_id = row['items.metadata_id'], | ||
Line 2,763: | Line 461: | ||
elseif tonumber(row['essences.type']) == tpl_args.essence_type - 1 then | elseif tonumber(row['essences.type']) == tpl_args.essence_type - 1 then | ||
-- corruption type change | -- corruption type change | ||
recipes[#recipes+1] = { | |||
automatic = true, | automatic = true, | ||
result_amount = 1, | |||
text = i18n.essence_type_change, | text = i18n.essence_type_change, | ||
parts = { | |||
{ | { | ||
item_id = row['items.metadata_id'], | item_id = row['items.metadata_id'], | ||
Line 2,786: | Line 485: | ||
-- data based on mapping | -- data based on mapping | ||
if tpl_args.drop_enabled and not tpl_args. | if tpl_args.drop_enabled and not tpl_args.disable_automatic_recipes then | ||
for | -- 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 | |||
if | |||
if not | |||
break | break | ||
end | end | ||
end | end | ||
end | end | ||
for _, condition in ipairs(data. | |||
-- Test anonymous conditions | |||
if not | for _, condition in ipairs(data.conditions) do | ||
valid = condition(tpl_args) and valid | |||
if not valid then | |||
break | break | ||
end | end | ||
end | end | ||
if | if valid then | ||
recipes[#recipes+1] = { | |||
automatic = true, | automatic = true, | ||
text = data.text( | result_amount = 1, | ||
text = data.text(), | |||
parts = data.parts, | |||
} | } | ||
for | for part_num, row in ipairs(data.parts) do | ||
if query_data['id'][row.item_id] then | if query_data['id'][row.item_id] then | ||
table.insert(query_data['id'][row.item_id], {# | table.insert(query_data['id'][row.item_id], {#recipes, part_num}) | ||
else | else | ||
query_data['id'][row.item_id] = {{# | query_data['id'][row.item_id] = {{#recipes, part_num}, } | ||
end | end | ||
end | end | ||
Line 2,833: | Line 532: | ||
end | end | ||
if # | if #recipes == 0 then | ||
return | return | ||
end | end | ||
-- | -- | ||
-- Fetch item data in a single query to sacrifice database load with a lot of | -- Fetch item data in a single query to sacrifice database load with a lot of references | ||
-- | -- | ||
local query_data_array = { | local query_data_array = { | ||
Line 2,868: | Line 567: | ||
} | } | ||
) | ) | ||
-- 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 _, row in ipairs(results) do | ||
for key, thing_array in pairs(query_data) do | for key, thing_array in pairs(query_data) do | ||
local | local recipe_parts = thing_array[row[query_fields[key]]] | ||
if | if recipe_parts then | ||
for _, | for _, recipe_part in ipairs(recipe_parts) do | ||
local entry = | local entry = recipes[recipe_part[1]].parts[recipe_part[2]] | ||
for entry_key, data_key in pairs(query_fields) do | for entry_key, data_key in pairs(query_fields) do | ||
-- metadata_id may be nil, since we don't know them for unique items | -- metadata_id may be nil, since we don't know them for unique items | ||
Line 2,892: | Line 623: | ||
-- query data was pruned of existing keys earlier, so only broken keys remain | -- query data was pruned of existing keys earlier, so only broken keys remain | ||
for key, array in pairs(query_data) do | for key, array in pairs(query_data) do | ||
for thing, | for thing, recipe_parts in pairs(array) do | ||
for _, | for _, recipe_part in ipairs(recipe_parts) do | ||
tpl_args._flags. | tpl_args._flags.invalid_recipe_parts = true | ||
tpl_args._errors[#tpl_args._errors+1] = string.format(i18n.errors. | 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 | ||
Line 2,904: | Line 635: | ||
-- Check for duplicates | -- Check for duplicates | ||
-- | -- | ||
local | local delete_recipes = {} | ||
for i=automatic_index, # | for i=automatic_index, #recipes do | ||
for j=1, automatic_index-1 do | for j=1, automatic_index-1 do | ||
if # | if #recipes[i].parts == #recipes[j].parts then | ||
local match = true | local match = true | ||
for row_id, row in ipairs( | 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. | -- 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 | for _, key in ipairs({'item_id', 'item_name', 'item_page'}) do | ||
match = match and (row[key] == | match = match and (row[key] == recipes[j].parts[row_id][key]) | ||
end | end | ||
end | end | ||
if match then | if match then | ||
tpl_args._flags. | tpl_args._flags.duplicate_recipes = true | ||
tpl_args._errors[#tpl_args._errors+1] = string.format(i18n.errors. | tpl_args._errors[#tpl_args._errors+1] = string.format(i18n.errors.duplicate_recipes, j) | ||
delete_recipes[#delete_recipes+1] = j | |||
end | end | ||
end | end | ||
Line 2,924: | Line 655: | ||
end | end | ||
for offset, index in ipairs( | for offset, index in ipairs(delete_recipes) do | ||
table.remove( | table.remove(recipes, index-(offset-1)) | ||
end | end | ||
-- | -- | ||
-- Set data | -- Set data | ||
-- | -- | ||
tpl_args. | tpl_args.recipes = recipes | ||
-- | -- Set recipes data | ||
for i, | for i, recipe in ipairs(recipes) do | ||
table.insert(tpl_args._store_data, { | |||
_table = ' | _table = 'acquisition_recipes', | ||
recipe_id = i, | |||
result_amount = recipe.result_amount, | |||
automatic = | description = recipe.text, | ||
} | automatic = recipe.automatic, | ||
}) | |||
for j, | for j, part in ipairs(recipe.parts) do | ||
table.insert(tpl_args._store_data, { | |||
_table = ' | _table = 'acquisition_recipe_parts', | ||
part_id = j, | |||
recipe_id = i, | |||
item_name = | item_name = part.item_name, | ||
item_id = | item_id = part.item_id, | ||
item_page = | item_page = part.item_page, | ||
amount = | amount = part.amount, | ||
notes = | notes = part.notes, | ||
} | }) | ||
end | end | ||
end | end | ||
Line 2,960: | Line 692: | ||
-- | -- | ||
function p.debug_validate_auto_upgraded_from( | function p.debug_validate_auto_upgraded_from() | ||
local q = {} | local q = {} | ||
local chk = {} | local chk = {} | ||
for _, data in ipairs(c. | for _, data in ipairs(c.automatic_recipes) do | ||
for _, | for _, part in ipairs(data.parts) do | ||
q[#q+1] = | q[#q+1] = part.item_id | ||
chk[ | chk[part.item_id] = { | ||
amount= | amount=part.amount, | ||
text=data.text( | text=data.text(), | ||
} | } | ||
end | end | ||
Line 2,991: | Line 721: | ||
end | end | ||
tbl = mw.html.create('table') | local tbl = mw.html.create('table') | ||
tbl:attr('class', 'wikitable sortable') | tbl:attr('class', 'wikitable sortable') | ||
for _, row in ipairs(results) do | for _, row in ipairs(results) do |
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