ServerPython/ZoneServerGroup/map1_8G/MapServer/MapServerData/Script/Attack/TurnAttack.py
@@ -10,19 +10,44 @@
# @version 1.0
#
# 详细描述: 回合制攻击逻辑,均使用NPC实例作为战斗主体
#        实时战斗:  仅用于主线,每个玩家最多仅允许存在一个实时战斗
#                    由前端控制战斗流程,后端进行计算
#        瞬时战斗:  用于非主线外的各种战斗,如副本PVE挑战,PVP挑战等,一样仅有一个瞬时战斗,
#                    一次性处理完,后端控制整体战斗流程及计算,前端仅做战报解析表现战斗过程
#
#-------------------------------------------------------------------------------
#"""Version = 2023-11-30 15:30"""
#-------------------------------------------------------------------------------
import ChConfig
import PlayerTask
import PlayerActivity
import PlayerViewCache
import ChPyNetSendPack
import NetPackCommon
import PlayerControl
import GameWorld
import GameObj
import IpyGameDataPY
import PlayerOnline
import NPCCommon
import ShareDefine
import PyGameData
import SkillCommon
import BattleObj
import TurnPassive
import TurnSkill
import TurnBuff
import FBCommon
import CommFunc
import ObjPool
import FBLogic
import random
import time
import json
PosNumMax = 7 # 最大站位编号
ActionNumStart = -1 # 起始行动位置编号,一般是从1开始,如果有加主公、红颜等则扣除相应位置值,如从0或-1开始
# 回合战斗流程状态
(
@@ -32,21 +57,862 @@
FightState_FightEnd, # 3 战斗结束
FightState_Award, # 4 结算奖励
FightState_Over, # 5 结束状态,无特殊意义,仅代表所有处理结束了,与Start对应
) = range(6)
FightState_Rest, # 6 结束并休息,一般是前端主动退出并休息的
) = range(7)
class BatLineup():
    ## 战斗阵容
    def __init__(self, faction, num, turnFight):
        self.turnFight = turnFight # TurnFight
        self.faction = faction # 所属阵营
        self.num = num # 该阵容所在阵营中的编号,不同阵营可重复,多V多
        self.ownerID = 0 # 阵容所属玩家ID,可能为0,0代表非玩家阵容
        self.shapeType = 0 # 阵型
        self.fightPower = 0 # 阵容总战力
        self.posObjIDDict = {} # 站位对应战斗实体 {站位编号:batObjID, ...}, 站位编号小于0为非主战单位,如主公、红颜等
        self.lingshouObjIDDict = {} # 灵兽战斗单位 {位置编号:batObjID, ...}
        self.beautyObjIDDict = {} # 红颜战斗单位 {位置编号:batObjID, ...}
        self.actionNum = ActionNumStart # 行动位置,从1开始
        self.totalHurt = 0 # 阵容总输出
        #特殊
        self.bossID = 0
        self.bossPosView = 0
        return
    def getPlayerID(self): return self.turnFight.playerID # 发起的玩家ID
    def isEmpty(self): return not self.posObjIDDict
    def setLineup(self, lineupInfo):
        ## 设置阵容
        # @param lineupInfo: 阵容信息
        self.clearLineup()
        self.ownerID = lineupInfo.get("PlayerID", 0) # 阵容所属的玩家ID
        self.shapeType = lineupInfo.get("ShapeType", 0)
        self.fightPower = lineupInfo.get("FightPower", 0)
        self.bossID = lineupInfo.get("BossID", 0)
        self.bossPosView = lineupInfo.get("BossPosView", 0)
        SummonLineupObjs(self, self.faction, self.num, lineupInfo, self.getPlayerID())
        return
    def clearLineup(self):
        ## 清除阵容
        batObjMgr = BattleObj.GetBatObjMgr()
        for objID in self.posObjIDDict.values():
            batObjMgr.delBatObj(objID)
        for objID in self.lingshouObjIDDict.values():
            batObjMgr.delBatObj(objID)
        for objID in self.beautyObjIDDict.values():
            batObjMgr.delBatObj(objID)
        self.posObjIDDict = {}
        self.lingshouObjIDDict = {}
        self.beautyObjIDDict = {}
        self.fightPower = 0
        self.totalHurt = 0
        return
    def getDeadObjCnt(self):
        ## 获取本阵容目前死亡队员数
        deadCnt = 0
        batObjMgr = BattleObj.GetBatObjMgr()
        for objID in self.posObjIDDict.values():
            batObj = batObjMgr.getBatObj(objID)
            if not batObj:
                continue
            if batObj.IsAlive():
                continue
            deadCnt += 1
        return deadCnt
