Files
bl/logic/service/fight/new.go
昔念 487ee0e726
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): 重构战斗系统代码结构

- 分离新旧组队协议的战斗创建逻辑
- 优化战斗输入验证和处理流程
- 改进战斗循环中的错误处理机制
```
2026-04-09 02:14:09 +08:00

446 lines
13 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
}