模块

卡牌:修订间差异

来自卡厄思梦境WIKI

律Rhyme留言 | 贡献
无编辑摘要
标签已被回退
律Rhyme留言 | 贡献
无编辑摘要
 
(未显示同一用户的77个中间版本)
第1行: 第1行:
local p = {}
local p = {}


-- 安全获取嵌套表数据
-- 使用官方参数解析器,自动合并父/子 frame 参数、去空白与空值
local function safeGet(tbl, key, default)
local getArgs = require('Module:Arguments').getArgs
     if tbl and tbl[key] ~= nil then
 
         return tbl[key]
-- 渲染模块
local displayModule = require('Module:卡牌/display')
 
-- 安全的错误输出
local function err(msg)
    return '<span style="color: red;">' .. mw.text.encode(msg) .. '</span>'
end
 
-- 解析 dict 字段
local function parseDictTokens(dictStr)
    local s = tostring(dictStr or '')
    if s == '' then return {} end
 
    local tokens = {}
    local processedText = s
 
    -- 提取 {{词典|机制}} 形式的 token(保留为 template 类型)
    for token in s:gmatch('{{%s*词典%s*|%s*([^}|]+)%s*[^}]*}}') do
        token = mw.text.trim(token or '')
        if token ~= '' then
            table.insert(tokens, { type = 'template', value = token })
        end
     end
 
    -- 提取 {{文本|颜色|内容}} 形式的内容 token(保留为模板)
    for content in s:gmatch('{{%s*文本%s*|%s*[^|]+%s*|%s*([^}]+)%s*}}') do
        content = mw.text.trim(content or '')
        if content ~= '' then
            table.insert(tokens, { type = 'template', value = content })
         end
    end
 
    -- 移除所有模板,保留纯文本部分
    processedText = processedText:gsub('{{%s*词典%s*|%s*[^}]*}}', '')
    processedText = processedText:gsub('{{%s*文本%s*|%s*[^}]*}}', '')
 
    -- 处理剩余的纯文本(支持中文顿号和逗号)
    processedText = processedText:gsub('、', ','):gsub(',', ',')
    for token in processedText:gmatch('([^,]+)') do
        token = mw.text.trim(token or '')
        if token ~= '' then
            table.insert(tokens, { type = 'text', value = token })
        end
     end
     end
     return default or ""
 
    -- 去重,保持顺序
    local seen, out = {}, {}
    for _, t in ipairs(tokens) do
        local key = t.type .. ':' .. t.value
        if not seen[key] then
            seen[key] = true
            table.insert(out, t)
        end
    end
     return out
end
end


-- 颜色映射表
-- 加载 模块:词典/data
local COLOR_MAP = {
local dictData = nil
     ["白"] = { bgColor = "#ffffff", textColor = "#333333" },
do
     ["蓝"] = { bgColor = "#4a90e2", textColor = "white" },
     local ok, dictModule = pcall(mw.loadData, 'Module:词典/data')
    ["紫"] = { bgColor = "#9b59b6", textColor = "white" },
     if ok and type(dictModule) == 'table' then
     ["金"] = { bgColor = "#f1c40f", textColor = "#333333" }
        if type(dictModule.dictionary) == 'table' then
}
            dictData = dictModule.dictionary
        else
            dictData = dictModule
        end
     end
end


-- 计算名称缩放比例
-- 根据 tokens 生成词典条目数组 { {key=..., desc=...}, ... }
local function calculateNameScale(name)
-- tokens 可以是:字符串数组 或 结构化数组 [{type="text",value="x"}, {type="template",value="y"}]
     local length = mw.ustring.len(name)
local function buildDictEntries(tokens)
    -- 基准是5个字符以内
    if not tokens or #tokens == 0 then return nil end
    if length <= 5 then
     local entries = {}
         return "1"
 
    elseif length <= 7 then
    for _, t in ipairs(tokens) do
         return "0.9"
        local key = nil
    elseif length <= 9 then
 
        return "0.8"
        -- 判断 token 是否为结构化对象(新格式)
    elseif length <= 11 then
        if type(t) == "table" and t.type and t.value then
        return "0.7"
            key = t.value
    else
         -- 判断 token 是否为字符串(旧格式兼容)
         return "0.6"
        elseif type(t) == "string" then
            key = t
        end
 
        -- 如果 key 为 nil,跳过这个 token
        if not key then
            -- 不做任何事,跳过
         else
            -- 去空格
            key = mw.text.trim(key)
 
            -- 如果去空格后为空,也跳过
            if key ~= '' then
                local desc = nil
                local d = dictData and dictData[key]
 
                if type(d) == 'table' then
                    if d[1] and type(d[1]) == 'table' then
                        desc = d[1]["描述"] or d[1].desc or desc
                    elseif type(d["描述"]) == 'string' then
                        desc = d["描述"]
                    elseif type(d.desc) == 'string' then
                        desc = d.desc
                    end
                end
 
                table.insert(entries, { key = key, desc = desc })
            end
         end
     end
     end
    return #entries > 0 and entries or nil
end
end


-- 处理描述文本
-- 合并 base 数据与变体数据
local function processDescription(frame, description)
local function mergeCardData(baseData, variantData, opts)
     if not description or description == "" then
     if not baseData then return nil end
         return ""
    local merged = {}
    -- 浅拷贝 base
    for k, v in pairs(baseData) do
        merged[k] = v
    end
    -- 清空衍生卡牌信息
    if opts and opts.clearSubUnlessSpecified then
        merged.sub = nil
    end
    -- 覆盖变体数据
    if variantData then
         for k, v in pairs(variantData) do
            merged[k] = v
        end
     end
     end
      
     return merged
    -- 处理换行符
    description = description:gsub("\r?\n", "<br>")
   
    -- 处理图标:{{图标名称}} 转换为对应图片
    description = description:gsub("{{(.-)}}", function(iconName)
        return string.format('[[File:icon_%s.png|16px|link=|style="vertical-align: -3px; margin: 0 1px;"]]', iconName)
    end)
   
    -- 处理高亮文本:<hl>文本</hl> 转换为金色
    description = description:gsub("<hl>(.-)</hl>", function(text)
        return string.format('<span style="color: #f1c832; font-weight: bold; text-shadow: 0 0 2px rgba(241, 200, 50, 0.5);">%s</span>', text)
    end)
   
    return frame:preprocess(description)
