package team import ( capp "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" "f1-game/internal/event" "f1-game/internal/extend/math" facade "f1-game/internal/facade" nameRoute "f1-game/internal/name/route" "f1-game/internal/pb" "f1-game/internal/sessions" "f1-game/internal/types" ao "f1-game/nodes/game/player/asset/origin" mapCall "f1-game/nodes/map/internal/call" "f1-game/nodes/map/internal/db" dbMapPlayer "f1-game/nodes/map/internal/db/data/player" mapFacade "f1-game/nodes/map/internal/facade" "f1-game/nodes/map/player/hero" ctime "github.com/cherry-game/cherry/extend/time" clog "github.com/cherry-game/cherry/logger" cproto "github.com/cherry-game/cherry/net/proto" ) var srv = &service{} func Service() *service { return srv } type service struct { facade.PlayerServiceBase[*db.MapPlayerTable] } func (p *service) OnLogin(playerTable *db.MapPlayerTable, session *cproto.Session) { if session.Contains(sessions.IsNewPlayer) { clog.Infof("map team init: playerID = %d", playerTable.PlayerID) // 初始化队伍 p.initTeams(playerTable) } // 计算城外征兵 TODO 优化成定时结算 var ( nowMillisecond = ctime.Now().ToMillisecond() diffSecond = (nowMillisecond - playerTable.Stats.LastLogoutTime) / ctime.MillisecondsPerSecond ) if p.doTeamOutsideAutoTroops(playerTable, int32(diffSecond)) { playerTable.SetChangeData() } if playerTable.IsChangeData() { playerTable.Save2Queue() } } func (p *service) initTeams(playerTable *db.MapPlayerTable) { // 检测未添加的队伍 teamCount := playerTable.Facilities.Attrs.GetI32(enum.AttrTeamCount) for i := int32(1); i <= teamCount; i++ { if _, success := playerTable.Teams.AddTeam(i); success { playerTable.SetChangeData() } } if playerTable.IsChangeData() { playerTable.Save2Queue() } } // func (p *service) OnLogined(playerTable *db.MapPlayerTable, session *cproto.Session) { // } // func (p *service) OnLogout(playerTable *db.MapPlayerTable) { // } // func (p *service) OnReset(playerTable *db.MapPlayerTable, time *ctime.CherryTime, byLogin bool) { // } // Push 推送队伍信息 func (p *service) Push(playerTable *db.MapPlayerTable, session *cproto.Session) { resp := playerTable.Teams.ToProto() sessions.PushWithSession(session, nameRoute.PushMapPlayer_Team, &resp) // 城外征兵推送 p.TeamOutsideTroopsPush(playerTable.PlayerID) } // ChangePush 推送变化的队伍信息 func (p *service) ChangePush(playerID int64, teams ...*dbMapPlayer.Team) { session, found := sessions.GetSession(playerID) if !found { return } var ( playerTable = db.GetMapPlayerTable(playerID) list []*pb.Team ) // 检查玩家表是否存在 if playerTable == nil { return } for _, team := range teams { proto := team.ToProto() list = append(list, &proto) } if len(list) == 0 { return } resp := &pb.Teams{ List: list, } sessions.PushWithSession(session, nameRoute.PushMapPlayer_TeamChange, resp) } // 英雄状态更新推送 func (p *service) HeroStateChangePush(playerID int64, updateHeroes ...*dbMapPlayer.Hero) { if len(updateHeroes) <= 0 { return } rsp := &pb.HeroStateResponse{} for _, hero := range updateHeroes { rsp.HeroState = append(rsp.HeroState, hero.ToHeroStateProto()) } sessions.PushPlayer(playerID, nameRoute.PushMapPlayer_HeroState, rsp) } func (p *service) CheckEnergy(playerID int64, teamID int32, energy int32) int32 { playerTable := db.GetMapPlayerTable(playerID) if playerTable == nil { return code.PlayerNotFound } team, ok := playerTable.Teams.GetTeam(teamID) if !ok { return code.TeamNotUnlock } for _, member := range team.Members { hero, ok := playerTable.Heroes.Get(member.HeroGUID) if !ok { return code.HeroNotExist } if hero.Energy < energy { return code.HeroEnergyLack } } return code.OK } // SubEnergy 队伍扣除体力 func (p *service) SubEnergy(playerID int64, teamID int32, energy int32) int32 { var ( playerTable = db.GetMapPlayerTable(playerID) updateHeroes []*dbMapPlayer.Hero ) // 检查玩家表是否存在 if playerTable == nil { return code.PlayerNotFound } team, ok := playerTable.Teams.GetTeam(teamID) if !ok { return code.TeamNotUnlock } for _, member := range team.Members { dbHero, ok := playerTable.Heroes.Get(member.HeroGUID) if !ok { return code.HeroNotExist } if !hero.Service().SubEnergy(dbHero, energy) { return code.HeroEnergyLack } updateHeroes = append(updateHeroes, dbHero) } playerTable.Save2Queue() // 推送英雄数据 hero.Service().ChangePush(playerID, updateHeroes...) return code.OK } // 回城自动补兵 return 是否补兵成功 func (p *service) CheckAutoAddTeamTroops(playerID int64, teamID int32) bool { if teamID <= 0 { return false } playerTable := db.GetMapPlayerTable(playerID) if playerTable == nil { clog.Warnf("[CheckAutoAddTeamTroops] player not found, playerID: %d", playerID) return false } team, ok := playerTable.Teams.GetTeam(teamID) if !ok { clog.Warnf("[CheckAutoAddTeamTroops] team not found, teamID: %d", teamID) return false } // 没设置自动补兵 跳过 if !team.AutoAddTroops { return false } var ( // 最终分兵数量 finalMemberTroops = map[int32]*pb.I32I32{} // 英雄带兵上限 key 英雄上阵阵容位置 heroArmsLimitMap = map[int32]int32{} // 通过士兵等级分组 troopsIDGroupMemberMap = team.GetTroopsIDGroupMemberMap() costAssets = types.Assets{} ) for troopsID, members := range troopsIDGroupMemberMap { // 可分配的兵力 hasTroopsNum := int32(mapFacade.Storage.Get(playerID, troopsID)) if hasTroopsNum <= 0 { continue } needCostCount := int32(0) for _, member := range members { // 当前需要补兵数量 = 带兵上限 - 当前剩余兵力 - 伤兵数 needCostCount += member.TroopsLimit - member.TroopsNum - member.InjureTroops heroArmsLimitMap[member.TeamIndex] = member.TroopsLimit } // 不需要补兵.0 if needCostCount <= 0 { continue } if needCostCount <= hasTroopsNum { // 全部补满 for _, member := range members { data := pb.I32I32{ Key: troopsID, Value: heroArmsLimitMap[member.TeamIndex] - member.TroopsNum - member.InjureTroops, } finalMemberTroops[member.TeamIndex] = &data } } else { // 优先持平补兵 needCostCount = p.balanceTroopsDistribution(members, troopsID, heroArmsLimitMap, hasTroopsNum, finalMemberTroops) if needCostCount <= 0 { continue } } costAssets.Add(troopsID, int64(needCostCount)) } if len(costAssets) <= 0 { return false } // 扣除兵力 _, errCode := mapFacade.Storage.AssetSubs(playerID, costAssets, ao.TeamAutoTroopsCost) if code.IsFail(errCode) { clog.Warnf("[AutoAddTeamTroops] sub troop fail, playerID: %d, teamID: %d, errCode: %d", playerID, teamID, errCode) return false } // 更新队伍上阵英雄带兵数量 team.UpdateTeamMemberTroopsNum(finalMemberTroops) // 同步到 redis 队伍信息 p.UpdatePlayerTeams(playerID, team) // 发送分兵事件 capp.PostEvent(event.NewTeamTroops(playerID, teamID)) return true } // 持平补兵分配算法 func (p *service) balanceTroopsDistribution(members []*dbMapPlayer.TeamMember, troopsID int32, heroArmsLimitMap map[int32]int32, hasTroopsNum int32, finalMemberTroops map[int32]*pb.I32I32) int32 { if len(members) == 0 || hasTroopsNum <= 0 { return 0 } // 初始化当前兵力状态 currentTroops := make(map[int32]int32) for _, member := range members { currentTroops[member.TeamIndex] = member.TroopsNum + member.InjureTroops finalMemberTroops[member.TeamIndex] = &pb.I32I32{Key: troopsID, Value: 0} // 初始化分配结果 } var ( intMaxValue = int32(1<<31 - 1) remaining = hasTroopsNum totalAdded = int32(0) ) for remaining > 0 { // 找出所有未达到上限的英雄 var availableHeroes []int32 for _, member := range members { if currentTroops[member.TeamIndex] < heroArmsLimitMap[member.TeamIndex] { availableHeroes = append(availableHeroes, member.TeamIndex) } } if len(availableHeroes) == 0 { break // 所有英雄都已满 } // 找出当前兵力最少的英雄们 minTroops := intMaxValue var minHeroes []int32 for _, teamIndex := range availableHeroes { if currentTroops[teamIndex] < minTroops { minTroops = currentTroops[teamIndex] minHeroes = []int32{teamIndex} } else if currentTroops[teamIndex] == minTroops { minHeroes = append(minHeroes, teamIndex) } } // 找出下一个更高的兵力水平(在可用英雄中) nextLevel := intMaxValue for _, teamIndex := range availableHeroes { if currentTroops[teamIndex] > minTroops && currentTroops[teamIndex] < nextLevel { nextLevel = currentTroops[teamIndex] } } // 如果没有找到更高的水平,使用最小上限值 if nextLevel == intMaxValue { for _, teamIndex := range minHeroes { nextLevel = math.Min(nextLevel, heroArmsLimitMap[teamIndex]) } } // 计算需要分配到下一个水平的总兵力 totalNeeded := int32(0) for _, teamIndex := range minHeroes { needed := math.Min(nextLevel, heroArmsLimitMap[teamIndex]) - currentTroops[teamIndex] if needed > 0 { totalNeeded += needed } } if totalNeeded <= remaining { // 有足够兵力让所有最低英雄达到下一个水平 for _, teamIndex := range minHeroes { addAmount := math.Min(nextLevel, heroArmsLimitMap[teamIndex]) - currentTroops[teamIndex] currentTroops[teamIndex] += addAmount finalMemberTroops[teamIndex].Value += addAmount totalAdded += addAmount remaining -= addAmount } } else { // 兵力不足,需要平均分配 numHeroes := int32(len(minHeroes)) baseAdd := remaining / numHeroes remainder := remaining % numHeroes for i, teamIndex := range minHeroes { addAmount := baseAdd if int32(i) < remainder { addAmount++ } // 确保不超过上限 maxPossibleAdd := heroArmsLimitMap[teamIndex] - currentTroops[teamIndex] if addAmount > maxPossibleAdd { addAmount = maxPossibleAdd } if addAmount > remaining { addAmount = remaining } currentTroops[teamIndex] += addAmount finalMemberTroops[teamIndex].Value += addAmount totalAdded += addAmount remaining -= addAmount if remaining <= 0 { break } } break } } return totalAdded } // 同步队伍信息到 redis func (p *service) UpdatePlayerTeams(playerID int64, teamList ...*dbMapPlayer.Team) { if len(teamList) == 0 { return } var battleTeamList []*pb.BattleTeam for _, team := range teamList { proto := team.ToBattleTeamProto() battleTeamList = append(battleTeamList, &proto) } redis.Game.UpdatePlayerTeams(playerID, battleTeamList...) } // 通过英雄列表获取羁绊列表 func (p *service) GetFetterListByHeroList(playerID int64, heroIDList types.Set[int32]) types.Set[int32] { if len(heroIDList) < 1 { return nil } activeFetterIDs := types.Set[int32]{} for heroID := range heroIDList { fetterIDs := data.HeroFetter.GetFetterIDs(heroID) // 当前英雄没有羁绊,则跳过 if len(fetterIDs) < 1 { continue } for fetterID := range fetterIDs { if activeFetterIDs.Contains(fetterID) { continue } fetterRow, found := data.HeroFetter.GetByID(fetterID) if !found { clog.Warnf("[GetFetterListByHeroList] fetter not found, playerID:%d, fetterID: %d", playerID, fetterID) continue } curNum := int32(0) for _, needHeroID := range fetterRow.HeroIDs { if heroIDList.Contains(needHeroID) { curNum++ // 如果当前所需英雄数量已经满足,则直接添加羁绊ID,并跳过 if curNum >= fetterRow.ActiveHeroNum { activeFetterIDs.Add(fetterID) break } } } } } return activeFetterIDs } // 获取英雄带兵上限 func (p *service) GetHeroTroopsNum(playerID int64, heroID, teamIndex int32) int32 { playerTable := db.GetMapPlayerTable(playerID) if playerTable == nil { clog.Warnf("[GetHeroTroopsNum] player not found, playerID: %d", playerID) return 0 } return p.doGetHeroTroopsNum(playerTable, heroID, teamIndex) } // 获取英雄带兵上限 func (p *service) doGetHeroTroopsNum(playerTable *db.MapPlayerTable, heroID, teamIndex int32) int32 { var heroTroopsLimit int32 // 1.英雄初始统兵数量 heroRow, found := data.Hero.GetByID(heroID) if found { heroTroopsLimit += heroRow.InitTroop } else { clog.Warnf("[GetHeroTroopsNum] hero config not found, heroID: %d", heroID) } // 2.判断是否为主将 主将额外增加带兵数 if constant.IsTeamMainIndex(teamIndex) { heroTroopsLimit += data.Const.TeamMainAddTroops } // 3.添加城建增加的带兵数 heroTroopsLimit += int32(playerTable.Facilities.Attrs.Get(enum.AttrTeamArms)) return heroTroopsLimit } // 部队羁绊加成 func (p *service) TeamFetterAttrs(team *dbMapPlayer.Team, heroes dbMapPlayer.Heroes) { fetterIDs := team.FetterIDs if fetterIDs.Size() < 1 { return } for fetterID := range fetterIDs { for _, member := range team.Members { hero, found := heroes.Get(member.HeroGUID) if !found { continue } attrs := types.NewAttrs() fetterRow, found := data.HeroFetter.GetByID(fetterID) if !found { clog.Warnf("[teamFetterAttrs] fetter not found, heroID:%d, fetterID: %d", hero.HeroID, fetterID) continue } // 当前羁绊不存在该英雄,则跳过 if !fetterRow.HeroIDs.Contains(hero.HeroID) { continue } for attrId, attrValue := range fetterRow.GetAttrs() { attrs.Add(attrId, attrValue) } member.FetterAttrs = attrs } } } // 更新队伍状态 func (p *service) UpdateTeamStateByMarchState(playerTable *db.MapPlayerTable, team *dbMapPlayer.Team, marchState enum.MarchState) { teamState := enum.MarchStateToTeamState(marchState) p.UpdateTeamState(playerTable, team, teamState) } // 更新队伍状态 func (p *service) UpdateTeamState(playerTable *db.MapPlayerTable, team *dbMapPlayer.Team, teamState enum.TeamState) { // 相同状态不处理 if team.State == teamState { return } team.State = teamState // 回城待命 if teamState == enum.TeamState_Standby { // 检测自动补兵 p.CheckAutoAddTeamTroops(playerTable.PlayerID, team.TeamID) } playerTable.Save2Queue() clog.Debugf("[UpdateTeamState] PlayerID = %d, TeamID = %d, TeamState = %s", playerTable.PlayerID, team.TeamID, enum.GetTeamStateName(teamState)) // 推送状态给客户端 p.ChangePush(playerTable.PlayerID, team) } // 获取队伍行军同步数据 func (p *service) GetTeamMarchSyncProto(playerTable *db.MapPlayerTable, dbTeam *dbMapPlayer.Team) *pb.MarchSync { var ( members []*pb.MarchMember injureEndTime int64 ) for _, member := range dbTeam.Members { // 士兵负重 var armWeight int64 if armTypeRow, ok := data.ArmType.GetByID(member.TroopsID); ok { armWeight = armTypeRow.Weight } heroEntity, _ := playerTable.Heroes.Get(member.HeroGUID) // 获取带兵上限 member.TroopsLimit = p.doGetHeroTroopsNum(playerTable, heroEntity.HeroID, member.TeamIndex) members = append(members, &pb.MarchMember{ TeamIndex: member.TeamIndex, LineupIndex: member.Index, HeroGUID: heroEntity.GUID, HeroConfigID: heroEntity.HeroID, Energy: heroEntity.Energy, ArmID: member.TroopTypeID, ArmWeight: armWeight, TroopsMax: member.TroopsLimit, TroopsNum: member.TroopsNum, }) injureEndTime = max(injureEndTime, heroEntity.InjuriesEndTime) } req := &pb.MarchSync{ PlayerID: playerTable.PlayerID, LeagueID: playerTable.LeagueID, TeamID: dbTeam.TeamID, ObjectID: dbTeam.MarchObjectID, Point: playerTable.Point.ToProto(), Members: members, GameNodeID: playerTable.GameNodeID, InjureEndTime: injureEndTime * ctime.MillisecondsPerSecond, } // 出征行军更新玩家队伍兵力上限 // TODO 这个需要拿出去不 p.UpdatePlayerTeams(playerTable.PlayerID, dbTeam) return req } // 检查行军消耗是否足够 func (p *service) CheckMarchCost(playerID int64, teamID int32, marchType enum.MarchType, pathLen int) int32 { row, ok := data.MapMarch.GetByMarchType(marchType) if !ok { return code.ConfigNotFound_MapMarch } // 检查体力是否足够 energyCost := row.GetEnergyCost(pathLen) if row.EnergyCost > 0 { errCode := p.CheckEnergy(playerID, teamID, energyCost) if code.IsFail(errCode) { return errCode } } // 检查扣除资源 if !row.Cost.IsEmpty() { if !mapFacade.Storage.IsAllEnough(playerID, row.Cost) { return code.ItemNotEnough } } return code.OK } // 行军消耗资源 func (p *service) MarchCost(playerID int64, teamID int32, marchType enum.MarchType, pathLen int) int32 { row, ok := data.MapMarch.GetByMarchType(marchType) if !ok { return code.ConfigNotFound_MapMarch } // 扣除资源 if !row.Cost.IsEmpty() { _, errCode := mapFacade.Storage.AssetSubs(playerID, row.Cost, ao.None, true) if code.IsFail(errCode) { return errCode } clog.Debugf("[marchCost] cost: PlayerID: %d, teamID: %d, cost: %v", playerID, teamID, row.Cost) } energyCost := row.GetEnergyCost(pathLen) // 扣体力 if row.EnergyCost > 0 { p.SubEnergy(playerID, teamID, energyCost) clog.Debugf("[marchCost] sub energy: PlayerID: %d, teamID: %d, energy: %d", playerID, teamID, energyCost) } return code.OK } // 执行城外征兵逻辑 func (p *service) DoTeamOutsideTroops(playerID int64, team *dbMapPlayer.Team, troopNums []*pb.I32I32) int32 { if len(troopNums) <= 0 { return code.IllegalArgument } var ( // 检测征兵数量+当前兵力是否超出上限 & 获取征兵消耗 costs = types.Assets{} playerTable = db.GetMapPlayerTable(playerID) ) for _, troopsNum := range troopNums { // 征兵数量错误 if troopsNum.Value <= 0 { return code.TeamTroopsNumError } teamMember, found := team.GetTeamMember(troopsNum.Key) if !found { return code.TeamMemberNotExist } // 判断当前是否已在城外征兵 if teamMember.IsOutsideTroopsing() { return code.TeamOutsideConscripting } // 兵力数量超出上限 if troopsNum.Value+teamMember.TroopsNum > teamMember.TroopsLimit { return code.TeamArmExceed } armLevelRow, found := data.ArmLevel.GetByID(teamMember.TroopsID) if !found { return code.ConfigNotExist_Conscript } singleCosts := armLevelRow.CalOutConscriptAssets(int64(troopsNum.Value)) costs.AddAssets(singleCosts) } // 消耗道具 _, errCode := mapFacade.Storage.AssetSubs(playerID, costs, ao.ConscriptOutsideCost, true) if code.IsFail(errCode) { return errCode } curTime := ctime.Now().ToSecond() for _, troopsNum := range troopNums { teamMember, _ := team.GetTeamMember(troopsNum.Key) teamMember.OutsideConscript.TroopsCount = 0 teamMember.OutsideConscript.TotalTroopsCount = troopsNum.Value // 计算耗时 armLevelRow, _ := data.ArmLevel.GetByID(teamMember.TroopsID) second := troopsNum.Value / armLevelRow.ConscriptNumSecond if troopsNum.Value%armLevelRow.ConscriptNumSecond > 0 { second++ } // 计算结束时间戳 teamMember.OutsideConscript.EndTime = curTime + int64(second) } // 通知地图自动征兵OK mapCall.Logic.AutoOutsideTroopsNotify(playerTable.PlayerID, team.TeamID, team.GetOutsideConscriptMaxTime()) return code.OK } // 定时检测城外征兵 func (p *service) CheckCanOutsideAutoTroopsTimer(playerID int64, nowSecond int64) { // 没到检测时间 if nowSecond%data.Const.MapTroopsCheckInterval != 0 { return } playerTable := db.GetMapPlayerTable(playerID) if playerTable == nil { clog.Warnf("[CheckCanOutsideAutoTroopsTimer] player not found, playerID: %d", playerID) return } save := false for _, team := range playerTable.Teams { if team.IsCanOutsideAutoTroops() { // 执行城外征兵 if p.OutsideAutoTroops(playerTable, team) { save = true } } } if save { playerTable.Save2Queue() } } // 城外自动征兵定时器 //TODO 暂定每秒执行 func (p *service) TeamOutsideAutoTroopsTimer(playerID int64, nowSecond int64) { playerTable := db.GetMapPlayerTable(playerID) if playerTable == nil { clog.Warnf("[TeamOutsideAutoTroopsTimer] player not found, playerID: %d", playerID) return } if save := p.doTeamOutsideAutoTroops(playerTable, 1); save { playerTable.Save2Queue() } } func (p *service) doTeamOutsideAutoTroops(playerTable *db.MapPlayerTable, seconds int32) bool { if seconds <= 0 { return false } updateTeams := []*dbMapPlayer.Team{} for _, team := range playerTable.Teams { update := false for _, member := range team.Members { if member.TroopsID <= 0 { continue } remainCount := member.GetRemainTroopsCount() // 城外征兵已完成 if remainCount <= 0 { continue } armLevelRow, found := data.ArmLevel.GetByID(member.TroopsID) if !found { clog.Warnf("[doTeamOutsideAutoTroops] armLevelRow not found, playerID: %d, troopsID: %d", playerTable.PlayerID, member.TroopsID) continue } getCount := seconds * armLevelRow.ConscriptNumSecond if getCount > remainCount { getCount = remainCount } if member.TroopsNum+getCount > member.TroopsLimit { getCount = member.TroopsLimit - member.TroopsNum } if getCount <= 0 { continue } member.OutsideConscript.TroopsCount += getCount member.TroopsNum += getCount update = true } if update { // 队伍城外征兵已完成 重置征兵数据 if team.IsTeamOutsideTroopsFinish() { for _, member := range team.Members { member.ResetOutsideTroops() } } updateTeams = append(updateTeams, team) } } if len(updateTeams) > 0 { // 推送城外征兵数据 TODO 是否合并到队伍信息中? p.TeamOutsideTroopsChangePush(playerTable.PlayerID, updateTeams...) // 推送队伍兵力变更 p.UpdatePlayerTeams(playerTable.PlayerID, updateTeams...) // 推送队伍兵力 p.ChangePush(playerTable.PlayerID, updateTeams...) return true } return false } // 城外征兵推送 func (p *service) TeamOutsideTroopsPush(playerID int64) { teamPush := &pb.TeamOutsideTroopsList{} playerTable := db.GetMapPlayerTable(playerID) for _, team := range playerTable.Teams { teamPush.List = append(teamPush.List, team.ToOutsideTroopsProto()) } if len(teamPush.List) > 0 { sessions.PushPlayer(playerID, nameRoute.PushMapPlayer_TeamOutsideTroops, teamPush) } } // 城外征兵推送 func (p *service) TeamOutsideTroopsChangePush(playerID int64, teams ...*dbMapPlayer.Team) { if len(teams) <= 0 { return } teamPush := &pb.TeamOutsideTroopsList{} for _, team := range teams { teamPush.List = append(teamPush.List, team.ToOutsideTroopsProto()) } session, found := sessions.GetSession(playerID) if !found { return } if len(teamPush.List) > 0 { sessions.PushWithSession(session, nameRoute.PushMapPlayer_TeamOutsideTroops, teamPush) } } // 取消城外征兵 func (p *service) TeamOutsideTroopsCancel(playerTable *db.MapPlayerTable, team *dbMapPlayer.Team, teamIndexs []int32) int32 { var ( returnCosts = types.Assets{} playerID = playerTable.PlayerID memberCancels = []*dbMapPlayer.TeamMember{} ) for _, teamIndex := range teamIndexs { teamMember, found := team.GetTeamMember(teamIndex) if !found { continue } // 当前没有城外征兵 if teamMember.OutsideConscript.TroopsCount <= 0 { continue } // 存在没征完的士兵 返还奖励 remainCount := teamMember.GetRemainTroopsCount() if remainCount > 0 { armLevelRow, found := data.ArmLevel.GetByID(teamMember.TroopsID) if !found { continue } singleCosts := armLevelRow.CalOutConscriptAssets(int64(remainCount)) returnCosts.AddAssets(singleCosts) } memberCancels = append(memberCancels, teamMember) } // 没有任何返还 说明也没有城外征兵/城外征兵已完成 if len(returnCosts) > 0 { _, errCode := mapFacade.Storage.AssetAdds(playerID, returnCosts, ao.ConscriptOutsideReturn, true) if code.IsFail(errCode) { return errCode } } // 重置需要需要的征兵数据 for _, member := range memberCancels { member.ResetOutsideTroops() } // 通知地图自动征兵OK mapCall.Logic.AutoOutsideTroopsNotify(playerTable.PlayerID, team.TeamID, team.GetOutsideConscriptMaxTime()) return code.OK } func (p *service) OutsideAutoTroops(playerTable *db.MapPlayerTable, team *dbMapPlayer.Team) bool { if !team.IsAutoTroops { return false } // 计算所需征兵数量&各成员所需征兵数量 var ( teamMembers = map[int32]*pb.TeamOutsideTroopsMember{} oneTroopCosts = map[int32]types.Assets{} curHasAssets = map[int32]int64{} ) for _, member := range team.Members { // 过滤没有士兵等级ID的成员 if member.TroopsID <= 0 { continue } armLevelRow, found := data.ArmLevel.GetByID(member.TroopsID) if !found { clog.Warnf("[outsideAutoTroops] armLevel not found. playerID:%d, teamID: %d, troopsID: %d", playerTable.PlayerID, team.TeamID, member.TroopsID) continue } needTroops := member.TroopsLimit - member.TroopsNum if needTroops <= 0 { continue } teamMembers[member.TeamIndex] = &pb.TeamOutsideTroopsMember{ TroopsID: member.TroopsID, TroopsNum: member.TroopsNum, TroopsLimit: member.TroopsLimit, } // 获取征一个士兵的消耗 _, found = oneTroopCosts[member.TroopsID] if !found { costs := armLevelRow.CalOutConscriptAssets(1) oneTroopCosts[member.TroopsID] = costs // 获取当前道具的数量 for _, cost := range costs { _, found := curHasAssets[cost.ID] if !found { hasNum := mapFacade.Storage.Get(playerTable.PlayerID, cost.ID) // TODO 如果数量<=0 是不是当前征兵失败 curHasAssets[cost.ID] = hasNum } } } } // 如果当前没有征兵需求 if len(teamMembers) <= 0 { return false } // 如果征兵数量>0 则获取每个成员最大征兵上限 finalMemberTroops := p.HandleAutoTroops(curHasAssets, oneTroopCosts, teamMembers) if len(finalMemberTroops) <= 0 { return false } // 调用城外征兵核心逻辑 errCode := p.DoTeamOutsideTroops(playerTable.PlayerID, team, finalMemberTroops) if code.IsFail(errCode) { clog.Warnf("[outsideAutoTroops] fail. playerID:%d, teamID: %d, errCode: %d", playerTable.PlayerID, team.TeamID, errCode) return false } // 同步给客户端 p.TeamOutsideTroopsChangePush(playerTable.PlayerID, team) return true } // 规则:优先拉平队伍每个成员兵力(即优先给最小兵力的成员征兵),如果成员当前兵力相同后,则同步分兵 // HandleAutoTroops 优化后的征兵逻辑 // TODO 后面看继续优化当前逻辑 // curHasAssets: 当前库存 // troopCosts: 兵种消耗配置 [兵种ID]消耗材料 // teamMembers: [位置]部队成员信息(作为源数据查询,不直接修改) func (p *service) HandleAutoTroops(curHasAssets map[int32]int64, troopCosts map[int32]types.Assets, teamMembers map[int32]*pb.TeamOutsideTroopsMember) []*pb.I32I32 { // 初始化结果 var ( finalMemberTroops = map[int32]int32{} // [位置]征兵数量 maxInt32 = int32(1<<31 - 1) maxInt64 = int64(1<<63 - 1) ) // 追踪每个位置的当前兵力 curTroops := make(map[int32]int32) for position, hero := range teamMembers { curTroops[position] = hero.TroopsNum } // 持续循环征兵,直到所有需求满足或资源耗尽 for { // 找出当前最少兵力的英雄(且需要征兵) minTroops := maxInt32 for _, troops := range curTroops { if troops < minTroops { minTroops = troops } } if minTroops == maxInt32 { break } // 找出所有兵力等于最少且需要征兵的位置 // (curTroops中只保留未达到上限的位置) var canRecruitPositions []int32 for position, troops := range curTroops { if troops == minTroops { canRecruitPositions = append(canRecruitPositions, position) } } if len(canRecruitPositions) == 0 { break } // 计算本轮所有英雄同步征兵的总材料消耗(每个英雄征1个兵) materialPerRound := map[int32]int64{} for _, position := range canRecruitPositions { troopID := teamMembers[position].TroopsID costs := troopCosts[troopID] for _, asset := range costs { materialPerRound[asset.ID] += asset.Num } } // 计算本轮最多能同步征兵多少次 maxRound := maxInt64 for itemID, cost := range materialPerRound { if cost == 0 { continue } stock := curHasAssets[itemID] if stock < cost { maxRound = 0 break } if rounds := stock / cost; rounds < maxRound { maxRound = rounds } } if maxRound <= 0 { break } // 找出本轮英雄中剩余需求最小的 minNeed := maxInt32 for _, position := range canRecruitPositions { need := teamMembers[position].TroopsLimit - curTroops[position] if need < minNeed { minNeed = need } } // 找出下一个更高兵力的等级(用于拉平计算) // (curTroops中只保留未达到上限的位置) nextLevelTroops := maxInt32 for _, troops := range curTroops { if troops > minTroops && troops < nextLevelTroops { nextLevelTroops = troops } } // 确定本轮征兵数量:取minNeed和(nextLevelTroops-minTroops)的较小值 roundRecruit := int32(maxRound) if maxRound > maxInt64 { roundRecruit = maxInt32 } if roundRecruit > minNeed { roundRecruit = minNeed } // 优化:当没有下一级兵力(所有可征兵位置兵力相同),不限制拉平 // 这样可以一次性征到资源耗尽或征满,而不是每次只征1个 if nextLevelTroops != maxInt32 { maxToLevel := nextLevelTroops - minTroops if roundRecruit > maxToLevel { roundRecruit = maxToLevel } } // 本轮执行征兵 recruited := false for _, position := range canRecruitPositions { need := teamMembers[position].TroopsLimit - curTroops[position] if need <= 0 { continue } troopID := teamMembers[position].TroopsID costs, ok := troopCosts[troopID] if !ok || len(costs) == 0 { continue } // 确定实际征兵量 actualRecruit := roundRecruit if need < actualRecruit { actualRecruit = need } // 更新征兵数据 finalMemberTroops[position] += actualRecruit for _, asset := range costs { costAmount := int64(actualRecruit) * asset.Num curHasAssets[asset.ID] -= costAmount } // 更新当前兵力 curTroops[position] += actualRecruit // 如果达到上限,从curTroops中删除该位置 if teamMembers[position].TroopsLimit-curTroops[position] <= 0 { delete(curTroops, position) } if actualRecruit > 0 { recruited = true } } // 本轮无征兵时退出循环 if !recruited { break } } result := []*pb.I32I32{} for position, value := range finalMemberTroops { result = append(result, &pb.I32I32{ Key: position, Value: value, }) } return result } // 行军至(回城\调度点\营帐)恢复伤兵,战败重伤的在重伤的逻辑恢复 func (p *service) RecoveryInjureTroops(playerTable *db.MapPlayerTable, team *dbMapPlayer.Team) { var marchTroops []*pb.MarchTroops for _, member := range team.Members { if member.InjureTroops <= 0 { continue } member.TroopsNum += member.InjureTroops marchTroops = append(marchTroops, &pb.MarchTroops{ TeamIndex: member.TeamIndex, TroopsNum: member.InjureTroops, }) // 重置伤兵数 member.InjureTroops = 0 } // 没有需要恢复的伤兵 if len(marchTroops) <= 0 { return } playerTable.Save2Queue() // 更新地图队伍兵力数据 mapCall.Logic.MarchSyncTroops(team.TeamID, team.MarchObjectID, marchTroops) // 同步队伍变更信息 p.ChangePush(playerTable.PlayerID, team) // 兵力同步到redis p.UpdatePlayerTeams(playerTable.PlayerID, team) } // 刷新队伍阵容位置 func (p *service) RefreshTeamLineup(playerTable *db.MapPlayerTable, team *dbMapPlayer.Team) { if team == nil { return } // 1.没有主将 或 主将为步兵/骑兵,主将上阵2(下标1)号位;副将1和副将2分别上阵4(下标3)号位和6(下标5)号位 当前为默认值 下标从0开始 teamIndex1, teamIndex2, teamIndex3 := int32(1), int32(3), int32(5) teamMember, found := team.GetTeamMember(constant.TeamMemberTeamIndex_Start) if found { // 获取主将兵种 hero, found := playerTable.Heroes.Get(teamMember.HeroGUID) if found { heroRow, found := data.Hero.GetByID(hero.HeroID) if found { //2.主将为弓兵,主将上阵5(下标4)号位;副将1和副将2分别上阵1(下标0)号位和3(下标2)号位 下标从0开始 if heroRow.Job == int32(enum.Job_Archer) { teamIndex1, teamIndex2, teamIndex3 = 4, 0, 2 } } else { clog.Warnf("[RefreshTeamLineup]hero config not found, playerID:%d, heroID: %d", playerTable.PlayerID, hero.HeroID) } } else { clog.Warnf("[RefreshTeamLineup] hero not found, playerID:%d, heroGUID: %d", playerTable.PlayerID, teamMember.HeroGUID) } teamMember.Index = teamIndex1 } teamMember2, found := team.GetTeamMember(constant.TeamMemberIndex_2) if found { teamMember2.Index = teamIndex2 } teamMember3, found := team.GetTeamMember(constant.TeamMemberIndex_3) if found { teamMember3.Index = teamIndex3 } } // 检测同队伍是否存在相同英雄 func (p *service) CheckTeamSameHero(playerTable *db.MapPlayerTable, heroID, srcTeamID, srcTeamIndex, excludeTeamID, excludeIndex int32) bool { for _, team := range playerTable.Teams { for _, member := range team.Members { // 排除原来的位置 if srcTeamID == team.TeamID && srcTeamIndex == member.TeamIndex { continue } // 排除现在上阵的队伍位置 if team.TeamID == excludeTeamID && member.TeamIndex == excludeIndex { continue } hero, ok := playerTable.Heroes.Get(member.HeroGUID) if !ok { // 没找到返回true 防止上阵相同英雄 return true } if hero.HeroID == heroID { return true } } } return false } // 英雄下阵处理 func (p *service) DoTeamLeave(playerTable *db.MapPlayerTable, team *dbMapPlayer.Team, teamIndex int32) (*dbMapPlayer.Hero, int32) { teamMember, ok := team.GetTeamMember(teamIndex) // 下阵位置不存在英雄 if !ok || teamMember.HeroGUID == 0 { return nil, code.OK } heroEntity, ok := playerTable.Heroes.Get(teamMember.HeroGUID) if !ok { return nil, code.HeroNotExist } team.DelTeamMember(teamIndex) heroEntity.TeamID = 0 if teamMember.TroopsNum > 0 { num := int64(teamMember.TroopsNum) // 返还兵力 直接返还兵力 mapFacade.Storage.AssetAddOne(playerTable.PlayerID, teamMember.TroopsID, num, ao.TeamLeaveTroopsReturn) push := &pb.AssetList{} push.List = append(push.List, &pb.Asset{ Id: teamMember.TroopsID, Num: num, }) // 推送返还兵力 sessions.PushPlayer(playerTable.PlayerID, nameRoute.PushMapPlayer_TeamTroopReturn, push) } return heroEntity, code.OK } // 检测是否可以城外征兵 func (p *service) CheckCanOusideConscript(playerID int64, configID int32) bool { mapBuildRow, found := data.MapBuild.GetByConfigID(configID) if found { return mapBuildRow.CanOutsideConscript } // 地图建筑表没找到 找地图-城池表 mapCastleRow, found := data.MapCastle.GetByConfigID(configID) if !found { clog.Warnf("[CheckCanOusideConscript] map castle not found. playerID:%d, configID:%d", playerID, configID) return false } return mapCastleRow.CanOutsideConscript } // 是否兵力不足 func (p *service) IsTeamTroopsLack(playerTable *db.MapPlayerTable, team *dbMapPlayer.Team) bool { var ( total int32 current = team.GetTeamTroopCount() ) for _, member := range team.Members { total += member.TroopsLimit } ratio := float32(current) / float32(total) * 10000.0 return ratio <= data.Const.TeamTroopsLackRatio } // 刷新队伍的带兵上限 func (p *service) RefreshTeamTroopsLimit(playerTable *db.MapPlayerTable, teams ...*dbMapPlayer.Team) []*dbMapPlayer.Team { if len(teams) <= 0 { return nil } updateTeams := []*dbMapPlayer.Team{} for _, curTeam := range teams { if len(curTeam.Members) <= 0 { continue } var update bool for _, member := range curTeam.Members { if member.HeroGUID <= 0 { continue } heroEntity, found := playerTable.Heroes.Get(member.HeroGUID) if !found { clog.Warnf("[RefreshTeamTroopsLimit] fail, hero not found. playerID:%d, heroGUID:%d", playerTable.PlayerID, member.HeroGUID) continue } troopNums := p.GetHeroTroopsNum(playerTable.PlayerID, heroEntity.HeroID, member.TeamIndex) if member.TroopsLimit != troopNums { member.TroopsLimit = troopNums update = true } } if update { updateTeams = append(updateTeams, curTeam) } } // 更新队伍列表 return updateTeams } // 获取队伍兵力万分比 func (p *service) GetTeamTroopRatio(playerTable *db.MapPlayerTable, team *dbMapPlayer.Team) int32 { var ( total int32 count = team.GetTeamTroopCount() ) for _, member := range team.Members { heroEntity, found := playerTable.Heroes.Get(member.HeroGUID) if !found { clog.Warnf("[GetTeamTroopRatio] fail, hero not found. playerID:%d, heroGUID:%d", playerTable.PlayerID, member.HeroGUID) return 0 } total += p.GetHeroTroopsNum(playerTable.PlayerID, heroEntity.HeroID, member.TeamIndex) } return math.Round[float32, int32](float32(count) / float32(total) * constant.FRatioBase) }