Files
bl/logic/service/fight/fightc.go
昔念 cccf26788e fix(socket): 玩家断开连接时增加保存锁,避免重复保存
在玩家断开连接时,使用 sync.Once 确保只保存一次玩家数据,
防止因并发或多次触发导致的数据异常。

feat(fight): 增加战斗资格判断与邀请取消功能

- 新增 Player.CanFight() 方法用于统一判断是否可以参与战斗
- 在多个战斗相关接口中加入 CanFight 检查
- 添加“取消战斗邀请”指令及处理逻辑(cmd: 2402)
- 修复部分错误码不准确的问题,提升提示一致性

refactor(login): 优化登录流程并增强健壮性

- 提前校验 session 合法性
- 增强获取玩家信息后的空指针检查
- 调整挖矿数据重置方式为 defer 执行
- 优化日志输出内容,便于调试追踪

docs(model): 更新部门、菜单等模型字段命名规范

将 orderNum 字段改为 ordernum,保持数据库列名风格一致,
同时更新了 base_sys_role 中 userId 为 userid。

perf(rate-limit): 提高登录接口的限流 Burst 容量

调整限流器配置,将请求 burst 容量从 2 提升至 5,
以应对短时间高频访问场景,改善用户体验。

chore(build): 忽略新增编译产物和临时文件

在 .gitignore 中添加 logic/logic2、login/login 等新生成文件路径,
避免误提交二进制文件到版本控制。
2025-10-31 00:53:22 +08:00

