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

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

模块:Shop:修订间差异

来自星砂岛百科
Sizau-bot留言 | 贡献
同步更新
Sizau-bot留言 | 贡献
同步更新
第36行: 第36行:
local REVERSE_SHOP_KEY = 'sid'
local REVERSE_SHOP_KEY = 'sid'
local DEFAULT_CURRENCY = 'Currency.Default'
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 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_data_cache
local shop_mapping_cache
local shop_mapping_cache
local by_item_cache
local by_item_cache
local get_entry_field


local function normalize_key(value)
local function normalize_key(value)
     return common.normalizeKey(value)
     return common.normalizeKey(value)
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 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
local function get_entry_item_id(entry)
    return common.trim(get_entry_field(entry, 'item_id') or '')
end
local function get_entry_item_record(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 label = ''
    if type(record) == 'table' then
        label = sanitize_display_text(record.type_display or '')
        if label ~= '' then
            return label
        end
    end
    local meta = item_common.getItemSortMeta(record, get_entry_item_id(entry))
    return ITEM_TYPE_LABEL_FALLBACKS[meta.type_family] or '未分类'
end
local function get_entry_type_group_key(entry, item_record)
    local meta = item_common.getItemSortMeta(item_record, get_entry_item_id(entry))
    if meta.item_type ~= '' then
        return meta.item_type
    end
    if meta.type_family ~= '' then
        return meta.type_family
    end
    return '未分类'
end
end


第77行: 第396行:
end
end


local function get_entry_field(entry, field)
get_entry_field = function(entry, field)
     if type(entry) ~= 'table' then
     if type(entry) ~= 'table' then
         return nil
         return nil
第180行: 第499行:
         suffix,
         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
end


第188行: 第523行:
         for _, cost in ipairs(costs) do
         for _, cost in ipairs(costs) do
             if type(cost) == 'table' and common.trim(cost[1] or '') ~= '' then
             if type(cost) == 'table' and common.trim(cost[1] or '') ~= '' then
                 parts[#parts + 1] = render_currency_item(frame, cost[1], cost[2])
                 parts[#parts + 1] = render_currency_value(frame, cost[1], cost[2])
             end
             end
         end
         end
第200行: 第535行:
             return '免费'
             return '免费'
         end
         end
         return tostring(math.floor(value)) .. ' 金币'
         return render_template_currency(frame, 'Gold', value)
     end
     end


     return render_currency_item(frame, currency, value)
     return render_currency_value(frame, currency, value)
end
end


第221行: 第556行:


local function format_limit(entry)
local function format_limit(entry)
    local parts = {}
     local max_count = tonumber(get_entry_field(entry, 'max_count')) or 0
     local max_count = tonumber(get_entry_field(entry, 'max_count')) or 0
     if max_count == -1 or max_count == 0 then
     if max_count == -1 or max_count == 0 then
         parts[#parts + 1] = '不限购'
         if get_entry_field(entry, 'only_one') then
     else
            return '不限购<br>单次仅购 1'
         parts[#parts + 1] = '限购 ' .. tostring(math.floor(max_count))
        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 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)
    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
     end


     local refresh_text = format_refresh(get_entry_field(entry, 'refresh_interval'))
     local left_item_id = get_entry_item_id(left_entry)
     if refresh_text ~= '' then
    local right_item_id = get_entry_item_id(right_entry)
         parts[#parts + 1] = refresh_text
     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
     end


     if get_entry_field(entry, 'only_one') then
     local left_price = tonumber(get_entry_field(left_entry, 'price_value')) or 0
        parts[#parts + 1] = '单次仅购 1'
    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
     end


     return table.concat(parts, ' / ')
     return text_sort_value(get_entry_field(left_entry, 'source') or '') < text_sort_value(get_entry_field(right_entry, 'source') or '')
end
end


local function format_condition(entry)
local function compare_sold_by_entries(left_entry, right_entry)
     local parts = {}
     local left_condition = get_condition_sort_meta(left_entry)
     local title_requirement = common.trim(get_entry_field(entry, 'title_requirement') or '')
     local right_condition = get_condition_sort_meta(right_entry)
     local condition = common.trim(get_entry_field(entry, 'condition') or '')
     if compare_condition_meta(left_condition, right_condition) then
     local discount = tonumber(get_entry_field(entry, 'discount')) or 0
        return true
     local need_discount = get_entry_field(entry, 'need_discount')
    end
     if compare_condition_meta(right_condition, left_condition) then
        return false
     end


     if title_requirement ~= '' then
    local left_price = tonumber(get_entry_field(left_entry, 'price_value')) or 0
         parts[#parts + 1] = title_requirement
    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
     end
     if condition ~= '' then
 
        parts[#parts + 1] = condition
     local left_shop = text_sort_value(left_entry[REVERSE_SHOP_KEY] or '')
    end
     local right_shop = text_sort_value(right_entry[REVERSE_SHOP_KEY] or '')
     if discount ~= 0 then
     if left_shop ~= right_shop then
        parts[#parts + 1] = '折扣值 ' .. tostring(math.floor(discount))
         return left_shop < right_shop
     elseif need_discount then
         parts[#parts + 1] = '受折扣系统影响'
     end
     end


     if #parts == 0 then
    return false
         return ''
end
 
local function sort_entries(entries, comparator)
    local ordered = copy_array(entries)
     if #ordered > 1 then
         table.sort(ordered, comparator)
     end
     end
     return table.concat(parts, '<br>')
     return ordered
end
end


local function render_item_cell(frame, entry)
local function render_inventory_group_table(frame, entries)
    local item_id = common.trim(get_entry_field(entry, 'item_id') or '')
    local out = {}
    if item_id == '' then
    out[#out + 1] = '{| class="wikitable"'
         return ''
    out[#out + 1] = '! 物品'
    out[#out + 1] = '! 分类'
    out[#out + 1] = '! 价格'
    out[#out + 1] = '! 限购'
    out[#out + 1] = '! 刷新'
    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)
        out[#out + 1] = '| ' .. format_limit(entry)
        out[#out + 1] = '| ' .. refresh_text
        out[#out + 1] = '| ' .. condition_meta.display
     end
     end
     return item.renderItemWithArgs(frame, { item_id, class = 'block' })
 
    out[#out + 1] = '|}'
     return table.concat(out, '\n')
end
end


第280行: 第697行:


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


     local out = {}
     local out = {}
第286行: 第704行:
     out[#out + 1] = '! 商店'
     out[#out + 1] = '! 商店'
     out[#out + 1] = '! 价格'
     out[#out + 1] = '! 价格'
     out[#out + 1] = '! 限购 / 刷新'
     out[#out + 1] = '! 限购'
    out[#out + 1] = '! 刷新'
     out[#out + 1] = '! 条件'
     out[#out + 1] = '! 条件'


     for _, entry in ipairs(entries) do
     for _, entry in ipairs(ordered_entries) do
         local shop_id = common.trim(entry[REVERSE_SHOP_KEY] or '')
         local shop_id = common.trim(entry[REVERSE_SHOP_KEY] or '')
         local shop_record = shop_data_cache[normalize_key(shop_id)]
         local shop_record = shop_data_cache[normalize_key(shop_id)]
第295行: 第714行:
         local price_cell = render_price(frame, entry)
         local price_cell = render_price(frame, entry)
         local limit_cell = format_limit(entry)
         local limit_cell = format_limit(entry)
         local condition_cell = format_condition(entry)
        local refresh_cell = format_refresh(get_entry_field(entry, 'refresh_interval'))
         if condition_cell == '' then
         local condition_cell = get_condition_sort_meta(entry).display
             condition_cell = ''
         if refresh_cell == '' then
             refresh_cell = ''
         end
         end


第304行: 第724行:
         out[#out + 1] = '| ' .. price_cell
         out[#out + 1] = '| ' .. price_cell
         out[#out + 1] = '| ' .. limit_cell
         out[#out + 1] = '| ' .. limit_cell
        out[#out + 1] = '| ' .. refresh_cell
         out[#out + 1] = '| ' .. condition_cell
         out[#out + 1] = '| ' .. condition_cell
     end
     end
第315行: 第736行:
     if not has_items(entries) then
     if not has_items(entries) then
         return ''
         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_key = get_entry_type_group_key(entry, item_record)
        local group = grouped[group_key]
        if not group then
            group = {
                key = group_key,
                label = get_entry_type_label(entry, item_record),
                entries = {},
            }
            grouped[group_key] = group
            ordered_groups[#ordered_groups + 1] = group
        end
        group.entries[#group.entries + 1] = entry
     end
     end


     local out = {}
     local out = {}
     out[#out + 1] = css.quickCall('Item') or ''
     out[#out + 1] = css.quickCall('Item') or ''
    out[#out + 1] = '{| class="wikitable"'
     for _, group in ipairs(ordered_groups) do
    out[#out + 1] = '! 物品'
         out[#out + 1] = '<h3>' .. group.label .. '</h3>'
    out[#out + 1] = '! 分类'
         out[#out + 1] = render_inventory_group_table(frame, group.entries)
    out[#out + 1] = '! 价格'
    out[#out + 1] = '! 限购 / 刷新'
    out[#out + 1] = '! 条件'
 
     for _, entry in ipairs(entries) do
        local category = common.trim(get_entry_field(entry, 'group') or '')
        local price_cell = render_price(frame, entry)
        local limit_cell = format_limit(entry)
        local condition_cell = format_condition(entry)
        if category == '' then
            category = '未分组'
        end
        if condition_cell == '' then
            condition_cell = '无'
        end
 
        out[#out + 1] = '|-'
         out[#out + 1] = '| ' .. render_item_cell(frame, entry)
        out[#out + 1] = '| ' .. category
        out[#out + 1] = '| ' .. price_cell
        out[#out + 1] = '| ' .. limit_cell
         out[#out + 1] = '| ' .. condition_cell
     end
     end
    out[#out + 1] = '|}'
     return table.concat(out, '\n')
     return table.concat(out, '\n')
end
end

2026年3月17日 (二) 17:20的版本

模板: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',
    kind = 'k',
    open_duration = 'od',
    shop_template = 'st',
    festival_template = 'ft',
    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 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 function normalize_key(value)
    return common.normalizeKey(value)
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 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

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

local function get_entry_item_record(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 label = ''
    if type(record) == 'table' then
        label = sanitize_display_text(record.type_display or '')
        if label ~= '' then
            return label
        end
    end

    local meta = item_common.getItemSortMeta(record, get_entry_item_id(entry))
    return ITEM_TYPE_LABEL_FALLBACKS[meta.type_family] or '未分类'
end

local function get_entry_type_group_key(entry, item_record)
    local meta = item_common.getItemSortMeta(item_record, get_entry_item_id(entry))
    if meta.item_type ~= '' then
        return meta.item_type
    end
    if meta.type_family ~= '' then
        return meta.type_family
    end
    return '未分类'
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 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 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)
    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 out = {}
    out[#out + 1] = '{| class="wikitable"'
    out[#out + 1] = '! 物品'
    out[#out + 1] = '! 分类'
    out[#out + 1] = '! 价格'
    out[#out + 1] = '! 限购'
    out[#out + 1] = '! 刷新'
    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)
        out[#out + 1] = '| ' .. format_limit(entry)
        out[#out + 1] = '| ' .. refresh_text
        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_key = get_entry_type_group_key(entry, item_record)
        local group = grouped[group_key]
        if not group then
            group = {
                key = group_key,
                label = get_entry_type_label(entry, item_record),
                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 ''
    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

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

    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