模块

模块:卡牌

来自卡厄思梦境WIKI

律Rhyme留言 | 贡献2025年10月5日 (日) 15:10的版本

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

local p = {}

-- 加载词典模块
local dictionaryData = require("模块:词典/data")

-- 颜色映射表
local COLOR_MAP = {
    ["白"] = { bgColor = "rgba(249, 249, 249, 0.5)", textColor = "white" },
    ["蓝"] = { bgColor = "rgba(115, 236, 254, 0.5)", textColor = "#7de5ff" },
    ["橙"] = { bgColor = "rgba(254, 199, 109, 0.5)", textColor = "#ffee75" },
    ["彩"] = { bgColor = "rgba(201, 88, 241, 0.5)", textColor = "#eba2fc" }
}

-- 边框颜色映射(用于词典)
local BORDER_COLOR_MAP = {
    ["白"] = "#ffffff",
    ["蓝"] = "#0099ff",
    ["红"] = "#ff3333",
    ["橙"] = "#f2ba02"
}

-- 安全获取数据字段
local function safeGet(data, field, default)
    return data and data[field] or default
end

-- 处理描述文本中的模板
local function processDescription(frame, description)
    if not description or description == "" then
        return description
    end
    
    local success, result = pcall(function()
        return frame:preprocess(description)
    end)
    
    return success and result or description
end

-- 安全的字符串分割函数(使用 MediaWiki 函数处理 UTF-8)
local function splitString(str, delimiter)
    if not str or str == "" then
        return {}
    end
    
    -- 使用 mw.text.split 进行分割(UTF-8 安全)
    local parts = mw.text.split(str, delimiter, true)
    
    local result = {}
    for _, part in ipairs(parts) do
        -- 使用 mw.text.trim 去除空格(UTF-8 安全)
        local trimmed = mw.text.trim(part)
        if trimmed ~= "" then
            table.insert(result, trimmed)
        end
    end
    
    return result
end

-- 生成机制说明HTML
local function generateMechanismHTML(mechanism)
    if not mechanism or mechanism == "" or mechanism == "无" then
        return ""
    end
    
    -- 使用 UTF-8 安全的分割方法
    local mechanismList = {}
    
    -- 先按"、"分割
    local parts1 = splitString(mechanism, "、")
    for _, part in ipairs(parts1) do
        -- 再按"/"分割
        local parts2 = splitString(part, "/")
        for _, p in ipairs(parts2) do
            table.insert(mechanismList, p)
        end
    end
    
    local htmlParts = {}
    
    for _, mechName in ipairs(mechanismList) do
        local entries = dictionaryData.dictionary[mechName]
        
        if entries then
            -- 找到"卡牌机制"类型的条目
            local entry = nil
            for _, e in ipairs(entries) do
                if e["类型"] == "卡牌机制" then
                    entry = e
                    break
                end
            end
            
            if entry then
                local borderColor = BORDER_COLOR_MAP[entry["颜色"]] or BORDER_COLOR_MAP["白"]
                local description = entry["描述"] or "暂无说明"
                local icon = entry["icon"] or ""
                
                -- 构建图标HTML
                local iconHTML = ""
                if icon ~= "" then
                    iconHTML = '[[File:' .. icon .. '|25px|link=]] '
                end
                
                -- 构建机制显示HTML
                local mechHTML = string.format([[
                    <div class="mechanism-card" style="margin-bottom: 10px;">
                        <div style="width: 100%%; height: 2px; background-color: %s;"></div>
                        <div style="width: 100%%; background-color: #343434; color: white; padding: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.3); line-height: 1.6;">
                            <div style="margin-bottom: 5px; font-weight: bold;">%s%s</div>
                            <div>%s</div>
                        </div>
                    </div>
                ]], borderColor, iconHTML, mechName, description)
                
                table.insert(htmlParts, mechHTML)
            else
                -- 没有找到对应类型的条目
                table.insert(htmlParts, string.format([[
                    <div class="mechanism-card" style="margin-bottom: 10px;">
                        <div style="width: 100%%; height: 2px; background-color: #f2ba02;"></div>
                        <div style="width: 100%%; background-color: #343434; color: white; padding: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.3);">
                            <div style="margin-bottom: 5px; font-weight: bold;">%s</div>
                            <div style="color: #ccc;">暂无详细说明</div>
                        </div>
                    </div>
                ]], mechName))
            end
        else
            -- 词典中没有该词条
            table.insert(htmlParts, string.format([[
                <div class="mechanism-card" style="margin-bottom: 10px;">
                    <div style="width: 100%%; height: 2px; background-color: #f2ba02;"></div>
                    <div style="width: 100%%; background-color: #343434; color: white; padding: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.3);">
                        <div style="margin-bottom: 5px; font-weight: bold;">%s</div>
                        <div style="color: #ccc;">暂无详细说明</div>
                    </div>
                </div>
            ]], mechName))
        end
    end
    
    if #htmlParts > 0 then
        -- 添加标题和容器
        return string.format([[
            <div class="mechanism-container" style="display: none;">
                %s
            </div>
        ]], table.concat(htmlParts))
    end
    
    return ""
