打开/关闭菜单
打开/关闭外观设置菜单
打开/关闭个人菜单
未登录
未登录用户的IP地址会在进行任意编辑后公开展示。

本站正在进行早期测试,目前仍存在许多内容的缺失。

模块:Machine:修订间差异

来自星砂岛百科
Sizau留言 | 贡献
无编辑摘要
Sizau-bot留言 | 贡献
优化 RecipeUsage 机器展示结构与样式
 
(未显示2个用户的10个中间版本)
第1行: 第1行:
local common = require('Module:Common')
local common = require('Module:Common')
local item_common = require('Module:ItemCommon')
local item = require('Module:Item')
local css = require('Module:CSS')
local css = require('Module:CSS')
local item_module = require('Module:Item')
local npc_module = require('Module:NPC')


local p = {}
local p = {}


local character_cache
local FIELD_MAP = {
local character_records
    type = 't',
local character_ids
    type_display = 'td',
local character_mapping
    item_level = 'lv',
local global_rules
    rarity = 'r',
local tag_items
    sell_price = 'sp',
 
    base_value = 'bv',
local item_cache
    max_stack = 'ms',
local item_by_index
    max_quality = 'mq',
local item_mapping
    tags = 'g',
local item_redirects
    origin = 'o',
 
    locked_origin = 'lo',
local id_registry
    use_description = 'ud',
local registry_character_ids
    unlock_title = 'ut',
local registry_item_ids
    construct_template = 'ct',
local registry_character_index_by_id
    construction_template_count = 'ctc',
local registry_item_index_by_id
    energy_capacity = 'ec',
local summary_cache
    production_formula = 'pf',
 
    production_formula_origin = 'pfo',
local LUACACHE_NAMESPACE = 'gifting:npc_tabs'
    craft = 'c',
local LUACACHE_VERSION = '2026-03-16-version-1'
    upgrade = 'u',
    prev_machine = 'pm',
    next_machine = 'nm',
}


local level_to_bucket = {
local RECIPE_FIELD_DEFAULTS = {
     resonance = 'resonance',
     result = 'r',
    special = 'special',
     result_count = 'rc',
    loved = 'loved',
     materials = 'm',
    liked = 'liked',
     time = 't',
    neutral = 'neutral',
     machine = 'mc',
    disliked = 'disliked',
     cooking_method = 'cm',
    hated = 'hated',
    r = 'resonance',
    s = 'special',
     v = 'loved',
     k = 'liked',
     n = 'neutral',
     d = 'disliked',
     h = 'hated',
}
}


local bucket_to_short = {
local data_cache
    resonance = 'r',
local mapping_cache
    special = 's',
local recipe_cache
    loved = 'v',
local recipe_list
    liked = 'k',
local item_recipe_cache
    neutral = 'n',
local field_map = RECIPE_FIELD_DEFAULTS
    disliked = 'd',
    hated = 'h',
}


local function normalize_key(value)
local function is_empty(value)
     return common.normalizeKey(value)
     if value == nil then
        return true
    end
    for _ in pairs(value) do
        return false
    end
    return true
end
end


local function resolve_level_bucket(level)
local function count_array(arr)
     if level == nil then
     if type(arr) ~= 'table' then
         return nil
         return 0
     end
     end


     if type(level) == 'string' then
     local count = 0
        local normalized = normalize_key(level)
    for _ in pairs(arr) do
         return level_to_bucket[normalized]
         count = count + 1
     end
     end
    return count
end


    local numeric = tonumber(level)
local function load_data()
     if numeric == 4 then
     if data_cache then
         return 'resonance'
         return
     end
     end
     if numeric == 3 then
 
         return 'special'
    local raw_data
    raw_data, mapping_cache = item_common.loadDomainData('数据:Machine/machine_index.json')
     if type(raw_data) == 'table' and type(raw_data.records) == 'table' then
         data_cache = raw_data.records
    else
        data_cache = raw_data or {}
     end
     end
     if numeric == 2 then
end
         return 'loved'
 
local function find_record(key)
    load_data()
    return item_common.findRecord(data_cache, mapping_cache, key)
end
 
local function get_short_field(field)
    return FIELD_MAP[field] or field
end
 
local function get_record_field(record, key, field)
     if type(record) ~= 'table' then
         return nil
     end
     end
     if numeric == 1 then
 
        return 'liked'
     local short_key = get_short_field(field)
    end
     if record[short_key] ~= nil then
     if numeric == 0 then
         return record[short_key]
         return 'neutral'
     end
     end
     if numeric == -1 then
     if record[field] ~= nil then
         return 'disliked'
         return record[field]
     end
     end
     if numeric == -2 then
     if field == 'id' or field == 'name' or field == 'name_en' then
         return 'hated'
         return item_common.getIdentityField(record, key, field)
     end
     end
     return nil
     return nil
end
end


local function empty_summary()
local function get_craft_field(craft, short_key, long_key)
     return {
     if type(craft) ~= 'table' then
        resonance = { personal = {}, global = {} },
         return nil
         special = { personal = {}, global = {} },
    end
        loved = { personal = {}, global = {} },
    if craft[short_key] ~= nil then
        liked = { personal = {}, global = {} },
         return craft[short_key]
         neutral = { personal = {}, global = {} },
    end
        disliked = { personal = {}, global = {} },
     return craft[long_key]
        hated = { personal = {}, global = {} },
     }
end
end


local function load_id_registry()
local function format_time(seconds)
     if id_registry then
    seconds = tonumber(seconds) or 0
         return
     if seconds <= 0 then
         return ''
     end
     end
     id_registry = common.loadJsonData('数据:Gifting/gift_id_registry.json') or {}
     if seconds < 60 then
    registry_character_ids = id_registry.characters or {}
        return tostring(math.floor(seconds)) .. ''
    registry_item_ids = id_registry.items or {}
    registry_character_index_by_id = {}
    for idx, value in ipairs(registry_character_ids) do
        local key = normalize_key(value)
        if key ~= '' then
            registry_character_index_by_id[key] = idx - 1
        end
     end
     end
    registry_item_index_by_id = {}
    for idx, value in ipairs(registry_item_ids) do
        local key = normalize_key(value)
        if key ~= '' then
            registry_item_index_by_id[key] = idx - 1
        end
    end
end


local function item_id_from_index(index)
    local minutes = math.floor(seconds / 60)
    load_id_registry()
     local remain = math.floor(seconds % 60)
     local n = tonumber(index)
     if remain == 0 then
     if not n then
         return tostring(minutes) .. ''
         return ''
     end
     end
     return common.trim(registry_item_ids[n + 1] or '')
     return tostring(minutes) .. '分' .. tostring(remain) .. ''
end
end


local function character_id_from_index(index)
local function render_materials(frame, materials)
     load_id_registry()
     if is_empty(materials) then
    local n = tonumber(index)
    if not n then
         return ''
         return ''
     end
     end
    return common.trim(registry_character_ids[n + 1] or '')
end


