battle.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430
  1. package battle
  2. import (
  3. capp "f1-game/internal/cherry_app"
  4. "f1-game/internal/code"
  5. "f1-game/internal/component/redis"
  6. "f1-game/internal/constant"
  7. "f1-game/internal/data"
  8. "f1-game/internal/enum"
  9. nameActor "f1-game/internal/name/actor"
  10. nameRemote "f1-game/internal/name/remote"
  11. "f1-game/internal/pb"
  12. "f1-game/internal/types"
  13. "fmt"
  14. "time"
  15. cfacade "github.com/cherry-game/cherry/facade"
  16. ctime "github.com/cherry-game/cherry/extend/time"
  17. clog "github.com/cherry-game/cherry/logger"
  18. cnats "github.com/cherry-game/cherry/net/nats"
  19. )
  20. const (
  21. BATTLE_VERIFY_SUBJECT = "BattleValidation" // 战斗验证主题
  22. BATTLE_RECORD_ID_ERR_PREFIX = "error" // 报错验证的录像ID前缀
  23. )
  24. // 战斗验证
  25. func Validation(recordID string, battleParam *pb.BattleParam) *pb.BattleResult {
  26. start := time.Now()
  27. ret := validationRequest(recordID, battleParam)
  28. cost := time.Since(start)
  29. // 打印耗时
  30. if cost > 100*time.Millisecond {
  31. clog.Warnf("[BattleValidation] cost time = %v", cost)
  32. }
  33. return ret
  34. }
  35. func validationRequest(recordID string, battleParam *pb.BattleParam) *pb.BattleResult {
  36. reqData, err := capp.Serializer().Marshal(battleParam)
  37. if err != nil {
  38. clog.Warnf("[BattleValidation] Marshal battleParam error. err = %v", err)
  39. return nil
  40. }
  41. conn := cnats.GetConnect()
  42. if conn == nil {
  43. clog.Errorf("[BattleValidation] nats connect is nil")
  44. return nil
  45. }
  46. // TODO 待后面战斗逻辑稳定后,修改为 conn.RequestSync()
  47. replyData, err := conn.Request(BATTLE_VERIFY_SUBJECT, reqData)
  48. if err != nil {
  49. clog.Warnf("[BattleValidation] Request replyData error. err = %v", err)
  50. return nil
  51. }
  52. reply := &pb.BattleReplay{}
  53. err = capp.Serializer().Unmarshal(replyData, reply)
  54. if err != nil {
  55. clog.Warnf("[BattleValidation] Unmarshal replyData error. err = %v", err)
  56. return nil
  57. }
  58. if code.IsFail(reply.ErrorCode) {
  59. clog.Warnf("[BattleValidation] battle fail. errorCode = %v, errorText = %v", reply.ErrorCode, reply.ErrorText)
  60. // 错误就以时间格式为recordID
  61. recordID = fmt.Sprintf("%s_%d_%s", BATTLE_RECORD_ID_ERR_PREFIX, reply.ErrorCode, ctime.Now().ToShortDateTimeFormat())
  62. sendBattleRecord(recordID, reqData, true)
  63. return nil
  64. }
  65. // 如果设置生成配置数据,则给录像赋值配置数据
  66. if battleParam.GenerateConfigData && len(reply.Result.ConfigData) > 0 {
  67. battleParam.ConfigData = reply.Result.ConfigData
  68. if reqData, err = capp.Serializer().Marshal(battleParam); err != nil {
  69. clog.Warnf("[BattleValidation] Marshal GenerateConfigData battleParam error. err = %v", err)
  70. return nil
  71. }
  72. }
  73. // 保存成功的请求数据
  74. sendBattleRecord(recordID, reqData, false)
  75. return reply.Result
  76. }
  77. // 发送给record节点存储战斗录像
  78. func sendBattleRecord(recordID string, reqData []byte, isFail bool) {
  79. var (
  80. req = &pb.RecordData{
  81. RecordID: recordID,
  82. Data: reqData,
  83. IsFail: isFail,
  84. }
  85. )
  86. // 随机一个record节点
  87. randRecord, found := capp.Discovery().Random(constant.NodeRecord)
  88. if !found {
  89. clog.Warnf("[sendBattleRecord] random record node error. node = %s", constant.NodeRecord)
  90. return
  91. }
  92. target := cfacade.NewPath(randRecord.GetNodeID(), nameActor.Record)
  93. capp.Call(target, nameRemote.Map_BattleRecord, req)
  94. }
  95. // 获取最大血量
  96. func GetMaxHealth(attrs *pb.Attrs) int64 {
  97. var maxHp int64
  98. for _, attr := range attrs.List {
  99. if attr.Key == enum.Attr_MaxHP {
  100. maxHp = attr.Value
  101. break
  102. }
  103. }
  104. return maxHp
  105. }
  106. // 设置剩余血量
  107. func SetHealth(attrs *pb.Attrs, hpPct int32) {
  108. maxHP := GetMaxHealth(attrs)
  109. //根据最大血量和当前剩余血量万分比计算剩余血量
  110. hp := maxHP * int64(hpPct) / constant.RatioBase
  111. for _, attr := range attrs.List {
  112. if attr.Key == enum.Attr_HP {
  113. clog.Debugf("SetHealth: maxHP=%d, hpPct=%d, hp=[%d ->%d]", maxHP, hpPct, attr.Value, hp)
  114. attr.Value = max(0, hp)
  115. break
  116. }
  117. }
  118. }
  119. // 检查所有战斗小队小兵数量
  120. func CheckBattleSquadTroopsNum(battleCampParam *pb.BattleCampParam) bool {
  121. // 一个小队都没有
  122. if len(battleCampParam.Squads) == 0 {
  123. return false
  124. }
  125. // 怪物的不需要检查,可以不带小兵
  126. if battleCampParam.PlayerID == 0 {
  127. return true
  128. }
  129. for _, squad := range battleCampParam.Squads {
  130. if squad.TroopsNum > 0 {
  131. return true
  132. }
  133. }
  134. return false
  135. }
  136. func NewTeamBattleDamage(teamID int32, objectID int64) *pb.TeamBattleDamage {
  137. return &pb.TeamBattleDamage{
  138. TeamID: teamID,
  139. ObjectID: objectID,
  140. Members: map[int32]*pb.TeamMemberDamage{},
  141. }
  142. }
  143. // 获取队伍战后数据
  144. func GetBattleSquadResult(camp int32, id int64, br *pb.BattleResult) *pb.BattleSquadResult {
  145. for _, result := range br.CampResult {
  146. if result.Camp == camp {
  147. for _, squad := range result.SquadResults {
  148. if squad.FighterResult.Id == id {
  149. return squad
  150. }
  151. }
  152. }
  153. }
  154. return nil
  155. }
  156. // 根据阵营获取战斗后的阵容数据
  157. func GetBattleCampResult(br *pb.BattleResult, camp int32) *pb.BattleCampResult {
  158. for _, result := range br.CampResult {
  159. if result.Camp == camp {
  160. return result
  161. }
  162. }
  163. return nil
  164. }
  165. // 更新战斗对象剩余兵力
  166. func UpdateBattleParam(battleParam *pb.BattleCampParam, battleResult *pb.BattleResult) int32 {
  167. var (
  168. totalTroopLoss = int32(0) // 总战损兵力
  169. newSquads []*pb.BattleSquadParam // 新的小队参数列表
  170. )
  171. for _, squad := range battleParam.Squads {
  172. squadResult := GetBattleSquadResult(battleParam.Camp, squad.Fighter.Id, battleResult)
  173. totalTroopLoss += squad.TroopsNum - squadResult.SoldierNum
  174. squad.TroopsNum = squadResult.SoldierNum
  175. // 设置血量
  176. // SetHealth(squad.Attrs, squadResult.FighterResult.Hp)
  177. // 过滤阵容剩余兵力为0的小队
  178. if squad.TroopsNum > 0 {
  179. newSquads = append(newSquads, squad)
  180. }
  181. }
  182. // 更新小队数据
  183. battleParam.Squads = newSquads
  184. return totalTroopLoss
  185. }
  186. // 统一处理英雄属性和小兵属性拆分
  187. func handleHeroAndSoldierAttr(heroID int32, attrs types.Attrs) (*pb.Attrs, *pb.Attrs) {
  188. var (
  189. heroConfig, _ = data.Hero.GetByID(heroID)
  190. job = enum.Job(heroConfig.Job)
  191. heroAttrs = types.Attrs{}
  192. troopsAttrs = types.Attrs{}
  193. )
  194. for key, attrVal := range attrs {
  195. if enum.SquadKeys[key] {
  196. switch key {
  197. case enum.Attr_SquadAttackBonus, enum.Attr_SquadHealthBonus:
  198. troopsAttrs.Add(key, attrVal)
  199. case enum.Attr_InfantryAllAttributeBonus:
  200. if job == enum.Job_Infantry {
  201. troopsAttrs.Add(key, attrVal)
  202. }
  203. case enum.Attr_CavalryAllAttributeBonus:
  204. if job == enum.Job_Cavalry {
  205. troopsAttrs.Add(key, attrVal)
  206. }
  207. case enum.Attr_ArcherAllAttributeBonus:
  208. if job == enum.Job_Archer {
  209. troopsAttrs.Add(key, attrVal)
  210. }
  211. }
  212. } else {
  213. heroAttrs.Add(key, attrVal)
  214. }
  215. }
  216. // TODO 设置英雄当前血量为最大血量
  217. heroAttrs.Set(enum.Attr_HP, heroAttrs.Get(enum.Attr_MaxHP))
  218. return heroAttrs.ToProto(), troopsAttrs.ToProto()
  219. }
  220. // 获取战斗请求的所有玩家战斗参数,包括所有英雄所在部队的位置
  221. func GetServerBattleCampParams(battleReq *pb.BattleReq) (map[int64]*pb.ServerBattleCampParam, bool) {
  222. battleTeamDataList := battleReq.Attackers
  223. // pvp 防守方战斗数据
  224. if enum.IsTilePVPBattle(enum.TileBattleType(battleReq.BattleType)) {
  225. battleTeamDataList = append(battleTeamDataList, battleReq.Defender)
  226. }
  227. if len(battleTeamDataList) == 0 {
  228. return nil, false
  229. }
  230. // 从redis一次性获取玩家战斗数据
  231. teamMap, heroMap := redis.Game.GetPlayerBattleData(battleTeamDataList...)
  232. if len(teamMap) == 0 || len(heroMap) == 0 {
  233. return nil, false
  234. }
  235. // 将单个玩家的战斗数据封装成一个ServerBattleCampParam
  236. buildCampParam := func(battleTeamData *pb.BattleTeamData) *pb.ServerBattleCampParam {
  237. team := teamMap[battleTeamData.PlayerID]
  238. if team == nil {
  239. return nil
  240. }
  241. param := &pb.ServerBattleCampParam{
  242. Param: &pb.BattleCampParam{
  243. Camp: enum.BattleCampType_Left,
  244. PlayerID: battleTeamData.PlayerID,
  245. },
  246. HeroTeamIndex: &pb.I64I32Map{
  247. Value: map[int64]int32{},
  248. },
  249. }
  250. // 判断是否是防守方
  251. if battleTeamData.PlayerID == battleReq.Defender.PlayerID {
  252. param.Param.Camp = enum.BattleCampType_Right
  253. }
  254. for _, heroID := range battleTeamData.TeamHeros {
  255. if fighter, found := heroMap[heroID]; found {
  256. teamMemeber := team.Members[heroID]
  257. attrs := types.NewAttrs()
  258. // 英雄的基础属性
  259. for _, attr := range fighter.Attrs.List {
  260. attrs.Add(attr.Key, attr.Value)
  261. }
  262. // 羁绊属性
  263. for _, attr := range teamMemeber.FetterAttrs.List {
  264. attrs.Add(attr.Key, attr.Value)
  265. }
  266. // 城建属性
  267. for _, attr := range team.FacilityAttrs.List {
  268. // TODO 判断环境增加和删除属性 307 308,要转成战斗对应的攻防血的属性
  269. attrs.Add(attr.Key, attr.Value)
  270. }
  271. // TODO 地图增加的属性
  272. // 处理英雄属性和小兵属性
  273. heroAttrs, troopsAttrs := handleHeroAndSoldierAttr(fighter.ConfigID, attrs)
  274. // 设置英雄属性
  275. fighter.Attrs = heroAttrs
  276. // 没有小兵的小队不参与战斗
  277. if teamMemeber.TroopsNum > 0 {
  278. // 构建一个战斗小队
  279. param.Param.Squads = append(param.Param.Squads, &pb.BattleSquadParam{
  280. TileIndex: teamMemeber.Index,
  281. Fighter: playerHeroToFighter(fighter),
  282. TroopsID: teamMemeber.TroopsID,
  283. TroopsNum: teamMemeber.TroopsNum,
  284. FirstTroopsNum: teamMemeber.TroopsNum, // 增加首战兵力值,连战过程不变(避免PVE连续战斗英雄衰减)
  285. HeroTroopNum: teamMemeber.TroopsLimit,
  286. Attrs: troopsAttrs, // 设置小兵属性
  287. })
  288. }
  289. // 记录英雄在队伍中的位置
  290. param.HeroTeamIndex.Value[fighter.Id] = teamMemeber.TeamIndex
  291. }
  292. }
  293. return param
  294. }
  295. // 逐个构建玩家战斗参数
  296. ret := map[int64]*pb.ServerBattleCampParam{}
  297. for _, battleTeamData := range battleTeamDataList {
  298. ret[battleTeamData.PlayerID] = buildCampParam(battleTeamData)
  299. }
  300. return ret, true
  301. }
  302. // pb.PlayerHero to pb.Fighter
  303. func playerHeroToFighter(fighter *pb.PlayerHero) *pb.Fighter {
  304. if fighter == nil {
  305. return nil
  306. }
  307. return &pb.Fighter{
  308. Id: fighter.Id,
  309. BattleObjType: enum.BattleObjType_Hero,
  310. ConfigID: fighter.ConfigID,
  311. Attrs: fighter.Attrs,
  312. Skills: fighter.Skills,
  313. Level: fighter.Level,
  314. Star: fighter.Star,
  315. }
  316. }
  317. // 根据战斗参数构建战斗结果
  318. func BuildValidationResult(battleParam *pb.BattleParam) *pb.BattleResult {
  319. winCamp := int32(enum.BattleResult_Draw)
  320. // 先判断左边的进攻方,这就代表同样是双方都是没有兵了,进攻方判输
  321. for _, camp := range battleParam.Camps {
  322. if camp.Camp == enum.BattleCampType_Left && len(camp.Squads) < 1 {
  323. winCamp = enum.BattleCampType_Right
  324. break
  325. }
  326. if camp.Camp == enum.BattleCampType_Right && len(camp.Squads) < 1 {
  327. winCamp = enum.BattleCampType_Left
  328. break
  329. }
  330. }
  331. ret := &pb.BattleResult{
  332. VictoriousCamp: winCamp,
  333. CampResult: buildCampResult(battleParam.Camps),
  334. }
  335. return ret
  336. }
  337. // 构建战斗结果
  338. func buildCampResult(Camps []*pb.BattleCampParam) []*pb.BattleCampResult {
  339. var result []*pb.BattleCampResult
  340. for _, camp := range Camps {
  341. var squadResults []*pb.BattleSquadResult
  342. for _, squad := range camp.Squads {
  343. squadResults = append(squadResults, &pb.BattleSquadResult{
  344. FighterResult: &pb.FighterResult{
  345. Id: squad.Fighter.Id,
  346. BattleObjType: squad.Fighter.BattleObjType,
  347. ConfigID: squad.Fighter.ConfigID,
  348. IsDead: squad.Fighter.IsDead,
  349. Hp: constant.RatioBase, // 默认满血
  350. },
  351. SoldierNum: squad.TroopsNum, // 剩余兵
  352. })
  353. }
  354. result = append(result, &pb.BattleCampResult{
  355. Camp: camp.Camp,
  356. PlayerID: camp.PlayerID,
  357. SquadResults: squadResults,
  358. })
  359. }
  360. return result
  361. }