ServerPython/ZoneServerGroup/map1_8G/MapServer/MapServerData/Script/Attack/TurnAttack.py
@@ -20,30 +20,32 @@
#-------------------------------------------------------------------------------
import ChConfig
import PlayerTask
import PlayerViewCache
import ChPyNetSendPack
import NetPackCommon
import PlayerControl
import GameWorld
import FBCommon
import IpyGameDataPY
import PlayerOnline
import NPCCommon
import ShareDefine
import PyGameData
import ItemControler
import SkillCommon
import BattleObj
import TurnPassive
import TurnSkill
import TurnBuff
import CommFunc
import ObjPool
import FBLogic
import random
import time
import json
TimelineSet = 10000 # 单回合最大时间轴
PosNumMax = 10 # 最大站位编号
PosNumMax = 7 # 最大站位编号
ActionNumStart = -1 # 起始行动位置编号,一般是从1开始,如果有加主公、红颜等则扣除相应位置值,如从0或-1开始
# 回合战斗流程状态
@@ -54,7 +56,8 @@
FightState_FightEnd, # 3 战斗结束
FightState_Award, # 4 结算奖励
FightState_Over, # 5 结束状态,无特殊意义,仅代表所有处理结束了,与Start对应
) = range(6)
FightState_Rest, # 6 结束并休息,一般是前端主动退出并休息的
) = range(7)
    
class BatLineup():
    ## 战斗阵容
@@ -147,44 +150,80 @@
        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.isWin = False
        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.actionIndex = 0 # 行动顺序索引
        self.timeline = 0 # 时间轴节点  turnNum*1000+actionIndex*100++actionNum
        self.startTime = 0 # 开始时间戳,支持毫秒小数
        self.costTime = 0 # 单场战斗总耗时,支持毫秒小数
        # pve 多小队 - 一般只有PVE用到
        self.lineupIndex = 0 # 当前小队索引
        self.lineupIDList = [] # npc小队列表
        self.strongerLV = 0 # npc成长等级
        self.difficulty = 0 # npc难度
        # pvp 目标
        self.tagPlayerID = 0
        self.tagViewCache = None
        return
    
    def setTurn(self, mapID, funcLineID, turnMax, isNeedReport=False, msgDict={}):
    def setTurnFight(self, mapID, funcLineID, turnMax, isNeedReport=False, msgDict={}):
        ## 设置本场回合战斗设定
        self.mapID = mapID
        self.funcLineID = funcLineID
        self.turnMax = turnMax # 最大回合数
        self.isNeedReport = isNeedReport
        self.setPVE()
        self.setPVP()
        self.msgDict = {}
        self.resetTurn(msgDict)
        self.nextTurnFight(msgDict)
        return
    
    def resetTurn(self, msgDict):
    def setPVE(self, lineupIDList=[], strongerLV=0, difficulty=0):
        self.lineupIndex = 0
        self.lineupIDList = lineupIDList
        self.strongerLV = strongerLV
        self.difficulty = difficulty
        return
    def setPVP(self, tagPlayerID=0, tagViewCache=None):
        self.tagPlayerID = tagPlayerID
        self.tagViewCache = tagViewCache
        return
    #def setPVPTeam(self):
    #    return
    def nextTurnFight(self, msgDict={}, resetByNextTeam=False):
        ## 一般用于玩家发起的战斗,在需要保留玩家阵容属性及状态的情况下,重置回合进入下一场战斗
        self.turnNum = 1
        self.enterLogic = False
        self.turnStart = 0
        self.turnEnd = 0
        self.winFaction = 0
        self.batBuffer = "" # 战报buffer
        self.isWin = False
        self.msgDict.update(msgDict)
        self.timeline = 0
        self.startTime = time.time()
        self.costTime = 0
        if resetByNextTeam:
            ResetByNextTeam(self)
        return
    def haveNextLineup(self):
        ## 是否可以打下一小队,获胜且还有下一小队时可打
        return (self.winFaction == ChConfig.Def_FactionA and self.lineupIndex < len(self.lineupIDList) - 1)
    def nextLineupID(self):
        ## 下一小队ID
        if not self.haveNextLineup():
            return 0
        self.lineupIndex += 1
        return self.lineupIDList[self.lineupIndex]
    
    def setFactionLineup(self, faction, lineupDict):
        ## 设置阵营阵容
@@ -219,21 +258,20 @@
        GameWorld.DebugLog("阵容行动顺序[f, n]: %s" % self.actionSortList)
        return
    
    def getTurnNumStartTimelin(self, turnNum): return turnNum * TimelineSet + 0 # 每回合的时间节点起点
    def getTimeline(self): return self.timeline
    def setTimeline(self, turnNum):
    def setTimeline(self, timeline, isEmpty=False):
        '''回合战斗的时间轴节点 ,即第几回合开始,每个回合支持9999个行动节点
        @param turnNum: 第x回合
        '''
        self.timeline = turnNum * TimelineSet + 0
        self.timeline = timeline
        GameWorld.DebugLog("时间节点更新: %s" % self.timeline)
        if isEmpty:
            # 空位置的节点可直接跳过
            return timeline
        OnTimelineChange(self)
        return
    def addTimeline(self):
        ## 每切换一个行动单位可视为一个行动节点,即代表单回合战斗中的某一个时间节点
        self.timeline += 1
        GameWorld.DebugLog("时间节点更新: %s" % self.timeline)
        OnTimelineChange(self)
        return
        return timeline
    
    def getBatFaction(self, faction=ChConfig.Def_FactionA):
        ## 默认阵营1
@@ -276,9 +314,9 @@
            
        return 0
    
    def exitFight(self):
    def exitFight(self, rest=False):
        ## 退出战斗
        self.syncState(FightState_Over)
        self.syncState(FightState_Over if not rest else FightState_Rest)
        for batFaction in self.factionDict.values():
            batFaction.clearLineups()
        self.state = -1
@@ -287,7 +325,8 @@
    def startFight(self):
        ## 准备就绪,开始战斗
        self.state = FightState_Start
        self.setTimeline(1)
        self.turnNum = 1
        self.timeline = self.getTurnNumStartTimelin(self.turnNum)
        self.syncInit()
        return
    
