Files
bl/logic/service/fight/loop.go
昔念 f9543a5156
All checks were successful
ci/woodpecker/push/my-first-workflow Pipeline was successful
```
feat(fight): 使用专用函数构建战斗结束数据包

为战斗结束消息创建专用的构建函数,
统一处理战斗结束信息的数据包构建逻辑,
提高代码的一致性和可维护性。

fix(config): 优化数据库查询语句以提高性能

将数组包含操作(@>)替换为 ANY 操作符,
在 Egg、MapPit、PetFusion 等服务中使用更高效
的查询方式
2026-04-12 13:27:39 +08:00

698 lines
18 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/data/xmlres"
"blazing/common/socket/errorcode"
"blazing/common/utils"
"blazing/cool"
"blazing/modules/player/model"
"context"
"runtime/debug"
"sort"
"sync/atomic"
"blazing/logic/service/common"
"blazing/logic/service/fight/action"
"blazing/logic/service/fight/info"
"blazing/logic/service/fight/input"
"blazing/logic/service/player"
"fmt"
"time"
"github.com/alpacahq/alpacadecimal"
"github.com/gogf/gf/v2/util/gconv"
"github.com/jinzhu/copier"
)
func consumeLimitedPetEffects(pet *model.PetInfo) {
if pet == nil || len(pet.EffectInfo) == 0 {
return
}
next := pet.EffectInfo[:0]
for _, eff := range pet.EffectInfo {
if eff.Status == 2 {
if eff.LeftCount > 0 {
eff.LeftCount--
}
if eff.LeftCount == 0 {
continue
}
}
next = append(next, eff)
}
pet.EffectInfo = next
}
func (f *FightC) battleLoop() {
defer func() {
if err := recover(); err != nil { // 恢复 panicerr 为 panic 错误值
// 1. 打印错误信息
var ctx = context.Background()
cool.Logger.Error(ctx, f.ownerID, err)
f.Broadcast(func(ff *input.Input) {
if p, ok := ff.Player.(*player.Player); ok {
head := common.NewTomeeHeader(1001, p.Info.UserID)
head.Result = uint32(errorcode.ErrorCodes.ErrSystemBusyTryLater)
p.SendPack(head.Pack(nil))
p.Service.Info.Save(*p.Info)
}
})
}
}()
//fmt.Println("战斗开始精灵", f.Our[0].Player.GetInfo().PetList[0].CatchTime)
//fmt.Println("开始收集玩家动作", waitr)
for !f.closefight {
f.Round++
if !f.sideHasActionableSlots(SideOur) {
if player := f.primaryOppPlayer(); player != nil {
f.WinnerId = player.GetInfo().UserID
}
f.Reason = model.BattleOverReason.DefaultEnd
f.FightOverInfo.WinnerId = f.WinnerId
f.FightOverInfo.Reason = f.Reason
f.closefight = true
break
}
if !f.sideHasActionableSlots(SideOpp) {
if player := f.primaryOurPlayer(); player != nil {
f.WinnerId = player.GetInfo().UserID
}
f.Reason = model.BattleOverReason.DefaultEnd
f.FightOverInfo.WinnerId = f.WinnerId
f.FightOverInfo.Reason = f.Reason
f.closefight = true
break
}
expectedSlots := f.expectedActionSlots()
actions := f.collectPlayerActions(expectedSlots)
if f.closefight {
break
}
f.resolveRound(actions)
}
f.Broadcast(func(ff *input.Input) {
//todo 将血量和技能pp传回enterturn
ff.Exec(func(tt input.Effect) bool {
tt.OnBattleEnd()
tt.Alive(false) //将所有属性变化失效掉
return true
})
for i := 0; i < len(ff.AllPet); i++ {
consumeLimitedPetEffects(&ff.AllPet[i].Info)
for j := 0; j < len(ff.Player.GetInfo().PetList); j++ {
if ff.Player.GetInfo().PetList[j].CatchTime != ff.AllPet[i].Info.CatchTime {
continue
}
ff.Player.GetInfo().PetList[j].EffectInfo = ff.AllPet[i].Info.EffectInfo
if f.Info.Mode == info.BattleMode.PET_MELEE {
continue
}
if ff.UserID == f.WinnerId {
currentPet := ff.CurrentPet()
if currentPet != nil && currentPet.Info.CatchTime == ff.Player.GetInfo().PetList[j].CatchTime {
f.Winpet = &ff.Player.GetInfo().PetList[j]
}
}
ff.Player.GetInfo().PetList[j].Hp = utils.Min(ff.Player.GetInfo().PetList[j].MaxHp, ff.AllPet[i].Info.Hp)
ff.Player.GetInfo().PetList[j].SkillList = ff.AllPet[i].Info.SkillList
}
}
})
if f.Info.Status == info.BattleMode.FIGHT_WITH_NPC {
addpet := f.Opp[0].Player.GetInfo().PetList[0]
if f.Reason == model.BattleOverReason.Cacthok {
f.WinnerId = f.ownerID
addpet.EffectInfo = nil //清空特性信息
f.Our[0].Player.(*player.Player).Service.Pet.PetAdd(&addpet, 0)
oppPet := f.Opp[0].CurrentPet()
petID := uint32(0)
if oppPet != nil {
petID = uint32(oppPet.ID)
}
f.Our[0].Player.SendPackCmd(2409, &info.CatchMonsterOutboundInfo{
CatchTime: uint32(addpet.CatchTime),
PetId: petID,
})
defer f.Our[0].Player.(*player.Player).Service.Done.UpdatePet(addpet, 0, 1)
//f.Reason = 0 //清空
}
if f.Reason == 0 {
defer f.Our[0].Player.(*player.Player).Service.Done.UpdatePet(addpet, 1, 0)
}
// f.Our[0].Player.(*player.Player).MapNPC.Reset(7 * time.Second)
copier.Copy(&f.FightOverInfo, f.Our[0].Player.(*player.Player).Info)
atomic.StoreUint32(&f.Our[0].Player.(*player.Player).Canmon, 2)
f.Our[0].Player.(*player.Player).MapNPC.Reset(10 * time.Second)
// f.Our[0].Player.(*player.Player).Info.FightTime = f.Our[0].Player.(*player.Player).Info.FightTime + time.Now().Unix() - f.StartTime.Unix()
}
//大乱斗,给个延迟
//<-time.After(1000)
f.BroadcastPlayers(func(p common.PlayerI) {
if f.LegacyGroupProtocol {
f.sendLegacyGroupOver(p, &f.FightOverInfo)
} else {
f.sendFightPacket(p, fightPacketOver, buildFightOverPayload(f.FightOverInfo))
}
p.QuitFight()
//待退出玩家战斗状态
})
//f.Reason = info.BattleOverReason.PlayerCaptureSuccess
//f.WinnerId = 0 //捕捉成功不算胜利
if f.callback != nil {
f.callback(f.FightOverInfo) //先执行回调,再执行返回信息,在回调内修改战斗判断
}
close(f.over)
}
// 收集玩家动作(含超时判定)
func (f *FightC) collectPlayerActions(expectedSlots map[actionSlotKey]struct{}) []action.BattleActionI {
actions := make(map[actionSlotKey]action.BattleActionI, len(expectedSlots))
f.openActionWindow()
defer f.closeActionWindow()
if f.Info.Status == info.BattleMode.FIGHT_WITH_NPC {
f.triggerNPCActions()
}
waitr := time.Duration(f.waittime)*time.Millisecond*10 + 30*time.Second
timeout := time.NewTimer(waitr)
defer timeout.Stop()
for len(actions) < len(expectedSlots) {
select {
case <-f.quit:
f.closefight = true
return flattenActionMap(actions)
case <-f.actionNotify:
paction := f.nextAction()
if paction == nil {
continue
}
key := actionSlotKeyFromAction(paction)
if _, ok := expectedSlots[key]; !ok {
continue
}
selfinput := f.GetInputByAction(paction, false)
if selfinput == nil {
continue
}
if ret, ok := paction.(*action.ActiveSwitchAction); ok {
//正常结束可以切换,以及死切后还能再切一次
if selfinput.CanChange == 0 {
currentPet := selfinput.PrimaryCurPet()
if currentPet != nil && currentPet.Info.Hp > 0 { //非死亡切换
selfinput.CanChange = 1
f.Broadcast(func(ff *input.Input) {
ff.Exec(func(t input.Effect) bool {
t.SwitchOut(selfinput)
return true
})
})
} else {
selfinput.CanChange = 2
}
// oldpet := selfinput.CurPet[0]
// InitAttackValue := *selfinput.AttackValue
nextPet, reason := selfinput.GetPet(ret.Cid)
if nextPet == nil {
continue
}
selfinput.SetCurPetAt(0, nextPet)
ret.Reason = reason
ret.Reason.ActorIndex = uint32(ret.ActorIndex)
if f.LegacyGroupProtocol {
f.sendLegacyGroupChangePetSuccess(selfinput.Player, selfinput, &ret.Reason)
} else {
f.sendFightPacket(selfinput.Player, fightPacketChangePetSuccess, &ret.Reason)
}
f.Switch[key] = ret
selfinput.InitAttackValue() //切换精灵消除能力提升
//这时候精灵已经切换过了,可以直接给新精灵加效果
f.Broadcast(func(ff *input.Input) {
ff.Exec(func(t input.Effect) bool {
t.SwitchIn(selfinput)
return true
})
})
if selfinput.CanChange == 2 {
selfinput.CanChange = 0
if f.Info.Status == info.BattleMode.FIGHT_WITH_NPC && paction.GetPlayerID() == 0 {
f.Switch = make(map[actionSlotKey]*action.ActiveSwitchAction)
if f.LegacyGroupProtocol {
f.sendLegacyGroupChangePetSuccess(f.Our[0].Player, selfinput, &ret.Reason)
} else {
f.sendFightPacket(f.Our[0].Player, fightPacketChangePetSuccess, &ret.Reason)
}
//println("AI出手死切")
f.triggerNPCActions() // boss出手后获取出招
}
continue
}
} else {
continue
}
} else {
// println("玩家执行释放技能动作:", pid, paction.(*action.SelectSkillAction).Info.ID)
currentPet := selfinput.PrimaryCurPet()
if currentPet == nil || currentPet.Info.Hp <= 0 { //0血执行非切换动作
//todo 记录异常操作
cool.Logger.Print(context.TODO(), "玩家执行了异常操作当前精灵血量为0不能执行非切换动作", paction.GetPlayerID())
continue
}
if selfinput.CanChange == 1 { //非被动死亡情况下,不能执行额外动作,0允许切2是死亡可以额外动作
cool.Logger.Print(context.TODO(), "玩家执行了异常操作,切换后二次释放技能,不能执行非切换动作", paction.GetPlayerID())
continue
}
}
actions[key] = paction
//fmt.Println("玩家执行动作:", pid, paction.Priority())
case <-timeout.C:
r := f.handleTimeout(expectedSlots, actions)
if r {
return flattenActionMap(actions)
}
timeout.Reset(waitr)
}
}
return flattenActionMap(actions)
}
// 超时处理逻辑
func (f *FightC) handleTimeout(expectedSlots map[actionSlotKey]struct{}, actions map[actionSlotKey]action.BattleActionI) bool {
if f.Info.Status == info.BattleMode.FIGHT_WITH_NPC {
for key := range expectedSlots {
if _, exists := actions[key]; exists || !f.isOurPlayerID(key.PlayerID) {
continue
}
player := f.getPlayerByID(key.PlayerID)
if player != nil {
targetIndex := 0
if self := f.getInputByUserID(key.PlayerID, key.ActorIndex, false); self != nil {
targetIndex = self.RandomOpponentSlotIndex()
}
f.UseSkillAt(player, 0, key.ActorIndex, targetIndex)
}
}
return false
} else {
missingOur := false
missingOpp := false
for key := range expectedSlots {
if _, exists := actions[key]; exists {
continue
}
if f.isOurPlayerID(key.PlayerID) {
missingOur = true
continue
}
missingOpp = true
}
switch {
case missingOur && !missingOpp:
if player := f.primaryOppPlayer(); player != nil {
f.WinnerId = player.GetInfo().UserID
}
case missingOpp && !missingOur:
if player := f.primaryOurPlayer(); player != nil {
f.WinnerId = player.GetInfo().UserID
}
default:
for _, act := range actions {
f.WinnerId = act.GetPlayerID()
break
}
if f.WinnerId == 0 {
f.WinnerId = f.ownerID
}
}
f.Reason = model.BattleOverReason.PlayerOVerTime
f.closefight = true
return true
}
}
func (f *FightC) triggerNPCActions() {
for slot, opponent := range f.Opp {
if opponent == nil {
continue
}
go func(slot int, opponent *input.Input) {
defer func() {
if err := recover(); err != nil {
cool.Logger.Error(context.Background(), "fight npc action panic", f.ownerID, slot, err, string(debug.Stack()))
}
}()
opponent.GetAction()
}(slot, opponent)
}
}
func flattenActionMap(actions map[actionSlotKey]action.BattleActionI) []action.BattleActionI {
flattened := make([]action.BattleActionI, 0, len(actions))
for _, act := range actions {
if act != nil {
flattened = append(flattened, act)
}
}
return flattened
}
func (f *FightC) actionSpeed(act action.BattleActionI) alpacadecimal.Decimal {
if act == nil {
return alpacadecimal.Zero
}
attacker := f.GetInputByAction(act, false)
if attacker == nil {
return alpacadecimal.Zero
}
return attacker.GetProp(4)
}
func (f *FightC) sortActions(actions []action.BattleActionI) {
sort.SliceStable(actions, func(i, j int) bool {
a, b := actions[i], actions[j]
if a == nil || b == nil {
return a != nil
}
if a.Priority() != b.Priority() {
return a.Priority() > b.Priority()
}
if speedA, speedB := f.actionSpeed(a), f.actionSpeed(b); speedA.Cmp(speedB) != 0 {
return speedA.Cmp(speedB) > 0
}
if a.GetActorIndex() != b.GetActorIndex() {
return a.GetActorIndex() < b.GetActorIndex()
}
if a.GetTargetIndex() != b.GetTargetIndex() {
return a.GetTargetIndex() < b.GetTargetIndex()
}
return a.GetPlayerID() < b.GetPlayerID()
})
}
func sortActionsByActor(actions []action.BattleActionI) {
sort.SliceStable(actions, func(i, j int) bool {
a, b := actions[i], actions[j]
if a == nil || b == nil {
return a != nil
}
if a.GetActorIndex() != b.GetActorIndex() {
return a.GetActorIndex() < b.GetActorIndex()
}
if a.GetTargetIndex() != b.GetTargetIndex() {
return a.GetTargetIndex() < b.GetTargetIndex()
}
return a.GetPlayerID() < b.GetPlayerID()
})
}
func (f *FightC) splitRoundActions(actions []action.BattleActionI) ([]action.BattleActionI, []action.BattleActionI) {
our := make([]action.BattleActionI, 0, len(actions))
opp := make([]action.BattleActionI, 0, len(actions))
for _, act := range actions {
if act == nil {
continue
}
if f.isOurPlayerID(act.GetPlayerID()) {
our = append(our, act)
} else {
opp = append(opp, act)
}
}
sortActionsByActor(our)
sortActionsByActor(opp)
return our, opp
}
// 根据动作类型执行一回合结算
func (f *FightC) resolveRound(actions []action.BattleActionI) {
if len(actions) == 0 {
cool.Logger.Debug(context.Background(), "当前回合没有可执行动作,自动跳过结算")
return
}
ourActions, oppActions := f.splitRoundActions(actions)
roundLen := len(ourActions)
if len(oppActions) > roundLen {
roundLen = len(oppActions)
}
for i := 0; i < roundLen; i++ {
if f.closefight {
return
}
var ourAct action.BattleActionI
var oppAct action.BattleActionI
if i < len(ourActions) {
ourAct = ourActions[i]
}
if i < len(oppActions) {
oppAct = oppActions[i]
}
f.resolveActionPair(ourAct, oppAct)
}
}
func (f *FightC) resolveActionPair(p1Action, p2Action action.BattleActionI) {
if p1Action == nil && p2Action == nil {
return
}
// 动作优先级排序
b1, b2 := f.Compare(p1Action, p2Action)
switch actionType := b1.(type) {
case *action.ActiveSwitchAction:
f.handleActiveSwitchAction(actionType, b2)
case *action.UseItemAction:
f.handleUseItemAction(actionType, b2)
default:
f.handleSkillActions(b1, b2)
}
}
// handleActiveSwitchAction 处理主动切换精灵动作
func (f *FightC) handleActiveSwitchAction(_ *action.ActiveSwitchAction, otherAction action.BattleActionI) {
if skillAction, ok := otherAction.(*action.SelectSkillAction); ok {
if skillAction.SkillEntity != nil && skillAction.XML.CD != nil {
f.waittime = *skillAction.XML.CD
}
f.enterturn(skillAction, nil)
} else {
f.enterturn(nil, nil)
}
}
// handleUseItemAction 处理使用道具动作
func (f *FightC) handleUseItemAction(itemAction *action.UseItemAction, otherAction action.BattleActionI) {
f.setActionAttackValue(itemAction)
f.handleItemAction(itemAction)
input := f.GetInputByAction(itemAction, false)
if input == nil {
return
}
if currentPet := input.PrimaryCurPet(); currentPet != nil && currentPet.Info.Hp <= 0 {
currentPet.Info.Hp = 1
}
if skillAction, ok := otherAction.(*action.SelectSkillAction); ok {
if skillAction.SkillEntity != nil && skillAction.XML.CD != nil {
f.waittime = *skillAction.XML.CD
}
f.enterturn(skillAction, nil)
} else {
if otherItemAction, ok := otherAction.(*action.UseItemAction); ok {
f.handleItemAction(otherItemAction)
}
f.enterturn(nil, nil)
}
}
// 使用道具的逻辑封装
func (f *FightC) handleItemAction(a *action.UseItemAction) {
source := f.GetInputByAction(a, false)
target := f.GetInputByAction(a, true)
if source == nil {
return
}
item, ok := xmlres.ItemsMAP[int(a.ItemID)]
if !ok {
return
}
r := source.Player.(*player.Player).Service.Item.CheakItem(uint32(a.ItemID))
if r <= 0 {
return
}
source.Player.(*player.Player).Service.Item.UPDATE(a.ItemID, -1)
switch {
case gconv.Int(item.Bonus) != 0:
if target != nil && target.CanCapture > 0 && target.PrimaryCurPet() != nil { //可以捕捉
target.PrimaryCurPet().CatchRate = target.CanCapture
ok, _ := source.Capture(target.PrimaryCurPet(), a.ItemID, -1)
our := source.Player.(*player.Player)
if ok {
r := input.GetFunc(int64(item.ID))
if r != nil {
r.Exec(f, &target.Player.GetInfo().PetList[0])
}
f.Reason = model.BattleOverReason.Cacthok
f.closefight = true
} else {
our.SendPack(common.NewTomeeHeader(2409, f.ownerID).Pack(&info.CatchMonsterOutboundInfo{}))
}
}
case gconv.Int(item.HP) != 0:
addhp := item.HP
source.Heal(source, a, alpacadecimal.NewFromInt(int64(addhp)))
f.BroadcastPlayers(func(p common.PlayerI) {
currentPet := source.PrimaryCurPet()
if currentPet == nil {
return
}
f.sendFightPacket(p, fightPacketUseItem, &info.UsePetIteminfo{
UserID: source.UserID,
ChangeHp: int32(addhp),
ItemID: uint32(item.ID),
UserHp: uint32(currentPet.Info.Hp),
})
})
case gconv.Int(item.PP) != 0:
source.HealPP(item.PP)
f.BroadcastPlayers(func(p common.PlayerI) {
currentPet := source.PrimaryCurPet()
if currentPet == nil {
return
}
f.sendFightPacket(p, fightPacketUseItem, &info.UsePetIteminfo{
UserID: source.UserID,
ItemID: uint32(item.ID),
UserHp: uint32(currentPet.Info.Hp),
})
})
default:
fmt.Println(a.ItemID, "ItemID 不在指定范围内")
}
}
// 双方都是技能时的结算逻辑
func (f *FightC) handleSkillActions(a1, a2 action.BattleActionI) {
s1, _ := a1.(*action.SelectSkillAction)
s2, _ := a2.(*action.SelectSkillAction)
switch {
case s1 == nil || s1.SkillEntity == nil:
if s2 != nil && s2.SkillEntity != nil {
if s2.XML.CD != nil {
f.waittime = *s2.XML.CD
}
}
f.enterturn(s2, nil)
// fmt.Println("1 空过 2玩家执行技能:", s2.PlayerID, s2.Info.ID)
case s2 == nil || s2.SkillEntity == nil:
if s1 != nil && s1.SkillEntity != nil {
if s1.XML.CD != nil {
f.waittime = *s1.XML.CD
}
}
f.enterturn(s1, nil)
//fmt.Println("2 空过 玩家执行技能:", s1.PlayerID, s1.Info.ID)
default:
if s1.XML.CD != nil {
f.waittime = *s1.XML.CD
}
if s2.XML.CD != nil {
f.waittime += *s2.XML.CD
}
f.enterturn(s1, s2)
}
}
// 根据玩家ID返回对应对象
func (f *FightC) getPlayerByID(id uint32) common.PlayerI {
for _, player := range f.OurPlayers {
if player != nil && player.GetInfo().UserID == id {
return player
}
}
for _, player := range f.OppPlayers {
if player != nil && player.GetInfo().UserID == id {
return player
}
}
return nil
}