end

-- 生成卡牌HTML样式组件
local function buildStyleComponents(cardData, cardName, baseCardData, originalCardData)
    local color = safeGet(cardData, "稀有度", "白")
    local colorStyle = COLOR_MAP[color] or COLOR_MAP["白"]
    local attribute = safeGet(cardData, "属性", "虚无")
    local cardDeck = safeGet(cardData, "卡组", "起始卡牌")
    
    local ap = safeGet(cardData, "AP", "")
    local originalAP = baseCardData and safeGet(baseCardData, "AP", ap) or ap
    
    -- 决定AP的发光颜色
    local apGlowColor = "#4a90e2" -- 默认蓝色
    if baseCardData and ap ~= originalAP then
        if ap == "X" then
            apGlowColor = "#4a90e2" -- X值保持默认蓝色
        elseif tonumber(ap) and tonumber(originalAP) then
            if tonumber(ap) < tonumber(originalAP) then
                apGlowColor = "#51f651" -- 绿色(AP降低)
            else
                apGlowColor = "#f65151" -- 红色(AP增加)
            end
        end
    end
    
    -- 检查原始变体卡牌数据中是否指定了机制字段
    local mechanism = safeGet(cardData, "机制", "")
    local mechanismChanged = false
    
    -- 判断机制是否发生了变化(变体卡牌主动指定了机制字段)
    if baseCardData and originalCardData then
        -- 检查原始变体卡牌数据中是否有"机制"字段
        if originalCardData["机制"] ~= nil then
            -- 变体卡牌主动指定了机制字段,无论内容是什么都标记为已更改
            mechanismChanged = true
        end
    end
    
    -- 将"无"视为空字符串
    if mechanism == "无" then mechanism = "" end
    
    return {
        color = color,
        colorStyle = colorStyle,
        attribute = attribute,
        ap = ap,
        apGlowColor = apGlowColor,
        cardType = safeGet(cardData, "类型", ""),
        art = safeGet(cardData, "art", ""),
        mechanism = mechanism,
        mechanismChanged = mechanismChanged
    }
end

-- 去除模板语法,计算纯文本字数
local function getPlainTextLength(text)
    if not text or text == "" then
        return 0
    end
    
    -- 去除所有模板 {{...}}(支持嵌套)
    local plainText = text
    local maxIterations = 10  -- 防止无限循环
    local iteration = 0
    
    while iteration < maxIterations do
        local newText = plainText:gsub("{{[^{}]*}}", "")
        if newText == plainText then
            break  -- 没有更多模板可以移除
        end
        plainText = newText
        iteration = iteration + 1
    end
    
    -- 去除HTML标签
    plainText = plainText:gsub("<[^>]*>", "")
    
    -- 去除多余的空白字符
    plainText = plainText:gsub("%s+", "")
    
    -- 返回字符数(UTF-8字符计数)
    local _, count = plainText:gsub("[%z\1-\127\194-\244][\128-\191]*", "")
    return count
end

