模块:CardData
来自夜幕之下 Wiki - Reign of Nightfall 中文资料站
更多操作
此模块的文档可以在模块:CardData/doc创建
-- Module:CardData
-- 用法:{{#invoke:CardData|render|复仇童谣}}
local p = {}
local AttrGrowth = require("Module:StyleAttributeGrowth")
local WeaponAttack = require("Module:WeaponAttackData")
local function val(v, fallback)
if v == nil or v == "" then return fallback or "—" end
return v
end
local function rarityStars(n)
n = tonumber(n) or 0
return string.rep("★", n)
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 makeTitle(titleText, subtitleText, extraClass)
local div = mw.html.create("div")
:addClass("card_content-item-title")
:addClass("card_content-item-title--mt")
if extraClass then div:addClass(extraClass) end
div:wikitext(titleText)
div:tag("span"):addClass("card_content-item-subtitle"):wikitext(subtitleText)
return div
end
local function makeHr(extraClass)
local div = mw.html.create("div"):addClass("card_content-item-hr")
if extraClass then div:addClass(extraClass) end
return div
end
-- 单条属性行:图标 + 标签 + 值
local function makeAttrRow(label, value)
local item = mw.html.create("div"):addClass("card_content_attribute-item")
local titleDiv = item:tag("div"):addClass("card_content_attribute-title")
titleDiv:tag("div"):addClass("card_content_attribute-icon")
titleDiv:tag("div"):wikitext(" " .. label)
item:tag("div"):wikitext(value or "—")
return item
end
local function formatAttrNumber(value)
local n = tonumber(value)
if not n then return "—" end
return mw.getContentLanguage():formatNum(math.floor(n))
end
local function makeDynamicAttrRow(label, key, value)
local item = mw.html.create("div"):addClass("card_content_attribute-item style_attr-stat-row")
local titleDiv = item:tag("div"):addClass("card_content_attribute-title")
titleDiv:tag("div"):addClass("card_content_attribute-icon")
titleDiv:tag("div"):wikitext(" " .. label)
item:tag("div")
:addClass("style_attr-stat-value")
:attr("data-style-attr-stat", key)
:wikitext(formatAttrNumber(value))
return item
end
local function addAttrDataAttrs(node, payload)
local data = payload and payload.data or {}
node:attr("data-style-attr-growth", "1")
:attr("data-default-level", data.default_level or 100)
:attr("data-formula-version", data.formula_version or "hero_level_growth_v1")
:attr("data-base-hp", data.base_hp)
:attr("data-base-atk", data.base_atk)
:attr("data-base-def", data.base_def)
:attr("data-rate-hp", data.hp_rate)
:attr("data-rate-atk", data.atk_rate)
:attr("data-rate-def", data.def_rate)
:attr("data-break-hp", data.break_hp)
:attr("data-break-atk", data.break_atk)
:attr("data-break-def", data.break_def)
end
local function makeStyleAttributeGrowthSection(card)
local payload = AttrGrowth.payload(card, 100)
local section = mw.html.create("div"):addClass("style_attr-growth")
section:node(makeTitle("等级", "Level", "style_attr-title"))
section:node(makeHr())
if not payload or not payload.result then
section:tag("div"):addClass("style_attr-empty"):wikitext("(属性成长数据待补充)")
return section
end
addAttrDataAttrs(section, payload)
local result = payload.result
local levelRow = section:tag("div"):addClass("style_attr-level-row")
local levelDisplay = levelRow:tag("div"):addClass("style_attr-level-display")
levelDisplay:tag("span"):addClass("style_attr-title-lv"):wikitext("Lv.")
levelDisplay:tag("span"):addClass("style_attr-title-level")
:attr("data-style-attr-level-number", "1")
:wikitext(tostring(result.level))
levelRow:tag("span"):addClass("style_attr-break style_attr-break--title")
:attr("data-style-attr-break-label", "1")
:wikitext("晋升 " .. tostring(result.break_count))
local slider = section:tag("div")
:addClass("style_attr-slider")
:attr("role", "slider")
:attr("tabindex", "0")
:attr("aria-label", "等级")
:attr("aria-valuemin", "1")
:attr("aria-valuemax", "100")
:attr("aria-valuenow", tostring(result.level))
:attr("data-style-attr-slider", "1")
slider:tag("div"):addClass("style_attr-slider-track")
:tag("div"):addClass("style_attr-slider-fill"):css("width", "100%")
slider:tag("div"):addClass("style_attr-slider-thumb"):css("left", "100%")
local presets = section:tag("div"):addClass("style_attr-presets")
local presetLevels = { 1, 20, 40, 60, 80, 100 }
for _, level in ipairs(presetLevels) do
presets:tag("div")
:addClass("style_attr-preset")
:attr("role", "button")
:attr("tabindex", "0")
:attr("data-style-attr-preset", tostring(level))
:wikitext(tostring(level))
end
section:node(makeDynamicAttrRow("生命", "hp", result.hp))
section:node(makeDynamicAttrRow("攻击", "atk", result.atk))
section:node(makeDynamicAttrRow("防御", "def", result.def))
return section
end
local collectionQualityMeta = {
[1] = { code = "C", class = "style_collection-quality--c" },
[2] = { code = "B", class = "style_collection-quality--b" },
[3] = { code = "A", class = "style_collection-quality--a" },
[4] = { code = "S", class = "style_collection-quality--s" },
}
local collectionAttrOrder = {
[27] = 1, [28] = 2, [29] = 3,
[23] = 4, [21] = 5, [24] = 6,
[8] = 7, [9] = 8, [12] = 9, [13] = 10,
[14] = 11, [11] = 12, [22] = 13, [58] = 14, [59] = 15, [60] = 16,
}
local collectionPercentAttrs = {
[8] = true, [9] = true, [11] = true, [12] = true,
[13] = true, [14] = true, [21] = true, [22] = true,
[23] = true, [24] = true, [58] = true, [59] = true, [60] = true,
}
local function collectionRows(input)
if type(input) ~= "table" then return {} end
return input
end
local function collectionFormatPercent(raw)
local n = (tonumber(raw) or 0) / 100
local text
if math.floor(n) == n then
text = string.format("%.0f", n)
elseif math.floor(n * 10) == n * 10 then
text = string.format("%.1f", n)
else
text = string.format("%.2f", n)
end
return "+" .. text .. "%"
end
local function collectionFormatAttr(attrId, raw)
local n = tonumber(raw)
if not n then return "—" end
if collectionPercentAttrs[tonumber(attrId)] then
return collectionFormatPercent(n)
end
return "+" .. mw.getContentLanguage():formatNum(math.floor(n))
end
local function collectionTermDescription(term)
if not term then return "—" end
if tonumber(term.effect_type) == 1 and term.attr_id and term.attr_value then
return val(term.attr_name or term.name, "属性") .. " " .. collectionFormatAttr(term.attr_id, term.attr_value)
end
return val(term.description, "效果描述待补充")
end
local function sortedCollectionFixedRows(rows)
local out = {}
for _, row in ipairs(collectionRows(rows)) do
if type(row) == "table" then table.insert(out, row) end
end
table.sort(out, function(a, b)
local ap = tonumber(a.position) or 99
local bp = tonumber(b.position) or 99
if ap ~= bp then return ap < bp end
local as = tonumber(a.slot_index) or 99
local bs = tonumber(b.slot_index) or 99
if as ~= bs then return as < bs end
return tostring(a.attr_name or "") < tostring(b.attr_name or "")
end)
return out
end
local function aggregateCollectionFixed(rows)
local byAttr = {}
for _, row in ipairs(sortedCollectionFixedRows(rows)) do
local attrId = tonumber(row.attr_id)
if attrId then
local item = byAttr[attrId]
if not item then
item = {
attr_id = attrId,
attr_name = val(row.attr_name, "属性" .. tostring(attrId)),
count = 0,
raw = 0,
}
byAttr[attrId] = item
end
item.count = item.count + 1
local raw = tostring(row.source_raw or ""):match("#(%d+)$")
item.raw = item.raw + (tonumber(raw) or 0)
end
end
local out = {}
for _, item in pairs(byAttr) do table.insert(out, item) end
table.sort(out, function(a, b)
local ao = collectionAttrOrder[a.attr_id] or 999
local bo = collectionAttrOrder[b.attr_id] or 999
if ao ~= bo then return ao < bo end
return tostring(a.attr_name) < tostring(b.attr_name)
end)
return out
end
local function sortedCollectionTerms(card)
local terms = {}
local pools = collectionRows(card.collection_refine_pools)
for _, pool in ipairs(pools) do
for _, term in ipairs(collectionRows(pool.terms)) do
if type(term) == "table" then
term.__pool_id = pool.pool_id or card.collection_refine_pool
table.insert(terms, term)
end
end
end
table.sort(terms, function(a, b)
local aq = tonumber(a.quality) or 0
local bq = tonumber(b.quality) or 0
if aq ~= bq then return aq > bq end
local ae = tonumber(a.effect_id) or 999
local be = tonumber(b.effect_id) or 999
if ae ~= be then return ae < be end
return tostring(a.name or "") < tostring(b.name or "")
end)
return terms
end
local function collectionFormatWeightPercent(weight, total)
local w = tonumber(weight) or 0
local t = tonumber(total) or 0
if w <= 0 or t <= 0 then
return "0%"
end
local p = w * 100 / t
if p < 1 then
return string.format("%.1f%%", p)
elseif p < 10 then
return string.format("%.1f%%", p)
end
return string.format("%.0f%%", p)
end
local function renderCollectionTerm(parent, term, totalWeight)
local quality = collectionQualityMeta[tonumber(term.quality)] or collectionQualityMeta[1]
local node = parent:tag("div")
:addClass("style_collection-term")
:addClass("style_collection-term--q" .. tostring(tonumber(term.quality) or 0))
if (tonumber(term.first_weight) or 0) > 0 then
node:addClass("style_collection-term--default")
end
local head = node:tag("div"):addClass("style_collection-term-head")
head:tag("span")
:addClass("style_collection-quality " .. quality.class)
:wikitext(quality.code)
local title = head:tag("span"):addClass("style_collection-term-title")
title:tag("span"):addClass("style_collection-term-name"):wikitext(val(term.name, "词条"))
if (tonumber(term.first_weight) or 0) > 0 then
title:tag("span"):addClass("style_collection-term-default"):wikitext("初始")
end
local weight = head:tag("span"):addClass("style_collection-term-weight")
weight:tag("span"):addClass("style_collection-term-weight-label"):wikitext("权重")
weight:tag("span"):addClass("style_collection-term-weight-value"):wikitext(collectionFormatWeightPercent(term.normal_weight, totalWeight))
node:tag("div"):addClass("style_collection-term-desc"):wikitext(collectionTermDescription(term))
end
local function makeStyleCollectionSection(card)
local fixedRows = sortedCollectionFixedRows(card.collection_fixed_attributes)
local terms = sortedCollectionTerms(card)
if #fixedRows == 0 and #terms == 0 then
local section = mw.html.create("div"):addClass("style_collection")
section:node(makeTitle("映像", "Reflection"))
section:node(makeHr())
section:tag("div"):addClass("style_attr-empty"):wikitext("(映像数据待补充)")
return section
end
local section = mw.html.create("div")
:addClass("style_collection")
:attr("data-style-collection", "1")
section:node(makeTitle("映像", "Reflection"))
section:node(makeHr())
local summary = section:tag("div"):addClass("style_collection-summary")
section:attr("data-style-collection-expanded", "false")
for _, item in ipairs(aggregateCollectionFixed(fixedRows)) do
local row = summary:tag("div"):addClass("card_content_attribute-item style_collection-summary-row")
local titleDiv = row:tag("div"):addClass("card_content_attribute-title")
titleDiv:tag("div"):addClass("card_content_attribute-icon")
titleDiv:tag("div"):wikitext(" " .. item.attr_name)
local value = row:tag("div"):addClass("style_collection-summary-value")
value:tag("span"):addClass("style_collection-summary-count"):wikitext("×" .. tostring(item.count))
value:tag("span"):addClass("style_collection-summary-number"):wikitext(collectionFormatAttr(item.attr_id, item.raw))
end
local toggle = section:tag("div")
:addClass("style_collection-toggle")
:attr("role", "button")
:attr("tabindex", "0")
:attr("data-style-collection-toggle", "1")
toggle:tag("span"):addClass("style_collection-toggle-text"):wikitext("展开映像详情")
toggle:tag("span"):addClass("style_collection-toggle-icon"):wikitext("+")
local attrDetails = section:tag("div")
:addClass("style_collection-attr-details")
:attr("data-style-collection-extra", "1")
local positions = attrDetails:tag("div"):addClass("style_collection-slots")
for position = 1, 3 do
local slot = positions:tag("div"):addClass("style_collection-slot")
slot:tag("div"):addClass("style_collection-slot-title"):wikitext("映像 " .. tostring(position))
for _, row in ipairs(fixedRows) do
if tonumber(row.position) == position then
local attr = slot:tag("div"):addClass("style_collection-slot-attr")
attr:tag("span"):wikitext(val(row.attr_name, "属性"))
local raw = tostring(row.source_raw or ""):match("#(%d+)$")
attr:tag("span"):addClass("style_collection-slot-value"):wikitext(collectionFormatAttr(row.attr_id, raw))
end
end
end
local featured = section:tag("div"):addClass("style_collection-featured")
local totalNormalWeight = 0
for _, term in ipairs(terms) do
totalNormalWeight = totalNormalWeight + (tonumber(term.normal_weight) or 0)
end
local featuredCount = 0
local extraCount = 0
for _, term in ipairs(terms) do
local q = tonumber(term.quality) or 0
if q >= 3 then
renderCollectionTerm(featured, term, totalNormalWeight)
featuredCount = featuredCount + 1
else
extraCount = extraCount + 1
end
end
if featuredCount == 0 then
featured:tag("div"):addClass("style_attr-empty"):wikitext("(暂无 S / A 词条)")
end
local extraTerms = section:tag("div")
:addClass("style_collection-extra-terms")
:attr("data-style-collection-extra", "1")
if extraCount == 0 then
extraTerms:tag("div"):addClass("style_attr-empty"):wikitext("(暂无更多词条)")
else
for _, term in ipairs(terms) do
local q = tonumber(term.quality) or 0
if q < 3 then
renderCollectionTerm(extraTerms, term, totalNormalWeight)
end
end
end
return section
end
local function makeInfoSection(intel, supply, execute, strategy, rarity)
local infoDiv = mw.html.create("div"):addClass("card_content_info")
local rarityItem = infoDiv:tag("div"):addClass("card_content_info-item")
rarityItem:tag("div"):wikitext("稀有度")
rarityItem:tag("div"):wikitext(rarityStars(rarity))
local dims = {
{ label = "情报", value = intel },
{ label = "物资", value = supply },
{ label = "执行", value = execute },
{ label = "策略", value = strategy },
}
for _, dim in ipairs(dims) do
local item = infoDiv:tag("div"):addClass("card_content_info-item")
item:tag("div"):wikitext(dim.label)
local n = tonumber(dim.value)
item:tag("div"):wikitext(n and ("※" .. n) or "—")
end
return infoDiv
end
local function makeTriggerLabel(triggerType, triggerValue)
local span = mw.html.create("span"):addClass("card_content_skill-meta-val")
local trigger = triggerType ~= nil and mw.text.trim(tostring(triggerType)) or ""
if trigger == "normal_attack" then
span:wikitext("每进行")
span:tag("span"):addClass("card_accent"):wikitext(tostring(triggerValue or "?"))
span:wikitext("次普攻")
elseif trigger == "interval" then
span:wikitext("冷却时间")
span:tag("span"):addClass("card_accent"):wikitext(tostring(triggerValue or "?"))
span:wikitext("秒")
elseif trigger == "hit_taken" or trigger == "每受到x次攻击" or trigger == "每受到X次攻击" then
span:wikitext("每受到")
span:tag("span"):addClass("card_accent"):wikitext(tostring(triggerValue or "?"))
span:wikitext("次攻击")
else
span:wikitext(val(triggerType))
end
return span
end
local function normalizeTags(tagsValue)
if not tagsValue then return nil end
if type(tagsValue) == "table" then return tagsValue end
if type(tagsValue) == "string" then return parseJson(tagsValue) end
return nil
end
local function makeTagsRow(tagsValue, rowClass, tagClass)
local div = mw.html.create("div"):addClass(rowClass or "card_content_skill-tags")
local tags = normalizeTags(tagsValue)
if tags and type(tags) == "table" then
for _, tag in ipairs(tags) do
local text = mw.text.trim(tostring(tag or ""))
if text ~= "" then
div:tag("div"):addClass(tagClass or "card_content_skill-tag"):wikitext(text)
end
end
end
return div
end
local function makeStyleTagsRow(tagsValue)
local tags = normalizeTags(tagsValue)
if not tags or type(tags) ~= "table" or #tags == 0 then return nil end
return makeTagsRow(tags, "card_content_style-tags card_content_skill-tags", "card_content_style-tag card_content_skill-tag")
end
local function skillParamRows(levelsJson)
local rows = parseJson(levelsJson)
if not rows or type(rows) ~= "table" then return {}, {} end
local ordered = {}
local byIndex = {}
for idx, row in ipairs(rows) do
local paramIndex = tonumber(row.source_param_index or row.sort or idx) or idx
row.__param_index = paramIndex
table.insert(ordered, row)
byIndex[paramIndex] = row
end
table.sort(ordered, function(a, b)
local ai = tonumber(a.__param_index) or 9999
local bi = tonumber(b.__param_index) or 9999
if ai ~= bi then return ai < bi end
return tostring(a.name or "") < tostring(b.name or "")
end)
return ordered, byIndex
end
local function skillParamValue(paramRow, level)
if not paramRow or type(paramRow.levels) ~= "table" then return "—" end
local lv = tonumber(level) or 1
local value = paramRow.levels[lv]
if value == nil or value == "" then
value = paramRow.levels[1]
end
if value == nil or value == "" then return "—" end
return tostring(value)
end
local function addSkillDescriptionText(node, text)
if text and text ~= "" then node:wikitext(text) end
end
local function addEffectTemplateText(node, template)
local text = mw.text.trim(tostring(template or ""))
if text == "" then return end
local pos = 1
while true do
local fStart, fEnd, effectName = string.find(text, "{fx:([^}]+)}", pos)
if not fStart then break end
addSkillDescriptionText(node, string.sub(text, pos, fStart - 1))
local label = mw.text.trim(effectName or "")
local effect = node:tag("span")
:addClass("card_content_skill-effect-term")
:attr("data-skill-effect", label)
effect:wikitext("[" .. label .. "]")
pos = fEnd + 1
end
addSkillDescriptionText(node, string.sub(text, pos))
end
local function makeSkillDescription(skillData, displayLevel)
local wrap = mw.html.create("div"):addClass("card_content_skill_effect")
local template = ""
if skillData then
if skillData.description_template ~= nil then
template = mw.text.trim(tostring(skillData.description_template))
end
if template == "" and skillData.description ~= nil then
template = mw.text.trim(tostring(skillData.description))
end
end
if template == "" then return wrap end
local _, paramsByIndex = skillParamRows(skillData.levels)
local pos = 1
while true do
local pStart, pEnd, pIndex = string.find(template, "{p(%d+)}", pos)
local fStart, fEnd, effectName = string.find(template, "{fx:([^}]+)}", pos)
if not pStart and not fStart then break end
local useParam = pStart and (not fStart or pStart < fStart)
local startPos = useParam and pStart or fStart
local endPos = useParam and pEnd or fEnd
addSkillDescriptionText(wrap, string.sub(template, pos, startPos - 1))
if useParam then
local index = tonumber(pIndex)
local value = skillParamValue(paramsByIndex[index], displayLevel or 1)
local span = wrap:tag("span")
:addClass("card_content_skill-param")
:attr("data-skill-param-index", tostring(index))
:attr("data-skill-param-level", tostring(displayLevel or 1))
span:wikitext(value)
span:tag("sup")
:addClass("card_content_skill-param-index")
:wikitext(tostring(index))
else
addEffectTemplateText(wrap, string.sub(template, fStart, fEnd))
end
pos = endPos + 1
end
addSkillDescriptionText(wrap, string.sub(template, pos))
return wrap
end
local function makeUpgradeTable(levelsJson)
local wrap = mw.html.create("div"):addClass("card_content_skill-upgrade")
local rows = parseJson(levelsJson)
if not rows or type(rows) ~= "table" or #rows == 0 then return wrap end
local maxCols = 0
for _, row in ipairs(rows) do
if type(row.levels) == "table" then
local displayCols = #row.levels >= 10 and (#row.levels - 1) or #row.levels
if displayCols > maxCols then
maxCols = displayCols
end
end
end
local DATA_COLS, gridClass
if maxCols > 8 then
DATA_COLS = 9
gridClass = "card_content_skill-upgrade-grid card_content_skill-upgrade-grid--10"
else
DATA_COLS = 8
gridClass = "card_content_skill-upgrade-grid"
end
local grid = wrap:tag("div"):addClass(gridClass)
grid:tag("div")
for i = 2, DATA_COLS + 1 do
grid:tag("div"):addClass("card_content_skill-upgrade-lv"):wikitext(tostring(i))
end
for idx, row in ipairs(rows) do
local labelCell = grid:tag("div"):addClass("card_content_skill-upgrade-row-label")
labelCell:tag("span"):addClass("card_content_skill-upgrade-badge"):wikitext(tostring(idx))
labelCell:tag("span"):addClass("card_content_skill-upgrade-badge-text"):wikitext(val(row.name, ""))
labelCell:tag("span"):addClass("card_content_skill-upgrade-arrow"):wikitext("→")
local levels = (type(row.levels) == "table") and row.levels or {}
local offset = #levels >= 10 and 1 or 0
for col = 1, DATA_COLS do
local v = levels[col + offset]
local cell = grid:tag("div"):addClass("card_content_skill-upgrade-val")
if v == nil or v == "" then
cell:addClass("card_content_skill-upgrade-val--null"):wikitext("—")
else
cell:addClass("card_accent"):wikitext(tostring(v))
end
end
end
return wrap
end
local function makeNameIcon(name, extraClass, iconSize)
local icon = mw.html.create("div"):addClass("card_content_name-icon")
if extraClass then icon:addClass(extraClass) end
local iconName = name == nil and "" or mw.text.trim(tostring(name))
local size = iconSize or "32x32px"
if iconName ~= "" and iconName ~= "—" then
icon:wikitext("[[文件:图标_" .. iconName .. ".png|" .. size .. "|link=]]")
end
return icon
end
local function makeSkillIcon(skillData, skillType, stylename)
local icon = mw.html.create("div"):addClass("card_content_skill-icon")
local skillName = skillData and skillData.name and tostring(skillData.name) or ""
local styleName = stylename and tostring(stylename) or ""
if skillName ~= "" and styleName ~= "" and styleName ~= "—" then
icon:wikitext("[[文件:技能_" .. styleName .. "_" .. skillName .. ".png|48x48px]]")
end
return icon
end
local function fetchWeaponAttackProfile(frame, card, skillData)
local mounted = WeaponAttack.pickProfile(card.weapon_attack_profiles, skillData)
if mounted then return mounted end
local sourceStyleId = card.source_style_id
if (sourceStyleId == nil or sourceStyleId == "") and card.attribute_growths and type(card.attribute_growths) == "table" and card.attribute_growths[1] then
sourceStyleId = card.attribute_growths[1].source_style_id
end
if sourceStyleId == nil or sourceStyleId == "" then return nil end
local profileUrl = "https://data.saltedkiss.org/items/weapon_attack_profiles"
.. "?fields=status,source_style_id,normal_skill_id,normal_skill_name,weapon_type,attack_range"
.. ",avg_ms_per_hit,hit_events_per_sec,action_cycles_per_sec,real_attack_cycle_ms"
.. ",hit_event_count,reload_count,reload_time_ms,fire_frames"
.. ",aim_target_rule,aim_time_ms,base_atk_spd_for_aim,real_aim_time_baseline_ms,real_aim_time_formula"
.. ",normal_action_ms_raw,normal_action_ms_tick,normal_action_tick_formula,normal_hit_event_count"
.. ",reload_per_action_ms,sustained_cycle_ms_no_aim,sustained_actions_per_sec_no_aim,sustained_hit_events_per_sec_no_aim"
.. ",first_magazine_cycle_ms_with_aim,first_magazine_actions_per_sec_with_aim,first_magazine_hit_events_per_sec_with_aim"
.. ",speed_formula_version,speed_formula_summary"
.. ",verification_status,video_observed_hits_per_sec,verification_note"
.. ",template.template_name,template.weapon_type,template.avg_ms_per_hit"
.. ",template.hit_events_per_sec,template.real_attack_cycle_ms,template.attack_range,template.hit_count"
.. ",template.reload_count,template.reload_time_ms,template.fire_frames_pattern"
.. "&filter[source_style_id][_eq]=" .. mw.uri.encode(tostring(sourceStyleId), "QUERY")
.. "&filter[status][_eq]=published"
.. "&limit=1"
local rawProfile = frame:preprocess(
'{{#get_web_data:url=' .. profileUrl
.. '|format=text|data=responseText=__text}}'
.. '{{#external_value:responseText}}'
)
rawProfile = mw.text.trim(rawProfile or "")
local parsedProfile = parseJson(rawProfile)
if parsedProfile and type(parsedProfile.data) == "table" then
return parsedProfile.data[1]
end
return nil
end
local function makeSkillCard(skillData, skillType, stylename, weaponAttackPayload)
local card = mw.html.create("div"):addClass("card_content_skill-card")
local left = card:tag("div"):addClass("card_content_skill-left")
local titleRow = left:tag("div"):addClass("card_content_skill-title")
titleRow:node(makeSkillIcon(skillData, skillType, stylename))
local titleText = titleRow:tag("div"):addClass("card_content_skill-title-text")
if weaponAttackPayload then titleText:addClass("card_weapon-popover-root") end
local nameRow = titleText:tag("div")
nameRow:tag("span"):addClass("card_content_skill-name"):wikitext(val(skillData.name))
local typeSpan = nameRow:tag("span"):addClass("card_content_skill-type card_accent--type")
if skillType == "normal_attack" and weaponAttackPayload then
typeSpan:wikitext("「")
typeSpan:node(WeaponAttack.makeTrigger(val(skillData.type), weaponAttackPayload, "card_weapon-trigger--type"))
typeSpan:wikitext("」")
else
typeSpan:wikitext("「" .. val(skillData.type) .. "」")
end
local metaRow = titleText:tag("div"):addClass("card_content_skill-meta")
if skillType == "normal_attack" then
metaRow:wikitext("使用武器 ")
local weaponSpan = metaRow:tag("span"):addClass("card_content_skill-meta-val card_accent")
if weaponAttackPayload then
weaponSpan:node(WeaponAttack.makeTrigger(val(skillData.weapon), weaponAttackPayload, "card_weapon-trigger--weapon"))
else
weaponSpan:wikitext(val(skillData.weapon))
end
elseif skillType == "passive" then
metaRow:wikitext("触发条件 ")
metaRow:node(makeTriggerLabel(skillData.trigger_type, skillData.trigger_value))
elseif skillType == "ultimate" then
metaRow:wikitext("欲火消耗 ")
metaRow:tag("span"):addClass("card_content_skill-meta-val")
:tag("span"):addClass("card_accent"):wikitext(val(skillData.desire_cost))
end
if skillType == "passive" or skillType == "ultimate" then
left:node(makeTagsRow(skillData.tags))
end
if skillType == "normal_attack" and weaponAttackPayload then
titleText:node(WeaponAttack.makePopover(weaponAttackPayload))
end
left:node(makeSkillDescription(skillData, 1))
card:node(makeUpgradeTable(skillData.levels))
return card
end
local function makeFeatList(stagesJson)
local ul = mw.html.create("ul"):addClass("card_content_feat-ul")
local stages = parseJson(stagesJson)
if not stages or type(stages) ~= "table" then
ul:tag("li"):wikitext("—")
return ul
end
for _, stage in ipairs(stages) do
local li = ul:tag("li"):addClass("card_content_feat-list")
local row = li:tag("div"):addClass("card_content_feat-row")
local sw = row:tag("div"):addClass("card_content_feat-stage-wrap")
sw:tag("p"):tag("span"):addClass("card_content_feat-stage"):wikitext("STAGE")
sw:tag("div"):addClass("card_content_feat-number")
:wikitext(string.format("%02d", stage.stage or 0))
local rd = row:tag("div")
if stage.value and type(stage.value) == "table" then
for _, v in ipairs(stage.value) do
if v.description then
local desc = rd:tag("span"):addClass("card_accent")
addEffectTemplateText(desc, v.description)
rd:tag("br")
end
end
end
if stage.stat_boosts and type(stage.stat_boosts) == "table" then
local boostTexts = {}
for _, boost in ipairs(stage.stat_boosts) do
local typeText = boost.type or ""
local valueText = boost.value or ""
if typeText ~= "" or valueText ~= "" then
table.insert(boostTexts, typeText .. "提升" .. valueText)
end
end
if #boostTexts > 0 then
rd:wikitext(table.concat(boostTexts, ","))
end
end
if stage.extra_name then
rd:tag("span"):addClass("card_content_feat-stage-cn")
:wikitext("「" .. stage.extra_name .. "」")
end
if stage.extra_description then
local extraDesc = rd:tag("span"):addClass("card_content_feat-extra-desc")
addEffectTemplateText(extraDesc, stage.extra_description)
end
end
return ul
end
local function cleanText(value)
if value == nil then return "" end
return mw.text.trim(tostring(value))
end
local function rewardTypeLabel(rewardType)
local labels = {
label = "标签",
avatar_frame = "头像框",
title = "称号",
namecard = "名片",
chat_bubble = "聊天气泡",
cosmetic = "装扮",
other = "奖励",
}
return labels[cleanText(rewardType)] or "奖励"
end
local function rewardTypeClass(rewardType)
local key = cleanText(rewardType)
if key == "label" or key == "avatar_frame" or key == "title"
or key == "namecard" or key == "chat_bubble" or key == "cosmetic" then
return "card_content_reward--" .. key
end
return "card_content_reward--other"
end
local function rewardFileName(fileName)
local name = cleanText(fileName)
if name == "" then return "" end
name = name:gsub("^文件:", ""):gsub("^File:", "")
return name
end
local function defaultRewardFile(itemType, imageKey, mode)
local key = cleanText(imageKey)
if key == "" then return "" end
local itemIconTypes = {
label = true,
avatar_frame = true,
namecard = true,
}
if mode == "icon" and itemIconTypes[cleanText(itemType)] then
return "道具_" .. key .. ".png"
end
local prefixes = {
title = { display = "称号_", icon = "道具_" },
chat_bubble = { display = "聊天气泡_", icon = "道具_" },
cosmetic = { display = "装扮_", icon = "道具_" },
other = { display = "奖励_", icon = "道具_" },
}
local selected = prefixes[cleanText(itemType)] or prefixes.other
local prefix = selected[mode] or selected.display
return prefix .. key .. ".png"
end
local function firstText(...)
for i = 1, select("#", ...) do
local text = cleanText(select(i, ...))
if text ~= "" then return text end
end
return ""
end
local function acquisitionLabel(acquisitionType)
local key = cleanText(acquisitionType)
if key == "limited" or key == "限定" then return "限定" end
if key == "permanent" or key == "常驻" then return "常驻" end
return key
end
local function isLimitedAcquisition(acquisitionType)
local key = cleanText(acquisitionType)
return key == "limited" or key == "限定"
end
local function formatReleaseDate(value)
local text = cleanText(value)
if text == "" then return "" end
text = text:gsub("T.*$", "")
text = text:gsub("-", ".")
return text
end
local function makeAvailabilitySection(card)
if not card or type(card) ~= "table" then return nil end
local poolData = type(card.summon_pool) == "table" and card.summon_pool or {}
local acquisition = acquisitionLabel(card.acquisition_type)
local releaseDate = formatReleaseDate(card.release_date)
local poolName = firstText(poolData.name, poolData.pool_id, card.summon_pool_name, card.summon_pool_id)
local poolFile = rewardFileName(firstText(poolData.title_wiki_file, card.summon_pool_title_wiki_file))
local limited = isLimitedAcquisition(card.acquisition_type)
if acquisition == "" and releaseDate == "" and poolName == "" and poolFile == "" then
return nil
end
local node = mw.html.create("div"):addClass("card_content_availability")
if limited then
node:addClass("card_content_availability--limited")
else
node:addClass("card_content_availability--permanent")
end
if limited and poolFile ~= "" then
node:tag("div"):addClass("card_content_availability-pool-image")
:wikitext("[[文件:" .. poolFile .. "|280px|link=]]")
elseif limited and poolName ~= "" then
node:tag("div"):addClass("card_content_availability-pool-name")
:wikitext(poolName)
end
local top = node:tag("div"):addClass("card_content_availability-top")
if limited then
top:tag("span"):addClass("card_content_availability-badge")
:wikitext(acquisition ~= "" and acquisition or "限定")
end
if releaseDate ~= "" then
local dateNode = top:tag("span"):addClass("card_content_availability-date")
dateNode:tag("span"):addClass("card_content_availability-date-label"):wikitext("上线日期")
dateNode:tag("span"):addClass("card_content_availability-date-value"):wikitext(releaseDate)
end
if (not limited) and poolName ~= "" then
node:tag("div"):addClass("card_content_availability-pool-name")
:wikitext("卡池 " .. poolName)
end
return node
end
local function rewardItemData(reward)
if reward and type(reward.cosmetic_item) == "table" then return reward.cosmetic_item end
if reward and type(reward.item) == "table" then return reward.item end
return {}
end
local function rewardUsesItemIcon(itemType)
local key = cleanText(itemType)
return key == "label" or key == "avatar_frame" or key == "namecard"
end
local function rewardSortPriority(reward)
local itemData = rewardItemData(reward)
local itemType = firstText(itemData.item_type, reward and reward.reward_type, "other")
local priority = {
label = 1,
avatar_frame = 2,
namecard = 3,
}
return priority[itemType] or 50
end
local function sortedRewardRows(rewards)
local rows = {}
for idx, reward in ipairs(rewards) do
table.insert(rows, { reward = reward, index = idx })
end
table.sort(rows, function(a, b)
local ap = rewardSortPriority(a.reward)
local bp = rewardSortPriority(b.reward)
if ap ~= bp then return ap < bp end
local as = tonumber(a.reward and a.reward.sort) or a.index
local bs = tonumber(b.reward and b.reward.sort) or b.index
if as ~= bs then return as < bs end
return a.index < b.index
end)
return rows
end
local function makeRewardsSection(rewards)
if not rewards or type(rewards) ~= "table" or #rewards == 0 then return nil end
local section = mw.html.create("div"):addClass("card_content_rewards-section")
section:node(makeTitle("奖励", "Rewards"))
section:node(makeHr())
local list = section:tag("div"):addClass("card_content_rewards")
local count = 0
for _, rewardRow in ipairs(sortedRewardRows(rewards)) do
local reward = rewardRow.reward
if reward and type(reward) == "table" then
local itemData = rewardItemData(reward)
local itemType = firstText(itemData.item_type, reward.reward_type, "other")
local name = firstText(itemData.name, reward.name)
local imageKey = firstText(itemData.image_key, name)
local description = firstText(itemData.item_description, reward.description)
local obtainNote = cleanText(reward.obtain_note)
local typeLabel = rewardTypeLabel(itemType)
local useItemIcon = rewardUsesItemIcon(itemType)
local displayFile = ""
if not useItemIcon then
displayFile = firstText(
rewardFileName(itemData.display_wiki_file),
rewardFileName(reward.display_wiki_file),
defaultRewardFile(itemType, imageKey, "display")
)
end
local iconFile = firstText(
defaultRewardFile(itemType, imageKey, "icon"),
rewardFileName(itemData.icon_wiki_file),
rewardFileName(reward.icon_wiki_file)
)
if name ~= "" or description ~= "" or displayFile ~= "" or iconFile ~= "" then
count = count + 1
local rewardNode = list:tag("div")
:addClass("card_content_reward")
:addClass(rewardTypeClass(itemType))
local header = rewardNode:tag("div"):addClass("card_content_reward-header")
if displayFile ~= "" then
header:tag("div"):addClass("card_content_reward-display")
:attr("role", "button")
:attr("tabindex", "0")
:attr("aria-label", "查看奖励图片")
:attr("data-card-lightbox-image", "1")
:wikitext("[[文件:" .. displayFile .. "|360x160px|link=]]")
elseif iconFile ~= "" then
header:tag("div"):addClass("card_content_reward-icon")
:attr("role", "button")
:attr("tabindex", "0")
:attr("aria-label", "查看奖励图片")
:attr("data-card-lightbox-image", "1")
:wikitext("[[文件:" .. iconFile .. "|72x72px|link=]]")
else
header:tag("div"):addClass("card_content_reward-visual card_content_reward-visual--empty")
:wikitext(typeLabel)
end
local heading = header:tag("div"):addClass("card_content_reward-heading")
local titleRow = heading:tag("div"):addClass("card_content_reward-title-row")
titleRow:tag("span"):addClass("card_content_reward-name"):wikitext(val(name))
titleRow:tag("span"):addClass("card_content_reward-type"):wikitext(typeLabel)
if obtainNote ~= "" then
heading:tag("div"):addClass("card_content_reward-obtain"):wikitext(obtainNote)
end
if description ~= "" then
rewardNode:tag("div"):addClass("card_content_reward-description"):wikitext(description)
end
end
end
end
if count == 0 then return nil end
return section
end
local function makeStoryTimeline(stories, cardTitle)
local timeline = mw.html.create("div")
:addClass("card_timeline card_content_story-timeline")
timeline:tag("div"):addClass("card_timeline-line")
if not stories or type(stories) ~= "table" or #stories == 0 then
timeline:tag("div"):addClass("card_timeline-item"):wikitext("(小传数据待补充)")
return timeline
end
local romans = { "Ⅰ", "Ⅱ", "Ⅲ", "Ⅳ", "Ⅴ" }
local unlockLevels = { "Lv.1 解锁", "Lv.10 解锁", "Lv.30 解锁", "Lv.50 解锁", "Lv.70 解锁" }
for idx, chapter in ipairs(stories) do
local item = timeline:tag("div"):addClass("card_timeline-item")
if idx == #stories then item:addClass("card_timeline-item--last") end
item:tag("div"):addClass("card_timeline-dot")
item:tag("div"):addClass("card_timeline-unlock")
:wikitext(unlockLevels[idx] or ("Lv." .. (idx * 20) .. " 解锁"))
local titleDiv = item:tag("div"):addClass("card_timeline-title")
titleDiv:tag("span"):addClass("card_timeline-numeral"):wikitext(romans[idx] or tostring(idx))
titleDiv:tag("span"):addClass("card_timeline-chapter"):wikitext(cardTitle or "")
titleDiv:tag("span"):addClass("card_timeline-label"):wikitext("/ Story")
local bodyDiv = item:tag("div"):addClass("card_timeline-body")
local text = chapter.text or ""
if text ~= "" then
text = text:gsub("\r\n", "\n")
local paragraphs = mw.text.split(text, "\n+")
for _, para in ipairs(paragraphs) do
para = mw.text.trim(para)
if para ~= "" then bodyDiv:tag("p"):wikitext(para) end
end
end
end
return timeline
end
function p.render(frame)
local name = mw.text.trim(frame.args[1] or frame.args.name or "")
if name == "" then
return '<span class="error">错误:请提供卡片名称,例如 {{#invoke:CardData|render|复仇童谣}}</span>'
end
local encodedName = mw.uri.encode(name, "QUERY")
local apiUrl = "https://data.saltedkiss.org/items/cards"
.. "?fields=source_style_id,stylename,rarity,tags,acquisition_type,release_date,summon_pool_id,summon_pool_name,summon_pool_title_wiki_file,summon_pool.pool_id,summon_pool.name,summon_pool.title_wiki_file,character.name,profession.name,desire.name"
.. ",collection_type,collection_refine_pool"
.. ",collection_fixed_attributes.status,collection_fixed_attributes.sort"
.. ",collection_fixed_attributes.source_style_id,collection_fixed_attributes.style_name"
.. ",collection_fixed_attributes.position,collection_fixed_attributes.slot_index"
.. ",collection_fixed_attributes.attr_id,collection_fixed_attributes.attr_name"
.. ",collection_fixed_attributes.attr_symbol,collection_fixed_attributes.source_raw"
.. ",collection_refine_pools.status,collection_refine_pools.sort"
.. ",collection_refine_pools.pool_id,collection_refine_pools.name"
.. ",collection_refine_pools.source_style_id,collection_refine_pools.style_name"
.. ",collection_refine_pools.collection_type"
.. ",collection_refine_pools.terms.status,collection_refine_pools.terms.sort"
.. ",collection_refine_pools.terms.pool_id,collection_refine_pools.terms.effect_id"
.. ",collection_refine_pools.terms.name,collection_refine_pools.terms.effect_type"
.. ",collection_refine_pools.terms.effect_value,collection_refine_pools.terms.attr_id"
.. ",collection_refine_pools.terms.attr_value,collection_refine_pools.terms.buff_prefix"
.. ",collection_refine_pools.terms.description,collection_refine_pools.terms.quality"
.. ",collection_refine_pools.terms.is_exclusive_red,collection_refine_pools.terms.first_weight"
.. ",collection_refine_pools.terms.normal_weight,collection_refine_pools.terms.guarantee_count"
.. ",intel,supply,execute,strategy"
.. ",attribute_growths.status,attribute_growths.sort"
.. ",attribute_growths.source_style_id,attribute_growths.source_hero_card_id"
.. ",attribute_growths.attr_rank,attribute_growths.base_hp"
.. ",attribute_growths.base_atk,attribute_growths.base_def"
.. ",attribute_growths.hp_rate,attribute_growths.atk_rate,attribute_growths.def_rate"
.. ",attribute_growths.break_hp,attribute_growths.break_atk,attribute_growths.break_def"
.. ",attribute_growths.formula_version"
.. ",skill_normal_attack.name,skill_normal_attack.type,skill_normal_attack.weapon"
.. ",skill_normal_attack.description,skill_normal_attack.description_template"
.. ",skill_normal_attack.levels.*"
.. ",weapon_attack_profiles.status,weapon_attack_profiles.source_style_id"
.. ",weapon_attack_profiles.normal_skill_id,weapon_attack_profiles.normal_skill_name"
.. ",weapon_attack_profiles.weapon_type,weapon_attack_profiles.attack_range,weapon_attack_profiles.avg_ms_per_hit"
.. ",weapon_attack_profiles.hit_events_per_sec,weapon_attack_profiles.action_cycles_per_sec"
.. ",weapon_attack_profiles.real_attack_cycle_ms,weapon_attack_profiles.hit_event_count"
.. ",weapon_attack_profiles.reload_count,weapon_attack_profiles.reload_time_ms"
.. ",weapon_attack_profiles.fire_frames,weapon_attack_profiles.verification_status"
.. ",weapon_attack_profiles.aim_target_rule,weapon_attack_profiles.aim_time_ms"
.. ",weapon_attack_profiles.base_atk_spd_for_aim,weapon_attack_profiles.real_aim_time_baseline_ms"
.. ",weapon_attack_profiles.real_aim_time_formula,weapon_attack_profiles.normal_action_ms_raw"
.. ",weapon_attack_profiles.normal_action_ms_tick,weapon_attack_profiles.normal_action_tick_formula"
.. ",weapon_attack_profiles.normal_hit_event_count,weapon_attack_profiles.reload_per_action_ms"
.. ",weapon_attack_profiles.sustained_cycle_ms_no_aim,weapon_attack_profiles.sustained_actions_per_sec_no_aim"
.. ",weapon_attack_profiles.sustained_hit_events_per_sec_no_aim,weapon_attack_profiles.first_magazine_cycle_ms_with_aim"
.. ",weapon_attack_profiles.first_magazine_actions_per_sec_with_aim,weapon_attack_profiles.first_magazine_hit_events_per_sec_with_aim"
.. ",weapon_attack_profiles.speed_formula_version,weapon_attack_profiles.speed_formula_summary"
.. ",weapon_attack_profiles.video_observed_hits_per_sec,weapon_attack_profiles.verification_note"
.. ",weapon_attack_profiles.template.template_name,weapon_attack_profiles.template.weapon_type"
.. ",weapon_attack_profiles.template.avg_ms_per_hit,weapon_attack_profiles.template.hit_events_per_sec"
.. ",weapon_attack_profiles.template.real_attack_cycle_ms,weapon_attack_profiles.template.attack_range,weapon_attack_profiles.template.hit_count"
.. ",weapon_attack_profiles.template.reload_count,weapon_attack_profiles.template.reload_time_ms"
.. ",weapon_attack_profiles.template.fire_frames_pattern"
.. ",skill_passive.name,skill_passive.type,skill_passive.trigger_type"
.. ",skill_passive.trigger_value,skill_passive.tags,skill_passive.description"
.. ",skill_passive.description_template,skill_passive.levels.*"
.. ",skill_ultimate.name,skill_ultimate.type,skill_ultimate.desire_cost"
.. ",skill_ultimate.tags,skill_ultimate.description"
.. ",skill_ultimate.description_template,skill_ultimate.levels.*"
.. ",feats.stages"
.. ",rewards.status,rewards.sort,rewards.reward_type,rewards.name"
.. ",rewards.description,rewards.obtain_note,rewards.display_wiki_file"
.. ",rewards.icon_wiki_file,rewards.source_item_id"
.. ",rewards.cosmetic_item.status,rewards.cosmetic_item.item_type"
.. ",rewards.cosmetic_item.name,rewards.cosmetic_item.usage_description"
.. ",rewards.cosmetic_item.item_description,rewards.cosmetic_item.image_key"
.. ",rewards.cosmetic_item.display_wiki_file,rewards.cosmetic_item.icon_wiki_file"
.. ",rewards.cosmetic_item.source_item_id"
.. ",rewards.item.status,rewards.item.item_type,rewards.item.name"
.. ",rewards.item.usage_description,rewards.item.item_description"
.. ",rewards.item.image_key,rewards.item.display_wiki_file"
.. ",rewards.item.icon_wiki_file,rewards.item.source_item_id"
.. ",stories.text"
.. "&filter[stylename][_eq]=" .. encodedName
.. "&deep[rewards][_filter][status][_eq]=published"
.. "&deep[rewards][_sort]=sort"
.. "&deep[collection_fixed_attributes][_filter][status][_eq]=published"
.. "&deep[collection_fixed_attributes][_sort]=position,slot_index"
.. "&deep[collection_refine_pools][_filter][status][_eq]=published"
.. "&deep[collection_refine_pools][_sort]=sort"
.. "&deep[collection_refine_pools][terms][_filter][status][_eq]=published"
.. "&deep[collection_refine_pools][terms][_sort]=effect_id"
.. "&deep[attribute_growths][_filter][status][_eq]=published"
.. "&deep[attribute_growths][_sort]=sort"
.. "&deep[weapon_attack_profiles][_filter][status][_eq]=published"
.. "&deep[weapon_attack_profiles][_sort]=sort"
local rawResponse = frame:preprocess(
'{{#get_web_data:url=' .. apiUrl
.. '|format=text|data=responseText=__text}}'
.. '{{#external_value:responseText}}'
)
rawResponse = mw.text.trim(rawResponse or "")
local parsed = parseJson(rawResponse)
local card = parsed and type(parsed.data) == "table" and parsed.data[1] or nil
if not card then
return '<span class="error">⚠ 无法加载卡片数据:' .. mw.text.nowiki(name)
.. '(response=' .. mw.text.nowiki(rawResponse) .. ')</span>'
end
local charData = card.character or {}
local profData = card.profession or {}
local desireData = card.desire or {}
local skillNA = card.skill_normal_attack or {}
local skillPA = card.skill_passive or {}
local skillUL = card.skill_ultimate or {}
local stories = card.stories or {}
local featsStagesJson = "[]"
if card.feats and type(card.feats) == "table" and card.feats[1] then
local sr = card.feats[1].stages
featsStagesJson = type(sr) == "table" and mw.text.jsonEncode(sr)
or type(sr) == "string" and sr or "[]"
end
local function levelsToJson(skill)
if not skill.levels then return "[]" end
if type(skill.levels) == "string" then return skill.levels end
if type(skill.levels) == "table" then return mw.text.jsonEncode(skill.levels) end
return "[]"
end
skillNA.levels = levelsToJson(skillNA)
skillPA.levels = levelsToJson(skillPA)
skillUL.levels = levelsToJson(skillUL)
local function tagsToJson(skill)
if not skill.tags then return "[]" end
if type(skill.tags) == "string" then return skill.tags end
if type(skill.tags) == "table" then return mw.text.jsonEncode(skill.tags) end
return "[]"
end
skillPA.tags = tagsToJson(skillPA)
skillUL.tags = tagsToJson(skillUL)
local weaponAttackProfile = fetchWeaponAttackProfile(frame, card, skillNA)
local weaponAttackPayload = WeaponAttack.payload(weaponAttackProfile)
local rarity = tonumber(card.rarity) or 0
local stylename = val(card.stylename)
local charName = val(charData.name)
local profName = val(profData.name)
local desName = val(desireData.name)
local root = mw.html.create("div")
:addClass("ron-card ron-card--rarity-" .. rarity)
:attr("data-rarity", rarity)
-- 全屏背景图,文件命名规则:卡面_卡面名.png
-- rarity 6 默认展示第二卡面:卡面_卡面名_2.png
local cardFaceFile1 = "卡面_" .. stylename .. ".png"
local cardFaceFile2 = "卡面_" .. stylename .. "_2.png"
local fullscreen = root:tag("div"):addClass("card_fullscreen-img")
if rarity == 6 then
fullscreen:addClass("card_fullscreen-img--switchable")
:attr("data-active-face", "2")
fullscreen:tag("div")
:addClass("card_face card_face--primary")
:attr("data-card-face", "1")
:wikitext("[[文件:" .. cardFaceFile1 .. "]]")
fullscreen:tag("div")
:addClass("card_face card_face--secondary is-active")
:attr("data-card-face", "2")
:wikitext("[[文件:" .. cardFaceFile2 .. "]]")
else
fullscreen:wikitext("[[文件:" .. cardFaceFile1 .. "]]")
end
local hero = root:tag("div"):addClass("card_hero"):css("height", "100vh")
if rarity == 6 then
local switch = hero:tag("div")
:addClass("card_face-switch")
:attr("role", "group")
:attr("aria-label", "切换卡面")
switch:tag("div")
:addClass("card_face-switch-btn")
:attr("role", "button")
:attr("tabindex", "0")
:attr("data-card-face", "1")
:wikitext("Ⅰ")
switch:tag("div")
:addClass("card_face-switch-btn is-active")
:attr("role", "button")
:attr("tabindex", "0")
:attr("data-card-face", "2")
:wikitext("Ⅱ")
end
local layout = root:tag("div"):addClass("card_content-container")
:tag("div"):addClass("card_content-background")
:tag("div"):addClass("card_content-inner")
:tag("div"):addClass("card_content card_content--layout")
-- 左栏
local leftCol = layout:tag("div"):addClass("card_content_left")
-- 卡名
local nc = leftCol:tag("div"):addClass("card_content_name-container")
local nr = nc:tag("div"):addClass("card_content_name-row")
nr:node(makeNameIcon(profName))
nr:tag("div"):addClass("card_content_name-classtext"):wikitext(profName)
nr:node(makeNameIcon(desName, "card_content_name-icon--ml", "38px"))
nr:tag("div"):addClass("card_content_name-classtext"):wikitext(desName)
local nt = nc:tag("div"):addClass("card_content_name-text")
nt:wikitext(stylename .. " ")
nt:tag("span"):addClass("card_accent"):wikitext("·")
nt:wikitext(" " .. charName)
local styleTags = makeStyleTagsRow(card.tags)
if styleTags then nc:node(styleTags) end
local availability = makeAvailabilitySection(card)
if availability then nc:node(availability) end
-- 信息(稀有度 + 四维)
leftCol:node(makeTitle("信息", "Info"))
leftCol:node(makeHr())
leftCol:node(makeInfoSection(card.intel, card.supply, card.execute, card.strategy, rarity))
-- 等级属性
leftCol:node(makeStyleAttributeGrowthSection(card))
-- 映像
leftCol:node(makeStyleCollectionSection(card))
local rewardsSection = makeRewardsSection(card.rewards)
if rewardsSection then
leftCol:node(rewardsSection)
end
-- 右栏
local rightCol = layout:tag("div"):addClass("card_content_right")
rightCol:node(makeTitle("战斗技能", "Tactical"))
rightCol:node(makeHr("card_content-item-hr--mb12"))
rightCol:node(makeSkillCard(skillNA, "normal_attack", stylename, weaponAttackPayload))
rightCol:tag("div"):addClass("card_content_skill-gap")
rightCol:node(makeSkillCard(skillPA, "passive", stylename))
rightCol:tag("div"):addClass("card_content_skill-gap")
rightCol:node(makeSkillCard(skillUL, "ultimate", stylename))
rightCol:node(makeTitle("觉醒", "Feat"))
rightCol:node(makeHr("card_content-item-hr--mb12"))
rightCol:node(makeFeatList(featsStagesJson))
-- 小传
local storySection = rightCol:tag("div"):addClass("card_content_story")
local storyHeader = storySection:tag("div"):addClass("card_content_story-header")
local storyLeft = storyHeader:tag("div"):addClass("card_content_story-header-left")
storyLeft:node(makeTitle("小传", "Story"))
storyLeft:node(makeHr("card_content-item-hr--mb12"))
storyLeft:tag("div"):addClass("card_content_story-toggle")
:attr("data-collapsed", "false")
:tag("span"):addClass("story-toggle-text"):wikitext("收起"):done()
:tag("span"):addClass("story-toggle-icon"):wikitext("-")
storyHeader:tag("div"):addClass("card_content_story-header-spacer")
storyHeader:node(makeStoryTimeline(stories, stylename))
-- 邀约
rightCol:tag("div"):css("height", "12px"):css("width", "100%")
rightCol:node(makeTitle("邀约", "Date"))
rightCol:node(makeHr("card_content-item-hr--mb12"))
rightCol:tag("div"):addClass("card_content_date")
:tag("div"):addClass("card_content_date-track")
:tag("ul"):addClass("card_content_date-list")
:tag("li"):addClass("card_content_date-item"):wikitext("(邀约数据待补充)")
return tostring(root)
end
return p