From 9ec66731c8a551035958aebe1fa974a140b99cf1 Mon Sep 17 00:00:00 2001
From: hxp <ale99527@vip.qq.com>
Date: 星期三, 02 七月 2025 17:34:10 +0800
Subject: [PATCH] 129 【战斗】战斗系统-服务端(初版战斗,支持基础的三维属性战斗,支持简单的普攻技能、怒气技能、回血技能;主线章节关卡过关支持;阵容保存支持多阵容;)

---
 ServerPython/ZoneServerGroup/map1_8G/MapServer/MapServerData/Script/Attack/TurnAttack.py | 1905 +++++++++++++++++++++++++++++++++++++++++++++++++++++++----
 1 files changed, 1,771 insertions(+), 134 deletions(-)

diff --git a/ServerPython/ZoneServerGroup/map1_8G/MapServer/MapServerData/Script/Attack/TurnAttack.py b/ServerPython/ZoneServerGroup/map1_8G/MapServer/MapServerData/Script/Attack/TurnAttack.py
index 3e17b07..f0c06e5 100644
--- a/ServerPython/ZoneServerGroup/map1_8G/MapServer/MapServerData/Script/Attack/TurnAttack.py
+++ b/ServerPython/ZoneServerGroup/map1_8G/MapServer/MapServerData/Script/Attack/TurnAttack.py
@@ -10,6 +10,10 @@
 # @version 1.0
 #
 # 详细描述: 回合制攻击逻辑,均使用NPC实例作为战斗主体
+#        实时战斗:  仅用于主线,每个玩家最多仅允许存在一个实时战斗
+#                    由前端控制战斗流程,后端进行计算
+#        瞬时战斗:  用于非主线外的各种战斗,如副本PVE挑战,PVP挑战等,一样仅有一个瞬时战斗,
+#                    一次性处理完,后端控制整体战斗流程及计算,前端仅做战报解析表现战斗过程
 #
 #-------------------------------------------------------------------------------
 #"""Version = 2023-11-30 15:30"""
@@ -22,7 +26,23 @@
 import GameWorld
 import GameObj
 import FBCommon
-import FBLogic
+import IpyGameDataPY
+import PlayerOnline
+import NPCCommon
+import ShareDefine
+import PyGameData
+import IPY_GameWorld
+import ItemControler
+import SkillCommon
+import SkillShell
+import BaseAttack
+
+import random
+import time
+import json
+
+PosNumMax = 10 # 最大站位编号
+ActionNumStart = -1 # 起始行动位置编号,一般是从1开始,如果有加主公、红颜等则扣除相应位置值,如从0或-1开始
 
 # 回合战斗流程状态
 (
@@ -34,169 +54,1786 @@
 FightState_Over, # 5 结束状态,无特殊意义,仅代表所有处理结束了,与Start对应
 ) = range(6)
 