@@ -373,14 +412,20 @@
            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)
        else:
            packBuff = clientPack.GetBuffer()
            buffLen = len(packBuff)
            GameWorld.DebugLog("回合战斗过程封包: %s, len:%s" % (headStr, buffLen))
            self.batBuffer += CommFunc.WriteWORD("", buffLen)
            self.batBuffer += packBuff
            ObjPool.GetPoolMgr().release(clientPack)
        else:
            GameWorld.DebugLog("回合战斗过程封包: %s" % (headStr))
            # 有玩家的统一每个包单独发送,同样也支持战报统计
            if self.curPlayer:
                NetPackCommon.SendFakePack(self.curPlayer, clientPack)
            else:
                ObjPool.GetPoolMgr().release(clientPack)
        return
    
class TurnFightMgr():
@@ -390,8 +435,10 @@
        self.turnFightDict = {} # {guid:TurnFight, ...}
        return
    
    def addTurnFight(self, mapID, funcLineID=0, playerID=0):
        tf = TurnFight(mapID, funcLineID, playerID, False)
    def addTurnFight(self, mapID, funcLineID=0, playerID=0, isNeedReport=False):
        tf = ObjPool.GetPoolMgr().acquire(TurnFight, mapID, funcLineID, playerID, isNeedReport)
        if not tf:
            tf = TurnFight(mapID, funcLineID, playerID, isNeedReport) # 一般是不可能,为了点出代码
        self.turnFightDict[tf.guid] = tf
        return tf
    
@@ -401,6 +448,7 @@
            return
        turnFight.exitFight()
        self.turnFightDict.pop(guid, None)
        ObjPool.GetPoolMgr().release(turnFight)
        return
    
    def getTurnFight(self, guid):
@@ -429,10 +477,6 @@
        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
    
@@ -455,10 +499,30 @@
                          % (chapterID, levelNum, nowChapterID, fixNowValue), curPlayer.GetPlayerID())
    return
def GetPlayerLineupInfoByCache(playerID, lineupID):
    ## 获取玩家阵容信息 - 根据玩家查看缓存
    lineupInfo = {}
def GetCacheLineupFightPower(tagViewCache, lineupID):
    lineupInfo = GetCacheLineupInfo(tagViewCache, lineupID)
    return lineupInfo.get("FightPower", 0)
def GetCacheLineupInfo(tagViewCache, lineupID):
    ## 根据查看缓存获取阵容信息,一般是 GetPlayerLineupInfo 返回的结果
    plusDict = tagViewCache.GetPlusDict()
    lineupDict = plusDict.get("Lineup", {})
    lineupInfo = lineupDict.get("%s" % lineupID, {})
    if not lineupInfo:
        lineupInfo = lineupDict.get("%s" % ShareDefine.Lineup_Main, {})
    return lineupInfo
def GetPlayerLineupFightPower(curPlayer, lineupID):
    ## 获取玩家阵容战力,一般用于直接获取阵容战力记录用
    return GetPlayerLineup(curPlayer, lineupID).fightPower
def GetPlayerLineup(curPlayer, lineupID):
    ## 获取玩家阵容
    olPlayer = PlayerOnline.GetOnlinePlayer(curPlayer)
    lineup = olPlayer.GetLineup(lineupID)
    if not lineup.lineupHeroDict:
        # 为空时默认取主阵容
        lineup = olPlayer.GetLineup(ShareDefine.Lineup_Main)
    return lineup
def GetPlayerLineupInfo(curPlayer, lineupID):
    ## 获取玩家阵容信息,可用于战斗或查看缓存,因为可能取玩家的缓存进行对战,所以统一使用json格式,前端通用
@@ -466,9 +530,10 @@
    # @return: 阵容全部信息json字典,前端通用格式
    
    playerID = curPlayer.GetPlayerID()
    lineup = PlayerOnline.GetOnlinePlayer(curPlayer).GetLineup(lineupID)
    lineup = GetPlayerLineup(curPlayer, lineupID)
    if not lineup.lineupHeroDict:
        return {}
    
    lineupInfo = {"PlayerID":playerID, "FightPower":lineup.fightPower, "ShapeType":lineup.shapeType}
    heroDict = {}
    curPack = curPlayer.GetItemManager().GetPack(ShareDefine.rptHero)
    for posNum in lineup.lineupHeroDict.keys():
@@ -494,13 +559,18 @@
                                 "AttrDict":{str(k):v for k, v in hero.heroBatAttrDict.items() if v > 0},
                                 "SkillIDList":skillIDlist,
                                 }
    lineupInfo.update({"Hero":heroDict})
    if not heroDict:
        return {}
    
    lineupInfo = {"PlayerID":playerID, "FightPower":lineup.fightPower, "ShapeType":lineup.shapeType, "Hero":heroDict}
    return lineupInfo
def GetNPCLineupInfo(lineupID):
def GetNPCLineupInfo(lineupID, strongerLV=0, difficulty=0):
    ## 获取NPC阵容信息
    # @param lineupID: 阵容ID
    # @param npcLV: 成长NPC等级
    # @param difficulty: 成长NPC难度系数
    # @return: 阵容全部信息json字典,前端通用格式
    ipyData = IpyGameDataPY.GetIpyGameData("NPCLineup", lineupID)
    if not ipyData:
@@ -513,9 +583,59 @@
        npcID = getattr(ipyData, "GetPosNPCID%s" % posNum)()
        if not npcID:
            continue
        npcData = NPCCommon.GetNPCDataPy(npcID)
        if not npcData:
        battleDict = GetNPCBattleDict(ipyData, npcID, strongerLV, difficulty)
        if not battleDict:
            continue
        heroDict[str(posNum)] = battleDict
    lineupInfo = {"NPCLineupID":lineupID, "Hero":heroDict}
    return lineupInfo