class BatFaction():
    ## 战斗阵营
    def __init__(self, faction, turnFight):
        self.turnFight = turnFight # TurnFight
        self.faction = faction
        self.lineupDict = {} # 该阵营所有阵容信息 {编号:BatLineup, ...}
        self.totalHurt = 0 # 阵营总输出
        return
    def getBatlineup(self, num=1):
        ## 获取战斗阵容
        lineup = None
        if num in self.lineupDict:
            lineup = self.lineupDict[num]
        else:
            lineup = BatLineup(self.faction, num, self.turnFight)
            self.lineupDict[num] = lineup
        return lineup
    def getTotalHurt(self): return self.totalHurt # 阵营总输出
    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.state = -1 # -1 代表未战斗
        self.turnNum = 1 # 当前第x回合,默认第1回合开始
        self.turnMax = 15 # 最大回合数
        self.turnNumStart = 0 # 已处理第x回合开始
        self.enterLogic = False # 是否已执行进场逻辑
        self.winFaction = 0 # 本场战斗结束标记,获胜阵营,为0时代表未结束,所有小队打完或失败才有结果,0-未结束,>0-获胜的阵营
        self.isWin = False
        self.batBuffer = "" # 战报buffer,战报暂时只保留最后一个小队的
        self.isNeedReport = isNeedReport # 是否需要战报
        self.msgDict = {} # 扩展信息字典,一般由MapID绑定的功能决定信息内容  {k:v, ...}
        self._kvDict = {} # 自定义信息字典,不会被重置  {k:v, ...}
        self.factionDict = {} # 战斗阵营 {faction:BatFaction, ...},一般是只有两个阵营,faction为1或2,每个阵营支持多个阵容
        self.actionSortList = [] # 阵容行动顺序 [[faction, num], ...]
        self.actionIndex = 0 # 行动顺序索引
        self.startTime = 0 # 开始时间戳,支持毫秒小数
        self.costTime = 0 # 单场战斗总耗时,支持毫秒小数
        self._oneActionUseSkillCntDict = {} # 某对象行动开始后所有对象累计使用技能次数,用于单对象单次行动中限制每个对象的最高触发技能次数 {objID:useCnt, ...}
        # 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 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._kvDict = {}
        self.nextTurnFight(msgDict)
        return
    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 isFBMap(self):
        ## 是否副本地图中,非主线的均视为副本
        return self.mapID not in [ChConfig.Def_FBMapID_Main, ChConfig.Def_FBMapID_MainBoss]
    def GetDictByKey(self, key): return self._kvDict.get(key, 0)
    def SetDict(self, key, value): self._kvDict[key] = value
    #def setPVPTeam(self):
    #    return
    def nextTurnFight(self, msgDict={}):
        ## 一般用于玩家发起的战斗,在需要保留玩家阵容属性及状态的情况下,重置回合进入下一场战斗
        self.turnNum = 1
        self.turnNumStart = 0
        self.enterLogic = False
        self.winFaction = 0
        self.isWin = False
        self.msgDict.update(msgDict)
        self.startTime = time.time()
        self.costTime = 0
        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):
        ## 设置阵营阵容
        # @param lineupDict: {阵容编号:阵容信息, ...} 每个阵营支持多个阵容,即支持多V多
        batFaction = self.getBatFaction(faction)
        batFaction.clearLineups()
        for num, lineupInfo in lineupDict.items():
            if not lineupInfo:
                continue
            batLineup = batFaction.getBatlineup(num)
            batLineup.setLineup(lineupInfo)
        return
    def sortActionQueue(self):
        ## 刷新出手顺序队列
        sortList = []
        for batFaction in self.factionDict.values():
            faction = batFaction.faction
            for num, batLineup in batFaction.lineupDict.items():
                isPlayer = 1 if batLineup.ownerID else 0 # 玩家阵容优先攻击
                fightPower = batLineup.fightPower
                sortValue = -(faction * 10 + num)
                sortList.append([isPlayer, 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("阵容战力排序[isPlayer, fp, sortV, f, n]: %s" % sortList)
        GameWorld.DebugLog("阵容行动顺序[f, n]: %s" % self.actionSortList)
        return
    def getBatFaction(self, faction=ChConfig.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
        batObjMgr = BattleObj.GetBatObjMgr()
        for faction, batFaction in self.factionDict.items():
            allKilled = True
            for batLineup in batFaction.lineupDict.values():
                if not allKilled:
                    break
                for posNum, objID in batLineup.posObjIDDict.items():
                    if posNum <= 0:
                        # 非主战位置不判断
                        continue
                    batObj = batObjMgr.getBatObj(objID)
                    if not batObj:
                        continue
                    if batObj.IsAlive():
                        allKilled = False
                        break
            if allKilled:
                self.winFaction = ChConfig.Def_FactionA if faction == ChConfig.Def_FactionB else ChConfig.Def_FactionB
                DoTurnFightOver(self.guid)
                return self.winFaction
        return 0
    def exitFight(self, rest=False):
        ## 退出战斗
        self.syncState(FightState_Over if not rest else FightState_Rest)
        for batFaction in self.factionDict.values():
            batFaction.clearLineups()
        self.state = -1
        return
    def startFight(self):
        ## 准备就绪,开始战斗
        self.state = FightState_Start
        self.turnNum = 1
        self.turnNumStart = 0
        self.syncInit()
        return
    def isInFight(self): return self.state != -1
    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 = []
        batObjMgr = BattleObj.GetBatObjMgr()
        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, objID in batLineup.posObjIDDict.items():
                    batObj = batObjMgr.getBatObj(objID)
                    if not batObj:
                        continue
                    tfObj = ChPyNetSendPack.tagSCTurnFightObj()
                    tfObj.ObjID = batObj.GetID()
                    tfObj.NPCID = batObj.GetNPCID()
                    tfObj.HeroID = batObj.GetHeroID()
                    tfObj.SkinID = batObj.GetSkinID()
                    tfObj.HP = batObj.GetHP() % ChConfig.Def_PerPointValue
                    tfObj.HPEx = batObj.GetHP() / ChConfig.Def_PerPointValue
                    tfObj.MaxHP = batObj.GetMaxHP() % ChConfig.Def_PerPointValue
                    tfObj.MaxHPEx = batObj.GetMaxHP() / ChConfig.Def_PerPointValue
                    tfObj.LV = batObj.GetLV()
                    if batLineup.bossPosView and batLineup.bossID == batObj.GetNPCID():
                        tfObj.PosNum = batLineup.bossPosView
                    else:
                        tfObj.PosNum = posNum
                    tfObj.AngreXP = batObj.GetXP()
                    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 syncHelp(self, msgDict):
        ## 通知帮助信息,一般是副本用
        self.syncState(self.state, msgDict)
        return
    def syncState(self, state, msgDict={}):
        self.state = state
        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, 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)
        if self.isNeedReport:
            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
    def ResetOneActionUseSkillCnt(self): self._oneActionUseSkillCntDict = {}
    def GetOneActionUseSkillCnt(self, objID): return self._oneActionUseSkillCntDict.get(objID, 0)
    def SetOneActionUseSkillCnt(self, objID, useCnt):
        self._oneActionUseSkillCntDict[objID] = useCnt
        return useCnt
class TurnFightMgr():
    ## 回合战斗管理器
    def __init__(self):
        self.lastRequestTick = 0
        self.turnFightDict = {} # {guid:TurnFight, ...}
        return
    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
    def delTurnFight(self, guid):
        turnFight = self.getTurnFight(guid)
        if not turnFight:
            return
        turnFight.exitFight()
        self.turnFightDict.pop(guid, None)
        ObjPool.GetPoolMgr().release(turnFight)
        return
    def getTurnFight(self, guid):
        tf = None
        if guid in self.turnFightDict:
            tf = self.turnFightDict[guid]
        elif False:
            tf = TurnFight()
        return tf
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.turnFight = GetTurnFightMgr().addTurnFight(ChConfig.Def_FBMapID_Main, 0, playerID)
        return
    def isLevelBoss(self):
        ## 当前战斗是否关卡boss
        return self.turnFight.mapID == ChConfig.Def_FBMapID_MainBoss
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 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 lineup.IsEmpty():
        GameWorld.DebugLog("玩家没有目标阵容默认取主阵容! lineupID=%s" % lineupID)
        lineup = olPlayer.GetLineup(ShareDefine.Lineup_Main)
    return lineup
def GetPlayerLineupInfo(curPlayer, lineupID):
    ## 获取玩家阵容信息,可用于战斗或查看缓存,因为可能取玩家的缓存进行对战,所以统一使用json格式,前端通用
    # @param lineupID: 阵容ID
    # @return: 阵容全部信息json字典,前端通用格式
    playerID = curPlayer.GetPlayerID()
    lineup = GetPlayerLineup(curPlayer, lineupID)
    if lineup.IsEmpty():
        return {}
    heroDict = {}
    curPack = curPlayer.GetItemManager().GetPack(ShareDefine.rptHero)
    for posNum in lineup.GetPosNumList():
        hero = lineup.GetLineupHero(posNum)
        heroID = hero.heroID
        itemIndex = hero.itemIndex
        userData = ""
        heroLV = 1
        if itemIndex >= 0 and itemIndex < curPack.GetCount():
            heroItem = curPack.GetAt(itemIndex)
            if heroItem and not heroItem.IsEmpty():
                userData = heroItem.GetUserData()
                heroLV = heroItem.GetUserAttr(ShareDefine.Def_IudetHeroLV)
        skillIDlist = []
        skillIDlist += hero.heroSkillIDList
        heroDict[str(posNum)] = {
                                 "HeroID":heroID,
                                 "SkinID":hero.skinID,
                                 "LV":heroLV,
                                 "Data":userData,
                                 "FightPower":hero.fightPower,
                                 "AttrDict":{str(k):v for k, v in hero.heroBatAttrDict.items() if v > 0},
                                 "SkillIDList":skillIDlist,
                                 }
    if not heroDict:
        return {}
    lineupInfo = {"PlayerID":playerID, "FightPower":lineup.fightPower, "ShapeType":lineup.shapeType, "Hero":heroDict}
    return lineupInfo
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:
        return {}
    bossID = ipyData.GetBossID()
    bossPosView = ipyData.GetBossPosView()
    heroDict = {}
    for posNum in range(1, 1 + 6):
        if not hasattr(ipyData, "GetPosNPCID%s" % posNum):
            break
        npcID = getattr(ipyData, "GetPosNPCID%s" % posNum)()
        if not npcID:
            continue
        battleDict = GetNPCBattleDict(ipyData, npcID, strongerLV, difficulty)
        if not battleDict:
            continue
        heroDict[str(posNum)] = battleDict
    lineupInfo = {"NPCLineupID":lineupID, "Hero":heroDict, "BossID":bossID, "BossPosView":bossPosView}
    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(),
                       ChConfig.AttrID_SuperHitRate:npcData.GetSuperHitRate(), ChConfig.AttrID_SuperHitRateDef:npcData.GetSuperHitRateDef(),
                       ChConfig.AttrID_StunRate:npcData.GetStunRate(), ChConfig.AttrID_StunRateDef:npcData.GetStunRateDef(),
                       ChConfig.AttrID_ComboRate:npcData.GetComboRate(), ChConfig.AttrID_ComboRateDef:npcData.GetComboRateDef(),
                       ChConfig.AttrID_ParryRate:npcData.GetParryRate(), ChConfig.AttrID_ParryRateDef:npcData.GetParryRateDef(),
                       ChConfig.AttrID_SuckHPPer:npcData.GetSuckHPPer(), ChConfig.AttrID_SuckHPPerDef:npcData.GetSuckHPPerDef(),
                       }
    exAttrDict = npcData.GetSpecAttrInfo()
    for attrIDStr, attrValue in exAttrDict.items():
        attrID = int(attrIDStr)
        batAttrDict[attrID] = batAttrDict.get(attrID, 0) + attrValue
    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.GetIpyGameDataListNotLog("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):
    '''召唤阵容战斗实例
    @param faction: 所属阵营,目前支持两个阵营,1或2
    @param num: 战斗阵容在该阵营上的编号,1V1时默认1,多对多时1~n
    @param lineupInfo: 阵容信息
    @param playerID: 发起的玩家ID,系统场次为0
    '''
    GameWorld.DebugLog("SummonLineupObjs faction:%s,num:%s,lineupInfo=%s" % (faction, num, lineupInfo), playerID)
    if playerID:
        curPlayer = GameWorld.GetPlayerManager().FindPlayerByID(playerID)
        if not curPlayer:
            return
    turnFight = batLineup.turnFight
    tfGUID = turnFight.guid
    lineupPlayerID = lineupInfo.get("PlayerID", 0) # 阵容所属玩家ID
    heroDict = lineupInfo.get("Hero", {})
    batObjMgr = BattleObj.GetBatObjMgr()
    initXP = IpyGameDataPY.GetFuncCfg("AngerXP", 1)
    for posNumKey, heroInfo in heroDict.items():
        posNum = int(posNumKey)
        fightPower = 0
        skillIDList = [] # 战斗对象可能改变属性或技能,重新创建,防止误修改来源值
        attrDict = {}
        skillIDList += heroInfo.get("SkillIDList", [])
        attrDict.update(heroInfo.get("AttrDict", {}))
        npcID = heroInfo.get("NPCID", 0)
        heroID = heroInfo.get("HeroID", 0)
        skinID = heroInfo.get("SkinID", 0)
        lv = heroInfo.get("LV", 1)
        specialty, atkDistType, country, sex, job = 0, 1, 0, 1, 0
        heroIpyData = IpyGameDataPY.GetIpyGameData("Hero", heroID) if heroID else None
        if heroIpyData:
            objName = heroIpyData.GetName()
            specialty = heroIpyData.GetSpecialty()
            atkDistType = heroIpyData.GetAtkDistType()
            country = heroIpyData.GetCountry()
            sex = heroIpyData.GetSex()
            job = heroIpyData.GetJob()
        if lineupPlayerID:
            fightPower = heroInfo.get("FightPower", 0)
            if not heroIpyData:
                continue
        else:
            npcDataEx = NPCCommon.GetNPCDataPy(npcID)
            if not npcDataEx:
                continue
            if not heroIpyData:
                objName = npcDataEx.GetNPCName()
        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)
        batObj.SetLineupPos(posNum, num)
        batObj.SetFightPower(fightPower)
        batObj.SetLV(lv)
        batObj.SetAtkDistType(atkDistType)
        batObj.SetSpecialty(specialty)
        batObj.SetCountry(country)
        batObj.SetSex(sex)
        batObj.SetJob(job)
        batObj.SetHero(heroID, skinID)
        skillManager = batObj.GetSkillManager()
        skillManager.SkillReset()
        for skillID in skillIDList:
            skillManager.LearnSkillByID(skillID)
        batLineup.posObjIDDict[posNum] = objID
        GameWorld.DebugLog("AddBatObj %s,skill=%s" % (GetObjName(batObj), skillManager.GetSkillIDList()))
        ResetObjSkill(batObj)
        if npcID:
            #副本指定NPC属性
            fbNPCInitAttrDict = FBLogic.GetFBNPCInitAttr(curPlayer, turnFight, batObj)
            if fbNPCInitAttrDict:
                GameWorld.DebugLog("副本指定NPC初始化属性: npcID=%s, %s" % (npcID, fbNPCInitAttrDict))
                attrDict = {str(k):v for k, v in fbNPCInitAttrDict.items()} # 统一格式
        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()))
        # 清除buff
        buffMgr = batObj.GetBuffManager()
        buffMgr.ClearBuff()
        # 重置技能
        ResetObjSkill(batObj)
        # 重刷属性、被动
        TurnBuff.RefreshBuffAttr(batObj)
        TurnPassive.RefreshPassive(batObj)
    return