end
end


-- 生成卡牌HTML样式组件
-- 根据 base AP 与目标 AP 文本计算发光样式
local function buildStyleComponents(cardData, cardName, baseCardData)
-- 返回: 'blue' | 'red' | 'green' | 'gray'
     local color = safeGet(cardData, "稀有度", "白")
local function calcApStyle(apText, baseApText, isVariant)
     local colorStyle = COLOR_MAP[color] or COLOR_MAP["白"]
     local text = apText ~= nil and tostring(apText) or ''
     local attribute = safeGet(cardData, "属性", "虚无")
     if not isVariant then
     local cardDeck = safeGet(cardData, "卡组", "起始卡牌")
        return 'blue'
      
     end
     local ap = safeGet(cardData, "AP", "")
     if text == 'X' then
     local originalAP = baseCardData and safeGet(baseCardData, "AP", ap) or ap
        return 'blue'
   
     end
    -- 决定AP的发光颜色
     local n = tonumber(text)
    local apGlowColor = "#4a90e2" -- 默认蓝色
     local baseN = baseApText ~= nil and tonumber(baseApText) or nil
     if baseCardData and ap ~= originalAP then
     if n and baseN then
         if ap == "X" then
         if n > baseN then
             apGlowColor = "#4a90e2" -- X值保持默认蓝色
             return 'red'
         elseif tonumber(ap) and tonumber(originalAP) then
         elseif n < baseN then
             if tonumber(ap) < tonumber(originalAP) then
             return 'green'
                apGlowColor = "#51f651" -- 绿色(AP降低)
        else
            else
             return 'blue'
                apGlowColor = "#f65151" -- 红色(AP增加)
             end
         end
         end
     end
     end
   
     -- 其他内容(非数值且非X)不发光
     -- 检查原卡牌与当前卡牌的机制变化(将"无"视为空字符串)
     return 'gray'
    local mechanism = safeGet(cardData, "机制", "")
    local originalMechanism = baseCardData and safeGet(baseCardData, "机制", "") or mechanism
   
    -- 将"无"视为空字符串
    if mechanism == "无" then mechanism = "" end
    if originalMechanism == "无" then originalMechanism = "" end
   
    local mechanismChanged = baseCardData and mechanism ~= "" and originalMechanism == ""
   
     return {
        color = color,
        colorStyle = colorStyle,
        attribute = attribute,
        ap = ap,
        apGlowColor = apGlowColor,
        cardType = safeGet(cardData, "类型", ""),
        art = safeGet(cardData, "art", "start_1041_01.png"),
        mechanism = mechanism,
        mechanismChanged = mechanismChanged
    }
end
end


