全站通知:

模块:GiftTastesNPC

来自星露谷物语维基
跳到导航 跳到搜索
[ 创建 | 刷新 ]文档页面
当前模块文档缺失,需要扩充。
-- #############################################################################
-- # Module:GiftTastesNPC (V7 - 伪代码重构版) - 按照伪代码逻辑重构
-- # ---------------------------------------------------------------------------
-- # 按照提供的伪代码逻辑重新实现的模块,确保正确的优先级处理。
-- # 依赖: Module:Object/data, Module:GiftTastesNPC/data
-- #############################################################################

local p = {}

-- 1. 加载依赖模块
local utils = require("Module:Utils")
local NPC = require('Module:NPC')
local ItemNames = require('Module:ItemNames')
local objectData = utils.lazyload('Module:Object/data')
local giftTasteData = utils.lazyload('Module:GiftTastesNPC/data')

-- 物品名称覆盖表:处理重复名称的情况
local nameOverrides = {
    ["诡异玩偶(绿)"] = "126",
    ["诡异玩偶(黄)"] = "127",
    ["Joja可乐"] = "167",
    ["垃圾(物品)"] = "168",
    ["破损的CD"] = "171",
    ["鱼饵(物品)"] = "685",
    ["针对性鱼饵"] = "SpecificBait",
    ["熏鱼"] = "Smoked",
    ["果干"] = "DriedFruit",
    ["蘑菇干"] = "DriedMushrooms",
    ["青蛙蛋"] = "FrogEgg",
    ["史莱姆蛋"] = "610",
}

-- 特殊物品配置表(非Objects类物品)
local specialItems = {
    ["FrogEgg"] = {
        chineseName = "青蛙蛋",
        fullId = "(TR)FrogEgg",
        wikiPage = "青蛙蛋"
    }
}

-- 不可赠送物品黑名单
local blacklistedItems = {
    -- stones
    ["2"] = true, ["4"] = true, ["6"] = true, ["8"] = true, ["10"] = true,
    ["12"] = true, ["14"] = true, ["25"] = true, ["32"] = true, ["34"] = true,
    ["36"] = true, ["38"] = true, ["40"] = true, ["42"] = true, ["44"] = true,
    ["46"] = true, ["48"] = true, ["50"] = true, ["52"] = true, ["54"] = true,
    ["56"] = true, ["58"] = true, ["75"] = true, ["76"] = true, ["77"] = true,
    ["95"] = true, ["290"] = true, ["343"] = true, ["450"] = true, ["668"] = true,
    ["670"] = true, ["751"] = true, ["760"] = true, ["762"] = true, ["764"] = true,
    ["765"] = true, ["816"] = true, ["817"] = true, ["818"] = true, ["819"] = true,
    ["843"] = true, ["844"] = true, ["845"] = true, ["846"] = true, ["847"] = true,
    ["849"] = true, ["850"] = true, ["BasicCoalNode0"] = true, ["BasicCoalNode1"] = true,
    ["CalicoEggStone_0"] = true, ["CalicoEggStone_1"] = true, ["CalicoEggStone_2"] = true,
    ["PotOfGold"] = true, ["VolcanoGoldNode"] = true, ["VolcanoCoalNode0"] = true, ["VolcanoCoalNode1"] = true,

    -- weeds
    ["0"] = true, ["313"] = true, ["314"] = true, ["315"] = true, ["316"] = true,
    ["317"] = true, ["318"] = true, ["319"] = true, ["320"] = true, ["321"] = true,
    ["452"] = true, ["674"] = true, ["675"] = true, ["676"] = true, ["677"] = true,
    ["678"] = true, ["679"] = true, ["750"] = true, ["784"] = true, ["785"] = true,
    ["786"] = true, ["792"] = true, ["793"] = true, ["794"] = true, ["882"] = true,
    ["883"] = true, ["884"] = true, ["GreenRainWeeds0"] = true, ["GreenRainWeeds1"] = true,
    ["GreenRainWeeds2"] = true, ["GreenRainWeeds3"] = true, ["GreenRainWeeds4"] = true,
    ["GreenRainWeeds5"] = true, ["GreenRainWeeds6"] = true, ["GreenRainWeeds7"] = true,

    -- twigs
    ["294"] = true, ["295"] = true,

    -- quest items
    ["71"] = true, ["191"] = true, ["742"] = true, ["788"] = true, ["790"] = true,
    ["864"] = true, ["865"] = true, ["866"] = true, ["867"] = true, ["868"] = true,
    ["869"] = true, ["870"] = true, ["875"] = true, ["876"] = true, ["897"] = true,
    ["GoldenBobber"] = true,

    -- unstorable items (used on pickup)
    ["73"] = true, ["434"] = true, ["803"] = true, ["858"] = true, ["930"] = true,
    ["GoldCoin"] = true,

    -- supply crates
    ["922"] = true, ["923"] = true, ["924"] = true, ["925"] = true,

    -- secret notes
    ["79"] = true, ["842"] = true,

    -- others
    ["277"] = true, ["458"] = true, ["460"] = true, ["808"] = true, ["809"] = true,

    -- rings
    ["516"] = true, ["517"] = true, ["518"] = true, ["519"] = true, ["520"] = true,
    ["521"] = true, ["522"] = true, ["523"] = true, ["524"] = true, ["525"] = true,
    ["526"] = true, ["527"] = true, ["529"] = true, ["530"] = true, ["531"] = true,
    ["532"] = true, ["533"] = true, ["534"] = true, ["801"] = true, ["810"] = true,
    ["811"] = true, ["839"] = true, ["859"] = true, ["860"] = true, ["861"] = true,
    ["862"] = true, ["863"] = true, ["880"] = true, ["887"] = true, ["888"] = true,

    -- unobtainable
    ["30"] = true, ["94"] = true, ["102"] = true, ["449"] = true, ["461"] = true,
    ["528"] = true, ["590"] = true, ["892"] = true, ["927"] = true, ["929"] = true,
    ["SeedSpot"] = true,

    -- other ungiftable
    ["911"] = true, ["CalicoEgg"] = true, ["FarAwayStone"] = true, ["PrizeTicket"] = true, ["Lantern"] = true,

    -- 特殊物品(史莱姆蛋)
    ["413"] = true, ["437"] = true, ["439"] = true, ["857"] = true
}

-- 检查物品是否在黑名单中(不可赠送)
local function _isBlacklisted(itemId)
    local id = tostring(itemId)
    return blacklistedItems[id] == true
end

-- 全局名称到ID映射表
local nameToIdMap = {}