def CheckFightCD(curPlayer, tick, selfKey):
    ## 是否战斗请求CD中
    # 所有玩家公共CD,待扩展
    tfMgr = GetTurnFightMgr()
    pubCD = IpyGameDataPY.GetFuncCfg("TurnFightCD", 1)
    if pubCD:
        if tfMgr.lastRequestTick and tick - tfMgr.lastRequestTick <= pubCD:
            GameWorld.DebugLog("回合制战斗请求服务器公共CD中!")
            PlayerControl.NotifyCode(curPlayer, "BattleCoolDown")
            return True
    # 个人CD
    selfCD = IpyGameDataPY.GetFuncCfg("TurnFightCD", 2)
    lastTick = curPlayer.GetDictByKey(selfKey)
    if selfCD and lastTick and tick - lastTick <= selfCD:
        GameWorld.DebugLog("回合制战斗请求CD中: %s" % selfKey)
        PlayerControl.NotifyCode(curPlayer, "BattleCoolDown")
        return True
    tfMgr.lastRequestTick = tick
    curPlayer.SetDict(selfKey, tick)
    return False
#// 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
#    BYTE        ValueCount;
#    DWORD        ValueList[ValueCount]; // 附加值列表,可选,具体含义由MapID决定
#};
def OnTurnFight(index, clientData, tick):
    ''' 本战斗会一次性处理完所有小队战斗,以战报的形式下发,目前除了主线小怪分段战斗外,均使用该战斗
    '''
    curPlayer = GameWorld.GetPlayerManager().GetPlayerByIndex(index)
    mapID = clientData.MapID
    funcLineID = clientData.FuncLineID