+Def_FactionA = 1
+Def_FactionB = 2
+
+class BatLineup():
+    ## 战斗阵容
+    
+    def __init__(self, faction, num, fightMgr):
+        self.fightMgr = fightMgr
+        self.faction = faction # 所属阵营
+        self.num = num # 该阵容所在阵营中的编号,不同阵营可重复,多V多
+        self.ownerID = 0 # 阵容所属玩家ID,可能为0,0代表非玩家阵容
+        self.npcPosDict = {} # 阵容NPC {站位编号:curNPC, ...}, 站位编号小于0为非主战单位,如主公、红颜等
+        self.npcObjIDDict = {} # NPC实例ID字典 {objID:curNPC, ...}
+        self.shapeType = 0 # 阵型
+        self.actionNum = ActionNumStart # 行动位置,从1开始
+        self.lordAttrDict = {} # 主公属性
+        self.npcAttrDict = {} # 阵容NPC属性 {npcID:{attrID:value, ...}, ...}
+        self.fightPower = 0 # 阵容总战力
+        
+        # 战斗统计
+        self.hurtStatDict = {} # 输出统计 {objID:totalValue, ...}
+        self.defStatDict = {} # 承伤统计 {objID:totalValue, ...}
+        self.cureStatDict = {} # 治疗统计 {objID:totalValue, ...}
+        return
+    
+    def getGUID(self): return self.fightMgr.guid
+    def getPlayerID(self): return self.fightMgr.playerID # 发起的玩家ID
+    
+    def isEmpty(self): return not self.npcPosDict
+    
+    def setLineup(self, lineupInfo):
+        ## 设置阵容
+        # @param lineupInfo: 阵容信息
+        self.clearLineup()
+        self.ownerID = lineupInfo.get("PlayerID", 0) # 阵容所属的玩家ID
+        self.shapeType = lineupInfo.get("Shape", 0)
+        self.lordAttrDict = lineupInfo.get("LordAttrDict", {})
+        SummonLineupObjs(self, self.faction, self.num, lineupInfo, self.getPlayerID())
+        return
+    
+    def refreshFightPower(self):
+        ## 刷新阵容总战力
+        if self.fightPower:
+            return self.fightPower
+        
+        return self.fightPower
+    
+    def __resetStat(self):
+        ## 重置战斗统计
+        self.actionNum = ActionNumStart
+        self.hurtStatDict = {}
+        self.defStatDict = {}
+        self.cureStatDict = {}
+        return
+    
+    def __resetAttrState(self, isReborn=True):
+        ## 重置属性状态
+        # @param isReborn: 是否复活
+        initXP = IpyGameDataPY.GetFuncCfg("AngerXP", 1)
+        for curNPC in self.npcPosDict.values():
+            if not curNPC:
+                continue
+            if GameObj.GetHP(curNPC) <= 0 and not isReborn:
+                continue
+            # 回满血,清除buff
+            GameObj.SetHPFull(curNPC, False)
+            # 重置怒气
+            GameObj.SetXP(curNPC, initXP, False)
+            # ...
+            
+        return
+    
+    def resetLineup(self, isReborn=True):
+        ## 重置阵容
+        self.__resetAttrState(isReborn)
+        self.__resetStat()
+        return
+    
+    def clearLineup(self):
+        ## 清除阵容
+        if not self.npcPosDict:
+            return
+        for curNPC in self.npcPosDict.values():
+            if not curNPC:
+                continue
+            NPCCommon.SetDeadEx(curNPC)
+        self.npcPosDict = {}
+        self.npcObjIDDict = {}
+        self.npcAttrDict = {}
+        self.fightPower = 0
+        self.__resetStat()
+        return
+    
+    def statHurtValue(self, objID, hurtValue):
+        ## 统计输出
+        if objID not in self.npcObjIDDict:
+            return
+        updValue = self.hurtStatDict.get(objID, 0) + hurtValue
+        self.hurtStatDict[objID] = updValue
+        return updValue
+    
+    def statDefValue(self, objID, lostHP):
+        ## 统计承伤
+        if objID not in self.npcObjIDDict:
+            return
+        updValue = self.defStatDict.get(objID, 0) + lostHP
+        self.defStatDict[objID] = updValue
+        return updValue
+    
+    def statCureValue(self, objID, cureValue):
+        ## 统计治疗
+        if objID not in self.npcObjIDDict:
+            return
+        updValue = self.cureStatDict.get(objID, 0) + cureValue
+        self.cureStatDict[objID] = updValue
+        return updValue
+    
+class BatFaction():
+    ## 战斗阵营
+    
+    def __init__(self, faction, fightMgr):
+        self.fightMgr = fightMgr
+        self.faction = faction
+        self.lineupDict = {} # 该阵营所有阵容信息 {编号:BatLineup, ...}
+        return
+    
+    def getBatlineup(self, num=1):
+        ## 获取战斗阵容
+        lineup = None
+        if num in self.lineupDict:
+            lineup = self.lineupDict[num]
+        else:
+            lineup = BatLineup(self.faction, num, self.fightMgr)
+            self.lineupDict[num] = lineup
+        return lineup
+    
+    def resetLineups(self):
+        ## 重置所有战斗阵容
+        for lineup in self.lineupDict.values():
+            lineup.resetLineup()
+        return
+    
+    def clearLineups(self):
+        ## 清除所有战斗阵容
+        for lineup in self.lineupDict.values():
+            lineup.clearLineup()
+        return
+    
+class TurnFight():
+    '''某场回合战斗,支持多V多,所有战斗通用,主线、爬塔、PVP等
+    可能为系统后台发起的,则攻击方、防守方阵容以玩家所保存的阵容镜像进行战斗
+    如果玩家发起的,则攻击方为发起方玩家,目标为防守方,支持多V多,如组队PK
+    多V多的情况也是某个队员各自发起战斗,只是用到队友的镜像阵容一起战斗,队友无感知
+    
+    多V多可使用两种模式:
+    1. 多个阵容一起出场,在一场战斗中完成
+    2. 以单阵容方式轮流出场战斗,该方式同样可以使用方式1的逻辑实现,固优先使用方式1战斗逻辑,方便扩展
+    '''
+    
+    def __init__(self, mapID=0, funcLineID=0, playerID=0, isNeedReport=False):
+        self.guid = GameWorld.GetGUID() # 某场战斗的唯一guid,可用于存储记录如战报等
+        self.playerID = playerID # 可能为0,系统后台自动处理的战斗时为0,某个玩家发起则为发起玩家ID,同个玩家ID可能同时存在多场战斗,如主线+其他
+        self.curPlayer = GameWorld.GetPlayerManager().FindPlayerByID(playerID) if playerID else None
+        self.mapID = mapID
+        self.funcLineID = funcLineID
+        self.turnNum = 1 # 当前第x回合,默认第1回合开始
+        self.turnMax = 15 # 最大回合数
+        self.enterLogic = False # 是否已执行进场逻辑
+        self.turnStart = 0 # 已执行回合开始值,如第1回合开始已执行则为1,第2回合为2
+        self.turnEnd = 0 # 已执行回合结束值,如第1回合结束已执行则为1,第2回合为2
+        self.winFaction = 0 # 本场战斗结束标记,获胜阵营,为0时代表未结束,所有小队打完或失败才有结果,0-未结束,>0-获胜的阵营
+        self.batBuffer = "" # 战报buffer,战报暂时只保留最后一个小队的
+        self.isNeedReport = isNeedReport # 是否需要战报
+        self.msgDict = {} # 扩展信息字典,一般由MapID绑定的功能决定信息内容  {k:v, ...}
+        
+        self.factionDict = {} # 战斗阵营 {faction:BatFaction, ...},一般是只有两个阵营,faction为1或2,每个阵营支持多个阵容
+        self.actionSortList = [] # 阵容行动顺序 [[faction, num], ...]
+        self.actionIndex = 0 # 行动顺序索引
+        
+        self.startTime = 0 # 开始时间戳,支持毫秒小数
+        self.costTime = 0 # 单场战斗总耗时,支持毫秒小数
+        return
+    
+    def setTurn(self, mapID, funcLineID, turnMax, isNeedReport=False, msgDict={}):
+        ## 设置本场回合战斗设定
+        self.mapID = mapID
+        self.funcLineID = funcLineID
+        self.turnMax = turnMax # 最大回合数
+        self.isNeedReport = isNeedReport
+        self.msgDict = {}
+        self.resetTurn(msgDict)
+        return
+    
+    def resetTurn(self, msgDict):
+        ## 一般用于玩家发起的战斗,在需要保留玩家阵容属性及状态的情况下,重置回合进入下一场战斗
+        self.turnNum = 1
+        self.enterLogic = False
+        self.turnStart = 0
+        self.turnEnd = 0
+        self.winFaction = 0
+        self.batBuffer = "" # 战报buffer
+        self.msgDict.update(msgDict)
+        self.startTime = time.time()
+        self.costTime = 0
+        return
+    
+    def setFactionLineup(self, faction, lineupDict, onlyReset=False):
+        ## 设置阵营阵容
+        # @param lineupDict: {阵容编号:阵容信息, ...} 每个阵营支持多个阵容,即支持多V多
+        # @param onlyReset: 是否仅重置,一般用于玩家的阵容,重复利用阵容实例,
+        batFaction = self.getBatFaction(faction)
+        if onlyReset:
+            batFaction.resetLineups()
+        else:
+            batFaction.clearLineups()
+        for num, lineupInfo in lineupDict.items():
+            batLineup = batFaction.getBatlineup(num)
+            if onlyReset and not batLineup.isEmpty():
+                continue
+            if not lineupInfo:
+                continue
+            batLineup.setLineup(lineupInfo)
+            batLineup.refreshFightPower()
+        return
+    
+    def sortActionQueue(self):
+        ## 刷新出手顺序队列
+        sortList = []
+        for batFaction in self.factionDict.values():
+            faction = batFaction.faction
+            for num, batLineup in batFaction.lineupDict.items():
+                fightPower = batLineup.refreshFightPower()
+                sortValue = -(faction * 10 + num)
+                sortList.append([fightPower, sortValue, faction, num])
+        sortList.sort(reverse=True) # 战力高的先手
+        
+        self.actionIndex = 0
+        self.actionSortList = []
+        for _, _, faction, num in sortList:
+            self.actionSortList.append([faction, num])
+            
+        GameWorld.DebugLog("阵容战力排序[fp, sortV, f, n]: %s" % sortList)
+        GameWorld.DebugLog("阵容行动顺序[f, n]: %s" % self.actionSortList)
+        return
+    
+    def getBatFaction(self, faction=Def_FactionA):
+        ## 默认阵营1
+        batFaction = None
+        if faction in self.factionDict:
+            batFaction = self.factionDict[faction]
+        else:
+            batFaction = BatFaction(faction, self)
+            self.factionDict[faction] = batFaction
+        return batFaction
+    
+    def checkOverByKilled(self):
+        ##检查是否击杀结束
+        # @return: 0-无获胜阵营,同时也代表战斗未结束;>0-获胜的阵营ID,同时也代表战斗已全部结束
+        
+        if self.winFaction:
+            return self.winFaction
+        
+        for faction, batFaction in self.factionDict.items():
+            allKilled = True
+            for batLineup in batFaction.lineupDict.values():
+                if not allKilled:
+                    break
+                for posNum, curNPC in batLineup.npcPosDict.items():
+                    if posNum <= 0:
+                        # 非主战位置不判断
+                        continue
+                    if GameObj.GetHP(curNPC) > 0:
+                        allKilled = False
+                        break
+                    
+            if allKilled:
+                self.winFaction = Def_FactionA if faction == Def_FactionB else Def_FactionA
+                DoTurnFightOver(self.guid)
+                return self.winFaction
+            
+        return 0
+    
+    def clearBatFaction(self, faction):
+        batFaction = self.getBatFaction(faction)
+        batFaction.clearLineups()
+        return
+    
+    def clearFight(self):
+        self.syncState(FightState_Over)
+        for batFaction in self.factionDict.values():
+            batFaction.clearLineups()
+        return
+    
+    def syncInit(self):
+        ## 初始化通知
+        msg = json.dumps(self.msgDict, ensure_ascii=False)
+        msg = msg.replace(" ", "")
+        clientPack = ChPyNetSendPack.tagSCTurnFightInit()
+        clientPack.MapID = self.mapID
+        clientPack.FuncLineID = self.funcLineID
+        clientPack.TurnMax = self.turnMax
+        clientPack.Msg = msg
+        clientPack.Len = len(clientPack.Msg)
+        clientPack.FactionList = []
+        for faction in self.factionDict.keys():
+            batFaction = self.getBatFaction(faction)
+            tfFaction = ChPyNetSendPack.tagSCTurnFightFaction()
+            tfFaction.Faction = faction
+            tfFaction.LineupList = []
+            for num in batFaction.lineupDict.keys():
+                batLineup = batFaction.getBatlineup(num)
+                tfLineup = ChPyNetSendPack.tagSCTurnFightLineup()
+                tfLineup.Num = num
+                tfLineup.OwnerID = batLineup.ownerID
+                tfLineup.ShapeType = batLineup.shapeType
+                tfLineup.ObjList = []
+                for posNum, curNPC in batLineup.npcPosDict.items():
+                    tfObj = ChPyNetSendPack.tagSCTurnFightObj()
+                    tfObj.ObjID = curNPC.GetID()
+                    tfObj.NPCID = curNPC.GetNPCID()
+                    tfObj.HP = curNPC.GetHP()
+                    tfObj.HPEx = curNPC.GetHPEx()
+                    tfObj.MaxHP = curNPC.GetMaxHP()
+                    tfObj.MaxHPEx = curNPC.GetMaxHPEx()
+                    tfObj.LV = NPCCommon.GetNPCLV(curNPC)
+                    tfObj.PosNum = posNum
+                    tfObj.AngreXP = GameObj.GetXP(curNPC)
+                    tfLineup.ObjList.append(tfObj)
+                tfLineup.ObjCnt = len(tfLineup.ObjList)
+                tfFaction.LineupList.append(tfLineup)
+            tfFaction.LineupCnt = len(tfFaction.LineupList)
+            clientPack.FactionList.append(tfFaction)
+        clientPack.FactionCnt = len(clientPack.FactionList)
+        self.addBatPack(clientPack)
+        return
+    
+    def syncState(self, state, msgDict={}):
+        msg = json.dumps(msgDict, ensure_ascii=False)
+        msg = msg.replace(" ", "")
+        clientPack = ChPyNetSendPack.tagMCTurnFightState()
+        clientPack.Clear()
+        clientPack.MapID = self.mapID
+        clientPack.FuncLineID = self.funcLineID
+        clientPack.State = state
+        clientPack.TurnNum = self.turnNum
+        clientPack.Msg = msg
+        clientPack.Len = len(clientPack.Msg)
+        self.addBatPack(clientPack)
+        return
+    
+    def syncObjAction(self, turnNum, objType, objID):
+        clientPack = ChPyNetSendPack.tagMCTurnFightObjAction()
+        clientPack.Clear()
+        clientPack.TurnNum = turnNum
+        clientPack.ObjID = objID
+        self.addBatPack(clientPack)
+        return
+    
+    def addBatPack(self, clientPack):
+        ## 添加战斗过程封包,非战斗相关的封包可以不使用该函数发送,如加经验、掉落等
+        ## 注:战斗相关的封包需调用本函数方便统一管理战报
+        if hasattr(clientPack, "Head"):
+            headStr = "%02x%02x" % (clientPack.Head.Cmd, clientPack.Head.SubCmd)
+        else:
+            headStr = "%02x%02x" % (clientPack.Cmd, clientPack.SubCmd)
+        GameWorld.DebugLog("回合战斗过程封包: %s, isNeedReport=%s,%s" % (headStr, self.isNeedReport, self.guid))
+        if self.isNeedReport:
+            self.batBuffer += clientPack.GetBuffer()
+        # 有玩家的统一每个包单独发送,同样也支持战报统计
+        if self.curPlayer:
+            NetPackCommon.SendFakePack(self.curPlayer, clientPack)
+        return
+    
+class TurnFightMgr():
+    ## 回合战斗管理器
+    
+    def __init__(self):
+        self.turnFightDict = {} # {guid:TurnFight, ...}
+        self.npcGUIDDict = {} # npc所属的某场战斗guid {objID:guid, ...}
+        return
+    
+    def addTurnFight(self, mapID, funcLineID=0, playerID=0):
+        tf = TurnFight(mapID, funcLineID, playerID, False)
+        self.turnFightDict[tf.guid] = tf
+        return tf
+    
+    def delTurnFight(self, guid):
+        turnFight = self.getTurnFight(guid)
+        if not turnFight:
+            return
+        turnFight.clearFight()
+        self.turnFightDict.pop(guid, None)
+        return
+    
+    def getTurnFight(self, guid):
+        tf = None
+        if guid in self.turnFightDict:
+            tf = self.turnFightDict[guid]
+        elif False:
+            tf = TurnFight()
+        return tf
+    
+    def getNPCTurnFight(self, objID):
+        ## 获取NPC所在的回合战斗,如果该npc不属于某场回合战斗则返回None
+        tf = None
+        if objID in self.npcGUIDDict:
+            guid = self.npcGUIDDict[objID]
+            if guid in self.turnFightDict:
+                tf = self.turnFightDict[guid]
+        if not tf and False:
+            tf = TurnFight()
+        return tf
+    
+    def setNPCGUID(self, objID, guid):
+        ## 设置NPC所属回合战斗guid
+        self.npcGUIDDict[objID] = guid
+        GameWorld.DebugLog("设置NPC所属回合战斗guid: %s,%s" % (objID, guid))
+        return
+    def delNPCGUID(self, objID):
+        guid = self.npcGUIDDict.pop(objID, None)
+        GameWorld.DebugLog("删除NPC所属回合战斗guid: %s,%s" % (objID, guid))
+        return
+    
+def GetTurnFightMgr():
+    tfMgr = None
+    if PyGameData.g_turnFightMgr:
+        tfMgr = PyGameData.g_turnFightMgr
+    else:
+        tfMgr = TurnFightMgr()
+        PyGameData.g_turnFightMgr = tfMgr
+    return tfMgr
+    
+class MainFight():
+    ## 主线战斗管理
+    
+    def __init__(self, playerID):
+        self.playerID = playerID
+        self.chapterID = 0 # 章节ID
+        self.levelNum = 0 # 关卡编号
+        self.waveMax = 6 # 本关最大波数,每波有多个小队,每个小队即为一张战斗 TurnFight
+        self.wave = 0 # 当前刷怪波,注意不是玩家当前进度波,比如被击杀会回退一波
+        self.teamNum = 1 # 当前小队
+        self.teamMax = 1 # 当前波最大小队,某场战斗可能包含多个小队,所有小队混流击杀完才算过了本波
+        self.nextTeam = False # 下次前端请求战斗是否是下一小队,否则都是重新刷新当前进度怪
+        self.waveLineupList = [] # 小队列表
+        self.turnFight = GetTurnFightMgr().addTurnFight(ChConfig.Def_FBMapID_Main, 0, playerID)
+        return
+    
+    def isLevelBoss(self):
+        ## 当前战斗是否关卡boss
+        return self.turnFight.mapID == ChConfig.Def_FBMapID_MainBoss
+    
+    def clear(self):
+        self.turnFight.clearFight()
+        return
+    
+    def playerLogin(self, curPlayer):
+        self.turnFight.curPlayer = curPlayer
+        return
+    
+    def playerOffline(self, curPlayer):
+        self.turnFight.curPlayer = None
+        return
+    
+def GetMainFightMgr(curPlayer):
+    ## 获取主线战斗管理
+    olPlayer = PlayerOnline.GetOnlineMgr().GetOnlinePlayer(curPlayer)
+    return olPlayer.mainFight
+
+def OnPlayerLogin(curPlayer):
+    chapterID, levelNum, _ = PlayerControl.GetMainLevelPassInfo(curPlayer)
+    nowChapterID, _, _ = PlayerControl.GetMainLevelNowInfo(curPlayer)
+    if chapterID != nowChapterID:
+        if IpyGameDataPY.GetIpyGameDataNotLog("MainChapter", chapterID) and IpyGameDataPY.GetIpyGameDataNotLog("MainLevel", chapterID, levelNum):
+            fixNowValue = PlayerControl.SetMainLevelNowInfo(curPlayer, chapterID, levelNum, 1)
+            GameWorld.Log("当前主线关卡章节与过关章节不一致时,且过关进度章节已经存在,强制修正当前刷怪进度为过关章节进度! passChapterID-LV=%s-%s,nowChapterID=%s,fixNowValue=%s" 
+                          % (chapterID, levelNum, nowChapterID, fixNowValue), curPlayer.GetPlayerID())
+    return
+
+def GetPlayerLineupByCache(playerID, lineupID):
+    ## 获取玩家阵容信息 - 根据玩家查看缓存
+    return {}
+
+def GetPlayerLineup(curPlayer, lineupID):
+    ## 获取玩家阵容信息
+    # @param lineupID: 阵容ID
+    # @return: 阵容全部信息json字典,前端通用格式
+    
+    playerID = curPlayer.GetPlayerID()
+    
+    # 武将
+    heroDict = {}
+    heroCount = 0
+    curPack = curPlayer.GetItemManager().GetPack(ShareDefine.rptHero)
+    for index in range(curPack.GetCount()):
+        heroItem = curPack.GetAt(index)
+        if not heroItem or heroItem.IsEmpty():
+            continue
+        lineupCount = heroItem.GetUserAttrCount(ShareDefine.Def_IudetHeroLineup)
+        if not lineupCount:
+            continue
+        posNum = 0 # 站位
+        for lpIndex in range(lineupCount):
+            lineupValue = heroItem.GetUserAttrByIndex(ShareDefine.Def_IudetHeroLineup, lpIndex)
+            #阵容类型*10000+阵型类型*100+位置编号
+            if lineupValue / 10000 != lineupID:
+                continue
+            posNum = lineupValue % 100
+            break
+        
+        if not posNum:
+            continue
+        
+        heroID = heroItem.GetItemTypeID()
+        heroIpyData = IpyGameDataPY.GetIpyGameData("Hero", heroID)
+        if not heroIpyData:
+            continue
+        
+        npcID = heroIpyData.GetSkinNPCIDList()[0]
+        heroDict[str(posNum)] = {
+                                 "ID":heroID,
+                                 "Data":heroItem.GetUserData(),
+                                 "NPCID":npcID
+                                 }
+        
+        heroCount += 1
+        if heroCount >= ShareDefine.LineupObjMax:
+            break
+        
+    # 主公属性
+    lordAttrDict = PlayerControl.GetLordAttr(curPlayer)
+    
+    # 其他
+    
+    lineupInfo = {"PlayerID":playerID, "LordAttrDict":lordAttrDict, "Hero":heroDict}
+    return lineupInfo
+
+def GetNPCLineup(lineupID):
+    ## 获取NPC阵容信息
+    # @param lineupID: 阵容ID
+    # @return: 阵容全部信息json字典,前端通用格式
+    ipyData = IpyGameDataPY.GetIpyGameData("NPCLineup", lineupID)
+    if not ipyData:
+        return {}
+    
+    heroDict = {}
+    for posNum in range(1, 1 + 10):
+        if not hasattr(ipyData, "GetPosNPCID%s" % posNum):
+            break
+        npcID = getattr(ipyData, "GetPosNPCID%s" % posNum)()
+        if not npcID:
+            continue
+        
+        heroDict[str(posNum)] = {"NPCID":npcID}
+        
+    lineupInfo = {"Hero":heroDict}
+    return lineupInfo
+
+def SummonLineupObjs(batLineup, faction, num, lineupInfo, playerID=0):
+    '''召唤阵容战斗实例
+    @param faction: 所属阵营,目前支持两个阵营,1或2
+    @param num: 战斗阵容在该阵营上的编号,1V1时默认1,多对多时1~n
+    @param lineupInfo: 阵容信息
+    @param playerID: 发起的玩家ID,系统场次为0
+    '''
+    
+    '''关于视野层级说明 sightLevel
+        玩家发起的战斗视野层级只能用玩家ID,才能达到只通知该玩家的效果
+        但是由于同个玩家可能同时存在多场战斗,如主线战斗 + 其他战斗,所以视野层级只能用作仅通知该玩家的作用,
+        不能用于处理NPC视野来遍历伤害对象等,因为不同战场的NPC视野层级可能相同,对于同场战斗NPC的遍历处理只能用 turnFight 管理遍历
+        鉴于视野层级仅用于通知的作用,故系统场次理论上可以只用一个视野层级,但是为了减少不必要的问题,系统场次的视野层级还是尽量区分开来
+        这里暂时用时间戳的后4位来做短时间内的理论上唯一视野层级
+    '''
+    GameWorld.DebugLog("SummonLineupObjs faction:%s,num:%s,lineupInfo=%s" % (faction, num, lineupInfo), playerID)
+    if playerID:
+        curPlayer = GameWorld.GetPlayerManager().FindPlayerByID(playerID)
+        if not curPlayer:
+            return
+        posX, posY = curPlayer.GetPosX(), curPlayer.GetPosY()
+        sightLevel = playerID
+        
+        # 开发过程中可以先开启视野层级及视野,方便检查战斗相关是否有遗漏
+        if curPlayer.GetSightLevel() != sightLevel:
+            PlayerControl.SetPlayerSightLevel(curPlayer, sightLevel)
+        sight = ChConfig.Def_PlayerSight_Default * 5
+        if curPlayer.GetSight() != sight:
+            PlayerControl.SetSight(curPlayer, sight)
+    else:
+        gameMap = GameWorld.GetMap()
+        posX, posY = gameMap.GetRebornMapX(), gameMap.GetRebornMapY() # 系统战斗默认取当前地图的复活点,需要优化
+        sightLevel = 2000000000 + int(time.time()) % 10000
+        
+    GameWorld.DebugLog("sightLevel=%s,pos=(%s,%s)" % (sightLevel, posX, posY), playerID)
+    
+    lineupPlayerID = lineupInfo.get("PlayerID", 0) # 阵容所属玩家ID
+    heroDict = lineupInfo.get("Hero", {})
+    tick = GameWorld.GetGameWorld().GetTick()
+    
+    initXP = IpyGameDataPY.GetFuncCfg("AngerXP", 1)
+    tfMgr = GetTurnFightMgr()
+    space = 3
+    for posNumKey, heroInfo in heroDict.items():
+        posNum = int(posNumKey)
+        
+        skillIDList = []
+        if lineupPlayerID:
+            heroID = heroInfo.get("ID", 0)
+            npcID = heroInfo.get("NPCID", 0)
+            heroIpyData = IpyGameDataPY.GetIpyGameData("Hero", heroID)
+            if not heroIpyData:
+                continue
+            normalSkillID = heroIpyData.GetNormalSkillID()
+            angerSkillID = heroIpyData.GetAngerSkillID()
+            skillIDList += [normalSkillID, angerSkillID]
+        else:
+            npcID = heroInfo.get("NPCID", 0)
+            npcDataEx = NPCCommon.GetNPCDataEx(npcID)
+            if not npcDataEx:
+                continue
+            skillIDList += npcDataEx.GetSkillIDList()
+            
+        if not npcID:
+            continue
+        
+        npcData = GameWorld.GetGameData().FindNPCDataByID(npcID)
+        if not npcData:
+            continue
+        curSummon = GameWorld.GetNPCManager().AddPlayerSummonNPC()
+        if not curSummon:
+            continue
+        objID = curSummon.GetID()
+        batLineup.npcPosDict[posNum] = curSummon
+        batLineup.npcObjIDDict[objID] = curSummon
+        tfMgr.setNPCGUID(objID, batLineup.getGUID())
+        
+        curSummon.SetNPCTypeID(npcID)
+        curSummon.SetBornTime(tick)
+        curSummon.SetAIType(0)
+        curSummon.SetSightLevel(sightLevel)
+        curSummon.SetIsNeedProcess(False)
+        curSummon.SetCanAttack(True)
+        curSummon.SetVisible(True)
+        curSummon.SetDict(ChConfig.Def_Obj_Dict_TurnFightPosInfo, num * 100 + posNum)
+        curSummon.SetDict(ChConfig.Def_Obj_Dict_LineupPlayerID, lineupPlayerID)
+        GameObj.SetFaction(curSummon, faction)
+        GameObj.SetXP(curSummon, initXP, False)
+        
+        skillManager = curSummon.GetSkillManager()
+        #有指定的技能,重新学习
+        if skillIDList:
+            skillManager.ResetSkill()
+            for skillID in skillIDList:
+                skillManager.LVUPSkillByID(skillID)
+                
+        rebornX = posX - space + (faction - 1) * space * 3 + ((posNum - 1) / 3 * space * 2 * (-1 if faction == 1 else 1))
+        rebornY = posY + (posNum - 1) % 3 * space
+        #GameWorld.DebugLog("Reborn ID:%s, faction:%s,posNum=%s, (%s,%s), %s" % (curSummon.GetID(), faction, posNum, rebornX, rebornY, skillIDList))
+        curSummon.Reborn(rebornX, rebornY, False)
+        NPCCommon.NPCControl(curSummon).DoNPCRebornCommLogic(tick)
+        
+    return
+
+#// B4 13 主线战斗请求 #tagCSMainFightReq
+#
+#struct    tagCSMainFightReq
+#{
+#    tagHead        Head;
+#    BYTE        ReqType;        // 0-停止战斗回城;1-设置消耗倍值;2-挑战关卡小怪;3-挑战关卡boss;4-继续战斗;
+#    DWORD        ReqValue;    // 请求值,ReqType为1时发送消耗倍值
+#};
+def OnMainFightReq(index, clientData, tick):
+    curPlayer = GameWorld.GetPlayerManager().GetPlayerByIndex(index)
+    reqType = clientData.ReqType
+    reqValue = clientData.ReqValue
+    
+    if reqType == 0:
+        __doExitMainFight(curPlayer)
+        return
+    elif reqType == 1:
+        __doSetFightPoint(curPlayer, reqValue)
+        return
+    
+    clientPack = ChPyNetSendPack.tagSCTurnFightReportSign()
+    clientPack.Sign = 0
+    NetPackCommon.SendFakePack(curPlayer, clientPack) # 标记开始
+    
+    if reqType == 2: # 前端主动请求开始关卡小怪的视为从休息中开始
+        __doMainLevelWave(curPlayer, True)
+    elif reqType == 3:
+        __doMainBossStart(curPlayer, tick)
+    elif reqType == 4:
+        __doMainFight(curPlayer, tick)
+    else:
+        pass
+    
+    # 标记结束
+    clientPack.Sign = 1
+    NetPackCommon.SendFakePack(curPlayer, clientPack)
+    return
+
+def __doExitMainFight(curPlayer):
+    ## 主线退出战斗 - 回城休息
+    mainFightMgr = GetMainFightMgr(curPlayer)
+    turnFight = mainFightMgr.turnFight
+    if turnFight:
+        turnFight.clearFight()
+    return
+
+def __doSetFightPoint(curPlayer, fightPoint):
+    ## 设置消耗倍值
+    GameWorld.DebugLog("设置战锤消耗倍值: %s" % fightPoint)
+    if fightPoint == 1:
+        pass
+    elif fightPoint == 2:
+        # 条件验证
+        pass
+    elif fightPoint == 3:
+        # 条件验证
+        pass
+    else:
+        return
+    curPlayer.SetFightPoint(fightPoint)
+    return
+
+def __doMainLevelWave(curPlayer, isRestStart=False):
+    ## 开始新的关卡波
+    # @param isRestStart: 是否从休息状态重新开始的
+    playerID = curPlayer.GetPlayerID()
+    chapterID, levelNum, wave = PlayerControl.GetMainLevelNowInfo(curPlayer)
+    if not chapterID:
+        chapterID = 1
+    if not levelNum:
+        levelNum =1
+    if not wave:
+        wave = 1
+    GameWorld.DebugLog("请求关卡波战斗: chapterID=%s,levelNum=%s,wave=%s,isRestStart=%s" % (chapterID, levelNum, wave, isRestStart), playerID)
+    fightPoint = max(curPlayer.GetFightPoint(), 1)
+    if not PlayerControl.HaveMoney(curPlayer, ShareDefine.TYPE_Price_Xiantao, fightPoint):
+        GameWorld.DebugLog("战锤不足,无法开始!")
+        return
+    chapterIpyData = IpyGameDataPY.GetIpyGameData("MainChapter", chapterID)
+    if not chapterIpyData:
+        return
+    levelIpyData = IpyGameDataPY.GetIpyGameData("MainLevel", chapterID, levelNum)
+    if not levelIpyData:
+        # 如上线后减少某个章节关卡数的情况,导致找不到,则从该章节回退到存在的关卡开始
+        while levelNum > 1:
+            levelNum -= 1 
+            levelIpyData = IpyGameDataPY.GetIpyGameDataNotLog("MainLevel", chapterID, levelNum)
+            if levelIpyData:
+                break
+    if not levelIpyData:
+        return
+    
+    # 本关卡最大波数,暂时支持最大6波
+    waveMax = 6
+    while waveMax >= 1 and (not hasattr(levelIpyData, "GetWaveLineupIDList%s" % waveMax) or not getattr(levelIpyData, "GetWaveLineupIDList%s" % waveMax)()):
+        waveMax -= 1
+    if waveMax < 1:
+        return
+    if wave > waveMax:
+        wave = waveMax
+        
+    waveLineupList = getattr(levelIpyData, "GetWaveLineupIDList%s" % wave)() # 小队1阵容ID|小队2阵容ID|...
+    if not waveLineupList:
+        return
+    teamMax = len(waveLineupList)
+    teamNum = 1
+    lineupID = waveLineupList[teamNum - 1] # NPC阵容ID
+    
+    mainFightMgr = GetMainFightMgr(curPlayer)
+    mainFightMgr.nextTeam = False
+    mainFightMgr.chapterID = chapterID
+    mainFightMgr.levelNum = levelNum
+    mainFightMgr.waveMax = waveMax
+    mainFightMgr.wave = wave
+    mainFightMgr.waveLineupList = waveLineupList
+    mainFightMgr.teamNum = teamNum
+    mainFightMgr.teamMax = teamMax
+    turnMax = IpyGameDataPY.GetFuncCfg("Mainline", 2)
+    mapID, funcLineID = ChConfig.Def_FBMapID_Main, PlayerControl.ComMainLevelValue(chapterID, levelNum, wave)
+    GameWorld.DebugLog("设置起始关卡波: 关卡%s-%s,波=%s/%s,teamMax=%s,mapID=%s,funcLineID=%s,lineupID=%s" 
+                       % (chapterID, levelNum, wave, waveMax, teamMax, mapID, funcLineID, lineupID), playerID)
+    
+    turnFight = mainFightMgr.turnFight
+    turnFight.setTurn(mapID, funcLineID, turnMax, False, {"teamNum":teamNum, "teamMax":teamMax})
+    turnFight.setFactionLineup(Def_FactionA, {1:GetPlayerLineup(curPlayer, ShareDefine.Lineup_Main)}, True)
+    turnFight.setFactionLineup(Def_FactionB, {1:GetNPCLineup(lineupID)})
+    turnFight.sortActionQueue()
+    turnFight.syncInit()
+    return
+
+def __doMainBossStart(curPlayer, tick):
+    ## 开始挑战关卡boss
+    playerID = curPlayer.GetPlayerID()
+    chapterID, levelNum, wave = PlayerControl.GetMainLevelPassInfo(curPlayer)
+    if not chapterID:
+        chapterID = 1
+    if not levelNum:
+        levelNum = 1
+    GameWorld.DebugLog("请求挑战关卡Boss! passInfo: chapterID=%s,levelNum=%s,wave=%s" % (chapterID, levelNum, wave), playerID)
+    chapterIpyData = IpyGameDataPY.GetIpyGameData("MainChapter", chapterID)
+    if not chapterIpyData:
+        return
+    levelIpyData = IpyGameDataPY.GetIpyGameData("MainLevel", chapterID, levelNum)
+    if not levelIpyData:
+        return
+    
+    nowChapterID, nowLevelNum, _ = PlayerControl.GetMainLevelNowInfo(curPlayer)
+    if chapterID != nowChapterID or levelNum != nowLevelNum:
+        '''一般有两种情况会导致
+        1. 理论上玩家当前刷怪的章节关卡一定是与过关的章节关卡是一致的,除了全部章节已通关
+                                当通关时,已过关的记录会被设置为下一章节第1关(版本关卡表实际不存在该关卡)
+                                但是当前刷怪的还是版本的最后一关,固会出现不一致的情况
+                                在增加章节关卡新版本更新后玩家重登会自动进行修复当前刷怪进度为最新章节第1关
+        2. 真的出现问题了,可能是bug引起,这种情况只能进行bug修复及针对异常账号特殊处理,或者重登后也会自动进行修正
+        '''
+        GameWorld.ErrLog("当前刷怪章节关卡与挑战的boss章节关卡不一致,无法挑战! chapterID=%s,levelNum=%s,nowChapterID=%s,nowLevelNum=%s" 
+                         % (chapterID, levelNum, nowChapterID, nowLevelNum), playerID)
+        return
+    
+    # 本关卡最大波数
+    waveMax = 6
+    while waveMax >= 1 and (not hasattr(levelIpyData, "GetWaveLineupIDList%s" % waveMax) or not getattr(levelIpyData, "GetWaveLineupIDList%s" % waveMax)()):
+        waveMax -= 1
+        
+    if wave < waveMax:
+        GameWorld.DebugLog("最后一波未通过,无法挑战本关boss! passWave=%s < %s" % (wave, waveMax))
+        return
+    
+    waveLineupList = levelIpyData.GetBossLineupIDList() # Boss波阵容ID列表,小队1阵容ID|小队2阵容ID|...
+    if not waveLineupList:
+        return
+    teamMax = len(waveLineupList)
+    teamNum = 1
+    lineupID = waveLineupList[teamNum - 1] # NPC阵容ID
+    
+    wave = waveMax = 1 # 关卡boss固定只有一波
+    
+    mainFightMgr = GetMainFightMgr(curPlayer)
+    mainFightMgr.nextTeam = False
+    mainFightMgr.chapterID = chapterID
+    mainFightMgr.levelNum = levelNum
+    mainFightMgr.waveMax = waveMax
+    mainFightMgr.wave = wave
+    mainFightMgr.waveLineupList = waveLineupList
+    mainFightMgr.teamNum = teamNum
+    mainFightMgr.teamMax = teamMax
+    turnMax = IpyGameDataPY.GetFuncCfg("Mainline", 3)
+    mapID, funcLineID = ChConfig.Def_FBMapID_MainBoss, PlayerControl.ComMainLevelValue(chapterID, levelNum, wave)
+    GameWorld.DebugLog("设置关卡boss: 关卡%s-%s,波=%s/%s,teamMax=%s,mapID=%s,funcLineID=%s,lineupID=%s" 
+                       % (chapterID, levelNum, wave, waveMax, teamMax, mapID, funcLineID, lineupID), playerID)
+    
+    turnFight = mainFightMgr.turnFight
+    turnFight.setTurn(mapID, funcLineID, turnMax, False, {"teamNum":teamNum, "teamMax":teamMax})
+    turnFight.setFactionLineup(Def_FactionA, {1:GetPlayerLineup(curPlayer, ShareDefine.Lineup_Main)}, True)
+    turnFight.setFactionLineup(Def_FactionB, {1:GetNPCLineup(lineupID)})
+    turnFight.sortActionQueue()
+    turnFight.syncInit()
+    
+    # 挑战boss无中间过程,每次执行直接挑战一队结果
+    __processTurnFight(turnFight.guid, tick)
+    return
+
+def __doMainFight(curPlayer, tick):
+    ## 主线执行战斗
+    mainFightMgr = GetMainFightMgr(curPlayer)
+    turnFight = mainFightMgr.turnFight
+    
+    isLevelBoss = mainFightMgr.isLevelBoss()
+    winFaction = turnFight.winFaction
+    if winFaction:
+        if mainFightMgr.nextTeam:
+            # 自动开始下一小队
+            teamNum = mainFightMgr.teamNum = mainFightMgr.teamNum + 1
+            GameWorld.DebugLog("开始进入下一小队: teamNum=%s" % teamNum)
+            if teamNum < 1 or teamNum > len(mainFightMgr.waveLineupList):
+                GameWorld.DebugLog("111111 teamNum=%s,mainFightMgr.waveLineupList=%s" % (teamNum, mainFightMgr.waveLineupList))
+                return
+            lineupID = mainFightMgr.waveLineupList[teamNum - 1] # NPC阵容ID
+            GameWorld.DebugLog("teamNum=%s,lineupID=%s" % (teamNum, lineupID))
+            
+            mainFightMgr.nextTeam = False
+            turnFight.resetTurn({"teamNum":teamNum})
+            # 切换小队时,玩家阵容不需要处理,保留状态
+            turnFight.setFactionLineup(Def_FactionB, {1:GetNPCLineup(lineupID)})
+            turnFight.sortActionQueue()
+            turnFight.syncInit()
+            
+            if mainFightMgr.isLevelBoss():
+                # 每次处理一小队的完整战斗,相当于一次完整战报
+                __processTurnFight(turnFight.guid, tick)
+                return
+        else:
+            __doMainLevelWave(curPlayer, False)
+        return
+    
+    if isLevelBoss:
+        ## 关卡boss是一次性处理完的,一般不可能走到这里,这边做下防范
+        return
+    
+    # 以下均是处理关卡小怪分段实时战斗
+    
+    EntryLogic(turnFight)
+    
+    # 小怪战斗,每次消耗1个战锤
+    fightPoint = max(curPlayer.GetFightPoint(), 1) # 主线战斗消耗倍值,默认1
+    
+    # 按阵营阵容执行顺序,逐个遍历
+    doCnt = 0
+    doMax = (PosNumMax + 2) * len(turnFight.actionSortList) # 防止死循环,做最大循环次数限制 = (最大位置数 + 主公、红颜位置)*行动阵容数
+    overLineupList = [] # 本回合已经结束行动的阵容列表 [(faction, num), ...], 所有阵容全部结束代表本回合结束
+    
+    turnNum = turnFight.turnNum
+    unXiantaoCnt = curPlayer.NomalDictGetProperty(ChConfig.Def_PDict_UnXiantaoCnt)
+    GameWorld.DebugLog("unXiantaoCnt=%s,turnNum=%s,doMax=%s,actionIndex=%s,%s" % (unXiantaoCnt, turnNum, doMax, turnFight.actionIndex, turnFight.actionSortList))
+    while doCnt < doMax and len(overLineupList) < len(turnFight.actionSortList):
+        doCnt += 1
+        turnNum = turnFight.turnNum
+        # 执行回合开始逻辑
+        if turnFight.turnStart < turnNum:
+            GameWorld.DebugLog("执行行动: turnNum=%s, 回合开始" % (turnFight.turnNum))
+            turnFight.syncState(FightState_Fighting)
+            if not PlayerControl.HaveMoney(curPlayer, ShareDefine.TYPE_Price_Xiantao, fightPoint):
+                GameWorld.DebugLog("回合开始时战锤不足!")
+                return
+            turnFight.turnStart = turnNum
+            turnFight.actionIndex = 0
+            for faction, num in turnFight.actionSortList:
+                batFaction = turnFight.getBatFaction(faction)
+                batLineup = batFaction.getBatlineup(num)
+                batLineup.actionNum = ActionNumStart
+                for curNPC in batLineup.npcPosDict.values():
+                    TurnFightObjPerTurnStart(curNPC, None, turnNum, tick)
+                    
+        if turnFight.actionIndex >= len(turnFight.actionSortList):
+            turnFight.actionIndex = 0
+            
+        faction, num = turnFight.actionSortList[turnFight.actionIndex]
+        batFaction = turnFight.getBatFaction(faction)
+        batLineup = batFaction.getBatlineup(num)
+        if batLineup.actionNum > max(batLineup.npcPosDict):
+            if (faction, num) not in overLineupList:
+                overLineupList.append((faction, num))
+                
+            turnFight.actionIndex += 1
+            continue
+        
+        if faction == Def_FactionA:
+            if not PlayerControl.HaveMoney(curPlayer, ShareDefine.TYPE_Price_Xiantao, fightPoint):
+                GameWorld.DebugLog("战锤不足!")
+                return
+        GameWorld.DebugLog("执行行动: turnNum=%s,faction=%s,num=%s,actionNum=%s" % (turnFight.turnNum, faction, num, batLineup.actionNum))
+        
+        # 主公
+        if batLineup.actionNum == -1:
+            pass
+        
+        # 红颜
+        elif batLineup.actionNum == 0:
+            pass
+        
+        # 武将
+        elif batLineup.actionNum > 0:
+            for posNum in range(batLineup.actionNum, PosNumMax + 1):
+                batLineup.actionNum = posNum
+                if posNum not in batLineup.npcPosDict:
+                    continue
+                curNPC = batLineup.npcPosDict[posNum]
+                if not curNPC or GameObj.GetHP(curNPC) <= 0:
+                    continue
+                objType = curNPC.GetGameObjType()
+                objID = curNPC.GetID()
+                turnFight.syncObjAction(turnNum, objType, objID)
+                
+                objName = GetObjName(curNPC)
+                curHP = GameObj.GetHP(curNPC)
+                
+                GameWorld.DebugLog("★回合%s %s 行动 : curHP=%s" % (turnNum, objName, curHP))
+                if not DoAttack(curNPC, None, tick):
+                    GameWorld.DebugLog("        攻击失败")
+                    continue
+                
+                if faction == Def_FactionA:
+                    if not PlayerControl.PayMoney(curPlayer, ShareDefine.TYPE_Price_Xiantao, fightPoint, isNotify=False):
+                        return
+                break
+            
+        turnFight.actionIndex += 1
+        batLineup.actionNum += 1
+        if batLineup.actionNum > max(batLineup.npcPosDict):
+            GameWorld.DebugLog("该阵容本回合已经全部行动完了: turnNum=%s,faction=%s,num=%s,actionNum=%s" % (turnFight.turnNum, faction, num, batLineup.actionNum))
+            if (faction, num) not in overLineupList:
+                overLineupList.append((faction, num))
+                
+        if turnFight.checkOverByKilled():
+            break
+        
+        nowUnXiantaoCnt = curPlayer.NomalDictGetProperty(ChConfig.Def_PDict_UnXiantaoCnt)
+        if unXiantaoCnt != nowUnXiantaoCnt:
+            # 玩家有消耗战锤则停止,一段段执行
+            GameWorld.DebugLog("玩家有消耗战锤则停止,nowUnXiantaoCnt=%s" % (nowUnXiantaoCnt))
+            break
+        
+    if turnFight.winFaction:
+        return
+    
+    GameWorld.DebugLog("turnNum=%s,doCnt=%s,overLineupList=%s" % (turnNum, doCnt, overLineupList))
+    if len(overLineupList) >= len(turnFight.actionSortList):
+        GameWorld.DebugLog("执行行动: turnNum=%s, 回合结束" % (turnFight.turnNum))
+        if turnFight.turnEnd < turnNum:
+            if not PlayerControl.HaveMoney(curPlayer, ShareDefine.TYPE_Price_Xiantao, fightPoint):
+                GameWorld.DebugLog("回合结束时战锤不足!")
+                return
+            turnFight.turnEnd = turnNum
+            for faction, num in turnFight.actionSortList:
+                batFaction = turnFight.getBatFaction(faction)
+                batLineup = batFaction.getBatlineup(num)
+                for curNPC in batLineup.npcPosDict.values():
+                    pass
+                
+            if turnFight.checkOverByKilled():
+                return
+            
+        if turnNum < turnFight.turnMax:
+            turnFight.turnNum += 1
+        else:
+            OnTurnAllOver(turnFight.guid)
+            
+    return
+
+def __processTurnFight(guid, tick):
+    ## 一次性处理完一个小队的战斗
+    turnFight = GetTurnFightMgr().getTurnFight(guid)
+    curPlayer = turnFight.curPlayer
+    turnMax = turnFight.turnMax
+    EntryLogic(turnFight)
+    for turnNum in range(1, turnMax + 1):
+        turnFight.turnNum = turnNum
+        GameWorld.DebugLog("【----- 回合制战斗轮次: %s -----】" % turnNum)
+        if curPlayer:
+            turnFight.syncState(FightState_Fighting)
+            
+        # 回合开始
+        for faction, num in turnFight.actionSortList:
+            GameWorld.DebugLog("回合开始逻辑: turnNum=%s,faction=%s, num=%s" % (turnNum, faction, num))
+            batFaction = turnFight.getBatFaction(faction)
+            batLineup = batFaction.getBatlineup(num)
+            batLineup.actionNum = 1
+            for curNPC in batLineup.npcPosDict.values():
+                TurnFightObjPerTurnStart(curNPC, None, turnNum, tick)
+                
+        # 主公
+        for faction, num in turnFight.actionSortList:
+            GameWorld.DebugLog("主公逻辑: turnNum=%s,faction=%s, num=%s" % (turnNum, faction, num))
+            batFaction = turnFight.getBatFaction(faction)
+            batLineup = batFaction.getBatlineup(num)
+            
+        # 红颜
+        for faction, num in turnFight.actionSortList:
+            GameWorld.DebugLog("红颜逻辑: turnNum=%s,faction=%s, num=%s" % (turnNum, faction, num))
+            batFaction = turnFight.getBatFaction(faction)
+            batLineup = batFaction.getBatlineup(num)
+            
+        if turnFight.checkOverByKilled():
+            break
+        
+        # 武将
+        doMax = PosNumMax * len(turnFight.actionSortList)
+        doCnt = 0
+        while doCnt < doMax and turnFight.actionIndex < len(turnFight.actionSortList):
+            doCnt += 1
+            faction, num = turnFight.actionSortList[turnFight.actionIndex]
+            batFaction = turnFight.getBatFaction(faction)
+            batLineup = batFaction.getBatlineup(num)
+            for posNum in range(batLineup.actionNum, PosNumMax + 1):
+                batLineup.actionNum = posNum + 1
+                if posNum not in batLineup.npcPosDict:
+                    continue
+                curNPC = batLineup.npcPosDict[posNum]
+                if not curNPC or GameObj.GetHP(curNPC) <= 0:
+                    continue
+                objType = curNPC.GetGameObjType()
+                objID = curNPC.GetID()
+                turnFight.syncObjAction(turnNum, objType, objID)
+                
+                objName = GetObjName(curNPC)
+                curHP = GameObj.GetHP(curNPC)
+                
+                GameWorld.DebugLog("★回合%s %s 行动 : curHP=%s" % (turnNum, objName, curHP))
+                if not DoAttack(curNPC, None, tick):
+                    continue
+                
+                break
+            
+            if turnFight.actionIndex >= len(turnFight.actionSortList) - 1:
+                turnFight.actionIndex = 0
+            else:
+                turnFight.actionIndex += 1
+                
+        # 回合结束
+        for faction, num in turnFight.actionSortList:
+            GameWorld.DebugLog("回合结束逻辑: turnNum=%s,faction=%s, num=%s" % (turnNum, faction, num))
+            batFaction = turnFight.getBatFaction(faction)
+            batLineup = batFaction.getBatlineup(num)
+            for curNPC in batLineup.npcPosDict.values():
+                pass
+            
+        if turnFight.checkOverByKilled():
+            break
+        
+    if not turnFight.winFaction:
+        OnTurnAllOver(turnFight.guid)
+    return
+
 #// B4 10 回合制战斗 #tagCMTurnFight
 #
 #struct    tagCMTurnFight
 #{
 #    tagHead        Head;
