模块:CharacterProfileData
来自夜幕之下 Wiki - Reign of Nightfall 中文资料站
更多操作
此模块的文档可以在模块:CharacterProfileData/doc创建
-- Module:CharacterProfileData
-- 用法:
-- {{#invoke:CharacterProfileData|render|幽灵}}
-- 或通过模板 {{CharacterProfile|幽灵}}
local p = {}
local function val(v, fallback)
if v == nil or v == "" then return fallback or "—" end
return v
end
local function esc(v)
return mw.text.encode(tostring(v or ""))
end
local function parseJson(str)
if not str or str == "" then return nil end
local ok, result = pcall(mw.text.jsonDecode, str)
if ok then return result else return nil end
end
local function sortBySort(a, b)
return (tonumber(a.sort) or 0) < (tonumber(b.sort) or 0)
end
local function asArray(v)
if type(v) ~= "table" then return {} end
table.sort(v, sortBySort)
return v
end
local function fetchProfile(frame, key)
local fields = table.concat({
"*",
"character.name",
"archive_entries.*",
"voice_lines.*",
"gifts.*",
"gifts.gift.*",
"memories.*",
"styles.*",
"styles.card.stylename",
"rumors.*",
"whispers.*",
}, ",")
local apiUrl = "https://data.saltedkiss.org/items/character_profiles"
.. "?fields=" .. mw.uri.encode(fields, "QUERY")
.. "&filter[slug][_eq]=" .. mw.uri.encode(key, "QUERY")
.. "&limit=1"
local raw = frame:preprocess(
"{{#get_web_data:url=" .. apiUrl
.. "|format=text|data=responseText=__text}}"
.. "{{#external_value:responseText}}"
)
raw = mw.text.trim(raw or "")
local parsed = parseJson(raw)
if parsed and type(parsed.data) == "table" and parsed.data[1] then
return parsed.data[1]
end
return nil
end
local function normalizeProfile(profile, key)
local data = profile
data.display_name = val(data.display_name, data.character and data.character.name or key)
data.latin_name = val(data.latin_name, "")
data.archive_entries = asArray(data.archive_entries)
data.voice_lines = asArray(data.voice_lines)
data.gifts = asArray(data.gifts)
data.memories = asArray(data.memories)
data.styles = asArray(data.styles)
data.rumors = asArray(data.rumors)
data.whispers = asArray(data.whispers)
return data
end
local function navItem(target, label, marker, meta, active, level, parentScope)
level = level or "main"
local isMain = level == "main"
local classes = "character_profile-nav-item " .. (isMain and "character_profile-nav-item--main" or "character_profile-nav-item--sub")
return '<div class="' .. classes .. '" role="button" tabindex="0" data-archive-nav-item="1" data-archive-nav-level="' .. level .. '" '
.. (parentScope and parentScope ~= "" and ('data-archive-nav-parent-scope="' .. esc(parentScope) .. '" ') or "")
.. 'data-archive-active="' .. (active and "1" or "0") .. '" data-archive-snap-target="' .. esc(target) .. '">'
.. '<span class="character_profile-nav-main"><span class="character_profile-nav-number" data-archive-nav-number="1">' .. esc(marker) .. '</span><span class="character_profile-nav-label" data-archive-nav-label="1">' .. esc(label) .. '</span></span>'
.. (meta and meta ~= "" and ('<span class="character_profile-nav-meta" data-archive-nav-meta="1">' .. esc(meta) .. '</span>') or "")
.. '</div>'
end
local function navTree(data)
local children = {
navItem("archive-dossier-top", "总览", "00", "", true, "sub", ""),
}
for i, item in ipairs(data.archive_entries) do
table.insert(children, navItem("archive-dossier-" .. string.format("%02d", i), item.category, string.format("%02d", i), "", false, "sub", ""))
end
local moduleRows = {
navItem("archive-module-voice", "语音", "02", "P1", false, "main", ""),
navItem("archive-module-gifts", "礼物", "03", "2项", false, "main", ""),
navItem("archive-module-memory", "时光记忆", "04", "节点", false, "main", ""),
navItem("archive-module-styles", "风格入口", "05", "卡面", false, "main", ""),
navItem("archive-module-rumors", "传闻", "06", "文本", false, "main", ""),
navItem("archive-module-whisper", "私语", "07", "长文", false, "main", ""),
}
return '<div class="character_profile-nav-tree">'
.. navItem("archive-dossier-top", "档案", "01", tostring(#data.archive_entries) .. "项", true, "main", "archive-dossier")
.. '<div class="character_profile-nav-children" data-archive-nav-children="archive-dossier">'
.. table.concat(children, "\n")
.. '</div>'
.. table.concat(moduleRows, "\n")
.. '</div>'
end
local function paraHtml(text, extraClass)
local out = {}
text = tostring(text or "")
text = text:gsub("\r\n", "\n")
if text == "" then return "" end
for para in string.gmatch(text .. "\n\n", "(.-)\n\n+") do
para = mw.text.trim(para)
if para ~= "" then
table.insert(out, '<p class="character_profile-paragraph ' .. (extraClass or "") .. '">' .. esc(para) .. '</p>')
end
end
return table.concat(out, "\n")
end
local function fileWikitext(fileName, size)
if not fileName or fileName == "" then return "" end
return "[[文件:" .. fileName .. "|" .. (size or "96px") .. "|link=]]"
end
local function firstFile(...)
for i = 1, select("#", ...) do
local fileName = select(i, ...)
if fileName and fileName ~= "" then return fileName end
end
return ""
end
local function wikiFileExists(fileName)
if not fileName or fileName == "" then return false end
local ok, title = pcall(mw.title.new, "文件:" .. fileName)
if not ok or not title or not title.file then return false end
return title.file.exists
end
local function characterArtFile(prefix, name, override)
override = mw.text.trim(tostring(override or ""))
if override ~= "" then
if mw.ustring.find(override, "%.") or mw.ustring.find(override, "_") then
return override
end
return prefix .. "_" .. name .. "_" .. override .. ".png"
end
return prefix .. "_" .. name .. ".png"
end
local function profileAvatarFile(profile)
local name = profile.display_name or ""
local explicit = characterArtFile("胸像", name, firstFile(profile.bust_wiki_file, profile.portrait_wiki_file, profile.avatar_wiki_file))
if explicit ~= "" and explicit ~= "胸像_" .. name .. ".png" then return explicit end
local inferred = characterArtFile("胸像", name, "")
if wikiFileExists(inferred) then return inferred end
return ""
end
local function profileFigureFile(profile)
local name = profile.display_name or ""
local explicit = characterArtFile("立绘", name, firstFile(profile.figure_wiki_file, profile.full_body_wiki_file))
if explicit ~= "" and explicit ~= "立绘_" .. name .. ".png" then return explicit end
local inferred = characterArtFile("立绘", name, "")
if wikiFileExists(inferred) then return inferred end
local legacy = "角色档案_" .. name .. "_全身.png"
if wikiFileExists(legacy) then return legacy end
return ""
end
local function profileBackgroundArtHtml(profile)
local fileName = profileFigureFile(profile)
if fileName ~= "" then
return '<div class="character_profile-bg-art" aria-hidden="true">' .. fileWikitext(fileName, "860px") .. '</div>'
end
return ""
end
local function portraitHtml(profile)
local avatarFile = profileAvatarFile(profile)
local figureFile = profileFigureFile(profile)
if avatarFile == "" and figureFile == "" then
return '<div class="character_profile-portrait"></div>'
end
local classes = "character_profile-portrait character_profile-portrait--art"
if avatarFile ~= "" then classes = classes .. " character_profile-portrait--has-avatar" end
if figureFile ~= "" then classes = classes .. " character_profile-portrait--has-figure" end
local toggle = ""
if figureFile ~= "" then
toggle = '<div class="character_profile-art-toggle" role="button" tabindex="0" aria-label="切换立绘层级" aria-pressed="false" title="切换立绘层级" data-character-profile-art-toggle="1"><span class="character_profile-art-toggle-icon" aria-hidden="true"></span></div>'
end
return '<div class="' .. classes .. '" data-character-profile-art-state="avatar">'
.. '<div class="character_profile-portrait-layer character_profile-portrait-layer--avatar">' .. fileWikitext(avatarFile, "640px") .. '</div>'
.. toggle
.. '</div>'
end
local function sectionRail(n, subtitle)
return '<div class="character_profile-section-rail"><div class="character_profile-section-number">' .. esc(n) .. '</div><div class="character_profile-section-rule"></div><div class="character_profile-section-subtitle">' .. esc(subtitle) .. '</div></div>'
end
local function moduleSectionRail(n, subtitle)
local initial = mw.ustring.sub(n, 1, 1)
local rest = mw.ustring.sub(n, 2)
local number = '<span class="character_profile-section-code-initial">' .. esc(initial) .. '</span><span class="character_profile-section-code-rest">' .. esc(rest) .. '</span>'
return '<div class="character_profile-section-rail"><div class="character_profile-section-number character_profile-section-number--module">' .. number .. '</div><div class="character_profile-section-rule"></div><div class="character_profile-section-subtitle">' .. esc(subtitle) .. '</div></div>'
end
local function moduleSection(id, n, title, subtitle, body, light)
return '<div id="' .. id .. '" class="character_profile-section character_profile-section--module ' .. (light and "character_profile-section--light" or "character_profile-section--dark") .. '">'
.. moduleSectionRail(n, subtitle)
.. '<div class="character_profile-surface">'
.. '<div class="character_profile-section-head"><div class="character_profile-section-title">' .. esc(title) .. '</div><div class="character_profile-section-code character_profile-section-code--module">' .. esc(n) .. '</div></div>'
.. '<div class="character_profile-module-body">' .. body .. '</div>'
.. '</div></div>'
end
local function archiveSectionHtml(item, index)
local n = string.format("%02d", index)
return '<div id="archive-dossier-' .. n .. '" class="character_profile-section character_profile-section--archive">'
.. sectionRail(n, item.category)
.. '<div class="character_profile-surface">'
.. '<div class="character_profile-section-head"><div class="character_profile-section-title">' .. esc(item.title) .. '</div><div class="character_profile-section-code">' .. n .. '</div></div>'
.. paraHtml(item.body, "character_profile-paragraph--shadow")
.. '</div></div>'
end
local function voiceHtml(rows)
local out = {}
for i, row in ipairs(rows) do
table.insert(out, '<div class="character_profile-voice-row"><div class="character_profile-voice-number">' .. string.format("%02d", i) .. '</div><div><strong class="character_profile-voice-label">' .. esc(row.label) .. '</strong><span class="character_profile-voice-text">' .. esc(row.text) .. '</span></div></div>')
end
return '<div class="character_profile-voice-grid">' .. table.concat(out, "") .. '</div>'
end
local function giftImage(item)
local gift = item.gift or {}
local name = gift.name or item.name
if name and name ~= "" then
return fileWikitext("礼物_" .. name .. ".png", "88px")
end
return "礼物图片"
end
local function giftPreferenceText(item)
local label = item.preference_type == "exclusive" and "专属" or (item.preference_type == "preferred" and "偏好" or "")
local bonus = tonumber(item.cognition_bonus)
if bonus and bonus > 0 then
return label ~= "" and (label .. " +" .. tostring(bonus)) or ("+" .. tostring(bonus))
end
return label
end
local function giftsHtml(rows)
local out = {}
for _, item in ipairs(rows) do
local gift = item.gift or {}
local name = val(gift.name, item.name)
local description = val(gift.item_description, gift.description or item.description or "")
local preference = giftPreferenceText(item)
local title = '<div class="character_profile-gift-title-line"><strong class="character_profile-gift-name">' .. esc(name) .. '</strong>' .. (preference ~= "" and '<span class="character_profile-gift-meta">' .. esc(preference) .. '</span>' or '') .. '</div>'
table.insert(out, '<div class="character_profile-gift-row"><div class="character_profile-gift-image">' .. giftImage(item) .. '</div><div>' .. title .. '<span class="character_profile-gift-description">' .. esc(description) .. '</span></div></div>')
end
return '<div class="character_profile-gift-grid">' .. table.concat(out, "") .. '</div>'
end
local function memoriesHtml(rows)
local out = {}
local sizes = { "112px", "118px", "126px" }
for i, item in ipairs(rows) do
table.insert(out, '<div class="character_profile-memory-row"><div class="character_profile-memory-image">' .. fileWikitext(item.image_wiki_file, sizes[i] or "118px") .. '</div><div><strong class="character_profile-memory-title">' .. esc(item.title) .. '</strong><span class="character_profile-memory-condition">' .. esc(item.condition) .. '</span><span class="character_profile-memory-description">' .. esc(item.description) .. '</span></div></div>')
end
return '<div class="character_profile-memory-list">' .. table.concat(out, "") .. '</div>'
end
local function stylesHtml(rows)
local out = {}
for _, row in ipairs(rows) do
local name = row.style_name or (row.card and row.card.stylename) or ""
table.insert(out, '<div class="character_profile-style-card"><strong class="character_profile-style-name">' .. esc(name) .. '</strong><span class="character_profile-style-desc">' .. esc(row.level_label) .. ' / ' .. esc(row.description) .. '</span></div>')
end
return '<div class="character_profile-style-grid">' .. table.concat(out, "") .. '</div>'
end
local function rumorsHtml(rows)
local out = {}
for i, row in ipairs(rows) do
table.insert(out, '<div class="character_profile-rumor-row"><strong class="character_profile-rumor-title">' .. string.format("%02d", i) .. " " .. esc(row.title) .. '</strong><br /><span class="character_profile-rumor-text">' .. esc(row.text) .. '</span></div>')
end
return '<div class="character_profile-rumor-grid">' .. table.concat(out, "") .. '</div>'
end
local function whisperHtml(rows)
local out = {}
for _, row in ipairs(rows) do
if row.title and row.title ~= "" then
table.insert(out, '<strong class="character_profile-whisper-title">' .. esc(row.title) .. '</strong>')
end
table.insert(out, paraHtml(row.body, ""))
end
return table.concat(out, "")
end
local function missingProfileHtml(key)
local safeKey = esc(key)
return table.concat({
"__NOTOC__",
'<div class="character_profile-root character_archive-fullbleed character_archive-snap character_archive-dossier">',
'<div class="character_profile-bg" data-archive-bg-layer="fixed"></div>',
'<div class="character_profile-shell"><div class="character_profile-layout"><div class="character_profile-content character_profile-content--missing">',
'<div class="character_profile-intro"><div class="character_profile-intro-title">角色档案未找到</div><div class="character_profile-intro-subtitle">DIRECTUS / MISSING PROFILE</div><div class="character_profile-intro-text">Directus 中没有找到 slug 为“' .. safeKey .. '”的已发布角色档案。请先在 character_profiles 中创建并发布对应记录。</div></div>',
'</div></div></div></div>',
}, "\n")
end
function p.render(frame)
local key = mw.text.trim(frame.args[1] or frame.args.name or mw.title.getCurrentTitle().text or "")
if key == "" then key = mw.title.getCurrentTitle().text end
local fetched = fetchProfile(frame, key)
if not fetched then
return missingProfileHtml(key)
end
local profile = normalizeProfile(fetched, key)
local html = {}
table.insert(html, "__NOTOC__")
local artModeAttr = profileFigureFile(profile) ~= "" and ' data-character-profile-art-mode="avatar"' or ""
table.insert(html, '<div class="character_profile-root character_archive-fullbleed character_archive-snap character_archive-dossier"' .. artModeAttr .. '>')
table.insert(html, '<div class="character_profile-bg" data-archive-bg-layer="fixed">' .. profileBackgroundArtHtml(profile) .. '</div>')
table.insert(html, '<div class="character_profile-shell"><div class="character_profile-layout">')
table.insert(html, '<div class="character_profile-sidebar">')
table.insert(html, '<div id="archive-dossier-top" class="character_profile-overview"><div class="character_profile-overview-head"><div class="character_profile-overview-label">档案总览</div>' .. portraitHtml(profile) .. '<div class="character_profile-name">' .. esc(profile.display_name) .. '</div><div class="character_profile-latin">' .. esc(profile.latin_name) .. '</div><div class="character_profile-rule"></div></div>')
table.insert(html, '<div class="character_profile-facts">')
local facts = {
{ "姓名", profile.full_name },
{ "身高", profile.height },
{ "生日", profile.birthday },
{ "出生地", profile.birthplace },
{ "组织", profile.organization },
{ "信息素", profile.pheromone },
}
for _, fact in ipairs(facts) do
table.insert(html, '<div class="character_profile-fact"><span class="character_profile-fact-label">' .. esc(fact[1]) .. '</span><strong class="character_profile-fact-value">' .. esc(val(fact[2])) .. '</strong></div>')
end
table.insert(html, '</div></div>')
table.insert(html, '<div class="character_profile-sticky-nav" data-archive-sticky-nav="1"><div class="character_profile-nav-panel"><div class="character_profile-nav-heading"><div class="character_profile-nav-title">角色目录</div><div class="character_profile-nav-kicker">INDEX</div></div>' .. navTree(profile) .. '</div></div>')
table.insert(html, '</div>')
table.insert(html, '<div class="character_profile-content"><div class="character_profile-intro"><div class="character_profile-intro-title">' .. esc(val(profile.intro_title, "角色档案")) .. '</div><div class="character_profile-intro-subtitle">' .. esc(val(profile.intro_subtitle, "DOSSIER / FULL TEXT")) .. '</div><div class="character_profile-intro-text">' .. esc(val(profile.intro_text, "")) .. '</div></div>')
for i, item in ipairs(profile.archive_entries) do
table.insert(html, archiveSectionHtml(item, i))
end
table.insert(html, moduleSection("archive-module-voice", "Voice", "角色语音", "语音", voiceHtml(profile.voice_lines), false))
table.insert(html, moduleSection("archive-module-gifts", "Gift", "喜好礼物", "礼物", giftsHtml(profile.gifts), true))
table.insert(html, moduleSection("archive-module-memory", "Memory", "时光记忆", "记忆", memoriesHtml(profile.memories), true))
table.insert(html, moduleSection("archive-module-styles", "Style", "风格入口", "风格", stylesHtml(profile.styles), false))
table.insert(html, moduleSection("archive-module-rumors", "Rumor", "传闻", "传闻", rumorsHtml(profile.rumors), false))
table.insert(html, moduleSection("archive-module-whisper", "Whisper", "私语", "长文", whisperHtml(profile.whispers), true))
table.insert(html, '</div></div></div></div>')
return table.concat(html, "\n")
end
return p