battle.go 11 KB

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