@@ -54,149 +920,890 @@
    tagID = clientData.TagID
    valueList = clientData.ValueList
    
    playerID = curPlayer.GetPlayerID()
    if tagType == ChConfig.TurnBattle_TagType_Player:
        if tagID == playerID:
            GameWorld.DebugLog("不能打自己!", playerID)
            return
    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
    funcLineID = reqRet[1] if len(reqRet) > 1 else funcLineID
    
    # 需要发送到GameServer验证
    if mapID in ChConfig.Def_TFMapID_SendToGameServer:
        SendToGameServer_TurnFight(curPlayer, "TurnFightRequest", [mapID, funcLineID, tagType, tagID, valueList])
        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):
    msgType, dataMsg, ret = msgData
    if not ret:
        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)
    return
def DoTurnFightProcess(curPlayer, mapID, funcLineID, tagType, tagID, valueList, tick):
    ## 执行回合制战斗的完整流程
    #if curPlayer.GetSightLevel() != curPlayer.GetID():
    #    PlayerControl.SetPlayerSightLevel(curPlayer, curPlayer.GetID())
    SyncTurnFightState(curPlayer, mapID, funcLineID, tagType, tagID, FightState_Start)
    tagPlayerInfo = {}
    if tagType == ChConfig.TurnBattle_TagType_Player and tagID >= 10000 :
        tagPlayer = GameWorld.GetMapCopyPlayerManager().FindPlayerByID(tagID)
        if tagPlayer:
            tagPlayerInfo = __GetPlayerInfo(tagPlayer)
        else:
            SendToGameServer_TurnFight(curPlayer, "TurnFightTagPlayerInfo", [mapID, funcLineID, tagType, tagID, valueList])
    fbIpyData = FBCommon.GetFBIpyData(mapID)
    fbLineIpyData = FBCommon.GetFBLineIpyData(mapID, funcLineID, False)
    if fbIpyData:
        if not fbLineIpyData:
            GameWorld.DebugLog("不存在该副本功能线路! mapID=%s,funcLineID=%s" % (mapID, funcLineID))
            return
        if FBCommon.CheckCanEnterFBComm(curPlayer, mapID, funcLineID, fbIpyData, fbLineIpyData) != ShareDefine.EntFBAskRet_OK:
            return
        
    DoTrunFight(curPlayer, mapID, funcLineID, tagType, tagID, valueList, tick, tagPlayerInfo)
    SyncTurnFightState(curPlayer, mapID, funcLineID, tagType, tagID, FightState_Over)
    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:
    # 攻防方所使用的阵容ID
    atkLineupID, defLineupID = FBLogic.GetFBPlayerLineupID(curPlayer, mapID, funcLineID)
    if atkLineupID not in ShareDefine.LineupList or defLineupID not in ShareDefine.LineupList:
        return
    if not tagInfo:
        tagInfo = {}
    if CheckFightCD(curPlayer, tick, "TurnFightReqTick"):
        return
    # 玩家
    if tagType == 1:
        if not OnTurnFightVSPlayer(curPlayer, mapID, funcLineID, atkLineupID, defLineupID, tagID):
            return
    # NPC
    else:
        npcLineupIDList, strongerLV, difficulty = [], 0, 0
        if fbLineIpyData:
            npcLineupIDList = fbLineIpyData.GetLineupIDList()
            strongerLV = fbLineIpyData.GetNPCLV()
            difficulty = fbLineIpyData.GetDifficulty()
        if not npcLineupIDList:
            ret = FBLogic.GetFBNPCLineupInfo(curPlayer, mapID, funcLineID)
            if not ret:
                return
            npcLineupIDList, strongerLV, difficulty = ret
        if not OnTurnFightVSNPC(curPlayer, mapID, funcLineID, atkLineupID, npcLineupIDList, strongerLV, difficulty):
            return
    return True
