全站通知:
            
            
        
模块:FishPond
                  
                  
                  刷
                  
                     
                               
                               历
                            
                  
                    
                      
                        
                        编
                      
                    
                
            
            
            
            
            
            跳到导航
            跳到搜索
            
            
                
            
            
            
            
        
    本模块用于计算和输出鱼塘的“任务”和“产物”表格。
本模块涉及到的相关模块:
本模块涉及到的相关模板:
代码复杂度过高,一般情况下不建议尝试修改(如果有能力,建议进行重构)。
[ 查看 | 编辑 | 历史 | 刷新 ]上述文档的内容来自模块:FishPond/doc。
local cache = require "mw.ext.LuaCache"
local KEY_PREFIX = "FishPond_Update"
local EXP_TIME = 172800
local utils = require("Module:Utils")
local object = require("Module:Object")
local items = require("Module:Items")
---@diagnostic disable-next-line: undefined-global
local mw = mw
local FishPondData = mw.loadData('Module:FishPond/data')
local FishPond = {}
local VALID_FISH_TAGS = {
    fish_legendary = true,
    fish_desert = true,
    fish_semi_rare = true,
    fish_carnivorous = true,
    fish_freshwater = true,
    fish_crab_pot = true,
    fish_ocean = true,
    fish_river = true,
    fish_lake = true
}
-- ---------------------------------------------------------------------------
--  Utility helpers
-- ---------------------------------------------------------------------------
local function generateRowLocal(serializedRow)
    local rowParts = {}
    for part in string.gmatch(serializedRow, "([^,]+)") do
        table.insert(rowParts, part)
    end
    local result = {}
    local i = 1
    table.insert(result, string.format('<td data-sort-value="%s">%s</td>',
                                       rowParts[i + 1], rowParts[i]))
    i = i + 2
    while i <= #rowParts - 3 do
        local colspan = tonumber(rowParts[i])
        local content = rowParts[i + 1] or "/"
        table.insert(result, string.format('<td colspan="%d">%s</td>', colspan,
                                           content))
        i = i + 2
    end
    table.insert(result, string.format('<td>%s</td>', rowParts[i]))
    table.insert(result, string.format('<td>%s</td>', rowParts[i + 1]))
    return "<tr>" .. table.concat(result) .. "</tr>"
end
local function toPercentage(value, omitSuffix, multiplier, formatter)
    if value == "/" then return "/" end
    if value == " " then return " " end
    if value == "—" then return "—" end
    multiplier = multiplier or 100
    formatter = formatter or "%.1f"
    local formatted = string.format(formatter, tonumber(value) * multiplier)
    formatted = formatted:gsub("%.00$", ""):gsub("%.0$", ""):gsub("%.([1-9])0$",
                                                                  ".%1")
    if omitSuffix then return formatted end
    return formatted .. "%"
end
local function normalizeOwnedId(rawId)
    if type(rawId) ~= "string" then return nil end
    if rawId:sub(1, 3) ~= "(O)" then return nil end
    return rawId:sub(4)
end
local function normaliseFishName(name)
    if type(name) ~= "string" then return nil end
    return name:lower():gsub(" ", "")
end
local function getFishTagsById(id)
    if not id or id == "" then return nil end
    local fields = object.getFieldsById(id)
    if type(fields) ~= "table" then return nil end
    local tags = fields.ContextTags
    if type(tags) ~= "table" then return nil end
    local matched = {}
    for _, tag in ipairs(tags) do
        if VALID_FISH_TAGS[tag] then table.insert(matched, tag) end
    end
    if #matched == 0 then return nil end
    return matched