-- 构建卡牌HTML
-- 将单张卡牌的数据转换为 cardInfo 结构(在此完成词典解析)
local function buildCardHTML(frame, cardName, cardData, baseCardData, characterName)
-- opts:
--  isVariant: boolean
--  baseApText: string|number
--  baseDictStr: string
function p.parseCardData(cardData, moduleName, cardName, moduleInfo, opts)
     if not cardData then
     if not cardData then
         return string.format("找不到卡牌数据: %s", cardName or "未指定")
         return nil
     end
     end
   
 
     local style = buildStyleComponents(cardData, cardName, baseCardData)
     local ego = ""
    local description = processDescription(frame, safeGet(cardData, "描述", ""))
     if moduleInfo and moduleInfo.ego then
      
         ego = moduleInfo.ego
    -- 获取衍生卡牌信息
     elseif cardData.ego then
    local derivedCards = safeGet(cardData, "衍生卡牌", "")
         ego = cardData.ego
    local deckType = safeGet(cardData, "卡组", "")
   
    -- 生成数据属性
    local dataAttrs = string.format('data-card-name="%s" data-character="%s" data-deck-type="%s" data-derived-cards="%s"',
         cardName or "", characterName or "", deckType or "", derivedCards or "")
   
    -- 处理机制文本(当机制为"无"或空时不显示)
    local mechanismHTML = ""
     if style.mechanism and style.mechanism ~= "" then
         local mechanismColor = style.mechanismChanged and "#b5f651" or "#f6c478"
        mechanismHTML = string.format('<div style="color: %s; margin-bottom: 4px;">[%s]</div>',
            mechanismColor, style.mechanism:gsub("、", "/"))
     end
     end
   
 
     -- 计算名称缩放比例
     -- 展示名称:优先 displayname,否则使用卡牌名
     local nameScale = calculateNameScale(cardName)
     local displayTitle = (cardData.displayname and mw.text.trim(tostring(cardData.displayname)) ~= '' and cardData.displayname) or cardName or ''
   
 
     -- 使用字符串拼接优化HTML生成
     -- AP 文本与样式
     local htmlParts = {
     local apText = cardData.ap ~= nil and tostring(cardData.ap) or ''
        string.format('<div class="game-card" %s style="display: inline-block; vertical-align: top; position: relative; width: 168px; height: 230px; overflow: hidden; margin: 0px; cursor: pointer;">', dataAttrs),
     local isVariant = opts and opts.isVariant or false
     }
     local baseApText = opts and opts.baseApText or nil
      
     local apStyle = calcApStyle(apText, baseApText, isVariant)
     -- 根据类型判断使用哪种布局
 
    if style.cardType == "状态异常" then
    -- 词典解析
        -- 状态异常卡牌的特殊布局
    local rawDictStr = cardData.dict or ""
        table.insert(htmlParts, string.format('<div style="position: absolute; top: 5px; left: 5px;">[[File:%s|150px|link=]]</div>', style.art))
    local dictStr = mw.text.trim(rawDictStr)
        table.insert(htmlParts, string.format('<div style="position: absolute; top: 0px; left: 0px;">[[File:card_状态异常_%s.png|159px|link=]]</div>', style.attribute))
    local dictTokens
       
    local dictEntries
        -- AP值
    local dictColor = "default"
        table.insert(htmlParts, string.format('<div style="position: absolute; left: 24px; top: 4px; color: white; font-weight: bold; text-shadow: 0 0 10px %s,0 0 20px %s, 0 0 30px %s, 0 0 40px %s; font-size: 32px;">%s</div>',
 
            style.apGlowColor, style.apGlowColor, style.apGlowColor, style.apGlowColor, style.ap))
    if isVariant then
        table.insert(htmlParts, string.format('<div style="position: absolute; left: 25px; top: 34px; color: white; font-weight: bold; text-shadow: 0 0 10px %s,0 0 20px %s, 0 0 30px %s, 0 0 40px %s;">—</div>',
         -- 获取base的dict用于比较
            style.apGlowColor, style.apGlowColor, style.apGlowColor, style.apGlowColor))
         local baseDictStr = opts and opts.baseDictStr or ""
       
         baseDictStr = mw.text.trim(baseDictStr)
        -- 卡牌名称(使用transform缩放)
        table.insert(htmlParts, string.format('<div style="position: absolute; left: 50px; top: 13px; width: 105px; transform-origin: left center; transform: scaleX(%s);"><span style="color: %s; font-size: 16px; white-space: nowrap;">%s</span></div>',
            nameScale, style.colorStyle.textColor, cardName))
       
         -- 卡牌类型
         table.insert(htmlParts, string.format('<div style="position: absolute; left: 48px; top: 30px;">[[File:icon_card_%s.png|18px|link=]]</div>',
            style.cardType))
         table.insert(htmlParts, string.format('<div style="position: absolute; left: 68px; top: 33px; color: white; font-size:14px">%s</div>',
            style.cardType))
       
        -- 顶层蒙版
        table.insert(htmlParts, '<div style="position: absolute; left: 0px; bottom: 0px;">[[File:card_顶层蒙版.png|168px|link=]]</div>')
          
          
        if dictStr == "空" then
            -- 显式设置为"空",说明是变体修改了
            dictTokens = {}
            dictEntries = nil
            dictColor = "green"
        elseif dictStr ~= "" and dictStr ~= baseDictStr then
            -- dict不为空,且与base不同,说明变体修改了
            dictTokens = parseDictTokens(dictStr)
            dictEntries = buildDictEntries(dictTokens)
            dictColor = "green"
        else
            -- dict为空或与base相同,说明是继承的
            dictTokens = parseDictTokens(dictStr)
            dictEntries = buildDictEntries(dictTokens)
            dictColor = "default"
        end
     else
     else
         -- 普通卡牌的布局
         dictTokens = parseDictTokens(dictStr)
        -- 背景图片层
         dictEntries = buildDictEntries(dictTokens)
        table.insert(htmlParts, string.format('<div style="position: absolute; top: 1px; left: 12px;">[[File:%s|151px|link=]]</div>', style.art))
        table.insert(htmlParts, '<div style="position: absolute; top: 1px; left: 12px;">[[File:card_黑色蒙版.png|151px|link=]]</div>')
       
        -- 颜色条
        table.insert(htmlParts, string.format('<div style="position: absolute; top: 21px; left: 10px; width: 148px; height: 8px; background-color: %s;"></div>',
            style.colorStyle.bgColor))
       
        -- AP值
        table.insert(htmlParts, string.format('<div style="position: absolute; left: 24px; top: 4px; color: white; font-weight: bold; text-shadow: 0 0 10px %s,0 0 20px %s, 0 0 30px %s, 0 0 40px %s; font-size: 32px;">%s</div>',
            style.apGlowColor, style.apGlowColor, style.apGlowColor, style.apGlowColor, style.ap))
        table.insert(htmlParts, string.format('<div style="position: absolute; left: 25px; top: 34px; color: white; font-weight: bold; text-shadow: 0 0 10px %s,0 0 20px %s, 0 0 30px %s, 0 0 40px %s;">—</div>',
            style.apGlowColor, style.apGlowColor, style.apGlowColor, style.apGlowColor))
          
        -- 卡牌名称(使用transform缩放)
        table.insert(htmlParts, string.format('<div style="position: absolute; left: 50px; top: 13px; width: 105px; transform-origin: left center; transform: scaleX(%s);"><span style="color: %s; font-size: 16px; white-space: nowrap;">%s</span></div>',
            nameScale, style.colorStyle.textColor, cardName))
       
        -- 卡牌类型
        table.insert(htmlParts, string.format('<div style="position: absolute; left: 48px; top: 30px;">[[File:icon_card_%s.png|18px|link=]]</div>',
            style.cardType))
        table.insert(htmlParts, string.format('<div style="position: absolute; left: 68px; top: 33px; color: white; font-size:14px">%s</div>',
            style.cardType))
       
        -- 边框
        table.insert(htmlParts, string.format('<div style="position: absolute; top: 0px; left: 1px;">[[File:card_属性边框_%s.png|160px|link=]]</div>',
            style.attribute))
        -- 稀有度
        table.insert(htmlParts, string.format('<div style="position: absolute; top: 10px; left: 0px;">[[File:card_稀有度_%s.png|22px|link=]]</div>',
            style.color))
        table.insert(htmlParts, string.format('<div style="position: absolute; top: 2px; right: 4px;">[[File:card_稀有度_边框_%s.png|11px|link=]]</div>',
            style.color))
       
        -- 顶层蒙版
        table.insert(htmlParts, '<div style="position: absolute; left: 0px; bottom: 0px;">[[File:card_顶层蒙版.png|168px|link=]]</div>')
     end
     end
   
    -- 描述文本(包含机制)- 对所有类型都显示
    table.insert(htmlParts, '<div style="position: absolute; bottom: 7px; left: 15px; width: 144px; height: 100px; font-size: 12px; color: white; line-height: 13px; display: flex; justify-content: center; align-items: center; text-align: center;">')
    table.insert(htmlParts, '<div style="line-height: 13px;">')
    table.insert(htmlParts, mechanismHTML)
    table.insert(htmlParts, description)
    table.insert(htmlParts, '</div>')
    table.insert(htmlParts, '</div>')
    table.insert(htmlParts, '</div>')
   
    return table.concat(htmlParts)