-- 构建卡牌HTML
local function buildCardHTML(frame, cardName, cardData, baseCardData, characterName, originalCardData)
    if not cardData then
        return string.format("找不到卡牌数据: %s", cardName or "未指定")
    end
    
    -- 获取显示名称,如果存在则使用,否则使用默认卡牌名称
    local displayName = safeGet(cardData, "显示名称", cardName)
    
    local style = buildStyleComponents(cardData, cardName, baseCardData, originalCardData)
    local description = processDescription(frame, safeGet(cardData, "描述", ""))
    
    -- 预处理机制字段中的模板
    local mechanism = safeGet(cardData, "机制", "")
    if mechanism == "无" then 
        mechanism = "" 
    elseif mechanism ~= "" then
        mechanism = processDescription(frame, mechanism)
    end
    
    -- 更新 style 中的 mechanism
    style.mechanism = mechanism
    
    -- 获取衍生卡牌信息
    local derivedCards = safeGet(cardData, "衍生卡牌", "")
    local deckType = safeGet(cardData, "卡组", "")
    
    -- 检查是否是自我意识技能
    if deckType == "自我意识技能" then
        -- 生成机制HTML
        local mechanismHTML = generateMechanismHTML(mechanism)
        
        -- 生成数据属性
        local hasMechanism = (mechanism ~= "" and mechanism ~= "无") and "true" or "false"
        local dataAttrs = string.format('data-card-name="%s" data-character="%s" data-deck-type="%s" data-derived-cards="%s" data-has-mechanism="%s"',
            cardName or "", characterName or "", deckType or "", derivedCards or "", hasMechanism)
        
        -- 自我意识技能的特殊布局
        local attribute = safeGet(cardData, "属性", "虚无")
        local cost = safeGet(cardData, "AP", "1")
        local art = safeGet(cardData, "art", "")
        
        local htmlParts = {
            string.format('<div class="game-card" %s style="display: inline-block; vertical-align: top; position: relative; width: 155px; height: 235px; overflow: hidden; margin: 0px; cursor: pointer;">', dataAttrs),
            string.format('<div style="position: absolute; top: 0px; left: 4px; pointer-events: none;">[[File:%s|150px|link=]]</div>', art),
            string.format('<div style="position: absolute; top: 0px; left: 0px; pointer-events: none;">[[File:tp_%s.png|154px|link=]]</div>', attribute),
            string.format('<div style="position: absolute; top: 0px; left: 0px; pointer-events: none;">[[File:tp_cost_%s.png|154px|link=]]</div>', cost),
            string.format('<div style="position: absolute; top: 105px; left: 17px; color: white; font-size: 13px; user-select: text; cursor: text;">%s</div>', displayName),
            string.format('<div style="position: absolute; top: 138px; left: 17px; width: 135px; height: 90px; color: white; font-size: 13px; line-height: 14px; user-select: text; cursor: text;">%s</div>', description),
            '<div style="position: absolute; top: 0px; left: 0px; pointer-events: none;">[[File:tp_顶层蒙版.png|154px|link=]]</div>',
            '</div>'
        }
        
        -- 在卡牌div之后添加隐藏的机制说明
        local result = table.concat(htmlParts)
        if mechanismHTML ~= "" then
            result = result .. mechanismHTML
        end
        
        return result
    end
    
    -- 普通卡牌
    -- 生成机制HTML
    local mechanismHTML = generateMechanismHTML(mechanism)
    
    -- 生成数据属性
    local hasMechanism = (mechanism ~= "" and mechanism ~= "无") and "true" or "false"
    local dataAttrs = string.format('data-card-name="%s" data-character="%s" data-deck-type="%s" data-derived-cards="%s" data-has-mechanism="%s"',
        cardName or "", characterName or "", deckType or "", derivedCards or "", hasMechanism)
    
    -- 处理机制文本
    local mechanismDisplayHTML = ""
    if style.mechanism and style.mechanism ~= "" and style.mechanism ~= "无" then
        -- 如果是变体卡牌且主动指定了机制字段,显示为绿色;否则显示为橙色
        local mechanismColor = style.mechanismChanged and "#b4f352" or "#f6c478"
        mechanismDisplayHTML = string.format('<div style="color: %s; margin-bottom: 4px;">[%s]</div>', 
            mechanismColor, style.mechanism:gsub("、", "/"))
    end
    
    -- 检测描述文本是否需要滚动(只检测描述部分,去除模板后计算字数)
    local plainTextLength = getPlainTextLength(description)
    local needScroll = plainTextLength > 50  -- 超过50个字符才启用滚动
    local scrollClass = needScroll and " card-description-scrollable" or ""
    
    -- 使用字符串拼接优化HTML生成
    local htmlParts = {
        string.format('<div class="game-card%s" %s style="display: inline-block; vertical-align: top; position: relative; width: 168px; height: 230px; overflow: hidden; margin: 0px; cursor: pointer;">', 
            scrollClass, dataAttrs),
    }
    
    -- 根据类型判断使用哪种布局
    if style.cardType == "状态异常" then
        -- 状态异常卡牌的特殊布局
        table.insert(htmlParts, string.format('<div style="position: absolute; top: 5px; left: 5px;">[[File:%s|150px|link=]]</div>', style.art))
        table.insert(htmlParts, string.format('<div style="position: absolute; top: 0px; left: 0px;">[[File:card_状态异常_%s.png|159px|link=]]</div>', style.attribute))
        
        -- 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))
        
        -- 卡牌名称和类型(使用显示名称)
        table.insert(htmlParts, string.format('<div style="position: absolute; left: 50px; top: 13px; color: %s; font-size:16px">%s</div>', 
            style.colorStyle.textColor, displayName))
        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>')
        
    else
        -- 普通卡牌的布局
        -- 背景图片层
        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))
        
        -- 卡牌名称和类型(使用显示名称)
        table.insert(htmlParts, string.format('<div style="position: absolute; left: 50px; top: 13px; color: %s; font-size:16px">%s</div>', 
            style.colorStyle.textColor, displayName))
        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
    
    -- 描述文本区域 - 机制文本固定在上方,只有描述文本在下方可滚动
    table.insert(htmlParts, [[
<div class="card-description-scroll">
    <div class="card-description-scroll-inner">]])
    
    -- 机制文本(固定,不滚动)
    if mechanismDisplayHTML ~= "" then
        table.insert(htmlParts, mechanismDisplayHTML)
    end
    
    -- 描述文本(可滚动)
    table.insert(htmlParts, string.format('<div class="card-description-text">%s</div>', description))
    
    table.insert(htmlParts, [[
    </div>
</div>]])
    
    table.insert(htmlParts, '</div>')
    
    -- 在卡牌div之后添加隐藏的机制说明
    if mechanismHTML ~= "" then
        table.insert(htmlParts, mechanismHTML)
    end
    
    return table.concat(htmlParts)
