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

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

模块:Shop

来自星砂岛百科

模板:Documentation subpage Shop 提供商店库存、物品反查与商店元数据读取能力,供 Template:SoldByTemplate:ShopInventoryTemplate:Infobox building 调用。

示例

  • {{#invoke:Shop|renderSoldBy|土豆种子}}
  • {{#invoke:Shop|renderShopInventory|肥记早餐店}}
  • {{#invoke:Shop|getShopField|Shop.BreakfastCar|open_duration}}
  • {{#invoke:Shop|getShopField|Shop.Grocery|currency_summary}}

导出函数

  • renderSoldBy:渲染物品页的商店出售反查表。
  • renderShopInventory:渲染商店库存表。
  • getShopField:获取商店元数据字段与页面级计算字段。

展示规则

  • 金币 / 星砂 / 经验会优先转成 {{Gold}} / {{Star}} / {{Exp}}
  • 商店库存页会按较宽的物品大类分组,并在每组前输出小标题。
  • 组内条目会按“大类 -> 物品类型 -> 分类语义 -> 条件层级 -> 物品系列 / 等级”稳定排序。
  • 条件层级默认是“无条件 -> 普通条件 -> 称号等级 -> 任务 / 特殊条件”。
  • 当某一组条目共享同一刷新周期时,`限购` 与 `刷新` 会自动合并成更紧凑的 `购买限制`。

字段

  • name
  • description
  • map_description
  • kind
  • open_duration
  • shop_template
  • festival_template
  • location_names
  • area_ids
  • sources
  • entries

计算字段

  • kind_display
  • source_display
  • location_display
  • area_ids_display
  • area_count
  • template_refs_display
  • entry_count
  • currency_summary
  • auto_categories

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

local p = {}

local SHOP_FIELD_MAP = {
    name = 'n',
    description = 'd',
    map_description = 'md',
    kind = 'k',
    open_duration = 'od',
    shop_template = 'st',
    festival_template = 'ft',
    location_names = 'ln',
    area_ids = 'a',
    sources = 'ss',
    entries = 'e',
}

local ENTRY_FIELD_MAP = {
    item_id = 'i',
    group = 'g',
    source = 's',
    price_value = 'pv',
    price_currency = 'pc',
    price_costs = 'ca',
    max_count = 'm',
    refresh_interval = 'r',
    condition = 'c',
    title_requirement = 'tr',
    discount = 'd',
    need_discount = 'nd',
    only_one = 'o',
    ui_sort = 'u',
}

local REVERSE_SHOP_KEY = 'sid'
local DEFAULT_CURRENCY = 'Currency.Default'
local SPECIAL_CURRENCY_TEMPLATES = {
    ['currency.default'] = 'Gold',
    ['item.gamecoin'] = 'Star',
    ['currency.star'] = 'Star',
    ['currency.exp'] = 'Exp',
    ['experience.default'] = 'Exp',
    ['exp.default'] = 'Exp',
}

local ITEM_TYPE_LABEL_FALLBACKS = {
    craft = '制作物',
    food = '食物',
    book = '书籍',
    clothing = '服饰',
    seed = '种子',
    plant = '植物',
    animal = '动物',
    collect = '采集物',
    mine = '矿石',
    tool = '工具',
    furniture = '家具',
    decoration = '装饰',
    light = '照明',
    vehicle = '载具',
    music = '唱片',
    emoji = '表情',
    consumable = '消耗品',
    dailyuse = '日用品',
    currency = '货币',
}

local ITEM_TYPE_GROUP_LABELS = {
    materials = '材料',
    plants = '种子与植物',
    food = '食品',
    books = '配方与书籍',
    clothing = '服饰',
    furniture = '家具与装饰',
    tools = '工具与设备',
    animals = '动物与农牧',
    vehicles = '载具',
    fun = '收藏与娱乐',
    currency = '货币',
    other = '其他',
}

local CATEGORY_FAMILY_RANKS = {
    ['素材'] = 10,
    ['种子'] = 20,
    ['肥料'] = 21,
    ['食谱'] = 30,
    ['配方'] = 31,
    ['图纸'] = 32,
    ['道具'] = 40,
    ['食品'] = 50,
    ['零食'] = 51,
    ['饮料'] = 52,
    ['饮品'] = 53,
    ['美食'] = 54,
    ['菜肴'] = 55,
    ['家具'] = 60,
    ['相框'] = 61,
    ['盆栽'] = 62,
    ['时装'] = 70,
    ['唱片'] = 80,
    ['杂志'] = 81,
    ['载具'] = 90,
}

local shop_data_cache
local shop_mapping_cache
local by_item_cache
local get_entry_field
local get_entry_item_id
local get_entry_item_record

local function normalize_key(value)
    return common.normalizeKey(value)
end

local function get_current_page_name()
    local title = mw.title.getCurrentTitle()
    if not title then
        return ''
    end
    return common.trim(title.text or '')
end

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

local function sanitize_display_text(value)
    local text = common.trim(common.toText(value))
    if text == '' then
        return ''
    end
    text = mw.ustring.gsub(text, '<[^>]+>', '')
    return common.trim(text)
end

local function copy_array(values)
    local out = {}
    if type(values) ~= 'table' then
        return out
    end
    for _, value in ipairs(values) do
        out[#out + 1] = value
    end
    return out
end

local function parse_optional_number(value, default_value)
    local numeric = tonumber(value)
    if numeric == nil then
        return default_value
    end
    return numeric
end

local function starts_with(value, prefix)
    return value:sub(1, #prefix) == prefix
end

local function any_prefix(value, prefixes)
    for _, prefix in ipairs(prefixes) do
        if starts_with(value, prefix) then
            return true
        end
    end
    return false
end

local function any_contains(value, patterns)
    for _, pattern in ipairs(patterns) do
        if mw.ustring.find(value, pattern, 1, true) then
            return true
        end
    end
    return false
end

local function get_semantic_level_rank(value)
    local text = sanitize_display_text(value)
    if text == '' then
        return 999, ''
    end
    if mw.ustring.find(text, '初始', 1, true) then
        return 0, text
    end
    if mw.ustring.find(text, '见习', 1, true) then
        return 10, text
    end
    if mw.ustring.find(text, '初级', 1, true) then
        return 20, text
    end
    if mw.ustring.find(text, '中级', 1, true) then
        return 30, text
    end
    if mw.ustring.find(text, '高级', 1, true) then
        return 40, text
    end
    if mw.ustring.find(text, '专家', 1, true) then
        return 50, text
    end
    if mw.ustring.find(text, '资深', 1, true) then
        return 60, text
    end

    local added = mw.ustring.match(text, '新增(%d+)')
    if added then
        return 70 + (tonumber(added) or 0), text
    end

    local numeric_suffix = mw.ustring.match(text, '(%d+)$')
    if numeric_suffix then
        return 200 + (tonumber(numeric_suffix) or 0), text
    end

    return 999, text
end

local function detect_category_family(value)
    local text = sanitize_display_text(value)
    if text == '' then
        return '', ''
    end

    local family, suffix = mw.ustring.match(text, '^(.-)%-(.+)$')
    if family and family ~= '' then
        return family, suffix
    end

    for candidate, _ in pairs(CATEGORY_FAMILY_RANKS) do
        if mw.ustring.find(text, candidate, 1, true) then
            return candidate, text
        end
    end

    return '', text
end

local function get_category_sort_meta(value)
    local label = sanitize_display_text(value)
    if label == '' then
        return {
            family_rank = 0,
            family = '',
            level_rank = 0,
            suffix = '',
            label = '',
        }
    end

    local family, suffix = detect_category_family(label)
    local level_rank, normalized_suffix = get_semantic_level_rank(suffix)
    return {
        family_rank = CATEGORY_FAMILY_RANKS[family] or 999,
        family = text_sort_value(family),
        level_rank = level_rank,
        suffix = text_sort_value(normalized_suffix),
        label = text_sort_value(label),
    }
end

local function compare_category_meta(left_meta, right_meta)
    if left_meta.family_rank ~= right_meta.family_rank then
        return left_meta.family_rank < right_meta.family_rank
    end
    if left_meta.family ~= right_meta.family then
        return left_meta.family < right_meta.family
    end
    if left_meta.level_rank ~= right_meta.level_rank then
        return left_meta.level_rank < right_meta.level_rank
    end
    if left_meta.suffix ~= right_meta.suffix then
        return left_meta.suffix < right_meta.suffix
    end
    if left_meta.label ~= right_meta.label then
        return left_meta.label < right_meta.label
    end
    return false
end

local function is_special_condition_text(value)
    local text = sanitize_display_text(value)
    if text == '' then
        return false
    end
    return mw.ustring.find(text, '任务', 1, true)
        or mw.ustring.find(text, 'Mission.', 1, true)
        or mw.ustring.find(text, '存档', 1, true)
        or mw.ustring.find(text, 'FreeShop', 1, true)
        or mw.ustring.find(text, 'NotSell', 1, true)
end

local function is_special_title_requirement(value)
    local text = sanitize_display_text(value)
    if text == '' then
        return false
    end
    return text == '小岛之光' or text == '星砂传奇'
end

local function get_condition_sort_meta(entry)
    local title_requirement = sanitize_display_text(get_entry_field(entry, 'title_requirement') or '')
    local condition = sanitize_display_text(get_entry_field(entry, 'condition') or '')
    local discount = tonumber(get_entry_field(entry, 'discount')) or 0
    local need_discount = get_entry_field(entry, 'need_discount')

    local parts = {}
    if title_requirement ~= '' then
        parts[#parts + 1] = title_requirement
    end
    if condition ~= '' then
        parts[#parts + 1] = condition
    end
    if discount ~= 0 then
        parts[#parts + 1] = '折扣值 ' .. tostring(math.floor(discount))
    elseif need_discount then
        parts[#parts + 1] = '受折扣系统影响'
    end

    local bucket = 0
    if #parts == 0 then
        bucket = 0
    elseif is_special_condition_text(condition) or is_special_title_requirement(title_requirement) then
        bucket = 3
    elseif title_requirement ~= '' then
        bucket = 2
    else
        bucket = 1
    end

    local title_rank = get_semantic_level_rank(title_requirement)
    return {
        bucket = bucket,
        title_rank = title_rank,
        title = text_sort_value(title_requirement),
        condition = text_sort_value(condition),
        display = #parts == 0 and '无' or table.concat(parts, '<br>'),
    }
end

local function compare_condition_meta(left_meta, right_meta)
    if left_meta.bucket ~= right_meta.bucket then
        return left_meta.bucket < right_meta.bucket
    end
    if left_meta.title_rank ~= right_meta.title_rank then
        return left_meta.title_rank < right_meta.title_rank
    end
    if left_meta.title ~= right_meta.title then
        return left_meta.title < right_meta.title
    end
    if left_meta.condition ~= right_meta.condition then
        return left_meta.condition < right_meta.condition
    end
    return false
end

local function compare_entry_type(left_entry, right_entry)
    local left_item_id = get_entry_item_id(left_entry)
    local right_item_id = get_entry_item_id(right_entry)
    local left_meta = item_common.getItemSortMeta(get_entry_item_record(left_entry), left_item_id)
    local right_meta = item_common.getItemSortMeta(get_entry_item_record(right_entry), right_item_id)

    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
    return false
end

get_entry_item_id = function(entry)
    return common.trim(get_entry_field(entry, 'item_id') or '')
end

get_entry_item_record = function(entry)
    local item_id = get_entry_item_id(entry)
    if item_id == '' then
        return nil
    end
    return item_common.findItemRecord(item_id)
end

local function get_entry_type_label(entry, item_record)
    local record = item_record or get_entry_item_record(entry)
    local item_id = get_entry_item_id(entry)
    local meta = item_common.getItemSortMeta(record, item_id)
    local item_type = meta.item_type or ''
    local type_family = meta.type_family or ''
    local type_label = ''
    if type(record) == 'table' then
        type_label = sanitize_display_text(record.type_display or '')
    end
    local category_label = sanitize_display_text(get_entry_field(entry, 'group') or '')

    local function build_group(rank, key)
        return {
            rank = rank,
            key = key,
            label = ITEM_TYPE_GROUP_LABELS[key] or '其他',
        }
    end

    if any_prefix(item_type, { 'itemtype.book_sewing', 'itemtype.clothing_' })
        or any_contains(type_label, { '时装', '发型', '脚部', '上装', '下装', '面具', '帽', '眼', '眉', '唇', '皮肤', '特征', '套装', '服饰' })
        or any_contains(category_label, { '时装' })
    then
        return build_group(50, 'clothing')
    end

    if any_prefix(item_type, {
        'itemtype.collect_',
        'itemtype.mine_',
        'itemtype.craft_processedproduct',
        'itemtype.craft_loom',
        'itemtype.craft_fiberingot',
        'itemtype.craft_semifinished',
        'itemtype.craft_productmod',
        'itemtype.craft_separate',
    })
        or any_contains(type_label, { '材料', '木料', '竹料', '矿石', '矿产', '加工产物' })
        or any_contains(category_label, { '素材' })
    then
        return build_group(10, 'materials')
    end

    if type_family == 'seed'
        or type_family == 'plant'
        or any_contains(type_label, { '种子', '树苗', '花卉', '作物', '植物' })
        or any_contains(category_label, { '种子' })
    then
        return build_group(20, 'plants')
    end

    if type_family == 'book'
        or any_prefix(item_type, { 'itemtype.book_' })
        or any_contains(type_label, { '配方', '图纸', '裁剪图', '食谱', '诗书', '杂志' })
        or any_contains(category_label, { '配方', '图纸', '食谱' })
    then
        return build_group(40, 'books')
    end

    if type_family == 'food'
        or any_prefix(item_type, { 'itemtype.food_' })
        or any_contains(type_label, { '食品', '零食', '饮品', '饮料', '菜肴', '美食', '食材', '调料', '食物' })
        or any_contains(category_label, { '食品', '零食', '饮品', '饮料', '菜肴', '美食' })
    then
        return build_group(30, 'food')
    end

    if type_family == 'furniture'
        or type_family == 'decoration'
        or type_family == 'light'
        or any_contains(type_label, { '家具', '装饰', '相框', '盆栽', '灯' })
        or any_contains(category_label, { '家具', '相框', '盆栽' })
    then
        return build_group(60, 'furniture')
    end

    if any_prefix(item_type, {
        'itemtype.craft_handcraft',
        'itemtype.craft_workbench',
        'itemtype.craft_manufacturefacility',
        'itemtype.craft_functionfacility',
        'itemtype.craft_collectfacility',
        'itemtype.craft_breedingfacility',
        'itemtype.craft_cookingfacility',
        'itemtype.craft_conversionfacility',
        'itemtype.craft_sewingtable',
    })
        or type_family == 'tool'
        or type_family == 'dailyuse'
        or type_family == 'consumable'
        or type_family == 'mics'
        or type_family == 'electric'
        or type_family == 'electronics'
        or any_contains(type_label, { '工具', '设备', '遥控器' })
        or any_contains(category_label, { '道具' })
    then
        return build_group(70, 'tools')
    end

    if type_family == 'animal'
        or any_contains(type_label, { '动物', '鱼类', '昆虫', '饲料' })
    then
        return build_group(80, 'animals')
    end

    if type_family == 'vehicle'
        or any_contains(type_label, { '载具' })
        or any_contains(category_label, { '载具' })
    then
        return build_group(90, 'vehicles')
    end

    if type_family == 'music'
        or type_family == 'emoji'
        or any_contains(type_label, { '唱片', '舞蹈', '表情', '扭蛋' })
        or any_contains(category_label, { '唱片' })
    then
        return build_group(100, 'fun')
    end

    if type_family == 'currency' then
        return build_group(110, 'currency')
    end

    if type_label ~= '' and ITEM_TYPE_LABEL_FALLBACKS[type_family] then
        return {
            rank = 999,
            key = type_family ~= '' and type_family or 'other',
            label = ITEM_TYPE_LABEL_FALLBACKS[type_family],
        }
    end

    return build_group(999, 'other')
end

local function load_shop_data()
    if shop_data_cache then
        return
    end

    local raw_data
    raw_data, shop_mapping_cache = item_common.loadDomainData('数据:Shop/shop_index.json', '数据:Shop/shop_mapping.json')
    if type(raw_data) == 'table' and type(raw_data.records) == 'table' then
        shop_data_cache = raw_data.records
    else
        shop_data_cache = raw_data or {}
    end

    local raw_reverse = common.loadJsonData('数据:Shop/shop_by_item.json') or {}
    if type(raw_reverse) == 'table' and type(raw_reverse.records) == 'table' then
        by_item_cache = raw_reverse.records
    else
        by_item_cache = raw_reverse or {}
    end
end

local function get_shop_field(record, field)
    if type(record) ~= 'table' then
        return nil
    end
    local short_key = SHOP_FIELD_MAP[field] or field
    if record[short_key] ~= nil then
        return record[short_key]
    end
    return record[field]
end

get_entry_field = function(entry, field)
    if type(entry) ~= 'table' then
        return nil
    end
    local short_key = ENTRY_FIELD_MAP[field] or field
    if entry[short_key] ~= nil then
        return entry[short_key]
    end
    return entry[field]
end

local function has_items(value)
    if type(value) ~= 'table' then
        return false
    end
    for _, _ in ipairs(value) do
        return true
    end
    return false
end

local function resolve_shop_id_from_mapping(value)
    if type(shop_mapping_cache) ~= 'table' then
        return ''
    end
    local normalized = normalize_key(value)
    if normalized == '' then
        return ''
    end
    if shop_mapping_cache.name_to_id and shop_mapping_cache.name_to_id[normalized] then
        return shop_mapping_cache.name_to_id[normalized]
    end
    if shop_mapping_cache.aliases and shop_mapping_cache.aliases[normalized] then
        return shop_mapping_cache.aliases[normalized]
    end
    return ''
end

local function find_shop_record(key)
    load_shop_data()

    local resolved = common.trim(key)
    if resolved == '' then
        resolved = common.getCurrentTitleText()
    end

    local normalized = normalize_key(resolved)
    if shop_data_cache[normalized] then
        return shop_data_cache[normalized], resolved
    end

    local mapped_id = resolve_shop_id_from_mapping(resolved)
    if mapped_id ~= '' then
        local mapped_key = normalize_key(mapped_id)
        if shop_data_cache[mapped_key] then
            return shop_data_cache[mapped_key], mapped_id
        end
    end

    return nil, ''
end

local function resolve_item_id(key)
    local record = item_common.findItemRecord(key)
    if type(record) == 'table' and common.trim(record.id or '') ~= '' then
        return common.trim(record.id)
    end
    return common.trim(key)
end

local function get_entries_for_item(key)
    load_shop_data()
    local item_id = resolve_item_id(key)
    if item_id == '' then
        return {}
    end
    return by_item_cache[normalize_key(item_id)] or {}
end

local function render_shop_link(shop_record, shop_id)
    local display_name = common.trim(get_shop_field(shop_record, 'name') or shop_id)
    if display_name == '' then
        display_name = common.trim(shop_id)
    end
    if display_name == '' then
        return ''
    end
    return ('[[%s]]'):format(display_name)
end

local function render_currency_item(frame, currency_id, count)
    local resolved_currency = common.trim(currency_id)
    if resolved_currency == '' then
        return ''
    end
    local suffix = ''
    if tonumber(count) and tonumber(count) > 0 then
        suffix = tostring(math.floor(tonumber(count)))
    end
    return item.renderItemWithArgs(frame, {
        resolved_currency,
        suffix,
    })
end

local function render_template_currency(frame, template_title, count)
    return frame:expandTemplate{
        title = template_title,
        args = { tostring(math.floor(tonumber(count) or 0)) },
    }
end

local function render_currency_value(frame, currency_id, count)
    local normalized = normalize_key(currency_id)
    local template_title = SPECIAL_CURRENCY_TEMPLATES[normalized]
    if template_title then
        return render_template_currency(frame, template_title, count)
    end
    return render_currency_item(frame, currency_id, count)
end

local function render_price(frame, entry)
    local costs = get_entry_field(entry, 'price_costs')
    if has_items(costs) then
        local parts = {}
        for _, cost in ipairs(costs) do
            if type(cost) == 'table' and common.trim(cost[1] or '') ~= '' then
                parts[#parts + 1] = render_currency_value(frame, cost[1], cost[2])
            end
        end
        return table.concat(parts, '')
    end

    local currency = common.trim(get_entry_field(entry, 'price_currency') or DEFAULT_CURRENCY)
    local value = tonumber(get_entry_field(entry, 'price_value')) or 0
    if currency == '' or currency == DEFAULT_CURRENCY then
        if value <= 0 then
            return '免费'
        end
        return render_template_currency(frame, 'Gold', value)
    end

    return render_currency_value(frame, currency, value)
end

local function format_refresh(refresh_interval)
    local refresh = tonumber(refresh_interval) or 0
    if refresh == -1 then
        return '不刷新'
    end
    if refresh == 1 then
        return '每日刷新'
    end
    if refresh > 1 then
        return tostring(math.floor(refresh)) .. ' 天刷新'
    end
    return ''
end

local function get_refresh_meta(refresh_interval)
    local refresh = tonumber(refresh_interval) or 0
    if refresh == -1 then
        return {
            key = 'never',
            prefix = '永久',
            text = '不刷新',
        }
    end
    if refresh == 1 then
        return {
            key = 'daily',
            prefix = '每日',
            text = '每日刷新',
        }
    end
    if refresh > 1 then
        local days = tostring(math.floor(refresh))
        return {
            key = 'every:' .. days,
            prefix = '每' .. days .. '天',
            text = days .. ' 天刷新',
        }
    end
    return {
        key = 'none',
        prefix = '',
        text = '',
    }
end

local function format_limit(entry)
    local max_count = tonumber(get_entry_field(entry, 'max_count')) or 0
    if max_count == -1 or max_count == 0 then
        if get_entry_field(entry, 'only_one') then
            return '不限购<br>单次仅购 1'
        end
        return '不限购'
    end
    if get_entry_field(entry, 'only_one') then
        return '限购 ' .. tostring(math.floor(max_count)) .. '<br>单次仅购 1'
    end
    return '限购 ' .. tostring(math.floor(max_count))
end

local function format_combined_limit(entry, refresh_prefix)
    local max_count = tonumber(get_entry_field(entry, 'max_count')) or 0
    local prefix = common.trim(refresh_prefix or '')
    local text = ''
    if max_count == -1 or max_count == 0 then
        text = prefix ~= '' and (prefix .. '不限购') or '不限购'
    else
        text = (prefix ~= '' and (prefix .. '限购 ') or '限购 ') .. tostring(math.floor(max_count))
    end

    if get_entry_field(entry, 'only_one') and max_count ~= 1 then
        text = text .. '<br>单次仅购 1'
    end
    return text
end

local function get_inventory_limit_layout(entries)
    local shared_refresh_key = nil
    local shared_refresh_prefix = ''

    for _, entry in ipairs(entries) do
        local refresh_meta = get_refresh_meta(get_entry_field(entry, 'refresh_interval'))
        if refresh_meta.key == 'none' then
            return {
                merged = false,
            }
        end
        if shared_refresh_key == nil then
            shared_refresh_key = refresh_meta.key
            shared_refresh_prefix = refresh_meta.prefix
        elseif shared_refresh_key ~= refresh_meta.key then
            return {
                merged = false,
            }
        end
    end

    if shared_refresh_key == nil then
        return {
            merged = false,
        }
    end

    return {
        merged = true,
        header = '购买限制',
        refresh_key = shared_refresh_key,
        refresh_prefix = shared_refresh_prefix,
    }
end

local function render_item_cell(frame, entry)
    local item_id = get_entry_item_id(entry)
    if item_id == '' then
        return ''
    end
    return item.renderItemWithArgs(frame, { item_id, class = 'block' })
end

local function compare_inventory_entries(left_entry, right_entry)
    local left_group = get_entry_type_label(left_entry, get_entry_item_record(left_entry))
    local right_group = get_entry_type_label(right_entry, get_entry_item_record(right_entry))
    if left_group.rank ~= right_group.rank then
        return left_group.rank < right_group.rank
    end
    if left_group.key ~= right_group.key then
        return left_group.key < right_group.key
    end

    if compare_entry_type(left_entry, right_entry) then
        return true
    end
    if compare_entry_type(right_entry, left_entry) then
        return false
    end

    local left_category = get_category_sort_meta(get_entry_field(left_entry, 'group') or '')
    local right_category = get_category_sort_meta(get_entry_field(right_entry, 'group') or '')
    if compare_category_meta(left_category, right_category) then
        return true
    end
    if compare_category_meta(right_category, left_category) then
        return false
    end

    local left_condition = get_condition_sort_meta(left_entry)
    local right_condition = get_condition_sort_meta(right_entry)
    if compare_condition_meta(left_condition, right_condition) then
        return true
    end
    if compare_condition_meta(right_condition, left_condition) then
        return false
    end

    local left_ui_sort = parse_optional_number(get_entry_field(left_entry, 'ui_sort'), 999999)
    local right_ui_sort = parse_optional_number(get_entry_field(right_entry, 'ui_sort'), 999999)
    if left_ui_sort ~= right_ui_sort then
        return left_ui_sort < right_ui_sort
    end

    local left_item_id = get_entry_item_id(left_entry)
    local right_item_id = get_entry_item_id(right_entry)
    if left_item_id ~= right_item_id then
        return item_common.compareItemKeys(left_item_id, right_item_id, get_entry_item_record(left_entry), get_entry_item_record(right_entry))
    end

    local left_price = tonumber(get_entry_field(left_entry, 'price_value')) or 0
    local right_price = tonumber(get_entry_field(right_entry, 'price_value')) or 0
    if left_price ~= right_price then
        return left_price < right_price
    end

    return text_sort_value(get_entry_field(left_entry, 'source') or '') < text_sort_value(get_entry_field(right_entry, 'source') or '')
end

local function compare_sold_by_entries(left_entry, right_entry)
    local left_condition = get_condition_sort_meta(left_entry)
    local right_condition = get_condition_sort_meta(right_entry)
    if compare_condition_meta(left_condition, right_condition) then
        return true
    end
    if compare_condition_meta(right_condition, left_condition) then
        return false
    end

    local left_price = tonumber(get_entry_field(left_entry, 'price_value')) or 0
    local right_price = tonumber(get_entry_field(right_entry, 'price_value')) or 0
    if left_price ~= right_price then
        return left_price < right_price
    end

    local left_shop = text_sort_value(left_entry[REVERSE_SHOP_KEY] or '')
    local right_shop = text_sort_value(right_entry[REVERSE_SHOP_KEY] or '')
    if left_shop ~= right_shop then
        return left_shop < right_shop
    end

    return false
end

local function sort_entries(entries, comparator)
    local ordered = copy_array(entries)
    if #ordered > 1 then
        table.sort(ordered, comparator)
    end
    return ordered
end

local function render_inventory_group_table(frame, entries)
    local limit_layout = get_inventory_limit_layout(entries)
    local out = {}
    out[#out + 1] = '{| class="wikitable"'
    out[#out + 1] = '! 物品'
    out[#out + 1] = '! 分类'
    out[#out + 1] = '! 价格'
    if limit_layout.merged then
        out[#out + 1] = '! ' .. limit_layout.header
    else
        out[#out + 1] = '! 限购'
        out[#out + 1] = '! 刷新'
    end
    out[#out + 1] = '! 条件'

    for _, entry in ipairs(entries) do
        local category = sanitize_display_text(get_entry_field(entry, 'group') or '')
        local condition_meta = get_condition_sort_meta(entry)
        local refresh_text = format_refresh(get_entry_field(entry, 'refresh_interval'))
        if category == '' then
            category = '未分组'
        end
        if refresh_text == '' then
            refresh_text = '—'
        end

        out[#out + 1] = '|-'
        out[#out + 1] = '| ' .. render_item_cell(frame, entry)
        out[#out + 1] = '| ' .. category
        out[#out + 1] = '| ' .. render_price(frame, entry)
        if limit_layout.merged then
            out[#out + 1] = '| ' .. format_combined_limit(entry, limit_layout.refresh_prefix)
        else
            out[#out + 1] = '| ' .. format_limit(entry)
            out[#out + 1] = '| ' .. refresh_text
        end
        out[#out + 1] = '| ' .. condition_meta.display
    end

    out[#out + 1] = '|}'
    return table.concat(out, '\n')
end

local function render_sold_by_table(frame, entries)
    if not has_items(entries) then
        return ''
    end

    load_shop_data()
    local ordered_entries = sort_entries(entries, compare_sold_by_entries)

    local out = {}
    out[#out + 1] = css.quickCall('Item') or ''
    out[#out + 1] = '{| class="wikitable"'
    out[#out + 1] = '! 商店'
    out[#out + 1] = '! 价格'
    out[#out + 1] = '! 限购'
    out[#out + 1] = '! 刷新'
    out[#out + 1] = '! 条件'

    for _, entry in ipairs(ordered_entries) do
        local shop_id = common.trim(entry[REVERSE_SHOP_KEY] or '')
        local shop_record = shop_data_cache[normalize_key(shop_id)]
        local shop_cell = render_shop_link(shop_record, shop_id)
        local price_cell = render_price(frame, entry)
        local limit_cell = format_limit(entry)
        local refresh_cell = format_refresh(get_entry_field(entry, 'refresh_interval'))
        local condition_cell = get_condition_sort_meta(entry).display
        if refresh_cell == '' then
            refresh_cell = '—'
        end

        out[#out + 1] = '|-'
        out[#out + 1] = '| ' .. shop_cell
        out[#out + 1] = '| ' .. price_cell
        out[#out + 1] = '| ' .. limit_cell
        out[#out + 1] = '| ' .. refresh_cell
        out[#out + 1] = '| ' .. condition_cell
    end

    out[#out + 1] = '|}'
    return table.concat(out, '\n')
end

local function render_inventory_table(frame, shop_record)
    local entries = get_shop_field(shop_record, 'entries')
    if not has_items(entries) then
        return ''
    end

    local ordered_entries = sort_entries(entries, compare_inventory_entries)
    local grouped = {}
    local ordered_groups = {}
    for _, entry in ipairs(ordered_entries) do
        local item_record = get_entry_item_record(entry)
        local group_meta = get_entry_type_label(entry, item_record)
        local group_key = group_meta.key
        local group = grouped[group_key]
        if not group then
            group = {
                key = group_key,
                label = group_meta.label,
                rank = group_meta.rank,
                entries = {},
            }
            grouped[group_key] = group
            ordered_groups[#ordered_groups + 1] = group
        end
        group.entries[#group.entries + 1] = entry
    end

    local out = {}
    out[#out + 1] = css.quickCall('Item') or ''
    table.sort(ordered_groups, function(left_group, right_group)
        if left_group.rank ~= right_group.rank then
            return left_group.rank < right_group.rank
        end
        return left_group.key < right_group.key
    end)
    for _, group in ipairs(ordered_groups) do
        out[#out + 1] = '<h3>' .. group.label .. '</h3>'
        out[#out + 1] = render_inventory_group_table(frame, group.entries)
    end
    return table.concat(out, '\n')
end

local function render_shop_kind(shop_record)
    local kind = common.trim(get_shop_field(shop_record, 'kind') or '')
    if kind == 'festival' then
        return '节日商店'
    end
    if kind == 'shop' then
        return '普通商店'
    end
    return kind
end

local function render_shop_sources(shop_record)
    local sources = get_shop_field(shop_record, 'sources')
    if type(sources) ~= 'table' then
        return ''
    end

    local labels = {
        festival = '节日商店',
        shop = '商店',
        npc = 'NPC 货架',
    }

    local parts = {}
    for _, source in ipairs(sources) do
        local key = common.trim(source)
        if key ~= '' then
            parts[#parts + 1] = labels[key] or key
        end
    end
    return table.concat(parts, ' / ')
end

local function render_shop_locations(shop_record)
    local location_names = get_shop_field(shop_record, 'location_names')
    if type(location_names) ~= 'table' then
        return ''
    end

    local parts = {}
    for _, name in ipairs(location_names) do
        local text = common.trim(name)
        if text ~= '' then
            parts[#parts + 1] = text
        end
    end
    return table.concat(parts, '<br>')
end

local function has_shop_source(shop_record, expected_source)
    local sources = get_shop_field(shop_record, 'sources')
    if type(sources) ~= 'table' then
        return false
    end

    local expected = common.trim(expected_source)
    if expected == '' then
        return false
    end

    for _, source in ipairs(sources) do
        if common.trim(source) == expected then
            return true
        end
    end
    return false
end

local function render_shop_area_ids(shop_record)
    local area_ids = get_shop_field(shop_record, 'area_ids')
    if type(area_ids) ~= 'table' then
        return ''
    end

    local parts = {}
    for _, area_id in ipairs(area_ids) do
        local text = common.trim(area_id)
        if text ~= '' then
            parts[#parts + 1] = '<code>' .. mw.text.nowiki(text) .. '</code>'
        end
    end
    return table.concat(parts, '<br>')
end

local function render_shop_area_count(shop_record)
    local area_ids = get_shop_field(shop_record, 'area_ids')
    if not has_items(area_ids) then
        return '0'
    end
    local count = 0
    for _, _ in ipairs(area_ids) do
        count = count + 1
    end
    return tostring(count)
end

local function render_shop_template_refs(shop_record)
    local refs = {}
    local shop_template = common.trim(get_shop_field(shop_record, 'shop_template') or '')
    local festival_template = common.trim(get_shop_field(shop_record, 'festival_template') or '')

    if shop_template ~= '' then
        refs[#refs + 1] = '<code>' .. mw.text.nowiki(shop_template) .. '</code>'
    end
    if festival_template ~= '' then
        refs[#refs + 1] = '<code>' .. mw.text.nowiki(festival_template) .. '</code>'
    end

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

local function render_shop_entry_count(shop_record)
    local entries = get_shop_field(shop_record, 'entries')
    if not has_items(entries) then
        return '0'
    end
    local count = 0
    for _, _ in ipairs(entries) do
        count = count + 1
    end
    return tostring(count)
end

local function add_currency_id(ordered_ids, seen_ids, currency_id)
    local resolved = common.trim(currency_id)
    if resolved == '' then
        return
    end
    local normalized = normalize_key(resolved)
    if seen_ids[normalized] then
        return
    end
    seen_ids[normalized] = true
    ordered_ids[#ordered_ids + 1] = resolved
end

local function render_currency_summary(frame, shop_record)
    local entries = get_shop_field(shop_record, 'entries')
    if not has_items(entries) then
        return ''
    end

    local ordered_ids = {}
    local seen_ids = {}
    for _, entry in ipairs(entries) do
        local costs = get_entry_field(entry, 'price_costs')
        if has_items(costs) then
            for _, cost in ipairs(costs) do
                if type(cost) == 'table' then
                    add_currency_id(ordered_ids, seen_ids, cost[1])
                end
            end
        else
            add_currency_id(ordered_ids, seen_ids, get_entry_field(entry, 'price_currency') or DEFAULT_CURRENCY)
        end
    end

    local parts = {}
    for _, currency_id in ipairs(ordered_ids) do
        local normalized = normalize_key(currency_id)
        if normalized == 'currency.default' then
            parts[#parts + 1] = '金币'
        elseif normalized == 'item.gamecoin' or normalized == 'currency.star' then
            parts[#parts + 1] = '星砂'
        elseif normalized == 'currency.exp' or normalized == 'experience.default' or normalized == 'exp.default' then
            parts[#parts + 1] = '经验'
        else
            parts[#parts + 1] = item.renderItemWithArgs(frame, { currency_id })
        end
    end
    return table.concat(parts, '<br>')
end

local function render_shop_auto_categories(shop_record)
    local categories = {
        '[[分类:商店建筑]]',
    }

    local kind = common.trim(get_shop_field(shop_record, 'kind') or '')
    if kind == 'festival' then
        categories[#categories + 1] = '[[分类:节日商店]]'
    end

    if has_shop_source(shop_record, 'npc') then
        categories[#categories + 1] = '[[分类:NPC商店]]'
    end

    return table.concat(categories, '')
end

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

    local shop_record = find_shop_record(key)
    if not shop_record then
        return ''
    end

    if field == 'kind_display' then
        return render_shop_kind(shop_record)
    end
    if field == 'name_display' then
        local name = common.trim(get_shop_field(shop_record, 'name') or '')
        if name ~= '' then
            return name
        end
        return get_current_page_name()
    end
    if field == 'source_display' then
        return render_shop_sources(shop_record)
    end
    if field == 'location_display' then
        return render_shop_locations(shop_record)
    end
    if field == 'area_ids_display' then
        return render_shop_area_ids(shop_record)
    end
    if field == 'area_count' then
        return render_shop_area_count(shop_record)
    end
    if field == 'template_refs_display' then
        return render_shop_template_refs(shop_record)
    end
    if field == 'entry_count' then
        return render_shop_entry_count(shop_record)
    end
    if field == 'currency_summary' then
        return render_currency_summary(frame, shop_record)
    end
    if field == 'auto_categories' then
        return render_shop_auto_categories(shop_record)
    end

    return common.toText(get_shop_field(shop_record, field))
end

function p.renderSoldBy(frame)
    local key = common.getArg(frame, 1, '')
    local entries = get_entries_for_item(key)
    if not has_items(entries) then
        return '暂无商店出售记录。'
    end
    return render_sold_by_table(frame, entries)
end

function p.renderShopInventory(frame)
    local key = common.getArg(frame, 1, '')
    local shop_record = find_shop_record(key)
    if not shop_record then
        return '未找到商店数据。'
    end
    local rendered = render_inventory_table(frame, shop_record)
    if rendered == '' then
        return '暂无商店库存记录。'
    end
    return rendered
end

return p