-#    DWORD        MapID;    // 自定义地图ID,可用于绑定战斗场景功能(如野外关卡,爬塔功能,竞技场等)
-#    WORD        FuncLineID;
-#    BYTE        TagType;    // 战斗目标类型,0-NPC,1-玩家,2-队伍
-#    DWORD        TagID;    // 战斗目标类型对应的ID
+#    DWORD        MapID;        // 自定义地图ID,可用于绑定战斗地图场景功能(如主线关卡、主线boss、爬塔、竞技场等)
+#    DWORD        FuncLineID;    // MapID对应的扩展值,如具体某个关卡等
+#    BYTE        TagType;    // 目标类型,0-NPC阵容,1-玩家
+#    DWORD        TagID;    // 目标类型对应的ID,如阵容ID或玩家ID
 #    BYTE        ValueCount;
 #    DWORD        ValueList[ValueCount]; // 附加值列表,可选,具体含义由MapID决定
 #};
 def OnTurnFight(index, clientData, tick):
-    curPlayer = GameWorld.GetPlayerManager().GetPlayerByIndex(index)
-    mapID = clientData.MapID
-    funcLineID = clientData.FuncLineID
-    tagType = clientData.TagType
-    tagID = clientData.TagID
-    valueList = clientData.ValueList
+    #curPlayer = GameWorld.GetPlayerManager().GetPlayerByIndex(index)
+    #mapID = clientData.MapID
+    #funcLineID = clientData.FuncLineID
+    return
+
+def GetObjName(gameObj):
+    objName = gameObj.GetName()
+    faction = GameObj.GetFaction(gameObj)
+    posInfo = gameObj.GetDictByKey(ChConfig.Def_Obj_Dict_TurnFightPosInfo)
+    npcID = gameObj.GetNPCID()
+    return "%s%s %s[%s-%s]" % ("A" if faction == Def_FactionA else "B", posInfo, objName, gameObj.GetID(), npcID)
+
+def EntryLogic(turnFight):
+    ## 执行进场逻辑
+    if turnFight.enterLogic:
+        return
+    GameWorld.DebugLog("执行进场逻辑...")
     
