package battle import ( cherryApp "f1-game/internal/cherry_app" "f1-game/internal/code" "f1-game/internal/component/redis" "f1-game/internal/constant" "f1-game/internal/data" "f1-game/internal/enum" nameActor "f1-game/internal/name/actor" nameRemote "f1-game/internal/name/remote" "f1-game/internal/pb" "f1-game/internal/types" "fmt" "time" cfacade "github.com/cherry-game/cherry/facade" ctime "github.com/cherry-game/cherry/extend/time" clog "github.com/cherry-game/cherry/logger" cnats "github.com/cherry-game/cherry/net/nats" ) const ( BATTLE_VERIFY_SUBJECT = "BattleValidation" // 战斗验证主题 BATTLE_RECORD_ID_ERR_PREFIX = "error" // 报错验证的录像ID前缀 ) // 战斗验证 func Validation(recordID string, battleParam *pb.BattleParam) *pb.BattleResult { reqData, err := cherryApp.Serializer().Marshal(battleParam) if err != nil { clog.Warnf("[BattleValidation] Marshal battleParam error. err = %v", err) return nil } start := time.Now() ret := validationRequest(recordID, reqData) cost := time.Since(start) // 打印耗时 if cost > 100*time.Millisecond { clog.Warnf("[BattleValidation] cost time = %v", cost) } return ret } func validationRequest(recordID string, reqData []byte) *pb.BattleResult { conn := cnats.GetConnect() if conn == nil { clog.Errorf("[BattleValidation] nats connect is nil") return nil } // TODO 待后面战斗逻辑稳定后,修改为 conn.RequestSync() replyData, err := conn.Request(BATTLE_VERIFY_SUBJECT, reqData) if err != nil { clog.Warnf("[BattleValidation] Request replyData error. err = %v", err) return nil } reply := &pb.BattleReplay{} err = cherryApp.Serializer().Unmarshal(replyData, reply) if err != nil { clog.Warnf("[BattleValidation] Unmarshal replyData error. err = %v", err) return nil } if code.IsFail(reply.ErrorCode) { clog.Warnf("[BattleValidation] battle fail. errorCode = %v, errorText = %v", reply.ErrorCode, reply.ErrorText) // 错误就以时间格式为recordID recordID = fmt.Sprintf("%s_%d_%s", BATTLE_RECORD_ID_ERR_PREFIX, reply.ErrorCode, ctime.Now().ToShortDateTimeFormat()) sendBattleRecord(recordID, reqData, true) return nil } // 保存成功的请求数据 sendBattleRecord(recordID, reqData, false) return reply.Result } // 发送给record节点存储战斗录像 func sendBattleRecord(recordID string, reqData []byte, isFail bool) { var ( req = &pb.RecordData{ RecordID: recordID, Data: reqData, IsFail: isFail, } ) // 随机一个record节点 randRecord, found := cherryApp.Discovery().Random(constant.NodeRecord) if !found { clog.Warnf("[sendBattleRecord] random record node error. node = %s", constant.NodeRecord) return } target := cfacade.NewPath(randRecord.GetNodeID(), nameActor.Record) cherryApp.Call(target, nameRemote.Map_BattleRecord, req) } // 获取最大血量 func GetMaxHealth(attrs *pb.Attrs) int64 { var maxHp int64 for _, attr := range attrs.List { if attr.Key == enum.Attr_MaxHP { maxHp = attr.Value break } } return maxHp } // 设置剩余血量 func SetHealth(attrs *pb.Attrs, hpPct int32) { maxHP := GetMaxHealth(attrs) //根据最大血量和当前剩余血量万分比计算剩余血量 hp := maxHP * int64(hpPct) / constant.RatioBase for _, attr := range attrs.List { if attr.Key == enum.Attr_HP { clog.Debugf("SetHealth: maxHP=%d, hpPct=%d, hp=[%d ->%d]", maxHP, hpPct, attr.Value, hp) attr.Value = max(0, hp) break } } } // 检查所有战斗小队小兵数量 func CheckBattleSquadTroopsNum(battleCampParam *pb.BattleCampParam) bool { // 一个小队都没有 if len(battleCampParam.Squads) == 0 { return false } // 怪物的不需要检查,可以不带小兵 if battleCampParam.PlayerID == 0 { return true } for _, squad := range battleCampParam.Squads { if squad.TroopsNum > 0 { return true } } return false } func NewTeamBattleDamage(teamID int32, objectID int64) *pb.TeamBattleDamage { return &pb.TeamBattleDamage{ TeamID: teamID, ObjectID: objectID, Members: map[int32]*pb.TeamMemberDamage{}, } } // 获取队伍战后数据 func GetBattleSquadResult(camp int32, id int64, br *pb.BattleResult) *pb.BattleSquadResult { for _, result := range br.CampResult { if result.Camp == camp { for _, squad := range result.SquadResults { if squad.FighterResult.Id == id { return squad } } } } return nil } // 根据阵营获取战斗后的阵容数据 func GetBattleCampResult(br *pb.BattleResult, camp int32) *pb.BattleCampResult { for _, result := range br.CampResult { if result.Camp == camp { return result } } return nil } // 更新战斗对象剩余兵力 func UpdateBattleParam(battleParam *pb.BattleCampParam, battleResult *pb.BattleResult) int32 { var ( totalTroopLoss = int32(0) // 总战损兵力 newSquads []*pb.BattleSquadParam // 新的小队参数列表 ) for _, squad := range battleParam.Squads { squadResult := GetBattleSquadResult(battleParam.Camp, squad.Fighter.Id, battleResult) totalTroopLoss += squad.TroopsNum - squadResult.SoldierNum squad.TroopsNum = squadResult.SoldierNum // 设置血量 // SetHealth(squad.Attrs, squadResult.FighterResult.Hp) // 过滤阵容剩余兵力为0的小队 if squad.TroopsNum > 0 { newSquads = append(newSquads, squad) } } // 更新小队数据 battleParam.Squads = newSquads return totalTroopLoss } // 统一处理英雄属性和小兵属性拆分 func handleHeroAndSoldierAttr(heroID int32, attrs types.Attrs) (*pb.Attrs, *pb.Attrs) { var ( heroConfig, _ = data.Hero.GetByID(heroID) job = enum.Job(heroConfig.Job) heroAttrs = types.Attrs{} troopsAttrs = types.Attrs{} ) for key, attrVal := range attrs { if enum.SquadKeys[key] { switch key { case enum.Attr_SquadAttackBonus, enum.Attr_SquadHealthBonus: troopsAttrs.Add(key, attrVal) case enum.Attr_InfantryAllAttributeBonus: if job == enum.Job_Infantry { troopsAttrs.Add(key, attrVal) } case enum.Attr_CavalryAllAttributeBonus: if job == enum.Job_Cavalry { troopsAttrs.Add(key, attrVal) } case enum.Attr_ArcherAllAttributeBonus: if job == enum.Job_Archer { troopsAttrs.Add(key, attrVal) } } } else { heroAttrs.Add(key, attrVal) } } // TODO 设置英雄当前血量为最大血量 heroAttrs.Set(enum.Attr_HP, heroAttrs.Get(enum.Attr_MaxHP)) return heroAttrs.ToProto(), troopsAttrs.ToProto() } // 获取战斗请求的所有玩家战斗参数,包括所有英雄所在部队的位置 func GetServerBattleCampParams(battleReq *pb.BattleReq) (map[int64]*pb.ServerBattleCampParam, bool) { battleTeamDataList := battleReq.Attackers // pvp 防守方战斗数据 if enum.IsTilePVPBattle(enum.TileBattleType(battleReq.BattleType)) { battleTeamDataList = append(battleTeamDataList, battleReq.Defender) } if len(battleTeamDataList) == 0 { return nil, false } // 从redis一次性获取玩家战斗数据 teamMap, heroMap := redis.Game.GetPlayerBattleData(battleTeamDataList...) if len(teamMap) == 0 || len(heroMap) == 0 { return nil, false } // 将单个玩家的战斗数据封装成一个ServerBattleCampParam buildCampParam := func(battleTeamData *pb.BattleTeamData) *pb.ServerBattleCampParam { team := teamMap[battleTeamData.PlayerID] if team == nil { return nil } param := &pb.ServerBattleCampParam{ Param: &pb.BattleCampParam{ Camp: enum.BattleCampType_Left, PlayerID: battleTeamData.PlayerID, }, HeroTeamIndex: &pb.I64I32Map{ Value: map[int64]int32{}, }, } // 判断是否是防守方 if battleTeamData.PlayerID == battleReq.Defender.PlayerID { param.Param.Camp = enum.BattleCampType_Right } for _, heroID := range battleTeamData.TeamHeros { if fighter, found := heroMap[heroID]; found { teamMemeber := team.Members[heroID] attrs := types.NewAttrs() // 英雄的基础属性 for _, attr := range fighter.Attrs.List { attrs.Add(attr.Key, attr.Value) } // 羁绊属性 for _, attr := range teamMemeber.FetterAttrs.List { attrs.Add(attr.Key, attr.Value) } // 城建属性 for _, attr := range team.FacilityAttrs.List { // TODO 判断环境增加和删除属性 307 308,要转成战斗对应的攻防血的属性 attrs.Add(attr.Key, attr.Value) } // TODO 地图增加的属性 // 处理英雄属性和小兵属性 heroAttrs, troopsAttrs := handleHeroAndSoldierAttr(fighter.ConfigID, attrs) // 设置英雄属性 fighter.Attrs = heroAttrs // 没有小兵的小队不参与战斗 if teamMemeber.TroopsNum > 0 { // 构建一个战斗小队 param.Param.Squads = append(param.Param.Squads, &pb.BattleSquadParam{ TileIndex: teamMemeber.Index, Fighter: playerHeroToFighter(fighter), TroopsID: teamMemeber.TroopsID, TroopsNum: teamMemeber.TroopsNum, FirstTroopsNum: teamMemeber.TroopsNum, // 增加首战兵力值,连战过程不变(避免PVE连续战斗英雄衰减) HeroTroopNum: teamMemeber.TroopsLimit, Attrs: troopsAttrs, // 设置小兵属性 }) } // 记录英雄在队伍中的位置 param.HeroTeamIndex.Value[fighter.Id] = teamMemeber.TeamIndex } } return param } // 逐个构建玩家战斗参数 ret := map[int64]*pb.ServerBattleCampParam{} for _, battleTeamData := range battleTeamDataList { ret[battleTeamData.PlayerID] = buildCampParam(battleTeamData) } return ret, true } // pb.PlayerHero to pb.Fighter func playerHeroToFighter(fighter *pb.PlayerHero) *pb.Fighter { if fighter == nil { return nil } return &pb.Fighter{ Id: fighter.Id, BattleObjType: enum.BattleObjType_Hero, ConfigID: fighter.ConfigID, Attrs: fighter.Attrs, Skills: fighter.Skills, Level: fighter.Level, Star: fighter.Star, } } // 根据战斗参数构建战斗结果 func BuildValidationResult(battleParam *pb.BattleParam) *pb.BattleResult { winCamp := int32(enum.BattleResult_Draw) // 先判断左边的进攻方,这就代表同样是双方都是没有兵了,进攻方判输 for _, camp := range battleParam.Camps { if camp.Camp == enum.BattleCampType_Left && len(camp.Squads) < 1 { winCamp = enum.BattleCampType_Right break } if camp.Camp == enum.BattleCampType_Right && len(camp.Squads) < 1 { winCamp = enum.BattleCampType_Left break } } ret := &pb.BattleResult{ VictoriousCamp: winCamp, CampResult: buildCampResult(battleParam.Camps), } return ret } // 构建战斗结果 func buildCampResult(Camps []*pb.BattleCampParam) []*pb.BattleCampResult { var result []*pb.BattleCampResult for _, camp := range Camps { var squadResults []*pb.BattleSquadResult for _, squad := range camp.Squads { squadResults = append(squadResults, &pb.BattleSquadResult{ FighterResult: &pb.FighterResult{ Id: squad.Fighter.Id, BattleObjType: squad.Fighter.BattleObjType, ConfigID: squad.Fighter.ConfigID, IsDead: squad.Fighter.IsDead, Hp: constant.RatioBase, // 默认满血 }, SoldierNum: squad.TroopsNum, // 剩余兵 }) } result = append(result, &pb.BattleCampResult{ Camp: camp.Camp, PlayerID: camp.PlayerID, SquadResults: squadResults, }) } return result }