模块:Shop
来自星砂岛百科
更多操作
模板:Documentation subpage Shop 提供商店库存、物品反查与商店元数据读取能力,供 Template:SoldBy、Template:ShopInventory 和 Template: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}}。 - 商店库存页会按较宽的物品大类分组,并在每组前输出小标题。
- 组内条目会按“大类 -> 物品类型 -> 分类语义 -> 条件层级 -> 物品系列 / 等级”稳定排序。
- 条件层级默认是“无条件 -> 普通条件 -> 称号等级 -> 任务 / 特殊条件”。
- 当某一组条目共享同一刷新周期时,`限购` 与 `刷新` 会自动合并成更紧凑的 `购买限制`。
字段
namedescriptionmap_descriptionkindopen_durationshop_templatefestival_templatelocation_namesarea_idssourcesentries
计算字段
kind_displaysource_displaylocation_displayarea_ids_displayarea_counttemplate_refs_displayentry_countcurrency_summaryauto_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