This commit is contained in:
昔念
2026-04-07 17:26:52 +08:00
91 changed files with 3417 additions and 811 deletions

View File

@@ -5,6 +5,7 @@ import (
"blazing/modules/player/model"
)
// FightI 定义 common 服务层依赖的战斗操作接口。
type FightI interface {
Over(c PlayerI, id model.EnumBattleOverReason) //逃跑
UseSkill(c PlayerI, id uint32) //使用技能

View File

@@ -7,24 +7,25 @@ import (
"github.com/gogf/gf/v2/os/glog"
)
// MyWriter 自定义日志写入器,用于逻辑服日志转发。
type MyWriter struct {
logger *glog.Logger
user uint32
logger *glog.Logger // 底层 glog 实例。
user uint32 // 关联的玩家 ID。
}
// Write 实现 io.Writer并将日志写入系统日志与底层 logger。
func (w *MyWriter) Write(p []byte) (n int, err error) {
var (
s = string(p)
//ctx = context.Background()
)
service.NewBaseSysLogService().RecordLog(w.user, s)
return w.logger.Write(p)
}
func init() {
cool.Logger.SetWriter(&MyWriter{
logger: glog.New(),
})
cool.Logger.SetAsync(true)
}

View File

@@ -8,20 +8,18 @@ import (
"github.com/lunixbochs/struc"
)
// TomeeHeader 结构体字段定义
// TomeeHeader 定义协议包头。
type TomeeHeader struct {
Len uint32 `json:"len"`
Version byte `json:"version" struc:"[1]byte"`
CMD uint32 `json:"cmdId" struc:"uint32"`
UserID uint32 `json:"userId"`
//Error uint32 `json:"error" struc:"skip"`
Result uint32 `json:"result"`
Data []byte `json:"data" struc:"skip"` //组包忽略此字段// struc:"skip"
Res []byte `struc:"skip"`
//Return []byte `struc:"skip"` //返回记录
Len uint32 `json:"len"` // 包总长度(包头 + 数据体)。
Version byte `json:"version" struc:"[1]byte"` // 协议版本。
CMD uint32 `json:"cmdId" struc:"uint32"` // 命令 ID。
UserID uint32 `json:"userId"` // 玩家 ID。
Result uint32 `json:"result"` // 结果码。
Data []byte `json:"data" struc:"skip"` // 数据体,序列化时跳过。
Res []byte `struc:"skip"` // 预留返回数据,序列化时跳过。
}
// NewTomeeHeader 创建用于下行封包的默认 TomeeHeader。
func NewTomeeHeader(cmd uint32, userid uint32) *TomeeHeader {
return &TomeeHeader{

View File

@@ -7,6 +7,7 @@ import (
"blazing/modules/player/model"
)
// PlayerI 定义战斗与 common 服务依赖的最小玩家能力接口。
type PlayerI interface {
ApplyPetDisplayInfo(*space.SimpleInfo)
GetPlayerCaptureContext() *info.PlayerCaptureContext

View File

@@ -0,0 +1,99 @@
package effect
import (
"blazing/logic/service/fight/action"
"blazing/logic/service/fight/info"
"blazing/logic/service/fight/input"
)
// 247. 固定增加体力/攻击/防御/特攻/特防/速度;a1-a6: hp/atk/def/spatk/spdef/spd
type NewSel247 struct {
NewSel0
}
func (e *NewSel247) TurnStart(fattack *action.SelectSkillAction, sattack *action.SelectSkillAction) {
if !e.IsOwner() {
return
}
pet := e.Ctx().Our.CurPet[0]
if pet == nil {
return
}
hpBonus := uint32(e.Args()[0].IntPart())
if hpBonus > 0 {
pet.Info.MaxHp += hpBonus
pet.Info.Hp += hpBonus
}
for i, propIdx := range []int{0, 1, 2, 3, 4} {
add := uint32(e.Args()[i+1].IntPart())
if add == 0 {
continue
}
pet.Info.Prop[propIdx] += add
}
}
func (e *NewSel247) TurnEnd() {
if !e.IsOwner() {
return
}
pet := e.Ctx().Our.CurPet[0]
if pet == nil {
return
}
hpBonus := uint32(e.Args()[0].IntPart())
if hpBonus > 0 {
if pet.Info.MaxHp > hpBonus {
pet.Info.MaxHp -= hpBonus
} else {
pet.Info.MaxHp = 1
}
if pet.Info.Hp > pet.Info.MaxHp {
pet.Info.Hp = pet.Info.MaxHp
}
}
for i, propIdx := range []int{0, 1, 2, 3, 4} {
sub := uint32(e.Args()[i+1].IntPart())
if sub == 0 {
continue
}
if pet.Info.Prop[propIdx] > sub {
pet.Info.Prop[propIdx] -= sub
} else {
pet.Info.Prop[propIdx] = 1
}
}
}
type NewSel239 struct {
NewSel0
}
func (e *NewSel239) ActionStart(a, b *action.SelectSkillAction) bool {
if !e.IsOwner() {
return true
}
if e.Ctx().SkillEntity == nil {
return true
}
if e.Ctx().SkillEntity.Category() == info.Category.STATUS {
return true
}
if len(e.Args()) == 0 {
return true
}
e.Ctx().SkillEntity.XML.Power += int(e.Args()[0].IntPart())
return true
}
func init() {
input.InitEffect(input.EffectType.NewSel, 239, &NewSel239{})
input.InitEffect(input.EffectType.NewSel, 247, &NewSel247{})
}

View File

@@ -11,10 +11,9 @@ type NewSel26 struct {
}
func (e *NewSel26) TurnStart(fattack *action.SelectSkillAction, sattack *action.SelectSkillAction) {
e.Ctx().Our.CurPet[0].Info.Prop[int(e.Args()[0].IntPart())] += uint32(e.Args()[1].IntPart())
}
func (e *NewSel26) TurnEnd() {
e.Ctx().Our.CurPet[0].Info.Prop[int(e.Args()[0].IntPart())] -= uint32(e.Args()[1].IntPart())
}
func init() {
input.InitEffect(input.EffectType.NewSel, 26, &NewSel26{})

View File

@@ -1,26 +1,25 @@
package effect
import (
"blazing/logic/service/fight/input"
)
import "blazing/logic/service/fight/input"
// 501. g1. 最后一个死 (只要有队友没死, 则自己又恢复hp和pp)
// TODO: 需要了解如何判断队友状态并恢复HP和PP
type NewSel501 struct {
NewSel0
}
// SwitchOut 在拥有者死亡离场时触发;若仍有存活队友,则把自己回满留作后续再上场。
func (e *NewSel501) SwitchOut(in *input.Input) bool {
//魂印特性有不在场的情况,绑定时候将精灵和特性绑定
if e.ID().GetCatchTime() != e.Ctx().Our.CurPet[0].Info.CatchTime {
owner := e.SourceInput()
if owner == nil || in != owner || !e.IsOwner() {
return true
}
currentPet := owner.CurrentPet()
if currentPet == nil || currentPet.Info.Hp > 0 || !owner.HasLivingTeammate() {
return true
}
// TODO: 检查是否有队友还活着
// 如果有队友活着恢复自身HP和PP
// e.Ctx().Our.Heal(e.Ctx().Our, nil, e.Ctx().Our.CurPet[0].GetMaxHP())
// TODO: 恢复PP值的方法
currentPet.Info.Hp = currentPet.Info.MaxHp
owner.HealPP(-1)
return true
}

View File

@@ -1,25 +1,31 @@
package effect
import (
"blazing/logic/service/fight/input"
)
import "blazing/logic/service/fight/input"
// 502. g2. 如果自身死亡, 恢复队友所有体力和PP值
// TODO: 实现恢复队友所有体力和PP值的核心逻辑
type NewSel502 struct {
NewSel0
}
// TODO: 需要找到精灵死亡时的回调接口
// SwitchOut 在拥有者死亡离场时触发;把仍在场的队友体力和 PP 恢复到满值。
func (e *NewSel502) SwitchOut(in *input.Input) bool {
//魂印特性有不在场的情况,绑定时候将精灵和特性绑定
if e.ID().GetCatchTime() != e.Ctx().Our.CurPet[0].Info.CatchTime {
owner := e.SourceInput()
if owner == nil || in != owner || !e.IsOwner() {
return true
}
currentPet := owner.CurrentPet()
if currentPet == nil || currentPet.Info.Hp > 0 {
return true
}
// TODO: 检查精灵是否死亡HP为0
// 如果死亡恢复队友所有体力和PP值
// 需要遍历队友的精灵并调用相应的方法
for _, teammate := range owner.LivingTeammates() {
pet := teammate.CurrentPet()
if pet == nil {
continue
}
pet.Info.Hp = pet.Info.MaxHp
teammate.HealPP(-1)
}
return true
}

View File

@@ -6,20 +6,35 @@ import (
)
// 503. g3. 群体攻击技能可额外增加一个目标(最多不超过5个目标)
// TODO: 需要了解如何修改群体攻击技能的目标数量
type NewSel503 struct {
NewSel0
}
// TurnStart 在拥有者本回合准备出手时触发;若本次技能是群体技能,则把目标数额外加 1。
func (e *NewSel503) TurnStart(fattack *action.SelectSkillAction, sattack *action.SelectSkillAction) {
//魂印特性有不在场的情况,绑定时候将精灵和特性绑定
if e.ID().GetCatchTime() != e.Ctx().Our.CurPet[0].Info.CatchTime {
owner := e.SourceInput()
if owner == nil || !e.IsOwner() {
return
}
// TODO: 检查技能是否是群体攻击技能
// 如果是群体攻击增加一个目标最多不超过5个
// 需要了解技能的目标数量限制机制
for _, act := range []*action.SelectSkillAction{fattack, sattack} {
if act == nil || act.SkillEntity == nil || act.SkillEntity.Pet == nil {
continue
}
if act.SkillEntity.Pet.Info.CatchTime != e.ID().GetCatchTime() {
continue
}
if act.SkillEntity.XML.AtkType != 0 {
return
}
if act.SkillEntity.XML.AtkNum <= 0 {
act.SkillEntity.XML.AtkNum = 1
}
if act.SkillEntity.XML.AtkNum < 5 {
act.SkillEntity.XML.AtkNum++
}
return
}
}
func init() {

View File

@@ -0,0 +1,148 @@
package fight
import (
"blazing/logic/service/common"
"blazing/modules/player/model"
)
// FightActionType 表示统一动作包里的动作类型。
type FightActionType string
const (
// FightActionTypeSkill 表示使用技能。
FightActionTypeSkill FightActionType = "skill"
// FightActionTypeItem 表示使用道具。
FightActionTypeItem FightActionType = "item"
// FightActionTypeChange 表示主动切宠。
FightActionTypeChange FightActionType = "change"
// FightActionTypeEscape 表示逃跑。
FightActionTypeEscape FightActionType = "escape"
// FightActionTypeChat 表示战斗内聊天。
FightActionTypeChat FightActionType = "chat"
)
// FightActionEnvelope 是统一入站动作结构。
// 约定:
// 1. actorIndex 始终表示发起方所在的我方槽位。
// 2. targetIndex 始终表示目标在所属阵营内的槽位。
// 3. targetRelation 用来区分 targetIndex 属于敌方、自己还是队友。
type FightActionEnvelope struct {
// ActionType 当前动作类型,例如 skill、item、change、escape、chat。
ActionType FightActionType `json:"actionType"`
// ActorIndex 发起动作的我方槽位。
ActorIndex int `json:"actorIndex"`
// TargetIndex 目标在所属阵营中的槽位下标。
TargetIndex int `json:"targetIndex"`
// TargetRelation 目标关系0=对方1=自己2=队友。
TargetRelation uint8 `json:"targetRelation,omitempty"`
// SkillID 技能 ID仅技能动作使用。
SkillID uint32 `json:"skillId,omitempty"`
// ItemID 道具 ID仅道具动作使用。
ItemID uint32 `json:"itemId,omitempty"`
// CatchTime 精灵实例 ID切宠或部分道具动作使用。
CatchTime uint32 `json:"catchTime,omitempty"`
// Escape 是否为逃跑动作;主要用于协议层兼容和调试。
Escape bool `json:"escape,omitempty"`
// Chat 聊天内容;仅聊天动作使用。
Chat string `json:"chat,omitempty"`
// AtkType 前端技能目标类型兜底值,沿用技能表 AtkType 定义。
AtkType uint8 `json:"atkType,omitempty"`
}
// NewSkillActionEnvelope 构造技能动作 envelope。
func NewSkillActionEnvelope(skillID uint32, actorIndex, targetIndex int, targetRelation, atkType uint8) FightActionEnvelope {
return FightActionEnvelope{
ActionType: FightActionTypeSkill,
ActorIndex: actorIndex,
TargetIndex: targetIndex,
TargetRelation: targetRelation,
SkillID: skillID,
AtkType: atkType,
}
}
// NewItemActionEnvelope 构造道具动作 envelope。
func NewItemActionEnvelope(catchTime, itemID uint32, actorIndex, targetIndex int, targetRelation uint8) FightActionEnvelope {
return FightActionEnvelope{
ActionType: FightActionTypeItem,
ActorIndex: actorIndex,
TargetIndex: targetIndex,
TargetRelation: targetRelation,
ItemID: itemID,
CatchTime: catchTime,
}
}
// NewChangeActionEnvelope 构造切宠动作 envelope。
func NewChangeActionEnvelope(catchTime uint32, actorIndex int) FightActionEnvelope {
return FightActionEnvelope{
ActionType: FightActionTypeChange,
ActorIndex: actorIndex,
CatchTime: catchTime,
}
}
// NewEscapeActionEnvelope 构造逃跑动作 envelope。
func NewEscapeActionEnvelope() FightActionEnvelope {
return FightActionEnvelope{
ActionType: FightActionTypeEscape,
Escape: true,
}
}
// NewChatActionEnvelope 构造聊天动作 envelope。
func NewChatActionEnvelope(chat string) FightActionEnvelope {
return FightActionEnvelope{
ActionType: FightActionTypeChat,
Chat: chat,
}
}
// normalizedTargetRelation 根据 TargetRelation 和 AtkType 兜底出最终目标关系。
func (e FightActionEnvelope) normalizedTargetRelation() uint8 {
if e.TargetRelation <= SkillTargetAlly {
return e.TargetRelation
}
switch e.AtkType {
case 3:
return SkillTargetSelf
case 1:
return SkillTargetAlly
default:
return SkillTargetOpponent
}
}
// EncodedTargetIndex 把统一结构里的目标信息编码成现有 FightC 内部使用的目标格式。
func (e FightActionEnvelope) EncodedTargetIndex() int {
targetIndex := e.TargetIndex
switch e.normalizedTargetRelation() {
case SkillTargetSelf:
targetIndex = e.ActorIndex
return EncodeTargetIndex(targetIndex, false)
case SkillTargetAlly:
return EncodeTargetIndex(targetIndex, false)
default:
return EncodeTargetIndex(targetIndex, true)
}
}
// HandleActionEnvelope 把统一动作结构派发到现有 FightC 的 indexed 接口上。
func (f *FightC) HandleActionEnvelope(c common.PlayerI, envelope FightActionEnvelope) {
if f == nil || c == nil {
return
}
switch envelope.ActionType {
case FightActionTypeSkill:
f.UseSkillAt(c, envelope.SkillID, envelope.ActorIndex, envelope.EncodedTargetIndex())
case FightActionTypeItem:
f.UseItemAt(c, envelope.CatchTime, envelope.ItemID, envelope.ActorIndex, envelope.EncodedTargetIndex())
case FightActionTypeChange:
f.ChangePetAt(c, envelope.CatchTime, envelope.ActorIndex)
case FightActionTypeEscape:
f.Over(c, model.BattleOverReason.PlayerEscape)
case FightActionTypeChat:
f.Chat(c, envelope.Chat)
}
}

View File

@@ -41,6 +41,35 @@ func (t *BattlePetEntity) Alive() bool {
}
func (t *BattlePetEntity) AddBattleAttr(attr int, value uint32) {
if t == nil || value == 0 {
return
}
switch attr {
case 0:
t.Info.MaxHp += value
t.Info.Hp += value
case 1, 2, 3, 4, 5:
t.Info.Prop[attr-1] += value
}
}
func (t *BattlePetEntity) ApplyInitEffectBonus(effect model.PetEffectInfo) {
if t == nil || effect.EID != 26 || len(effect.Args) < 2 {
return
}
for i := 0; i+1 < len(effect.Args); i += 2 {
attr := effect.Args[i]
value := effect.Args[i+1]
if value <= 0 {
continue
}
t.AddBattleAttr(attr, uint32(value))
}
}
// 创建精灵实例
func CreateBattlePetEntity(info model.PetInfo) *BattlePetEntity {
ret := &BattlePetEntity{}

View File

@@ -0,0 +1,114 @@
package info
import "blazing/modules/player/model"
// FightStatePhase 表示统一战斗状态包的阶段。
type FightStatePhase string
const (
// FightStatePhaseStart 表示开战快照。
FightStatePhaseStart FightStatePhase = "start"
// FightStatePhaseSkillHurt 表示技能结算后的伤害快照。
FightStatePhaseSkillHurt FightStatePhase = "skill_hurt"
// FightStatePhaseChange 表示切宠快照。
FightStatePhaseChange FightStatePhase = "change"
// FightStatePhaseOver 表示战斗结束快照。
FightStatePhaseOver FightStatePhase = "over"
// FightStatePhaseLoad 表示加载进度快照。
FightStatePhaseLoad FightStatePhase = "load"
// FightStatePhaseChat 表示战斗内聊天快照。
FightStatePhaseChat FightStatePhase = "chat"
)
// FighterState 是统一的战位快照。
// position 表示该 side 下的槽位编号actorIndex/targetIndex 与其一一对应。
type FighterState struct {
// Side 阵营标识1=我方2=敌方。
Side int `json:"side"`
// Position 战位下标;在各自 side 内部从 0 开始编号。
Position int `json:"position"`
// UserID 当前战位所属玩家 ID野怪/NPC 通常为 0。
UserID uint32 `json:"userId"`
// ControllerUserID 当前上场精灵的实际操作者 ID组队时可与 UserID 联合定位操作者和战位归属。
ControllerUserID uint32 `json:"controllerUserId"`
// PetID 当前上场精灵的物种/配置 ID。
PetID uint32 `json:"petId"`
// CatchTime 当前上场精灵的唯一实例 ID可理解为这只精灵在玩家背包中的唯一标识。
CatchTime uint32 `json:"catchTime"`
// Name 当前上场精灵名字。
Name string `json:"name,omitempty"`
// HP 当前生命值。
HP uint32 `json:"hp"`
// MaxHP 最大生命值。
MaxHP uint32 `json:"maxHp"`
// Level 当前等级。
Level uint32 `json:"level"`
// Anger 怒气值;当前服务端主链路暂未实际填充时默认为 0先为协议对齐预留。
Anger uint32 `json:"anger"`
// Status 当前异常或增益状态回合数组;下标语义沿用现有战斗状态定义。
Status [20]int8 `json:"status"`
// Prop 当前能力等级变化数组:攻击、防御、特攻、特防、速度、命中。
Prop [6]int8 `json:"prop"`
// Skills 当前可见技能列表,包含技能 ID 和当前 PP 等信息。
Skills []model.SkillInfo `json:"skills,omitempty"`
}
// FightStateMeta 是统一状态包的公共元数据。
type FightStateMeta struct {
// Round 当前回合数。
Round uint32 `json:"round"`
// Weather 当前天气或场地编号;当前主链路未填充时可为 0。
Weather uint32 `json:"weather,omitempty"`
// WinnerID 当前已确定的胜者 ID未结束时通常为 0。
WinnerID uint32 `json:"winnerId,omitempty"`
// Reason 当前已确定的结束原因;未结束时通常为 0。
Reason model.EnumBattleOverReason `json:"reason,omitempty"`
// LegacyCmd 对应旧协议命令号,便于新旧包对照和过渡期调试。
LegacyCmd uint32 `json:"legacyCmd,omitempty"`
}
// FightSkillHurtState 保存技能结算阶段的详细战报。
type FightSkillHurtState struct {
// Left 我方阵营本次技能结算后的攻击值快照列表。
Left []model.AttackValue `json:"left,omitempty"`
// Right 敌方阵营本次技能结算后的攻击值快照列表。
Right []model.AttackValue `json:"right,omitempty"`
}
// FightLoadState 保存加载进度信息。
type FightLoadState struct {
// UserID 当前上报加载进度的玩家 ID。
UserID uint32 `json:"userId"`
// Percent 当前加载百分比。
Percent uint32 `json:"percent"`
}
// FightChatState 保存战斗内聊天信息。
type FightChatState struct {
// SenderID 发言玩家 ID。
SenderID uint32 `json:"senderId"`
// SenderNickname 发言玩家昵称。
SenderNickname string `json:"senderNickname"`
// Message 聊天内容。
Message string `json:"message"`
}
// FightStateEnvelope 是统一出站状态结构。
type FightStateEnvelope struct {
// Phase 当前下发阶段,例如 start、skill_hurt、change、over、load、chat。
Phase FightStatePhase `json:"phase"`
// Left 我方阵营当前所有战位快照。
Left []FighterState `json:"left,omitempty"`
// Right 敌方阵营当前所有战位快照。
Right []FighterState `json:"right,omitempty"`
// Meta 当前阶段共用的元数据,如回合号、胜方、结束原因、旧协议命令号。
Meta FightStateMeta `json:"meta"`
// SkillHurt 技能结算阶段附带的详细战报;仅 phase=skill_hurt 时使用。
SkillHurt *FightSkillHurtState `json:"skillHurt,omitempty"`
// Change 切宠阶段附带的切宠详情;仅 phase=change 时使用。
Change *ChangePetInfo `json:"change,omitempty"`
// Load 加载阶段附带的进度信息;仅 phase=load 时使用。
Load *FightLoadState `json:"load,omitempty"`
// Chat 聊天阶段附带的聊天内容;仅 phase=chat 时使用。
Chat *FightChatState `json:"chat,omitempty"`
}

View File

@@ -2,51 +2,64 @@ package input
import (
"blazing/logic/service/fight/info"
"blazing/logic/service/player"
configmodel "blazing/modules/config/model"
playermodel "blazing/modules/player/model"
"strings"
"github.com/gogf/gf/v2/util/grand"
)
// Shuffle 打乱切片顺序,使用 Fisher-Yates 洗牌算法,泛型支持任意类型
func Shuffle[T any](slice []T) {
// 从后往前遍历,逐个交换
for i := len(slice) - 1; i > 0; i-- {
// 生成 0 到 i 之间的随机索引
j := grand.Intn(i + 1)
// 交换 i 和 j 位置的元素
slice[i], slice[j] = slice[j], slice[i]
}
}
func (our *Input) GetAction() {
next := our.Exec(func(t Effect) bool {
return t.HookAction()
})
scriptCtx := buildBossHookActionContext(our, next)
if aiPlayer, ok := our.Player.(*player.AI_player); ok && aiPlayer.BossScript != "" {
scriptBoss := &configmodel.BossConfig{Script: aiPlayer.BossScript}
nextByScript, err := scriptBoss.RunHookActionScript(scriptCtx)
if err != nil {
return
}
next = nextByScript
}
if !next {
return
}
// 获取己方当前宠物和对方当前宠物
if applyBossScriptAction(our, scriptCtx) {
return
}
selfPet := our.FightC.GetCurrPET(our.Player)
//没血就切换精灵
if selfPet == nil {
return
}
if selfPet.Info.Hp <= 0 {
for _, v := range our.AllPet {
if v.Info.Hp > 0 {
// println("AI出手,切换宠物")
our.FightC.ChangePet(our.Player, v.Info.CatchTime)
return
}
}
// 如果没有可用宠物,则直接返回,不执行任何操作
return
}
//oppPet := opp.FightC.GetCurrPET(opp.Player)
skills := selfPet.Skills
// 空技能列表直接返回,避免错误
skills := selfPet.Skills
if len(skills) == 0 {
return
}
//println("AI出手,选择技能")
var usedskill *info.SkillEntity
for _, s := range skills {
if s == nil {
@@ -55,30 +68,25 @@ func (our *Input) GetAction() {
if !s.CanUse() {
continue
}
// 计算技能对对方的伤害假设CalculatePower返回伤害值或需从技能中获取
s.DamageValue = our.CalculatePower(our.Opp, s)
oppPet := our.Opp.CurrentPet()
if oppPet == nil {
continue
}
// 判断是否能秒杀(伤害 >= 对方当前生命值)
if s.DamageValue.Cmp(oppPet.GetHP()) != -1 { // 假设oppPet.HP为对方当前剩余生命值
if s.DamageValue.Cmp(oppPet.GetHP()) != -1 {
if usedskill != nil {
if s.DamageValue.Cmp(usedskill.DamageValue) != -1 {
usedskill = s
}
} else {
usedskill = s
}
}
}
Shuffle(skills)
if usedskill == nil {
for _, s := range skills {
if s == nil {
continue
@@ -87,14 +95,121 @@ func (our *Input) GetAction() {
continue
}
usedskill = s
}
}
if usedskill != nil {
our.FightC.UseSkill(our.Player, uint32(usedskill.XML.ID))
} else {
our.FightC.UseSkill(our.Player, 0)
}
}
func buildBossHookActionContext(our *Input, hookAction bool) *configmodel.BossHookActionContext {
ctx := &configmodel.BossHookActionContext{
HookAction: hookAction,
Action: "auto",
}
ctx.UseSkillFn = func(skillID uint32) {
ctx.Action = "skill"
ctx.SkillID = skillID
}
ctx.SwitchPetFn = func(catchTime uint32) {
ctx.Action = "switch"
ctx.CatchTime = catchTime
}
if our == nil || our.FightC == nil || our.Player == nil {
return ctx
}
overInfo := our.FightC.GetOverInfo()
ctx.Round = overInfo.Round
ctx.IsFirst = our.FightC.IsFirst(our.Player)
if selfPet := our.CurrentPet(); selfPet != nil {
ctx.Our = &configmodel.BossHookPetContext{
PetID: selfPet.Info.ID,
CatchTime: selfPet.Info.CatchTime,
Hp: selfPet.Info.Hp,
MaxHp: selfPet.Info.MaxHp,
}
ctx.Skills = make([]configmodel.BossHookSkillContext, 0, len(selfPet.Skills))
for _, s := range selfPet.Skills {
if s == nil || s.Info == nil {
continue
}
ctx.Skills = append(ctx.Skills, configmodel.BossHookSkillContext{
SkillID: s.Info.ID,
PP: s.Info.PP,
CanUse: s.CanUse(),
})
}
}
if our.AttackValue != nil {
ctx.OurAttack = convertAttackValue(our.AttackValue)
}
if our.Opp != nil {
if oppPet := our.Opp.CurrentPet(); oppPet != nil {
ctx.Opp = &configmodel.BossHookPetContext{
PetID: oppPet.Info.ID,
CatchTime: oppPet.Info.CatchTime,
Hp: oppPet.Info.Hp,
MaxHp: oppPet.Info.MaxHp,
}
}
if our.Opp.AttackValue != nil {
ctx.OppAttack = convertAttackValue(our.Opp.AttackValue)
}
}
return ctx
}
func convertAttackValue(src *playermodel.AttackValue) *configmodel.BossHookAttackContext {
if src == nil {
return nil
}
status := make([]int8, len(src.Status))
for i := range src.Status {
status[i] = src.Status[i]
}
prop := make([]int8, len(src.Prop))
for i := range src.Prop {
prop[i] = src.Prop[i]
}
return &configmodel.BossHookAttackContext{
SkillID: src.SkillID,
AttackTime: src.AttackTime,
IsCritical: src.IsCritical,
LostHp: src.LostHp,
GainHp: src.GainHp,
RemainHp: src.RemainHp,
MaxHp: src.MaxHp,
State: src.State,
Offensive: src.Offensive,
Status: status,
Prop: prop,
}
}
func applyBossScriptAction(our *Input, ctx *configmodel.BossHookActionContext) bool {
if our == nil || ctx == nil {
return false
}
switch strings.ToLower(strings.TrimSpace(ctx.Action)) {
case "", "auto":
return false
case "skill", "use_skill", "useskill":
our.FightC.UseSkill(our.Player, ctx.SkillID)
return true
case "switch", "change_pet", "changepet":
our.FightC.ChangePet(our.Player, ctx.CatchTime)
return true
default:
return false
}
}

View File

@@ -167,6 +167,7 @@ func (our *Input) SortPet() {
t.Duration(-1)
s.ApplyInitEffectBonus(e1)
our.AddEffect(our, t)
}
@@ -338,9 +339,6 @@ func (our *Input) Parseskill(skill *action.SelectSkillAction) {
args := xmlres.EffectArgs[v]
t := our.InitEffect(EffectType.Skill, v, temparg[:args]...)
//这里是给双方添加buff
if t != nil {
// t.SetArgs(our, temparg[:args]...) //设置入参,施加方永远是我方

View File

@@ -0,0 +1,55 @@
package input
// TeamSlots 返回当前输入所在阵营的全部有效战斗位视图。
func (our *Input) TeamSlots() []*Input {
if our == nil {
return nil
}
if len(our.Team) == 0 {
return []*Input{our}
}
slots := make([]*Input, 0, len(our.Team))
for _, teammate := range our.Team {
if teammate == nil {
continue
}
slots = append(slots, teammate)
}
return slots
}
// Teammates 返回队友列表,不包含自己。
func (our *Input) Teammates() []*Input {
if our == nil {
return nil
}
teammates := make([]*Input, 0, len(our.Team))
for _, teammate := range our.TeamSlots() {
if teammate == nil || teammate == our {
continue
}
teammates = append(teammates, teammate)
}
return teammates
}
// LivingTeammates 返回当前仍有存活出战精灵的队友列表。
func (our *Input) LivingTeammates() []*Input {
if our == nil {
return nil
}
teammates := make([]*Input, 0, len(our.Team))
for _, teammate := range our.Teammates() {
currentPet := teammate.CurrentPet()
if currentPet == nil || currentPet.Info.Hp == 0 {
continue
}
teammates = append(teammates, teammate)
}
return teammates
}
// HasLivingTeammate 用于快速判断当前战斗位是否还有存活队友。
func (our *Input) HasLivingTeammate() bool {
return len(our.LivingTeammates()) > 0
}

View File

@@ -0,0 +1,30 @@
package input
import (
"testing"
fightinfo "blazing/logic/service/fight/info"
"blazing/modules/player/model"
)
func TestLivingTeammatesFiltersSelfAndDeadSlots(t *testing.T) {
owner := &Input{CurPet: []*fightinfo.BattlePetEntity{{Info: model.PetInfo{Hp: 10}}}}
aliveMate := &Input{CurPet: []*fightinfo.BattlePetEntity{{Info: model.PetInfo{Hp: 5}}}}
deadMate := &Input{CurPet: []*fightinfo.BattlePetEntity{{Info: model.PetInfo{Hp: 0}}}}
team := []*Input{owner, aliveMate, deadMate}
owner.Team = team
aliveMate.Team = team
deadMate.Team = team
teammates := owner.LivingTeammates()
if len(teammates) != 1 {
t.Fatalf("expected 1 living teammate, got %d", len(teammates))
}
if teammates[0] != aliveMate {
t.Fatalf("expected alive teammate to be returned")
}
if owner.HasLivingTeammate() != true {
t.Fatalf("expected owner to detect living teammate")
}
}

View File

@@ -24,6 +24,26 @@ import (
"github.com/jinzhu/copier"
)
func consumeLimitedPetEffects(pet *model.PetInfo) {
if pet == nil || len(pet.EffectInfo) == 0 {
return
}
next := pet.EffectInfo[:0]
for _, eff := range pet.EffectInfo {
if eff.Status == 2 {
if eff.LeftCount > 0 {
eff.LeftCount--
}
if eff.LeftCount == 0 {
continue
}
}
next = append(next, eff)
}
pet.EffectInfo = next
}
func (f *FightC) battleLoop() {
defer func() {
if err := recover(); err != nil { // 恢复 panicerr 为 panic 错误值
@@ -69,26 +89,28 @@ func (f *FightC) battleLoop() {
tt.Alive(false) //将所有属性变化失效掉
return true
})
if f.Info.Mode != info.BattleMode.PET_MELEE { //不是乱斗,传回血量
for i := 0; i < len(ff.AllPet); i++ {
for j := 0; j < len(ff.Player.GetInfo().PetList); j++ {
if ff.Player.GetInfo().PetList[j].CatchTime == ff.AllPet[i].Info.CatchTime {
if ff.UserID == f.WinnerId {
currentPet := ff.CurrentPet()
if currentPet != nil && currentPet.Info.CatchTime == ff.Player.GetInfo().PetList[j].CatchTime {
f.Winpet = &ff.Player.GetInfo().PetList[j]
}
}
ff.Player.GetInfo().PetList[j].Hp = utils.Min(ff.Player.GetInfo().PetList[j].MaxHp, ff.AllPet[i].Info.Hp)
ff.Player.GetInfo().PetList[j].SkillList = ff.AllPet[i].Info.SkillList
}
for i := 0; i < len(ff.AllPet); i++ {
consumeLimitedPetEffects(&ff.AllPet[i].Info)
for j := 0; j < len(ff.Player.GetInfo().PetList); j++ {
if ff.Player.GetInfo().PetList[j].CatchTime != ff.AllPet[i].Info.CatchTime {
continue
}
}
ff.Player.GetInfo().PetList[j].EffectInfo = ff.AllPet[i].Info.EffectInfo
if f.Info.Mode == info.BattleMode.PET_MELEE {
continue
}
if ff.UserID == f.WinnerId {
currentPet := ff.CurrentPet()
if currentPet != nil && currentPet.Info.CatchTime == ff.Player.GetInfo().PetList[j].CatchTime {
f.Winpet = &ff.Player.GetInfo().PetList[j]
}
}
ff.Player.GetInfo().PetList[j].Hp = utils.Min(ff.Player.GetInfo().PetList[j].MaxHp, ff.AllPet[i].Info.Hp)
ff.Player.GetInfo().PetList[j].SkillList = ff.AllPet[i].Info.SkillList
}
}
})
@@ -162,7 +184,8 @@ func (f *FightC) collectPlayerActions(expectedSlots map[actionSlotKey]struct{})
waitr := time.Duration(f.waittime)*time.Millisecond*10 + 30*time.Second
timeout := time.After(waitr)
timeout := time.NewTimer(waitr)
defer timeout.Stop()
for len(actions) < len(expectedSlots) {
select {
@@ -266,11 +289,12 @@ func (f *FightC) collectPlayerActions(expectedSlots map[actionSlotKey]struct{})
actions[key] = paction
//fmt.Println("玩家执行动作:", pid, paction.Priority())
case <-timeout:
case <-timeout.C:
r := f.handleTimeout(expectedSlots, actions)
if r {
return flattenActionMap(actions)
}
timeout.Reset(waitr)
}
}
@@ -287,7 +311,7 @@ func (f *FightC) handleTimeout(expectedSlots map[actionSlotKey]struct{}, actions
}
player := f.getPlayerByID(key.PlayerID)
if player != nil {
go f.UseSkillAt(player, 0, key.ActorIndex, 0)
f.UseSkillAt(player, 0, key.ActorIndex, 0)
}
}
return false

View File

@@ -154,7 +154,7 @@ func buildFight(opts *fightBuildOptions) (*FightC, errorcode.ErrorCode) {
if ai.CanCapture > 0 {
opp.CanCapture = ai.CanCapture
}
opp.AttackValue.Prop = ai.Prop
ai.ApplyBattleProps(opp.AttackValue)
}
}
}

View File

@@ -28,6 +28,7 @@ const (
queueTTL = 12 * time.Second
banPickTimeout = 45 * time.Second
banPickStartCmd = 2461
battleLevelCap = 100
)
type localQueueTicket struct {
@@ -665,7 +666,11 @@ func resolveBattlePets(catchTimes []uint32, limit int) []model.PetInfo {
if pet == nil || pet.Data.ID == 0 {
continue
}
result = append(result, pet.Data)
petInfo := pet.Data
if petInfo.Level > battleLevelCap {
petInfo.Level = battleLevelCap
}
result = append(result, petInfo)
}
return result
}

View File

@@ -0,0 +1,113 @@
package fight
import (
"blazing/logic/service/fight/info"
"blazing/logic/service/fight/input"
)
// BuildFightStateStartEnvelope 构造开战阶段的统一状态包。
func (f *FightC) BuildFightStateStartEnvelope() info.FightStateEnvelope {
return f.buildFightStateEnvelope(info.FightStatePhaseStart, 2504)
}
// BuildFightStateSkillHurtEnvelope 构造技能结算阶段的统一状态包。
func (f *FightC) BuildFightStateSkillHurtEnvelope() info.FightStateEnvelope {
envelope := f.buildFightStateEnvelope(info.FightStatePhaseSkillHurt, 2505)
envelope.SkillHurt = &info.FightSkillHurtState{
Left: f.collectAttackValues(f.Our),
Right: f.collectAttackValues(f.Opp),
}
return envelope
}
// BuildFightStateChangeEnvelope 构造切宠阶段的统一状态包。
func (f *FightC) BuildFightStateChangeEnvelope(change info.ChangePetInfo) info.FightStateEnvelope {
envelope := f.buildFightStateEnvelope(info.FightStatePhaseChange, 2407)
envelope.Change = &change
return envelope
}
// BuildFightStateOverEnvelope 构造结束阶段的统一状态包。
func (f *FightC) BuildFightStateOverEnvelope() info.FightStateEnvelope {
envelope := f.buildFightStateEnvelope(info.FightStatePhaseOver, 2506)
envelope.Meta.WinnerID = f.FightOverInfo.WinnerId
envelope.Meta.Reason = f.FightOverInfo.Reason
return envelope
}
// BuildFightStateLoadEnvelope 构造加载阶段的统一状态包。
func (f *FightC) BuildFightStateLoadEnvelope(userID, percent uint32) info.FightStateEnvelope {
envelope := f.buildFightStateEnvelope(info.FightStatePhaseLoad, 2441)
envelope.Load = &info.FightLoadState{
UserID: userID,
Percent: percent,
}
return envelope
}
// BuildFightStateChatEnvelope 构造聊天阶段的统一状态包。
func (f *FightC) BuildFightStateChatEnvelope(senderID uint32, senderNickname, message string) info.FightStateEnvelope {
envelope := f.buildFightStateEnvelope(info.FightStatePhaseChat, 50002)
envelope.Chat = &info.FightChatState{
SenderID: senderID,
SenderNickname: senderNickname,
Message: message,
}
return envelope
}
// buildFightStateEnvelope 组装左右两侧通用快照和元数据。
func (f *FightC) buildFightStateEnvelope(phase info.FightStatePhase, legacyCmd uint32) info.FightStateEnvelope {
if f == nil {
return info.FightStateEnvelope{Phase: phase}
}
return info.FightStateEnvelope{
Phase: phase,
Left: snapshotFighterStates(SideOur, f.Our),
Right: snapshotFighterStates(SideOpp, f.Opp),
Meta: info.FightStateMeta{
Round: uint32(f.Round),
WinnerID: f.FightOverInfo.WinnerId,
Reason: f.FightOverInfo.Reason,
LegacyCmd: legacyCmd,
},
}
}
// snapshotFighterStates 把指定侧的战斗位数组转成统一 fighter 快照。
func snapshotFighterStates(side int, fighters []*input.Input) []info.FighterState {
states := make([]info.FighterState, 0, len(fighters))
for position, fighter := range fighters {
if fighter == nil {
continue
}
state := info.FighterState{
Side: side,
Position: position,
}
if fighter.Player != nil && fighter.Player.GetInfo() != nil {
state.UserID = fighter.Player.GetInfo().UserID
}
if fighter.AttackValue != nil {
state.Status = fighter.AttackValue.Status
state.Prop = fighter.AttackValue.Prop
}
currentPet := fighter.CurrentPet()
if currentPet == nil {
states = append(states, state)
continue
}
state.ControllerUserID = currentPet.ControllerUserID
state.PetID = currentPet.Info.ID
state.CatchTime = currentPet.Info.CatchTime
state.Name = currentPet.Info.Name
state.HP = currentPet.Info.Hp
state.MaxHP = currentPet.Info.MaxHp
state.Level = currentPet.Info.Level
if len(currentPet.Info.SkillList) > 0 {
state.Skills = append(state.Skills, currentPet.Info.SkillList...)
}
states = append(states, state)
}
return states
}

View File

@@ -0,0 +1,113 @@
package fight
import (
"testing"
"blazing/common/socket/errorcode"
"blazing/logic/service/common"
fightinfo "blazing/logic/service/fight/info"
"blazing/logic/service/fight/input"
spaceinfo "blazing/logic/service/space/info"
"blazing/modules/player/model"
)
type stubPlayer struct {
info model.PlayerInfo
}
func (*stubPlayer) ApplyPetDisplayInfo(*spaceinfo.SimpleInfo) {}
func (*stubPlayer) GetPlayerCaptureContext() *fightinfo.PlayerCaptureContext { return nil }
func (*stubPlayer) Roll(int, int) (bool, float64, float64) { return false, 0, 0 }
func (*stubPlayer) Getfightinfo() fightinfo.Fightinfo { return fightinfo.Fightinfo{} }
func (*stubPlayer) ItemAdd(int64, int64) bool { return false }
func (p *stubPlayer) GetInfo() *model.PlayerInfo { return &p.info }
func (*stubPlayer) InvitePlayer(common.PlayerI) {}
func (*stubPlayer) SetFightC(common.FightI) {}
func (*stubPlayer) QuitFight() {}
func (*stubPlayer) MessWin(bool) {}
func (*stubPlayer) CanFight() errorcode.ErrorCode { return 0 }
func (*stubPlayer) SendPackCmd(uint32, any) {}
func (*stubPlayer) GetPetInfo(uint32) []model.PetInfo { return nil }
func TestFightActionEnvelopeEncodedTargetIndex(t *testing.T) {
self := NewSkillActionEnvelope(1, 2, 0, SkillTargetSelf, 0)
if got := self.EncodedTargetIndex(); got != EncodeTargetIndex(2, false) {
t.Fatalf("expected self target to encode actor slot, got %d", got)
}
ally := NewSkillActionEnvelope(1, 0, 1, SkillTargetAlly, 0)
if got := ally.EncodedTargetIndex(); got != EncodeTargetIndex(1, false) {
t.Fatalf("expected ally target to keep friendly slot, got %d", got)
}
fallbackSelf := NewSkillActionEnvelope(1, 3, 0, 9, 3)
if got := fallbackSelf.EncodedTargetIndex(); got != EncodeTargetIndex(3, false) {
t.Fatalf("expected atkType=3 to fall back to self target, got %d", got)
}
opponent := NewSkillActionEnvelope(1, 0, 2, SkillTargetOpponent, 0)
if got := opponent.EncodedTargetIndex(); got != EncodeTargetIndex(2, true) {
t.Fatalf("expected opponent target to stay on opposite side, got %d", got)
}
}
func TestBuildFightStateStartEnvelope(t *testing.T) {
ourPlayer := &stubPlayer{info: model.PlayerInfo{UserID: 1001}}
oppPlayer := &stubPlayer{info: model.PlayerInfo{UserID: 2002}}
our := input.NewInput(nil, ourPlayer)
our.InitAttackValue()
our.AttackValue.Prop[0] = 2
our.AttackValue.Status[1] = 1
ourPet := fightinfo.CreateBattlePetEntity(model.PetInfo{
ID: 11,
Name: "Alpha",
Level: 20,
Hp: 88,
MaxHp: 100,
CatchTime: 101,
SkillList: []model.SkillInfo{{ID: 300, PP: 10}},
})
ourPet.BindController(ourPlayer.info.UserID)
our.SetCurPetAt(0, ourPet)
opp := input.NewInput(nil, oppPlayer)
opp.InitAttackValue()
oppPet := fightinfo.CreateBattlePetEntity(model.PetInfo{
ID: 22,
Name: "Beta",
Level: 21,
Hp: 77,
MaxHp: 110,
CatchTime: 202,
SkillList: []model.SkillInfo{{ID: 400, PP: 5}},
})
oppPet.BindController(oppPlayer.info.UserID)
opp.SetCurPetAt(0, oppPet)
fc := &FightC{
Our: []*input.Input{our},
Opp: []*input.Input{opp},
}
fc.Round = 7
envelope := fc.BuildFightStateStartEnvelope()
if envelope.Phase != fightinfo.FightStatePhaseStart {
t.Fatalf("expected start phase, got %s", envelope.Phase)
}
if envelope.Meta.Round != 7 {
t.Fatalf("expected round 7, got %d", envelope.Meta.Round)
}
if len(envelope.Left) != 1 || len(envelope.Right) != 1 {
t.Fatalf("expected one fighter on each side, got left=%d right=%d", len(envelope.Left), len(envelope.Right))
}
if envelope.Left[0].UserID != 1001 || envelope.Left[0].PetID != 11 {
t.Fatalf("unexpected left fighter snapshot: %+v", envelope.Left[0])
}
if envelope.Left[0].Prop[0] != 2 || envelope.Left[0].Status[1] != 1 {
t.Fatalf("expected prop/status snapshot to be copied, got %+v %+v", envelope.Left[0].Prop, envelope.Left[0].Status)
}
if envelope.Right[0].UserID != 2002 || envelope.Right[0].CatchTime != 202 {
t.Fatalf("unexpected right fighter snapshot: %+v", envelope.Right[0])
}
}

View File

@@ -2,6 +2,7 @@ package item
import (
"blazing/common/data/xmlres"
"blazing/common/socket/errorcode"
"blazing/common/utils"
"blazing/modules/player/model"
"strings"
@@ -26,6 +27,11 @@ type SetHandler struct {
Handler PetItemHandler
}
var fallbackPetItemNewSeIdx = map[uint32]int{
300741: 1103, // 瞬杀能量珠
300854: 1103, // 瞬杀能量珠Ω
}
// PetItemHandlerRegistry 道具处理器注册器
type PetItemHandlerRegistry struct {
exactHandlers map[uint32]PetItemHandler // 精确ID映射
@@ -111,6 +117,91 @@ func nvfunc(itemid uint32, onpet *model.PetInfo) bool {
return true
}
func resolvePetItemNewSeIdx(itemid uint32) (itemCfg xmlres.Item, newSeIdx int, ok bool) {
itemCfg, ok = xmlres.ItemsMAP[int(itemid)]
if ok && itemCfg.NewSeIdx != 0 {
return itemCfg, itemCfg.NewSeIdx, true
}
if newSeIdx, ok = fallbackPetItemNewSeIdx[itemid]; ok {
return itemCfg, newSeIdx, true
}
for idx, effectCfg := range xmlres.EffectMAP {
if effectCfg.ItemId == nil || gconv.Uint32(*effectCfg.ItemId) != itemid {
continue
}
return itemCfg, idx, true
}
return itemCfg, 0, false
}
func handleNewSeIdxPetItem(itemid uint32, onpet *model.PetInfo) errorcode.ErrorCode {
itemCfg, newSeIdx, ok := resolvePetItemNewSeIdx(itemid)
if ok && newSeIdx == 0 {
if itemCfg.MaxHPUp > 0 {
if !onpet.AddMaxHPUpEffect(itemid, itemCfg.MaxHPUp) {
return errorcode.ErrorCodes.ErrCannotInjectPillAgain
}
return 0
}
return errorcode.ErrorCodes.ErrItemUnusable
}
if !ok {
return errorcode.ErrorCodes.ErrItemUnusable
}
effectCfg, ok := xmlres.EffectMAP[newSeIdx]
if !ok {
return errorcode.ErrorCodes.ErrSystemError
}
effectStatus := byte(gconv.Int(effectCfg.Stat))
effectIdx := uint16(newSeIdx)
leftCount := 1
if effectCfg.Times != nil && *effectCfg.Times != "" {
leftCount = gconv.Int(*effectCfg.Times)
if leftCount <= 0 {
leftCount = 1
}
}
limitedCount := 0
for _, eff := range onpet.EffectInfo {
if eff.Idx == effectIdx {
return errorcode.ErrorCodes.ErrCannotInjectPillAgain
}
if eff.Status == 2 {
limitedCount++
}
}
if effectStatus == 2 && limitedCount >= 2 {
return errorcode.ErrorCodes.ErrTooManyEnergyOrbs
}
onpet.EffectInfo = append(onpet.EffectInfo, model.PetEffectInfo{
ItemID: itemid,
Idx: effectIdx,
Status: effectStatus,
LeftCount: byte(leftCount),
EID: uint16(gconv.Int(effectCfg.Eid)),
Args: effectCfg.ArgsS,
})
return 0
}
func (r *PetItemHandlerRegistry) Handle(itemID uint32, onpet *model.PetInfo) errorcode.ErrorCode {
handler := r.GetHandler(itemID)
if handler != nil {
if handler(itemID, onpet) {
return 0
}
return errorcode.ErrorCodes.ErrItemUnusable
}
return handleNewSeIdxPetItem(itemID, onpet)
}
// -------------------------- 6. 初始化注册器(注册所有处理器) --------------------------
func init() {

View File

@@ -2,7 +2,8 @@ package player
import (
"blazing/common/utils"
"blazing/modules/config/model"
configmodel "blazing/modules/config/model"
playermodel "blazing/modules/player/model"
"sync/atomic"
"time"
@@ -10,15 +11,17 @@ import (
"github.com/samber/lo"
)
func (p *Player) IsMatch(t model.Event) bool {
_, ok := lo.Find(t.Weather, func(item int32) bool {
return item == int32(p.GetSpace().MapBossSInfo.Wer)
})
if !ok {
// 不在同一天气下
return false
func (p *Player) IsMatch(t configmodel.Event) bool {
if len(t.Weather) > 0 {
_, ok := lo.Find(t.Weather, func(item int32) bool {
return item == int32(p.GetSpace().MapBossSInfo.Wer)
})
if !ok {
// 不在同一天气下
return false
}
}
if t.StartTime != "" && t.EndTime != "" {
ok, _ := utils.IsCurrentTimeInRange(t.StartTime, t.EndTime)
if !ok {
@@ -26,8 +29,74 @@ func (p *Player) IsMatch(t model.Event) bool {
}
}
return true
if len(t.Week) > 0 {
week := int32(time.Now().Weekday())
if week == 0 {
week = 7
}
_, ok := lo.Find(t.Week, func(item int32) bool {
return item == week
})
if !ok {
return false
}
}
if len(t.Sprites) > 0 && !matchPetIDInList(t.Sprites, p.Info.PetList, p.Info.BackupPetList) {
return false
}
if len(t.FirstSprites) > 0 {
if len(p.Info.PetList) == 0 {
return false
}
firstPetID := int32(p.Info.PetList[0].ID)
_, ok := lo.Find(t.FirstSprites, func(item int32) bool {
return item == firstPetID
})
if !ok {
return false
}
}
if len(t.MustTask) > 0 {
for _, taskID := range t.MustTask {
if p.Info.GetTask(int(taskID)) != playermodel.Completed {
return false
}
}
}
if len(t.MustItem) > 0 {
if p.Service == nil || p.Service.Item == nil {
return false
}
for _, itemID := range t.MustItem {
if p.Service.Item.CheakItem(uint32(itemID)) <= 0 {
return false
}
}
}
return true
}
func matchPetIDInList(targetIDs []int32, petLists ...[]playermodel.PetInfo) bool {
for _, pets := range petLists {
for _, pet := range pets {
petID := int32(pet.ID)
_, ok := lo.Find(targetIDs, func(item int32) bool {
return item == petID
})
if ok {
return true
}
}
}
return false
}
// 应该根据怪物信息决定后端生成

View File

@@ -4,5 +4,5 @@ type AI_player struct {
baseplayer
CanCapture int
BossScript string
}

View File

@@ -44,6 +44,27 @@ func (p *baseplayer) GetPetInfo(limitlevel uint32) []model.PetInfo {
}
return ret
}
func (p *baseplayer) AddBattleProp(index int, level int8) {
if p == nil || index < 0 || index >= len(p.Prop) || level == 0 {
return
}
p.Prop[index] += level
if p.Prop[index] > 6 {
p.Prop[index] = 6
}
if p.Prop[index] < -6 {
p.Prop[index] = -6
}
}
func (p *baseplayer) ApplyBattleProps(target *model.AttackValue) {
if p == nil || target == nil {
return
}
target.Prop = p.Prop
}
func (f *baseplayer) InvitePlayer(ff common.PlayerI) {
}

View File

@@ -7,6 +7,7 @@ import (
"encoding/binary"
"encoding/hex"
"sync"
"sync/atomic"
"context"
@@ -22,6 +23,11 @@ import (
"github.com/panjf2000/gnet/v2"
)
const (
minPacketLen = 17
maxPacketLen = 10 * 1024
)
// getUnderlyingValue 递归解析reflect.Value解包指针、interface{}到底层具体类型
func getUnderlyingValue(val reflect.Value) (reflect.Value, error) {
for {
@@ -47,6 +53,44 @@ func getUnderlyingValue(val reflect.Value) (reflect.Value, error) {
}
}
func setFieldByIndex(root reflect.Value, index []int, value reflect.Value) bool {
current := root
for pos, idx := range index {
if current.Kind() == reflect.Ptr {
if current.IsNil() {
current.Set(reflect.New(current.Type().Elem()))
}
current = current.Elem()
}
if current.Kind() != reflect.Struct || idx < 0 || idx >= current.NumField() {
return false
}
field := current.Field(idx)
if pos == len(index)-1 {
if !field.CanSet() {
return false
}
if value.Type().AssignableTo(field.Type()) {
field.Set(value)
return true
}
if field.Kind() == reflect.Ptr && value.Type().AssignableTo(field.Type().Elem()) {
ptr := reflect.New(field.Type().Elem())
ptr.Elem().Set(value)
field.Set(ptr)
return true
}
return false
}
current = field
}
return false
}
// XORDecryptU 原地执行异或解密,避免额外分配和拷贝。
func XORDecryptU(encryptedData []byte, key uint32) []byte {
if len(encryptedData) == 0 {
@@ -102,6 +146,16 @@ func putPacketData(buf []byte) {
}
func (h *ClientData) PushEvent(v []byte, submit func(task func()) error) {
if h == nil || h.IsClosed() {
return
}
if len(v) < minPacketLen || len(v) > maxPacketLen {
return
}
if binary.BigEndian.Uint32(v[0:4]) != uint32(len(v)) {
return
}
var header common.TomeeHeader
header.Len = binary.BigEndian.Uint32(v[0:4])
header.CMD = binary.BigEndian.Uint32(v[5:9])
@@ -111,9 +165,18 @@ func (h *ClientData) PushEvent(v []byte, submit func(task func()) error) {
copy(header.Data, v[17:])
}
_ = submit(func() {
h.LF.Producer().Write(header)
err := submit(func() {
if h.IsClosed() || h.LF == nil || !h.LF.Running() {
putPacketData(header.Data)
return
}
if err := h.LF.Producer().Write(header); err != nil {
putPacketData(header.Data)
}
})
if err != nil {
putPacketData(header.Data)
}
}
// 重写
@@ -168,45 +231,38 @@ func (h *ClientData) OnEvent(data common.TomeeHeader) {
return //TODO 待实现cmd未注册
}
params := []reflect.Value{}
var ptrValue reflect.Value
if cmdlister.NewReqValue != nil {
ptrValue = cmdlister.NewReqValue()
} else {
ptrValue = reflect.New(cmdlister.Req)
}
//funct := cmdlister.Type().NumIn()
// 如果需要可设置的变量(用于修改值),创建指针并解引用
ptrValue := reflect.New(cmdlister.Req)
// fmt.Println(tt1)
if data.Res != nil {
tt1 := ptrValue.Elem().Addr().Interface()
err := struc.Unpack(bytes.NewBuffer(data.Res), tt1)
err := struc.Unpack(bytes.NewBuffer(data.Res), ptrValue.Interface())
if err != nil {
cool.Logger.Error(context.Background(), data.UserID, data.CMD, "解包失败,", err, hex.EncodeToString(data.Res))
//fmt.Println(data.UserID, data.CMD, "解包失败,", hex.EncodeToString(data.Data))
data.Result = uint32(errorcode.ErrorCodes.ErrSystemProcessingError)
h.SendPack(data.Pack(nil))
return
}
}
ptrValue1 := ptrValue.Elem().Addr()
// 设置 Name 字段
nameField := ptrValue.Elem().Field(0) //首个为header
nameField.Set(reflect.ValueOf(data))
if data.CMD > 1001 { //if cmdlister.Type().In(1) == reflect.TypeOf(&Player{}) {
//t := GetPlayer(c, data.UserID)
// fmt.Println(data.CMD, "接收 变量的地址 ", &t.Info, t.Info.UserID)
params = append(params, ptrValue1, reflect.ValueOf(h.Player))
} else {
params = append(params, ptrValue1, reflect.ValueOf(h.Conn))
if !setFieldByIndex(ptrValue.Elem(), cmdlister.HeaderFieldIndex, reflect.ValueOf(data)) {
cool.Logger.Warning(context.Background(), data.UserID, data.CMD, "设置请求头失败")
return
}
ret := cmdlister.Func.Call(params)
var params [2]reflect.Value
params[0] = ptrValue
if cmdlister.UseConn {
params[1] = reflect.ValueOf(h.Conn)
} else {
params[1] = reflect.ValueOf(h.Player)
}
ret := cmdlister.Func.Call(params[:])
if len(ret) <= 0 { //如果判断没有参数,那就说明这个包没有返回参数
return
@@ -235,12 +291,34 @@ func (h *ClientData) OnEvent(data common.TomeeHeader) {
}
type ClientData struct {
IsCrossDomain sync.Once //是否跨域过
Player *Player //客户实体
ERROR_CONNUT int
Wsmsg *WsCodec
Conn gnet.Conn
LF *lockfree.Lockfree[common.TomeeHeader]
Player *Player //客户实体
ERROR_CONNUT int
Wsmsg *WsCodec
Conn gnet.Conn
LF *lockfree.Lockfree[common.TomeeHeader]
closed int32
crossDomainChecked uint32
}
func (p *ClientData) IsClosed() bool {
return atomic.LoadInt32(&p.closed) == 1
}
func (p *ClientData) Close() {
if !atomic.CompareAndSwapInt32(&p.closed, 0, 1) {
return
}
if p.LF != nil && p.LF.Running() {
_ = p.LF.Close()
}
}
func (p *ClientData) IsCrossDomainChecked() bool {
return atomic.LoadUint32(&p.crossDomainChecked) == 1
}
func (p *ClientData) MarkCrossDomainChecked() {
atomic.StoreUint32(&p.crossDomainChecked, 1)
}
func (p *ClientData) GetPlayer(userid uint32) *Player { //TODO 这里待优化,可能存在内存泄漏问题

View File

@@ -1,13 +1,16 @@
package player
import "blazing/common/data/share"
func KickPlayer(userid uint32) error { //踢出玩家
//TODO 返回错误码
//var player *entity.Player
if player1, ok := Mainplayer.Load(userid); ok {
player1.Player.Kick(false)
return nil
}
//return player
// 已不在本服在线列表,视为离线并清理僵尸在线标记
_ = share.ShareManager.DeleteUserOnline(userid)
return nil
}

View File

@@ -2,6 +2,7 @@ package player
import (
"bytes"
"encoding/binary"
"errors"
"io"
@@ -11,12 +12,18 @@ import (
"github.com/panjf2000/gnet/v2/pkg/logging"
)
const (
minTCPPacketLen = 17
maxTCPPacketLen = 10 * 1024
tomeeVersion = 49
)
type WsCodec struct {
Tcp bool
Upgraded bool // 链接是否升级
Buf bytes.Buffer // 从实际socket中读取到的数据缓存
wsMsgBuf wsMessageBuf // ws 消息缓存
//Isinitws bool
Tcp bool
Upgraded bool // 链接是否升级
Buf bytes.Buffer // 从实际socket中读取到的数据缓存
wsMsgBuf wsMessageBuf // ws 消息缓存
bufferedInbound int // 已镜像到 Buf 中的 inbound 字节数
}
type wsMessageBuf struct {
@@ -24,92 +31,115 @@ type wsMessageBuf struct {
cachedBuf bytes.Buffer
}
type UpgradeState uint8
const (
UpgradeNeedMoreData UpgradeState = iota
UpgradeUseTCP
UpgradeUseWS
)
type readWrite struct {
io.Reader
io.Writer
}
func CompareLeftBytes(array1, array2 []byte, leftBytesCount int) bool {
// 检查切片长度是否足够比较左边的字节
if len(array1) < leftBytesCount || len(array2) < leftBytesCount {
return false
}
// 提取左边的字节切片
left1 := array1[:leftBytesCount]
left2 := array2[:leftBytesCount]
// 比较左边的字节切片
for i := 0; i < leftBytesCount; i++ {
if left1[i] != left2[i] {
return false
}
}
return true
}
func (w *WsCodec) Upgrade(c gnet.Conn) (ok bool, action gnet.Action) {
func (w *WsCodec) Upgrade(c gnet.Conn) (state UpgradeState, action gnet.Action) {
if w.Upgraded {
ok = true
state = UpgradeUseWS
return
}
if w.Tcp {
ok = false
state = UpgradeUseTCP
return
}
buf := &w.Buf
if CompareLeftBytes(buf.Bytes(), []byte{0, 0}, 2) {
w.Tcp = true
return
}
tmpReader := bytes.NewReader(buf.Bytes())
oldLen := tmpReader.Len()
//logging.Infof("do Upgrade")
buf := w.Buf.Bytes()
if looksLikeTCPPacket(buf) {
w.SwitchToTCP()
state = UpgradeUseTCP
return
}
if len(buf) == 0 {
state = UpgradeNeedMoreData
return
}
tmpReader := bytes.NewReader(buf)
oldLen := tmpReader.Len()
hs, err := ws.Upgrade(readWrite{tmpReader, c})
skipN := oldLen - tmpReader.Len()
if err != nil {
if err == io.EOF || errors.Is(err, io.ErrUnexpectedEOF) { //数据不完整,不跳过 buf 中的 skipN 字节(此时 buf 中存放的仅是部分 "handshake data" bytes下次再尝试读取
if err == io.EOF || errors.Is(err, io.ErrUnexpectedEOF) {
state = UpgradeNeedMoreData
return
}
buf.Next(skipN)
w.Buf.Next(skipN)
logging.Errorf("conn[%v] [err=%v]", c.RemoteAddr().String(), err.Error())
action = gnet.Close
//ok = true
//w.Tcp = true
return
}
buf.Next(skipN)
logging.Infof("conn[%v] upgrade websocket protocol! Handshake: %v", c.RemoteAddr().String(), hs)
ok = true
w.Buf.Next(skipN)
logging.Infof("conn[%v] upgrade websocket protocol! Handshake: %v", c.RemoteAddr().String(), hs)
w.Upgraded = true
state = UpgradeUseWS
return
}
func looksLikeTCPPacket(buf []byte) bool {
if len(buf) < 4 {
return false
}
packetLen := binary.BigEndian.Uint32(buf[:4])
if packetLen < minTCPPacketLen || packetLen > maxTCPPacketLen {
return false
}
if len(buf) >= 5 && buf[4] != tomeeVersion {
return false
}
return true
}
func (w *WsCodec) ReadBufferBytes(c gnet.Conn) (gnet.Action, int) {
size := c.InboundBuffered()
//buf := make([]byte, size)
if size < w.bufferedInbound {
w.bufferedInbound = 0
}
if size == w.bufferedInbound {
return gnet.None, size
}
read, err := c.Peek(size)
if err != nil {
logging.Errorf("read err! %v", err)
return gnet.Close, 0
}
// if read < size {
// logging.Errorf("read bytes len err! size: %d read: %d", size, read)
// return gnet.Close
// }
w.Buf.Write(read)
w.Buf.Write(read[w.bufferedInbound:])
w.bufferedInbound = size
return gnet.None, size
}
func (w *WsCodec) ResetInboundMirror() {
w.bufferedInbound = 0
}
func (w *WsCodec) SwitchToTCP() {
w.Tcp = true
w.Upgraded = false
w.bufferedInbound = 0
w.Buf.Reset()
w.wsMsgBuf.curHeader = nil
w.wsMsgBuf.cachedBuf.Reset()
}
func (w *WsCodec) Decode(c gnet.Conn) (outs []wsutil.Message, err error) {
// fmt.Println("do Decode")
messages, err := w.readWsMessages()
if err != nil {
logging.Errorf("Error reading message! %v", err)
return nil, err
}
if len(messages) <= 0 { //没有读到完整数据 不处理
if len(messages) <= 0 {
return
}
for _, message := range messages {
@@ -131,9 +161,8 @@ func (w *WsCodec) readWsMessages() (messages []wsutil.Message, err error) {
msgBuf := &w.wsMsgBuf
in := &w.Buf
for {
// 从 in 中读出 header并将 header bytes 写入 msgBuf.cachedBuf
if msgBuf.curHeader == nil {
if in.Len() < ws.MinHeaderSize { //头长度至少是2
if in.Len() < ws.MinHeaderSize {
return
}
var head ws.Header
@@ -142,13 +171,13 @@ func (w *WsCodec) readWsMessages() (messages []wsutil.Message, err error) {
if err != nil {
return messages, err
}
} else { //有可能不完整,构建新的 reader 读取 head读取成功才实际对 in 进行读操作
} else {
tmpReader := bytes.NewReader(in.Bytes())
oldLen := tmpReader.Len()
head, err = ws.ReadHeader(tmpReader)
skipN := oldLen - tmpReader.Len()
if err != nil {
if err == io.EOF || errors.Is(err, io.ErrUnexpectedEOF) { //数据不完整
if err == io.EOF || errors.Is(err, io.ErrUnexpectedEOF) {
return messages, nil
}
in.Next(skipN)
@@ -163,21 +192,19 @@ func (w *WsCodec) readWsMessages() (messages []wsutil.Message, err error) {
return nil, err
}
}
dataLen := (int)(msgBuf.curHeader.Length)
// 从 in 中读出 data并将 data bytes 写入 msgBuf.cachedBuf
if dataLen > 0 {
if in.Len() < dataLen { //数据不完整
dataLen := int(msgBuf.curHeader.Length)
if dataLen > 0 {
if in.Len() < dataLen {
logging.Infof("incomplete data")
return
}
_, err = io.CopyN(&msgBuf.cachedBuf, in, int64(dataLen))
if err != nil {
return
}
}
if msgBuf.curHeader.Fin { //当前 header 已经是一个完整消息
if msgBuf.curHeader.Fin {
messages, err = wsutil.ReadClientMessage(&msgBuf.cachedBuf, messages)
if err != nil {
return nil, err

View File

@@ -45,6 +45,7 @@ type Space struct {
IsTime bool
DropItemIds []uint32
PitS *csmap.CsMap[int, []model.MapPit]
MapNodeS *csmap.CsMap[uint32, *model.MapNode]
}
func NewSpace() *Space {
@@ -52,6 +53,7 @@ func NewSpace() *Space {
ret := &Space{
User: csmap.New[uint32, common.PlayerI](),
UserInfo: csmap.New[uint32, info.SimpleInfo](),
MapNodeS: csmap.New[uint32, *model.MapNode](),
}
return ret
@@ -185,12 +187,20 @@ func (ret *Space) init() {
}
ret.MapBossSInfo = info.MapModelBroadcastInfo{}
ret.MapBossSInfo.INFO = make([]info.MapModelBroadcastEntry, 0)
mapNodes := service.NewMapNodeService().GetData(ret.ID)
for i := range mapNodes {
ret.MapNodeS.Store(mapNodes[i].NodeID, &mapNodes[i])
}
if len(r.WeatherType) > 1 {
ret.WeatherType = r.WeatherType
cool.Cron.CustomFunc(ret, ret.GenWer)
}
for _, v := range service.NewMapNodeService().GetDataB(ret.ID) {
for _, v := range mapNodes {
if v.IsBroadcast == 0 {
continue
}
r := service.NewMapmodelService().GetDataByModelId(v.IsBroadcast)
if r == nil {
@@ -220,15 +230,29 @@ func (ret *Space) init() {
}
}
func (p *Space) IsMatch(t model.Event) bool {
_, ok := lo.Find(t.Weather, func(item int32) bool {
return item == int32(p.MapBossSInfo.Wer)
})
if !ok {
return false
func (p *Space) GetMatchedMapNode(nodeID uint32) *model.MapNode {
if p == nil || p.MapNodeS == nil {
return nil
}
mapNode, ok := p.MapNodeS.Load(nodeID)
if !ok || mapNode == nil || mapNode.Event == nil || !p.IsMatch(*mapNode.Event) {
return nil
}
return mapNode
}
func (p *Space) IsMatch(t model.Event) bool {
if len(t.Weather) > 0 {
_, ok := lo.Find(t.Weather, func(item int32) bool {
return item == int32(p.MapBossSInfo.Wer)
})
if !ok {
return false
}
}
if t.StartTime != "" && t.EndTime != "" {
ok, _ := utils.IsCurrentTimeInRange(t.StartTime, t.EndTime)
if !ok {
@@ -236,6 +260,20 @@ func (p *Space) IsMatch(t model.Event) bool {
}
}
if len(t.Week) > 0 {
week := int32(time.Now().Weekday())
if week == 0 {
week = 7
}
_, ok := lo.Find(t.Week, func(item int32) bool {
return item == week
})
if !ok {
return false
}
}
return true
}