Bläddra i källkod

修改百战成神

gitxsm 4 dagar sedan
förälder
incheckning
fa230718be

+ 5 - 2
script/module/baiZhanChengShen/BaiZhanChengShenCS.lua

@@ -171,8 +171,10 @@ local function issueRewardBatch(serverId, rewardList)
         local msgData = InnerMsg.wl.WL_BZCS_ISSUE_REWARD
         msgData.rewardList = rewardList
         InnerMsg.sendMsg(fd, msgData)
+        BzcsLog.logAction("reward_issue_send", string.format("serverId=%s cnt=%s", serverId, #rewardList))
     else
         BaiZhanChengShenDB.AddPendingRewards(serverId, rewardList)
+        BzcsLog.logAction("reward_issue_offline", string.format("serverId=%s cnt=%s", serverId, #rewardList))
     end
 end
 
@@ -181,6 +183,7 @@ local function issueRewardBatchFinish(serverId, rewardList, markIssued)
     issueRewardBatch(serverId, rewardList)
     if markIssued == 1 then
         BaiZhanChengShenDB.SetRewardIssued(true)
+        BzcsLog.logAction("reward_issue_done", string.format("serverId=%s lastBatchCnt=%s", serverId, #rewardList))
     end
 end
 
@@ -326,7 +329,7 @@ local function errTips(sourceServerId, playerUuid, errCode)
     sendWL(fd, msgData)
 end
 
--- LW_BZCS_MATCH -> WL_BZCS_MATCH (±500步进扩大匹配最多3人; refreshRanks 非空时仅刷新展示)
+-- LW_BZCS_MATCH -> WL_BZCS_MATCH (±500步进扩大, 窗口内随机匹配最多3人; refreshRanks 非空时仅刷新展示)
 function N2C_Match(msg)
     local fd = MiddleManager.getFDBySvrIndex(msg.sourceServerId)
     if not isRunning() then
@@ -406,7 +409,7 @@ function N2C_CanFight(msg)
     msgData.playerUuid = msg.playerUuid
     msgData.targetRank = targetRank
     msgData.defUuid = target.uuid
-    msgData.defServerId = BaiZhanChengShenDefine.GetClientServerId(target)
+    msgData.defServerId = target.serverId or 0
     local si = target.showInfo or {}
     msgData.defName = si.name or ""
     msgData.defScore = target.score or BaiZhanChengShenDefine.BZCS_INIT_SCORE

+ 66 - 45
script/module/baiZhanChengShen/BaiZhanChengShenDB.lua

@@ -3,7 +3,7 @@
 -- 运行环境: 仅跨服进程 _G.is_middle == true
 -- 存储: Mongo 集合 DB.db_bzcs
 --
--- 全服积分榜 rankCache: 含机器人池+真人; 排序 score 降序 -> scoreTime 升序(先达者优先) -> uuid
+-- 全服积分榜 rankCache + uuid2rank(与 rankDirty 同步重建); 排序 score 降序 -> scoreTime 升序 -> uuid
 -- 客户端展示榜仅前 BZCS_RANK_MAX 名, 与匹配用的全服榜不是同一展示范围
 --[=[
 BzcsData = {
@@ -39,6 +39,7 @@ local dbUpdate = {_id = nil}
 local dbUpdateField = {}
 
 local rankCache = nil
+local uuid2rank = nil
 local rankDirty = true
 
 -- 增量更新 Mongo 字段
@@ -81,6 +82,7 @@ end
 local function sortRankCache()
     if not rankDirty then return end
     rankCache = {}
+    uuid2rank = {}
     for uuid, pinfo in pairs(BzcsData.playerList or {}) do
         rankCache[#rankCache + 1] = {
             uuid = uuid,
@@ -97,9 +99,17 @@ local function sortRankCache()
         end
         return a.uuid < b.uuid
     end)
+    for rank, entry in ipairs(rankCache) do
+        uuid2rank[entry.uuid] = rank
+    end
     rankDirty = false
 end
 
+-- 保证 rankCache / uuid2rank 已按当前 playerList 重建
+local function ensureRankIndex()
+    sortRankCache()
+end
+
 -- 保证 showInfo 结构完整
 local function normalizePlayer(pinfo)
     if not pinfo then return end
@@ -324,19 +334,15 @@ end
 
 -- 查询玩家当前全服名次, 0=未上榜
 function GetRankByUuid(uuid)
-    sortRankCache()
-    for rank, entry in ipairs(rankCache or {}) do
-        if entry.uuid == uuid then
-            return rank
-        end
-    end
-    return 0
+    if not uuid then return 0 end
+    ensureRankIndex()
+    return uuid2rank[uuid] or 0
 end
 
 -- 按全服名次取玩家(含机器人), rank 从 1 起
 function GetPlayerByRank(rank)
     if not rank or rank < 1 then return nil end
-    sortRankCache()
+    ensureRankIndex()
     local entry = rankCache and rankCache[rank]
     if not entry then return nil end
     return GetPlayer(entry.uuid)
@@ -352,7 +358,7 @@ function BuildRankInfoEntry(pinfo, rank)
         name = si.name or "",
         head = si.head or 0,
         headFrame = si.headFrame or 0,
-        serverId = BaiZhanChengShenDefine.GetClientServerId(pinfo),
+        serverId = pinfo.serverId or 0,
         power = BaiZhanChengShenDefine.CalcPlayerPower(si),
         score = pinfo.score or BaiZhanChengShenDefine.BZCS_INIT_SCORE,
         isRobot = pinfo.isRobot,
@@ -393,7 +399,7 @@ end
 
 -- 客户端展示榜前 limit 名(默认100), 含 rank/uuid/展示字段
 function GetRankList(limit)
-    sortRankCache()
+    ensureRankIndex()
     limit = limit or BaiZhanChengShenDefine.BZCS_RANK_MAX
     local ret = {}
     for i = 1, math.min(limit, #(rankCache or {})) do
@@ -417,13 +423,13 @@ local function getOpponentBody(uuid, pinfo)
     return db and db.body or 0
 end
 
-local function makeMatchOpponentEntry(uuid, pinfo, uuid2rank)
+local function makeMatchOpponentEntry(uuid, pinfo)
     local si = pinfo.showInfo or {}
     local score = pinfo.score or BaiZhanChengShenDefine.BZCS_INIT_SCORE
     return {
         rank = uuid2rank[uuid] or 0,
         uuid = uuid,
-        serverId = BaiZhanChengShenDefine.GetClientServerId(pinfo),
+        serverId = pinfo.serverId or 0,
         name = si.name,
         body = getOpponentBody(uuid, pinfo),
         power = BaiZhanChengShenDefine.CalcPlayerPower(si),
@@ -432,8 +438,42 @@ local function makeMatchOpponentEntry(uuid, pinfo, uuid2rank)
     }
 end
 
+-- 收集当前积分窗口内可匹配候选(已选/排除名单不入列)
+local function collectWindowCandidates(minScore, maxScore, excludeTb)
+    local candidates = {}
+    for uuid, pinfo in pairs(BzcsData.playerList or {}) do
+        if not excludeTb[uuid] then
+            local score = pinfo.score or BaiZhanChengShenDefine.BZCS_INIT_SCORE
+            if score >= minScore and score <= maxScore then
+                local oppRank = uuid2rank[uuid] or 0
+                if oppRank > 0 then
+                    candidates[#candidates + 1] = {uuid = uuid, pinfo = pinfo}
+                end
+            end
+        end
+    end
+    return candidates
+end
+
+-- 从候选中随机抽取至多 need 名加入结果
+local function pickRandomFromCandidates(selected, excludeTb, candidates, need)
+    if need <= 0 or #candidates < 1 then
+        return
+    end
+    table.shuffle(candidates)
+    for i = 1, #candidates do
+        if need <= 0 then
+            break
+        end
+        local c = candidates[i]
+        excludeTb[c.uuid] = true
+        selected[#selected + 1] = makeMatchOpponentEntry(c.uuid, c.pinfo)
+        need = need - 1
+    end
+end
+
 -- 步进匹配不足时: 按与己方积分差升序补满(真人与机器人一视同仁, 可超出步进窗口)
-local function fillMatchOpponentsFallback(selected, excludeTb, myScore, uuid2rank)
+local function fillMatchOpponentsFallback(selected, excludeTb, myScore)
     local need = BaiZhanChengShenDefine.BZCS_OPPONENT_CNT - #selected
     if need <= 0 then
         return
@@ -464,44 +504,29 @@ local function fillMatchOpponentsFallback(selected, excludeTb, myScore, uuid2ran
             break
         end
         excludeTb[c.uuid] = true
-        selected[#selected + 1] = makeMatchOpponentEntry(c.uuid, c.pinfo, uuid2rank)
+        selected[#selected + 1] = makeMatchOpponentEntry(c.uuid, c.pinfo)
         need = need - 1
     end
 end
 
--- 按积分±step*500 步进扩大(±500/±1000/±1500...); 不足3人时按积分差兜底补满
+-- 按积分±step*500 步进扩大(±500/±1000/±1500...); 每步窗口内随机抽选; 不足3人时按积分差兜底补满
 -- 返回 {rank,uuid,serverId,name,power,score,isRobot}[], rank 为全服积分榜名次(匹配标识)
 function GetMatchOpponents(myUuid, myScore, excludeTb)
     excludeTb = excludeTb or {}
     excludeTb[myUuid] = true
-    sortRankCache()
-    local uuid2rank = {}
-    for r, entry in ipairs(rankCache or {}) do
-        uuid2rank[entry.uuid] = r
-    end
+    ensureRankIndex()
     local selected = {}
+    local need = BaiZhanChengShenDefine.BZCS_OPPONENT_CNT
     local step = 1
-    while #selected < BaiZhanChengShenDefine.BZCS_OPPONENT_CNT and step <= BaiZhanChengShenDefine.BZCS_MATCH_MAX_STEP do
+    while need > 0 and step <= BaiZhanChengShenDefine.BZCS_MATCH_MAX_STEP do
         local minScore = myScore - step * BaiZhanChengShenDefine.BZCS_MATCH_STEP
         local maxScore = myScore + step * BaiZhanChengShenDefine.BZCS_MATCH_STEP
-        for uuid, pinfo in pairs(BzcsData.playerList or {}) do
-            if not excludeTb[uuid] then
-                local score = pinfo.score or BaiZhanChengShenDefine.BZCS_INIT_SCORE
-                if score >= minScore and score <= maxScore then
-                    local oppRank = uuid2rank[uuid] or 0
-                    if oppRank > 0 then
-                        excludeTb[uuid] = true
-                        selected[#selected + 1] = makeMatchOpponentEntry(uuid, pinfo, uuid2rank)
-                    end
-                    if #selected >= BaiZhanChengShenDefine.BZCS_OPPONENT_CNT then
-                        break
-                    end
-                end
-            end
-        end
+        local candidates = collectWindowCandidates(minScore, maxScore, excludeTb)
+        pickRandomFromCandidates(selected, excludeTb, candidates, need)
+        need = BaiZhanChengShenDefine.BZCS_OPPONENT_CNT - #selected
         step = step + 1
     end
-    fillMatchOpponentsFallback(selected, excludeTb, myScore, uuid2rank)
+    fillMatchOpponentsFallback(selected, excludeTb, myScore)
     table.sort(selected, function(a, b)
         return (a.rank or 0) < (b.rank or 0)
     end)
@@ -513,16 +538,12 @@ function GetMatchOpponentsByRanks(ranks)
     if not ranks or #ranks == 0 then
         return {}
     end
-    sortRankCache()
-    local uuid2rank = {}
-    for r, entry in ipairs(rankCache or {}) do
-        uuid2rank[entry.uuid] = r
-    end
+    ensureRankIndex()
     local ret = {}
     for _, rank in ipairs(ranks) do
         local pinfo = GetPlayerByRank(rank)
         if pinfo then
-            ret[#ret + 1] = makeMatchOpponentEntry(pinfo.uuid, pinfo, uuid2rank)
+            ret[#ret + 1] = makeMatchOpponentEntry(pinfo.uuid, pinfo)
         end
     end
     table.sort(ret, function(a, b)
@@ -564,7 +585,7 @@ end
 
 -- 周期发奖列表: 仅 firstJoinTime>0 且非机器人, 返回 {uuid, rank, serverId}
 function GetAllPlayersForReward()
-    sortRankCache()
+    ensureRankIndex()
     local ret = {}
     for rank, entry in ipairs(rankCache or {}) do
         if rank > BaiZhanChengShenDefine.BZCS_REWARD_RANK_MAX then

+ 11 - 8
script/module/baiZhanChengShen/BaiZhanChengShenDefine.lua

@@ -85,18 +85,21 @@ BZCS_WAR_TYPE_DEF_LOSE = 4        -- 被挑战, 己方失败
 BZCS_AWARD_MAIL_ID    = 7038      -- 周期结算邮件 id(需在 excel/mail 配置)
 
 -- 机器人池数量 = 策划 robotList 条数(见 GetRobotListCount)
-BZCS_SVR_BASE_NUM     = 810537    -- 协议 serverId 基准, 第几服 = serverId - 本值
-BZCS_ROBOT_DISPLAY_SERVER_ID = BZCS_SVR_BASE_NUM + 1  -- 机器人展示用区服值(810538=1服)
+BZCS_SVR_BASE_NUM     = 810537    -- 逻辑服 serverId 基准(Config.SVR_INDEX 等)
+BZCS_ROBOT_DISPLAY_SERVER_ID = 1  -- 协议下发: 机器人展示为第1服
 
--- 下发客户端的 serverId(机器人不写库, 临时映射为 BZCS_ROBOT_DISPLAY_SERVER_ID)
-function GetClientServerId(pinfo)
-    if not pinfo then
+-- 逻辑服 serverId -> GC 协议展示服号(仅 NS 下发客户端时调用); isRobot=1 固定为第1服
+function ToClientServerId(serverId, isRobot)
+    if isRobot == 1 then
+        return BZCS_ROBOT_DISPLAY_SERVER_ID
+    end
+    if not serverId or serverId <= 0 then
         return 0
     end
-    if pinfo.isRobot == 1 then
-        return BZCS_ROBOT_DISPLAY_SERVER_ID
+    if serverId > BZCS_SVR_BASE_NUM then
+        return serverId - BZCS_SVR_BASE_NUM
     end
-    return pinfo.serverId or 0
+    return serverId
 end
 
 --------------------------------------------------------------------------------

+ 74 - 17
script/module/baiZhanChengShen/BaiZhanChengShenNS.lua

@@ -16,7 +16,7 @@
 --
 -- 战斗:
 --   挑战一次依次走 CG_COMBAT_BEGIN(COMBAT_TYPE39~43); 仅首场校验次数/道具并请求跨服
---   扣次在 C2N_CanFight; onFightEnd 链式 nextCombatType 打满 5 族或 3 胜
+--   扣次在 C2N_CanFight; onFightEnd 链式 nextCombatType 打满 5 族或 3 胜, 整场结束发 fightReward
 --   机器人: combatBegin; 真人: MiddleCommonLogic_CombatBegin_LW
 --
 -- 文件结构:
@@ -275,7 +275,7 @@ local function sendMatchListGC(human, myScore, opponentList)
         net.body = opp.body or 0
         net.power = opp.power
         net.score = opp.score
-        net.serverId = opp.serverId or 0
+        net.serverId = BaiZhanChengShenDefine.ToClientServerId(opp.serverId, opp.isRobot)
     end
     Msg.send(msgRet, human.fd)
 end
@@ -463,31 +463,66 @@ local function getRankReward(rank)
     end
 end
 
+-- 道具列表转日志串 itemId:cnt,...
+local function formatAwardList(awardList)
+    if not awardList or #awardList < 1 then
+        return ""
+    end
+    local parts = {}
+    for i, item in ipairs(awardList) do
+        parts[i] = string.format("%s:%s", item[1] or 0, item[2] or 0)
+    end
+    return table.concat(parts, ",")
+end
+
 -- 整场挑战结束发放道具奖励(读 excel fightReward: 1=胜 2=负)
-local function grantFightReward(human, atkWin)
+-- combatInfo.rewardItem 供 GC_COMBAT_FINISH 展示, 参考 BattleLogic.onFightEnd
+local function grantFightReward(human, atkWin, combatInfo)
     if not human or not human.db then return end
     local rewardId = atkWin and BaiZhanChengShenDefine.BZCS_FIGHT_REWARD_WIN_ID
         or BaiZhanChengShenDefine.BZCS_FIGHT_REWARD_LOSE_ID
     local fightReward = BzcsConfig.fightReward
     local cfg = fightReward and fightReward[rewardId]
     if not cfg or not cfg.awardList or not next(cfg.awardList) then
-        BzcsLog.logAction("fight_reward_miss", string.format("uuid=%s rewardId=%s", human.db._id, rewardId))
+        BzcsLog.logAction("fight_reward_miss", string.format(
+            "uuid=%s atkWin=%s rewardId=%s", human.db._id, atkWin and 1 or 0, rewardId
+        ))
         return
     end
+    if combatInfo then
+        combatInfo.rewardItem = {}
+    end
     local awardList = {}
     for i, itemInfo in ipairs(cfg.awardList) do
-        awardList[i] = {itemInfo[1], itemInfo[2]}
+        local itemID = itemInfo[1]
+        local itemCnt = itemInfo[2]
+        awardList[i] = {itemID, itemCnt}
+        if combatInfo then
+            combatInfo.rewardItem[i] = {itemID, itemCnt}
+        end
+        BagLogic.addItem(human, itemID, itemCnt, LOGTAG)
     end
-    BagLogic.addItemList(human, awardList, LOGTAG)
     BzcsLog.logAction("fight_reward", string.format(
-        "uuid=%s rewardId=%s itemCnt=%s", human.db._id, rewardId, #awardList
+        "uuid=%s atkWin=%s rewardId=%s itemCnt=%s items=%s",
+        human.db._id, atkWin and 1 or 0, rewardId, #awardList, formatAwardList(awardList)
     ))
 end
 
 -- 发周期结算邮件(排名写入正文)
 local function sendMail(mailId, receiverUuid, itemArray, rank)
     local mailCfg = MailExcel.mail[mailId]
-    if not mailCfg then return end
+    if not mailCfg then
+        BzcsLog.logAction("reward_mail_miss", string.format(
+            "uuid=%s rank=%s mailId=%s", receiverUuid or "", rank or 0, mailId or 0
+        ))
+        return
+    end
+    if not itemArray or not next(itemArray) then
+        BzcsLog.logAction("reward_mail_empty", string.format(
+            "uuid=%s rank=%s mailId=%s", receiverUuid or "", rank or 0, mailId
+        ))
+        return
+    end
     local content = mailCfg.content
     if rank then
         if rank > BaiZhanChengShenDefine.BZCS_RANK_MAX then
@@ -496,6 +531,10 @@ local function sendMail(mailId, receiverUuid, itemArray, rank)
         content = Util.format(content, rank)
     end
     MailManager.add(MailManager.SYSTEM, receiverUuid, mailCfg.title, content, itemArray, mailCfg.senderName or "GM")
+    BzcsLog.logAction("reward_mail_ok", string.format(
+        "uuid=%s rank=%s mailId=%s itemCnt=%s items=%s",
+        receiverUuid, rank or 0, mailId, #itemArray, formatAwardList(itemArray)
+    ))
 end
 
 -- 批量发奖队列(分批发邮件, 失败重试)
@@ -506,13 +545,26 @@ local function createRewardQueue()
         local maxNum = math.min(self.insertMaxNum, #self.playerArray)
         for _ = 1, maxNum do
             local info = table.remove(self.playerArray)
-            local ok, err = pcall(sendMail, BaiZhanChengShenDefine.BZCS_AWARD_MAIL_ID, info[1], getRankReward(info[2]), info[2])
-            if not ok then
-                if not self.repeatTb[info[1]] or self.repeatTb[info[1]] < self.repeatMaxTimes then
-                    q:add(info)
-                    self.repeatTb[info[1]] = (self.repeatTb[info[1]] or 0) + 1
+            local uuid, rank = info[1], info[2]
+            local itemArray = getRankReward(rank)
+            if not itemArray then
+                BzcsLog.logAction("reward_cfg_miss", string.format("uuid=%s rank=%s", uuid or "", rank or 0))
+            else
+                local ok, err = pcall(sendMail, BaiZhanChengShenDefine.BZCS_AWARD_MAIL_ID, uuid, itemArray, rank)
+                if not ok then
+                    local retry = (self.repeatTb[uuid] or 0) + 1
+                    if retry <= self.repeatMaxTimes then
+                        self.repeatTb[uuid] = retry
+                        q:add(info)
+                        BzcsLog.logAction("reward_mail_fail", string.format(
+                            "uuid=%s rank=%s retry=%s err=%s", uuid, rank, retry, err
+                        ))
+                    else
+                        BzcsLog.logAction("reward_mail_giveup", string.format(
+                            "uuid=%s rank=%s retry=%s err=%s", uuid, rank, retry, err
+                        ))
+                    end
                 end
-                BzcsLog.logAction("reward_mail_fail", string.format("uuid=%s rank=%s err=%s", info[1], info[2], err))
             end
         end
         if #self.playerArray > 0 then
@@ -625,7 +677,6 @@ local function challengeFinish(human, cache)
         oppName = cache.defName or "",
         scoreChange = scoreChange,
     })
-    grantFightReward(human, atkWin)
 
     human.bzcs_Battle_Cache = nil
 end
@@ -745,7 +796,7 @@ function BZCS_WarReport(human)
         local r = list[total - j + 1]
         local net = msgRet.reportList[j]
         net.warType = r.warType or 0
-        net.oppServerId = r.oppServerId or 0
+        net.oppServerId = BaiZhanChengShenDefine.ToClientServerId(r.oppServerId, 0)
         net.oppName = r.oppName or ""
         net.scoreChange = r.scoreChange or 0
     end
@@ -840,7 +891,7 @@ local function fillBzcsRankNet(net, info)
     net.headFrame = info.headFrame or 0
     net.power = info.power or 0
     net.score = info.score or BaiZhanChengShenDefine.BZCS_INIT_SCORE
-    net.serverId = info.serverId or 0
+    net.serverId = BaiZhanChengShenDefine.ToClientServerId(info.serverId, info.isRobot or 0)
     net.uuid = info.uuid or ""
 end
 
@@ -923,6 +974,7 @@ function C2N_IssueReward(msg)
     if not rewardList or #rewardList == 0 then
         return
     end
+    BzcsLog.logAction("reward_issue_ns", string.format("cnt=%s svr=%s", #rewardList, Config.SVR_INDEX))
     local q = createRewardQueue()
     for _, info in ipairs(rewardList) do
         q:add({info[1], info[2]})
@@ -1035,6 +1087,8 @@ function onFightEnd(human, result, combatType, cbParam, combatInfo)
         cache.defW = cache.defW + 1
     end
 
+    combatInfo.defender.name = cache.defName
+
     local seriesOver = cache.atkW >= BaiZhanChengShenDefine.BZCS_WIN_TARGET
         or cache.defW >= BaiZhanChengShenDefine.BZCS_WIN_TARGET
         or cache.raceIdx >= BaiZhanChengShenDefine.BZCS_RACE_CNT
@@ -1045,6 +1099,9 @@ function onFightEnd(human, result, combatType, cbParam, combatInfo)
         return
     end
 
+    -- 整场结束: 发挑战奖励并写入 combatInfo.rewardItem(GC_COMBAT_FINISH 展示)
+    local atkWin = cache.atkW >= BaiZhanChengShenDefine.BZCS_WIN_TARGET
+    grantFightReward(human, atkWin, combatInfo)
     challengeFinish(human, cache)
 end
 

+ 3 - 3
script/module/baiZhanChengShen/Proto.lua

@@ -40,7 +40,7 @@ BZCS_OPPONENT_BRIEF = {
     {"body",            1,      "int"},    -- 形象 body(同 RoleHeadLogic.HEAD_TYPE_3)
     {"power",           1,      "double"}, -- 对手总战力(五族之和)
     {"score",           1,      "int"},    -- 对手当前积分
-    {"serverId",        1,      "int"},    -- 区服值, 减 BZCS_SVR_BASE_NUM(810537) 为第几服
+    {"serverId",        1,      "int"},    -- 第几服(NS 下发前已减 BZCS_SVR_BASE_NUM)
 }
 
 -- 排行榜单条记录(榜单条目与 myRankInfo 共用)
@@ -51,14 +51,14 @@ BZCS_RANK_INFO = {
     {"headFrame",       1,      "int"},    -- 头像框
     {"power",           1,      "double"}, -- 总战力
     {"score",           1,      "int"},    -- 积分
-    {"serverId",        1,      "int"},    -- 区服值, 减 BZCS_SVR_BASE_NUM(810537) 为第几服
+    {"serverId",        1,      "int"},    -- 第几服(NS 下发前已减 BZCS_SVR_BASE_NUM)
     {"uuid",            1,      "string"}, -- 玩家 uuid
 }
 
 -- 本地战报单条(存 warReport 尾插最多 20 条, GC 下发时新记录在前)
 BZCS_WAR_REPORT_INFO = {
     {"warType",         1,      "byte"},   -- 1主动胜 2主动负 3被挑战胜 4被挑战负(已含胜负)
-    {"oppServerId",     1,      "int"},    -- 对手区服值, 减 BZCS_SVR_BASE_NUM 为第几服
+    {"oppServerId",     1,      "int"},    -- 对手第几服(NS 下发前已减 BZCS_SVR_BASE_NUM)
     {"oppName",         1,      "string"}, -- 对手昵称
     {"scoreChange",     1,      "short"},  -- 本场积分变化(+100/-50/+50/-50)
 }

+ 299 - 0
script/module/baiZhanChengShen/百战成神.md

@@ -0,0 +1,299 @@
+# 百战成神
+
+跨服五族积分 PvP:玩家为妖、人、兽、仙、魔五个种族各配置独立阵容,向积分榜上的对手发起 **5 局 3 胜** 的系列战,按整场胜负结算积分;周期结束按全服排名发奖。
+
+---
+
+## 1. 玩法概要
+
+| 项目 | 规则 |
+|------|------|
+| 战斗形式 | 五族独立阵容,对应战斗类型 `COMBAT_TYPE39~43` |
+| 系列赛制 | 最多打满 5 族;先赢 **3** 族者赢得整场 |
+| 积分 | 初始 **3000**;进攻胜 +20 / 负 -10;防守胜 +10 / 负 -20 |
+| 对手 | 全服积分榜(含机器人),单次展示 **3** 名匹配对手 |
+| 次数 | 每日 **5** 次免费;用尽后每次消耗道具 **115 × 5** |
+| 架构 | 普通服 **NS** + 跨服 **CS** + Mongo **`db_bzcs`** |
+
+---
+
+## 2. 活动时间
+
+### 2.1 开放周期
+
+- **可挑战星期**:周六、周日(`getWeekDay`:7=周六,1=周日)
+- **每日时段**:0:10 ~ 23:00(`BZCS_START_SEC=600`,`BZCS_END_SEC=82800`)
+- **单轮跨度**:从周六 0:10 对齐开局,至周日 23:00 结束(持续约 2 天)
+- **轮次间隔**:距上次开轮满 **21 天**(3 周)且处于开放日,可开启新轮
+
+跨服 `timedStageHandle`(每分钟/每小时)负责:
+
+1. 满足新轮条件(首次开轮,或满 21 天且在开放日)→ 开新轮;若上轮仍未发奖则**放弃发奖**(清 `pendingRewards`、标记已发奖、`ActEnd`)后直接开轮  
+2. 已过 `endTime` 且未发奖且未满 21 天新轮 → 结算发奖并广播活动结束  
+3. 记录中活动应进行中但 `isRunning` 为假 → 修正时间并重新 `ActOpen`
+
+### 2.2 参与条件
+
+- 本服开服天数 ≥ **45**(`BZCS_OPEN_SVR_DAY`)
+- 跨服已广播本轮 `startTime`(`CommonDB.KEY_BZCS_START_TIME`)
+- 当前星期、时刻在开放窗口内(`actStartTimeCheck` / `IsRunning`)
+
+### 2.3 活动状态(客户端)
+
+- `getActState`:`0` 未开启,`2` 进行中,并返回 `leftSec`
+- `isActRed`:可参与且仍有免费次数
+
+---
+
+## 3. 积分与排行
+
+### 3.1 积分变化(整场结束后结算)
+
+| 角色 | 整场结果 | 积分变化 |
+|------|----------|----------|
+| 进攻方 | 胜 | +20 |
+| 进攻方 | 负 | -10 |
+| 防守方 | 胜(攻方负) | +10 |
+| 防守方 | 负(攻方胜) | -20 |
+
+- 新周期开始时:真人重置为 **3000**,`firstJoinTime` 清零;机器人按策划表 `robotList[].score` 恢复  
+- 同分排序:**积分高优先** → **先达到该积分者优先**(`scoreTime` 更小)→ `uuid` 字典序
+
+### 3.2 排行榜
+
+- 客户端展示前 **100** 名(`BZCS_RANK_MAX`)
+- 匹配与名次标识使用 **全服积分榜**(含机器人,规模大于展示榜)
+- 周期发奖排名上限 **9999**(`BZCS_REWARD_RANK_MAX`)
+
+---
+
+## 4. 匹配规则
+
+1. 排除自己,在积分榜中按积分 **±100 × step** 步进扩大(`step` 1~50,最大约 ±5000)  
+2. 每步在分数窗口内随机选取对手,直至满 **3** 人  
+3. 仍不足时,按与己方 **积分差升序** 兜底补满(可超出 ±5000 窗口)  
+4. 结果按对手 **全服名次** 升序排序  
+
+**本地缓存**(普通服):
+
+- 积分未变且已有 `matchList` 时,`CG_BZCS_MATCH_LIST` 可直接返回缓存  
+- 积分变化、刷新对手、战斗结束后会清空缓存,需重新拉取  
+
+**说明**:打开匹配界面与刷新对手均不写 `match` 业务日志。
+
+---
+
+## 5. 战斗流程
+
+### 5.1 阵容要求
+
+- 五个种族各对应一种 `COMBAT_TYPE`(39~43),上阵英雄 **camp** 必须与该族一致  
+- 挑战前校验五族均已至少上阵 1 名英雄  
+
+### 5.2 发起挑战
+
+```mermaid
+sequenceDiagram
+    participant C as 客户端
+    participant NS as 普通服 NS
+    participant CS as 跨服 CS
+
+    C->>NS: CG_COMBAT_BEGIN(TYPE39, param=rank|expectUuid)
+    NS->>NS: 校验次数/阵容/活动
+    NS->>CS: LW_BZCS_CAN_FIGHT
+    CS->>NS: WL_BZCS_CAN_FIGHT
+    NS->>NS: 扣次,创建 bzcs_Battle_Cache
+    NS->>NS: 开打第1族
+    loop 第2~5族
+        C->>NS: CG_COMBAT_BEGIN(TYPE40~43)
+        NS->>NS: 沿用 Cache,不扣次
+    end
+    NS->>CS: LW_BZCS_FIGHT_END
+    CS->>CS: 双方 UpdateScore
+    CS->>NS: WL_BZCS_FIGHT_END / WL_BZCS_DEF_NOTIFY
+```
+
+- **仅首场**(`COMBAT_TYPE39`)请求跨服校验并 **扣除 1 次** 挑战次数  
+- **第 2~5 场** 仅本地 `bzcs_Battle_Cache` 链式开战,不再扣次  
+- **机器人**:防守方在普通服本地 `combatBegin`,`param={defUuid, raceIdx}`  
+- **真人**:防守方走 `MiddleCommonLogic_CombatBegin_LW` 到对方逻辑服取阵容  
+
+### 5.3 胜负判定
+
+- 单族:战斗胜利计攻方 1 胜,否则守方 1 胜  
+- 整场:`atkW` 或 `defW` ≥ **3** 时结束;或打满 5 族后结束  
+- 整场结束后:`LW_BZCS_FIGHT_END` 改分;攻方 **首次完成挑战** 时 `LW_BZCS_REGISTER`(每轮一次)
+
+---
+
+## 6. 机器人
+
+### 6.1 配置表
+
+策划表:`excel/ssecy/baiZhanChengShen.lua` → **`robotList`**
+
+| 字段 | 说明 |
+|------|------|
+| `score` | 该机器人在积分榜上的积分 |
+| `monsterOutIDs[1..5]` | 五族 `monsterOutID`,顺序:**妖、人、兽、仙、魔** |
+
+- 机器人数量 = `robotList` 条数(当前 **101** 条)  
+- 跨服 uuid:`bzcs_robot_N` 与 `robotList[N]` **一一对应**  
+- 展示区服:固定为 `810538`(`BZCS_ROBOT_DISPLAY_SERVER_ID`,即「1 服」展示用)
+
+### 6.2 队伍与战力
+
+- 每个 `monsterOutID` 对应 `monster.lua` 中一条 `monsterOut`,战斗时由 `CombatLogic.getMonsterObjList()` 展开为上阵怪物列表  
+- 跨服仅存 `monsterOutID` + `racePower`(由 `monsterOut` 算出);客户端展示经 `ExpandBzcsRaceShow` 展开英雄模型  
+- 生成时随机 `name` / `head` / `headFrame` / `body`
+
+### 6.3 战斗
+
+- `getCombatMonsterOutID`:按 `defUuid` + `raceIdx` 取 `robotList[N].monsterOutIDs[raceIdx]`
+
+---
+
+## 7. 玩家展示数据(跨服 showInfo)
+
+注册(`REGISTER`)时全量写入:
+
+- `name`、`head`、`headFrame`、`body`、`heroArr[1..5]`(五族阵容展示)
+
+增量同步(`LW_BZCS_UPDATE_SHOW`,须已 `crossRegistered`):
+
+| updateType | 含义 |
+|------------|------|
+| 1 | 昵称 |
+| 3 | 头像 |
+| 4 | 头像框 |
+| 5 | 单族阵容(需 `race` 1~5) |
+| 6 | 形象 body |
+
+触发示例:改头像/框/形象/上阵 → `RoleHeadLogic` 或 `onCombatPosUpdate` 调用 `UpdateShowInfo`。
+
+---
+
+## 8. 周期结算奖励
+
+### 8.1 策划表 `rankReward`
+
+| 名次区间 | 示例奖励(道具 id, 数量) |
+|----------|---------------------------|
+| 1 | 7056×1, 186×10000, 102×2000, 1026×10 |
+| 2 | 7057×1, 186×9000, … |
+| 3 | 7058×1, … |
+| 4~5 | 186×7000, … |
+| 6~10 | … |
+| 11~20 | … |
+| 21~50 | … |
+| 51~100 | … |
+| 101~9999 | 186×2000, 102×500, 118×10 |
+
+### 8.2 发奖条件
+
+- 仅 **`firstJoinTime > 0`** 的真人参与发奖(本轮至少完成过一次挑战注册)  
+- 机器人不参与发奖、不参与 `serverList` 路由  
+- 邮件 id:**7038**(`BZCS_AWARD_MAIL_ID`),正文带名次  
+- 跨服按逻辑服批量 `WL_BZCS_ISSUE_REWARD`;断连时写入 `pendingRewards`,重连补发  
+
+---
+
+## 9. 客户端协议(节选)
+
+| 协议 | 说明 |
+|------|------|
+| `CG_BZCS_MATCH_LIST` / `GC_BZCS_MATCH_LIST` | 匹配主界面:己方积分、免费次数、门票消耗、3 名对手(含 `body`) |
+| `CG_BZCS_MATCH_REFRESH` | 强制刷新对手(回包同 MATCH_LIST) |
+| `CG_BZCS_RANK_LIST` / `GC_BZCS_RANK_LIST` | 排行榜前 100 |
+| `CG_BZCS_OPPONENT_INFO` | 按全服 `rank` 查对手头像/战力/积分 |
+| `CG_BZCS_OPPONENT_LINEUP` | 按 `rank` 查五族阵容展示 |
+| `CG_BZCS_MY_LINEUP` | 己方五族阵容(本地) |
+| `CG_BZCS_WAR_REPORT` | 本地战报最多 20 条 |
+| `CG_BZCS_RANK_REWARD` | 排名奖励预览 |
+| `CG_COMBAT_BEGIN` | 挑战入口,`combatType=39~43`,首场 `param=rank\|expectUuid` |
+
+**区服显示**:`serverId - 810537` = 第几服;机器人为固定展示服。
+
+---
+
+## 10. 数据存储
+
+### 10.1 跨服 Mongo `db_bzcs`
+
+```
+BzcsData = {
+    activityStartTime, activityEndTime, lastResetTime, rewardIssued,
+    playerList[uuid] = {
+        uuid, serverId, score, scoreTime, isRobot, firstJoinTime,
+        showInfo = { name, head, headFrame, body, heroArr[race] }
+    },
+    serverList[serverId] = { uuid, ... },   -- 仅真人,发奖路由
+    pendingRewards[serverId] = { {uuid, rank}, ... }
+}
+```
+
+### 10.2 普通服 `human.db.baiZhanChengShen`
+
+| 字段 | 说明 |
+|------|------|
+| `freeTimes` | 当日剩余免费次数(0 点重置为 5) |
+| `actStartTime` | 本轮开始时间(与跨服同步) |
+| `crossRegistered` | 本轮是否已向跨服注册 |
+| `matchList` / `matchScore` | 匹配缓存 |
+| `lastScore` | 上次已知积分 |
+| `warReport` | 本地战报,最多 20 条 |
+
+---
+
+## 11. 代码结构
+
+| 文件 | 职责 |
+|------|------|
+| `BaiZhanChengShenDefine.lua` | 常量、排行/机器人工具、`MergeShowInfo`、`ExpandBzcsRaceShow` |
+| `BaiZhanChengShenNS.lua` | 普通服逻辑、协议、战斗钩子、`UpdateShowInfo` |
+| `BaiZhanChengShenCS.lua` | 跨服活动周期、LW 处理、发奖调度 |
+| `BaiZhanChengShenDB.lua` | 跨服数据、匹配、积分、机器人池 |
+| `BaiZhanChengShenLog.lua` | 业务日志封装 |
+| `Proto.lua` | 客户端协议结构 |
+| `Handler.lua` | CG 入口 |
+| `excel/ssecy/baiZhanChengShen.lua` | `rankReward`、`robotList` |
+
+---
+
+## 12. 业务日志
+
+- 日志文件:**`log/oss_bzcs`**(`Log.LOGID_OSS_BZCS`)  
+- 格式:`[action] key=value ...`  
+
+| action | 场景 |
+|--------|------|
+| `act_open` / `act_end` | 开轮 / 结束发奖 |
+| `round_reset` | 新周期重置 DB |
+| `robot_gen` / `db_init` | 生成机器人 / 跨服加载 |
+| `register` / `register_send` | 跨服注册 |
+| `update_show` / `update_show_send` | 展示增量 |
+| `score` | 积分变更 |
+| `fight_end` / `fight_end_send` / `fight_end_ns` | 整场结算 |
+| `can_fight` | 开战扣次 |
+| `reward_issue` / `reward_pending` / `reward_reissue` / `reward_mail_fail` | 发奖 |
+| `err_tips` | 跨服业务错误 |
+| `fight_end_middle_down` / `fight_end_wl_fail` | 跨服断连或 WL 失败 |
+
+**不写日志**:匹配列表拉取与刷新对手。
+
+---
+
+## 13. 跨服错误码(WL_BZCS_TIPS)
+
+| errCode | 含义 |
+|---------|------|
+| 1 | 活动未开启 |
+| 2 | 不满足参与条件 |
+| 3 | 挑战次数不足 |
+| 4 | 对手无效 |
+| 5 | 数据异常 |
+| 6 | 战斗中 |
+
+---
+
+*文档依据当前服务端实现整理;策划数值以 `excel/ssecy/baiZhanChengShen.lua` 为准。*

+ 3 - 0
script/module/combat/BeSkill.lua

@@ -1291,6 +1291,9 @@ function onDelBingDongBuffer(obj, buffer)
 	--local conf = BufferExcel[buffer.id]
 	local conf = CombatBuff.GetBuffConfig(buffer.id)
 	local attacker = CombatImpl.objList[buffer.attackPos]
+	if not attacker then
+		return
+	end
     if conf.cmd == "bingdong" then
         for _, pos in ipairs(CombatDefine.SIDE2POS[attacker.side]) do
 			local target = CombatImpl.objList[pos]