-    playerID = curPlayer.GetPlayerID()
-    if tagType == ChConfig.TurnBattle_TagType_Player:
-        if tagID == playerID:
-            GameWorld.DebugLog("不能打自己!", playerID)
+    turnFight.enterLogic = True
+    return
+
+def TurnFightObjPerTurnStart(gameObj, tagObj, turnNum, tick):
+    ## 回合制战斗实例 - 每回合开始时处理
+    if not gameObj:
+        return
+    
+#    SetTimeline(gameObj, turnNum, 0)
+#    # 重置连击、反击数
+#    gameObj.SetDict(ChConfig.Def_Obj_Dict_TurnComboNum, 0)
+#    gameObj.SetDict(ChConfig.Def_Obj_Dict_TurnAtkBackNum, 0)
+#    gameObj.SetDict(ChConfig.Def_Obj_Dict_TurnParryNum, 0)
+#    gameObj.SetDict(ChConfig.Def_Obj_Dict_TurnMissNum, 0)
+#    gameObj.SetDict(ChConfig.Def_Obj_Dict_TurnAtkAddXPCount, 0)
+#    gameObj.SetDict(ChConfig.Def_Obj_Dict_TurnXPUseState, 0)
+#    gameObj.SetDict(ChConfig.Def_PlayerKey_AttrFaintCD, 0) # 击晕CD
+#    
+#    objType = gameObj.GetGameObjType()
+#    objID = gameObj.GetID()
+#    objName = GetObjName(gameObj)
+#    GameWorld.DebugLog("ObjPerTurnStart: 回合%s, %s objType-ID-HP(%s-%s-%s)" % (turnNum, objName, objType, objID, GameObj.GetHP(gameObj)))
+#    
+#    # 每回合开始减技能CD
+#    skillManager = gameObj.GetSkillManager()
+#    for i in range(skillManager.GetSkillCount()):
+#        skill = skillManager.GetSkillByIndex(i)
+#        remainTime = skill.GetRemainTime()
+#        if not remainTime:
+#            continue
+#        skillID = skill.GetSkillID()
+#        updRemainTime = max(0, remainTime - ChConfig.Def_PerTurnTick)
+#        skill.SetRemainTime(updRemainTime)
+#        GameWorld.DebugLog("    skillID=%s,remainTime=%s,updRemainTime=%s" % (skillID, remainTime, updRemainTime))
+#        
+#    # 刷新定时处理的buff效果
+#    SkillShell.ProcessPersistBuff(gameObj, tick)
+#    
+#    PassiveBuffEffMng.OnPassiveSkillTrigger(gameObj, tagObj, None, ChConfig.TriggerType_TurnNum, tick)
+#    
+#    __logGameObjAttr(gameObj)
+    return
+
+def AddTurnObjCureHP(curObj, srcObj, addValue, cureHP, skillID=0):
+    ## 回合对象添加治疗值
+    if not curObj.GetDictByKey(ChConfig.Def_Obj_Dict_TurnFightPosInfo):
+        return
+    
+    curFaction = GameObj.GetFaction(curObj)
+    if not srcObj or curFaction != GameObj.GetFaction(srcObj):
+        # 同阵营的治疗才统计
+        return
+    
+    curID = curObj.GetID()
+    srcID = srcObj.GetID()
+    turnFight = GetTurnFightMgr().getNPCTurnFight(curID)
+    if not turnFight:
+        return
+    
+    curBatFaction = turnFight.getBatFaction(curFaction)
+    for num in curBatFaction.lineupDict.keys():
+        batLineup = curBatFaction.getBatlineup(num)
+        updStatValue = batLineup.statCureValue(srcID, cureHP)
+        if updStatValue == None:
+            continue
+        GameWorld.DebugLog("        统计治疗: curTD=%s,srcID=%s,skillID=%s,addValue=%s,cureHP=%s,updStatValue=%s" 
+                       % (curID, srcID, skillID, addValue, cureHP, updStatValue))
+        break
+    
+    return
+
+def AddTurnObjHurtValue(curObj, tagObj, hurtType, hurtValue, lostHP, curSkill=None):
+    ## 回合对象添加伤害值
+    if not curObj.GetDictByKey(ChConfig.Def_Obj_Dict_TurnFightPosInfo):
+        return
+    curID = curObj.GetID()
+    tagID = tagObj.GetID()
+    skillID = curSkill.GetSkillID() if curSkill else 0
+    turnFight = GetTurnFightMgr().getNPCTurnFight(curID)
+    if not turnFight:
+        return
+    
+    if curID != tagID:
+        curBatFaction = turnFight.getBatFaction(GameObj.GetFaction(curObj))
+        for num in curBatFaction.lineupDict.keys():
+            batLineup = curBatFaction.getBatlineup(num)
+            updStatValue = batLineup.statHurtValue(curID, lostHP)
+            if updStatValue == None:
+                continue
+            GameWorld.DebugLog("        统计伤血: curTD=%s,tagID=%s,skillID=%s,hurtType=%s,hurtValue=%s,lostHP=%s,updStatValue=%s,tagHP=%s" 
+                           % (curID, tagID, skillID, hurtType, hurtValue, lostHP, updStatValue, GameObj.GetHP(tagObj)))
+            break
+        
+        tagBatFaction = turnFight.getBatFaction(GameObj.GetFaction(tagObj))
+        for num in tagBatFaction.lineupDict.keys():
+            batLineup = tagBatFaction.getBatlineup(num)
+            updStatValue = batLineup.statDefValue(tagID, lostHP)
+            if updStatValue == None:
+                continue
+            GameWorld.DebugLog("        统计承伤: curTD=%s,tagID=%s,skillID=%s,hurtType=%s,hurtValue=%s,lostHP=%s,updStatValue=%s,curHP=%s" 
+                           % (tagID, curID, skillID, hurtType, hurtValue, lostHP, updStatValue, GameObj.GetHP(tagObj)))
+            break
+        
+    else:
+        # 如换血类技能,自残的伤害不算输出
+        GameWorld.DebugLog("        自残: curTD=%s,tagID=%s,skillID=%s,hurtType=%s,hurtValue=%s,lostHP=%s,curHP=%s" 
+                           % (curID, tagID, skillID, hurtType, hurtValue, lostHP, GameObj.GetHP(curObj)))
+        
+    if lostHP and curID != tagID:
+        AddTurnFightXP(tagObj, __GetAddXP_Defender(tagObj, lostHP), "skillID:%s" % skillID)
+    return
+
+def __GetAddXP_Attack(attack, curSkill):
+    ## 攻击方增加的XP值根据主动普攻技能获得
+    if not curSkill:
+        return 0
+    if not SkillCommon.isNormalAtkSkill(curSkill):
+        return 0
+    return IpyGameDataPY.GetFuncCfg("AngerXP", 3)
+
+def __GetAddXP_Defender(defender, lostHP):
+    ## 掉血方增加的XP值根据掉血百分比获得
+    if lostHP <= 0:
+        return 0
+    return IpyGameDataPY.GetFuncCfg("AngerXP", 4)
+
+def AddTurnFightXP(gameObj, addXP, reason=""):
+    ## 回合战斗增加XP
+    if addXP <= 0 or not addXP:
+        #GameWorld.DebugLog("        没有增加XP! curID=%s" % (gameObj.GetID()))
+        return
+    posNum = gameObj.GetDictByKey(ChConfig.Def_Obj_Dict_TurnFightPosInfo) % 10
+    if posNum <= 0:
+        #非主战单位不加
+        return
+    curXP = GameObj.GetXP(gameObj)
+    updXP = curXP + addXP
+    GameObj.SetXP(gameObj, updXP)
+    GameWorld.DebugLog("        更新XP: curID=%s,curXP=%s,addXP=%s,updXP=%s,reason=%s" % (gameObj.GetID(), curXP, addXP, updXP, reason))
+    return
+
+def DoAttack(curNPC, tagNPC, tick, turnBattleType=ChConfig.TurnBattleType_Normal, useSkill=None):
+    curID = curNPC.GetID()
+    npcID = curNPC.GetNPCID()
+    objName = GetObjName(curNPC)
+    GameWorld.DebugLog("    ● %s DoAttack: curID=%s,,turnBattleType=%s" % (objName, curID, turnBattleType))
+    
+    atkOK = False
+    tagObj = tagNPC
+    curNPC.SetDict(ChConfig.Def_Obj_Dict_TurnBattleType, turnBattleType)
+    
+    if turnBattleType == ChConfig.TurnBattleType_AtkBack:
+        if not tagObj:
             return
