From 0051ac0be83ae02527111aa4038fcc849578f661 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=98=94=E5=BF=B5?= <12574910+72wo@users.noreply.github.com> Date: Wed, 8 Apr 2026 01:28:55 +0800 Subject: [PATCH] =?UTF-8?q?```=20feat(fight):=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E6=97=A7=E7=BB=84=E9=98=9F=E5=8D=8F=E8=AE=AE=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E5=B9=B6=E4=BC=98=E5=8C=96=E6=88=98=E6=96=97=E7=B3=BB=E7=BB=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 实现了旧组队协议相关功能,包括GroupReadyFightFinish、GroupUseSkill、 GroupUseItem、GroupChangePet和GroupEscape方法 - 新增组队战斗相关的入站信息结构体定义 - 实现了组队BOSS战斗逻辑,添加groupBossSlotLimit常量 - 重构宠物技能设置逻辑,调整金币消耗时机 - 优化战斗循环逻辑,添加对无行动槽位的处理 - 改进AI行动逻辑,增加多位置目标选择 --- .vscode/launch.json | 2 +- logic/controller/fight_base.go | 48 +++ logic/controller/fight_boss野怪和地图怪.go | 62 ++- logic/controller/inbound_fight.go | 30 ++ logic/controller/pet_skill.go | 43 ++- logic/service/fight/action.go | 31 +- logic/service/fight/boss/NewSeIdx_41.go | 2 +- logic/service/fight/fightc.go | 18 + logic/service/fight/group_legacy.go | 428 +++++++++++++++++++++ logic/service/fight/info/ctx.go | 27 ++ logic/service/fight/input.go | 63 ++- logic/service/fight/input/Capture.go | 16 +- logic/service/fight/input/ai.go | 39 +- logic/service/fight/input/fight.go | 16 + logic/service/fight/input/team.go | 67 ++++ logic/service/fight/input/team_test.go | 30 ++ logic/service/fight/loop.go | 59 ++- logic/service/fight/new.go | 52 ++- logic/service/fight/new_options.go | 8 + logic/service/player/base.go | 17 + modules/config/model/map_node.go | 2 + 21 files changed, 993 insertions(+), 67 deletions(-) create mode 100644 logic/service/fight/group_legacy.go diff --git a/.vscode/launch.json b/.vscode/launch.json index 9b02d30d4..ab0e44cb6 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -29,7 +29,7 @@ "request": "launch", "mode": "auto", "cwd": "${workspaceFolder}", - "args": ["-id=2"], + "args": ["-id=99"], "program": "${workspaceFolder}/logic" } diff --git a/logic/controller/fight_base.go b/logic/controller/fight_base.go index b9278bbaf..6d793bf7d 100644 --- a/logic/controller/fight_base.go +++ b/logic/controller/fight_base.go @@ -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 { diff --git a/logic/controller/fight_boss野怪和地图怪.go b/logic/controller/fight_boss野怪和地图怪.go index ec519c69c..424d91f00 100644 --- a/logic/controller/fight_boss野怪和地图怪.go +++ b/logic/controller/fight_boss野怪和地图怪.go @@ -18,7 +18,8 @@ import ( ) const ( - rewardItemExpPool = 3 + 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 { diff --git a/logic/controller/inbound_fight.go b/logic/controller/inbound_fight.go index f36472cbe..11b220d24 100644 --- a/logic/controller/inbound_fight.go +++ b/logic/controller/inbound_fight.go @@ -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"` diff --git a/logic/controller/pet_skill.go b/logic/controller/pet_skill.go index 9c815644d..6e1f4c424 100644 --- a/logic/controller/pet_skill.go +++ b/logic/controller/pet_skill.go @@ -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 diff --git a/logic/service/fight/action.go b/logic/service/fight/action.go index 46e116a0d..ab1d4c47b 100644 --- a/logic/service/fight/action.go +++ b/logic/service/fight/action.go @@ -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) { - ff.Player.SendPackCmd(2504, &startInfo) + f.sendLegacyGroupStart(ff.Player) }) }) } diff --git a/logic/service/fight/boss/NewSeIdx_41.go b/logic/service/fight/boss/NewSeIdx_41.go index b622df03c..2cfdb4772 100644 --- a/logic/service/fight/boss/NewSeIdx_41.go +++ b/logic/service/fight/boss/NewSeIdx_41.go @@ -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 } diff --git a/logic/service/fight/fightc.go b/logic/service/fight/fightc.go index 6769fa87a..d0aaffa74 100644 --- a/logic/service/fight/fightc.go +++ b/logic/service/fight/fightc.go @@ -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 diff --git a/logic/service/fight/group_legacy.go b/logic/service/fight/group_legacy.go new file mode 100644 index 000000000..dba1c4791 --- /dev/null +++ b/logic/service/fight/group_legacy.go @@ -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 +} diff --git a/logic/service/fight/info/ctx.go b/logic/service/fight/info/ctx.go index 7c7c3f433..f4b93fdc1 100644 --- a/logic/service/fight/info/ctx.go +++ b/logic/service/fight/info/ctx.go @@ -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 } diff --git a/logic/service/fight/input.go b/logic/service/fight/input.go index 2887b3688..ce39cea61 100644 --- a/logic/service/fight/input.go +++ b/logic/service/fight/input.go @@ -23,14 +23,15 @@ type FightC struct { ReadyInfo model.NoteReadyToFightInfo //开始战斗信息 info.FightStartOutboundInfo - Info info.Fightinfo - IsReady bool - ownerID uint32 // 战斗发起者ID - Our []*input.Input // 我方战斗位 - Opp []*input.Input // 敌方战斗位 - OurPlayers []common.PlayerI // 我方操作者 - OppPlayers []common.PlayerI // 敌方操作者 - Switch map[actionSlotKey]*action.ActiveSwitchAction + 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 @@ -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) { - slots[newActionSlotKey(slot.ControllerUserID, slot.SlotIndex)] = struct{}{} + if f.slotNeedsAction(slot.Input) { + slots[newActionSlotKey(slot.ControllerUserID, slot.SlotIndex)] = struct{}{} + } } for _, slot := range f.SideSlots(SideOpp) { - slots[newActionSlotKey(slot.ControllerUserID, slot.SlotIndex)] = struct{}{} + 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 { - - return f.TrueFirst.Player == play + 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 { diff --git a/logic/service/fight/input/Capture.go b/logic/service/fight/input/Capture.go index 3261cbc78..12abe6bb6 100644 --- a/logic/service/fight/input/Capture.go +++ b/logic/service/fight/input/Capture.go @@ -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 } diff --git a/logic/service/fight/input/ai.go b/logic/service/fight/input/ai.go index 5ccca933a..8932dcbc6 100644 --- a/logic/service/fight/input/ai.go +++ b/logic/service/fight/input/ai.go @@ -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 diff --git a/logic/service/fight/input/fight.go b/logic/service/fight/input/fight.go index 42cdccbec..c5f9d6dc9 100644 --- a/logic/service/fight/input/fight.go +++ b/logic/service/fight/input/fight.go @@ -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 diff --git a/logic/service/fight/input/team.go b/logic/service/fight/input/team.go index f84a9090d..64c935dbd 100644 --- a/logic/service/fight/input/team.go +++ b/logic/service/fight/input/team.go @@ -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))] +} diff --git a/logic/service/fight/input/team_test.go b/logic/service/fight/input/team_test.go index 9059a99a3..1a4bd469c 100644 --- a/logic/service/fight/input/team_test.go +++ b/logic/service/fight/input/team_test.go @@ -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") + } +} diff --git a/logic/service/fight/loop.go b/logic/service/fight/loop.go index c7c47e4b9..7b503f893 100644 --- a/logic/service/fight/loop.go +++ b/logic/service/fight/loop.go @@ -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) { - - ff.Player.SendPackCmd(2506, &f.FightOverInfo) + 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 } diff --git a/logic/service/fight/new.go b/logic/service/fight/new.go index b0ac43047..bcebbb98e 100644 --- a/logic/service/fight/new.go +++ b/logic/service/fight/new.go @@ -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() - f.Broadcast(func(ff *input.Input) { - ff.Player.SendPackCmd(2503, &f.ReadyInfo) - }) + 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) { - ff.Player.SendPackCmd(2506, &f.FightOverInfo) + if f.LegacyGroupProtocol { + f.sendLegacyGroupOver(ff.Player, &f.FightOverInfo) + } else { + ff.Player.SendPackCmd(2506, &f.FightOverInfo) + } ff.Player.QuitFight() }) } diff --git a/logic/service/fight/new_options.go b/logic/service/fight/new_options.go index 34afd37fa..f2e84abfc 100644 --- a/logic/service/fight/new_options.go +++ b/logic/service/fight/new_options.go @@ -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 { diff --git a/logic/service/player/base.go b/logic/service/player/base.go index abb577e7f..3bccf68f9 100644 --- a/logic/service/player/base.go +++ b/logic/service/player/base.go @@ -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) { diff --git a/modules/config/model/map_node.go b/modules/config/model/map_node.go index e58614c53..e55bfb99b 100644 --- a/modules/config/model/map_node.go +++ b/modules/config/model/map_node.go @@ -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:'触发剧情ID(0表示无剧情)'" json:"trigger_plot_id" description:"触发剧情ID"` BossIds []uint32 `gorm:"type:jsonb;comment:'塔层BOSS ID列表'" json:"boss_ids"`