打开/关闭菜单
8
231
3
1270
夜幕之下 Wiki - Reign of Nightfall 中文资料站
打开/关闭外观设置菜单
打开/关闭个人菜单
未登录
未登录用户的IP地址会在进行任意编辑后公开展示。

模块: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