refactor(fight): 重构战斗相关代码

- 移除未使用的战斗模式枚举和相关代码
- 更新 BurnEffect 结构,增加生命周期管理
- 删除多余的 Skill 结构和 Effect 相关代码
- 调整 NoteReadyToFightInfo 结构的位置
This commit is contained in:
2025-08-25 04:23:32 +08:00
parent 043cef2b64
commit 5ba81e2f90
22 changed files with 997 additions and 437 deletions

View File

@@ -0,0 +1,20 @@
package skill
import (
"fmt"
"testing"
"github.com/ECUST-XX/xml"
)
func Test_main(t *testing.T) {
// 解析XML到结构体
var maps MovesTbl
t1, _ := getxml()
xml.Unmarshal(t1, &maps)
//tf, _ := xml.MarshalIndentShortForm(tt, " ", " ")
fmt.Println(maps.Moves[4].SideEffect)
fmt.Println(maps.Moves[4].SideEffectArg)
}

View File

@@ -0,0 +1,117 @@
package skill
import (
"encoding/xml"
"fmt"
"io"
"log"
"net/http"
"time"
)
// MovesTbl 定义 XML 根元素
type MovesTbl struct {
XMLName xml.Name `xml:"MovesTbl"`
Moves []Move `xml:"Moves>Move"`
EFF []SideEffect `xml:"SideEffects>SideEffect"`
}
type MovesMap struct {
XMLName xml.Name `xml:"MovesTbl"`
Moves map[int64]Move
EFF []SideEffect `xml:"SideEffects>SideEffect"`
}
// Move 定义单个技能的结构
type Move struct {
ID int `xml:"ID,attr"`
Name string `xml:"Name,attr"`
Category int `xml:"Category,attr"`
Type int `xml:"Type,attr"`
Power int `xml:"Power,attr"`
MaxPP int `xml:"MaxPP,attr"`
Accuracy int `xml:"Accuracy,attr"`
CritRate int `xml:"CritRate,attr,omitempty"`
Priority int `xml:"Priority,attr,omitempty"`
MustHit int `xml:"MustHit,attr,omitempty"`
SwapElemType int `xml:"SwapElemType,attr,omitempty"`
CopyElemType int `xml:"CopyElemType,attr,omitempty"`
CritAtkFirst int `xml:"CritAtkFirst,attr,omitempty"`
CritAtkSecond int `xml:"CritAtkSecond,attr,omitempty"`
CritSelfHalfHp int `xml:"CritSelfHalfHp,attr,omitempty"`
CritFoeHalfHp int `xml:"CritFoeHalfHp,attr,omitempty"`
DmgBindLv int `xml:"DmgBindLv,attr,omitempty"`
PwrBindDv int `xml:"PwrBindDv,attr,omitempty"`
PwrDouble int `xml:"PwrDouble,attr,omitempty"`
SideEffect string `xml:"SideEffect,attr,omitempty"`
SideEffectArg string `xml:"SideEffectArg,attr,omitempty"`
AtkNum int `xml:"AtkNum,attr,omitempty"`
Url string `xml:"Url,attr,omitempty"`
Info string `xml:"info,attr,omitempty"`
CD int `xml:"CD,attr"`
}
type SideEffect struct {
ID int `xml:"ID,attr"`
Help string `xml:"help,attr"`
Des string `xml:"des,attr"`
}
// ReadHTTPFile 通过HTTP GET请求获取远程文件内容
// url: 远程文件的URL地址
// 返回文件内容字节流和可能的错误
func ReadHTTPFile(url string) ([]byte, error) {
// 创建HTTP客户端并设置超时时间避免无限等待
client := &http.Client{
Timeout: 30 * time.Second, // 30秒超时
}
// 发送GET请求
resp, err := client.Get(url)
if err != nil {
return nil, fmt.Errorf("请求失败: %w", err)
}
defer resp.Body.Close() // 确保响应体被关闭
// 检查响应状态码
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("请求返回非成功状态码: %d", resp.StatusCode)
}
// 读取响应体内容
content, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("读取内容失败: %w", err)
}
return content, nil
}
func getxml() ([]byte, error) {
// 读取整个文件内容,返回字节切片和错误
content, err := ReadHTTPFile("http://127.0.0.1:8080/assets/227.xml")
if err != nil {
// 处理错误(文件不存在、权限问题等)
log.Fatalf("无法读取文件: %v", err)
}
return content, nil
}
func getMoves() MovesMap {
// 解析XML到结构体
var maps MovesTbl
t1, _ := getxml()
xml.Unmarshal(t1, &maps)
var mapss MovesMap
mapss.Moves = make(map[int64]Move, 0)
for _, v := range maps.Moves {
mapss.Moves[int64(v.ID)] = v
}
return mapss
}
// 全局函数配置
var MovesConfig = getMoves()

View File