+        atkOK = BaseAttack.Attack(curNPC, tagObj, None, tick) # 反击为单体普攻
+    elif turnBattleType == ChConfig.TurnBattleType_Combo:
+        if not tagObj:
+            return
+        atkOK = SkillShell.DoLogic_UseSkill(curNPC, tagObj, useSkill, tick)
+    else:
+        curXP = GameObj.GetXP(curNPC)
+        xpMax = IpyGameDataPY.GetFuncCfg("AngerXP", 2) 
+        skillManager = curNPC.GetSkillManager()
+        useSkillList = []
+        GameWorld.DebugLog('skillCount=%s' % skillManager.GetSkillCount(), npcID)
+        for index in range(0, skillManager.GetSkillCount()):
+            useSkill = skillManager.GetSkillByIndex(index)
+            #已经到尾部了
+            if not useSkill or useSkill.GetSkillTypeID() == 0:
+                break
+            #被动技能无法使用
+            if SkillCommon.isPassiveSkill(useSkill):
+                continue
+            #还在冷却时间内无法释放
+            if SkillCommon.RefreshSkillRemainTime(useSkill, tick) != 0:
+                continue
+            skillTypeID = useSkill.GetSkillTypeID()
+            # 常规攻击优先xp
+            if SkillCommon.isXPSkill(useSkill) and turnBattleType == ChConfig.TurnBattleType_Normal:
+                if curXP < xpMax:
+                    continue
+                useCnt = -1 # xp技能优先释放
+            else:
+                useCnt = curNPC.GetDictByKey(ChConfig.Def_NPC_Dict_SkillUseCnt % skillTypeID) # 该技能已使用次数
+            useSkillList.append([useCnt, index, skillTypeID, useSkill])
+            
+        useSkillList.sort() # 按使用次数优先升序排,使用次数低的优先判断使用
+        #GameWorld.DebugLog('    技能使用顺序 = useSkillList%s' % str(useSkillList), npcID)
         
