```
All checks were successful
ci/woodpecker/push/my-first-workflow Pipeline was successful

feat(fight): 添加旧组队协议支持并优化战斗系统

- 实现了旧组队协议相关功能,包括GroupReadyFightFinish、GroupUseSkill、
  GroupUseItem、GroupChangePet和GroupEscape方法
- 新增组队战斗相关的入站信息结构体定义
- 实现了组队BOSS战斗逻辑,添加groupBossSlotLimit常量
- 重构宠物技能设置逻辑,调整金币消耗时机
- 优化战斗循环逻辑,添加对无行动槽位的处理
- 改进AI行动逻辑,增加多位置目标选择
This commit is contained in:
昔念
2026-04-08 01:28:55 +08:00
parent 918cdeac0e
commit 0051ac0be8
21 changed files with 993 additions and 67 deletions

2
.vscode/launch.json vendored
View File

@@ -29,7 +29,7 @@
"request": "launch",
"mode": "auto",
"cwd": "${workspaceFolder}",
"args": ["-id=2"],
"args": ["-id=99"],
"program": "${workspaceFolder}/logic"
}

View File

@@ -25,6 +25,54 @@ func (h Controller) OnReadyToFight(data *ReadyToFightInboundInfo, c *player.Play
return nil, -1
}
// GroupReadyFightFinish 旧组队协议准备完成。
func (h Controller) GroupReadyFightFinish(data *GroupReadyFightFinishInboundInfo, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
if err := h.checkFightStatus(c); err != 0 {
return nil, err
}
go c.FightC.ReadyFight(c)
return nil, -1
}
func (h Controller) GroupUseSkill(data *GroupUseSkillInboundInfo, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
if err := h.checkFightStatus(c); err != 0 {
return nil, err
}
targetRelation := fight.SkillTargetOpponent
if data.TargetSide == 1 {
targetRelation = fight.SkillTargetAlly
}
h.dispatchFightActionEnvelope(c, fight.NewSkillActionEnvelope(data.SkillId, int(data.ActorIndex), int(data.TargetPos), targetRelation, 0))
return nil, 0
}
func (h Controller) GroupUseItem(data *GroupUseItemInboundInfo, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
if err := h.checkFightStatus(c); err != 0 {
return nil, err
}
h.dispatchFightActionEnvelope(c, fight.NewItemActionEnvelope(0, data.ItemId, int(data.ActorIndex), int(data.ActorIndex), fight.SkillTargetSelf))
return nil, -1
}
func (h Controller) GroupChangePet(data *GroupChangePetInboundInfo, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
if err := h.checkFightStatus(c); err != 0 {
return nil, err
}
h.dispatchFightActionEnvelope(c, fight.NewChangeActionEnvelope(data.CatchTime, int(data.ActorIndex)))
return nil, -1
}
func (h Controller) GroupEscape(data *GroupEscapeInboundInfo, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
if err := h.checkFightStatus(c); err != 0 {
return nil, err
}
if fightC, ok := c.FightC.(*fight.FightC); ok && fightC != nil && fightC.LegacyGroupProtocol {
fightC.SendLegacyEscapeSuccess(c, int(data.ActorIndex))
}
h.dispatchFightActionEnvelope(c, fight.NewEscapeActionEnvelope())
return nil, 0
}
// UseSkill 使用技能包
func (h Controller) UseSkill(data *UseSkillInInfo, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
if err := h.checkFightStatus(c); err != 0 {

View File

@@ -19,6 +19,7 @@ import (
const (
rewardItemExpPool = 3
groupBossSlotLimit = 3
)
// PlayerFightBoss 挑战地图boss
@@ -50,7 +51,7 @@ func (Controller) PlayerFightBoss(req *ChallengeBossInboundInfo, p *player.Playe
ai.AddBattleProp(0, 2)
var fightC *fight.FightC
fightC, err = fight.NewFight(p, ai, p.GetPetInfo(100), ai.GetPetInfo(0), func(foi model.FightOverInfo) {
fightC, err = startMapBossFight(mapNode, p, ai, func(foi model.FightOverInfo) {
if mapNode.WinBonusID == 0 {
return
}
@@ -65,6 +66,63 @@ func (Controller) PlayerFightBoss(req *ChallengeBossInboundInfo, p *player.Playe
return nil, -1
}
func startMapBossFight(
mapNode *configmodel.MapNode,
p *player.Player,
ai *player.AI_player,
fn func(model.FightOverInfo),
) (*fight.FightC, errorcode.ErrorCode) {
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)
}
}
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

@@ -19,6 +19,36 @@ type ReadyToFightInboundInfo struct {
Head common.TomeeHeader `cmd:"2404" struc:"skip"`
}
// GroupReadyFightFinishInboundInfo 旧组队协议准备完成。
type GroupReadyFightFinishInboundInfo struct {
Head common.TomeeHeader `cmd:"7556" struc:"skip"`
}
type GroupUseSkillInboundInfo struct {
Head common.TomeeHeader `cmd:"7558" struc:"skip"`
ActorIndex uint8
TargetSide uint8
TargetPos uint8
SkillId uint32
}
type GroupUseItemInboundInfo struct {
Head common.TomeeHeader `cmd:"7562" struc:"skip"`
ActorIndex uint8
ItemId uint32
}
type GroupChangePetInboundInfo struct {
Head common.TomeeHeader `cmd:"7563" struc:"skip"`
ActorIndex uint8
CatchTime uint32
}
type GroupEscapeInboundInfo struct {
Head common.TomeeHeader `cmd:"7565" struc:"skip"`
ActorIndex uint8
}
// EscapeFightInboundInfo 定义请求或响应数据结构。
type EscapeFightInboundInfo struct {
Head common.TomeeHeader `cmd:"2410" struc:"skip"`

View File

@@ -69,12 +69,6 @@ func (h Controller) GetPetLearnableSkills(
func (h Controller) SetPetSkill(data *ChangeSkillInfo, c *player.Player) (result *pet.ChangeSkillOutInfo, err errorcode.ErrorCode) {
const setSkillCost = 50
if !c.GetCoins(setSkillCost) {
return nil, errorcode.ErrorCodes.ErrSunDouInsufficient10016
}
c.Info.Coins -= setSkillCost
_, currentPet, ok := c.FindPet(data.CatchTime)
if !ok {
return nil, errorcode.ErrorCodes.ErrSystemBusy
@@ -93,28 +87,39 @@ func (h Controller) SetPetSkill(data *ChangeSkillInfo, c *player.Player) (result
return nil, errorcode.ErrorCodes.ErrSystemBusy
}
_, _, ok = utils.FindWithIndex(currentPet.SkillList, func(item model.SkillInfo) bool { //已经存在技能
_, _, ok = utils.FindWithIndex(currentPet.SkillList, func(item model.SkillInfo) bool {
return item.ID == data.ReplaceSkill
})
if ok {
return nil, errorcode.ErrorCodes.ErrSystemBusy
}
maxPP := uint32(skillInfo.MaxPP)
if data.HasSkill == 0 && len(currentPet.SkillList) >= 4 {
return nil, errorcode.ErrorCodes.ErrSystemBusy
}
if data.HasSkill != 0 {
// 查找要学习的技能并替换
_, targetSkill, found := utils.FindWithIndex(currentPet.SkillList, func(item model.SkillInfo) bool {
_, _, found := utils.FindWithIndex(currentPet.SkillList, func(item model.SkillInfo) bool {
return item.ID == data.HasSkill
})
if !found {
return nil, errorcode.ErrorCodes.ErrSystemBusy
}
}
if !c.GetCoins(setSkillCost) {
return nil, errorcode.ErrorCodes.ErrSunDouInsufficient10016
}
c.Info.Coins -= setSkillCost
maxPP := uint32(skillInfo.MaxPP)
if data.HasSkill != 0 {
_, targetSkill, _ := utils.FindWithIndex(currentPet.SkillList, func(item model.SkillInfo) bool {
return item.ID == data.HasSkill
})
targetSkill.ID = data.ReplaceSkill
targetSkill.PP = maxPP
} else {
if len(currentPet.SkillList) >= 4 {
return nil, errorcode.ErrorCodes.ErrSystemBusy
}
currentPet.SkillList = append(currentPet.SkillList, model.SkillInfo{
ID: data.ReplaceSkill,
PP: maxPP,
@@ -130,12 +135,6 @@ func (h Controller) SetPetSkill(data *ChangeSkillInfo, c *player.Player) (result
func (h Controller) SortPetSkills(data *C2S_Skill_Sort, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
const skillSortCost = 50
if !c.GetCoins(skillSortCost) {
return nil, errorcode.ErrorCodes.ErrSunDouInsufficient10016
}
c.Info.Coins -= skillSortCost
_, currentPet, ok := c.FindPet(data.CapTm)
if !ok {
return nil, errorcode.ErrorCodes.ErrPokemonNotExists
@@ -175,6 +174,12 @@ func (h Controller) SortPetSkills(data *C2S_Skill_Sort, c *player.Player) (resul
if len(newSkillList) > 4 {
newSkillList = newSkillList[:4]
}
if !c.GetCoins(skillSortCost) {
return nil, errorcode.ErrorCodes.ErrSunDouInsufficient10016
}
c.Info.Coins -= skillSortCost
currentPet.SkillList = newSkillList
return nil, 0

View File

@@ -147,6 +147,9 @@ func (f *FightC) Over(c common.PlayerI, res model.EnumBattleOverReason) {
if f.GetInputByPlayer(c, true) != nil {
f.WinnerId = f.GetInputByPlayer(c, true).UserID
}
f.FightOverInfo.Reason = f.Reason
f.FightOverInfo.WinnerId = f.WinnerId
f.closefight = true
close(f.quit)
@@ -281,6 +284,17 @@ func (f *FightC) UseItemAt(c common.PlayerI, cacthid, itemid uint32, actorIndex,
// ReadyFight 处理玩家战斗准备逻辑,当满足条件时启动战斗循环
func (f *FightC) ReadyFight(c common.PlayerI) {
if f.LegacyGroupProtocol {
input := f.GetInputByPlayer(c, false)
if input == nil {
return
}
input.Finished = true
if f.checkBothPlayersReady(c) {
f.startLegacyGroupBattle()
}
return
}
f.Broadcast(func(ff *input.Input) {
ff.Player.SendPackCmd(2404, &info.S2C_2404{UserID: c.GetInfo().UserID})
@@ -355,8 +369,23 @@ func (f *FightC) startBattle(startInfo info.FightStartOutboundInfo) {
go f.battleLoop()
// 向双方广播战斗开始信息
if f.LegacyGroupProtocol {
f.Broadcast(func(ff *input.Input) {
f.sendLegacyGroupStart(ff.Player)
})
} else {
f.Broadcast(func(ff *input.Input) {
ff.Player.SendPackCmd(2504, &startInfo)
})
}
})
}
func (f *FightC) startLegacyGroupBattle() {
f.startl.Do(func() {
go f.battleLoop()
f.Broadcast(func(ff *input.Input) {
f.sendLegacyGroupStart(ff.Player)
})
})
}

View File

@@ -10,7 +10,7 @@ type NewSel41 struct {
NewSel0
}
func (e *NewSel41) Skill_Use_ex() bool {
func (e *NewSel41) Skill_Use() bool {
if e.ID().GetCatchTime() != e.Ctx().Our.CurPet[0].Info.CatchTime {
return true
}

View File

@@ -416,6 +416,10 @@ func (f *FightC) enterturn(firstAttack, secondAttack *action.SelectSkillAction)
}
if f.LegacyGroupProtocol {
f.sendLegacyRoundBroadcast(firstAttack, secondAttack)
}
attackValueResult := f.buildNoteUseSkillOutboundInfo()
//因为切完才能广播,所以必须和回合结束分开结算
f.Broadcast(func(fighter *input.Input) {
@@ -446,6 +450,17 @@ func (f *FightC) enterturn(firstAttack, secondAttack *action.SelectSkillAction)
}
func (f *FightC) TURNOVER(cur *input.Input) {
var _hasBackup bool
if cur == nil {
return
}
for _, pet := range cur.BenchPets() {
if pet != nil && pet.Info.Hp > 0 {
_hasBackup = true
break
}
}
f.sendLegacySpriteDie(cur, _hasBackup)
f.Broadcast(func(ff *input.Input) {
@@ -459,6 +474,9 @@ func (f *FightC) TURNOVER(cur *input.Input) {
if f.IsWin(f.GetInputByPlayer(cur.Player, true)) { //然后检查是否战斗结束
f.FightOverInfo.WinnerId = f.GetInputByPlayer(cur.Player, true).UserID
f.FightOverInfo.Reason = model.BattleOverReason.DefaultEnd
f.WinnerId = f.FightOverInfo.WinnerId
f.Reason = model.BattleOverReason.DefaultEnd
f.closefight = true
// break

View File

@@ -0,0 +1,428 @@
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"
"bytes"
"encoding/binary"
)
const (
groupCmdReadyToFight uint32 = 7555
groupCmdReadyFightFinish uint32 = 7556
groupCmdStartFight uint32 = 7557
groupCmdUseSkill uint32 = 7558
groupCmdSkillHurt uint32 = 7559
groupCmdFightOver uint32 = 7560
groupCmdSpriteDie uint32 = 7561
groupCmdUseItem uint32 = 7562
groupCmdChangePet uint32 = 7563
groupCmdEscape uint32 = 7565
groupCmdBoutDone uint32 = 7566
groupCmdChangePetSuc uint32 = 7567
groupCmdEscapeSuc uint32 = 7568
groupCmdChat uint32 = 7569
groupCmdLoadPercent uint32 = 7571
groupCmdLoadPercentNotice uint32 = 7572
groupCmdSpriteNotice uint32 = 7573
groupCmdFightWinClose uint32 = 7574
groupCmdFightOvertime uint32 = 7585
groupCmdSkillPlayOver uint32 = 7586
groupCmdFightTimeoutExit uint32 = 7587
groupCmdFightRelation uint32 = 7588
groupModelNPC uint32 = 3
groupModelBoss uint32 = 4
groupModelPlayerSingle uint32 = 5
groupModelPlayerMulti uint32 = 6
)
func groupModelByFight(f *FightC) uint32 {
if f == nil {
return groupModelBoss
}
switch {
case f.Info.Status == 0:
return groupModelNPC
case f.Info.Status == 1 && f.Info.Mode == 1:
return groupModelPlayerSingle
case f.Info.Status == 1:
return groupModelPlayerMulti
default:
return groupModelBoss
}
}
func writeUint8(buf *bytes.Buffer, v uint8) {
_ = buf.WriteByte(v)
}
func writeUint32(buf *bytes.Buffer, v uint32) {
_ = binary.Write(buf, binary.BigEndian, v)
}
func writeInt32(buf *bytes.Buffer, v int32) {
_ = binary.Write(buf, binary.BigEndian, v)
}
func writeFixedString16(buf *bytes.Buffer, s string) {
raw := make([]byte, 16)
copy(raw, []byte(s))
_, _ = buf.Write(raw)
}
func buildPacket(cmd uint32, userID uint32, payload []byte) []byte {
header := common.NewTomeeHeader(cmd, userID)
totalLen := uint32(17 + len(payload))
header.Len = totalLen
buf := make([]byte, totalLen)
binary.BigEndian.PutUint32(buf[0:4], totalLen)
buf[4] = header.Version
binary.BigEndian.PutUint32(buf[5:9], cmd)
binary.BigEndian.PutUint32(buf[9:13], userID)
binary.BigEndian.PutUint32(buf[13:17], 0)
copy(buf[17:], payload)
return buf
}
func (f *FightC) sendLegacyGroupReady() {
f.Broadcast(func(ff *input.Input) {
if ff == nil || ff.Player == nil {
return
}
sendLegacyPacket(ff.Player, groupCmdReadyToFight, f.buildLegacyGroupReadyPayload())
})
}
func (f *FightC) sendLegacyGroupStart(player common.PlayerI) {
if player == nil {
return
}
sendLegacyPacket(player, groupCmdStartFight, f.buildLegacyGroupStartPayload())
}
func (f *FightC) sendLegacyGroupOver(player common.PlayerI, over *model.FightOverInfo) {
if player == nil {
return
}
sendLegacyPacket(player, groupCmdFightOver, f.buildLegacyGroupOverPayload(over))
}
func sendLegacyPacket(player common.PlayerI, cmd uint32, payload []byte) {
if player == nil {
return
}
if sender, ok := player.(interface{ SendPack([]byte) error }); ok {
_ = sender.SendPack(buildPacket(cmd, player.GetInfo().UserID, payload))
}
}
func (f *FightC) buildLegacyGroupReadyPayload() []byte {
var (
buf bytes.Buffer
users [][]*input.Input
)
writeUint32(&buf, groupModelByFight(f))
users = [][]*input.Input{f.Our, f.Opp}
for sideIndex, sideInputs := range users {
writeUint8(&buf, 1)
var leaderID uint32
if len(sideInputs) > 0 && sideInputs[0] != nil && sideInputs[0].Player != nil {
leaderID = sideInputs[0].Player.GetInfo().UserID
}
writeUint32(&buf, leaderID)
writeUint8(&buf, 1)
if leaderID == 0 {
writeUint32(&buf, 0)
writeFixedString16(&buf, "boss")
} else {
writeUint32(&buf, leaderID)
writeFixedString16(&buf, sideInputs[0].Player.GetInfo().Nick)
}
writeUint32(&buf, uint32(len(sideInputs)))
for _, slot := range sideInputs {
if slot == nil || slot.CurrentPet() == nil {
continue
}
currentPet := slot.CurrentPet()
writeUint32(&buf, currentPet.Info.ID)
writeUint32(&buf, uint32(len(currentPet.Info.SkillList)))
for _, skill := range currentPet.Info.SkillList {
writeUint32(&buf, skill.ID)
}
}
if sideIndex == 0 && len(sideInputs) == 0 {
writeUint32(&buf, 0)
writeFixedString16(&buf, "")
writeUint32(&buf, 0)
}
}
return buf.Bytes()
}
func (f *FightC) buildLegacyGroupStartPayload() []byte {
var (
buf bytes.Buffer
sides [][]*input.Input
)
writeUint8(&buf, 0)
sides = [][]*input.Input{f.Our, f.Opp}
for sideIndex, sideInputs := range sides {
writeUint8(&buf, uint8(len(sideInputs)))
for pos, slot := range sideInputs {
if slot == nil || slot.CurrentPet() == nil {
continue
}
currentPet := slot.CurrentPet()
writeUint8(&buf, uint8(sideIndex+1))
writeUint8(&buf, uint8(pos))
if slot.Player != nil {
writeUint32(&buf, slot.Player.GetInfo().UserID)
} else {
writeUint32(&buf, 0)
}
writeUint8(&buf, 0)
writeUint32(&buf, currentPet.Info.ID)
writeUint32(&buf, currentPet.Info.CatchTime)
writeUint32(&buf, currentPet.Info.Hp)
writeUint32(&buf, currentPet.Info.MaxHp)
writeUint32(&buf, currentPet.Info.Level)
writeUint32(&buf, 0)
writeUint32(&buf, 1)
}
}
return buf.Bytes()
}
func (f *FightC) buildLegacyGroupOverPayload(over *model.FightOverInfo) []byte {
var (
buf bytes.Buffer
winnerID uint32
endReason uint32
playerInfo *model.PlayerInfo
)
if over != nil {
winnerID = over.WinnerId
endReason = uint32(over.Reason)
}
if our := f.primaryOurPlayer(); our != nil {
playerInfo = our.GetInfo()
}
writeUint8(&buf, 0)
writeUint32(&buf, endReason)
writeUint32(&buf, winnerID)
writeUint32(&buf, 0)
if playerInfo != nil {
writeUint32(&buf, uint32(playerInfo.TwoTimes))
writeUint32(&buf, uint32(playerInfo.ThreeTimes))
writeUint32(&buf, playerInfo.AutoFightTime)
writeUint32(&buf, 0)
writeUint32(&buf, uint32(playerInfo.EnergyTime))
writeUint32(&buf, playerInfo.LearnTimes)
} else {
for i := 0; i < 6; i++ {
writeUint32(&buf, 0)
}
}
return buf.Bytes()
}
func (f *FightC) SendLegacyEscapeSuccess(player common.PlayerI, actorIndex int) {
if f == nil || !f.LegacyGroupProtocol || player == nil {
return
}
payload := f.buildLegacyEscapePayload(player, actorIndex)
f.Broadcast(func(ff *input.Input) {
if ff == nil || ff.Player == nil {
return
}
sendLegacyPacket(ff.Player, groupCmdEscapeSuc, payload)
})
}
func (f *FightC) buildLegacyEscapePayload(player common.PlayerI, actorIndex int) []byte {
var buf bytes.Buffer
side := uint8(1)
if !f.isOurPlayerID(player.GetInfo().UserID) {
side = 2
}
writeUint32(&buf, player.GetInfo().UserID)
writeFixedString16(&buf, player.GetInfo().Nick)
writeUint8(&buf, side)
writeUint8(&buf, uint8(actorIndex))
return buf.Bytes()
}
func (f *FightC) sendLegacyRoundBroadcast(firstAttack, secondAttack *action.SelectSkillAction) {
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) {
var payload []byte
if skillAction == nil {
return
}
payload = f.buildLegacyGroupSkillHurtPayload(skillAction)
if len(payload) == 0 {
return
}
f.Broadcast(func(ff *input.Input) {
if ff == nil || ff.Player == nil {
return
}
sendLegacyPacket(ff.Player, groupCmdSkillHurt, payload)
})
}
func (f *FightC) buildLegacyGroupSkillHurtPayload(skillAction *action.SelectSkillAction) []byte {
var buf bytes.Buffer
attacker := f.GetInputByAction(skillAction, false)
defender := f.GetInputByAction(skillAction, true)
if attacker == nil || defender == nil || attacker.AttackValue == nil || defender.AttackValue == nil {
return nil
}
writeUint8(&buf, 0)
f.writeLegacySkillHurtInfo(&buf, skillAction, attacker, defender, true)
f.writeLegacySkillHurtInfo(&buf, skillAction, defender, attacker, false)
return buf.Bytes()
}
func (f *FightC) writeLegacySkillHurtInfo(buf *bytes.Buffer, skillAction *action.SelectSkillAction, self *input.Input, opponent *input.Input, isAttacker bool) {
var (
moveID uint32
attackVal *model.AttackValue
currentPet *info.BattlePetEntity
side uint8
pos uint8
)
if self == nil || buf == nil {
return
}
if self.AttackValue == nil {
attackVal = info.NewAttackValue(self.UserID)
} else {
attackVal = self.AttackValue
}
currentPet = self.CurrentPet()
side = 1
if !f.isOurPlayerID(self.UserID) {
side = 2
}
pos = uint8(self.TeamSlotIndex())
moveID = attackVal.SkillID
if isAttacker {
if skillAction != nil && skillAction.SkillEntity != nil {
moveID = uint32(skillAction.SkillEntity.XML.ID)
}
writeUint8(buf, 0)
} else {
writeUint8(buf, 1)
moveID = 0
}
writeUint8(buf, side)
writeUint8(buf, pos)
writeUint32(buf, self.UserID)
for i := 0; i < 20; i++ {
writeUint8(buf, uint8(attackVal.Status[i]))
}
writeUint8(buf, 0)
writeUint8(buf, 0)
for i := 0; i < 6; i++ {
writeUint8(buf, uint8(attackVal.Prop[i]))
}
if currentPet != nil {
writeUint32(buf, currentPet.Info.ID)
} else {
writeUint32(buf, 0)
}
writeUint32(buf, moveID)
if currentPet != nil {
writeUint32(buf, currentPet.Info.Hp)
writeUint32(buf, currentPet.Info.MaxHp)
writeUint32(buf, uint32(len(currentPet.Info.SkillList)))
for _, skill := range currentPet.Info.SkillList {
writeUint32(buf, skill.ID)
writeUint32(buf, skill.PP)
}
} else {
writeUint32(buf, uint32(maxInt32(attackVal.RemainHp)))
writeUint32(buf, attackVal.MaxHp)
writeUint32(buf, uint32(len(attackVal.SkillList)))
for _, skill := range attackVal.SkillList {
writeUint32(buf, skill.ID)
writeUint32(buf, skill.PP)
}
}
writeUint32(buf, attackVal.State)
if isAttacker {
writeUint32(buf, attackVal.IsCritical)
writeUint32(buf, attackVal.State)
writeUint32(buf, 1)
writeInt32(buf, int32(attackVal.LostHp))
writeInt32(buf, attackVal.GainHp)
}
writeUint32(buf, 0)
}
func (f *FightC) sendLegacyGroupBoutDone() {
var buf bytes.Buffer
if f == nil || !f.LegacyGroupProtocol {
return
}
writeUint32(&buf, f.Round)
f.Broadcast(func(ff *input.Input) {
if ff == nil || ff.Player == nil {
return
}
sendLegacyPacket(ff.Player, groupCmdBoutDone, buf.Bytes())
})
}
func (f *FightC) sendLegacySpriteDie(in *input.Input, hasBackup bool) {
var (
buf bytes.Buffer
side uint8
data uint32
)
if f == nil || !f.LegacyGroupProtocol || in == nil {
return
}
side = 1
if !f.isOurPlayerID(in.UserID) {
side = 2
}
if hasBackup {
data = 1
}
writeUint8(&buf, 1)
writeUint8(&buf, side)
writeUint8(&buf, uint8(in.TeamSlotIndex()))
writeUint8(&buf, 1)
writeUint32(&buf, data)
f.Broadcast(func(ff *input.Input) {
if ff == nil || ff.Player == nil {
return
}
sendLegacyPacket(ff.Player, groupCmdSpriteDie, buf.Bytes())
})
}
func maxInt32(v int32) int32 {
if v < 0 {
return 0
}
return v
}

View File

@@ -10,11 +10,23 @@ type PlayerCaptureContext struct {
Guarantees map[int]int // 按分母分组的保底分子 map[denominator]numerator
}
func NewPlayerCaptureContext() *PlayerCaptureContext {
return &PlayerCaptureContext{
Denominator: 1000,
DecayFactor: 0.10,
MinDecayNum: 1,
Guarantees: make(map[int]int),
}
}
// Roll 通用概率判定(带共享保底) 返回成功,基础,保底
func (c *PlayerCaptureContext) Roll(numerator, denominator int) (bool, float64, float64) {
if denominator <= 0 {
return false, 0, 0
}
if c == nil {
c = NewPlayerCaptureContext()
}
base := float64(numerator) / float64(denominator)
bonusNumerator := c.getGuaranteeNumerator(denominator)
@@ -41,14 +53,29 @@ func (c *PlayerCaptureContext) Roll(numerator, denominator int) (bool, float64,
// 保底操作
func (c *PlayerCaptureContext) getGuaranteeNumerator(denominator int) int {
if c == nil || c.Guarantees == nil {
return 0
}
if num, ok := c.Guarantees[denominator]; ok {
return num
}
return 0
}
func (c *PlayerCaptureContext) increaseGuarantee(denominator int) {
if c == nil {
return
}
if c.Guarantees == nil {
c.Guarantees = make(map[int]int)
}
c.Guarantees[denominator]++
}
func (c *PlayerCaptureContext) resetGuarantee(denominator int) {
if c == nil {
return
}
if c.Guarantees == nil {
c.Guarantees = make(map[int]int)
}
c.Guarantees[denominator] = 0
}

View File

@@ -25,6 +25,7 @@ type FightC struct {
info.FightStartOutboundInfo
Info info.Fightinfo
IsReady bool
LegacyGroupProtocol bool
ownerID uint32 // 战斗发起者ID
Our []*input.Input // 我方战斗位
Opp []*input.Input // 敌方战斗位
@@ -227,14 +228,44 @@ func (f *FightC) getInputByController(userID uint32, isOpposite bool) *input.Inp
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 {
var bench []*info.BattlePetEntity
if in == nil {
return false
}
if current := in.CurrentPet(); current != nil && current.Info.Hp > 0 {
return true
}
bench = in.BenchPets()
for _, pet := range bench {
if pet != nil && pet.Info.Hp > 0 {
return true
}
}
return false
}
func (f *FightC) setActionAttackValue(act action.BattleActionI) {
if act == nil {
return
@@ -329,8 +360,16 @@ func (f *FightC) GetOpp(c common.PlayerI) *input.Input {
// // 获取随机数
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 {

View File

@@ -41,6 +41,10 @@ func getItemBonus(itemID uint32) float64 {
// -1是保底模式0是锁定模式》0是衰减模式
// Capture 执行捕捉 ,捕捉精灵,使用的道具,模式
func (our *Input) Capture(pet *info.BattlePetEntity, ItemID uint32, ownerpet int) (bool, CaptureDetails) {
captureCtx := our.Player.GetPlayerCaptureContext()
if captureCtx == nil {
captureCtx = info.NewPlayerCaptureContext()
}
if getItemBonus(ItemID) >= 255 {
return true, CaptureDetails{
@@ -69,12 +73,12 @@ func (our *Input) Capture(pet *info.BattlePetEntity, ItemID uint32, ownerpet int
// 计算基础捕捉率
baseRate := our.calcBaseRate(pet, ItemID)
denominator := our.Player.GetPlayerCaptureContext().Denominator
denominator := captureCtx.Denominator
numerator := int(baseRate * float64(denominator))
// 衰减模式
if ownerpet > 0 {
decay := math.Pow(1-our.Player.GetPlayerCaptureContext().DecayFactor, float64(ownerpet))
decay := math.Pow(1-captureCtx.DecayFactor, float64(ownerpet))
baseRate *= decay
if baseRate < 0.01 {
baseRate = 0.01 // 最低1%成功率
@@ -83,7 +87,7 @@ func (our *Input) Capture(pet *info.BattlePetEntity, ItemID uint32, ownerpet int
}
// 走统一保底判定
success, basePct, bonusPct := our.Player.Roll(numerator, denominator)
success, basePct, bonusPct := captureCtx.Roll(numerator, denominator)
return success, CaptureDetails{
Success: success,
@@ -98,8 +102,12 @@ func (our *Input) Capture(pet *info.BattlePetEntity, ItemID uint32, ownerpet int
// calcBaseA 按公式计算a值
func (our *Input) calcBaseA(pet *info.BattlePetEntity, ItemID uint32) int {
captureCtx := our.Player.GetPlayerCaptureContext()
if captureCtx == nil {
captureCtx = info.NewPlayerCaptureContext()
}
catchRate := gconv.Int(pet.CatchRate)
catchRate = (catchRate * our.Player.GetPlayerCaptureContext().Denominator) / 1000 // 归一化到1000分母
catchRate = (catchRate * captureCtx.Denominator) / 1000 // 归一化到1000分母
if catchRate < 3 {
catchRate = 3
}

View File

@@ -19,11 +19,15 @@ func Shuffle[T any](slice []T) {
}
func (our *Input) GetAction() {
actorIndex := our.TeamSlotIndex()
targetIndex := our.RandomOpponentSlotIndex()
target := our.OpponentSlotAt(targetIndex)
next := our.Exec(func(t Effect) bool {
return t.HookAction()
})
scriptCtx := buildBossHookActionContext(our, next)
scriptCtx := buildBossHookActionContext(our, target, next)
if aiPlayer, ok := our.Player.(*player.AI_player); ok && aiPlayer.BossScript != "" {
scriptBoss := &configmodel.BossConfig{Script: aiPlayer.BossScript}
nextByScript, err := scriptBoss.RunHookActionScript(scriptCtx)
@@ -37,18 +41,18 @@ func (our *Input) GetAction() {
return
}
if applyBossScriptAction(our, scriptCtx) {
if applyBossScriptAction(our, scriptCtx, actorIndex, targetIndex) {
return
}
selfPet := our.FightC.GetCurrPET(our.Player)
selfPet := our.FightC.GetCurrPETAt(our.Player, actorIndex)
if selfPet == nil {
return
}
if selfPet.Info.Hp <= 0 {
for _, v := range our.AllPet {
if v.Info.Hp > 0 {
our.FightC.ChangePet(our.Player, v.Info.CatchTime)
our.FightC.ChangePetAt(our.Player, v.Info.CatchTime, actorIndex)
return
}
}
@@ -68,9 +72,12 @@ func (our *Input) GetAction() {
if !s.CanUse() {
continue
}
s.DamageValue = our.CalculatePower(our.Opp, s)
if target == nil {
continue
}
s.DamageValue = our.CalculatePower(target, s)
oppPet := our.Opp.CurrentPet()
oppPet := target.CurrentPet()
if oppPet == nil {
continue
}
@@ -99,13 +106,13 @@ func (our *Input) GetAction() {
}
if usedskill != nil {
our.FightC.UseSkill(our.Player, uint32(usedskill.XML.ID))
our.FightC.UseSkillAt(our.Player, uint32(usedskill.XML.ID), actorIndex, targetIndex)
} else {
our.FightC.UseSkill(our.Player, 0)
our.FightC.UseSkillAt(our.Player, 0, actorIndex, targetIndex)
}
}
func buildBossHookActionContext(our *Input, hookAction bool) *configmodel.BossHookActionContext {
func buildBossHookActionContext(our, opponent *Input, hookAction bool) *configmodel.BossHookActionContext {
ctx := &configmodel.BossHookActionContext{
HookAction: hookAction,
Action: "auto",
@@ -150,8 +157,8 @@ func buildBossHookActionContext(our *Input, hookAction bool) *configmodel.BossHo
if our.AttackValue != nil {
ctx.OurAttack = convertAttackValue(our.AttackValue)
}
if our.Opp != nil {
if oppPet := our.Opp.CurrentPet(); oppPet != nil {
if opponent != nil {
if oppPet := opponent.CurrentPet(); oppPet != nil {
ctx.Opp = &configmodel.BossHookPetContext{
PetID: oppPet.Info.ID,
CatchTime: oppPet.Info.CatchTime,
@@ -159,8 +166,8 @@ func buildBossHookActionContext(our *Input, hookAction bool) *configmodel.BossHo
MaxHp: oppPet.Info.MaxHp,
}
}
if our.Opp.AttackValue != nil {
ctx.OppAttack = convertAttackValue(our.Opp.AttackValue)
if opponent.AttackValue != nil {
ctx.OppAttack = convertAttackValue(opponent.AttackValue)
}
}
@@ -195,7 +202,7 @@ func convertAttackValue(src *playermodel.AttackValue) *configmodel.BossHookAttac
}
}
func applyBossScriptAction(our *Input, ctx *configmodel.BossHookActionContext) bool {
func applyBossScriptAction(our *Input, ctx *configmodel.BossHookActionContext, actorIndex, targetIndex int) bool {
if our == nil || ctx == nil {
return false
}
@@ -204,10 +211,10 @@ func applyBossScriptAction(our *Input, ctx *configmodel.BossHookActionContext) b
case "", "auto":
return false
case "skill", "use_skill", "useskill":
our.FightC.UseSkill(our.Player, ctx.SkillID)
our.FightC.UseSkillAt(our.Player, ctx.SkillID, actorIndex, targetIndex)
return true
case "switch", "change_pet", "changepet":
our.FightC.ChangePet(our.Player, ctx.CatchTime)
our.FightC.ChangePetAt(our.Player, ctx.CatchTime, actorIndex)
return true
default:
return false

View File

@@ -77,15 +77,27 @@ func (our *Input) Heal(in *Input, ac action.BattleActionI, value alpacadecimal.D
if healValue >= 0 {
currentPet.Info.ModelHP(int64(healValue))
if our.AttackValue != nil {
our.AttackValue.RemainHp = int32(currentPet.Info.Hp)
our.AttackValue.MaxHp = currentPet.Info.MaxHp
}
return
}
damage := uint32(-healValue)
if damage >= currentPet.Info.Hp {
currentPet.Info.Hp = 0
if our.AttackValue != nil {
our.AttackValue.RemainHp = 0
our.AttackValue.MaxHp = currentPet.Info.MaxHp
}
return
}
currentPet.Info.Hp -= damage
if our.AttackValue != nil {
our.AttackValue.RemainHp = int32(currentPet.Info.Hp)
our.AttackValue.MaxHp = currentPet.Info.MaxHp
}
}
func (our *Input) HealPP(value int) {
@@ -227,6 +239,10 @@ func (our *Input) Damage(in *Input, sub *info.DamageZone) {
} else {
currentPet.Info.Hp = currentPet.Info.Hp - uint32(sub.Damage.IntPart())
}
if our.AttackValue != nil {
our.AttackValue.RemainHp = int32(currentPet.Info.Hp)
our.AttackValue.MaxHp = currentPet.Info.MaxHp
}
//todo 待实现死亡effet

View File

@@ -1,5 +1,7 @@
package input
import "github.com/gogf/gf/v2/util/grand"
// TeamSlots 返回当前输入所在阵营的全部有效战斗位视图。
func (our *Input) TeamSlots() []*Input {
if our == nil {
@@ -18,6 +20,19 @@ func (our *Input) TeamSlots() []*Input {
return slots
}
// TeamSlotIndex 返回当前输入在本阵营中的原始站位下标。
func (our *Input) TeamSlotIndex() int {
if our == nil || len(our.Team) == 0 {
return 0
}
for idx, teammate := range our.Team {
if teammate == our {
return idx
}
}
return 0
}
// Teammates 返回队友列表,不包含自己。
func (our *Input) Teammates() []*Input {
if our == nil {
@@ -53,3 +68,55 @@ func (our *Input) LivingTeammates() []*Input {
func (our *Input) HasLivingTeammate() bool {
return len(our.LivingTeammates()) > 0
}
// OpponentSlotAt 返回指定敌方站位。
func (our *Input) OpponentSlotAt(index int) *Input {
if our == nil {
return nil
}
if index >= 0 && index < len(our.OppTeam) {
return our.OppTeam[index]
}
if index == 0 {
return our.Opp
}
return nil
}
// RandomOpponentSlotIndex 返回一个可用的敌方站位下标,优先从存活站位中随机。
func (our *Input) RandomOpponentSlotIndex() int {
if our == nil {
return 0
}
if len(our.OppTeam) == 0 {
if our.Opp != nil {
return 0
}
return 0
}
living := make([]int, 0, len(our.OppTeam))
available := make([]int, 0, len(our.OppTeam))
for idx, opponent := range our.OppTeam {
if opponent == nil {
continue
}
available = append(available, idx)
current := opponent.CurrentPet()
if current != nil && current.Info.Hp > 0 {
living = append(living, idx)
}
}
candidates := living
if len(candidates) == 0 {
candidates = available
}
if len(candidates) == 0 {
return 0
}
if len(candidates) == 1 {
return candidates[0]
}
return candidates[grand.Intn(len(candidates))]
}

View File

@@ -28,3 +28,33 @@ func TestLivingTeammatesFiltersSelfAndDeadSlots(t *testing.T) {
t.Fatalf("expected owner to detect living teammate")
}
}
func TestTeamSlotIndexKeepsOriginalSlot(t *testing.T) {
first := &Input{}
second := &Input{}
third := &Input{}
team := []*Input{first, second, third}
first.Team = team
second.Team = team
third.Team = team
if got := third.TeamSlotIndex(); got != 2 {
t.Fatalf("expected slot index 2, got %d", got)
}
}
func TestRandomOpponentSlotIndexPrefersLivingTarget(t *testing.T) {
owner := &Input{}
deadOpp := &Input{CurPet: []*fightinfo.BattlePetEntity{{Info: model.PetInfo{Hp: 0}}}}
liveOpp := &Input{CurPet: []*fightinfo.BattlePetEntity{{Info: model.PetInfo{Hp: 12}}}}
owner.OppTeam = []*Input{deadOpp, liveOpp}
if got := owner.RandomOpponentSlotIndex(); got != 1 {
t.Fatalf("expected living opponent slot 1, got %d", got)
}
if got := owner.OpponentSlotAt(1); got != liveOpp {
t.Fatalf("expected opponent slot 1 to return live opponent")
}
}

View File

@@ -7,6 +7,7 @@ import (
"blazing/cool"
"blazing/modules/player/model"
"context"
"runtime/debug"
"sort"
"sync/atomic"
@@ -71,6 +72,27 @@ func (f *FightC) battleLoop() {
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 {
@@ -152,8 +174,11 @@ func (f *FightC) battleLoop() {
//大乱斗,给个延迟
//<-time.After(1000)
f.Broadcast(func(ff *input.Input) {
if f.LegacyGroupProtocol {
f.sendLegacyGroupOver(ff.Player, &f.FightOverInfo)
} else {
ff.Player.SendPackCmd(2506, &f.FightOverInfo)
}
ff.Player.QuitFight()
@@ -179,7 +204,7 @@ func (f *FightC) collectPlayerActions(expectedSlots map[actionSlotKey]struct{})
defer f.closeActionWindow()
if f.Info.Status == info.BattleMode.FIGHT_WITH_NPC {
go f.Opp[0].GetAction()
f.triggerNPCActions()
}
waitr := time.Duration(f.waittime)*time.Millisecond*10 + 30*time.Second
@@ -262,7 +287,7 @@ func (f *FightC) collectPlayerActions(expectedSlots map[actionSlotKey]struct{})
f.Switch = make(map[actionSlotKey]*action.ActiveSwitchAction)
f.Our[0].Player.SendPackCmd(2407, &ret.Reason)
//println("AI出手死切")
go f.Opp[0].GetAction() //boss出手后获取出招
f.triggerNPCActions() // boss出手后获取出招
}
continue
@@ -311,7 +336,11 @@ func (f *FightC) handleTimeout(expectedSlots map[actionSlotKey]struct{}, actions
}
player := f.getPlayerByID(key.PlayerID)
if player != nil {
f.UseSkillAt(player, 0, key.ActorIndex, 0)
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
@@ -353,6 +382,22 @@ func (f *FightC) handleTimeout(expectedSlots map[actionSlotKey]struct{}, actions
}
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 {
@@ -599,7 +644,7 @@ func (f *FightC) handleSkillActions(a1, a2 action.BattleActionI) {
switch {
case s1 == nil || s1.SkillEntity == nil:
if s2.SkillEntity != nil {
if s2 != nil && s2.SkillEntity != nil {
if s2.XML.CD != nil {
f.waittime = *s2.XML.CD
}
@@ -608,7 +653,7 @@ func (f *FightC) handleSkillActions(a1, a2 action.BattleActionI) {
f.enterturn(s2, nil)
// fmt.Println("1 空过 2玩家执行技能:", s2.PlayerID, s2.Info.ID)
case s2 == nil || s2.SkillEntity == nil:
if s1.SkillEntity != nil {
if s1 != nil && s1.SkillEntity != nil {
if s1.XML.CD != nil {
f.waittime = *s1.XML.CD
}

View File

@@ -47,6 +47,41 @@ func NewFightSingleControllerN(
)
}
// 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),
)
}
// NewFightPerSlotControllerN 创建 N 打战斗(多人各控制一个站位)。
// ourPlayers/oppPlayers 与 ourPetsBySlot/oppPetsBySlot 按站位一一对应。
func NewFightPerSlotControllerN(
@@ -116,6 +151,7 @@ func buildFight(opts *fightBuildOptions) (*FightC, errorcode.ErrorCode) {
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)
@@ -160,9 +196,13 @@ func buildFight(opts *fightBuildOptions) (*FightC, errorcode.ErrorCode) {
}
f.FightStartOutboundInfo = f.buildFightStartInfo()
if f.LegacyGroupProtocol {
f.sendLegacyGroupReady()
} else {
f.Broadcast(func(ff *input.Input) {
ff.Player.SendPackCmd(2503, &f.ReadyInfo)
})
}
cool.Cron.AfterFunc(loadtime, func() {
our := f.primaryOur()
@@ -180,7 +220,11 @@ func buildFight(opts *fightBuildOptions) (*FightC, errorcode.ErrorCode) {
f.WinnerId = opp.Player.GetInfo().UserID
}
f.Broadcast(func(ff *input.Input) {
if f.LegacyGroupProtocol {
f.sendLegacyGroupOver(ff.Player, &f.FightOverInfo)
} else {
ff.Player.SendPackCmd(2506, &f.FightOverInfo)
}
ff.Player.QuitFight()
})
}

View File

@@ -35,6 +35,7 @@ type fightBuildOptions struct {
fightInfo *info.Fightinfo
controllerBinding int
legacyGroupProtocol bool
}
// defaultFightBuildOptions 返回建战默认参数。
@@ -89,6 +90,13 @@ func WithInputControllerBinding(mode int) FightOption {
}
}
// WithLegacyGroupProtocol 设置是否使用旧组队协议 CMD。
func WithLegacyGroupProtocol(enabled bool) FightOption {
return func(opts *fightBuildOptions) {
opts.legacyGroupProtocol = enabled
}
}
func NewFightWithOptions(opts ...FightOption) (*FightC, errorcode.ErrorCode) {
buildOpts := defaultFightBuildOptions()
for _, opt := range opts {

View File

@@ -7,6 +7,8 @@ import (
"blazing/logic/service/fight/info"
spaceinfo "blazing/logic/service/space/info"
"blazing/modules/player/model"
"github.com/gogf/gf/v2/util/grand"
)
type baseplayer struct {
@@ -85,6 +87,21 @@ func (f *baseplayer) GetPlayerCaptureContext() *info.PlayerCaptureContext {
return f.PlayerCaptureContext
}
func (f *baseplayer) Roll(numerator, denominator int) (bool, float64, float64) {
if denominator <= 0 {
return false, 0, 0
}
if numerator < 0 {
numerator = 0
}
if numerator > denominator {
numerator = denominator
}
base := float64(numerator) / float64(denominator) * 100
return grand.Intn(denominator) < numerator, base, 0
}
// FindPet 根据捕捉时间查找宠物
// 返回值: (索引, 宠物信息, 是否找到)
func (f *baseplayer) FindPet(catchTime uint32) (int, *model.PetInfo, bool) {

View File

@@ -31,6 +31,8 @@ type MapNode struct {
IsBroadcast uint32 `gorm:"type:int;default:0;comment:'广播模型ID(0表示不广播)'" json:"is_broadcast"`
IsGroupBoss uint32 `gorm:"type:int;default:0;comment:'是否为组队Boss(0否1是)'" json:"is_group_boss" description:"是否为组队Boss"`
TriggerPlotID uint32 `gorm:"default:0;comment:'触发剧情ID0表示无剧情'" json:"trigger_plot_id" description:"触发剧情ID"`
BossIds []uint32 `gorm:"type:jsonb;comment:'塔层BOSS ID列表'" json:"boss_ids"`