米斯特利亚Wiki正在建设中,本WIKI编辑权限开放!欢迎参与~!

全站通知:

模块:Npcs

来自米斯特利亚WIKI_BWIKI_哔哩哔哩
跳到导航 跳到搜索

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

local NpcsData = require('模块:Npcs/Data')
local Items = require('模块:Items')
local Get = require('模块:Get')
-- 模块开头
local p = {}

local function trim(s)
    if type(s) ~= 'string' then return s end
    return s:gsub('^%s+', ''):gsub('%s+$', '')
end






-- 构建人物肖像精灵名
-- 参数:npcId, seasonOrBeach, keyName, idx(0或1)
-- 返回:spr_portrait_<npcid>_<四季/海滩>_<portraits.键名>_<0/1>
function p._portraitSprite(npcId, seasonOrBeach, keyName, idx)
    local id  = npcId or ''
    local sb  = seasonOrBeach or 'spring'
    local key = keyName or 'neutral'
    local i   = tostring(idx or '0')
    if i ~= '1' then i = '0' end
    return string.format('spr_portrait_%s_%s_%s_%s', id, sb, 'portraits.'..key, i)
end

-- 对外包装:从 frame 读取参数
-- 使用:{{#invoke:模块名|portraitSprite|npcId|season|key|0/1}}
function p.portraitSprite(frame)
    local npcId = frame.args[1] or frame.args.npc or ''
    local seasonOrBeach = frame.args[2] or frame.args.outfit or ''
    local keyName = frame.args[3] or frame.args.key or ''
    local idx = frame.args[4] or frame.args.index or '0'
    return p._portraitSprite(npcId, seasonOrBeach, keyName, idx)
end


-- 根据 tags(如 vendor,dateable,child 等)筛选 NPC,返回 npcId 列表(table)
-- @param tags string|table 逗号/中文逗号分隔的字符串,或字符串数组
-- @param npcData table 可选,默认使用 NpcsData
-- @return table { npcId, ... }
function p._npcIdsByTags(tags, npcData)
    local data = npcData or NpcsData
    local result = {}

    if type(data) ~= 'table' or not next(data) then return result end

    -- 解析待匹配的标签列表
    local tags_list = {}
    if type(tags) == 'string' then
        if tags ~= '' then
            local s = tags:gsub(',', ',')
            for part in mw.text.gsplit(s, ',', true) do
                local trimmed_part = trim(part)
                if trimmed_part ~= '' then
                    table.insert(tags_list, trimmed_part)
                end
            end
        end
    elseif type(tags) == 'table' then
        for _, t in ipairs(tags) do
            if type(t) == 'string' then
                local trimmed_t = trim(t)
                if trimmed_t ~= '' then table.insert(tags_list, trimmed_t) end
            end
        end
    else
        return result
    end
    if #tags_list == 0 then return result end

    for npcId, npc in pairs(data) do
        if type(npc) == 'table' and type(npc.tags) == 'table' then
            local hit = false
            for _, t in ipairs(tags_list) do
                for _, x in ipairs(npc.tags) do
                    if t == x then hit = true break end
                end
                if hit then break end
            end
            if hit then table.insert(result, npcId) end
        end
    end
    return result
end

-- 可选:对外包装,返回逗号分隔的 npcId 字符串
function p.npcIdsByTags(frame)
    local arg = frame.args[1] or frame.args.tags or ''
    local ids = p._npcIdsByTags(arg, NpcsData)
    return table.concat(ids, ',')
end

-- 将标签渲染为:tag:<物品渲染>(逐行)
local function renderTagsWithItems(tags)
    if type(tags) ~= 'table' or not next(tags) then return '' end
    local parts = {}
    for _, tag in ipairs(tags) do
        local ids = Items._findItemByTag(tag) or {}
        local rendered = Items._renderItemList(ids)
        table.insert(parts, (tag .. ':' .. (rendered or '')))
    end
    return table.concat(parts, '<br/>')
end

-- 核心纯函数:按 NPC 输出礼物偏好
-- 返回: { loved = {itemId...}, liked = {...}, disliked = {tag...}, banned = {tag...}, hated = {itemId?} }
function p._giftByNpc(npcArg, npcData, options)
    local data = npcData or NpcsData
    local res = { loved = {}, liked = {}, disliked = {}, banned = {}, hated = {} }
    if not npcArg or type(data) ~= 'table' then return res end

    local npc = data[npcArg] or Get(data):values(function(n)
        return type(n) == 'table' and (n.name == npcArg or n.name_en == npcArg)
    end):one()
    if type(npc) ~= 'table' then return res end

    for _, v in ipairs(npc.loved_gifts or {}) do table.insert(res.loved, v) end
    for _, v in ipairs(npc.liked_gifts or {}) do table.insert(res.liked, v) end
    for _, v in ipairs(npc.disliked_gift_tags or {}) do table.insert(res.disliked, v) end
    for _, v in ipairs(npc.banned_gift_tags or {}) do table.insert(res.banned, v) end
    if npc.hated_gift and npc.hated_gift ~= '' then table.insert(res.hated, npc.hated_gift) end

    if options and options.sort == 'alpha' then
        table.sort(res.loved); table.sort(res.liked); table.sort(res.disliked); table.sort(res.banned); table.sort(res.hated)
    end

    return res
end

-- 对外包装:giftByNpc
-- 使用:{{#invoke:模块名|giftByNpc|<npcId或中文名或英文名>|translate=id|sort=alpha}}
function p.giftByNpc(frame)
    local npcArg = frame.args[1] or frame.args.npc
    if not npcArg or npcArg == '' then
        return '<!-- npc 参数为空 -->'
    end
    local translate = frame.args.translate -- 'id' | 'cn'(当前实现仅透传ID,中文需后续扩展)
    local sortOpt = frame.args.sort

    local options = {
        translate = translate == 'cn' and 'cn' or 'id',
        sort = (sortOpt == 'alpha') and 'alpha' or 'none',
    }

    local ret = p._giftByNpc(npcArg, NpcsData, options)

    local lovedStr    = Items._renderItemList(ret.loved)
    local likedStr    = Items._renderItemList(ret.liked)
    local dislikedStr = renderTagsWithItems(ret.disliked)
    local bannedStr   = renderTagsWithItems(ret.banned)
    local hatedStr    = Items._renderItemList(ret.hated)

    return frame:expandTemplate{
        title = 'GiftByNpc',
        args = {
            loved = lovedStr,
            liked = likedStr,
            disliked = dislikedStr,
            banned = bannedStr,
            hated = hatedStr,
            npc = npcArg, -- 供表头可选展示
        }
    }


end

-- ==== 核心纯函数 ====
-- 参数:
--   itemId  : string 物品ID
--   tags    : string 物品标签,逗号/中文逗号分隔
--   npcData : table  NPC 数据字典(root 下是 npcId -> npcAttrTable)
-- 返回:
--   { loved = {npc...}, liked = {npc...}, hated = {npc...}, disliked_by_tag = {npc...}, banned_by_tag = {npc...} }
function p._npcsByGift(itemId, tags, npcData)
    local data = npcData or NpcsData
    local res = { loved = {}, liked = {}, hated = {}, disliked_by_tag = {}, banned_by_tag = {} }

    if type(data) ~= 'table' or type(itemId) ~= 'string' or itemId == '' then
        return res
    end

    local tags_list = {}
    if type(tags) == 'string' then
        if tags ~= '' then
            local s = tags:gsub(',', ',')
            for part in mw.text.gsplit(s, ',', true) do
                local trimmed_part = trim(part)
                if trimmed_part ~= '' then
                    table.insert(tags_list, trimmed_part)
                end
            end
        end
    elseif type(tags) == 'table' then
        for _, t in ipairs(tags) do
            if type(t) == 'string' then
                local trimmed_t = trim(t)
                if trimmed_t ~= '' then table.insert(tags_list, trimmed_t) end
            end
        end
    end

    for _, npc in pairs(data) do
        if type(npc) == 'table' then
            -- loved / liked
            if type(npc.loved_gifts) == 'table' then
                for _, v in ipairs(npc.loved_gifts) do
                    if v == itemId then table.insert(res.loved, npc) break end
                end
            end
            if type(npc.liked_gifts) == 'table' then
                for _, v in ipairs(npc.liked_gifts) do
                    if v == itemId then table.insert(res.liked, npc) break end
                end
            end
            -- hated (single item id)
            if type(npc.hated_gift) == 'string' and npc.hated_gift == itemId then
                table.insert(res.hated, npc)
            end
            -- tag based
            if #tags_list > 0 then
                if type(npc.disliked_gift_tags) == 'table' then
                    local hit = false
                    for _, t in ipairs(tags_list) do
                        for _, x in ipairs(npc.disliked_gift_tags) do
                            if t == x then hit = true break end
                        end
                        if hit then break end
                    end
                    if hit then table.insert(res.disliked_by_tag, npc) end
                end
                if type(npc.banned_gift_tags) == 'table' then
                    local hit2 = false
                    for _, t in ipairs(tags_list) do
                        for _, x in ipairs(npc.banned_gift_tags) do
                            if t == x then hit2 = true break end
                        end
                        if hit2 then break end
                    end
                    if hit2 then table.insert(res.banned_by_tag, npc) end
                end
            end
        end
    end

    return res
end

-- ==== 对外暴露的包装函数(可选)====
-- 用法:
--   {{#invoke:Npcs|npcsByGift|golden_cookies|dessert,sweet}}
-- 也可以在别的 Lua 里直接调用 p._npcsByGift(itemId, tags, NpcsData)

function p.npcsByGift(frame)
    local itemNameOrId = frame.args[1] or frame.args.id or ""
    if itemNameOrId == '' then return "<!-- No item specified -->" end

    local tags = frame.args[2] or frame.args.tags
    if not tags or tags == '' then
        tags = Items._tags(itemNameOrId)
    end

    -- Resolve name to ID for direct gift matching, but use original name for tag lookup.
    local internalId = Items.id({ args = { itemNameOrId } })
    if type(internalId) ~= 'string' or internalId:find("<!--", 1, true) then
        internalId = itemNameOrId -- Fallback to original if resolution fails
    end

    local res = p._npcsByGift(internalId, tags, NpcsData)
    local npcNumber = 0
    for _, likeType in pairs(res) do
    	npcNumber = npcNumber + #likeType
    end
    if npcNumber <= 0 then
    	return "此物品没有NPC偏好或讨厌"
    end
    

    local function npcsToNames(list)
        return Get(list):items():map(function(npc)
            return npc.name or npc.name_en or ''
        end):all()
    end

    local lovedStr = table.concat(npcsToNames(res.loved), ',')
    local likedStr = table.concat(npcsToNames(res.liked), ',')
    local hatedStr = table.concat(npcsToNames(res.hated), ',')
    local dislikedStr = table.concat(npcsToNames(res.disliked_by_tag), ',')
    local bannedStr = table.concat(npcsToNames(res.banned_by_tag), ',')

    return frame:expandTemplate{
        title = 'npcsByGift',
        args = {
            loved = lovedStr,
            liked = likedStr,
            hated = hatedStr,
            disliked = dislikedStr,
            banned = bannedStr,
            item = itemNameOrId,
        }
    }
end

-- 根据是否可婚来获取NPC ID列表
-- @param isDatable boolean true表示可婚, false表示不可婚
-- @param npcData table, 可选, 默认使用NpcsData
-- @return table { npcId, ... }
function p._getNpcIdsByDatable(isDatable, npcData)
    local data = npcData or NpcsData
    local result = {}
    if type(data) ~= 'table' then return result end

    for npcId, npc in pairs(data) do
        if type(npc) == 'table' and npc.dateable == isDatable then
            table.insert(result, npcId)
        end
    end
    return result
end

-- 渲染NPC列表(简单实现,输出名字链接)
local function renderNpcList(frame, npcIdList)
	local moduleName = frame.args["模板"] or "人物"
    if type(npcIdList) ~= 'table' or #npcIdList == 0 then return '' end
    local parts = {}
    for _, npcId in ipairs(npcIdList) do
        local npc = NpcsData[npcId]
        if npc and npc.name then
            table.insert(parts, frame:expandTemplate{title=moduleName,args={npc.name,npc.icon_sprite}})
        else
            table.insert(parts, '[[' .. npcId .. ']]')
        end
    end
    return table.concat(parts, ' ')
end

-- 外部接口:获取可婚/不可婚NPC列表(函数1)
-- @param frame.args[1] string 'true' for datable, 'false' for not datable
function p.getNpcIdsByDatable(frame)
    local arg = frame.args[1]
    local isDatable = (arg == 'true' or arg == true)
    local ids = p._getNpcIdsByDatable(isDatable, NpcsData)
    return ids
end

-- 外部接口:渲染可婚/不可婚NPC列表(函数2)
-- @param frame.args[1] string 'true' for datable, 'false' for not datable
function p.renderNpcListByDatable(frame)
    local arg = frame.args[1]
    local isDatable = (arg == 'true' or arg == true)
    local ids = p._getNpcIdsByDatable(isDatable, NpcsData)
    return renderNpcList(frame, ids)
end

--[[ 测试代码
local frame = mw.getCurrentFrame()
frame.args[1] = "巧克力"
mw.logObject(p.npcsByGift(frame))

local frame = mw.getCurrentFrame()
frame.args[1] = "red_toadstool"
frame.args[2] = "material, mushroom, mushroomy"
mw.logObject(p.npcsByGift(frame))

local frame = mw.getCurrentFrame()
frame.args[1] = "dozy"
mw.logObject(p.giftByNpc(frame))

--]]
return p