def GetNPCBattleDict(lineupIpyData, npcID, strongerLV=0, difficulty=0):
    ## 获取NPC战斗相关字典,支持成长NPC
    # @param strongerLV: 成长等级
    # @param difficulty: 难度系数
    npcData = NPCCommon.GetNPCDataPy(npcID)
    if not npcData:
        return
    heroID = npcData.GetRelatedHeroID()
    npcLV = npcData.GetLV()
    lvIpyData = None
    heroIpyData = IpyGameDataPY.GetIpyGameData("Hero", heroID) if heroID else None
    npcStronger = IpyGameDataPY.GetIpyGameDataNotLog("NPCStronger", npcID)
    if npcStronger and strongerLV:
        lvIpyData = IpyGameDataPY.GetIpyGameData("PlayerLV", strongerLV)
        if lvIpyData:
            npcLV = strongerLV
    if not lvIpyData:
        lvIpyData = IpyGameDataPY.GetIpyGameData("PlayerLV", npcLV)
    if heroIpyData and lvIpyData:
        skinIDList = heroIpyData.GetSkinIDList()
        skinID = skinIDList[0] if skinIDList else 0
        skillIDList = GetNPCHeroSkillIDList(heroID, heroIpyData, lvIpyData.GetReHeroBreakLV(), lvIpyData.GetReHeroAwakeLV())
    else:
        heroID = 0
        skinID = 0
        skillIDList = [] + npcData.GetSkillIDList()
    # boss额外随机技能
    bossID = lineupIpyData.GetBossID()
    if npcID == bossID:
        skillIDExList = lineupIpyData.GetSkillIDExList()
        if skillIDExList:
            randSkillIDExList = [] + list(skillIDExList)
            skillExCnt = lineupIpyData.GetSkillExCnt()
            if skillExCnt > 0 and len(randSkillIDExList) > skillExCnt:
                random.shuffle(randSkillIDExList)
                randSkillIDExList = randSkillIDExList[:skillExCnt]
            skillIDList += randSkillIDExList
            GameWorld.DebugLog("阵容boss技能: %s, 随机附加技能: %s" % (skillIDList, randSkillIDExList))
    # 成长怪属性
    batAttrDict = GetNPCStrongerAttrDict(npcID, lvIpyData, npcStronger, difficulty)
    if not batAttrDict:
        batAttrDict = {ChConfig.AttrID_Atk:npcData.GetAtk(), ChConfig.AttrID_Def:npcData.GetDef(), ChConfig.AttrID_MaxHP:npcData.GetMaxHP(), 
                       ChConfig.AttrID_FinalDamPer:npcData.GetFinalDamPer(), ChConfig.AttrID_FinalDamPerDef:npcData.GetFinalDamPerDef(), 
                       ChConfig.AttrID_MissRate:npcData.GetMissRate(), ChConfig.AttrID_MissRateDef:npcData.GetMissRateDef(), 
@@ -526,14 +646,68 @@
                       ChConfig.AttrID_SuckHPPer:npcData.GetSuckHPPer(), ChConfig.AttrID_SuckHPPerDef:npcData.GetSuckHPPerDef(), 
                       }
        batAttrDict.update(npcData.GetSpecAttrInfo())
        skillIDList = [] + npcData.GetSkillIDList()
        heroDict[str(posNum)] = {"NPCID":npcID,
                                 "AttrDict":{str(k):v for k, v in batAttrDict.items() if v > 0},
                                 "SkillIDList":skillIDList
                                 }
        
    lineupInfo = {"NPCLineupID":lineupID, "Hero":heroDict}
    return lineupInfo
    battleDict = {"NPCID":npcID,
                  "HeroID":heroID,
                  "SkinID":skinID,
                  "LV":npcLV,
                  "AttrDict":{str(k):v for k, v in batAttrDict.items() if v > 0},
                  "SkillIDList":skillIDList,
                  }
    GameWorld.DebugLog("GetNPCBattleDict npcID=%s,strongerLV=%s,difficulty=%s,%s" % (npcID, strongerLV, difficulty, battleDict))
    return battleDict
def GetNPCHeroSkillIDList(heroID, heroIpyData, breakLV, awakeLV):
    ## 获取NPC对应武将的技能ID列表
    normalSkillID = heroIpyData.GetNormalSkillID()
    angerSkillID = heroIpyData.GetAngerSkillID()
    skillIDList = [normalSkillID, angerSkillID]
    breakIpyDataList = IpyGameDataPY.GetIpyGameDataList("HeroBreak", heroID)
    if breakIpyDataList:
        for breakIpyData in breakIpyDataList:
            if breakIpyData.GetBreakLV() > breakLV:
                break
            skillID = breakIpyData.GetSkillID()
            if skillID:
                skillIDList.append(skillID)
    awakeIpyDataList = IpyGameDataPY.GetIpyGameDataListNotLog("HeroAwake", heroID)
    if awakeIpyDataList:
        for awakeIpyData in awakeIpyDataList:
            if awakeIpyData.GetAwakeLV() > awakeLV:
                break
            skillID = awakeIpyData.GetSkillID()
            if skillID:
                skillIDList.append(skillID)
    return skillIDList
def GetNPCStrongerAttrDict(npcID, lvIpyData, npcStronger, difficulty):
    ## 获取NPC成长属性
    # @param strongerLV: 成长等级
    # @param difficulty: 难度系数
    batAttrDict = {}
    if not lvIpyData or not npcStronger or not difficulty:
        return batAttrDict
    lv = lvIpyData.GetLV()
    for attrID in ChConfig.CalcBattleAttrIDList:
        attrIpyData = IpyGameDataPY.GetIpyGameData("PlayerAttr", attrID)
        if not attrIpyData:
            continue
        attrName = attrIpyData.GetParameter()
        if not hasattr(lvIpyData, "GetRe%s" % attrName):
            continue
        reValue = getattr(lvIpyData, "GetRe%s" % attrName)() # 基础参考值
        ratio = getattr(npcStronger, "Get%sRatio" % attrName)() if hasattr(npcStronger, "Get%sRatio" % attrName) else 1 # 属性系数
        attrValue = int(reValue * ratio * difficulty)
        batAttrDict[attrID] = attrValue
        #GameWorld.DebugLog("    attrID=%s,attrValue=%s,reValue=%s,ratio=%s,difficulty=%s" % (attrID, attrValue, reValue, ratio, difficulty))
    GameWorld.DebugLog("NPC成长属性: npcID=%s,lv=%s,difficulty=%s,%s" % (npcID, lv, difficulty, batAttrDict))
    return batAttrDict