end
local function findFishData(fishKey)
    if type(fishKey) == "table" then
        for _, fish in ipairs(FishPondData) do
            local requiredTags = fish["RequiredTags"] or {}
            if requiredTags == {} then return nil end -- Intentional no-op parity
            if utils.tablesEqual(fishKey, requiredTags) then
                return fish
            end
        end
        for _, fish in ipairs(FishPondData) do
            local requiredTags = fish["RequiredTags"] or {}
            if requiredTags == {} then return nil end -- Intentional no-op parity
            if utils.tablesOverlap(fishKey, requiredTags) then
                return fish
            end
        end
        return nil
    end
    local lowerFishName = normaliseFishName(fishKey)
    if not lowerFishName then return nil end
    for _, fish in ipairs(FishPondData) do
        local fishId = fish['Id']
        if normaliseFishName(fishId) == lowerFishName then return fish end
    end
    return nil
end
local function resolveFishData(context)
    local fishData = findFishData(context.name)
    if fishData then return fishData, nil end
    local tag = getFishTagsById(context.id)
    if tag then
        local tagData = findFishData(tag)
        if tagData then return tagData, tag end
    end
    return nil, tag
end
local function buildFishContext(fishName)
    if not fishName then return nil end
    local rawId = items.getId(fishName)
    local id = normalizeOwnedId(rawId)
    if not id then return nil end
    local fields = object.getFieldsById(id) or {}
    local name = fields["Name"]
    if not name then return nil end
    local context = {
        original = fishName,
        rawId = rawId,
        id = id,
        fields = fields,
        name = name
    }
    local fishData, tag = resolveFishData(context)
    context.data = fishData
    context.tag = tag
    return context
end
local function ensureContext(frame)
    local frameArgs = frame and frame.args or {}
    local fishName = frameArgs[1]
    local context = buildFishContext(fishName)
    return context
end
local function computeSpawnTime(context)
    if not context or not context.data then return nil end
    local spawnTime = context.data['SpawnTime']
    if tonumber(spawnTime or -1) ~= -1 then return spawnTime end
    local price = tonumber(object.getPriceById(context.id)) or 0
    if price <= 30 then
        return 1
    elseif price <= 80 then
        return 2
    elseif price <= 120 then
        return 3
    elseif price <= 250 then
        return 4
    else
        return 5
    end
end
local function getPopulationGateList(context)
    if not context or not context.data then return nil end
    local populationGates = context.data['PopulationGates']
    if not populationGates then return nil end
    local sortedGates = {}
    for population, gates in pairs(populationGates) do
        table.insert(sortedGates,
                     {population = tonumber(population), gates = gates})
    end
    table.sort(sortedGates,
               function(a, b) return a.population < b.population end)
    return sortedGates