def OnTurnFightVSNPC(curPlayer, mapID, funcLineID, atkLineupID, npcLineupIDList, strongerLV, difficulty):
    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=""):
    if not curPlayer:
    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
    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)
    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()
        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 True
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 True
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-挑战关卡小怪;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
    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 == 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.exitFight(True)
    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)
    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
    lineupIDList = getattr(levelIpyData, "GetWaveLineupIDList%s" % wave)() # 小队1阵容ID|小队2阵容ID|...
    if not lineupIDList:
        return
    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.chapterID = chapterID
    mainFightMgr.levelNum = levelNum
    mainFightMgr.waveMax = waveMax
    mainFightMgr.wave = wave
    mapID, funcLineID = ChConfig.Def_FBMapID_Main, PlayerControl.ComMainLevelValue(chapterID, levelNum, wave)
    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.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, strongerLV, difficulty)})
    turnFight.sortActionQueue()
    turnFight.startFight()
    __doMainFight(curPlayer)
    return
def __doMainFight(curPlayer, tick=0):
    '''执行主线战斗,单场战斗断电式战斗,以前端玩家手动点击节点做为断点处
    每场战斗的初始化、结束默认断点,由前端决定自动继续或者点击继续
    '''
    # 限制请求CD
    if tick:
        if CheckFightCD(curPlayer, tick, "MainFightReqTick"):
            return
    mainFightMgr = GetMainFightMgr(curPlayer)
    turnFight = mainFightMgr.turnFight
    isLevelBoss = mainFightMgr.isLevelBoss()
    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()
            # 切换小队时,玩家阵容不需要处理,保留状态
            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
    # 以下均是处理关卡小怪分段实时战斗
    isEntry = EntryLogic(turnFight)
    # 是否开始检查断点,预判可断的点,方便前端点击体验,点下去就是玩家放的某个行动
    # 初始开始进场后,默认开始断点
    checkBreakpoint = True if isEntry else False
    batObjMgr = BattleObj.GetBatObjMgr()
    turnNum = turnFight.turnNum
    turnMax = turnFight.turnMax
    turnNumStart = turnFight.turnNumStart
    for turnNum in range(turnNum, turnMax + 1):
        if turnFight.winFaction:
            break
        # 回合开始
        if turnNumStart < turnNum:
            GameWorld.DebugLog("【----- 回合制战斗轮次: %s -----】 " % (turnNum))
            turnFight.turnNum = turnNum
            turnFight.turnNumStart = turnNum
            if curPlayer:
                turnFight.syncState(FightState_Fighting)
            TurnFightPerTurnBigStart(turnFight, turnNum)
        # 红颜
        # 灵兽
        if turnFight.winFaction:
            break
        # 武将
        doMax = PosNumMax * len(turnFight.actionSortList)
        doCnt = 0
        while doCnt < doMax and turnFight.actionIndex < len(turnFight.actionSortList) and not turnFight.winFaction:
            doCnt += 1
            faction, num = turnFight.actionSortList[turnFight.actionIndex]
            batFaction = turnFight.getBatFaction(faction)
            batLineup = batFaction.getBatlineup(num)
            for posNum in range(batLineup.actionNum, PosNumMax + 1):
                #GameWorld.DebugLog("武将位置: faction=%s,posNum=%s" % (faction, posNum))
                if posNum not in batLineup.posObjIDDict:
                    batLineup.actionNum = posNum + 1
                    #GameWorld.DebugLog("没有武将: faction=%s,posNum=%s" % (faction, posNum))
                    continue
                objID = batLineup.posObjIDDict[posNum]
                batObj = batObjMgr.getBatObj(objID)
                # 玩家自己阵营,预判可否行动
                if checkBreakpoint and faction == ChConfig.Def_FactionA and batObj:
                    if batObj.CanAction():
                        GameWorld.DebugLog("玩家阵容下一个可行动的武将,断点: nextPosNum=%s" % (posNum))
                        return
                batLineup.actionNum = posNum + 1
                TurnFightHeroTurnStart(turnFight, batObj, turnNum)
                isAction = OnObjAction(turnFight, batObj)
                TurnFightHeroTurnEnd(turnFight, batObj, turnNum)
                if not isAction:
                    continue
                if not checkBreakpoint and faction == ChConfig.Def_FactionA:
                    checkBreakpoint = True
                break
            if turnFight.actionIndex >= len(turnFight.actionSortList) - 1:
                turnFight.actionIndex = 0
            else:
                turnFight.actionIndex += 1
        if turnFight.winFaction:
            break
        # 回合结束
        TurnFightPerTurnBigEnd(turnFight, turnNum)
    if not turnFight.winFaction:
        OnTurnAllOver(turnFight.guid)
    return
