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

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

模块:ItemCommon

来自星砂岛百科

概述

ItemCommon 提供物品域共享的查找、身份回退与配方域构建能力,供各子域模块复用。

常用函数

  • loadItemIdentityData:读取 数据:Item/item_name_index.json数据:Item/item_mapping.json
  • findItemRecord:按中文名、英文名或 ID 查找物品身份记录。
  • resolveItemId:返回统一物品 ID。
  • resolveItemName:返回统一中文名。
  • resolveItemNameEn:返回统一英文名。
  • sortItemKeys:按物品类型、系列、等级/稀有度对物品键进行稳定排序。
  • sortRecordsByItemKey:按记录中的物品键复用同一套排序规则,适合商店、配方等列表域。
  • buildRecipeDomain:为子域模块挂接 getField / processRecipeList / productionRecipeList / machineList
  • getField:会统一处理部分显示映射与“0 视为空值”规则,例如宠物类型、摆放类型、可食用布尔值、发射类型等。

排序规则

  • 先按物品类型聚合,再按系列分组。
  • 同一系列内优先按等级或稀有度从低到高排列。
  • 如果基础款没有 Lv1 后缀,会按同系列最低档处理,便于把 简易制造台 / 标准制造台 / 精良制造台 这类条目排在一起。

数据来源


local common = require('Module:Common')

local p = {}

local item_name_cache
local item_mapping_cache

local ITEM_TYPE_FAMILY_RANKS = {
    Currency = 10,
    Seed = 20,
    Plant = 30,
    Animal = 40,
    Collect = 50,
    Mine = 60,
    Craft = 70,
    Food = 80,
    DailyUse = 90,
    Consumable = 95,
    Tool = 100,
    Electric = 105,
    Electronics = 106,
    Furniture = 110,
    Decoration = 111,
    Light = 112,
    Book = 120,
    Clothing = 130,
    Music = 140,
    Mics = 141,
    Emoji = 142,
    Vehicle = 150,
    Mission = 160,
}

local ITEM_TYPE_SORT_RULES = {
    { prefix = 'ItemType.Craft_ProcessedProduct', rank = 70 },
    { prefix = 'ItemType.Craft_Loom', rank = 71 },
    { prefix = 'ItemType.Craft_FiberIngot', rank = 72 },
    { prefix = 'ItemType.Craft_SemiFinished', rank = 73 },
    { prefix = 'ItemType.Craft_ProductMod', rank = 74 },
    { prefix = 'ItemType.Craft_Separate_Show', rank = 76 },
    { prefix = 'ItemType.Craft_Separate', rank = 75 },
    { prefix = 'ItemType.Craft_Jam', rank = 77 },
    { prefix = 'ItemType.Food_Animal', rank = 78 },
    { prefix = 'ItemType.Food_Seasoning', rank = 79 },
    { prefix = 'ItemType.Craft_HandCraft', rank = 98 },
    { prefix = 'ItemType.Craft_Workbench', rank = 99 },
    { prefix = 'ItemType.Craft_ManufactureFacility', rank = 100 },
    { prefix = 'ItemType.Craft_FunctionFacility', rank = 101 },
    { prefix = 'ItemType.Craft_CollectFacility', rank = 102 },
    { prefix = 'ItemType.Craft_BreedingFacility', rank = 103 },
    { prefix = 'ItemType.Craft_CookingFacility', rank = 104 },
    { prefix = 'ItemType.Craft_ConversionFacility', rank = 105 },
    { prefix = 'ItemType.Craft_SewingTable', rank = 106 },
}

local RARITY_SORT_RANKS = {
    ['1'] = 1,
    ['2'] = 2,
    ['3'] = 3,
    ['4'] = 4,
    ['5'] = 5,
    ['6'] = 6,
    common = 1,
    normal = 1,
    uncommon = 2,
    rare = 3,
    epic = 4,
    legendary = 5,
    myth = 6,
    mythic = 6,
    ['普通'] = 1,
    ['优秀'] = 2,
    ['精良'] = 3,
    ['稀有'] = 4,
    ['史诗'] = 5,
    ['传说'] = 6,
}