-- 初始化名称映射表
local function _initializeNameMap()
    if next(nameToIdMap) ~= nil then
        return -- 已经初始化过了
    end
    
    mw.log("开始初始化物品名称映射表...")
    
    -- 首先处理覆盖表中的项目
    for chineseName, itemId in pairs(nameOverrides) do
        nameToIdMap[chineseName] = itemId
        mw.log("添加覆盖映射: " .. chineseName .. " -> " .. itemId)
    end
    
    -- 然后处理所有物品数据
    for itemId, itemInfo in pairs(objectData) do
        if not _isBlacklisted(itemId) then
            local englishName = ItemNames.getEnglishName("(O)" .. itemId)
            if englishName and englishName ~= "" then
                -- 如果覆盖表中没有这个名称,才添加到映射表
                if not nameToIdMap[englishName] then
                    nameToIdMap[englishName] = itemId
                else
                    mw.log("跳过重复名称: " .. englishName .. " (原有ID: " .. nameToIdMap[englishName] .. ", 新ID: " .. itemId .. ")")
                end
            end
        end
    end
    
    mw.log("名称映射表初始化完成,共 " .. tostring(#nameToIdMap) .. " 个条目")
end

-- #############################################################################
-- # UTF-8安全字符串处理函数
-- #############################################################################

-- 返回当前字符实际占用的字节数
local function _getUTF8ByteCount(str, index)
    local curByte = string.byte(str, index)
    local byteCount = 1
    if curByte == nil then
        byteCount = 0
    elseif curByte > 0 and curByte <= 127 then
        byteCount = 1
    elseif curByte >= 192 and curByte <= 223 then
        byteCount = 2
    elseif curByte >= 224 and curByte <= 239 then
        byteCount = 3
    elseif curByte >= 240 and curByte <= 247 then
        byteCount = 4
    end
    return byteCount
end

-- 获取UTF-8字符串的真实字符数量
local function _getUTF8Length(str)
    local curIndex = 0
    local i = 1
    local lastCount = 1
    repeat 
        lastCount = _getUTF8ByteCount(str, i)
        i = i + lastCount
        curIndex = curIndex + 1
    until (lastCount == 0)
    return curIndex - 1
end

-- 获取UTF-8字符串中指定字符索引的真实字节位置
local function _getUTF8TrueIndex(str, index)
    local curIndex = 0
    local i = 1
    local lastCount = 1
    repeat 
        lastCount = _getUTF8ByteCount(str, i)
        i = i + lastCount
        curIndex = curIndex + 1
    until (curIndex >= index)
    return i - lastCount
end

-- 安全截取UTF-8字符串,支持中英混合
local function _subStringUTF8(str, startIndex, endIndex)
    if startIndex < 0 then
        startIndex = _getUTF8Length(str) + startIndex + 1
    end

    if endIndex ~= nil and endIndex < 0 then
        endIndex = _getUTF8Length(str) + endIndex + 1
    end

    if endIndex == nil then 
        return string.sub(str, _getUTF8TrueIndex(str, startIndex))
    else
        return string.sub(str, _getUTF8TrueIndex(str, startIndex), _getUTF8TrueIndex(str, endIndex + 1) - 1)
    end
end

-- 安全查找UTF-8字符串中的子串位置(返回字符位置,不是字节位置)
local function _findUTF8(str, pattern, init, plain)
    init = init or 1
    local byteInit = _getUTF8TrueIndex(str, init)
    local byteStart, byteEnd = string.find(str, pattern, byteInit, plain)
    if not byteStart then
        return nil
    end
    
    -- 将字节位置转换为字符位置
    local charStart = 1
    local i = 1
    while i < byteStart do
        local byteCount = _getUTF8ByteCount(str, i)
        if byteCount == 0 then break end
        i = i + byteCount
        charStart = charStart + 1
    end
    
    local charEnd = charStart
    while i <= byteEnd do
        local byteCount = _getUTF8ByteCount(str, i)
        if byteCount == 0 then break end
        i = i + byteCount
        charEnd = charEnd + 1
    end
    charEnd = charEnd - 1
    
    return charStart, charEnd
end

-- #############################################################################
-- # 内部辅助函数与数据映射
-- #############################################################################

-- 不可赠送分类黑名单
local blacklistedCategories = {
    [-96] = true, -- 戒指
    [-99] = true, -- 工具
    [-97] = true, -- 鞋类
    [-24] = true, -- 装饰
}

local categoryNames = {
    [-103] = "技能书", -- skillBook_Category
    [-102] = "书", -- Book_Category
    [-100] = "服装", -- category_clothes
    [-99] = "工具", -- Tool.cs.14307
    [-97] = "鞋类", -- Boots.cs.12501
    [-96] = "戒指", -- Ring.cs.1
    [-81] = "采集品", -- Object.cs.12869
    [-80] = "花", -- Object.cs.12866
    [-79] = "水果", -- Object.cs.12854
    [-75] = "蔬菜", -- Object.cs.12851
    [-74] = "种子", -- Object.cs.12855
    [-28] = "怪物战利品", -- Object.cs.12867
    [-27] = "树液", -- Object.cs.12862(树液 工匠物品)
    [-26] = "工匠物品", -- Object.cs.12862(工匠物品)
    [-25] = "菜品", -- Object.cs.12853
    [-24] = "装饰", -- Object.cs.12859 / Furniture_Decoration
    [-22] = "钓具", -- Object.cs.12858
    [-21] = "鱼饵", -- Object.cs.12857
    [-20] = "垃圾", -- Object.cs.12860
    [-19] = "化肥", -- Object.cs.12856
    [-18] = "动物副产品(羊毛、兔子的脚和鸭毛)", -- Object.cs.12864(副产品 - 羊毛、兔子的脚、鸭毛)
    [-16] = "资源", -- Object.cs.12868
    [-15] = "资源", -- Object.cs.12868
    [-14] = "动物制品", -- Object.cs.12864(空,黑名单)
    [-12] = "矿物", -- Object.cs.12850
    [-8] = "制造品", -- Object.cs.12863
    [-7] = "菜品", -- Object.cs.12853
    [-6] = "奶类物品", -- Object.cs.12864(奶类 动物制品)
    [-5] = "蛋类物品", -- Object.cs.12864(蛋类 动物制品)
    [-4] = "鱼", -- Object.cs.12852
    [-2] = "矿物" -- Object.cs.12850
}

local contextTagNames = {
    book_item = "所有书",
    category_trinket = "所有饰品",
    category_bait = "所有鱼饵",
    category_monster_loot = "所有怪物战利品",
    ancient_item = "所有古物",  -- 添加ancient_item的中文名称
}

-- 分类名称到维基链接的映射
local categoryLinks = {
    ["动物制品"] = ":分类:动物制品",
    ["矿物"] = "矿物",
    ["鱼"] = "鱼",
    ["蔬菜"] = "蔬菜",
    ["水果"] = "水果",
    ["花"] = "花",
    ["采集品"] = "采集#可采集物品",
    ["制造品"] = "打造#可打造物品",
    ["工匠物品"] = "工匠物品",
    ["菜品"] = ":分类:烹饪", -- 分类:菜品
    ["资源"] = ":分类:资源",
    ["化肥"] = "肥料", -- 化肥
    ["种子"] = ":分类:种子",
    ["垃圾"] = "垃圾",
    ["鱼饵"] = "鱼饵",
    ["钓具"] = "渔具",
    -- ["装饰"] = "装饰",
    ["怪物战利品"] = "怪物战利品",
    ["书"] = "书",
    ["技能书"] = "书#技能之书",
    ["古物"] = "古物",
    ["树液"] = "树液采集器#产品"
}

-- 普遍类分类到友谊链接的映射
local universalCategoryLinks = {
    -- ["普遍最爱的礼物"] = "友谊#最爱的礼物",
    -- ["普遍喜欢的礼物"] = "友谊#喜欢的礼物", 
    -- ["普遍一般的礼物"] = "友谊#一般的礼物",
    -- ["普遍不喜欢的礼物"] = "友谊#不喜欢的礼物",
    -- ["普遍讨厌的礼物"] = "友谊#讨厌的礼物"
}

-- 格式化分类文本,处理普遍类加粗和分类链接
local function _formatCategoryText(categoryName)
    -- 检查是否是普遍类分类
    if _findUTF8(categoryName, "普遍", 1, true) then
        -- 普遍类分类加粗并添加友谊链接
        local linkTarget = universalCategoryLinks[categoryName]
        if linkTarget then
            return "所有'''[[" .. linkTarget .. "|" .. categoryName .. "]]'''"
        else
            -- 没有对应友谊链接,只加粗
            return "所有'''" .. categoryName .. "'''"
        end
    else
        -- 检查是否有对应的维基链接
        local linkTarget = categoryLinks[categoryName]
        if linkTarget then
            if linkTarget == categoryName then
                -- 链接名称与分类名称相同
                return "所有[[" .. linkTarget .. "]]"
            else
                -- 链接名称与分类名称不同
                return "所有[[" .. linkTarget .. "|" .. categoryName .. "]]"
            end
        else
            -- 没有对应链接,直接返回
            return "所有" .. categoryName
        end
    end
end

-- 特殊类型映射(根据Type字段)
local typeNames = {
    Arch = "古物",
    asdf = "书"  -- 添加书籍类型映射
}

local function _getItemInfo(itemId)
    -- 优先检查是否为特殊物品
    if specialItems[itemId] then
        return specialItems[itemId]
    end
    
    return objectData[tostring(itemId)]
end

-- 通过物品ID获取显示名称(优先使用覆盖表中的中文名称,否则使用英文名称)
local function _getItemDisplayName(itemId)
    -- 优先检查是否为特殊物品
    if specialItems[itemId] then
        return specialItems[itemId].chineseName
    end
    
    -- 首先检查覆盖表中是否有对应的中文名称
    for chineseName, overrideId in pairs(nameOverrides) do
        if tostring(overrideId) == tostring(itemId) then
            return chineseName
        end
    end
    
    -- 如果覆盖表中没有,使用 ItemNames 模块获取英文名称
    local englishName = ItemNames.getEnglishName("(O)" .. itemId)
    if englishName and englishName ~= "" then
        return englishName
    end
    -- 如果获取失败,返回原始ID
    return tostring(itemId)
end

-- 通过物品ID获取中文名称(优先使用覆盖表中的中文名称,否则使用中文名称),和上面的功能不同
local function _getItemChineseName(itemId)
    -- 优先检查是否为特殊物品
    if specialItems[itemId] then
        return specialItems[itemId].chineseName
    end
    
    -- 首先检查覆盖表中是否有对应的中文名称
    for chineseName, overrideId in pairs(nameOverrides) do
        if tostring(overrideId) == tostring(itemId) then
            return chineseName
        end
    end
    
    -- 如果覆盖表中没有,使用 ItemNames 模块获取英文名称
    local chineseName = ItemNames.getChineseName("(O)" .. itemId)
    if chineseName and chineseName ~= "" then
        return chineseName
    end
    -- 如果获取失败,返回原始ID
    return tostring(itemId)
end

-- 通过物品名称获取物品ID(支持中文名称和英文名称)
local function _getItemIdByName(itemName)
    _initializeNameMap() -- 确保映射表已初始化
    local result = nameToIdMap[itemName]
    return result
end

-- Name 模板特殊规则配置
local nameTemplateRules = {
    -- 特殊链接规则
    links = {
        ["Green Slime Egg"] = "史莱姆蛋"
    },
    -- 文本处理规则
    textProcessors = {
        -- 去掉英语冒号
        function(name) 
            if _findUTF8(name, ":", 1, true) then
                return name:gsub(":", "")
            end
            return name
        end
    }
}

-- 添加更多特殊链接
-- nameTemplateRules.links["Another Item"] = "另一个物品"

-- 添加更多文本处理器
-- table.insert(nameTemplateRules.textProcessors, function(name)
    -- 新的处理逻辑
    -- return processedName
-- end)

-- 展开 Name 模板并应用特殊规则
local function _expandNameTemplate(itemName, templateParams)
    -- 应用物品名称特殊规则
    local processedName = itemName
    local extraParams = {}
    
    -- 应用特殊链接规则
    if nameTemplateRules.links[itemName] then
        extraParams.link = nameTemplateRules.links[itemName]
    end
    
    -- 应用文本处理规则
    for _, processor in ipairs(nameTemplateRules.textProcessors) do
        processedName = processor(processedName)
    end
    
    -- 构建完整的模板参数
    local finalParams = {processedName}
    
    -- 添加原有参数
    if templateParams then
        for key, value in pairs(templateParams) do
            if type(key) == "number" then
                -- 数字索引参数,追加到位置参数
                table.insert(finalParams, value)
            else
                -- 命名参数
                finalParams[key] = value
            end
        end
    end
    
    -- 添加额外参数
    for key, value in pairs(extraParams) do
        finalParams[key] = value
    end
    
    return utils.expandTemplate("Name", finalParams)
end

local function _getCategoryName(categoryId)
    return categoryNames[categoryId] or "未知分类"
end

local function _getTypeName(typeName)
    return typeNames[typeName] or typeName
end

local function _getContextTagName(tag)
    return contextTagNames[tag] or tag
end

local function _itemMatchesRule(itemInfo, ruleId)
    if not itemInfo then return false end
    if type(ruleId) == 'number' then
        if ruleId > 0 then return false
        else return itemInfo.Category == ruleId end
    elseif type(ruleId) == 'string' then
        -- 字符串规则:包含下划线的为contexttag,不包含下划线的为物品ID
        if string.find(ruleId, '_') then
            -- 包含下划线,作为contexttag处理
            
            if objectData[ruleId] then return false
            else
                if itemInfo.ContextTags then
                    for _, tag in ipairs(itemInfo.ContextTags) do
                        if tag == ruleId then return true end
                    end
                end
                return false
            end
        else
            -- 不包含下划线,作为物品ID处理
            return false  -- 这里应该返回false,因为此函数检查分类匹配,而物品ID不应该在这里匹配
        end
    end
    return false
end

-- 检查分类是否在黑名单中(不可赠送的分类)
local function _isCategoryBlacklisted(category)
    return blacklistedCategories[category] == true
end

-- 检查物品是否因分类而被黑名单(组合检查)
local function _isItemBlacklistedByCategory(itemInfo)
    if not itemInfo or not itemInfo.Category then return false end
    return _isCategoryBlacklisted(itemInfo.Category)
end

-- 按照分类、类型、价格排序物品名称列表
local function _sortItemNames(itemNames)
    _initializeNameMap() -- 确保映射表已初始化
    
    local itemData = {}
    
    -- 为每个物品名称找到对应的详细信息
    for _, itemName in ipairs(itemNames) do
        local itemId = _getItemIdByName(itemName)
        local itemInfo = nil
        
        if itemId then
            itemInfo = objectData[tostring(itemId)]
        end
        
        if not itemInfo then
            mw.log("警告: 无法找到物品信息: " .. itemName)
        end
        
        table.insert(itemData, {
            name = itemName,
            category = itemInfo and itemInfo.Category or 99999,
            type = itemInfo and itemInfo.Type or "unknown",
            price = itemInfo and itemInfo.Price or 0
        })
    end
    
    -- 按照规则排序
    table.sort(itemData, function(a, b)
        if a.category ~= b.category then
            return a.category < b.category
        end
        if a.type ~= b.type then
            return a.type < b.type
        end
        return a.price < b.price
    end)
    
    -- 返回排序后的名称列表
    local sortedNames = {}
    for _, data in ipairs(itemData) do
        table.insert(sortedNames, data.name)
    end
    return sortedNames
end

-- 格式化除外物品列表,暂时不展开模板,在最终输出时再展开
local function _formatExceptionItems(exceptions)
    if #exceptions == 0 then
        return ""
    end
    
    return "(" .. table.concat(exceptions, "、") .. "除外)"
end

-- 在最终输出时展开除外物品的 Name 模板
local function _expandExceptionItems(text)
    -- 查找所有"(...除外)"的模式并展开Name模板
    return string.gsub(text, "((.-)除外)", function(exceptions)
        local formattedExceptions = {}
        -- 使用UTF-8安全的字符串分割方法
        local totalLen = _getUTF8Length(exceptions)
        local pos = 1
        while pos <= totalLen do
            local nextPos = _findUTF8(exceptions, "、", pos, true)
            local itemName
            if nextPos then
                itemName = _subStringUTF8(exceptions, pos, nextPos - 1)
                pos = nextPos + 1  -- "、"是1个UTF-8字符
            else
                itemName = _subStringUTF8(exceptions, pos)
                pos = totalLen + 1
            end
            if itemName and itemName ~= "" then
                local nameTemplate = _expandNameTemplate(itemName, {
                    class = "inline"
                })
                table.insert(formattedExceptions, nameTemplate)
            end
        end
        return "(" .. table.concat(formattedExceptions, "、") .. "除外)"
    end)
end

-- #############################################################################
-- # 核心逻辑函数 - 按照伪代码实现
-- #############################################################################

local done = false

-- 根据伪代码实现的礼物偏好计算逻辑
local function _getTasteForItem(itemId, itemInfo, npcName, personalTastes, universalTastes)
    if not itemInfo then return "neutral" end
    
    local TASTE = "neutral"
    local HAS_UNIVERSAL_ID = false
    local HAS_UNIVERSAL_NEUTRAL_ID = false
    local HAS_UNIVERSAL_CATEGORY = false
    
    -- Part I: universal taste by category
    if itemInfo.Category then
        for _, ruleId in ipairs(universalTastes.love or {}) do
            if _itemMatchesRule(itemInfo, ruleId) then
                TASTE = "love"
                HAS_UNIVERSAL_CATEGORY = true
                break
            end
        end
        if TASTE == "neutral" then
            for _, ruleId in ipairs(universalTastes.hate or {}) do
                if _itemMatchesRule(itemInfo, ruleId) then
                    TASTE = "hate"
                    HAS_UNIVERSAL_CATEGORY = true
                    break
                end
            end
        end
        if TASTE == "neutral" then
            for _, ruleId in ipairs(universalTastes.like or {}) do
                if _itemMatchesRule(itemInfo, ruleId) then
                    TASTE = "like"
                    HAS_UNIVERSAL_CATEGORY = true
                    break
                end
            end
        end
        if TASTE == "neutral" then
            for _, ruleId in ipairs(universalTastes.dislike or {}) do
                if _itemMatchesRule(itemInfo, ruleId) then
                    TASTE = "dislike"
                    HAS_UNIVERSAL_CATEGORY = true
                    break
                end
            end
        end
    end
    
    -- Part II: universal taste by item ID
    local itemIdStr = tostring(itemId)
    for _, ruleId in ipairs(universalTastes.love or {}) do
        if tostring(ruleId) == itemIdStr then
            TASTE = "love"
            HAS_UNIVERSAL_ID = true
            break
        end
    end
    if not HAS_UNIVERSAL_ID then
        for _, ruleId in ipairs(universalTastes.hate or {}) do
            if tostring(ruleId) == itemIdStr then
                TASTE = "hate"
                HAS_UNIVERSAL_ID = true
                break
            end
        end
    end
    if not HAS_UNIVERSAL_ID then
        for _, ruleId in ipairs(universalTastes.like or {}) do
            if tostring(ruleId) == itemIdStr then
                TASTE = "like"
                HAS_UNIVERSAL_ID = true
                break
            end
        end
    end
    if not HAS_UNIVERSAL_ID then
        for _, ruleId in ipairs(universalTastes.dislike or {}) do
            if tostring(ruleId) == itemIdStr then
                TASTE = "dislike"
                HAS_UNIVERSAL_ID = true
                break
            end
        end
    end
    if not HAS_UNIVERSAL_ID then
        for _, ruleId in ipairs(universalTastes.neutral or {}) do
            if tostring(ruleId) == itemIdStr then
                TASTE = "neutral"
                HAS_UNIVERSAL_ID = true
                HAS_UNIVERSAL_NEUTRAL_ID = true
                break
            end
        end
    end
    
    -- Part III: override neutral if it's from universal category
    if TASTE == "neutral" and not HAS_UNIVERSAL_NEUTRAL_ID then
        if itemInfo.Edibility and itemInfo.Edibility < 0 and itemInfo.Edibility ~= -300 then
            TASTE = "hate"
        elseif itemInfo.Price and itemInfo.Price < 20 then
            TASTE = "dislike"
        end
    end
    
    -- Special rule: Artifacts (古物规则)
    if itemInfo.Type == "Arch" then
        if npcName == "Penny" or npcName == "Dwarf" then
            -- Check if personally overridden
            local personallyOverridden = false
            for _, level in ipairs({"love", "hate", "dislike", "neutral"}) do
                local items = (personalTastes[level] and personalTastes[level].items) or {}
                for _, rule in ipairs(items) do
                    if tostring(rule) == itemIdStr or _itemMatchesRule(itemInfo, rule) then
                        personallyOverridden = true
                        break
                    end
                end
                if personallyOverridden then break end
            end
            if not personallyOverridden and not HAS_UNIVERSAL_ID then
                TASTE = "like"
            end
        else
            -- Other NPCs dislike artifacts by default
            local personallyOverridden = false
            for _, level in ipairs({"love", "like", "hate", "neutral"}) do
                local items = (personalTastes[level] and personalTastes[level].items) or {}
                for _, rule in ipairs(items) do
                    if tostring(rule) == itemIdStr or _itemMatchesRule(itemInfo, rule) then
                        personallyOverridden = true
                        break
                    end
                end
                if personallyOverridden then break end
            end
            if not personallyOverridden and not HAS_UNIVERSAL_ID then
                TASTE = "dislike"
            end
        end
    end
    
    -- Part IV: override with personal tastes
    local function checkPersonalOverride(level)
        local items = (personalTastes[level] and personalTastes[level].items) or {}
        for _, rule in ipairs(items) do
            if tostring(rule) == itemIdStr then
                -- Direct item match always overrides everything
                return true
            elseif itemInfo.Category and _itemMatchesRule(itemInfo, rule) then
                -- Context tag match always overrides (no wasIndividualUniversal check for context tags)
                -- Category match only overrides if not affected by universal individual rules
                if type(rule) == 'string' and not objectData[rule] then
                    -- This is a context tag, always override
                    return true
                elseif not HAS_UNIVERSAL_ID then
                    -- This is a category, only override if no universal individual rules
                    return true
                end
            end
        end
        return false
    end
    
    if checkPersonalOverride("love") then
        return "love"
    end
    if checkPersonalOverride("hate") then
        return "hate"
    end
    if checkPersonalOverride("like") then
        return "like"
    end
    if checkPersonalOverride("dislike") then
        return "dislike"
    end
    if checkPersonalOverride("neutral") then
        return "neutral"
    end
    
    -- Part V: return taste if not overridden
    return TASTE
end

-- #############################################################################
-- # 主要外部函数
-- #############################################################################

-- 处理分类规则的通用函数
local function _processCategoryRule(ruleId, ruleSource, targetLevel, npcName, personalTastes, universalTastes, seenRules, outputPhrases, personalSpecialItems, addPersonalSpecialItem)
    if seenRules[tostring(ruleId)] or (type(ruleId) == 'number' and _isCategoryBlacklisted(ruleId)) then
        return
    end
    
    -- 特殊处理ancient_item:如果已经有Arch规则,则跳过ancient_item
    if ruleId == "ancient_item" and seenRules["Arch_implicit"] then
        mw.log("跳过ancient_item规则,因为已经有Arch隐性分类规则")
        return
    end
    
    local ruleName
    if type(ruleId) == 'number' and ruleId < 0 then 
        ruleName = "所有" .. _getCategoryName(ruleId)
    elseif type(ruleId) == 'string' then 
        
        -- 字符串规则:包含下划线的为contexttag,不包含下划线的为物品
        if string.find(ruleId, '_') then
            ruleName = _getContextTagName(ruleId)
        else
            -- 不包含下划线,作为物品名称处理
            if objectData[ruleId] then
                ruleName = objectData[ruleId].name
            else
                ruleName = ruleId  -- 使用原始名称
            end
        end
    end

    if not ruleName then return end
    
    mw.log("处理" .. ruleSource .. "分类规则 " .. tostring(ruleId) .. " -> " .. ruleName .. " (targetLevel: " .. targetLevel .. ")")
    
    local exceptions = {}
    local seenExceptions = {}
    local matchedItems = 0  -- 添加计数器
    
    -- 遍历所有物品,找到匹配这个规则的物品
    for itemId, itemInfo in pairs(objectData) do
        if itemInfo.Category ~= -999 and itemInfo.CanBeGivenAsGift and _itemMatchesRule(itemInfo, ruleId) and not _isBlacklisted(itemId) and not _isItemBlacklistedByCategory(itemInfo) then
            matchedItems = matchedItems + 1
            local actualTaste = _getTasteForItem(itemId, itemInfo, npcName, personalTastes, universalTastes)
            local chineseName = _getItemDisplayName(itemId)

            if actualTaste == targetLevel and chineseName and addPersonalSpecialItem then
                -- 符合个人分类规则的物品,添加到个人特殊喜好
                addPersonalSpecialItem(chineseName)
            elseif actualTaste ~= targetLevel and chineseName and not seenExceptions[chineseName] then
                table.insert(exceptions, chineseName)
                seenExceptions[chineseName] = true
            end
        end
    end
    
    mw.log("规则 " .. tostring(ruleId) .. " 匹配了 " .. matchedItems .. " 个物品,除外 " .. #exceptions .. " 个")
    
    -- 检查除外是否过多,如果除外超过匹配项的50%,则不创建分类规则
    local validItems = 0
    for itemId, itemInfo in pairs(objectData) do
        if itemInfo.Category ~= -999 and itemInfo.CanBeGivenAsGift and _itemMatchesRule(itemInfo, ruleId) and not _isBlacklisted(itemId) and not _isItemBlacklistedByCategory(itemInfo) then
            local actualTaste = _getTasteForItem(itemId, itemInfo, npcName, personalTastes, universalTastes)
            if actualTaste == targetLevel then
                validItems = validItems + 1
            end
        end
    end
    
    if #exceptions > validItems * 0.5 and validItems > 0 then
        mw.log("分类规则" .. ruleName .. "除外项过多(" .. #exceptions .. "/" .. validItems .. "),不创建分类")
        return
    end
    
    local text = ruleName
    if #exceptions > 0 then 
        exceptions = _sortItemNames(exceptions)
        text = text .. _formatExceptionItems(exceptions)
    end
    mw.log("添加分类规则: " .. text)
    table.insert(outputPhrases, text)
    seenRules[tostring(ruleId)] = true
end

-- 处理个人偏好规则
local function _processPersonalPreferences(personalTastes, targetLevel, npcName, universalTastes, seenRules, outputPhrases, personalSpecialItems, seenPersonalItems)
    local personalIds = (personalTastes[targetLevel] and personalTastes[targetLevel].items) or {}
    
    local function addPersonalSpecialItem(name)
        if name and not seenPersonalItems[name] then
            table.insert(personalSpecialItems, name)
            seenPersonalItems[name] = true
        end
    end
    
    for _, id in ipairs(personalIds) do
        if type(id) == 'number' and id >= 0 or (type(id) == 'string' and objectData[id] or id == "FrogEgg") then
            -- 具体物品
            local itemInfo = _getItemInfo(id)
            local chineseName = _getItemDisplayName(id)
            if itemInfo and chineseName and not _isBlacklisted(id) and not _isItemBlacklistedByCategory(itemInfo) then
                -- 验证这个物品确实应该是这个偏好等级
                local actualTaste = _getTasteForItem(id, itemInfo, npcName, personalTastes, universalTastes)
                if actualTaste == targetLevel then
                    addPersonalSpecialItem(chineseName)
                end
            end
        else
            -- 分类或标签规则 - 不添加物品到个人特殊喜好列表,因为会被分类规则覆盖
            _processCategoryRule(id, "个人偏好", targetLevel, npcName, personalTastes, universalTastes, seenRules, outputPhrases, personalSpecialItems, nil)
        end
    end
end

-- 处理通用偏好规则
local function _processUniversalPreferences(universalTastes, targetLevel, npcName, personalTastes, seenRules, outputPhrases, universalRuleExceptions)
    local universalTargetRules = universalTastes[targetLevel] or {}
    
    -- 首先检查是否有通用具体物品,如果有,创建"所有普遍XX的礼物"分类
    local hasUniversalItems = false
    for _, u_rule in ipairs(universalTargetRules) do
        if type(u_rule) == 'number' and u_rule >= 0 or (type(u_rule) == 'string' and objectData[u_rule]) then
            hasUniversalItems = true
            break
        end
    end
    
    if hasUniversalItems then
        local levelNames = {
            love = "最爱",
            like = "喜欢", 
            neutral = "一般",
            dislike = "不喜欢",
            hate = "讨厌"
        }
        local ruleName = "所有普遍" .. (levelNames[targetLevel] or targetLevel) .. "的礼物"
        mw.log("处理通用偏好物品规则 -> " .. ruleName .. " (targetLevel: " .. targetLevel .. ")")
        
        local exceptions = {}
        local seenExceptions = {}
        
        -- 查找被个人偏好覆盖的通用物品
        for _, u_rule in ipairs(universalTargetRules) do
            if type(u_rule) == 'number' and u_rule >= 0 or (type(u_rule) == 'string' and objectData[u_rule]) then
                local u_info = _getItemInfo(u_rule)
                local chineseName = _getItemDisplayName(u_rule)
                if u_info and chineseName and not _isBlacklisted(u_rule) and not _isItemBlacklistedByCategory(u_info) then
                    local actualTaste = _getTasteForItem(u_rule, u_info, npcName, personalTastes, universalTastes)
                    if actualTaste ~= targetLevel and not seenExceptions[chineseName] then
                        table.insert(exceptions, chineseName)
                        seenExceptions[chineseName] = true
                    end
                end
            end
        end
        
        local text = ruleName
        if #exceptions > 0 then
            exceptions = _sortItemNames(exceptions)
            text = text .. _formatExceptionItems(exceptions)
            -- 存储除外信息到全局变量
            universalRuleExceptions[targetLevel] = exceptions
        end
        mw.log("添加通用物品分类规则: " .. text)
        table.insert(outputPhrases, text)
        seenRules["universal_items_" .. targetLevel] = true
    end
    
    -- 然后处理通用分类规则
    for _, u_rule in ipairs(universalTargetRules) do
        if not (type(u_rule) == 'number' and u_rule >= 0 or (type(u_rule) == 'string' and objectData[u_rule])) then
            -- 检查这个通用规则是否已经被个人偏好定义为其他等级
            local isPersonallyOverridden = false
            for _, level in ipairs({"love", "like", "dislike", "hate"}) do
                if level ~= targetLevel then  -- 检查其他等级
                    local personalItems = (personalTastes[level] and personalTastes[level].items) or {}
                    for _, rule in ipairs(personalItems) do
                        if tostring(rule) == tostring(u_rule) then
                            isPersonallyOverridden = true
                            mw.log("跳过通用" .. targetLevel .. "规则 " .. tostring(u_rule) .. ",因为被个人" .. level .. "覆盖")
                            break
                        end
                    end
                    if isPersonallyOverridden then break end
                end
            end
            
            -- 只有当没有被个人偏好覆盖时,才处理这个通用分类规则
            if not isPersonallyOverridden then
                _processCategoryRule(u_rule, "通用偏好", targetLevel, npcName, personalTastes, universalTastes, seenRules, outputPhrases, nil, nil)
            end
        end
    end
end

-- 检查物品是否被规则覆盖
local function _isItemCoveredByRules(itemId, itemInfo, seenRules, universalTastes, targetLevel, personalTastes)
    -- 检查是否被通用物品规则覆盖
    local universalItemIds = universalTastes[targetLevel] or {}
    for _, u_rule in ipairs(universalItemIds) do
        if type(u_rule) == 'number' and u_rule >= 0 or (type(u_rule) == 'string' and objectData[u_rule]) then
            if tostring(u_rule) == tostring(itemId) then
                return true
            end
        end
    end
    
    -- 检查其他规则覆盖
    for ruleKey, _ in pairs(seenRules) do
        local rule = tonumber(ruleKey) or ruleKey
        if _itemMatchesRule(itemInfo, rule) or tostring(rule) == tostring(itemId) then
            -- 添加调试日志来跟踪哪些物品被哪些规则覆盖
            local chineseName = _getItemDisplayName(itemId)
            mw.log("物品 " .. (chineseName or "unknown") .. " (ID: " .. itemId .. ") 被规则 " .. tostring(rule) .. " 覆盖")
            return true
        end
    end
    
    -- 检查个人规则覆盖
    for _, level in ipairs({"love", "like", "neutral", "dislike", "hate"}) do
        local items = (personalTastes[level] and personalTastes[level].items) or {}
        for _, rule in ipairs(items) do
            if tostring(rule) == tostring(itemId) or _itemMatchesRule(itemInfo, rule) then
                return true
            end
        end
    end
    
    return false
end

-- 处理默认规则产生的物品
local function _processDefaultItems(targetLevel, npcName, personalTastes, universalTastes, seenRules, outputItems, seenItems)
    local function addItem(name)
        if name and not seenItems[name] then
            table.insert(outputItems, name)
            seenItems[name] = true
        end
    end
    
    for itemId, itemInfo in pairs(objectData) do
        if itemInfo.Category ~= -999 and itemInfo.CanBeGivenAsGift and itemInfo.Price ~= 0 and not _isBlacklisted(itemId) and not _isItemBlacklistedByCategory(itemInfo) then
            local actualTaste = _getTasteForItem(itemId, itemInfo, npcName, personalTastes, universalTastes)
            if actualTaste == targetLevel then
                local coveredByRule = _isItemCoveredByRules(itemId, itemInfo, seenRules, universalTastes, targetLevel, personalTastes)
                local chineseName = _getItemDisplayName(itemId)
                if not coveredByRule and chineseName then
                    addItem(chineseName)
                end
            end
        end
    end
end

-- 处理隐性分类(如古物)
local function _processImplicitCategories(targetLevel, npcName, personalTastes, universalTastes, seenRules, outputPhrases, outputItems)
    local typeGroups = {}
    
    -- 收集当前找到的所有具体物品,按类型分组
    for itemId, itemInfo in pairs(objectData) do
        if itemInfo.Category ~= -999 and itemInfo.CanBeGivenAsGift and not _isBlacklisted(itemId) and not _isItemBlacklistedByCategory(itemInfo) then
            local actualTaste = _getTasteForItem(itemId, itemInfo, npcName, personalTastes, universalTastes)
            if actualTaste == targetLevel then
                if itemInfo.Type and typeNames[itemInfo.Type] then
                    if not typeGroups[itemInfo.Type] then
                        typeGroups[itemInfo.Type] = {total = 0, covered = 0, items = {}}
                    end
                    typeGroups[itemInfo.Type].total = typeGroups[itemInfo.Type].total + 1
                    typeGroups[itemInfo.Type].items[itemId] = _getItemDisplayName(itemId)
                    
                    local coveredByRule = _isItemCoveredByRules(itemId, itemInfo, seenRules, universalTastes, targetLevel, personalTastes)
                    if not coveredByRule then
                        typeGroups[itemInfo.Type].covered = typeGroups[itemInfo.Type].covered + 1
                    end
                end
            end
        end
    end
    
    -- 对于每个类型,如果大部分物品都是目标偏好等级,则创建分类规则
    for typeName, group in pairs(typeGroups) do
        -- 跳过书籍类型,因为书籍已经通过 book_item 标签处理了
        if typeName == "asdf" and seenRules["book_item"] then
            mw.log("跳过书籍类型的隐性分类,因为已经有 book_item 规则")
        elseif group.covered >= 3 and group.covered / group.total >= 0.8 then
            local exceptions = {}
            local seenExceptions = {}
            
            -- 找到不是目标偏好等级的物品
            for itemId, itemInfo in pairs(objectData) do
                if itemInfo.Type == typeName and itemInfo.Category ~= -999 and itemInfo.CanBeGivenAsGift and not _isBlacklisted(itemId) and not _isItemBlacklistedByCategory(itemInfo) then
                    local actualTaste = _getTasteForItem(itemId, itemInfo, npcName, personalTastes, universalTastes)
                    local chineseName = _getItemDisplayName(itemId)
                    if actualTaste ~= targetLevel and chineseName and not seenExceptions[chineseName] then
                        table.insert(exceptions, chineseName)
                        seenExceptions[chineseName] = true
                    end
                end
            end
            
            -- 如果除外项过多,则不创建分类
            if #exceptions > group.covered * 0.5 then
                mw.log("隐性分类" .. typeName .. "除外项过多(" .. #exceptions .. "/" .. group.covered .. "),不创建分类")
            else
                local text = "所有" .. _getTypeName(typeName)
                if #exceptions > 0 then 
                    exceptions = _sortItemNames(exceptions)
                    text = text .. _formatExceptionItems(exceptions)
                end
                mw.log("添加隐性分类规则: " .. text)
                table.insert(outputPhrases, text)
                
                -- 如果是Arch类型,标记为已处理,以便跳过ancient_item规则
                if typeName == "Arch" then
                    seenRules["Arch_implicit"] = true
                    mw.log("标记Arch隐性分类已处理,将跳过后续的ancient_item规则")
                end
                
                -- 从个别物品列表中移除这个类型的物品
                local newOutputItems = {}
                for _, itemName in ipairs(outputItems) do
                    local shouldRemove = false
                    for itemId, itemInfo in pairs(objectData) do
                        if _getItemDisplayName(itemId) == itemName and itemInfo.Type == typeName then
                            shouldRemove = true
                            break
                        end
                    end
                    if not shouldRemove then
                        table.insert(newOutputItems, itemName)
                    end
                end
                for i = 1, #outputItems do outputItems[i] = nil end
                for _, item in ipairs(newOutputItems) do
                    table.insert(outputItems, item)
                end
            end
        end
    end
end

-- 处理中立查询的默认分类
local function _processNeutralDefaults(targetLevel, npcName, personalTastes, universalTastes, seenRules, outputPhrases)
    if targetLevel ~= "neutral" then return end
    
    local giftableCategoryIds = { -2, -4, -5, -6, -12, -75, -79, -80, -81 }
    for _, catId in ipairs(giftableCategoryIds) do
        if not seenRules[tostring(catId)] then
            -- 检查这个分类是否有任何规则定义
            local hasRule = false
            for _, level in ipairs({"love", "like", "dislike", "hate"}) do
                -- 检查个人规则
                local personalItems = (personalTastes[level] and personalTastes[level].items) or {}
                for _, rule in ipairs(personalItems) do
                    if tostring(rule) == tostring(catId) then
                        hasRule = true
                        break
                    end
                end
                if hasRule then break end
                
                -- 检查通用规则
                local universalItems = universalTastes[level] or {}
                for _, rule in ipairs(universalItems) do
                    if tostring(rule) == tostring(catId) then
                        hasRule = true
                        break
                    end
                end
                if hasRule then break end
            end
            
            if not hasRule then
                local neutralItems = 0
                local totalItems = 0
                local exceptions = {}
                local seenExceptions = {}
                
                for itemId, itemInfo in pairs(objectData) do
                    if itemInfo.Category == catId and itemInfo.CanBeGivenAsGift and not _isBlacklisted(itemId) and not _isItemBlacklistedByCategory(itemInfo) then
                        totalItems = totalItems + 1
                        local actualTaste = _getTasteForItem(itemId, itemInfo, npcName, personalTastes, universalTastes)
                        local chineseName = _getItemDisplayName(itemId)
                        
                        if actualTaste == "neutral" then
                            neutralItems = neutralItems + 1
                        elseif actualTaste ~= "neutral" and chineseName and not seenExceptions[chineseName] then
                            table.insert(exceptions, chineseName)
                            seenExceptions[chineseName] = true
                        end
                    end
                end
                
                -- 只有当中立物品占大多数(超过80%)且总数超过3个时,才创建分类规则
                if totalItems >= 3 and neutralItems >= totalItems * 0.8 then
                    local text = "所有" .. _getCategoryName(catId)
                    if #exceptions > 0 then 
                        exceptions = _sortItemNames(exceptions)
                        text = text .. _formatExceptionItems(exceptions)
                    end
                    mw.log("添加中立分类规则: " .. text .. " (中立: " .. neutralItems .. "/" .. totalItems .. ")")
                    table.insert(outputPhrases, text)
                    seenRules[tostring(catId)] = true
                else
                    mw.log("跳过中立分类规则 " .. _getCategoryName(catId) .. " (中立: " .. neutralItems .. "/" .. totalItems .. ", 比例不足)")
                end
            end
        end
    end
end

-- 合并和排序最终输出
local function _buildFinalOutput(outputPhrases, personalSpecialItems, outputItems)
    -- 预解析所有分类规则,避免重复字符串切片操作
    local parsedCategoryRules = {}  -- 存储解析后的分类规则信息
    local categoryMap = {}
    local phraseData = {}
    
    -- 解析分类规则的安全函数
    local function parsePhrase(phrase)
        local categoryName = nil
        local exceptions = {}
        local exceptionSet = {}
        
        -- 使用UTF-8安全函数查找除外开始位置
        local exceptionStart = _findUTF8(phrase, "(", 1, true)
        if exceptionStart then
            -- 有除外的情况
            categoryName = _subStringUTF8(phrase, 3, exceptionStart - 1)  -- "所有"是2个UTF-8字符
            local exceptionEnd = _findUTF8(phrase, "除外)", exceptionStart, true)
            if exceptionEnd then
                local exceptionsStr = _subStringUTF8(phrase, exceptionStart + 1, exceptionEnd - 1)
                -- 使用UTF-8安全的字符串分割
                local totalLen = _getUTF8Length(exceptionsStr)
                local pos = 1
                while pos <= totalLen do
                    local nextPos = _findUTF8(exceptionsStr, "、", pos, true)
                    local exception
                    if nextPos then
                        exception = _subStringUTF8(exceptionsStr, pos, nextPos - 1)
                        pos = nextPos + 1  -- "、"是1个UTF-8字符
                    else
                        exception = _subStringUTF8(exceptionsStr, pos)
                        pos = totalLen + 1
                    end
                    if exception and exception ~= "" then
                        table.insert(exceptions, exception)
                        exceptionSet[exception] = true
                    end
                end
            end
        else
            -- 没有除外的情况
            categoryName = _subStringUTF8(phrase, 3)  -- "所有"是2个UTF-8字符
        end
        
        return categoryName, exceptions, exceptionSet
    end
    
    -- 解析所有分类规则
    for _, phrase in ipairs(outputPhrases) do
        if _subStringUTF8(phrase, 1, 2) == "所有" then
            local categoryName, exceptions, exceptionSet = parsePhrase(phrase)
            if categoryName then
                -- 存储解析后的规则信息,用于后续快速查找
                parsedCategoryRules[phrase] = {
                    categoryName = categoryName,
                    exceptions = exceptions,
                    exceptionSet = exceptionSet
                }
                
                -- 维护categoryMap用于合并逻辑
                if not categoryMap[categoryName] then
                    categoryMap[categoryName] = {}
                end
                for _, exception in ipairs(exceptions) do
                    categoryMap[categoryName][exception] = true
                end
            end
        else
            table.insert(phraseData, {
                text = phrase,
                type = "other",
                isUniversal = false,
                category = 99999,
                price = 0
            })
        end
    end
    
    -- 重建分类规则并分离可合并的分类
    local mergableCategories = {}
    local standaloneCategories = {}
    
    for categoryName, exceptionsMap in pairs(categoryMap) do
        local exceptions = {}
        for exception, _ in pairs(exceptionsMap) do
            table.insert(exceptions, exception)
        end
        
        -- 使用格式化函数生成基础文本
        local text = _formatCategoryText(categoryName)
        if #exceptions > 0 then
            exceptions = _sortItemNames(exceptions)
            text = text .. _formatExceptionItems(exceptions)
        end
        
        local isUniversal = _findUTF8(categoryName, "普遍", 1, true) ~= nil
        local hasExceptions = #exceptions > 0
        
        if isUniversal or hasExceptions then
            table.insert(standaloneCategories, {
                text = text,
                type = "category", 
                isUniversal = isUniversal,
                category = isUniversal and 1 or 2,
                price = 0
            })
        else
            table.insert(mergableCategories, categoryName)
        end
    end
    
    -- 处理可合并的分类
    if #mergableCategories > 0 then
        local mergedText = ""
        if #mergableCategories == 1 then
            -- 单个分类,直接使用格式化函数
            mergedText = _formatCategoryText(mergableCategories[1])
        else
            -- 多个分类合并,需要特殊处理链接
            local formattedParts = {}
            
            if #mergableCategories == 2 then
                -- 两个分类
                for _, categoryName in ipairs(mergableCategories) do
                    local linkTarget = categoryLinks[categoryName]
                    if linkTarget then
                        if linkTarget == categoryName then
                            table.insert(formattedParts, "[[" .. linkTarget .. "]]")
                        else
                            table.insert(formattedParts, "[[" .. linkTarget .. "|" .. categoryName .. "]]")
                        end
                    else
                        table.insert(formattedParts, categoryName)
                    end
                end
                mergedText = "所有" .. formattedParts[1] .. "和" .. formattedParts[2]
            else
                -- 三个或以上分类
                for i = 1, #mergableCategories do
                    local categoryName = mergableCategories[i]
                    local linkTarget = categoryLinks[categoryName]
                    if linkTarget then
                        if linkTarget == categoryName then
                            table.insert(formattedParts, "[[" .. linkTarget .. "]]")
                        else
                            table.insert(formattedParts, "[[" .. linkTarget .. "|" .. categoryName .. "]]")
                        end
                    else
                        table.insert(formattedParts, categoryName)
                    end
                end
                
                local parts = {}
                for i = 1, #formattedParts - 1 do
                    table.insert(parts, formattedParts[i])
                end
                mergedText = "所有" .. table.concat(parts, "、") .. "和" .. formattedParts[#formattedParts]
            end
        end
        
        table.insert(standaloneCategories, {
            text = mergedText,
            type = "category",
            isUniversal = false,
            category = 3,
            price = 0
        })
    end
    
    -- 添加所有分类到phraseData并排序
    for _, data in ipairs(standaloneCategories) do
        table.insert(phraseData, data)
    end
    
    table.sort(phraseData, function(a, b)
        if a.isUniversal ~= b.isUniversal then
            return a.isUniversal
        end
        if a.category ~= b.category then
            return a.category < b.category
        end
        if a.type ~= b.type then
            return a.type < b.type
        end
        return a.text < b.text
    end)
    
    -- 物品数据创建函数
    local function createItemData(itemNames, itemType)
        _initializeNameMap()
        local itemData = {}
        for _, itemName in ipairs(itemNames) do
            local itemId = _getItemIdByName(itemName)
            if not _isBlacklisted(itemId) then
                local itemInfo = itemId and objectData[tostring(itemId)]
                
                table.insert(itemData, {
                    text = itemName,
                    type = itemType,
                    isUniversal = false,
                    category = itemInfo and itemInfo.Category or 0,
                    price = itemInfo and itemInfo.Price or 0
                })
            end
        end
        
        table.sort(itemData, function(a, b)
            if a.category ~= b.category then
                return a.category < b.category
            end
            if a.type ~= b.type then
                return a.type < b.type
            end
            return a.price < b.price
        end)
        
        return itemData
    end
    
    local personalItemData = createItemData(personalSpecialItems, "personal")
    
    -- 高效的物品覆盖检查函数
    local function isItemCoveredByPhrases(itemName)
        local itemId = _getItemIdByName(itemName)
        if not itemId then return false end
        
        local itemInfo = objectData[tostring(itemId)]
        if not itemInfo then return false end
        
        -- 使用预解析的规则信息进行快速检查
        for phrase, ruleInfo in pairs(parsedCategoryRules) do
            local categoryName = ruleInfo.categoryName
            local isMatched = false
            
            -- 检查分类匹配
            if categoryNames then
                for catId, catName in pairs(categoryNames) do
                    if catName == categoryName and itemInfo.Category == catId then
                        isMatched = true
                        break
                    end
                end
            end
            
            -- 检查Context Tag匹配
            if not isMatched and contextTagNames then
                for tag, tagName in pairs(contextTagNames) do
                    if tagName == categoryName and itemInfo.ContextTags then
                        for _, itemTag in ipairs(itemInfo.ContextTags) do
                            if itemTag == tag then
                                isMatched = true
                                break
                            end
                        end
                        if isMatched then break end
                    end
                end
            end
            
            -- 检查Type匹配
            if not isMatched and typeNames then
                for typeName, typeDisplayName in pairs(typeNames) do
                    if typeDisplayName == categoryName and itemInfo.Type == typeName then
                        isMatched = true
                        break
                    end
                end
            end
            
            -- 如果匹配,检查是否在除外列表中
            if isMatched then
                if not ruleInfo.exceptionSet[itemName] then
                    return true  -- 不在除外列表中,被覆盖
                end
            end
        end
        
        return false
    end
    
    -- 过滤其他物品
    local otherItems = {}
    local personalItemsMap = {}
    for _, item in ipairs(personalSpecialItems) do
        personalItemsMap[item] = true
    end
    
    for _, itemName in ipairs(outputItems) do
        if not personalItemsMap[itemName] and not isItemCoveredByPhrases(itemName) then
            table.insert(otherItems, itemName)
        end
    end
    
    local otherItemData = createItemData(otherItems, "other")
    
    -- 构建最终输出
    local finalOutput = {}
    for _, data in ipairs(phraseData) do
        table.insert(finalOutput, data.text)
    end
    for _, data in ipairs(personalItemData) do
        table.insert(finalOutput, data.text)
    end
    for _, data in ipairs(otherItemData) do
        table.insert(finalOutput, data.text)
    end
    
    return finalOutput, phraseData, personalItemData, otherItemData
end

function p.get(frame)
    -- 在主函数开始时初始化映射表
    _initializeNameMap()
    
    -- 全局临时变量存储通用规则的除外信息
    local universalRuleExceptions = {}
    
    local args = frame.args
    local npcName = args.npc or args[1]
    npcName = NPC.getEnglishName(npcName)
    local targetLevel = args.taste or args[2]
    if not npcName or not targetLevel then
        return ''
    end

    npcName = mw.text.trim(npcName)
    targetLevel = mw.text.trim(targetLevel)
    
    -- 中文到英文的偏好等级映射
    local chineseToEnglish = {
        ["最爱"] = "love", 
        ["喜欢"] = "like",
        ["一般"] = "neutral",
        ["中立"] = "neutral",  -- 保留中立作为一般的同义词
        ["普通"] = "neutral",
        ["不喜欢"] = "dislike",
        ["讨厌"] = "hate"
    }
    
    -- 如果是中文输入,转换为英文
    if chineseToEnglish[targetLevel] then
        targetLevel = chineseToEnglish[targetLevel]
    else
        targetLevel = targetLevel:lower()
    end

    local personalTastes = giftTasteData.npcs[npcName]
    if not personalTastes then
        return ''
    end

    local universalTastes = giftTasteData.universal

    -- 初始化数据结构
    local outputPhrases = {}
    local outputItems = {}
    local personalSpecialItems = {}
    local seenItems = {}
    local seenPersonalItems = {}
    local seenRules = {}

    -- 1. 处理个人偏好规则
    _processPersonalPreferences(personalTastes, targetLevel, npcName, universalTastes, seenRules, outputPhrases, personalSpecialItems, seenPersonalItems)

    -- 2. 处理通用偏好规则
    _processUniversalPreferences(universalTastes, targetLevel, npcName, personalTastes, seenRules, outputPhrases, universalRuleExceptions)

    -- 3. 处理默认规则产生的物品
    _processDefaultItems(targetLevel, npcName, personalTastes, universalTastes, seenRules, outputItems, seenItems)

    -- 4. 检测隐性分类
    _processImplicitCategories(targetLevel, npcName, personalTastes, universalTastes, seenRules, outputPhrases, outputItems)

    -- 5. 对于中立查询,添加默认中立的分类
    _processNeutralDefaults(targetLevel, npcName, personalTastes, universalTastes, seenRules, outputPhrases)

    -- 6. 合并和排序最终输出
    local finalOutput, phraseData, personalItemData, otherItemData = _buildFinalOutput(outputPhrases, personalSpecialItems, outputItems)
    
    -- 分类规则
    local phraseTexts = {}
    for _, data in ipairs(phraseData) do
        table.insert(phraseTexts, data.text)
    end
    -- 个人特殊喜好物品
    local personalTexts = {}
    for _, data in ipairs(personalItemData) do
        table.insert(personalTexts, data.text)
    end
    -- 其他具体物品
    local otherTexts = {}
    for _, data in ipairs(otherItemData) do
        table.insert(otherTexts, data.text)
    end
    
    -- 设置临时变量缓存
    local levelNames = {
        love = "最爱",
        like = "喜欢", 
        neutral = "一般",
        dislike = "不喜欢",
        hate = "讨厌"
    }
    local chineseLevelName = levelNames[targetLevel] or targetLevel
    local universalTargetRules = universalTastes[targetLevel] or {}
    
    -- 获取存储的除外信息
    local universalExceptions = universalRuleExceptions[targetLevel] or {}
    
    -- 最终输出    
    if #finalOutput == 0 then 
        return "" 
    end

    -- 构建最终输出的 wikitext
    local resultParts = {}
    
    -- 1. 展开 Gifts head 模板
    local universalText = ""
    if #phraseTexts > 0 then
        -- 为每个条目添加 <li> 标签
        local liWrappedTexts = {}
        for _, text in ipairs(phraseTexts) do
            table.insert(liWrappedTexts, "<li>" .. text .. "</li>")
        end
        -- 先连接文本,然后展开除外物品的 Name 模板
        local rawUniversalText = "<ul>\n" .. table.concat(liWrappedTexts, "\n") .. "\n</ul>"
        universalText = _expandExceptionItems(rawUniversalText)
    end
    
    local headTemplate = utils.expandTemplate(
    "Gifts",
        {
            "head",
            villager = npcName,
            type = targetLevel,
            universal = universalText
        }
    )
    table.insert(resultParts, headTemplate)
    
    local hasPersonalHeader = false

    -- 2. 展开个人特殊喜好物品
    for _, itemName in ipairs(personalTexts) do

        if not hasPersonalHeader then
            hasPersonalHeader = true
            table.insert(resultParts, "<tr>")
            table.insert(resultParts, '<th colspan="4">个人喜好</th>')
            table.insert(resultParts, "</tr>")
        end

        -- 获取中文物品名称
        local itemId = _getItemIdByName(itemName)
        local chineseName = itemId and _getItemChineseName(itemId) or itemName
        
        local giftRowTemplate = utils.expandTemplate(
            ":" .. chineseName,
            {"GiftRow"}
        )
        table.insert(resultParts, giftRowTemplate)
    end
    
    -- 3. 输出分隔行和其他具体物品
    -- 获取通用偏好中的具体物品(非分类物品),并排除除外物品
    local universalItemLinks = {}
    
    for _, ruleId in ipairs(universalTargetRules) do
        if type(ruleId) == 'number' and ruleId >= 0 then
            -- 这是具体物品ID
            local englishName = ItemNames.getEnglishName("(O)" .. ruleId)
            if englishName and englishName ~= "" then
                -- 检查是否在除外列表中,如果是则跳过
                local isException = false
                local displayName = _getItemDisplayName(ruleId)
                
                for _, exceptionName in ipairs(universalExceptions) do
                    if displayName == exceptionName or englishName == exceptionName then
                        isException = true
                        break
                    end
                end
                
                if not isException then
                    local nameTemplate = _expandNameTemplate(englishName, {
                        class = "inline"
                    })
                    table.insert(universalItemLinks, nameTemplate)
                end
            end
        end
    end
    
    -- 只有当有通用物品或其他物品时才输出这个部分
    if #universalItemLinks > 0 or #otherTexts > 0 then
        table.insert(resultParts, "<tr>")
        table.insert(resultParts, '<th colspan="4">普遍' .. chineseLevelName .. '</th>')
        table.insert(resultParts, "</tr>")

        table.insert(resultParts, "<tr>")
        table.insert(resultParts, '<td colspan="4" style="line-break: anywhere;">')
        
        -- 构建其他物品的内联链接
        local otherItemLinks = {}
        for _, itemName in ipairs(otherTexts) do
            local nameTemplate = _expandNameTemplate(itemName, {
                class = "inline"
            })
            table.insert(otherItemLinks, nameTemplate)
        end
        
        -- 合并通用物品和其他物品,通用物品在前
        local allItemLinks = {}
        for _, link in ipairs(universalItemLinks) do
            table.insert(allItemLinks, '<li>' .. link .. '</li>')
        end
        for _, link in ipairs(otherItemLinks) do
            table.insert(allItemLinks, '<li>' .. link .. '</li>')
        end
        
        -- 连接所有物品
        if #allItemLinks > 0 then
            table.insert(resultParts, '<ul style="column-width: 120px; column-gap: 0.25em; list-style: none; margin: 0 !important;">' .. table.concat(allItemLinks, "")  .. '</ul>')
            -- table.insert(resultParts, table.concat(allItemLinks, " • "))
        end
        
        table.insert(resultParts, "</td>")
        table.insert(resultParts, "</tr>")
    end
    
    -- 4. 展开 Gifts foot 模板
    local footTemplate = utils.expandTemplate(
        "Gifts",
        {"foot"}
    )
    table.insert(resultParts, footTemplate)
    
    return table.concat(resultParts, "\n")

end

return p