Compare commits

..

84 Commits

Author SHA1 Message Date
昔念
de6c700bb3 1
Some checks failed
ci/woodpecker/push/my-first-workflow Pipeline failed
2026-04-18 01:30:48 +08:00
昔念
3232efd05a 1
Some checks failed
ci/woodpecker/push/my-first-workflow Pipeline failed
2026-04-17 00:48:43 +08:00
昔念
0c79fee8af 1
Some checks failed
ci/woodpecker/push/my-first-workflow Pipeline failed
2026-04-17 00:35:17 +08:00
昔念
3d77e146e9 1
Some checks failed
ci/woodpecker/push/my-first-workflow Pipeline failed
2026-04-16 23:49:28 +08:00
xinian
a43a25c610 test: cover legacy round broadcast handling
All checks were successful
ci/woodpecker/push/my-first-workflow Pipeline was successful
2026-04-16 10:25:56 +08:00
xinian
3cfde577eb test: add pet fusion transaction coverage
All checks were successful
ci/woodpecker/push/my-first-workflow Pipeline was successful
2026-04-16 09:21:39 +08:00
xinian
85f9c02ced fix: correct self-destruct mutual KO handling
Some checks failed
ci/woodpecker/push/my-first-workflow Pipeline failed
2026-04-16 09:21:02 +08:00
昔念
9f7fd83626 1
All checks were successful
ci/woodpecker/push/my-first-workflow Pipeline was successful
2026-04-15 22:42:56 +08:00
昔念
ee8b0a2182 Merge branch 'main' of https://cnb.cool/blzing/blazing
All checks were successful
ci/woodpecker/push/my-first-workflow Pipeline was successful
2026-04-15 22:17:08 +08:00
昔念
6e95e014fa 1 2026-04-15 22:16:56 +08:00
xinian
61a135b3a7 fix: 修复宠物升级经验显示与动态结算
All checks were successful
ci/woodpecker/push/my-first-workflow Pipeline was successful
2026-04-15 15:34:16 +08:00
xinian
5a81534e84 1
All checks were successful
ci/woodpecker/push/my-first-workflow Pipeline was successful
2026-04-15 15:07:27 +08:00
xinian
523d835ac0 boss属性丢失,全部被限制属性不能超越,应该给ai和玩家施加不同的getinfo方法
All checks were successful
ci/woodpecker/push/my-first-workflow Pipeline was successful
道具扣除判断,因为判断扣完大于0,所以导致只剩一个的时候,道具成功使用但是不会扣除数量
2026-04-15 14:44:46 +08:00
昔念
5a7e20efec 1
All checks were successful
ci/woodpecker/push/my-first-workflow Pipeline was successful
2026-04-15 03:46:55 +08:00
昔念
5f47bf0589 1
All checks were successful
ci/woodpecker/push/my-first-workflow Pipeline was successful
2026-04-15 03:22:59 +08:00
昔念
a58ef20fab 1
All checks were successful
ci/woodpecker/push/my-first-workflow Pipeline was successful
2026-04-15 00:19:21 +08:00
昔念
3999f34f77 1
Some checks failed
ci/woodpecker/push/my-first-workflow Pipeline failed
2026-04-15 00:17:06 +08:00
昔念
6f51a2e349 1
All checks were successful
ci/woodpecker/push/my-first-workflow Pipeline was successful
2026-04-15 00:07:36 +08:00
xinian
de755f8fd0 fix: 修正效果33为消除敌方阵营所有强化
Some checks failed
ci/woodpecker/push/my-first-workflow Pipeline failed
2026-04-14 16:26:05 +08:00
xinian
803aa71771 更新说明
Some checks failed
ci/woodpecker/push/my-first-workflow Pipeline failed
2026-04-14 15:55:28 +08:00
xinian
4a77066d08 refactor: 重构持续伤害触发时机为回合开始
Some checks failed
ci/woodpecker/push/my-first-workflow Pipeline failed
2026-04-14 15:21:47 +08:00
xinian
c9b5f8569f fix: 修复道具扣除和宠物融合事务处理问题
Some checks failed
ci/woodpecker/push/my-first-workflow Pipeline failed
2026-04-14 13:06:28 +08:00
xinian
ddbfe91d8b fix: 修复扭蛋道具扣除逻辑
Some checks failed
ci/woodpecker/push/my-first-workflow Pipeline failed
2026-04-14 11:06:04 +08:00
昔念
74ac6ce940 ```
All checks were successful
ci/woodpecker/push/my-first-workflow Pipeline was successful
feat
2026-04-14 01:00:34 +08:00
昔念
43b0bc2dec ```
All checks were successful
ci/woodpecker/push/my-first-workflow Pipeline was successful
feat(fight_boss): 优化BOSS战斗奖励逻辑并修复宠物等级突破100级限制

重构了handleMapBossFightRewards函数,将奖励逻辑分离到独立的处理函数中,
增加了shouldGrantBossWinBonus条件判断,确保只有满足条件时才发放胜利奖励。

同时修复了宠物等级系统,允许宠物等级突破100级限制但面板属性仍保持100级上限,
改进了经验获取和面板更新逻辑。

fix(item_use): 添加全能性格转化剂使用验证

添加了UniversalNatureItemID常量定义,增加对道具ID和性格配置的有效性验证,
确保只有正确的道具和性格类型才能被使用。

refactor(fight): 统一战斗结束原因处理逻辑

引入normalizeFightOverReason函数来标准化战斗结束原因,
统一了不同模块中的战斗结果映射逻辑,提高了代码一致性。

perf(pet): 优化宠物升级和经验计算性能

移除了等级100的硬性限制,在保证面板属性不超限的前提下允许宠物等级继续增长,
优化了经验分配和面板重新计算的逻辑流程。
```
2026-04-14 00:43:32 +08:00
昔念
b953e7831a ```
feat(fight_boss): 优化BOSS战斗奖励逻辑并修复宠物等级突破100级限制

重构了handleMapBossFightRewards函数,将奖励逻辑分离到独立的处理函数中,
增加了shouldGrantBossWinBonus条件判断,确保只有满足条件时才发放胜利奖励。

同时修复了宠物等级系统,允许宠物等级突破100级限制但面板属性仍保持100级上限,
改进了经验获取和面板更新逻辑。