local PLACEMENT_TEMPLATE_DISPLAY = {
    ['ItemPlacement.Dish_01'] = '菜肴',
    ['ItemPlacement.Drink_01'] = '饮品',
    ['ItemPlacement.Flower_01'] = '花卉',
    ['ItemPlacement.Ingredients_01'] = '食材',
    ['ItemPlacement.Snack_01'] = '零食',
}

local PET_TARGET_TAG_DISPLAY = {
    cat = '猫',
    dog = '狗',
    EatMeat = '肉食宠物',
    EatPlant = '草食宠物',
    pet = '通用宠物',
}

local HUNTING_WEAPON_TYPE_DISPLAY = {
    Arrow = '弓箭',
    Crossbow = '弩',
    FarmGun = '种植枪',
    Slingshot = '弹弓',
}

local SEED_VARIANT_DISPLAY = {
    Eternity = '不朽',
}

local BOOLEAN_DISPLAY = {
    ['0'] = '',
    ['1'] = '是',
    ['false'] = '',
    ['true'] = '是',
    ['no'] = '',
    ['yes'] = '是',
}

local ZERO_EMPTY_FIELDS = {
    stamina_restore = true,
    health_restore = true,
    extra_health_restore = true,
    pet_affection = true,
    water_capacity = true,
    target_distance = true,
    fishing_capture_velocity = true,
    fishing_line_threshold = true,
}

local function default_mapping()
    return {
        name_to_id = {},
        id_to_name = {},
        aliases = {},
        overrides = {
            name_to_id = {},
            aliases = {},
        },
    }
end

local function find_mapped_record(data, mapping, normalized)
    if type(mapping) ~= 'table' then
        return nil
    end

    local override_id = mapping.overrides and mapping.overrides.name_to_id and mapping.overrides.name_to_id[normalized]
    if override_id and data[p.normalizeKey(override_id)] then
        return data[p.normalizeKey(override_id)]
    end

    local override_alias = mapping.overrides and mapping.overrides.aliases and mapping.overrides.aliases[normalized]
    if override_alias and data[p.normalizeKey(override_alias)] then
        return data[p.normalizeKey(override_alias)]
    end

    local mapped_id = mapping.name_to_id and mapping.name_to_id[normalized]
    if mapped_id and data[p.normalizeKey(mapped_id)] then
        return data[p.normalizeKey(mapped_id)]
    end

    local alias_id = mapping.aliases and mapping.aliases[normalized]
    if alias_id and data[p.normalizeKey(alias_id)] then
        return data[p.normalizeKey(alias_id)]
    end

    return nil
end

function p.normalizeKey(value)
    return common.normalizeKey(value)
end

function p.loadDomainData(data_page, mapping_page)
    local data = common.loadJsonData(data_page) or {}
    local mapping = nil
    if common.trim(mapping_page or '') ~= '' then
        mapping = common.loadJsonData(mapping_page) or default_mapping()
    end
    return data, mapping
end

function p.loadItemIdentityData()
    if item_name_cache then
        return item_name_cache, item_mapping_cache
    end

    item_name_cache = common.loadJsonData('数据:Item/item_name_index.json') or {}
    item_mapping_cache = common.loadJsonData('数据:Item/item_mapping.json') or default_mapping()
    return item_name_cache, item_mapping_cache
end

function p.findRecord(data, mapping, key)
    local resolved = common.trim(key)
    if resolved == '' then
        resolved = common.getCurrentTitleText()
    end

    local normalized = p.normalizeKey(resolved)
    if data[normalized] then
        return data[normalized]
    end

    local by_domain_mapping = find_mapped_record(data, mapping, normalized)
    if by_domain_mapping then
        return by_domain_mapping
    end

    local _, item_mapping = p.loadItemIdentityData()
    local by_item_mapping = find_mapped_record(data, item_mapping, normalized)
    if by_item_mapping then
        return by_item_mapping
    end

    return nil
end

function p.findItemRecord(key)
    local item_data, item_mapping = p.loadItemIdentityData()
    return p.findRecord(item_data, item_mapping, key)
end

function p.getItemField(key, field)
    local record = p.findItemRecord(key)
    if not record then
        return ''
    end
    return common.toText(record[field])
end