end


-- 获取基础卡牌数据(用于对比)
    local cardInfo = {
local function getBaseCardData(cardName)
        moduleName = moduleName or "",
    local data = mw.loadData("模块:卡牌数据")
        cardName = cardName or "",
     return data[cardName]
        displayTitle = displayTitle or "",
        art = cardData.art or "",
        ego = ego or "",
        rarity = cardData.rarity or "",
        apText = apText,
        apStyle = apStyle,
        cardType = cardData.type or "",
        desc = cardData.desc_global or "",
        dict = dictStr,
        dictTokens = dictTokens,
        dictEntries = dictEntries,
        dictColor = dictColor,
        group = cardData.group or "",
        tag = cardData.tag or "",
        hasInspiration = false,
        inspirationVariants = {}
    }
 
     return cardInfo
end
end


-- 主函数:显示卡牌
-- 解析衍生卡牌列表(支持递归)
function p.displayCard(frame)
function p.parseSubCardData(subCardNames, dataModule, moduleInfo)
     local args = frame.args
     if not subCardNames or not dataModule then
    local cardName = args[1] or args["name"]
         return nil
    local characterName = args[2] or args["character"] or ""
    local deckType = args[3] or args["deck"] or ""
   
    if not cardName then
         return "错误:未指定卡牌名称"
     end
     end
   
     local subCards = {}
    -- 优先从角色专属数据加载
     for subName in string.gmatch(subCardNames, '([^,]+)') do
     local cardData = nil
         subName = mw.text.trim(subName)
     local baseCardData = nil
         if subName ~= "" then
   
            local subCardData = dataModule[subName]
    if characterName ~= "" then
             if subCardData and subCardData.base then
         local charDataModule = "模块:卡牌数据/" .. characterName
                 local parsedCard = p.parseCardData(subCardData.base, "", subName, moduleInfo, { isVariant = false })
        local success, charData = pcall(mw.loadData, charDataModule)
                 if parsedCard then
         if success then
                     if subCardData.base.sub then
            -- 根据卡组类型查找
                         parsedCard.subCards = p.parseSubCardData(subCardData.base.sub, dataModule, moduleInfo)
            if deckType ~= "" and charData[deckType] then
                cardData = charData[deckType][cardName]
             else
                 -- 遍历所有卡组查找
                 for _, deck in pairs(charData) do
                     if type(deck) == "table" and deck[cardName] then
                         cardData = deck[cardName]
                        break
                     end
                     end
                    table.insert(subCards, parsedCard)
                 end
                 end
             end
             end
        end
       
        -- 如果是变体卡牌,获取基础卡牌数据
        if cardData then
            baseCardData = getBaseCardData(cardName)
         end
         end
     end
     end
   
     if #subCards == 0 then
    -- 如果没有找到专属数据,从通用数据加载
         return nil
     if not cardData then
         local data = mw.loadData("模块:卡牌数据")
        cardData = data[cardName]
     end
     end
      
     return subCards
    if not cardData then
        return string.format("错误:找不到卡牌 '%s' 的数据", cardName)
    end
   
    return buildCardHTML(frame, cardName, cardData, baseCardData, characterName)
end
end


-- 批量显示卡牌
-- 收集全部灵光一闪变体
function p.displayCards(frame)
local function collectInspirationVariants(baseData, cardDataWrapper, moduleName, cardName, moduleInfo, dataModule)
     local args = frame.args
     local variants = {}
     local cardList = args[1] or args["cards"] or ""
     if not cardDataWrapper.var or not cardDataWrapper.var.inspiration then
    local characterName = args[2] or args["character"] or ""
         return variants
    local columns = tonumber(args[3] or args["columns"]) or 5
   
    if cardList == "" then
         return "错误:未指定卡牌列表"
     end
     end
   
     for i, inspData in ipairs(cardDataWrapper.var.inspiration) do
    -- 解析卡牌列表
         local mergedData = mergeCardData(baseData, inspData, { clearSubUnlessSpecified = true })
    local cards = {}
         if mergedData then
     for card in mw.text.gsplit(cardList, ",") do
             local variantInfo = p.parseCardData(mergedData, moduleName, cardName, moduleInfo, {
         card = mw.text.trim(card)
                isVariant = true,
         if card ~= "" then
                baseApText = baseData.ap,
             table.insert(cards, card)
                baseDictStr = baseData.dict or ""
        end
             })
    end
             if variantInfo then
   
                 if mergedData.sub then
    if #cards == 0 then
                     variantInfo.subCards = p.parseSubCardData(mergedData.sub, dataModule, moduleInfo)
        return "错误:卡牌列表为空"
    end
   
    -- 构建HTML
    local html = {}
    table.insert(html, '<div style="display: flex; flex-wrap: wrap; gap: 10px;">')
   
    for i, cardName in ipairs(cards) do
        -- 获取卡牌数据
        local cardData = nil
        local baseCardData = nil
       
        if characterName ~= "" then
             local charDataModule = "模块:卡牌数据/" .. characterName
            local success, charData = pcall(mw.loadData, charDataModule)
             if success then
                 for _, deck in pairs(charData) do
                     if type(deck) == "table" and deck[cardName] then
                        cardData = deck[cardName]
                        break
                    end
                 end
                 end
                table.insert(variants, variantInfo)
             end
             end
           
            if cardData then
                baseCardData = getBaseCardData(cardName)
            end
        end
       
        if not cardData then
            local data = mw.loadData("模块:卡牌数据")
            cardData = data[cardName]
        end
       
        if cardData then
            table.insert(html, buildCardHTML(frame, cardName, cardData, baseCardData, characterName))
        else
            table.insert(html, string.format('<div style="color: red;">找不到卡牌: %s</div>', cardName))
        end
       
        -- 添加换行
        if i % columns == 0 and i < #cards then
            table.insert(html, '</div><div style="display: flex; flex-wrap: wrap; gap: 10px; margin-top: 10px;">')
         end
         end
     end
     end
   
     return variants
    table.insert(html, '</div>')
   
     return table.concat(html)