local function load_character_data()
    local item_ids = {}
    if character_cache then
    for item_id in pairs(materials) do
         return
         item_ids[#item_ids + 1] = item_id
     end
     end
    item_common.sortItemKeys(item_ids)


     character_cache = common.loadJsonData('数据:Gifting/gift_preferences.json') or {}
     local parts = {}
    character_records = character_cache.characters or {}
     for _, item_id in ipairs(item_ids) do
    global_rules = character_cache.global or {}
         parts[#parts + 1] = frame:expandTemplate{
    tag_items = character_cache.tag_items or {}
            title = 'Item',
    load_id_registry()
             args = { item_id, tostring(materials[item_id]), class = 'block' },
 
         }
    character_ids = {}
     for idx, record in ipairs(character_records) do
         local cid = character_id_from_index(idx - 1)
        if cid ~= '' then
             character_ids[normalize_key(cid)] = idx
         end
     end
     end
 
     return table.concat(parts, '')
     character_mapping = common.loadJsonData('数据:Gifting/gifting_mapping.json') or {
        name_to_id = {},
        id_to_name = {},
        aliases = {},
        overrides = {
            name_to_id = {},
            aliases = {},
        },
    }
end
end


local function load_item_data()
local function render_item_template(frame, item_id)
     if item_cache then
    local resolved_id = common.trim(item_id or '')
         return
     if resolved_id == '' then
         return ''
     end
     end


     item_cache = common.loadJsonData('数据:Gifting/gift_preferences_by_item.json') or {}
     local css_out = css.quickCall('Item') or ''
     item_by_index = item_cache.items or {}
     return css_out .. item.renderItemWithArgs(frame, { resolved_id })
    item_redirects = item_cache.item_redirects or {}
    item_mapping = common.loadJsonData('数据:Item/item_mapping.json') or {
        name_to_id = {},
        id_to_name = {},
        aliases = {},
        overrides = {
            name_to_id = {},
            aliases = {},
        },
    }
end
end


local function find_character_record(key)
local function page_exists(title_text)
     load_character_data()
     local title = mw.title.new(common.trim(title_text))
    return title and title.exists or false
end


     local resolved = common.trim(key)
local function render_machine_reference(machine_id)
     if resolved == '' then
     local resolved_id = common.trim(machine_id or '')
         resolved = common.getCurrentTitleText()
     if resolved_id == '' then
         return ''
     end
     end


     local normalized = normalize_key(resolved)
     local machine_name = common.trim(item.getNameByKey(resolved_id) or '')
     local idx = character_ids[normalized]
     if machine_name == '' then
 
         machine_name = resolved_id
    if not idx then
         local override_id = character_mapping.overrides and character_mapping.overrides.name_to_id and character_mapping.overrides.name_to_id[normalized]
        if override_id then
            idx = character_ids[normalize_key(override_id)]
        end
     end
     end


     if not idx then
     local image_name = resolved_id .. '.png'
        local override_alias = character_mapping.overrides and character_mapping.overrides.aliases and character_mapping.overrides.aliases[normalized]
    local has_icon = common.filePageExists(image_name)
         if override_alias then
    local out = { '<span class="itemtemplate itemtemplateblock recipe-usage-machine-chip">' }
            idx = character_ids[normalize_key(override_alias)]
    if has_icon then
         end
        out[#out + 1] = '<span class="item-icon-container">'
         out[#out + 1] = ('[[File:%s|24x24px|link=]]'):format(image_name)
         out[#out + 1] = '</span>'
     end
     end
 
    out[#out + 1] = '<span class="item-text">'
     if not idx then
     if page_exists(machine_name) then
         local mapped_id = character_mapping.name_to_id and character_mapping.name_to_id[normalized]
         out[#out + 1] = ('[[%s|%s]]'):format(machine_name, machine_name)
         if mapped_id then
    else
            idx = character_ids[normalize_key(mapped_id)]
         out[#out + 1] = mw.text.encode(machine_name)
        end
     end
     end
    out[#out + 1] = '</span>'
    out[#out + 1] = '</span>'
    return table.concat(out)
end


     if not idx then
local function render_craft_table(title_text, station_label, station_value, materials_label, materials_value, time_label, time_value, exp_label, exp_value)
        local alias_id = character_mapping.aliases and character_mapping.aliases[normalized]
    local out = {}
        if alias_id then
    out[#out + 1] = '<div class="recipe-usage-table-wrap">'
            idx = character_ids[normalize_key(alias_id)]
     out[#out + 1] = '<table class="wikitable machine-craft-table recipe-usage-table">'
        end
    out[#out + 1] = '<tr><th colspan="2">' .. title_text .. '</th></tr>'
    out[#out + 1] = '<tr><th>' .. station_label .. '</th><td>' .. station_value .. '</td></tr>'
    out[#out + 1] = '<tr><th>' .. materials_label .. '</th><td>' .. materials_value .. '</td></tr>'
    if time_value ~= '' then
        out[#out + 1] = '<tr><th>' .. time_label .. '</th><td>' .. time_value .. '</td></tr>'
     end
     end
 
     if exp_value ~= '' then
     if not idx then
         out[#out + 1] = '<tr><th>' .. exp_label .. '</th><td>' .. exp_value .. '</td></tr>'
         return nil
     end
     end
     return character_records[idx], idx - 1
     out[#out + 1] = '</table>'
    out[#out + 1] = '</div>'
    return table.concat(out, '\n')
end
end


local function resolve_item_index(index)
local function is_array_table(value)
     local key = tostring(index)
     if type(value) ~= 'table' then
    local num_key = tonumber(index)
         return false
   
    if item_by_index[key] then
         return key
     end
     end
   
 
    if num_key and item_by_index[num_key] then
     local count = 0
        return num_key
     for key in pairs(value) do
    end
         if type(key) ~= 'number' then
   
             return false
     local redirected = item_redirects and item_redirects[key]
     if not redirected and num_key then
         redirected = item_redirects[num_key]
    end
   
    if redirected then
        local redirected_key = tostring(redirected)
        local redirected_num = tonumber(redirected)
       
        if item_by_index[redirected_key] then
            return redirected_key
        end
       
        if redirected_num and item_by_index[redirected_num] then
             return redirected_num
         end
         end
        count = count + 1
     end
     end
   
    return nil
end


local function find_item_record(key)
    if count == 0 then
    load_item_data()
        return false
     load_id_registry()
     end


     local resolved = common.trim(key)
     for index = 1, count do
    if resolved == '' then
        if value[index] == nil then
         resolved = common.getCurrentTitleText()
            return false
         end
     end
     end


     local normalized = normalize_key(resolved)
     return true
    local item_id = ''
end
    local item_index = registry_item_index_by_id[normalized]
    if item_index ~= nil then
        item_id = registry_item_ids[item_index + 1] or ''
    end


    if item_id == '' then
local function normalize_material_options(materials)
        local override_id = item_mapping.overrides and item_mapping.overrides.name_to_id and item_mapping.overrides.name_to_id[normalized]
    if type(materials) ~= 'table' then
        if override_id and override_id ~= '' then
         return {}
            item_id = override_id
         end
     end
     end


     if item_id == '' then
     if is_array_table(materials) then
         local override_alias = item_mapping.overrides and item_mapping.overrides.aliases and item_mapping.overrides.aliases[normalized]
         local options = {}
         if override_alias and override_alias ~= '' then
         for _, option in ipairs(materials) do
             item_id = override_alias
            if type(option) == 'table' and not is_empty(option) then
                options[#options + 1] = option
             end
         end
         end
        return options
     end
     end


     if item_id == '' then
     if is_empty(materials) then
         local mapped_id = item_mapping.name_to_id and item_mapping.name_to_id[normalized]
         return {}
        if mapped_id and mapped_id ~= '' then
            item_id = mapped_id
        end
     end
     end


     if item_id == '' then
     return { materials }
        local alias_id = item_mapping.aliases and item_mapping.aliases[normalized]
end
        if alias_id and alias_id ~= '' then
            item_id = alias_id
        end
    end


     if item_id == '' then
local function render_material_option(frame, materials)
         return nil
     if type(materials) ~= 'table' or is_empty(materials) then
         return ''
     end
     end


     if item_index == nil then
     local item_ids = {}
         item_index = registry_item_index_by_id[normalize_key(item_id)]
    for item_id in pairs(materials) do
    end
         item_ids[#item_ids + 1] = item_id
    if item_index == nil then
        return nil
     end
     end
    item_common.sortItemKeys(item_ids)


     local resolved_index = resolve_item_index(item_index)
     local parts = {}
    if not resolved_index then
    for _, item_id in ipairs(item_ids) do
         return nil
         parts[#parts + 1] = item.renderItemWithArgs(frame, { item_id, tostring(materials[item_id]), class = 'block' })
     end
     end
 
     return table.concat(parts, '')
     return item_by_index[resolved_index]
end
end


local function append_unique_indexes(values, seen, target)
local function filter_material_option(materials, excluded_item_id)
     if type(values) ~= 'table' then
     if type(materials) ~= 'table' then
         return
         return {}
     end
     end
     for _, value in ipairs(values) do
 
        local index = tonumber(value)
    local filtered = {}
         if index ~= nil and not seen[index] then
     for item_id, count in pairs(materials) do
             seen[index] = true
         if item_id ~= excluded_item_id then
            target[#target + 1] = index
             filtered[item_id] = count
         end
         end
     end
     end
    return filtered
end
end


local function expand_rule_items(rule)
local function render_material_option_or_empty(frame, materials)
     local result = {}
     local rendered = render_material_option(frame, materials)
     local seen = {}
     if rendered == '' then
    for _, tag in ipairs(rule.t or {}) do
         return '—'
         append_unique_indexes(tag_items[tag], seen, result)
     end
     end
    append_unique_indexes(rule.s, seen, result)
     return rendered
     return result
end
end


local function build_character_summary(record, record_index)
local function render_recipe_materials(frame, materials, excluded_item_id)
     if record_index ~= nil then
    local options = normalize_material_options(materials)
         summary_cache = summary_cache or {}
     if #options == 0 then
        if summary_cache[record_index] then
         return ''
            return summary_cache[record_index]
        end
     end
     end
    local summary = empty_summary()
    local global_seen = {
        resonance = {}, special = {}, loved = {}, liked = {}, neutral = {}, disliked = {}, hated = {},
    }
    local personal_seen = {
        resonance = {}, special = {}, loved = {}, liked = {}, neutral = {}, disliked = {}, hated = {},
    }
    local all_personal_items = {}


     for _, rule in ipairs(record.p or {}) do
     if excluded_item_id and excluded_item_id ~= '' then
         for _, item_idx in ipairs(expand_rule_items(rule)) do
        local filtered_options = {}
             all_personal_items[item_idx] = true
        local has_non_empty_option = false
         for _, option in ipairs(options) do
            local filtered = filter_material_option(option, excluded_item_id)
            if not is_empty(filtered) then
                has_non_empty_option = true
            end
             filtered_options[#filtered_options + 1] = filtered
         end
         end
    end
         if not has_non_empty_option then
 
             return '—'
    for _, rule in ipairs(global_rules) do
         local bucket = resolve_level_bucket(rule.l)
        if bucket and summary[bucket] then
            for _, item_idx in ipairs(expand_rule_items(rule)) do
                if not all_personal_items[item_idx] and not global_seen[bucket][item_idx] then
                    global_seen[bucket][item_idx] = true
                    summary[bucket].global[#summary[bucket].global + 1] = item_idx
                end
             end
         end
         end
        options = filtered_options
     end
     end


     for _, rule in ipairs(record.p or {}) do
     if #options == 1 then
         local bucket = resolve_level_bucket(rule.l)
         return render_material_option_or_empty(frame, options[1])
        if bucket and summary[bucket] then
            for _, item_idx in ipairs(expand_rule_items(rule)) do
                if not personal_seen[bucket][item_idx] then
                    personal_seen[bucket][item_idx] = true
                    summary[bucket].personal[#summary[bucket].personal + 1] = item_idx
                end
            end
        end
     end
     end


     if record_index ~= nil then
     local out = {}
         summary_cache[record_index] = summary
    out[#out + 1] = '<div class="recipe-usage-option-label">可选材料方案</div>'
    out[#out + 1] = '<ul class="recipe-usage-option-list">'
    for _, option in ipairs(options) do
         out[#out + 1] = '<li>' .. render_material_option_or_empty(frame, option) .. '</li>'
     end
     end
     return summary
    out[#out + 1] = '</ul>'
     return table.concat(out, '')
end
end


local function get_summary_bucket(record, bucket, record_index)
local function material_counts_for_item(materials, item_id)
     if type(record) ~= 'table' then
     local counts = {}
         return nil
    local seen = {}
    for _, option in ipairs(normalize_material_options(materials)) do
        local count = tonumber(option[item_id]) or 0
        if count > 0 and not seen[count] then
            seen[count] = true
            counts[#counts + 1] = count
         end
     end
     end
     local resolved_bucket = level_to_bucket[bucket] or normalize_key(bucket)
     table.sort(counts)
    local summary = build_character_summary(record, record_index)
     return counts
     return summary[resolved_bucket]
end
end


local function item_label_from_index(item_index)
local function render_material_count_list(counts)
    load_item_data()
     if #counts == 0 then
    local item_id = item_id_from_index(item_index)
     if item_id == '' then
         return ''
         return ''
     end
     end
    return common.trim(item_mapping.id_to_name and item_mapping.id_to_name[normalize_key(item_id)] or '')
     if #counts == 1 then
end
         return tostring(counts[1])
 
local function item_display_meta(item_index)
    local item_id = item_id_from_index(item_index)
     if item_id == '' then
         return nil
    end
 
    local name = item_label_from_index(item_index)
    if name == '' then
        name = item_id
     end
     end


     local file_name = item_id
     local out = { '<ul class="recipe-usage-count-list">' }
     if file_name == '' then
     for _, count in ipairs(counts) do
        file_name = name
        out[#out + 1] = '<li>' .. tostring(count) .. '</li>'
     end
     end
 
    out[#out + 1] = '</ul>'
     return {
     return table.concat(out, '')
        name = name,
        image = file_name .. '.png',
    }
end
end


local character_index_cache
local function load_recipes()
 
     if recipe_cache then
local function load_character_index()
     if character_index_cache then
         return
         return
     end
     end
    character_index_cache = common.loadJsonData('数据:Character/character_index.json') or {}
end


local function is_creature(name)
     local data = common.loadJsonData('数据:Machine/machine_recipes.json') or {}
    load_character_index()
     recipe_list = data.recipes or {}
     local normalized = normalize_key(name)
     recipe_cache = data.by_machine or {}
     local record = character_index_cache[normalized]
     item_recipe_cache = data.by_item or {}
     if record and record.entity_kind == 'creature' then
     field_map = (data._meta or {}).field_map or RECIPE_FIELD_DEFAULTS
        return true
     end
     return false
end
end


local function item_template_from_name(frame, name)
local function recipe_time_text(recipe)
     if not frame or common.trim(name) == '' then
     if not recipe then
         return nil
         return ''
     end
     end


     if is_creature(name) then
     local value = tonumber(recipe[field_map.time]) or 0
         return '[[' .. name .. ']]'
    if value <= 0 then
         return ''
     end
     end
 
     return format_time(value)
    local output = npc_module.renderNPC(frame:newChild{
        title = 'Module:NPC',
        args = { [1] = name, class = 'block' }
    })
     return common.trim(output) ~= '' and output or ''
end
end


local function character_label_from_index(character_index)
local function render_recipe_result(frame, recipe)
    load_character_data()
     if not recipe then
    local character_id = character_id_from_index(character_index)
     if character_id == '' then
         return ''
         return ''
     end
     end
    return common.trim(character_mapping.id_to_name and character_mapping.id_to_name[normalize_key(character_id)] or '')
end


local function join_indexes(values, label_resolver, fallback_resolver)
    local result_id = recipe[field_map.result]
     if type(values) ~= 'table' or #values == 0 then
     if not result_id or result_id == '' then
         return ''
         return ''
     end
     end


     local parts = {}
     local result_count = tonumber(recipe[field_map.result_count]) or 1
    for _, value in ipairs(values) do
    if result_count > 1 then
        local idx = tonumber(value)
        return item.renderItemWithArgs(frame, { result_id, tostring(result_count), class = 'block' })
        if idx ~= nil then
            local label = label_resolver and label_resolver(idx) or ''
            if label ~= '' then
                parts[#parts + 1] = ('[[%s]]'):format(label)
            else
                local fallback = fallback_resolver and fallback_resolver(idx) or ''
                if fallback ~= '' then
                    parts[#parts + 1] = fallback
                end
            end
        end
     end
     end
     return table.concat(parts, '')
     return item.renderItemWithArgs(frame, { result_id, class = 'block' })
end
end


local function render_bucket(bucket_data, personal_label, global_label, label_resolver, fallback_resolver)
local function machine_group_heading(frame, machine_id)
     if type(bucket_data) ~= 'table' then
     local rendered = render_machine_reference(machine_id)
    if rendered == '' then
         return ''
         return ''
     end
     end
     local parts = {}
     return '<div class="recipe-usage-machine"><div class="recipe-usage-machine-label">' .. rendered .. '</div></div>'
    local personal = join_indexes(bucket_data.personal, label_resolver, fallback_resolver)
    local global = join_indexes(bucket_data.global, label_resolver, fallback_resolver)
    if personal ~= '' then
        parts[#parts + 1] = personal_label .. ':' .. personal
    end
    if global ~= '' then
        parts[#parts + 1] = global_label .. ':' .. global
    end
    return table.concat(parts, '<br />')
end
end


local function render_item_grid(frame, values)
local function compare_recipe_ids(left_rid, right_rid)
     local items = {}
     local left_recipe = recipe_list[left_rid + 1]
     local seen = {}
     local right_recipe = recipe_list[right_rid + 1]
     if type(values) == 'table' then
     if not left_recipe or not right_recipe then
         for _, value in ipairs(values) do
         return (tonumber(left_rid) or 0) < (tonumber(right_rid) or 0)
            local idx = tonumber(value)
            if idx ~= nil then
            local meta = item_display_meta(idx)
                if meta then
                    local dedupe_key = normalize_key(meta.name) .. '|' .. (meta.image or "default")
                    if not seen[dedupe_key] then
                        seen[dedupe_key] = true
                        local item_markup = item_template_from_name(frame, meta.name)
                        if item_markup and item_markup ~= '' then
                            items[#items + 1] = item_markup
                        end
                    end
                end
            end
        end
     end
     end


     if #items == 0 then
    local left_result_id = common.trim(left_recipe[field_map.result] or '')
         return '<div class="gifting-empty">暂无可显示物品</div>'
    local right_result_id = common.trim(right_recipe[field_map.result] or '')
     if left_result_id ~= right_result_id then
         return item_common.compareItemKeys(left_result_id, right_result_id)
     end
     end


     local root = mw.html.create('div'):addClass('gifting-item-grid')
     local left_machine_id = common.trim(left_recipe[field_map.machine] or '')
     for _, item_markup in ipairs(items) do
    local right_machine_id = common.trim(right_recipe[field_map.machine] or '')
         root:tag('div')
     if left_machine_id ~= right_machine_id then
            :addClass('gifting-item-entry')
         return item_common.compareItemKeys(left_machine_id, right_machine_id)
            :wikitext(item_markup)
     end
     end


     return tostring(root)
     local left_time = tonumber(left_recipe[field_map.time]) or 0
end
    local right_time = tonumber(right_recipe[field_map.time]) or 0
    if left_time ~= right_time then
        return left_time < right_time
    end


local function render_preference_panel(frame, title, values)
     return left_rid < right_rid
    local panel = mw.html.create('div'):addClass('gifting-preference-panel')
    panel:tag('div'):addClass('gifting-preference-panel-title'):wikitext(title)
    panel:tag('div'):addClass('gifting-preference-panel-body'):wikitext(render_item_grid(frame, values))
     return tostring(panel)
end
end


local function render_npc_level(frame, record, record_index, bucket)
local function sort_recipe_ids(recipe_ids)
    local bucket_data = get_summary_bucket(record, bucket, record_index)
     if type(recipe_ids) ~= 'table' or #recipe_ids <= 1 then
     if type(bucket_data) ~= 'table' then
         return recipe_ids
         return '<div class="gifting-level-grid"><div class="gifting-empty">暂无数据</div></div>'
     end
     end


     local root = mw.html.create('div')
     table.sort(recipe_ids, compare_recipe_ids)
        :addClass('gifting-level-grid')
     return recipe_ids
        :addClass('gifting-level-grid--' .. bucket)
    root:wikitext(render_preference_panel(frame, '个人喜好', bucket_data.personal or {}))
    root:wikitext(render_preference_panel(frame, '通用喜好', bucket_data.global or {}))
     return tostring(root)
end
end


local function build_tabber_content(frame, record, record_index)
local function split_recipe_groups_by_machine(recipe_ids)
     local tabs = {
     local ordered_groups = {}
        { key = 'loved', label = '最爱' },
    local groups_by_machine = {}
         { key = 'liked', label = '喜欢' },
 
         { key = 'neutral', label = '一般' },
    for _, rid in ipairs(recipe_ids) do
        { key = 'disliked', label = '不喜欢' },
         local recipe = recipe_list[rid + 1]
        { key = 'hated', label = '讨厌' },
         if recipe then
        { key = 'resonance', label = '共鸣' },
            local machine_id = common.trim(recipe[field_map.machine] or '')
         { key = 'special', label = '特殊' },
            local group = groups_by_machine[machine_id]
     }
            if not group then
                group = {
                    machine_id = machine_id,
                    recipe_ids = {},
                }
                groups_by_machine[machine_id] = group
                ordered_groups[#ordered_groups + 1] = group
            end
            group.recipe_ids[#group.recipe_ids + 1] = rid
         end
     end


     local parts = {}
     local featured_groups = {}
     for index, tab in ipairs(tabs) do
    local remaining_ids = {}
         parts[#parts + 1] = tab.label .. '=' .. render_npc_level(frame, record, record_index, tab.key)
     for _, group in ipairs(ordered_groups) do
        if index < #tabs then
         sort_recipe_ids(group.recipe_ids)
            parts[#parts + 1] = '|-|'
        if #group.recipe_ids >= 3 then
            featured_groups[#featured_groups + 1] = group
        else
            for _, rid in ipairs(group.recipe_ids) do
                remaining_ids[#remaining_ids + 1] = rid
            end
         end
         end
     end
     end
     return table.concat(parts, '\n')
 
     table.sort(featured_groups, function(left_group, right_group)
        return item_common.compareItemKeys(left_group.machine_id, right_group.machine_id)
    end)
    sort_recipe_ids(remaining_ids)
 
    return featured_groups, remaining_ids
end
end


第619行: 第509行:
         return ''
         return ''
     end
     end
     local record, character_index = find_character_record(key)
 
     local record = find_record(key)
     if not record then
     if not record then
         return ''
         return ''
     end
     end


     if field == 'id' then
    local craft = get_record_field(record, key, 'craft')
         return character_id_from_index(character_index)
    if field == 'craft_machine' and craft then
        local machine_id = get_craft_field(craft, 'm', 'machine')
        if machine_id and machine_id ~= '' then
            return render_item_template(frame, machine_id)
        end
        return ''
    end
     if field == 'craft_time' and craft then
         return format_time(get_craft_field(craft, 't', 'time'))
     end
     end
     if field == 'name' then
     if field == 'craft_exp' and craft then
         return common.toText(record.n)
         return common.toText(get_craft_field(craft, 'e', 'exp'))
     end
     end
     if field == 'name_en' then
 
         return common.toText(record.en)
    local upgrade = get_record_field(record, key, 'upgrade')
     if field == 'upgrade_from' and upgrade then
         local from_id = get_craft_field(upgrade, 'f', 'from_machine')
        if from_id and from_id ~= '' then
            local from_name = item.getNameByKey(from_id)
            if from_name and from_name ~= '' then
                return '[[' .. from_name .. ']]'
            end
        end
        return ''
     end
     end
     if field == 'character_type' then
     if field == 'upgrade_time' and upgrade then
         return common.toText(record.t)
         return format_time(get_craft_field(upgrade, 't', 'time'))
     end
     end
     if field == 'personal_rules' then
     if field == 'upgrade_exp' and upgrade then
         return common.toText(record.p)
         return common.toText(get_craft_field(upgrade, 'e', 'exp'))
     end
     end
     return common.toText(record[field])
    if field == 'construction_template_count' then
        local value = tonumber(get_record_field(record, key, field)) or 0
        if value <= 0 then
            return ''
        end
        return tostring(value)
    end
    if field == 'energy_capacity' then
        local value = tonumber(get_record_field(record, key, field)) or 0
        if value <= 0 then
            return ''
        end
        return tostring(value)
    end
    if field == 'prev_machine' then
        local prev_id = get_record_field(record, key, 'prev_machine')
        if prev_id and prev_id ~= '' then
            return render_item_template(frame, prev_id)
        end
        return ''
    end
    if field == 'next_machine' then
        local next_id = get_record_field(record, key, 'next_machine')
        if next_id and next_id ~= '' then
            return render_item_template(frame, next_id)
        end
        return ''
    end
 
     return common.toText(get_record_field(record, key, field))
end
end


function p.preferenceList(frame)
function p.ingredientList(frame)
     local key = common.getArg(frame, 1, '')
     local key = common.getArg(frame, 1, '')
     local field = common.getArg(frame, 2, '')
     local record = find_record(key)
     if field == '' then
     if not record then
         return ''
         return ''
     end
     end
     local record = find_character_record(key)
 
     if not record then
     local craft = get_record_field(record, key, 'craft')
     if not craft then
         return ''
         return ''
     end
     end
     return render_bucket(get_summary_bucket(record, field), '个人喜好', '通用喜好', item_label_from_index, item_id_from_index)
     return render_materials(frame, get_craft_field(craft, 'mat', 'materials'))
end
end


function p.renderNpcTabs(frame)
function p.upgradeIngredientList(frame)
     local key = common.getArg(frame, 1, '')
     local key = common.getArg(frame, 1, '')
     local resolved = common.trim(key)
     local record = find_record(key)
     if resolved == '' then
     if not record then
         resolved = common.getCurrentTitleText()
         return ''
     end
     end


     local cache_key = common.buildLuaCacheKey(LUACACHE_NAMESPACE, LUACACHE_VERSION, normalize_key(resolved))
     local upgrade = get_record_field(record, key, 'upgrade')
    local content = common.luaCacheGet(cache_key)
     if not upgrade then
     if content == nil then
         return ''
         local record, record_index = find_character_record(resolved)
        if not record then
            common.luaCacheSet(cache_key, '')
            content = ''
        else
            content = build_tabber_content(frame, record, record_index)
            common.luaCacheSet(cache_key, content)
        end
     end
     end
    return render_materials(frame, get_craft_field(upgrade, 'mat', 'materials'))
end


     local css_out = (css.quickCall('GiftsByNPC') or '') .. (css.quickCall('Item') or '')
function p.craftInfo(frame)
     if content == '' then
     local key = common.getArg(frame, 1, '')
         return css_out .. '<div class="gifting-empty">未找到送礼数据</div>'
    local record = find_record(key)
     if not record then
         return ''
     end
     end
    return css_out .. frame:extensionTag('tabber', content, { class = 'tabber-no-active-indicator gifting-tabber' })
end


local function render_character_list(frame, values)
    local out = {}
     if type(values) ~= 'table' then
 
         return ''
    local craft = get_record_field(record, key, 'craft')
    local craft_materials = get_craft_field(craft, 'mat', 'materials')
     if craft and not is_empty(craft_materials) then
        local machine_id = get_craft_field(craft, 'm', 'machine')
        local machine_display = '—'
        if machine_id and common.trim(machine_id) ~= '' then
            local machine_name = item.getNameByKey(machine_id)
            if machine_name and machine_name ~= '' then
                machine_display = '[[' .. machine_name .. ']]'
            end
        end
         out[#out + 1] = render_craft_table(
            '制造配方',
            '制造站',
            machine_display,
            '材料',
            render_materials(frame, craft_materials),
            '时间',
            format_time(get_craft_field(craft, 't', 'time')),
            '经验',
            common.toText(get_craft_field(craft, 'e', 'exp'))
        )
     end
     end
      
 
     local parts = {}
     local upgrade = get_record_field(record, key, 'upgrade')
     for _, value in pairs(values) do
     local upgrade_materials = get_craft_field(upgrade, 'mat', 'materials')
         local idx = tonumber(value)
     if upgrade and not is_empty(upgrade_materials) then
         if idx ~= nil then
         local from_id = get_craft_field(upgrade, 'f', 'from_machine')
             local name = character_label_from_index(idx)
        local from_display = '—'
             if name ~= '' then
         if from_id and common.trim(from_id) ~= '' then
                 if is_creature(name) then
             local from_name = item.getNameByKey(from_id)
                    parts[#parts + 1] = '[[' .. name .. ']]'
             if from_name and from_name ~= '' then
                else
                 from_display = '[[' .. from_name .. ']]'
                    local output = npc_module.renderNPC(frame:newChild{
                        title = 'Module:NPC',
                        args = { [1] = name }
                    })
                    if common.trim(output) ~= '' then
                        parts[#parts + 1] = output
                    end
                end
             end
             end
         end
         end
        out[#out + 1] = render_craft_table(
            '升级配方',
            '升级自',
            from_display,
            '材料',
            render_materials(frame, upgrade_materials),
            '时间',
            format_time(get_craft_field(upgrade, 't', 'time')),
            '经验',
            common.toText(get_craft_field(upgrade, 'e', 'exp'))
        )
     end
     end
     return table.concat(parts, '')
 
    local css_out = (css.quickCall('Item') or '') .. (css.quickCall('RecipeUsage') or '')
     return css_out .. table.concat(out, '\n')
end
end


function p.preferenceListByItem(frame)
function p.recipeList(frame)
     local key = common.getArg(frame, 1, '')
     local key = common.getArg(frame, 1, '')
     local field = common.getArg(frame, 2, '')
     local record = find_record(key)
     if field == '' then
     if not record then
         return ''
         return ''
     end
     end


     local record = find_item_record(key)
     local machine_id = get_record_field(record, key, 'id')
     if not record then
     if not machine_id or machine_id == '' then
         return ''
         return ''
     end
     end


     local bucket = level_to_bucket[field] or normalize_key(field)
    load_recipes()
     local short = bucket_to_short[bucket]
     local recipe_ids = recipe_cache[mw.ustring.lower(machine_id)]
     local compact_bucket = record.s and short and record.s[short] or nil
    if not recipe_ids then
    if type(compact_bucket) ~= 'table' then
        return '该机器暂无独有配方。'
         return ''
    end
 
     local has_time = false
     for _, rid in ipairs(recipe_ids) do
        local recipe = recipe_list[rid + 1]
        if recipe and recipe_time_text(recipe) ~= '' then
            has_time = true
            break
        end
    end
 
    local out = {}
    out[#out + 1] = '<div class="recipe-usage-table-wrap">'
    out[#out + 1] = '<table class="wikitable recipe-usage-table">'
    out[#out + 1] = '<tr><th>产物</th><th>材料</th>'
    if has_time then
         out[#out + 1] = '<th>时间</th>'
     end
     end
    out[#out + 1] = '</tr>'


     local parts = {}
     local ordered_recipe_ids = {}
     local personal = render_character_list(frame, compact_bucket.p or {})
     for _, rid in ipairs(recipe_ids) do
    local global = render_character_list(frame, compact_bucket.g or {})
         ordered_recipe_ids[#ordered_recipe_ids + 1] = rid
   
    if personal ~= '' then
         parts[#parts + 1] = '个人喜好角色:' .. personal
     end
     end
     if global ~= '' then
     sort_recipe_ids(ordered_recipe_ids)
        parts[#parts + 1] = '通用喜好角色:' .. global
 
    for _, rid in ipairs(ordered_recipe_ids) do
        local recipe = recipe_list[rid + 1]
        if recipe then
            out[#out + 1] = '<tr>'
            out[#out + 1] = '<td>' .. render_recipe_result(frame, recipe) .. '</td>'
            out[#out + 1] = '<td>' .. render_recipe_materials(frame, recipe[field_map.materials]) .. '</td>'
            if has_time then
                out[#out + 1] = '<td>' .. recipe_time_text(recipe) .. '</td>'
            end
            out[#out + 1] = '</tr>'
        end
     end
     end
      
 
     return table.concat(parts, '<br />')
     out[#out + 1] = '</table>'
    out[#out + 1] = '</div>'
    local css_out = (css.quickCall('Item') or '') .. (css.quickCall('RecipeUsage') or '')
     return css_out .. table.concat(out, '\n')
end
end


local BLOCKED_CHARACTERS = { '程洲', '弦一', '貂', '熊猫', '鹦鹉' }
local function recipe_ids_have_time(recipe_ids)
local PET_ANIMALS = { '水豚', '猫', '狗', '狐狸', '小熊猫', '松鼠' }
     for _, rid in ipairs(recipe_ids) do
 
        local recipe = recipe_list[rid + 1]
local function is_blocked_character(name)
         if recipe and recipe_time_text(recipe) ~= '' then
     for _, blocked in ipairs(BLOCKED_CHARACTERS) do
         if name == blocked then
             return true
             return true
         end
         end
第755行: 第739行:
end
end


local function is_pet_animal(name)
local function render_product_recipe_table(frame, recipe_ids, include_machine_column)
     for _, pet in ipairs(PET_ANIMALS) do
    local ordered_recipe_ids = {}
         if name == pet then
    for _, rid in ipairs(recipe_ids) do
             return true
        ordered_recipe_ids[#ordered_recipe_ids + 1] = rid
    end
    sort_recipe_ids(ordered_recipe_ids)
 
    local has_time = recipe_ids_have_time(ordered_recipe_ids)
    local out = {}
    out[#out + 1] = '<div class="recipe-usage-table-wrap">'
    out[#out + 1] = '<table class="wikitable recipe-usage-table">'
    out[#out + 1] = '<tr><th>产出</th>'
    if include_machine_column then
        out[#out + 1] = '<th>机器</th>'
    end
    out[#out + 1] = '<th>所需材料</th>'
    if has_time then
        out[#out + 1] = '<th>时间</th>'
    end
    out[#out + 1] = '</tr>'
 
     for _, rid in ipairs(ordered_recipe_ids) do
        local recipe = recipe_list[rid + 1]
         if recipe then
            out[#out + 1] = '<tr>'
            out[#out + 1] = '<td>' .. render_recipe_result(frame, recipe) .. '</td>'
            if include_machine_column then
                out[#out + 1] = '<td>' .. render_machine_reference(recipe[field_map.machine]) .. '</td>'
            end
            out[#out + 1] = '<td>' .. render_recipe_materials(frame, recipe[field_map.materials]) .. '</td>'
            if has_time then
                out[#out + 1] = '<td>' .. recipe_time_text(recipe) .. '</td>'
            end
             out[#out + 1] = '</tr>'
         end
         end
     end
     end
     return false
 
    out[#out + 1] = '</table>'
    out[#out + 1] = '</div>'
     return table.concat(out, '')
end
end


local function render_character_list_filtered(frame, values)
local function render_ingredient_recipe_table(frame, recipe_ids, current_item_id, include_machine_column)
     if type(values) ~= 'table' then
    local ordered_recipe_ids = {}
         return ''
    for _, rid in ipairs(recipe_ids) do
        ordered_recipe_ids[#ordered_recipe_ids + 1] = rid
    end
     sort_recipe_ids(ordered_recipe_ids)
 
    local out = {}
    out[#out + 1] = '<div class="recipe-usage-table-wrap">'
    out[#out + 1] = '<table class="wikitable recipe-usage-table">'
    out[#out + 1] = '<tr><th>产物</th>'
    if include_machine_column then
         out[#out + 1] = '<th>机器</th>'
     end
     end
      
     out[#out + 1] = '<th>所需材料</th></tr>'
    local npc_parts = {}
 
    local pet_parts = {}
     for _, rid in ipairs(ordered_recipe_ids) do
    local seen = {}
         local recipe = recipe_list[rid + 1]
   
         if recipe then
     for _, value in pairs(values) do
             local counts = material_counts_for_item(recipe[field_map.materials], current_item_id)
         local idx = tonumber(value)
             if #counts > 0 then
         if idx ~= nil then
                 out[#out + 1] = '<tr>'
             local name = character_label_from_index(idx)
                 out[#out + 1] = '<td>' .. render_recipe_result(frame, recipe) .. '</td>'
             if name ~= '' and not is_blocked_character(name) and not seen[name] then
                 if include_machine_column then
                 seen[name] = true
                     out[#out + 1] = '<td>' .. render_machine_reference(recipe[field_map.machine]) .. '</td>'
                 if is_pet_animal(name) then
                    pet_parts[#pet_parts + 1] = '<span class="npctemplate">' .. name .. '</span>'
                 else
                     local output = npc_module.renderNPC(frame:newChild{
                        title = 'Module:NPC',
                        args = { [1] = name }
                    })
                    if common.trim(output) ~= '' then
                        npc_parts[#npc_parts + 1] = output
                    end
                 end
                 end
                out[#out + 1] = '<td>' .. render_recipe_materials(frame, recipe[field_map.materials]) .. '</td>'
                out[#out + 1] = '</tr>'
             end
             end
         end
         end
     end
     end
      
 
     local all_parts = {}
    out[#out + 1] = '</table>'
     for _, part in ipairs(npc_parts) do
     out[#out + 1] = '</div>'
         all_parts[#all_parts + 1] = part
     return table.concat(out, '')
end
 
local function append_recipe_usage_sections(out, frame, recipe_ids, current_item_id, section_title, render_table)
    local featured_groups, remaining_ids = split_recipe_groups_by_machine(recipe_ids)
 
    out[#out + 1] = '<div class="recipe-usage-section">'
    out[#out + 1] = '<h3 class="recipe-usage-section-title">' .. section_title .. '</h3>'
     for _, group in ipairs(featured_groups) do
         local heading = machine_group_heading(frame, group.machine_id)
        if heading ~= '' then
            out[#out + 1] = heading
        end
        out[#out + 1] = render_table(frame, group.recipe_ids, current_item_id, false)
     end
     end
     for _, part in ipairs(pet_parts) do
 
         all_parts[#all_parts + 1] = part
     if #remaining_ids > 0 then
        if #featured_groups > 0 then
            out[#out + 1] = '<h4 class="recipe-usage-subtitle">其他制作站</h4>'
        end
         out[#out + 1] = render_table(frame, remaining_ids, current_item_id, true)
     end
     end
      
     out[#out + 1] = '</div>'
    return table.concat(all_parts, ' · ')
end
end


function p.renderGiftsByItem(frame)
function p.itemRecipes(frame)
    local item_key = common.getArg(frame, 1, '')
    if item_key == '' then
        item_key = common.getCurrentTitleText()
    end
 
    local item_id = item.getIdByKey(item_key)
    if not item_id or item_id == '' then
        return ''
    end
 
    load_recipes()
    local item_data = item_recipe_cache[mw.ustring.lower(item_id)]
    if not item_data then
        return ''
    end
 
    local product_ids = item_data.p or {}
    local ingredient_ids = item_data.i or {}
    local product_count = count_array(product_ids)
    local ingredient_count = count_array(ingredient_ids)
    if product_count == 0 and ingredient_count == 0 then
        return ''
    end
 
    local out = {}
    if product_count > 0 then
        append_recipe_usage_sections(out, frame, product_ids, item_id, '制作配方', function(current_frame, recipe_ids_for_table, _, include_machine_column)
            return render_product_recipe_table(current_frame, recipe_ids_for_table, include_machine_column)
        end)
    end
 
    if ingredient_count > 0 then
        if #out > 0 then
            out[#out + 1] = ''
        end
        append_recipe_usage_sections(out, frame, ingredient_ids, item_id, '用于制作', render_ingredient_recipe_table)
    end
 
    local css_out = (css.quickCall('Item') or '') .. (css.quickCall('RecipeUsage') or '')
    return css_out .. '<div class="recipe-usage">' .. table.concat(out, '\n') .. '</div>'
end
 
function p.machineAsIngredient(frame)
     local key = common.getArg(frame, 1, '')
     local key = common.getArg(frame, 1, '')
     if key == '' then
    local record = find_record(key)
         key = common.getCurrentTitleText()
     if not record then
        return ''
    end
 
    local machine_id = get_record_field(record, key, 'id')
    if not machine_id or machine_id == '' then
         return ''
    end
 
    load_recipes()
    local item_data = item_recipe_cache[mw.ustring.lower(machine_id)]
    if not item_data or not item_data.i or count_array(item_data.i) == 0 then
        return ''
     end
     end
   
 
     local record = find_item_record(key)
     local ordered_recipe_ids = {}
     if not record or type(record.s) ~= 'table' then
     for _, rid in ipairs(item_data.i) do
         return "'''" .. key .. "'''不可用于送礼。"
         ordered_recipe_ids[#ordered_recipe_ids + 1] = rid
     end
     end
      
     sort_recipe_ids(ordered_recipe_ids)
     local buckets = {
 
        { key = 'loved', short = 'v', label = '最爱', icon = 'Gifting love.png' },
     local out = {}
        { key = 'liked', short = 'k', label = '喜欢', icon = 'Gifting like.png' },
    out[#out + 1] = '<div class="recipe-usage-table-wrap">'
        { key = 'neutral', short = 'n', label = '一般', icon = '' },
    out[#out + 1] = '<table class="wikitable recipe-usage-table">'
        { key = 'disliked', short = 'd', label = '不喜欢', icon = '' },
    out[#out + 1] = '<tr><th>产物</th><th>所需数量</th></tr>'
        { key = 'hated', short = 'h', label = '讨厌', icon = '' },
 
        { key = 'resonance', short = 'r', label = '共鸣', icon = '' },
     for _, rid in ipairs(ordered_recipe_ids) do
        { key = 'special', short = 's', label = '特殊', icon = '' },
         local recipe = recipe_list[rid + 1]
    }
         if recipe then
   
             local counts = material_counts_for_item(recipe[field_map.materials], machine_id)
    local rows = {}
             if #counts > 0 then
     for _, bucket in ipairs(buckets) do
                 out[#out + 1] = '<tr>'
         local compact_bucket = record.s[bucket.short]
                 out[#out + 1] = '<td>' .. render_recipe_result(frame, recipe) .. '</td>'
         if type(compact_bucket) == 'table' then
                out[#out + 1] = '<td>' .. render_material_count_list(counts) .. '</td>'
             local personal = render_character_list_filtered(frame, compact_bucket.p or {})
                 out[#out + 1] = '</tr>'
            local global = render_character_list_filtered(frame, compact_bucket.g or {})
           
            local all_chars = {}
             if personal ~= '' then
                 all_chars[#all_chars + 1] = personal
            end
            if global ~= '' then
                 all_chars[#all_chars + 1] = global
            end
           
            if #all_chars > 0 then
                local label_html = bucket.label
                if bucket.icon ~= '' then
                    label_html = '[[File:' .. bucket.icon .. '|16px|link=]] ' .. bucket.label
                 end
                rows[#rows + 1] = { label = label_html, content = table.concat(all_chars, ' ') }
             end
             end
         end
         end
     end
     end
   
 
     if #rows == 0 then
     out[#out + 1] = '</table>'
        return "'''" .. key .. "'''不可用于送礼。"
     out[#out + 1] = '</div>'
     end
     local css_out = (css.quickCall('Item') or '') .. (css.quickCall('RecipeUsage') or '')
   
     return css_out .. table.concat(out, '\n')
    local html = mw.html.create('table'):addClass('wikitable')
     html:tag('tr')
        :tag('th'):css('white-space', 'nowrap'):wikitext('类型'):done()
        :tag('th'):wikitext('人物'):done()
      
    for _, row in ipairs(rows) do
        html:tag('tr')
            :tag('th'):css('white-space', 'nowrap'):wikitext(row.label):done()
            :tag('td'):wikitext(row.content):done()
    end
   
    return tostring(html)
end
end


return p
return p

2026年4月2日 (四) 17:08的最新版本

概述

Machine 提供机器域的字段读取、材料列表、配方信息与反查展示,供 {{Infobox machine}}{{MachineRecipes}}{{RecipeUsage}} 调用。

用法

{{#invoke:Machine|getField|工作台|type_display}}
{{#invoke:Machine|craftInfo|工作台}}
{{#invoke:Machine|recipeList|工作台}}
{{#invoke:Machine|itemRecipes|木板}}

函数

  • getField:读取机器字段。
  • ingredientList:渲染建造材料。
  • upgradeIngredientList:渲染升级材料。
  • craftInfo:渲染机器信息块。
  • recipeList:渲染机器产出配方列表。
  • itemRecipes:渲染物品的机器配方反查。
  • machineAsIngredient:渲染机器作为材料的反查结果。

数据来源


local common = require('Module:Common')
local item_common = require('Module:ItemCommon')
local item = require('Module:Item')
local css = require('Module:CSS')

local p = {}

local FIELD_MAP = {
    type = 't',
    type_display = 'td',
    item_level = 'lv',
    rarity = 'r',
    sell_price = 'sp',
    base_value = 'bv',
    max_stack = 'ms',
    max_quality = 'mq',
    tags = 'g',
    origin = 'o',
    locked_origin = 'lo',
    use_description = 'ud',
    unlock_title = 'ut',
    construct_template = 'ct',
    construction_template_count = 'ctc',
    energy_capacity = 'ec',
    production_formula = 'pf',
    production_formula_origin = 'pfo',
    craft = 'c',
    upgrade = 'u',
    prev_machine = 'pm',
    next_machine = 'nm',
}

local RECIPE_FIELD_DEFAULTS = {
    result = 'r',
    result_count = 'rc',
    materials = 'm',
    time = 't',
    machine = 'mc',
    cooking_method = 'cm',
}

local data_cache
local mapping_cache
local recipe_cache
local recipe_list
local item_recipe_cache
local field_map = RECIPE_FIELD_DEFAULTS

local function is_empty(value)
    if value == nil then
        return true
    end
    for _ in pairs(value) do
        return false
    end
    return true
end

local function count_array(arr)
    if type(arr) ~= 'table' then
        return 0
    end

    local count = 0
    for _ in pairs(arr) do
        count = count + 1
    end
    return count
end

local function load_data()
    if data_cache then
        return
    end

    local raw_data
    raw_data, mapping_cache = item_common.loadDomainData('数据:Machine/machine_index.json')
    if type(raw_data) == 'table' and type(raw_data.records) == 'table' then
        data_cache = raw_data.records
    else
        data_cache = raw_data or {}
    end
end

local function find_record(key)
    load_data()
    return item_common.findRecord(data_cache, mapping_cache, key)
end

local function get_short_field(field)
    return FIELD_MAP[field] or field
end

local function get_record_field(record, key, field)
    if type(record) ~= 'table' then
        return nil
    end

    local short_key = get_short_field(field)
    if record[short_key] ~= nil then
        return record[short_key]
    end
    if record[field] ~= nil then
        return record[field]
    end
    if field == 'id' or field == 'name' or field == 'name_en' then
        return item_common.getIdentityField(record, key, field)
    end
    return nil
end

local function get_craft_field(craft, short_key, long_key)
    if type(craft) ~= 'table' then
        return nil
    end
    if craft[short_key] ~= nil then
        return craft[short_key]
    end
    return craft[long_key]
end

local function format_time(seconds)
    seconds = tonumber(seconds) or 0
    if seconds <= 0 then
        return ''
    end
    if seconds < 60 then
        return tostring(math.floor(seconds)) .. '秒'
    end

    local minutes = math.floor(seconds / 60)
    local remain = math.floor(seconds % 60)
    if remain == 0 then
        return tostring(minutes) .. '分'
    end
    return tostring(minutes) .. '分' .. tostring(remain) .. '秒'
end

local function render_materials(frame, materials)
    if is_empty(materials) then
        return ''
    end

    local item_ids = {}
    for item_id in pairs(materials) do
        item_ids[#item_ids + 1] = item_id
    end
    item_common.sortItemKeys(item_ids)

    local parts = {}
    for _, item_id in ipairs(item_ids) do
        parts[#parts + 1] = frame:expandTemplate{
            title = 'Item',
            args = { item_id, tostring(materials[item_id]), class = 'block' },
        }
    end
    return table.concat(parts, '')
end

local function render_item_template(frame, item_id)
    local resolved_id = common.trim(item_id or '')
    if resolved_id == '' then
        return ''
    end

    local css_out = css.quickCall('Item') or ''
    return css_out .. item.renderItemWithArgs(frame, { resolved_id })
end

local function page_exists(title_text)
    local title = mw.title.new(common.trim(title_text))
    return title and title.exists or false
end

local function render_machine_reference(machine_id)
    local resolved_id = common.trim(machine_id or '')
    if resolved_id == '' then
        return ''
    end

    local machine_name = common.trim(item.getNameByKey(resolved_id) or '')
    if machine_name == '' then
        machine_name = resolved_id
    end

    local image_name = resolved_id .. '.png'
    local has_icon = common.filePageExists(image_name)
    local out = { '<span class="itemtemplate itemtemplateblock recipe-usage-machine-chip">' }
    if has_icon then
        out[#out + 1] = '<span class="item-icon-container">'
        out[#out + 1] = ('[[File:%s|24x24px|link=]]'):format(image_name)
        out[#out + 1] = '</span>'
    end
    out[#out + 1] = '<span class="item-text">'
    if page_exists(machine_name) then
        out[#out + 1] = ('[[%s|%s]]'):format(machine_name, machine_name)
    else
        out[#out + 1] = mw.text.encode(machine_name)
    end
    out[#out + 1] = '</span>'
    out[#out + 1] = '</span>'
    return table.concat(out)
end

local function render_craft_table(title_text, station_label, station_value, materials_label, materials_value, time_label, time_value, exp_label, exp_value)
    local out = {}
    out[#out + 1] = '<div class="recipe-usage-table-wrap">'
    out[#out + 1] = '<table class="wikitable machine-craft-table recipe-usage-table">'
    out[#out + 1] = '<tr><th colspan="2">' .. title_text .. '</th></tr>'
    out[#out + 1] = '<tr><th>' .. station_label .. '</th><td>' .. station_value .. '</td></tr>'
    out[#out + 1] = '<tr><th>' .. materials_label .. '</th><td>' .. materials_value .. '</td></tr>'
    if time_value ~= '' then
        out[#out + 1] = '<tr><th>' .. time_label .. '</th><td>' .. time_value .. '</td></tr>'
    end
    if exp_value ~= '' then
        out[#out + 1] = '<tr><th>' .. exp_label .. '</th><td>' .. exp_value .. '</td></tr>'
    end
    out[#out + 1] = '</table>'
    out[#out + 1] = '</div>'
    return table.concat(out, '\n')
end

local function is_array_table(value)
    if type(value) ~= 'table' then
        return false
    end

    local count = 0
    for key in pairs(value) do
        if type(key) ~= 'number' then
            return false
        end
        count = count + 1
    end

    if count == 0 then
        return false
    end

    for index = 1, count do
        if value[index] == nil then
            return false
        end
    end

    return true
end

local function normalize_material_options(materials)
    if type(materials) ~= 'table' then
        return {}
    end

    if is_array_table(materials) then
        local options = {}
        for _, option in ipairs(materials) do
            if type(option) == 'table' and not is_empty(option) then
                options[#options + 1] = option
            end
        end
        return options
    end

    if is_empty(materials) then
        return {}
    end

    return { materials }
end

local function render_material_option(frame, materials)
    if type(materials) ~= 'table' or is_empty(materials) then
        return ''
    end

    local item_ids = {}
    for item_id in pairs(materials) do
        item_ids[#item_ids + 1] = item_id
    end
    item_common.sortItemKeys(item_ids)

    local parts = {}
    for _, item_id in ipairs(item_ids) do
        parts[#parts + 1] = item.renderItemWithArgs(frame, { item_id, tostring(materials[item_id]), class = 'block' })
    end
    return table.concat(parts, '')
end

local function filter_material_option(materials, excluded_item_id)
    if type(materials) ~= 'table' then
        return {}
    end

    local filtered = {}
    for item_id, count in pairs(materials) do
        if item_id ~= excluded_item_id then
            filtered[item_id] = count
        end
    end
    return filtered
end

local function render_material_option_or_empty(frame, materials)
    local rendered = render_material_option(frame, materials)
    if rendered == '' then
        return '—'
    end
    return rendered
end

local function render_recipe_materials(frame, materials, excluded_item_id)
    local options = normalize_material_options(materials)
    if #options == 0 then
        return ''
    end

    if excluded_item_id and excluded_item_id ~= '' then
        local filtered_options = {}
        local has_non_empty_option = false
        for _, option in ipairs(options) do
            local filtered = filter_material_option(option, excluded_item_id)
            if not is_empty(filtered) then
                has_non_empty_option = true
            end
            filtered_options[#filtered_options + 1] = filtered
        end
        if not has_non_empty_option then
            return '—'
        end
        options = filtered_options
    end

    if #options == 1 then
        return render_material_option_or_empty(frame, options[1])
    end

    local out = {}
    out[#out + 1] = '<div class="recipe-usage-option-label">可选材料方案</div>'
    out[#out + 1] = '<ul class="recipe-usage-option-list">'
    for _, option in ipairs(options) do
        out[#out + 1] = '<li>' .. render_material_option_or_empty(frame, option) .. '</li>'
    end
    out[#out + 1] = '</ul>'
    return table.concat(out, '')
end

local function material_counts_for_item(materials, item_id)
    local counts = {}
    local seen = {}
    for _, option in ipairs(normalize_material_options(materials)) do
        local count = tonumber(option[item_id]) or 0
        if count > 0 and not seen[count] then
            seen[count] = true
            counts[#counts + 1] = count
        end
    end
    table.sort(counts)
    return counts
end

local function render_material_count_list(counts)
    if #counts == 0 then
        return ''
    end
    if #counts == 1 then
        return tostring(counts[1])
    end

    local out = { '<ul class="recipe-usage-count-list">' }
    for _, count in ipairs(counts) do
        out[#out + 1] = '<li>' .. tostring(count) .. '</li>'
    end
    out[#out + 1] = '</ul>'
    return table.concat(out, '')
end

local function load_recipes()
    if recipe_cache then
        return
    end

    local data = common.loadJsonData('数据:Machine/machine_recipes.json') or {}
    recipe_list = data.recipes or {}
    recipe_cache = data.by_machine or {}
    item_recipe_cache = data.by_item or {}
    field_map = (data._meta or {}).field_map or RECIPE_FIELD_DEFAULTS
end

local function recipe_time_text(recipe)
    if not recipe then
        return ''
    end

    local value = tonumber(recipe[field_map.time]) or 0
    if value <= 0 then
        return ''
    end
    return format_time(value)
end

local function render_recipe_result(frame, recipe)
    if not recipe then
        return ''
    end

    local result_id = recipe[field_map.result]
    if not result_id or result_id == '' then
        return ''
    end

    local result_count = tonumber(recipe[field_map.result_count]) or 1
    if result_count > 1 then
        return item.renderItemWithArgs(frame, { result_id, tostring(result_count), class = 'block' })
    end
    return item.renderItemWithArgs(frame, { result_id, class = 'block' })
end

local function machine_group_heading(frame, machine_id)
    local rendered = render_machine_reference(machine_id)
    if rendered == '' then
        return ''
    end
    return '<div class="recipe-usage-machine"><div class="recipe-usage-machine-label">' .. rendered .. '</div></div>'
end

local function compare_recipe_ids(left_rid, right_rid)
    local left_recipe = recipe_list[left_rid + 1]
    local right_recipe = recipe_list[right_rid + 1]
    if not left_recipe or not right_recipe then
        return (tonumber(left_rid) or 0) < (tonumber(right_rid) or 0)
    end

    local left_result_id = common.trim(left_recipe[field_map.result] or '')
    local right_result_id = common.trim(right_recipe[field_map.result] or '')
    if left_result_id ~= right_result_id then
        return item_common.compareItemKeys(left_result_id, right_result_id)
    end

    local left_machine_id = common.trim(left_recipe[field_map.machine] or '')
    local right_machine_id = common.trim(right_recipe[field_map.machine] or '')
    if left_machine_id ~= right_machine_id then
        return item_common.compareItemKeys(left_machine_id, right_machine_id)
    end

    local left_time = tonumber(left_recipe[field_map.time]) or 0
    local right_time = tonumber(right_recipe[field_map.time]) or 0
    if left_time ~= right_time then
        return left_time < right_time
    end

    return left_rid < right_rid
end

local function sort_recipe_ids(recipe_ids)
    if type(recipe_ids) ~= 'table' or #recipe_ids <= 1 then
        return recipe_ids
    end

    table.sort(recipe_ids, compare_recipe_ids)
    return recipe_ids
end

local function split_recipe_groups_by_machine(recipe_ids)
    local ordered_groups = {}
    local groups_by_machine = {}

    for _, rid in ipairs(recipe_ids) do
        local recipe = recipe_list[rid + 1]
        if recipe then
            local machine_id = common.trim(recipe[field_map.machine] or '')
            local group = groups_by_machine[machine_id]
            if not group then
                group = {
                    machine_id = machine_id,
                    recipe_ids = {},
                }
                groups_by_machine[machine_id] = group
                ordered_groups[#ordered_groups + 1] = group
            end
            group.recipe_ids[#group.recipe_ids + 1] = rid
        end
    end

    local featured_groups = {}
    local remaining_ids = {}
    for _, group in ipairs(ordered_groups) do
        sort_recipe_ids(group.recipe_ids)
        if #group.recipe_ids >= 3 then
            featured_groups[#featured_groups + 1] = group
        else
            for _, rid in ipairs(group.recipe_ids) do
                remaining_ids[#remaining_ids + 1] = rid
            end
        end
    end

    table.sort(featured_groups, function(left_group, right_group)
        return item_common.compareItemKeys(left_group.machine_id, right_group.machine_id)
    end)
    sort_recipe_ids(remaining_ids)

    return featured_groups, remaining_ids
end

function p.getField(frame)
    local key = common.getArg(frame, 1, '')
    local field = common.getArg(frame, 2, '')
    if field == '' then
        return ''
    end

    local record = find_record(key)
    if not record then
        return ''
    end

    local craft = get_record_field(record, key, 'craft')
    if field == 'craft_machine' and craft then
        local machine_id = get_craft_field(craft, 'm', 'machine')
        if machine_id and machine_id ~= '' then
            return render_item_template(frame, machine_id)
        end
        return ''
    end
    if field == 'craft_time' and craft then
        return format_time(get_craft_field(craft, 't', 'time'))
    end
    if field == 'craft_exp' and craft then
        return common.toText(get_craft_field(craft, 'e', 'exp'))
    end

    local upgrade = get_record_field(record, key, 'upgrade')
    if field == 'upgrade_from' and upgrade then
        local from_id = get_craft_field(upgrade, 'f', 'from_machine')
        if from_id and from_id ~= '' then
            local from_name = item.getNameByKey(from_id)
            if from_name and from_name ~= '' then
                return '[[' .. from_name .. ']]'
            end
        end
        return ''
    end
    if field == 'upgrade_time' and upgrade then
        return format_time(get_craft_field(upgrade, 't', 'time'))
    end
    if field == 'upgrade_exp' and upgrade then
        return common.toText(get_craft_field(upgrade, 'e', 'exp'))
    end
    if field == 'construction_template_count' then
        local value = tonumber(get_record_field(record, key, field)) or 0
        if value <= 0 then
            return ''
        end
        return tostring(value)
    end
    if field == 'energy_capacity' then
        local value = tonumber(get_record_field(record, key, field)) or 0
        if value <= 0 then
            return ''
        end
        return tostring(value)
    end
    if field == 'prev_machine' then
        local prev_id = get_record_field(record, key, 'prev_machine')
        if prev_id and prev_id ~= '' then
            return render_item_template(frame, prev_id)
        end
        return ''
    end
    if field == 'next_machine' then
        local next_id = get_record_field(record, key, 'next_machine')
        if next_id and next_id ~= '' then
            return render_item_template(frame, next_id)
        end
        return ''
    end

    return common.toText(get_record_field(record, key, field))
end

function p.ingredientList(frame)
    local key = common.getArg(frame, 1, '')
    local record = find_record(key)
    if not record then
        return ''
    end

    local craft = get_record_field(record, key, 'craft')
    if not craft then
        return ''
    end
    return render_materials(frame, get_craft_field(craft, 'mat', 'materials'))
end

function p.upgradeIngredientList(frame)
    local key = common.getArg(frame, 1, '')
    local record = find_record(key)
    if not record then
        return ''
    end

    local upgrade = get_record_field(record, key, 'upgrade')
    if not upgrade then
        return ''
    end
    return render_materials(frame, get_craft_field(upgrade, 'mat', 'materials'))
end

function p.craftInfo(frame)
    local key = common.getArg(frame, 1, '')
    local record = find_record(key)
    if not record then
        return ''
    end

    local out = {}

    local craft = get_record_field(record, key, 'craft')
    local craft_materials = get_craft_field(craft, 'mat', 'materials')
    if craft and not is_empty(craft_materials) then
        local machine_id = get_craft_field(craft, 'm', 'machine')
        local machine_display = '—'
        if machine_id and common.trim(machine_id) ~= '' then
            local machine_name = item.getNameByKey(machine_id)
            if machine_name and machine_name ~= '' then
                machine_display = '[[' .. machine_name .. ']]'
            end
        end
        out[#out + 1] = render_craft_table(
            '制造配方',
            '制造站',
            machine_display,
            '材料',
            render_materials(frame, craft_materials),
            '时间',
            format_time(get_craft_field(craft, 't', 'time')),
            '经验',
            common.toText(get_craft_field(craft, 'e', 'exp'))
        )
    end

    local upgrade = get_record_field(record, key, 'upgrade')
    local upgrade_materials = get_craft_field(upgrade, 'mat', 'materials')
    if upgrade and not is_empty(upgrade_materials) then
        local from_id = get_craft_field(upgrade, 'f', 'from_machine')
        local from_display = '—'
        if from_id and common.trim(from_id) ~= '' then
            local from_name = item.getNameByKey(from_id)
            if from_name and from_name ~= '' then
                from_display = '[[' .. from_name .. ']]'
            end
        end
        out[#out + 1] = render_craft_table(
            '升级配方',
            '升级自',
            from_display,
            '材料',
            render_materials(frame, upgrade_materials),
            '时间',
            format_time(get_craft_field(upgrade, 't', 'time')),
            '经验',
            common.toText(get_craft_field(upgrade, 'e', 'exp'))
        )
    end

    local css_out = (css.quickCall('Item') or '') .. (css.quickCall('RecipeUsage') or '')
    return css_out .. table.concat(out, '\n')
end

function p.recipeList(frame)
    local key = common.getArg(frame, 1, '')
    local record = find_record(key)
    if not record then
        return ''
    end

    local machine_id = get_record_field(record, key, 'id')
    if not machine_id or machine_id == '' then
        return ''
    end

    load_recipes()
    local recipe_ids = recipe_cache[mw.ustring.lower(machine_id)]
    if not recipe_ids then
        return '该机器暂无独有配方。'
    end

    local has_time = false
    for _, rid in ipairs(recipe_ids) do
        local recipe = recipe_list[rid + 1]
        if recipe and recipe_time_text(recipe) ~= '' then
            has_time = true
            break
        end
    end

    local out = {}
    out[#out + 1] = '<div class="recipe-usage-table-wrap">'
    out[#out + 1] = '<table class="wikitable recipe-usage-table">'
    out[#out + 1] = '<tr><th>产物</th><th>材料</th>'
    if has_time then
        out[#out + 1] = '<th>时间</th>'
    end
    out[#out + 1] = '</tr>'

    local ordered_recipe_ids = {}
    for _, rid in ipairs(recipe_ids) do
        ordered_recipe_ids[#ordered_recipe_ids + 1] = rid
    end
    sort_recipe_ids(ordered_recipe_ids)

    for _, rid in ipairs(ordered_recipe_ids) do
        local recipe = recipe_list[rid + 1]
        if recipe then
            out[#out + 1] = '<tr>'
            out[#out + 1] = '<td>' .. render_recipe_result(frame, recipe) .. '</td>'
            out[#out + 1] = '<td>' .. render_recipe_materials(frame, recipe[field_map.materials]) .. '</td>'
            if has_time then
                out[#out + 1] = '<td>' .. recipe_time_text(recipe) .. '</td>'
            end
            out[#out + 1] = '</tr>'
        end
    end

    out[#out + 1] = '</table>'
    out[#out + 1] = '</div>'
    local css_out = (css.quickCall('Item') or '') .. (css.quickCall('RecipeUsage') or '')
    return css_out .. table.concat(out, '\n')
end

local function recipe_ids_have_time(recipe_ids)
    for _, rid in ipairs(recipe_ids) do
        local recipe = recipe_list[rid + 1]
        if recipe and recipe_time_text(recipe) ~= '' then
            return true
        end
    end
    return false
end

local function render_product_recipe_table(frame, recipe_ids, include_machine_column)
    local ordered_recipe_ids = {}
    for _, rid in ipairs(recipe_ids) do
        ordered_recipe_ids[#ordered_recipe_ids + 1] = rid
    end
    sort_recipe_ids(ordered_recipe_ids)

    local has_time = recipe_ids_have_time(ordered_recipe_ids)
    local out = {}
    out[#out + 1] = '<div class="recipe-usage-table-wrap">'
    out[#out + 1] = '<table class="wikitable recipe-usage-table">'
    out[#out + 1] = '<tr><th>产出</th>'
    if include_machine_column then
        out[#out + 1] = '<th>机器</th>'
    end
    out[#out + 1] = '<th>所需材料</th>'
    if has_time then
        out[#out + 1] = '<th>时间</th>'
    end
    out[#out + 1] = '</tr>'

    for _, rid in ipairs(ordered_recipe_ids) do
        local recipe = recipe_list[rid + 1]
        if recipe then
            out[#out + 1] = '<tr>'
            out[#out + 1] = '<td>' .. render_recipe_result(frame, recipe) .. '</td>'
            if include_machine_column then
                out[#out + 1] = '<td>' .. render_machine_reference(recipe[field_map.machine]) .. '</td>'
            end
            out[#out + 1] = '<td>' .. render_recipe_materials(frame, recipe[field_map.materials]) .. '</td>'
            if has_time then
                out[#out + 1] = '<td>' .. recipe_time_text(recipe) .. '</td>'
            end
            out[#out + 1] = '</tr>'
        end
    end

    out[#out + 1] = '</table>'
    out[#out + 1] = '</div>'
    return table.concat(out, '')
end

local function render_ingredient_recipe_table(frame, recipe_ids, current_item_id, include_machine_column)
    local ordered_recipe_ids = {}
    for _, rid in ipairs(recipe_ids) do
        ordered_recipe_ids[#ordered_recipe_ids + 1] = rid
    end
    sort_recipe_ids(ordered_recipe_ids)

    local out = {}
    out[#out + 1] = '<div class="recipe-usage-table-wrap">'
    out[#out + 1] = '<table class="wikitable recipe-usage-table">'
    out[#out + 1] = '<tr><th>产物</th>'
    if include_machine_column then
        out[#out + 1] = '<th>机器</th>'
    end
    out[#out + 1] = '<th>所需材料</th></tr>'

    for _, rid in ipairs(ordered_recipe_ids) do
        local recipe = recipe_list[rid + 1]
        if recipe then
            local counts = material_counts_for_item(recipe[field_map.materials], current_item_id)
            if #counts > 0 then
                out[#out + 1] = '<tr>'
                out[#out + 1] = '<td>' .. render_recipe_result(frame, recipe) .. '</td>'
                if include_machine_column then
                    out[#out + 1] = '<td>' .. render_machine_reference(recipe[field_map.machine]) .. '</td>'
                end
                out[#out + 1] = '<td>' .. render_recipe_materials(frame, recipe[field_map.materials]) .. '</td>'
                out[#out + 1] = '</tr>'
            end
        end
    end

    out[#out + 1] = '</table>'
    out[#out + 1] = '</div>'
    return table.concat(out, '')
end

local function append_recipe_usage_sections(out, frame, recipe_ids, current_item_id, section_title, render_table)
    local featured_groups, remaining_ids = split_recipe_groups_by_machine(recipe_ids)

    out[#out + 1] = '<div class="recipe-usage-section">'
    out[#out + 1] = '<h3 class="recipe-usage-section-title">' .. section_title .. '</h3>'
    for _, group in ipairs(featured_groups) do
        local heading = machine_group_heading(frame, group.machine_id)
        if heading ~= '' then
            out[#out + 1] = heading
        end
        out[#out + 1] = render_table(frame, group.recipe_ids, current_item_id, false)
    end

    if #remaining_ids > 0 then
        if #featured_groups > 0 then
            out[#out + 1] = '<h4 class="recipe-usage-subtitle">其他制作站</h4>'
        end
        out[#out + 1] = render_table(frame, remaining_ids, current_item_id, true)
    end
    out[#out + 1] = '</div>'
end

function p.itemRecipes(frame)
    local item_key = common.getArg(frame, 1, '')
    if item_key == '' then
        item_key = common.getCurrentTitleText()
    end

    local item_id = item.getIdByKey(item_key)
    if not item_id or item_id == '' then
        return ''
    end

    load_recipes()
    local item_data = item_recipe_cache[mw.ustring.lower(item_id)]
    if not item_data then
        return ''
    end

    local product_ids = item_data.p or {}
    local ingredient_ids = item_data.i or {}
    local product_count = count_array(product_ids)
    local ingredient_count = count_array(ingredient_ids)
    if product_count == 0 and ingredient_count == 0 then
        return ''
    end

    local out = {}
    if product_count > 0 then
        append_recipe_usage_sections(out, frame, product_ids, item_id, '制作配方', function(current_frame, recipe_ids_for_table, _, include_machine_column)
            return render_product_recipe_table(current_frame, recipe_ids_for_table, include_machine_column)
        end)
    end

    if ingredient_count > 0 then
        if #out > 0 then
            out[#out + 1] = ''
        end
        append_recipe_usage_sections(out, frame, ingredient_ids, item_id, '用于制作', render_ingredient_recipe_table)
    end

    local css_out = (css.quickCall('Item') or '') .. (css.quickCall('RecipeUsage') or '')
    return css_out .. '<div class="recipe-usage">' .. table.concat(out, '\n') .. '</div>'
end

function p.machineAsIngredient(frame)
    local key = common.getArg(frame, 1, '')
    local record = find_record(key)
    if not record then
        return ''
    end

    local machine_id = get_record_field(record, key, 'id')
    if not machine_id or machine_id == '' then
        return ''
    end

    load_recipes()
    local item_data = item_recipe_cache[mw.ustring.lower(machine_id)]
    if not item_data or not item_data.i or count_array(item_data.i) == 0 then
        return ''
    end

    local ordered_recipe_ids = {}
    for _, rid in ipairs(item_data.i) do
        ordered_recipe_ids[#ordered_recipe_ids + 1] = rid
    end
    sort_recipe_ids(ordered_recipe_ids)

    local out = {}
    out[#out + 1] = '<div class="recipe-usage-table-wrap">'
    out[#out + 1] = '<table class="wikitable recipe-usage-table">'
    out[#out + 1] = '<tr><th>产物</th><th>所需数量</th></tr>'

    for _, rid in ipairs(ordered_recipe_ids) do
        local recipe = recipe_list[rid + 1]
        if recipe then
            local counts = material_counts_for_item(recipe[field_map.materials], machine_id)
            if #counts > 0 then
                out[#out + 1] = '<tr>'
                out[#out + 1] = '<td>' .. render_recipe_result(frame, recipe) .. '</td>'
                out[#out + 1] = '<td>' .. render_material_count_list(counts) .. '</td>'
                out[#out + 1] = '</tr>'
            end
        end
    end

    out[#out + 1] = '</table>'
    out[#out + 1] = '</div>'
    local css_out = (css.quickCall('Item') or '') .. (css.quickCall('RecipeUsage') or '')
    return css_out .. table.concat(out, '\n')
end

return p