737 lines
19 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package fight
import (
"blazing/common/utils"
"blazing/logic/service/common"
"blazing/logic/service/fight/action"
"blazing/logic/service/fight/info"
"blazing/logic/service/fight/input"
"blazing/logic/service/player"
"blazing/modules/blazing/model"
"fmt"
"math/rand"
"reflect"
"sort"
"time"
"github.com/barkimedes/go-deepcopy"
"github.com/jinzhu/copier"
"github.com/shopspring/decimal"
)
type FightC struct {
Info info.NoteReadyToFightInfo
ownerID uint32 // 战斗发起者ID
Our *input.Input //始终等于房主ID
Opp *input.Input //对手ID
Switch []*action.ActiveSwitchAction
rand *rand.Rand
StartTime time.Time
actionChan chan action.BattleActionI // 所有操作统一从这里进入
Round int //回合数
First *input.Input
Second *input.Input
closefight bool
}
func (f *FightC) Ownerid() uint32 {
return f.ownerID
}
func (f *FightC) GetInputByPlayer(c common.PlayerI, isOpposite bool) *input.Input {
// 判断当前玩家是否为我方玩家
isOurPlayer := c.GetInfo().UserID == f.ownerID
// 当isOurPlayer与isOpposite值不同时返回我方相同时返回对方
if isOurPlayer != isOpposite {
return f.Our
}
return f.Opp
}
func (f *FightC) GetInputByAction(c action.BattleActionI, isOpposite bool) *input.Input {
// 判断动作所属玩家是否为我方
isOurAction := c.GetPlayerID() == f.Our.Player.GetInfo().UserID
// 根据isOpposite决定是否返回相反方向的输入
if isOurAction == !isOpposite {
return f.Our
}
return f.Opp
}
// 玩家使用技能
func (f *FightC) GetCurrPET(c common.PlayerI) *info.BattlePetEntity {
if f.Our.Player.GetInfo().UserID == c.GetInfo().UserID {
return f.Our.CurrentPet
} else {
return f.Opp.CurrentPet
}
}
// 获取随机数
func (f *FightC) GetRand() *rand.Rand {
return f.rand
}
// 获取随机数
func (f *FightC) IsFirst(play common.PlayerI) bool {
return f.First.Player == play
}
// 加载进度
func (f *FightC) LoadPercent(c common.PlayerI, percent int32) {
f.GetInputByPlayer(c, true).Player.SendLoadPercent(info.LoadPercentOutboundInfo{
Id: c.GetInfo().UserID,
Percent: uint32(percent),
})
}
func (f *FightC) initplayer(c common.PlayerI, opp bool) bool {
if len(c.GetInfo().PetList) == 0 {
return false
}
temp := input.NewInput(f, c)
temp.AllPet = make([]*info.BattlePetEntity, 0)
for i := 0; i < len(c.GetInfo().PetList); i++ {
if f.Info.MAXPET == 0 || i < int(f.Info.MAXPET) {
temp.AllPet = append(temp.AllPet, info.CreateBattlePetEntity(&c.GetInfo().PetList[i], f.rand))
}
}
sort.Slice(temp.AllPet, func(i, j int) bool {
x, y := temp.AllPet[i], temp.AllPet[j]
// 若x血量>0且y血量=0则x排在前
if x.Info.Hp > 0 && y.Info.Hp <= 0 {
return true
}
// 若x血量=0且y血量>0则x排在后
if x.Info.Hp <= 0 && y.Info.Hp > 0 {
return false
}
// 同类型(都>0或都=0保持原有顺序
return i < j
})
if opp {
f.Opp = temp //这里是对方的
copier.Copy(&f.Info.OpponentInfo, f.Opp.Player.GetInfo())
f.Info.OpponentPetList = make([]info.ReadyFightPetInfo, len(temp.AllPet))
for i := 0; i < len(temp.AllPet); i++ {
err := copier.CopyWithOption(&f.Info.OpponentPetList[i], &temp.AllPet[i].Info, copier.Option{IgnoreEmpty: true, DeepCopy: true})
if err != nil {
panic(err)
}
}
} else {
f.Our = temp
copier.Copy(&f.Info.OurInfo, f.Our.Player.GetInfo())
f.Info.OurPetList = make([]info.ReadyFightPetInfo, len(temp.AllPet))
for i := 0; i < len(temp.AllPet); i++ {
err := copier.CopyWithOption(&f.Info.OurPetList[i], &temp.AllPet[i].Info, copier.Option{IgnoreEmpty: true, DeepCopy: true})
if err != nil {
panic(err)
}
}
}
for _, v := range temp.AllPet {
if v.Info.Hp == 0 {
v.NotAlive = true
}
}
temp.CurrentPet = temp.AllPet[0]
return true
}
// 创建新战斗,邀请方和被邀请方,或者玩家和野怪方
func NewFight(mode, status info.EnumBattleMode, p1 common.PlayerI, p2 common.PlayerI) *FightC {
f := &FightC{}
f.ownerID = p1.GetInfo().UserID
f.Info.Status = status //房主
f.Info.Mode = mode
switch mode {
case info.BattleMode.SINGLE_MODE:
f.Info.MAXPET = 1
default:
}
f.StartTime = time.Now()
seed := f.StartTime.UnixNano() ^ int64(p1.GetInfo().UserID) ^ int64(p2.GetInfo().UserID) // ^ int64(f.Round) // 用异或运算混合多维度信息
f.rand = rand.New(rand.NewSource(seed))
f.Info = info.NoteReadyToFightInfo{
Status: status,
}
ok := f.initplayer(p1, false)
if !ok {
return nil
}
ok = f.initplayer(p2, true)
if !ok {
return nil
}
defer func() {
rr := Fightpool.Submit(f.battleLoop)
if rr != nil {
panic(rr)
}
f.Broadcast(func(ff *input.Input) {
ff.Player.SendNoteReadyToFightInfo(f.Info)
})
}()
return f
}
// 被击败的ID
func (b *FightC) IsWin(c *input.Input, cache uint32) bool {
var tt []*info.BattlePetEntity
bbb := b.Our.AllPet
if c.Player.GetInfo().UserID == b.ownerID { //如果是房主
bbb = b.Opp.AllPet
}
for _, v := range bbb {
if v.Info.CatchTime == cache {
v.NotAlive = true
}
tt = append(tt, v)
}
for _, v := range tt {
if !v.NotAlive { //如果存活
return false
}
}
return true
}
// 广播,并是否结束回合
func (f *FightC) Broadcast(t func(ff *input.Input)) {
t(f.Our)
t(f.Opp)
}
// 战斗回合循环
func (f *FightC) battleLoop() {
f.actionChan = make(chan action.BattleActionI, 2) // 初始化全局操作通道
fmt.Println("战斗开始精灵", f.Our.Player.GetInfo().PetList[0].CatchTime)
//战斗开始前操作
for {
if f.closefight { //回合数超过250,战斗平局结束f.Round > 250 ||
close(f.actionChan)
break
}
f.Round++ //回合数自增
actions := make(map[uint32]action.BattleActionI) // 每个玩家一条记录
timeout := time.After(60 * time.Second)
for len(actions) < 2 {
select {
case paction, ok := <-f.actionChan:
// 只接受有效玩家 ID\
if paction == nil {
continue
}
if !ok {
fmt.Println("战斗结束")
break
}
if f.closefight { //回合数超过250,战斗平局结束f.Round > 250 ||
break
}
if paction.GetPlayerID() != f.Our.Player.GetInfo().UserID && paction.GetPlayerID() != f.Opp.Player.GetInfo().UserID {
continue
}
if _, isExpelled := paction.(*action.ActiveSwitchAction); isExpelled {
if f.GetInputByAction(paction, false).CanChange {
//如果是被动切换,不计入回合结算
f.GetInputByAction(paction, false).CanChange = false
continue
}
}
if paction.GetPlayerID() != 0 {
if f.Info.Status == info.BattleStatus.FIGHT_WITH_BOSS || f.Info.Status == info.BattleStatus.FIGHT_WITH_NPC {
//AI的action实质上就是放技能如果阻止掉比如中毒那就也不能逃跑
f.GetInputByAction(paction, true).GetAction(f.Our)
}
}
// 如果该玩家已经提交过,就忽略重复动作
if _, exists := actions[uint32(paction.GetPlayerID())]; exists {
fmt.Printf("玩家%d 已经提交过动作,忽略重复\n", paction.GetPlayerID())
continue
}
actions[uint32(paction.GetPlayerID())] = paction
fmt.Println("玩家 执行动作", paction.GetPlayerID(), paction.Priority())
case <-timeout:
if _, exists := actions[f.Our.Player.GetInfo().UserID]; !exists {
f.Over(f.Opp.Player, info.BattleOverReason.PlayerOVerTime)
}
if _, exists := actions[f.Opp.Player.GetInfo().UserID]; !exists {
f.Over(f.Our.Player, info.BattleOverReason.PlayerOVerTime)
}
}
}
// 双方动作齐了,取出来结算
//todo 如果一方没有选择,实际上就是后端判断PP是否还有,前端是直接发的
p1Action := actions[f.Our.Player.GetInfo().UserID]
p2Action := actions[f.Opp.Player.GetInfo().UserID]
fmt.Println("开始结算回合")
// 统一赋值,减少重复代码
var BattleActionI [2]action.BattleActionI
BattleActionI[0], BattleActionI[1] = f.Compare(p1Action, p2Action)
switch faction := BattleActionI[0].(type) {
case *action.PlayerOfflineAction: // 单方掉线
f.Broadcast(func(ff *input.Input) {
ff.Player.SendFightEndInfo(faction.Reason) // 广播逃跑原因
})
f.closefight = true
case *action.EscapeAction: // 优先逃跑
f.Broadcast(func(ff *input.Input) {
ff.Player.SendFightEndInfo(faction.Reason) // 广播逃跑原因
})
f.closefight = true
case *action.ActiveSwitchAction: // 切换上场的, 切换方放弃出手
// 如果后手不是技能,替换成空技能(放弃出手)
if _, ok := BattleActionI[1].(*action.SelectSkillAction); !ok {
f.enterturn(nil, nil) //双方都不出手
} else {
//后手方先手,先手方放弃出手
f.enterturn(BattleActionI[1].(*action.SelectSkillAction), nil)
}
case *action.UseItemAction: // 使用道具
switch {
case faction.ItemID >= 30001 && faction.ItemID <= 300010: // 胶囊
tt, ok := f.Our.Player.(*player.Player)
mo, ism := f.Opp.Player.(*player.AI_player)
if ok && ism && mo.CanCapture {
ok, res := f.Our.Capture(f.Opp.CurrentPet, faction.ItemID, -1)
if ok {
fmt.Println(res)
tt.Service.PetAdd(*f.Opp.CurrentPet.Info)
tt.CatchPetInfo(info.CatchMonsterOutboundInfo{
CatchTime: uint32(f.Opp.CurrentPet.Info.CatchTime),
PetId: uint32(f.Opp.CurrentPet.ID),
})
tt.SendFightEndInfo(info.FightOverInfo{
WinnerId: f.ownerID,
})
f.closefight = true
} else {
tt.CatchPetInfo(info.CatchMonsterOutboundInfo{})
}
} else {
tt.CatchPetInfo(info.CatchMonsterOutboundInfo{})
}
fmt.Println("ItemID 在范围内")
case faction.ItemID == 300001:
fmt.Println("ItemID 是 300001")
default:
fmt.Println("ItemID 不在指定范围内")
}
if _, ok := BattleActionI[1].(*action.SelectSkillAction); !ok {
f.enterturn(nil, nil)
} else {
f.enterturn(BattleActionI[1].(*action.SelectSkillAction), nil)
}
default: // 双方都是技能或者默认情况
f.enterturn(BattleActionI[0].(*action.SelectSkillAction), BattleActionI[1].(*action.SelectSkillAction))
}
}
}
// 处理技能攻击逻辑
func (f *FightC) processSkillAttack(attacker, defender *input.Input, a *action.SelectSkillAction) {
attacker.Exec(func(t input.Effect) bool { //计算命中 miss改命中
t.Skill_Hit_Pre(input.Ctx{ //调基础命中
Input: defender,
SelectSkillAction: a,
}) //相当于先调整基础命中,不光调整命中,这里还能调整技能属性,暴击率
return true
})
a.AttackTimeC(attacker.GetProp(5, true)) //计算命中
defender.Exec(func(t input.Effect) bool { //计算闪避 ,然后修改对方命中),同时相当于计算属性无效这种
t.Skill_Hit_to(input.Ctx{ //计算命中后,我方强制改命中效果
Input: attacker,
SelectSkillAction: a,
})
return true
})
attacker.AttackValue.AttackTime = a.AttackTime //是否命中赋值
attacker.Exec(func(t input.Effect) bool { //计算命中 miss改命中
t.Skill_Hit(input.Ctx{ //计算变威力
Input: attacker,
SelectSkillAction: a,
}) //相当于先调整基础命中,不光调整命中,这里还能调整技能属性,暴击率
return true
})
//技能命中+效果失效 这里就是修改效果命中为false
//技能miss+效果生效 这里属于强制改命中效果,但是正常来说,技能miss掉后效果也应该失效
//技能失效+效果失效
// 记录技能信息
attacker.AttackValue.SkillID = uint32(a.ID) //获取技能ID
if attacker.AttackValue.AttackTime > 0 { //如果命中
attacker.UseSkill(defender, a) //暴击计算
attacker.AttackValue.IsCritical = a.Crit
attacker.Exec(func(t input.Effect) bool { //计算命中 miss改命中
t.Calculate_Pre(input.Ctx{ //计算视为效果
Input: defender,
SelectSkillAction: a,
}) //相当于先调整基础命中,不光调整命中,这里还能调整技能属性,暴击率
return true
})
attacker.DamageZone.Damage = attacker.CalculatePower(defender, a.SkillEntity)
if attacker.AttackValue.IsCritical == 1 {
//暴击翻倍
attacker.DamageZone.Damage = attacker.DamageZone.Damage.Mul(decimal.NewFromInt(2))
}
}
for _, e := range attacker.EffectCache {
//这里实现应该参考本地技能是否命中,然后
e.Hit(a.AttackTime != 0) //我方效果命中
}
for _, t := range defender.EffectCache {
if t.GetInput() == attacker { //如果取反,说明是给对方添加的回合效果
t.Hit(a.AttackTime != 0)
}
}
// 扣减防御方血量
attacker.Exec(func(t input.Effect) bool {
t.OnSkill(input.Ctx{
Input: defender,
SelectSkillAction: a,
}) //调用伤害计算
return true
})
defender.Damage(input.Ctx{
Input: attacker,
SelectSkillAction: a,
DamageZone: &info.DamageZone{
Type: info.DamageType.Red,
Damage: attacker.DamageZone.Damage,
},
})
//这里其实是受到致死伤害
//然后先触发死亡效果消除所有buff
//然后触发回神效果
}
func IsNil(x interface{}) bool {
if x == nil {
return true
}
rv := reflect.ValueOf(x)
return rv.Kind() == reflect.Ptr && rv.IsNil()
}
func copyskill(t *action.SelectSkillAction) *action.SelectSkillAction {
oldskill, _ := deepcopy.Anything(t) //备份技能
return oldskill.(*action.SelectSkillAction)
}
//回合有先手方和后手方,同时有攻击方和被攻击方
func (f *FightC) enterturn(fattack, sattack *action.SelectSkillAction) {
if f.closefight { //战斗结束
return
}
// 伤害值
// 根据攻击方归属设置当前战斗的主/次攻击方属性
if fattack != nil {
if fattack.GetPlayerID() == f.ownerID {
f.First, f.Second = f.Our, f.Opp // 攻击方为我方时,主攻击方是我方
} else {
f.First, f.Second = f.Opp, f.Our // 攻击方为对方时,主攻击方是对方
}
} else {
f.First, f.Second = f.Our, f.Opp
}
f.First.InitAttackValue()
f.Second.InitAttackValue()
switch {
case sattack != nil:
f.Second.Parseskill(f.Second, sattack) //解析到临时数据
f.Second.Exec(func(t input.Effect) bool { //回合开始前
//结算状态
t.Compare_Pre(fattack, sattack) //先结算技能的优先级
return true
})
switch {
case fattack != nil: //房主也放弃出手
//是否miss都应该施加解析effect
f.First.Parseskill(f.Second, fattack) //解析到临时数据
f.First.Exec(func(t input.Effect) bool { //回合开始前
//结算状态
t.Compare_Pre(fattack, sattack) //先结算技能的优先级
return true
})
switch {
case fattack.SkillEntity.Priority < sattack.SkillEntity.Priority:
fattack, sattack = sattack, fattack //互换先手权
f.First, f.Second = f.Second, f.First
case fattack.SkillEntity.Priority == sattack.SkillEntity.Priority:
if f.Second.GetProp(4, false) > f.First.GetProp(4, false) {
fattack, sattack = sattack, fattack //互换先手权
f.First, f.Second = f.Second, f.First
}
}
default: //房主放弃出手
fattack, sattack = sattack, fattack //互换先手权
f.First, f.Second = f.Second, f.First
}
}
var attacker, defender *input.Input
f.First.Exec(func(t input.Effect) bool { //回合开始前
//结算状态
t.Turn_Start(input.Ctx{Input: f.First})
return true
})
f.Second.Exec(func(t input.Effect) bool { //回合开始前
//结算状态
t.Turn_Start(input.Ctx{Input: f.Second})
return true
})
//开始回合操作
for i := 0; i < 2; i++ {
var oldskill *action.SelectSkillAction //原始技能
var currentskill *action.SelectSkillAction //当前技能
if i == 0 { //
attacker, defender = f.First, f.Second
oldskill = copyskill(fattack)
currentskill = fattack
} else {
attacker, defender = f.Second, f.First
oldskill = copyskill(sattack)
currentskill = sattack
}
canuseskill := true
// 实际上攻击方 还有系统选择放弃出手的
if IsNil(currentskill) || attacker.CurrentPet.Info.Hp <= 0 {
// attacker.AttackValue.SkillID = 0
canuseskill = false
} else {
if !action.CanUse(currentskill) {
// attacker.AttackValue.SkillID = 0
canuseskill = false
}
}
canuseskillok := attacker.Exec(func(t input.Effect) bool { //这个是能否使用技能
//结算状态
//然后这里还可以处理自爆类
return t.Skill_Hit_Pre(input.Ctx{
Input: defender,
SelectSkillAction: currentskill,
}) //返回本身结算,如果false,说明不能使用技能了
})
if canuseskill && canuseskillok { //可以使用技能
f.processSkillAttack(attacker, defender, currentskill)
currentskill = oldskill
_, skill, ok := utils.FindWithIndex(attacker.CurrentPet.Info.SkillList, func(item model.SkillInfo) bool {
return item.ID == currentskill.Info.ID
})
if ok {
skill.PP--
}
}
//技能使用后
defender.Exec(func(t input.Effect) bool {
t.Skill_Use(input.Ctx{Input: attacker})
return true
})
//技能使用后
attacker.Exec(func(t input.Effect) bool { //技能使用后的我方效果
t.Skill_Useed(input.Ctx{Input: defender, SelectSkillAction: currentskill})
return true
})
fmt.Println(i,
// "玩家技能:", oldskill.(*info.SkillEntity).ID,
"玩家技能伤害:", attacker.DamageZone.Damage,
"自身剩余血量:", attacker.CurrentPet.Info.Hp,
"对手剩余血量:", defender.CurrentPet.Info.Hp,
)
if defender.CurrentPet.Info.Hp == 0 {
// defender.AttackValue.SkillID = 0
defender.CanChange = true //被打死就可以切精灵了
if f.IsWin(attacker, defender.CurrentPet.Info.CatchTime) { //然后检查是否战斗结束
var WinnerId uint32
if i == 0 {
WinnerId = f.First.Player.GetInfo().UserID
} else {
WinnerId = f.Second.Player.GetInfo().UserID
}
defer f.Broadcast(func(ff *input.Input) {
//todo 将血量和技能pp传回enterturn
ff.Player.SendFightEndInfo(info.FightOverInfo{
WinnerId: WinnerId,
})
})
f.closefight = true
}
}
}
f.First.Exec(func(t input.Effect) bool { //这个是能否使用技能
//结算状态
t.Turn_End(input.Ctx{
Input: f.Second,
}) //返回本身结算,如果false,说明不能使用技能了
return true
})
f.Second.Exec(func(t input.Effect) bool { //这个是能否使用技能
//结算状态
t.Turn_End(input.Ctx{
Input: f.First,
})
return true
})
f.First.AttackValue.RemainHp = int32(f.First.CurrentPet.Info.Hp)
f.First.AttackValue.SkillList = f.First.CurrentPet.Info.SkillList
f.Second.AttackValue.SkillList = f.Second.CurrentPet.Info.SkillList
f.Second.AttackValue.RemainHp = int32(f.Second.CurrentPet.Info.Hp)
ret := info.AttackValueS{
FAttack: *f.First.AttackValue,
SAttack: *f.Second.AttackValue,
}
for i := 0; i < 20; i++ { //堆叠状态剩余回合
t := f.First.GetEffect(input.EffectType.Status, i)
if t.ID != 0 {
ret.FAttack.Status[i] = int8(t.Effect.Duration())
}
t = f.Second.GetEffect(input.EffectType.Status, i)
if t.ID != 0 {
ret.SAttack.Status[i] = int8(t.Effect.Duration())
}
}
f.Broadcast(func(ff *input.Input) {
for _, v := range f.Switch {
if ff.Player.GetInfo().UserID != v.PlayerID {
ff.Player.SendChangePet(v.Reason)
}
}
ff.Player.SendAttackValue(ret)
})
f.Switch = []*action.ActiveSwitchAction{}
}