Files
bl/logic/service/fight/input.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

566 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/common/utils"
"blazing/modules/player/model"
"blazing/logic/service/common"
"blazing/logic/service/fight/action"
"blazing/logic/service/fight/info"
"blazing/logic/service/fight/input"
"blazing/logic/service/user"
"sync"
"sync/atomic"
"time"
"github.com/gogf/gf/v2/util/grand"
"github.com/jinzhu/copier"
)
type FightC struct {
//准备战斗信息
ReadyInfo model.NoteReadyToFightInfo
//开始战斗信息
info.FightStartOutboundInfo
Info info.Fightinfo
IsReady bool
LegacyGroupProtocol bool
ownerID uint32 // 战斗发起者ID
Our []*input.Input // 我方战斗位
Opp []*input.Input // 敌方战斗位
OurPlayers []common.PlayerI // 我方操作者
OppPlayers []common.PlayerI // 敌方操作者
Switch map[actionSlotKey]*action.ActiveSwitchAction
startl sync.Once
StartTime time.Time
actionMu sync.Mutex
actionNotify chan struct{}
acceptActions bool
pendingActions []action.BattleActionI // 待处理动作队列,同一战斗位最多保留一个动作
pendingHead int
actionRound atomic.Uint32
quit chan struct{}
over chan struct{}
First *input.Input
TrueFirst *input.Input
Second *input.Input
closefight bool
overl sync.Once
waittime int
model.FightOverInfo
//战斗结束的插装
callback func(model.FightOverInfo)
}
type actionSlotKey struct {
PlayerID uint32
ActorIndex int
}
func newActionSlotKey(playerID uint32, actorIndex int) actionSlotKey {
return actionSlotKey{
PlayerID: playerID,
ActorIndex: actorIndex,
}
}
func actionSlotKeyFromAction(act action.BattleActionI) actionSlotKey {
if act == nil {
return actionSlotKey{}
}
return newActionSlotKey(act.GetPlayerID(), act.GetActorIndex())
}
func (f *FightC) primaryOur() *input.Input {
if len(f.Our) == 0 {
return nil
}
return f.Our[0]
}
func (f *FightC) primaryOpp() *input.Input {
if len(f.Opp) == 0 {
return nil
}
return f.Opp[0]
}
func (f *FightC) primaryOurPlayer() common.PlayerI {
if len(f.OurPlayers) == 0 {
return nil
}
return f.OurPlayers[0]
}
func (f *FightC) primaryOppPlayer() common.PlayerI {
if len(f.OppPlayers) == 0 {
return nil
}
return f.OppPlayers[0]
}
func (f *FightC) selectInput(inputs []*input.Input, index int) *input.Input {
if len(inputs) == 0 {
return nil
}
if index >= 0 && index < len(inputs) && inputs[index] != nil {
return inputs[index]
}
for _, in := range inputs {
if in != nil {
return in
}
}
return nil
}
func (f *FightC) isPlayerInSide(players []common.PlayerI, userID uint32) bool {
for _, player := range players {
if player != nil && player.GetInfo().UserID == userID {
return true
}
}
return false
}
func (f *FightC) isOurPlayerID(userID uint32) bool {
if f.isPlayerInSide(f.OurPlayers, userID) {
return true
}
if f.isPlayerInSide(f.OppPlayers, userID) {
return false
}
return userID == f.ownerID
}
// bindInputFightContext 为输入站位绑定战斗上下文与玩家战斗容器。
// 支持一次传入多组输入(如 Our/Opp
func (f *FightC) bindInputFightContext(inputGroups ...[]*input.Input) {
for _, inputs := range inputGroups {
for _, fighter := range inputs {
if fighter == nil {
continue
}
fighter.FightC = f
if fighter.Player != nil {
fighter.Player.SetFightC(f)
}
}
}
}
// linkTeamViews 建立每个输入的同阵营/对阵营视图Team/OppTeam
func (f *FightC) linkTeamViews() {
for _, fighter := range f.Our {
if fighter == nil {
continue
}
fighter.Team = f.Our
fighter.OppTeam = f.Opp
}
for _, fighter := range f.Opp {
if fighter == nil {
continue
}
fighter.Team = f.Opp
fighter.OppTeam = f.Our
}
}
// getSideInputs 按 userID 判定所属阵营后返回目标侧输入集合。
func (f *FightC) getSideInputs(userID uint32, isOpposite bool) []*input.Input {
isOur := f.isOurPlayerID(userID)
if isOpposite {
if isOur {
return f.Opp
}
return f.Our
}
if isOur {
return f.Our
}
return f.Opp
}
// findInputByUserID 在双方站位中按控制者查找任一输入,并返回是否属于我方。
func (f *FightC) findInputByUserID(userID uint32) (*input.Input, bool) {
for _, in := range f.Our {
if in != nil && in.ControlledBy(userID) {
return in, true
}
}
for _, in := range f.Opp {
if in != nil && in.ControlledBy(userID) {
return in, false
}
}
return nil, false
}
// getInputByUserID 按 userID + 站位下标获取输入。
// 当查询本侧站位时,要求该站位必须由 userID 控制。
func (f *FightC) getInputByUserID(userID uint32, index int, isOpposite bool) *input.Input {
selected := f.selectInput(f.getSideInputs(userID, isOpposite), index)
if selected == nil {
return nil
}
// 操作自身站位时,必须由该站位控制者发起。
if !isOpposite && !selected.ControlledBy(userID) {
return nil
}
return selected
}
// getInputByController 按控制者获取其首个可操作站位(常用于兼容单站位接口)。
func (f *FightC) getInputByController(userID uint32, isOpposite bool) *input.Input {
sideInputs := f.getSideInputs(userID, isOpposite)
for _, in := range sideInputs {
if in != nil && in.ControlledBy(userID) {
return in
}
}
return f.selectInput(sideInputs, 0)
}
func (f *FightC) expectedActionSlots() map[actionSlotKey]struct{} {
slots := make(map[actionSlotKey]struct{}, len(f.Our)+len(f.Opp))
for _, slot := range f.SideSlots(SideOur) {
if f.slotNeedsAction(slot.Input) {
slots[newActionSlotKey(slot.ControllerUserID, slot.SlotIndex)] = struct{}{}
}
}
for _, slot := range f.SideSlots(SideOpp) {
if f.slotNeedsAction(slot.Input) {
slots[newActionSlotKey(slot.ControllerUserID, slot.SlotIndex)] = struct{}{}
}
}
return slots
}
func (f *FightC) sideHasActionableSlots(side int) bool {
for _, slot := range f.SideSlots(side) {
if f.slotNeedsAction(slot.Input) {
return true
}
}
return false
}
func (f *FightC) slotNeedsAction(in *input.Input) bool {
if in == nil {
return false
}
if current := in.CurrentPet(); current != nil && current.Info.Hp > 0 {
return true
}
return in.HasLivingBench()
}
func (f *FightC) setActionAttackValue(act action.BattleActionI) {
if act == nil {
return
}
attacker := f.GetInputByAction(act, false)
if attacker == nil || attacker.AttackValue == nil {
return
}
attacker.AttackValue.ActorIndex = uint32(act.GetActorIndex())
targetIndex, _ := DecodeTargetIndex(act.GetTargetIndex())
if _, resolvedIndex, ok := f.resolveActionTarget(act); ok && resolvedIndex >= 0 {
targetIndex = resolvedIndex
}
attacker.AttackValue.TargetIndex = uint32(targetIndex)
}
func (f *FightC) Ownerid() uint32 {
return f.ownerID
}
func (f *FightC) GetInputByPlayer(c common.PlayerI, isOpposite bool) *input.Input {
if c == nil {
if isOpposite {
return f.primaryOpp()
}
return f.primaryOur()
}
return f.getInputByController(c.GetInfo().UserID, isOpposite)
}
// GetInputsByPlayer 返回玩家在指定侧的全部可控站位。
func (f *FightC) GetInputsByPlayer(c common.PlayerI, isOpposite bool) []*input.Input {
if c == nil {
return nil
}
sideInputs := f.getSideInputs(c.GetInfo().UserID, isOpposite)
result := make([]*input.Input, 0, len(sideInputs))
for _, in := range sideInputs {
if in != nil && in.ControlledBy(c.GetInfo().UserID) {
result = append(result, in)
}
}
return result
}
// GetInputByPlayerAt 按玩家+站位下标获取输入。
func (f *FightC) GetInputByPlayerAt(c common.PlayerI, actorIndex int, isOpposite bool) *input.Input {
if c == nil {
return nil
}
return f.getInputByUserID(c.GetInfo().UserID, actorIndex, isOpposite)
}
func (f *FightC) resolveActionTarget(c action.BattleActionI) (*input.Input, int, bool) {
if c == nil {
return nil, -1, false
}
attacker := f.getInputByUserID(c.GetPlayerID(), c.GetActorIndex(), false)
if attacker == nil {
return nil, -1, false
}
encodedTargetIndex := c.GetTargetIndex()
targetIndex, targetIsOpposite := DecodeTargetIndex(encodedTargetIndex)
if !targetIsOpposite {
return attacker.TeamSlotAt(targetIndex), targetIndex, false
}
if target, resolvedIndex := attacker.OpponentSlotAtOrNextLiving(targetIndex); target != nil {
return target, resolvedIndex, true
}
return attacker.OpponentSlotAt(targetIndex), targetIndex, true
}
func (f *FightC) GetInputByAction(c action.BattleActionI, isOpposite bool) *input.Input {
if c == nil {
if isOpposite {
return f.primaryOpp()
}
return f.primaryOur()
}
index := c.GetActorIndex()
if !isOpposite {
return f.getInputByUserID(c.GetPlayerID(), index, false)
}
target, _, _ := f.resolveActionTarget(c)
return target
}
// 玩家使用技能
func (f *FightC) GetCurrPET(c common.PlayerI) *info.BattlePetEntity {
return f.GetCurrPETAt(c, 0)
}
func (f *FightC) GetCurrPETAt(c common.PlayerI, actorIndex int) *info.BattlePetEntity {
if c == nil {
return nil
}
in := f.getInputByUserID(c.GetInfo().UserID, actorIndex, false)
if in == nil {
return nil
}
return in.PrimaryCurPet()
}
func (f *FightC) GetCurrPETByAction(c action.BattleActionI, isOpposite bool) *info.BattlePetEntity {
in := f.GetInputByAction(c, isOpposite)
if in == nil {
return nil
}
return in.CurrentPet()
}
func (f *FightC) GetOpp(c common.PlayerI) *input.Input {
return f.GetInputByPlayer(c, true)
}
// // 获取随机数
func (f *FightC) IsFirst(play common.PlayerI) bool {
if f == nil || play == nil {
return false
}
if f.TrueFirst != nil && f.TrueFirst.Player != nil {
return f.TrueFirst.Player == play
}
if f.First != nil && f.First.Player != nil {
return f.First.Player == play
}
return false
}
func (f *FightC) GetRound() uint32 {
return f.Round
}
func (f *FightC) Chat(c common.PlayerI, msg string) {
f.sendFightPacket(f.GetInputByPlayer(c, true).Player, fightPacketChat, &user.ChatOutboundInfo{
SenderId: c.GetInfo().UserID,
SenderNickname: c.GetInfo().Nick,
Message: utils.RemoveLast(msg),
})
}
// 加载进度
func (f *FightC) LoadPercent(c common.PlayerI, percent int32) {
if f.Info.Mode == info.BattleMode.PET_MELEE {
return
}
if f.Info.Status == info.BattleMode.FIGHT_WITH_NPC {
return
}
f.sendFightPacket(f.GetInputByPlayer(c, true).Player, fightPacketLoadPercentNotice, &info.LoadPercentOutboundInfo{
Id: c.GetInfo().UserID,
Percent: uint32(percent),
})
}
func (f *FightC) initplayer(c common.PlayerI, b []model.PetInfo) (*input.Input, errorcode.ErrorCode) {
r := c.CanFight()
if c.CanFight() != 0 {
return nil, r
}
in := input.NewInput(f, c)
in.AllPet = make([]*info.BattlePetEntity, 0)
in.InitAttackValue()
for i := 0; i < len(b); i++ {
//玩家精灵重置到100等级
pet := b[i]
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
}
switch f.Info.Mode {
case info.BattleMode.SINGLE_MODE:
in.AllPet = in.AllPet[:1]
default:
}
in.SetCurPetAt(0, in.AllPet[0])
return in, 0
}
// RandomElfIDs 从1-2000中随机抽取n个不重复的精灵ID
func RandomElfIDs(n int) []int {
if n <= 0 || n > 2000 {
return nil
}
// 用map记录已抽取的ID避免重复
used := make(map[int]struct{}, n)
ids := make([]int, 0, n)
for len(ids) < n {
// 生成1-2000的随机数
id := grand.Intn(2000) + 1 // rand.Intn(2000)生成0-1999+1后为1-2000
// 检查是否已抽取
if _, exists := used[id]; !exists {
used[id] = struct{}{}
ids = append(ids, id)
}
}
return ids
}
func initfightready(in *input.Input) (model.FightUserInfo, []model.ReadyFightPetInfo) {
t := make([]model.ReadyFightPetInfo, len(in.AllPet))
userindo := model.FightUserInfo{
UserID: in.UserID,
Nick: in.Player.GetInfo().Nick,
}
for i := 0; i < len(in.AllPet); i++ {
err := copier.CopyWithOption(&t[i], &in.AllPet[i].Info, copier.Option{IgnoreEmpty: true, DeepCopy: true})
if err != nil {
panic(err)
}
if i == 0 && in.CanCapture > 0 {
t[i].IsCapture = uint32(in.CanCapture)
}
}
return userindo, t
}
// 被击败的ID
func (f *FightC) IsWin(c *input.Input) bool {
if c == nil || c.Player == nil {
return false
}
for _, sideInput := range f.getSideInputs(c.Player.GetInfo().UserID, true) {
if sideInput == nil {
continue
}
for _, v := range sideInput.AllPet {
if v.Alive() {
return false
}
}
}
return true
}
// 广播,并是否结束回合
func (f *FightC) Broadcast(t func(ff *input.Input)) {
for _, ff := range f.Our {
if ff != nil {
t(ff)
}
}
for _, ff := range f.Opp {
if ff != nil {
t(ff)
}
}
}
func (f *FightC) BroadcastPlayers(t func(common.PlayerI)) {
if f == nil || t == nil {
return
}
seen := make(map[uint32]struct{}, len(f.OurPlayers)+len(f.OppPlayers))
visit := func(players []common.PlayerI) {
for _, p := range players {
if p == nil || p.GetInfo() == nil {
continue
}
if _, ok := seen[p.GetInfo().UserID]; ok {
continue
}
seen[p.GetInfo().UserID] = struct{}{}
t(p)
}
}
visit(f.OurPlayers)
visit(f.OppPlayers)
}
func (f *FightC) GetOverChan() chan struct{} {
return f.over
}
func (f *FightC) GetOverInfo() model.FightOverInfo {
return f.FightOverInfo
}
func (f *FightC) GetAttackValue(b bool) *model.AttackValue {
our := f.primaryOur()
if our == nil {
return nil
}
return f.GetInputByPlayer(our.Player, b).AttackValue
}