All checks were successful
ci/woodpecker/push/my-first-workflow Pipeline was successful
feat(fight): 添加旧组队协议支持并优化战斗系统 - 实现了旧组队协议相关功能,包括GroupReadyFightFinish、GroupUseSkill、 GroupUseItem、GroupChangePet和GroupEscape方法 - 新增组队战斗相关的入站信息结构体定义 - 实现了组队BOSS战斗逻辑,添加groupBossSlotLimit常量 - 重构宠物技能设置逻辑,调整金币消耗时机 - 优化战斗循环逻辑,添加对无行动槽位的处理 - 改进AI行动逻辑,增加多位置目标选择机制 - 完善捕获系统上下文处理,修复空指针问题 - 添加战斗状态更新和数据同步机制 fix(pet-skill): 修复宠物技能设置中的金币扣除逻辑错误 - 将金币扣除逻辑移到验证之后 - 修正宠物技能数量限制检查的顺序 - 防止重复添加已有技能的情况 refactor(fight): 重构战斗系统代码结构 - 分离新旧组队协议的战斗创建逻辑 - 优化战斗输入验证和处理流程 - 改进战斗循环中的错误处理机制 ```
446 lines
13 KiB
Go
446 lines
13 KiB
Go
package fight
|
||
|
||
import (
|
||
"blazing/common/socket/errorcode"
|
||
"blazing/cool"
|
||
"blazing/logic/service/common"
|
||
"blazing/logic/service/fight/action"
|
||
"blazing/logic/service/fight/info"
|
||
"blazing/logic/service/fight/input"
|
||
"blazing/logic/service/player"
|
||
"blazing/modules/player/model"
|
||
"time"
|
||
)
|
||
|
||
// NewFightSingleControllerN 创建 N 打战斗(单人控制多站位)。
|
||
// ourPetsBySlot/oppPetsBySlot 的每个元素代表一个站位携带的宠物列表。
|
||
func NewFightSingleControllerN(
|
||
ourController common.PlayerI,
|
||
oppController common.PlayerI,
|
||
ourPetsBySlot [][]model.PetInfo,
|
||
oppPetsBySlot [][]model.PetInfo,
|
||
fn func(model.FightOverInfo),
|
||
) (*FightC, errorcode.ErrorCode) {
|
||
if ourController == nil || oppController == nil {
|
||
return nil, errorcode.ErrorCodes.ErrSystemBusyTryLater
|
||
}
|
||
fightInfo := ourController.Getfightinfo()
|
||
|
||
ourInputs, err := buildSideInputsByController(ourController, ourPetsBySlot, fightInfo.Mode)
|
||
if err > 0 {
|
||
return nil, err
|
||
}
|
||
oppInputs, err := buildSideInputsByController(oppController, oppPetsBySlot, fightInfo.Mode)
|
||
if err > 0 {
|
||
return nil, err
|
||
}
|
||
|
||
return NewFightWithOptions(
|
||
WithFightInputs(ourInputs, oppInputs),
|
||
WithFightPlayersOnSide(
|
||
[]common.PlayerI{ourController},
|
||
[]common.PlayerI{oppController},
|
||
),
|
||
WithInputControllerBinding(InputControllerBindingSingle),
|
||
WithFightCallback(fn),
|
||
WithFightInfo(fightInfo),
|
||
)
|
||
}
|
||
|
||
// ArrangePetsBySlotLimit 按站位上限切分宠物。
|
||
// 规则:
|
||
// 1. 前 slotLimit 只存活宠物优先占据出战位。
|
||
// 2. 其余宠物按 1..slotLimit 轮转挂到对应站位作为后备。
|
||
// 3. 每个站位最多保留 6 只宠物。
|
||
func ArrangePetsBySlotLimit(pets []model.PetInfo, slotLimit int) [][]model.PetInfo {
|
||
var (
|
||
alivePets []model.PetInfo
|
||
slots [][]model.PetInfo
|
||
idx int
|
||
)
|
||
for _, pet := range pets {
|
||
if pet.Hp == 0 {
|
||
continue
|
||
}
|
||
alivePets = append(alivePets, pet)
|
||
}
|
||
if len(alivePets) == 0 {
|
||
return nil
|
||
}
|
||
if slotLimit <= 0 {
|
||
slotLimit = 1
|
||
}
|
||
if slotLimit > len(alivePets) {
|
||
slotLimit = len(alivePets)
|
||
}
|
||
slots = make([][]model.PetInfo, 0, slotLimit)
|
||
for i := 0; i < slotLimit; i++ {
|
||
slots = append(slots, []model.PetInfo{alivePets[i]})
|
||
}
|
||
for _, pet := range alivePets[slotLimit:] {
|
||
for step := 0; step < len(slots); step++ {
|
||
slotIdx := (idx + step) % len(slots)
|
||
if len(slots[slotIdx]) >= 6 {
|
||
continue
|
||
}
|
||
slots[slotIdx] = append(slots[slotIdx], pet)
|
||
idx = (slotIdx + 1) % len(slots)
|
||
break
|
||
}
|
||
}
|
||
return slots
|
||
}
|
||
|
||
// ExpandPlayersWithSlotLimit 将“每位玩家的宠物列表”按站位限制展开为扁平站位列表。
|
||
// 例如:
|
||
// 1. slotLimit=1: 每位玩家占 1 个站位,其余为该站位后备。
|
||
// 2. slotLimit=3: 每位玩家最多展开 3 个站位,每个站位带各自后备。
|
||
func ExpandPlayersWithSlotLimit(
|
||
players []common.PlayerI,
|
||
petsByPlayer [][]model.PetInfo,
|
||
slotLimit int,
|
||
) ([]common.PlayerI, [][]model.PetInfo, errorcode.ErrorCode) {
|
||
var (
|
||
flatPlayers []common.PlayerI
|
||
flatSlots [][]model.PetInfo
|
||
)
|
||
if len(players) == 0 || len(players) != len(petsByPlayer) {
|
||
return nil, nil, errorcode.ErrorCodes.ErrSystemBusyTryLater
|
||
}
|
||
for idx, p := range players {
|
||
if p == nil {
|
||
return nil, nil, errorcode.ErrorCodes.ErrSystemBusyTryLater
|
||
}
|
||
slots := ArrangePetsBySlotLimit(petsByPlayer[idx], slotLimit)
|
||
for _, slotPets := range slots {
|
||
flatPlayers = append(flatPlayers, p)
|
||
flatSlots = append(flatSlots, slotPets)
|
||
}
|
||
}
|
||
if len(flatPlayers) == 0 || len(flatPlayers) != len(flatSlots) {
|
||
return nil, nil, errorcode.ErrorCodes.ErrSystemBusyTryLater
|
||
}
|
||
return flatPlayers, flatSlots, 0
|
||
}
|
||
|
||
// NewFightSingleController 使用站位限制规则创建单人控制多站位战斗。
|
||
func NewFightSingleController(
|
||
ourController common.PlayerI,
|
||
oppController common.PlayerI,
|
||
ourPets []model.PetInfo,
|
||
oppPets []model.PetInfo,
|
||
slotLimit int,
|
||
fn func(model.FightOverInfo),
|
||
) (*FightC, errorcode.ErrorCode) {
|
||
return NewFightSingleControllerN(
|
||
ourController,
|
||
oppController,
|
||
ArrangePetsBySlotLimit(ourPets, slotLimit),
|
||
ArrangePetsBySlotLimit(oppPets, slotLimit),
|
||
fn,
|
||
)
|
||
}
|
||
|
||
// NewLegacyGroupFightSingleControllerN 创建旧组队协议的单人控制多站位战斗。
|
||
func NewLegacyGroupFightSingleControllerN(
|
||
ourController common.PlayerI,
|
||
oppController common.PlayerI,
|
||
ourPetsBySlot [][]model.PetInfo,
|
||
oppPetsBySlot [][]model.PetInfo,
|
||
fn func(model.FightOverInfo),
|
||
) (*FightC, errorcode.ErrorCode) {
|
||
if ourController == nil || oppController == nil {
|
||
return nil, errorcode.ErrorCodes.ErrSystemBusyTryLater
|
||
}
|
||
fightInfo := ourController.Getfightinfo()
|
||
|
||
ourInputs, err := buildSideInputsByController(ourController, ourPetsBySlot, fightInfo.Mode)
|
||
if err > 0 {
|
||
return nil, err
|
||
}
|
||
oppInputs, err := buildSideInputsByController(oppController, oppPetsBySlot, fightInfo.Mode)
|
||
if err > 0 {
|
||
return nil, err
|
||
}
|
||
|
||
return NewFightWithOptions(
|
||
WithFightInputs(ourInputs, oppInputs),
|
||
WithFightPlayersOnSide(
|
||
[]common.PlayerI{ourController},
|
||
[]common.PlayerI{oppController},
|
||
),
|
||
WithInputControllerBinding(InputControllerBindingSingle),
|
||
WithLegacyGroupProtocol(true),
|
||
WithFightCallback(fn),
|
||
WithFightInfo(fightInfo),
|
||
)
|
||
}
|
||
|
||
// NewLegacyGroupFightSingleController 使用站位限制规则创建旧组队协议战斗。
|
||
func NewLegacyGroupFightSingleController(
|
||
ourController common.PlayerI,
|
||
oppController common.PlayerI,
|
||
ourPets []model.PetInfo,
|
||
oppPets []model.PetInfo,
|
||
slotLimit int,
|
||
fn func(model.FightOverInfo),
|
||
) (*FightC, errorcode.ErrorCode) {
|
||
return NewLegacyGroupFightSingleControllerN(
|
||
ourController,
|
||
oppController,
|
||
ArrangePetsBySlotLimit(ourPets, slotLimit),
|
||
ArrangePetsBySlotLimit(oppPets, slotLimit),
|
||
fn,
|
||
)
|
||
}
|
||
|
||
// NewFightPerSlotControllerN 创建 N 打战斗(多人各控制一个站位)。
|
||
// ourPlayers/oppPlayers 与 ourPetsBySlot/oppPetsBySlot 按站位一一对应。
|
||
func NewFightPerSlotControllerN(
|
||
ourPlayers []common.PlayerI,
|
||
oppPlayers []common.PlayerI,
|
||
ourPetsBySlot [][]model.PetInfo,
|
||
oppPetsBySlot [][]model.PetInfo,
|
||
fn func(model.FightOverInfo),
|
||
) (*FightC, errorcode.ErrorCode) {
|
||
if len(ourPlayers) == 0 || len(oppPlayers) == 0 {
|
||
return nil, errorcode.ErrorCodes.ErrSystemBusyTryLater
|
||
}
|
||
if len(ourPlayers) != len(ourPetsBySlot) || len(oppPlayers) != len(oppPetsBySlot) {
|
||
return nil, errorcode.ErrorCodes.ErrSystemBusyTryLater
|
||
}
|
||
|
||
fightInfo := ourPlayers[0].Getfightinfo()
|
||
ourInputs, err := buildSideInputsByPlayers(ourPlayers, ourPetsBySlot, fightInfo.Mode)
|
||
if err > 0 {
|
||
return nil, err
|
||
}
|
||
oppInputs, err := buildSideInputsByPlayers(oppPlayers, oppPetsBySlot, fightInfo.Mode)
|
||
if err > 0 {
|
||
return nil, err
|
||
}
|
||
|
||
return NewFightWithOptions(
|
||
WithFightInputs(ourInputs, oppInputs),
|
||
WithFightPlayersOnSide(ourPlayers, oppPlayers),
|
||
WithInputControllerBinding(InputControllerBindingPerSlot),
|
||
WithFightCallback(fn),
|
||
WithFightInfo(fightInfo),
|
||
)
|
||
}
|
||
|
||
// NewFightPerPlayerControllers 使用“每位玩家 + 站位限制”创建多人战斗。
|
||
func NewFightPerPlayerControllers(
|
||
ourPlayers []common.PlayerI,
|
||
oppPlayers []common.PlayerI,
|
||
ourPetsByPlayer [][]model.PetInfo,
|
||
oppPetsByPlayer [][]model.PetInfo,
|
||
slotLimit int,
|
||
fn func(model.FightOverInfo),
|
||
) (*FightC, errorcode.ErrorCode) {
|
||
flatOurPlayers, flatOurSlots, err := ExpandPlayersWithSlotLimit(ourPlayers, ourPetsByPlayer, slotLimit)
|
||
if err > 0 {
|
||
return nil, err
|
||
}
|
||
flatOppPlayers, flatOppSlots, err := ExpandPlayersWithSlotLimit(oppPlayers, oppPetsByPlayer, slotLimit)
|
||
if err > 0 {
|
||
return nil, err
|
||
}
|
||
return NewFightPerSlotControllerN(flatOurPlayers, flatOppPlayers, flatOurSlots, flatOppSlots, fn)
|
||
}
|
||
|
||
// 创建新战斗,邀请方和被邀请方,或者玩家和野怪方
|
||
func NewFight(p1, p2 common.PlayerI, b1, b2 []model.PetInfo, fn func(model.FightOverInfo)) (*FightC, errorcode.ErrorCode) {
|
||
if p1 == nil || p2 == nil {
|
||
return nil, errorcode.ErrorCodes.ErrSystemBusyTryLater
|
||
}
|
||
return NewFightSingleController(p1, p2, b1, b2, 1, fn)
|
||
}
|
||
|
||
// buildFight 基于已准备好的双方 Inputs 构建战斗实例。
|
||
// 约束:opts.ourInputs/opts.oppInputs 必须非空。
|
||
func buildFight(opts *fightBuildOptions) (*FightC, errorcode.ErrorCode) {
|
||
if opts == nil {
|
||
return nil, errorcode.ErrorCodes.ErrSystemBusyTryLater
|
||
}
|
||
opts.normalizePlayers()
|
||
if opts.owner == nil || opts.opponent == nil {
|
||
return nil, errorcode.ErrorCodes.ErrSystemBusyTryLater
|
||
}
|
||
|
||
f := &FightC{}
|
||
f.ownerID = opts.owner.GetInfo().UserID
|
||
f.LegacyGroupProtocol = opts.legacyGroupProtocol
|
||
f.OurPlayers = opts.ourPlayers
|
||
f.OppPlayers = opts.oppPlayers
|
||
f.Switch = make(map[actionSlotKey]*action.ActiveSwitchAction)
|
||
f.callback = opts.callback
|
||
f.quit = make(chan struct{})
|
||
f.over = make(chan struct{})
|
||
f.actionNotify = make(chan struct{}, 1)
|
||
f.pendingActions = make([]action.BattleActionI, 0, 4)
|
||
f.StartTime = opts.startTime
|
||
if opts.fightInfo != nil {
|
||
f.Info = *opts.fightInfo
|
||
} else {
|
||
f.Info = opts.owner.Getfightinfo()
|
||
}
|
||
f.ReadyInfo.Status = f.Info.Status
|
||
|
||
if len(opts.ourInputs) == 0 || len(opts.oppInputs) == 0 {
|
||
return nil, errorcode.ErrorCodes.ErrSystemBusyTryLater
|
||
}
|
||
f.Our = opts.ourInputs
|
||
f.Opp = opts.oppInputs
|
||
f.bindInputControllers(f.Our, f.OurPlayers, opts.controllerBinding)
|
||
f.bindInputControllers(f.Opp, f.OppPlayers, opts.controllerBinding)
|
||
f.bindInputFightContext(f.Our, f.Opp)
|
||
f.linkTeamViews()
|
||
|
||
f.ReadyInfo.OurInfo, f.ReadyInfo.OurPetList = initfightready(f.primaryOur())
|
||
f.ReadyInfo.OpponentInfo, f.ReadyInfo.OpponentPetList = initfightready(f.primaryOpp())
|
||
|
||
loadtime := 120 * time.Second
|
||
if f.Info.Status == info.BattleMode.FIGHT_WITH_NPC {
|
||
if opp := f.primaryOpp(); opp != nil {
|
||
opp.Finished = true
|
||
loadtime = 60 * time.Second
|
||
if ai, ok := opp.Player.(*player.AI_player); ok {
|
||
if ai.CanCapture > 0 {
|
||
opp.CanCapture = ai.CanCapture
|
||
}
|
||
ai.ApplyBattleProps(opp.AttackValue)
|
||
}
|
||
}
|
||
}
|
||
f.FightStartOutboundInfo = f.buildFightStartInfo()
|
||
|
||
f.BroadcastPlayers(func(p common.PlayerI) {
|
||
if f.LegacyGroupProtocol {
|
||
f.sendLegacyGroupReady(p)
|
||
return
|
||
}
|
||
f.sendFightPacket(p, fightPacketReady, &f.ReadyInfo)
|
||
})
|
||
|
||
cool.Cron.AfterFunc(loadtime, func() {
|
||
our := f.primaryOur()
|
||
opp := f.primaryOpp()
|
||
if our == nil || opp == nil {
|
||
return
|
||
}
|
||
if !our.Finished || !opp.Finished {
|
||
f.closefight = true
|
||
f.Reason = model.BattleOverReason.PlayerOffline
|
||
switch {
|
||
case !opp.Finished:
|
||
f.WinnerId = our.Player.GetInfo().UserID
|
||
case !our.Finished:
|
||
f.WinnerId = opp.Player.GetInfo().UserID
|
||
}
|
||
f.BroadcastPlayers(func(p common.PlayerI) {
|
||
if f.LegacyGroupProtocol {
|
||
f.sendLegacyGroupOver(p, &f.FightOverInfo)
|
||
} else {
|
||
f.sendFightPacket(p, fightPacketOver, &f.FightOverInfo)
|
||
}
|
||
p.QuitFight()
|
||
})
|
||
}
|
||
})
|
||
|
||
return f, 0
|
||
}
|
||
|
||
// bindInputControllers 按配置模式重绑站位控制者(Input.Player)。
|
||
// Keep: 不改;Single: 全部绑定 players[0];PerSlot: 按下标绑定 players[i]。
|
||
func (f *FightC) bindInputControllers(inputs []*input.Input, players []common.PlayerI, mode int) {
|
||
if len(inputs) == 0 || len(players) == 0 {
|
||
return
|
||
}
|
||
switch mode {
|
||
case InputControllerBindingSingle:
|
||
controller := players[0]
|
||
for _, in := range inputs {
|
||
if in == nil {
|
||
continue
|
||
}
|
||
in.Player = controller
|
||
}
|
||
case InputControllerBindingPerSlot:
|
||
for idx, in := range inputs {
|
||
if in == nil {
|
||
continue
|
||
}
|
||
if idx < len(players) && players[idx] != nil {
|
||
in.Player = players[idx]
|
||
continue
|
||
}
|
||
in.Player = players[0]
|
||
}
|
||
default:
|
||
// keep existing input player binding
|
||
}
|
||
}
|
||
|
||
// buildInputFromPets 根据玩家与宠物列表构建一个站位 Input。
|
||
func buildInputFromPets(c common.PlayerI, pets []model.PetInfo, mode uint32) (*input.Input, errorcode.ErrorCode) {
|
||
if c == nil {
|
||
return nil, errorcode.ErrorCodes.ErrSystemBusyTryLater
|
||
}
|
||
if r := c.CanFight(); r != 0 {
|
||
return nil, r
|
||
}
|
||
|
||
in := input.NewInput(nil, c)
|
||
in.AllPet = make([]*info.BattlePetEntity, 0, len(pets))
|
||
in.InitAttackValue()
|
||
for _, pet := range pets {
|
||
entity := info.CreateBattlePetEntity(pet)
|
||
entity.BindController(c.GetInfo().UserID)
|
||
in.AllPet = append(in.AllPet, entity)
|
||
}
|
||
|
||
in.SortPet()
|
||
if len(in.AllPet) == 0 {
|
||
return nil, errorcode.ErrorCodes.ErrNoEligiblePokemon
|
||
}
|
||
if mode == info.BattleMode.SINGLE_MODE {
|
||
in.AllPet = in.AllPet[:1]
|
||
}
|
||
in.SetCurPetAt(0, in.AllPet[0])
|
||
return in, 0
|
||
}
|
||
|
||
// buildSideInputsByController 用同一控制者构建多个站位输入(单人多站位)。
|
||
func buildSideInputsByController(controller common.PlayerI, petsBySlot [][]model.PetInfo, mode uint32) ([]*input.Input, errorcode.ErrorCode) {
|
||
if controller == nil || len(petsBySlot) == 0 {
|
||
return nil, errorcode.ErrorCodes.ErrSystemBusyTryLater
|
||
}
|
||
inputs := make([]*input.Input, 0, len(petsBySlot))
|
||
for _, slotPets := range petsBySlot {
|
||
in, err := buildInputFromPets(controller, slotPets, mode)
|
||
if err > 0 {
|
||
return nil, err
|
||
}
|
||
inputs = append(inputs, in)
|
||
}
|
||
return inputs, 0
|
||
}
|
||
|
||
// buildSideInputsByPlayers 按站位玩家一一对应构建输入(多人分站位)。
|
||
func buildSideInputsByPlayers(players []common.PlayerI, petsBySlot [][]model.PetInfo, mode uint32) ([]*input.Input, errorcode.ErrorCode) {
|
||
if len(players) == 0 || len(players) != len(petsBySlot) {
|
||
return nil, errorcode.ErrorCodes.ErrSystemBusyTryLater
|
||
}
|
||
inputs := make([]*input.Input, 0, len(players))
|
||
for idx := range players {
|
||
in, err := buildInputFromPets(players[idx], petsBySlot[idx], mode)
|
||
if err > 0 {
|
||
return nil, err
|
||
}
|
||
inputs = append(inputs, in)
|
||
}
|
||
return inputs, 0
|
||
}
|