end
local function gateItemsToString(gates)
    local requiredItems = {}
    for _, gateItem in ipairs(gates) do
        local itemId, quantity = gateItem:match("^(%S+) (%S+)%s*(%S*)$")
        if not itemId then
            itemId = gateItem
            quantity = 1
        elseif quantity == "" then
            quantity = itemId
            itemId = gateItem
        elseif quantity:find("~") then
            quantity = quantity
        end
        local english = items.getEnglishNameById(itemId)
        local safeName = english and english:gsub(":", "") or ""
        table.insert(requiredItems, utils.expandTemplate("模板:Name", {
            safeName,
            quantity,
            class = "inline"
        }))
    end
    if #requiredItems == 1 then return requiredItems[1] end
    if #requiredItems == 2 then
        return requiredItems[1] .. "或" .. requiredItems[2]
    end
    return table.concat(requiredItems, "、", 1, #requiredItems - 1) .. "或" ..
               requiredItems[#requiredItems]
end
local function collectOpacityRows(context)
    local data = context and context.data
    if not data then return nil end
    if context.tag == "fish_legendary" then return nil end
    local gates = getPopulationGateList(context)
    if not gates or #gates == 0 then return nil end
    local maximumPopulation = data['MaxPopulation'] or 11
    if tonumber(maximumPopulation) == -1 then maximumPopulation = 11 end
    local spawnTime = computeSpawnTime(context) or 0
    local rows = {}
    for gateIndex, gateInfo in ipairs(gates) do
        local currentPopulation = gateInfo.population
        local thresholdPopulation
        if gateIndex < #gates then
            thresholdPopulation = gates[gateIndex + 1].population - 1
        else
            thresholdPopulation = tonumber(maximumPopulation) - 1
        end
        local itemsSummary = gateItemsToString(gateInfo.gates)
        local experienceReward = 20 + spawnTime * 5
        table.insert(rows, {
            population = currentPopulation,
            nextPopulation = thresholdPopulation,
            items = itemsSummary,
            exp = experienceReward
        })
    end
    return {rows = rows, spawnTime = spawnTime}
end
local function prepareProducedItemContext(context)
    local data = context.data
    if not data or not data['ProducedItems'] then return nil end
    if context.tag == "fish_legendary" then return nil end
    local maxPopulation = data['MaxPopulation'] or 11
    if maxPopulation == "-1" or maxPopulation == -1 then maxPopulation = 11 end
    local basePrice = 30 + (tonumber(object.getPriceById(context.id)) or 0) *
                          0.5
    local roeColor = object.getColorById(context.id)
    local iconName = (roeColor and roeColor .. " Roe") or "Roe"
    if not utils.fileExists(iconName .. ".png") then iconName = "Roe" end
    local producedItems = data['ProducedItems']
    local productEntries = {}
    local productOrder = {}
    local sortedProducedItems = {}
    for index, item in ipairs(producedItems) do
        local clonedItem = utils.deepCopy(item)
        clonedItem.priority = index
        table.insert(sortedProducedItems, {
            requiredPopulation = tonumber(item.RequiredPopulation),
            chance = clonedItem.Chance,
            item = clonedItem
        })
    end
    table.sort(sortedProducedItems, function(a, b)
        return a.requiredPopulation < b.requiredPopulation
    end)
    local emptySlotNeeded = false
    for _, wrappedItem in ipairs(sortedProducedItems) do
        local item = wrappedItem.item
        local requiredPopulation = item.RequiredPopulation > 0 and
                                       item.RequiredPopulation or 1
        local rawItemId = item.ItemId
        local cleanItemId = normalizeOwnedId(rawItemId) or rawItemId
        local price = tonumber(object.getPriceById(cleanItemId)) or 0
        if cleanItemId == "812" then price = basePrice end
        local experienceReward = 10 + price * 0.04
        local englishName = items.getEnglishNameById(rawItemId)
        local minStack = item.MinStack ~= -1 and item.MinStack or 1
        local maxStack = item.MaxStack ~= -1 and item.MaxStack or minStack
        local quantityLabel = (minStack == maxStack) and tostring(minStack) or
                                  (minStack .. "~" .. maxStack)
        local entryKey = (englishName or "") .. "_" .. minStack .. "_" ..
                             maxStack
        local safeName = englishName and englishName:gsub(":", "") or ""
        if not productEntries[entryKey] then
            productEntries[entryKey] = {
                item = utils.expandTemplate("模板:Name", {
                    safeName,
                    quantityLabel,
                    class = "inline"
                }),
                requiredData = {},
                quantity = quantityLabel,
                priority = item.priority,
                experience = experienceReward or 0
            }
            table.insert(productOrder, entryKey)
        end
        local requirementSegments = productEntries[entryKey].requiredData
        if requiredPopulation == 10 then
            table.insert(requirementSegments, {
                rangeA = requiredPopulation,
                rangeB = requiredPopulation,
                priority = item.priority
            })
        else
            table.insert(requirementSegments, {
                rangeA = requiredPopulation,
                priority = item.priority
            })
        end
        if #productOrder == 1 and item.Chance ~= 1 then
            if not productEntries['ZZZZ'] then
                productEntries['ZZZZ'] = {
                    item = '无',
                    requiredData = {},
                    quantity = quantityLabel,
                    priority = -1,
                    experience = "—"
                }
            end
            local emptySegments = productEntries['ZZZZ'].requiredData
            local existingCount = #emptySegments
            if existingCount ~= 0 then
                local currentRangeEnd = emptySegments[existingCount].rangeB
                if not currentRangeEnd then
                    emptySegments[existingCount].rangeB = requiredPopulation - 1
                end
            end
            table.insert(emptySegments,
                         {rangeA = requiredPopulation, priority = -1})
            emptySlotNeeded = true
        end
        local evaluationOrder = utils.deepCopy(productOrder)
        if emptySlotNeeded then table.insert(evaluationOrder, 'ZZZZ') end
        for _, evaluationKey in ipairs(evaluationOrder) do
            local segments = productEntries[evaluationKey].requiredData
            local segmentIndex = #segments
            local segmentPriority = segments[segmentIndex].priority or -1
            if segmentPriority >= item.priority or evaluationKey == 'ZZZZ' then
                local previousPopulation = requiredPopulation - 1
                local rangeStart = segments[segmentIndex].rangeA
                local rangeEnd = segments[segmentIndex].rangeB
                if rangeStart <= previousPopulation then
                    if not rangeEnd and rangeStart <= previousPopulation then
                        segments[segmentIndex].rangeB = previousPopulation
                    end
                    local nextRangeEnd = nil
                    if previousPopulation + 1 == 10 then
                        nextRangeEnd = 10
                    end
                    if rangeEnd ~= 10 then
                        table.insert(segments, {
                            chance = nil,
                            rangeA = previousPopulation + 1,
                            rangeB = nextRangeEnd,
                            priority = segmentPriority
                        })
                    end
                end
            end
        end
        local priorSegmentIndex = #requirementSegments - 1
        if priorSegmentIndex ~= 0 and
            not requirementSegments[priorSegmentIndex].rangeB then
            requirementSegments[priorSegmentIndex].rangeB =
                requiredPopulation - 1
        end
    end
    if emptySlotNeeded then table.insert(productOrder, 'ZZZZ') end
    for _, entryKey in ipairs(productOrder) do
        local segments = productEntries[entryKey].requiredData
        local lastSegment = segments[#segments]
        if lastSegment and not lastSegment.rangeB then
            lastSegment.rangeB = maxPopulation - 1
        end
    end
    local producedItemsClone = utils.deepCopy(producedItems)
    for index, clone in ipairs(producedItemsClone) do clone.priority = index end
    local totalItems = #producedItemsClone
    local processedPopulations = {}
    local lastRequired = -1
    local lastChance = -1
    local probabilityLayers = {}
    local populationThresholds = {}
    for _, clone in ipairs(producedItemsClone) do
        table.insert(populationThresholds, clone.RequiredPopulation)
    end
    local uniqueThresholds = utils.uniqueValues(populationThresholds)
    table.sort(uniqueThresholds, function(a, b) return a > b end)
    for startIndex = 1, totalItems do
        local values = {}
        local priorChances = {}
        local totalProbability = 0
        local currentRequiredPopulation =
            producedItemsClone[startIndex].RequiredPopulation
        if not utils.tableContains(processedPopulations,
                                   currentRequiredPopulation) or lastRequired ==
            -1 then
            lastRequired = currentRequiredPopulation
            table.insert(processedPopulations, currentRequiredPopulation)
            for i = 1, totalItems do
                local item = producedItemsClone[i]
                if currentRequiredPopulation >= item.RequiredPopulation and
                    lastChance ~= 1 then
                    lastChance = item.Chance
                    local availableProbability = 1
                    for _, previousChance in ipairs(priorChances) do
                        availableProbability =
                            availableProbability * (1 - previousChance)
                    end
                    local itemId = item.ItemId
                    local englishName = items.getEnglishNameById(itemId)
                    local minStack = item.MinStack ~= -1 and item.MinStack or 1
                    local maxStack = item.MaxStack ~= -1 and item.MaxStack or
                                         minStack
                    local entryKey = (englishName or "") .. "_" .. minStack ..
                                         "_" .. maxStack
                    table.insert(priorChances, item.Chance)
                    local exactValue = availableProbability * item.Chance * 100
                    table.insert(values, {id = entryKey, prob = exactValue})
                    totalProbability = totalProbability + exactValue
                end
                if lastChance == 1 then lastChance = -1 end
            end
            if totalProbability <= 99 then
                table.insert(values,
                             {id = "ZZZZ", prob = 100 - totalProbability})
            else
                table.insert(values, {id = "ZZZZ", prob = 0})
            end
            probabilityLayers[currentRequiredPopulation] = values
        end
    end
    local orderedProbabilityLayers = {}
    for _, threshold in ipairs(uniqueThresholds) do
        table.insert(orderedProbabilityLayers, probabilityLayers[threshold])
    end
    local mergedValues = {}
    for _, values in ipairs(orderedProbabilityLayers) do
        local aggregation = {}
        for _, value in ipairs(values) do
            if aggregation[value.id] then
                aggregation[value.id].prob =
                    aggregation[value.id].prob + value.prob
            else
                aggregation[value.id] = {id = value.id, prob = value.prob}
            end
        end
        local mergedLayer = {}
        for _, mergedValue in pairs(aggregation) do
            table.insert(mergedLayer, mergedValue)
        end
        table.insert(mergedValues, mergedLayer)
    end
    local finalMerged = {}
    for index = #mergedValues, 1, -1 do
        local layer = mergedValues[index]
        for _, item in ipairs(layer) do
            local entryKey = item.id
            if finalMerged[entryKey] then
                table.insert(finalMerged[entryKey], 1, item.prob)
            else
                finalMerged[entryKey] = {item.prob}
            end
        end
    end
    for _, entryKey in ipairs(productOrder) do
        local probabilityIndex = 1
        local deduplicated = {}
        local seen = {}
        local mergedProbabilities = finalMerged[entryKey] or {}
        for _, probability in ipairs(mergedProbabilities) do
            if not seen[probability] then
                seen[probability] = true
                table.insert(deduplicated, probability)
            end
        end
        for index = #deduplicated, 1, -1 do
            productEntries[entryKey].requiredData[probabilityIndex].probability =
                deduplicated[index]
            productEntries[entryKey].requiredData[probabilityIndex].priority =
                nil
            probabilityIndex = probabilityIndex + 1
        end
    end
    local englishName = context.name:gsub(":", "")
    local itemDesc = utils.expandTemplate("模板:Name",
                                          {englishName, nil, class = "inline"}) or
                         nil
    if utils.stringContains(itemDesc, "Blank") then itemDesc = nil end
    return {
        entries = productEntries,
        order = productOrder,
        finalMerged = finalMerged,
        maxPopulation = maxPopulation,
        iconName = iconName,
        itemDesc = itemDesc
    }
end
local function buildProductRows(productContext)
    local rows = {}
    local productEntries = productContext.entries or productContext.itemTable
    local finalMerged = productContext.finalMerged
    local productOrder = productContext.order or productContext.producesOrder
    local maxPopulation = productContext.maxPopulation
    local iconName = productContext.iconName
    for rowIndex, entryKey in ipairs(productOrder) do
        local productEntry = productEntries[entryKey]
        local segmentCursor = 1
        local mergedProbabilities = utils.uniqueValues(
                                        finalMerged[entryKey] or {})
        local startCount = 1
        local startProbability = 0
        local endCount = 10
        local endProbability = 0
        local coveredPopulation = 0
        local firstElement = productEntry.item
        if utils.stringContains(firstElement, "File:Roe.png") and iconName ~=
            "Roe" then
            firstElement = firstElement:gsub("File:Roe.png",
                                             "File:" .. iconName .. ".png")
        end
        local rowSegments = {firstElement, entryKey}
        for _, probability in ipairs(mergedProbabilities) do
            local segment = productEntry.requiredData[segmentCursor]
            if #rowSegments == 2 then
                startProbability = segment.probability or 0
            end
            if #rowSegments == 2 and segment.rangeA ~= 1 then
                startCount = segment.rangeA
                local skippedPopulation = segment.rangeA - 1
                coveredPopulation = coveredPopulation + skippedPopulation
                table.insert(rowSegments, skippedPopulation)
                table.insert(rowSegments, toPercentage("—", false, 1))
            end
            local segmentLength = segment.rangeB - segment.rangeA + 1
            local probabilityValue = segment.probability
            table.insert(rowSegments, segmentLength)
            table.insert(rowSegments, toPercentage(probabilityValue, false, 1))
            endCount = segment.rangeB
            if probabilityValue ~= 0 then
                endProbability = probabilityValue
            end
            segmentCursor = segmentCursor + 1
            if segmentLength == 0 then
                coveredPopulation = coveredPopulation + 1
            end
            coveredPopulation = coveredPopulation + segmentLength
        end
        if coveredPopulation ~= maxPopulation - 1 then
            local segmentLength = #rowSegments
            if segmentLength >= 3 then
                if rowSegments[segmentLength - 1] == 0 then
                    rowSegments[segmentLength - 1] = 2
                else
                    rowSegments[segmentLength - 1] =
                        rowSegments[segmentLength - 1] + maxPopulation - 1 -
                            coveredPopulation
                end
            end
        end
        local startValue = toPercentage((startCount * 0.08 + 0.15) *
                                            startProbability / 100, true)
        local endValue = toPercentage(
                             (endCount * 0.08 + 0.15) * endProbability / 100,
                             true)
        local actualProbability
        if startValue == endValue then
            actualProbability = endValue .. "%"
        else
            local a = tonumber(startValue)
            local b = tonumber(endValue)
            if b < a then a, b = b, a end
            actualProbability = a .. " ~ " .. b .. "%"
        end
        if entryKey == "ZZZZ" then actualProbability = "—" end
        table.insert(rowSegments, actualProbability)
        if productEntry.experience == "—" then
            table.insert(rowSegments, "—")
        else
            table.insert(rowSegments, math.floor(productEntry.experience))
        end
        table.insert(rows,
                     {index = rowIndex, key = entryKey, segments = rowSegments})
    end
    return rows
end
-- ---------------------------------------------------------------------------
--  Public API
-- ---------------------------------------------------------------------------
function FishPond.getMaxPopulation(frame)
    local context = ensureContext(frame)
    if not context or not context.data then return '' end
    local result = context.data['MaxPopulation'] or -1
    if tonumber(result) == -1 then result = 10 end
    return result or ''
end
function FishPond.getSpawnTime(frame)
    local context = ensureContext(frame)
    if not context or not context.data then return '' end
    local spawnTime = context.data['SpawnTime']
    if tonumber(spawnTime or -1) == -1 then return computeSpawnTime(context) end
    return spawnTime
end
function FishPond.getPopulationMissionCount(frame)
    local context = ensureContext(frame)
    local opacityData = collectOpacityRows(context)
    if not opacityData then return 0 end
    return #opacityData.rows
end
function FishPond.getFishCount(frame)
    local context = ensureContext(frame)
    if not context then return '' end
    local maxPopulation = FishPond.getMaxPopulation(frame)
    if tonumber(maxPopulation) == 1 then return 1 end
    local gates = FishPond.getPopulationGates(frame)
    if type(gates) ~= 'table' then return '' end
    local availablePopulation = tonumber(maxPopulation) or 0
    for _, gate in pairs(gates) do
        local gatePopulation = tonumber(gate.Population)
        if gatePopulation and availablePopulation > gatePopulation then
            availablePopulation = gatePopulation
        end
    end
    if availablePopulation < 2 then return 1 end
    return availablePopulation - 1
end
function FishPond.getColor(frame)
    local context = ensureContext(frame)
    if not context or not context.data then return '' end
    local waterColors = context.data['WaterColor']
    if not waterColors then return '' end
    local totalCount = 0
    local trueColor
    local countRequired
    local gateRequired
    for _, colorInfo in pairs(waterColors) do
        trueColor = colorInfo['Color'] and colorInfo['Color']:gsub(" ", ", ")
        countRequired = colorInfo['MinPopulation']
        gateRequired = colorInfo['MinUnlockedPopulationGate']
        totalCount = totalCount + 1
    end
    if totalCount ~= 1 then return '' end
    local first = ''
    if tonumber(gateRequired) == 2 then first = '完成首个任务,并且' end
    local ref = "(RGB 色值:" .. tostring(trueColor or '') .. ")"
    return string.format(
               "%s鱼塘内有至少 %s 条鱼时,鱼塘的颜色会变为 <span style=\"background-color: rgb(%s); border: 1px solid #202122; margin: 2px;\">          </span>%s。",
               tostring(first or ''), tostring(countRequired or ''),
               tostring(trueColor or ''), tostring(ref or ''))
end
function FishPond.getProducedItems(frame)
    local context = ensureContext(frame)
    if not context or not context.data or not context.data['ProducedItems'] then
        return ''
    end
    local result = {}
    for _, item in ipairs(context.data['ProducedItems']) do
        table.insert(result, {
            RequiredPopulation = item['RequiredPopulation'],
            Chance = item['Chance'],
            Condition = item['Condition'],
            Id = item['Id'],
            MinStack = item['MinStack'],
            MaxStack = item['MaxStack']
        })
    end
    return result
end
function FishPond.getPopulationGates(frame)
    local context = ensureContext(frame)
    if not context or not context.data or not context.data['PopulationGates'] then
        return ''
    end
    local result = {}
    for population, gates in pairs(context.data['PopulationGates']) do
        table.insert(result, {Population = population, Gates = gates})
    end
    return result
end
function FishPond.debug(frame)
    local output = {}
    output['MaxPopulation'] = FishPond.getMaxPopulation(frame)
    output['SpawnTime'] = FishPond.getSpawnTime(frame)
    output['ProducedItems'] = FishPond.getProducedItems(frame)
    output['PopulationGates'] = FishPond.getPopulationGates(frame)
    local result = {}
    for key, value in pairs(output) do
        if type(value) == 'table' then
            for _, item in ipairs(value) do
                table.insert(result, key .. ': ' .. mw.text.jsonEncode(item))
            end
        else
            table.insert(result, key .. ': ' .. tostring(value))
        end
    end
    return table.concat(result, '\n')
end
function FishPond.getOpacityTable(frame)
    local context = ensureContext(frame)
    if not context then return '' end
    local opacityData = collectOpacityRows(context)
    if not opacityData then return '' end
    local result = {}
    for _, row in ipairs(opacityData.rows) do
        table.insert(result,
                     string.format(
                         "<tr><td>%s</td><td>%s</td><td>%s</td><td>%s</td></tr>",
                         row.population - 1, row.nextPopulation, row.items,
                         row.exp))
    end
    return table.concat(result, "\n")
end
function FishPond.getOpacityTableAlt(frame)
    local context = ensureContext(frame)
    if not context then return '' end
    local fishName = frame.args and frame.args[1]
    local displayFishName = frame.args and frame.args[2]
    local cacheKey = KEY_PREFIX .. "|getOpacityTableAlt|" .. (fishName or '') ..
                         "|" .. (displayFishName or '')
    local cached = cache.get(cacheKey)
    if cached then return cached end
    local opacityData = collectOpacityRows(context)
    if not opacityData then return '' end
    if not displayFishName or displayFishName == '' then
        displayFishName = utils.expandTemplate("模板:Name", {
            context.name,
            nil,
            class = "inline"
        })
    end
    local rowCount = #opacityData.rows
    local result = {}
    for index, row in ipairs(opacityData.rows) do
        local prefix = ""
        if index == 1 then
            prefix = string.format(
            "<tr><td rowspan=\"%s\">%s</td><td>%s</td><td>%s</td><td>%s</td><td rowspan=\"%s\">%s</td></tr>",
                         rowCount, displayFishName, row.population - 1,
                         row.nextPopulation, row.items, rowCount, "每 " ..
                             tostring(opacityData.spawnTime or '') .. " 天")
        else
            prefix = string.format("<tr><td>%s</td><td>%s</td><td>%s</td></tr>",
                                   row.population - 1, row.nextPopulation,
                                   row.items)
        end
        table.insert(result, prefix)
    end
    local content = table.concat(result, "\n")
    cache.set(cacheKey, content, EXP_TIME)
    return content
end
function FishPond.getProductTable(frame)
    local context = ensureContext(frame)
    if not context then return '' end
    local productContext = prepareProducedItemContext(context)
    if not productContext then return '' end
    local rows = buildProductRows(productContext)
    local itemDesc = productContext.itemDesc
    local maxPopulation = productContext.maxPopulation
    local headers = {}
    for i = 1, (maxPopulation - 1) do
        table.insert(headers,
                     string.format('<th data-sort-type="number">%d</th>', i))
    end
    local headersString = table.concat(headers)
    local result = {
        string.format(
            '<table class="wikitable roundedborder sortable" style="text-align: center;"><tr><th rowspan="2" class="no-wrap">%s鱼塘产物</th><th colspan="%s" class="no-wrap">鱼的数量及对应概率</th><th rowspan="2" data-sort-type="number" class="no-wrap">实际概率</th><th rowspan="2" data-sort-type="number" class="no-wrap">钓鱼<br>经验值</th></tr><tr>%s</tr>',
            itemDesc and (itemDesc .. "<br>") or "", maxPopulation - 1,
            headersString)
    }
    for _, row in ipairs(rows) do
        local concatenated = table.concat(row.segments, ",")
        table.insert(result, generateRowLocal(concatenated or "无,无,无,无"))
    end
    table.insert(result, "</table>")
    return table.concat(result, "\n")
end
function FishPond.getProductTableAlt(frame)
    local context = ensureContext(frame)
    if not context then return '' end
    local fishName = frame.args and frame.args[1]
    local displayFishName = frame.args and frame.args[2]
    local cacheKey = KEY_PREFIX .. "|getProductTableAlt|" .. (fishName or '') ..
                         "|" .. (displayFishName or '')
    local cached = cache.get(cacheKey)
    if cached then return cached end
    local productContext = prepareProducedItemContext(context)
    if not productContext then return '' end
    local rows = buildProductRows(productContext)
    if not displayFishName or displayFishName == '' then
        displayFishName = utils.expandTemplate("模板:Name", {
            context.name,
            nil,
            class = "inline"
        })
    end
    local productOrder = productContext.order or productContext.producesOrder
    local result = {}
    for index, row in ipairs(rows) do
        local insertContent = generateRowLocal(
                                  table.concat(row.segments, ",") or
                                      "无,无,无,无")
        if index == 1 then
            insertContent = insertContent:gsub('<tr><td data',
                                               '<tr><td rowspan="' ..
                                                   #productOrder .. '">' ..
                                                   displayFishName ..
                                                   '</td><td data')
        end
        table.insert(result, insertContent)
    end
    local content = table.concat(result, "\n")
    cache.set(cacheKey, content, EXP_TIME)
    return content
end
return FishPond
                
                    沪公网安备 31011002002714 号