end
end


-- 显示角色的所有卡牌
-- 主入口
function p.displayCharacterCards(frame)
function p.main(frame)
     local args = frame.args
     local args = getArgs(frame, { removeBlank = true })
     local characterName = args[1] or args["character"]
 
     local deckType = args[2] or args["deck"] or "all"
     local moduleName = args[1]
     local columns = tonumber(args[3] or args["columns"]) or 5
    local cardName = args[2]
      
     local variantType = args[3]
     if not characterName then
    local variantParam = args[4]
         return "错误:未指定角色名称"
 
    if not moduleName or moduleName == "" then
        return err("错误: 未指定模块名")
    end
    if not cardName or cardName == "" then
        return err("错误: 未指定卡牌名")
    end
 
     local success, dataModule = pcall(require, 'Module:卡牌/' .. moduleName)
    if not success or not dataModule then
        return err('错误: 找不到模块 "Module:卡牌/' .. moduleName .. '"')
    end
 
    local moduleInfo = dataModule.info or {}
     local cardDataWrapper = dataModule[cardName]
     if not cardDataWrapper then
         return err('错误: 在模块 "' .. moduleName .. '" 中找不到卡牌 "' .. cardName .. '"')
     end
     end
   
 
     local charDataModule = "模块:卡牌数据/" .. characterName
     local baseData = cardDataWrapper.base
    local success, charData = pcall(mw.loadData, charDataModule)
     if not baseData then
   
         return err('错误: 卡牌 "' .. cardName .. '" 没有 base 数据')
     if not success then
         return string.format("错误:找不到角色 '%s' 的卡牌数据", characterName)
     end
     end
   
 
     -- 收集要显示的卡牌
     local finalCardData = baseData
     local cardsToDisplay = {}
     local isVariantFlag = false
   
 
     if deckType == "all" then
     if variantType then
         -- 显示所有卡组
         variantType = mw.text.trim(variantType)
         local deckOrder = {"起始卡牌", "基础卡牌", "高级卡牌", "其他卡牌"}
         if variantType == "灵光一闪" then
        for _, dName in ipairs(deckOrder) do
             if not cardDataWrapper.var or not cardDataWrapper.var.inspiration then
             if charData[dName] then
                 return err('错误: 卡牌 "' .. cardName .. '" 没有灵光一闪变体')
                 for cardName, cardData in pairs(charData[dName]) do
                    table.insert(cardsToDisplay, {name = cardName, data = cardData, deck = dName})
                end
             end
             end
        end
            local inspIndex = tonumber(variantParam)
    else
            if not inspIndex or inspIndex < 1 or inspIndex > #cardDataWrapper.var.inspiration then
        -- 显示特定卡组
                return err('错误: 灵光一闪变体索引 "' .. tostring(variantParam) .. '" 无效')
        if charData[deckType] then
            for cardName, cardData in pairs(charData[deckType]) do
                table.insert(cardsToDisplay, {name = cardName, data = cardData, deck = deckType})
             end
             end
            finalCardData = mergeCardData(baseData, cardDataWrapper.var.inspiration[inspIndex], { clearSubUnlessSpecified = true })
            isVariantFlag = true
         else
         else
             return string.format("错误:角色 '%s' 没有 '%s' 卡组", characterName, deckType)
             return err('错误: 未知变体类型 "' .. variantType .. '"')
         end
         end
     end
     end
   
 
     -- 排序(可选:按稀有度、AP值等排序)
     if not finalCardData then
    table.sort(cardsToDisplay, function(a, b)
         return err("错误: 最终卡牌数据为空")
         return a.name < b.name
    end)
   
    -- 构建HTML
    local html = {}
    table.insert(html, '<div style="display: flex; flex-wrap: wrap; gap: 10px;">')
   
    for i, card in ipairs(cardsToDisplay) do
        local baseCardData = getBaseCardData(card.name)
        table.insert(html, buildCardHTML(frame, card.name, card.data, baseCardData, characterName))
       
        if i % columns == 0 and i < #cardsToDisplay then
            table.insert(html, '</div><div style="display: flex; flex-wrap: wrap; gap: 10px; margin-top: 10px;">')
        end
     end
     end
   
    table.insert(html, '</div>')
   
    return table.concat(html)
end