fix(item
2026-04-14 00:38:50 +08:00
昔念
62d93f65e7 根据提供的code differences信息,由于没有具体的代码变更内容,我将生成一个通用的commit message模板:
All checks were successful
ci/woodpecker/push/my-first-workflow Pipeline was successful
```
docs(readme): 更新文档说明

- 添加项目使用指南
- 完善API接口说明
- 修正错误的配置示例
```

注意:由于未提供具体的代码差异信息,以上为示例格式。实际使用时请根据具体的代码变更内容填写相应的type、scope、subject和body信息。
2026-04-13 22:53:02 +08:00
昔念
7dfa9c297e ```
All checks were successful
ci/woodpecker/push/my-first-workflow Pipeline was successful
feat(fight): 新增疲惫状态并优化睡眠状态机制

- 实现疲惫状态(StatusTired),仅限制攻击技能,允许属性技能正常使用
- 重构睡眠状态,改为在被攻击且未miss时立即解除,而非技能使用后
- 修复寄生种子效果触发时机,改为回合开始时触发
- 调整寄生效果的目标为技能施放者而非对手

fix(fight): 修正战斗回合逻辑和技能持续时间处理

- 修复Effect2194中状态添加函数调用,使用带时间参数的版本
- 修正Effect13中技能持续时间计算,避免额外减1的问题
- 优化回合处理逻辑,当双方都未出手时跳过动作阶段

refactor(cdk): 重构CDK配置结构和服务器冠名功能

- 将CDKConfig中的CDKType字段重命名为Type以符合GORM映射
- 优化UseServerNamingCDK方法的上下文处理逻辑
- 修复服务器冠名CDK使用时的类型检查条件

feat(player): 完善宠物经验系统和CDK兑换功能

- 增强AddPetExp方法,处理宠物等级达到100级的情况
- 添加查询当前账号有效期内服务器冠名信息的API接口
- 实现服务器服务相关的数据模型和查询方法

fix(task): 任务查询支持启用和未启用状态

- 修改任务服务中的Get、GetDaily、GetWeek方法
- 当启用状态下无结果时,自动查询未启用状态的任务配置
```
2026-04-13 22:27:27 +08:00
昔念
f95fd49efd ```
All checks were successful
ci/woodpecker/push/my-first-workflow Pipeline was successful
feat(fight): 新增疲惫状态并优化睡眠状态机制

- 实现疲惫状态(StatusTired),仅限制攻击技能,允许属性技能正常使用
- 重构睡眠状态,改为在被攻击且未miss时立即解除,而非技能使用后
- 修复寄生种子效果触发时机,改为回合开始时触发
- 调整寄生效果的目标为技能施放者而非
2026-04-13 21:06:45 +08:00
昔念
ce1a2a3588 ```
All checks were successful
ci/woodpecker/push/my-first-workflow Pipeline was successful
feat(xmlres): 使用rawFlexibleString替换字符串类型以支持灵活解析

- 将EffectArg结构体中的SideEffectArg字段类型从string改为rawFlexibleString
- 将Move结构体中的Name字段类型从string改为rawFlexibleString,并更新反序列化逻辑
- 统一配置文件解析方式,移除磁盘回退机制并简化readConfigContent函数
- 移除不再使用的导入包和变量

fix(fight): 修复战斗系统中的空技能和无效数据问题

- 在collectAttackValues函数中过滤掉SkillID为0的攻击值
- 添加检查避免发送空的攻击信息到客户端
- 移除输入模块中未使用的捕捉逻辑

refactor(middleware): 重构中间件配置并添加CDK权限控制

- 简化middleware.go文件结构
- 为CDK相关接口添加适当的权限中间件
- 优化服务器代理配置

feat(player): 移除宠物捕捉状态字段

- 从ReadyFightPetInfo结构体中移除IsCapture字段
- 简化宠物准备信息的数据结构
```
2026-04-13 11:34:28 +08:00
昔念
3739c2a6f9 ```
All checks were successful
ci/woodpecker/push/my-first-workflow Pipeline was successful
feat(xmlres): 使用rawFlexibleString替换字符串类型以支持灵活解析

- 将EffectArg结构体中的SideEffectArg字段类型从string改为rawFlexibleString
- 将Move结构体中的Name字段类型从string改为rawFlexibleString,并更新反序列化逻辑
- 统一配置文件解析方式,移除磁盘回退机制并简化readConfigContent函数
- 移除不再使用的导入包和变量

fix(fight): 修复战斗系统中的空技能和无效数据问题

- 在
2026-04-13 11:28:30 +08:00
昔念
eca7dd86e1 ```
All checks were successful
ci/woodpecker/push/my-first-workflow Pipeline was successful
fix(fight): 修复单输入战斗中效果处理逻辑错误

- 在Effect201的OnSkill方法中调整了多输入战斗检查的位置,
  确保单输入战斗中的单目标效果被正确忽略

- 添加了针对单输入战斗中单目标效果的测试用例

- 移除了重复的多输入战斗检查代码

feat(fight): 添加战斗初始化时捕获标识设置功能

- 在initfightready函数中添加对CanCapture字段的处理
  将玩家的捕获能力信息传递到战斗准备信息中

- 在ReadyFightPetInfo结构体中添加IsCapture字段用于
  标识宠物是否为捕获类型

refactor(fight): 调整战斗初始化顺序确保数据一致性

- 将ReadyInfo初始化移到绑定输入上下文之后执行
  确保团队视图链接完成后再进行准备信息构建

fix(player): 增加宠物血量检查避免无效匹配

- 在玩家匹配检测中增加首只宠物血量检查
  当首只宠物血量为0时不参与匹配以防止异常情况
```
2026-04-13 10:21:13 +08:00
昔念
e161e3626f ```
All checks were successful
ci/woodpecker/push/my-first-workflow Pipeline was successful
fix(fight): 修复单输入战斗中效果处理逻辑错误

- 在Effect201的OnSkill方法中调整了多输入战斗检查的位置,
  确保单输入战斗中的单目标效果被正确忽略

- 添加了针对单输入战斗中单目标效果的测试用例

- 移除了重复的多输入战斗检查代码

feat(fight): 添加战斗初始化时捕获标识
2026-04-13 09:59:09 +08:00
昔念
e1a994ba11 ```
All checks were successful
ci/woodpecker/push/my-first-workflow Pipeline was successful
feat(fight): 添加效果工厂模式支持以解决闭包变量捕获问题

- 新增initskillFactory函数用于注册效果工厂
- 修改技能效果注册逻辑从直接实例化改为工厂模式
- 解决循环中闭包捕获变量导致的潜在问题

feat(fight): 实现对手输入获取逻辑优化回合处理

- 添加roundOpponentInput方法获取对手输入
- 重构enterturn方法中的先后手逻辑
- 确保攻击方和被攻击
2026-04-12 22:44:13 +08:00
昔念
82bb99d141 ```
refactor(common/rpc): 移除Redis PubSub心跳机制并优化连接管理

移除Redis PubSub连接的心跳保活功能,因为PubSub连接只应负责订阅和接收,
避免在同一连接上并发执行PING操作。更新了ListenFunc和ListenFight函数,
统一代码结构,移除了context包依赖,并添加了相关注释说明。

feat(logic/pet): 新增宠物技能提交功能

新增CommitPetSkills接口用于一次性提交宠物技能学习/替换/排序结果。
实现技能验证、费用计算和状态更新逻辑,包括新技能学习成本和排序费用。
添加isSameUint32Slice辅助函数用于比较技能数组。
```
2026-04-12 19:14:18 +08:00
昔念
f9543a5156 ```
All checks were successful
ci/woodpecker/push/my-first-workflow Pipeline was successful
feat(fight): 使用专用函数构建战斗结束数据包

为战斗结束消息创建专用的构建函数,
统一处理战斗结束信息的数据包构建逻辑,
提高代码的一致性和可维护性。

fix(config): 优化数据库查询语句以提高性能

将数组包含操作(@>)替换为 ANY 操作符,
在 Egg、MapPit、PetFusion 等服务中使用更高效
的查询方式
2026-04-12 13:27:39 +08:00
昔念
174830731c ```
All checks were successful
ci/woodpecker/push/my-first-workflow Pipeline was successful
feat(xmlres): 添加磁盘配置文件回退机制并支持JSON格式配置

- 新增readConfigContent函数,优先从资源包读取配置,失败时回退到磁盘文件
- 添加diskConfigPath变量存储本地配置路径
- 支持从磁盘读取JSON格式配置文件,增强配置灵活性
- 修改getJson函数增加错误处理和调试日志输出
- 将技能配置从XML格式改为JSON格式,提升数据解析效率
- 初始化时设置默认磁盘配置路径为public/config目录
```
2026-04-12 04:09:19 +08:00
xinian
3a7f593105 fix: 修复 Effect201 在单人战斗中误生效的问题
All checks were successful
ci/woodpecker/push/my-first-workflow Pipeline was successful
2026-04-11 22:22:23 +08:00
xinian
f6aa0c3339 feat: 重构任务奖励系统并增加宠物技能和皮肤奖励
All checks were successful
ci/woodpecker/push/my-first-workflow Pipeline was successful
将任务奖励逻辑重构到单独的文件中,增加对宠物技能和皮肤奖励的支持,优化任务完成处理流程
2026-04-11 19:25:59 +08:00
xinian
ecc483a11a Merge commit '5f5634d999893b23650cba92f2914be2cc895049' 2026-04-11 11:21:58 +00:00
xinian
97c8231b44 编辑文件 help.md
All checks were successful
ci/woodpecker/push/my-first-workflow Pipeline was successful
2026-04-11 19:21:32 +08:00
xinian
5f5634d999 perf: 优化战斗逻辑性能与内存分配 2026-04-11 09:39:00 +08:00
昔念
5bfdb5c32b 缺少代码差异信息,无法生成具体的commit message。
All checks were successful
ci/woodpecker/push/my-first-workflow Pipeline was successful
请提供具体的代码差异内容,我将根据Angular规范为您生成符合要求的中文commit message,包含适当的type、scope、subject和body部分,并确保每行不超过100个字符。
2026-04-11 00:46:43 +08:00
xinian
90f1447d48 refactor: 重构服务器冠名逻辑至独立表
All checks were successful
ci/woodpecker/push/my-first-workflow Pipeline was successful
2026-04-10 19:36:59 +08:00
xinian
ee3f25438f 编辑文件 help.md
All checks were successful
ci/woodpecker/push/my-first-workflow Pipeline was successful
2026-04-10 14:25:36 +08:00
xinian
2d8969bed2 编辑文件 config.yaml 2026-04-10 12:18:32 +08:00
xinian
fa5d50955d 编辑文件 my-first-workflow.yaml 2026-04-10 12:15:11 +08:00
xinian
6574450489 编辑文件 my-first-workflow.yaml 2026-04-10 12:11:13 +08:00
xinian
0daeb70900 fix: 修复日志格式化字符串错误和任务奖励逻辑
All checks were successful
ci/woodpecker/push/my-first-workflow Pipeline was successful
2026-04-10 10:28:22 +08:00
昔念
061e4f0c51 Merge branch 'main' of https://cnb.cool/blzing/blazing
Some checks failed
ci/woodpecker/push/my-first-workflow Pipeline failed
2026-04-10 01:55:25 +08:00
昔念
5c76aa7079 ```
feat(fight): 新增团战胜利关闭和超时退出功能

新增 GroupFightWinClose 和 GroupFightTimeoutExit 方法,
用于处理团战胜利关闭和超时退出逻辑,统一调用 QuitFight() 退出战斗。

fix(gold_list): 修复挂单服务中的逻辑错误和潜在异常

修复了 GoldListService 中的多处问题:
- 修正条件判断语句格式
- 添加数据库查询错误检查
- 优化
2026-04-10 01:55:13 +08:00
xinian
b327398448 refactor: 重构任务服务读写逻辑
Some checks failed
ci/woodpecker/push/my-first-workflow Pipeline failed
2026-04-09 22:59:28 +08:00
xinian
d0abb08d5b fix: 修复获取全部/文件读取/ReqShop/ReqShopReqShop 请求错误
All checks were successful
ci/woodpecker/push/my-first-workflow Pipeline was successful
2026-04-09 15:37:48 +08:00
xinian
d2cd601802 更新说明
All checks were successful
ci/woodpecker/push/my-first-workflow Pipeline was successful
2026-04-09 13:11:59 +08:00
昔念
487ee0e726 ```
All checks were successful
ci/woodpecker/push/my-first-workflow Pipeline was successful
feat(fight): 添加旧组队协议支持并优化战斗系统

- 实现了旧组队协议相关功能,包括GroupReadyFightFinish、GroupUseSkill、
  GroupUseItem、GroupChangePet和GroupEscape方法
- 新增组队战斗相关的入站信息结构体定义
- 实现了组队BOSS战斗逻辑,添加groupBossSlotLimit常量
- 重构宠物技能设置逻辑,调整金币消耗时机
- 优化战斗循环逻辑,添加对无行动槽位的处理
- 改进AI行动逻辑,增加多位置目标选择机制
- 完善捕获系统上下文处理,修复空指针问题
- 添加战斗状态更新和数据同步机制

fix(pet-skill): 修复宠物技能设置中的金币扣除逻辑错误

- 将金币扣除逻辑移到验证之后
- 修正宠物技能数量限制检查的顺序
- 防止重复添加已有技能的情况

refactor(fight): 重构战斗系统代码结构

- 分离新旧组队协议的战斗创建逻辑
- 优化战斗输入验证和处理流程
- 改进战斗循环中的错误处理机制
```
2026-04-09 02:14:09 +08:00
xinian
3b35789b47 feat: 优化CDK服务器冠名逻辑与鉴权
Some checks failed
ci/woodpecker/push/my-first-workflow Pipeline failed
2026-04-08 19:31:44 +08:00
xinian
28b6386963 feat: 新增CDK兑换冠名接口 2026-04-08 18:12:02 +08:00
xinian
1ca0ff344e feat: 新增服务器冠名CDK兑换功能
Some checks failed
ci/woodpecker/push/my-first-workflow Pipeline failed
2026-04-08 15:49:03 +08:00
xinian
9825944efc feat: 添加批量生成CDK功能
All checks were successful
ci/woodpecker/push/my-first-workflow Pipeline was successful
2026-04-08 14:17:10 +08:00
xinian
ca96be3905 refactor: 统一战斗报文发送逻辑
All checks were successful
ci/woodpecker/push/my-first-workflow Pipeline was successful
2026-04-08 12:26:37 +08:00
xinian
4b89588c22 编辑文件 Dockerfile
Some checks failed
ci/woodpecker/push/my-first-workflow Pipeline failed
2026-04-08 10:11:33 +08:00
昔念
0051ac0be8 ```
All checks were successful
ci/woodpecker/push/my-first-workflow Pipeline was successful
feat(fight): 添加旧组队协议支持并优化战斗系统

- 实现了旧组队协议相关功能,包括GroupReadyFightFinish、GroupUseSkill、
  GroupUseItem、GroupChangePet和GroupEscape方法
- 新增组队战斗相关的入站信息结构体定义
- 实现了组队BOSS战斗逻辑,添加groupBossSlotLimit常量
- 重构宠物技能设置逻辑,调整金币消耗时机
- 优化战斗循环逻辑,添加对无行动槽位的处理
- 改进AI行动逻辑,增加多位置目标选择
2026-04-08 01:28:55 +08:00
昔念
918cdeac0e Merge branch 'main' of https://cnb.cool/blzing/blazing 2026-04-07 17:26:52 +08:00
xinian
13244313f1 编辑文件 gold_list.go
All checks were successful
ci/woodpecker/push/my-first-workflow Pipeline was successful
2026-04-06 12:09:40 +08:00
xinian
4ea9864833 perf: 使用数组代替map优化元素计算性能
All checks were successful
ci/woodpecker/push/my-first-workflow Pipeline was successful
2026-04-06 07:16:57 +08:00
xinian
77057e01b6 refactor: 优化命令注册和请求处理逻辑 2026-04-06 07:07:15 +08:00
xinian
f030b61645 fix: 优化TCP/WebSocket协议检测与处理逻辑
All checks were successful
ci/woodpecker/push/my-first-workflow Pipeline was successful
2026-04-06 06:33:24 +08:00
xinian
5a44154d30 feat: 添加地图节点匹配和战斗等级上限 2026-04-06 05:24:14 +08:00
xinian
a905954b5c feat: 添加宠物训练加成效果
All checks were successful
ci/woodpecker/push/my-first-workflow Pipeline was successful
2026-04-06 03:47:17 +08:00
xinian
99748ba41e refactor: 重构奖励发放逻辑并支持签到默认奖励
Some checks failed
ci/woodpecker/push/my-first-workflow Pipeline failed
2026-04-06 03:42:48 +08:00
xinian
40ec827342 refactor: 重构战斗属性和特效应用逻辑
All checks were successful
ci/woodpecker/push/my-first-workflow Pipeline was successful
2026-04-06 03:11:38 +08:00
xinian
a16a06e389 refactor: 重构签到系统和战斗特效逻辑 2026-04-06 02:51:13 +08:00
xinian
5b37d9493b feat: 实现每日签到功能并优化战斗和道具逻辑 2026-04-06 02:06:11 +08:00
xinian
f433a26a6d refactor: 重构战斗系统为统一动作包结构
All checks were successful
ci/woodpecker/push/my-first-workflow Pipeline was successful
2026-04-06 00:58:23 +08:00
xinian
141ba67014 编辑文件 Dockerfile
All checks were successful
ci/woodpecker/push/my-first-workflow Pipeline was successful
2026-04-05 23:14:54 +08:00
xinian
d83cf365ac 更新说明
Some checks failed
ci/woodpecker/push/my-first-workflow Pipeline failed
2026-04-05 23:13:06 +08:00
xinian
24b463f0aa feat: 增强 Boss 脚本 HookAction 接入能力
All checks were successful
ci/woodpecker/push/my-first-workflow Pipeline was successful
引入 BossHookActionContext 封装战斗上下文,并支持脚本调用 useSkill 和 switchPet 函数控制战斗行为。
2026-04-05 22:27:38 +08:00
xinian
c021b40fbe feat: 增强踢人逻辑与BOSS脚本支持
All checks were successful
ci/woodpecker/push/my-first-workflow Pipeline was successful
优化踢人超时处理和僵尸连接清理,支持BOSS动作脚本并增加测试,修复事件匹配与战斗循环中的并发问题。
2026-04-05 21:59:22 +08:00
xinian
36dd93b076 编辑文件 Dockerfile
All checks were successful
ci/woodpecker/push/my-first-workflow Pipeline was successful
2026-04-05 21:16:01 +08:00
昔念
3ee1283a2c ```
feat(pet): 新增精灵可学习技能查询功能

新增 GetPetLearnableSkills 接口用于查询当前精灵可学习技能(包含等级技能和额外技能ExtSKill),
优化 SetPetSkill 和 SortPetSkills 方法中的技能处理逻辑,提升技能管理和排序的准确性。

同时修复了宠物存储信息查询时缺少参数验证的问题,在管理后台接口中增加 free 参数支持。

BREAKING CHANGE: 管理后台
2026-04-05 12:45:00 +08:00
昔念
c3da3162ee ```
feat(player): 添加玩家断开连接时的安全保存机制

- 实现 SaveOnDisconnect 方法,确保玩家数据在断开连接时安全保存
- 添加并发控制防止重复保存操作,使用互斥锁和完成通道确保一次保存
- 在 socket 关闭事件中改为异步调用 SaveOnDisconnect 避免阻塞
- 添加 panic 恢复机制保护保存过程中的异常情况

refactor(login): 优化登录时的踢人逻辑和超时处理
2026-04-05 11:14:25 +08:00
xinian
37cd641942 refactor: 重构 Prop 字段位置至 baseplayer
Some checks failed
ci/woodpecker/push/my-first-workflow Pipeline failed
2026-04-05 07:45:51 +08:00
xinian
87145579e6 refactor: 移除宠物显示提供者接口
Some checks failed
ci/woodpecker/push/my-first-workflow Pipeline failed
2026-04-05 07:41:50 +08:00
xinian
7ec6381cf1 111 2026-04-05 07:30:55 +08:00
203 changed files with 10881 additions and 364067 deletions

3
.gitignore vendored
View File

@@ -46,4 +46,5 @@ public/login-linux-amd64
.cache/gomod/**
public/login-login-linux-amd64
public/logic_linux-amd64_1
.cache/**
.cache/**
.agents/**

View File

@@ -18,9 +18,11 @@ ENV GOMODCACHE=/workspace/.cache/gomod
# ==========================================
# 2. Codex 配置 (更换时修改这里重新 build)
# ==========================================
ENV CODEX_BASE_URL="https://fastai.fast"
ENV CODEX_BASE_URL="https://api.jucode.cn/v1"
ENV CODEX_MODEL="gpt-5.4"
ENV OPENAI_API_KEY="sk-63e660ffa319cac900c17185c69282c1f62403f04717334fc8f492d1816bf657"
ENV OPENAI_API_KEY="sk-E0ZZIFNnD0RkhMC9pT2AGMutz9vNy2VLNrgyyobT5voa81pQ"
# ==========================================
# 3. 安装系统依赖GolangCode-server

View File

@@ -1,12 +1,7 @@
青氧,十九禁给
https://api.aibh.site/console 张晟 2922919493Zs.
RUN curl -fsSL https://oss.itbzzb.cn/setup-codex.sh | \
YES=1 bash -s -- --base-url https://api.aibh.site \
--api-key sk-foAHgsJtmanACECtBlFYZE2z4LkwBboEOYETO3ZdWvCxdmNr \
--mirror auto
https://api.gemai.cc/console/token 免费给部分额度 ,还有100块
https://api.jucode.cn/
fastai.fast 使用谷歌邮箱https://linshiguge.com/白嫖
https://zread.ai/tawer-blog/lmarena-2api/1-overview GLM web2 pai
https://crazyrouter.com/console 模型最便宜,看看能不能1:10
@@ -14,7 +9,7 @@ https://crazyrouter.com/console 模型最便宜,看看能不能1:10
https://agentrouter.org/pricing 签到给,有175
kuaipao.ai 充了十块 cjf19970621 cjf19970621
充了十块
使用网址https://www.jnm.lol
@@ -26,3 +21,10 @@ kuaipao.ai 充了十块 cjf19970621 cjf19970621
fastai.fast 575560454@qq.com 575560454

2
.vscode/launch.json vendored
View File

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

View File

@@ -144,14 +144,14 @@ steps:
scp-exe-to-servers: # 与fetch-deploy-config同级缩进2个空格
image: appleboy/drone-scp:1.6.2 # 子元素缩进4个空格
settings: # 子元素缩进4个空格
host: &ssh_host 2697v22.mc5173.cn
port: &ssh_port 16493
host: &ssh_host 43.248.3.21
port: &ssh_port 22
username: &ssh_user root
password: &ssh_pass xIy9PQcBF96C
password: &ssh_pass KQv7yzna7BDukK
source:
- blazing/build/**
target: /opt/blazing/
target: /ext/blazing/
strip_components: 1 # 统一缩进6个空格
skip_verify: true # 统一缩进6个空格
timeout: 30s # 统一缩进6个空格
@@ -167,7 +167,7 @@ steps:
password: *ssh_pass
script:
- |
cd /opt/blazing/build
cd /ext/blazing/build
ls -t login_* 2>/dev/null | head -1
BIN_NAME=$(ls -t login_* 2>/dev/null | head -1)
echo "BIN_NAME: $BIN_NAME"
@@ -201,9 +201,9 @@ steps:
# 移动logic产物到public目录
LOGIC_BIN=$(ls -t logic_* 2>/dev/null | head -1)
if [ -n "$LOGIC_BIN" ]; then
mkdir -p /opt/blazing/build/public
mv $LOGIC_BIN /opt/blazing/build/public/
echo "✅ Logic产物已移动到 /opt/blazing/build/public/ | 文件: $(basename $LOGIC_BIN)"
mkdir -p /ext/blazing/build/public
mv $LOGIC_BIN /ext/blazing/build/public/
echo "✅ Logic产物已移动到 /ext/blazing/build/public/ | 文件: $(basename $LOGIC_BIN)"
else
echo "⚠️ 未找到Logic产物"
fi

View File

@@ -0,0 +1,56 @@
package main
import (
"database/sql"
"fmt"
"log"
_ "github.com/lib/pq"
)
func main() {
const dsn = "user=user_YrK4j7 password=password_jSDm76 host=43.248.3.21 port=5432 dbname=bl sslmode=disable timezone=Asia/Shanghai"
db, err := sql.Open("postgres", dsn)
if err != nil {
log.Fatal(err)
}
defer db.Close()
var (
id int64
cdkCode string
cdkType int64
exchangeRemainCount int64
bindUserID int64
validEndTime sql.NullTime
remark sql.NullString
)
err = db.QueryRow(`
select id, cdk_code, type, exchange_remain_count, bind_user_id, valid_end_time, remark
from config_gift_cdk
where cdk_code = $1
`, "nrTbdXFBhKkaTdDk").Scan(
&id,
&cdkCode,
&cdkType,
&exchangeRemainCount,
&bindUserID,
&validEndTime,
&remark,
)
if err != nil {
log.Fatal(err)
}
fmt.Printf("id=%d\ncdk_code=%s\ntype=%d\nexchange_remain_count=%d\nbind_user_id=%d\nvalid_end_time=%v\nremark=%q\n",
id,
cdkCode,
cdkType,
exchangeRemainCount,
bindUserID,
validEndTime.Time,
remark.String,
)
}

View File

@@ -1,121 +1,121 @@
package coolconfig
import (
"time"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/util/gconv"
)
// cool config
type sConfig struct {
AutoMigrate bool `json:"auto_migrate,omitempty"` // 是否自动创建表
Eps bool `json:"eps,omitempty"` // 是否开启eps
File *file `json:"file,omitempty"` // 文件上传配置
Name string `json:"name"` // 项目名称
// LoginPort string `json:"port"`
GameOnlineID uint32 `json:"port_bl"` //这个是命令行输入的参数
ServerInfo ServerList
Address string //rpc端口
}
type ServerList struct {
OnlineID uint32 `gorm:"column:online_id;comment:'在线ID';uniqueIndex" json:"online_id"`
//服务器名称Desc
Name string `gorm:"comment:'服务器名称'" json:"name"`
IP string `gorm:"type:string;comment:'服务器IP'" json:"ip"`
Port uint32 `gorm:"comment:'端口号,通常是小整数'" json:"port"`
IsOpen uint8 `gorm:"default:0;not null;comment:'是否开启'" json:"is_open"`
//登录地址
LoginAddr string `gorm:"type:string;comment:'登录地址'" json:"login_addr"`
//账号
Account string `gorm:"type:string;comment:'账号'" json:"account"`
//密码
Password string `gorm:"type:string;comment:'密码'" json:"password"`
CanPort []uint32 `gorm:"type:jsonb;comment:'可连接端口'" json:"can_port"`
//是否测试服
IsVip uint32 `gorm:"default:0;not null;comment:'是否为VIP服务器'" json:"is_vip"`
//isdebug 是否本地服
IsDebug uint8 `gorm:"default:0;comment:'是否为调试模式'" json:"is_debug"`
//服务器属主Desc
Owner uint32 `gorm:"comment:'服务器属主'" json:"owner"`
Desc string `gorm:"comment:'服务器描述'" json:"desc"`
OldScreen string `gorm:"comment:'服务器screen参数'" json:"old_screen"`
//到期时间ServerList
ExpireTime time.Time `gorm:"default:0;comment:'到期时间'" json:"expire_time"`
}
func (s *ServerList) GetID() string {
return gconv.String(100000*s.OnlineID + s.Port)
}
// OSS相关配置
type oss struct {
Endpoint string `json:"endpoint"`
AccessKeyID string `json:"accessKeyID"`
SecretAccessKey string `json:"secretAccessKey"`
UseSSL bool `json:"useSSL"`
BucketName string `json:"bucketName"`
Location string `json:"location"`
}
// 文件上传配置
type file struct {
Mode string `json:"mode"` // 模式 local oss
Domain string `json:"domain"` // 域名 http://
Oss *oss `json:"oss,omitempty"`
}
// NewConfig new config
func newConfig() *sConfig {
var ctx g.Ctx
config := &sConfig{
AutoMigrate: GetCfgWithDefault(ctx, "blazing.autoMigrate", g.NewVar(false)).Bool(),
Name: GetCfgWithDefault(ctx, "server.name", g.NewVar("")).String(),
Eps: GetCfgWithDefault(ctx, "blazing.eps", g.NewVar(false)).Bool(),
// LoginPort: string(GetCfgWithDefault(ctx, "server.port", g.NewVar("8080")).String()),
Address: GetCfgWithDefault(ctx, "server.address", g.NewVar("8080")).String(),
//GamePort: GetCfgWithDefault(ctx, "server.game", g.NewVar("8080")).Uint64s(),
File: &file{
Mode: GetCfgWithDefault(ctx, "blazing.file.mode", g.NewVar("none")).String(),
Domain: GetCfgWithDefault(ctx, "blazing.file.domain", g.NewVar("http://127.0.0.1:8300")).String(),
Oss: &oss{
Endpoint: GetCfgWithDefault(ctx, "blazing.file.oss.endpoint", g.NewVar("127.0.0.1:9000")).String(),
AccessKeyID: GetCfgWithDefault(ctx, "blazing.file.oss.accessKeyID", g.NewVar("")).String(),
SecretAccessKey: GetCfgWithDefault(ctx, "blazing.file.oss.secretAccessKey", g.NewVar("")).String(),
UseSSL: GetCfgWithDefault(ctx, "blazing.file.oss.useSSL", g.NewVar(false)).Bool(),
BucketName: GetCfgWithDefault(ctx, "blazing.file.oss.bucketName", g.NewVar("blazing")).String(),
Location: GetCfgWithDefault(ctx, "blazing.file.oss.location", g.NewVar("us-east-1")).String(),
},
},
}
return config
}
// qiniu 七牛云配置
type qiniu struct {
AccessKey string `json:"ak"`
SecretKey string `json:"sk"`
Bucket string `json:"bucket"`
CDN string `json:"cdn"`
}
// Config config
var Config = newConfig()
// GetCfgWithDefault get config with default value
func GetCfgWithDefault(ctx g.Ctx, key string, defaultValue *g.Var) *g.Var {
value, err := g.Cfg().GetWithEnv(ctx, key)
if err != nil {
return defaultValue
}
if value.IsEmpty() || value.IsNil() {
return defaultValue
}
return value
}
package coolconfig
import (
"time"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/util/gconv"
)
// cool config
type sConfig struct {
AutoMigrate bool `json:"auto_migrate,omitempty"` // 是否自动创建表
Eps bool `json:"eps,omitempty"` // 是否开启eps
File *file `json:"file,omitempty"` // 文件上传配置
Name string `json:"name"` // 项目名称
// LoginPort string `json:"port"`
GameOnlineID uint32 `json:"port_bl"` //这个是命令行输入的参数
ServerInfo ServerList
Address string //rpc端口
}
type ServerList struct {
OnlineID uint32 `gorm:"column:online_id;comment:'在线ID';uniqueIndex" json:"online_id"`
//服务器名称Desc
Name string `gorm:"comment:'服务器名称'" json:"name"`
IP string `gorm:"type:string;comment:'服务器IP'" json:"ip"`
Port uint32 `gorm:"comment:'端口号,通常是小整数'" json:"port"`
IsOpen uint8 `gorm:"default:0;not null;comment:'是否开启'" json:"is_open"`
//登录地址
LoginAddr string `gorm:"type:string;comment:'登录地址'" json:"login_addr"`
//账号
Account string `gorm:"type:string;comment:'账号'" json:"account"`
//密码
Password string `gorm:"type:string;comment:'密码'" json:"password"`
CanPort []uint32 `gorm:"type:jsonb;comment:'可连接端口'" json:"can_port"`
//是否测试服
IsVip uint32 `gorm:"default:0;not null;comment:'是否为VIP服务器'" json:"is_vip"`
//isdebug 是否本地服
IsDebug uint8 `gorm:"default:0;comment:'是否为调试模式'" json:"is_debug"`
//服务器属主Desc
Owner uint32 `gorm:"comment:'服务器属主'" json:"owner"`
Desc string `gorm:"comment:'服务器描述'" json:"desc"`
OldScreen string `gorm:"comment:'服务器screen参数'" json:"old_screen"`
//到期时间ServerList
ExpireTime time.Time `gorm:"default:0;comment:'到期时间'" json:"expire_time"`
}
func (s *ServerList) GetID() string {
return gconv.String(100000*s.OnlineID + s.Port)
}
// OSS相关配置
type oss struct {
Endpoint string `json:"endpoint"`
AccessKeyID string `json:"accessKeyID"`
SecretAccessKey string `json:"secretAccessKey"`
UseSSL bool `json:"useSSL"`
BucketName string `json:"bucketName"`
Location string `json:"location"`
}
// 文件上传配置
type file struct {
Mode string `json:"mode"` // 模式 local oss
Domain string `json:"domain"` // 域名 http://
Oss *oss `json:"oss,omitempty"`
}
// NewConfig new config
func newConfig() *sConfig {
var ctx g.Ctx
config := &sConfig{
AutoMigrate: GetCfgWithDefault(ctx, "blazing.autoMigrate", g.NewVar(false)).Bool(),
Name: GetCfgWithDefault(ctx, "server.name", g.NewVar("")).String(),
Eps: GetCfgWithDefault(ctx, "blazing.eps", g.NewVar(false)).Bool(),
// LoginPort: string(GetCfgWithDefault(ctx, "server.port", g.NewVar("8080")).String()),
Address: GetCfgWithDefault(ctx, "server.address", g.NewVar("8080")).String(),
//GamePort: GetCfgWithDefault(ctx, "server.game", g.NewVar("8080")).Uint64s(),
File: &file{
Mode: GetCfgWithDefault(ctx, "blazing.file.mode", g.NewVar("none")).String(),
Domain: GetCfgWithDefault(ctx, "blazing.file.domain", g.NewVar("http://127.0.0.1:8300")).String(),
Oss: &oss{
Endpoint: GetCfgWithDefault(ctx, "blazing.file.oss.endpoint", g.NewVar("127.0.0.1:9000")).String(),
AccessKeyID: GetCfgWithDefault(ctx, "blazing.file.oss.accessKeyID", g.NewVar("")).String(),
SecretAccessKey: GetCfgWithDefault(ctx, "blazing.file.oss.secretAccessKey", g.NewVar("")).String(),
UseSSL: GetCfgWithDefault(ctx, "blazing.file.oss.useSSL", g.NewVar(false)).Bool(),
BucketName: GetCfgWithDefault(ctx, "blazing.file.oss.bucketName", g.NewVar("blazing")).String(),
Location: GetCfgWithDefault(ctx, "blazing.file.oss.location", g.NewVar("us-east-1")).String(),
},
},
}
return config
}
// qiniu 七牛云配置
type qiniu struct {
AccessKey string `json:"ak"`
SecretKey string `json:"sk"`
Bucket string `json:"bucket"`
CDN string `json:"cdn"`
}
// Config config
var Config = newConfig()
// GetCfgWithDefault get config with default value
func GetCfgWithDefault(ctx g.Ctx, key string, defaultValue *g.Var) *g.Var {
value, err := g.Cfg().GetWithEnv(ctx, key)
if err != nil {
return defaultValue
}
if value.IsEmpty() || value.IsNil() {
return defaultValue
}
return value
}

View File

@@ -22,8 +22,14 @@ var ctx = context.TODO()
type Cmd struct {
Func reflect.Value //方法函数
Req reflect.Type //请求体
// HeaderFieldIndex 是请求结构体中 TomeeHeader 字段的索引路径。
HeaderFieldIndex []int
// UseConn 标记第二个参数是否为 gnet.Conn。
UseConn bool
// 新增预缓存的req创建函数返回结构体指针
NewReqFunc func() interface{}
// NewReqValue 返回请求结构体指针的 reflect.Value避免重复构造类型信息。
NewReqValue func() reflect.Value
//Res reflect.Value //返回体
}

View File

@@ -3,6 +3,7 @@ package cool
import (
_ "blazing/contrib/drivers/pgsql"
"blazing/cool/cooldb"
"sync"
"github.com/gogf/gf/v2/encoding/gjson"
"github.com/gogf/gf/v2/frame/g"
@@ -10,6 +11,11 @@ import (
"gorm.io/gorm"
)
var (
autoMigrateMu sync.Mutex
autoMigrateModels []IModel
)
// 初始化数据库连接供gorm使用
func InitDB(group string) (*gorm.DB, error) {
// var ctx context.Context
@@ -54,9 +60,33 @@ func getDBbyModel(model IModel) *gorm.DB {
// 根据entity结构体创建表
func CreateTable(model IModel) error {
if Config.AutoMigrate {
autoMigrateMu.Lock()
autoMigrateModels = append(autoMigrateModels, model)
autoMigrateMu.Unlock()
return nil
}
// RunAutoMigrate 显式执行已注册模型的建表/迁移。
func RunAutoMigrate() error {
if !Config.AutoMigrate {
return nil
}
autoMigrateMu.Lock()
models := append([]IModel(nil), autoMigrateModels...)
autoMigrateMu.Unlock()
seen := make(map[string]struct{}, len(models))
for _, model := range models {
key := model.GroupName() + ":" + model.TableName()
if _, ok := seen[key]; ok {
continue
}
seen[key] = struct{}{}
db := getDBbyModel(model)
return db.AutoMigrate(model)
if err := db.AutoMigrate(model); err != nil {
return err
}
}
return nil
}

View File

@@ -6,6 +6,16 @@ func AddClient(id uint32, client *ClientHandler) {
Clientmap.Store(id, client) // sync.Map存值
}
// 清理指定clientuid=100000*onlineID+port
func DeleteClientOnly(uid uint32) {
Clientmap.Delete(uid)
}
// 清理指定clientonlineID+port
func DeleteClient(id, port uint32) {
Clientmap.Delete(100000*id + port)
}
// 取值示例
func GetClient(id, port uint32) (*ClientHandler, bool) {
// 普通mapclient, ok := Clientmap[id]

View File

@@ -42,15 +42,15 @@ const (
maxMatrixSize = 227 // 矩阵维度覆盖最大属性ID 226
)
// 合法单属性ID集合快速校验
var validSingleElementIDs = map[int]bool{
// 合法单属性ID集合按ID直接索引避免运行时 map 查找
var validSingleElementIDs = [maxMatrixSize]bool{
1: true, 2: true, 3: true, 4: true, 5: true, 6: true, 7: true, 8: true, 9: true, 10: true,
11: true, 12: true, 13: true, 14: true, 15: true, 16: true, 17: true, 18: true, 19: true, 20: true,
221: true, 222: true, 223: true, 224: true, 225: true, 226: true,
}
// 元素名称映射(全属性对应,便于日志输出)
var elementNameMap = map[ElementType]string{
// 元素名称映射(按ID直接索引,便于日志输出)
var elementNameMap = [maxMatrixSize]string{
ElementTypeGrass: "GRASS",
ElementTypeWater: "WATER",
ElementTypeFire: "FIRE",
@@ -198,46 +198,55 @@ type ElementCombination struct {
ID int // 组合唯一ID
}
// 全局预加载资源(程序启动时init初始化,运行时直接使用
// 全局预加载资源(程序启动时初始化,运行时只读
var (
// 元素组合池key=组合IDvalue=组合实例(预加载所有合法组合)
elementCombinationPool = make(map[int]*ElementCombination, 150) // 128双+26单=154预分配足够容量
// 单属性克制矩阵预初始化所有特殊克制关系默认1.0
matrix [maxMatrixSize][maxMatrixSize]float64
validCombinationIDs [maxMatrixSize]bool
elementCombinationPool [maxMatrixSize]ElementCombination
dualElementSecondaryPool [maxMatrixSize]ElementType
matrix [maxMatrixSize][maxMatrixSize]float64
Calculator *ElementCalculator
)
// init 预加载所有资源(程序启动时执行一次,无并发问题)
func init() {
// 1. 初始化单属性克制矩阵
initFullTableMatrix()
initElementCombinationPool()
Calculator = NewElementCalculator()
}
// 2. 预加载所有单属性组合
for id := range validSingleElementIDs {
combo := &ElementCombination{
Primary: ElementType(id),
Secondary: nil,
ID: id,
func initElementCombinationPool() {
for id, valid := range validSingleElementIDs {
if !valid {
continue
}
validCombinationIDs[id] = true
elementCombinationPool[id] = ElementCombination{
Primary: ElementType(id),
ID: id,
}
elementCombinationPool[id] = combo
}
// 3. 预加载所有双属性组合
for dualID, atts := range dualElementMap {
primaryID, secondaryID := atts[0], atts[1]
// 按ID升序排序保证组合一致性
primary, secondary := ElementType(primaryID), ElementType(secondaryID)
if primary > secondary {
primary, secondary = secondary, primary
}
combo := &ElementCombination{
dualElementSecondaryPool[dualID] = secondary
validCombinationIDs[dualID] = true
elementCombinationPool[dualID] = ElementCombination{
Primary: primary,
Secondary: &secondary,
Secondary: &dualElementSecondaryPool[dualID],
ID: dualID,
}
elementCombinationPool[dualID] = combo
}
}
func isValidCombinationID(id int) bool {
return id > 0 && id < maxMatrixSize && validCombinationIDs[id]
}
// IsDual 判断是否为双属性
func (ec *ElementCombination) IsDual() bool {
return ec.Secondary != nil
@@ -245,84 +254,82 @@ func (ec *ElementCombination) IsDual() bool {
// Elements 获取所有属性列表
func (ec *ElementCombination) Elements() []ElementType {
if ec.IsDual() {
return []ElementType{ec.Primary, *ec.Secondary}
if secondary := ec.Secondary; secondary != nil {
return []ElementType{ec.Primary, *secondary}
}
return []ElementType{ec.Primary}
}
// String 友好格式化输出
func (ec *ElementCombination) String() string {
primaryName := elementNameMap[ec.Primary]
if !ec.IsDual() {
return fmt.Sprintf("(%s)", primaryName)
if secondary := ec.Secondary; secondary != nil {
return fmt.Sprintf("(%s, %s)", elementNameMap[ec.Primary], elementNameMap[*secondary])
}
return fmt.Sprintf("(%s, %s)", primaryName, elementNameMap[*ec.Secondary])
return fmt.Sprintf("(%s)", elementNameMap[ec.Primary])
}
// ElementCalculator 无锁元素克制计算器(依赖预加载资源
// ElementCalculator 无锁元素克制计算器(所有倍数在初始化阶段预计算
type ElementCalculator struct {
offensiveCache map[string]float64 // 攻击克制缓存(运行时填充,无并发写)
offensiveTable [maxMatrixSize][maxMatrixSize]float64
}
// NewElementCalculator 创建计算器实例(仅初始化缓存)
// NewElementCalculator 创建计算器实例(构建只读查表缓存)
func NewElementCalculator() *ElementCalculator {
return &ElementCalculator{
offensiveCache: make(map[string]float64, 4096), // 预分配大容量缓存
c := &ElementCalculator{}
c.initOffensiveTable()
return c
}
func (c *ElementCalculator) initOffensiveTable() {
for attackerID, valid := range validCombinationIDs {
if !valid {
continue
}
attacker := &elementCombinationPool[attackerID]
for defenderID, valid := range validCombinationIDs {
if !valid {
continue
}
defender := &elementCombinationPool[defenderID]
c.offensiveTable[attackerID][defenderID] = c.calculateMultiplier(attacker, defender)
}
}
}
// getMatrixValue 直接返回矩阵值修复核心问题不再将0转换为1
func (c *ElementCalculator) getMatrixValue(attacker, defender ElementType) float64 {
return matrix[attacker][defender] // 矩阵默认已初始化1.0,特殊值直接返回
return matrix[attacker][defender]
}
// GetCombination 获取元素组合(直接从预加载池读取
// GetCombination 获取元素组合(直接按ID索引
func (c *ElementCalculator) GetCombination(id int) (*ElementCombination, error) {
combo, exists := elementCombinationPool[id]
if !exists {
if !isValidCombinationID(id) {
return nil, fmt.Errorf("invalid element combination ID: %d", id)
}
return combo, nil
return &elementCombinationPool[id], nil
}
// GetOffensiveMultiplier 计算攻击方→防御方的克制倍数(缓存优先
// GetOffensiveMultiplier 计算攻击方→防御方的克制倍数(只读查表
func (c *ElementCalculator) GetOffensiveMultiplier(attackerID, defenderID int) (float64, error) {
// 1. 获取预加载的组合实例
attacker, err := c.GetCombination(attackerID)
if err != nil {
return 0, fmt.Errorf("attacker invalid: %w", err)
if !isValidCombinationID(attackerID) {
return 0, fmt.Errorf("attacker invalid: invalid element combination ID: %d", attackerID)
}
defender, err := c.GetCombination(defenderID)
if err != nil {
return 0, fmt.Errorf("defender invalid: %w", err)
if !isValidCombinationID(defenderID) {
return 0, fmt.Errorf("defender invalid: invalid element combination ID: %d", defenderID)
}
// 2. 缓存键(全局唯一)
cacheKey := fmt.Sprintf("a%d_d%d", attackerID, defenderID)
if val, exists := c.offensiveCache[cacheKey]; exists {
return val, nil
}
// 3. 核心计算+缓存
val := c.calculateMultiplier(attacker, defender)
c.offensiveCache[cacheKey] = val
return val, nil
return c.offensiveTable[attackerID][defenderID], nil
}
// calculateMultiplier 核心克制计算逻辑
func (c *ElementCalculator) calculateMultiplier(attacker, defender *ElementCombination) float64 {
// 场景1单→单
if !attacker.IsDual() && !defender.IsDual() {
return c.getMatrixValue(attacker.Primary, defender.Primary)
}
// 场景2单→双
if !attacker.IsDual() {
y1, y2 := defender.Primary, *defender.Secondary
m1 := c.getMatrixValue(attacker.Primary, y1)
m2 := c.getMatrixValue(attacker.Primary, y2)
switch {
case m1 == 2 && m2 == 2:
return 4.0
@@ -333,12 +340,10 @@ func (c *ElementCalculator) calculateMultiplier(attacker, defender *ElementCombi
}
}
// 场景3双→单
if !defender.IsDual() {
return c.calculateDualToSingle(attacker.Primary, *attacker.Secondary, defender.Primary)
}
// 场景4双→双
x1, x2 := attacker.Primary, *attacker.Secondary
y1, y2 := defender.Primary, *defender.Secondary
coeffY1 := c.calculateDualToSingle(x1, x2, y1)
@@ -350,7 +355,6 @@ func (c *ElementCalculator) calculateMultiplier(attacker, defender *ElementCombi
func (c *ElementCalculator) calculateDualToSingle(attacker1, attacker2, defender ElementType) float64 {
k1 := c.getMatrixValue(attacker1, defender)
k2 := c.getMatrixValue(attacker2, defender)
switch {
case k1 == 2 && k2 == 2:
return 4.0
@@ -361,60 +365,49 @@ func (c *ElementCalculator) calculateDualToSingle(attacker1, attacker2, defender
}
}
var Calculator = NewElementCalculator()
// TestAllScenarios 全场景测试(验证预加载和计算逻辑)
func TestAllScenarios() {
// 测试1单→单草→水
m1, _ := Calculator.GetOffensiveMultiplier(1, 2)
fmt.Println("草→水: %.2f预期2.0", m1)
if math.Abs(m1-2.0) > 0.001 {
fmt.Println("测试1失败实际%.2f", m1)
}
// 测试2特殊单→单混沌→虚空
m2, _ := Calculator.GetOffensiveMultiplier(222, 226)
fmt.Println("混沌→虚空: %.2f预期0.0", m2)
if math.Abs(m2-0.0) > 0.001 {
fmt.Println("测试2失败实际%.2f", m2)
}
// 测试3单→双火→冰龙43
m3, _ := Calculator.GetOffensiveMultiplier(3, 43)
fmt.Println("火→冰龙: %.2f预期1.5", m3)
if math.Abs(m3-1.5) > 0.001 {
fmt.Println("测试3失败实际%.2f", m3)
}
// 测试4双→特殊单混沌暗影92→神灵223
m4, _ := Calculator.GetOffensiveMultiplier(92, 223)
fmt.Println("混沌暗影→神灵: %.2f预期1.25", m4)
if math.Abs(m4-1.25) > 0.001 {
fmt.Println("测试4失败实际%.2f", m4)
}
// 测试5双→双虚空邪灵113→混沌远古98
m5, _ := Calculator.GetOffensiveMultiplier(113, 98)
fmt.Println("虚空邪灵→混沌远古: %.2f预期0.875", m5)
if math.Abs(m5-0.875) > 0.001 {
fmt.Println("测试5失败实际%.2f", m5)
}
// 测试6缓存命中
m6, _ := Calculator.GetOffensiveMultiplier(113, 98)
if math.Abs(m6-m5) > 0.001 {
fmt.Println("测试6失败缓存未命中")
}
// 测试7含无效组合电→地面
m7, _ := Calculator.GetOffensiveMultiplier(5, 7)
fmt.Println("电→地面: %.2f预期0.0", m7)
if math.Abs(m7-0.0) > 0.001 {
fmt.Println("测试7失败实际%.2f", m7)
}
// 测试8双属性含无效电战斗→地面
m8, _ := Calculator.GetOffensiveMultiplier(35, 7)
fmt.Println("电战斗→地面: %.2f预期0.25", m8)
if math.Abs(m8-0.25) > 0.001 {

View File

@@ -5,7 +5,7 @@ type EffectArg struct {
SideEffect []struct {
ID int `json:"ID"`
SideEffectArgcount int `json:"SideEffectArgcount"`
SideEffectArg string `json:"SideEffectArg,omitempty"`
SideEffectArg rawFlexibleString `json:"SideEffectArg,omitempty"`
} `json:"SideEffect"`
} `json:"SideEffects"`
}

View File

@@ -5,7 +5,7 @@ import (
_ "blazing/common/data/xmlres/packed"
"encoding/json"
"os"
"fmt"
"github.com/ECUST-XX/xml"
"github.com/gogf/gf/v2/os/gres"
@@ -14,22 +14,36 @@ import (
var path string
func readConfigContent(path string) []byte {
return gres.GetContent(path)
}
func getXml[T any](path string) T {
// 解析XML到结构体
var xmls T
t1 := gres.GetContent(path)
t1 := readConfigContent(path)
xml.Unmarshal(t1, &xmls)
return xmls
}
func getJson[T any](path string) T {
// 解析XML到结构体
// 解析JSON到结构体
var xmls T
t1 := gres.GetContent(path)
json.Unmarshal(t1, &xmls)
t1 := readConfigContent(path)
if len(t1) == 0 {
fmt.Printf("[xmlres] getJson empty content: path=%s\n", path)
return xmls
}
if err := json.Unmarshal(t1, &xmls); err != nil {
head := string(t1)
if len(head) > 300 {
head = head[:300]
}
fmt.Printf("[xmlres] getJson unmarshal failed: path=%s len=%d err=%v head=%q\n", path, len(t1), err, head)
}
return xmls
}
@@ -58,8 +72,6 @@ var (
func Initfile() {
//gres.Dump()
path1, _ := os.Getwd()
path = path1 + "/public/config/"
path = "config/"
MapConfig = getXml[Maps](path + "210.xml")
@@ -87,10 +99,10 @@ func Initfile() {
return gconv.Int(m.ProductID)
})
Skill := getXml[MovesTbl](path + "227.xml")
skillConfig := getJson[MovesJSON](path + "moves_flash.json")
SkillMap = make(map[int]Move, len(Skill.Moves))
for _, v := range Skill.Moves {
SkillMap = make(map[int]Move, len(skillConfig.MovesTbl.Moves.Move))
for _, v := range skillConfig.MovesTbl.Moves.Move {
v.SideEffectS = ParseSideEffectArgs(v.SideEffect)
v.SideEffectArgS = ParseSideEffectArgs(v.SideEffectArg)
SkillMap[v.ID] = v
@@ -101,7 +113,11 @@ func Initfile() {
})
PetMAP = utils.ToMap[PetInfo, int](getXml[Monsters](path+"226.xml").Monsters, func(m PetInfo) int {
pets := getXml[Monsters](path + "226.xml").Monsters
for i := range pets {
pets[i].YieldingEVValues = parseYieldingEV(pets[i].YieldingEV)
}
PetMAP = utils.ToMap[PetInfo, int](pets, func(m PetInfo) int {
return m.ID
})

View File

@@ -0,0 +1,26 @@
package xmlres
import (
"encoding/json"
"testing"
)
func TestMoveUnmarshalJSONAcceptsNumericName(t *testing.T) {
var move Move
if err := json.Unmarshal([]byte(`{"ID":10001,"Name":1,"Category":1,"Type":8,"Power":35,"MaxPP":35,"Accuracy":95}`), &move); err != nil {
t.Fatalf("unmarshal move failed: %v", err)
}
if move.Name != "1" {
t.Fatalf("expected numeric name to convert to string, got %q", move.Name)
}
}
func TestEffectArgUnmarshalJSONAcceptsNumericSideEffectArg(t *testing.T) {
var cfg EffectArg
if err := json.Unmarshal([]byte(`{"SideEffects":{"SideEffect":[{"ID":1,"SideEffectArgcount":1,"SideEffectArg":3}]}}`), &cfg); err != nil {
t.Fatalf("unmarshal effect arg failed: %v", err)
}
if got := string(cfg.SideEffects.SideEffect[0].SideEffectArg); got != "3" {
t.Fatalf("expected numeric side effect arg to convert to string, got %q", got)
}
}

File diff suppressed because one or more lines are too long

View File

@@ -1,6 +1,11 @@
package xmlres
import "github.com/ECUST-XX/xml"
import (
"strconv"
"strings"
"github.com/ECUST-XX/xml"
)
// Move 表示怪物可学习的技能
type PetMoves struct {
@@ -45,6 +50,7 @@ type PetInfo struct {
Recycle int `xml:"Recycle,attr"` // 是否可回收
LearnableMoves LearnableMoves `xml:"LearnableMoves"` // 可学习的技能
NaturalEnemy string `xml:"NaturalEnemy,attr"` //天敌
YieldingEVValues []int64 `xml:"-"` // 预解析后的努力值奖励
}
func (basic *PetInfo) GetBasic() uint32 {
@@ -61,3 +67,16 @@ type Monsters struct {
XMLName xml.Name `xml:"Monsters"`
Monsters []PetInfo `xml:"Monster"`
}
func parseYieldingEV(raw string) []int64 {
values := make([]int64, 6)
parts := strings.Fields(raw)
for i := 0; i < len(parts) && i < len(values); i++ {
value, err := strconv.ParseInt(parts[i], 10, 64)
if err != nil {
continue
}
values[i] = value
}
return values
}

View File

@@ -1,6 +1,7 @@
package xmlres
import (
"encoding/json"
"encoding/xml"
"fmt"
"io"
@@ -33,52 +34,156 @@ type MovesTbl struct {
Moves []Move `xml:"Moves>Move"`
EFF []SideEffect `xml:"SideEffects>SideEffect"`
}
type MovesJSON struct {
MovesTbl MovesJSONRoot `json:"MovesTbl"`
}
type MovesJSONRoot struct {
Moves struct {
Move []Move `json:"Move"`
} `json:"Moves"`
SideEffects struct {
SideEffect []SideEffect `json:"SideEffect"`
} `json:"SideEffects"`
}
type MovesMap struct {
XMLName xml.Name `xml:"MovesTbl"`
Moves map[int]Move
EFF []SideEffect `xml:"SideEffects>SideEffect"`
}
type rawFlexibleString string
func (s *rawFlexibleString) UnmarshalJSON(data []byte) error {
text := strings.TrimSpace(string(data))
if text == "" || text == "null" {
*s = ""
return nil
}
if len(text) >= 2 && text[0] == '"' && text[len(text)-1] == '"' {
var decoded string
if err := json.Unmarshal(data, &decoded); err != nil {
return err
}
*s = rawFlexibleString(decoded)
return nil
}
*s = rawFlexibleString(text)
return nil
}
// Move 定义单个技能的结构
type Move struct {
ID int `xml:"ID,attr"`
Name string `xml:"Name,attr"`
ID int `xml:"ID,attr" json:"ID"`
Name string `xml:"Name,attr" json:"Name"`
Category int `xml:"Category,attr"` //属性
Type int `xml:"Type,attr"` //类型
Power int `xml:"Power,attr"` //威力
MaxPP int `xml:"MaxPP,attr"` //最大PP
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"` //威力power取决于自身的潜力个体值
PwrDouble int `xml:"PwrDouble,attr,omitempty"` //攻击时,若对方处于异常状态, 则威力翻倍;
DmgBindHpDv int `xml:"DmgBindHpDv,attr,omitempty"` //使对方受到的伤害值等于自身的体力值
SideEffect string `xml:"SideEffect,attr,omitempty"`
SideEffectArg string `xml:"SideEffectArg,attr,omitempty"`
Category int `xml:"Category,attr" json:"Category"` //属性
Type int `xml:"Type,attr" json:"Type"` //类型
Power int `xml:"Power,attr" json:"Power"` //威力
MaxPP int `xml:"MaxPP,attr" json:"MaxPP"` //最大PP
Accuracy int `xml:"Accuracy,attr" json:"Accuracy"` //命中率
CritRate int `xml:"CritRate,attr,omitempty" json:"CritRate,omitempty"` //暴击率
Priority int `xml:"Priority,attr,omitempty" json:"Priority,omitempty"` //优先级
MustHit int `xml:"MustHit,attr,omitempty" json:"MustHit,omitempty"` //是否必中
SwapElemType int `xml:"SwapElemType,attr,omitempty" json:"SwapElemType,omitempty"` //技能交换属性
CopyElemType int `xml:"CopyElemType,attr,omitempty" json:"CopyElemType,omitempty"` // 技能复制属性
CritAtkFirst int `xml:"CritAtkFirst,attr,omitempty" json:"CritAtkFirst,omitempty"` // 先出手时必定致命一击
CritAtkSecond int `xml:"CritAtkSecond,attr,omitempty" json:"CritAtkSecond,omitempty"` //后出手时必定致命一击
CritSelfHalfHp int `xml:"CritSelfHalfHp,attr,omitempty" json:"CritSelfHalfHp,omitempty"` //自身体力低于一半时必定致命一击
CritFoeHalfHp int `xml:"CritFoeHalfHp,attr,omitempty" json:"CritFoeHalfHp,omitempty"` //对方体力低于一半时必定致命一击
DmgBindLv int `xml:"DmgBindLv,attr,omitempty" json:"DmgBindLv,omitempty"` //使对方受到的伤害值等于自身的等级
PwrBindDv int `xml:"PwrBindDv,attr,omitempty" json:"PwrBindDv,omitempty"` //威力power取决于自身的潜力个体值
PwrDouble int `xml:"PwrDouble,attr,omitempty" json:"PwrDouble,omitempty"` //攻击时,若对方处于异常状态, 则威力翻倍;
DmgBindHpDv int `xml:"DmgBindHpDv,attr,omitempty" json:"DmgBindHpDv,omitempty"` //使对方受到的伤害值等于自身的体力值
SideEffect string `xml:"SideEffect,attr,omitempty" json:"SideEffect,omitempty"`
SideEffectArg string `xml:"SideEffectArg,attr,omitempty" json:"SideEffectArg,omitempty"`
SideEffectS []int
SideEffectArgS []int
AtkNum int `xml:"AtkNum,attr,omitempty"`
AtkType int `xml:"AtkType,attr,omitempty"` // 0:所有人 1:仅己方 2:仅对方 3:仅自己
Url string `xml:"Url,attr,omitempty"`
AtkNum int `xml:"AtkNum,attr,omitempty" json:"AtkNum,omitempty"`
AtkType int `xml:"AtkType,attr,omitempty" json:"AtkType,omitempty"` // 0:所有人 1:仅己方 2:仅对方 3:仅自己
Url string `xml:"Url,attr,omitempty" json:"Url,omitempty"`
Info string `xml:"info,attr,omitempty"`
Info string `xml:"info,attr,omitempty" json:"info,omitempty"`
CD *int `xml:"CD,attr"`
CD *int `xml:"CD,attr" json:"CD"`
}
func (m *Move) UnmarshalJSON(data []byte) error {
type moveAlias struct {
ID int `json:"ID"`
Name rawFlexibleString `json:"Name"`
Category int `json:"Category"`
Type int `json:"Type"`
Power int `json:"Power"`
MaxPP int `json:"MaxPP"`
Accuracy int `json:"Accuracy"`
CritRate int `json:"CritRate,omitempty"`
Priority int `json:"Priority,omitempty"`
MustHit int `json:"MustHit,omitempty"`
SwapElemType int `json:"SwapElemType,omitempty"`
CopyElemType int `json:"CopyElemType,omitempty"`
CritAtkFirst int `json:"CritAtkFirst,omitempty"`
CritAtkSecond int `json:"CritAtkSecond,omitempty"`
CritSelfHalfHp int `json:"CritSelfHalfHp,omitempty"`
CritFoeHalfHp int `json:"CritFoeHalfHp,omitempty"`
DmgBindLv int `json:"DmgBindLv,omitempty"`
PwrBindDv int `json:"PwrBindDv,omitempty"`
PwrDouble int `json:"PwrDouble,omitempty"`
DmgBindHpDv int `json:"DmgBindHpDv,omitempty"`
SideEffect rawFlexibleString `json:"SideEffect,omitempty"`
SideEffectArg rawFlexibleString `json:"SideEffectArg,omitempty"`
AtkNum int `json:"AtkNum,omitempty"`
AtkType int `json:"AtkType,omitempty"`
Url string `json:"Url,omitempty"`
Info string `json:"info,omitempty"`
CD *int `json:"CD"`
}
var aux moveAlias
if err := json.Unmarshal(data, &aux); err != nil {
return err
}
*m = Move{
ID: aux.ID,
Name: string(aux.Name),
Category: aux.Category,
Type: aux.Type,
Power: aux.Power,
MaxPP: aux.MaxPP,
Accuracy: aux.Accuracy,
CritRate: aux.CritRate,
Priority: aux.Priority,
MustHit: aux.MustHit,
SwapElemType: aux.SwapElemType,
CopyElemType: aux.CopyElemType,
CritAtkFirst: aux.CritAtkFirst,
CritAtkSecond: aux.CritAtkSecond,
CritSelfHalfHp: aux.CritSelfHalfHp,
CritFoeHalfHp: aux.CritFoeHalfHp,
DmgBindLv: aux.DmgBindLv,
PwrBindDv: aux.PwrBindDv,
PwrDouble: aux.PwrDouble,
DmgBindHpDv: aux.DmgBindHpDv,
SideEffect: string(aux.SideEffect),
SideEffectArg: string(aux.SideEffectArg),
AtkNum: aux.AtkNum,
AtkType: aux.AtkType,
Url: aux.Url,
Info: aux.Info,
CD: aux.CD,
}
return nil
}
type SideEffect struct {
ID int `xml:"ID,attr"`
Help string `xml:"help,attr"`
Des string `xml:"des,attr"`
ID int `xml:"ID,attr" json:"ID"`
Help string `xml:"help,attr" json:"help"`
Des string `xml:"des,attr" json:"des"`
}
// ReadHTTPFile 通过HTTP GET请求获取远程文件内容

