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

模块:CharacterAtlasData

来自夜幕之下 Wiki - Reign of Nightfall 中文资料站

此模块的文档可以在模块:CharacterAtlasData/doc创建

-- Module:CharacterAtlasData
-- Character atlas renderer using the Style Atlas frame classes.

local p = {}

local function trim(value)
    if value == nil then return "" end
    return mw.text.trim(tostring(value))
end

local function esc(value)
    return mw.text.encode(tostring(value 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 fetchProfiles(frame)
    local fields = table.concat({
        "*",
        "character.id",
        "character.name",
        "character.source_character_id",
        "archive_entries.id",
        "voice_lines.id",
        "gifts.id",
        "gifts.preference_type",
        "gifts.cognition_bonus",
        "gifts.gift.name",
        "memories.id",
        "styles.id",
        "rumors.id",
        "whispers.id",
    }, ",")
    local apiUrl = "https://data.saltedkiss.org/items/character_profiles"
        .. "?fields=" .. mw.uri.encode(fields, "QUERY")
        .. "&filter[status][_eq]=published"
        .. "&sort=" .. mw.uri.encode("character.source_character_id,character.id,display_name", "QUERY")
        .. "&limit=100"
    local raw = frame:preprocess(
        "{{#get_web_data:url=" .. apiUrl
        .. "|format=text|data=responseText=__text}}"
        .. "{{#external_value:responseText}}"
    )
    raw = trim(raw or "")
    local parsed = parseJson(raw)
    if parsed and type(parsed.data) == "table" then return parsed.data end
    return {}
end

local function firstText(...)
    for i = 1, select("#", ...) do
        local text = trim(select(i, ...))
        if text ~= "" then return text end
    end
    return ""
end

local function displayName(profile)
    return firstText(profile.display_name, profile.character and profile.character.name, profile.slug, "未知角色")
end

local function pageName(profile)
    return firstText(profile.slug, displayName(profile))
end

local function fileExists(fileName)
    if 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 fileWikitext(fileName, size)
    if fileName == "" then return "" end
    return "[[文件:" .. fileName .. "|" .. (size or "320px") .. "|link=]]"
end

local function characterArtFile(prefix, name, override)
    override = trim(override)
    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 portraitFile(profile)
    local name = displayName(profile)
    local explicit = characterArtFile("胸像", name, firstText(profile.bust_wiki_file, profile.portrait_wiki_file, profile.avatar_wiki_file))
    if explicit ~= "" and explicit ~= "胸像_" .. name .. ".png" then return explicit end
    local bust = characterArtFile("胸像", name, "")
    if fileExists(bust) then return bust end
    local dossier = "角色档案_" .. name .. "_档案头像.png"
    if fileExists(dossier) then return dossier end
    local head = "角色档案_" .. name .. "_头像.png"
    if fileExists(head) then return head end
    return ""
end

local function totalNodes(profile)
    local keys = { "archive_entries", "voice_lines", "gifts", "memories", "styles", "rumors", "whispers" }
    local total = 0
    for _, key in ipairs(keys) do
        if type(profile[key]) == "table" then total = total + #profile[key] end
    end
    return total
end

local function countProfilesWithPortrait(profiles)
    local count = 0
    for _, profile in ipairs(profiles) do
        local name = displayName(profile)
        if firstText(profile.bust_wiki_file, profile.portrait_wiki_file, profile.avatar_wiki_file) ~= ""
            or fileExists("胸像_" .. name .. ".png")
            or fileExists("角色档案_" .. name .. "_档案头像.png")
            or fileExists("角色档案_" .. name .. "_头像.png") then
            count = count + 1
        end
    end
    return count
end

local function characterOrder(profile)
    local character = profile.character
    if type(character) == "table" then
        local sourceId = tonumber(character.source_character_id)
        if sourceId then return sourceId end
        local id = tonumber(character.id)
        if id then return id end
    end
    return tonumber(profile.id) or 999999
end

local function profileFactRows(profile)
    local rows = {
        { "本名", firstText(profile.full_name, profile.name_full) },
        { "代号", firstText(profile.latin_name, profile.code_name) },
        { "生日", firstText(profile.birthday) },
        { "身高", firstText(profile.height) },
        { "出生地", firstText(profile.birthplace) },
        { "信息素", firstText(profile.pheromone) },
    }
    local filtered = {}
    for _, row in ipairs(rows) do
        if trim(row[2]) ~= "" then table.insert(filtered, row) end
    end
    return filtered
end

local function giftName(giftRow)
    if type(giftRow) ~= "table" then return "" end
    if type(giftRow.gift) == "table" then
        local related = firstText(giftRow.gift.name, giftRow.gift.display_name)
        if related ~= "" then return related end
    end
    return firstText(giftRow.name)
end

local function giftRows(profile)
    local rows = {}
    if type(profile.gifts) ~= "table" then return rows end
    for _, giftRow in ipairs(profile.gifts) do
        local name = giftName(giftRow)
        if name ~= "" then table.insert(rows, name) end
        if #rows >= 2 then break end
    end
    return rows
end


local function profileByKey(profiles, key)
    key = trim(key)
    if key == "" then key = "将军" end
    for _, profile in ipairs(profiles) do
        if trim(profile.slug) == key or displayName(profile) == key then return profile end
    end
    for _, profile in ipairs(profiles) do
        if trim(profile.slug) == "将军" or displayName(profile) == "将军" then return profile end
    end
    return profiles[1]
end

local function filterOption(parent, key, label, active)
    local item = parent:tag("div")
        :addClass("atlas_frame-filter-option")
        :attr("role", "button")
        :attr("tabindex", "0")
        :attr("data-character-atlas-filter", key)
    if active then item:addClass("is-active") end
    item:wikitext(label)
    return item
end

local function statusItem(parent, key, label, count, active)
    local row = parent:tag("div")
        :addClass("atlas_frame-status-item")
        :attr("role", "button")
        :attr("tabindex", "0")
        :attr("data-character-atlas-status", key)
    if active then row:addClass("is-active") end
    row:tag("span"):wikitext(label)
    row:tag("span"):addClass("atlas_frame-status-num"):wikitext(tostring(count))
end

local function moduleCount(profile, key)
    return type(profile[key]) == "table" and #profile[key] or 0
end

local function makeCharacterCard(profile, index, selected)
    local name = displayName(profile)
    local org = firstText(profile.organization)
    local pheromone = firstText(profile.pheromone)
    local card = mw.html.create("div")
        :addClass("character_atlas-card")
        :attr("role", "button")
        :attr("tabindex", "0")
        :attr("data-character-atlas-card", "1")
        :attr("data-character-atlas-name", name)
        :attr("data-character-atlas-org", org)
        :attr("data-character-atlas-pheromone", pheromone)
    if selected then card:addClass("is-active") end
    card:tag("div"):addClass("character_atlas-card-index"):wikitext(string.format("%02d", index))
    local media = card:tag("div"):addClass("character_atlas-card-media")
    media:tag("div"):addClass("character_atlas-card-portrait"):wikitext(fileWikitext(portraitFile(profile), "420px"))
    local hover = media:tag("div"):addClass("character_atlas-card-hover")
    local facts = hover:tag("div"):addClass("character_atlas-card-facts")
    local factRows = profileFactRows(profile)
    for _, fact in ipairs(factRows) do
        local factNode = facts:tag("div"):addClass("character_atlas-card-fact")
        factNode:tag("span"):addClass("character_atlas-card-fact-label"):wikitext(fact[1])
        factNode:tag("span"):addClass("character_atlas-card-fact-value"):wikitext(fact[2])
    end
    local gifts = giftRows(profile)
    if #gifts > 0 then
        local giftNode = hover:tag("div"):addClass("character_atlas-card-gifts")
        giftNode:tag("span"):addClass("character_atlas-card-gifts-label"):wikitext("礼物")
        local giftList = giftNode:tag("div"):addClass("character_atlas-card-gift-list")
        for _, gift in ipairs(gifts) do
            giftList:tag("span"):addClass("character_atlas-card-gift"):wikitext(gift)
        end
    end
    local body = card:tag("div"):addClass("character_atlas-card-body")
    body:tag("div"):addClass("character_atlas-card-kicker"):wikitext(org)
    body:tag("div"):addClass("character_atlas-card-name"):wikitext(name)
    body:tag("div"):addClass("character_atlas-card-meta"):wikitext(pheromone)
    body:tag("div"):addClass("character_atlas-card-count"):wikitext(tostring(totalNodes(profile)) .. " 条")
    body:tag("div"):addClass("character_atlas-card-link"):wikitext("[[" .. pageName(profile) .. "|档案]]")
    return card
end

local function renderEmpty()
    return '<div class="atlas_frame-root character_atlas-root"><div class="atlas_frame-surface-wrap"><div class="atlas_frame-surface"><div class="atlas_frame-shell"><div class="atlas_frame-empty">角色图鉴暂无数据</div></div></div></div></div>'
end

function p.render(frame)
    local profiles = fetchProfiles(frame)
    if #profiles == 0 then return renderEmpty() end
    table.sort(profiles, function(a, b)
        local orderA = characterOrder(a)
        local orderB = characterOrder(b)
        if orderA ~= orderB then return orderA < orderB end
        return displayName(a) < displayName(b)
    end)

    local selected = profileByKey(profiles, frame.args[1] or frame.args.selected or "将军")

    local root = mw.html.create("div")
        :addClass("atlas_frame-root")
        :addClass("character_atlas-root")
        :attr("data-character-atlas-root", "1")
        :attr("data-atlas-frame-sandbox", "1")

    local hero = root:tag("div"):addClass("atlas_frame-hero")
    hero:tag("div"):addClass("atlas_frame-hero-art card_fullscreen-img character_atlas-hero-bg")
    local heroInner = hero:tag("div"):addClass("atlas_frame-hero-inner")
    heroInner:tag("div"):addClass("atlas_frame-kicker"):wikitext("Wiki Atlas Prototype")
    heroInner:tag("div"):addClass("atlas_frame-title")
        :wikitext("角色图鉴")
        :tag("span"):addClass("atlas_frame-title-sub"):wikitext("CHARACTER")

    local status = hero:tag("div")
        :addClass("atlas_frame-status")
        :attr("role", "group")
        :attr("aria-label", "角色图鉴状态")
    statusItem(status, "all", "全部角色", #profiles, true)

    local surface = root:tag("div"):addClass("atlas_frame-surface-wrap")
        :tag("div"):addClass("atlas_frame-surface")
        :tag("div"):addClass("atlas_frame-shell")

    local toolbar = surface:tag("div"):addClass("atlas_frame-toolbar")
    local primary = toolbar:tag("div"):addClass("atlas_frame-filter-row atlas_frame-filter-row--primary")
    local left = primary:tag("div")
        :addClass("atlas_frame-filter-left")
        :attr("role", "group")
        :attr("aria-label", "角色筛选")
    local group = left:tag("div")
        :addClass("atlas_frame-filter-group")
        :addClass("character_atlas-filter-group")
        :attr("data-character-atlas-filter-group", "basic")
    group:tag("span"):addClass("atlas_frame-filter-label"):wikitext("目录")
    filterOption(group, "all", "全部", true)
    filterOption(group, "org", "组织", false)
    filterOption(group, "pheromone", "信息素", false)
    filterOption(group, "profile", "档案", false)

    local search = primary:tag("div"):addClass("atlas_frame-search"):attr("role", "search")
    search:tag("span"):addClass("atlas_frame-search-mark")
    search:tag("div")
        :addClass("atlas_frame-search-input")
        :attr("role", "textbox")
        :attr("tabindex", "0")
        :attr("contenteditable", "true")
        :attr("spellcheck", "false")
        :attr("aria-label", "搜索角色名、组织或信息素")
        :attr("data-atlas-placeholder", "搜索角色名、组织或信息素")
        :attr("data-character-atlas-search", "1")

    local tagsRow = toolbar:tag("div"):addClass("atlas_frame-filter-row atlas_frame-filter-row--tags character_atlas-summary-row")
    tagsRow:tag("div"):addClass("atlas_frame-tag-toggle character_atlas-summary")
        :attr("role", "button")
        :attr("tabindex", "0")
        :wikitext("当前以 Directus 已发布角色档案为数据源")
    tagsRow:tag("div"):addClass("character_atlas-summary-count"):wikitext(tostring(#profiles) .. " 个角色")

    local layout = surface:tag("div"):addClass("atlas_frame-layout character_atlas-layout")
    local grid = layout:tag("div")
        :addClass("atlas_frame-grid")
        :addClass("atlas_frame-grid--card-protos")
        :addClass("character_atlas-grid")
        :attr("aria-label", "角色图鉴列表")
    for i, profile in ipairs(profiles) do
        grid:node(makeCharacterCard(profile, i, profile == selected))
    end
    grid:tag("div")
        :addClass("atlas_frame-empty")
        :attr("data-character-atlas-empty", "1")
        :attr("hidden", "hidden")
        :wikitext("没有符合条件的角色")
    return tostring(root)
end

return p