@@ -5,18 +5,19 @@ import (
"blazing/common/socket/errorcode"
"blazing/common/socket/handler"
"blazing/logic/service/fight"
"blazing/logic/service/fight/info"
"blazing/modules/blazing/model"
)
func (h Controller) OnPlayerFightNpcMonster(data *fight.FightNpcMonsterInboundInfo, c *entity.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
c.IsFighting = true
t1 := handler.NewTomeeHeader(2503, c.UserID)
ttt := fight.NoteReadyToFightInfo{
ttt := info.NoteReadyToFightInfo{
FightId: 3,
}
ttt.OurInfo = fight.FightUserInfo{UserID: c.UserID}
ttt.OurInfo = info.FightUserInfo{UserID: c.UserID}
ttt.OurPetList = []fight.ReadyFightPetInfo{{ID: 300,
ttt.OurPetList = []info.ReadyFightPetInfo{{ID: 300,
Level: 100,
MaxHp: 100,
@@ -27,8 +28,8 @@ func (h Controller) OnPlayerFightNpcMonster(data *fight.FightNpcMonsterInboundIn
for i := 0; i < 4; i++ {
ttt.OurPetList[0].SkillList[i] = model.SkillInfo{ID: 10001, Pp: 1}
}
ttt.OpponentInfo = fight.FightUserInfo{UserID: 0}
ttt.OpponentPetList = []fight.ReadyFightPetInfo{{ID: 1,
ttt.OpponentInfo = info.FightUserInfo{UserID: 0}
ttt.OpponentPetList = []info.ReadyFightPetInfo{{ID: 1,
Level: 100,
MaxHp: 100,
SkillListLen: 4,

View File

@@ -29,6 +29,7 @@ func main() {
go Start(cool.Config.PortBL) //注入service
fmt.Println("Process start, pid:", os.Getpid())
gproc.AddSigHandlerShutdown(
signalHandlerForMain,

View File

@@ -0,0 +1,59 @@
package action
import (
"blazing/logic/service/fight/battle/random"
"blazing/logic/service/fight/info"
)
type BattleAction struct {
Priority int
ramdom *random.RandomXS128
Skill *info.BattleSkillEntity
Pet *info.BattlePetEntity
}
// NewBattle1v1ActionComparator 创建1v1战斗动作比较器实例
// 入参为1v1战斗实例从战斗中获取随机数生成器对应原 Java 构造函数)
func NewBattleAction(seek *random.RandomXS128) *BattleAction {
return &BattleAction{
ramdom: seek,
}
}
// Compare 比较两个1v1战斗动作的执行优先级核心逻辑
// 完全遵循原 Java 比较逻辑:优先级 → 技能优先级 → 速度 → AI拼速 → 玩家随机拼速
func (c *BattleAction) Compare(o2 *BattleAction) *BattleAction {
// 1. 第一步比较动作本身的优先级o2 - o1优先级高的先执行
p1 := o2.Priority - c.Priority
if p1 > 0 { //说明对手快
return o2
} else if p1 < 0 {
return c
}
// 2. 第二步:如果是“使用技能”类型的动作,额外比较技能优先级和速度
// 类型断言:判断 o1 是否为 Use1v1SkillAction对应原 Java instanceof
if o2.Skill != nil && c.Skill != nil {
p2 := o2.Skill.Priority - c.Skill.Priority // 假设 Use1v1SkillAction 有 SkillPriority() 方法
if p2 > 0 {
return o2
} else if p2 < 0 {
return c
}
}
// 2.2 比较动作所有者的主力宠物速度o2 - o1速度快的先执行
// 假设Action.Owner() 返回动作所有者(玩家/AIOwner.MainPet() 返回主力宠物Pet.SpeedValue() 返回速度值
p2 := int(o2.Pet.GetSpeed()) - int(c.Pet.GetSpeed()) // 假设 Use1v1SkillAction 有 SkillPriority() 方法
if p2 > 0 {
return o2
} else if p2 < 0 {
return c
}
return c // 3.3 速度相同时 ,发起方优先
}

View File

@@ -0,0 +1,23 @@
package battle
const (
StateStart BattleState = iota
StatePlayerTurn
StateEnemyTurn
StateEnd
)
type BattleStateMachine struct {
State BattleState
}
func (fsm *BattleStateMachine) Next() {
switch fsm.State {
case StateStart:
fsm.State = StatePlayerTurn
case StatePlayerTurn:
fsm.State = StateEnemyTurn
case StateEnemyTurn:
fsm.State = StatePlayerTurn
}
}

View File

@@ -1,6 +1,9 @@
package battle
import "math/rand/v2"
import (
"blazing/logic/service/fight/battle/skill/effect"
"math/rand/v2"
)
type BattleState int
@@ -13,7 +16,7 @@ type BattleUnit struct {
Def int
MaxHP int
HP int
Buffs map[string]Effect
Buffs map[string]effect.Effect
IsCritical bool
}
@@ -26,20 +29,10 @@ func NewBattleUnit(name string, level, atk, def, maxHP int) *BattleUnit {
Def: def,
MaxHP: maxHP,
HP: maxHP,
Buffs: make(map[string]Effect),
Buffs: make(map[string]effect.Effect),
}
}
// ==== 技能类型 ====
const (
SkillTypePhysical SkillType = iota + 1
SkillTypeSpecial
SkillTypeGrass
SkillTypeWater
SkillTypeFire
)
// ==== 战斗上下文 ====
type BattleContext struct {
@@ -57,25 +50,3 @@ type Skill struct {
Defender *BattleUnit
Effects []Effect
}
const (
StateStart BattleState = iota
StatePlayerTurn
StateEnemyTurn
StateEnd
)
type BattleStateMachine struct {
State BattleState
}
func (fsm *BattleStateMachine) Next() {
switch fsm.State {
case StateStart:
fsm.State = StatePlayerTurn
case StatePlayerTurn:
fsm.State = StateEnemyTurn
case StateEnemyTurn:
fsm.State = StatePlayerTurn
}
}

View File

@@ -0,0 +1,12 @@
package battle
type BattleContainer struct {
FightUserInfo
}
b.Turn++
fmt.Printf("=== 回合 %d 开始 ===\n", b.Turn)
b.PublishTrigger(EffectTrigger.TurnStart, containers)
fmt.Println("=== 玩家操作阶段 ===")
b.PublishTrigger(EffectTrigger.TurnEnd, containers)
fmt.Printf("=== 回合 %d 结束 ===\n\n", b.Turn)
}

View File

@@ -0,0 +1,143 @@
package random
import (
"math/rand"
"time"
)
// RandomXS128 实现 xorshift128+ 伪随机数生成器
type RandomXS128 struct {
seed0 uint64 // 内部状态前半部分
seed1 uint64 // 内部状态后半部分
}
// 归一化常数
const (
normDouble = 1.0 / (1 << 53)
normFloat = 1.0 / (1 << 24)
)
// NewRandomXS128 创建一个新的随机数生成器,自动生成初始种子
func NewRandomXS128() *RandomXS128 {
// 用系统随机数初始化种子(模拟 Java 中 new Random().nextLong() 的行为)
// 实际使用中可替换为更安全的种子源(如 crypto/rand
source := rand.NewSource(time.Now().UnixNano()) // 基于时间的种子源
r := rand.New(source) // 用种子源创建 Rand 实例
seed := uint64(r.Int())<<32 | uint64(r.Int())
return NewRandomXS128WithSeed(seed)
}
// NewRandomXS128WithSeed 用单个 uint64 种子创建生成器
func NewRandomXS128WithSeed(seed uint64) *RandomXS128 {
seed0 := murmurHash3(seed)
seed1 := murmurHash3(seed0)
return &RandomXS128{seed0: seed0, seed1: seed1}
}
// NewRandomXS128WithTwoSeeds 用两个 uint64 种子直接设置状态
func NewRandomXS128WithTwoSeeds(seed0, seed1 uint64) *RandomXS128 {
return &RandomXS128{seed0: seed0, seed1: seed1}
}
// NextLong 生成下一个 64 位随机整数
func (r *RandomXS128) NextLong() uint64 {
s1 := r.seed0
s0 := r.seed1
r.seed0 = s0
s1 ^= s1 << 23
r.seed1 = s1 ^ s0 ^ (s1 >> 17) ^ (s0 >> 26)
return r.seed1 + s0
}
// NextInt 生成下一个 32 位随机整数
func (r *RandomXS128) NextInt() int {
return int(r.NextLong() & 0xFFFFFFFF)
}
// NextIntN 生成 [0, n) 范围内的随机整数
func (r *RandomXS128) NextIntN(n int) int {
if n <= 0 {
panic("n must be positive")
}
for {
bits := int(r.NextLong() >> 1)
value := int(bits % n)
if bits-value+n-1 >= 0 {
return value
}
}
}
// NextLongN 生成 [0, n) 范围内的随机 64 位整数
func (r *RandomXS128) NextLongN(n uint64) uint64 {
if n <= 0 {
panic("n must be positive")
}
for {
bits := r.NextLong() >> 1
value := bits % n
if bits-value+(n-1) >= 0 {
return value
}
}
}
// NextDouble 生成 [0.0, 1.0) 范围内的随机浮点数
func (r *RandomXS128) NextDouble() float64 {
return float64(r.NextLong()>>11) * normDouble
}
// NextFloat 生成 [0.0, 1.0) 范围内的随机单精度浮点数
func (r *RandomXS128) NextFloat() float32 {
return float32(float64(r.NextLong()>>40) * normFloat)
}
// NextBoolean 生成随机布尔值
func (r *RandomXS128) NextBoolean() bool {
return (r.NextLong() & 1) != 0
}
// NextBytes 填充字节数组为随机字节
func (r *RandomXS128) NextBytes(b []byte) {
i := len(b)
for i > 0 {
n := i
if n > 8 {
n = 8
}
bits := r.NextLong()
for j := 0; j < n; j++ {
b[i-1-j] = byte(bits >> (8 * j))
}
i -= n
}
}
// SetSeed 用单个种子重置生成器状态
func (r *RandomXS128) SetSeed(seed uint64) {
seed0 := murmurHash3(seed)
r.seed0 = seed0
r.seed1 = murmurHash3(seed0)
}
// SetState 直接设置内部状态
func (r *RandomXS128) SetState(seed0, seed1 uint64) {
r.seed0 = seed0
r.seed1 = seed1
}
// GetState 获取内部状态
func (r *RandomXS128) GetState() (seed0, seed1 uint64) {
return r.seed0, r.seed1
}
// murmurHash3 用于种子扩展的哈希函数
func murmurHash3(x uint64) uint64 {
x ^= x >> 33
x *= 0xff51afd7ed558ccd
x ^= x >> 33
x *= 0xc4ceb9fe1a85ec53
x ^= x >> 33
return x
}

View File

@@ -5,15 +5,28 @@ import (
"fmt"
)
// 灼烧效果
type BurnEffect struct{}
type BurnEffect struct {
life LifeCycle
}
func NewBurnEffect(turns int) *BurnEffect {
return &BurnEffect{life: NewTurnLife(turns)}
}
func (b *BurnEffect) Trigger() node.EnumEffectTrigger {
return node.EffectTrigger.OnHit
return node.EffectTrigger.TurnEnd // 每回合结束触发掉血
}
func (b *BurnEffect) Apply(ctx *node.EffectContext, next func()) {
fmt.Printf("[%s] 命中 [%s],触发灼烧效果!\n", ctx.Actor, ctx.Target)
ctx.Extra["Burn"] = true
next() // 继续执行后续效果
fmt.Printf("[%s] 在回合结束时受灼烧伤害!\n", ctx.Target)
b.life.Use() // 消耗一次触发次数(如果你定义为按次数存活)
next()
}
func (b *BurnEffect) Tick() {
b.life.Tick()
}
func (b *BurnEffect) Alive() bool {
return b.life.Alive()
}

View File

@@ -0,0 +1,36 @@
package base
// 生命周期管理器
type LifeCycle struct {
remainTurn int // 剩余回合(-1 表示不限回合)
remainCount int // 剩余次数(-1 表示不限次数)
}
// 创建一个回合持续类效果
func NewTurnLife(turns int) LifeCycle {
return LifeCycle{remainTurn: turns, remainCount: -1}
}
// 创建一个次数持续类效果
func NewCountLife(count int) LifeCycle {
return LifeCycle{remainTurn: -1, remainCount: count}
}
// 每回合 tick
func (lc *LifeCycle) Tick() {
if lc.remainTurn > 0 {
lc.remainTurn--
}
}
// 使用一次
func (lc *LifeCycle) Use() {
if lc.remainCount > 0 {
lc.remainCount--
}
}
// 是否还活跃
func (lc *LifeCycle) Alive() bool {
return (lc.remainTurn != 0) && (lc.remainCount != 0)
}

View File

@@ -0,0 +1,59 @@
package effect
import (
"sort"
"github.com/badu/bus"
)
type EffectContainer struct {
ID string
Effects []Effect
Subs []*bus.Listener[*EffectContext]
Battle *Battle
Parent ContextType
}
func NewEffectContainer(id string, effects []Effect, battle *Battle, parent ContextType) *EffectContainer {
c := &EffectContainer{
ID: id,
Effects: effects,
Battle: battle,
Parent: parent,
}
// 自动订阅 triggers
for _, e := range effects {
for _, trig := range e.Triggers() {
sub := battle.GetTopic(trig).Sub(func(ctx *EffectContext) {
if ctx.Available && e.Apply != nil {
ctx.Effect = e
c.executeWithPriority([]*EffectContext{ctx})
}
})
c.Subs = append(c.Subs, sub)
}
}
return c
}
// done/next 执行队列,按优先级
func (c *EffectContainer) executeWithPriority(queue []*EffectContext) {
sort.SliceStable(queue, func(i, j int) bool {
return queue[i].Effect.Priority() > queue[j].Effect.Priority()
})
var runNext func(idx int)
runNext = func(idx int) {
if idx >= len(queue) {
return
}
ctx := queue[idx]
if ctx.Available {
ctx.Effect.Apply(ctx, func() {
runNext(idx + 1)
})
} else {
runNext(idx + 1)
}
}
runNext(0)
}

View File

@@ -1,215 +0,0 @@
package effect
import (
"fmt"
"sort"
"github.com/badu/bus"
"github.com/tnnmigga/enum"
)
// ========================
// BattleMode 枚举
// ========================
type EnumBattleMode string
var BattleMode = enum.New[struct {
PVE EnumBattleMode `enum:"3"`
PVP EnumBattleMode `enum:"1"`
}]()
// ========================
// Effect 框架
// ========================
type ContextType interface {
SourceID() string
}
type EffectContext struct {
Parent ContextType
Trigger EnumEffectTrigger
Container *EffectContainer
Effect *Effect
Available bool
Success bool
Done bool
}
func (c *EffectContext) Stop() { c.Done = true }
func (c *EffectContext) SourceID() string {
if c.Container != nil {
return c.Container.ID
}
return "unknown"
}
type EffectFunc func(ctx *EffectContext, next func())
type Effect struct {
ID string
Priority int
Triggers []EnumEffectTrigger
Condition func(*EffectContext) bool
Apply EffectFunc
}
type EffectContainer struct {
ID string
Effects []*Effect
Subs []*bus.Listener[*EffectContext]
Battle *Battle
Parent ContextType
}
func NewEffectContainer(id string, effects []*Effect, battle *Battle, parent ContextType) *EffectContainer {
c := &EffectContainer{
ID: id,
Effects: effects,
Battle: battle,
Parent: parent,
}
// 自动订阅 triggers
for _, e := range effects {
for _, trig := range e.Triggers {
sub := battle.GetTopic(trig).Sub(func(ctx *EffectContext) {
if ctx.Available && e.Apply != nil {
ctx.Effect = e
c.executeWithPriority([]*EffectContext{ctx})
}
})
c.Subs = append(c.Subs, sub)
}
}
return c
}
// done/next 执行队列,按优先级
func (c *EffectContainer) executeWithPriority(queue []*EffectContext) {
sort.SliceStable(queue, func(i, j int) bool {
return queue[i].Effect.Priority > queue[j].Effect.Priority
})
var runNext func(idx int)
runNext = func(idx int) {
if idx >= len(queue) {
return
}
ctx := queue[idx]
if ctx.Available {
ctx.Effect.Apply(ctx, func() {
runNext(idx + 1)
})
} else {
runNext(idx + 1)
}
}
runNext(0)
}
// ========================
// Battle
// ========================
type Battle struct {
Turn int
topics map[EnumEffectTrigger]*bus.Topic[*EffectContext]
}
func NewBattle() *Battle {
allTriggers := []EnumEffectTrigger{
EffectTrigger.OnBattleStart,
EffectTrigger.BeforeSort,
EffectTrigger.BeforeUseSkillCheck,
EffectTrigger.AfterUseSkillCheck,
EffectTrigger.BeforeMultiHit,
EffectTrigger.BeforeHit,
EffectTrigger.OnCritPreDamage,
EffectTrigger.PreDamage,
EffectTrigger.OnHit,
EffectTrigger.OnMiss,
EffectTrigger.AfterAttacked,
EffectTrigger.OnDefeat,
EffectTrigger.SkillUseEnd,
EffectTrigger.OnBeforeCalculateDamage,
EffectTrigger.OnDamage,
EffectTrigger.Shield,
EffectTrigger.PostDamage,
EffectTrigger.OnCritPostDamage,
EffectTrigger.OnTransform,
EffectTrigger.OnTransformEnd,
EffectTrigger.BeforeTransform,
EffectTrigger.AfterTransform,
EffectTrigger.TurnStart,
EffectTrigger.TurnEnd,
EffectTrigger.OnBeforeAddMark,
EffectTrigger.OnAnyMarkAdded,
EffectTrigger.OnRemoveMark,
EffectTrigger.OnMarkCreated,
EffectTrigger.OnMarkDestroy,
EffectTrigger.OnMarkDurationEnd,
EffectTrigger.OnStackBefore,
EffectTrigger.OnStack,
EffectTrigger.OnBeforeConsumeStack,
EffectTrigger.OnConsumeStack,
EffectTrigger.OnBeforeHeal,
EffectTrigger.OnHeal,
EffectTrigger.BeforeRageGain,
EffectTrigger.BeforeRageLoss,
EffectTrigger.OnRageGain,
EffectTrigger.OnRageLoss,
EffectTrigger.OnSwitchIn,
EffectTrigger.OnSwitchOut,
EffectTrigger.OnOwnerSwitchIn,
EffectTrigger.OnOwnerSwitchOut,
EffectTrigger.BeforeEffect,
EffectTrigger.AfterEffect,
}
topics := make(map[EnumEffectTrigger]*bus.Topic[*EffectContext])
for _, trig := range allTriggers {
topics[trig] = bus.NewTopic[*EffectContext]()
}
return &Battle{Turn: 0, topics: topics}
}
func (b *Battle) GetTopic(trig EnumEffectTrigger) *bus.Topic[*EffectContext] {
return b.topics[trig]
}
func (b *Battle) NextTurn(containers []*EffectContainer) {
b.Turn++
fmt.Printf("=== 回合 %d 开始 ===\n", b.Turn)
b.PublishTrigger(EffectTrigger.TurnStart, containers)
fmt.Println("=== 玩家操作阶段 ===")
b.PublishTrigger(EffectTrigger.TurnEnd, containers)
fmt.Printf("=== 回合 %d 结束 ===\n\n", b.Turn)
}
func (b *Battle) PublishTrigger(trigger EnumEffectTrigger, containers []*EffectContainer) {
for _, c := range containers {
for _, e := range c.Effects {
for _, t := range e.Triggers {
if t == trigger {
ctx := &EffectContext{
Parent: c.Parent,
Trigger: trigger,
Container: c,
Effect: e,
Available: true,
}
b.GetTopic(trigger).Pub(ctx)
}
}
}
}
}
// ========================
// 示例
// ========================
type Player struct {
ID string
}
func (p *Player) SourceID() string { return p.ID }

View File

@@ -1,40 +0,0 @@
package effect
import (
"fmt"
"testing"
"time"
)
func Test_mainTT(t *testing.T) {
battle := NewBattle()
player := &Player{ID: "player1"}
container := NewEffectContainer("container1", []*Effect{
{
ID: "startEffect",
Priority: 10,
Triggers: []EnumEffectTrigger{EffectTrigger.TurnStart},
Apply: func(ctx *EffectContext, next func()) {
fmt.Println("回合开始效果: 增加护盾")
next()
},
},
{
ID: "endEffect",
Priority: 5,
Triggers: []EnumEffectTrigger{EffectTrigger.TurnEnd},
Apply: func(ctx *EffectContext, next func()) {
fmt.Println("回合结束效果: 恢复怒气")
next()
},
},
}, battle, player)
for i := 0; i < 2; i++ {
battle.NextTurn([]*EffectContainer{container})
time.Sleep(500 * time.Millisecond)
}
fmt.Println("测试 BattleMode 枚举:", BattleMode.PVE, BattleMode.PVP)
}

View File

@@ -0,0 +1,117 @@
package effect
import (
"blazing/logic/service/fight/battle/node"
"sort"
)
type Effect interface {
Trigger() node.EnumEffectTrigger
Apply(ctx *EffectContext, next func())
Alive() bool // 是否还有效
Tick() // 每回合开始/结束时调用,用来减少回合数或次数
Priority() int // 优先级
}
// ========================
// 上下文:一次效果执行环境
// ========================
type EffectContext struct {
Parent string // 上下文来源(比如 "Skill"、"Buff"、"Passive"
Trigger EnumEffectTrigger // 当前触发的节点
Container *EffectContainer // 效果容器(通常挂在 Actor 身上)
Effect *Effect // 当前正在执行的 Effect
Available bool // 是否可用
Success bool // 是否执行成功
Done bool // 是否中止后续执行
}
// ========================
// Effect: 单个效果
// ========================
type Effect struct {
ID string // 唯一标识
Trigger EnumEffectTrigger // 触发节点
Priority int // 执行优先级,数值越大越先执行
Duration int // 持续回合0 = 即时生效,>0 = 回合数)
Stacks int // 当前层数
MaxStack int // 最大叠加层数
Apply func(ctx *EffectContext) bool // 执行逻辑,返回 true 表示继续保留
}
// ========================
// 容器:存放多个效果
// ========================
type EffectContainer struct {
Effects []*Effect
}
// 添加效果
func (c *EffectContainer) AddEffect(e *Effect) {
// 如果已有同 ID 的效果,尝试叠加
for _, eff := range c.Effects {
if eff.ID == e.ID {
if eff.Stacks < eff.MaxStack {
eff.Stacks++
}
return
}
}
// 否则新加入
c.Effects = append(c.Effects, e)
}
// 触发执行
func (c *EffectContainer) Trigger(trigger EnumEffectTrigger, ctx *EffectContext) {
var candidates []*Effect
for _, eff := range c.Effects {
if eff.Trigger == trigger {
candidates = append(candidates, eff)
}
}
// 按优先级排序
sort.SliceStable(candidates, func(i, j int) bool {
return candidates[i].Priority > candidates[j].Priority
})
// 执行
for _, eff := range candidates {
ctx.Effect = eff
keep := eff.Apply(ctx)
if !keep {
// 持续回合结束 / 返回 false 的 effect 删除
c.removeEffect(eff)
}
if ctx.Done {
break // 被拦截
}
}
}
// 每回合结束时调用,用于处理持续时间
func (c *EffectContainer) Tick() {
var remain []*Effect
for _, eff := range c.Effects {
if eff.Duration > 0 {
eff.Duration--
}
if eff.Duration != 0 { // 保留 (负数表示永久)
remain = append(remain, eff)
}
}
c.Effects = remain
}
// 删除
func (c *EffectContainer) removeEffect(e *Effect) {
var remain []*Effect
for _, eff := range c.Effects {
if eff != e {
remain = append(remain, eff)
}
}
c.Effects = remain
}

View File

@@ -1,11 +0,0 @@
package skill
type Skill struct {
Name string
Type SkillType
Power int
IsAttack bool
Attacker *BattleUnit
Defender *BattleUnit
Effects []Effect
}

View File

@@ -0,0 +1,64 @@
package fight
import (
"blazing/common/socket/handler"
)
// 野怪对战包
type FightNpcMonsterInboundInfo struct {
Head handler.TomeeHeader `cmd:"2408" struc:"[0]pad"`
// Number 地图刷新怪物结构体对应的序号1-9的位置序号
// 对应Java的@UInt long类型使用uint32保持无符号特性
Number uint32 `fieldDesc:"地图刷新怪物结构体对应的序号 1 - 9 的位置序号" `
}
type NullOutboundInfo struct {
}
// 准备战斗包
type ReadyToFightInboundInfo struct {
Head handler.TomeeHeader `cmd:"2404" struc:"[0]pad"`
}
type FightStartOutboundInfo struct {
// 对应Java的@UInt long类型
IsCanAuto uint32 `fieldDesc:"是否自动 默认给0 怀疑是自动战斗器使用的" `
// 当前战斗精灵信息1前端通过userid判断是否为我方
Info1 FightPetInfo `fieldDesc:"当前战斗精灵的信息 可能不准.看前端代码是以userid来判断哪个结构体是我方的" serialize:"struct"`
// 当前战斗精灵信息2前端通过userid判断是否为我方
Info2 FightPetInfo `fieldDesc:"当前战斗精灵的信息 可能不准.看前端代码是以userid来判断哪个结构体是我方的" serialize:"struct"`
}
// FightPetInfo 战斗精灵信息结构体对应Java的FightPetInfo类
type FightPetInfo struct {
// 用户ID野怪为0对应Java的@UInt long
UserID uint32 `fieldDesc:"用户ID 野怪为0" `
// 当前对战精灵ID对应Java的@UInt long
PetID uint32 `fieldDesc:"当前对战精灵ID" `
// 空的16字节byte对应Java的固定长度字符串
// 使用[16]byte匹配固定长度16字节的要求
PetName [16]byte `fieldDesc:"空的16字节byte" serialize:"fixedLength=16,type=byteArray"`
// 精灵的捕获时间对应Java的@UInt long
CatchTime uint32 `fieldDesc:"精灵的捕获时间" `
// 当前HP对应Java的@UInt long
Hp uint32 `fieldDesc:"当前HP" `
// 最大HP对应Java的@UInt long
MaxHp uint32 `fieldDesc:"最大HP" `
// 当前等级对应Java的@UInt long
Level uint32 `fieldDesc:"当前等级" `
// 精灵是否能捕捉1为能捕捉0为不能捕捉对应Java的@UInt long
Catchable uint32 `fieldDesc:"精灵是否能捕捉. 1为能捕捉 0为不能捕捉" `
// 战斗属性等级数组6个单字节
// 对应Java的byte[]固定长度6存储buff等级、攻击、速度等属性
BattleLV [6]byte `fieldDesc:"这里实际上应该是6个单字节byte, 内容为buff等级 攻击 速度 特攻 防御 特防 命中等.但具体顺序未知可能需要测试. 具体数值为1-6等级" serialize:"fixedLength=6,type=byteArray"`
}

View File

@@ -1,125 +0,0 @@
package fight
import (
"blazing/common/socket/handler"
"blazing/modules/blazing/model"
)
type FightNpcMonsterInboundInfo struct {
Head handler.TomeeHeader `cmd:"2408" struc:"[0]pad"` //玩家登录
// Number 地图刷新怪物结构体对应的序号1-9的位置序号
// 对应Java的@UInt long类型使用uint32保持无符号特性
Number uint32 `fieldDesc:"地图刷新怪物结构体对应的序号 1 - 9 的位置序号" `
}
type NullOutboundInfo struct {
}
type ReadyToFightInboundInfo struct {
Head handler.TomeeHeader `cmd:"2404" struc:"[0]pad"` //玩家登录
}
type FightStartOutboundInfo struct {
// 对应Java的@UInt long类型
IsCanAuto uint32 `fieldDesc:"是否自动 默认给0 怀疑是自动战斗器使用的" `
// 当前战斗精灵信息1前端通过userid判断是否为我方
Info1 FightPetInfo `fieldDesc:"当前战斗精灵的信息 可能不准.看前端代码是以userid来判断哪个结构体是我方的" serialize:"struct"`
// 当前战斗精灵信息2前端通过userid判断是否为我方
Info2 FightPetInfo `fieldDesc:"当前战斗精灵的信息 可能不准.看前端代码是以userid来判断哪个结构体是我方的" serialize:"struct"`
}
// FightPetInfo 战斗精灵信息结构体对应Java的FightPetInfo类
type FightPetInfo struct {
// 用户ID野怪为0对应Java的@UInt long
UserID uint32 `fieldDesc:"用户ID 野怪为0" `
// 当前对战精灵ID对应Java的@UInt long
PetID uint32 `fieldDesc:"当前对战精灵ID" `
// 空的16字节byte对应Java的固定长度字符串
// 使用[16]byte匹配固定长度16字节的要求
PetName [16]byte `fieldDesc:"空的16字节byte" serialize:"fixedLength=16,type=byteArray"`
// 精灵的捕获时间对应Java的@UInt long
CatchTime uint32 `fieldDesc:"精灵的捕获时间" `
// 当前HP对应Java的@UInt long
Hp uint32 `fieldDesc:"当前HP" `
// 最大HP对应Java的@UInt long
MaxHp uint32 `fieldDesc:"最大HP" `
// 当前等级对应Java的@UInt long
Level uint32 `fieldDesc:"当前等级" `
// 精灵是否能捕捉1为能捕捉0为不能捕捉对应Java的@UInt long
Catchable uint32 `fieldDesc:"精灵是否能捕捉. 1为能捕捉 0为不能捕捉" `
// 战斗属性等级数组6个单字节
// 对应Java的byte[]固定长度6存储buff等级、攻击、速度等属性
BattleLV [6]byte `fieldDesc:"这里实际上应该是6个单字节byte, 内容为buff等级 攻击 速度 特攻 防御 特防 命中等.但具体顺序未知可能需要测试. 具体数值为1-6等级" serialize:"fixedLength=6,type=byteArray"`
}
// NoteReadyToFightInfo 战斗准备就绪消息结构体对应Java的NoteReadyToFightInfo
type NoteReadyToFightInfo struct {
// 战斗类型ID与野怪战斗为3与人战斗为1前端似乎未使用
// 对应Java的@UInt long
FightId uint32 `fieldDesc:"战斗类型ID 但前端好像没有用到 与野怪战斗为3与人战斗似乎是1" `
// 我方信息
OurInfo FightUserInfo `fieldDesc:"我方信息" serialize:"struct"`
OurPetListLen uint32 `struc:"sizeof=OurPetList"`
// 我方携带精灵的信息
// 对应Java的ArrayList<ReadyFightPetInfo>,使用切片模拟动态列表
OurPetList []ReadyFightPetInfo `fieldDesc:"我方携带精灵的信息" serialize:"lengthFirst,lengthType=uint16,type=structArray"`
// 对方信息
OpponentInfo FightUserInfo `fieldDesc:"对方信息" serialize:"struct"`
OpponentPetListLen uint32 `struc:"sizeof=OpponentPetList"`
// 敌方的精灵信息
// 野怪战斗时客户端接收此包前已生成精灵PetInfo将部分信息写入该列表
OpponentPetList []ReadyFightPetInfo `fieldDesc:"敌方的精灵信息 如果是野怪 那么再给客户端发送这个包体时就提前生成好了这只精灵的PetInfo,然后把从PetInfo中把部分信息写入到这个敌方的精灵信息中再发送这个包结构体" serialize:"lengthFirst,lengthType=uint16,type=structArray"`
}
type FightUserInfo struct {
// 用户ID野怪为0对应Java的@UInt long
UserID uint32 `fieldDesc:"userID 如果为野怪则为0" `
// 玩家名称野怪为UTF-8的'-'固定16字节
// 使用[16]byte存储固定长度的字节数组
Nickname [16]byte ` `
}
// ReadyFightPetInfo 准备战斗的精灵信息结构体对应Java的ReadyFightPetInfo类
type ReadyFightPetInfo struct {
// 精灵ID对应Java的@UInt long
ID uint32 `fieldDesc:"精灵ID" `
// 精灵等级对应Java的@UInt long
Level uint32 `fieldDesc:"精灵等级" `
// 精灵当前HP对应Java的@UInt long
Hp uint32 `fieldDesc:"精灵HP" `
// 精灵最大HP对应Java的@UInt long
MaxHp uint32 `fieldDesc:"最大HP" `
SkillListLen uint32
// 技能信息列表固定4个元素技能ID和剩余PP无技能则为0
// 对应Java的List<SkillInfo>初始化容量为4
SkillList [4]model.SkillInfo `fieldDesc:"技能信息 技能ID跟剩余PP 固定32字节 没有给0" serialize:"fixedLength=4,type=structArray"`
// 精灵捕获时间对应Java的@UInt long
CatchTime uint32 `fieldDesc:"精灵捕获时间" `
// 捕捉地图固定给0对应Java的@UInt long
CatchMap uint32 `fieldDesc:"捕捉地图 给0" `
// 固定给0对应Java的@UInt long
CatchRect uint32 `fieldDesc:"给0" `
// 固定给0对应Java的@UInt long
CatchLevel uint32 `fieldDesc:"给0" `
SkinID uint32 `fieldDesc:"精灵皮肤ID" `
Shiny uint32 `fieldDesc:"精灵是否闪" `
}

View File

@@ -0,0 +1,10 @@
package info
type FightUserInfo struct {
// 用户ID野怪为0对应Java的@UInt long
UserID uint32 `fieldDesc:"userID 如果为野怪则为0" `
// 玩家名称野怪为UTF-8的'-'固定16字节
// 使用[16]byte存储固定长度的字节数组
Nickname [16]byte ` `
}

View File

@@ -0,0 +1,87 @@
package info
import (
"blazing/modules/blazing/model"
"sync"
)
type BattlePetEntity struct {
// /host *BattleInputSourceEntity // 宠物的主人(输入源)
//uuid string // 唯一标识
*model.PetInfo
GainHp int64 // 获得的生命值
Capturable bool // 是否可捕获
// 状态条件(如中毒、烧伤等)
statusConditions sync.Map // key: StatusCondition, value: int (剩余回合)
// 状态变化值
attackStat int
defenseStat int
specialAttackStat int
specialDefenseStat int
speedStat int
accuracyStat int
skills [4]*BattleSkillEntity // 技能槽最多4个技能
// 特殊属性
Perseverance int // 毅力值:抵消致命伤
Stubborn bool // 顽强特性
StubbornProbability int // 顽强触发概率
Revival bool // 回神特性
RevivalProbability int // 回神触发概率
Definitely int // 必定命中概率
Dodge int // 闪避概率
// 护盾相关
MaxShield int64 // 最大护盾值
Shield int64 // 当前护盾值
CountShield int // 次数盾
// 属性变化回合
//petAttributeRound map[BattleRoundStarEffect]int // 属性变化类型 -> 剩余回合
petImmunityEffectIds map[int]int // 免疫效果ID -> 剩余回合 (-1表示永久)
petImmunityBuffs map[string]int // 免疫buff效果ID -> 剩余回合 (-1表示永久)
IsDead bool // 是否死亡
// 战斗开始时拥有的特殊buff
//battleStartHavingBuffs []buff.BattleBuffInterface
}
func (b *BattlePetEntity) GetSpeed() uint32 {
b.calculateRealValue(int64(b.Speed), b.speedStat)
return 0
}
// calculateRealValue 计算实际属性值根据状态变化计算实际值
// value 基础属性值
// stat 状态变化值(可正可负)
// 返回// 返回计算后的实际属性值确保结果至少为1
func (b *BattlePetEntity) calculateRealValue(value int64, stat int) int64 {
if stat == 0 {
if value <= 0 {
return 1
}
return value
} else if stat > 0 {
r := int64(float64(value) * (float64(stat+2) / 2.0))
if r <= 0 {
return 1
}
return r
} else {
r := int64(float64(value) * (2.0 / float64(2-stat)))
if r <= 0 {
return 1
}
return r
}
}

View File

@@ -0,0 +1,159 @@
package info
import (
"blazing/common/data/xml/skill"
"context"
"fmt"
"strconv"
"strings"
"github.com/gogf/gf/v2/os/glog"
"github.com/tnnmigga/enum"
)
// EnumSkillType 技能类型枚举基础类型
type EnumSkillType int
// SkillType 技能类型枚举实例使用enum包创建
// 与原Java枚举保持相同的数值映射PHYSICAL=1, SPECIAL=2, STATUS=4
var SkillType = enum.New[struct {
PHYSICAL EnumSkillType `enum:"1"` // 物理攻击
SPECIAL EnumSkillType `enum:"2"` // 特殊攻击
STATUS EnumSkillType `enum:"4"` // 状态技能
}]()
// BattleSkillEntity 战斗技能实体
// 实现了战斗中技能的所有属性和行为包括PP管理、技能使用、属性获取等
type BattleSkillEntity struct {
skill.Move
SideEffects []int
SideEffectArgs []int
PP int
InfinityPP bool
// 技能类型属性
SkillType EnumSkillType // 技能类型(物理/特殊/状态)
}
// CreateBattleSkill 创建战斗技能实例
func CreateBattleSkill(id int64, pp int) *BattleSkillEntity {
return CreateBattleSkillWithInfinity(id, pp, false)
}
// CreateBattleSkillWithInfinity 创建战斗技能实例可指定是否无限PP
func CreateBattleSkillWithInfinity(id int64, pp int, infinityPP bool) *BattleSkillEntity {
// ID小于10001的视为无效技能
if id < 10001 {
return nil
}
// 从资源仓库获取技能数据
move, ok := skill.MovesConfig.Moves[id]
if !ok {
glog.Error(context.Background(), "技能ID无效", "id", id)
}
var ret BattleSkillEntity
// 解析副作用参数
sideEffectArgs := parseSideEffectArgs(move.SideEffectArg)
tt := strings.Split(move.SideEffect, " ")
rf, err := strSliceToIntSlice(tt)
if err == nil {
ret.SideEffects = rf
}
ret.SideEffectArgs = sideEffectArgs
return &ret
}
// 将字符串切片转换为整数切片
func strSliceToIntSlice(strs []string) ([]int, error) {
intSlice := make([]int, 0, len(strs)) // 预分配容量,提高性能
for _, s := range strs {
// 将字符串转换为整数
num, err := strconv.Atoi(s)
if err != nil {
// 转换失败时返回错误(包含具体的错误信息和失败的字符串)
return nil, fmt.Errorf("无法将字符串 '%s' 转换为整数: %v", s, err)
}
intSlice = append(intSlice, num)
}
return intSlice, nil
}
// CanUse 检查技能是否可以使用PP是否充足
func (s *BattleSkillEntity) CanUse() bool {
return s.PP > 0
}
// UseOnce 使用一次技能消耗1点PP
func (s *BattleSkillEntity) UseOnce() *BattleSkillEntity {
if !s.InfinityPP {
s.PP--
s.correctPP()
}
return s
}
// AddPP 增加PP值
func (s *BattleSkillEntity) AddPP(value int) *BattleSkillEntity {
if !s.InfinityPP {
s.PP += value
s.correctPP()
}
return s
}
// MinusPP 减少PP值
func (s *BattleSkillEntity) MinusPP(value int) *BattleSkillEntity {
if !s.InfinityPP {
s.PP -= value
s.correctPP()
}
return s
}
// 修正PP值确保在0到MaxPP之间
func (s *BattleSkillEntity) correctPP() {
if s.PP < 0 {
s.PP = 0
} else if s.PP > s.MaxPP {
s.PP = s.MaxPP
}
}
// 解析副作用参数字符串为整数列表
func parseSideEffectArgs(argsStr string) []int {
if argsStr == "" {
return []int{}
}
parts := strings.Fields(argsStr)
args := make([]int, 0, len(parts))
for _, part := range parts {
if num, err := strconv.Atoi(part); err == nil {
args = append(args, num)
}
}
return args
}
// 获取技能名称为空时使用ID
func getSkillName(move *BattleSkillEntity) string {
if move.Name == "" {
return strconv.FormatInt(int64(move.ID), 10)
}
return move.Name
}
// 获取副作用列表,处理空值情况
func getSideEffects(move *BattleSkillEntity) []int {
if move.SideEffect == "" {
return []int{}
}
return move.SideEffects
}

View File

@@ -0,0 +1,59 @@
package info
import (
"blazing/modules/blazing/model"
)
// NoteReadyToFightInfo 战斗准备就绪消息结构体对应Java的NoteReadyToFightInfo
type NoteReadyToFightInfo struct {
// 战斗类型ID与野怪战斗为3与人战斗为1前端似乎未使用
// 对应Java的@UInt long
FightId uint32 `fieldDesc:"战斗类型ID 但前端好像没有用到 与野怪战斗为3与人战斗似乎是1" `
// 我方信息
OurInfo FightUserInfo `fieldDesc:"我方信息" serialize:"struct"`
OurPetListLen uint32 `struc:"sizeof=OurPetList"`
// 我方携带精灵的信息
// 对应Java的ArrayList<ReadyFightPetInfo>,使用切片模拟动态列表
OurPetList []ReadyFightPetInfo `fieldDesc:"我方携带精灵的信息" serialize:"lengthFirst,lengthType=uint16,type=structArray"`
// 对方信息
OpponentInfo FightUserInfo `fieldDesc:"对方信息" serialize:"struct"`
OpponentPetListLen uint32 `struc:"sizeof=OpponentPetList"`
// 敌方的精灵信息
// 野怪战斗时客户端接收此包前已生成精灵PetInfo将部分信息写入该列表
OpponentPetList []ReadyFightPetInfo `fieldDesc:"敌方的精灵信息 如果是野怪 那么再给客户端发送这个包体时就提前生成好了这只精灵的PetInfo,然后把从PetInfo中把部分信息写入到这个敌方的精灵信息中再发送这个包结构体" serialize:"lengthFirst,lengthType=uint16,type=structArray"`
}
// ReadyFightPetInfo 准备战斗的精灵信息结构体对应Java的ReadyFightPetInfo类
type ReadyFightPetInfo struct {
// 精灵ID对应Java的@UInt long
ID uint32 `fieldDesc:"精灵ID" `
// 精灵等级对应Java的@UInt long
Level uint32 `fieldDesc:"精灵等级" `
// 精灵当前HP对应Java的@UInt long
Hp uint32 `fieldDesc:"精灵HP" `
// 精灵最大HP对应Java的@UInt long
MaxHp uint32 `fieldDesc:"最大HP" `
SkillListLen uint32
// 技能信息列表固定4个元素技能ID和剩余PP无技能则为0
// 对应Java的List<SkillInfo>初始化容量为4
SkillList [4]model.SkillInfo `fieldDesc:"技能信息 技能ID跟剩余PP 固定32字节 没有给0" serialize:"fixedLength=4,type=structArray"`
// 精灵捕获时间对应Java的@UInt long
CatchTime uint32 `fieldDesc:"精灵捕获时间" `
// 捕捉地图固定给0对应Java的@UInt long
CatchMap uint32 `fieldDesc:"捕捉地图 给0" `
// 固定给0对应Java的@UInt long
CatchRect uint32 `fieldDesc:"给0" `
// 固定给0对应Java的@UInt long
CatchLevel uint32 `fieldDesc:"给0" `
SkinID uint32 `fieldDesc:"精灵皮肤ID" `
Shiny uint32 `fieldDesc:"精灵是否闪" `
}