function p.getIdentityField(record, key, field)
    if type(record) == 'table' and record[field] ~= nil and common.trim(record[field]) ~= '' then
        return record[field]
    end

    local item_record = p.findItemRecord(key)
    if type(item_record) == 'table' then
        return item_record[field] or ''
    end

    if field == 'id' then
        return common.trim(key)
    end
    return ''
end

function p.resolveItemId(record, key)
    return common.trim(p.getIdentityField(record, key, 'id'))
end

function p.resolveItemName(record, key)
    local value = common.trim(p.getIdentityField(record, key, 'name'))
    if value ~= '' then
        return value
    end
    return common.trim(key)
end

function p.resolveItemNameEn(record, key)
    return common.trim(p.getIdentityField(record, key, 'name_en'))
end

function p.getField(record, field)
    if type(record) ~= 'table' then
        return ''
    end
    return record[field]
end

local function text_sort_value(value)
    return mw.ustring.lower(common.trim(common.toText(value)))
end

local function map_display_value(value, mapping)
    local resolved = common.trim(common.toText(value))
    if resolved == '' then
        return ''
    end
    return mapping[resolved] or resolved
end

local function normalize_optional_number(value)
    local text = common.trim(common.toText(value))
    if text == '' then
        return ''
    end

    local number = tonumber(text)
    if not number or number == 0 then
        return ''
    end

    return text
end

local function resolve_related_item_name(item_name, item_id)
    local resolved_name = common.trim(common.toText(item_name))
    if resolved_name ~= '' then
        return resolved_name
    end

    local resolved_id = common.trim(common.toText(item_id))
    if resolved_id == '' then
        return ''
    end

    return common.trim(p.getItemField(resolved_id, 'name'))
end

local function make_wikilink(title)
    local resolved = common.trim(title)
    if resolved == '' then
        return ''
    end
    return '[[' .. resolved .. ']]'
end

local function format_related_item_link(item_name, item_id)
    local resolved_name = resolve_related_item_name(item_name, item_id)
    if resolved_name == '' then
        return ''
    end
    return make_wikilink(resolved_name)
end

function p.getDisplayField(record, field)
    if type(record) ~= 'table' then
        return ''
    end

    if field == 'production_machines_display' then
        return p.machineLinks(record.production_machines)
    end
    if field == 'process_machines_display' then
        return p.machineLinks(record.process_machines)
    end
    if field == 'crop_item_link' then
        return format_related_item_link(record.crop_item_name, record.crop_item_id)
    end
    if field == 'seed_item_link' then
        return format_related_item_link(record.seed_item_name, record.seed_item_id)
    end
    if field == 'placement_template' then
        return map_display_value(record.placement_template, PLACEMENT_TEMPLATE_DISPLAY)
    end
    if field == 'pet_target_tag' then
        return map_display_value(record.pet_target_tag, PET_TARGET_TAG_DISPLAY)
    end
    if field == 'hunting_weapon_type' then
        return map_display_value(record.hunting_weapon_type, HUNTING_WEAPON_TYPE_DISPLAY)
    end
    if field == 'seed_variant' then
        return map_display_value(record.seed_variant, SEED_VARIANT_DISPLAY)
    end
    if field == 'is_edible' then
        return map_display_value(record.is_edible, BOOLEAN_DISPLAY)
    end
    if ZERO_EMPTY_FIELDS[field] then
        return normalize_optional_number(record[field])
    end

    return common.toText(p.getField(record, field))
end

local function extract_item_type_family(item_type)
    local resolved_type = common.trim(item_type)
    if resolved_type == '' then
        return ''
    end

    local suffix = resolved_type:match('^ItemType%.(.+)$') or resolved_type
    local family = suffix:match('^[^_%.]+')
    return family or suffix
end

local function parse_sort_number(value)
    local resolved = common.trim(common.toText(value))
    if resolved == '' then
        return 999
    end

    local numeric = tonumber(resolved)
    if numeric then
        return numeric
    end

    local mapped = RARITY_SORT_RANKS[text_sort_value(resolved)]
    if mapped then
        return mapped
    end

    return 999
end

local function extract_series_group(item_id)
    local value = text_sort_value(item_id)
    value = mw.ustring.gsub(value, '[_%-]?lv%d+', '')
    value = mw.ustring.gsub(value, '%d+', '#')
    value = mw.ustring.gsub(value, '[_%-]+$', '')
    return value
