模块:NPC:修订间差异
来自星砂岛百科
更多操作
无编辑摘要 |
无编辑摘要 标签:已被回退 |
||
| 第1行: | 第1行: | ||
local common = require('Module:Common') | local common = require('Module:Common') | ||
local css = require('Module:CSS') | local css = require('Module:CSS') | ||
local item_module = require('Module:Item') | |||
local npc_module = require('Module:NPC') | |||
local p = {} | local p = {} | ||
local | local character_cache | ||
local | local character_records | ||
local character_ids | |||
local character_mapping | |||
local global_rules | |||
local tag_items | |||
local function | local item_cache | ||
if | local item_by_index | ||
local item_mapping | |||
local item_redirects | |||
local id_registry | |||
local registry_character_ids | |||
local registry_item_ids | |||
local registry_character_index_by_id | |||
local registry_item_index_by_id | |||
local summary_cache | |||
local LUACACHE_NAMESPACE = 'gifting:npc_tabs' | |||
local LUACACHE_VERSION = '2026-03-12-version-2' | |||
local level_to_bucket = { | |||
resonance = 'resonance', | |||
special = 'special', | |||
loved = 'loved', | |||
liked = 'liked', | |||
neutral = 'neutral', | |||
disliked = 'disliked', | |||
hated = 'hated', | |||
r = 'resonance', | |||
s = 'special', | |||
v = 'loved', | |||
k = 'liked', | |||
n = 'neutral', | |||
d = 'disliked', | |||
h = 'hated', | |||
} | |||
local bucket_to_short = { | |||
resonance = 'r', | |||
special = 's', | |||
loved = 'v', | |||
liked = 'k', | |||
neutral = 'n', | |||
disliked = 'd', | |||
hated = 'h', | |||
} | |||
local function normalize_key(value) | |||
return common.normalizeKey(value) | |||
end | |||
local function resolve_level_bucket(level) | |||
if level == nil then | |||
return nil | |||
end | |||
if type(level) == 'string' then | |||
local normalized = normalize_key(level) | |||
return level_to_bucket[normalized] | |||
end | |||
local numeric = tonumber(level) | |||
if numeric == 4 then | |||
return 'resonance' | |||
end | |||
if numeric == 3 then | |||
return 'special' | |||
end | |||
if numeric == 2 then | |||
return 'loved' | |||
end | |||
if numeric == 1 then | |||
return 'liked' | |||
end | |||
if numeric == 0 then | |||
return 'neutral' | |||
end | |||
if numeric == -1 then | |||
return 'disliked' | |||
end | |||
if numeric == -2 then | |||
return 'hated' | |||
end | |||
return nil | |||
end | |||
local function empty_summary() | |||
return { | |||
resonance = { personal = {}, global = {} }, | |||
special = { personal = {}, global = {} }, | |||
loved = { personal = {}, global = {} }, | |||
liked = { personal = {}, global = {} }, | |||
neutral = { personal = {}, global = {} }, | |||
disliked = { personal = {}, global = {} }, | |||
hated = { personal = {}, global = {} }, | |||
} | |||
end | |||
local function load_id_registry() | |||
if id_registry then | |||
return | |||
end | |||
id_registry = common.loadJsonData('数据:Gifting/gift_id_registry.json') or {} | |||
registry_character_ids = id_registry.characters or {} | |||
registry_item_ids = id_registry.items or {} | |||
registry_character_index_by_id = {} | |||
for idx, value in ipairs(registry_character_ids) do | |||
local key = normalize_key(value) | |||
if key ~= '' then | |||
registry_character_index_by_id[key] = idx - 1 | |||
end | |||
end | |||
registry_item_index_by_id = {} | |||
for idx, value in ipairs(registry_item_ids) do | |||
local key = normalize_key(value) | |||
if key ~= '' then | |||
registry_item_index_by_id[key] = idx - 1 | |||
end | |||
end | |||
end | |||
local function item_id_from_index(index) | |||
load_id_registry() | |||
local n = tonumber(index) | |||
if not n then | |||
return '' | |||
end | |||
return common.trim(registry_item_ids[n + 1] or '') | |||
end | |||
local function character_id_from_index(index) | |||
load_id_registry() | |||
local n = tonumber(index) | |||
if not n then | |||
return '' | |||
end | |||
return common.trim(registry_character_ids[n + 1] or '') | |||
end | |||
local function load_character_data() | |||
if character_cache then | |||
return | return | ||
end | end | ||
local | character_cache = common.loadJsonData('数据:Gifting/gift_preferences.json') or {} | ||
character_records = character_cache.characters or {} | |||
global_rules = character_cache.global or {} | |||
tag_items = character_cache.tag_items or {} | |||
load_id_registry() | |||
character_ids = {} | |||
for idx, record in ipairs(character_records) do | |||
local cid = character_id_from_index(idx - 1) | |||
if cid ~= '' then | |||
character_ids[normalize_key(cid)] = idx | |||
end | |||
end | |||
character_mapping = common.loadJsonData('数据:Gifting/gifting_mapping.json') or { | |||
name_to_id = {}, | |||
id_to_name = {}, | |||
aliases = {}, | |||
overrides = { | |||
name_to_id = {}, | |||
aliases = {}, | |||
}, | |||
} | |||
end | end | ||
local function | local function load_item_data() | ||
return common. | if item_cache then | ||
return | |||
end | |||
item_cache = common.loadJsonData('数据:Gifting/gift_preferences_by_item.json') or {} | |||
item_by_index = item_cache.items or {} | |||
item_redirects = item_cache.item_redirects or {} | |||
item_mapping = common.loadJsonData('数据:Item/item_mapping.json') or { | |||
name_to_id = {}, | |||
id_to_name = {}, | |||
aliases = {}, | |||
overrides = { | |||
name_to_id = {}, | |||
aliases = {}, | |||
}, | |||
} | |||
end | end | ||
local function | local function find_character_record(key) | ||
load_character_data() | |||
local resolved = common.trim(key) | local resolved = common.trim(key) | ||
if resolved == '' then | if resolved == '' then | ||
resolved = common.getCurrentTitleText() | resolved = common.getCurrentTitleText() | ||
end | end | ||
local normalized = normalize_key(resolved) | local normalized = normalize_key(resolved) | ||
if | local idx = character_ids[normalized] | ||
if not idx then | |||
local override_id = character_mapping.overrides and character_mapping.overrides.name_to_id and character_mapping.overrides.name_to_id[normalized] | |||
if override_id then | |||
idx = character_ids[normalize_key(override_id)] | |||
end | |||
end | |||
if not idx then | |||
local override_alias = character_mapping.overrides and character_mapping.overrides.aliases and character_mapping.overrides.aliases[normalized] | |||
if override_alias then | |||
idx = character_ids[normalize_key(override_alias)] | |||
end | |||
end | |||
if not idx then | |||
local mapped_id = character_mapping.name_to_id and character_mapping.name_to_id[normalized] | |||
if mapped_id then | |||
idx = character_ids[normalize_key(mapped_id)] | |||
end | |||
end | |||
if not idx then | |||
local alias_id = character_mapping.aliases and character_mapping.aliases[normalized] | |||
if alias_id then | |||
idx = character_ids[normalize_key(alias_id)] | |||
end | |||
end | end | ||
local | if not idx then | ||
return nil | |||
end | |||
return character_records[idx], idx - 1 | |||
end | |||
local function resolve_item_index(index) | |||
local key = tostring(index) | |||
if item_by_index[key] then | |||
return key | |||
end | |||
local redirected = item_redirects and item_redirects[key] | |||
if redirected then | |||
local redirected_key = tostring(redirected) | |||
if item_by_index[redirected_key] then | |||
return redirected_key | |||
end | |||
end | end | ||
return nil | return nil | ||
end | end | ||
function p. | local function find_item_record(key) | ||
load_item_data() | |||
load_id_registry() | |||
local resolved = common.trim(key) | |||
if resolved == '' then | |||
resolved = common.getCurrentTitleText() | |||
end | |||
local normalized = normalize_key(resolved) | |||
local item_id = '' | |||
local item_index = registry_item_index_by_id[normalized] | |||
if item_index ~= nil then | |||
item_id = registry_item_ids[item_index + 1] or '' | |||
end | |||
if item_id == '' then | |||
local override_id = item_mapping.overrides and item_mapping.overrides.name_to_id and item_mapping.overrides.name_to_id[normalized] | |||
if override_id and override_id ~= '' then | |||
item_id = override_id | |||
end | |||
end | |||
if item_id == '' then | |||
local override_alias = item_mapping.overrides and item_mapping.overrides.aliases and item_mapping.overrides.aliases[normalized] | |||
if override_alias and override_alias ~= '' then | |||
item_id = override_alias | |||
end | |||
end | |||
if item_id == '' then | |||
local mapped_id = item_mapping.name_to_id and item_mapping.name_to_id[normalized] | |||
if mapped_id and mapped_id ~= '' then | |||
item_id = mapped_id | |||
end | |||
end | |||
if item_id == '' then | |||
local alias_id = item_mapping.aliases and item_mapping.aliases[normalized] | |||
if alias_id and alias_id ~= '' then | |||
item_id = alias_id | |||
end | |||
end | |||
if item_id == '' then | |||
return nil | |||
end | |||
if item_index == nil then | |||
item_index = registry_item_index_by_id[normalize_key(item_id)] | |||
end | |||
if item_index == nil then | |||
return nil | |||
end | |||
local resolved_index = resolve_item_index(item_index) | |||
if not resolved_index then | |||
return nil | |||
end | |||
return item_by_index[resolved_index] | |||
end | |||
local function append_unique_indexes(values, seen, target) | |||
if type(values) ~= 'table' then | |||
return | |||
end | |||
for _, value in ipairs(values) do | |||
local index = tonumber(value) | |||
if index ~= nil and not seen[index] then | |||
seen[index] = true | |||
target[#target + 1] = index | |||
end | |||
end | |||
end | |||
local function expand_rule_items(rule) | |||
local result = {} | |||
local seen = {} | |||
for _, tag in ipairs(rule.t or {}) do | |||
append_unique_indexes(tag_items[tag], seen, result) | |||
end | |||
append_unique_indexes(rule.s, seen, result) | |||
return result | |||
end | |||
local function build_character_summary(record, record_index) | |||
if record_index ~= nil then | |||
summary_cache = summary_cache or {} | |||
if summary_cache[record_index] then | |||
return summary_cache[record_index] | |||
end | |||
end | |||
local summary = empty_summary() | |||
local global_seen = { | |||
resonance = {}, special = {}, loved = {}, liked = {}, neutral = {}, disliked = {}, hated = {}, | |||
} | |||
local personal_seen = { | |||
resonance = {}, special = {}, loved = {}, liked = {}, neutral = {}, disliked = {}, hated = {}, | |||
} | |||
for _, rule in ipairs(global_rules) do | |||
local bucket = resolve_level_bucket(rule.l) | |||
if bucket and summary[bucket] then | |||
for _, item_idx in ipairs(expand_rule_items(rule)) do | |||
if not global_seen[bucket][item_idx] then | |||
global_seen[bucket][item_idx] = true | |||
summary[bucket].global[#summary[bucket].global + 1] = item_idx | |||
end | |||
end | |||
end | |||
end | |||
for _, rule in ipairs(record.p or {}) do | |||
local bucket = resolve_level_bucket(rule.l) | |||
if bucket and summary[bucket] then | |||
for _, item_idx in ipairs(expand_rule_items(rule)) do | |||
if not global_seen[bucket][item_idx] and not personal_seen[bucket][item_idx] then | |||
personal_seen[bucket][item_idx] = true | |||
summary[bucket].personal[#summary[bucket].personal + 1] = item_idx | |||
end | |||
end | |||
end | |||
end | |||
if record_index ~= nil then | |||
summary_cache[record_index] = summary | |||
end | |||
return summary | |||
end | |||
local function get_summary_bucket(record, bucket, record_index) | |||
if type(record) ~= 'table' then | |||
return nil | |||
end | |||
local resolved_bucket = level_to_bucket[bucket] or normalize_key(bucket) | |||
local summary = build_character_summary(record, record_index) | |||
return summary[resolved_bucket] | |||
end | |||
local function item_label_from_index(item_index) | |||
load_item_data() | |||
local item_id = item_id_from_index(item_index) | |||
if item_id == '' then | |||
return '' | |||
end | |||
return common.trim(item_mapping.id_to_name and item_mapping.id_to_name[normalize_key(item_id)] or '') | |||
end | |||
local function item_display_meta(item_index) | |||
local item_id = item_id_from_index(item_index) | |||
if item_id == '' then | |||
return nil | |||
end | |||
local name = item_label_from_index(item_index) | |||
if name == '' then | |||
name = item_id | |||
end | |||
local file_name = item_id | |||
if file_name == '' then | |||
file_name = name | |||
end | |||
return { | |||
name = name, | |||
image = file_name .. '.png', | |||
} | |||
end | |||
local character_index_cache | |||
local function load_character_index() | |||
if character_index_cache then | |||
return | |||
end | |||
character_index_cache = common.loadJsonData('数据:Character/character_index.json') or {} | |||
end | |||
local function is_creature(name) | |||
load_character_index() | |||
local normalized = normalize_key(name) | |||
local record = character_index_cache[normalized] | |||
if record and record.entity_kind == 'creature' then | |||
return true | |||
end | |||
return false | |||
end | |||
local function item_template_from_name(frame, name) | |||
if not frame or common.trim(name) == '' then | |||
return nil | |||
end | |||
if is_creature(name) then | |||
return '[[' .. name .. ']]' | |||
end | |||
local output = npc_module.renderNPC(frame:newChild{ | |||
title = 'Module:NPC', | |||
args = { [1] = name, class = 'block' } | |||
}) | |||
return common.trim(output) ~= '' and output or '' | |||
end | |||
local function character_label_from_index(character_index) | |||
load_character_data() | |||
local character_id = character_id_from_index(character_index) | |||
if character_id == '' then | |||
return '' | |||
end | |||
return common.trim(character_mapping.id_to_name and character_mapping.id_to_name[normalize_key(character_id)] or '') | |||
end | |||
local function join_indexes(values, label_resolver, fallback_resolver) | |||
if type(values) ~= 'table' or #values == 0 then | |||
return '' | |||
end | |||
local parts = {} | |||
for _, value in ipairs(values) do | |||
local idx = tonumber(value) | |||
if idx ~= nil then | |||
local label = label_resolver and label_resolver(idx) or '' | |||
if label ~= '' then | |||
parts[#parts + 1] = ('[[%s]]'):format(label) | |||
else | |||
local fallback = fallback_resolver and fallback_resolver(idx) or '' | |||
if fallback ~= '' then | |||
parts[#parts + 1] = fallback | |||
end | |||
end | |||
end | |||
end | |||
return table.concat(parts, '、') | |||
end | |||
local function render_bucket(bucket_data, personal_label, global_label, label_resolver, fallback_resolver) | |||
if type(bucket_data) ~= 'table' then | |||
return '' | |||
end | |||
local parts = {} | |||
local personal = join_indexes(bucket_data.personal, label_resolver, fallback_resolver) | |||
local global = join_indexes(bucket_data.global, label_resolver, fallback_resolver) | |||
if personal ~= '' then | |||
parts[#parts + 1] = personal_label .. ':' .. personal | |||
end | |||
if global ~= '' then | |||
parts[#parts + 1] = global_label .. ':' .. global | |||
end | |||
return table.concat(parts, '<br />') | |||
end | |||
local function render_item_grid(frame, values) | |||
local items = {} | |||
local seen = {} | |||
if type(values) == 'table' then | |||
for _, value in ipairs(values) do | |||
local idx = tonumber(value) | |||
if idx ~= nil then | |||
local meta = item_display_meta(idx) | |||
if meta then | |||
local dedupe_key = normalize_key(meta.name) .. '|' .. (meta.image or "default") | |||
if not seen[dedupe_key] then | |||
seen[dedupe_key] = true | |||
local item_markup = item_template_from_name(frame, meta.name) | |||
if item_markup and item_markup ~= '' then | |||
items[#items + 1] = item_markup | |||
end | |||
end | |||
end | |||
end | |||
end | |||
end | |||
if #items == 0 then | |||
return '<div class="gifting-empty">暂无可显示物品</div>' | |||
end | |||
local root = mw.html.create('div'):addClass('gifting-item-grid') | |||
for _, item_markup in ipairs(items) do | |||
root:tag('div') | |||
:addClass('gifting-item-entry') | |||
:wikitext(item_markup) | |||
end | |||
return tostring(root) | |||
end | |||
local function render_preference_panel(frame, title, values) | |||
local panel = mw.html.create('div'):addClass('gifting-preference-panel') | |||
panel:tag('div'):addClass('gifting-preference-panel-title'):wikitext(title) | |||
panel:tag('div'):addClass('gifting-preference-panel-body'):wikitext(render_item_grid(frame, values)) | |||
return tostring(panel) | |||
end | |||
local function render_npc_level(frame, record, record_index, bucket) | |||
local bucket_data = get_summary_bucket(record, bucket, record_index) | |||
if type(bucket_data) ~= 'table' then | |||
return '<div class="gifting-level-grid"><div class="gifting-empty">暂无数据</div></div>' | |||
end | |||
local root = mw.html.create('div') | |||
:addClass('gifting-level-grid') | |||
:addClass('gifting-level-grid--' .. bucket) | |||
root:wikitext(render_preference_panel(frame, '个人喜好', bucket_data.personal or {})) | |||
root:wikitext(render_preference_panel(frame, '通用喜好', bucket_data.global or {})) | |||
return tostring(root) | |||
end | |||
local function build_tabber_content(frame, record, record_index) | |||
local tabs = { | |||
{ key = 'loved', label = '最爱' }, | |||
{ key = 'liked', label = '喜欢' }, | |||
{ key = 'neutral', label = '一般' }, | |||
{ key = 'disliked', label = '不喜欢' }, | |||
{ key = 'hated', label = '讨厌' }, | |||
{ key = 'resonance', label = '共鸣' }, | |||
{ key = 'special', label = '特殊' }, | |||
} | |||
local parts = {} | |||
for index, tab in ipairs(tabs) do | |||
parts[#parts + 1] = tab.label .. '=' .. render_npc_level(frame, record, record_index, tab.key) | |||
if index < #tabs then | |||
parts[#parts + 1] = '|-|' | |||
end | |||
end | |||
return table.concat(parts, '\n') | |||
end | |||
function p.getField(frame) | |||
local key = common.getArg(frame, 1, '') | local key = common.getArg(frame, 1, '') | ||
local record = | local field = common.getArg(frame, 2, '') | ||
if record then | if field == '' then | ||
return record. | return '' | ||
end | |||
local record, character_index = find_character_record(key) | |||
if not record then | |||
return '' | |||
end | |||
if field == 'id' then | |||
return character_id_from_index(character_index) | |||
end | |||
if field == 'name' then | |||
return common.toText(record.n) | |||
end | |||
if field == 'name_en' then | |||
return common.toText(record.en) | |||
end | |||
if field == 'character_type' then | |||
return common.toText(record.t) | |||
end | end | ||
if field == 'personal_rules' then | |||
return common.toText(record.p) | |||
end | |||
return common.toText(record[field]) | |||
end | end | ||
function p. | function p.preferenceList(frame) | ||
local key = common.getArg(frame, 1, '') | local key = common.getArg(frame, 1, '') | ||
local record = | local field = common.getArg(frame, 2, '') | ||
if record then | if field == '' then | ||
return | return '' | ||
end | |||
local record = find_character_record(key) | |||
if not record then | |||
return '' | |||
end | end | ||
return '' | return render_bucket(get_summary_bucket(record, field), '个人喜好', '通用喜好', item_label_from_index, item_id_from_index) | ||
end | end | ||
function p. | function p.renderNpcTabs(frame) | ||
local key = common.getArg(frame, 1, '') | local key = common.getArg(frame, 1, '') | ||
local | local resolved = common.trim(key) | ||
local | if resolved == '' then | ||
resolved = common.getCurrentTitleText() | |||
end | |||
local cache_key = common.buildLuaCacheKey(LUACACHE_NAMESPACE, LUACACHE_VERSION, normalize_key(resolved)) | |||
local content = common.luaCacheGet(cache_key) | |||
if content == nil then | |||
local record, record_index = find_character_record(resolved) | |||
if not record then | |||
common.luaCacheSet(cache_key, '') | |||
content = '' | |||
else | |||
content = build_tabber_content(frame, record, record_index) | |||
common.luaCacheSet(cache_key, content) | |||
end | |||
end | |||
local css_out = (css.quickCall('GiftsByNPC') or '') .. (css.quickCall('Item') or '') | |||
if content == '' then | |||
return css_out .. '<div class="gifting-empty">未找到送礼数据</div>' | |||
end | |||
return css_out .. frame:extensionTag('tabber', content, { class = 'tabber-no-active-indicator gifting-tabber' }) | |||
end | |||
local function render_character_list(frame, values) | |||
if type(values) ~= 'table' or #values == 0 then | |||
return '' | |||
end | |||
local record = | local parts = {} | ||
for _, value in ipairs(values) do | |||
local idx = tonumber(value) | |||
if idx ~= nil then | |||
local name = character_label_from_index(idx) | |||
if name ~= '' then | |||
if is_creature(name) then | |||
parts[#parts + 1] = '[[' .. name .. ']]' | |||
else | |||
local output = npc_module.renderNPC(frame:newChild{ | |||
title = 'Module:NPC', | |||
args = { [1] = name } | |||
}) | |||
if common.trim(output) ~= '' then | |||
parts[#parts + 1] = output | |||
end | |||
end | |||
end | |||
end | |||
end | |||
return table.concat(parts, '、') | |||
end | |||
function p.preferenceListByItem(frame) | |||
local key = common.getArg(frame, 1, '') | |||
local field = common.getArg(frame, 2, '') | |||
if field == '' then | |||
return '' | |||
end | |||
local record = find_item_record(key) | |||
if not record then | if not record then | ||
return '' | return '' | ||
end | end | ||
local | local bucket = level_to_bucket[field] or normalize_key(field) | ||
local | local short = bucket_to_short[bucket] | ||
if | local compact_bucket = record.s and short and record.s[short] or nil | ||
if type(compact_bucket) ~= 'table' then | |||
return '' | |||
end | end | ||
local parts = {} | |||
local personal = render_character_list(frame, compact_bucket.p or {}) | |||
local global = render_character_list(frame, compact_bucket.g or {}) | |||
if personal ~= '' then | |||
parts[#parts + 1] = '个人喜好角色:' .. personal | |||
if | |||
end | end | ||
if global ~= '' then | |||
parts[#parts + 1] = '通用喜好角色:' .. global | |||
if | |||
end | end | ||
return table.concat(parts, '<br />') | |||
end | end | ||
return p | return p | ||
2026年3月16日 (一) 00:45的版本
概述
NPC 用于输出角色头像卡片与名称展示,供 {{NPC}}、赠礼页和人物列表类模板调用。
用法
{{#invoke:NPC|renderNPC|晨星}}
{{#invoke:NPC|renderNPC|晨星|size=32|class=block}}
示例
{{#invoke:NPC|getId|晨星}}
函数
getName:返回角色名称。getId:返回角色 ID。renderNPCByKey:按查找键输出 NPC 卡片,适合 Lua 内部调用。renderNPC:按模板参数输出 NPC 卡片。
数据来源
local common = require('Module:Common')
local css = require('Module:CSS')
local item_module = require('Module:Item')
local npc_module = require('Module:NPC')
local p = {}
local character_cache
local character_records
local character_ids
local character_mapping
local global_rules
local tag_items
local item_cache
local item_by_index
local item_mapping
local item_redirects
local id_registry
local registry_character_ids
local registry_item_ids
local registry_character_index_by_id
local registry_item_index_by_id
local summary_cache
local LUACACHE_NAMESPACE = 'gifting:npc_tabs'
local LUACACHE_VERSION = '2026-03-12-version-2'
local level_to_bucket = {
resonance = 'resonance',
special = 'special',
loved = 'loved',
liked = 'liked',
neutral = 'neutral',
disliked = 'disliked',
hated = 'hated',
r = 'resonance',
s = 'special',
v = 'loved',
k = 'liked',
n = 'neutral',
d = 'disliked',
h = 'hated',
}
local bucket_to_short = {
resonance = 'r',
special = 's',
loved = 'v',
liked = 'k',
neutral = 'n',
disliked = 'd',
hated = 'h',
}
local function normalize_key(value)
return common.normalizeKey(value)
end
local function resolve_level_bucket(level)
if level == nil then
return nil
end
if type(level) == 'string' then
local normalized = normalize_key(level)
return level_to_bucket[normalized]
end
local numeric = tonumber(level)
if numeric == 4 then
return 'resonance'
end
if numeric == 3 then
return 'special'
end
if numeric == 2 then
return 'loved'
end
if numeric == 1 then
return 'liked'
end
if numeric == 0 then
return 'neutral'
end
if numeric == -1 then
return 'disliked'
end
if numeric == -2 then
return 'hated'
end
return nil
end
local function empty_summary()
return {
resonance = { personal = {}, global = {} },
special = { personal = {}, global = {} },
loved = { personal = {}, global = {} },
liked = { personal = {}, global = {} },
neutral = { personal = {}, global = {} },
disliked = { personal = {}, global = {} },
hated = { personal = {}, global = {} },
}
end
local function load_id_registry()
if id_registry then
return
end
id_registry = common.loadJsonData('数据:Gifting/gift_id_registry.json') or {}
registry_character_ids = id_registry.characters or {}
registry_item_ids = id_registry.items or {}
registry_character_index_by_id = {}
for idx, value in ipairs(registry_character_ids) do
local key = normalize_key(value)
if key ~= '' then
registry_character_index_by_id[key] = idx - 1
end
end
registry_item_index_by_id = {}
for idx, value in ipairs(registry_item_ids) do
local key = normalize_key(value)
if key ~= '' then
registry_item_index_by_id[key] = idx - 1
end
end
end
local function item_id_from_index(index)
load_id_registry()
local n = tonumber(index)
if not n then
return ''
end
return common.trim(registry_item_ids[n + 1] or '')
end
local function character_id_from_index(index)
load_id_registry()
local n = tonumber(index)
if not n then
return ''
end
return common.trim(registry_character_ids[n + 1] or '')
end
local function load_character_data()
if character_cache then
return
end
character_cache = common.loadJsonData('数据:Gifting/gift_preferences.json') or {}
character_records = character_cache.characters or {}
global_rules = character_cache.global or {}
tag_items = character_cache.tag_items or {}
load_id_registry()
character_ids = {}
for idx, record in ipairs(character_records) do
local cid = character_id_from_index(idx - 1)
if cid ~= '' then
character_ids[normalize_key(cid)] = idx
end
end
character_mapping = common.loadJsonData('数据:Gifting/gifting_mapping.json') or {
name_to_id = {},
id_to_name = {},
aliases = {},
overrides = {
name_to_id = {},
aliases = {},
},
}
end
local function load_item_data()
if item_cache then
return
end
item_cache = common.loadJsonData('数据:Gifting/gift_preferences_by_item.json') or {}
item_by_index = item_cache.items or {}
item_redirects = item_cache.item_redirects or {}
item_mapping = common.loadJsonData('数据:Item/item_mapping.json') or {
name_to_id = {},
id_to_name = {},
aliases = {},
overrides = {
name_to_id = {},
aliases = {},
},
}
end
local function find_character_record(key)
load_character_data()
local resolved = common.trim(key)
if resolved == '' then
resolved = common.getCurrentTitleText()
end
local normalized = normalize_key(resolved)
local idx = character_ids[normalized]
if not idx then
local override_id = character_mapping.overrides and character_mapping.overrides.name_to_id and character_mapping.overrides.name_to_id[normalized]
if override_id then
idx = character_ids[normalize_key(override_id)]
end
end
if not idx then
local override_alias = character_mapping.overrides and character_mapping.overrides.aliases and character_mapping.overrides.aliases[normalized]
if override_alias then
idx = character_ids[normalize_key(override_alias)]
end
end
if not idx then
local mapped_id = character_mapping.name_to_id and character_mapping.name_to_id[normalized]
if mapped_id then
idx = character_ids[normalize_key(mapped_id)]
end
end
if not idx then
local alias_id = character_mapping.aliases and character_mapping.aliases[normalized]
if alias_id then
idx = character_ids[normalize_key(alias_id)]
end
end
if not idx then
return nil
end
return character_records[idx], idx - 1
end
local function resolve_item_index(index)
local key = tostring(index)
if item_by_index[key] then
return key
end
local redirected = item_redirects and item_redirects[key]
if redirected then
local redirected_key = tostring(redirected)
if item_by_index[redirected_key] then
return redirected_key
end
end
return nil
end
local function find_item_record(key)
load_item_data()
load_id_registry()
local resolved = common.trim(key)
if resolved == '' then
resolved = common.getCurrentTitleText()
end
local normalized = normalize_key(resolved)
local item_id = ''
local item_index = registry_item_index_by_id[normalized]
if item_index ~= nil then
item_id = registry_item_ids[item_index + 1] or ''
end
if item_id == '' then
local override_id = item_mapping.overrides and item_mapping.overrides.name_to_id and item_mapping.overrides.name_to_id[normalized]
if override_id and override_id ~= '' then
item_id = override_id
end
end
if item_id == '' then
local override_alias = item_mapping.overrides and item_mapping.overrides.aliases and item_mapping.overrides.aliases[normalized]
if override_alias and override_alias ~= '' then
item_id = override_alias
end
end
if item_id == '' then
local mapped_id = item_mapping.name_to_id and item_mapping.name_to_id[normalized]
if mapped_id and mapped_id ~= '' then
item_id = mapped_id
end
end
if item_id == '' then
local alias_id = item_mapping.aliases and item_mapping.aliases[normalized]
if alias_id and alias_id ~= '' then
item_id = alias_id
end
end
if item_id == '' then
return nil
end
if item_index == nil then
item_index = registry_item_index_by_id[normalize_key(item_id)]
end
if item_index == nil then
return nil
end
local resolved_index = resolve_item_index(item_index)
if not resolved_index then
return nil
end
return item_by_index[resolved_index]
end
local function append_unique_indexes(values, seen, target)
if type(values) ~= 'table' then
return
end
for _, value in ipairs(values) do
local index = tonumber(value)
if index ~= nil and not seen[index] then
seen[index] = true
target[#target + 1] = index
end
end
end
local function expand_rule_items(rule)
local result = {}
local seen = {}
for _, tag in ipairs(rule.t or {}) do
append_unique_indexes(tag_items[tag], seen, result)
end
append_unique_indexes(rule.s, seen, result)
return result
end
local function build_character_summary(record, record_index)
if record_index ~= nil then
summary_cache = summary_cache or {}
if summary_cache[record_index] then
return summary_cache[record_index]
end
end
local summary = empty_summary()
local global_seen = {
resonance = {}, special = {}, loved = {}, liked = {}, neutral = {}, disliked = {}, hated = {},
}
local personal_seen = {
resonance = {}, special = {}, loved = {}, liked = {}, neutral = {}, disliked = {}, hated = {},
}
for _, rule in ipairs(global_rules) do
local bucket = resolve_level_bucket(rule.l)
if bucket and summary[bucket] then
for _, item_idx in ipairs(expand_rule_items(rule)) do
if not global_seen[bucket][item_idx] then
global_seen[bucket][item_idx] = true
summary[bucket].global[#summary[bucket].global + 1] = item_idx
end
end
end
end
for _, rule in ipairs(record.p or {}) do
local bucket = resolve_level_bucket(rule.l)
if bucket and summary[bucket] then
for _, item_idx in ipairs(expand_rule_items(rule)) do
if not global_seen[bucket][item_idx] and not personal_seen[bucket][item_idx] then
personal_seen[bucket][item_idx] = true
summary[bucket].personal[#summary[bucket].personal + 1] = item_idx
end
end
end
end
if record_index ~= nil then
summary_cache[record_index] = summary
end
return summary
end
local function get_summary_bucket(record, bucket, record_index)
if type(record) ~= 'table' then
return nil
end
local resolved_bucket = level_to_bucket[bucket] or normalize_key(bucket)
local summary = build_character_summary(record, record_index)
return summary[resolved_bucket]
end
local function item_label_from_index(item_index)
load_item_data()
local item_id = item_id_from_index(item_index)
if item_id == '' then
return ''
end
return common.trim(item_mapping.id_to_name and item_mapping.id_to_name[normalize_key(item_id)] or '')
end
local function item_display_meta(item_index)
local item_id = item_id_from_index(item_index)
if item_id == '' then
return nil
end
local name = item_label_from_index(item_index)
if name == '' then
name = item_id
end
local file_name = item_id
if file_name == '' then
file_name = name
end
return {
name = name,
image = file_name .. '.png',
}
end
local character_index_cache
local function load_character_index()
if character_index_cache then
return
end
character_index_cache = common.loadJsonData('数据:Character/character_index.json') or {}
end
local function is_creature(name)
load_character_index()
local normalized = normalize_key(name)
local record = character_index_cache[normalized]
if record and record.entity_kind == 'creature' then
return true
end
return false
end
local function item_template_from_name(frame, name)
if not frame or common.trim(name) == '' then
return nil
end
if is_creature(name) then
return '[[' .. name .. ']]'
end
local output = npc_module.renderNPC(frame:newChild{
title = 'Module:NPC',
args = { [1] = name, class = 'block' }
})
return common.trim(output) ~= '' and output or ''
end
local function character_label_from_index(character_index)
load_character_data()
local character_id = character_id_from_index(character_index)
if character_id == '' then
return ''
end
return common.trim(character_mapping.id_to_name and character_mapping.id_to_name[normalize_key(character_id)] or '')
end
local function join_indexes(values, label_resolver, fallback_resolver)
if type(values) ~= 'table' or #values == 0 then
return ''
end
local parts = {}
for _, value in ipairs(values) do
local idx = tonumber(value)
if idx ~= nil then
local label = label_resolver and label_resolver(idx) or ''
if label ~= '' then
parts[#parts + 1] = ('[[%s]]'):format(label)
else
local fallback = fallback_resolver and fallback_resolver(idx) or ''
if fallback ~= '' then
parts[#parts + 1] = fallback
end
end
end
end
return table.concat(parts, '、')
end
local function render_bucket(bucket_data, personal_label, global_label, label_resolver, fallback_resolver)
if type(bucket_data) ~= 'table' then
return ''
end
local parts = {}
local personal = join_indexes(bucket_data.personal, label_resolver, fallback_resolver)
local global = join_indexes(bucket_data.global, label_resolver, fallback_resolver)
if personal ~= '' then
parts[#parts + 1] = personal_label .. ':' .. personal
end
if global ~= '' then
parts[#parts + 1] = global_label .. ':' .. global
end
return table.concat(parts, '<br />')
end
local function render_item_grid(frame, values)
local items = {}
local seen = {}
if type(values) == 'table' then
for _, value in ipairs(values) do
local idx = tonumber(value)
if idx ~= nil then
local meta = item_display_meta(idx)
if meta then
local dedupe_key = normalize_key(meta.name) .. '|' .. (meta.image or "default")
if not seen[dedupe_key] then
seen[dedupe_key] = true
local item_markup = item_template_from_name(frame, meta.name)
if item_markup and item_markup ~= '' then
items[#items + 1] = item_markup
end
end
end
end
end
end
if #items == 0 then
return '<div class="gifting-empty">暂无可显示物品</div>'
end
local root = mw.html.create('div'):addClass('gifting-item-grid')
for _, item_markup in ipairs(items) do
root:tag('div')
:addClass('gifting-item-entry')
:wikitext(item_markup)
end
return tostring(root)
end
local function render_preference_panel(frame, title, values)
local panel = mw.html.create('div'):addClass('gifting-preference-panel')
panel:tag('div'):addClass('gifting-preference-panel-title'):wikitext(title)
panel:tag('div'):addClass('gifting-preference-panel-body'):wikitext(render_item_grid(frame, values))
return tostring(panel)
end
local function render_npc_level(frame, record, record_index, bucket)
local bucket_data = get_summary_bucket(record, bucket, record_index)
if type(bucket_data) ~= 'table' then
return '<div class="gifting-level-grid"><div class="gifting-empty">暂无数据</div></div>'
end
local root = mw.html.create('div')
:addClass('gifting-level-grid')
:addClass('gifting-level-grid--' .. bucket)
root:wikitext(render_preference_panel(frame, '个人喜好', bucket_data.personal or {}))
root:wikitext(render_preference_panel(frame, '通用喜好', bucket_data.global or {}))
return tostring(root)
end
local function build_tabber_content(frame, record, record_index)
local tabs = {
{ key = 'loved', label = '最爱' },
{ key = 'liked', label = '喜欢' },
{ key = 'neutral', label = '一般' },
{ key = 'disliked', label = '不喜欢' },
{ key = 'hated', label = '讨厌' },
{ key = 'resonance', label = '共鸣' },
{ key = 'special', label = '特殊' },
}
local parts = {}
for index, tab in ipairs(tabs) do
parts[#parts + 1] = tab.label .. '=' .. render_npc_level(frame, record, record_index, tab.key)
if index < #tabs then
parts[#parts + 1] = '|-|'
end
end
return table.concat(parts, '\n')
end
function p.getField(frame)
local key = common.getArg(frame, 1, '')
local field = common.getArg(frame, 2, '')
if field == '' then
return ''
end
local record, character_index = find_character_record(key)
if not record then
return ''
end
if field == 'id' then
return character_id_from_index(character_index)
end
if field == 'name' then
return common.toText(record.n)
end
if field == 'name_en' then
return common.toText(record.en)
end
if field == 'character_type' then
return common.toText(record.t)
end
if field == 'personal_rules' then
return common.toText(record.p)
end
return common.toText(record[field])
end
function p.preferenceList(frame)
local key = common.getArg(frame, 1, '')
local field = common.getArg(frame, 2, '')
if field == '' then
return ''
end
local record = find_character_record(key)
if not record then
return ''
end
return render_bucket(get_summary_bucket(record, field), '个人喜好', '通用喜好', item_label_from_index, item_id_from_index)
end
function p.renderNpcTabs(frame)
local key = common.getArg(frame, 1, '')
local resolved = common.trim(key)
if resolved == '' then
resolved = common.getCurrentTitleText()
end
local cache_key = common.buildLuaCacheKey(LUACACHE_NAMESPACE, LUACACHE_VERSION, normalize_key(resolved))
local content = common.luaCacheGet(cache_key)
if content == nil then
local record, record_index = find_character_record(resolved)
if not record then
common.luaCacheSet(cache_key, '')
content = ''
else
content = build_tabber_content(frame, record, record_index)
common.luaCacheSet(cache_key, content)
end
end
local css_out = (css.quickCall('GiftsByNPC') or '') .. (css.quickCall('Item') or '')
if content == '' then
return css_out .. '<div class="gifting-empty">未找到送礼数据</div>'
end
return css_out .. frame:extensionTag('tabber', content, { class = 'tabber-no-active-indicator gifting-tabber' })
end
local function render_character_list(frame, values)
if type(values) ~= 'table' or #values == 0 then
return ''
end
local parts = {}
for _, value in ipairs(values) do
local idx = tonumber(value)
if idx ~= nil then
local name = character_label_from_index(idx)
if name ~= '' then
if is_creature(name) then
parts[#parts + 1] = '[[' .. name .. ']]'
else
local output = npc_module.renderNPC(frame:newChild{
title = 'Module:NPC',
args = { [1] = name }
})
if common.trim(output) ~= '' then
parts[#parts + 1] = output
end
end
end
end
end
return table.concat(parts, '、')
end
function p.preferenceListByItem(frame)
local key = common.getArg(frame, 1, '')
local field = common.getArg(frame, 2, '')
if field == '' then
return ''
end
local record = find_item_record(key)
if not record then
return ''
end
local bucket = level_to_bucket[field] or normalize_key(field)
local short = bucket_to_short[bucket]
local compact_bucket = record.s and short and record.s[short] or nil
if type(compact_bucket) ~= 'table' then
return ''
end
local parts = {}
local personal = render_character_list(frame, compact_bucket.p or {})
local global = render_character_list(frame, compact_bucket.g or {})
if personal ~= '' then
parts[#parts + 1] = '个人喜好角色:' .. personal
end
if global ~= '' then
parts[#parts + 1] = '通用喜好角色:' .. global
end
return table.concat(parts, '<br />')
end
return p