def SummonLineupObjs(batLineup, faction, num, lineupInfo, playerID=0):
    '''召唤阵容战斗实例
@@ -554,41 +728,50 @@
    
    batObjMgr = BattleObj.GetBatObjMgr()
    initXP = IpyGameDataPY.GetFuncCfg("AngerXP", 1)
    atkBackSkillIDList = IpyGameDataPY.GetFuncEvalCfg("ParryCfg", 2)
    for posNumKey, heroInfo in heroDict.items():
        posNum = int(posNumKey)
        
        npcID, heroID, skinID = 0, 0, 0
        atkBackSkillID = 0 # 反击技能ID
        fightPower = 0
        skillIDList = [] # 战斗对象可能改变属性或技能,重新创建,防止误修改来源值
        attrDict = {}
        skillIDList += heroInfo.get("SkillIDList", [])
        attrDict.update(heroInfo.get("AttrDict", {}))
        objName = ""
        if lineupPlayerID:
            heroID = heroInfo.get("HeroID", 0)
            skinID = heroInfo.get("SkinID", 0)
            lv = heroInfo.get("LV", 1)
            fightPower = heroInfo.get("FightPower", 0)
            heroIpyData = IpyGameDataPY.GetIpyGameData("Hero", heroID)
            if not heroIpyData:
                continue
        npcID = heroInfo.get("NPCID", 0)
        heroID = heroInfo.get("HeroID", 0)
        skinID = heroInfo.get("SkinID", 0)
        lv = heroInfo.get("LV", 1)
        heroIpyData = IpyGameDataPY.GetIpyGameData("Hero", heroID) if heroID else None
        if heroIpyData:
            specialty = heroIpyData.GetSpecialty()
            atkDistType = heroIpyData.GetAtkDistType()
            objName = heroIpyData.GetName()
            country = heroIpyData.GetCountry()
            sex = heroIpyData.GetSex()
        if lineupPlayerID:
            fightPower = heroInfo.get("FightPower", 0)
            if not heroIpyData:
                continue
        else:
            npcID = heroInfo.get("NPCID", 0)
            npcDataEx = NPCCommon.GetNPCDataPy(npcID)
            if not npcDataEx:
                continue
            objName = npcDataEx.GetNPCName()
            atkDistType = npcDataEx.GetAtkDistType()
            lv = npcDataEx.GetLV()
            if not heroIpyData:
                specialty = 0
                atkDistType = npcDataEx.GetAtkDistType()
                objName = npcDataEx.GetNPCName()
                country = npcDataEx.GetCountry()
                sex = npcDataEx.GetSex()
        batObj = batObjMgr.addBatObj()
        if not batObj:
            break
        objID = batObj.GetID()
        if npcID:
            batObj.SetNPCID(npcID)
        elif lineupPlayerID:
            batObj.SetOwnerID(lineupPlayerID)
        batObj.SetTFGUID(tfGUID)
        batObj.SetName(objName)
        batObj.SetFaction(faction)
@@ -596,34 +779,200 @@
        batObj.SetFightPower(fightPower)
        batObj.SetLV(lv)
        batObj.SetAtkDistType(atkDistType)
        if npcID:
            batObj.SetNPCID(npcID)
        elif lineupPlayerID:
            batObj.SetOwnerHero(lineupPlayerID, heroID, skinID)
        if atkDistType == ChConfig.AtkDistType_Short:
            atkBackSkillID = atkBackSkillIDList[0] if len(atkBackSkillIDList) > 0 else 0
        elif atkDistType == ChConfig.AtkDistType_Long:
            atkBackSkillID = atkBackSkillIDList[1] if len(atkBackSkillIDList) > 1 else 0
        if atkBackSkillID:
            skillIDList.append(atkBackSkillID)
        batObj.SetSpecialty(specialty)
        batObj.SetCountry(country)
        batObj.SetSex(sex)
        batObj.SetHero(heroID, skinID)
        skillManager = batObj.GetSkillManager()
        skillManager.SkillReset()
        for skillID in skillIDList:
            skillManager.LearnSkillByID(skillID)
            
        batLineup.posObjIDDict[posNum] = objID
        GameWorld.DebugLog("AddBatObj ID:%s,%s,skill=%s" % (objID, GetObjName(batObj), skillIDList))
        GameWorld.DebugLog("AddBatObj %s,skill=%s" % (GetObjName(batObj), skillManager.GetSkillIDList()))
        batObj.InitBatAttr({int(k):v for k, v in attrDict.items()}, initXP)
        
    return
def ResetByNextTeam(turnFight):
    ## 切换下一小队时重置相关,目前设置仅保留血量、怒气、已被击杀的不复活,其他重置
    batFaction = turnFight.getBatFaction(ChConfig.Def_FactionA)
    if not batFaction:
        return
    GameWorld.DebugLog("切换小队重置玩家阵容武将...")
    batLineup = batFaction.getBatlineup(1) # 只处理玩家阵容
    batObjMgr = BattleObj.GetBatObjMgr()
    for objID in batLineup.posObjIDDict.values():
        batObj = batObjMgr.getBatObj(objID)
        if not batObj:
            continue
        objName = GetObjName(batObj)
        if not batObj.IsAlive():
            GameWorld.DebugLog("    已被击杀不处理! %s" % (objName))
            continue
        GameWorld.DebugLog("    重置武将: %s, HP:%s/%s, XP:%s" % (objName, batObj.GetHP(), batObj.GetMaxHP(), batObj.GetXP()))
        batObj.TurnReset()
        # 清除buff
        buffMgr = batObj.GetBuffManager()
        buffMgr.ClearBuff()
        # 重置CD
        # 重刷属性、被动
        TurnBuff.RefreshBuffAttr(batObj)
        TurnPassive.RefreshPassive(batObj)
    return
#// B4 10 回合制战斗 #tagCMTurnFight
#
#struct    tagCMTurnFight
#{
#    tagHead        Head;
#    DWORD        MapID;        // 自定义地图ID,可用于绑定战斗地图场景功能(如主线boss、爬塔、竞技场等)
#    DWORD        FuncLineID;    // MapID对应的扩展值,如具体某个关卡等
#    BYTE        TagType;        // 目标类型,0-NPC阵容,1-玩家
#    DWORD        TagID;        // 目标类型对应的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
    GameWorld.DebugLog("回合制战斗请求: mapID=%s,funcLineID=%s,tagType=%s,tagID=%s,valueList=%s"
                       % (mapID, funcLineID, tagType, tagID, valueList), curPlayer.GetPlayerID())
    reqRet = FBLogic.OnTurnFightRequest(curPlayer, mapID, funcLineID, tagType, tagID, valueList)
    if not reqRet:
        return
    # 攻防方所使用的阵容ID
    atkLineupID, defLineupID = FBLogic.GetFBPlayerLineupID(curPlayer, mapID, funcLineID)
    if atkLineupID not in ShareDefine.LineupList or defLineupID not in ShareDefine.LineupList:
        return
    # 玩家
    if tagType == 1:
        OnTurnFightVSPlayer(curPlayer, mapID, funcLineID, atkLineupID, defLineupID, tagID)
    # NPC
    else:
        ret = FBLogic.GetFBNPCLineupInfo(curPlayer, mapID, funcLineID)
        if not ret:
            return
        npcLineupIDList, strongerLV, difficulty = ret
        OnTurnFightVSNPC(curPlayer, mapID, funcLineID, atkLineupID, npcLineupIDList, strongerLV, difficulty)
    return
def OnTurnFightVSNPC(curPlayer, mapID, funcLineID, atkLineupID, npcLineupIDList, strongerLV, difficulty):
    playerID = curPlayer.GetPlayerID()
    GameWorld.DebugLog("OnTurnFightVSNPC: mapID=%s,funcLineID=%s,atkLineupID=%s,npcLineupIDList=%s,strongerLV=%s,difficulty=%s"
                       % (mapID, funcLineID, atkLineupID, npcLineupIDList, strongerLV, difficulty), playerID)
    if not npcLineupIDList:
        return
    lineupMainInfo = GetPlayerLineupInfo(curPlayer, atkLineupID)
    if not lineupMainInfo:
        GameWorld.DebugLog("玩家没有该阵容数据! atkLineupID=%s" % atkLineupID, playerID)
        return
    turnMax = GetTurnMax(mapID)
    if mapID == ChConfig.Def_FBMapID_MainBoss:
        # 停止主线小怪战斗、清空
        mainFightMgr = GetMainFightMgr(curPlayer)
        mainTF = mainFightMgr.turnFight
        if mainTF.isInFight():
            mainTF.exitFight()
    tfMgr = GetTurnFightMgr()
    turnFight = tfMgr.addTurnFight(mapID, funcLineID, playerID)
    guid = turnFight.guid
    turnFight.setTurnFight(mapID, funcLineID, turnMax, True)
    turnFight.setPVE(npcLineupIDList, strongerLV, difficulty)
    turnFight.setFactionLineup(ChConfig.Def_FactionA, {1:lineupMainInfo})
    for index, lineupID in enumerate(npcLineupIDList):
        turnFight.lineupIndex = index
        GameWorld.DebugLog("对战NPC阵容: index=%s, lineupID=%s" % (index, lineupID))
        if index > 0:
            turnFight.nextTurnFight(resetByNextTeam=True)
        turnFight.setFactionLineup(ChConfig.Def_FactionB, {1:GetNPCLineupInfo(lineupID, strongerLV, difficulty)})
        turnFight.sortActionQueue()
        turnFight.startFight()
        __processTurnFight(turnFight.guid)
        if not turnFight.isWin:
            break
    PlayerOnline.GetOnlinePlayer(curPlayer).SetLastBatBuffer(guid, turnFight.batBuffer)
    SyncTurnFightReport(curPlayer, guid, turnFight.batBuffer)
    tfMgr.delTurnFight(guid)
    return
def OnTurnFightVSPlayer(curPlayer, mapID, funcLineID, atkLineupID, defLineupID, tagPlayerID):
    playerID = curPlayer.GetPlayerID()
    GameWorld.DebugLog("OnTurnFightVSPlayer: mapID=%s,funcLineID=%s,atkLineupID=%s,defLineupID=%s,tagPlayerID=%s"
                       % (mapID, funcLineID, atkLineupID, defLineupID, tagPlayerID), playerID)
    atkLineupInfo = GetPlayerLineupInfo(curPlayer, atkLineupID)
    if not atkLineupInfo:
        GameWorld.DebugLog("玩家没有该阵容数据! atkLineupID=%s" % atkLineupID, playerID)
        return
    tagViewCache = PlayerViewCache.FindViewCache(tagPlayerID)
    if not tagViewCache:
        GameWorld.DebugLog("目标玩家没有缓存数据! tagPlayerID=%s" % tagPlayerID, playerID)
        return {}
    defLineupInfo = GetCacheLineupInfo(tagViewCache, defLineupID)
    if not defLineupInfo:
        GameWorld.DebugLog("目标玩家没有该阵容数据! tagPlayerID=%s,defLineupID=%s" % (tagPlayerID, defLineupID), playerID)
        return
    turnMax = GetTurnMax(mapID)
    tfMgr = GetTurnFightMgr()
    turnFight = tfMgr.addTurnFight(mapID, funcLineID, playerID)
    guid = turnFight.guid
    turnFight.setTurnFight(mapID, funcLineID, turnMax, True)
    turnFight.setPVP(tagPlayerID, tagViewCache)
    turnFight.setFactionLineup(ChConfig.Def_FactionA, {1:atkLineupInfo})
    turnFight.setFactionLineup(ChConfig.Def_FactionB, {1:defLineupInfo})
    turnFight.sortActionQueue()
    turnFight.startFight()
    __processTurnFight(turnFight.guid)
    PlayerOnline.GetOnlinePlayer(curPlayer).SetLastBatBuffer(guid, turnFight.batBuffer)
    SyncTurnFightReport(curPlayer, guid, turnFight.batBuffer)
    tfMgr.delTurnFight(guid)
    return
def GetTurnMax(mapID):
    if mapID == ChConfig.Def_FBMapID_Main:
        return IpyGameDataPY.GetFuncCfg("TurnMax", 1)
    mapTurnMaxDict= IpyGameDataPY.GetFuncEvalCfg("TurnMax", 3, {})
    return mapTurnMaxDict.get(mapID, IpyGameDataPY.GetFuncCfg("TurnMax", 2))
#// B4 13 主线战斗请求 #tagCSMainFightReq
#
#struct    tagCSMainFightReq
#{
#    tagHead        Head;
#    BYTE        ReqType;        // 0-停止战斗回城;1-设置消耗倍值;2-挑战关卡小怪;3-挑战关卡boss;4-继续战斗;
#    BYTE        ReqType;        // 0-停止战斗回城;1-设置消耗倍值;2-挑战关卡小怪;4-继续战斗;
#    DWORD        ReqValue;    // 请求值,ReqType为1时发送消耗倍值
#};
def OnMainFightReq(index, clientData, tick):
@@ -638,14 +987,13 @@
        __doSetFightPoint(curPlayer, reqValue)
        return
    
    GameWorld.DebugLog("主线战斗请求: reqType=%s" % reqType, curPlayer.GetPlayerID())
    clientPack = ChPyNetSendPack.tagSCTurnFightReportSign()
    clientPack.Sign = 0
    NetPackCommon.SendFakePack(curPlayer, clientPack) # 标记开始
    
    if reqType == 2: # 前端主动请求开始关卡小怪的视为从休息中开始
        __doMainLevelWave(curPlayer, True)
    elif reqType == 3:
        __doMainBossStart(curPlayer)
    elif reqType == 4:
        __doMainFight(curPlayer, tick)
    else:
@@ -661,7 +1009,7 @@
    mainFightMgr = GetMainFightMgr(curPlayer)
    turnFight = mainFightMgr.turnFight
    if turnFight:
        turnFight.exitFight()
        turnFight.exitFight(True)
    return
def __doSetFightPoint(curPlayer, fightPoint):
@@ -685,9 +1033,6 @@
    # @param isRestStart: 是否从休息状态重新开始的
    playerID = curPlayer.GetPlayerID()
    chapterID, levelNum, wave = PlayerControl.GetMainLevelNowInfo(curPlayer)
    if not chapterID and not levelNum:
        PlayerControl.SetMainLevelNowInfo(curPlayer, 1, 1, 1)
        chapterID, levelNum, wave = PlayerControl.GetMainLevelNowInfo(curPlayer)
    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):
@@ -716,287 +1061,184 @@
    if wave > waveMax:
        wave = waveMax
        
    waveLineupList = getattr(levelIpyData, "GetWaveLineupIDList%s" % wave)() # 小队1阵容ID|小队2阵容ID|...
    if not waveLineupList:
    lineupIDList = getattr(levelIpyData, "GetWaveLineupIDList%s" % wave)() # 小队1阵容ID|小队2阵容ID|...
    if not lineupIDList:
        return
    teamMax = len(waveLineupList)
    teamNum = 1
    lineupID = waveLineupList[teamNum - 1] # NPC阵容ID
    lineupID = lineupIDList[0] # NPC阵容ID
    
    lineupMainInfo = GetPlayerLineupInfo(curPlayer, ShareDefine.Lineup_Main)
    if not lineupMainInfo:
        GameWorld.DebugLog("没有设置主阵容!", playerID)
        return
    
    strongerLV = levelIpyData.GetNPCLV()
    difficulty = levelIpyData.GetDifficulty()
    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)
    turnMax = GetTurnMax(mapID)
    GameWorld.DebugLog("设置起始关卡波: 关卡%s-%s,波=%s/%s,lineupIDList=%s,mapID=%s,funcLineID=%s,lineupID=%s,strongerLV=%s,difficulty=%s"
                       % (chapterID, levelNum, wave, waveMax, lineupIDList, mapID, funcLineID, lineupID, strongerLV, difficulty), playerID)
    
    turnFight = mainFightMgr.turnFight
    turnFight.setTurn(mapID, funcLineID, turnMax, False, {"teamNum":teamNum, "teamMax":teamMax})
    turnFight.setTurnFight(mapID, funcLineID, turnMax, False)
    turnFight.setPVE(lineupIDList, strongerLV, difficulty)
    turnFight.setFactionLineup(ChConfig.Def_FactionA, {1:lineupMainInfo})
    turnFight.setFactionLineup(ChConfig.Def_FactionB, {1:GetNPCLineupInfo(lineupID)})
    turnFight.sortActionQueue()
    turnFight.startFight()
    return
def __doMainBossStart(curPlayer):
    ## 开始挑战关卡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固定只有一波
    lineupMainInfo = GetPlayerLineupInfo(curPlayer, ShareDefine.Lineup_Main)
    if not lineupMainInfo:
        GameWorld.DebugLog("没有设置主阵容!", playerID)
        return
    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(ChConfig.Def_FactionA, {1:lineupMainInfo})
    turnFight.setFactionLineup(ChConfig.Def_FactionB, {1:GetNPCLineupInfo(lineupID)})
    turnFight.setFactionLineup(ChConfig.Def_FactionB, {1:GetNPCLineupInfo(lineupID, strongerLV, difficulty)})
    turnFight.sortActionQueue()
    turnFight.startFight()
    
    # 挑战boss无中间过程,每次执行直接挑战一队结果
    __processTurnFight(turnFight.guid)
    __doMainFight(curPlayer)
    return
def __doMainFight(curPlayer, tick):
    ## 主线执行战斗
def __doMainFight(curPlayer, tick=0):
    '''执行主线战斗,单场战斗断电式战斗,以前端玩家手动点击节点做为断点处
    每场战斗的初始化、结束默认断点,由前端决定自动继续或者点击继续
    '''
    
    # 限制请求CD
    #if not GameWorld.GetGameWorld().GetDebugLevel():
    key = "MainFightReqTick"
    lastTick = curPlayer.GetDictByKey(key)
    if lastTick and tick - lastTick <= 1000:
        GameWorld.DebugLog("主线战斗请求CD中")
        return
    curPlayer.SetDict(key, tick)
    if tick:
        key = "MainFightReqTick"
        lastTick = curPlayer.GetDictByKey(key)
        if lastTick and tick - lastTick <= 1000:
            GameWorld.DebugLog("主线战斗请求CD中")
            return
        curPlayer.SetDict(key, 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):
                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(ChConfig.Def_FactionB, {1:GetNPCLineupInfo(lineupID)})
            turnFight.sortActionQueue()
            turnFight.startFight()
            if mainFightMgr.isLevelBoss():
                # 每次处理一小队的完整战斗,相当于一次完整战报
                __processTurnFight(turnFight.guid)
                return
        else:
            __doMainLevelWave(curPlayer, False)
        return
    if isLevelBoss:
        ## 关卡boss是一次性处理完的,一般不可能走到这里,这边做下防范
        return
    
    if not turnFight.isInFight():
        __doMainLevelWave(curPlayer, True)
        return
    winFaction = turnFight.winFaction
    if winFaction:
        nextLineupID = turnFight.nextLineupID()
        if nextLineupID:
            GameWorld.DebugLog("---开始进入下一小队: lineupIndex=%s,nextLineupID=%s,%s" % (turnFight.lineupIndex, nextLineupID, turnFight.lineupIDList))
            turnFight.nextTurnFight(resetByNextTeam=True)
            # 切换小队时,玩家阵容不需要处理,保留状态
            turnFight.setFactionLineup(ChConfig.Def_FactionB, {1:GetNPCLineupInfo(nextLineupID, turnFight.strongerLV, turnFight.difficulty)})
            turnFight.sortActionQueue()
            turnFight.startFight()
            __doMainFight(curPlayer)
        else:
            __doMainLevelWave(curPlayer, False)
        return
    # 小怪战斗,每次消耗1个战锤
    fightPoint = max(curPlayer.GetFightPoint(), 1) # 主线战斗消耗倍值,默认1
    if not PlayerControl.HaveMoney(curPlayer, ShareDefine.TYPE_Price_Xiantao, fightPoint):
        GameWorld.DebugLog("回合开始时战锤不足!")
        return
    
    # 以下均是处理关卡小怪分段实时战斗
    EntryLogic(turnFight)
    isEntry = EntryLogic(turnFight)
    
    # 按阵营阵容执行顺序,逐个遍历
    doCnt = 0
    doMax = (PosNumMax + 2) * len(turnFight.actionSortList) # 防止死循环,做最大循环次数限制 = (最大位置数 + 主公、红颜位置)*行动阵容数
    overLineupList = [] # 本回合已经结束行动的阵容列表 [(faction, num), ...], 所有阵容全部结束代表本回合结束
    # 是否开始检查断点,预判可断的点,方便前端点击体验,点下去就是玩家放的某个行动
    # 初始开始进场后,默认开始断点
    checkBreakpoint = True if isEntry else False
    
    batObjMgr = BattleObj.GetBatObjMgr()
    turnNum = turnFight.turnNum
    GameWorld.DebugLog("turnNum=%s,doMax=%s,actionIndex=%s,%s" % (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
            turnFight.addTimeline() # 每回合开始算一个时间节点
    turnMax = turnFight.turnMax
    for turnNum in range(turnNum, turnMax + 1):
        turnTimeline = turnFight.getTurnNumStartTimelin(turnNum) # 本回合起始时间节点
        curTimeline = turnFight.getTimeline()
        # 回合开始
        turnTimeline += 1 # 每回合开始算一个时间节点
        if curTimeline < turnTimeline:
            curTimeline = turnFight.setTimeline(turnTimeline)
            GameWorld.DebugLog("【----- 回合制战斗轮次: %s -----】 curTimeline=%s" % (turnNum, curTimeline))
            turnFight.turnNum = 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 = ActionNumStart
                batLineup.actionNum = 1
                for objID in batLineup.posObjIDDict.values():
                    batObj = batObjMgr.getBatObj(objID)
                    TurnFightPerTurnBigStart(turnFight, batObj, turnNum)
                    
        if turnFight.actionIndex >= len(turnFight.actionSortList):
            turnFight.actionIndex = 0
        playerHeroAtk = False # 玩家阵容行动标记
        faction, num = turnFight.actionSortList[turnFight.actionIndex]
        batFaction = turnFight.getBatFaction(faction)
        batLineup = batFaction.getBatlineup(num)
        if batLineup.actionNum > max(batLineup.posObjIDDict):
            if (faction, num) not in overLineupList:
                overLineupList.append((faction, num))
            turnFight.actionIndex += 1
            continue
        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
        # 灵兽
        if turnFight.checkOverByKilled():
            break
        
        # 武将
        elif batLineup.actionNum > 0:
        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):
                turnFight.addTimeline() # 每个武将位算一个时间节点
                batLineup.actionNum = posNum
                # 每个武将位算一个时间节点
                #GameWorld.DebugLog("武将节点: faction=%s,posNum=%s,curTimeline=%s" % (faction, posNum, curTimeline))
                if posNum not in batLineup.posObjIDDict:
                    batLineup.actionNum = posNum + 1
                    curTimeline = turnFight.setTimeline(curTimeline + 1, True)
                    #GameWorld.DebugLog("空位节点: faction=%s,posNum=%s,curTimeline=%s" % (faction, posNum, curTimeline))
                    continue
                objID = batLineup.posObjIDDict[posNum]
                batObj = batObjMgr.getBatObj(objID)
                # 玩家自己阵营,预判可否行动
                if checkBreakpoint and faction == ChConfig.Def_FactionA and batObj:
                    if batObj.CanAction():
                        GameWorld.DebugLog("玩家阵容下一个可行动的武将,断点: curTimeline=%s,nextPosNum=%s" % (curTimeline, posNum))
                        return
                batLineup.actionNum = posNum + 1
                curTimeline = turnFight.setTimeline(curTimeline + 1)
                TurnFightHeroTurnStart(turnFight, batObj, turnNum)
                if not OnObjAction(turnFight, batObj):
                    continue
                
                if faction == ChConfig.Def_FactionA:
                    playerHeroAtk = True
                if not checkBreakpoint and faction == ChConfig.Def_FactionA:
                    checkBreakpoint = True
                    
                break
            
        turnFight.actionIndex += 1
        batLineup.actionNum += 1
        if batLineup.actionNum > max(batLineup.posObjIDDict):
            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.actionIndex >= len(turnFight.actionSortList) - 1:
                turnFight.actionIndex = 0
            else:
                turnFight.actionIndex += 1
        # 回合结束
        curTimeline = turnFight.setTimeline(curTimeline + 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 objID in batLineup.posObjIDDict.values():
                batObj = batObjMgr.getBatObj(objID)
                TurnFightPerTurnBigEnd(turnFight, batObj, turnNum)
                
        if turnFight.checkOverByKilled():
            break
        
        if playerHeroAtk:
            # 玩家武将有行动则停止,一段段执行
            GameWorld.DebugLog("玩家武将有行动则停止!")
            break
    if not turnFight.winFaction:
        OnTurnAllOver(turnFight.guid)
        
    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
            turnFight.addTimeline() # 每回合结束算一个时间节点
            for faction, num in turnFight.actionSortList:
                batFaction = turnFight.getBatFaction(faction)
                batLineup = batFaction.getBatlineup(num)
                for objID in batLineup.posObjIDDict.values():
                    batObj = batObjMgr.getBatObj(objID)
                    TurnFightPerTurnBigEnd(turnFight, batObj, turnNum)
            if turnFight.checkOverByKilled():
                return
        if turnNum < turnFight.turnMax:
            turnFight.turnNum += 1
            turnFight.setTimeline(turnFight.turnNum) # 回合变更,直接设置新回合时间节点
        else:
            OnTurnAllOver(turnFight.guid)
    return
def __processTurnFight(guid):
@@ -1009,12 +1251,12 @@
    for turnNum in range(1, turnMax + 1):
        turnFight.turnNum = turnNum
        GameWorld.DebugLog("【----- 回合制战斗轮次: %s -----】" % turnNum)
        turnFight.setTimeline(turnNum)
        curTimeline = turnFight.getTurnNumStartTimelin(turnNum) # 本回合起始时间节点
        curTimeline = turnFight.setTimeline(curTimeline + 1)
        if curPlayer:
            turnFight.syncState(FightState_Fighting)
            
        # 回合开始
        turnFight.addTimeline() # 每回合开始算一个时间节点
        for faction, num in turnFight.actionSortList:
            GameWorld.DebugLog("回合开始逻辑: turnNum=%s,faction=%s, num=%s" % (turnNum, faction, num))
            batFaction = turnFight.getBatFaction(faction)
@@ -1024,17 +1266,9 @@
                batObj = batObjMgr.getBatObj(objID)
                TurnFightPerTurnBigStart(turnFight, batObj, turnNum)
                
        # 主公
        #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
@@ -1048,10 +1282,11 @@
            batFaction = turnFight.getBatFaction(faction)
            batLineup = batFaction.getBatlineup(num)
            for posNum in range(batLineup.actionNum, PosNumMax + 1):
                turnFight.addTimeline() # 每个武将位算一个时间节点
                batLineup.actionNum = posNum + 1
                if posNum not in batLineup.posObjIDDict:
                    curTimeline = turnFight.setTimeline(curTimeline + 1, True)
                    continue
                curTimeline = turnFight.setTimeline(curTimeline + 1) # 每个武将位算一个时间节点
                objID = batLineup.posObjIDDict[posNum]
                batObj = batObjMgr.getBatObj(objID)
                TurnFightHeroTurnStart(turnFight, batObj, turnNum)
@@ -1066,7 +1301,7 @@
                turnFight.actionIndex += 1
                
        # 回合结束
        turnFight.addTimeline() # 每回合结束算一个时间节点
        curTimeline = turnFight.setTimeline(curTimeline + 1) # 每回合结束算一个时间节点
        for faction, num in turnFight.actionSortList:
            GameWorld.DebugLog("回合结束逻辑: turnNum=%s,faction=%s, num=%s" % (turnNum, faction, num))
            batFaction = turnFight.getBatFaction(faction)
@@ -1087,10 +1322,13 @@
    num = batObj.lineupNum
    posNum = batObj.posNum
    heroID = batObj.heroID
    if not heroID:
        heroID = batObj.npcID
    npcID = batObj.npcID
    objName = GameWorld.CodeToGbk(batObj.GetName())
    return "%s%s-%s [%s-%s] %s" % ("A" if faction == ChConfig.Def_FactionA else "B", num, posNum, batObj.GetID(), heroID, objName)
    if heroID:
        objName += " Hero:%s" % heroID
    if npcID:
        objName += " NPC:%s" % npcID
    return "%s%s-P%s ID:%s %s" % ("A" if faction == ChConfig.Def_FactionA else "B", num, posNum, batObj.GetID(), objName)
def EntryLogic(turnFight):
    ## 执行进场逻辑
@@ -1108,7 +1346,7 @@
            TurnPassive.OnTriggerPassiveEffect(turnFight, batObj, ChConfig.TriggerWay_FightStart)
            
    turnFight.enterLogic = True
    return
    return True
def OnTimelineChange(turnFight):
    ## 每个时间节点变化时处理
@@ -1220,7 +1458,7 @@
    return
def TurnFightHeroTurnStart(turnFight, batObj, turnNum):
    ## 武将回合开始时
    ## 武将回合开始时,不能行动也要触发
    if not batObj:
        return
    
@@ -1347,7 +1585,12 @@
    clientPack.ObjID = objID
    clientPack.KillerObjID = killerObjID
    clientPack.SkillID = skillID
    turnFight.addBatPack(clientPack)
    turnFight.addBatPack(clientPack)
    curPlayer = turnFight.curPlayer
    # 暂时只算主线小怪
    if curPlayer and turnFight.mapID == ChConfig.Def_FBMapID_Main and gameObj.GetFaction() != ChConfig.Def_FactionA:
        PlayerTask.AddTaskValue(curPlayer, ChConfig.TaskType_KillNPC, 1)
    return True
def OnTurnAllOver(guid):
@@ -1379,6 +1622,7 @@
    
    turnFight.costTime = time.time() - turnFight.startTime
    winFaction = turnFight.winFaction
    turnFight.isWin = (winFaction == ChConfig.Def_FactionA)
    GameWorld.DebugLog("--- 战斗结束处理 ---, winFaction=%s, costTime=%ss, %s" % (winFaction, turnFight.costTime, guid))
    
    # 统计明细
@@ -1409,128 +1653,15 @@
                                   % (posNum, objID, npcID, heroID, batObj.GetHP(), batObj.GetMaxHP(), atkHurt, defHurt, cureHP))
                lineupStatInfo[str(posNum)] = {"ObjID":objID, "HeroID":heroID, "NPCID":npcID, "AtkHurt":atkHurt, "DefHurt":defHurt, "CureHP":cureHP}
                
    awardItemList = []
    playerID = turnFight.playerID
    if playerID:
        curPlayer = turnFight.curPlayer
        isWin = (winFaction == ChConfig.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:
            pass
    overMsg = {"winFaction":winFaction, "statInfo":statInfo, FBCommon.Over_itemInfo:FBCommon.GetJsonItemList(awardItemList)}
    overMsg = {"winFaction":winFaction, "statInfo":statInfo}
    curPlayer = turnFight.curPlayer
    mapID = turnFight.mapID
    funcLineID = turnFight.funcLineID
    FBLogic.OnTurnFightOver(curPlayer, turnFight, mapID, funcLineID, overMsg)
    turnFight.syncState(FightState_Award, overMsg)
    # 除了主线外,其余战斗结束后均直接删除
    if not turnFight.playerID:
        tfMgr.delTurnFight(guid)
    return
def OnOver_MainLevel(curPlayer, isWin, awardItemList):
    ## 战斗结束额外处理 - 主线关卡
    if not curPlayer:
        return
    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
#
@@ -1540,4 +1671,27 @@
#    char        GUID[40];    //战报guid
#};
def OnTurnFightReportView(index, clientData, tick):
    curPlayer = GameWorld.GetPlayerManager().GetPlayerByIndex(index)
    guid = clientData.GUID
    lastBatBufferInfo = PlayerOnline.GetOnlinePlayer(curPlayer).GetLastBatBuffer()
    if lastBatBufferInfo and len(lastBatBufferInfo) == 2 and guid == lastBatBufferInfo[0]:
        guid, reprot = lastBatBufferInfo
        SyncTurnFightReport(curPlayer, guid, reprot)
        return
    # 其他战报,一般是入库存储的,待扩展
    # 战报已过期
    PlayerControl.NotifyCode(curPlayer, "FightReportExpired")
    return
def SyncTurnFightReport(curPlayer, guid, reprot):
    ## 通知完整战报
    clientPack = ChPyNetSendPack.tagSCTurnFightReport()
    clientPack.GUID = guid
    clientPack.Report = reprot
    clientPack.Len = len(clientPack.Report)
    NetPackCommon.SendFakePack(curPlayer, clientPack)
    return