end

local function extract_series_order(item_id)
    local value = text_sort_value(item_id)
    local level = mw.ustring.match(value, 'lv(%d+)')
    if level then
        return tonumber(level) or 999
    end

    local trailing = mw.ustring.match(value, '(%d+)[^%d]*$')
    if trailing then
        return tonumber(trailing) or 999
    end

    return 0
end

function p.getItemTypeSortRank(item_type)
    local resolved_type = common.trim(item_type)
    if resolved_type == '' then
        return 999
    end

    for _, rule in ipairs(ITEM_TYPE_SORT_RULES) do
        if resolved_type:sub(1, #rule.prefix) == rule.prefix then
            return rule.rank
        end
    end

    local family = extract_item_type_family(resolved_type)
    return ITEM_TYPE_FAMILY_RANKS[family] or 999
end

function p.getItemSortMeta(record, key)
    local item_record = record
    local item_type = ''
    local type_display = ''
    local item_level = ''
    local rarity = ''
    if type(item_record) == 'table' then
        item_type = common.trim(item_record.type or '')
        type_display = common.trim(item_record.type_display or '')
        item_level = common.trim(common.toText(item_record.item_level or item_record.lv or ''))
        rarity = common.trim(common.toText(item_record.rarity or item_record.r or ''))
    else
        item_record = nil
    end

    if item_type == '' or type_display == '' or item_level == '' or rarity == '' then
        local identity_record = p.findItemRecord(key)
        if type(identity_record) == 'table' then
            if item_type == '' then
                item_type = common.trim(identity_record.type or '')
            end
            if type_display == '' then
                type_display = common.trim(identity_record.type_display or '')
            end
            if item_level == '' then
                item_level = common.trim(common.toText(identity_record.item_level or identity_record.lv or ''))
            end
            if rarity == '' then
                rarity = common.trim(common.toText(identity_record.rarity or identity_record.r or ''))
            end
            if not item_record then
                item_record = identity_record
            end
        end
    end

    local item_id = p.getIdentityField(item_record, key, 'id')

    return {
        type_rank = p.getItemTypeSortRank(item_type),
        type_family = text_sort_value(extract_item_type_family(item_type)),
        item_type = text_sort_value(item_type),
        type_display = text_sort_value(type_display),
        series_group = extract_series_group(item_id),
        item_level = parse_sort_number(item_level),
        rarity = parse_sort_number(rarity),
        series_order = extract_series_order(item_id),
        item_name = text_sort_value(p.getIdentityField(item_record, key, 'name')),
        item_name_en = text_sort_value(p.getIdentityField(item_record, key, 'name_en')),
        item_id = text_sort_value(item_id),
    }
end

function p.compareItemKeys(left_key, right_key, left_record, right_record)
    local left_meta = p.getItemSortMeta(left_record, left_key)
    local right_meta = p.getItemSortMeta(right_record, right_key)

    if left_meta.type_rank ~= right_meta.type_rank then
        return left_meta.type_rank < right_meta.type_rank
    end
    if left_meta.type_family ~= right_meta.type_family then
        return left_meta.type_family < right_meta.type_family
    end
    if left_meta.item_type ~= right_meta.item_type then
        return left_meta.item_type < right_meta.item_type
    end
    if left_meta.series_group ~= right_meta.series_group then
        return left_meta.series_group < right_meta.series_group
    end
    if left_meta.item_level ~= right_meta.item_level then
        return left_meta.item_level < right_meta.item_level
    end
    if left_meta.rarity ~= right_meta.rarity then
        return left_meta.rarity < right_meta.rarity
    end
    if left_meta.series_order ~= right_meta.series_order then
        return left_meta.series_order < right_meta.series_order
    end
    if left_meta.item_name ~= right_meta.item_name then
        return left_meta.item_name < right_meta.item_name
    end
    if left_meta.item_name_en ~= right_meta.item_name_en then
        return left_meta.item_name_en < right_meta.item_name_en
    end
    if left_meta.item_id ~= right_meta.item_id then
        return left_meta.item_id < right_meta.item_id
    end
    return false
end

function p.sortItemKeys(item_keys, record_lookup)
    if type(item_keys) ~= 'table' or #item_keys <= 1 then
        return item_keys
    end

    table.sort(item_keys, function(left_key, right_key)
        local left_record = nil
        local right_record = nil
        if type(record_lookup) == 'function' then
            left_record = record_lookup(left_key)
            right_record = record_lookup(right_key)
        end
        return p.compareItemKeys(left_key, right_key, left_record, right_record)
    end)

    return item_keys
end

function p.sortRecordsByItemKey(records, key_getter, record_getter)
    if type(records) ~= 'table' or #records <= 1 or type(key_getter) ~= 'function' then
        return records
    end

    table.sort(records, function(left_record, right_record)
        local left_key = key_getter(left_record)
        local right_key = key_getter(right_record)
        local left_item_record = nil
        local right_item_record = nil
        if type(record_getter) == 'function' then
            left_item_record = record_getter(left_record, left_key)
            right_item_record = record_getter(right_record, right_key)
        end
        return p.compareItemKeys(left_key, right_key, left_item_record, right_item_record)
    end)

    return records
end

function p.itemLink(frame, item_name, count)
    if common.trim(item_name) == '' then
        return ''
    end

    local args = { item_name }
    if count and tonumber(count) and tonumber(count) ~= 1 then
        args[2] = tostring(count)
    end

    return frame:expandTemplate{
        title = 'Item',
        args = args,
    }
end

function p.machineLinks(machine_names)
    if type(machine_names) ~= 'table' or #machine_names == 0 then
        return ''
    end

    local parts = {}
    for _, machine_name in ipairs(machine_names) do
        if common.trim(machine_name) ~= '' then
            parts[#parts + 1] = '[[' .. machine_name .. ']]'
        end
    end

    return table.concat(parts, '、')
end

function p.renderRecipeList(frame, recipes)
    if type(recipes) ~= 'table' or #recipes == 0 then
        return ''
    end

    local out = {}
    for _, recipe in ipairs(recipes) do
        if type(recipe) == 'table' then
            local row = {}
            local machine_name = common.trim(recipe.machine)
            if machine_name ~= '' then
                row[#row + 1] = ('[[%s]]'):format(machine_name)
            end

            local material_parts = {}
            if type(recipe.materials) == 'table' then
                local material_names = {}
                for material_name in pairs(recipe.materials) do
                    material_names[#material_names + 1] = material_name
                end
                p.sortItemKeys(material_names)

                for _, material_name in ipairs(material_names) do
                    local count = recipe.materials[material_name]
                    material_parts[#material_parts + 1] = p.itemLink(frame, material_name, count)
                end
            end
            if #material_parts > 0 then
                row[#row + 1] = table.concat(material_parts, '')
            end

            if #row > 0 then
                out[#out + 1] = table.concat(row, ':')
            end
        end
    end

    return table.concat(out, '<br>')
end

function p.buildRecipeDomain(module_table, data_page, mapping_page)
    local data_cache
    local mapping_cache

    local function load_data()
        if data_cache then
            return
        end
        data_cache, mapping_cache = p.loadDomainData(data_page, mapping_page)
    end

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

    module_table.findRecord = find_record

    function module_table.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

        if field == 'id' or field == 'name' or field == 'name_en' then
            return common.toText(p.getIdentityField(record, key, field))
        end

        if field == 'image' then
            local item_id = p.resolveItemId(record, key)
            if item_id == '' then
                return ''
            end
            local image_name = item_id .. '.png'
            if common.filePageExists(image_name) then
                return image_name
            end
            return ''
        end

        return p.getDisplayField(record, field)
    end

    function module_table.productionRecipeList(frame)
        local record = find_record(common.getArg(frame, 1, ''))
        if not record then
            return ''
        end
        return p.renderRecipeList(frame, record.production_recipes)
    end

    function module_table.processRecipeList(frame)
        local record = find_record(common.getArg(frame, 1, ''))
        if not record then
            return ''
        end
        return p.renderRecipeList(frame, record.process_recipes)
    end

    function module_table.machineList(frame)
        local record = find_record(common.getArg(frame, 1, ''))
        if not record then
            return ''
        end
        if common.getArg(frame, 2, 'process') == 'production' then
            return p.machineLinks(record.production_machines)
        end
        return p.machineLinks(record.process_machines)
    end
end

return p