-- 搜索卡牌
     local cardInfo = p.parseCardData(finalCardData, moduleName, cardName, moduleInfo, {
function p.searchCards(frame)
        isVariant = isVariantFlag,
     local args = frame.args
        baseApText = baseData and baseData.ap or nil,
    local searchTerm = args[1] or args["search"] or ""
        baseDictStr = baseData and baseData.dict or ""
    local searchType = args[2] or args["type"] or "name" -- name, description, mechanism
     })
    local columns = tonumber(args[3] or args["columns"]) or 5
     if not cardInfo then
      
         return err("错误: 解析卡牌数据失败")
     if searchTerm == "" then
         return "错误:未指定搜索词"
     end
     end
   
 
     searchTerm = mw.ustring.lower(searchTerm)
     -- 灵光一闪按钮集合
      
     if not variantType and cardDataWrapper.var and cardDataWrapper.var.inspiration and (baseData.isinspiration == 1) then
    local data = mw.loadData("模块:卡牌数据")
         cardInfo.hasInspiration = true
    local results = {}
         cardInfo.inspirationVariants = collectInspirationVariants(baseData, cardDataWrapper, moduleName, cardName, moduleInfo, dataModule)
   
    for cardName, cardData in pairs(data) do
        local match = false
       
        if searchType == "name" then
            match = mw.ustring.find(mw.ustring.lower(cardName), searchTerm) ~= nil
        elseif searchType == "description" then
            local desc = safeGet(cardData, "描述", "")
            match = mw.ustring.find(mw.ustring.lower(desc), searchTerm) ~= nil
         elseif searchType == "mechanism" then
            local mech = safeGet(cardData, "机制", "")
            match = mw.ustring.find(mw.ustring.lower(mech), searchTerm) ~= nil
         elseif searchType == "all" then
            local nameMatch = mw.ustring.find(mw.ustring.lower(cardName), searchTerm) ~= nil
            local descMatch = mw.ustring.find(mw.ustring.lower(safeGet(cardData, "描述", "")), searchTerm) ~= nil
            local mechMatch = mw.ustring.find(mw.ustring.lower(safeGet(cardData, "机制", "")), searchTerm) ~= nil
            match = nameMatch or descMatch or mechMatch
        end
       
        if match then
            table.insert(results, {name = cardName, data = cardData})
        end
     end
     end
      
 
     if #results == 0 then
    -- 主卡牌的衍生牌
         return "没有找到符合条件的卡牌"
     local subCardInfo = nil
     if finalCardData.sub then
         subCardInfo = p.parseSubCardData(finalCardData.sub, dataModule, moduleInfo)
     end
     end
    return displayModule.render(cardInfo, subCardInfo)