def __processTurnFight(guid):
    ## 一次性处理完一个小队的战斗
    turnFight = GetTurnFightMgr().getTurnFight(guid)
    curPlayer = turnFight.curPlayer
    turnMax = turnFight.turnMax
    EntryLogic(turnFight)
    batObjMgr = BattleObj.GetBatObjMgr()
    for turnNum in range(1, turnMax + 1):
        if turnFight.winFaction:
            break
        turnFight.turnNum = turnNum
        GameWorld.DebugLog("【----- 回合制战斗轮次: %s -----】" % turnNum)
        if curPlayer:
            turnFight.syncState(FightState_Fighting)
        # 回合开始
        TurnFightPerTurnBigStart(turnFight, turnNum)
        # 红颜
        # 灵兽
        if turnFight.winFaction:
            break
        # 武将
        doMax = PosNumMax * len(turnFight.actionSortList)
        doCnt = 0
        while doCnt < doMax and turnFight.actionIndex < len(turnFight.actionSortList) and not turnFight.winFaction:
            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.posObjIDDict:
                    continue
                objID = batLineup.posObjIDDict[posNum]
                batObj = batObjMgr.getBatObj(objID)
                TurnFightHeroTurnStart(turnFight, batObj, turnNum)
                isAction = OnObjAction(turnFight, batObj)
                TurnFightHeroTurnEnd(turnFight, batObj, turnNum)
                if not isAction:
                    continue
                break
            if turnFight.actionIndex >= len(turnFight.actionSortList) - 1:
                turnFight.actionIndex = 0
            else:
                turnFight.actionIndex += 1
        if turnFight.winFaction:
            break
        # 回合结束
        TurnFightPerTurnBigEnd(turnFight, turnNum)
    if not turnFight.winFaction:
        OnTurnAllOver(turnFight.guid)
    return
def GetObjName(batObj):
    faction = batObj.faction
    num = batObj.lineupNum
    posNum = batObj.posNum
    heroID = batObj.heroID
    npcID = batObj.npcID
    objName = GameWorld.CodeToGbk(batObj.GetName())
    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):
    ## 执行进场逻辑
    if turnFight.enterLogic:
        return
    GameWorld.DebugLog("执行进场逻辑...")
    batObjMgr = BattleObj.GetBatObjMgr()
    for faction, num in turnFight.actionSortList:
        batFaction = turnFight.getBatFaction(faction)
        batLineup = batFaction.getBatlineup(num)
        batLineup.actionNum = ActionNumStart
        for objID in batLineup.posObjIDDict.values():
            batObj = batObjMgr.getBatObj(objID)
            turnFight.ResetOneActionUseSkillCnt()
            TurnPassive.OnTriggerPassiveEffect(turnFight, batObj, ChConfig.TriggerWay_FightStart)
    turnFight.enterLogic = True
    return True
def TurnFightPerTurnBigStart(turnFight, turnNum):
    ## 大回合开始时
    batObjMgr = BattleObj.GetBatObjMgr()
    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 objID in batLineup.posObjIDDict.values():
            batObj = batObjMgr.getBatObj(objID)
            if not batObj:
                continue
            if not batObj.IsAlive():
                continue
            turnFight.ResetOneActionUseSkillCnt()
            batObj.SetTiming(ChConfig.TurnTiming_Before) # 重置时机到回合前
            if turnNum > 1: # 第1回合不用刷新技能
                RefreshObjSkillByTurn(batObj)
            TurnPassive.OnTriggerPassiveEffect(turnFight, batObj, ChConfig.TriggerWay_BigTurnStart)
    return
def TurnFightPerTurnBigEnd(turnFight, turnNum):
    ## 大回合结束时
    batObjMgr = BattleObj.GetBatObjMgr()
    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)
            if not batObj:
                continue
            if not batObj.IsAlive():
                continue
            turnFight.ResetOneActionUseSkillCnt()
            TurnPassive.OnTriggerPassiveEffect(turnFight, batObj, ChConfig.TriggerWay_BigTurnEnd)
    return
def TurnFightHeroTurnStart(turnFight, batObj, turnNum):
    ## 武将回合开始时,不能行动也要触发
    if not batObj:
        return
    if not batObj.IsAlive():
        return
    GameWorld.DebugLog("---[武将回合开始] : curID=%s,curHP=%s/%s" % (batObj.GetID(), batObj.GetHP(), batObj.GetMaxHP()))
    turnFight.ResetOneActionUseSkillCnt()
    RefreshObjBuffTime(turnFight, batObj)
    TurnPassive.OnTriggerPassiveEffect(turnFight, batObj, ChConfig.TriggerWay_HeroTurnStart)
    # 最后设置时机为回合后,说明:回合开启前的逻辑处理完后,其他的均默认为回合后
    batObj.SetTiming(ChConfig.TurnTiming_After)
    return
def TurnFightHeroTurnEnd(turnFight, batObj, turnNum):
    ## 武将回合结束时,不能行动也要触发
    if not batObj:
        return
    if not batObj.IsAlive():
        return
    GameWorld.DebugLog("---[武将回合结束] : curID=%s,curHP=%s/%s" % (batObj.GetID(), batObj.GetHP(), batObj.GetMaxHP()))
    turnFight.ResetOneActionUseSkillCnt()
    RefreshObjBuffTime(turnFight, batObj)
    return
def ResetObjSkill(batObj):
    ## 重置所有技能,一般是每场战斗开始的重置
    curID = batObj.GetID()
    batObj.ResetSkillUseCnt() # 使用次数
    skillManager = batObj.GetSkillManager()
    for index in range(0, skillManager.GetSkillCount()):
        curSkill = skillManager.GetSkillByIndex(index)
        if not curSkill:
            continue
        skillID = curSkill.GetSkillID()
        initCD = curSkill.GetCoolDownInit()
        if initCD:
            curSkill.SetRemainTime(initCD)
            GameWorld.DebugLog("技能初始CD: curID=%s,skillID=%s,initCD=%s" % (curID, skillID, initCD))
        elif curSkill.GetRemainTime():
            curSkill.SetRemainTime(0)
        curSkill.SetEnergy(0)
    return