-    reqRet = FBLogic.OnTurnFightRequest(curPlayer, mapID, funcLineID, tagType, tagID, valueList)
-    if not reqRet:
+        for useInfo in useSkillList:
+            useSkill = useInfo[-1]
+            skillID = useSkill.GetSkillID()
+            atkOK, tagObj = DoNPCUseSkill(curNPC, useSkill, tick)
+            if atkOK:
+                AddTurnFightXP(curNPC, __GetAddXP_Attack(curNPC, useSkill), "skillID:%s" % skillID)
+                break
+            
+        if not atkOK:
+            tagObj = GetEnemyObj(curNPC)
+            if tagObj:
+                GameWorld.DebugLog('    无技能可攻击,直接使用普攻! tagID=%s(%s)' % (tagObj.GetID(), GetObjName(tagObj)), npcID)
+                atkOK = BaseAttack.Attack(curNPC, tagObj, None, tick)
+                
+    curNPC.SetDict(ChConfig.Def_Obj_Dict_TurnBattleType, 0) # 无论攻击成功与否都重置战斗类型
+    
+    tagID = 0
+    tagHP = 0
+    if tagObj:
+        tagID = tagObj.GetID()
+        tagHP = GameObj.GetHP(tagObj)
+    GameWorld.DebugLog('        atkOK=%s,tagID=%s,tagHP=%s' % (atkOK, tagID, tagHP), npcID)
+    if not atkOK:
         return
     
-    # 需要发送到GameServer验证
-    if mapID in ChConfig.Def_TFMapID_SendToGameServer:
-        SendToGameServer_TurnFight(curPlayer, "TurnFightRequest", [mapID, funcLineID, tagType, tagID, valueList])
+    if not tagObj:
         return
     
-    DoTurnFightProcess(curPlayer, mapID, funcLineID, tagType, tagID, valueList, tick)
-    return
-
-def SendToGameServer_TurnFight(curPlayer, msgType, dataMsg=""):
-    playerID = curPlayer.GetPlayerID()
-    msgList = str([msgType, dataMsg])
-    GameWorld.GetPlayerManager().GameServer_QueryPlayerResult(playerID, 0, 0, "TurnFight", msgList, len(msgList))
-    GameWorld.Log("回合战斗发送GameServer: %s, %s" % (msgType, dataMsg), playerID)
-    return
-
-def GameServer_TurnFight_DoResult(curPlayer, msgData, tick):
+    if GameObj.GetFaction(curNPC) == GameObj.GetFaction(tagObj):
+        # 同阵营直接返回,不处理连击、反击
+        return True
     
-    msgType, dataMsg, ret = msgData
+    curHP = GameObj.GetHP(curNPC)
+    tagHP = GameObj.GetHP(tagObj)
+    tagID = tagObj.GetID()
+    GameWorld.DebugLog("            curID-HP=(%s-%s),tagID-HP=(%s-%s)" % (curID, curHP, tagID, tagHP))
+    if tagHP <= 0 or curHP <= 0:
+        return True
     