end
function p.batch(frame)
    local args = getArgs(frame, { removeBlank = false })
    local html = {}
      
      
     -- 排序
     -- 开始包裹容器
     table.sort(results, function(a, b)
     table.insert(html, '<div style="display:flex;gap:10px;flex-wrap:wrap;">')
        return a.name < b.name
    end)
      
      
     -- 构建HTML
     -- 遍历所有参数组
     local html = {}
     local i = 1
     table.insert(html, string.format('<div>找到 %d 张卡牌:</div>', #results))
     while args[i] do
    table.insert(html, '<div style="display: flex; flex-wrap: wrap; gap: 10px; margin-top: 10px;">')
        local moduleName = args[i]
   
        local cardName = args[i+1]
    for i, result in ipairs(results) do
        local variantType = args[i+2]
         table.insert(html, buildCardHTML(frame, result.name, result.data, nil, ""))
         local variantParam = args[i+3]
          
          
         if i % columns == 0 and i < #results then
         if moduleName and cardName then
             table.insert(html, '</div><div style="display: flex; flex-wrap: wrap; gap: 10px; margin-top: 10px;">')
            -- 清理空字符串为 nil
            if variantType == "" then variantType = nil end
            if variantParam == "" then variantParam = nil end
           
            -- 构造子 frame 调用 p.main
            local subArgs = {moduleName, cardName}
           
            -- 只有在变体类型非空时才添加变体参数
            if variantType then
                table.insert(subArgs, variantType)
                if variantParam then
                    table.insert(subArgs, variantParam)
                end
            end
           
            local subFrame = {
                args = subArgs,
                getParent = function() return frame end
            }
             table.insert(html, p.main(subFrame))
         end
         end
       
        i = i + 4  -- 每组4个参数(模块名,卡牌名,变体类型,变体参数)
     end
     end
      
      
    -- 结束包裹容器
     table.insert(html, '</div>')
     table.insert(html, '</div>')
      
      
     return table.concat(html)
     return table.concat(html, '')
end
end


return p
return p

2025年11月9日 (日) 21:02的最新版本

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

local p = {}

-- 使用官方参数解析器,自动合并父/子 frame 参数、去空白与空值
local getArgs = require('Module:Arguments').getArgs

-- 渲染模块
local displayModule = require('Module:卡牌/display')

-- 安全的错误输出
local function err(msg)
    return '<span style="color: red;">' .. mw.text.encode(msg) .. '</span>'
end

-- 解析 dict 字段
local function parseDictTokens(dictStr)
    local s = tostring(dictStr or '')
    if s == '' then return {} end

    local tokens = {}
    local processedText = s

    -- 提取 {{词典|机制}} 形式的 token(保留为 template 类型)
    for token in s:gmatch('{{%s*词典%s*|%s*([^}|]+)%s*[^}]*}}') do
        token = mw.text.trim(token or '')
        if token ~= '' then
            table.insert(tokens, { type = 'template', value = token })
        end
    end

    -- 提取 {{文本|颜色|内容}} 形式的内容 token(保留为模板)
    for content in s:gmatch('{{%s*文本%s*|%s*[^|]+%s*|%s*([^}]+)%s*}}') do
        content = mw.text.trim(content or '')
        if content ~= '' then
            table.insert(tokens, { type = 'template', value = content })
        end
    end

    -- 移除所有模板,保留纯文本部分
    processedText = processedText:gsub('{{%s*词典%s*|%s*[^}]*}}', '')
    processedText = processedText:gsub('{{%s*文本%s*|%s*[^}]*}}', '')

    -- 处理剩余的纯文本(支持中文顿号和逗号)
    processedText = processedText:gsub('、', ','):gsub(',', ',')
    for token in processedText:gmatch('([^,]+)') do
        token = mw.text.trim(token or '')
        if token ~= '' then
            table.insert(tokens, { type = 'text', value = token })
        end
    end

    -- 去重,保持顺序
    local seen, out = {}, {}
    for _, t in ipairs(tokens) do
        local key = t.type .. ':' .. t.value
        if not seen[key] then
            seen[key] = true
            table.insert(out, t)
        end
    end
    return out
end

-- 加载 模块:词典/data
local dictData = nil
do
    local ok, dictModule = pcall(mw.loadData, 'Module:词典/data')
    if ok and type(dictModule) == 'table' then
        if type(dictModule.dictionary) == 'table' then
            dictData = dictModule.dictionary
        else
            dictData = dictModule
        end
    end
end

-- 根据 tokens 生成词典条目数组 { {key=..., desc=...}, ... }
-- tokens 可以是:字符串数组 或 结构化数组 [{type="text",value="x"}, {type="template",value="y"}]
local function buildDictEntries(tokens)
    if not tokens or #tokens == 0 then return nil end
    local entries = {}

    for _, t in ipairs(tokens) do
        local key = nil

        -- 判断 token 是否为结构化对象(新格式)
        if type(t) == "table" and t.type and t.value then
            key = t.value
        -- 判断 token 是否为字符串(旧格式兼容)
        elseif type(t) == "string" then
            key = t
        end

        -- 如果 key 为 nil,跳过这个 token
        if not key then
            -- 不做任何事,跳过
        else
            -- 去空格
            key = mw.text.trim(key)

            -- 如果去空格后为空,也跳过
            if key ~= '' then
                local desc = nil
                local d = dictData and dictData[key]

                if type(d) == 'table' then
                    if d[1] and type(d[1]) == 'table' then
                        desc = d[1]["描述"] or d[1].desc or desc
                    elseif type(d["描述"]) == 'string' then
                        desc = d["描述"]
                    elseif type(d.desc) == 'string' then
                        desc = d.desc
                    end
                end

                table.insert(entries, { key = key, desc = desc })
            end
        end
    end

    return #entries > 0 and entries or nil
end

-- 合并 base 数据与变体数据
local function mergeCardData(baseData, variantData, opts)
    if not baseData then return nil end
    local merged = {}
    -- 浅拷贝 base
    for k, v in pairs(baseData) do
        merged[k] = v
    end
    -- 清空衍生卡牌信息
    if opts and opts.clearSubUnlessSpecified then
        merged.sub = nil
    end
    -- 覆盖变体数据
    if variantData then
        for k, v in pairs(variantData) do
            merged[k] = v
        end
    end
    return merged
end

-- 根据 base AP 与目标 AP 文本计算发光样式
-- 返回: 'blue' | 'red' | 'green' | 'gray'
local function calcApStyle(apText, baseApText, isVariant)
    local text = apText ~= nil and tostring(apText) or ''
    if not isVariant then
        return 'blue'
    end
    if text == 'X' then
        return 'blue'
    end
    local n = tonumber(text)
    local baseN = baseApText ~= nil and tonumber(baseApText) or nil
    if n and baseN then
        if n > baseN then
            return 'red'
        elseif n < baseN then
            return 'green'
        else
            return 'blue'
        end
    end
    -- 其他内容(非数值且非X)不发光
    return 'gray'
end

-- 将单张卡牌的数据转换为 cardInfo 结构(在此完成词典解析)
-- opts:
--   isVariant: boolean
--   baseApText: string|number
--   baseDictStr: string
function p.parseCardData(cardData, moduleName, cardName, moduleInfo, opts)
    if not cardData then
        return nil
    end

    local ego = ""
    if moduleInfo and moduleInfo.ego then
        ego = moduleInfo.ego
    elseif cardData.ego then
        ego = cardData.ego
    end

    -- 展示名称:优先 displayname,否则使用卡牌名
    local displayTitle = (cardData.displayname and mw.text.trim(tostring(cardData.displayname)) ~= '' and cardData.displayname) or cardName or ''

    -- AP 文本与样式
    local apText = cardData.ap ~= nil and tostring(cardData.ap) or ''
    local isVariant = opts and opts.isVariant or false
    local baseApText = opts and opts.baseApText or nil
    local apStyle = calcApStyle(apText, baseApText, isVariant)

    -- 词典解析
    local rawDictStr = cardData.dict or ""
    local dictStr = mw.text.trim(rawDictStr)
    local dictTokens
    local dictEntries
    local dictColor = "default"

    if isVariant then
        -- 获取base的dict用于比较
        local baseDictStr = opts and opts.baseDictStr or ""
        baseDictStr = mw.text.trim(baseDictStr)
        
        if dictStr == "空" then
            -- 显式设置为"空",说明是变体修改了
            dictTokens = {}
            dictEntries = nil
            dictColor = "green"
        elseif dictStr ~= "" and dictStr ~= baseDictStr then
            -- dict不为空,且与base不同,说明变体修改了
            dictTokens = parseDictTokens(dictStr)
            dictEntries = buildDictEntries(dictTokens)
            dictColor = "green"
        else
            -- dict为空或与base相同,说明是继承的
            dictTokens = parseDictTokens(dictStr)
            dictEntries = buildDictEntries(dictTokens)
            dictColor = "default"
        end
    else
        dictTokens = parseDictTokens(dictStr)
        dictEntries = buildDictEntries(dictTokens)
    end

    local cardInfo = {
        moduleName = moduleName or "",
        cardName = cardName or "",
        displayTitle = displayTitle or "",
        art = cardData.art or "",
        ego = ego or "",
        rarity = cardData.rarity or "",
        apText = apText,
        apStyle = apStyle,
        cardType = cardData.type or "",
        desc = cardData.desc_global or "",
        dict = dictStr,
        dictTokens = dictTokens,
        dictEntries = dictEntries,
        dictColor = dictColor,
        group = cardData.group or "",
        tag = cardData.tag or "",
        hasInspiration = false,
        inspirationVariants = {}
    }

    return cardInfo
end

-- 解析衍生卡牌列表(支持递归)
function p.parseSubCardData(subCardNames, dataModule, moduleInfo)
    if not subCardNames or not dataModule then
        return nil
    end
    local subCards = {}
    for subName in string.gmatch(subCardNames, '([^,]+)') do
        subName = mw.text.trim(subName)
        if subName ~= "" then
            local subCardData = dataModule[subName]
            if subCardData and subCardData.base then
                local parsedCard = p.parseCardData(subCardData.base, "", subName, moduleInfo, { isVariant = false })
                if parsedCard then
                    if subCardData.base.sub then
                        parsedCard.subCards = p.parseSubCardData(subCardData.base.sub, dataModule, moduleInfo)
                    end
                    table.insert(subCards, parsedCard)
                end
            end
        end
    end
    if #subCards == 0 then
        return nil
    end
    return subCards
end

-- 收集全部灵光一闪变体
local function collectInspirationVariants(baseData, cardDataWrapper, moduleName, cardName, moduleInfo, dataModule)
    local variants = {}
    if not cardDataWrapper.var or not cardDataWrapper.var.inspiration then
        return variants
    end
    for i, inspData in ipairs(cardDataWrapper.var.inspiration) do
        local mergedData = mergeCardData(baseData, inspData, { clearSubUnlessSpecified = true })
        if mergedData then
            local variantInfo = p.parseCardData(mergedData, moduleName, cardName, moduleInfo, {
                isVariant = true,
                baseApText = baseData.ap,
                baseDictStr = baseData.dict or ""
            })
            if variantInfo then
                if mergedData.sub then
                    variantInfo.subCards = p.parseSubCardData(mergedData.sub, dataModule, moduleInfo)
                end
                table.insert(variants, variantInfo)
            end
        end
    end
    return variants
end

-- 主入口
function p.main(frame)
    local args = getArgs(frame, { removeBlank = true })

    local moduleName = args[1]
    local cardName = args[2]
    local variantType = args[3]
    local variantParam = args[4]

    if not moduleName or moduleName == "" then
        return err("错误: 未指定模块名")
    end
    if not cardName or cardName == "" then
        return err("错误: 未指定卡牌名")
    end

    local success, dataModule = pcall(require, 'Module:卡牌/' .. moduleName)
    if not success or not dataModule then
        return err('错误: 找不到模块 "Module:卡牌/' .. moduleName .. '"')
    end

    local moduleInfo = dataModule.info or {}
    local cardDataWrapper = dataModule[cardName]
    if not cardDataWrapper then
        return err('错误: 在模块 "' .. moduleName .. '" 中找不到卡牌 "' .. cardName .. '"')
    end

    local baseData = cardDataWrapper.base
    if not baseData then
        return err('错误: 卡牌 "' .. cardName .. '" 没有 base 数据')
    end

    local finalCardData = baseData
    local isVariantFlag = false

    if variantType then
        variantType = mw.text.trim(variantType)
        if variantType == "灵光一闪" then
            if not cardDataWrapper.var or not cardDataWrapper.var.inspiration then
                return err('错误: 卡牌 "' .. cardName .. '" 没有灵光一闪变体')
            end
            local inspIndex = tonumber(variantParam)
            if not inspIndex or inspIndex < 1 or inspIndex > #cardDataWrapper.var.inspiration then
                return err('错误: 灵光一闪变体索引 "' .. tostring(variantParam) .. '" 无效')
            end
            finalCardData = mergeCardData(baseData, cardDataWrapper.var.inspiration[inspIndex], { clearSubUnlessSpecified = true })
            isVariantFlag = true
        else
            return err('错误: 未知变体类型 "' .. variantType .. '"')
        end
    end

    if not finalCardData then
        return err("错误: 最终卡牌数据为空")
    end

    local cardInfo = p.parseCardData(finalCardData, moduleName, cardName, moduleInfo, {
        isVariant = isVariantFlag,
        baseApText = baseData and baseData.ap or nil,
        baseDictStr = baseData and baseData.dict or ""
    })
    if not cardInfo then
        return err("错误: 解析卡牌数据失败")
    end

    -- 灵光一闪按钮集合
    if not variantType and cardDataWrapper.var and cardDataWrapper.var.inspiration and (baseData.isinspiration == 1) then
        cardInfo.hasInspiration = true
        cardInfo.inspirationVariants = collectInspirationVariants(baseData, cardDataWrapper, moduleName, cardName, moduleInfo, dataModule)
    end

    -- 主卡牌的衍生牌
    local subCardInfo = nil
    if finalCardData.sub then
        subCardInfo = p.parseSubCardData(finalCardData.sub, dataModule, moduleInfo)
    end

    return displayModule.render(cardInfo, subCardInfo)
end

function p.batch(frame)
    local args = getArgs(frame, { removeBlank = false })
    local html = {}
    
    -- 开始包裹容器
    table.insert(html, '<div style="display:flex;gap:10px;flex-wrap:wrap;">')
    
    -- 遍历所有参数组
    local i = 1
    while args[i] do
        local moduleName = args[i]
        local cardName = args[i+1]
        local variantType = args[i+2]
        local variantParam = args[i+3]
        
        if moduleName and cardName then
            -- 清理空字符串为 nil
            if variantType == "" then variantType = nil end
            if variantParam == "" then variantParam = nil end
            
            -- 构造子 frame 调用 p.main
            local subArgs = {moduleName, cardName}
            
            -- 只有在变体类型非空时才添加变体参数
            if variantType then
                table.insert(subArgs, variantType)
                if variantParam then
                    table.insert(subArgs, variantParam)
                end
            end
            
            local subFrame = {
                args = subArgs,
                getParent = function() return frame end
            }
            table.insert(html, p.main(subFrame))
        end
        
        i = i + 4  -- 每组4个参数(模块名,卡牌名,变体类型,变体参数)
    end
    
    -- 结束包裹容器
    table.insert(html, '</div>')
    
    return table.concat(html, '')
end

return p