end

-- 合并卡牌数据
local function mergeCardData(baseData, overrideData)
    local result = {}
    -- 复制基础卡牌的所有属性
    for k, v in pairs(baseData or {}) do
        result[k] = v
    end
    
    -- 清空衍生卡牌属性
    if overrideData and next(overrideData) ~= nil then
        result["衍生卡牌"] = nil
    end
    
    -- 应用覆盖数据
    for k, v in pairs(overrideData or {}) do
        result[k] = v
    end
    
    return result
end

-- 获取并显示所有默认卡牌
local function displayAllDefaultCards(frame, characterModule, characterName)
    local cards = characterModule.card or {}
    local cardOrder = characterModule.cardOrder or {}
    local displayedCards = {}
    
    -- 按cardOrder顺序遍历
    for orderIndex, name in ipairs(cardOrder) do
        local cardArray = cards[name]
        if cardArray and #cardArray > 0 then
            -- 只显示第一张卡牌
            table.insert(displayedCards, {
                name = name,
                data = cardArray[1],
                order = orderIndex
            })
        end
    end
    
    -- 生成HTML
    local htmlParts = {}
    for i, cardInfo in ipairs(displayedCards) do
        table.insert(htmlParts, buildCardHTML(frame, cardInfo.name, cardInfo.data, nil, characterName, nil))
    end
    
    return table.concat(htmlParts)
end

-- 处理特定卡牌请求
local function handleSpecificCard(frame, cardName, cardData, deckFilter, cardIndex, characterName)
    local baseCard = cardData[1]
    
    -- 如果没有指定卡组筛选,返回基础卡牌
    if deckFilter == "" then
        return buildCardHTML(frame, cardName, baseCard, nil, characterName, nil)
    end
    
    -- 筛选指定卡组的卡牌
    local filteredCards = {}
    for _, card in ipairs(cardData) do
        if card["卡组"] == deckFilter then
            table.insert(filteredCards, card)
        end
    end
    
    if #filteredCards == 0 then
        return string.format("找不到卡组 '%s' 的卡牌: %s", deckFilter, cardName)
    end
    
    -- 如果指定了索引,返回特定卡牌
    if cardIndex > 0 and cardIndex <= #filteredCards then
        local originalCard = filteredCards[cardIndex]  -- 保存原始变体卡牌数据
        local mergedCard = mergeCardData(baseCard, originalCard)
        return buildCardHTML(frame, cardName, mergedCard, baseCard, characterName, originalCard)
    end
    
    -- 否则返回该卡组的所有卡牌
    local htmlParts = {}
    for i, card in ipairs(filteredCards) do
        local originalCard = card  -- 保存原始变体卡牌数据
        local mergedCard = mergeCardData(baseCard, originalCard)
        table.insert(htmlParts, buildCardHTML(frame, cardName, mergedCard, baseCard, characterName, originalCard))
    end
    
    return table.concat(htmlParts)
end

-- 主函数
function p.main(frame)
    local args = frame.args
    local characterName = args[1] or ""
    local cardName = args[2] or ""
    local deckFilter = args[3] or ""
    local cardIndex = tonumber(args[4]) or 0
    
    -- 加载战斗员模块
    local success, characterModule = pcall(require, "模块:卡牌/" .. characterName)
    if not success or not characterModule then
        return string.format("找不到战斗员卡牌数据模块: 模块:卡牌/%s", characterName)
    end
    
    -- 如果没有指定卡牌名,显示所有默认卡牌
    if cardName == "" then
        return displayAllDefaultCards(frame, characterModule, characterName)
    end
    
    -- 获取指定卡牌数据
    local cardData = (characterModule.card or {})[cardName]
    if not cardData then
        return string.format("找不到卡牌: %s", cardName)
    end
    
    return handleSpecificCard(frame, cardName, cardData, deckFilter, cardIndex, characterName)
end

-- 元表设置,支持直接调用战斗员名称作为方法
setmetatable(p, {
    __index = function(t, characterName)
        return function(frame)
            frame.args = {
                [1] = characterName,
                [2] = frame.args[1] or "",
                [3] = frame.args[2] or "",
                [4] = frame.args[3] or 0
            }
            return p.main(frame)
        end
    end
})

return p