def RefreshObjSkillByTurn(batObj):
    '''按回合刷新技能:默认以大回合统一减1回合
    '''
    curID = batObj.GetID()
    skillManager = batObj.GetSkillManager()
    for index in range(0, skillManager.GetSkillCount()):
        curSkill = skillManager.GetSkillByIndex(index)
        if not curSkill:
            continue
        skillID = curSkill.GetSkillID()
        preTurnUseCnt = batObj.GetSkillTurnUseCnt(skillID)
        remainTime = curSkill.GetRemainTime()
        if remainTime <= 0:
            continue
        if preTurnUseCnt > 0:
            GameWorld.DebugLog("    上回合有使用技能本回合不刷新CD: curID=%s,skillID=%s,remainTime=%s,preTurnUseCnt=%s" % (curID, skillID, remainTime, preTurnUseCnt))
            continue
        remainTime -= 1
        curSkill.SetRemainTime(remainTime)
        GameWorld.DebugLog("    更新技能CD: curID=%s,skillID=%s,remainTime=%s" % (curID, skillID, remainTime))
    batObj.ResetSkillTurnUseCnt() # 重置回合使用次数,放刷新CD后重置
    return
def RefreshObjBuffTime(turnFight, batObj):
    '''刷新buff持续时间:以武将自身回合前、回合后处理buff持续时间
    回合前添加的buff在回合开始减1,回合后添加的buff在回合结束减1
    '''
    curID = batObj.GetID()
    curTiming = batObj.GetTiming() # 时间节点 0-回合前;1-回合后
    buffMgr = batObj.GetBuffManager()
    for index in range(buffMgr.GetBuffCount())[::-1]:
        buff = buffMgr.GetBuffByIndex(index)
        buffID = buff.GetBuffID()
        skillID = buff.GetSkillID()
        skillData = buff.GetSkillData()
        if skillData.GetSkillType() in ChConfig.Def_LstBuff_List:
            #GameWorld.DebugLog("    持续类buff由触发时机决定剩余时间! curID=%s,index=%s,skillID=%s,buffID=%s" % (curID, index, skillID, buffID))
            continue
        remainTime = buff.GetRemainTime()
        if remainTime <= 0:
            # 永久buff不处理
            #GameWorld.DebugLog("    永久buff不处理! curID=%s,index=%s,skillID=%s" % (curID, index, skillID))
            continue
        addTiming = buff.GetAddTiming()
        if not buff.GetRefreshState():
            GameWorld.DebugLog("    未刷新过的buff更新为刷新过! curID=%s,index=%s,skillID=%s,buffID=%s,addTiming=%s" % (curID, index, skillID, buffID, addTiming))
            buff.SetRefreshState(1)
            continue
        if addTiming != curTiming:
            GameWorld.DebugLog("    buff添加时机不同不处理! curID=%s,index=%s,skillID=%s,buffID=%s,addTiming=%s" % (curID, index, skillID, buffID, addTiming))
            continue
        remainTime -= 1
        GameWorld.DebugLog("    更新buff回合: curID=%s,buffID=%s,skillID=%s,remainTime=%s,addTiming=%s" % (curID, buffID, skillID, remainTime, addTiming))
        if remainTime > 0:
            buff.SetRemainTime(remainTime)
            TurnBuff.SyncBuffRefresh(turnFight, batObj, buff)
        else:
            TurnBuff.DoBuffDel(turnFight, batObj, buff)
    return
def AddTurnObjCureHP(curObj, srcObj, addValue, cureHP, skillID=0):
    ## 回合对象添加治疗值
    # @param curObj: 获得治疗的对象
    # @param srcObj: 来自谁的治疗
    # @param addValue: 治疗值
    # @param cureHP: 实际回血量
    if not srcObj:
        return
    curID = curObj.GetID()
    srcID = srcObj.GetID()
    updStatValue = srcObj.StatCureValue(cureHP)
    GameWorld.DebugLog("        统计治疗: curID=%s,srcID=%s,skillID=%s,addValue=%s,cureHP=%s,updStatValue=%s"
                   % (curID, srcID, skillID, addValue, cureHP, updStatValue))
    return
def AddTurnObjHurtValue(curBatObj, tagBatObj, hurtValue, lostHP, skillID=0, isBounce=False):
    ## 回合对象添加伤害值
    # @param isBounce: 是否反弹伤害
    if hurtValue <= 0:
        return
    curID = curBatObj.GetID()
    tagID = tagBatObj.GetID()
    if curID != tagID:
        updStatValue = curBatObj.StatHurtValue(hurtValue)
        GameWorld.DebugLog("        统计伤血: curID=%s,tagID=%s,skillID=%s,hurtValue=%s,lostHP=%s,updStatValue=%s,tagHP=%s,isBounce=%s"
                       % (curID, tagID, skillID, hurtValue, lostHP, updStatValue, tagBatObj.GetHP(), isBounce))
        if tagBatObj:
            updStatValue = tagBatObj.StatDefValue(hurtValue)
            GameWorld.DebugLog("        统计承伤: curID=%s,tagID=%s,skillID=%s,hurtValue=%s,lostHP=%s,updStatValue=%s,curHP=%s,isBounce=%s"
                           % (tagID, curID, skillID, hurtValue, lostHP, updStatValue, tagBatObj.GetHP(), isBounce))
    else:
        # 如换血类技能,自残的伤害不算输出
        GameWorld.DebugLog("        自残: curID=%s,tagID=%s,skillID=%s,hurtValue=%s,lostHP=%s,curHP=%s"
                           % (curID, tagID, skillID, hurtValue, lostHP, curBatObj.GetHP()))
    return