-    if not ret:
+    # 反击,反击可打断连击,所以优先判断
+    if CanAtkBack(curNPC, tagObj):
+        DoAttack(tagObj, curNPC, tick, ChConfig.TurnBattleType_AtkBack)
+        return True
+    
+    # 连击
+    if CanCombo(curNPC, tagObj):
+        DoAttack(curNPC, tagObj, tick, ChConfig.TurnBattleType_Combo, useSkill)
+        
+    return True
+
+def CanAtkBack(atkObj, defObj):
+    ## 可否反击
+    posNum = atkObj.GetDictByKey(ChConfig.Def_Obj_Dict_TurnFightPosInfo) % 100
+    if posNum <= 0:
+        GameWorld.DebugLog("            被非主战单位攻击时无法触发反击: atkID=%s,posNum=%s" % (atkObj.GetID(), posNum))
+        return False
+    defAtkBackRate = GameObj.GetAtkBackRate(defObj) # 防方反击率
+    atkBackNum = defObj.GetDictByKey(ChConfig.Def_Obj_Dict_TurnAtkBackNum) # 已反击次数
+    if atkBackNum > 10:
+        # 内置最高反击数防范
+        return False
+    if atkBackNum > 0:
+        validPerList = IpyGameDataPY.GetFuncEvalCfg("TurnFight", 4)
+        vaildPer = validPerList[atkBackNum - 1] if len(validPerList) >= atkBackNum else 0
+        defAtkBackRate = int(defAtkBackRate * vaildPer / 100.0)
+        
+    atkAtkBackDefRate = GameObj.GetAtkBackDefRate(atkObj) # 攻方抵抗反击率
+    atkBackRate = max(0, defAtkBackRate - atkAtkBackDefRate)
+    if atkBackRate <= 0 or not GameWorld.CanHappen(atkBackRate):
+        GameWorld.DebugLog("            无法反击: defID=%s,atkBackNum=%s,atkBackRate=%s=(defAtkBackRate=%s - atkAtkBackDefRate=%s)" 
+                           % (defObj.GetID(), atkBackNum, atkBackRate, defAtkBackRate, atkAtkBackDefRate))
+        return False
+    GameWorld.DebugLog("            可以反击: defID=%s,atkBackNum=%s,atkBackRate=%s=(defAtkBackRate=%s - atkAtkBackDefRate=%s)" 
+                       % (defObj.GetID(), atkBackNum, atkBackRate, defAtkBackRate, atkAtkBackDefRate))
+    return True
+
+def CanCombo(atkObj, defObj):
+    ## 可否连击
+    
+    atkComboRate = GameObj.GetComboRate(atkObj) # 攻方连击率
+    comboNum = atkObj.GetDictByKey(ChConfig.Def_Obj_Dict_TurnComboNum) # 已连击次数
+    if comboNum > 10:
+        # 内置最高连击数防范
+        return False
+    if comboNum > 0:
+        validPerList = IpyGameDataPY.GetFuncEvalCfg("TurnFight", 3)
+        vaildPer = validPerList[comboNum - 1] if len(validPerList) >= comboNum else 0
+        atkComboRate = int(atkComboRate * vaildPer / 100.0)
+        
+    defComboReduce = GameObj.GetComboDefRate(defObj) # 防方抵抗连击率
+    comboRate = max(0, atkComboRate - defComboReduce)
+    if comboRate <= 0 or not GameWorld.CanHappen(comboRate):
+        GameWorld.DebugLog("            无法连击: atkID=%s,comboNum=%s,comboRate=%s=(atkComboRate=%s - defComboReduce=%s)" 
+                           % (atkObj.GetID(), comboNum, comboRate, atkComboRate, defComboReduce))
+        return False
+    GameWorld.DebugLog("            可以连击: atkID=%s,comboNum=%s,comboRate=%s=(atkComboRate=%s - defComboReduce=%s)" 
+                       % (atkObj.GetID(), comboNum, comboRate, atkComboRate, defComboReduce))
+    return True
+
+def DoNPCUseSkill(curNPC, useSkill, tick):
+    '''NPC使用技能
+    @return: 是否成功, tagObj
+    '''
+    if not useSkill or useSkill.GetSkillTypeID() == 0:
+        return False, None
+    
+    #检查是否几率触发
+    rate = useSkill.GetHappenRate()
+    if rate != ChConfig.Def_MaxRateValue and not GameWorld.CanHappen(rate, ChConfig.Def_MaxRateValue):
+        #GameWorld.Log('检查是否几率触发 = %s失败 = %s'%(rate, useSkill.GetSkillName()))
+        return False, None
+    
+    tagObj = None
+    skillTag = SkillShell.GetSkillAffectTag(useSkill)
+    skillAim = SkillShell.GetSkillFireAim(useSkill)
+    
+    # 注: 多V多的情况友好目标仅针对本阵容、敌对目标可针对任意敌对阵容
+    
+    #---对自己释放,或者无目标技能---
+    if skillTag in ChConfig.Def_ST_CanNPCUseSkill or skillAim == ChConfig.Def_UseSkillAim_None:
+        #释放自身类技能
+        tagObj = curNPC
+        isOK = SkillShell.DoLogic_UseSkill(curNPC, tagObj, useSkill, tick)
+        return isOK, tagObj
+    
+    # 召唤兽对主人释放技能
+    elif skillTag == ChConfig.Def_UseSkillTag_SummonMaster:
+        if not NPCCommon.IsSummonNPC(curNPC):
+            GameWorld.ErrLog("该NPC非召唤兽,无法获得主人释放技能")
+            return False, None
+        
+        curSummonOwner = NPCCommon.GetSummonNPCOwner(IPY_GameWorld.gotPlayer, curNPC)    
+        if curSummonOwner == None:
+            curSummonOwner = NPCCommon.GetSummonNPCOwner(IPY_GameWorld.gotNPC, curNPC)
+            
+        if curSummonOwner == None:
+            GameWorld.ErrLog("召唤兽(%s)对主人释放技能,找不到主人" % curNPC.GetNPCID())
+            return False, None
+        
+        tagObj = curSummonOwner
+        
+    # 友好死亡目标,一般是复活技能
+    elif skillTag == ChConfig.Def_UseSkillTag_FriendDeath:
+        deathList = GetFriendDeathList(curNPC)
+        if not deathList:
+            return False, None
+        tagObj = random.choice(deathList) # 随机选一个
+        
+    # 友好目标
+    elif skillTag in ChConfig.Def_ST_CanNPCUseSkillFriend:
+        friendList = GetFriendObjList(curNPC)
+        if not friendList:
+            return False, None
+        tagObj = random.choice(friendList) # 随机选一个,可扩展其他选择规则
+        
+    # 敌方目标
+    else:
+        tagObj = GetEnemyObj(curNPC)
+        
+    if not tagObj:
+        return False, None
+    
+    GameWorld.DebugLog("    技能释放: skillID=%s,atkID=%s,def=%s,HP:%s/%s" 
+                       % (useSkill.GetSkillID(), curNPC.GetID(), GetObjName(tagObj), GameObj.GetHP(tagObj), GameObj.GetMaxHP(tagObj)))
+    isOK = SkillShell.DoLogic_UseSkill(curNPC, tagObj, useSkill, tick)
+    return isOK, tagObj
+
+def GetFriendDeathList(curNPC):
+    ## 获取友方死亡单位,仅针对本阵容主战单位
+    objID = curNPC.GetID()
+    turnFight = GetTurnFightMgr().getNPCTurnFight(objID)
+    if not turnFight:
+        return []
+    batFaction = turnFight.getBatFaction(GameObj.GetFaction(curNPC))
+    num = curNPC.GetDictByKey(ChConfig.Def_Obj_Dict_TurnFightPosInfo) / 100
+    batLineup = batFaction.getBatlineup(num)
+    deathList = []
+    for posNum, tagNPC in batLineup.npcPosDict.items():
+        if posNum <= 0 or not tagNPC:
+            continue
+        if GameObj.GetHP(tagNPC) > 0:
+            continue
+        deathList.append(tagNPC)
+        
+    return deathList
+
+def GetFriendObjList(curNPC):
+    ## 获取友方单位,仅针对本阵容主战单位
+    objID = curNPC.GetID()
+    turnFight = GetTurnFightMgr().getNPCTurnFight(objID)
+    if not turnFight:
+        return []
+    batFaction = turnFight.getBatFaction(GameObj.GetFaction(curNPC))
+    num = curNPC.GetDictByKey(ChConfig.Def_Obj_Dict_TurnFightPosInfo) / 100
+    batLineup = batFaction.getBatlineup(num)
+    friendList = []
+    for posNum, tagNPC in batLineup.npcPosDict.items():
+        if posNum <= 0 or not tagNPC:
+            continue
+        if GameObj.GetHP(tagNPC) <= 0:
+            continue
+        friendList.append(tagNPC)
+    return friendList
+
+def GetEnemyObj(curNPC, ruleType=0):
+    ## 获取一个敌对单位,针对所有敌对阵容主战单位,优先选择对位阵容单位,仅限活着的单位
+    # @param ruleType: 选择规则,默认0任意,1-最低血量;2-最高血量
+    objID = curNPC.GetID()
+    faction = GameObj.GetFaction(curNPC)
+    posInfo = curNPC.GetDictByKey(ChConfig.Def_Obj_Dict_TurnFightPosInfo)
+    num = posInfo / 100 # 阵容编号
+    posNum = posInfo % 100 # 所在阵容站位
+    turnFight = GetTurnFightMgr().getNPCTurnFight(objID)
+    if not turnFight:
         return
     
-    if msgType == "TurnFightRequest":
-        mapID, funcLineID, tagType, tagID, valueList = dataMsg
-        DoTurnFightProcess(curPlayer, mapID, funcLineID, tagType, tagID, valueList, tick)
-        
-    elif msgType == "TurnFightOver":
-        mapID, funcLineID, tagType, tagID, valueList, fightRet, awardItemList = dataMsg
-        FBLogic.OnTurnFightOver_GameServerRet(curPlayer, mapID, funcLineID, tagType, tagID, valueList, fightRet, awardItemList, ret)
-        
-    elif msgType == "TurnFightTagPlayerInfo":
-        mapID, funcLineID, tagType, tagID, valueList = dataMsg
-        DoTrunFightVSPlayer(curPlayer, tagID, [mapID, funcLineID, valueList], ret)
-           
+    tagBatFaction = turnFight.getBatFaction(Def_FactionB if faction == Def_FactionA else Def_FactionA)
+    
+    tagLineupNumList = [num] # 目标阵容选择顺序,优先对位阵容,再其他阵容
+    if num in tagBatFaction.lineupDict:
+        tagLineupNumList = [num]
+    for tagNum in tagBatFaction.lineupDict.keys():
+        if tagNum not in tagLineupNumList:
+            tagLineupNumList.append(tagNum)
+            
+    for tagNum in tagLineupNumList:
+        tagLineup = tagBatFaction.getBatlineup(tagNum)
+        tagPosNumList = [posNum] # 优先对位位置,再其他位置按顺序遍历
+        pNumList = tagLineup.npcPosDict.keys()
+        pNumList.sort()
+        for pNum in pNumList:
+            if pNum > 0 and pNum not in tagPosNumList:
+                tagPosNumList.append(pNum)
+        for pNum in tagPosNumList:
+            tagNPC = tagLineup.npcPosDict.get(pNum)
+            if not tagNPC:
+                continue
+            if GameObj.GetHP(tagNPC) <= 0:
+                continue
+            return tagNPC
     return
 
-def DoTurnFightProcess(curPlayer, mapID, funcLineID, tagType, tagID, valueList, tick):
-    ## 执行回合制战斗的完整流程
+def SetKilled(gameObj, killer=None):
+    if not gameObj.GetDictByKey(ChConfig.Def_Obj_Dict_TurnFightPosInfo):
+        #GameWorld.DebugLog("非回合战斗主体被击杀: curID=%s" % gameObj.GetID())
+        return
     
-    #if curPlayer.GetSightLevel() != curPlayer.GetID():
-    #    PlayerControl.SetPlayerSightLevel(curPlayer, curPlayer.GetID())
+    GameWorld.DebugLog("        %s 回合战斗主体被击杀: curID=%s" % (GetObjName(gameObj), gameObj.GetID()))
+    GameObj.SetHP(gameObj, 0) # 回合制死亡仅设置为0,实例暂时不回收
+    gameObj.SetVisible(False)
+    
+    turnFight = GetTurnFightMgr().getNPCTurnFight(gameObj.GetID())
+    if turnFight:
+        clientPack = ChPyNetSendPack.tagMCTurnFightObjDead()
+        clientPack.ObjID = gameObj.GetID()
+        turnFight.addBatPack(clientPack)
         
-    SyncTurnFightState(curPlayer, mapID, funcLineID, tagType, tagID, FightState_Start)
+    return True
+
+def OnTurnAllOver(guid):
+    ## 所有回合已经全部执行完毕
+    GameWorld.DebugLog("所有回合结束")
+    turnFight = GetTurnFightMgr().getTurnFight(guid)
+    if not turnFight:
+        return
     
-    tagPlayerInfo = {}
-    if tagType == ChConfig.TurnBattle_TagType_Player and tagID >= 10000 :
-        tagPlayer = GameWorld.GetMapCopyPlayerManager().FindPlayerByID(tagID)
-        if tagPlayer:
-            tagPlayerInfo = __GetPlayerInfo(tagPlayer)
+    if turnFight.winFaction:
+        return
+    
+    if turnFight.playerID:
+        # 玩家发起的,未击杀对方,算玩家输
+        turnFight.winFaction = Def_FactionB
+    else:
+        # 系统场次,按一定规则来,这里先随机
+        turnFight.winFaction = random.choice([Def_FactionA, Def_FactionB])
+        
+    DoTurnFightOver(guid)
+    return
+
+def DoTurnFightOver(guid):
+    ## 执行回合战斗结算逻辑
+    tfMgr = GetTurnFightMgr()
+    turnFight = tfMgr.getTurnFight(guid)
+    if not turnFight:
+        return
+    
+    turnFight.costTime = time.time() - turnFight.startTime
+    winFaction = turnFight.winFaction
+    GameWorld.DebugLog("--- 战斗结束处理 --- %s, winFaction=%s, costTime=%ss" % (guid, winFaction, turnFight.costTime))
+    
+    # 统计明细
+    statInfo = {}
+    for faction in turnFight.factionDict.keys():
+        if str(faction) not in statInfo:
+            statInfo[str(faction)] = {}
+        facStatInfo = statInfo[str(faction)]
+        batFaction = turnFight.getBatFaction(faction)
+        for num in batFaction.lineupDict.keys():
+            if str(num) not in facStatInfo:
+                facStatInfo[str(num)] = {}
+            lineupStatInfo = facStatInfo[str(num)]
+            batLineup = batFaction.getBatlineup(num)
+            hurtStatDict = batLineup.hurtStatDict
+            defStatDict = batLineup.defStatDict
+            cureStatDict = batLineup.cureStatDict
+            GameWorld.DebugLog("阵容明细: faction=%s,num=%s" % (faction, num))
+            for posNum, curNPC in batLineup.npcPosDict.items():
+                objID = curNPC.GetID()
+                npcID = curNPC.GetNPCID()
+                atkHurt = hurtStatDict.get(objID, 0)
+                defHurt = defStatDict.get(objID, 0)
+                cureHP = cureStatDict.get(objID, 0)
+                GameWorld.DebugLog("    Pos:%s ID=%s-%s,,HP=%s/%s, 输出=%s,承伤=%s,治疗=%s" 
+                                   % (posNum, objID, npcID, GameObj.GetHP(curNPC), GameObj.GetMaxHP(curNPC), atkHurt, defHurt, cureHP))
+                lineupStatInfo[str(posNum)] = {"ObjID":objID, "NPCID":npcID, "AtkHurt":atkHurt, "DefHurt":defHurt, "CureHP":cureHP}
+                
+    awardItemList = []
+    playerID = turnFight.playerID
+    if playerID:
+        curPlayer = turnFight.curPlayer
+        isWin = (winFaction == Def_FactionA)
+        # 主线小怪
+        if turnFight.mapID == ChConfig.Def_FBMapID_Main:
+            OnOver_MainLevel(curPlayer, isWin, awardItemList)
+        # 主线boss
+        elif turnFight.mapID == ChConfig.Def_FBMapID_MainBoss:
+            OnOver_MainLevelBoss(curPlayer, isWin, awardItemList)    
         else:
-            SendToGameServer_TurnFight(curPlayer, "TurnFightTagPlayerInfo", [mapID, funcLineID, tagType, tagID, valueList])
-            return
+            pass
         
-    DoTrunFight(curPlayer, mapID, funcLineID, tagType, tagID, valueList, tick, tagPlayerInfo)
+    overMsg = {"winFaction":winFaction, "statInfo":statInfo, FBCommon.Over_itemInfo:FBCommon.GetJsonItemList(awardItemList)}
+    turnFight.syncState(FightState_Award, overMsg)
     
-    SyncTurnFightState(curPlayer, mapID, funcLineID, tagType, tagID, FightState_Over)
+    # 除了主线外,其余战斗结束后均直接删除
+    if not turnFight.playerID:
+        tfMgr.delTurnFight(guid)
     return
 
-def __GetPlayerInfo(curPlayer):
-    infoDict = {
-                "Name":curPlayer.GetPlayerName(),
-                "Job":curPlayer.GetJob(),
-                "LV":curPlayer.GetLV(),
-                "RealmLV":curPlayer.GetOfficialRank(),
-                "MaxHP":GameObj.GetMaxHP(curPlayer),
-                "FightPower":PlayerControl.GetFightPower(curPlayer),
-                }
-    return infoDict
-
-def DoTrunFightVSPlayer(curPlayer, tagPlayerID, callData, tagPlayerInfo):
-    tagType = ChConfig.TurnBattle_TagType_Player
-    tagID = tagPlayerID
-    mapID, funcLineID, valueList = callData
-    if tagPlayerInfo and curPlayer.GetPlayerID() != tagPlayerID:
-        tick = GameWorld.GetGameWorld().GetTick()
-        DoTrunFight(curPlayer, mapID, funcLineID, tagType, tagID, valueList, tick, tagPlayerInfo)
-    SyncTurnFightState(curPlayer, mapID, funcLineID, tagType, tagID, FightState_Over)
-    return
-
-def DoTrunFight(curPlayer, mapID, funcLineID, tagType, tagID, valueList, tick, tagInfo=None):
-    if not tagID:
-        return
-    if not tagInfo:
-        tagInfo = {}
-    playerID = curPlayer.GetPlayerID()
-    GameWorld.DebugLog("回合战斗: mapID=%s,funcLineID=%s,tagType=%s,tagID=%s,valueList=%s,tagInfo=%s" 
-                       % (mapID, funcLineID, tagType, tagID, valueList, tagInfo), playerID)
-    
-    factionSyncInfoA = __GetPlayerInfo(curPlayer)
-    factionSyncInfoB = tagInfo
-    SyncTurnFightState(curPlayer, mapID, funcLineID, tagType, tagID, FightState_PrepareOK, msg=[factionSyncInfoA, factionSyncInfoB])
-    
-    turnNum, turnMax = 1, 15
-    
-    curFightPower = PlayerControl.GetFightPower(curPlayer)
-    tagFightPower = tagInfo.get("FightPower", 0)
-    isWin = 1 if curFightPower >= tagFightPower else 0
-    GameWorld.DebugLog("    战斗结果: isWin=%s,curFightPower=%s,tagFightPower=%s" % (isWin, curFightPower, tagFightPower), playerID)
-    
-    factionTotalHurtDict = {}
-    
-    playbackID = 0 # 战斗回放ID,可根据该ID查看回放
-    
-    # 战斗结束后处理
-    fightRet = [isWin, turnNum, turnMax, factionTotalHurtDict, playbackID]
-    
-    needSendGameServer, awardItemList, overInfoEx = False, [], {}
-    overRet = FBLogic.OnTurnFightOver(curPlayer, mapID, funcLineID, tagType, tagID, valueList, fightRet)
-    if overRet != None:
-        needSendGameServer, awardItemList, overInfoEx = overRet
-        
-    if needSendGameServer or mapID in ChConfig.Def_TFMapID_SendToGameServer:
-        SendToGameServer_TurnFight(curPlayer, "TurnFightOver", [mapID, funcLineID, tagType, tagID, valueList, fightRet, awardItemList])
-        
-    overMsg = {"isWin":isWin, FBCommon.Over_itemInfo:FBCommon.GetJsonItemList(awardItemList), "totalHurt":0}
-    playbackID and overMsg.update({"playbackID":playbackID})
-    overInfoEx and overMsg.update(overInfoEx)
-    SyncTurnFightState(curPlayer, mapID, funcLineID, tagType, tagID, FightState_Award, turnNum, turnMax, overMsg)
-    return
-
-def SyncTurnFightState(curPlayer, mapID, funcLineID, tagType, tagID, state, turnNum=0, turnMax=0, msg=""):
+def OnOver_MainLevel(curPlayer, isWin, awardItemList):
+    ## 战斗结束额外处理 - 主线关卡
     if not curPlayer:
         return
-    clientPack = ChPyNetSendPack.tagMCTurnFightState()
-    clientPack.Clear()
-    clientPack.MapID = mapID
-    clientPack.FuncLineID = funcLineID
-    clientPack.TagType = tagType
-    clientPack.TagID = tagID
-    clientPack.State = state
-    clientPack.TurnNum = turnNum
-    clientPack.TurnMax = turnMax
-    clientPack.Msg = str(msg)
-    clientPack.Len = len(clientPack.Msg)    
-    NetPackCommon.SendFakePack(curPlayer, clientPack)
+    playerID = curPlayer.GetPlayerID()
+    mainFightMgr = GetMainFightMgr(curPlayer)
+    mainFightMgr.nextTeam = False
+    chapterID, levelNum, wave = PlayerControl.GetMainLevelNowInfo(curPlayer)
+    
+    if not isWin:
+        nextWave = max(1, wave - 1)
+        nowValue = PlayerControl.SetMainLevelNowInfo(curPlayer, chapterID, levelNum, nextWave)
+        GameWorld.DebugLog("主线小怪战斗失败,降一波! chapterID=%s,levelNum=%s,wave=%s,nextWave=%s,nowValue=%s" 
+                           % (chapterID, levelNum, wave, nextWave, nowValue), playerID)
+        return
+    
+    if mainFightMgr.teamNum < mainFightMgr.teamMax:
+        mainFightMgr.nextTeam = True
+        GameWorld.DebugLog("主线小怪战斗胜利,下一小队! chapterID=%s,levelNum=%s,wave=%s,teamNum=%s/%s" 
+                           % (chapterID, levelNum, wave, mainFightMgr.teamNum, mainFightMgr.teamMax), playerID)
+        return
+    
+    # 获胜过波
+    if wave < mainFightMgr.waveMax:
+        nextWave = min(mainFightMgr.waveMax, wave + 1)
+        nowValue = PlayerControl.SetMainLevelNowInfo(curPlayer, chapterID, levelNum, nextWave)
+        GameWorld.DebugLog("主线小怪波战斗胜利,下一波! chapterID=%s,levelNum=%s,wave=%s,nextWave=%s,nowValue=%s" 
+                           % (chapterID, levelNum, wave, nextWave, nowValue), playerID)
+    else:
+        GameWorld.DebugLog("主线小怪波战斗胜利,最后一波循环刷! chapterID=%s,levelNum=%s,wave=%s" % (chapterID, levelNum, wave), playerID)
+        
+    # 小怪可能会退波,所以只在有超过已过关卡进度时才更新值
+    hisPassValue = PlayerControl.GetMainLevelPassValue(curPlayer)
+    curPassValue = PlayerControl.ComMainLevelValue(chapterID, levelNum, wave)
+    if curPassValue > hisPassValue:
+        GameWorld.DebugLog("更新当前过关进度! curPassValue=%s,hisPassValue=%s" % (curPassValue, hisPassValue), playerID)
+        PlayerControl.SetMainLevelPassValue(curPlayer, curPassValue)
+    else:
+        GameWorld.DebugLog("未超过当前过关进度,不更新! curPassValue=%s <= hisPassValue=%s" % (curPassValue, hisPassValue), playerID)
+        
+    return
+
+def OnOver_MainLevelBoss(curPlayer, isWin, awardItemList):
+    ## 战斗结束额外处理 - 主线关卡boss
+    if not curPlayer:
+        return
+    playerID = curPlayer.GetPlayerID()
+    mainFightMgr = GetMainFightMgr(curPlayer)
+    mainFightMgr.nextTeam = False
+    chapterID, levelNum, _ = PlayerControl.GetMainLevelPassInfo(curPlayer)
+    if not isWin:
+        nowValue = PlayerControl.GetMainLevelNowValue(curPlayer)
+        GameWorld.DebugLog("主线boss战斗失败!保持当前刷怪波进度不变! nowValue=%s" % nowValue, playerID)
+        return
+    
+    if mainFightMgr.teamNum < mainFightMgr.teamMax:
+        mainFightMgr.nextTeam = True
+        GameWorld.DebugLog("主线boss小队战斗胜利,下一小队! chapterID=%s,levelNum=%s,teamNum=%s/%s" 
+                           % (chapterID, levelNum, mainFightMgr.teamNum, mainFightMgr.teamMax), playerID)
+        return
+    
+    isAllPass = False # 是否通关
+    if IpyGameDataPY.GetIpyGameDataNotLog("MainLevel", chapterID, levelNum + 1):
+        nextChapterID, nextLevelNum = chapterID, levelNum + 1
+        GameWorld.DebugLog("主线boss波战斗胜利!下一关! chapterID=%s,levelNum=%s,nextLevelNum=%s" 
+                           % (chapterID, levelNum, nextLevelNum), playerID)
+    elif IpyGameDataPY.GetIpyGameDataNotLog("MainLevel", chapterID + 1, 1):
+        nextChapterID, nextLevelNum = chapterID + 1, 1
+        GameWorld.DebugLog("主线boss波战斗胜利!下一章! chapterID=%s,levelNum=%s,nextChapterID=%s,nextLevelNum=%s" 
+                           % (chapterID, levelNum, nextChapterID, nextLevelNum), playerID)
+    else:
+        # 已通关的暂时先保持不变
+        # 注意防范最后一关奖励重复获得
+        nextChapterID, nextLevelNum = chapterID + 1, 1
+        GameWorld.DebugLog("主线boss波战斗胜利!已通关! chapterID=%s,levelNum=%s,nextChapterID=%s,nextLevelNum=%s" 
+                            % (chapterID, levelNum, nextChapterID, nextLevelNum), playerID)
+        isAllPass = True
+        
+    updPassValue = PlayerControl.SetMainLevelPassInfo(curPlayer, nextChapterID, nextLevelNum, 0)
+    if isAllPass:
+        # 已通关的刷怪进度保持不变
+        updNowValue = PlayerControl.GetMainLevelNowValue(curPlayer)
+        GameWorld.DebugLog("已通关的刷怪进度保持不变: updNowValue=%s" % updNowValue, playerID)
+    else:
+        updNowValue = PlayerControl.SetMainLevelNowInfo(curPlayer, nextChapterID, nextLevelNum, 1)
+        GameWorld.DebugLog("为通关的刷怪进度设置为下一关的第1波: updNowValue=%s" % updNowValue, playerID)
+    GameWorld.DebugLog("updPassValue=%s,updNowValue=%s" % (updPassValue, updNowValue), playerID)
+    
+    # 发放过关奖励
+    levelIpyData = IpyGameDataPY.GetIpyGameData("MainLevel", chapterID, levelNum)
+    if not levelIpyData:
+        return
+    itemList = levelIpyData.GetAwardItemList()
+    GameWorld.DebugLog("过关奖励: chapterID=%s,levelNum=%s,itemList=%s" % (chapterID, levelNum, itemList), playerID)
+    
+    ItemControler.GivePlayerItemOrMail(curPlayer, itemList, event=["MainLevelBoss", False, {}], isNotifyAward=False)
+    
+    awardItemList += itemList
+    return
+
+
+#// B4 14 查看战报 #tagCSTurnFightReportView
+#
+#struct    tagCSTurnFightReportView
+#{
+#    tagHead        Head;
+#    char        GUID[40];    //战报guid
+#};
+def OnTurnFightReportView(index, clientData, tick):
     return

--
Gitblit v1.8.0