```
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): 重构战斗系统代码结构

- 分离新旧组队协议的战斗创建逻辑
- 优化战斗输入验证和处理流程
- 改进战斗循环中的错误处理机制
```
This commit is contained in:
昔念
2026-04-09 02:14:09 +08:00
parent 3b35789b47
commit 487ee0e726
10 changed files with 796 additions and 130 deletions

View File

@@ -43,7 +43,8 @@ func (h Controller) GroupUseSkill(data *GroupUseSkillInboundInfo, c *player.Play
targetRelation = fight.SkillTargetAlly
}
h.dispatchFightActionEnvelope(c, fight.NewSkillActionEnvelope(data.SkillId, int(data.ActorIndex), int(data.TargetPos), targetRelation, 0))
return nil, 0
c.SendPackCmd(7558, nil)
return nil, -1
}
func (h Controller) GroupUseItem(data *GroupUseItemInboundInfo, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {

View File

@@ -75,54 +75,13 @@ func startMapBossFight(
ourPets := p.GetPetInfo(100)
oppPets := ai.GetPetInfo(0)
if mapNode != nil && mapNode.IsGroupBoss != 0 {
ourSlots := buildGroupBossPetSlots(ourPets, groupBossSlotLimit)
oppSlots := buildGroupBossPetSlots(oppPets, groupBossSlotLimit)
if len(ourSlots) > 0 && len(oppSlots) > 0 {
return fight.NewLegacyGroupFightSingleControllerN(p, ai, ourSlots, oppSlots, fn)
if len(ourPets) > 0 && len(oppPets) > 0 {
return fight.NewLegacyGroupFightSingleController(p, ai, ourPets, oppPets, groupBossSlotLimit, fn)
}
}
return fight.NewFight(p, ai, ourPets, oppPets, fn)
}
func buildGroupBossPetSlots(pets []model.PetInfo, slotLimit int) [][]model.PetInfo {
if len(pets) == 0 {
return nil
}
slots := make([][]model.PetInfo, 0, slotLimit)
for _, pet := range pets {
if pet.Hp == 0 {
continue
}
if slotLimit <= 0 {
slotLimit = 3
}
if len(slots) < slotLimit {
slots = append(slots, []model.PetInfo{pet})
continue
}
break
}
if len(slots) == 0 {
return nil
}
var idx int = 0
for _, pet := range pets[len(slots):] {
if pet.Hp == 0 {
continue
}
for step := 0; step < len(slots); step++ {
slotIdx := (idx + step) % len(slots)
if len(slots[slotIdx]) < 6 {
slots[slotIdx] = append(slots[slotIdx], pet)
idx = (slotIdx + 1) % len(slots)
break
}
}
}
return slots
}
// OnPlayerFightNpcMonster 战斗野怪
func (Controller) OnPlayerFightNpcMonster(req *FightNpcMonsterInboundInfo, p *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
if err = p.CanFight(); err != 0 {

View File

@@ -64,6 +64,7 @@ func (h *Controller) SwitchFlying(data *SwitchFlyingInboundInfo, c *player.Playe
// PlayerPetCure 处理控制器请求。
func (h *Controller) PlayerPetCure(data *PetCureInboundInfo, c *player.Player) (result *nono.PetCureOutboundEmpty, err errorcode.ErrorCode) { //这个时候player应该是空的
_ = data
result = &nono.PetCureOutboundEmpty{}
if c.IsArenaHealLocked() {
return result, errorcode.ErrorCodes.ErrChampionCannotHeal
}
@@ -73,6 +74,9 @@ func (h *Controller) PlayerPetCure(data *PetCureInboundInfo, c *player.Player) (
for i := range c.Info.PetList {
c.Info.PetList[i].Cure()
}
for i := range c.Info.BackupPetList {
c.Info.BackupPetList[i].Cure()
}
c.Info.Coins -= nonoPetCureCost
return
}

View File

@@ -82,6 +82,10 @@ func (f *FightC) submitAction(act action.BattleActionI) {
break
}
if replaceIndex >= 0 {
if f.LegacyGroupProtocol {
f.actionMu.Unlock()
return
}
f.pendingActions[replaceIndex] = act
} else {
f.pendingActions = append(f.pendingActions, act)
@@ -295,9 +299,8 @@ func (f *FightC) ReadyFight(c common.PlayerI) {
}
return
}
f.Broadcast(func(ff *input.Input) {
ff.Player.SendPackCmd(2404, &info.S2C_2404{UserID: c.GetInfo().UserID})
f.BroadcastPlayers(func(p common.PlayerI) {
p.SendPackCmd(2404, &info.S2C_2404{UserID: c.GetInfo().UserID})
})
// 2. 标记当前玩家已准备完成
input := f.GetInputByPlayer(c, false)
@@ -369,8 +372,12 @@ func (f *FightC) startBattle(startInfo info.FightStartOutboundInfo) {
go f.battleLoop()
// 向双方广播战斗开始信息
f.Broadcast(func(ff *input.Input) {
f.sendFightPacket(ff.Player, fightPacketStart, &startInfo)
f.BroadcastPlayers(func(p common.PlayerI) {
if f.LegacyGroupProtocol {
f.sendLegacyGroupStart(p)
return
}
f.sendFightPacket(p, fightPacketStart, &startInfo)
})
})
}

View File

@@ -3,6 +3,7 @@ package fight
import (
"blazing/common/utils"
"blazing/logic/service/common"
"blazing/logic/service/fight/action"
"blazing/logic/service/fight/info"
"blazing/logic/service/fight/input"
@@ -422,11 +423,19 @@ func (f *FightC) enterturn(firstAttack, secondAttack *action.SelectSkillAction)
attackValueResult := f.buildNoteUseSkillOutboundInfo()
//因为切完才能广播,所以必须和回合结束分开结算
f.Broadcast(func(fighter *input.Input) {
f.BroadcastPlayers(func(p common.PlayerI) {
for _, switchAction := range f.Switch {
if fighter.Player.GetInfo().UserID != switchAction.Reason.UserId {
if p.GetInfo().UserID != switchAction.Reason.UserId {
// println("切精灵", switchAction.Reason.UserId, switchAction.Reason.ID)
f.sendFightPacket(fighter.Player, fightPacketChangePetSuccess, &switchAction.Reason)
if f.LegacyGroupProtocol {
switchedInput := f.getInputByUserID(switchAction.Reason.UserId, int(switchAction.Reason.ActorIndex), false)
if switchedInput == nil {
switchedInput = f.getInputByUserID(switchAction.Reason.UserId, int(switchAction.ActorIndex), false)
}
f.sendLegacyGroupChangePetSuccess(p, switchedInput, &switchAction.Reason)
} else {
f.sendFightPacket(p, fightPacketChangePetSuccess, &switchAction.Reason)
}
}
}
})
@@ -440,8 +449,12 @@ func (f *FightC) enterturn(firstAttack, secondAttack *action.SelectSkillAction)
// })
return
}
f.BroadcastPlayers(func(p common.PlayerI) {
if !f.LegacyGroupProtocol {
f.sendFightPacket(p, fightPacketSkillResult, &attackValueResult)
}
})
f.Broadcast(func(fighter *input.Input) {
f.sendFightPacket(fighter.Player, fightPacketSkillResult, &attackValueResult)
fighter.CanChange = 0
})
if f.closefight {

View File

@@ -3,6 +3,7 @@ package fight
import (
"blazing/logic/service/common"
"blazing/logic/service/fight/action"
"blazing/logic/service/fight/info"
"blazing/logic/service/fight/input"
"blazing/modules/player/model"
)
@@ -38,6 +39,17 @@ const (
type fightPacketKind uint8
const (
fightPacketReady fightPacketKind = iota
fightPacketStart
fightPacketSkillResult
fightPacketOver
fightPacketChangePetSuccess
fightPacketUseItem
fightPacketChat
fightPacketLoadPercentNotice
)
type legacyEscapeSuccessInfo struct {
UserID uint32 `struc:"uint32"`
Nick string `struc:"[16]byte"`
@@ -50,6 +62,14 @@ type legacyBoutDoneInfo struct {
}
type legacySpriteDieInfo struct {
Count uint8 `struc:"uint8"`
Side uint8 `struc:"uint8"`
ActorIndex uint8 `struc:"uint8"`
Flag uint8 `struc:"uint8"`
HasBackup uint32 `struc:"uint32"`
}
type legacyLegacySpriteDieItem struct {
Flag uint8 `struc:"uint8"`
Side uint8 `struc:"uint8"`
ActorIndex uint8 `struc:"uint8"`
@@ -57,16 +77,132 @@ type legacySpriteDieInfo struct {
HasBackup uint32 `struc:"uint32"`
}
const (
fightPacketReady fightPacketKind = iota
fightPacketStart
fightPacketSkillResult
fightPacketOver
fightPacketChangePetSuccess
fightPacketUseItem
fightPacketChat
fightPacketLoadPercentNotice
)
type legacyGroupReadyToFightInfo struct {
Model uint32 `struc:"uint32"`
GroupOneInfo legacyReadyToFightTeam `struc:""`
GroupTwoInfo legacyReadyToFightTeam `struc:""`
}
type legacyReadyToFightTeam struct {
InvitorID uint8 `struc:"uint8"`
LeaderID uint32 `struc:"uint32"`
GroupMembCnt uint8 `struc:"sizeof=GroupList"`
GroupList []legacyReadyFightUser `struc:""`
}
type legacyReadyFightUser struct {
UserID uint32 `struc:"uint32"`
Nick string `struc:"[16]byte"`
MonCnt uint32 `struc:"sizeof=MonList"`
MonList []legacyReadyFightPet `struc:""`
}
type legacyReadyFightPet struct {
ID uint32 `struc:"uint32"`
MoveCnt uint32 `struc:"sizeof=MoveList"`
MoveList []uint32 `struc:"[]uint32"`
}
type legacyGroupStartInfo struct {
IsGank uint8 `struc:"uint8"`
GroupOneN uint8 `struc:"sizeof=GroupOne"`
GroupOne []legacyGroupStartPet `struc:""`
GroupTwoN uint8 `struc:"sizeof=GroupTwo"`
GroupTwo []legacyGroupStartPet `struc:""`
}
type legacyGroupStartPet struct {
Side uint8 `struc:"uint8"`
Pos uint8 `struc:"uint8"`
UserID uint32 `struc:"uint32"`
IsChange uint8 `struc:"uint8"`
PetID uint32 `struc:"uint32"`
CatchTime uint32 `struc:"uint32"`
Hp uint32 `struc:"uint32"`
MaxHp uint32 `struc:"uint32"`
Level uint32 `struc:"uint32"`
Reserve uint32 `struc:"uint32"`
Flag uint32 `struc:"uint32"`
}
type legacyGroupSkillHurtPacket struct {
IsGank uint8 `struc:"uint8"`
Attack legacyGroupSkillAttackInfo `struc:""`
Attacked legacyGroupSkillDefendInfo `struc:""`
}
type legacyGroupSkillAttackInfo struct {
IsAttackor uint8 `struc:"uint8"`
Side uint8 `struc:"uint8"`
Pos uint8 `struc:"uint8"`
UserID uint32 `struc:"uint32"`
StatusList [20]uint8 `struc:"[20]byte"`
Reserve1 uint8 `struc:"uint8"`
Reserve2 uint8 `struc:"uint8"`
BatLvList [6]uint8 `struc:"[6]byte"`
PetID uint32 `struc:"uint32"`
MoveID uint32 `struc:"uint32"`
Hp uint32 `struc:"uint32"`
MaxHp uint32 `struc:"uint32"`
MoveCnt uint32 `struc:"sizeof=MoveMap"`
MoveMap []legacyGroupSkillMoveInfo `struc:""`
Flag uint32 `struc:"uint32"`
IsCrit uint32 `struc:"uint32"`
EffectName uint32 `struc:"uint32"`
AtkTimes uint32 `struc:"uint32"`
Dmg int32 `struc:"int32"`
ChgHp int32 `struc:"int32"`
SideEffectLen uint32 `struc:"uint32"`
}
type legacyGroupSkillDefendInfo struct {
IsAttackor uint8 `struc:"uint8"`
Side uint8 `struc:"uint8"`
Pos uint8 `struc:"uint8"`
UserID uint32 `struc:"uint32"`
StatusList [20]uint8 `struc:"[20]byte"`
Reserve1 uint8 `struc:"uint8"`
Reserve2 uint8 `struc:"uint8"`
BatLvList [6]uint8 `struc:"[6]byte"`
PetID uint32 `struc:"uint32"`
MoveID uint32 `struc:"uint32"`
Hp uint32 `struc:"uint32"`
MaxHp uint32 `struc:"uint32"`
MoveCnt uint32 `struc:"sizeof=MoveMap"`
MoveMap []legacyGroupSkillMoveInfo `struc:""`
Flag uint32 `struc:"uint32"`
SideEffectLen uint32 `struc:"uint32"`
}
type legacyGroupSkillMoveInfo struct {
MoveID uint32 `struc:"uint32"`
PP uint32 `struc:"uint32"`
}
type legacyGroupFightOverInfo struct {
IsGank uint8 `struc:"uint8"`
Reason uint32 `struc:"uint32"`
WinnerID uint32 `struc:"uint32"`
Reserve uint32 `struc:"uint32"`
TwoTimes uint32 `struc:"uint32"`
ThreeTimes uint32 `struc:"uint32"`
AutoFightTime uint32 `struc:"uint32"`
Reserve2 uint32 `struc:"uint32"`
EnergyTime uint32 `struc:"uint32"`
LearnTimes uint32 `struc:"uint32"`
}
type legacyGroupChangePetSuccessInfo struct {
Side uint8 `struc:"uint8"`
Pos uint8 `struc:"uint8"`
UserID uint32 `struc:"uint32"`
PetID uint32 `struc:"uint32"`
CatchTime uint32 `struc:"uint32"`
Level uint32 `struc:"uint32"`
Hp uint32 `struc:"uint32"`
MaxHp uint32 `struc:"uint32"`
SkinID uint32 `struc:"uint32"`
}
func groupModelByFight(f *FightC) uint32 {
if f == nil {
@@ -84,37 +220,15 @@ func groupModelByFight(f *FightC) uint32 {
}
}
func (f *FightC) sendLegacyGroupOver(player common.PlayerI, over *model.FightOverInfo) {
if player == nil {
return
}
if over == nil {
over = &model.FightOverInfo{}
}
f.sendFightPacket(player, fightPacketOver, over)
}
func (f *FightC) fightPacketCmd(kind fightPacketKind) uint32 {
switch kind {
case fightPacketReady:
if f != nil && f.LegacyGroupProtocol {
return groupCmdReadyToFight
}
return 2503
case fightPacketStart:
if f != nil && f.LegacyGroupProtocol {
return groupCmdStartFight
}
return 2504
case fightPacketSkillResult:
if f != nil && f.LegacyGroupProtocol {
return groupCmdSkillHurt
}
return 2505
case fightPacketOver:
if f != nil && f.LegacyGroupProtocol {
return groupCmdFightOver
}
return 2506
case fightPacketChangePetSuccess:
if f != nil && f.LegacyGroupProtocol {
@@ -152,6 +266,232 @@ func (f *FightC) sendFightPacket(player common.PlayerI, kind fightPacketKind, pa
player.SendPackCmd(cmd, payload)
}
func (f *FightC) sendLegacyGroupReady(player common.PlayerI) {
if f == nil || !f.LegacyGroupProtocol || player == nil {
return
}
player.SendPackCmd(groupCmdReadyToFight, f.buildLegacyGroupReadyInfo())
}
func (f *FightC) buildLegacyGroupReadyInfo() *legacyGroupReadyToFightInfo {
return &legacyGroupReadyToFightInfo{
Model: groupModelByFight(f),
GroupOneInfo: f.buildLegacyReadyTeam(f.OurPlayers, f.Our),
GroupTwoInfo: f.buildLegacyReadyTeam(f.OppPlayers, f.Opp),
}
}
func (f *FightC) buildLegacyReadyTeam(players []common.PlayerI, inputs []*input.Input) legacyReadyToFightTeam {
team := legacyReadyToFightTeam{InvitorID: 1}
users := make([]legacyReadyFightUser, 0, len(players))
for _, p := range players {
if p == nil || p.GetInfo() == nil {
continue
}
users = append(users, legacyReadyFightUser{
UserID: p.GetInfo().UserID,
Nick: p.GetInfo().Nick,
MonList: collectLegacyReadyPetsByController(inputs, p.GetInfo().UserID),
})
}
if len(users) == 0 {
if fallback := firstNonNilInput(inputs); fallback != nil && fallback.Player != nil && fallback.Player.GetInfo() != nil {
info := fallback.Player.GetInfo()
users = append(users, legacyReadyFightUser{
UserID: info.UserID,
Nick: info.Nick,
MonList: collectLegacyReadyPetsByController(inputs, info.UserID),
})
}
}
for idx := range users {
users[idx].MonCnt = uint32(len(users[idx].MonList))
}
if len(users) > 0 {
team.LeaderID = users[0].UserID
}
team.GroupList = users
return team
}
func collectLegacyReadyPetsByController(inputs []*input.Input, controllerID uint32) []legacyReadyFightPet {
pets := make([]legacyReadyFightPet, 0, 6)
for _, in := range inputs {
if in == nil || !in.ControlledBy(controllerID) {
continue
}
currentPet := in.CurrentPet()
if currentPet == nil {
continue
}
pets = append(pets, buildLegacyReadyFightPet(currentPet))
}
return pets
}
func buildLegacyReadyFightPet(pet *info.BattlePetEntity) legacyReadyFightPet {
result := legacyReadyFightPet{}
if pet == nil {
return result
}
moves := make([]uint32, 0, len(pet.Info.SkillList))
for _, skill := range pet.Info.SkillList {
if skill.ID == 0 {
continue
}
moves = append(moves, skill.ID)
}
result.ID = pet.Info.ID
result.MoveList = moves
return result
}
func firstNonNilInput(inputs []*input.Input) *input.Input {
for _, in := range inputs {
if in != nil {
return in
}
}
return nil
}
func (f *FightC) sendLegacyGroupStart(player common.PlayerI) {
if f == nil || !f.LegacyGroupProtocol || player == nil {
return
}
player.SendPackCmd(groupCmdStartFight, f.buildLegacyGroupStartInfo())
}
func (f *FightC) buildLegacyGroupStartInfo() *legacyGroupStartInfo {
return &legacyGroupStartInfo{
IsGank: 0,
GroupOne: f.collectLegacyGroupStartPets(f.Our, 1),
GroupTwo: f.collectLegacyGroupStartPets(f.Opp, 2),
}
}
func (f *FightC) collectLegacyGroupStartPets(inputs []*input.Input, side uint8) []legacyGroupStartPet {
ret := make([]legacyGroupStartPet, 0, len(inputs))
for pos, in := range inputs {
if in == nil {
continue
}
currentPet := in.CurrentPet()
if currentPet == nil {
continue
}
userID := uint32(0)
if in.Player != nil && in.Player.GetInfo() != nil {
userID = in.Player.GetInfo().UserID
}
ret = append(ret, legacyGroupStartPet{
Side: side,
Pos: uint8(pos),
UserID: userID,
IsChange: 0,
PetID: currentPet.Info.ID,
CatchTime: currentPet.Info.CatchTime,
Hp: currentPet.Info.Hp,
MaxHp: currentPet.Info.MaxHp,
Level: currentPet.Info.Level,
Reserve: 0,
Flag: 1,
})
}
return ret
}
func (f *FightC) sendLegacyGroupOver(player common.PlayerI, over *model.FightOverInfo) {
if f == nil || !f.LegacyGroupProtocol || player == nil {
return
}
player.SendPackCmd(groupCmdFightOver, f.buildLegacyGroupOverInfo(over))
}
func (f *FightC) buildLegacyGroupOverInfo(over *model.FightOverInfo) *legacyGroupFightOverInfo {
result := &legacyGroupFightOverInfo{}
if over != nil {
result.Reason = resolveLegacyGroupFightOverReason(over)
result.WinnerID = over.WinnerId
}
if our := f.primaryOurPlayer(); our != nil && our.GetInfo() != nil {
playerInfo := our.GetInfo()
result.TwoTimes = uint32(playerInfo.TwoTimes)
result.ThreeTimes = uint32(playerInfo.ThreeTimes)
result.AutoFightTime = playerInfo.AutoFightTime
result.EnergyTime = uint32(playerInfo.EnergyTime)
result.LearnTimes = playerInfo.LearnTimes
}
return result
}
func mapLegacyGroupFightOverReason(reason model.EnumBattleOverReason) uint32 {
switch reason {
case model.BattleOverReason.PlayerOffline:
return 2
case model.BattleOverReason.PlayerOVerTime:
return 3
case model.BattleOverReason.NOTwind:
return 4
case model.BattleOverReason.DefaultEnd:
return 1
case model.BattleOverReason.PlayerEscape:
return 6
default:
return 5
}
}
func resolveLegacyGroupFightOverReason(over *model.FightOverInfo) uint32 {
if over == nil {
return 5
}
switch over.Reason {
case model.BattleOverReason.PlayerOffline:
return 2
case model.BattleOverReason.PlayerOVerTime:
return 3
case model.BattleOverReason.PlayerEscape:
return 6
case model.BattleOverReason.NOTwind:
return 4
}
if over.WinnerId != 0 {
return 1
}
return mapLegacyGroupFightOverReason(over.Reason)
}
func (f *FightC) sendLegacyGroupChangePetSuccess(player common.PlayerI, in *input.Input, reason *info.ChangePetInfo) {
if f == nil || !f.LegacyGroupProtocol || player == nil || in == nil || reason == nil {
return
}
player.SendPackCmd(groupCmdChangePetSuc, f.buildLegacyGroupChangePetSuccessInfo(in, reason))
}
func (f *FightC) buildLegacyGroupChangePetSuccessInfo(in *input.Input, reason *info.ChangePetInfo) *legacyGroupChangePetSuccessInfo {
result := &legacyGroupChangePetSuccessInfo{}
if in == nil || reason == nil {
return result
}
if !f.isOurPlayerID(in.UserID) {
result.Side = 2
} else {
result.Side = 1
}
result.Pos = uint8(in.TeamSlotIndex())
result.UserID = reason.UserId
result.PetID = reason.ID
result.CatchTime = reason.CatchTime
result.Level = reason.Level
result.Hp = reason.Hp
result.MaxHp = reason.MaxHp
if currentPet := in.CurrentPet(); currentPet != nil {
result.SkinID = currentPet.Info.SkinID
}
return result
}
func (f *FightC) SendLegacyEscapeSuccess(player common.PlayerI, actorIndex int) {
if f == nil || !f.LegacyGroupProtocol || player == nil {
return
@@ -166,11 +506,8 @@ func (f *FightC) SendLegacyEscapeSuccess(player common.PlayerI, actorIndex int)
Side: side,
ActorIndex: uint8(actorIndex),
}
f.Broadcast(func(ff *input.Input) {
if ff == nil || ff.Player == nil {
return
}
ff.Player.SendPackCmd(groupCmdEscapeSuc, &payload)
f.BroadcastPlayers(func(p common.PlayerI) {
p.SendPackCmd(groupCmdEscapeSuc, &payload)
})
}
@@ -178,19 +515,147 @@ func (f *FightC) sendLegacyRoundBroadcast(firstAttack, secondAttack *action.Sele
if f == nil || !f.LegacyGroupProtocol {
return
}
if firstAttack != nil {
f.sendLegacyGroupSkillHurt(firstAttack)
}
if secondAttack != nil {
f.sendLegacyGroupSkillHurt(secondAttack)
}
f.sendLegacyGroupBoutDone()
}
func (f *FightC) sendLegacyGroupSkillHurt(skillAction *action.SelectSkillAction) {
if f == nil || !f.LegacyGroupProtocol || skillAction == nil {
return
}
packet := f.buildLegacyGroupSkillHurtPacket(skillAction)
if packet == nil {
return
}
f.BroadcastPlayers(func(p common.PlayerI) {
p.SendPackCmd(groupCmdSkillHurt, packet)
})
}
func (f *FightC) buildLegacyGroupSkillHurtPacket(skillAction *action.SelectSkillAction) *legacyGroupSkillHurtPacket {
attacker := f.GetInputByAction(skillAction, false)
defender := f.GetInputByAction(skillAction, true)
if attacker == nil || defender == nil {
return nil
}
return &legacyGroupSkillHurtPacket{
IsGank: 0,
Attack: f.buildLegacyGroupSkillAttackInfo(skillAction, attacker),
Attacked: f.buildLegacyGroupSkillDefendInfo(defender),
}
}
func (f *FightC) fillLegacyGroupSkillCommonFields(
self *input.Input,
isAttackor uint8,
statusList *[20]uint8,
batLvList *[6]uint8,
) (side uint8, pos uint8, userID uint32, petID uint32, hp uint32, maxHP uint32, moveMap []legacyGroupSkillMoveInfo, flag uint32) {
if self == nil {
return
}
if !f.isOurPlayerID(self.UserID) {
side = 2
} else {
side = 1
}
pos = uint8(self.TeamSlotIndex())
userID = self.UserID
attackValue := self.AttackValue
if attackValue == nil {
attackValue = info.NewAttackValue(self.UserID)
}
for i := 0; i < len(attackValue.Status) && i < 20; i++ {
statusList[i] = uint8(attackValue.Status[i])
}
for i := 0; i < len(attackValue.Prop) && i < len(batLvList); i++ {
batLvList[i] = uint8(attackValue.Prop[i])
}
currentPet := self.CurrentPet()
if currentPet != nil {
petID = currentPet.Info.ID
hp = currentPet.Info.Hp
maxHP = currentPet.Info.MaxHp
moveMap = collectLegacyGroupSkillMoves(currentPet.Info.SkillList)
} else {
hp = clampLegacyInt32ToUint32(attackValue.RemainHp)
maxHP = attackValue.MaxHp
moveMap = collectLegacyGroupSkillMoves(attackValue.SkillList)
}
flag = attackValue.State
return
}
func (f *FightC) buildLegacyGroupSkillAttackInfo(skillAction *action.SelectSkillAction, self *input.Input) legacyGroupSkillAttackInfo {
result := legacyGroupSkillAttackInfo{}
if self == nil {
return result
}
result.IsAttackor = 0
result.Side, result.Pos, result.UserID, result.PetID, result.Hp, result.MaxHp, result.MoveMap, result.Flag =
f.fillLegacyGroupSkillCommonFields(self, result.IsAttackor, &result.StatusList, &result.BatLvList)
attackValue := self.AttackValue
if attackValue == nil {
attackValue = info.NewAttackValue(self.UserID)
}
if skillAction != nil && skillAction.SkillEntity != nil {
result.MoveID = uint32(skillAction.SkillEntity.XML.ID)
} else {
result.MoveID = attackValue.SkillID
}
result.IsCrit = attackValue.IsCritical
result.EffectName = attackValue.State
result.AtkTimes = 1
result.Dmg = int32(attackValue.LostHp)
result.ChgHp = attackValue.GainHp
return result
}
func (f *FightC) buildLegacyGroupSkillDefendInfo(self *input.Input) legacyGroupSkillDefendInfo {
result := legacyGroupSkillDefendInfo{}
if self == nil {
return result
}
result.IsAttackor = 1
result.Side, result.Pos, result.UserID, result.PetID, result.Hp, result.MaxHp, result.MoveMap, result.Flag =
f.fillLegacyGroupSkillCommonFields(self, result.IsAttackor, &result.StatusList, &result.BatLvList)
result.MoveID = 0
return result
}
func collectLegacyGroupSkillMoves(skills []model.SkillInfo) []legacyGroupSkillMoveInfo {
moves := make([]legacyGroupSkillMoveInfo, 0, len(skills))
for _, skill := range skills {
if skill.ID == 0 {
continue
}
moves = append(moves, legacyGroupSkillMoveInfo{
MoveID: skill.ID,
PP: skill.PP,
})
}
return moves
}
func clampLegacyInt32ToUint32(v int32) uint32 {
if v < 0 {
return 0
}
return uint32(v)
}
func (f *FightC) sendLegacyGroupBoutDone() {
if f == nil || !f.LegacyGroupProtocol {
return
}
payload := legacyBoutDoneInfo{Round: f.Round}
f.Broadcast(func(ff *input.Input) {
if ff == nil || ff.Player == nil {
return
}
ff.Player.SendPackCmd(groupCmdBoutDone, &payload)
f.BroadcastPlayers(func(p common.PlayerI) {
p.SendPackCmd(groupCmdBoutDone, &payload)
})
}
@@ -207,16 +672,13 @@ func (f *FightC) sendLegacySpriteDie(in *input.Input, hasBackup bool) {
data = 1
}
payload := legacySpriteDieInfo{
Flag: 1,
Count: 1,
Side: side,
ActorIndex: uint8(in.TeamSlotIndex()),
Reserve: 1,
Flag: 1,
HasBackup: data,
}
f.Broadcast(func(ff *input.Input) {
if ff == nil || ff.Player == nil {
return
}
ff.Player.SendPackCmd(groupCmdSpriteDie, &payload)
f.BroadcastPlayers(func(p common.PlayerI) {
p.SendPackCmd(groupCmdSpriteDie, &payload)
})
}

View File

@@ -528,6 +528,27 @@ func (f *FightC) Broadcast(t func(ff *input.Input)) {
}
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

View File

@@ -173,10 +173,14 @@ func (f *FightC) battleLoop() {
//大乱斗,给个延迟
//<-time.After(1000)
f.Broadcast(func(ff *input.Input) {
f.sendFightPacket(ff.Player, fightPacketOver, &f.FightOverInfo)
f.BroadcastPlayers(func(p common.PlayerI) {
if f.LegacyGroupProtocol {
f.sendLegacyGroupOver(p, &f.FightOverInfo)
} else {
f.sendFightPacket(p, fightPacketOver, &f.FightOverInfo)
}
ff.Player.QuitFight()
p.QuitFight()
//待退出玩家战斗状态
})
@@ -260,7 +264,11 @@ func (f *FightC) collectPlayerActions(expectedSlots map[actionSlotKey]struct{})
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
@@ -281,7 +289,11 @@ func (f *FightC) collectPlayerActions(expectedSlots map[actionSlotKey]struct{})
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出手后获取出招
@@ -597,12 +609,12 @@ func (f *FightC) handleItemAction(a *action.UseItemAction) {
case gconv.Int(item.HP) != 0:
addhp := item.HP
source.Heal(source, a, alpacadecimal.NewFromInt(int64(addhp)))
f.Broadcast(func(ff *input.Input) {
f.BroadcastPlayers(func(p common.PlayerI) {
currentPet := source.PrimaryCurPet()
if currentPet == nil {
return
}
f.sendFightPacket(ff.Player, fightPacketUseItem, &info.UsePetIteminfo{
f.sendFightPacket(p, fightPacketUseItem, &info.UsePetIteminfo{
UserID: source.UserID,
ChangeHp: int32(addhp),
ItemID: uint32(item.ID),
@@ -612,12 +624,12 @@ func (f *FightC) handleItemAction(a *action.UseItemAction) {
})
case gconv.Int(item.PP) != 0:
source.HealPP(item.PP)
f.Broadcast(func(ff *input.Input) {
f.BroadcastPlayers(func(p common.PlayerI) {
currentPet := source.PrimaryCurPet()
if currentPet == nil {
return
}
f.sendFightPacket(ff.Player, fightPacketUseItem, &info.UsePetIteminfo{
f.sendFightPacket(p, fightPacketUseItem, &info.UsePetIteminfo{
UserID: source.UserID,
ItemID: uint32(item.ID),

View File

@@ -47,6 +47,100 @@ func NewFightSingleControllerN(
)
}
// 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,
@@ -82,6 +176,24 @@ func NewLegacyGroupFightSingleControllerN(
)
}
// 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(
@@ -117,25 +229,32 @@ func NewFightPerSlotControllerN(
)
}
// 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
}
fightInfo := p1.Getfightinfo()
ourInput, err := buildInputFromPets(p1, b1, fightInfo.Mode)
if err > 0 {
return nil, err
}
oppInput, err := buildInputFromPets(p2, b2, fightInfo.Mode)
if err > 0 {
return nil, err
}
return NewFightWithOptions(
WithFightInputs([]*input.Input{ourInput}, []*input.Input{oppInput}),
WithFightCallback(fn),
WithFightInfo(fightInfo),
)
return NewFightSingleController(p1, p2, b1, b2, 1, fn)
}
// buildFight 基于已准备好的双方 Inputs 构建战斗实例。
@@ -196,8 +315,12 @@ func buildFight(opts *fightBuildOptions) (*FightC, errorcode.ErrorCode) {
}
f.FightStartOutboundInfo = f.buildFightStartInfo()
f.Broadcast(func(ff *input.Input) {
f.sendFightPacket(ff.Player, fightPacketReady, &f.ReadyInfo)
f.BroadcastPlayers(func(p common.PlayerI) {
if f.LegacyGroupProtocol {
f.sendLegacyGroupReady(p)
return
}
f.sendFightPacket(p, fightPacketReady, &f.ReadyInfo)
})
cool.Cron.AfterFunc(loadtime, func() {
@@ -215,9 +338,13 @@ func buildFight(opts *fightBuildOptions) (*FightC, errorcode.ErrorCode) {
case !our.Finished:
f.WinnerId = opp.Player.GetInfo().UserID
}
f.Broadcast(func(ff *input.Input) {
f.sendFightPacket(ff.Player, fightPacketOver, &f.FightOverInfo)
ff.Player.QuitFight()
f.BroadcastPlayers(func(p common.PlayerI) {
if f.LegacyGroupProtocol {
f.sendLegacyGroupOver(p, &f.FightOverInfo)
} else {
f.sendFightPacket(p, fightPacketOver, &f.FightOverInfo)
}
p.QuitFight()
})
}
})

View File

@@ -0,0 +1,60 @@
package fight
import (
"blazing/logic/service/common"
"blazing/modules/player/model"
"testing"
)
func TestArrangePetsBySlotLimit(t *testing.T) {
pets := []model.PetInfo{
{ID: 1, Hp: 10},
{ID: 2, Hp: 10},
{ID: 3, Hp: 10},
{ID: 4, Hp: 10},
{ID: 5, Hp: 10},
{ID: 6, Hp: 10},
}
slots := ArrangePetsBySlotLimit(pets, 3)
if len(slots) != 3 {
t.Fatalf("expected 3 slots, got %d", len(slots))
}
if len(slots[0]) != 2 || slots[0][0].ID != 1 || slots[0][1].ID != 4 {
t.Fatalf("slot 0 mismatch: %+v", slots[0])
}
if len(slots[1]) != 2 || slots[1][0].ID != 2 || slots[1][1].ID != 5 {
t.Fatalf("slot 1 mismatch: %+v", slots[1])
}
if len(slots[2]) != 2 || slots[2][0].ID != 3 || slots[2][1].ID != 6 {
t.Fatalf("slot 2 mismatch: %+v", slots[2])
}
}
func TestExpandPlayersWithSlotLimit(t *testing.T) {
players := []common.PlayerI{&stubPlayer{}, &stubPlayer{}}
petsByPlayer := [][]model.PetInfo{
{
{ID: 1, Hp: 10},
{ID: 2, Hp: 10},
{ID: 3, Hp: 10},
},
{
{ID: 11, Hp: 10},
{ID: 12, Hp: 10},
},
}
flatPlayers, flatSlots, err := ExpandPlayersWithSlotLimit(players, petsByPlayer, 1)
if err != 0 {
t.Fatalf("unexpected err: %v", err)
}
if len(flatPlayers) != 2 || len(flatSlots) != 2 {
t.Fatalf("unexpected flatten result: players=%d slots=%d", len(flatPlayers), len(flatSlots))
}
if len(flatSlots[0]) != 3 || flatSlots[0][0].ID != 1 || flatSlots[0][1].ID != 2 || flatSlots[0][2].ID != 3 {
t.Fatalf("player0 slot mismatch: %+v", flatSlots[0])
}
if len(flatSlots[1]) != 2 || flatSlots[1][0].ID != 11 || flatSlots[1][1].ID != 12 {
t.Fatalf("player1 slot mismatch: %+v", flatSlots[1])
}
}