def OnObjAction(turnFight, curBatObj, isExtra=False):
    ## 战斗单位行动
    # @param isExtra: 是否额外行动的
    if not curBatObj:
        return
    curHP = curBatObj.GetHP()
    objID = curBatObj.GetID()
    if curHP <= 0:
        return
    turnNum = turnFight.turnNum
    objName = GetObjName(curBatObj)
    # 是否可行动状态判断
    canAction = curBatObj.CanAction()
    if not canAction:
        GameWorld.DebugLog("★回合%s %s 当前状态不可行动! isExtra=%s" % (turnNum, objName, isExtra))
        return
    atk = curBatObj.GetAtk()
    curXP = curBatObj.GetXP()
    GameWorld.DebugLog("★回合%s %s %s行动 : atk=%s,curHP=%s/%s,curXP=%s" % (turnNum, objName, "额外" if isExtra else "", atk, curHP, curBatObj.GetMaxHP(), curXP))
    turnFight.ResetOneActionUseSkillCnt()
    turnFight.syncObjAction(turnNum, objID)
    TurnPassive.OnTriggerPassiveEffect(turnFight, curBatObj, ChConfig.TriggerWay_HeroActionStart)
    xpMax = IpyGameDataPY.GetFuncCfg("AngerXP", 2)
    skillManager = curBatObj.GetSkillManager()
    useSkillList = []
    #GameWorld.DebugLog('skillCount=%s' % skillManager.GetSkillCount(), npcID)
    for index in range(0, skillManager.GetSkillCount()):
        useSkill = skillManager.GetSkillByIndex(index)
        if not useSkill:
            continue
        if useSkill.GetFuncType() not in [ChConfig.Def_SkillFuncType_TurnNormaSkill, ChConfig.Def_SkillFuncType_AngerSkill]:
            #只能主动释放普攻或怒技
            continue
        #被动技能无法使用
        if SkillCommon.isPassiveSkill(useSkill):
            continue
        #还在冷却时间内无法释放
        if useSkill.GetRemainTime():
            continue
        skillID = useSkill.GetSkillID()
        # 常规攻击优先xp
        if SkillCommon.isAngerSkill(useSkill):
            if curXP < xpMax and not useSkill.GetEffectByID(ChConfig.SkillEff_AngerSkillNoXP):
                continue
            if curBatObj.CheckInState(ChConfig.BatObjState_Sneer):
                GameWorld.DebugLog("嘲讽状态下,无法主动释放怒技! skillID=%s" % skillID) # 可被动释放怒技,如怒技追击
                continue
            useCnt = -1 # xp技能优先释放
        else:
            if useSkill.GetEffectByID(ChConfig.SkillEff_ActionUseInvalid): # 配置该效果时如果被嘲讽,相当于无法行动
                GameWorld.DebugLog("行动时无法释放该技能! skillID=%s" % skillID)
                continue
            useCnt = curBatObj.GetSkillUseCnt(skillID)
        useSkillList.append([useCnt, skillID, useSkill])
    useSkillList.sort() # 按使用次数优先升序排,使用次数低的优先判断使用
    #GameWorld.DebugLog('    技能使用顺序 = useSkillList%s' % str(useSkillList), npcID)
    for useInfo in useSkillList:
        useSkill = useInfo[-1]
        if TurnSkill.OnUseSkill(turnFight, curBatObj, useSkill):
            break
    TurnPassive.OnTriggerPassiveEffect(turnFight, curBatObj, ChConfig.TriggerWay_HeroActionEnd)
    return True
def SetObjKilled(turnFight, gameObj, killer=None, useSkill=None):
    curPlayer = turnFight.curPlayer
    npcID = gameObj.GetNPCID()
    # 非主线的PVE目标怪物
    if npcID and curPlayer and turnFight.isFBMap() and gameObj.GetFaction() != ChConfig.Def_FactionA:
        if not FBLogic.OnFBNPCKilledBefore(curPlayer, turnFight, gameObj, killer, useSkill):
            return
    objID = gameObj.GetID()
    killerObjID = killer.GetID() if killer else 0
    skillID = useSkill.GetSkillID() if useSkill else 0
    GameWorld.DebugLog("        %s 回合战斗主体被击杀: curID=%s,killerObjID=%s,skillID=%s" % (GetObjName(gameObj), objID, killerObjID, skillID))
    gameObj.SetDead()
    clientPack = ObjPool.GetPoolMgr().acquire(ChPyNetSendPack.tagMCTurnFightObjDead)
    clientPack.ObjID = objID
    clientPack.KillerObjID = killerObjID
    clientPack.SkillID = skillID
    turnFight.addBatPack(clientPack)
    # 暂时只算主线小怪
    if curPlayer and turnFight.mapID == ChConfig.Def_FBMapID_Main and gameObj.GetFaction() != ChConfig.Def_FactionA:
        PlayerTask.AddTaskValue(curPlayer, ChConfig.TaskType_KillNPC, 1)
        PlayerActivity.AddDailyTaskValue(curPlayer, ChConfig.DailyTask_KillNPC, 1)
    return True
def OnTurnAllOver(guid):
    ## 所有回合已经全部执行完毕
    GameWorld.DebugLog("所有回合结束")
    turnFight = GetTurnFightMgr().getTurnFight(guid)
    if not turnFight:
        return
    if turnFight.winFaction:
        return
    if turnFight.playerID:
        # 玩家发起的,未击杀对方,算玩家输
        turnFight.winFaction = ChConfig.Def_FactionB
    else:
        # 系统场次,按一定规则来,这里先随机
        turnFight.winFaction = random.choice([ChConfig.Def_FactionA, ChConfig.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
    turnFight.isWin = (winFaction == ChConfig.Def_FactionA)
    GameWorld.DebugLog("--- 战斗结束处理 ---, winFaction=%s, costTime=%ss, %s" % (winFaction, turnFight.costTime, guid))
    # 统计明细
    batObjMgr = BattleObj.GetBatObjMgr()
    statInfo = {}
    for faction in turnFight.factionDict.keys():
        if str(faction) not in statInfo:
            statInfo[str(faction)] = {}
        facStatInfo = statInfo[str(faction)]
        batFaction = turnFight.getBatFaction(faction)
        batFaction.totalHurt = 0
        for num in batFaction.lineupDict.keys():
            if str(num) not in facStatInfo:
                facStatInfo[str(num)] = {}
            lineupStatInfo = facStatInfo[str(num)]
            batLineup = batFaction.getBatlineup(num)
            batLineup.totalHurt = 0
            GameWorld.DebugLog("阵容明细: faction=%s,num=%s" % (faction, num))
            for posNum, objID in batLineup.posObjIDDict.items():
                batObj = batObjMgr.getBatObj(objID)
                if not batObj:
                    continue
                objID = batObj.GetID()
                npcID = batObj.GetNPCID()
                heroID = batObj.GetHeroID()
                atkHurt = batObj.hurtStat
                defHurt = batObj.defStat
                cureHP = batObj.cureStat
                batLineup.totalHurt += atkHurt
                batFaction.totalHurt += atkHurt
                GameWorld.DebugLog("    Pos:%s ID=%s,npcID=%s,heroID=%s,HP=%s/%s, 输出=%s,承伤=%s,治疗=%s"
                                   % (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}
    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)
    return
#// B4 14 查看战报 #tagCSTurnFightReportView
#
#struct    tagCSTurnFightReportView
#{
#    tagHead        Head;
#    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