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