Files
bl/logic/service/fight/new.go
昔念 e161e3626f
All checks were successful
ci/woodpecker/push/my-first-workflow Pipeline was successful
```
fix(fight): 修复单输入战斗中效果处理逻辑错误

- 在Effect201的OnSkill方法中调整了多输入战斗检查的位置,
  确保单输入战斗中的单目标效果被正确忽略

- 添加了针对单输入战斗中单目标效果的测试用例

- 移除了重复的多输入战斗检查代码

feat(fight): 添加战斗初始化时捕获标识
2026-04-13 09:59: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()
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.ReadyInfo.OurInfo, f.ReadyInfo.OurPetList = initfightready(f.primaryOur())
f.ReadyInfo.OpponentInfo, f.ReadyInfo.OpponentPetList = initfightready(f.primaryOpp())
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, buildFightOverPayload(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
}