View File

@@ -4,7 +4,7 @@ import (
"blazing/cool"
"blazing/logic/service/fight/pvp"
"blazing/logic/service/fight/pvpwire"
"context"
"fmt"
"time"
@@ -15,7 +15,8 @@ import (
)
// ListenFunc 监听函数
// ListenFunc 改造后的 Redis PubSub 监听函数,支持自动重连和心跳保活
// ListenFunc 改造后的 Redis PubSub 监听函数,支持自动重连
// 注意PubSub 连接只负责订阅和接收,避免在同一连接上并发 PING。
func ListenFunc(ctx g.Ctx) {
if !cool.IsRedisMode {
panic(gerror.New("集群模式下, 请使用Redis作为缓存"))
@@ -23,9 +24,8 @@ func ListenFunc(ctx g.Ctx) {
// 定义常量配置
const (
subscribeTopic = "cool:func" // 订阅的主题
retryDelay = 10 * time.Second // 连接失败重试间隔
heartbeatInterval = 30 * time.Second // 心跳保活间隔
subscribeTopic = "cool:func" // 订阅的主题
retryDelay = 10 * time.Second // 连接失败重试间隔
)
// 外层循环:负责连接断开后的整体重连
@@ -46,47 +46,25 @@ func ListenFunc(ctx g.Ctx) {
continue
}
// 2. 启动心跳保活协程,防止连接因空闲被断开
heartbeatCtx, heartbeatCancel := context.WithCancel(context.Background())
go func() {
ticker := time.NewTicker(heartbeatInterval)
defer func() {
ticker.Stop()
heartbeatCancel()
}()
for {
select {
case <-heartbeatCtx.Done():
cool.Logger.Info(ctx, "心跳协程退出")
return
case <-ticker.C:
// 发送 PING 心跳,保持连接活跃
_, pingErr := conn.Do(ctx, "PING")
if pingErr != nil {
cool.Logger.Error(ctx, "Redis 心跳失败,触发重连", "error", pingErr)
// 心跳失败时主动关闭连接,触发外层重连
_ = conn.Close(ctx)
return
}
}
}
}()
// 3. 订阅主题
// 2. 订阅主题
_, err = conn.Do(ctx, "subscribe", subscribeTopic)
if err != nil {
cool.Logger.Error(ctx, "订阅 Redis 主题失败", "topic", subscribeTopic, "error", err)
heartbeatCancel() // 关闭心跳协程
_ = conn.Close(ctx)
time.Sleep(retryDelay)
continue
}
cool.Logger.Info(ctx, "成功订阅 Redis 主题", "topic", subscribeTopic)
_, err = conn.Do(ctx, "subscribe", "sun:join") //加入队列
if err != nil {
cool.Logger.Error(ctx, "订阅 Redis 主题失败", "topic", "sun:join", "error", err)
_ = conn.Close(ctx)
time.Sleep(retryDelay)
continue
}
cool.Logger.Info(ctx, "成功订阅 Redis 主题", "topic", "sun:join")
// 4. 循环接收消息
// 3. 循环接收消息
connError := false
for !connError {
select {
@@ -129,15 +107,15 @@ func ListenFunc(ctx g.Ctx) {
}
}
// 5. 清理资源,准备重连
heartbeatCancel() // 关闭心跳协程
// 4. 清理资源,准备重连
_ = conn.Close(ctx) // 关闭当前连接
// Logger.Warn(ctx, "Redis 连接异常,准备重连", "retry_after", retryDelay)
cool.Logger.Info(ctx, "Redis 订阅连接异常,准备重连", "retry_after", retryDelay)
time.Sleep(retryDelay)
}
}
// ListenFight 完全对齐 ListenFunc 写法,修复收不到消息问题
// ListenFight 完全对齐 ListenFunc 写法,修复收不到消息问题
// 注意PubSub 连接只负责订阅和接收,避免在同一连接上并发 PING。
func ListenFight(ctx g.Ctx) {
if !cool.IsRedisMode {
panic(gerror.New("集群模式下, 请使用Redis作为缓存"))
@@ -145,8 +123,7 @@ func ListenFight(ctx g.Ctx) {
// 定义常量配置(对齐 ListenFunc 风格)
const (
retryDelay = 10 * time.Second // 连接失败重试间隔
heartbeatInterval = 30 * time.Second // 心跳保活间隔
retryDelay = 10 * time.Second // 连接失败重试间隔
)
// 提前拼接订阅主题(避免重复拼接,便于日志打印)
@@ -175,35 +152,7 @@ func ListenFight(ctx g.Ctx) {
continue
}
// 2. 启动心跳保活协程(完全对齐 ListenFunc 逻辑
heartbeatCtx, heartbeatCancel := context.WithCancel(context.Background())
go func() {
ticker := time.NewTicker(heartbeatInterval)
defer func() {
ticker.Stop()
heartbeatCancel()
}()
for {
select {
case <-heartbeatCtx.Done():
cool.Logger.Info(ctx, "心跳协程退出")
return
case <-ticker.C:
// 发送 PING 心跳,保持连接活跃
_, pingErr := conn.Do(ctx, "PING")
if pingErr != nil {
cool.Logger.Error(ctx, "Redis 心跳失败,触发重连", "error", pingErr)
// 心跳失败时主动关闭连接,触发外层重连
_ = conn.Close(ctx)
return
}
cool.Logger.Debug(ctx, "Redis 心跳发送成功,连接正常")
}
}
}()
// 3. 订阅主题(对齐 ListenFunc 的错误处理,替换 panic 为优雅重连)
// 2. 订阅主题(对齐 ListenFunc 的错误处理,替换 panic 为优雅重连
subscribeTopics := []string{startTopic, pvpServerTopic}
if cool.Config.GameOnlineID == pvp.CoordinatorOnlineID {
subscribeTopics = append(subscribeTopics, pvpCoordinatorTopic)
@@ -213,7 +162,6 @@ func ListenFight(ctx g.Ctx) {
_, err = conn.Do(ctx, "subscribe", topic)
if err != nil {
cool.Logger.Error(ctx, "订阅 Redis 主题失败", "topic", topic, "error", err)
heartbeatCancel()
_ = conn.Close(ctx)
time.Sleep(retryDelay)
subscribeFailed = true
@@ -239,7 +187,7 @@ func ListenFight(ctx g.Ctx) {
// 打印监听提示(保留原有日志)
fmt.Println("监听战斗", startTopic)
// 4. 循环接收消息(完全对齐 ListenFunc 逻辑)
// 3. 循环接收消息(完全对齐 ListenFunc 逻辑)
connError := false
for !connError {
select {
@@ -281,9 +229,9 @@ func ListenFight(ctx g.Ctx) {
}
}
// 5. 清理资源,准备重连(完全对齐 ListenFunc
heartbeatCancel() // 关闭心跳协程
// 4. 清理资源,准备重连(完全对齐 ListenFunc
_ = conn.Close(ctx) // 关闭当前连接
cool.Logger.Info(ctx, "Redis 战斗订阅连接异常,准备重连", "retry_after", retryDelay)
time.Sleep(retryDelay)
}
}

163
common/rpc/pvp_match.go Normal file
View File

@@ -0,0 +1,163 @@
package rpc
import (
"blazing/cool"
"blazing/logic/service/fight/pvpwire"
"context"
"encoding/json"
"fmt"
"sync"
"time"
)
const (
pvpMatchQueueTTL = 12 * time.Second
pvpMatchBanPickSecond = 45
)
type PVPMatchJoinPayload struct {
RuntimeServerID uint32 `json:"runtimeServerId"`
UserID uint32 `json:"userId"`
Nick string `json:"nick"`
FightMode uint32 `json:"fightMode"`
Status uint32 `json:"status"`
CatchTimes []uint32 `json:"catchTimes"`
}
type pvpMatchCoordinator struct {
mu sync.Mutex
queues map[uint32][]pvpwire.QueuePlayerSnapshot
lastSeen map[uint32]time.Time
}
var defaultPVPMatchCoordinator = &pvpMatchCoordinator{
queues: make(map[uint32][]pvpwire.QueuePlayerSnapshot),
lastSeen: make(map[uint32]time.Time),
}
func DefaultPVPMatchCoordinator() *pvpMatchCoordinator {
return defaultPVPMatchCoordinator
}
func (m *pvpMatchCoordinator) JoinOrUpdate(payload PVPMatchJoinPayload) error {
if payload.UserID == 0 || payload.RuntimeServerID == 0 || payload.FightMode == 0 {
return fmt.Errorf("invalid pvp match payload: uid=%d server=%d mode=%d", payload.UserID, payload.RuntimeServerID, payload.FightMode)
}
now := time.Now()
player := pvpwire.QueuePlayerSnapshot{
RuntimeServerID: payload.RuntimeServerID,
UserID: payload.UserID,
Nick: payload.Nick,
FightMode: payload.FightMode,
Status: payload.Status,
JoinedAtUnix: now.Unix(),
CatchTimes: append([]uint32(nil), payload.CatchTimes...),
}
var match *pvpwire.MatchFoundPayload
m.mu.Lock()
m.pruneExpiredLocked(now)
m.removeUserLocked(payload.UserID)
m.lastSeen[payload.UserID] = now
queue := m.queues[payload.FightMode]
if len(queue) > 0 {
host := queue[0]
queue = queue[1:]
m.queues[payload.FightMode] = queue
delete(m.lastSeen, host.UserID)
delete(m.lastSeen, payload.UserID)
result := pvpwire.MatchFoundPayload{
SessionID: buildPVPMatchSessionID(host.UserID, payload.UserID),
Stage: pvpwire.StageBanPick,
Host: host,
Guest: player,
BanPickTimeout: pvpMatchBanPickSecond,
}
match = &result
} else {
m.queues[payload.FightMode] = append(queue, player)
}
m.mu.Unlock()
if match == nil {
return nil
}
if err := publishPVPMatchMessage(pvpwire.ServerTopic(match.Host.RuntimeServerID), pvpwire.MessageTypeMatchFound, *match); err != nil {
return err
}
if match.Guest.RuntimeServerID != match.Host.RuntimeServerID {
if err := publishPVPMatchMessage(pvpwire.ServerTopic(match.Guest.RuntimeServerID), pvpwire.MessageTypeMatchFound, *match); err != nil {
return err
}
}
return nil
}
func (m *pvpMatchCoordinator) Cancel(userID uint32) {
if userID == 0 {
return
}
m.mu.Lock()
defer m.mu.Unlock()
delete(m.lastSeen, userID)
m.removeUserLocked(userID)
}
func (m *pvpMatchCoordinator) pruneExpiredLocked(now time.Time) {
for mode, queue := range m.queues {
next := make([]pvpwire.QueuePlayerSnapshot, 0, len(queue))
for _, queued := range queue {
last := m.lastSeen[queued.UserID]
if last.IsZero() || now.Sub(last) > pvpMatchQueueTTL {
delete(m.lastSeen, queued.UserID)
continue
}
next = append(next, queued)
}
m.queues[mode] = next
}
}
func (m *pvpMatchCoordinator) removeUserLocked(userID uint32) {
for mode, queue := range m.queues {
next := make([]pvpwire.QueuePlayerSnapshot, 0, len(queue))
for _, queued := range queue {
if queued.UserID == userID {
continue
}
next = append(next, queued)
}
m.queues[mode] = next
}
}
func publishPVPMatchMessage(topic, msgType string, body any) error {
payload, err := json.Marshal(body)
if err != nil {
return err
}
envelope, err := json.Marshal(pvpwire.Envelope{
Type: msgType,
Body: payload,
})
if err != nil {
return err
}
conn, err := cool.Redis.Conn(context.Background())
if err != nil {
return err
}
defer conn.Close(context.Background())
_, err = conn.Do(context.Background(), "publish", topic, envelope)
return err
}
func buildPVPMatchSessionID(hostUserID, guestUserID uint32) string {
return fmt.Sprintf("xsvr-%d-%d-%d", hostUserID, guestUserID, time.Now().UnixNano())
}

View File

@@ -1,106 +1,166 @@
package rpc
import (
"blazing/common/data/share"
"blazing/cool"
"context"
"fmt"
"log"
config "blazing/modules/config/service"
"github.com/filecoin-project/go-jsonrpc"
"github.com/gogf/gf/v2/util/gconv"
)
// Define the server handler
type ServerHandler struct{}
// 实现踢人
func (*ServerHandler) Kick(_ context.Context, userid uint32) error {
useid1, _ := share.ShareManager.GetUserOnline(userid)
if useid1 == 0 {
return nil
}
cl, ok := cool.GetClientOnly(useid1)
if !ok {
return nil
}
cl.KickPerson(userid) //实现指定服务器踢人
return nil
}
// 注册logic服务器
func (*ServerHandler) RegisterLogic(ctx context.Context, id, port uint32) error {
fmt.Println("注册logic服务器", id, port)
//TODO 待修复滚动更新可能导致的玩家可以同时在旧服务器和新服务器同时在线的bug
revClient, ok := jsonrpc.ExtractReverseClient[cool.ClientHandler](ctx)
if !ok {
return fmt.Errorf("no reverse client")
}
t := config.NewServerService().GetServerID((id))
aa, ok := cool.GetClient(t.OnlineID, t.Port)
if ok && aa != nil { //如果已经存在且这个端口已经被存过
aa.QuitSelf(0)
}
cool.AddClient(100000*id+port, &revClient)
//Refurh()
return nil
}
func CServer() *jsonrpc.RPCServer {
// create a new server instance
rpcServer := jsonrpc.NewServer(jsonrpc.WithReverseClient[cool.ClientHandler](""))
rpcServer.Register("", &ServerHandler{})
return rpcServer
}
var closer jsonrpc.ClientCloser
func StartClient(id, port uint32, callback any) *struct {
Kick func(uint32) error
RegisterLogic func(uint32, uint32) error
} {
//cool.Config.File.Domain = "127.0.0.1"
var rpcaddr = "ws://" + cool.Config.File.Domain + gconv.String(cool.Config.Address) + "/rpc"
closer1, err := jsonrpc.NewMergeClient(context.Background(),
rpcaddr, "", []interface{}{
&RPCClient,
}, nil, jsonrpc.WithClientHandler("", callback),
jsonrpc.WithReconnFun(func() { RPCClient.RegisterLogic(id, port) }),
)
if err != nil {
log.Fatalf("Failed to create client: %v", err)
}
//if port != 0 { //注册logic
defer RPCClient.RegisterLogic(id, port)
//}
closer = closer1
return &RPCClient
}
// Setup RPCClient with reverse call handler
var RPCClient struct {
Kick func(uint32) error //踢人
RegisterLogic func(uint32, uint32) error
// UserLogin func(int32, int32) error //用户登录事件
// UserLogout func(int32, int32) error //用户登出事件
}
package rpc
import (
"blazing/common/data/share"
"blazing/cool"
"context"
"fmt"
"log"
"time"
config "blazing/modules/config/service"
"github.com/filecoin-project/go-jsonrpc"
"github.com/gogf/gf/v2/util/gconv"
)
// Define the server handler
type ServerHandler struct{}
const kickForwardTimeout = 3 * time.Second
// 实现踢人
func (*ServerHandler) Kick(_ context.Context, userid uint32) error {
useid1, err := share.ShareManager.GetUserOnline(userid)
if err != nil || useid1 == 0 {
// 请求到达时用户已离线,直接视为成功
return nil
}
cl, ok := cool.GetClientOnly(useid1)
if !ok || cl == nil {
// 目标服务器不在线,清理僵尸在线标记并视为成功
_ = share.ShareManager.DeleteUserOnline(userid)
cool.DeleteClientOnly(useid1)
return nil
}
resultCh := make(chan error, 1)
go func() {
resultCh <- cl.KickPerson(userid) // 实现指定服务器踢人
}()
select {
case callErr := <-resultCh:
if callErr == nil {
return nil
}
// 调用失败后兜底:用户若已离线/切服/目标服不在线都算成功
useid2, err2 := share.ShareManager.GetUserOnline(userid)
if err2 != nil || useid2 == 0 || useid2 != useid1 {
return nil
}
if cl2, ok2 := cool.GetClientOnly(useid2); !ok2 || cl2 == nil {
_ = share.ShareManager.DeleteUserOnline(userid)
cool.DeleteClientOnly(useid2)
return nil
}
// 仍在线则返回失败,不按成功处理
return callErr
case <-time.After(kickForwardTimeout):
// 仅防止无限等待;超时不算成功
useid2, err2 := share.ShareManager.GetUserOnline(userid)
if err2 != nil || useid2 == 0 || useid2 != useid1 {
return nil
}
if cl2, ok2 := cool.GetClientOnly(useid2); !ok2 || cl2 == nil {
_ = share.ShareManager.DeleteUserOnline(userid)
cool.DeleteClientOnly(useid2)
return nil
}
return fmt.Errorf("kick timeout, user still online: uid=%d server=%d", userid, useid2)
}
}
// 注册logic服务器
func (*ServerHandler) RegisterLogic(ctx context.Context, id, port uint32) error {
fmt.Println("注册logic服务器", id, port)
//TODO 待修复滚动更新可能导致的玩家可以同时在旧服务器和新服务器同时在线的bug
revClient, ok := jsonrpc.ExtractReverseClient[cool.ClientHandler](ctx)
if !ok {
return fmt.Errorf("no reverse client")
}
t := config.NewServerService().GetServerID((id))
aa, ok := cool.GetClient(t.OnlineID, t.Port)
if ok && aa != nil { //如果已经存在且这个端口已经被存过
aa.QuitSelf(0)
}
cool.AddClient(100000*id+port, &revClient)
//Refurh()
return nil
}
func (*ServerHandler) MatchJoinOrUpdate(_ context.Context, payload PVPMatchJoinPayload) error {
return DefaultPVPMatchCoordinator().JoinOrUpdate(payload)
}
func (*ServerHandler) MatchCancel(_ context.Context, userID uint32) error {
DefaultPVPMatchCoordinator().Cancel(userID)
return nil
}
func CServer() *jsonrpc.RPCServer {
// create a new server instance
rpcServer := jsonrpc.NewServer(jsonrpc.WithReverseClient[cool.ClientHandler](""))
rpcServer.Register("", &ServerHandler{})
return rpcServer
}
var closer jsonrpc.ClientCloser
func StartClient(id, port uint32, callback any) *struct {
Kick func(uint32) error
RegisterLogic func(uint32, uint32) error
MatchJoinOrUpdate func(PVPMatchJoinPayload) error
MatchCancel func(uint32) error
} {
//cool.Config.File.Domain = "127.0.0.1"
var rpcaddr = "ws://" + cool.Config.File.Domain + gconv.String(cool.Config.Address) + "/rpc"
closer1, err := jsonrpc.NewMergeClient(context.Background(),
rpcaddr, "", []interface{}{
&RPCClient,
}, nil, jsonrpc.WithClientHandler("", callback),
jsonrpc.WithReconnFun(func() { RPCClient.RegisterLogic(id, port) }),
)
if err != nil {
log.Fatalf("Failed to create client: %v", err)
}
//if port != 0 { //注册logic
defer RPCClient.RegisterLogic(id, port)
//}
closer = closer1
return &RPCClient
}
// Setup RPCClient with reverse call handler
var RPCClient struct {
Kick func(uint32) error //踢人
RegisterLogic func(uint32, uint32) error
MatchJoinOrUpdate func(PVPMatchJoinPayload) error
MatchCancel func(uint32) error
// UserLogin func(int32, int32) error //用户登录事件
// UserLogout func(int32, int32) error //用户登出事件
}

View File

@@ -1,24 +1,29 @@
package socket
import (
"blazing/common/socket/codec"
"blazing/cool"
"blazing/logic/service/player"
"blazing/modules/config/service"
"bytes"
"context"
"encoding/binary"
"errors"
"io"
"log"
"os"
"sync/atomic"
"time"
"blazing/cool"
"blazing/logic/service/player"
"blazing/modules/config/service"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gtime"
"github.com/panjf2000/gnet/v2"
)
const (
minPacketLen = 17
maxPacketLen = 10 * 1024
)
func (s *Server) Boot(serverid, port uint32) error {
// go s.bootws()
s.serverid = serverid
@@ -53,62 +58,43 @@ func (s *Server) Stop() error {
func (s *Server) OnClose(c gnet.Conn, err error) (action gnet.Action) {
defer func() {
if err := recover(); err != nil { // 恢复 panicerr 为 panic 错误值
// 1. 打印错误信息
if t, ok := c.Context().(*player.ClientData); ok {
if t.Player != nil {
if t.Player.Info != nil {
cool.Logger.Error(context.TODO(), "OnClose 错误:", cool.Config.ServerInfo.OnlineID, t.Player.Info.UserID, err)
t.Player.Service.Info.Save(*t.Player.Info)
go t.Player.SaveOnDisconnect()
}
}
} else {
cool.Logger.Error(context.TODO(), "OnClose 错误:", cool.Config.ServerInfo.OnlineID, err)
}
}
}()
// 识别 RST 导致的连接中断(错误信息含 "connection reset"
// if err != nil && (strings.Contains(err.Error(), "connection reset") || strings.Contains(err.Error(), "reset by peer")) {
// remoteIP := c.RemoteAddr().(*net.TCPAddr).IP.String()
// log.Printf("RST 攻击检测: 来源 %s, 累计攻击次数 %d", remoteIP)
// // 防护逻辑:临时封禁异常 IP可扩展为 IP 黑名单)
// // go s.tempBlockIP(remoteIP, 5*time.Minute)
// }
//fmt.Println(err, c.RemoteAddr().String(), "断开连接")
atomic.AddInt64(&cool.Connected, -1)
//logging.Infof("conn[%v] disconnected", c.RemoteAddr().String())
v, _ := c.Context().(*player.ClientData)
v.LF.Close()
// v.LF.Close()
//close(v.MsgChan)
if v.Player != nil {
v.Player.Save() //保存玩家数据
if v != nil {
v.Close()
if v.Player != nil {
v.Player.Save() //保存玩家数据
}
}
//}
//关闭连接
return
}
func (s *Server) OnTick() (delay time.Duration, action gnet.Action) {
g.Log().Async().Info(context.Background(), gtime.Now().ISO8601(), "服务器ID", cool.Config.ServerInfo.OnlineID, "链接数", atomic.LoadInt64(&cool.Connected))
if s.quit && atomic.LoadInt64(&cool.Connected) == 0 {
//执行正常退出逻辑
os.Exit(0)
}
return 30 * time.Second, gnet.None
}
func (s *Server) OnBoot(eng gnet.Engine) gnet.Action {
s.eng = eng
service.NewServerService().SetServerID(s.serverid, s.port) //设置当前服务器端口
service.NewServerService().SetServerID(s.serverid, s.port)
return gnet.None
}
@@ -116,59 +102,68 @@ func (s *Server) OnOpen(conn gnet.Conn) (out []byte, action gnet.Action) {
if s.network != "tcp" {
return nil, gnet.Close
}
if conn.Context() == nil {
conn.SetContext(player.NewClientData(conn)) //注入data
conn.SetContext(player.NewClientData(conn))
}
atomic.AddInt64(&cool.Connected, 1)
return nil, gnet.None
}
func (s *Server) OnTraffic(c gnet.Conn) (action gnet.Action) {
defer func() {
if err := recover(); err != nil { // 恢复 panicerr 为 panic 错误值
// 1. 打印错误信息
if err := recover(); err != nil {
if t, ok := c.Context().(*player.ClientData); ok {
if t.Player != nil {
if t.Player.Info != nil {
cool.Logger.Error(context.TODO(), "OnTraffic 错误:", cool.Config.ServerInfo.OnlineID, t.Player.Info.UserID, err)
t.Player.Service.Info.Save(*t.Player.Info)
}
if t.Player != nil && t.Player.Info != nil {
cool.Logger.Error(context.TODO(), "OnTraffic 错误:", cool.Config.ServerInfo.OnlineID, t.Player.Info.UserID, err)
t.Player.Service.Info.Save(*t.Player.Info)
}
}
}
}()
ws := c.Context().(*player.ClientData).Wsmsg
if ws.Tcp { //升级失败时候防止缓冲区溢出
return s.handleTCP(c)
client := c.Context().(*player.ClientData)
if s.discorse && !client.IsCrossDomainChecked() {
handled, ready, action := handle(c)
if action != gnet.None {
return action
}
if handled {
client.MarkCrossDomainChecked()
return gnet.None
}
if !ready {
return gnet.None
}
client.MarkCrossDomainChecked()
}
tt, len1 := ws.ReadBufferBytes(c)
if tt == gnet.Close {
ws := client.Wsmsg
if ws.Tcp {
return s.handleTCP(c)
}
readAction, inboundLen := ws.ReadBufferBytes(c)
if readAction == gnet.Close {
return gnet.Close
}
ok, action := ws.Upgrade(c)
if action != gnet.None { //连接断开
state, action := ws.Upgrade(c)
if action != gnet.None {
return action
}
if !ok { //升级失败,说明是tcp连接
ws.Tcp = true
return s.handleTCP(c)
if state == player.UpgradeNeedMoreData {
return gnet.None
}
if state == player.UpgradeUseTCP {
return s.handleTCP(c)
}
if inboundLen > 0 {
if _, err := c.Discard(inboundLen); err != nil {
return gnet.Close
}
ws.ResetInboundMirror()
}
// fmt.Println(ws.Buf.Bytes())
c.Discard(len1)
messages, err := ws.Decode(c)
if err != nil {
@@ -179,91 +174,93 @@ func (s *Server) OnTraffic(c gnet.Conn) (action gnet.Action) {
}
for _, msg := range messages {
s.onevent(c, msg.Payload)
//t.OnEvent(msg.Payload)
if !s.onevent(c, msg.Payload) {
return gnet.Close
}
}
return gnet.None
}
const maxBodyLen = 10 * 1024 // 业务最大包体长度,按需调整
func (s *Server) handleTCP(conn gnet.Conn) (action gnet.Action) {
client := conn.Context().(*player.ClientData)
if s.discorse && !client.IsCrossDomainChecked() {
handled, ready, action := handle(conn)
if action != gnet.None {
return action
}
if !ready {
return gnet.None
}
if handled {
client.MarkCrossDomainChecked()
return gnet.None
}
client.MarkCrossDomainChecked()
}
conn.Context().(*player.ClientData).IsCrossDomain.Do(func() { //跨域检测
handle(conn)
})
// handle(c)
// 先读取4字节的包长度
lenBuf, err := conn.Peek(4)
body, err := s.codec.Decode(conn)
if err != nil {
if errors.Is(err, io.ErrShortBuffer) {
return
if errors.Is(err, codec.ErrIncompletePacket) {
return gnet.None
}
return gnet.Close
}
bodyLen := binary.BigEndian.Uint32(lenBuf)
if bodyLen > maxBodyLen {
if !s.onevent(conn, body) {
return gnet.Close
}
if conn.InboundBuffered() < int(bodyLen) {
return
}
// 提取包体
body, err := conn.Next(int(bodyLen))
if err != nil {
if errors.Is(err, io.ErrShortBuffer) {
return
}
return gnet.Close
}
s.onevent(conn, body)
if conn.InboundBuffered() > 0 {
if err := conn.Wake(nil); err != nil { // wake up the connection manually to avoid missing the leftover data
if err := conn.Wake(nil); err != nil {
return gnet.Close
}
}
return action
}
// CROSS_DOMAIN 定义跨域策略文件内容
const CROSS_DOMAIN = "<?xml version=\"1.0\"?><!DOCTYPE cross-domain-policy><cross-domain-policy><allow-access-from domain=\"*\" to-ports=\"*\" /></cross-domain-policy>\x00"
// TEXT 定义跨域请求的文本格式
const TEXT = "<policy-file-request/>\x00"
func handle(c gnet.Conn) {
func handle(c gnet.Conn) (handled bool, ready bool, action gnet.Action) {
probeLen := c.InboundBuffered()
if probeLen == 0 {
return false, false, gnet.None
}
if probeLen > len(TEXT) {
probeLen = len(TEXT)
}
// 读取数据并检查是否为跨域请求
data, err := c.Peek(len(TEXT))
data, err := c.Peek(probeLen)
if err != nil {
log.Printf("Error reading cross-domain request: %v", err)
return
return false, false, gnet.Close
}
if string(data) == TEXT { //判断是否是跨域请求
//log.Printf("Received cross-domain request from %s", c.RemoteAddr())
// 处理跨域请求
c.Write([]byte(CROSS_DOMAIN))
c.Discard(len(TEXT))
return
if !bytes.Equal(data, []byte(TEXT[:probeLen])) {
return false, true, gnet.None
}
//return
if probeLen < len(TEXT) {
return false, false, gnet.None
}
if _, err := c.Write([]byte(CROSS_DOMAIN)); err != nil {
return false, true, gnet.Close
}
if _, err := c.Discard(len(TEXT)); err != nil {
return false, true, gnet.Close
}
return true, true, gnet.None
}
func (s *Server) onevent(c gnet.Conn, v []byte) {
func (s *Server) onevent(c gnet.Conn, v []byte) bool {
if !isValidPacket(v) {
return false
}
if t, ok := c.Context().(*player.ClientData); ok {
t.PushEvent(v, s.workerPool.Submit)
}
return true
}
func isValidPacket(v []byte) bool {
if len(v) < minPacketLen || len(v) > maxPacketLen {
return false
}
return binary.BigEndian.Uint32(v[0:4]) == uint32(len(v))
}

View File

@@ -2,33 +2,4 @@ module github.com/zmexing/go-sensitive-word
go 1.20
require (
github.com/orcaman/concurrent-map/v2 v2.0.1
)
require (
github.com/andybalholm/brotli v1.1.0 // indirect
github.com/cloudflare/circl v1.3.7 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/klauspost/compress v1.17.9 // indirect
github.com/onsi/ginkgo/v2 v2.16.0 // indirect
github.com/quic-go/qpack v0.4.0 // indirect
github.com/quic-go/qtls-go1-20 v0.4.1 // indirect
github.com/quic-go/quic-go v0.40.1 // indirect
github.com/refraction-networking/utls v1.6.3 // indirect
github.com/stretchr/testify v1.11.1 // indirect
go.uber.org/mock v0.4.0 // indirect
golang.org/x/crypto v0.31.0 // indirect
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 // indirect
golang.org/x/mod v0.17.0 // indirect
golang.org/x/net v0.33.0 // indirect
golang.org/x/sys v0.30.0 // indirect
golang.org/x/text v0.22.0 // indirect
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
google.golang.org/protobuf v1.31.0 // indirect
)
require github.com/orcaman/concurrent-map/v2 v2.0.1

View File

@@ -1,55 +1,2 @@
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU=
github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7 h1:y3N7Bm7Y9/CtpiVkw/ZWj6lSlDF3F74SfKwfTCer72Q=
github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/imroc/req/v3 v3.42.3 h1:ryPG2AiwouutAopwPxKpWKyxgvO8fB3hts4JXlh3PaE=
github.com/imroc/req/v3 v3.42.3/go.mod h1:Axz9Y/a2b++w5/Jht3IhQsdBzrG1ftJd1OJhu21bB2Q=
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
github.com/onsi/ginkgo/v2 v2.16.0 h1:7q1w9frJDzninhXxjZd+Y/x54XNjG/UlRLIYPZafsPM=
github.com/onsi/ginkgo/v2 v2.16.0/go.mod h1:llBI3WDLL9Z6taip6f33H76YcWtJv+7R3HigUjbIBOs=
github.com/onsi/gomega v1.30.0 h1:hvMK7xYz4D3HapigLTeGdId/NcfQx1VHMJc60ew99+8=
github.com/orcaman/concurrent-map/v2 v2.0.1 h1:jOJ5Pg2w1oeB6PeDurIYf6k9PQ+aTITr/6lP/L/zp6c=
github.com/orcaman/concurrent-map/v2 v2.0.1/go.mod h1:9Eq3TG2oBe5FirmYWQfYO5iH1q0Jv47PLaNK++uCdOM=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo=
github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A=
github.com/quic-go/qtls-go1-20 v0.4.1 h1:D33340mCNDAIKBqXuAvexTNMUByrYmFYVfKfDN5nfFs=
github.com/quic-go/qtls-go1-20 v0.4.1/go.mod h1:X9Nh97ZL80Z+bX/gUXMbipO6OxdiDi58b/fMC9mAL+k=
github.com/quic-go/quic-go v0.40.1 h1:X3AGzUNFs0jVuO3esAGnTfvdgvL4fq655WaOi1snv1Q=
github.com/quic-go/quic-go v0.40.1/go.mod h1:PeN7kuVJ4xZbxSv/4OX6S1USOX8MJvydwpTx31vx60c=
github.com/refraction-networking/utls v1.6.3 h1:MFOfRN35sSx6K5AZNIoESsBuBxS2LCgRilRIdHb6fDc=
github.com/refraction-networking/utls v1.6.3/go.mod h1:yil9+7qSl+gBwJqztoQseO6Pr3h62pQoY1lXiNR/FPs=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU=
go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 h1:LfspQV/FYTatPTr/3HzIcmiUFH7PGP+OQ6mgDYo3yuQ=
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc=
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

View File

@@ -0,0 +1,183 @@
# Boss ScriptHookAction接入说明
日期2026-04-05
## 1. 执行流程
1. 先执行战斗效果链 `HookAction()`
2. 执行脚本 `hookAction(hookaction)`
3. 用脚本返回值决定是否继续出手
4. 脚本可直接调用 Go 绑定函数`useSkill()``switchPet()`
## 2. JS 可调用的 Go 函数
1. `useSkill(skillId: number)`
2. `switchPet(catchTime: number)`
## 3. `hookaction` 参数字段
基础字段
1. `hookaction.hookaction: boolean`
2. `hookaction.round: number`
3. `hookaction.is_first: boolean`
4. `hookaction.our: { pet_id, catch_time, hp, max_hp } | null`
5. `hookaction.opp: { pet_id, catch_time, hp, max_hp } | null`
6. `hookaction.skills: Array<{ skill_id, pp, can_use }>`
AttackValue 映射字段重点
1. `hookaction.our_attack`
2. `hookaction.opp_attack`
结构
```ts
{
skill_id: number;
attack_time: number;
is_critical: number;
lost_hp: number;
gain_hp: number;
remain_hp: number;
max_hp: number;
state: number;
offensive: number;
status: number[]; // 对应 AttackValue.Status[20]
prop: number[]; // 对应 AttackValue.Prop[6]
}
```
其中
- `prop` 索引`[攻, 防, 特攻, 特防, 速度, 命中]`
- 对应值 `> 0` 代表强化`< 0` 代表下降`0` 代表无变化
返回值
- `true`继续行动
- `false`阻止行动
- 不返回默认回退到 `hookaction.hookaction`
## 4. 脚本示例
### 4.1 判断对方是否存在强化你问的这个
```js
function hookAction(hookaction) {
if (!hookaction.hookaction) return false;
var oppAtk = hookaction.opp_attack;
var oppHasBuff = false;
if (oppAtk && oppAtk.prop) {
for (var i = 0; i < oppAtk.prop.length; i++) {
if (oppAtk.prop[i] > 0) {
oppHasBuff = true;
break;
}
}
}
if (oppHasBuff) {
// 对方有强化时,放一个针对技能
useSkill(5001);
return true;
}
return true;
}
```
### 4.2 判断对方是否有异常状态
```js
function hookAction(hookaction) {
if (!hookaction.hookaction) return false;
var oppAtk = hookaction.opp_attack;
var hasStatus = false;
if (oppAtk && oppAtk.status) {
for (var i = 0; i < oppAtk.status.length; i++) {
if (oppAtk.status[i] > 0) {
hasStatus = true;
break;
}
}
}
if (!hasStatus) {
// 没有异常时尝试上异常
useSkill(6002);
}
return true;
}
```
## 5. Monaco 类型提示
```ts
import * as monaco from "monaco-editor";
monaco.languages.typescript.javascriptDefaults.setCompilerOptions({
allowNonTsExtensions: true,
checkJs: true,
target: monaco.languages.typescript.ScriptTarget.ES2020,
});
monaco.languages.typescript.javascriptDefaults.addExtraLib(
`
interface BossHookPetContext {
pet_id: number;
catch_time: number;
hp: number;
max_hp: number;
}
interface BossHookSkillContext {
skill_id: number;
pp: number;
can_use: boolean;
}
interface BossHookAttackContext {
skill_id: number;
attack_time: number;
is_critical: number;
lost_hp: number;
gain_hp: number;
remain_hp: number;
max_hp: number;
state: number;
offensive: number;
status: number[];
prop: number[];
}
interface BossHookActionContext {
hookaction: boolean;
round: number;
is_first: boolean;
our: BossHookPetContext | null;
opp: BossHookPetContext | null;
skills: BossHookSkillContext[];
our_attack: BossHookAttackContext | null;
opp_attack: BossHookAttackContext | null;
}
declare function hookAction(hookaction: BossHookActionContext): boolean;
declare function HookAction(hookaction: BossHookActionContext): boolean;
declare function hookaction(hookaction: BossHookActionContext): boolean;
declare function useSkill(skillId: number): void;
declare function switchPet(catchTime: number): void;
`,
"ts:boss-script.d.ts"
);
```
## 6. 后端代码
- 脚本执行器与函数绑定`modules/config/model/boss_pet.go`
- AI 出手转发与上下文构建`logic/service/fight/input/ai.go`

View File

@@ -0,0 +1,224 @@
# PVP Match Via RPC, Battle Via Redis
## 目标
本次调整先不解决 `login` 更新期间的排队保活和补偿问题只收敛到一个更简单可控的方案
- 匹配请求走 `logic -> login` 的同步 RPC
- 对战过程仍走 `logic` 本地战斗 + Redis 转发战斗指令
- `login` 不可用时`logic` 直接返回匹配服务不可用
- 前端通过轮询重新发起 / 更新匹配请求不在后端保留离线补偿队列
这个方案的核心是先把能否立即判断匹配服务可用做好不继续依赖 Redis PubSub 做匹配入口
## 当前现状
### 现有匹配入口
- 前端 `2458` 进入 [logic/controller/fight_巅峰.go](/workspace/logic/controller/fight_巅峰.go#L19)
- 当前 `JoINtop` 直接调用 [logic/service/fight/pvp/service.go](/workspace/logic/service/fight/pvp/service.go#L83) `JoinPeakQueue`
- `JoinPeakQueue` 当前实现是本地建 `localQueueTicket`并通过 Redis `publish` `queue_join`
### 现有跨服协调
- `logic` 侧订阅 PVP Redis topic 的入口在 [common/rpc/func.go](/workspace/common/rpc/func.go#L153)
- PVP 匹配状态当前存在 `logic/service/fight/pvp/service.go` manager 内存里
- `queues`
- `lastSeen`
- `localQueues`
- `sessions`
- `userSession`
### 现有 RPC 能力
- `logic` 启动时通过 [common/rpc/rpc.go](/workspace/common/rpc/rpc.go#L113) 建立到 `login` RPC client
- `login` `/rpc/*` 入口绑定在 [modules/base/middleware/middleware.go](/workspace/modules/base/middleware/middleware.go#L152)
- `login` RPC server [common/rpc/rpc.go](/workspace/common/rpc/rpc.go#L101) 暴露
### 当前问题
Redis PubSub 适合广播消息不适合同步判断服务是否可用
如果继续让匹配入口走 PubSub
- `logic` 无法在请求当下知道 `login` 是否真能处理
- `login` 更新重启未订阅时匹配请求可能直接丢失
- 前端即使轮询也只是重复投递不能精确表达当前匹配服务可用/不可用
## 收敛后的职责划分
### login
`login` 只负责匹配控制面
- 接收 `logic` 发来的同步匹配 RPC
- 判断当前匹配服务是否可用
- 维护匹配队列
- 找到对手后记录 match 结果
- 再通过 Redis 或其他异步方式通知对应 `logic` 开始 Ban/Pick / Battle
### logic
`logic` 只负责
- 接收前端匹配请求
- 同步 RPC `login`
- RPC 失败时立即返回匹配服务不可用
- RPC 成功时返回排队中
- 收到 match 结果后负责真正 `fight.NewFight(...)`
- 对战期间继续使用现有 Redis topic 转发战斗指令
### Redis
Redis 只保留在对战消息面
- `match_found`
- `ban_pick_submit`
- `battle_command`
- `packet_relay`
- `session_close`
也就是说
- 匹配入口走 RPC
- 对战过程走 Redis
## 推荐目标链路
### 1. 前端加入/更新匹配
前端定期轮询 `logic` 的加入/更新接口
`logic` 处理流程
1. 校验玩家当前战斗状态
2. 同步调用 `login` 的匹配 RPC
3. 如果 RPC 成功返回排队中
4. 如果 RPC 失败清理本地匹配状态返回匹配服务不可用
### 2. login 完成匹配
`login` 维护排队队列和匹配结果匹配成功后
1. 确定 host / guest 所在 `logic`
2. 通过 Redis 通知两个 `logic`
3. host `logic` 开战
4. guest `logic` 设置远端代理并进入 Ban/Pick 或战斗态
### 3. 对战期间
继续复用当前 `logic/service/fight/pvp/service.go` 内的 Redis 指令转发模式
- 战斗操作通过 Redis topic 转发
- host `logic` 维持真实战斗对象
- guest `logic` 维持 remote proxy
## 失败语义
本阶段不做补偿不做离线保队列
### login 不在线
如果 `logic -> login` RPC 调用失败
- 本次匹配直接失败
- `logic` 清理本地匹配状态
- 返回前端匹配服务不可用
### 前端轮询停止
如果前端不再轮询
- 视为用户不再持续请求匹配
- `logic` 不负责继续保活
- 是否从 `login` 队列移除 `login` 的超时策略决定
### login 更新中
如果 `login` 正在更新
- `logic` 的同步 RPC 会失败
- 前端当前轮询会收到匹配服务不可用
- `login` 恢复后前端下一轮再发起匹配
这是本阶段明确接受的行为不在后端做补偿
## 最小实现建议
### 先增加 RPC 健康/匹配接口
[common/rpc/rpc.go](/workspace/common/rpc/rpc.go) 增加面向 `logic -> login` RPC 方法
建议最小接口
- `MatchJoinOrUpdate(PVPMatchJoinPayload) error`
- `MatchCancel(userID) error`
如果需要单独健康检查也可以加
- `MatchPing() error`
但在最小方案里`MatchJoinOrUpdate` 自身就可以承担健康检查职责
### logic 的匹配入口改为同步 RPC
改造 [logic/controller/fight_巅峰.go](/workspace/logic/controller/fight_巅峰.go#L19) [logic/service/fight/pvp/service.go](/workspace/logic/service/fight/pvp/service.go#L83)
- 入口不再直接发布 `queue_join`
- 先发 RPC `login`
- 成功才更新本地匹配状态
- 失败直接返回错误
- 取消匹配时通过 `MatchCancel` best-effort 清理
### 保留 Redis 对战链路
[logic/service/fight/pvp/service.go](/workspace/logic/service/fight/pvp/service.go#L170) 之后的 Redis 消费match result 处理Ban/Pick战斗 relay 不需要一次性重写可以继续保留
调整重点是
- 不再让匹配入口依赖 PubSub
- 让对战过程继续走 Redis
## 对前端的要求
前端不要无脑重复 join而是按轮询更新匹配状态处理
建议行为
1. 首次点击匹配时发一次加入
2. 匹配中每隔 `3~5s` 轮询一次更新
3. 如果返回匹配服务不可用前端退出匹配态并提示
4. 如果返回已匹配/进入 Ban/Pick前端切换到对应界面
## 本阶段不做的事
以下内容明确不在这次最小改造内
- `login` 更新期间的排队保活
- 持久化消息补偿
- `login` 重启后的队列恢复
- Redis Stream
- `login` 实例协调
- 匹配服务自动拉起目标 `logic`
## 后续可选增强
如果后面要继续提高可用性可以再逐步演进为
1. 匹配入口仍走 RPC
2. `login` 内部把队列落 Redis
3. 加入 ticket 和续租机制
4. login 更新时支持恢复匹配状态
但这不是当前阶段的目标
## 最终收敛结论
当前阶段建议明确成一句话
`匹配走 RPC对战走 Redis。`
对应业务语义
- 需要立即判断服务可用性的时候 RPC
- 需要跨服转发战斗消息的时候 Redis

View File

@@ -1,99 +0,0 @@
# 屎山代码分析报告
## 总体评估
- **质量评分**: 31.03/100
- **质量等级**: 🌸 偶有异味 - 基本没事但是有伤风化
- **分析文件数**: 203
- **代码总行数**: 20972
## 质量指标
| 指标 | 得分 | 权重 | 状态 |
|------|------|------|------|
| 状态管理 | 4.84 | 0.15 | |
| 循环复杂度 | 6.28 | 0.25 | |
| 命名规范 | 25.00 | 0.10 | |
| 错误处理 | 35.00 | 0.15 | |
| 代码结构 | 45.00 | 0.20 | |
| 代码重复度 | 55.00 | 0.15 | |
| 注释覆盖率 | 55.94 | 0.15 | |
## 问题文件 (Top 5)
### 1. /workspace/blazing/common/utils/sturc/field.go (得分: 53.85)
**问题分类**: 🔄 复杂度问题:10, 📝 注释问题:1, 其他问题:5
**主要问题**:
- 函数 Size 的循环复杂度较高 (12)建议简化
- 函数 packVal 的循环复杂度过高 (23)考虑重构
- 函数 Pack 的循环复杂度较高 (14)建议简化
- 函数 unpackVal 的循环复杂度过高 (21)考虑重构
- 函数 Unpack 的循环复杂度较高 (12)建议简化
- 函数 'Size' () 较长 (33 )可考虑重构
- 函数 'Size' () 复杂度过高 (12)建议简化
- 函数 'packVal' () 过长 (69 )建议拆分
- 函数 'packVal' () 复杂度严重过高 (23)必须简化
- 函数 'Pack' () 较长 (48 )可考虑重构
- 函数 'Pack' () 复杂度过高 (14)建议简化
- 函数 'unpackVal' () 过长 (57 )建议拆分
- 函数 'unpackVal' () 复杂度严重过高 (21)必须简化
- 函数 'Unpack' () 较长 (33 )可考虑重构
- 函数 'Unpack' () 复杂度过高 (12)建议简化
- 代码注释率极低 (1.38%)几乎没有注释
### 2. /workspace/blazing/common/utils/sturc/fields.go (得分: 46.83)
**问题分类**: 🔄 复杂度问题:4, 📝 注释问题:1, 其他问题:2
**主要问题**:
- 函数 Pack 的循环复杂度较高 (12)建议简化
- 函数 Unpack 的循环复杂度过高 (21)考虑重构
- 函数 'Pack' () 较长 (42 )可考虑重构
- 函数 'Pack' () 复杂度过高 (12)建议简化
- 函数 'Unpack' () 过长 (73 )建议拆分
- 函数 'Unpack' () 复杂度严重过高 (21)必须简化
- 代码注释率极低 (3.91%)几乎没有注释
### 3. /workspace/blazing/common/utils/sturc/parse.go (得分: 46.68)
**问题分类**: 🔄 复杂度问题:4, 📝 注释问题:1, 其他问题:3
**主要问题**:
- 代码注释率较低 (6.93%)建议增加注释
- 函数 parseField 的循环复杂度较高 (13)建议简化
- 函数 parseFieldsLocked 的循环复杂度过高 (18)考虑重构
- 函数 'parseField' () 过长 (64 )建议拆分
- 函数 'parseField' () 复杂度过高 (13)建议简化
- 函数 'parseFieldsLocked' () 过长 (64 )建议拆分
- 函数 'parseFieldsLocked' () 复杂度严重过高 (18)必须简化
- 函数 'parseFields' () 较长 (31 )可考虑重构
### 4. /workspace/blazing/common/utils/xml/typeinfo.go (得分: 46.13)
**问题分类**: 🔄 复杂度问题:6, 其他问题:3
**主要问题**:
- 函数 getTypeInfo 的循环复杂度过高 (18)考虑重构
- 函数 structFieldInfo 的循环复杂度过高 (33)考虑重构
- 函数 addFieldInfo 的循环复杂度过高 (20)考虑重构
- 函数 'getTypeInfo' () 过长 (58 )建议拆分
- 函数 'getTypeInfo' () 复杂度严重过高 (18)必须简化
- 函数 'structFieldInfo' () 极度过长 (114 )必须拆分
- 函数 'structFieldInfo' () 复杂度严重过高 (33)必须简化
- 函数 'addFieldInfo' () 过长 (66 )建议拆分
- 函数 'addFieldInfo' () 复杂度严重过高 (20)必须简化
### 5. /workspace/blazing/common/utils/go-jsonrpc/auth/handler.go (得分: 45.61)
**问题分类**: 📝 注释问题:1, 其他问题:1
**主要问题**:
- 函数 'ServeHTTP' () 较长 (31 )可考虑重构
- 代码注释率极低 (0.00%)几乎没有注释
## 改进建议
### 高优先级
- 继续保持当前的代码质量标准
### 中优先级
- 可以考虑进一步优化性能和可读性
- 完善文档和注释便于团队协作

View File

@@ -4,6 +4,7 @@
package controller
import (
"blazing/common/rpc"
"blazing/cool"
"blazing/logic/service/common"
"bytes"
@@ -16,8 +17,10 @@ import (
"github.com/gogf/gf/v2/os/glog"
"github.com/lunixbochs/struc"
"github.com/panjf2000/gnet/v2"
)
// Maincontroller 是控制器层共享变量。
var Maincontroller = &Controller{} //注入service
// Controller 分发cmd逻辑实现
@@ -27,6 +30,10 @@ type Controller struct {
Kick func(uint32) error
RegisterLogic func(uint32, uint32) error
MatchJoinOrUpdate func(rpc.PVPMatchJoinPayload) error
MatchCancel func(uint32) error
}
}
@@ -44,19 +51,14 @@ func ParseCmd[T any](data []byte) T {
// Init 初始化控制器注册所有cmd处理方法
// 参数 isGame: 标识是否为游戏服务器(true)或登录服务器(false)
func Init(isGame bool) {
// 获取控制器实例的反射值
controllerValue := reflect.ValueOf(Maincontroller)
// 获取控制器类型
controllerType := controllerValue.Type()
// 遍历控制器的所有方法
for i := 0; i < controllerType.NumMethod(); i++ {
method := controllerType.Method(i)
methodValue := controllerValue.MethodByName(method.Name)
methodValue := controllerValue.Method(i)
methodType := methodValue.Type()
// 获取方法第一个参数的类型(请求结构体)
if methodType.NumIn() == 0 {
continue
}
@@ -67,43 +69,46 @@ func Init(isGame bool) {
continue
}
reqType := reqArgType.Elem()
binding := getCmdBinding(reqType)
// 解析请求结构体中的cmd标签
for _, cmd := range getCmd(reqType) {
if cmd == 0 { // 说明不是有效的注册方法
for _, cmd := range binding.cmds {
if cmd == 0 {
glog.Warning(context.Background(), "方法参数必须包含CMD参数", method.Name, "跳过注册")
continue
}
// 根据服务器类型过滤cmd
// 登录服务器只处理小于1000的cmd
if methodType.NumIn() != 2 {
glog.Warning(context.Background(), "方法参数数量必须为2", method.Name, "跳过注册")
continue
}
if !isGame && cmd > 1000 {
continue
}
// 游戏服务器只处理大于等于1000的cmd
if isGame && cmd < 1000 {
continue
}
// 注册命令处理函数
if cool.Config.ServerInfo.IsDebug != 0 {
fmt.Println("注册方法", cmd, method.Name)
}
cmdInfo := cool.Cmd{
Func: methodValue,
Req: reqType,
// Res: , // TODO 待实现对不同用户初始化方法以取消全局cmdcache
}
// 预编译创建req实例的函数返回结构体指针
reqTypeForNew := reqType
cmdInfo.NewReqFunc = func() interface{} {
return reflect.New(reqTypeForNew).Interface()
cmdInfo := cool.Cmd{
Func: methodValue,
Req: reqType,
HeaderFieldIndex: append([]int(nil), binding.headerFieldIndex...),
UseConn: methodType.In(1) == connType,
NewReqFunc: func() interface{} {
return reflect.New(reqTypeForNew).Interface()
},
NewReqValue: func() reflect.Value {
return reflect.New(reqTypeForNew)
},
}
if _, exists := cool.CmdCache[cmd]; exists { // 方法已存在
if _, exists := cool.CmdCache[cmd]; exists {
panic(fmt.Sprintf("命令处理方法已存在,跳过注册 %d %s", cmd, method.Name))
}
cool.CmdCache[cmd] = cmdInfo
@@ -111,12 +116,20 @@ func Init(isGame bool) {
}
}
var targetType = reflect.TypeOf(common.TomeeHeader{})
var cmdTypeCache sync.Map
var (
targetType = reflect.TypeOf(common.TomeeHeader{})
connType = reflect.TypeOf((*gnet.Conn)(nil)).Elem()
cmdTypeCache sync.Map
)
// 默认返回值(无匹配字段/解析失败时)
const defaultCmdValue = 0
type cmdBinding struct {
cmds []uint32
headerFieldIndex []int
}
func normalizeStructType(typ reflect.Type) reflect.Type {
for typ.Kind() == reflect.Ptr {
typ = typ.Elem()
@@ -124,92 +137,93 @@ func normalizeStructType(typ reflect.Type) reflect.Type {
return typ
}
// getCmd 从结构体类型中提取绑定的cmd指令(递归查找嵌套结构体,支持值/指针类型的TomeeHeader
// 参数 typ: 待解析的结构体类型(支持多层指针)
// 返回值: 解析到的cmd切片无匹配/解析失败时返回[defaultCmdValue]
func getCmd(typ reflect.Type) []uint32 {
// getCmdBinding 从结构体类型中提取绑定的cmd指令和头字段位置。
func getCmdBinding(typ reflect.Type) cmdBinding {
typ = normalizeStructType(typ)
if cached, ok := cmdTypeCache.Load(typ); ok {
return cached.([]uint32)
return cached.(cmdBinding)
}
// 非结构体类型直接返回默认值
if typ.Kind() != reflect.Struct {
return []uint32{defaultCmdValue}
binding := cmdBinding{cmds: []uint32{defaultCmdValue}}
cmdTypeCache.Store(typ, binding)
return binding
}
if cmd, ok := findCmd(typ, make(map[reflect.Type]struct{})); ok {
cmdTypeCache.Store(typ, cmd)
return cmd
if binding, ok := findCmdBinding(typ, make(map[reflect.Type]struct{})); ok {
cmdTypeCache.Store(typ, binding)
return binding
}
// 未找到目标字段/所有解析失败,返回默认值
defaultCmd := []uint32{defaultCmdValue}
cmdTypeCache.Store(typ, defaultCmd)
return defaultCmd
binding := cmdBinding{cmds: []uint32{defaultCmdValue}}
cmdTypeCache.Store(typ, binding)
return binding
}
func findCmd(typ reflect.Type, visiting map[reflect.Type]struct{}) ([]uint32, bool) {
func findCmdBinding(typ reflect.Type, visiting map[reflect.Type]struct{}) (cmdBinding, bool) {
typ = normalizeStructType(typ)
if typ.Kind() != reflect.Struct {
return nil, false
return cmdBinding{}, false
}
if _, seen := visiting[typ]; seen {
return nil, false
return cmdBinding{}, false
}
visiting[typ] = struct{}{}
defer delete(visiting, typ)
// 遍历结构体字段查找TomeeHeader字段并解析cmd
for i := 0; i < typ.NumField(); i++ {
field := typ.Field(i)
// 尝试解析当前字段的cmd标签
cmdSlice, isHeader, err := parseCmdTagWithStructField(field)
if isHeader && err == nil { // 解析成功,直接返回结果
return cmdSlice, true
if isHeader && err == nil {
return cmdBinding{
cmds: cmdSlice,
headerFieldIndex: append([]int(nil), field.Index...),
}, true
}
// 递归处理嵌套结构体(值/指针类型)
nestedTyp := normalizeStructType(field.Type)
if nestedTyp.Kind() == reflect.Struct {
// 递归查找找到有效cmd则立即返回
if nestedCmd, ok := findCmd(nestedTyp, visiting); ok {
return nestedCmd, true
}
if nestedTyp.Kind() != reflect.Struct {
continue
}
nestedBinding, ok := findCmdBinding(nestedTyp, visiting)
if !ok {
continue
}
fieldIndex := make([]int, 0, len(field.Index)+len(nestedBinding.headerFieldIndex))
fieldIndex = append(fieldIndex, field.Index...)
fieldIndex = append(fieldIndex, nestedBinding.headerFieldIndex...)
nestedBinding.headerFieldIndex = fieldIndex
return nestedBinding, true
}
return nil, false
return cmdBinding{}, false
}
// parseCmdTagWithStructField 校验字段是否为TomeeHeader值/指针并解析cmd标签
// 参数 field: 结构体字段元信息
// 返回值: 解析后的cmd切片是否为目标类型解析失败错误
func parseCmdTagWithStructField(field reflect.StructField) ([]uint32, bool, error) {
// 判断字段类型是否为 TomeeHeader 或 *TomeeHeader
if field.Type != targetType && !(field.Type.Kind() == reflect.Ptr && field.Type.Elem() == targetType) {
return nil, false, nil
}
// 提取cmd标签
cmdStr := field.Tag.Get("cmd")
if cmdStr == "" {
return nil, true, fmt.Errorf("field %s cmd tag is empty", field.Name)
}
// 高性能解析标签为uint32切片替代gconv减少第三方依赖且可控
result := make([]uint32, 0, strings.Count(cmdStr, "|")+1)
remain := cmdStr
for idx := 0; ; idx++ {
part, next, found := strings.Cut(remain, "|")
// 去除空白字符(兼容标签中意外的空格)
s := strings.TrimSpace(part)
if s == "" {
return nil, true, fmt.Errorf("field %s cmd tag part %d is empty", field.Name, idx)
}
// 手动解析uint32比gconv更可控避免隐式转换问题
num, err := strconv.ParseUint(s, 10, 32)
if err != nil {
return nil, true, fmt.Errorf("field %s cmd tag part %d parse error: %v (value: %s)",

View File

@@ -43,6 +43,7 @@ var masterCupRequiredItems = map[uint32][]ItemS{
},
}
// DASHIbei 处理控制器请求。
func (h Controller) DASHIbei(req *C2s_MASTER_REWARDS, c *player.Player) (result *S2C_MASTER_REWARDS, err errorcode.ErrorCode) {
_ = req
result = &S2C_MASTER_REWARDS{}
@@ -52,6 +53,7 @@ func (h Controller) DASHIbei(req *C2s_MASTER_REWARDS, c *player.Player) (result
return
}
// DASHIbeiR 处理控制器请求。
func (h Controller) DASHIbeiR(req *C2s_MASTER_REWARDSR, c *player.Player) (result *S2C_MASTER_REWARDSR, err errorcode.ErrorCode) {
result = &S2C_MASTER_REWARDSR{}
@@ -70,30 +72,37 @@ func (h Controller) DASHIbeiR(req *C2s_MASTER_REWARDSR, c *player.Player) (resul
}
result.ItemList = make([]data.ItemInfo, 0, len(taskInfo.ItemList))
c.Service.Task.Exec(masterCupTaskID, func(te *model.Task) bool {
progress := bitset32.From(te.Data)
if progress.Test(uint(req.ElementType)) {
err = errorcode.ErrorCode(errorcode.ErrorCodes.ErrAwardAlreadyClaimed)
return false
}
taskData, taskErr := c.Service.Task.GetTask(masterCupTaskID)
if taskErr != nil {
return nil, errorcode.ErrorCode(errorcode.ErrorCodes.ErrSystemError)
}
consumeMasterCupItems(c, requiredItems)
progress.Set(uint(req.ElementType))
te.Data = progress.Bytes()
progress := bitset32.From(taskData.Data)
if progress.Test(uint(req.ElementType)) {
return nil, errorcode.ErrorCode(errorcode.ErrorCodes.ErrAwardAlreadyClaimed)
}
if taskInfo.Pet != nil {
c.Service.Pet.PetAdd(taskInfo.Pet, 0)
result.CaptureTime = taskInfo.Pet.CatchTime
result.PetTypeId = taskInfo.Pet.ID
}
if err := consumeMasterCupItems(c, requiredItems); err != nil {
return nil, errorcode.ErrorCode(errorcode.ErrorCodes.ErrInsufficientItems)
}
progress.Set(uint(req.ElementType))
taskData.Data = progress.Bytes()
if taskErr = c.Service.Task.SetTask(taskData); taskErr != nil {
return nil, errorcode.ErrorCode(errorcode.ErrorCodes.ErrSystemError)
}
appendMasterCupRewardItems(c, result, taskInfo.ItemList)
return true
})
if taskInfo.Pet != nil {
c.Service.Pet.PetAdd(taskInfo.Pet, 0)
result.CaptureTime = taskInfo.Pet.CatchTime
result.PetTypeId = taskInfo.Pet.ID
}
appendMasterCupRewardItems(c, result, taskInfo.ItemList)
return
}
// ItemS 定义请求或响应数据结构。
type ItemS struct {
ItemId uint32
ItemCnt uint32
@@ -123,10 +132,13 @@ func hasEnoughMasterCupItems(c *player.Player, requiredItems []ItemS) bool {
return true
}
func consumeMasterCupItems(c *player.Player, requiredItems []ItemS) {
func consumeMasterCupItems(c *player.Player, requiredItems []ItemS) error {
for _, item := range requiredItems {
c.Service.Item.UPDATE(item.ItemId, -int(item.ItemCnt))
if err := c.Service.Item.UPDATE(item.ItemId, -int(item.ItemCnt)); err != nil {
return err
}
}
return nil
}
func appendMasterCupRewardItems(c *player.Player, result *S2C_MASTER_REWARDSR, itemList []data.ItemInfo) {
@@ -137,6 +149,7 @@ func appendMasterCupRewardItems(c *player.Player, result *S2C_MASTER_REWARDSR, i
}
}
// C2s_MASTER_REWARDS 定义请求或响应数据结构。
type C2s_MASTER_REWARDS struct {
Head common.TomeeHeader `cmd:"2611" struc:"skip"` //玩家登录
}
@@ -147,6 +160,7 @@ type S2C_MASTER_REWARDS struct {
Reward []uint32 `json:"Reward"`
}
// C2s_MASTER_REWARDSR 定义请求或响应数据结构。
type C2s_MASTER_REWARDSR struct {
Head common.TomeeHeader `cmd:"2612" struc:"skip"` //玩家登录
ElementType uint32

View File

@@ -12,6 +12,7 @@ import (
"github.com/gogf/gf/v2/util/grand"
)
// EggGamePlay 处理控制器请求。
func (h Controller) EggGamePlay(data1 *C2S_EGG_GAME_PLAY, c *player.Player) (result *S2C_EGG_GAME_PLAY, err errorcode.ErrorCode) {
switch data1.EggNum {
@@ -25,12 +26,11 @@ func (h Controller) EggGamePlay(data1 *C2S_EGG_GAME_PLAY, c *player.Player) (res
if data1.EggNum > 10 || data1.EggNum <= 0 {
return nil, errorcode.ErrorCode(errorcode.ErrorCodes.ErrSystemError)
}
if r < 0 {
if r <= 0 || data1.EggNum > r {
return nil, errorcode.ErrorCode(errorcode.ErrorCodes.ErrGachaTicketsInsufficient)
}
if data1.EggNum > r {
if err := c.Service.Item.UPDATE(400501, int(-data1.EggNum)); err != nil {
return nil, errorcode.ErrorCode(errorcode.ErrorCodes.ErrGachaTicketsInsufficient)
}
result = &S2C_EGG_GAME_PLAY{ListInfo: []data.ItemInfo{}}
if grand.Meet(int(data1.EggNum), 100) {
@@ -51,8 +51,6 @@ func (h Controller) EggGamePlay(data1 *C2S_EGG_GAME_PLAY, c *player.Player) (res
for _, item := range addedItems {
result.ListInfo = append(result.ListInfo, data.ItemInfo{ItemId: item.ItemId, ItemCnt: item.ItemCnt})
}
c.Service.Item.UPDATE(400501, int(-data1.EggNum))
return
}

View File

@@ -37,6 +37,7 @@ func Draw15To10WithBitSet() uint32 {
return resultBits
}
// GET_XUANCAI 处理控制器请求。
func (h Controller) GET_XUANCAI(data *C2s_GET_XUANCAI, c *player.Player) (result *S2C_GET_XUANCAI, err errorcode.ErrorCode) {
result = &S2C_GET_XUANCAI{}
selectedCount := 0 // 已选中的数量
@@ -56,6 +57,7 @@ func (h Controller) GET_XUANCAI(data *C2s_GET_XUANCAI, c *player.Player) (result
// 检查该位是否未被选中(避免重复)
if (result.Status & mask) == 0 {
result.Status |= mask
itemID := uint32(400686 + randBitIdx + 1)
selectedItems = append(selectedItems, itemID)
itemMask[itemID] = mask
@@ -74,6 +76,7 @@ func (h Controller) GET_XUANCAI(data *C2s_GET_XUANCAI, c *player.Player) (result
}
// C2s_GET_XUANCAI 定义请求或响应数据结构。
type C2s_GET_XUANCAI struct {
Head common.TomeeHeader `cmd:"60001" struc:"skip"` //玩家登录
}

View File

@@ -30,6 +30,7 @@ func (h Controller) TimeMap(data *C2s_SP, c *player.Player) (result *S2C_SP, err
}
// C2s_SP 定义请求或响应数据结构。
type C2s_SP struct {
Head common.TomeeHeader `cmd:"60002" struc:"skip"` //超时空地图
}
@@ -40,6 +41,7 @@ type S2C_SP struct {
MapList []ServerInfo
}
// ServerInfo 定义请求或响应数据结构。
type ServerInfo struct {
ID uint32 //地图ID
PetLen uint32 `struc:"sizeof=Pet"`

View File

@@ -7,6 +7,7 @@ import (
"blazing/logic/service/player"
)
// GetLeiyiTrainStatus 处理控制器请求。
func (h Controller) GetLeiyiTrainStatus(data *C2s_LEIYI_TRAIN_GET_STATUS, c *player.Player) (result *S2C_LEIYI_TRAIN_GET_STATUS, err errorcode.ErrorCode) {
result = &S2C_LEIYI_TRAIN_GET_STATUS{}
@@ -19,6 +20,7 @@ func (h Controller) GetLeiyiTrainStatus(data *C2s_LEIYI_TRAIN_GET_STATUS, c *pla
}
// C2s_LEIYI_TRAIN_GET_STATUS 定义请求或响应数据结构。
type C2s_LEIYI_TRAIN_GET_STATUS struct {
Head common.TomeeHeader `cmd:"2393" struc:"skip"` //玩家登录
}
@@ -28,6 +30,7 @@ type S2C_LEIYI_TRAIN_GET_STATUS struct {
Status [10]S2C_LEIYI_TRAIN_GET_STATUS_info `json:"status"`
}
// S2C_LEIYI_TRAIN_GET_STATUS_info 定义请求或响应数据结构。
type S2C_LEIYI_TRAIN_GET_STATUS_info struct {
// Today uint32 // 今日训练HP次数
Current uint32 // 当前训练HP次数

View File

@@ -35,6 +35,7 @@ func (h Controller) HanLiuQiang(data *C2S_2608, c *player.Player) (result *fight
return result, -1
}
// C2S_2608 定义请求或响应数据结构。
type C2S_2608 struct {
Head common.TomeeHeader `cmd:"2608" struc:"skip"`
}

View File

@@ -2,7 +2,6 @@ package controller
import (
"blazing/common/socket/errorcode"
"blazing/modules/player/model"
"blazing/logic/service/fight"
"blazing/logic/service/fight/info"
@@ -26,12 +25,75 @@ func (h Controller) OnReadyToFight(data *ReadyToFightInboundInfo, c *player.Play
return nil, -1
}
// GroupReadyFightFinish 旧组队协议准备完成。
func (h Controller) GroupReadyFightFinish(data *GroupReadyFightFinishInboundInfo, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
if err := h.checkFightStatus(c); err != 0 {
return nil, err
}
go c.FightC.ReadyFight(c)
return nil, -1
}
func (h Controller) GroupUseSkill(data *GroupUseSkillInboundInfo, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
if err := h.checkFightStatus(c); err != 0 {
return nil, err
}
targetRelation := fight.SkillTargetOpponent
if data.TargetSide == 1 {
targetRelation = fight.SkillTargetAlly
}
h.dispatchFightActionEnvelope(c, fight.NewSkillActionEnvelope(data.SkillId, int(data.ActorIndex), int(data.TargetPos), targetRelation, 0))
c.SendPackCmd(7558, nil)
return nil, -1
}
func (h Controller) GroupUseItem(data *GroupUseItemInboundInfo, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
if err := h.checkFightStatus(c); err != 0 {
return nil, err
}
h.dispatchFightActionEnvelope(c, fight.NewItemActionEnvelope(0, data.ItemId, int(data.ActorIndex), int(data.ActorIndex), fight.SkillTargetSelf))
return nil, -1
}
func (h Controller) GroupChangePet(data *GroupChangePetInboundInfo, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
if err := h.checkFightStatus(c); err != 0 {
return nil, err
}
h.dispatchFightActionEnvelope(c, fight.NewChangeActionEnvelope(data.CatchTime, int(data.ActorIndex)))
return nil, -1
}
func (h Controller) GroupEscape(data *GroupEscapeInboundInfo, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
if err := h.checkFightStatus(c); err != 0 {
return nil, err
}
if fightC, ok := c.FightC.(*fight.FightC); ok && fightC != nil && fightC.LegacyGroupProtocol {
fightC.SendLegacyEscapeSuccess(c, int(data.ActorIndex))
}
h.dispatchFightActionEnvelope(c, fight.NewEscapeActionEnvelope())
return nil, 0
}
func (h Controller) GroupFightWinClose(data *GroupFightWinCloseInboundInfo, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
if c != nil {
c.QuitFight()
}
return nil, -1
}
func (h Controller) GroupFightTimeoutExit(data *GroupFightTimeoutExitInboundInfo, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
if c != nil {
c.QuitFight()
}
return nil, -1
}
// UseSkill 使用技能包
func (h Controller) UseSkill(data *UseSkillInInfo, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
if err := h.checkFightStatus(c); err != 0 {
return nil, err
}
go c.FightC.UseSkill(c, data.SkillId)
h.dispatchFightActionEnvelope(c, buildLegacyUseSkillEnvelope(data))
return nil, 0
}
@@ -41,31 +103,7 @@ func (h Controller) UseSkillAt(data *UseSkillAtInboundInfo, c *player.Player) (r
if err := h.checkFightStatus(c); err != 0 {
return nil, err
}
actorIndex := int(data.ActorIndex)
targetIndex := int(data.TargetIndex)
targetRelation := data.TargetRelation
// 前端未显式给 relation 时,按 AtkType 兜底3=自己1=己方,其他按对方。
if targetRelation > fight.SkillTargetAlly {
switch data.AtkType {
case 3:
targetRelation = fight.SkillTargetSelf
case 1:
targetRelation = fight.SkillTargetAlly
default:
targetRelation = fight.SkillTargetOpponent
}
}
switch targetRelation {
case fight.SkillTargetSelf:
targetIndex = actorIndex
go c.FightC.UseSkillAt(c, data.SkillId, actorIndex, fight.EncodeTargetIndex(targetIndex, false))
case fight.SkillTargetAlly:
go c.FightC.UseSkillAt(c, data.SkillId, actorIndex, fight.EncodeTargetIndex(targetIndex, false))
default:
go c.FightC.UseSkillAt(c, data.SkillId, actorIndex, fight.EncodeTargetIndex(targetIndex, true))
}
h.dispatchFightActionEnvelope(c, buildIndexedUseSkillEnvelope(data))
return nil, 0
}
@@ -74,8 +112,7 @@ func (h Controller) Escape(data *EscapeFightInboundInfo, c *player.Player) (resu
if err := h.checkFightStatus(c); err != 0 {
return nil, err
}
go c.FightC.Over(c, model.BattleOverReason.PlayerEscape)
h.dispatchFightActionEnvelope(c, buildLegacyEscapeEnvelope())
return nil, 0
}
@@ -84,7 +121,7 @@ func (h Controller) ChangePet(data *ChangePetInboundInfo, c *player.Player) (res
if err := h.checkFightStatus(c); err != 0 {
return nil, err
}
go c.FightC.ChangePet(c, data.CatchTime)
h.dispatchFightActionEnvelope(c, buildLegacyChangeEnvelope(data))
return nil, -1
}
@@ -123,7 +160,7 @@ func (h Controller) UsePetItemInboundInfo(data *UsePetItemInboundInfo, c *player
}
}
go c.FightC.UseItem(c, data.CatchTime, data.ItemId)
h.dispatchFightActionEnvelope(c, buildLegacyUseItemEnvelope(data))
return nil, -1
}
@@ -132,6 +169,6 @@ func (h Controller) FightChat(data *ChatInfo, c *player.Player) (result *fight.N
if err := h.checkFightStatus(c); err != 0 {
return nil, err
}
go c.FightC.Chat(c, data.Message)
h.dispatchFightActionEnvelope(c, buildChatEnvelope(data))
return nil, -1
}

View File

@@ -4,7 +4,6 @@ import (
"blazing/common/data"
"blazing/common/data/xmlres"
"blazing/common/socket/errorcode"
"strings"
"blazing/logic/service/fight"
fightinfo "blazing/logic/service/fight/info"
@@ -19,7 +18,8 @@ import (
)
const (
rewardItemExpPool = 3
rewardItemExpPool = 3
groupBossSlotLimit = 3
)
// PlayerFightBoss 挑战地图boss
@@ -28,7 +28,10 @@ func (Controller) PlayerFightBoss(req *ChallengeBossInboundInfo, p *player.Playe
return nil, err
}
mapNode := service.NewMapNodeService().GetDataNode(p.Info.MapID, req.BossId)
mapNode := p.GetSpace().GetMatchedMapNode(req.BossId)
if mapNode == nil {
return nil, errorcode.ErrorCodes.ErrPokemonNotExists
}
bossConfigs, err := loadMapBossConfigs(mapNode)
if err != 0 {
return nil, err
@@ -40,14 +43,15 @@ func (Controller) PlayerFightBoss(req *ChallengeBossInboundInfo, p *player.Playe
}
p.Fightinfo.Status = fightinfo.BattleMode.FIGHT_WITH_NPC
p.Fightinfo.Mode = fightinfo.BattleMode.MULTI_MODE
p.Fightinfo.Mode = resolveMapNodeFightMode(mapNode)
ai := player.NewAI_player(monsterInfo)
ai.CanCapture = resolveBossCaptureRate(bossConfigs[0].IsCapture, leadMonsterID)
ai.Prop[0] = 2
ai.BossScript = bossConfigs[0].Script
ai.AddBattleProp(0, 2)
var fightC *fight.FightC
fightC, err = fight.NewFight(p, ai, p.GetPetInfo(100), ai.GetPetInfo(0), func(foi model.FightOverInfo) {
fightC, err = startMapBossFight(mapNode, p, ai, func(foi model.FightOverInfo) {
if mapNode.WinBonusID == 0 {
return
}
@@ -62,13 +66,40 @@ func (Controller) PlayerFightBoss(req *ChallengeBossInboundInfo, p *player.Playe
return nil, -1
}
func startMapBossFight(
mapNode *configmodel.MapNode,
p *player.Player,
ai *player.AI_player,
fn func(model.FightOverInfo),
) (*fight.FightC, errorcode.ErrorCode) {
ourPets := p.GetPetInfo(p.CurrentMapPetLevelLimit())
oppPets := ai.GetPetInfo(0)
if mapNode != nil && mapNode.IsGroupBoss != 0 {
if len(ourPets) > 0 && len(oppPets) > 0 {
slotLimit := groupBossSlotLimit
if mapNode.PkFlag != 0 {
slotLimit = 1
}
return fight.NewLegacyGroupFightSingleController(p, ai, ourPets, oppPets, slotLimit, fn)
}
}
return fight.NewFight(p, ai, ourPets, oppPets, fn)
}
func resolveMapNodeFightMode(mapNode *configmodel.MapNode) uint32 {
if mapNode != nil && mapNode.PkFlag != 0 {
return fightinfo.BattleMode.SINGLE_MODE
}
return fightinfo.BattleMode.MULTI_MODE
}
// OnPlayerFightNpcMonster 战斗野怪
func (Controller) OnPlayerFightNpcMonster(req *FightNpcMonsterInboundInfo, p *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
if err = p.CanFight(); err != 0 {
return nil, err
}
if req.Number > 9 {
return nil, errorcode.ErrorCodes.ErrSystemError
if int(req.Number) >= len(p.Data) {
return nil, errorcode.ErrorCodes.ErrPokemonNotHere
}
refPet := p.Data[req.Number]
@@ -83,7 +114,7 @@ func (Controller) OnPlayerFightNpcMonster(req *FightNpcMonsterInboundInfo, p *pl
p.Fightinfo.Status = fightinfo.BattleMode.FIGHT_WITH_NPC
p.Fightinfo.Mode = fightinfo.BattleMode.MULTI_MODE
_, err = fight.NewFight(p, ai, p.GetPetInfo(100), ai.GetPetInfo(0), func(foi model.FightOverInfo) {
_, err = fight.NewFight(p, ai, p.GetPetInfo(p.CurrentMapPetLevelLimit()), ai.GetPetInfo(0), func(foi model.FightOverInfo) {
handleNpcFightRewards(p, foi, monster)
})
if err != 0 {
@@ -205,7 +236,7 @@ func shouldGrantBossWinBonus(fightC *fight.FightC, playerID uint32, bossConfig c
func buildNpcMonsterInfo(refPet player.OgrePetInfo, mapID uint32) (*model.PetInfo, *model.PlayerInfo, errorcode.ErrorCode) {
if refPet.ID == 0 {
return nil, nil, errorcode.ErrorCodes.ErrPokemonNotExists
return nil, nil, errorcode.ErrorCodes.ErrPokemonNotHere
}
monster := model.GenPetInfo(
@@ -279,5 +310,5 @@ func handleNpcFightRewards(p *player.Player, foi model.FightOverInfo, monster *m
if rewards.HasReward() {
p.SendPackCmd(8004, rewards)
}
foi.Winpet.AddEV(gconv.Int64s(strings.Fields(petCfg.YieldingEV)))
foi.Winpet.AddEV(petCfg.YieldingEVValues)
}

View File

@@ -70,9 +70,12 @@ func (h Controller) PetMelee(data *StartPetWarInboundInfo, c *player.Player) (re
return
}
// PetKing 处理控制器请求。
func (h Controller) PetKing(data *PetKingJoinInboundInfo, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
c.Fightinfo.Status = info.BattleMode.PET_TOPLEVEL
// ElementTypeNumbers 是控制器层共享变量。
var ElementTypeNumbers = []int{1, 2, 3, 5, 11, 4, 6, 7, 9}
switch data.Type {

View File

@@ -55,7 +55,7 @@ func (h Controller) OnPlayerHandleFightInvite(data *HandleFightInviteInboundInfo
return
}
_, err = fight.NewFight(v, c, v.GetInfo().PetList, c.GetInfo().PetList, func(foi model.FightOverInfo) {
_, err = fight.NewFight(v, c, v.GetPetInfo(100), c.GetPetInfo(100), func(foi model.FightOverInfo) {
//println("好友对战测试", foi.Reason)

View File

@@ -0,0 +1,79 @@
package controller
import (
"blazing/modules/player/model"
"blazing/logic/service/fight"
"blazing/logic/service/player"
)
// dispatchFightActionEnvelope 把控制器层收到的统一动作结构分发回现有 FightI 接口。
func (h Controller) dispatchFightActionEnvelope(c *player.Player, envelope fight.FightActionEnvelope) {
if c == nil || c.FightC == nil {
return
}
switch envelope.ActionType {
case fight.FightActionTypeSkill:
go c.FightC.UseSkillAt(c, envelope.SkillID, envelope.ActorIndex, envelope.EncodedTargetIndex())
case fight.FightActionTypeItem:
go c.FightC.UseItemAt(c, envelope.CatchTime, envelope.ItemID, envelope.ActorIndex, envelope.EncodedTargetIndex())
case fight.FightActionTypeChange:
go c.FightC.ChangePetAt(c, envelope.CatchTime, envelope.ActorIndex)
case fight.FightActionTypeEscape:
go c.FightC.Over(c, model.BattleOverReason.PlayerEscape)
case fight.FightActionTypeChat:
go c.FightC.Chat(c, envelope.Chat)
}
}
// buildLegacyUseSkillEnvelope 把旧 2405 技能包映射成统一动作结构。
func buildLegacyUseSkillEnvelope(data *UseSkillInInfo) fight.FightActionEnvelope {
if data == nil {
return fight.NewSkillActionEnvelope(0, 0, 0, fight.SkillTargetOpponent, 0)
}
return fight.NewSkillActionEnvelope(data.SkillId, 0, 0, fight.SkillTargetOpponent, 0)
}
// buildIndexedUseSkillEnvelope 把 7505 多战位技能包映射成统一动作结构。
func buildIndexedUseSkillEnvelope(data *UseSkillAtInboundInfo) fight.FightActionEnvelope {
if data == nil {
return fight.NewSkillActionEnvelope(0, 0, 0, fight.SkillTargetOpponent, 0)
}
return fight.NewSkillActionEnvelope(
data.SkillId,
int(data.ActorIndex),
int(data.TargetIndex),
data.TargetRelation,
data.AtkType,
)
}
// buildLegacyUseItemEnvelope 把旧 2406 道具包映射成统一动作结构。
func buildLegacyUseItemEnvelope(data *UsePetItemInboundInfo) fight.FightActionEnvelope {
if data == nil {
return fight.NewItemActionEnvelope(0, 0, 0, 0, fight.SkillTargetOpponent)
}
return fight.NewItemActionEnvelope(data.CatchTime, data.ItemId, 0, 0, fight.SkillTargetOpponent)
}
// buildLegacyChangeEnvelope 把旧 2407 切宠包映射成统一动作结构。
func buildLegacyChangeEnvelope(data *ChangePetInboundInfo) fight.FightActionEnvelope {
if data == nil {
return fight.NewChangeActionEnvelope(0, 0)
}
return fight.NewChangeActionEnvelope(data.CatchTime, 0)
}
// buildLegacyEscapeEnvelope 构造旧 2410 逃跑包对应的统一动作结构。
func buildLegacyEscapeEnvelope() fight.FightActionEnvelope {
return fight.NewEscapeActionEnvelope()
}
// buildChatEnvelope 把战斗聊天包映射成统一动作结构。
func buildChatEnvelope(data *ChatInfo) fight.FightActionEnvelope {
if data == nil {
return fight.NewChatActionEnvelope("")
}
return fight.NewChatActionEnvelope(data.Message)
}

View File

@@ -82,6 +82,7 @@ func (h Controller) FreshChoiceFightLevel(data *C2S_FRESH_CHOICE_FIGHT_LEVEL, c
return result, 0
}
// FreshLeaveFightLevel 处理控制器请求。
func (h Controller) FreshLeaveFightLevel(data *FRESH_LEAVE_FIGHT_LEVEL, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
_ = data
defer c.GetSpace().EnterMap(c)
@@ -92,6 +93,7 @@ func (h Controller) FreshLeaveFightLevel(data *FRESH_LEAVE_FIGHT_LEVEL, c *playe
return result, 0
}
// PetTawor 处理控制器请求。
func (h Controller) PetTawor(data *StartTwarInboundInfo, c *player.Player) (result *fight.S2C_ChoiceLevelRequestInfo, err errorcode.ErrorCode) {
if err = c.CanFight(); err != 0 {
return nil, err
@@ -110,7 +112,7 @@ func (h Controller) PetTawor(data *StartTwarInboundInfo, c *player.Player) (resu
result = &fight.S2C_ChoiceLevelRequestInfo{CurFightLevel: currentLevel}
appendTowerNextBossPreview(&result.BossID, bossList)
monsterInfo, ok := buildTowerMonsterInfo(currentBoss)
monsterInfo, bossScript, ok := buildTowerMonsterInfo(currentBoss)
if !ok {
return nil, errorcode.ErrorCodes.ErrPokemonNotExists
}
@@ -119,6 +121,7 @@ func (h Controller) PetTawor(data *StartTwarInboundInfo, c *player.Player) (resu
c.Fightinfo.Status = fightinfo.BattleMode.FIGHT_WITH_NPC
ai := player.NewAI_player(monsterInfo)
ai.BossScript = bossScript
_, err = fight.NewFight(c, ai, c.GetPetInfo(100), ai.GetPetInfo(0), func(foi model.FightOverInfo) {
if foi.Reason != 0 || foi.WinnerId != c.Info.UserID {
return
@@ -195,10 +198,10 @@ func appendTowerNextBossPreview(dst *[]uint32, bossList []configmodel.BaseTowerC
}
}
func buildTowerMonsterInfo(towerBoss configmodel.BaseTowerConfig) (*model.PlayerInfo, bool) {
func buildTowerMonsterInfo(towerBoss configmodel.BaseTowerConfig) (*model.PlayerInfo, string, bool) {
bosses := service.NewBossService().Get(towerBoss.BossIds[0])
if len(bosses) == 0 {
return nil, false
return nil, "", false
}
monsterInfo := &model.PlayerInfo{Nick: towerBoss.Name}
@@ -234,7 +237,7 @@ func buildTowerMonsterInfo(towerBoss configmodel.BaseTowerConfig) (*model.Player
monsterInfo.PetList = append(monsterInfo.PetList, *monster)
}
return monsterInfo, true
return monsterInfo, bosses[0].Script, true
}
func handleTowerFightWin(c *player.Player, cmd uint32, taskID int, currentLevel uint32) {

View File

@@ -1,6 +1,7 @@
package controller
import (
"blazing/common/rpc"
"blazing/common/socket/errorcode"
"blazing/logic/service/common"
"blazing/logic/service/fight"
@@ -15,19 +16,46 @@ type PetTOPLEVELnboundInfo struct {
}
// JoINtop 处理控制器请求。
func (h Controller) JoINtop(data *PetTOPLEVELnboundInfo, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
err = pvp.JoinPeakQueue(c, data.Mode)
if err != 0 {
return nil, err
}
if Maincontroller.RPCClient == nil || Maincontroller.RPCClient.MatchJoinOrUpdate == nil {
pvp.CancelPeakQueue(c)
return nil, errorcode.ErrorCodes.ErrSystemBusyTryLater
}
fightMode, status, err := pvp.NormalizePeakMode(data.Mode)
if err != 0 {
pvp.CancelPeakQueue(c)
return nil, err
}
joinPayload := rpc.PVPMatchJoinPayload{
RuntimeServerID: h.UID,
UserID: c.Info.UserID,
Nick: c.Info.Nick,
FightMode: fightMode,
Status: status,
CatchTimes: pvp.AvailableCatchTimes(c.GetPetInfo(0)),
}
if callErr := Maincontroller.RPCClient.MatchJoinOrUpdate(joinPayload); callErr != nil {
pvp.CancelPeakQueue(c)
return nil, errorcode.ErrorCodes.ErrSystemBusyTryLater
}
return nil, -1
}
// CancelPeakQueue 处理控制器请求。
func (h Controller) CancelPeakQueue(data *PeakQueueCancelInboundInfo, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
if Maincontroller.RPCClient != nil && Maincontroller.RPCClient.MatchCancel != nil {
_ = Maincontroller.RPCClient.MatchCancel(c.Info.UserID)
}
pvp.CancelPeakQueue(c)
return nil, -1
}
// SubmitPeakBanPick 处理控制器请求。
func (h Controller) SubmitPeakBanPick(data *PeakBanPickSubmitInboundInfo, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
err = pvp.SubmitBanPick(c, data.SelectedCatchTimes, data.BanCatchTimes)
if err != 0 {

View File

@@ -12,6 +12,7 @@ import (
"blazing/logic/service/space"
)
// ARENA_SET_OWENR 定义请求或响应数据结构。
type ARENA_SET_OWENR struct {
Head common.TomeeHeader `cmd:"2417" struc:"skip"`
}
@@ -35,6 +36,7 @@ func (h Controller) ArenaSetOwner(data *ARENA_SET_OWENR, c *player.Player) (resu
return nil, errorcode.ErrorCodes.ErrChampionExists
}
// ARENA_FIGHT_OWENR 定义请求或响应数据结构。
type ARENA_FIGHT_OWENR struct {
Head common.TomeeHeader `cmd:"2418" struc:"skip"`
}

View File

@@ -2,56 +2,104 @@ package controller
import "blazing/logic/service/common"
// FightNpcMonsterInboundInfo 定义请求或响应数据结构。
type FightNpcMonsterInboundInfo struct {
Head common.TomeeHeader `cmd:"2408" struc:"skip"`
Number uint32 `fieldDesc:"地图刷新怪物结构体对应的序号 1 - 9 的位置序号" `
}
// ChallengeBossInboundInfo 定义请求或响应数据结构。
type ChallengeBossInboundInfo struct {
Head common.TomeeHeader `cmd:"2411" struc:"skip"`
BossId uint32 `json:"bossId"`
}
// ReadyToFightInboundInfo 定义请求或响应数据结构。
type ReadyToFightInboundInfo struct {
Head common.TomeeHeader `cmd:"2404" struc:"skip"`
}
// GroupReadyFightFinishInboundInfo 旧组队协议准备完成。
type GroupReadyFightFinishInboundInfo struct {
Head common.TomeeHeader `cmd:"7556" struc:"skip"`
}
type GroupUseSkillInboundInfo struct {
Head common.TomeeHeader `cmd:"7558" struc:"skip"`
ActorIndex uint8
TargetSide uint8
TargetPos uint8
SkillId uint32
}
type GroupUseItemInboundInfo struct {
Head common.TomeeHeader `cmd:"7562" struc:"skip"`
ActorIndex uint8
ItemId uint32
}
type GroupChangePetInboundInfo struct {
Head common.TomeeHeader `cmd:"7563" struc:"skip"`
ActorIndex uint8
CatchTime uint32
}
type GroupEscapeInboundInfo struct {
Head common.TomeeHeader `cmd:"7565" struc:"skip"`
ActorIndex uint8
}
type GroupFightWinCloseInboundInfo struct {
Head common.TomeeHeader `cmd:"7574" struc:"skip"`
}
type GroupFightTimeoutExitInboundInfo struct {
Head common.TomeeHeader `cmd:"7587" struc:"skip"`
}
// EscapeFightInboundInfo 定义请求或响应数据结构。
type EscapeFightInboundInfo struct {
Head common.TomeeHeader `cmd:"2410" struc:"skip"`
}
// StartPetWarInboundInfo 定义请求或响应数据结构。
type StartPetWarInboundInfo struct {
Head common.TomeeHeader `cmd:"2431" struc:"skip"`
}
// StartTwarInboundInfo 定义请求或响应数据结构。
type StartTwarInboundInfo struct {
Head common.TomeeHeader `cmd:"2429|2415|2425" struc:"skip"`
}
// ARENA_GET_INFO 定义请求或响应数据结构。
type ARENA_GET_INFO struct {
Head common.TomeeHeader `cmd:"2419" struc:"skip"`
}
// ARENA_UPFIGHT 定义请求或响应数据结构。
type ARENA_UPFIGHT struct {
Head common.TomeeHeader `cmd:"2420" struc:"skip"`
}
// ARENA_OWENR_ACCE 定义请求或响应数据结构。
type ARENA_OWENR_ACCE struct {
Head common.TomeeHeader `cmd:"2422" struc:"skip"`
}
// PetKingJoinInboundInfo 定义请求或响应数据结构。
type PetKingJoinInboundInfo struct {
Head common.TomeeHeader `cmd:"2413" struc:"skip"`
Type uint32
FightType uint32
}
// PeakQueueCancelInboundInfo 定义请求或响应数据结构。
type PeakQueueCancelInboundInfo struct {
Head common.TomeeHeader `cmd:"2459" struc:"skip"`
}
// PeakBanPickSubmitInboundInfo 定义请求或响应数据结构。
type PeakBanPickSubmitInboundInfo struct {
Head common.TomeeHeader `cmd:"2460" struc:"skip"`
@@ -62,6 +110,7 @@ type PeakBanPickSubmitInboundInfo struct {
BanCatchTimes []uint32 `json:"banCatchTimes"`
}
// HandleFightInviteInboundInfo 定义请求或响应数据结构。
type HandleFightInviteInboundInfo struct {
Head common.TomeeHeader `cmd:"2403" struc:"skip"`
UserID uint32 `json:"userId" codec:"userId,uint"`
@@ -69,21 +118,25 @@ type HandleFightInviteInboundInfo struct {
Mode uint32 `json:"mode" codec:"mode,uint"`
}
// InviteToFightInboundInfo 定义请求或响应数据结构。
type InviteToFightInboundInfo struct {
Head common.TomeeHeader `cmd:"2401" struc:"skip"`
UserID uint32
Mode uint32
}
// InviteFightCancelInboundInfo 定义请求或响应数据结构。
type InviteFightCancelInboundInfo struct {
Head common.TomeeHeader `cmd:"2402" struc:"skip"`
}
// UseSkillInInfo 定义请求或响应数据结构。
type UseSkillInInfo struct {
Head common.TomeeHeader `cmd:"2405" struc:"skip"`
SkillId uint32
}
// UseSkillAtInboundInfo 定义请求或响应数据结构。
type UseSkillAtInboundInfo struct {
Head common.TomeeHeader `cmd:"7505" struc:"skip"`
SkillId uint32 `json:"skillId"`
@@ -93,21 +146,25 @@ type UseSkillAtInboundInfo struct {
AtkType uint8 `json:"atkType"`
}
// ChangePetInboundInfo 定义请求或响应数据结构。
type ChangePetInboundInfo struct {
Head common.TomeeHeader `cmd:"2407" struc:"skip"`
CatchTime uint32 `json:"catchTime"`
}
// CatchMonsterInboundInfo 定义请求或响应数据结构。
type CatchMonsterInboundInfo struct {
Head common.TomeeHeader `cmd:"2409" struc:"skip"`
CapsuleId uint32 `json:"capsuleId" fieldDescription:"胶囊id" uint:"true"`
}
// LoadPercentInboundInfo 定义请求或响应数据结构。
type LoadPercentInboundInfo struct {
Head common.TomeeHeader `cmd:"2441" struc:"skip"`
Percent uint32 `fieldDescription:"加载百分比"`
}
// UsePetItemInboundInfo 定义请求或响应数据结构。
type UsePetItemInboundInfo struct {
Head common.TomeeHeader `cmd:"2406" struc:"skip"`
CatchTime uint32 `description:"精灵捕获时间" codec:"catchTime"`
@@ -115,6 +172,7 @@ type UsePetItemInboundInfo struct {
Reversed1 uint32 `description:"填充字段 0" codec:"reversed1"`
}
// ChatInfo 定义请求或响应数据结构。
type ChatInfo struct {
Head common.TomeeHeader `cmd:"50002" struc:"skip"`
Reserve uint32 `json:"reserve" fieldDescription:"填充 默认值为0" uint:"true"`
@@ -122,16 +180,19 @@ type ChatInfo struct {
Message string `json:"message" fieldDescription:"消息内容, 结束符为utf-8的数字0"`
}
// C2S_FRESH_CHOICE_FIGHT_LEVEL 定义请求或响应数据结构。
type C2S_FRESH_CHOICE_FIGHT_LEVEL struct {
Head common.TomeeHeader `cmd:"2428|2414" struc:"skip"`
Level uint `json:"level"`
}
// C2S_OPEN_DARKPORTAL 定义请求或响应数据结构。
type C2S_OPEN_DARKPORTAL struct {
Head common.TomeeHeader `cmd:"2424" struc:"skip"`
Level uint32 `json:"level"`
}
// FRESH_LEAVE_FIGHT_LEVEL 定义请求或响应数据结构。
type FRESH_LEAVE_FIGHT_LEVEL struct {
Head common.TomeeHeader `cmd:"2430|2416|2426" struc:"skip"`
}

View File

@@ -2,50 +2,59 @@ package controller
import "blazing/logic/service/common"
// SeeOnlineInboundInfo 定义请求或响应数据结构。
type SeeOnlineInboundInfo struct {
Head common.TomeeHeader `cmd:"2157" struc:"skip"`
UserIdsLen uint32 `json:"userIdsLen" struc:"sizeof=UserIds"`
UserIds []uint32 `json:"userIds" `
}
// FriendAddInboundInfo 定义请求或响应数据结构。
type FriendAddInboundInfo struct {
Head common.TomeeHeader `cmd:"2151" struc:"skip"`
UserID uint32 `json:"userID"`
}
// FriendAnswerInboundInfo 定义请求或响应数据结构。
type FriendAnswerInboundInfo struct {
Head common.TomeeHeader `cmd:"2152" struc:"skip"`
UserID uint32 `json:"userID"`
Flag uint32 `json:"flag"`
}
// FriendRemoveInboundInfo 定义请求或响应数据结构。
type FriendRemoveInboundInfo struct {
Head common.TomeeHeader `cmd:"2153" struc:"skip"`
UserID uint32 `json:"userID"`
}
// AcceptTaskInboundInfo 定义请求或响应数据结构。
type AcceptTaskInboundInfo struct {
Head common.TomeeHeader `cmd:"2201|2231" struc:"skip"`
TaskId uint32 `json:"taskId" description:"任务ID"`
}
// AddTaskBufInboundInfo 定义请求或响应数据结构。
type AddTaskBufInboundInfo struct {
Head common.TomeeHeader `cmd:"2204|2235" struc:"skip"`
TaskId uint32 `json:"taskId" description:"任务ID"`
TaskList []uint32 `struc:"[20]byte"`
}
// CompleteTaskInboundInfo 定义请求或响应数据结构。
type CompleteTaskInboundInfo struct {
Head common.TomeeHeader `cmd:"2202|2233" struc:"skip"`
TaskId uint32 `json:"taskId" description:"任务ID"`
OutState uint32 `json:"outState" `
}
// GetTaskBufInboundInfo 定义请求或响应数据结构。
type GetTaskBufInboundInfo struct {
Head common.TomeeHeader `cmd:"2203|2234" struc:"skip"`
TaskId uint32 `json:"taskId" description:"任务ID"`
}
// DeleteTaskInboundInfo 定义请求或响应数据结构。
type DeleteTaskInboundInfo struct {
Head common.TomeeHeader `cmd:"2205|2232" struc:"skip"`
TaskId uint32 `json:"taskId" description:"任务ID"`

View File

@@ -2,18 +2,21 @@ package controller
import "blazing/logic/service/common"
// BuyInboundInfo 定义请求或响应数据结构。
type BuyInboundInfo struct {
Head common.TomeeHeader `cmd:"2601" struc:"skip"`
ItemId int64 `struc:"uint32"`
Count int64 `struc:"uint32"`
}
// BuyMultiInboundInfo 定义请求或响应数据结构。
type BuyMultiInboundInfo struct {
Head common.TomeeHeader `cmd:"2606" struc:"skip"`
ItemListLen uint32 `struc:"sizeof=ItemIds"`
ItemIds []uint32 `json:"itemIds" description:"购买的物品ID列表"`
}
// C2S_GOLD_BUY_PRODUCT 定义请求或响应数据结构。
type C2S_GOLD_BUY_PRODUCT struct {
Head common.TomeeHeader `cmd:"1104" struc:"skip"`
Type uint32 `json:"type"`
@@ -21,6 +24,7 @@ type C2S_GOLD_BUY_PRODUCT struct {
Count int64 `struc:"uint32"`
}
// ItemListInboundInfo 定义请求或响应数据结构。
type ItemListInboundInfo struct {
Head common.TomeeHeader `cmd:"2605|4475" struc:"skip"`
Param1 uint32
@@ -28,36 +32,43 @@ type ItemListInboundInfo struct {
Param3 uint32
}
// GoldOnlineRemainInboundInfo 定义请求或响应数据结构。
type GoldOnlineRemainInboundInfo struct {
Head common.TomeeHeader `cmd:"1105|1106" struc:"skip"`
}
// ExpTotalRemainInboundInfo 定义请求或响应数据结构。
type ExpTotalRemainInboundInfo struct {
Head common.TomeeHeader `cmd:"2319" struc:"skip"`
}
// ChangePlayerClothInboundInfo 定义请求或响应数据结构。
type ChangePlayerClothInboundInfo struct {
Head common.TomeeHeader `cmd:"2604" struc:"skip"`
ClothesLen uint32 `struc:"sizeof=ClothList" fieldDesc:"穿戴装备的信息" json:"clothes_len"`
ClothList []uint32 `description:"玩家装备列表" codec:"list"`
}
// TalkCountInboundInfo 定义请求或响应数据结构。
type TalkCountInboundInfo struct {
Head common.TomeeHeader `cmd:"2701" struc:"skip"`
ID uint32 `description:"奖品的Type, 即ID" codec:"uint"`
}
// TalkCateInboundInfo 定义请求或响应数据结构。
type TalkCateInboundInfo struct {
Head common.TomeeHeader `cmd:"2702" struc:"skip"`
ID uint32 `description:"奖品的Type, 即ID" codec:"uint"`
}
// C2S_USE_PET_ITEM_OUT_OF_FIGHT 定义请求或响应数据结构。
type C2S_USE_PET_ITEM_OUT_OF_FIGHT struct {
Head common.TomeeHeader `cmd:"2326" struc:"skip"`
CatchTime uint32 `json:"catch_time"`
ItemID int32 `struc:"uint32"`
}
// C2S_PET_RESET_NATURE 定义请求或响应数据结构。
type C2S_PET_RESET_NATURE struct {
Head common.TomeeHeader `cmd:"2343" struc:"skip"`
CatchTime uint32
@@ -65,22 +76,26 @@ type C2S_PET_RESET_NATURE struct {
ItemId uint32
}
// C2S_ITEM_SALE 定义请求或响应数据结构。
type C2S_ITEM_SALE struct {
Head common.TomeeHeader `cmd:"2602" struc:"skip"`
ItemId uint32
Amount uint32
}
// C2S_USE_SPEEDUP_ITEM 定义请求或响应数据结构。
type C2S_USE_SPEEDUP_ITEM struct {
Head common.TomeeHeader `cmd:"2327" struc:"skip"`
ItemID uint32
}
// C2S_USE_ENERGY_XISHOU 定义请求或响应数据结构。
type C2S_USE_ENERGY_XISHOU struct {
Head common.TomeeHeader `cmd:"2331" struc:"skip"`
ItemID uint32
}
// C2S_USE_AUTO_FIGHT_ITEM 定义请求或响应数据结构。
type C2S_USE_AUTO_FIGHT_ITEM struct {
Head common.TomeeHeader `cmd:"2329" struc:"skip"`
ItemID uint32

View File

@@ -5,6 +5,7 @@ import (
"blazing/modules/player/model"
)
// EnterMapInboundInfo 定义请求或响应数据结构。
type EnterMapInboundInfo struct {
Head common.TomeeHeader `cmd:"2001" struc:"skip"`
MapType uint32
@@ -12,22 +13,27 @@ type EnterMapInboundInfo struct {
Point model.Pos `fieldDesc:"直接给坐标xy"`
}
// GetMapHotInboundInfo 定义请求或响应数据结构。
type GetMapHotInboundInfo struct {
Head common.TomeeHeader `cmd:"1004" struc:"skip"`
}
// LeaveMapInboundInfo 定义请求或响应数据结构。
type LeaveMapInboundInfo struct {
Head common.TomeeHeader `cmd:"2002" struc:"skip"`
}
// ListMapPlayerInboundInfo 定义请求或响应数据结构。
type ListMapPlayerInboundInfo struct {
Head common.TomeeHeader `cmd:"2003" struc:"skip"`
}
// AttackBossInboundInfo 定义请求或响应数据结构。
type AttackBossInboundInfo struct {
Head common.TomeeHeader `cmd:"2412" struc:"skip"`
}
// WalkInInfo 定义请求或响应数据结构。
type WalkInInfo struct {
Head common.TomeeHeader `cmd:"2101" struc:"skip"`
Flag uint32
@@ -36,20 +42,24 @@ type WalkInInfo struct {
Path string
}
// FitmentUseringInboundInfo 定义请求或响应数据结构。
type FitmentUseringInboundInfo struct {
Head common.TomeeHeader `cmd:"10006" struc:"skip"`
TargetUserID uint32 `json:"targetUserId"`
}
// PetRoomListInboundInfo 定义请求或响应数据结构。
type PetRoomListInboundInfo struct {
Head common.TomeeHeader `cmd:"2324" struc:"skip"`
TargetUserID uint32 `json:"targetUserId"`
}
// FitmentAllInboundEmpty 定义请求或响应数据结构。
type FitmentAllInboundEmpty struct {
Head common.TomeeHeader `cmd:"10007" struc:"skip"`
}
// SET_FITMENT 定义请求或响应数据结构。
type SET_FITMENT struct {
Head common.TomeeHeader `cmd:"10008" struc:"skip"`
RoomID uint32 `json:"roomID"`
@@ -57,44 +67,52 @@ type SET_FITMENT struct {
Fitments []model.FitmentShowInfo `json:"usedList"`
}
// C2S_PetShowList 定义请求或响应数据结构。
type C2S_PetShowList struct {
CatchTime uint32 `json:"catchTime"`
PetID uint32 `json:"petID"`
}
// C2S_PET_ROOM_SHOW 定义请求或响应数据结构。
type C2S_PET_ROOM_SHOW struct {
Head common.TomeeHeader `cmd:"2323" struc:"skip"`
PetShowInfoLen uint32 `json:"PetShowInfoLen" struc:"sizeof=PetShowList"`
PetShowList []C2S_PetShowList `json:"PetShowList"`
}
// C2S_RoomPetInfo 定义请求或响应数据结构。
type C2S_RoomPetInfo struct {
Head common.TomeeHeader `cmd:"2325" struc:"skip"`
UserID uint32 `json:"userID"`
CatchTime uint32 `json:"catchTime"`
}
// C2S_BUY_FITMENT 定义请求或响应数据结构。
type C2S_BUY_FITMENT struct {
Head common.TomeeHeader `cmd:"10004" struc:"skip"`
ID uint32 `json:"id"`
Count uint32 `json:"count"`
}
// NonoInboundInfo 定义请求或响应数据结构。
type NonoInboundInfo struct {
Head common.TomeeHeader `cmd:"9003" struc:"skip"`
UserID uint32
}
// NonoFollowOrHomeInInfo 定义请求或响应数据结构。
type NonoFollowOrHomeInInfo struct {
Head common.TomeeHeader `cmd:"9019" struc:"skip"`
Flag uint32 `fieldDescription:"1为跟随 0为收回 且如果为收回 那么后续结构不需要发送" uint:"true"`
}
// SwitchFlyingInboundInfo 定义请求或响应数据结构。
type SwitchFlyingInboundInfo struct {
Head common.TomeeHeader `cmd:"2112" struc:"skip"`
Type uint32 `description:"开关, 0为取消飞行模式, 大于0为开启飞行模式" codec:"auto" uint:"true"`
}
// PetCureInboundInfo 定义请求或响应数据结构。
type PetCureInboundInfo struct {
Head common.TomeeHeader `cmd:"2306" struc:"skip"`
}

View File

@@ -2,15 +2,18 @@ package controller
import "blazing/logic/service/common"
// GetPetInfoInboundInfo 定义请求或响应数据结构。
type GetPetInfoInboundInfo struct {
Head common.TomeeHeader `cmd:"2301" struc:"skip"`
CatchTime uint32
}
// GetUserBagPetInfoInboundEmpty 定义请求或响应数据结构。
type GetUserBagPetInfoInboundEmpty struct {
Head common.TomeeHeader `cmd:"4483" struc:"skip"`
}
// SavePetBagOrderInboundInfo 定义请求或响应数据结构。
type SavePetBagOrderInboundInfo struct {
Head common.TomeeHeader `cmd:"4484" struc:"skip"`
@@ -20,51 +23,60 @@ type SavePetBagOrderInboundInfo struct {
BackupPetList []uint32
}
// PetReleaseInboundInfo 定义请求或响应数据结构。
type PetReleaseInboundInfo struct {
Head common.TomeeHeader `cmd:"2304" struc:"skip"`
CatchTime uint32
Flag uint32 `json:"flag" fieldDescription:"0为放入仓库1为放入背包" autoCodec:"true" uint:"true"`
}
// PetShowInboundInfo 定义请求或响应数据结构。
type PetShowInboundInfo struct {
Head common.TomeeHeader `cmd:"2305" struc:"skip"`
CatchTime uint32 `codec:"catchTime" inboundMessageType:"Pet_Show"`
Flag uint32 `codec:"flag"`
}
// PetOneCureInboundInfo 定义请求或响应数据结构。
type PetOneCureInboundInfo struct {
Head common.TomeeHeader `cmd:"2310" struc:"skip"`
CatchTime uint32 `json:"catchTime" fieldDescription:"精灵捕捉时间" uint:"true"`
}
// PET_ROWEI 定义请求或响应数据结构。
type PET_ROWEI struct {
Head common.TomeeHeader `cmd:"2321" struc:"skip"`
ID uint32
CatchTime uint32 `json:"catchTime" fieldDescription:"精灵捕捉时间" uint:"true"`
}
// PET_RETRIEVE 定义请求或响应数据结构。
type PET_RETRIEVE struct {
Head common.TomeeHeader `cmd:"2322" struc:"skip"`
CatchTime uint32 `json:"catchTime" fieldDescription:"精灵捕捉时间" uint:"true"`
}
// PetDefaultInboundInfo 定义请求或响应数据结构。
type PetDefaultInboundInfo struct {
Head common.TomeeHeader `cmd:"2308" struc:"skip"`
CatchTime uint32 `json:"catchTime" fieldDescription:"精灵捕捉时间" uint:"true" autoCodec:"true" inboundMessageType:"Pet_Default"`
}
// PetSetExpInboundInfo 定义请求或响应数据结构。
type PetSetExpInboundInfo struct {
Head common.TomeeHeader `cmd:"2318" struc:"skip"`
CatchTime uint32 `fieldDescription:"精灵获取时间" uint:"true" autoCodec:"true"`
Exp int64 `struc:"uint32"`
}
// PetBargeListInboundInfo 定义请求或响应数据结构。
type PetBargeListInboundInfo struct {
Head common.TomeeHeader `cmd:"2309" struc:"skip"`
StartPetId uint32 `description:"开始精灵id" codec:"startPetId"`
EndPetId uint32 `description:"结束精灵id" codec:"endPetId"`
}
// ChangeSkillInfo 定义请求或响应数据结构。
type ChangeSkillInfo struct {
Head common.TomeeHeader `cmd:"2312" struc:"skip"`
CatchTime uint32 `json:"catchTime"`
@@ -74,12 +86,25 @@ type ChangeSkillInfo struct {
ReplaceSkill uint32 `json:"replaceSkill"`
}
// C2S_Skill_Sort 定义请求或响应数据结构。
type C2S_Skill_Sort struct {
Head common.TomeeHeader `cmd:"2328" struc:"skip"`
CapTm uint32 `json:"capTm"`
Skill [4]uint32 `json:"skill_1"`
}
// GetPetLearnableSkillsInboundInfo 查询当前精灵可学习技能含额外技能ExtSKill
type GetPetLearnableSkillsInboundInfo struct {
Head common.TomeeHeader `cmd:"52312" struc:"skip"`
CatchTime uint32 `json:"catchTime"`
}
type CommitPetSkillsInboundInfo struct {
Head common.TomeeHeader `cmd:"52313" struc:"skip"`
CatchTime uint32 `json:"catchTime"`
Skill [4]uint32 `json:"skill"`
}
type C2S_PetFusion struct {
Head common.TomeeHeader `cmd:"2351" struc:"skip"`
Mcatchtime uint32 `json:"mcatchtime" msgpack:"mcatchtime"`

View File

@@ -10,11 +10,13 @@ import (
"hash/crc32"
)
// MAIN_LOGIN_IN 定义请求或响应数据结构。
type MAIN_LOGIN_IN struct {
Head common.TomeeHeader `cmd:"1001" struc:"skip"`
Sid []byte `struc:"[16]byte"`
}
// CheakSession 校验登录session。
func (l *MAIN_LOGIN_IN) CheakSession() (bool, uint32) {
t1 := hex.EncodeToString(l.Sid)
r, err := cool.CacheManager.Get(context.Background(), fmt.Sprintf("session:%d", l.Head.UserID))
@@ -30,16 +32,19 @@ func (l *MAIN_LOGIN_IN) CheakSession() (bool, uint32) {
return true, crcValue
}
// SimUserInfoInboundInfo 定义请求或响应数据结构。
type SimUserInfoInboundInfo struct {
Head common.TomeeHeader `cmd:"2051" struc:"skip"`
UserId uint32 `fieldDescription:"米米号" uint:"true" codec:"true"`
}
// MoreUserInfoInboundInfo 定义请求或响应数据结构。
type MoreUserInfoInboundInfo struct {
Head common.TomeeHeader `cmd:"2052" struc:"skip"`
UserId uint32 `fieldDescription:"米米号" uint:"true" codec:"true"`
}
// AimatInboundInfo 定义请求或响应数据结构。
type AimatInboundInfo struct {
Head common.TomeeHeader `cmd:"2104" struc:"skip"`
ItemId uint32 `description:"物品id 射击激光 物品id为0" codec:"auto" uint:"true"`
@@ -47,6 +52,7 @@ type AimatInboundInfo struct {
Point model.Pos `description:"射击的坐标 x y" codec:"auto"`
}
// ChatInboundInfo 定义请求或响应数据结构。
type ChatInboundInfo struct {
Head common.TomeeHeader `cmd:"2102" struc:"skip"`
Reserve uint32 `json:"reserve" fieldDescription:"填充 默认值为0" uint:"true"`
@@ -54,43 +60,51 @@ type ChatInboundInfo struct {
Message string `json:"message" fieldDescription:"消息内容, 结束符为utf-8的数字0"`
}
// ChangeColorInboundInfo 定义请求或响应数据结构。
type ChangeColorInboundInfo struct {
Head common.TomeeHeader `cmd:"2063" struc:"skip"`
Color uint32 `codec:"color"`
}
// ChangeDoodleInboundInfo 定义请求或响应数据结构。
type ChangeDoodleInboundInfo struct {
Head common.TomeeHeader `cmd:"2062" struc:"skip"`
Id uint32 `codec:"id"`
Color uint32 `codec:"color"`
}
// ChangeNONOColorInboundInfo 定义请求或响应数据结构。
type ChangeNONOColorInboundInfo struct {
Head common.TomeeHeader `cmd:"9012" struc:"skip"`
Color uint32 `codec:"color"`
}
// C2SDanceAction 定义请求或响应数据结构。
type C2SDanceAction struct {
Head common.TomeeHeader `cmd:"2103" struc:"skip"`
Reserve uint32 `struc:"uint32,big"`
Type uint32 `struc:"uint32,big"`
}
// C2SPEOPLE_TRANSFROM 定义请求或响应数据结构。
type C2SPEOPLE_TRANSFROM struct {
Head common.TomeeHeader `cmd:"2111" struc:"skip"`
SuitID uint32 `struc:"uint32,big"`
}
// ChangePlayerNameInboundInfo 定义请求或响应数据结构。
type ChangePlayerNameInboundInfo struct {
Head common.TomeeHeader `cmd:"2061" struc:"skip"`
Nickname string `struc:"[16]byte"`
}
// ChangeTitleInboundInfo 定义请求或响应数据结构。
type ChangeTitleInboundInfo struct {
Head common.TomeeHeader `cmd:"3404" struc:"skip"`
TileID uint32
}
// C2S_GET_GIFT_COMPLETE 定义请求或响应数据结构。
type C2S_GET_GIFT_COMPLETE struct {
Head common.TomeeHeader `cmd:"2801" struc:"skip"`
PassText string `struc:"[16]byte"`

View File

@@ -18,10 +18,13 @@ func (h Controller) ItemSale(data *C2S_ITEM_SALE, c *player.Player) (result *fig
return nil, errorcode.ErrorCodes.ErrSystemError
}
if err := c.Service.Item.UPDATE(data.ItemId, -gconv.Int(data.Amount)); err != nil {
return nil, errorcode.ErrorCodes.ErrInsufficientItems
}
itemConfig := xmlres.ItemsMAP[int(data.ItemId)]
if itemConfig.SellPrice != 0 {
c.Info.Coins += int64(int64(data.Amount) * int64(itemConfig.SellPrice))
}
c.Service.Item.UPDATE(data.ItemId, -gconv.Int(data.Amount))
return result, 0
}

View File

@@ -15,6 +15,8 @@ import (
const (
// ItemDefaultLeftTime 道具默认剩余时间(毫秒)
ItemDefaultLeftTime = 360000
// UniversalNatureItemID 全能性格转化剂Ω
UniversalNatureItemID uint32 = 300136
)
// GetUserItemList 获取用户道具列表
@@ -33,22 +35,38 @@ func (h Controller) GetUserItemList(data *ItemListInboundInfo, c *player.Player)
// c: 当前玩家对象
// 返回: 使用后的宠物信息和错误码
func (h Controller) UsePetItemOutOfFight(data *C2S_USE_PET_ITEM_OUT_OF_FIGHT, c *player.Player) (result *item.S2C_USE_PET_ITEM_OUT_OF_FIGHT, err errorcode.ErrorCode) {
_, currentPet, found := c.FindPet(data.CatchTime)
slot, found := c.FindPetBagSlot(data.CatchTime)
if !found {
return nil, errorcode.ErrorCodes.Err10401
}
currentPet := slot.PetInfoPtr()
if currentPet == nil {
return nil, errorcode.ErrorCodes.Err10401
}
itemID := uint32(data.ItemID)
if c.Service.Item.CheakItem(itemID) == 0 {
return nil, errorcode.ErrorCodes.ErrInsufficientItems
}
oldHP := currentPet.Hp
itemCfg, ok := xmlres.ItemsMAP[int(itemID)]
if !ok {
return nil, errorcode.ErrorCodes.ErrSystemError
errcode := h.handleRegularPetItem(itemID, currentPet)
if errcode != 0 {
return nil, errcode
}
refreshPetPaneKeepHP(currentPet, oldHP)
if err := c.Service.Item.UPDATE(itemID, -1); err != nil {
return nil, errorcode.ErrorCodes.ErrInsufficientItems
}
c.Service.Info.Save(*c.Info)
result = &item.S2C_USE_PET_ITEM_OUT_OF_FIGHT{}
copier.Copy(&result, currentPet)
return result, 0
}
oldHP := currentPet.Hp
var errcode errorcode.ErrorCode
switch {
case itemID == 300036:
@@ -75,7 +93,10 @@ func (h Controller) UsePetItemOutOfFight(data *C2S_USE_PET_ITEM_OUT_OF_FIGHT, c
return nil, errcode
}
c.Service.Item.UPDATE(itemID, -1)
if err := c.Service.Item.UPDATE(itemID, -1); err != nil {
return nil, errorcode.ErrorCodes.ErrInsufficientItems
}
c.Service.Info.Save(*c.Info)
result = &item.S2C_USE_PET_ITEM_OUT_OF_FIGHT{}
copier.Copy(&result, currentPet)
return result, 0
@@ -118,7 +139,9 @@ func (h Controller) handlexuancaiItem(currentPet *model.PetInfo, c *player.Playe
return errorcode.ErrorCodes.ErrItemUnusable
}
c.Service.Item.UPDATE(itemid, -100)
if err := c.Service.Item.UPDATE(itemid, -100); err != nil {
return errorcode.ErrorCodes.ErrInsufficientItems
}
return 0
}
@@ -166,14 +189,7 @@ func refreshPetPaneKeepHP(currentPet *model.PetInfo, hp uint32) {
// handleRegularPetItem 处理普通宠物道具
func (h Controller) handleRegularPetItem(itemID uint32, currentPet *model.PetInfo) errorcode.ErrorCode {
handler := item.PetItemRegistry.GetHandler(itemID)
if handler == nil {
return errorcode.ErrorCodes.ErrItemUnusable
}
if !handler(itemID, currentPet) {
return errorcode.ErrorCodes.ErrItemUnusable
}
return 0
return item.PetItemRegistry.Handle(itemID, currentPet)
}
// ResetNature 重置宠物性格
@@ -181,11 +197,24 @@ func (h Controller) handleRegularPetItem(itemID uint32, currentPet *model.PetInf
// c: 当前玩家对象
// 返回: 无数据和错误码
func (h Controller) ResetNature(data *C2S_PET_RESET_NATURE, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
_, currentPet, found := c.FindPet(data.CatchTime)
slot, found := c.FindPetBagSlot(data.CatchTime)
if !found {
return nil, errorcode.ErrorCodes.Err10401
}
currentPet := slot.PetInfoPtr()
if currentPet == nil {
return nil, errorcode.ErrorCodes.Err10401
}
if data.ItemId != UniversalNatureItemID {
return nil, errorcode.ErrorCodes.ErrItemUnusable
}
if _, ok := xmlres.NatureRootMap[int(data.Nature)]; !ok {
return nil, errorcode.ErrorCodes.ErrItemUnusable
}
if c.Service.Item.CheakItem(data.ItemId) <= 0 {
return nil, errorcode.ErrorCodes.ErrInsufficientItems
}
@@ -193,7 +222,10 @@ func (h Controller) ResetNature(data *C2S_PET_RESET_NATURE, c *player.Player) (r
currentHP := currentPet.Hp
currentPet.Nature = data.Nature
refreshPetPaneKeepHP(currentPet, currentHP)
c.Service.Item.UPDATE(data.ItemId, -1)
if err := c.Service.Item.UPDATE(data.ItemId, -1); err != nil {
return nil, errorcode.ErrorCodes.ErrInsufficientItems
}
c.Service.Info.Save(*c.Info)
return result, 0
}
@@ -221,29 +253,38 @@ func (h Controller) UseSpeedupItem(data *C2S_USE_SPEEDUP_ITEM, c *player.Player)
if c.Info.TwoTimes != 0 {
return nil, errorcode.ErrorCodes.ErrItemInUse
}
c.Info.TwoTimes += 50 // 玩家对象新增 TwoTimesExp 字段存储双倍剩余次数
case 300067:
if c.Info.TwoTimes != 0 {
return nil, errorcode.ErrorCodes.ErrItemInUse
}
c.Info.TwoTimes += 25 // 玩家对象新增 TwoTimesExp 字段存储双倍剩余次数
case 300051: // 假设1002是三倍经验加速器道具ID
if c.Info.ThreeTimes != 0 {
return nil, errorcode.ErrorCodes.ErrItemInUse
}
c.Info.ThreeTimes += 50 // 玩家对象新增 ThreeTimesExp 字段存储三倍剩余次数
case 300115:
if c.Info.ThreeTimes != 0 {
return nil, errorcode.ErrorCodes.ErrItemInUse
}
c.Info.ThreeTimes += 30 // 玩家对象新增 ThreeTimesExp 字段存储三倍剩余次数
default:
return nil, errorcode.ErrorCodes.ErrSystemError // 未知道具ID
}
// 3. 扣减道具(数量-1
c.Service.Item.UPDATE(data.ItemID, -1)
if err := c.Service.Item.UPDATE(data.ItemID, -1); err != nil {
return nil, errorcode.ErrorCodes.ErrInsufficientItems
}
switch data.ItemID {
case 300027: // 假设1001是双倍经验加速器道具ID
c.Info.TwoTimes += 50 // 玩家对象新增 TwoTimesExp 字段存储双倍剩余次数
case 300067:
c.Info.TwoTimes += 25 // 玩家对象新增 TwoTimesExp 字段存储双倍剩余次数
case 300051: // 假设1002是三倍经验加速器道具ID
c.Info.ThreeTimes += 50 // 玩家对象新增 ThreeTimesExp 字段存储三倍剩余次数
case 300115:
c.Info.ThreeTimes += 30 // 玩家对象新增 ThreeTimesExp 字段存储三倍剩余次数
}
result.ThreeTimes = uint32(c.Info.ThreeTimes) // 返回三倍经验剩余次数
result.TwoTimes = uint32(c.Info.TwoTimes) // 返回双倍经验剩余次数
@@ -274,10 +315,11 @@ func (h Controller) UseEnergyXishou(data *C2S_USE_ENERGY_XISHOU, c *player.Playe
}
// 2. 核心业务逻辑:更新能量吸收器剩余次数
// 可根据道具ID配置不同的次数加成此处默认+1
if err := c.Service.Item.UPDATE(data.ItemID, -1); err != nil {
return nil, errorcode.ErrorCodes.ErrInsufficientItems
}
c.Info.EnergyTime += 40 // 玩家对象新增 EnergyTimes 字段存储能量吸收剩余次数
// 3. 扣减道具(数量-1
c.Service.Item.UPDATE(data.ItemID, -1)
result = &item.S2C_USE_ENERGY_XISHOU{
EnergyTimes: uint32(c.Info.EnergyTime),
}
@@ -308,6 +350,9 @@ func (h Controller) UseAutoFightItem(data *C2S_USE_AUTO_FIGHT_ITEM, c *player.Pl
if c.Info.AutoFightTime != 0 {
return nil, errorcode.ErrorCodes.ErrItemInUse
}
if err := c.Service.Item.UPDATE(data.ItemID, -1); err != nil {
return nil, errorcode.ErrorCodes.ErrInsufficientItems
}
result = &item.S2C_USE_AUTO_FIGHT_ITEM{}
// 2. 核心业务逻辑:开启自动战斗 + 更新剩余次数
c.Info.AutoFight = 3 // 按需求设置自动战斗flag为3需测试
@@ -323,8 +368,6 @@ func (h Controller) UseAutoFightItem(data *C2S_USE_AUTO_FIGHT_ITEM, c *player.Pl
c.Info.AutoFightTime += 50
}
result.AutoFightTimes = c.Info.AutoFightTime
// 3. 扣减道具(数量-1
c.Service.Item.UPDATE(data.ItemID, -1)
return result, 0
}

View File

@@ -0,0 +1,60 @@
package controller
import (
"blazing/common/data/xmlres"
"blazing/logic/service/player"
playermodel "blazing/modules/player/model"
blservice "blazing/modules/player/service"
"testing"
)
func TestUsePetItemOutOfFightAppliesToBackupPetInMemory(t *testing.T) {
petID := firstPetIDForControllerTest(t)
backupPet := playermodel.GenPetInfo(petID, 31, 0, 0, 50, nil, 0)
if backupPet == nil {
t.Fatal("failed to generate backup pet")
}
if backupPet.MaxHp <= 1 {
t.Fatalf("expected generated pet to have max hp > 1, got %d", backupPet.MaxHp)
}
backupPet.Hp = 1
testPlayer := player.NewPlayer(nil)
testPlayer.Info = &playermodel.PlayerInfo{
UserID: 1,
PetList: []playermodel.PetInfo{},
BackupPetList: []playermodel.PetInfo{*backupPet},
}
testPlayer.Service = blservice.NewUserService(testPlayer.Info.UserID)
itemID, recoverHP := firstRecoverHPItemForControllerTest(t)
if recoverHP <= 0 {
t.Fatalf("expected positive recover hp for item %d, got %d", itemID, recoverHP)
}
_, err := (Controller{}).UsePetItemOutOfFight(&C2S_USE_PET_ITEM_OUT_OF_FIGHT{
CatchTime: backupPet.CatchTime,
ItemID: int32(itemID),
}, testPlayer)
if err != 0 {
t.Fatalf("expected backup pet item use to succeed in-memory, got err=%d", err)
}
updatedPet := testPlayer.Info.BackupPetList[0]
if updatedPet.Hp <= 1 {
t.Fatalf("expected backup pet hp to increase in memory, got hp=%d", updatedPet.Hp)
}
}
func firstRecoverHPItemForControllerTest(t *testing.T) (uint32, int) {
t.Helper()
for id, cfg := range xmlres.ItemsMAP {
if cfg.HP > 0 {
return uint32(id), cfg.HP
}
}
t.Fatal("xmlres.ItemsMAP has no HP recovery item")
return 0, 0
}

View File

@@ -2,14 +2,11 @@ package controller
import (
"blazing/common/data/share"
"blazing/cool"
"blazing/common/socket/errorcode"
"blazing/logic/service/user"
"blazing/cool"
"blazing/logic/service/player"
"blazing/logic/service/space"
"blazing/logic/service/user"
"blazing/modules/player/service"
"context"
"time"
@@ -17,6 +14,32 @@ import (
"github.com/panjf2000/gnet/v2"
)
const (
waitUserOfflineTimeout = 30 * time.Second
waitUserOfflineInterval = 200 * time.Millisecond
waitUserOfflineKickGap = 5 * time.Second
)
func waitUserOffline(userID uint32, timeout time.Duration) bool {
deadline := time.Now().Add(timeout)
lastKickAt := time.Now()
for {
if _, onlineErr := share.ShareManager.GetUserOnline(userID); onlineErr != nil {
return true
}
if time.Now().After(deadline) {
return false
}
if time.Since(lastKickAt) >= waitUserOfflineKickGap {
if kickErr := Maincontroller.RPCClient.Kick(userID); kickErr != nil {
cool.Logger.Error(context.Background(), "补踢失败", userID, kickErr)
}
lastKickAt = time.Now()
}
time.Sleep(waitUserOfflineInterval)
}
}
// Login 处理命令: 1001
func (h Controller) Login(data *MAIN_LOGIN_IN, c gnet.Conn) (result *user.LoginMSInfo, err errorcode.ErrorCode) { //这个时候player应该是空的
@@ -30,12 +53,18 @@ func (h Controller) Login(data *MAIN_LOGIN_IN, c gnet.Conn) (result *user.LoginM
defer c.Close()
return
}
_, erre := share.ShareManager.GetUserOnline(data.Head.UserID)
if erre == nil {
error := Maincontroller.RPCClient.Kick(data.Head.UserID) //通知其他服务器踢人
if error != nil {
cool.Logger.Error(context.Background(), "踢人失败", err)
if onlineServerID, onlineErr := share.ShareManager.GetUserOnline(data.Head.UserID); onlineErr == nil {
kickErr := Maincontroller.RPCClient.Kick(data.Head.UserID) //通知其他服务器踢人
if kickErr != nil {
cool.Logger.Error(context.Background(), "踢人失败", data.Head.UserID, onlineServerID, kickErr)
err = errorcode.ErrorCodes.ErrSystemBusyTryLater
defer c.Close()
return
}
if ok := waitUserOffline(data.Head.UserID, waitUserOfflineTimeout); !ok {
cool.Logger.Error(context.Background(), "等待旧会话下线超时", data.Head.UserID, onlineServerID, waitUserOfflineTimeout)
err = errorcode.ErrorCodes.ErrSystemBusyTryLater
defer c.Close()
return
}

View File

@@ -39,6 +39,7 @@ func (h Controller) EnterMap(data *EnterMapInboundInfo, c *player.Player) (resul
return nil, -1
}
// GetMapHot 处理控制器请求。
func (h Controller) GetMapHot(data *GetMapHotInboundInfo, c *player.Player) (result *maphot.OutInfo, err errorcode.ErrorCode) {
result = &maphot.OutInfo{
HotInfos: space.GetMapHot(),

View File

@@ -14,6 +14,7 @@ const (
nonoPetCureCost int64 = 50
)
// NonoFollowOrHome 处理控制器请求。
func (h Controller) NonoFollowOrHome(data *NonoFollowOrHomeInInfo, c *player.Player) (result *nono.NonoFollowOutInfo, err errorcode.ErrorCode) { //这个时候player应该是空的
c.Info.NONO.Flag = data.Flag
result = &nono.NonoFollowOutInfo{
@@ -49,6 +50,7 @@ func (h *Controller) GetNonoInfo(data *NonoInboundInfo, c *player.Player) (resul
return
}
// SwitchFlying 处理控制器请求。
func (h *Controller) SwitchFlying(data *SwitchFlyingInboundInfo, c *player.Player) (result *nono.SwitchFlyingOutboundInfo, err errorcode.ErrorCode) { //这个时候player应该是空的
result = &nono.SwitchFlyingOutboundInfo{
UserId: data.Head.UserID,
@@ -59,8 +61,10 @@ func (h *Controller) SwitchFlying(data *SwitchFlyingInboundInfo, c *player.Playe
return
}
// PlayerPetCure 处理控制器请求。
func (h *Controller) PlayerPetCure(data *PetCureInboundInfo, c *player.Player) (result *nono.PetCureOutboundEmpty, err errorcode.ErrorCode) { //这个时候player应该是空的
_ = data
result = &nono.PetCureOutboundEmpty{}
if c.IsArenaHealLocked() {
return result, errorcode.ErrorCodes.ErrChampionCannotHeal
}
@@ -70,6 +74,9 @@ func (h *Controller) PlayerPetCure(data *PetCureInboundInfo, c *player.Player) (
for i := range c.Info.PetList {
c.Info.PetList[i].Cure()
}
for i := range c.Info.BackupPetList {
c.Info.BackupPetList[i].Cure()
}
c.Info.Coins -= nonoPetCureCost
return
}

View File

@@ -19,20 +19,13 @@ func (h Controller) SavePetBagOrder(
return nil, 0
}
// PetRetrieveFromWarehouse 领回仓库精灵
// PetRetrieveFromWarehouse 从放生仓库领回精灵
func (h Controller) PetRetrieveFromWarehouse(
data *PET_RETRIEVE, player *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
if _, ok := player.FindPetBagSlot(data.CatchTime); ok {
return nil, 0
if !player.Service.Pet.UpdateFree(data.CatchTime, 1, 0) {
return nil, errorcode.ErrorCodes.ErrPokemonIDMismatch
}
petInfo := player.Service.Pet.PetInfoOneByCatchTime(data.CatchTime)
if petInfo == nil {
return nil, 0
}
player.AddPetToAvailableBag(petInfo.Data)
return nil, 0
}

View File

@@ -12,6 +12,7 @@ import (
"github.com/jinzhu/copier"
)
// PetELV 处理控制器请求。
func (h Controller) PetELV(data *C2S_PET_EVOLVTION, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
_, currentPet, found := c.FindPet(data.CacthTime)
if !found {
@@ -36,7 +37,9 @@ func (h Controller) PetELV(data *C2S_PET_EVOLVTION, c *player.Player) (result *f
return nil, errorcode.ErrorCodes.ErrInsufficientItemsMulti
}
if branch.EvolvItem != 0 {
c.Service.Item.UPDATE(uint32(branch.EvolvItem), -branch.EvolvItemCount)
if err := c.Service.Item.UPDATE(uint32(branch.EvolvItem), -branch.EvolvItemCount); err != nil {
return nil, errorcode.ErrorCodes.ErrInsufficientItemsMulti
}
}
currentPet.ID = uint32(branch.MonTo)

View File

@@ -17,11 +17,16 @@ const (
// c: 当前玩家对象
// 返回: 分配结果和错误码
func (h Controller) PetEVDiy(data *PetEV, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
_, currentPet, found := c.FindPet(data.CacthTime)
slot, found := c.FindPetBagSlot(data.CacthTime)
if !found {
return nil, errorcode.ErrorCodes.Err10401
}
currentPet := slot.PetInfoPtr()
if currentPet == nil {
return nil, errorcode.ErrorCodes.Err10401
}
var targetTotal uint32
var currentTotal uint32
for i, evValue := range data.EVs {
@@ -63,6 +68,7 @@ func (h Controller) PetEVDiy(data *PetEV, c *player.Player) (result *fight.NullO
return result, 0
}
// PetEV 定义请求或响应数据结构。
type PetEV struct {
Head common.TomeeHeader `cmd:"50001" struc:"skip"`
CacthTime uint32 `description:"捕捉时间" codec:"cacthTime"`

View File

@@ -0,0 +1,45 @@
package controller
import (
"testing"
"blazing/logic/service/player"
playermodel "blazing/modules/player/model"
)
func TestPetEVDiy_AppliesToBackupPet(t *testing.T) {
p := player.NewPlayer(nil)
p.Info = &playermodel.PlayerInfo{
EVPool: 20,
PetList: []playermodel.PetInfo{
{CatchTime: 1},
},
BackupPetList: []playermodel.PetInfo{
{
CatchTime: 2,
Level: 100,
Ev: [6]uint32{0, 4, 0, 0, 0, 0},
},
},
}
data := &PetEV{
CacthTime: 2,
EVs: [6]uint32{0, 8, 4, 0, 0, 0},
}
_, err := (Controller{}).PetEVDiy(data, p)
if err != 0 {
t.Fatalf("PetEVDiy returned error: %v", err)
}
got := p.Info.BackupPetList[0].Ev
want := [6]uint32{0, 8, 4, 0, 0, 0}
if got != want {
t.Fatalf("backup pet EV mismatch, got %v want %v", got, want)
}
if gotPool, wantPool := p.Info.EVPool, int64(12); gotPool != wantPool {
t.Fatalf("EVPool mismatch, got %d want %d", gotPool, wantPool)
}
}

View File

@@ -16,6 +16,7 @@ func (h Controller) PetExt(
}
// C2S_NONO_EXE_LIST 定义请求或响应数据结构。
type C2S_NONO_EXE_LIST struct {
Head common.TomeeHeader `cmd:"9015" struc:"skip"`
}

View File

@@ -19,6 +19,7 @@ const (
petFusionSoulID = 1000017
)
// PetFusion 处理控制器请求。
func (h Controller) PetFusion(data *C2S_PetFusion, c *player.Player) (result *pet.PetFusionInfo, err errorcode.ErrorCode) {
result = &pet.PetFusionInfo{
SoulID: petFusionSoulID,
@@ -64,16 +65,33 @@ func (h Controller) PetFusion(data *C2S_PetFusion, c *player.Player) (result *pe
return result, errorcode.ErrorCodes.ErrSunDouInsufficient10016
}
consumeItems(c, materialCounts)
c.Info.Coins -= petFusionCost
if resultPetID == 0 {
if useOptionalItem(c, data.GoldItem1[:], petFusionFailureItemID) {
result.CostItemFlag = 1
} else if auxPet.Level > 5 {
auxPet.Downgrade(auxPet.Level - 5)
failedAux := *auxPet
if auxPet.Level > 5 {
failedAux.Downgrade(auxPet.Level - 5)
} else {
auxPet.Downgrade(1)
failedAux.Downgrade(1)
}
txResult, errCode := c.Service.PetFusionTx(
*c.Info,
data.Mcatchtime,
data.Auxcatchtime,
materialCounts,
data.GoldItem1[:],
petFusionKeepAuxItemID,
petFusionFailureItemID,
petFusionCost,
nil,
&failedAux,
)
if errCode != 0 {
return result, errCode
}
c.Info.Coins -= petFusionCost
if txResult.CostItemUsed {
result.CostItemFlag = 1
} else if txResult.UpdatedAux != nil {
*auxPet = *txResult.UpdatedAux
}
return &pet.PetFusionInfo{}, 0
}
@@ -100,18 +118,37 @@ func (h Controller) PetFusion(data *C2S_PetFusion, c *player.Player) (result *pe
newPet.RandomByWeightShiny()
}
c.Service.Pet.PetAdd(newPet, 0)
//println(c.Info.UserID, "进行融合", len(c.Info.PetList), masterPet.ID, auxPet.ID, newPet.ID)
c.PetDel(data.Mcatchtime)
if useOptionalItem(c, data.GoldItem1[:], petFusionKeepAuxItemID) {
result.CostItemFlag = 1
} else {
c.PetDel(data.Auxcatchtime)
txResult, errCode := c.Service.PetFusionTx(
*c.Info,
data.Mcatchtime,
data.Auxcatchtime,
materialCounts,
data.GoldItem1[:],
petFusionKeepAuxItemID,
petFusionFailureItemID,
petFusionCost,
newPet,
nil,
)
if errCode != 0 {
return result, errCode
}
result.ObtainTime = newPet.CatchTime
result.StarterCpTm = newPet.ID
c.Info.Coins -= petFusionCost
if txResult.CostItemUsed {
result.CostItemFlag = 1
} else {
removePetFromPlayerInfo(c, data.Auxcatchtime)
}
removePetFromPlayerInfo(c, data.Mcatchtime)
if txResult.NewPet == nil {
return result, errorcode.ErrorCodes.ErrSystemError
}
c.Info.PetList = append(c.Info.PetList, *txResult.NewPet)
result.ObtainTime = txResult.NewPet.CatchTime
result.StarterCpTm = txResult.NewPet.ID
return result, 0
}
@@ -148,21 +185,10 @@ func hasEnoughItems(c *player.Player, itemCounts map[uint32]int) bool {
return true
}
func consumeItems(c *player.Player, itemCounts map[uint32]int) {
for itemID, count := range itemCounts {
_ = c.Service.Item.UPDATE(itemID, -count)
func removePetFromPlayerInfo(c *player.Player, catchTime uint32) {
index, _, ok := c.FindPet(catchTime)
if !ok {
return
}
}
func useOptionalItem(c *player.Player, itemIDs []uint32, target uint32) bool {
if c.Service.Item.CheakItem(target) <= 0 {
return false
}
for _, itemID := range itemIDs {
if itemID == target {
_ = c.Service.Item.UPDATE(target, -1)
return true
}
}
return false
c.Info.PetList = append(c.Info.PetList[:index], c.Info.PetList[index+1:]...)
}

View File

@@ -4,19 +4,22 @@ import (
"blazing/common/socket/errorcode"
"blazing/logic/service/common"
"blazing/logic/service/pet"
"blazing/logic/service/player"
playersvc "blazing/logic/service/player"
"blazing/modules/player/model"
)
// GetPetInfo 获取精灵信息
func (h Controller) GetPetInfo(
data *GetPetInfoInboundInfo,
player *player.Player) (result *model.PetInfo,
player *playersvc.Player) (result *model.PetInfo,
err errorcode.ErrorCode) {
_, petInfo, found := player.FindPet(data.CatchTime)
if found {
result = petInfo
return result, 0
levelLimit := player.CurrentMapPetLevelLimit()
if slot, found := player.FindPetBagSlot(data.CatchTime); found {
if petInfo := slot.PetInfoPtr(); petInfo != nil {
petCopy := playersvc.ApplyPetLevelLimit(*petInfo, levelLimit)
result = &petCopy
return result, 0
}
}
ret := player.Service.Pet.PetInfoOneByCatchTime(data.CatchTime)
@@ -24,18 +27,21 @@ func (h Controller) GetPetInfo(
return nil, errorcode.ErrorCodes.ErrPokemonNotExists
}
result = &ret.Data
petData := ret.Data
petData = playersvc.ApplyPetLevelLimit(petData, levelLimit)
result = &petData
return result, 0
}
// GetUserBagPetInfo 获取主背包和并列备用精灵列表
func (h Controller) GetUserBagPetInfo(
data *GetUserBagPetInfoInboundEmpty,
player *player.Player) (result *pet.GetUserBagPetInfoOutboundInfo,
player *playersvc.Player) (result *pet.GetUserBagPetInfoOutboundInfo,
err errorcode.ErrorCode) {
return player.GetUserBagPetInfo(), 0
return player.GetUserBagPetInfo(player.CurrentMapPetLevelLimit()), 0
}
// GetPetListInboundEmpty 定义请求或响应数据结构。
type GetPetListInboundEmpty struct {
Head common.TomeeHeader `cmd:"2303" struc:"skip"`
}
@@ -43,11 +49,12 @@ type GetPetListInboundEmpty struct {
// GetPetList 获取当前主背包列表
func (h Controller) GetPetList(
data *GetPetListInboundEmpty,
player *player.Player) (result *pet.GetPetListOutboundInfo,
player *playersvc.Player) (result *pet.GetPetListOutboundInfo,
err errorcode.ErrorCode) {
return buildPetListOutboundInfo(player.Info.PetList), 0
}
// GetPetListFreeInboundEmpty 定义请求或响应数据结构。
type GetPetListFreeInboundEmpty struct {
Head common.TomeeHeader `cmd:"2320" struc:"skip"`
}
@@ -55,7 +62,7 @@ type GetPetListFreeInboundEmpty struct {
// GetPetReleaseList 获取仓库可放生列表
func (h Controller) GetPetReleaseList(
data *GetPetListFreeInboundEmpty,
player *player.Player) (result *pet.GetPetListOutboundInfo,
player *playersvc.Player) (result *pet.GetPetListOutboundInfo,
err errorcode.ErrorCode) {
return buildPetListOutboundInfo(player.WarehousePetList()), 0
@@ -64,14 +71,13 @@ func (h Controller) GetPetReleaseList(
// PlayerShowPet 精灵展示
func (h Controller) PlayerShowPet(
data *PetShowInboundInfo,
player *player.Player) (result *pet.PetShowOutboundInfo, err errorcode.ErrorCode) {
player *playersvc.Player) (result *pet.PetShowOutboundInfo, err errorcode.ErrorCode) {
result = &pet.PetShowOutboundInfo{
UserID: data.Head.UserID,
CatchTime: data.CatchTime,
Flag: data.Flag,
}
_, currentPet, ok := player.FindPet(data.CatchTime)
if data.Flag == 0 {
player.SetPetDisplay(0, nil)
player.GetSpace().RefreshUserInfo(player)
@@ -79,10 +85,16 @@ func (h Controller) PlayerShowPet(
return
}
slot, ok := player.FindPetBagSlot(data.CatchTime)
if !ok {
return nil, errorcode.ErrorCodes.ErrPokemonNotExists
}
currentPet := slot.PetInfoPtr()
if currentPet == nil {
return nil, errorcode.ErrorCodes.ErrPokemonNotExists
}
player.SetPetDisplay(data.Flag, currentPet)
player.GetSpace().RefreshUserInfo(player)
result = buildPetShowOutboundInfo(data.Head.UserID, data.Flag, currentPet)

View File

@@ -6,8 +6,39 @@ import (
"blazing/logic/service/fight"
"blazing/logic/service/pet"
"blazing/logic/service/player"
playermodel "blazing/modules/player/model"
)
func petSetExpLimit(currentPet *playermodel.PetInfo) int64 {
if currentPet == nil || currentPet.Level >= 100 {
return 0
}
simulatedPet := *currentPet
allowedExp := simulatedPet.NextLvExp - simulatedPet.Exp
if allowedExp < 0 {
allowedExp = 0
}
for simulatedPet.Level < 100 && simulatedPet.NextLvExp > 0 {
simulatedPet.Level++
simulatedPet.Update(true)
if simulatedPet.Level >= 100 {
break
}
allowedExp += simulatedPet.NextLvExp
}
return allowedExp
}
func minInt64(a, b int64) int64 {
if a < b {
return a
}
return b
}
// PetReleaseToWarehouse 将精灵从仓库包中放生
func (h Controller) PetReleaseToWarehouse(
data *PET_ROWEI, player *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
@@ -17,9 +48,8 @@ func (h Controller) PetReleaseToWarehouse(
if inBag || inBackup || freeForbidden == 1 {
return nil, errorcode.ErrorCodes.ErrCannotReleaseNonWarehouse
}
if !player.Service.Pet.UpdateFree(data.CatchTime, 1) {
return nil, errorcode.ErrorCodes.ErrSystemError
if !player.Service.Pet.UpdateFree(data.CatchTime, 0, 1) {
return nil, errorcode.ErrorCodes.ErrCannotReleaseNonWarehouse
}
return nil, 0
@@ -32,9 +62,11 @@ func (h Controller) PetOneCure(
return result, errorcode.ErrorCodes.ErrChampionCannotHeal
}
_, currentPet, ok := player.FindPet(data.CatchTime)
if ok {
defer currentPet.Cure()
if slot, ok := player.FindPetBagSlot(data.CatchTime); ok {
currentPet := slot.PetInfoPtr()
if currentPet != nil {
defer currentPet.Cure()
}
}
return &pet.PetOneCureOutboundInfo{
@@ -63,11 +95,17 @@ func (h Controller) PetFirst(
func (h Controller) SetPetExp(
data *PetSetExpInboundInfo,
player *player.Player) (result *pet.PetSetExpOutboundInfo, err errorcode.ErrorCode) {
_, currentPet, found := player.FindPet(data.CatchTime)
if !found || currentPet.Level >= 100 {
slot, found := player.FindPetBagSlot(data.CatchTime)
currentPet := slot.PetInfoPtr()
if !found || currentPet == nil || currentPet.Level >= 100 {
return &pet.PetSetExpOutboundInfo{Exp: player.Info.ExpPool}, errorcode.ErrorCodes.ErrSystemError
}
player.AddPetExp(currentPet, data.Exp)
allowedExp := petSetExpLimit(currentPet)
if allowedExp <= 0 {
return &pet.PetSetExpOutboundInfo{Exp: player.Info.ExpPool}, errorcode.ErrorCodes.ErrSystemError
}
player.AddPetExp(currentPet, minInt64(data.Exp, allowedExp))
return &pet.PetSetExpOutboundInfo{Exp: player.Info.ExpPool}, 0
}

View File

@@ -0,0 +1,75 @@
package controller
import (
"blazing/common/data/xmlres"
"blazing/common/socket/errorcode"
"blazing/logic/service/player"
playermodel "blazing/modules/player/model"
"testing"
)
func firstPetIDForControllerTest(t *testing.T) int {
t.Helper()
for id := range xmlres.PetMAP {
return id
}
t.Fatal("xmlres.PetMAP is empty")
return 0
}
func TestSetPetExpCapsLevelAt100(t *testing.T) {
petID := firstPetIDForControllerTest(t)
petInfo := playermodel.GenPetInfo(petID, 31, 0, 0, 99, nil, 0)
if petInfo == nil {
t.Fatal("failed to generate test pet")
}
expPool := petInfo.NextLvExp + 10_000
testPlayer := player.NewPlayer(nil)
testPlayer.Info = &playermodel.PlayerInfo{
ExpPool: expPool,
PetList: []playermodel.PetInfo{*petInfo},
}
currentPet := &testPlayer.Info.PetList[0]
result, err := (Controller{}).SetPetExp(&PetSetExpInboundInfo{
CatchTime: currentPet.CatchTime,
Exp: expPool,
}, testPlayer)
if err != 0 {
t.Fatalf("expected SetPetExp to succeed, got err=%d", err)
}
if currentPet.Level != 100 {
t.Fatalf("expected pet level to stop at 100, got %d", currentPet.Level)
}
if result.Exp != 10_000 {
t.Fatalf("expected overflow exp to remain in pool, got %d", result.Exp)
}
}
func TestSetPetExpRejectsPetAtLevel100(t *testing.T) {
petID := firstPetIDForControllerTest(t)
petInfo := playermodel.GenPetInfo(petID, 31, 0, 0, 100, nil, 0)
if petInfo == nil {
t.Fatal("failed to generate test pet")
}
testPlayer := player.NewPlayer(nil)
testPlayer.Info = &playermodel.PlayerInfo{
ExpPool: 50_000,
PetList: []playermodel.PetInfo{*petInfo},
}
result, err := (Controller{}).SetPetExp(&PetSetExpInboundInfo{
CatchTime: petInfo.CatchTime,
Exp: 12_345,
}, testPlayer)
if err != errorcode.ErrorCodes.ErrSystemError {
t.Fatalf("expected level-100 pet to be rejected, got err=%d", err)
}
if result.Exp != 50_000 {
t.Fatalf("expected exp pool to remain unchanged, got %d", result.Exp)
}
}

View File

@@ -8,47 +8,136 @@ import (
"blazing/logic/service/pet"
"blazing/logic/service/player"
"blazing/modules/player/model"
"github.com/samber/lo"
)
type GetPetLearnableSkillsOutboundInfo struct {
SkillListLen uint32 `struc:"sizeof=SkillList"`
SkillList []uint32 `json:"skillList"`
}
func isSameUint32Slice(a []uint32, b []uint32) bool {
if len(a) != len(b) {
return false
}
for index := range a {
if a[index] != b[index] {
return false
}
}
return true
}
func collectPetLearnableSkillList(currentPet *model.PetInfo) []uint32 {
skillSet := make(map[uint32]struct{})
skills := make([]uint32, 0)
appendSkill := func(skillID uint32) {
if skillID == 0 {
return
}
if _, exists := skillSet[skillID]; exists {
return
}
skillSet[skillID] = struct{}{}
skills = append(skills, skillID)
}
for _, skillID := range currentPet.GetLevelRangeCanLearningSkills(1, currentPet.Level) {
appendSkill(skillID)
}
for _, skillID := range currentPet.ExtSKill {
appendSkill(skillID)
}
for _, skill := range currentPet.SkillList {
delete(skillSet, skill.ID)
}
result := make([]uint32, 0, len(skillSet))
for _, skillID := range skills {
if _, exists := skillSet[skillID]; exists {
result = append(result, skillID)
}
}
return result
}
// GetPetLearnableSkills 查询当前精灵可学习技能(等级技能 + 额外技能ExtSKill
func (h Controller) GetPetLearnableSkills(
data *GetPetLearnableSkillsInboundInfo,
c *player.Player,
) (result *GetPetLearnableSkillsOutboundInfo, err errorcode.ErrorCode) {
slot, ok := c.FindPetBagSlot(data.CatchTime)
currentPet := slot.PetInfoPtr()
if !ok || currentPet == nil {
return nil, errorcode.ErrorCodes.ErrPokemonNotExists
}
return &GetPetLearnableSkillsOutboundInfo{
SkillList: collectPetLearnableSkillList(currentPet),
}, 0
}
// SetPetSkill 设置宠物技能消耗50赛尔豆
func (h Controller) SetPetSkill(data *ChangeSkillInfo, c *player.Player) (result *pet.ChangeSkillOutInfo, err errorcode.ErrorCode) {
const setSkillCost = 50
if !c.GetCoins(setSkillCost) {
return nil, errorcode.ErrorCodes.ErrSunDouInsufficient10016
}
c.Info.Coins -= setSkillCost
_, currentPet, ok := c.FindPet(data.CatchTime)
if !ok {
slot, ok := c.FindPetBagSlot(data.CatchTime)
currentPet := slot.PetInfoPtr()
if !ok || currentPet == nil {
return nil, errorcode.ErrorCodes.ErrSystemBusy
}
canleaernskill := currentPet.GetLevelRangeCanLearningSkills(1, currentPet.Level)
_, ok = lo.Find(canleaernskill, func(item uint32) bool {
return item == data.ReplaceSkill
})
if !ok {
return result, errorcode.ErrorCodes.ErrSystemBusy
canLearnSkillSet := make(map[uint32]struct{})
for _, skillID := range collectPetLearnableSkillList(currentPet) {
canLearnSkillSet[skillID] = struct{}{}
}
_, _, ok = utils.FindWithIndex(currentPet.SkillList, func(item model.SkillInfo) bool { //已经存在技能
if _, exists := canLearnSkillSet[data.ReplaceSkill]; !exists {
return nil, errorcode.ErrorCodes.ErrSystemBusy
}
skillInfo, exists := xmlres.SkillMap[int(data.ReplaceSkill)]
if !exists {
return nil, errorcode.ErrorCodes.ErrSystemBusy
}
_, _, ok = utils.FindWithIndex(currentPet.SkillList, func(item model.SkillInfo) bool {
return item.ID == data.ReplaceSkill
})
if ok {
return nil, errorcode.ErrorCodes.ErrSystemBusy
}
// 查找要学习的技能并替换
_, targetSkill, ok := utils.FindWithIndex(currentPet.SkillList, func(item model.SkillInfo) bool {
return item.ID == data.HasSkill
})
if ok {
if data.HasSkill == 0 && len(currentPet.SkillList) >= 4 {
return nil, errorcode.ErrorCodes.ErrSystemBusy
}
if data.HasSkill != 0 {
_, _, found := utils.FindWithIndex(currentPet.SkillList, func(item model.SkillInfo) bool {
return item.ID == data.HasSkill
})
if !found {
return nil, errorcode.ErrorCodes.ErrSystemBusy
}
}
if !c.GetCoins(setSkillCost) {
return nil, errorcode.ErrorCodes.ErrSunDouInsufficient10016
}
c.Info.Coins -= setSkillCost
maxPP := uint32(skillInfo.MaxPP)
if data.HasSkill != 0 {
_, targetSkill, _ := utils.FindWithIndex(currentPet.SkillList, func(item model.SkillInfo) bool {
return item.ID == data.HasSkill
})
targetSkill.ID = data.ReplaceSkill
targetSkill.PP = uint32(xmlres.SkillMap[int(targetSkill.ID)].MaxPP)
targetSkill.PP = maxPP
} else {
currentPet.SkillList = append(currentPet.SkillList, model.SkillInfo{
ID: data.ReplaceSkill,
PP: maxPP,
})
}
return &pet.ChangeSkillOutInfo{
@@ -60,25 +149,139 @@ func (h Controller) SetPetSkill(data *ChangeSkillInfo, c *player.Player) (result
func (h Controller) SortPetSkills(data *C2S_Skill_Sort, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
const skillSortCost = 50
slot, ok := c.FindPetBagSlot(data.CapTm)
currentPet := slot.PetInfoPtr()
if !ok || currentPet == nil {
return nil, errorcode.ErrorCodes.ErrPokemonNotExists
}
usedSkillSet := make(map[uint32]struct{})
newSkillList := make([]model.SkillInfo, 0, 4)
for _, skillID := range data.Skill {
if skillID == 0 {
continue
}
if _, used := usedSkillSet[skillID]; used {
continue
}
_, skill, found := utils.FindWithIndex(currentPet.SkillList, func(item model.SkillInfo) bool {
return item.ID == skillID
})
if !found {
continue
}
newSkillList = append(newSkillList, *skill)
usedSkillSet[skillID] = struct{}{}
}
for _, skill := range currentPet.SkillList {
if skill.ID == 0 {
continue
}
if _, used := usedSkillSet[skill.ID]; used {
continue
}
newSkillList = append(newSkillList, skill)
usedSkillSet[skill.ID] = struct{}{}
}
if len(newSkillList) > 4 {
newSkillList = newSkillList[:4]
}
if !c.GetCoins(skillSortCost) {
return nil, errorcode.ErrorCodes.ErrSunDouInsufficient10016
}
c.Info.Coins -= skillSortCost
_, currentPet, ok := c.FindPet(data.CapTm)
if ok {
var newSkillList []model.SkillInfo
for _, skillID := range data.Skill {
_, skill, found := utils.FindWithIndex(currentPet.SkillList, func(item model.SkillInfo) bool {
return item.ID == skillID
})
if found {
newSkillList = append(newSkillList, *skill)
}
}
currentPet.SkillList = newSkillList
}
currentPet.SkillList = newSkillList
return nil, 0
}
// CommitPetSkills 按最终技能列表一次性提交学习/替换/排序结果。
func (h Controller) CommitPetSkills(
data *CommitPetSkillsInboundInfo,
c *player.Player,
) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
const setSkillCost = 50
const skillSortCost = 50
slot, ok := c.FindPetBagSlot(data.CatchTime)
currentPet := slot.PetInfoPtr()
if !ok || currentPet == nil {
return nil, errorcode.ErrorCodes.ErrPokemonNotExists
}
currentSkillSet := make(map[uint32]model.SkillInfo, len(currentPet.SkillList))
currentSkillOrder := make([]uint32, 0, len(currentPet.SkillList))
for _, skill := range currentPet.SkillList {
if skill.ID == 0 {
continue
}
currentSkillSet[skill.ID] = skill
currentSkillOrder = append(currentSkillOrder, skill.ID)
}
finalSkillIDs := make([]uint32, 0, 4)
usedSkillSet := make(map[uint32]struct{}, 4)
for _, skillID := range data.Skill {
if skillID == 0 {
continue
}
if _, exists := usedSkillSet[skillID]; exists {
continue
}
usedSkillSet[skillID] = struct{}{}
finalSkillIDs = append(finalSkillIDs, skillID)
}
if len(finalSkillIDs) == 0 {
return nil, errorcode.ErrorCodes.ErrSystemBusy
}
if len(finalSkillIDs) > 4 {
finalSkillIDs = finalSkillIDs[:4]
}
if isSameUint32Slice(currentSkillOrder, finalSkillIDs) {
return nil, 0
}
learnableSkillSet := make(map[uint32]struct{})
for _, skillID := range collectPetLearnableSkillList(currentPet) {
learnableSkillSet[skillID] = struct{}{}
}
newSkillCount := 0
finalSkillList := make([]model.SkillInfo, 0, len(finalSkillIDs))
for _, skillID := range finalSkillIDs {
if skill, exists := currentSkillSet[skillID]; exists {
finalSkillList = append(finalSkillList, skill)
continue
}
if _, exists := learnableSkillSet[skillID]; !exists {
return nil, errorcode.ErrorCodes.ErrSystemBusy
}
skillInfo, exists := xmlres.SkillMap[int(skillID)]
if !exists {
return nil, errorcode.ErrorCodes.ErrSystemBusy
}
newSkillCount++
finalSkillList = append(finalSkillList, model.SkillInfo{
ID: skillID,
PP: uint32(skillInfo.MaxPP),
})
}
totalCost := int64(newSkillCount * setSkillCost)
if newSkillCount == 0 {
totalCost += int64(skillSortCost)
}
if totalCost > 0 && !c.GetCoins(totalCost) {
return nil, errorcode.ErrorCodes.ErrSunDouInsufficient10016
}
c.Info.Coins -= totalCost
currentPet.SkillList = finalSkillList
return nil, 0
}

View File

@@ -10,21 +10,20 @@ import (
"github.com/samber/lo"
)
// IsCollect 处理控制器请求。
func (h Controller) IsCollect(
data *pet.C2S_IS_COLLECT, c *player.Player) (result *pet.S2C_IS_COLLECT, err errorcode.ErrorCode) { //这个时候player应该是空的
result = &pet.S2C_IS_COLLECT{
ID: data.Type,
}
c.Service.Task.Exec(uint32(1335), func(te *model.Task) bool {
r := bitset32.From(te.Data)
// 分支未完成时,标记完成并发放奖励
taskData, taskErr := c.Service.Task.GetTask(uint32(1335))
if taskErr == nil {
r := bitset32.From(taskData.Data)
if r.Test(uint(data.Type)) {
result.IsCom = 1
}
return false
})
}
_, ok := lo.Find([]uint32{1, 2, 3, 4, 301}, func(item uint32) bool {
return data.Type == item
@@ -49,6 +48,7 @@ var validTypeIDMap = map[int][]uint32{
100: {856, 857, 858}, //测试
}
// Collect 处理控制器请求。
func (h Controller) Collect(
data *pet.C2S_PET_COLLECT, c *player.Player) (result *pet.S2C_PET_COLLECT, err errorcode.ErrorCode) { //这个时候player应该是空的
result = &pet.S2C_PET_COLLECT{ID: data.ID}
@@ -57,14 +57,17 @@ func (h Controller) Collect(
return data.Type == item
})
if res == model.Completed && ok { //这块是为了兼容旧版本
c.Service.Task.Exec(uint32(1335), func(te *model.Task) bool {
taskData, taskErr := c.Service.Task.GetTask(uint32(1335))
if taskErr != nil {
return nil, errorcode.ErrorCode(errorcode.ErrorCodes.ErrSystemError)
}
r := bitset32.From(te.Data)
r.Set(uint(data.Type))
te.Data = r.Bytes()
return true
})
r := bitset32.From(taskData.Data)
r.Set(uint(data.Type))
taskData.Data = r.Bytes()
if taskErr = c.Service.Task.SetTask(taskData); taskErr != nil {
return nil, errorcode.ErrorCode(errorcode.ErrorCodes.ErrSystemError)
}
return nil, errorcode.ErrorCode(errorcode.ErrorCodes.ErrSystemError)
}
@@ -78,21 +81,22 @@ func (h Controller) Collect(
if !lo.Contains(validIDs, data.ID) {
return nil, errorcode.ErrorCode(errorcode.ErrorCodes.ErrSystemError)
}
c.Service.Task.Exec(uint32(1335), func(te *model.Task) bool {
taskData, taskErr := c.Service.Task.GetTask(uint32(1335))
if taskErr != nil {
return nil, errorcode.ErrorCode(errorcode.ErrorCodes.ErrSystemError)
}
r := bitset32.From(te.Data)
// 分支未完成时,标记完成并发放奖励
if !r.Test(uint(data.Type)) {
r.Set(uint(data.Type))
te.Data = r.Bytes()
r := model.GenPetInfo(int(data.ID), -1, -1, 0, 1, nil, 0)
c.Service.Pet.PetAdd(r, 0)
result.CatchTime = r.CatchTime
return true
r := bitset32.From(taskData.Data)
if !r.Test(uint(data.Type)) {
r.Set(uint(data.Type))
taskData.Data = r.Bytes()
if taskErr = c.Service.Task.SetTask(taskData); taskErr != nil {
return nil, errorcode.ErrorCode(errorcode.ErrorCodes.ErrSystemError)
}
return false
})
petInfo := model.GenPetInfo(int(data.ID), -1, -1, 0, 1, nil, 0)
c.Service.Pet.PetAdd(petInfo, 0)
result.CatchTime = petInfo.CatchTime
}
if result.CatchTime == 0 {
return nil, errorcode.ErrorCode(errorcode.ErrorCodes.ErrAwardAlreadyClaimed)

View File

@@ -120,6 +120,7 @@ func canBreedPair(maleID, femaleID uint32) bool {
return ok
}
// GetEggList 处理控制器请求。
func (ctl Controller) GetEggList(
data *pet.C2S_GET_EGG_LIST, player *player.Player) (result *pet.S2C_GET_EGG_LIST, err errorcode.ErrorCode) { //这个时候player应该是空的

View File

@@ -8,6 +8,7 @@ import (
"blazing/logic/service/player"
)
// SystemTimeInfo 处理控制器请求。
func (h Controller) SystemTimeInfo(data *InInfo, c *player.Player) (result *OutInfo, err errorcode.ErrorCode) {
return &OutInfo{

View File

@@ -171,6 +171,7 @@ func (h Controller) ChangePlayerCloth(data *ChangePlayerClothInboundInfo, player
return
}
// ChangePlayerName 处理控制器请求。
func (h Controller) ChangePlayerName(data *ChangePlayerNameInboundInfo, c *player.Player) (result *user.ChangePlayerNameOutboundInfo, err errorcode.ErrorCode) {
newNickname := cool.Filter.Replace(strings.Trim(data.Nickname, "\x00"), '*')
@@ -183,6 +184,8 @@ func (h Controller) ChangePlayerName(data *ChangePlayerNameInboundInfo, c *playe
return result, 0
}
// ChangeTile 处理控制器请求。
func (h Controller) ChangeTile(data *ChangeTitleInboundInfo, c *player.Player) (result *user.ChangeTitleOutboundInfo, err errorcode.ErrorCode) {
result = &user.ChangeTitleOutboundInfo{

View File

@@ -2,61 +2,66 @@ package controller
import (
"blazing/common/socket/errorcode"
"blazing/logic/service/player"
logicplayer "blazing/logic/service/player"
"blazing/logic/service/user"
"blazing/modules/config/service"
"blazing/modules/player/model"
configservice "blazing/modules/config/service"
playerservice "blazing/modules/player/service"
"strings"
"time"
)
func (h Controller) CDK(data *C2S_GET_GIFT_COMPLETE, player *player.Player) (result *user.S2C_GET_GIFT_COMPLETE, err errorcode.ErrorCode) {
// CDK 处理控制器请求。
func (h Controller) CDK(data *C2S_GET_GIFT_COMPLETE, player *logicplayer.Player) (result *user.S2C_GET_GIFT_COMPLETE, err errorcode.ErrorCode) {
result = &user.S2C_GET_GIFT_COMPLETE{}
cdkService := service.NewCdkService()
rewardPetService := service.NewPetRewardService()
itemRewardService := service.NewItemService()
cdkCode := strings.Trim(data.PassText, "\x00")
cdkService := configservice.NewCdkService()
now := time.Now()
r := cdkService.Get(data.PassText)
r := cdkService.Get(cdkCode)
if r == nil {
return nil, errorcode.ErrorCodes.ErrMolecularCodeNotExists
}
if r.BindUserId != 0 && r.BindUserId != data.Head.UserID {
return nil, errorcode.ErrorCodes.ErrMolecularCodeFrozen
}
if r.ValidEndTime.Compare(now) == -1 {
return nil, errorcode.ErrorCodes.ErrMolecularCodeExpired
}
if !player.Service.Cdk.CanGet(uint32(r.ID)) {
return
}
if !cdkService.Set(data.PassText) {
if !cdkService.Set(cdkCode) {
return nil, errorcode.ErrorCodes.ErrMolecularCodeGiftsGone
}
reward, grantErr := playerservice.NewCdkService(data.Head.UserID).GrantConfigReward(uint32(r.ID))
if grantErr != nil {
return nil, errorcode.ErrorCodes.ErrSystemError
}
result.Flag = 1
for _, rewardID := range r.ElfRewardIds {
pet := rewardPetService.Get(rewardID)
if pet == nil {
continue
appendGift := func(giftID, count int64) {
if giftID == 0 || count <= 0 {
return
}
petInfo := model.GenPetInfo(int(pet.MonID), int(pet.DV), int(pet.Nature), int(pet.Effect), int(pet.Lv), nil, 0)
player.Service.Pet.PetAdd(petInfo, 0)
result.PetGift = append(result.PetGift, user.PetGiftInfo{PetID: petInfo.ID, CacthTime: petInfo.CatchTime})
result.GiftList = append(result.GiftList, user.GiftInfo{GiftID: giftID, Count: count})
}
for _, rewardID := range r.ItemRewardIds {
itemInfo := itemRewardService.GetItemCount(rewardID)
player.ItemAdd(itemInfo.ItemId, itemInfo.ItemCnt)
result.GiftList = append(result.GiftList, user.GiftInfo{GiftID: itemInfo.ItemId, Count: itemInfo.ItemCnt})
appendGift(1, reward.Coins)
appendGift(3, reward.ExpPool)
appendGift(5, reward.Gold)
appendGift(9, reward.EVPool)
for _, item := range reward.Items {
appendGift(item.ItemId, item.ItemCnt)
}
if r.TitleRewardIds != 0 {
player.Service.Title.Give(r.TitleRewardIds)
result.Tile = r.TitleRewardIds
for _, pet := range reward.Pets {
result.PetGift = append(result.PetGift, user.PetGiftInfo{PetID: pet.PetID, CacthTime: pet.CatchTime})
}
if len(reward.TitleIDs) > 0 {
result.Tile = reward.TitleIDs[0]
}
player.Service.Cdk.Log(uint32(r.ID))
return
}

View File

@@ -7,6 +7,7 @@ import (
"blazing/modules/config/service"
)
// GetTalkCount 处理控制器请求。
func (h Controller) GetTalkCount(data *TalkCountInboundInfo, c *player.Player) (result *item.TalkCountOutboundInfo, err errorcode.ErrorCode) {
result = &item.TalkCountOutboundInfo{}
talkCount, ok := c.Service.Talk.Cheak(c.Info.MapID, int(data.ID))

View File

@@ -28,12 +28,15 @@ func (h Controller) AcceptTask(data *AcceptTaskInboundInfo, c *player.Player) (r
}
c.Info.SetTask(int(data.TaskId), model.Accepted)
c.Service.Task.Exec(uint32(data.TaskId), func(t *model.Task) bool {
t.Data = []uint32{}
taskData, taskErr := c.Service.Task.GetTask(uint32(data.TaskId))
if taskErr != nil {
return nil, errorcode.ErrorCodes.ErrSystemError
}
taskData.Data = []uint32{}
if taskErr = c.Service.Task.SetTask(taskData); taskErr != nil {
return nil, errorcode.ErrorCodes.ErrSystemError
}
return true
})
result = &task.AcceptTaskOutboundInfo{}
result.TaskId = data.TaskId
return result, 0
@@ -48,10 +51,14 @@ func (h Controller) AddTaskBuf(data *AddTaskBufInboundInfo, c *player.Player) (r
if c.Info.GetTask(int(data.TaskId)) != model.Accepted {
return result, errorcode.ErrorCodes.ErrAwardAlreadyClaimed
}
c.Service.Task.Exec(data.TaskId, func(taskEx *model.Task) bool {
taskEx.Data = data.TaskList
return true
})
taskData, taskErr := c.Service.Task.GetTask(data.TaskId)
if taskErr != nil {
return nil, errorcode.ErrorCodes.ErrSystemError
}
taskData.Data = data.TaskList
if taskErr = c.Service.Task.SetTask(taskData); taskErr != nil {
return nil, errorcode.ErrorCodes.ErrSystemError
}
return result, 0
}
@@ -70,31 +77,16 @@ func (h Controller) CompleteTask(data1 *CompleteTaskInboundInfo, c *player.Playe
// if service.NewTaskService().IsAcceptable(data1.TaskId) == nil {
// return nil, errorcode.ErrorCodes.ErrSystemError
// }
c.Info.SetTask(int(data1.TaskId), model.Completed)
result = &task.CompleteTaskOutboundInfo{
TaskId: data1.TaskId,
ItemList: make([]data.ItemInfo, 0),
}
taskInfo := task.GetTaskInfo(int(data1.TaskId), int(data1.OutState))
if taskInfo == nil {
return nil, errorcode.ErrorCodes.ErrNeedCompleteTaskForPrize
if _, err = c.ApplyTaskCompletion(data1.TaskId, int(data1.OutState), result); err != 0 {
return nil, err
}
if taskInfo.Pet != nil {
c.Service.Pet.PetAdd(taskInfo.Pet, 0)
result.CaptureTime = taskInfo.Pet.CatchTime
result.PetTypeId = taskInfo.Pet.ID
}
for _, item := range taskInfo.ItemList {
success := c.ItemAdd(item.ItemId, item.ItemCnt)
if success {
result.ItemList = append(result.ItemList, item)
}
if taskErr := c.Info.SetTask(int(data1.TaskId), model.Completed); taskErr != nil {
return nil, errorcode.ErrorCodes.ErrSystemError
}
return result, 0 //通过PUB/SUB回包
@@ -105,11 +97,12 @@ func (h Controller) GetTaskBuf(data *GetTaskBufInboundInfo, c *player.Player) (r
result = &task.GetTaskBufOutboundInfo{
TaskId: data.TaskId,
}
c.Service.Task.Exec(data.TaskId, func(te *model.Task) bool {
result.TaskList = te.Data
return false
})
taskData, taskErr := c.Service.Task.GetTask(data.TaskId)
if taskErr != nil {
return nil, errorcode.ErrorCodes.ErrSystemError
}
result.TaskList = taskData.Data
return result, 0
}

View File

@@ -6,6 +6,7 @@ import (
"blazing/logic/service/space/info"
)
// PlayerWalk 处理控制器请求。
func (h Controller) PlayerWalk(data *WalkInInfo, c *player.Player) (result *info.WalkOutInfo, err errorcode.ErrorCode) {
result = &info.WalkOutInfo{
Flag: data.Flag,

BIN
logic/fight.test Executable file

Binary file not shown.

View File

@@ -52,7 +52,7 @@ func PprofWeb() {
}
// 所有端口都失败时的兜底
errMsg := fmt.Sprintf("[FATAL] 端口9909/9910均监听失败pprof服务启动失败")
errMsg := "[FATAL] 端口9909/9910均监听失败pprof服务启动失败"
fmt.Println(errMsg)
// 可选根据业务需求决定是否panic
// panic(errMsg)
@@ -148,7 +148,7 @@ func monitorMemAndQuit() {
// 4. 超70%阈值,执行优雅退出
if usedRatio >= memThresholdRatio {
log.Fatalf("内存占比达%.1f%%超过90%阈值,程序开始退出", usedRatio*100)
log.Fatalf("内存占比达%.1f%%超过90%%阈值,程序开始退出", usedRatio*100)
// ########## 可选:这里添加你的优雅清理逻辑 ##########
// 如:关闭数据库连接、释放文件句柄、保存业务状态、推送退出告警等
cleanup()

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

@@ -3,10 +3,13 @@ package common
import (
"blazing/common/socket/errorcode"
"blazing/logic/service/fight/info"
space "blazing/logic/service/space/info"
"blazing/modules/player/model"
)
// PlayerI 定义战斗与 common 服务依赖的最小玩家能力接口。
type PlayerI interface {
ApplyPetDisplayInfo(*space.SimpleInfo)
GetPlayerCaptureContext() *info.PlayerCaptureContext
Roll(int, int) (bool, float64, float64)
//SendPack(b []byte) error

View File

@@ -44,6 +44,7 @@ func (f *FightC) openActionWindow() {
f.actionMu.Lock()
f.acceptActions = true
f.pendingActions = f.pendingActions[:0]
f.pendingHead = 0
f.actionRound.Store(uint32(f.Round))
f.actionMu.Unlock()
}
@@ -52,6 +53,7 @@ func (f *FightC) closeActionWindow() {
f.actionMu.Lock()
f.acceptActions = false
f.pendingActions = f.pendingActions[:0]
f.pendingHead = 0
f.actionRound.Store(0)
f.actionMu.Unlock()
@@ -73,8 +75,10 @@ func (f *FightC) submitAction(act action.BattleActionI) {
f.actionMu.Unlock()
return
}
f.compactPendingActionsLocked()
replaceIndex := -1
for i, pending := range f.pendingActions {
for i := f.pendingHead; i < len(f.pendingActions); i++ {
pending := f.pendingActions[i]
if pending == nil || actionSlotKeyFromAction(pending) != actionSlotKeyFromAction(act) {
continue
}
@@ -82,6 +86,10 @@ func (f *FightC) submitAction(act action.BattleActionI) {
break
}
if replaceIndex >= 0 {
if f.LegacyGroupProtocol {
f.actionMu.Unlock()
return
}
f.pendingActions[replaceIndex] = act
} else {
f.pendingActions = append(f.pendingActions, act)
@@ -101,15 +109,23 @@ func (f *FightC) submitAction(act action.BattleActionI) {
func (f *FightC) nextAction() action.BattleActionI {
f.actionMu.Lock()
if len(f.pendingActions) == 0 {
if f.pendingHead >= len(f.pendingActions) {
f.pendingActions = f.pendingActions[:0]
f.pendingHead = 0
f.actionMu.Unlock()
return nil
}
act := f.pendingActions[0]
copy(f.pendingActions, f.pendingActions[1:])
f.pendingActions = f.pendingActions[:len(f.pendingActions)-1]
hasMore := len(f.pendingActions) > 0
act := f.pendingActions[f.pendingHead]
f.pendingActions[f.pendingHead] = nil
f.pendingHead++
hasMore := f.pendingHead < len(f.pendingActions)
if !hasMore {
f.pendingActions = f.pendingActions[:0]
f.pendingHead = 0
} else {
f.compactPendingActionsLocked()
}
notify := f.actionNotify
f.actionMu.Unlock()
@@ -123,6 +139,22 @@ func (f *FightC) nextAction() action.BattleActionI {
return act
}
func (f *FightC) compactPendingActionsLocked() {
if f.pendingHead == 0 {
return
}
if f.pendingHead < len(f.pendingActions)/2 && len(f.pendingActions) < cap(f.pendingActions) {
return
}
remaining := len(f.pendingActions) - f.pendingHead
copy(f.pendingActions, f.pendingActions[f.pendingHead:])
for i := remaining; i < len(f.pendingActions); i++ {
f.pendingActions[i] = nil
}
f.pendingActions = f.pendingActions[:remaining]
f.pendingHead = 0
}
// 玩家逃跑/无响应/掉线
func (f *FightC) Over(c common.PlayerI, res model.EnumBattleOverReason) {
if f.closefight {
@@ -143,10 +175,13 @@ func (f *FightC) Over(c common.PlayerI, res model.EnumBattleOverReason) {
// }
f.overl.Do(func() {
f.Reason = res
f.Reason = normalizeFightOverReason(res)
if f.GetInputByPlayer(c, true) != nil {
f.WinnerId = f.GetInputByPlayer(c, true).UserID
}
f.FightOverInfo.Reason = f.Reason
f.FightOverInfo.WinnerId = f.WinnerId
f.closefight = true
close(f.quit)
@@ -281,9 +316,19 @@ func (f *FightC) UseItemAt(c common.PlayerI, cacthid, itemid uint32, actorIndex,
// ReadyFight 处理玩家战斗准备逻辑,当满足条件时启动战斗循环
func (f *FightC) ReadyFight(c common.PlayerI) {
f.Broadcast(func(ff *input.Input) {
ff.Player.SendPackCmd(2404, &info.S2C_2404{UserID: c.GetInfo().UserID})
if f.LegacyGroupProtocol {
input := f.GetInputByPlayer(c, false)
if input == nil {
return
}
input.Finished = true
if f.checkBothPlayersReady(c) {
f.startBattle(f.FightStartOutboundInfo)
}
return
}
f.BroadcastPlayers(func(p common.PlayerI) {
p.SendPackCmd(2404, &info.S2C_2404{UserID: c.GetInfo().UserID})
})
// 2. 标记当前玩家已准备完成
input := f.GetInputByPlayer(c, false)
@@ -325,7 +370,7 @@ func (f *FightC) collectFightPetInfos(inputs []*input.Input) []info.FightPetInfo
Hp: currentPet.Info.Hp,
MaxHp: currentPet.Info.MaxHp,
Level: currentPet.Info.Level,
Catchable: uint32(fighter.CanCapture),
Catchable: fightPetCatchableFlag(fighter.CanCapture),
}
if fighter.AttackValue != nil {
fightInfo.Prop = fighter.AttackValue.Prop
@@ -335,6 +380,13 @@ func (f *FightC) collectFightPetInfos(inputs []*input.Input) []info.FightPetInfo
return infos
}
func fightPetCatchableFlag(catchRate int) uint32 {
if catchRate > 0 {
return 1
}
return 0
}
// checkBothPlayersReady 检查PVP战斗中双方是否都已准备完成
// 参数c为当前准备的玩家返回true表示双方均准备完成
func (f *FightC) checkBothPlayersReady(currentPlayer common.PlayerI) bool {
@@ -355,8 +407,12 @@ func (f *FightC) startBattle(startInfo info.FightStartOutboundInfo) {
go f.battleLoop()
// 向双方广播战斗开始信息
f.Broadcast(func(ff *input.Input) {
ff.Player.SendPackCmd(2504, &startInfo)
f.BroadcastPlayers(func(p common.PlayerI) {
if f.LegacyGroupProtocol {
f.sendLegacyGroupStart(p)
return
}
f.sendFightPacket(p, fightPacketStart, &startInfo)
})
})
}

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

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

View File

@@ -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

@@ -140,7 +140,7 @@ func (e *Effect1181) OnSkill() bool {
type Effect1182 struct{ node.EffectNode }
func (e *Effect1182) Skill_Use() bool {
if len(e.Args()) < 2 || e.Ctx().Our == nil || e.Ctx().Our.CurPet[0] == nil || e.Ctx().Opp == nil || e.Ctx().Opp.CurPet[0] == nil {
if len(e.Args()) < 2 || e.Ctx().Our == nil || e.Ctx().Our.CurPet[0] == nil {
return true
}
@@ -153,9 +153,15 @@ func (e *Effect1182) Skill_Use() bool {
if targetHP.Cmp(alpacadecimal.Zero) < 0 {
targetHP = alpacadecimal.Zero
}
if e.Ctx().Opp.CurPet[0].GetHP().Cmp(targetHP) > 0 {
e.Ctx().Opp.CurPet[0].Info.Hp = uint32(targetHP.IntPart())
}
forEachEnemyTargetBySkill(e.Ctx().Our, e.Ctx().Opp, e.Ctx().SkillEntity, func(target *input.Input) bool {
if target == nil || target.CurrentPet() == nil {
return true
}
if target.CurrentPet().GetHP().Cmp(targetHP) > 0 {
target.CurrentPet().Info.Hp = uint32(targetHP.IntPart())
}
return true
})
sub := e.Ctx().Our.InitEffect(input.EffectType.Sub, 1182, int(e.Args()[1].IntPart()))
if sub != nil {

View File

@@ -10,20 +10,23 @@ type Effect169 struct {
}
func (e *Effect169) OnSkill() bool {
chance := e.Args()[1].IntPart()
success, _, _ := e.Input.Player.Roll(int(chance), 100)
if success {
// 添加异常状态
statusEffect := e.CarrierInput().InitEffect(input.EffectType.Status, int(e.Args()[2].IntPart())) // 以麻痹为例
if statusEffect != nil {
e.TargetInput().AddEffect(e.CarrierInput(), statusEffect)
}
e.ForEachOpponentSlot(func(target *input.Input) bool {
if target == nil {
return true
}
statusEffect := e.CarrierInput().InitEffect(input.EffectType.Status, int(e.Args()[2].IntPart()))
if statusEffect != nil {
target.AddEffect(e.CarrierInput(), statusEffect)
}
return true
})
}
return true
}
func init() {
input.InitEffect(input.EffectType.Skill, 169, &Effect169{})
}

View File

@@ -311,7 +311,7 @@ func (e *Effect2194) OnSkill() bool {
if e.Ctx().Opp.CurPet[0] == nil {
return true
}
addStatusByID(e.Ctx().Our, e.Ctx().Opp, int(info.PetStatus.DrainedHP))
addTimedStatus(e.Ctx().Our, e.Ctx().Opp, int(info.PetStatus.DrainedHP), 4)
return true
}

View File

@@ -34,7 +34,7 @@ func (e *Effect13) OnSkill() bool {
if eff == nil {
return true
}
eff.Duration(e.EffectNode.SideEffectArgs[0] - 1)
eff.Duration(e.EffectNode.SideEffectArgs[0])
e.Ctx().Opp.AddEffect(e.Ctx().Our, eff)
return true

View File

@@ -1,6 +1,7 @@
package effect
import (
"blazing/logic/service/fight/input"
"blazing/logic/service/fight/node"
)
@@ -41,14 +42,16 @@ type Effect5 struct {
// 技能触发时调用
// -----------------------------------------------------------
func (e *Effect5) Skill_Use() bool {
// 概率判定
ok, _, _ := e.Input.Player.Roll(e.SideEffectArgs[1], 100)
if !ok {
return true
}
e.Ctx().Opp.SetProp(e.Ctx().Our, int8(e.SideEffectArgs[0]), int8(e.SideEffectArgs[2]))
forEachEnemyTargetBySkill(e.Ctx().Our, e.Ctx().Opp, e.Ctx().SkillEntity, func(target *input.Input) bool {
target.SetProp(e.Ctx().Our, int8(e.SideEffectArgs[0]), int8(e.SideEffectArgs[2]))
return true
})
return true
}

View File

@@ -22,15 +22,19 @@ type Effect76 struct {
}
func (e *Effect76) OnSkill() bool {
// 概率判定
ok, _, _ := e.Input.Player.Roll(int(e.Args()[0].IntPart()), 100)
if !ok {
return true
}
e.Ctx().Opp.Damage(e.Ctx().Our, &info.DamageZone{
Type: info.DamageType.Fixed,
Damage: alpacadecimal.NewFromInt(int64(e.SideEffectArgs[2])),
damage := alpacadecimal.NewFromInt(int64(e.SideEffectArgs[2]))
forEachEnemyTargetBySkill(e.Ctx().Our, e.Ctx().Opp, e.Ctx().SkillEntity, func(target *input.Input) bool {
target.Damage(e.Ctx().Our, &info.DamageZone{
Type: info.DamageType.Fixed,
Damage: damage,
})
return true
})
return true
}

View File

@@ -19,7 +19,7 @@ var effectInfoByID = map[int]string{
29: "额外附加{0}点固定伤害",
31: "",
32: "使用后{0}回合攻击击中对象要害概率增加1/16",
33: "消除对手能力提升状态",
33: "消除敌方阵营所有强化",
34: "将所受的伤害{0}倍反馈给对手",
35: "惩罚,对方能力等级越高,此技能威力越大",
36: "命中时{0}%的概率秒杀对方",
@@ -120,7 +120,7 @@ var effectInfoByID = map[int]string{
164: "{0}回合内若受到攻击则有{1}%概率令对手{2}",
165: "{0}回合内每回合防御和特防等级+{1}",
166: "{0}回合内若对手使用属性攻击则{2}%对手{1}等级{3}",
169: "{0}回合内每回合额外附加{1}%概率令对{2}",
169: "{0}回合内每回合额外附加{1}%概率令对方阵营全体{2}",
170: "若先出手则免疫当回合伤害并回复1/{0}的最大体力值",
171: "{0}回合内自身使用属性技能时能较快出手",
172: "若后出手则给予对方损伤的1/{0}会回复自己的体力",

View File

@@ -27,7 +27,7 @@ func (e *Effect3) Skill_Use() bool {
return true
}
// Effect 33: 消除对手能力提升状态
// Effect 33: 消除敌方阵营所有强化
type Effect33 struct {
node.EffectNode
Reverse bool
@@ -38,13 +38,17 @@ type Effect33 struct {
// 执行时逻辑
// ----------------------
func (e *Effect33) Skill_Use() bool {
for i, v := range e.Ctx().Opp.Prop[:] {
if v > 0 {
e.Ctx().Opp.SetProp(e.Ctx().Our, int8(i), 0)
e.ForEachOpponentSlot(func(target *input.Input) bool {
if target == nil {
return true
}
}
for i, v := range target.Prop[:] {
if v > 0 {
target.SetProp(e.Ctx().Our, int8(i), 0)
}
}
return true
})
return true
}
@@ -54,8 +58,8 @@ func (e *Effect33) Skill_Use() bool {
// ----------------------
func init() {
// {3, false, 0}, // 解除自身能力下降状态
// {33, true, 0}, // 消除对手能力提升状态{3, false, 0}, // 解除自身能力下降状态
// {33, true, 0}, // 消除对手能力提升状态
// {33, true, 0}, // 消除敌方阵营所有强化{3, false, 0}, // 解除自身能力下降状态
// {33, true, 0}, // 消除敌方阵营所有强化
input.InitEffect(input.EffectType.Skill, 3, &Effect3{})
input.InitEffect(input.EffectType.Skill, 33, &Effect33{})
}

View File

@@ -36,43 +36,73 @@ func (e *StatusCannotAct) ActionStart(attacker, defender *action.SelectSkillActi
return false
}
// 疲惫状态:仅限制攻击技能,本回合属性技能仍可正常使用。
type StatusTired struct {
BaseStatus
}
func (e *StatusTired) ActionStart(attacker, defender *action.SelectSkillAction) bool {
if e.Ctx().SkillEntity == nil {
return false
}
return e.Ctx().SkillEntity.Category() == info.Category.STATUS
}
// 睡眠状态:受击后解除
type StatusSleep struct {
StatusCannotAct
hasTriedAct bool // 标记是否尝试过行动
hasTriedAct bool
}
// 睡眠在“被攻击且未 miss”后立即解除而不是等到技能使用后节点。
func (e *StatusSleep) DamageSubEx(zone *info.DamageZone) bool {
if zone == nil || e.Ctx().SkillEntity == nil {
return true
}
if e.Ctx().SkillEntity.Category() != info.Category.STATUS {
e.Alive(false)
}
return true
}
// 尝试出手时标记状态
func (e *StatusSleep) ActionStart(attacker, defender *action.SelectSkillAction) bool {
if e.Duration() <= 0 {
e.hasTriedAct = false
return true
}
e.hasTriedAct = true
return e.StatusCannotAct.ActionStart(attacker, defender)
}
// 技能使用后处理:非状态类技能触发后解除睡眠
func (e *StatusSleep) Skill_Use_ex() bool {
if !e.hasTriedAct {
return true
}
// 技能实体存在且非状态类型技能,解除睡眠
if e.Ctx().SkillEntity != nil && e.Ctx().Category() != info.Category.STATUS {
if e.Duration() <= 0 && e.Ctx().SkillEntity != nil && e.Ctx().Category() != info.Category.STATUS {
e.Alive(false)
}
e.hasTriedAct = false
return true
}
func (e *StatusSleep) TurnEnd() {
e.hasTriedAct = false
e.StatusCannotAct.TurnEnd()
}
// 持续伤害状态基类(中毒、冻伤、烧伤等)
type ContinuousDamage struct {
BaseStatus
isheal bool //是否回血
}
// 技能命中前触发伤害1/8最大生命值真实伤害
func (e *ContinuousDamage) ActionStart(attacker, defender *action.SelectSkillAction) bool {
// 回合开始触发持续伤害,保证吃药/空过回合时也会正常结算。
func (e *ContinuousDamage) TurnStart(attacker, defender *action.SelectSkillAction) {
carrier := e.CarrierInput()
source := e.SourceInput()
opp := e.TargetInput()
if carrier == nil {
return true
return
}
damage := e.calculateDamage()
@@ -81,7 +111,7 @@ func (e *ContinuousDamage) ActionStart(attacker, defender *action.SelectSkillAct
Damage: damage,
})
if len(e.SideEffectArgs) == 0 {
return true
return
}
// 额外效果
carrier.Damage(source, &info.DamageZone{
@@ -89,12 +119,11 @@ func (e *ContinuousDamage) ActionStart(attacker, defender *action.SelectSkillAct
Damage: damage,
})
if opp == nil || opp.CurPet[0].GetHP().IntPart() == 0 {
return true
return
}
// 给对方回血(不受回血限制影响)
opp.Heal(carrier, nil, damage)
return true
}
// 计算伤害最大生命值的1/8
@@ -131,15 +160,13 @@ func (e *ParasiticSeed) SwitchOut(in *input.Input) bool {
return true
}
// 技能命中前触发寄生效果
func (e *ParasiticSeed) ActionStartEx(attacker, defender *action.SelectSkillAction) bool {
// 回合开始触发寄生效果。寄生属于完整回合流程的一部分,不依赖本回合是否成功出手。
func (e *ParasiticSeed) TurnStart(attacker, defender *action.SelectSkillAction) {
carrier := e.CarrierInput()
source := e.SourceInput()
opp := e.TargetInput()
if carrier == nil {
return true
return
}
// 过滤特定类型单位假设1是植物类型使用枚举替代魔法数字
damage := alpacadecimal.NewFromInt(int64(carrier.CurPet[0].Info.MaxHp)).
Div(alpacadecimal.NewFromInt(8))
@@ -149,13 +176,12 @@ func (e *ParasiticSeed) ActionStartEx(attacker, defender *action.SelectSkillActi
Type: info.DamageType.True,
Damage: damage,
})
if opp == nil || opp.CurPet[0].GetHP().IntPart() == 0 {
return true
if source == nil || source.CurPet[0] == nil || source.CurPet[0].GetHP().IntPart() == 0 {
return
}
// 给对方回血(不受回血限制影响)
opp.Heal(carrier, nil, damage)
return true
// 给寄生种子的施放者回血(不受回血限制影响)
source.Heal(carrier, nil, damage)
}
type Flammable struct {
@@ -271,7 +297,6 @@ func init() {
// 批量注册不能行动的状态
nonActingStatuses := []info.EnumPetStatus{
info.PetStatus.Paralysis, // 麻痹
info.PetStatus.Tired, // 疲惫
info.PetStatus.Fear, // 害怕
info.PetStatus.Petrified, // 石化
}
@@ -281,6 +306,10 @@ func init() {
input.InitEffect(input.EffectType.Status, int(status), effect)
}
tired := &StatusTired{}
tired.Status = info.PetStatus.Tired
input.InitEffect(input.EffectType.Status, int(info.PetStatus.Tired), tired)
// 注册睡眠状态使用枚举常量替代硬编码8
input.InitEffect(input.EffectType.Status, int(info.PetStatus.Sleep), &StatusSleep{})
}

View File

@@ -83,6 +83,10 @@ func (e *Effect201) OnSkill() bool {
return true
}
if !carrier.IsMultiInputBattle() {
return true
}
divisorIndex := len(args) - 1
if len(args) > 1 {
divisorIndex = 1

View File

@@ -0,0 +1,91 @@
package effect
import (
"testing"
fightinfo "blazing/logic/service/fight/info"
"blazing/logic/service/fight/input"
"blazing/modules/player/model"
)
func newEffect201TestInput(hp, maxHP uint32) *input.Input {
in := &input.Input{
CurPet: []*fightinfo.BattlePetEntity{{
Info: model.PetInfo{
Hp: hp,
MaxHp: maxHP,
},
}},
}
in.AttackValue = fightinfo.NewAttackValue(0)
return in
}
func TestEffect201HealAllIgnoredInSingleInputBattle(t *testing.T) {
carrier := newEffect201TestInput(40, 100)
opponent := newEffect201TestInput(60, 100)
carrier.Team = []*input.Input{carrier}
carrier.OppTeam = []*input.Input{opponent}
eff := &Effect201{}
eff.SetArgs(carrier, 1, 2)
eff.EffectNode.EffectContextHolder.Ctx = input.Ctx{
LegacySides: input.LegacySides{Our: carrier, Opp: opponent},
EffectBinding: input.EffectBinding{Carrier: carrier, Source: carrier},
}
if !eff.OnSkill() {
t.Fatalf("expected effect to finish successfully")
}
if got := carrier.CurrentPet().Info.Hp; got != 40 {
t.Fatalf("expected single-input full-team heal to be ignored, got hp %d", got)
}
}
func TestEffect201SingleTargetIgnoredInSingleInputBattle(t *testing.T) {
carrier := newEffect201TestInput(40, 100)
opponent := newEffect201TestInput(60, 100)
carrier.Team = []*input.Input{carrier}
carrier.OppTeam = []*input.Input{opponent}
eff := &Effect201{}
eff.SetArgs(carrier, 2)
eff.EffectNode.EffectContextHolder.Ctx = input.Ctx{
LegacySides: input.LegacySides{Our: carrier, Opp: opponent},
EffectBinding: input.EffectBinding{Carrier: carrier, Source: carrier},
}
if !eff.OnSkill() {
t.Fatalf("expected effect to finish successfully")
}
if got := carrier.CurrentPet().Info.Hp; got != 40 {
t.Fatalf("expected single-input single-target heal to be ignored, got hp %d", got)
}
}
func TestEffect201HealAllWorksInMultiInputBattle(t *testing.T) {
carrier := newEffect201TestInput(40, 100)
ally := newEffect201TestInput(10, 80)
opponent := newEffect201TestInput(60, 100)
carrier.Team = []*input.Input{carrier, ally}
carrier.OppTeam = []*input.Input{opponent}
ally.Team = carrier.Team
ally.OppTeam = carrier.OppTeam
eff := &Effect201{}
eff.SetArgs(carrier, 1, 2)
eff.EffectNode.EffectContextHolder.Ctx = input.Ctx{
LegacySides: input.LegacySides{Our: carrier, Opp: opponent},
EffectBinding: input.EffectBinding{Carrier: carrier, Source: carrier},
}
if !eff.OnSkill() {
t.Fatalf("expected effect to finish successfully")
}
if got := carrier.CurrentPet().Info.Hp; got != 90 {
t.Fatalf("expected carrier hp 90 after full-team heal, got %d", got)
}
if got := ally.CurrentPet().Info.Hp; got != 50 {
t.Fatalf("expected ally hp 50 after full-team heal, got %d", got)
}
}

View File

@@ -5,3 +5,7 @@ import "blazing/logic/service/fight/input"
func initskill(id int, e input.Effect) {
input.InitEffect(input.EffectType.Skill, id, e)
}
func initskillFactory(id int, factory func() input.Effect) {
input.InitEffectFactory(input.EffectType.Skill, id, factory)
}

View File

@@ -158,7 +158,10 @@ func registerSelfDamageSkillHitEffects() {
}
for effectID, handler := range handlers {
initskill(effectID, newSkillHitRegistrarEffect(handler))
currentHandler := handler
initskillFactory(effectID, func() input.Effect {
return newSkillHitRegistrarEffect(currentHandler)
})
}
}
@@ -204,9 +207,15 @@ func registerSelfDamageOnSkillEffects() {
})
}
e.Ctx().Opp.Damage(e.Ctx().Our, &info.DamageZone{
Type: info.DamageType.Fixed,
Damage: opponentDamage,
forEachEnemyTargetBySkill(e.Ctx().Our, e.Ctx().Opp, e.Ctx().SkillEntity, func(target *input.Input) bool {
if target == nil || target.CurrentPet() == nil {
return true
}
target.Damage(e.Ctx().Our, &info.DamageZone{
Type: info.DamageType.Fixed,
Damage: opponentDamage,
})
return true
})
return true
},
@@ -223,7 +232,10 @@ func registerSelfDamageOnSkillEffects() {
}
for effectID, handler := range handlers {
initskill(effectID, newOnSkillRegistrarEffect(handler))
currentHandler := handler
initskillFactory(effectID, func() input.Effect {
return newOnSkillRegistrarEffect(currentHandler)
})
}
}
@@ -235,9 +247,15 @@ func registerSelfDamageSkillUseEffects() {
Type: info.DamageType.Fixed,
Damage: damage,
})
e.Ctx().Opp.Damage(e.Ctx().Our, &info.DamageZone{
Type: info.DamageType.Fixed,
Damage: damage,
forEachEnemyTargetBySkill(e.Ctx().Our, e.Ctx().Opp, e.Ctx().SkillEntity, func(target *input.Input) bool {
if target == nil || target.CurrentPet() == nil {
return true
}
target.Damage(e.Ctx().Our, &info.DamageZone{
Type: info.DamageType.Fixed,
Damage: damage,
})
return true
})
return true
},
@@ -247,9 +265,23 @@ func registerSelfDamageSkillUseEffects() {
Damage: alpacadecimal.NewFromInt(int64(e.Ctx().Our.CurPet[0].Info.MaxHp)),
})
damage := int64(grand.N(250, 300))
e.Ctx().Opp.Damage(e.Ctx().Our, &info.DamageZone{
Type: info.DamageType.Fixed,
Damage: alpacadecimal.Min(alpacadecimal.NewFromInt(damage), e.Ctx().Opp.CurPet[0].GetHP().Sub(alpacadecimal.NewFromInt(1))),
forEachEnemyTargetBySkill(e.Ctx().Our, e.Ctx().Opp, e.Ctx().SkillEntity, func(target *input.Input) bool {
if target == nil {
return true
}
targetPet := target.CurrentPet()
if targetPet == nil {
return true
}
remainHP := targetPet.GetHP().Sub(alpacadecimal.NewFromInt(1))
if remainHP.Cmp(alpacadecimal.Zero) <= 0 {
return true
}
target.Damage(e.Ctx().Our, &info.DamageZone{
Type: info.DamageType.Fixed,
Damage: alpacadecimal.Min(alpacadecimal.NewFromInt(damage), remainHP),
})
return true
})
return true
},
@@ -274,15 +306,25 @@ func registerSelfDamageSkillUseEffects() {
randomDamage = grand.N(minDamage, maxDamage)
}
remainHP := e.Ctx().Opp.CurPet[0].GetHP().Sub(alpacadecimal.NewFromInt(1))
if remainHP.Cmp(alpacadecimal.Zero) <= 0 {
return true
}
forEachEnemyTargetBySkill(e.Ctx().Our, e.Ctx().Opp, e.Ctx().SkillEntity, func(target *input.Input) bool {
if target == nil {
return true
}
targetPet := target.CurrentPet()
if targetPet == nil {
return true
}
remainHP := targetPet.GetHP().Sub(alpacadecimal.NewFromInt(1))
if remainHP.Cmp(alpacadecimal.Zero) <= 0 {
return true
}
damage := alpacadecimal.Min(alpacadecimal.NewFromInt(int64(randomDamage)), remainHP)
e.Ctx().Opp.Damage(e.Ctx().Our, &info.DamageZone{
Type: info.DamageType.Fixed,
Damage: damage,
damage := alpacadecimal.Min(alpacadecimal.NewFromInt(int64(randomDamage)), remainHP)
target.Damage(e.Ctx().Our, &info.DamageZone{
Type: info.DamageType.Fixed,
Damage: damage,
})
return true
})
return true
},
@@ -291,11 +333,17 @@ func registerSelfDamageSkillUseEffects() {
return true
}
applyAllPropDown(e.Ctx().Our, e.Ctx().Opp, int8(e.Args()[0].IntPart()))
sub := e.Ctx().Our.InitEffect(input.EffectType.Sub, 1380, int(e.Args()[1].IntPart()), int(e.Args()[2].IntPart()))
if sub != nil {
e.Ctx().Opp.AddEffect(e.Ctx().Our, sub)
}
forEachEnemyTargetBySkill(e.Ctx().Our, e.Ctx().Opp, e.Ctx().SkillEntity, func(target *input.Input) bool {
if target == nil || target.CurrentPet() == nil {
return true
}
applyAllPropDown(e.Ctx().Our, target, int8(e.Args()[0].IntPart()))
sub := e.Ctx().Our.InitEffect(input.EffectType.Sub, 1380, int(e.Args()[1].IntPart()), int(e.Args()[2].IntPart()))
if sub != nil {
target.AddEffect(e.Ctx().Our, sub)
}
return true
})
e.Ctx().Our.Damage(e.Ctx().Our, &info.DamageZone{
Type: info.DamageType.Fixed,
Damage: e.Ctx().Our.CurPet[0].GetHP(),
@@ -305,7 +353,10 @@ func registerSelfDamageSkillUseEffects() {
}
for effectID, handler := range handlers {
initskill(effectID, newSkillUseRegistrarEffect(handler))
currentHandler := handler
initskillFactory(effectID, func() input.Effect {
return newSkillUseRegistrarEffect(currentHandler)
})
}
}
@@ -339,7 +390,10 @@ func registerSelfDamageComparePreOnSkillEffects() {
}
for effectID, effect := range effects {
initskill(effectID, effect)
currentEffect := effect
initskillFactory(effectID, func() input.Effect {
return newComparePreOnSkillRegistrarEffect(currentEffect.comparePreHandler, currentEffect.onSkillHandler)
})
}
}

View File

@@ -0,0 +1,34 @@
package effect
import (
"blazing/logic/service/fight/info"
"blazing/logic/service/fight/input"
)
// forEachEnemyTargetBySkill 在普通情况下对单个目标生效;
// 当技能为 AtkType=3仅自己且当前目标仍在己方时改为遍历敌方全部站位。
func forEachEnemyTargetBySkill(carrier, target *input.Input, skill *info.SkillEntity, fn func(*input.Input) bool) {
if fn == nil {
return
}
if carrier == nil {
if target != nil {
fn(target)
}
return
}
if skill == nil || skill.XML.AtkType != 3 || !isSameSideTarget(carrier, target) {
if target != nil {
fn(target)
}
return
}
for _, opponent := range carrier.OpponentSlots() {
if opponent == nil {
continue
}
if !fn(opponent) {
return
}
}
}

View File

@@ -0,0 +1,40 @@
package fight
import "blazing/modules/player/model"
// buildFightOverPayload builds the legacy 2506 payload expected by the flash client.
// Regular fight-over packets use a different reason mapping than group fight 7560:
// 0=normal end 1=player lost/offline 2=overtime 3=draw 4=system error 5=npc escape.
func buildFightOverPayload(over model.FightOverInfo) *model.FightOverInfo {
payload := over
payload.Reason = model.EnumBattleOverReason(mapUnifiedFightOverReason(over.Reason))
return &payload
}
func normalizeFightOverReason(reason model.EnumBattleOverReason) model.EnumBattleOverReason {
if reason == model.BattleOverReason.DefaultEnd {
return 0
}
return reason
}
func mapUnifiedFightOverReason(reason model.EnumBattleOverReason) uint32 {
switch normalizeFightOverReason(reason) {
case 0, model.BattleOverReason.Cacthok:
return 0
case model.BattleOverReason.PlayerOffline:
return 1
case model.BattleOverReason.PlayerOVerTime:
return 2
case model.BattleOverReason.NOTwind:
return 3
case model.BattleOverReason.PlayerEscape:
return 5
default:
return 4
}
}
func mapFightOverReasonFor2506(reason model.EnumBattleOverReason) model.EnumBattleOverReason {
return model.EnumBattleOverReason(mapUnifiedFightOverReason(reason))
}

Some files were not shown because too many files have changed in this diff Show More