Compare commits

...

111 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
xinian
2ee0cbc094 fix: 修复boss奖励发放逻辑
All checks were successful
ci/woodpecker/push/my-first-workflow Pipeline was successful
2026-04-05 07:28:39 +08:00
xinian
6510e4e09b refactor: 重构入参类型引用
All checks were successful
ci/woodpecker/push/my-first-workflow Pipeline was successful
2026-04-05 07:24:36 +08:00
xinian
34bc35a6b2 feat: 新增游戏协议入站结构体定义
All checks were successful
ci/woodpecker/push/my-first-workflow Pipeline was successful
2026-04-05 06:12:32 +08:00
xinian
8352d23164 refactor: 优化精灵背包仓库切换逻辑
All checks were successful
ci/woodpecker/push/my-first-workflow Pipeline was successful
2026-04-05 06:02:27 +08:00
xinian
e71971d0b4 refactor: 重构宠物背包逻辑到玩家服务
All checks were successful
ci/woodpecker/push/my-first-workflow Pipeline was successful
2026-04-05 05:47:25 +08:00
xinian
bceb7965f7 refactor: 重构宠物仓库列表获取逻辑 2026-04-05 05:32:39 +08:00
xinian
c3f052ef30 refactor: 移除 syncBackupPetList 调用和定义 2026-04-05 05:24:55 +08:00
xinian
7d054bbe91 feat: 实现跨服PVP匹配和战斗功能 2026-04-05 05:04:04 +08:00
xinian
102d87da3e 编辑文件 Dockerfile
Some checks failed
ci/woodpecker/push/my-first-workflow Pipeline failed
2026-04-05 02:27:37 +08:00
xinian
78a68148ce chore: update fight logic and effect implementations
Some checks failed
ci/woodpecker/push/my-first-workflow Pipeline failed
2026-04-05 02:25:44 +08:00
xinian
f473c54880 feat: 支持多站位战斗控制绑定模式
All checks were successful
ci/woodpecker/push/my-first-workflow Pipeline was successful
2026-04-05 00:03:32 +08:00
xinian
2eba4b7915 feat: 实现乱舞效果并完善战斗输入上下文
All checks were successful
ci/woodpecker/push/my-first-workflow Pipeline was successful
2026-04-04 22:39:56 +08:00
xinian
39e1d4c42f refactor: 重构战斗结构体以支持双打模式 2026-04-04 22:13:42 +08:00
昔念
7916f90992 ```
All checks were successful
ci/woodpecker/push/my-first-workflow Pipeline was successful
refactor(player): 移除废弃的宠物列表计数字段

移除了 PlayerInfo 结构体中不再使用的 PetListCount 字段,
该字段为旧登录协议中的精灵列表长度,现已废弃并不再使用。
```
2026-04-04 14:00:36 +08:00
昔念
8ac2833ce2 1 2026-04-04 13:52:57 +08:00
xinian
fbc845526b refactor: 优化控制器初始化和命令解析逻辑
All checks were successful
ci/woodpecker/push/my-first-workflow Pipeline was successful
2026-04-04 09:33:31 +08:00
xinian
257a979f93 refactor: 重构效果参数处理逻辑
All checks were successful
ci/woodpecker/push/my-first-workflow Pipeline was successful
2026-04-04 09:26:57 +08:00
xinian
ce7be73e49 fix: 修复 CODEX_BASE_URL 双斜杠问题
All checks were successful
ci/woodpecker/push/my-first-workflow Pipeline was successful
2026-04-04 08:59:40 +08:00
xinian
28f2199142 编辑文件 Dockerfile
All checks were successful
ci/woodpecker/push/my-first-workflow Pipeline was successful
2026-04-04 08:48:25 +08:00
xinian
80cfa0a07e refactor: 替换过时的上下文访问方法
All checks were successful
ci/woodpecker/push/my-first-workflow Pipeline was successful
2026-04-04 07:26:08 +08:00
xinian
c89632b409 refactor: 重构效果系统中的上下文引用 2026-04-04 07:22:28 +08:00
xinian
5a5a1db2a3 refactor: 迁移 effect 至新语义上下文 2026-04-04 07:06:00 +08:00
xinian
0ac84a9509 新纪元 2026-04-04 06:27:15 +08:00
xinian
3a9932e307 refactor: 重、、、、 2026-04-04 06:11:01 +08:00
xinian
28d92c1e18 refactor: 重构战斗系统支持多单位多动作 2026-04-04 05:44:02 +08:00
xinian
b62b4af628 style: 清理代码注释和格式 2026-04-04 05:12:30 +08:00
xinian
31d274dd9d feat: 新增战斗效果1630-1634及1609-1624 2026-04-04 04:58:49 +08:00
384 changed files with 18965 additions and 367172 deletions

1
.gitignore vendored
View File

@@ -47,3 +47,4 @@ public/login-linux-amd64
public/login-login-linux-amd64 public/login-login-linux-amd64
public/logic_linux-amd64_1 public/logic_linux-amd64_1
.cache/** .cache/**
.agents/**

View File

@@ -18,9 +18,11 @@ ENV GOMODCACHE=/workspace/.cache/gomod
# ========================================== # ==========================================
# 2. Codex 配置 (更换时修改这里重新 build) # 2. Codex 配置 (更换时修改这里重新 build)
# ========================================== # ==========================================
ENV CODEX_BASE_URL="http://43.153.195.6:8080/v1" ENV CODEX_BASE_URL="https://api.jucode.cn/v1"
ENV CODEX_MODEL="gpt-5.4" ENV CODEX_MODEL="gpt-5.4"
ENV OPENAI_API_KEY="sk-bd7cb9893fe234fef37fa09d017f3145007c6472586f78d9986b35119515dab5"
ENV OPENAI_API_KEY="sk-E0ZZIFNnD0RkhMC9pT2AGMutz9vNy2VLNrgyyobT5voa81pQ"
# ========================================== # ==========================================
# 3. 安装系统依赖GolangCode-server # 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://zread.ai/tawer-blog/lmarena-2api/1-overview GLM web2 pai
https://crazyrouter.com/console 模型最便宜,看看能不能1:10 https://crazyrouter.com/console 模型最便宜,看看能不能1:10
@@ -14,7 +9,7 @@ https://crazyrouter.com/console 模型最便宜,看看能不能1:10
https://agentrouter.org/pricing 签到给,有175 https://agentrouter.org/pricing 签到给,有175
kuaipao.ai 充了十块 cjf19970621 cjf19970621
充了十块 充了十块
使用网址https://www.jnm.lol 使用网址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", "request": "launch",
"mode": "auto", "mode": "auto",
"cwd": "${workspaceFolder}", "cwd": "${workspaceFolder}",
"args": ["-id=2"], "args": ["-id=99"],
"program": "${workspaceFolder}/logic" "program": "${workspaceFolder}/logic"
} }

View File

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

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

View File

@@ -3,6 +3,7 @@ package cool
import ( import (
_ "blazing/contrib/drivers/pgsql" _ "blazing/contrib/drivers/pgsql"
"blazing/cool/cooldb" "blazing/cool/cooldb"
"sync"
"github.com/gogf/gf/v2/encoding/gjson" "github.com/gogf/gf/v2/encoding/gjson"
"github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/frame/g"
@@ -10,6 +11,11 @@ import (
"gorm.io/gorm" "gorm.io/gorm"
) )
var (
autoMigrateMu sync.Mutex
autoMigrateModels []IModel
)
// 初始化数据库连接供gorm使用 // 初始化数据库连接供gorm使用
func InitDB(group string) (*gorm.DB, error) { func InitDB(group string) (*gorm.DB, error) {
// var ctx context.Context // var ctx context.Context
@@ -54,9 +60,33 @@ func getDBbyModel(model IModel) *gorm.DB {
// 根据entity结构体创建表 // 根据entity结构体创建表
func CreateTable(model IModel) error { 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) db := getDBbyModel(model)
return db.AutoMigrate(model) if err := db.AutoMigrate(model); err != nil {
return err
}
} }
return nil return nil
} }

View File

@@ -6,6 +6,16 @@ func AddClient(id uint32, client *ClientHandler) {
Clientmap.Store(id, client) // sync.Map存值 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) { func GetClient(id, port uint32) (*ClientHandler, bool) {
// 普通mapclient, ok := Clientmap[id] // 普通mapclient, ok := Clientmap[id]

View File

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

View File

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

View File

@@ -5,7 +5,7 @@ import (
_ "blazing/common/data/xmlres/packed" _ "blazing/common/data/xmlres/packed"
"encoding/json" "encoding/json"
"os" "fmt"
"github.com/ECUST-XX/xml" "github.com/ECUST-XX/xml"
"github.com/gogf/gf/v2/os/gres" "github.com/gogf/gf/v2/os/gres"
@@ -14,22 +14,36 @@ import (
var path string var path string
func readConfigContent(path string) []byte {
return gres.GetContent(path)
}
func getXml[T any](path string) T { func getXml[T any](path string) T {
// 解析XML到结构体 // 解析XML到结构体
var xmls T var xmls T
t1 := gres.GetContent(path) t1 := readConfigContent(path)
xml.Unmarshal(t1, &xmls) xml.Unmarshal(t1, &xmls)
return xmls return xmls
} }
func getJson[T any](path string) T { func getJson[T any](path string) T {
// 解析XML到结构体 // 解析JSON到结构体
var xmls T var xmls T
t1 := gres.GetContent(path) t1 := readConfigContent(path)
json.Unmarshal(t1, &xmls) 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 return xmls
} }
@@ -58,8 +72,6 @@ var (
func Initfile() { func Initfile() {
//gres.Dump() //gres.Dump()
path1, _ := os.Getwd()
path = path1 + "/public/config/"
path = "config/" path = "config/"
MapConfig = getXml[Maps](path + "210.xml") MapConfig = getXml[Maps](path + "210.xml")
@@ -87,10 +99,10 @@ func Initfile() {
return gconv.Int(m.ProductID) 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)) SkillMap = make(map[int]Move, len(skillConfig.MovesTbl.Moves.Move))
for _, v := range Skill.Moves { for _, v := range skillConfig.MovesTbl.Moves.Move {
v.SideEffectS = ParseSideEffectArgs(v.SideEffect) v.SideEffectS = ParseSideEffectArgs(v.SideEffect)
v.SideEffectArgS = ParseSideEffectArgs(v.SideEffectArg) v.SideEffectArgS = ParseSideEffectArgs(v.SideEffectArg)
SkillMap[v.ID] = v 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 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 package xmlres
import "github.com/ECUST-XX/xml" import (
"strconv"
"strings"
"github.com/ECUST-XX/xml"
)
// Move 表示怪物可学习的技能 // Move 表示怪物可学习的技能
type PetMoves struct { type PetMoves struct {
@@ -45,6 +50,7 @@ type PetInfo struct {
Recycle int `xml:"Recycle,attr"` // 是否可回收 Recycle int `xml:"Recycle,attr"` // 是否可回收
LearnableMoves LearnableMoves `xml:"LearnableMoves"` // 可学习的技能 LearnableMoves LearnableMoves `xml:"LearnableMoves"` // 可学习的技能
NaturalEnemy string `xml:"NaturalEnemy,attr"` //天敌 NaturalEnemy string `xml:"NaturalEnemy,attr"` //天敌
YieldingEVValues []int64 `xml:"-"` // 预解析后的努力值奖励
} }
func (basic *PetInfo) GetBasic() uint32 { func (basic *PetInfo) GetBasic() uint32 {
@@ -61,3 +67,16 @@ type Monsters struct {
XMLName xml.Name `xml:"Monsters"` XMLName xml.Name `xml:"Monsters"`
Monsters []PetInfo `xml:"Monster"` 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 package xmlres
import ( import (
"encoding/json"
"encoding/xml" "encoding/xml"
"fmt" "fmt"
"io" "io"
@@ -33,51 +34,156 @@ type MovesTbl struct {
Moves []Move `xml:"Moves>Move"` Moves []Move `xml:"Moves>Move"`
EFF []SideEffect `xml:"SideEffects>SideEffect"` 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 { type MovesMap struct {
XMLName xml.Name `xml:"MovesTbl"` XMLName xml.Name `xml:"MovesTbl"`
Moves map[int]Move Moves map[int]Move
EFF []SideEffect `xml:"SideEffects>SideEffect"` 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 定义单个技能的结构 // Move 定义单个技能的结构
type Move struct { type Move struct {
ID int `xml:"ID,attr"` ID int `xml:"ID,attr" json:"ID"`
Name string `xml:"Name,attr"` Name string `xml:"Name,attr" json:"Name"`
Category int `xml:"Category,attr"` //属性 Category int `xml:"Category,attr" json:"Category"` //属性
Type int `xml:"Type,attr"` //类型 Type int `xml:"Type,attr" json:"Type"` //类型
Power int `xml:"Power,attr"` //威力 Power int `xml:"Power,attr" json:"Power"` //威力
MaxPP int `xml:"MaxPP,attr"` //最大PP MaxPP int `xml:"MaxPP,attr" json:"MaxPP"` //最大PP
Accuracy int `xml:"Accuracy,attr"` //命中率 Accuracy int `xml:"Accuracy,attr" json:"Accuracy"` //命中率
CritRate int `xml:"CritRate,attr,omitempty"` //暴击率 CritRate int `xml:"CritRate,attr,omitempty" json:"CritRate,omitempty"` //暴击率
Priority int `xml:"Priority,attr,omitempty"` //优先级 Priority int `xml:"Priority,attr,omitempty" json:"Priority,omitempty"` //优先级
MustHit int `xml:"MustHit,attr,omitempty"` //是否必中 MustHit int `xml:"MustHit,attr,omitempty" json:"MustHit,omitempty"` //是否必中
SwapElemType int `xml:"SwapElemType,attr,omitempty"` //技能交换属性 SwapElemType int `xml:"SwapElemType,attr,omitempty" json:"SwapElemType,omitempty"` //技能交换属性
CopyElemType int `xml:"CopyElemType,attr,omitempty"` // 技能复制属性 CopyElemType int `xml:"CopyElemType,attr,omitempty" json:"CopyElemType,omitempty"` // 技能复制属性
CritAtkFirst int `xml:"CritAtkFirst,attr,omitempty"` // 先出手时必定致命一击 CritAtkFirst int `xml:"CritAtkFirst,attr,omitempty" json:"CritAtkFirst,omitempty"` // 先出手时必定致命一击
CritAtkSecond int `xml:"CritAtkSecond,attr,omitempty"` //后出手时必定致命一击 CritAtkSecond int `xml:"CritAtkSecond,attr,omitempty" json:"CritAtkSecond,omitempty"` //后出手时必定致命一击
CritSelfHalfHp int `xml:"CritSelfHalfHp,attr,omitempty"` //自身体力低于一半时必定致命一击 CritSelfHalfHp int `xml:"CritSelfHalfHp,attr,omitempty" json:"CritSelfHalfHp,omitempty"` //自身体力低于一半时必定致命一击
CritFoeHalfHp int `xml:"CritFoeHalfHp,attr,omitempty"` //对方体力低于一半时必定致命一击 CritFoeHalfHp int `xml:"CritFoeHalfHp,attr,omitempty" json:"CritFoeHalfHp,omitempty"` //对方体力低于一半时必定致命一击
DmgBindLv int `xml:"DmgBindLv,attr,omitempty"` //使对方受到的伤害值等于自身的等级 DmgBindLv int `xml:"DmgBindLv,attr,omitempty" json:"DmgBindLv,omitempty"` //使对方受到的伤害值等于自身的等级
PwrBindDv int `xml:"PwrBindDv,attr,omitempty"` //威力power取决于自身的潜力个体值 PwrBindDv int `xml:"PwrBindDv,attr,omitempty" json:"PwrBindDv,omitempty"` //威力power取决于自身的潜力个体值
PwrDouble int `xml:"PwrDouble,attr,omitempty"` //攻击时,若对方处于异常状态, 则威力翻倍; PwrDouble int `xml:"PwrDouble,attr,omitempty" json:"PwrDouble,omitempty"` //攻击时,若对方处于异常状态, 则威力翻倍;
DmgBindHpDv int `xml:"DmgBindHpDv,attr,omitempty"` //使对方受到的伤害值等于自身的体力值 DmgBindHpDv int `xml:"DmgBindHpDv,attr,omitempty" json:"DmgBindHpDv,omitempty"` //使对方受到的伤害值等于自身的体力值
SideEffect string `xml:"SideEffect,attr,omitempty"` SideEffect string `xml:"SideEffect,attr,omitempty" json:"SideEffect,omitempty"`
SideEffectArg string `xml:"SideEffectArg,attr,omitempty"` SideEffectArg string `xml:"SideEffectArg,attr,omitempty" json:"SideEffectArg,omitempty"`
SideEffectS []int SideEffectS []int
SideEffectArgS []int SideEffectArgS []int
AtkNum int `xml:"AtkNum,attr,omitempty"` AtkNum int `xml:"AtkNum,attr,omitempty" json:"AtkNum,omitempty"`
Url string `xml:"Url,attr,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 { type SideEffect struct {
ID int `xml:"ID,attr"` ID int `xml:"ID,attr" json:"ID"`
Help string `xml:"help,attr"` Help string `xml:"help,attr" json:"help"`
Des string `xml:"des,attr"` Des string `xml:"des,attr" json:"des"`
} }
// ReadHTTPFile 通过HTTP GET请求获取远程文件内容 // ReadHTTPFile 通过HTTP GET请求获取远程文件内容

View File

@@ -2,17 +2,21 @@ package rpc
import ( import (
"blazing/cool" "blazing/cool"
"context" "blazing/logic/service/fight/pvp"
"blazing/logic/service/fight/pvpwire"
"fmt" "fmt"
"time" "time"
"github.com/gogf/gf/v2/database/gredis" "github.com/gogf/gf/v2/database/gredis"
"github.com/gogf/gf/v2/errors/gerror" "github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/util/gconv"
) )
// ListenFunc 监听函数 // ListenFunc 监听函数
// ListenFunc 改造后的 Redis PubSub 监听函数,支持自动重连和心跳保活 // ListenFunc 改造后的 Redis PubSub 监听函数,支持自动重连
// 注意PubSub 连接只负责订阅和接收,避免在同一连接上并发 PING。
func ListenFunc(ctx g.Ctx) { func ListenFunc(ctx g.Ctx) {
if !cool.IsRedisMode { if !cool.IsRedisMode {
panic(gerror.New("集群模式下, 请使用Redis作为缓存")) panic(gerror.New("集群模式下, 请使用Redis作为缓存"))
@@ -22,7 +26,6 @@ func ListenFunc(ctx g.Ctx) {
const ( const (
subscribeTopic = "cool:func" // 订阅的主题 subscribeTopic = "cool:func" // 订阅的主题
retryDelay = 10 * time.Second // 连接失败重试间隔 retryDelay = 10 * time.Second // 连接失败重试间隔
heartbeatInterval = 30 * time.Second // 心跳保活间隔
) )
// 外层循环:负责连接断开后的整体重连 // 外层循环:负责连接断开后的整体重连
@@ -43,47 +46,25 @@ func ListenFunc(ctx g.Ctx) {
continue continue
} }
// 2. 启动心跳保活协程,防止连接因空闲被断开 // 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. 订阅主题
_, err = conn.Do(ctx, "subscribe", subscribeTopic) _, err = conn.Do(ctx, "subscribe", subscribeTopic)
if err != nil { if err != nil {
cool.Logger.Error(ctx, "订阅 Redis 主题失败", "topic", subscribeTopic, "error", err) cool.Logger.Error(ctx, "订阅 Redis 主题失败", "topic", subscribeTopic, "error", err)
heartbeatCancel() // 关闭心跳协程
_ = conn.Close(ctx) _ = conn.Close(ctx)
time.Sleep(retryDelay) time.Sleep(retryDelay)
continue continue
} }
cool.Logger.Info(ctx, "成功订阅 Redis 主题", "topic", subscribeTopic) cool.Logger.Info(ctx, "成功订阅 Redis 主题", "topic", subscribeTopic)
_, err = conn.Do(ctx, "subscribe", "sun:join") //加入队列 _, 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 connError := false
for !connError { for !connError {
select { select {
@@ -126,15 +107,15 @@ func ListenFunc(ctx g.Ctx) {
} }
} }
// 5. 清理资源,准备重连 // 4. 清理资源,准备重连
heartbeatCancel() // 关闭心跳协程
_ = conn.Close(ctx) // 关闭当前连接 _ = conn.Close(ctx) // 关闭当前连接
// Logger.Warn(ctx, "Redis 连接异常,准备重连", "retry_after", retryDelay) cool.Logger.Info(ctx, "Redis 订阅连接异常,准备重连", "retry_after", retryDelay)
time.Sleep(retryDelay) time.Sleep(retryDelay)
} }
} }
// ListenFight 完全对齐 ListenFunc 写法,修复收不到消息问题 // ListenFight 完全对齐 ListenFunc 写法,修复收不到消息问题
// 注意PubSub 连接只负责订阅和接收,避免在同一连接上并发 PING。
func ListenFight(ctx g.Ctx) { func ListenFight(ctx g.Ctx) {
if !cool.IsRedisMode { if !cool.IsRedisMode {
panic(gerror.New("集群模式下, 请使用Redis作为缓存")) panic(gerror.New("集群模式下, 请使用Redis作为缓存"))
@@ -143,13 +124,14 @@ func ListenFight(ctx g.Ctx) {
// 定义常量配置(对齐 ListenFunc 风格) // 定义常量配置(对齐 ListenFunc 风格)
const ( const (
retryDelay = 10 * time.Second // 连接失败重试间隔 retryDelay = 10 * time.Second // 连接失败重试间隔
heartbeatInterval = 30 * time.Second // 心跳保活间隔
) )
// 提前拼接订阅主题(避免重复拼接,便于日志打印) // 提前拼接订阅主题(避免重复拼接,便于日志打印)
serverID := cool.Config.ServerInfo.GetID() serverID := cool.Config.ServerInfo.GetID()
startTopic := "sun:start:" + serverID startTopic := "sun:start:" + serverID
sendPackTopic := "sendpack:" + serverID sendPackTopic := "sendpack:" + serverID
pvpServerTopic := pvpwire.ServerTopic(gconv.Uint32(serverID))
pvpCoordinatorTopic := pvpwire.CoordinatorTopicPrefix
// 外层循环:负责连接断开后的整体重连 // 外层循环:负责连接断开后的整体重连
for { for {
@@ -170,45 +152,26 @@ func ListenFight(ctx g.Ctx) {
continue continue
} }
// 2. 启动心跳保活协程(完全对齐 ListenFunc 逻辑 // 2. 订阅主题(对齐 ListenFunc 的错误处理,替换 panic 为优雅重连
heartbeatCtx, heartbeatCancel := context.WithCancel(context.Background()) subscribeTopics := []string{startTopic, pvpServerTopic}
go func() { if cool.Config.GameOnlineID == pvp.CoordinatorOnlineID {
ticker := time.NewTicker(heartbeatInterval) subscribeTopics = append(subscribeTopics, pvpCoordinatorTopic)
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 心跳发送成功,连接正常") subscribeFailed := false
} for _, topic := range subscribeTopics {
} _, err = conn.Do(ctx, "subscribe", topic)
}()
// 3. 订阅主题(对齐 ListenFunc 的错误处理,替换 panic 为优雅重连)
// 订阅 sun:start:服务器ID
_, err = conn.Do(ctx, "subscribe", startTopic)
if err != nil { if err != nil {
cool.Logger.Error(ctx, "订阅 Redis 主题失败", "topic", startTopic, "error", err) cool.Logger.Error(ctx, "订阅 Redis 主题失败", "topic", topic, "error", err)
heartbeatCancel() // 关闭心跳协程
_ = conn.Close(ctx) _ = conn.Close(ctx)
time.Sleep(retryDelay) time.Sleep(retryDelay)
subscribeFailed = true
break
}
cool.Logger.Info(ctx, "成功订阅 Redis 主题", "topic", topic)
}
if subscribeFailed {
continue continue
} }
cool.Logger.Info(ctx, "成功订阅 Redis 主题", "topic", startTopic)
// // 订阅 sun:sendpack:服务器ID // // 订阅 sun:sendpack:服务器ID
// _, err = conn.Do(ctx, "subscribe", sendPackTopic) // _, err = conn.Do(ctx, "subscribe", sendPackTopic)
@@ -224,7 +187,7 @@ func ListenFight(ctx g.Ctx) {
// 打印监听提示(保留原有日志) // 打印监听提示(保留原有日志)
fmt.Println("监听战斗", startTopic) fmt.Println("监听战斗", startTopic)
// 4. 循环接收消息(完全对齐 ListenFunc 逻辑) // 3. 循环接收消息(完全对齐 ListenFunc 逻辑)
connError := false connError := false
for !connError { for !connError {
select { select {
@@ -255,6 +218,10 @@ func ListenFight(ctx g.Ctx) {
// universalClient, _ := g.Redis("cool").Client().(goredis.UniversalClient) // universalClient, _ := g.Redis("cool").Client().(goredis.UniversalClient)
} }
if dataMap.Channel == pvpServerTopic || dataMap.Channel == pvpCoordinatorTopic {
pvp.HandleRedisMessage(dataMap.Channel, dataMap.Payload)
}
// 【可选】处理 sun:sendpack:服务器ID 消息(如果需要) // 【可选】处理 sun:sendpack:服务器ID 消息(如果需要)
if dataMap.Channel == sendPackTopic { if dataMap.Channel == sendPackTopic {
fmt.Println("收到战斗包", dataMap.Payload) fmt.Println("收到战斗包", dataMap.Payload)
@@ -262,9 +229,9 @@ func ListenFight(ctx g.Ctx) {
} }
} }
// 5. 清理资源,准备重连(完全对齐 ListenFunc // 4. 清理资源,准备重连(完全对齐 ListenFunc
heartbeatCancel() // 关闭心跳协程
_ = conn.Close(ctx) // 关闭当前连接 _ = conn.Close(ctx) // 关闭当前连接
cool.Logger.Info(ctx, "Redis 战斗订阅连接异常,准备重连", "retry_after", retryDelay)
time.Sleep(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

@@ -7,6 +7,7 @@ import (
"context" "context"
"fmt" "fmt"
"log" "log"
"time"
config "blazing/modules/config/service" config "blazing/modules/config/service"
@@ -17,22 +18,64 @@ import (
// Define the server handler // Define the server handler
type ServerHandler struct{} type ServerHandler struct{}
const kickForwardTimeout = 3 * time.Second
// 实现踢人 // 实现踢人
func (*ServerHandler) Kick(_ context.Context, userid uint32) error { func (*ServerHandler) Kick(_ context.Context, userid uint32) error {
useid1, err := share.ShareManager.GetUserOnline(userid)
useid1, _ := share.ShareManager.GetUserOnline(userid) if err != nil || useid1 == 0 {
if useid1 == 0 { // 请求到达时用户已离线,直接视为成功
return nil return nil
} }
cl, ok := cool.GetClientOnly(useid1) cl, ok := cool.GetClientOnly(useid1)
if !ok { if !ok || cl == nil {
// 目标服务器不在线,清理僵尸在线标记并视为成功
_ = share.ShareManager.DeleteUserOnline(userid)
cool.DeleteClientOnly(useid1)
return nil return nil
} }
cl.KickPerson(userid) //实现指定服务器踢人
resultCh := make(chan error, 1)
go func() {
resultCh <- cl.KickPerson(userid) // 实现指定服务器踢人
}()
select {
case callErr := <-resultCh:
if callErr == nil {
return 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服务器 // 注册logic服务器
func (*ServerHandler) RegisterLogic(ctx context.Context, id, port uint32) error { func (*ServerHandler) RegisterLogic(ctx context.Context, id, port uint32) error {
fmt.Println("注册logic服务器", id, port) fmt.Println("注册logic服务器", id, port)
@@ -55,6 +98,15 @@ func (*ServerHandler) RegisterLogic(ctx context.Context, id, port uint32) error
} }
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 { func CServer() *jsonrpc.RPCServer {
// create a new server instance // create a new server instance
rpcServer := jsonrpc.NewServer(jsonrpc.WithReverseClient[cool.ClientHandler]("")) rpcServer := jsonrpc.NewServer(jsonrpc.WithReverseClient[cool.ClientHandler](""))
@@ -71,6 +123,10 @@ func StartClient(id, port uint32, callback any) *struct {
Kick func(uint32) error Kick func(uint32) error
RegisterLogic func(uint32, uint32) error RegisterLogic func(uint32, uint32) error
MatchJoinOrUpdate func(PVPMatchJoinPayload) error
MatchCancel func(uint32) error
} { } {
//cool.Config.File.Domain = "127.0.0.1" //cool.Config.File.Domain = "127.0.0.1"
var rpcaddr = "ws://" + cool.Config.File.Domain + gconv.String(cool.Config.Address) + "/rpc" var rpcaddr = "ws://" + cool.Config.File.Domain + gconv.String(cool.Config.Address) + "/rpc"
@@ -101,6 +157,10 @@ var RPCClient struct {
RegisterLogic func(uint32, uint32) error RegisterLogic func(uint32, uint32) error
MatchJoinOrUpdate func(PVPMatchJoinPayload) error
MatchCancel func(uint32) error
// UserLogin func(int32, int32) error //用户登录事件 // UserLogin func(int32, int32) error //用户登录事件
// UserLogout func(int32, int32) error //用户登出事件 // UserLogout func(int32, int32) error //用户登出事件
} }

View File

@@ -1,24 +1,29 @@
package socket package socket
import ( import (
"blazing/common/socket/codec"
"blazing/cool"
"blazing/logic/service/player"
"blazing/modules/config/service"
"bytes"
"context" "context"
"encoding/binary" "encoding/binary"
"errors" "errors"
"io"
"log" "log"
"os" "os"
"sync/atomic" "sync/atomic"
"time" "time"
"blazing/cool"
"blazing/logic/service/player"
"blazing/modules/config/service"
"github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gtime" "github.com/gogf/gf/v2/os/gtime"
"github.com/panjf2000/gnet/v2" "github.com/panjf2000/gnet/v2"
) )
const (
minPacketLen = 17
maxPacketLen = 10 * 1024
)
func (s *Server) Boot(serverid, port uint32) error { func (s *Server) Boot(serverid, port uint32) error {
// go s.bootws() // go s.bootws()
s.serverid = serverid s.serverid = serverid
@@ -53,62 +58,43 @@ func (s *Server) Stop() error {
func (s *Server) OnClose(c gnet.Conn, err error) (action gnet.Action) { func (s *Server) OnClose(c gnet.Conn, err error) (action gnet.Action) {
defer func() { defer func() {
if err := recover(); err != nil { // 恢复 panicerr 为 panic 错误值 if err := recover(); err != nil { // 恢复 panicerr 为 panic 错误值
// 1. 打印错误信息
if t, ok := c.Context().(*player.ClientData); ok { if t, ok := c.Context().(*player.ClientData); ok {
if t.Player != nil { if t.Player != nil {
if t.Player.Info != nil { if t.Player.Info != nil {
cool.Logger.Error(context.TODO(), "OnClose 错误:", cool.Config.ServerInfo.OnlineID, t.Player.Info.UserID, err) 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 { } else {
cool.Logger.Error(context.TODO(), "OnClose 错误:", cool.Config.ServerInfo.OnlineID, err) 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) atomic.AddInt64(&cool.Connected, -1)
//logging.Infof("conn[%v] disconnected", c.RemoteAddr().String())
v, _ := c.Context().(*player.ClientData) v, _ := c.Context().(*player.ClientData)
if v != nil {
v.LF.Close() v.Close()
// v.LF.Close()
//close(v.MsgChan)
if v.Player != nil { if v.Player != nil {
v.Player.Save() //保存玩家数据 v.Player.Save() //保存玩家数据
} }
}
//}
//关闭连接
return return
} }
func (s *Server) OnTick() (delay time.Duration, action gnet.Action) { 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)) 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 { if s.quit && atomic.LoadInt64(&cool.Connected) == 0 {
//执行正常退出逻辑
os.Exit(0) os.Exit(0)
} }
return 30 * time.Second, gnet.None return 30 * time.Second, gnet.None
} }
func (s *Server) OnBoot(eng gnet.Engine) gnet.Action { func (s *Server) OnBoot(eng gnet.Engine) gnet.Action {
s.eng = eng s.eng = eng
service.NewServerService().SetServerID(s.serverid, s.port)
service.NewServerService().SetServerID(s.serverid, s.port) //设置当前服务器端口
return gnet.None return gnet.None
} }
@@ -116,59 +102,68 @@ func (s *Server) OnOpen(conn gnet.Conn) (out []byte, action gnet.Action) {
if s.network != "tcp" { if s.network != "tcp" {
return nil, gnet.Close return nil, gnet.Close
} }
if conn.Context() == nil { if conn.Context() == nil {
conn.SetContext(player.NewClientData(conn)) //注入data conn.SetContext(player.NewClientData(conn))
} }
atomic.AddInt64(&cool.Connected, 1) atomic.AddInt64(&cool.Connected, 1)
return nil, gnet.None return nil, gnet.None
} }
func (s *Server) OnTraffic(c gnet.Conn) (action gnet.Action) { func (s *Server) OnTraffic(c gnet.Conn) (action gnet.Action) {
defer func() { defer func() {
if err := recover(); err != nil { // 恢复 panicerr 为 panic 错误值 if err := recover(); err != nil {
// 1. 打印错误信息
if t, ok := c.Context().(*player.ClientData); ok { if t, ok := c.Context().(*player.ClientData); ok {
if t.Player != nil { if t.Player != nil && t.Player.Info != nil {
if t.Player.Info != nil {
cool.Logger.Error(context.TODO(), "OnTraffic 错误:", cool.Config.ServerInfo.OnlineID, t.Player.Info.UserID, err) cool.Logger.Error(context.TODO(), "OnTraffic 错误:", cool.Config.ServerInfo.OnlineID, t.Player.Info.UserID, err)
t.Player.Service.Info.Save(*t.Player.Info) t.Player.Service.Info.Save(*t.Player.Info)
} }
} }
}
} }
}() }()
ws := c.Context().(*player.ClientData).Wsmsg client := c.Context().(*player.ClientData)
if ws.Tcp { //升级失败时候防止缓冲区溢出 if s.discorse && !client.IsCrossDomainChecked() {
return s.handleTCP(c) 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) ws := client.Wsmsg
if tt == gnet.Close { if ws.Tcp {
return s.handleTCP(c)
}
readAction, inboundLen := ws.ReadBufferBytes(c)
if readAction == gnet.Close {
return gnet.Close return gnet.Close
} }
ok, action := ws.Upgrade(c) state, action := ws.Upgrade(c)
if action != gnet.None { //连接断开 if action != gnet.None {
return action return action
} }
if !ok { //升级失败,说明是tcp连接 if state == player.UpgradeNeedMoreData {
ws.Tcp = true return gnet.None
}
return s.handleTCP(c) 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) messages, err := ws.Decode(c)
if err != nil { if err != nil {
@@ -179,91 +174,93 @@ func (s *Server) OnTraffic(c gnet.Conn) (action gnet.Action) {
} }
for _, msg := range messages { for _, msg := range messages {
if !s.onevent(c, msg.Payload) {
s.onevent(c, msg.Payload) return gnet.Close
//t.OnEvent(msg.Payload) }
} }
return gnet.None return gnet.None
} }
const maxBodyLen = 10 * 1024 // 业务最大包体长度,按需调整
func (s *Server) handleTCP(conn gnet.Conn) (action gnet.Action) { 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() { //跨域检测 body, err := s.codec.Decode(conn)
handle(conn)
})
// handle(c)
// 先读取4字节的包长度
lenBuf, err := conn.Peek(4)
if err != nil { if err != nil {
if errors.Is(err, io.ErrShortBuffer) { if errors.Is(err, codec.ErrIncompletePacket) {
return return gnet.None
} }
return gnet.Close return gnet.Close
} }
if !s.onevent(conn, body) {
bodyLen := binary.BigEndian.Uint32(lenBuf)
if bodyLen > maxBodyLen {
return gnet.Close 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 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 gnet.Close
} }
} }
return action 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" 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" 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(probeLen)
data, err := c.Peek(len(TEXT))
if err != nil { if err != nil {
log.Printf("Error reading cross-domain request: %v", err) log.Printf("Error reading cross-domain request: %v", err)
return return false, false, gnet.Close
}
if !bytes.Equal(data, []byte(TEXT[:probeLen])) {
return false, true, gnet.None
}
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
} }
if string(data) == TEXT { //判断是否是跨域请求 func (s *Server) onevent(c gnet.Conn, v []byte) bool {
//log.Printf("Received cross-domain request from %s", c.RemoteAddr()) if !isValidPacket(v) {
// 处理跨域请求 return false
c.Write([]byte(CROSS_DOMAIN))
c.Discard(len(TEXT))
return
} }
//return
}
func (s *Server) onevent(c gnet.Conn, v []byte) {
if t, ok := c.Context().(*player.ClientData); ok { if t, ok := c.Context().(*player.ClientData); ok {
t.PushEvent(v, s.workerPool.Submit) 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 go 1.20
require ( require github.com/orcaman/concurrent-map/v2 v2.0.1
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
)

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 h1:jOJ5Pg2w1oeB6PeDurIYf6k9PQ+aTITr/6lP/L/zp6c=
github.com/orcaman/concurrent-map/v2 v2.0.1/go.mod h1:9Eq3TG2oBe5FirmYWQfYO5iH1q0Jv47PLaNK++uCdOM= 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

@@ -1,648 +0,0 @@
# Effect 重构会话总结2026-03-28
## 1. 本次会话完成内容
### 1.1 注释与说明统一
- 已将 `logic/service/fight/effect` 下效果注释统一为
- `// Effect <id>: <desc>`
- 说明来源
- `public/config/effectInfo.json`
### 1.2 结构整理与公共能力抽取
- 新增并使用了子效果统一挂载 helper
- `addSubEffect(...)`
- 已清理 `effect` 目录中的 `GenSub(...)` 直接调用残留统一走 helper
### 1.3 回合类基类组合继承已落地
- 当前已引入的 base位于 `logic/service/fight/effect/sub_effect_helper.go`
- `RoundEffectArg0Base`
- `RoundEffectSideArg0Base`
- `FixedDuration1Base`
- `FixedDurationNeg1Base`
- `FixedDuration2Base`
- `RoundEffectArg1Base`
- `RoundEffectSideArg1Base`
- `RoundEffectSideArg0Minus1Base`
- `RoundEffectSideArg0Minus1CanStackBase`
- 已有大量效果结构体改为嵌入上述 base删除重复 `SetArgs` 模板代码
### 1.4 编译状态
- 已多轮执行并通过
- `go test ./logic/service/fight/effect`
### 1.5 本轮新增 effect 实现
- 新增缺失效果实现
- `400` 若和对手属性相同则技能威力翻倍
- `480` `{0}`回合内自身所有攻击威力为两倍
- `586` `{0}`回合内自己的属性攻击必中
- `599` `{0}`回合内受到`{1}`伤害减少`{2}%`
- `610` 遇到天敌时先制+`{0}`
- `611` `{0}`回合自身使用攻击技能则附加`{1}`点固定伤害
- `613` `{0}`回合内自身令对手使用的`{1}`系攻击技能无效
- `573` `{0}`回合内若自身能力提升状态被消除或吸取则`{1}`%使对手`{2}``{3}`回合
- `587` `{0}`回合内若被对手击败则对手损失`{1}`点体力造成致命伤害时对手剩余1点体力
- `591` 造成伤害大于`{0}`则下`{1}`回合自己所有直接攻击先制+`{2}`
- `592` `{0}`回合每回合使用攻击技能`{1}`%令对手`{2}`
- `594` 造成的伤害低于`{0}``{1}`%令对手`{2}`
- `596` 技能使用成功时`{0}`%给予对手冻伤中毒烧伤中任意一种异常状态
- `597` `{0}`回合内每回合使用技能吸取对手最大体力的1/`{1}`
- `598` `{0}`%恢复自己所有技能PP值`{1}`
- `627` 对手处于能力提升状态时附加其`{0}``{1}`%的百分比伤害
- `628` 若对手处于能力下降状态则造成伤害的`{0}`%恢复体力
- `629` 消除`{0}`状态消除成功下回合自身先制+`{1}`
- `630` `{0}`回合内`{1}`状态被消除则有`{2}`%概率使对手`{3}`
- `631` 消除`{0}`状态消除成功下回合造成伤害提升`{1}`%
- `632` 造成伤害`{0}``{1}`则下`{2}`回合必定暴击
- `633` 造成伤害`{0}``{1}`则造成伤害的`{2}`%恢复体力
- `634` 若当前体力`{0}`对手则造成伤害的`{1}`%恢复体力
- `635` 吸收对手能力上升状态吸收成功下回合先制+`{0}`
- `636` 消除`{0}`状态消除成功则令对手`{1}`
- `637` 若对手处于异常状态则对手`{0}``{1}`
- `638` 若对手`{0}`技能威力提升`{1}`%
- `639` 造成伤害`{0}``{1}`则下`{2}`回合所有技能附带`{3}`点固定伤害
- `640` 命中后`{0}`%使对手`{1}``{2}`回合,遇到天敌概率翻倍
- `641` 命中后`{0}`%使对手进入流血状态
- `401` 若和对手属性相同则技能威力翻倍
- `585` 技能使用成功时`{0}`
- `589` 复制对手`{0}`的能力提升状态
- `590` 使对手`{0}``{6}`%弱化效果翻倍
- `593` 附加`{0}``{1}`值的`{2}`%的百分比伤害
- `595` 技能使用成功时`{0}`%使对手`{1}`若没有触发则对手`{2}`
- 已同步更新
- `logic/service/fight/effect/effect_info_map.go`
### 1.6 本轮新增文件
- `logic/service/fight/effect/400_480_586_599_610_611_613.go`
- `logic/service/fight/effect/573_587_591_592_594_596_597_598.go`
- `logic/service/fight/effect/627_631.go`
- `logic/service/fight/effect/632_636.go`
- `logic/service/fight/effect/637_641.go`
- `logic/service/fight/effect/effect_info_map.go`
### 1.7 本轮验证
- 已执行
- `go test ./service/fight/effect`
- 结果
- 通过
### 1.8 本轮结论
- 当前这轮更适合按低风险可复用现有模式 effect 小批次推进而不是一次性追求 `effectInfo.json` 全量覆盖
- 现有 `effect` 目录里已经有不少共享实现 + 批量注册的写法后续判断缺失项时不能只靠 grep `InitEffect(...)`
- 文档第 3 节里的未实现列表目前仍是历史扫描快照只能作为候选列表不能直接当最终事实使用
---
## 2. 当前仍保留自定义 `SetArgs` 的效果建议下一轮重点
以下属于非纯模板仍待抽象 SetArgs
- `Effect570``570.go`
- `Effect123``effect_119_123.go`
- `Effect41``effect_41.go`
- `Effect42``effect_42.go`
- `Effect46``effect_46.go`
- `Effect47``effect_47.go`
- `Effect48``effect_48.go`
- `Effect60``effect_60.go`
- `EffectPropSyncReverse``effect_attr.go`
- `SelfKill``selfkill.go`
建议分三类继续抽 base
- 随机回合/随机次数类 4142
- 次数型常驻类 464748SelfKill570
- SetArgs + 额外上下文初始化 Effect123EffectPropSyncReverse
---
## 3. 未实现或疑似未实现效果清单
### 3.0 `effectInfo.json` 提取的未实现总览自动扫描
- JSON 配置总效果数`2112`
- 代码已注册效果数Skill`338`
- JSON 中存在但代码未注册`1779`
- 代码中注册但 JSON 无对应条目`5``21, 31, 41, 42, 174`
说明
- 这个口径是配置覆盖率不是bug 数量
- 其中大量属于未来版本/未迁移内容不建议一次性全补建议按战斗系统实际启用范围分批实现
- 以下列表为上一轮扫描快照未随本轮新增实现实时回算
- 此外扫描脚本若未把共享实现中的批量注册统计进去也会把已实现效果误判成缺失
JSON 中存在但代码未注册示例前 60
- 2, 10, 11, 12, 14, 15, 16, 17, 22, 30
- 38, 40, 45, 51, 55, 56, 61, 64, 66, 67
- 70, 78, 84, 86, 92, 94, 96, 97, 99, 102
- 103, 104, 106, 108, 109, 114, 118, 132, 133, 139
- 141, 158, 162, 167, 168, 185, 401, 421, 431, 529
- 543, 554, 569, 573, 581, 582, 583, 584, 585, 586
### 3.1 明确标记未实装
- 文件存在明确标记
- `logic/service/fight/effect/529.go未实装`
### 3.2 已注册但缺少同名 `Effect{id}` 结构体需人工确认是否由合并实现覆盖
- `53`
- `74`
- `75`
- `186`
- `402`
- `433`
- `446`
- `451`
- `463`
- `497`
- `564`
- `588`
说明
- 这类通常可能是多个 id 共用一个结构体实现命名与 id 不一致需要逐个确认是否真正缺失行为
### 3.3 疑似未实现扫描规则存在 `Effect{id}` 类型但没有核心战斗 Hook
- `519``effect_519.go`
- `532``532.go`
- `552``552.go`
- `560``560.go`
- `576``576.go`
说明
- 这批是高优先级人工复查项不一定真的没实现可能通过组合/间接机制生效
### 3.4 当前更可信的下一批候选
这一组是结合本轮人工核对后仍然值得优先继续补的缺失 effect 候选
- 当前文档 3.4 中这批候选已在本轮补齐
说明
- 这批大多是 58x/59x 段的新效果和当前目录中已有实现重叠较少
- 相比继续深挖旧扫描误差这批更适合直接新增文件推进
---
## 4. 下次继续的建议顺序
建议严格按下面顺序继续不要重新从全量扫描开始
1. 先复核文档 3.4 里的候选项是否仍未实现
2. 优先补单次触发命中附加固定伤害恢复概率状态这类低风险逻辑
3. 再处理复制对手状态 / 多分支条件 / 触发链式子效果 58x/59x 复杂效果
4. 最后再回头处理文档 2 中那些仍保留自定义 `SetArgs` 的结构整理
本轮更推荐的下一批实现顺序
- 第一组`529 / 552 / 560 / 576`
- 第二组`10 / 11 / 12 / 14 / 15 / 16`
- 第三组`94 / 99 / 103 / 114`
---
## 5. 下一次继续让我实现时可直接复制的指令
可直接用下面这句发起
`继续处理 effect按 docs/effect-refactor-summary-2026-03-28.md 的 3.3 和 4 执行:先复核 529/552/560/576再补低风险状态附加类 10/11/12/14/15/16每实现一批就更新同一文档和 effect_info_map.go并跑 go test ./service/fight/effect。`
如果你希望按 JSON 覆盖率推进可用这句
`继续处理 effect按 docs/effect-refactor-summary-2026-03-28.md 的 3.0 从低风险效果开始补实现先补状态附加类10/11/12/14/15/16/94/99/103/114每实现一批就更新文档中的“已实现列表”和“剩余列表”。`
---
## 6. 扫描口径说明供后续排查
- 已注册效果 ID 扫描来源
- `InitEffect(input.EffectType.Skill, <id>, ...)`
- `initskill(<id>, ...)`
- 但这还不够后续扫描必须额外统计这些共享实现/批量注册文件
- `sterStatusEffects.go`
- `effect_power_doblue.go`
- `EffectAttackMiss.go`
- `EffectPhysicalAttackAddStatus.go`
- `EffectDefeatTrigger.go`
- `effect_attr.go`
- `effect_EffectConditionalAddDamage.go`
- `effect_74_75.go`
- `effect_104_109.go`
- `Effect{id}` 结构体与方法扫描来源
- `logic/service/fight/effect/*.go`
- 疑似未实现判断是启发式不是最终结论仍需代码级确认
---
## 7. 这次改动涉及的关键文件
- `public/config/effectInfo.json`
- `logic/service/fight/effect/400_480_586_599_610_611_613.go`
- `logic/service/fight/effect/573_587_591_592_594_596_597_598.go`
- `logic/service/fight/effect/effect_info_map.go`
- `logic/service/fight/effect/sub_effect_helper.go`
- `docs/effect-refactor-summary-2026-03-28.md`
---
## 8. 2026-03-29 增量记录
### 8.1 本轮补齐的 effect
- `663` `{0}回合内若对手使用攻击技能则{1}%使对手{2}`
- `664` 若先出手则当回合对手无法造成攻击伤害
- `665` 造成的伤害低于`{0}``{1}`回合内自身受到的伤害减少`{2}`
- `666` 使自身下回合攻击必定先手必定暴击
- `667` 自身为满体力时`{0}{1}`
### 8.2 实现口径
- `663` 复用与 `614` 同类的对手使用攻击技能时触发路径 `Skill_Use_ex()` 中按概率给对手附加状态
- `664` 复用 `170` 的先手免伤模式 `DamageLockEx()` 中将当回合受到的攻击伤害归零
- `665` 按技能实参 `250 3 100` 的实际使用方式落为低于阈值则给自己挂 3 回合 100 点固定减伤子效果不是百分比减伤
- `666` 落为仅对下回合攻击技能生效的先手与暴击保证`ComparePre()` 强制先手`ActionStart()` 强制暴击
- `667` 按配置说明自身攻击+a防御+b特攻+c特防+d速度+e命中+f处理仅在满体力时给自己附加前 6 项能力等级
### 8.3 本轮新增文件
---
## 16. 2026-03-31 增量记录
### 16.1 本轮补齐的 effect
- `764` `{0}回合内若对手使用攻击技能降低对手最大体力的1/{1}`
- `765` `{0}回合对手无法使自身能力出现提升状态`
- `766` 消除对手能力提升状态消除成功则`{0}`回合内对手造成的攻击伤害不超过`{1}`
- `767` `{0}`回合内每回合使用技能且出手流程结束后若对手处于能力下降状态则附加给对手`{1}`点固定伤害
- `768` 对手每处于一种异常状态则附加`{0}`点固定伤害
- `774` 若自身当前体力高于对手则附加对手最大体力1/`{0}`的百分比伤害
- `775` `{0}`回合内若受到的伤害大于`{1}`则恢复自身所有体力
- `777` 消除对手能力上升状态消除成功下`{0}`回合必定先出手
- `778` 反转对手的能力提升状态反转成功则恢复自身所有体力
- `779` 若对手处于能力提升状态则先制+2
### 9.2 已存在并复核通过
- `776` 已实现于 `logic/service/fight/effect/effect_776.go`
### 9.3 本轮新增文件
- `logic/service/fight/effect/764_768.go`
- `logic/service/fight/effect/774_779.go`
### 9.4 本轮同步更新
- `logic/service/fight/effect/effect_info_map.go`
### 9.5 本轮顺手修复的同包编译阻塞
- `logic/service/fight/effect/2195_2219.go`
- 修正 `uint32 * int` 的类型不匹配
- `logic/service/fight/effect/2220_2244.go`
- 修正将 `info.Category` 误当函数调用的问题改为 `info.EnumCategory`
### 9.6 本轮验证
- 已执行
- `cd /workspace/logic && go test ./service/fight/effect`
- `cd /workspace/logic && go build ./...`
- 结果
- 通过
### 9.7 任务文档状态
- `task-031-effects-764-768.md` 本轮已可视为完成
- `task-033-effects-774-779.md` 本轮已可视为完成`776` 为既有实现
- 本轮未删除任务文档如下一轮继续清理 backlog可直接移除这两份任务文件
### 9.8 后续增量
- 已继续补齐
- `785` 若自身攻击对手时克制关系为微弱则先制+2
- `786` 令对手随机进入`{0}`种异常状态
- `787` `{0}`回合内使用技能后若对手处于能力提升状态则附加对手最大体力1/`{1}`的百分比伤害
- `788` 消除对手能力提升消除成功`{0}`回合内免疫异常状态
- `789` 消除对手回合类效果消除成功对手下`{0}`回合受到的伤害翻倍
- 新增文件
- `logic/service/fight/effect/785_789.go`
- `logic/service/fight/effect/795_799.go`
- 已继续补齐
- `795` 每次使用则当回合造成的攻击伤害额外提升`{0}`%最高额外提升`{1}`%
- `796` `{0}`回合内每回合吸取对手当前体力的1/`{1}`
- `797` 消除对手回合类效果消除成功`{0}`回合内对手无法通过自身技能恢复体力
- `798` 若对手处于能力提升状态则对手`{0}`回合内造成的伤害不超过`{1}`
- `799` 恢复自身最大体力的1/`{0}`并给对手造成等量百分比伤害自身体力低于1/`{1}`时效果翻倍
- `logic/service/fight/effect/663_667.go`
### 8.4 本轮同步更新
- `logic/service/fight/effect/effect_info_map.go`
- `docs/effect-unimplemented-tasks/task-013-effects-663-667.md` 已完成可从任务目录移除
### 8.5 本轮验证
- `cd /workspace/logic && go test ./service/fight/effect`
- `cd /workspace/logic && go build ./...`
---
## 9. 2026-03-29 增量记录
### 16.1 本轮补齐的 effect
- `668` 若对手处于能力提升状态则先制额外+1
- `669` 当回合击败对手则下回合自身攻击先制+1
- `670` `{0}`回合每回合附加`{1}``{2}`值的`{3}%`的百分比伤害
- `671` 若对手处于异常状态则恢复造成伤害的`{0}%`的体力
- `672` 当回合击败对手则恢复自身全部体力
### 16.2 实现口径
- `668` 复用 `539` 的条件先制模式 `ComparePre()` 中于对手存在能力提升状态时直接给当前技能先制+1
- `669` 当回合击败后为下回合攻击技能生效处理 `SwitchOut()` 中标记击败成立下一回合 `ComparePre()` 仅对攻击技能追加先制+1
- `670` 参照 `419` `593` 的组合语义实现为回合类附加伤害效果持续期间每次使用技能时附加一次基于指定目标属性值的固定伤害
- `671` 复用 `687` 的伤害回血模式但条件改为对手处于任意异常状态
- `672` 按击败即时触发处理在对手因本次攻击退场时立刻将自身体力回复至满值
### 9.3 本轮新增文件
- `logic/service/fight/effect/668_672.go`
### 9.4 本轮同步更新
- `logic/service/fight/effect/effect_info_map.go`
- `docs/effect-unimplemented-tasks/task-014-effects-668-672.md` 已完成可从任务目录移除
### 9.5 本轮验证
- `cd /workspace/logic && go test ./service/fight/effect`
- `cd /workspace/logic && go build ./...`
---
## 10. 2026-03-29 增量记录
### 10.1 本轮补齐的 effect
- `642` `{0}回合内若对手攻击技能命中则己方在场精灵{1}%做出{2}`
- `643` `{0}%概率使对手{1}回合内{2}能力每回合变化{3}`
- `644` 当回合未击败对手则减少对手当前体力`1/{0}`
- `645` 体力低于`1/{0}`时威力`{1}`
- `646` 体力高于对手时此技能命中后 100% 使对手能力下降
### 10.2 实现口径
- `642` `moves.json` `1000642` 的说明实现反击档位映射`0-5` 分别对应 `50/100/150/200/250/300` 点固定伤害挂在 defender `Skill_Use_ex()`仅对命中的攻击技能生效
- `643` 按真实技能实参 `55 2 1 -1` 这类布局处理为命中后按概率给对手挂回合子效果子效果在 `TurnEnd()` 对指定能力执行一次 `SetProp`
- `644` 复用 `579` 未击败对手判定时机落在 `Action_end()`按对手当前体力结算 `1/n` 的百分比伤害
- `645` 复用 `37` 的低血线威力倍率模式 `SkillHit()` 中按当前体力是否低于最大体力 `1/n` 改写技能威力
- `646` 未按任务文档里的占位 `param` 硬编码改按实际技能配置的 6 段能力变化参数通用处理仅在自身体力高于对手且本次技能命中后生效
### 10.3 本轮新增文件
- `logic/service/fight/effect/642_646.go`
### 10.4 本轮同步更新
- `logic/service/fight/effect/effect_info_map.go`
- `docs/effect-unimplemented-tasks/task-009-effects-642-646.md` 已完成可从任务目录移除
---
## 11. 2026-03-29 增量记录
### 11.1 本轮补齐的 effect
- `769` 若对手不处于异常状态则造成的攻击伤害额外提升`{0}%`
- `770` 若对手处于异常状态则恢复自身全部体力
- `771` `{0}`回合内每次使用攻击技能都有`{1}%`概率使对手进入任意一种异常状态
- `772` `{0}`回合内若对手使用攻击技能则有`{1}%`概率随机进入烧伤冻伤中毒麻痹害怕睡眠中的一种异常状态
- `773` 若自身体力低于对手则与对手互换体力
### 11.2 实现口径
- `769` 复用 `1103` 的条件增伤写法 `SkillHit()` 中仅对攻击技能生效并在对手不存在任意异常状态时追加威力百分比
- `770` 按技能结算后触发处理落在 `Skill_Use()`满足对手处于异常状态时直接回复自身满体力
- `771` 作为回合类自身增益实现持续期间在 `OnSkill()` 针对每次攻击技能按概率给对手附加一项随机异常状态
- `772` 参照 `559` defender 侧监听时机落在 `Skill_Use_ex()`对手使用攻击技能时按概率附加六选一异常状态
- `773` 复用 `529` 的直接改写当前体力思路在满足自身体力低于对手时交换双方当前体力并分别按各自最大体力上限截断
### 11.3 本轮新增文件
- `logic/service/fight/effect/769_773.go`
### 11.4 本轮同步更新
- `logic/service/fight/effect/effect_info_map.go`
- `docs/effect-unimplemented-tasks/task-032-effects-769-773.md` 已完成可从任务目录移除
---
## 12. 2026-03-29 增量记录
### 12.1 本轮补齐的 effect
- `1498` 随机附加烧伤冻伤失明失神中的 `{0}` 种异常状态未触发则自身下 `{1}` 回合造成的伤害提升 `{2}%`
- `1499` 体力低于最大体力的 `1/3` 时先制 `+3`
- `1500` 1 回合做 `{0}-{1}` 次攻击自身处于护盾状态下连击上限为 `{2}`
- `1501` 命中后为对手种下一颗黑暗之种
- `1502` 对手身上存在黑暗之种时先制 `+1`
### 12.2 实现口径
- `1498` 复用 `1111` 未触发则挂自身增伤子效果模式随机状态池按任务文案落为烧伤冻伤失明失神若本次一个状态都未成功挂上则给自身添加持续 `{1}` 回合的增伤子效果
- `1499` 复用现有条件先制写法 `ComparePre()` 中按当前体力是否低于最大体力 `1/3` 直接修改当前技能优先级
- `1500` 参照仓库现有多段技能处理口径不做逐段攻击而是在 `Damage_Mul()` 中按随机连击次数折算红伤倍率若自身当前存在护盾则用 `{2}` 约束连击上限
- `1501` 作为挂在 defender 身上的持久子效果实现命中后附加黑暗之种 4 `TurnEnd()` 随机扣 1 个技能 PP成熟后每回合所有技能 PP `-1`下场后清除
- `1502` `ComparePre()` 中检查对手是否持有 `1501` 的子效果存在时当前技能先制 `+1`
### 12.3 模型假设
- 仓库当前未注册失神状态本轮按状态 ID `29` 追加了一个最小可用实现行为复用 `StatusCannotAct`
- `1500` 仍受当前战斗模型限制只能按总连击数折算伤害不能表现逐段命中逐段触发的细粒度行为
### 12.4 本轮新增文件
- `logic/service/fight/effect/1498_1502.go`
### 12.5 本轮同步更新
- `logic/service/fight/effect/effect_info_map.go`
- `docs/effect-unimplemented-tasks/README.md`
- `docs/effect-unimplemented-tasks/task-177-effects-1498-1502.md` 已完成可从任务目录移除
### 12.6 本轮验证
- `cd /workspace/logic && go test ./service/fight/effect`
- `cd /workspace/logic && go build ./...`
---
## 13. 2026-03-29 增量记录
### 13.1 本轮补齐的 effect
- `1503` 清除对手身上的黑暗之种清除成功则令对手随机受到 `1-500` 点固定伤害
- `1504` `40%` 令对手诅咒若对手身上存在黑暗之种则概率翻倍
- `1505` 黑暗之种成长期时附加 `200` 点固定伤害黑暗之种长大后固定伤害翻倍
- `1506` 若对手不是龙系精灵则恢复自身 `{0}` 点体力
- `1507` `{0}` 回合内自身受到攻击则令对手随机进入 `{1}` 种异常状态未触发则消除对手回合类效果
### 13.2 实现口径
- `1503` 复用本轮新增的黑暗之种清理 helper `Skill_Use()` 中清除对手持有的 `1501` 子效果仅当成功清除时追加一次 `1-500` 的随机固定伤害
- `1504` 按任务文案直接实现为基础 `40%` 概率若对手当前仍持有黑暗之种则翻倍到 `80%`仓库尚无诅咒状态注册本轮补了一个最小可用的状态壳以承接后续联动
- `1505` 读取 `1501` 子效果当前成长阶段成长期附加 `200` 固定伤害成熟后附加 `400` 固定伤害若对手不存在黑暗之种则不触发
- `1506` 使用宠物当前系别组合判断是否包含龙系仅在对手主属性副属性均不为龙系时恢复自身体力
- `1507` 复用 `1228` defender 侧监听模式落在 `Skill_Use_ex()`对手使用攻击技能命中本体时随机附加若干异常状态若整段持续时间内一次都未成功触发则在最后一回合结束时清除对手回合类效果
### 13.3 模型假设
- 仓库当前缺少诅咒状态实现本轮按状态 ID `23` 注册了一个最小 `BaseStatus` 版本只提供状态存在性与常规下场清理不额外附带持续结算逻辑
- `1505` 黑暗之种成长期/长大后判断直接复用 `1501` 子效果内部阶段计数 4 次回合结束视为成长期 5 次起视为成熟
### 13.4 本轮新增文件
- `logic/service/fight/effect/1503_1507.go`
### 13.5 本轮同步更新
- `logic/service/fight/effect/effect_info_map.go`
- `docs/effect-unimplemented-tasks/README.md`
- `docs/effect-unimplemented-tasks/task-178-effects-1503-1507.md` 已完成可从任务目录移除
### 13.6 本轮验证
- `cd /workspace/logic && go test ./service/fight/effect`
- `cd /workspace/logic && go build ./...`
---
## 14. 2026-03-30 增量记录
### 14.1 本轮补齐的 effect
- `1508` 先出手时无视攻击免疫效果
- `1509` 令对手全属性-`{0}` 且随机 `{1}` 个技能 PP 值归零技能无效时消耗自身全部体力并令对手全属性-1然后对手下 3 次使用技能消耗的 PP 值为 3
- `1510` `{0}` 回合内对手主动切换精灵则登场精灵 `{1}%` 随机进入 `{2}` 种异常状态
- `1511` 先出手时免疫当回合受到的攻击伤害若对手为自身天敌则免疫并反弹给对手造成伤害值 `{0}%` 的百分比伤害
- `1512` 集结天幕四龙之神力使自身下 2 回合先制+3且攻击必定命中必定致命
### 14.2 实现口径
- `1508` 采用仓库现有模型下的局部支持方案若自身本次先出手且使用攻击技能则在伤害结算前临时屏蔽若干常见攻击伤害清零类 immunity effect并在技能结算结束后恢复
- `1509` 正常命中时直接令对手全属性下降并随机清空若干技能 PP若本次技能实体存在但 `AttackTime == 0`则按技能无效分支处理自损全部体力令对手全属性-1并给对手挂 3 PP 三倍消耗子效果
- `1510` 复用 `1562` 主动切换后对登场精灵生效模式效果挂在对手侧仅在对手主动切换下场时置 pending登场后按概率随机附加若干异常状态
- `1511` 复用 `170/1011` 的免疫伤害写法落在 `DamageLockEx()`若自身先出手则直接免疫本回合受到的红伤若对手同时为自身天敌则按原伤害值 `{0}%` 追加一次百分比伤害反弹
- `1512` 作为持续 2 回合的自身子效果实现 `ComparePre()` 中固定追加先制 `+3`并在 `ActionStart()` 中对攻击技能同时赋予必中与必定致命
### 14.3 模型假设
- `1508` 当前没有通用无视攻击免疫标记位本轮仅覆盖仓库内已识别的常见攻击免疫 effect`170/525/570/850/1011/1511`对其他未来新增或语义不同的 defender 侧免疫实现不保证自动生效
- `1509` 技能无效时按仓库现有口径解释为技能实体存在但本次结算后 `AttackTime == 0`不把被控未出手 PP 无法释放这类情况算作技能无效
### 14.4 本轮新增文件
- `logic/service/fight/effect/1508_1512.go`
### 14.5 本轮同步更新
- `logic/service/fight/effect/effect_info_map.go`
- `docs/effect-unimplemented-tasks/README.md`
- `docs/effect-unimplemented-tasks/task-179-effects-1508-1512.md` 已完成可从任务目录移除
### 14.6 本轮验证
- `cd /workspace/logic && go test ./service/fight/effect`
- `cd /workspace/logic && go build ./...`
---
## 15. 2026-03-31 增量记录
### 15.1 本轮新增实现
- `780` `{0}` 回合内受到攻击则 `{1}%` 令对手随机 `{2}` 个技能 PP 值归零
- `781` 消除对手回合类效果消除成功则 `{0}` 回合内令对手使用的属性技能无效
- `782` `{0}%` 令对手 `{1}`每次使用概率增加 `{2}%`最高概率 `{3}%`
- `783` `{0}` 回合内自身能力提升状态被消除或吸取时附加对手最大体力 `1/{1}` 的百分比伤害
- `784` 若本回合击败对手则将对手的能力提升效果转移到自己身上
- `790` `{0}` 回合内自身所有攻击无视伤害限制效果
- `791` `{0}` 回合内每回合使用技能恢复自身最大体力的 `1/{1}`当前体力低于对手时恢复翻倍
- `792` 先出手时对手当回合攻击技能无效
- `793` 若造成的伤害低于 `{0}`则下 `{1}` 回合每回合造成 `{2}` 点固定伤害
- `794` 消除对手能力提升消除成功则抵挡 `{0}` 回合内对手的攻击伤害
### 15.2 本轮复核后确认已存在
- `637-641` 已在 `637_641.go` 落地相关任务文档可删除
- `764-768` 已在 `764_768.go` 落地相关任务文档可删除
- `774-779` 已在 `774_779.go` 落地相关任务文档可删除
- `785-789` 已在 `785_789.go` 落地相关任务文档可删除
### 15.3 本轮同步更新
- 新增 `logic/service/fight/effect/780_784.go`
- 新增 `logic/service/fight/effect/790_794.go`
- 删除 `docs/effect-unimplemented-tasks/task-008-effects-637-641.md`
- 删除 `docs/effect-unimplemented-tasks/task-031-effects-764-768.md`
- 删除 `docs/effect-unimplemented-tasks/task-033-effects-774-779.md`
- 删除 `docs/effect-unimplemented-tasks/task-034-effects-780-784.md`
- 删除 `docs/effect-unimplemented-tasks/task-035-effects-785-789.md`
- 删除 `docs/effect-unimplemented-tasks/task-036-effects-790-794.md`
### 15.4 本轮验证
- `cd /workspace/logic && go test ./service/fight/effect`
- `cd /workspace/logic && go build ./...`
## 16. 2026-03-31 增量记录
### 16.1 本轮补齐的 effect
- `1067` `{0}回合内每回合使用技能恢复自身最大体力的1/{1}恢复体力时若自身体力低于最大体力的1/{2}则恢复效果转变为吸取对手最大体力的1/{3}`
- `1068` `{0}`回合受到致命伤害时残留`{1}`点体力
- `1069` 反转自身能力下降状态反转成功则`{0}`回合内躲避所有攻击
- `1070` 对手处于能力下降状态时自身先制+1
- `1071` `{0}`回合内若对手恢复体力药剂恢复除外`{1}`回合内自身攻击附加`{2}`点固定伤害
### 16.2 实现口径
- `1067` 复用 `707` 每回合使用技能后回复模式在低血线时改为按对手最大体力比例吸取并同步治疗自身
- `1068` 通过回合子效果在致命伤害结算前锁定剩余体力值满足{0}回合保留{1}点体力
- `1069` 先反转自身全部能力下降再在成功时挂载受击 MISS 子效果仅拦截攻击技能
- `1070` 复用现有 `1243` 同类优先级判断对手存在任一能力下降时自身先制+1
- `1071` 在对手身上挂恢复监听子效果排除药剂恢复触发后给自身挂固定伤害增益子效果
### 16.3 本轮同步项
- 已补 `logic/service/fight/effect/effect_info_map.go` `1067-1071` 的说明映射
- `docs/effect-unimplemented-tasks/task-091-effects-1067-1071.md` 已删除
## 17. 2026-03-31 增量记录
### 17.1 本轮补齐的 effect
- `1072` 附加自身当前体力`{0}`%的百分比伤害连续使用每次增加`{1}`%最高`{2}`%
- `1073` `{0}`回合内受到的伤害大于`{1}`则主动恢复自身全部体力并造成等同于恢复量的固定伤害
- `1074` 造成的伤害大于`{0}`则对手`{1}`%`{2}`未触发则自身下`{3}`回合攻击有`{4}`%的概率使对手`{5}`
- `1075` 恢复自身最大体力的1/`{0}`自身体力低于1/`{1}`时回满
- `1076` 对手不处于能力提升状态时先制+2
### 17.2 实现口径
- `1072` 参考 `1280`按当前体力百分比追加伤害并在连续使用时累计加成封顶于实参上限
- `1073` 参考 `775`在受击伤害超过阈值时于行动结束节点回满自身并按实际恢复量反打固定伤害
- `1074` 参考 `1256/1271`先按本次造成伤害判定即时概率状态未触发时再给自身挂后续攻击概率附加状态的回合子效果
- `1075` 参考 `704/900`常态回复最大体力的分数值低血线时改为直接补满缺失体力
- `1076` 参考 `779` 的反向条件对手不存在能力提升状态时令自身先制+2
### 17.3 本轮同步项
- 已补 `logic/service/fight/effect/effect_info_map.go` `1072-1076` 的说明映射
- `docs/effect-unimplemented-tasks/task-092-effects-1072-1076.md` 已删除
## 18. 2026-03-31 增量记录
### 18.1 本轮补齐的 effect
- `1077` `{0}`回合内对手使用攻击技能后使对手`{1}`未触发则对手下`{2}`回合属性技能命中效果失效
- `1078` 使对手随机进入`{0}`种异常状态未触发则下`{1}`回合自身属性技能先制+`{2}`
- `1079` 命中后`{0}`%令对手`{1}`未触发则下`{2}`回合自身攻击技能先制+`{3}`
- `1080` 连续使用时先制+1
- `1081` 若对手处于能力提升状态则先制+1
### 18.2 实现口径
- `1077` 参考 `1267/998`在己方身上挂对手攻击技能监听若整段持续期未触发则给对手补一段属性技能失效子效果
- `1078` 先尝试随机附加异常若没有新增异常状态成功落到对手身上再给自身挂属性技能先制子效果
- `1079` 参考概率异常+后续先制类效果命中后先判即时异常未触发则给自身挂攻击技能先制子效果
- `1080` 复用 `AddLvelEffect` 的连续使用计数在连续使用同一技能时给当前技能先制+1
- `1081` 直接复用 `779` 的同类判断只是条件改为对手处于能力提升状态且先制值为+1
### 18.3 本轮同步项
- 已补 `logic/service/fight/effect/effect_info_map.go` `1077-1081` 的说明映射
- `docs/effect-unimplemented-tasks/task-093-effects-1077-1081.md` 已删除
## 18. 2026-03-31 增量记录
### 18.1 本轮补齐的 effect
- `1077` `{0}`回合内对手使用攻击技能后使对手`{1}`未触发则对手下`{2}`回合属性技能命中效果失效
- `1078` 使对手随机进入`{0}`种异常状态未触发则下`{1}`回合自身属性技能先制+`{2}`
- `1079` 命中后`{0}`%令对手`{1}`未触发则下`{2}`回合自身攻击技能先制+`{3}`
- `1080` 连续使用时先制+1
- `1081` 若对手处于能力提升状态则先制+1
### 18.2 实现口径
- `1077` 参考 `1267/1490/738`在己方身上挂对手攻击技能监听状态未成功挂上时再给对手挂属性技能命中效果失效子效果
- `1078` 参考 `786/1100`先尝试随机异常状态未触发时给自身挂仅对属性技能生效的先制子效果
- `1079` 参考 `756/1230`命中后按概率给对手附加状态未触发时给自身挂仅对攻击技能生效的回合先制子效果
- `1080` 记录上次使用技能 ID在连续使用同一技能时给予先制+1
- `1081` 复用 `779` 的同类条件判断对手存在能力提升状态时自身先制+1
### 18.3 本轮同步项
- 已补 `logic/service/fight/effect/effect_info_map.go` `1077-1081` 的说明映射
- `docs/effect-unimplemented-tasks/task-093-effects-1077-1081.md` 已删除
## 19. 2026-04-03 同类实现合并记录
### 19.1 本轮抽取的公共 base
- `RandomDurationArg01Base`封装 `args[0]/args[1]` 随机持续回合
- `FixedDurationNeg1Arg0CountBase`封装 `Duration(-1)` 且从 `args[0]` 初始化次数上限
### 19.2 本轮合并到公共实现的 effect
- `41/42`改为复用随机持续回合 base
- `47/48`改为复用 `arg0` 持续回合 base
- `60`改为复用 `SideEffectArgs[0]` 持续回合 base
- `46/570`改为复用永久持续 + arg0 次数耗尽 base
### 19.3 本轮未动的同类候选
- `SelfKill`文件 `logic/service/fight/effect/selfkill.go` 已存在本地改动本轮为避免覆盖未继续并入 `FixedDurationNeg1Base`
- `Effect123``EffectPropSyncReverse`仍包含额外上下文初始化不适合只靠 SetArgs 模板直接下沉
### 19.4 本轮验证
- 已执行 `cd /workspace/logic && go test ./service/fight/effect`
- 已执行 `cd /workspace/logic && go build ./...`

View File

@@ -1,36 +0,0 @@
# Task 199: Effects 1609-1613
## 目标
- 补齐以下 5 或最后一组不足 5 当前判定未实现的 skill effect
- 实现位置优先放在 `logic/service/fight/effect/`
- effect 需要展示说明同步更新 `logic/service/fight/effect/effect_info_map.go`
- 完成后至少执行`cd /workspace/logic && go test ./service/fight/effect`
## Effect 列表
### Effect 1609
- `argsNum`: `0`
- `info`: `召唤自己的伙伴进行5-10次攻击布布犬发起时可额外令自身下回合攻击造成的伤害翻倍`
### Effect 1610
- `argsNum`: `0`
- `info`: `召唤自己的伙伴进行5-10次攻击布布熊发起时可额外令对手下回合属性技能失效`
### Effect 1611
- `argsNum`: `0`
- `info`: `攻击命中后5%的概率汲取泰坦源脉的力量本次攻击造成5倍伤害且战斗结束后获得5000泰坦之灵每日上限50000`
### Effect 1612
- `argsNum`: `5`
- `info`: `{0}回合内受到的伤害低于{1}时{2}%令对手{3},未触发则附加{4}点固定伤害`
- `param`: `1,3,3`
### Effect 1613
- `argsNum`: `2`
- `info`: `自身不处于能力提升状态则吸取对手{0}点体力,若先出手则额外吸取{1}点体力`
## 备注
- 该清单按当前仓库静态注册结果生成如果某个 effect 实际通过其他模块或运行时路径实现需要先复核后再落代码
- `201``445` 这类占位 effect优先补核心逻辑或补充明确的不可实现说明

View File

@@ -1,37 +0,0 @@
# Task 200: Effects 1614-1619
## 目标
- 补齐以下 5 或最后一组不足 5 当前判定未实现的 skill effect
- 实现位置优先放在 `logic/service/fight/effect/`
- effect 需要展示说明同步更新 `logic/service/fight/effect/effect_info_map.go`
- 完成后至少执行`cd /workspace/logic && go test ./service/fight/effect`
## Effect 列表
### Effect 1614
- `argsNum`: `4`
- `info`: `{0}回合内对手使用攻击技能后{1}%令对手{2},未触发则令对手全属性-{3}`
- `param`: `1,2,2`
### Effect 1616
- `argsNum`: `0`
- `info`: `当回合使用的技能克制对手时获得本系属性加成`
### Effect 1617
- `argsNum`: `4`
- `info`: `{0}回合内受到攻击后{1}%使对手{2},未触发则自身全属性+{3}`
- `param`: `1,2,2`
### Effect 1618
- `argsNum`: `2`
- `info`: `{0}回合内每回合结束时令对手随机{1}个技能PP值归零`
### Effect 1619
- `argsNum`: `0`
- `info`: `50%复制对手当回合释放的技能未触发则恢复自身最大体力的1/2且令对手全属性-1`
## 备注
- 该清单按当前仓库静态注册结果生成如果某个 effect 实际通过其他模块或运行时路径实现需要先复核后再落代码
- `201``445` 这类占位 effect优先补核心逻辑或补充明确的不可实现说明

View File

@@ -1,36 +0,0 @@
# Task 201: Effects 1620-1624
## 目标
- 补齐以下 5 或最后一组不足 5 当前判定未实现的 skill effect
- 实现位置优先放在 `logic/service/fight/effect/`
- effect 需要展示说明同步更新 `logic/service/fight/effect/effect_info_map.go`
- 完成后至少执行`cd /workspace/logic && go test ./service/fight/effect`
## Effect 列表
### Effect 1620
- `argsNum`: `1`
- `info`: `对手基础速度值高于{0}则下回合先制-1`
### Effect 1621
- `argsNum`: `2`
- `info`: `{0}%令对手所有技能PP值-{1},自身满体力时效果翻倍`
### Effect 1622
- `argsNum`: `4`
- `info`: `{0}回合内每回合{1}%对手属性技能无效,未触发则下{2}次受到的攻击伤害减少{3}%`
### Effect 1623
- `argsNum`: `3`
- `info`: `若对手是{0}精灵则下{1}回合对手受到的伤害提高{2}%`
- `param`: `5,0,0`
### Effect 1624
- `argsNum`: `1`
- `info`: `对手不处于异常状态时随机附加{0}种异常状态`
## 备注
- 该清单按当前仓库静态注册结果生成如果某个 effect 实际通过其他模块或运行时路径实现需要先复核后再落代码
- `201``445` 这类占位 effect优先补核心逻辑或补充明确的不可实现说明

View File

@@ -1,36 +0,0 @@
# Task 202: Effects 1625-1629
## 目标
- 补齐以下 5 或最后一组不足 5 当前判定未实现的 skill effect
- 实现位置优先放在 `logic/service/fight/effect/`
- effect 需要展示说明同步更新 `logic/service/fight/effect/effect_info_map.go`
- 完成后至少执行`cd /workspace/logic && go test ./service/fight/effect`
## Effect 列表
### Effect 1625
- `argsNum`: `3`
- `info`: `造成的伤害高于{0}则{1}%令自身全属性+{2}`
### Effect 1626
- `argsNum`: `1`
- `info`: `后出手时将当回合护盾所承受的伤害值以百分比伤害的形式{0}%反弹给对手`
### Effect 1627
- `argsNum`: `3`
- `info`: `{0}回合做{1}-{2}次攻击,若本回合攻击次数达到最大则必定秒杀对手`
### Effect 1628
- `argsNum`: `2`
- `info`: `每次使用该技能击败对手则恢复自身全部体力,同时重置该技能使用次数并使该技能攻击威力提升{0}点,未击败对手时令自身下回合攻击技能先制+{1}`
### Effect 1629
- `argsNum`: `4`
- `info`: `{0}基础速度值{1}{2}则自身下回合先制+{3}`
- `param`: `4,0,0|7,1,1`
## 备注
- 该清单按当前仓库静态注册结果生成如果某个 effect 实际通过其他模块或运行时路径实现需要先复核后再落代码
- `201``445` 这类占位 effect优先补核心逻辑或补充明确的不可实现说明

View File

@@ -1,37 +0,0 @@
# Task 203: Effects 1630-1634
## 目标
- 补齐以下 5 或最后一组不足 5 当前判定未实现的 skill effect
- 实现位置优先放在 `logic/service/fight/effect/`
- effect 需要展示说明同步更新 `logic/service/fight/effect/effect_info_map.go`
- 完成后至少执行`cd /workspace/logic && go test ./service/fight/effect`
## Effect 列表
### Effect 1630
- `argsNum`: `2`
- `info`: `若对手当回合使用的技能为攻击技能则自身必定先出手且命中后{0}%令对手{1}`
- `param`: `1,1,1`
### Effect 1631
- `argsNum`: `2`
- `info`: `{0}回合内每回合若自身未受到攻击伤害则回合结束后附加对手最大体力1/{1}的百分比伤害自身体力为0时也可触发`
### Effect 1632
- `argsNum`: `3`
- `info`: `吸取对手{0}点体力若对手任意1项技能PP值小于{1}点则额外吸取{2}点体力`
### Effect 1633
- `argsNum`: `2`
- `info`: `使自身体力百分比与对手体力百分比对调,自身体力百分比高于对手时{0}%令对手{1}`
- `param`: `1,1,1`
### Effect 1634
- `argsNum`: `0`
- `info`: `自身体力低于250时必定先手`
## 备注
- 该清单按当前仓库静态注册结果生成如果某个 effect 实际通过其他模块或运行时路径实现需要先复核后再落代码
- `201``445` 这类占位 effect优先补核心逻辑或补充明确的不可实现说明

View File

@@ -1,35 +0,0 @@
# Task 204: Effects 1635-1639
## 目标
- 补齐以下 5 或最后一组不足 5 当前判定未实现的 skill effect
- 实现位置优先放在 `logic/service/fight/effect/`
- effect 需要展示说明同步更新 `logic/service/fight/effect/effect_info_map.go`
- 完成后至少执行`cd /workspace/logic && go test ./service/fight/effect`
## Effect 列表
### Effect 1635
- `argsNum`: `2`
- `info`: `立刻恢复自身{0}点体力,{1}回合后恢复自身全部体力`
### Effect 1636
- `argsNum`: `0`
- `info`: `涵双1回合释放4-8张玫瑰卡牌进行攻击每张卡牌额外附加50点固定伤害自身体力低于最大体力的1/3时效果翻倍`
### Effect 1637
- `argsNum`: `2`
- `info`: `{0}回合内若对手使用属性技能,则使用属性技能后的下{1}回合属性技能命中效果失效`
### Effect 1638
- `argsNum`: `2`
- `info`: `{0}回合内若自身未受到攻击伤害则令对手全属性-{1}`
### Effect 1639
- `argsNum`: `0`
- `info`: `自身处于能力提升状态时100%打出致命一击`
## 备注
- 该清单按当前仓库静态注册结果生成如果某个 effect 实际通过其他模块或运行时路径实现需要先复核后再落代码
- `201``445` 这类占位 effect优先补核心逻辑或补充明确的不可实现说明

View File

@@ -1,35 +0,0 @@
# Task 205: Effects 1640-1644
## 目标
- 补齐以下 5 或最后一组不足 5 当前判定未实现的 skill effect
- 实现位置优先放在 `logic/service/fight/effect/`
- effect 需要展示说明同步更新 `logic/service/fight/effect/effect_info_map.go`
- 完成后至少执行`cd /workspace/logic && go test ./service/fight/effect`
## Effect 列表
### Effect 1640
- `argsNum`: `0`
- `info`: `出手时若自身满体力则100%打出致命一击`
### Effect 1641
- `argsNum`: `2`
- `info`: `自身处于能力提升状态时造成伤害的{0}%恢复自身体力值当前体力低于最大体力的1/{1}时附加等量百分比伤害`
### Effect 1642
- `argsNum`: `2`
- `info`: `消除对手能力提升状态,消除成功则{0}%随机为对手任意技能散布{1}枚致命印记`
### Effect 1643
- `argsNum`: `1`
- `info`: `对手每存在1层致命裂痕则附加{0}点真实伤害`
### Effect 1644
- `argsNum`: `3`
- `info`: `{0}回合内对手使用攻击技能则随机进入{1}种异常状态,未触发则随机为对手任意技能散布{2}枚致命印记`
## 备注
- 该清单按当前仓库静态注册结果生成如果某个 effect 实际通过其他模块或运行时路径实现需要先复核后再落代码
- `201``445` 这类占位 effect优先补核心逻辑或补充明确的不可实现说明

View File

@@ -1,35 +0,0 @@
# Task 206: Effects 1645-1649
## 目标
- 补齐以下 5 或最后一组不足 5 当前判定未实现的 skill effect
- 实现位置优先放在 `logic/service/fight/effect/`
- effect 需要展示说明同步更新 `logic/service/fight/effect/effect_info_map.go`
- 完成后至少执行`cd /workspace/logic && go test ./service/fight/effect`
## Effect 列表
### Effect 1645
- `argsNum`: `3`
- `info`: `{0}回合内对手使用属性技能则自身下{1}次受到的攻击伤害减少{2}%`
### Effect 1646
- `argsNum`: `1`
- `info`: `全属性+{0},对手存在致命裂痕时强化效果翻倍`
### Effect 1647
- `argsNum`: `4`
- `info`: `{0}回合内每回合使用技能吸取对手最大体力的1/{1}吸取体力时若自身体力低于最大体力的1/{2}则吸取效果翻倍,对手免疫百分比伤害时额外附加{3}点真实伤害`
### Effect 1648
- `argsNum`: `1`
- `info`: `附加自身最大体力{0}%的百分比伤害并恢复等量体力值,对手存在致命裂痕时转变为等量的真实伤害`
### Effect 1649
- `argsNum`: `4`
- `info`: `{0}%概率造成的攻击伤害为{1}倍对手每存在1层致命裂痕则概率提升{2}%,未触发则{3}回合内令对手使用的属性技能无效`
## 备注
- 该清单按当前仓库静态注册结果生成如果某个 effect 实际通过其他模块或运行时路径实现需要先复核后再落代码
- `201``445` 这类占位 effect优先补核心逻辑或补充明确的不可实现说明

View File

@@ -1,35 +0,0 @@
# Task 207: Effects 1650-1654
## 目标
- 补齐以下 5 或最后一组不足 5 当前判定未实现的 skill effect
- 实现位置优先放在 `logic/service/fight/effect/`
- effect 需要展示说明同步更新 `logic/service/fight/effect/effect_info_map.go`
- 完成后至少执行`cd /workspace/logic && go test ./service/fight/effect`
## Effect 列表
### Effect 1650
- `argsNum`: `4`
- `info`: `命中后{0}%随机为对手任意技能散布{1}枚致命印记,若对手当前精灵致命裂痕≥{2}层则额外散布{3}枚致命印记`
### Effect 1651
- `argsNum`: `2`
- `info`: `当回合击败对手则令对手下{0}次触发致命印记真实伤害效果转变为1/{1}`
### Effect 1652
- `argsNum`: `2`
- `info`: `释放技能时自身每损失{0}%的体力值则此技能威力提升{1}点`
### Effect 1653
- `argsNum`: `2`
- `info`: `释放技能时对手每残留{0}%的体力值则此技能附加{1}点固定伤害`
### Effect 1654
- `argsNum`: `1`
- `info`: `当回合击败对手则令对手下只登场精灵首次使用的技能所附加的效果失效`
## 备注
- 该清单按当前仓库静态注册结果生成如果某个 effect 实际通过其他模块或运行时路径实现需要先复核后再落代码
- `201``445` 这类占位 effect优先补核心逻辑或补充明确的不可实现说明

View File

@@ -1,36 +0,0 @@
# Task 208: Effects 1655-1659
## 目标
- 补齐以下 5 或最后一组不足 5 当前判定未实现的 skill effect
- 实现位置优先放在 `logic/service/fight/effect/`
- effect 需要展示说明同步更新 `logic/service/fight/effect/effect_info_map.go`
- 完成后至少执行`cd /workspace/logic && go test ./service/fight/effect`
## Effect 列表
### Effect 1655
- `argsNum`: `3`
- `info`: `{0}回合内每回合结束后{1}恢复自身所有技能{2}点PP值`
- `param`: `25,1,1`
### Effect 1656
- `argsNum`: `0`
- `info`: `100%复制对手当回合释放的技能若对手当回合使用的技能为攻击技能则令对手随机1个技能PP值归零若对手当回合使用的技能为属性技能则令对手下回合先制-2`
### Effect 1657
- `argsNum`: `3`
- `info`: `己方每有一只精灵死亡则附加{0}点固定伤害对手体力高于最大体力的1/{1}时转变为{2}点`
### Effect 1658
- `argsNum`: `0`
- `info`: `3回合内每回合80%闪避对手攻击未触发时自身处于圣念状态则使对手随机1项技能PP值归零自身处于邪念状态则使对手失明`
### Effect 1659
- `argsNum`: `4`
- `info`: `随机附加给对手{0}-{1}点固定伤害,若打出致命一击则效果转变为吸取对手{2}-{3}点体力`
## 备注
- 该清单按当前仓库静态注册结果生成如果某个 effect 实际通过其他模块或运行时路径实现需要先复核后再落代码
- `201``445` 这类占位 effect优先补核心逻辑或补充明确的不可实现说明

View File

@@ -1,35 +0,0 @@
# Task 210: Effects 1665-1669
## 目标
- 补齐以下 5 或最后一组不足 5 当前判定未实现的 skill effect
- 实现位置优先放在 `logic/service/fight/effect/`
- effect 需要展示说明同步更新 `logic/service/fight/effect/effect_info_map.go`
- 完成后至少执行`cd /workspace/logic && go test ./service/fight/effect`
## Effect 列表
### Effect 1665
- `argsNum`: `2`
- `info`: `全属性+{0}自身背包内每阵亡1只精灵则{1}%的概率强化效果翻倍`
### Effect 1666
- `argsNum`: `3`
- `info`: `1回合做{0}-{1}次攻击,自身处于领域效果下连击上限为{2}`
### Effect 1667
- `argsNum`: `0`
- `info`: `开启时空漩涡使用后必定令对手束缚且下2回合令对手无法主动切换精灵`
### Effect 1668
- `argsNum`: `1`
- `info`: `附加对手当前已损失技能PP值总和×{0}的固定伤害,若对手未受到固定伤害则额外附加等量的真实伤害`
### Effect 1669
- `argsNum`: `2`
- `info`: `全属性+{0}自身背包内每存活1只精灵则{1}%的概率强化效果翻倍`
## 备注
- 该清单按当前仓库静态注册结果生成如果某个 effect 实际通过其他模块或运行时路径实现需要先复核后再落代码
- `201``445` 这类占位 effect优先补核心逻辑或补充明确的不可实现说明

View File

@@ -1,36 +0,0 @@
# Task 233: Effects 1780-1784
## 目标
- 补齐以下 5 或最后一组不足 5 当前判定未实现的 skill effect
- 实现位置优先放在 `logic/service/fight/effect/`
- effect 需要展示说明同步更新 `logic/service/fight/effect/effect_info_map.go`
- 完成后至少执行`cd /workspace/logic && go test ./service/fight/effect`
## Effect 列表
### Effect 1780
- `argsNum`: `5`
- `info`: `消耗自身全部体力减少对手当前体力的1/{0}同时使己方下1只出场精灵获得点数等同于对手最大体力值1/{1}的护盾且下{2}次技能先制+{3},护盾最多可获得{4}点`
### Effect 1781
- `argsNum`: `2`
- `info`: `若自身拥有的护盾值高于{0}则令自身下{1}回合回合类效果无法被消除`
### Effect 1782
- `argsNum`: `0`
- `info`: `自身存在护盾则先制+2`
### Effect 1783
- `argsNum`: `2`
- `info`: `消除敌我双方回合类效果,消除任意一项成功则敌我双方同时进入{0}状态,若任意一方回合类效果无法被消除则令对手下{1}回合无法主动切换精灵`
- `param`: `1,0,0`
### Effect 1784
- `argsNum`: `1`
- `info`: `{0}回合内对手使用技能时自身免疫对手下1次非致命一击造成的攻击伤害`
## 备注
- 该清单按当前仓库静态注册结果生成如果某个 effect 实际通过其他模块或运行时路径实现需要先复核后再落代码
- `201``445` 这类占位 effect优先补核心逻辑或补充明确的不可实现说明

View File

@@ -1,36 +0,0 @@
# Task 234: Effects 1785-1789
## 目标
- 补齐以下 5 或最后一组不足 5 当前判定未实现的 skill effect
- 实现位置优先放在 `logic/service/fight/effect/`
- effect 需要展示说明同步更新 `logic/service/fight/effect/effect_info_map.go`
- 完成后至少执行`cd /workspace/logic && go test ./service/fight/effect`
## Effect 列表
### Effect 1785
- `argsNum`: `3`
- `info`: `{0}回合内每回合使用技能吸取对手最大体力的1/{1},吸取体力时若自身为满体力则恢复己方所有不在场精灵{2}点体力`
### Effect 1786
- `argsNum`: `2`
- `info`: `{0}回合内若自身回合类效果被消除则对手下{1}次使用的攻击技能附加效果失效`
### Effect 1787
- `argsNum`: `1`
- `info`: `沙之力量觉醒,使自身下{0}次攻击获得黯天之尘效果`
### Effect 1788
- `argsNum`: `3`
- `info`: `{0}%令对手{1},未触发则附加自身最大体力{2}%的百分比伤害,若对手免疫百分比伤害则转变为真实伤害`
- `param`: `1,1,1`
### Effect 1789
- `argsNum`: `1`
- `info`: `预留,{0}`
## 备注
- 该清单按当前仓库静态注册结果生成如果某个 effect 实际通过其他模块或运行时路径实现需要先复核后再落代码
- `201``445` 这类占位 effect优先补核心逻辑或补充明确的不可实现说明

View File

@@ -1,36 +0,0 @@
# Task 295: Effects 2090-2094
## 目标
- 补齐以下 5 或最后一组不足 5 当前判定未实现的 skill effect
- 实现位置优先放在 `logic/service/fight/effect/`
- effect 需要展示说明同步更新 `logic/service/fight/effect/effect_info_map.go`
- 完成后至少执行`cd /workspace/logic && go test ./service/fight/effect`
## Effect 列表
### Effect 2090
- `argsNum`: `0`
- `info`: `空元之诗·均附加双方体力上限差值50%的次元·龙系伤害自身体力上限高于对手时额外吸取对手第五技能剩余的PP值自身体力上限低于对手时附加伤害翻倍`
### Effect 2091
- `argsNum`: `1`
- `info`: ``
### Effect 2092
- `argsNum`: `2`
- `info`: `全属性+{0}并将空妄诗章补充至与自身先制等级相等,自身没有空妄诗章时额外书写{1}篇,拥有时强化效果翻倍`
### Effect 2093
- `argsNum`: `5`
- `info`: `{0}回合内使用技能吸取对手最大体力的1/{1}自身体力低于1/{2}时效果翻倍,吸取后若对手体力未减少则{3}回合内对手{4}`
- `param`: `23,4,4`
### Effect 2094
- `argsNum`: `3`
- `info`: `自身每有1篇空妄诗章造成的攻击伤害提升{0}%,若空妄诗章的篇数高于{1}则每有1篇额外附加{2}点真实伤害`
## 备注
- 该清单按当前仓库静态注册结果生成如果某个 effect 实际通过其他模块或运行时路径实现需要先复核后再落代码
- `201``445` 这类占位 effect优先补核心逻辑或补充明确的不可实现说明

View File

@@ -1,35 +0,0 @@
# Task 301: Effects 2120-2124
## 目标
- 补齐以下 5 或最后一组不足 5 当前判定未实现的 skill effect
- 实现位置优先放在 `logic/service/fight/effect/`
- effect 需要展示说明同步更新 `logic/service/fight/effect/effect_info_map.go`
- 完成后至少执行`cd /workspace/logic && go test ./service/fight/effect`
## Effect 列表
### Effect 2120
- `argsNum`: `2`
- `info`: `自身为对手天敌时下{0}次受到攻击的伤害减少{1}%`
### Effect 2121
- `argsNum`: `1`
- `info`: `未击败对手则己方在场精灵吸取对手最大体力的{0}%,吸取后若对手体力未减少则延续至下回合`
### Effect 2122
- `argsNum`: `2`
- `info`: `获得{0}点护罩护罩消失时自身所有技能PP值+{1}`
### Effect 2123
- `argsNum`: `1`
- `info`: `消除对手回合类效果消除成功令对手感染且自身每有1点灵茉之芯额外附加中毒、睡眠、寄生中的前1种异常状态未触发则吸取对手最大体力的1/{0}`
### Effect 2124
- `argsNum`: `1`
- `info`: `{0}%的概率造成伤害翻倍,自身处于能力提升状态或对手处于能力下降状态时概率翻倍`
## 备注
- 该清单按当前仓库静态注册结果生成如果某个 effect 实际通过其他模块或运行时路径实现需要先复核后再落代码
- `201``445` 这类占位 effect优先补核心逻辑或补充明确的不可实现说明

View File

@@ -1,36 +0,0 @@
# Task 317: Effects 2200-2204
## 目标
- 补齐以下 5 或最后一组不足 5 当前判定未实现的 skill effect
- 实现位置优先放在 `logic/service/fight/effect/`
- effect 需要展示说明同步更新 `logic/service/fight/effect/effect_info_map.go`
- 完成后至少执行`cd /workspace/logic && go test ./service/fight/effect`
## Effect 列表
### Effect 2200
- `argsNum`: `2`
- `info`: `令双方{0},任意一方未触发则额外令对手{1}`
- `param`: `1,0,0|1,1,1`
### Effect 2201
- `argsNum`: `1`
- `info`: `自身携带技能中含有的光系多于暗影系时{0}%令对手疲惫,暗影系多于光系时{0}%令对手害怕`
### Effect 2202
- `argsNum`: `1`
- `info`: `自身携带技能中含有的光系不少于暗影系时必定打出致命一击暗影系不少于光系时吸取对手最大体力的1/{0}`
### Effect 2203
- `argsNum`: `0`
- `info`: `技能无效时,免疫下次对手的攻击`
### Effect 2204
- `argsNum`: `2`
- `info`: `技能威力减少{0}%,对手处于异常状态时改为提升{1}%`
## 备注
- 该清单按当前仓库静态注册结果生成如果某个 effect 实际通过其他模块或运行时路径实现需要先复核后再落代码
- `201``445` 这类占位 effect优先补核心逻辑或补充明确的不可实现说明

View File

@@ -1,35 +0,0 @@
# Task 318: Effects 2205-2209
## 目标
- 补齐以下 5 或最后一组不足 5 当前判定未实现的 skill effect
- 实现位置优先放在 `logic/service/fight/effect/`
- effect 需要展示说明同步更新 `logic/service/fight/effect/effect_info_map.go`
- 完成后至少执行`cd /workspace/logic && go test ./service/fight/effect`
## Effect 列表
### Effect 2205
- `argsNum`: `0`
- `info`: `自身处于异常状态时,克制倍数取自身对对手、对手对自身克制倍数中的最大值`
### Effect 2206
- `argsNum`: `1`
- `info`: `{0}回合内自身能力提升状态被消除则解除自身所处的异常状态`
### Effect 2207
- `argsNum`: `1`
- `info`: `使自身所处的异常状态剩余回合数-{0}`
### Effect 2208
- `argsNum`: `1`
- `info`: `自身星盘每转动1刻技能提升{0}点威力,回返期间提升效果翻倍`
### Effect 2209
- `argsNum`: `1`
- `info`: `自身星盘每转动1刻吸取对手{0}点体力,回返期间改为汲取体力`
## 备注
- 该清单按当前仓库静态注册结果生成如果某个 effect 实际通过其他模块或运行时路径实现需要先复核后再落代码
- `201``445` 这类占位 effect优先补核心逻辑或补充明确的不可实现说明

View File

@@ -1,35 +0,0 @@
# Task 322: Effects 2225-2229
## 目标
- 补齐以下 5 或最后一组不足 5 当前判定未实现的 skill effect
- 实现位置优先放在 `logic/service/fight/effect/`
- effect 需要展示说明同步更新 `logic/service/fight/effect/effect_info_map.go`
- 完成后至少执行`cd /workspace/logic && go test ./service/fight/effect`
## Effect 列表
### Effect 2225
- `argsNum`: `1`
- `info`: `减少对手体力上限的1/{0}`
### Effect 2226
- `argsNum`: `1`
- `info`: `解除双方所处的异常状态,解除成功则免疫下{0}次对手的攻击`
### Effect 2227
- `argsNum`: `1`
- `info`: `消除对手能力提升状态,消除成功对手下{0}回合无法主动切换精灵`
### Effect 2228
- `argsNum`: `2`
- `info`: `{0}回合内对手造成的固定伤害和百分比伤害减少{1}%`
### Effect 2229
- `argsNum`: `1`
- `info`: `{0}回合内使用技能则全属性+1自身回合类效果、能力提升效果被消除、吸取时触发自身的登场效果`
## 备注
- 该清单按当前仓库静态注册结果生成如果某个 effect 实际通过其他模块或运行时路径实现需要先复核后再落代码
- `201``445` 这类占位 effect优先补核心逻辑或补充明确的不可实现说明

View File

@@ -1,218 +0,0 @@
---
name: fight-effect-impl
description: Implement or repair Go fight effects in the Blazing battle system. Use when working in logic/service/fight/effect or nearby boss hooks, especially for missing effect tasks, effect registration, hook selection, delayed/round effects, status application, effect_info_map updates, and package-level validation.
---
# Fight Effect Impl
Implement effect work in the existing battle framework instead of inventing a parallel pattern.
## Workflow
1. Read the task source first.
If the request comes from `docs/effect-unimplemented-tasks/`, open the task file and extract effect IDs, arg counts, and description text.
2. Confirm whether each effect is actually missing.
Search for both type names and registrations. Do not rely only on direct `InitEffect(...)` grep results.
Also inspect shared registration files such as:
- `logic/service/fight/effect/sterStatusEffects.go`
- `logic/service/fight/effect/effect_power_doblue.go`
- `logic/service/fight/effect/EffectAttackMiss.go`
- `logic/service/fight/effect/EffectPhysicalAttackAddStatus.go`
- `logic/service/fight/effect/EffectDefeatTrigger.go`
- `logic/service/fight/effect/effect_attr.go`
3. Reuse the nearest local pattern.
Open effects with similar timing or semantics before writing code. Prefer matching existing hooks, helper bases, registration style, and comments over building a generic abstraction.
4. Choose the hook from battle flow, not from description text alone.
Read `logic/service/fight/input/interface.go`, `logic/service/fight/fightc.go`, and `logic/service/fight/loop.go` when timing is unclear.
## Effect Hooks
Use this section when effect timing is unclear.
### Core call order
The main references are:
- `logic/service/fight/input/interface.go`
- `logic/service/fight/fightc.go`
- `logic/service/fight/loop.go`
Typical attack flow inside `processSkillAttack` and `enterturn` is:
1. defender `SkillHit_ex()`
2. attacker `SkillHit()`
3. attacker `CalculatePre()`
4. attacker `OnSkill()`
5. defender `Damage(...)` settles red/fixed/true damage
6. defender `Skill_Use_ex()`
7. attacker `Skill_Use()`
8. defender `Action_end_ex()`
9. attacker `Action_end()`
10. both sides `TurnEnd()` at round end
11. all live effects `OnBattleEnd()` at fight end
### Hook selection cheatsheet
- `SkillHit_ex()`
Use for defender-side pre-hit interception, miss forcing, and hit-rate disruption.
- `SkillHit()`
Use for attacker-side power, crit, or skill-property changes before damage is computed.
- `CalculatePre()`
Use for temporary state rewrites that must exist during power calculation and then be restored.
- `OnSkill()`
Use for on-hit side effects, extra fixed damage setup, healing, status attach, or delayed-effect spawning.
- `ActionStartEx()`
Use for defender-side pre-action gates.
- `ActionStart()`
Use for attacker-side action gating, forced no-action behavior, and same-turn priority-sensitive logic.
- `Skill_Use_ex()`
Use for defender-side after-being-targeted behavior.
- `Skill_Use()`
Use for attacker-side after-using-skill behavior.
- `ComparePre()`
Use for priority changes before turn order is finalized.
- `TurnStart()`
Use for per-round setup or replacing the current round's selected action before execution.
- `TurnEnd()`
Use for countdown or expiry. The default node decrements positive durations and clears zero-duration effects.
- `OnBattleEnd()`
Use only when the effect truly settles at battle end. Confirm any reward path can be persisted from this hook.
### Repo-specific cautions
- `EffectCache` matters.
Parsed skill effects are stored in `EffectCache` before execution. If a first-turn charge effect must suppress the rest of the skill's side effects, explicitly disable sibling cached effects for that turn.
- `addSubEffect(...)` is lightweight.
Read `logic/service/fight/effect/sub_effect_helper.go` before assuming helper behavior. The current helper forwards IDs and args, but does not automatically apply the `duration` argument to the sub-effect instance.
- Team-wide healing is limited by current model.
There is no generic battle-target abstraction for friendly bench targets. If the effect heals all owned pets, iterate `AllPet` and mutate non-active pets carefully.
- Static task scans can be false positives.
Task documents may flag effects as missing even when they already exist in grouped files or shared registration files. Verify before editing.
## Implementation Rules
- Prefer existing base structs in `logic/service/fight/effect/sub_effect_helper.go` when they already match duration behavior.
- Verify helper semantics before using them. In this repo, some helpers are thinner than their names suggest.
- For status effects, create them through `InitEffect(input.EffectType.Status, ...)` and add them through `AddEffect(...)` on the target input.
- For healing, use `Input.Heal(...)` for the active battler and mutate non-active owned pets only when the current model already stores them in `AllPet`.
- For battle-end rewards or delayed settlement, confirm the hook is actually executed in `loop.go` before coding against it.
- Keep comments short and effect-focused.
## Batch Work Rules
When continuing `docs/effect-unimplemented-tasks/` in batches:
- Split work by grouped file and assign disjoint write ranges.
- Avoid touching `logic/service/fight/effect/effect_info_map.go` during parallel effect implementation unless the user explicitly asks for description-map updates in the same pass.
- Treat task docs as a backlog, not ground truth. A task file may still exist even when some IDs in the slice were already implemented or partially repaired.
- Prefer finishing the easiest grounded IDs in a partial slice instead of repeatedly rescanning the entire slice.
- Keep a clear list of:
- newly implemented IDs,
- already-existing IDs,
- still-partial IDs,
- task docs safe to delete.
## Frequent Compile Pitfalls
Before considering a slice done, check these repo-specific issues:
- `input.InitEffect(...)` always needs all three args: effect type, numeric ID, effect instance.
- If a new sub-effect type is added, also register the sub-effect explicitly in `init()`.
- `SkillEntity.XML.Power` and `Priority` are `int`-based in current generated structs; avoid mixing with `int8`, `int32`, or `int64` arithmetic.
- Skill PP on runtime battle state is commonly `uint32`; cast carefully when subtracting or restoring PP.
- `model.SkillInfo` does not expose fields like `MaxPP` or `Category`; look them up through `xmlres.SkillMap[int(skill.ID)]`.
- `xmlres.Move` does not expose every runtime field. Use runtime state such as `SkillEntity.AttackTime` when the XML struct lacks a field.
- `input.Input` does not always expose nested objects assumed by task text, such as `Opp.SkillEntity`; verify available runtime fields before coding.
- Decimal math must use `alpacadecimal` values, not raw integer literals.
- Large grouped files can accidentally keep stale duplicate `init()` registration blocks after manual merges or batch patches. Check the file tail before closing out a slice.
## Partial Slice Strategy
When a grouped file is only partially grounded:
- Do not delete the task docs for that slice yet.
- Keep the implemented IDs in place and make the remaining gaps explicit.
- Prefer conservative, repo-shaped implementations over speculative full feature work for heavy system effects.
- Good candidates to finish in partial slices are:
- simple priority modifiers,
- PP-based power changes,
- round-based sub-effects using existing helper bases,
- status immunity or status application patterns that already exist nearby.
- Leave highly coupled systems partial if they appear to depend on larger mechanics not yet modeled in nearby code, such as custom resource tracks or complex transformation states.
## Task Doc Deletion
Delete a task file only when one of these is true:
- every effect ID in the task range is now implemented in repo code,
- the IDs already existed and were verified as not missing,
- or the user explicitly accepts documented non-implementation for reserved placeholders.
Do not delete the task doc when the slice is still mixed between implemented and partial IDs.
## Common Tasks
### Random power or conditional power effects
Use `SkillHit()` when the effect changes `SkillEntity.XML.Power` before damage is calculated.
Examples in repo: `139.go`, `effect_power_doblue.go`, `600_605.go`.
### Hit-time status or side effects
Use `OnSkill()` when the effect should fire after hit/damage calculation setup and before final damage application is settled.
For direct status application, initialize the status effect from the source input and add it to the opponent.
### Round or delayed effects
For multi-turn logic, confirm whether the effect should:
- modify this turn only,
- start next turn,
- trigger when attacked,
- or replace the next selected skill.
For next-turn logic, inspect nearby effects such as `62`, `407`, `440`, `499`, `551`, `560`, and any adjacent ID patterns.
### Two-turn charge effects
Preserve the repo's existing battle loop assumptions.
A practical pattern is:
- cache the release skill on the first turn,
- suppress first-turn damage/effect output,
- inject the cached skill into the next turn's selected action in `TurnStart`,
- avoid double PP consumption in `HookPP`.
### Reward-on-battle-end effects
Check `OnBattleEnd()` execution in `logic/service/fight/loop.go`.
If a reward has a daily cap, prefer the shared counter utilities under `common/data/share/` over inventing new state.
## Validation
Run, at minimum:
- `cd /workspace/logic && go test ./service/fight/effect`
- `cd /workspace/logic && go build ./...`
If the user explicitly says tests are not required for the current pass, downgrade validation to:
- `gofmt -w` on every edited Go file,
- fix current editor/compiler diagnostics for touched files,
- and report that package tests/build were intentionally skipped by user request.
If the task came from `docs/effect-unimplemented-tasks/`, remove the completed task file when the user asked for it.
## Output
When finishing a task, report:
- which effect IDs were truly implemented,
- which IDs already existed and were left untouched,
- validation commands actually run,
- any remaining model limitations or behavior assumptions.

View File

@@ -0,0 +1,400 @@
# 战斗系统对齐 `flash/group` 组队战斗实施清单执行版
日期2026-04-04
适用仓库`E:\newcode\sun`
参考客户端仓库`E:\newcode\flash`
---
## 1. 结论与范围
### 1.1 结论
- `sun` 当前战斗系统具备多战位骨架`ActorIndex/TargetIndex``Our/Opp []*input.Input`但未完成组队战斗全链路
- `flash` `group` 分支当前 HEAD 已回滚组队重构组队实现主要存在于历史提交 `4c07fa07`
- 因此本次不是直接搬代码而是按协议与行为对齐实现
### 1.2 本清单目标
- 在不破坏现有 `1v1` 的前提下落地组队战斗可运行版本MVP
- 对齐 `flash`/社区实现中的关键行为开战出招切宠道具结算战斗结束
- 协议层采用一个统一结构体 + phase 字段方案单打/双打共用同一序列化模型
- 保留旧 `24xx/25xx` 流程入口通过服务端适配映射到统一结构体
### 1.3 非目标
- 不要求一次性 100% 复刻客户端所有 UI/演出细节
- 不要求一次性改完全部 effect先保证核心流程可跑再分批清理
---
## 2. 基线事实实施前必须统一认知
### 2.1 `flash` 仓库事实
- `group` 分支相对 `main` 的提交
- `4c07fa07 refactor(group-fight)`引入组队
- `a410bfca Revert "refactor(group-fight)"`回滚组队
- `e2382a4f`地图重构
- `bd84f206`.gitignore
- 所以 `group` HEAD 不再包含 `GroupFightDLL``core/group/*` 组队代码需参考 `4c07fa07` 的内容
### 2.2 `sun` 战斗现状
- 已有多战位骨架
- `logic/service/fight/input.go``Our/Opp []*input.Input`
- `logic/service/fight/action/BattleAction.go``ActorIndex/TargetIndex`
- `logic/service/fight/new_options.go``WithFightPlayersOnSide/WithFightInputs`
- 仍有关键缺口
- 控制器入站仍是单战位参数 `2405/2406/2407` 只传技能/道具/catchTime
- 回合主链仍以双动作兼容流程为中心
- 组队相关特性存在 TODO例如 `501/502/503`
### 2.3 外部实现参考本次新增
- `arcadia-star/seer2-fight-ui`
- 双打核心模型不是独立命令集而是统一帧模型 + `uiStyle + side + position`
- `uiStyle` 支持 `2v2/2v1`战位通过 `position(main/sub)` 区分
- `arcadia-star/seer2-next-message/src/entity/fight.rs`
- 采用统一战斗实体结构`team/user/pet` + `side/position`
- 行为包拆分为 `Load/Hurt/Change/Escape/...`但底层字段模型统一
- `ukuq/seer2-server/src/seer2/fight`
- `ArenaResourceLoadCMD -> TeamInfo -> FightUserInfo -> FighterInfo` 为层级化统一结构
- `FighterInfo` 直接包含 `position/hp/maxHp/anger/skills`适合直接映射为本项目统一结构体
---
## 3. 协议对齐清单按优先级
> 说明本清单改为统一协议结构体路线不再强制先实现 `75xx` 独立命令族
> 推荐做法保留旧入口命令服务端内部统一转为 `FightActionEnvelope/FightStateEnvelope`
### 3.1 P0 必做MVP 必须
- [ ] 统一入站动作结构 `FightActionEnvelope`
- 最少字段`actionType/actorIndex/targetIndex/skillId/itemId/catchTime/escape/chat`
- 兼容映射
- `2405 -> actionType=skill`
- `2406 -> actionType=item`
- `2407 -> actionType=change`
- `2410 -> actionType=escape`
- [ ] 统一出站状态结构 `FightStateEnvelope`
- 最少字段
- `phase``start/skill_hurt/change/over/load/chat`
- `left[]/right[]`元素为统一 `FighterState`
- `meta`回合号天气胜负结束原因
- [ ] 统一战位子结构 `FighterState`
- 每项至少包含`side/position(userSlot)/userId/petId(catchTime)/hp/maxHp/level/anger/status/prop/skills`
### 3.2 P1 强烈建议提升一致性
- [ ] 完善 `phase=skill_hurt`
- 至少带施法方快照受击方快照技能暴击伤害HP 变更
- [ ] 完善 `phase=change`
- 至少带切宠发起位切入目标位新精灵状态
- [ ] 完善 `phase=over`
- 至少带结束原因胜方收益主体
- [ ] 完善 `phase=load/chat`
- 组队加载进度战斗内聊天统一走同一 envelope
### 3.3 P2 视时间补齐
- [ ] `phase=sprite_die/sprite_notice/win_close`
- [ ] `phase=skill_wait/skill_wait_notice`
- [ ] `phase=overtime/timeout_exit/relation_notice`
---
## 4. 代码改造任务清单可直接分工
## 4.1 协议与结构层Owner A
- [ ] 新增统一协议结构文件
- 建议新建`logic/service/fight/cmd_unified.go`
- 要求统一定义 `FightActionEnvelope` 和映射辅助结构
- [ ] 新增统一出站结构
- 建议新建`logic/service/fight/info/unified_info.go`
- 要求定义 `FightStateEnvelope/FighterState`支持单打与双打
- [ ] 统一战位字段命名规范
- `actorIndex`我方执行位
- `targetIndex`敌方目标位
- `side+pos` `actorIndex/targetIndex` 转换规则写入注释
验收
- [ ] cmd`2405/2406/2407/2410`可无损映射到统一入站结构
- [ ] 统一出站结构在 `start/skill_hurt/change/over` phase 均可序列化
---
## 4.2 控制器与路由层Owner B
- [ ] 新增统一动作入口可单文件
- 建议新建`logic/controller/fight_unified.go`
- 用途将旧包和未来扩展包统一落到 `FightActionEnvelope`
- [ ] 兼容旧协议入口
- `2405/2406/2407` 保持可用默认 `actorIndex=0,targetIndex=0`
- 组队场景由 `actorIndex/targetIndex` 与战斗上下文决定不再依赖独立 `75xx`
- [ ] 增加战前校验
- 成员是否在同一组队房间
- 战斗状态互斥
- 战位可操作权限
验收
- [ ] 任意技能动作都能转化为 `UseSkillAt(...)` `actorIndex/targetIndex`
- [ ] 非法战位命令被拒绝不影响其他战位
---
## 4.3 战斗核心层Owner C
- [ ] 固化多动作一回合模型
- `collectPlayerActions`按预期战位数收集不是按两人收集
- `resolveRound`每回合一次统一排序与执行
- [ ] 降低对双动作 enterturn的耦合
- 当前 `enterturn(first, second)` 作为兼容层保留
- 新逻辑要确保
- 回合开始钩子只执行一次/回合
- 回合结束钩子只执行一次/回合
- 不因 pair 分片导致重复触发
- [ ] 完善动作-战位映射
- `GetInputByAction` 在组队模式下严格按 `playerID + actorIndex/targetIndex` 定位
- 超时补默认动作按战位补齐
- [ ] 完善死亡换宠/主动换宠
- actorIndex 粒度处理
- 切宠广播必须携带 actor 位信息
验收
- [ ] 2v2 场景一回合四动作都参与排序不丢动作
- [ ] 同玩家多战位动作不会互相覆盖
- [ ] 任一战位死亡只影响对应战位换宠链路
---
## 4.4 组队战报与广播层Owner D
- [ ] 统一战报快照结构
- 至少包含
- 施法方`userId/actorIndex/skillId/crit/dmg/hpAfter/status/prop`
- 受击方`userId/actorIndex/hpAfter/status/prop`
- [ ] 完成关键广播
- 开战广播
- 技能结果广播
- 切宠成功广播
- 战斗结束广播
- [ ] 保留旧包兼容必要时双发
- 单打/双打统一走同一结构体
- 如前端未升级可按需保留旧 `2503/2505/2506` 过渡映射
验收
- [ ] 观战端/队友端收到的战位与 HP 同步一致
- [ ] 切宠后不会出现错位显示
---
## 4.5 Effect 与规则层Owner E
- [ ] 先补明确组队依赖效果
- `logic/service/fight/boss/NewSeIdx_501.go`
- `logic/service/fight/boss/NewSeIdx_502.go`
- `logic/service/fight/boss/NewSeIdx_503.go`
- [ ] 统一队友查询工具函数
- 建议在 `input` `fight` 层提供
- 获取同阵营存活战位
- 获取队友列表排除自己
- 群体目标选择上限
- [ ] 扫描组队敏感 effect
- 特别关注含组队对战时无效描述项 effect 457
- 明确是直接禁用还是按组队模式替代逻辑
验收
- [ ] `501/502/503` 2v2 场景行为符合设计
- [ ] 组队模式下不再出现空指针或越界
---
## 4.6 测试与回归Owner F
- [ ] 单测补齐
- `logic/service/fight/action_test.go`继续扩充多战位覆盖
- 新增建议
- `logic/service/fight/loop_multi_test.go`
- `logic/service/fight/fight_group_test.go`
- [ ] 集成回归用例最少
- Case 11v1 旧流程
- Case 22v2 双方四动作
- Case 3同一玩家两战位各自出招
- Case 4中途切宠 + 被动死亡切宠
- Case 5超时默认动作补齐
- Case 6逃跑/掉线结束
- [ ] 构建与测试命令
- `cd logic && go test ./service/fight/...`
- `cd logic && go test ./controller/...`
- `cd logic && go build ./...`
---
## 5. 文件级任务地图便于派工
- 协议/结构
- `logic/service/fight/cmd.go`
- `logic/service/fight/cmd_unified.go`新增
- `logic/service/fight/info/info.go`
- `logic/service/fight/info/unified_info.go`新增
- 控制器
- `logic/controller/fight_base.go`
- `logic/controller/fight_pvp_withplayer.go`
- `logic/controller/fight_unified.go`新增
- 核心流程
- `logic/service/fight/new.go`
- `logic/service/fight/new_options.go`
- `logic/service/fight/input.go`
- `logic/service/fight/action.go`
- `logic/service/fight/loop.go`
- `logic/service/fight/fightc.go`
- Effect
- `logic/service/fight/boss/NewSeIdx_501.go`
- `logic/service/fight/boss/NewSeIdx_502.go`
- `logic/service/fight/boss/NewSeIdx_503.go`
- 其他含组队语义的 effect 文件
- 测试
- `logic/service/fight/action_test.go`
- `logic/service/fight/*_test.go`新增
---
## 6. 里程碑与交付标准
### M1协议可通
- [ ] 统一结构体可完成 `start/skill_hurt/change/over` 四类下发
- [ ] 旧命令入口均可映射到 `FightC` indexed 接口
### M2核心可跑
- [ ] 2v2 全回合可稳定执行
- [ ] 切宠/道具/超时可用
### M3规则可用
- [ ] 501/502/503 完成
- [ ] 主要组队战报可用
### M4回归上线
- [ ] 1v1 不回归
- [ ] `go test` `go build` 通过
- [ ] 文档补充已完成项与遗留项
---
## 7. 风险清单与缓解
- 风险旧逻辑大量默认 `CurPet[0]`多人战位容易错位
缓解引入统一 `CurrentPetByActor`/`TargetByIndex` 访问函数禁止新代码直接写死 `[0]`
- 风险`enterturn` 兼容层导致钩子重复触发
缓解回合开始/结束 pair 执行中抽离确保每回合只触发一次
- 风险协议切换导致旧客户端不可用
缓解服务端保持旧入口不变先做旧包 -> 统一结构映射前端按版本切流
- 风险effect 批量改动引发回归
缓解先做组队关键 effect其他 effect 分批迁移并每批回归
---
## 8. 实施顺序建议最小阻塞
1. 协议结构与控制器入口
2. 动作收集与回合统一执行
3. 切宠/道具/超时按战位修正
4. 关键广播与战报
5. 组队 effect501/502/503
6. 全量测试与回归
---
## 9. 交接要求给执行同学
- 每完成一个里程碑 `docs/` 新增一段完成项/未完成项/阻塞项
- 如改动协议字段必须附抓包样例或字段注释不允许只改代码不补说明
- 如发现与本清单冲突的历史逻辑兼容线上行为优先并在文档记录偏差原因
---
## 10. 可实现性结论统一协议结构体
- 结论可实现且风险可控
- 依据
- `seer2-fight-ui` 的双打模型本质是统一数据结构 + `uiStyle/side/position`不是强依赖独立命令族
- `seer2-next-message` `seer2-server` 都采用统一 `team/user/pet` 层级结构`position` 作为战位核心字段
- 本仓库已具备 `actorIndex/targetIndex` `UseSkillAt/ChangePetAt/UseItemAt` 能力协议统一后只需补齐映射和广播
- 实施建议
- 先完成旧入口 -> 统一入站结构映射
- 再完成统一出站结构 + phase 广播
- 最后做前端切换与旧包退场或长期双通道兼容
---
## AtkType 目标语义补充2026-04-05
来源`flash` `SkillXMLInfo.getGpFtSkillType(skillID)`读取 `movesMap/moveStoneMap` `AtkType`
GBTL 规则已确认
1. `AtkNum`本技能同时攻击数量默认 `1`不能为 `0`
2. `AtkType`目标范围
- `0`所有人
- `1`仅己方
- `2`仅对方
- `3`仅自己
- 默认`2`
前端目标选择行为`SkillMouseController.attack(skillID, attackType)`
1. `attackType=0` -> `allPetWinList`全体可选
2. `attackType=1` -> `membPetWinList`己方可选含自己与队友
3. `attackType=2` -> `oppPetWinList`敌方可选
4. `attackType=3` -> `[playerMode.petWin]`仅自己
后端目标关系判定组队/多战位必须遵循
1. 若协议传 `actor + target(side,pos)`
- `target.side != actor.side` => 对方目标
- `target.side == actor.side && target.pos == actor.pos` => 自身目标
- `target.side == actor.side && target.pos != actor.pos` => 队友目标
2. 若协议未显式传目标 `2405`
- `AtkType` 兜底
- `AtkType=3` => 强制自身
- `AtkType=1` => 默认自身无显式队友位时
- 其他 => 维持旧行为默认对方 `0`
实施要求与现有清单并行
1. `common/data/xmlres/skill.go` `Move` 需包含 `AtkType` 字段解析
2. 动作目标不再依赖默认 Opp 绑定effect 上下文必须使用本次动作的实际目标
3. 需支持区分 `self` `ally`例如同为 `AtkType=1` 不能混用同一默认目标
4. 保持旧协议兼容旧入口不报错但按上述兜底规则执行

View File

@@ -0,0 +1,194 @@
# Fight Input 控制绑定说明
日期2026-04-04
## 1. 背景
当前战斗模型中一个 `Input` 对应一个战斗站位`actorIndex`
每个 `Input` 通过 `Input.Player` 绑定操作者
当前建战主路径已收敛为`WithFightInputs(ourInputs, oppInputs)`
先由调用方创建并组装双方 `Input`再传给战斗模块
为了同时支持以下两种玩法新增了可配置绑定策略
1. 双打一个玩家控制多个站位单人多 `Input`
2. 组队一个玩家控制一个站位每人一个 `Input`
## 2. 绑定策略
文件`logic/service/fight/new_options.go`
- `InputControllerBindingKeep`
- 含义保持输入中已有 `Input.Player` 绑定不覆盖
- 适用调用方已手动构造 `Input` 绑定
- `InputControllerBindingSingle`
- 含义单侧全部站位统一绑定为 `players[0]`
- 适用双打中一个人控制多个站位
- `InputControllerBindingPerSlot`
- 含义按站位顺序绑定为 `players[i]`
- 适用组队中一人一个站位
- 说明 `players` 数量不足时回退绑定 `players[0]`
## 3. 选项接口
文件`logic/service/fight/new_options.go`
新增选项
```go
WithInputControllerBinding(mode int)
```
## 4. 生效时机
文件`logic/service/fight/new.go`
`buildFight` 构建完 `Our/Opp` 输入后先执行控制绑定再执行上下文绑定
1. `bindInputControllers(f.Our, f.OurPlayers, opts.controllerBinding)`
2. `bindInputControllers(f.Opp, f.OppPlayers, opts.controllerBinding)`
3. `bindInputFightContext(...)`
4. `linkTeamViews()`
5. `linkOppInputs()`
## 5. 使用示例
### 5.1 双打单人控多站位
```go
fight.NewFightWithOptions(
fight.WithFightPlayersOnSide(
[]common.PlayerI{ourPlayer},
[]common.PlayerI{oppPlayer},
),
fight.WithFightInputs(ourInputs, oppInputs),
fight.WithInputControllerBinding(fight.InputControllerBindingSingle),
)
```
### 5.2 组队一人一个站位
```go
fight.NewFightWithOptions(
fight.WithFightPlayersOnSide(
[]common.PlayerI{ourP1, ourP2},
[]common.PlayerI{oppP1, oppP2},
),
fight.WithFightInputs(ourInputs, oppInputs),
fight.WithInputControllerBinding(fight.InputControllerBindingPerSlot),
)
```
### 5.3 仅传已绑定 Input推荐灵活接入
```go
ourInputs := []*input.Input{
input.NewInput(nil, ourP1), // 站位0
input.NewInput(nil, ourP2), // 站位1
}
oppInputs := []*input.Input{
input.NewInput(nil, oppP1), // 站位0
input.NewInput(nil, oppP2), // 站位1
}
fc, err := fight.NewFightWithOptions(
fight.WithFightInputs(ourInputs, oppInputs),
// 不传 WithFightPlayersOnSide 也可
// owner/opponent 与 side players 会从 inputs 自动提取
)
_ = fc
_ = err
```
说明`InputControllerBindingSingle/PerSlot` 会覆盖 `ourInputs/oppInputs` 中原有的 `Input.Player` 绑定`Keep` 不覆盖
## 6. 新模式绑定实例逐模式
以下示例假设我方有两个站位`ourInputs[0]``ourInputs[1]`
### 6.1 Keep保持输入原绑定
调用
```go
fight.NewFightWithOptions(
fight.WithFightInputs(ourInputs, oppInputs),
fight.WithInputControllerBinding(fight.InputControllerBindingKeep),
)
```
输入调用前
- `ourInputs[0].Player = ourP1`
- `ourInputs[1].Player = ourP2`
结果调用后
- `ourInputs[0].Player = ourP1`
- `ourInputs[1].Player = ourP2`
适用调用方已提前把每个站位绑定好不希望框架覆盖
### 6.2 Single单人控制全部站位
调用
```go
fight.NewFightWithOptions(
fight.WithFightPlayersOnSide(
[]common.PlayerI{ourCaptain},
[]common.PlayerI{oppCaptain},
),
fight.WithFightInputs(ourInputs, oppInputs),
fight.WithInputControllerBinding(fight.InputControllerBindingSingle),
)
```
输入调用前
- `ourInputs[0].Player = ourP1`
- `ourInputs[1].Player = ourP2`
结果调用后
- `ourInputs[0].Player = ourCaptain`
- `ourInputs[1].Player = ourCaptain`
适用双打或多站位由同一玩家操作
### 6.3 PerSlot按站位顺序绑定玩家
调用
```go
fight.NewFightWithOptions(
fight.WithFightPlayersOnSide(
[]common.PlayerI{ourP1, ourP2},
[]common.PlayerI{oppP1, oppP2},
),
fight.WithFightInputs(ourInputs, oppInputs),
fight.WithInputControllerBinding(fight.InputControllerBindingPerSlot),
)
```
输入调用前
- `ourInputs[0].Player = anyA`
- `ourInputs[1].Player = anyB`
结果调用后
- `ourInputs[0].Player = ourP1`
- `ourInputs[1].Player = ourP2`
补位规则 `players` 数量不足例如只传一个 `ourP1`剩余站位回退绑定 `players[0]`
## 7. 注意事项
1. 默认模式是 `InputControllerBindingKeep`不影响现有调用
2. 若传入 `WithFightInputs(...)` 且每个 `Input.Player` 已预先绑定可继续用默认模式
3. 仅传 `WithFightInputs(...)` 也可工作框架会从 `ourInputs/oppInputs` 自动提取 `ourPlayers/oppPlayers`并以各侧首位玩家作为 owner/opponent
4. 推荐在新组队逻辑中显式传 `WithInputControllerBinding(...)`避免调用方歧义

View File

@@ -0,0 +1,327 @@
# 战斗多单位模式改造交接文档2026-04-04
## 0. 2026-04-04 本轮完成情况
本轮已完成以下落地项
- 动作提交改为按 `playerID + actorIndex` 去重同一玩家同回合的多个上场位动作不会再互相覆盖
- 主循环已从双动作入口改为动作列表入口`resolveRound` 现接收并处理 `[]action.BattleActionI`
- 回合结算增加了基于优先级和速度的统一排序并按跨阵营动作对子顺序执行保留现有 `enterturn(first, second)` 兼容层
- 技能和道具的目标选择已接入 `targetIndex`不再固定打对面 `0` 号位
- 切宠同步改为携带 `actorIndex`同一玩家多上场位的切宠播报不再冲突
- 开战同步结构新增当前战斗位数组同时保留 `Info1/Info2` 兼容旧结构
- `FightI` 已补充 `UseSkillAt/ChangePetAt/UseItemAt/GetCurrPETAt`
- `NewFight` 已改为包装 `NewFightWithOptions(...)`创建阶段开始支持 option/builder 扩展
- `Ctx` 已拆分为 `LegacySides + EffectBinding`effect 本体上挂载上下文并补充 `Source/Carrier/Target`
- 核心执行链已开始迁移到真实 source/target 语义`AddEffect``Exec``Damage``SetProp`主技能结算流程会注入实际对手上下文
- 已迁移一批公共/高复用 effect 到新语义包括状态基类击败触发物攻附加状态`1097-1101``680-690`部分魂印基础逻辑
- 本轮继续完成了 `1263-1287``1288-1312``1448-1472``1473-1497` 四组 effect 的迁移已不再直接依赖 `Ctx().Our/Opp`统一改为 `CarrierInput()/OpponentInput()` 访问当前承载侧与对位侧
- 增加了动作队列的基础单测覆盖同玩家不同槽位保留同槽位动作替换
本轮仍保留的限制
- `enterturn` 和大量 `effect/node` 逻辑仍是双动作上下文因此当前实现采用动作列表排序 + 跨阵营配对兼容执行的过渡方案而不是一次性重写所有效果系统
- `NewFight` 仍按现有建房流程创建双方 1 个战斗位本轮打通的是多战斗位结算骨架和接口不是外部建房入口的全量切换
- 大量具体 effect 仍在使用旧的 `Ctx().Our/Opp` 语义当前已迁移的是上下文承载方式执行链和部分公共基类具体 effect 仍需继续分批迁移
### 0.1 effect 迁移增量记录
本轮新增完成
- `logic/service/fight/effect/1263_1287.go`
- `logic/service/fight/effect/1288_1312.go`
- `logic/service/fight/effect/1448_1472.go`
- `logic/service/fight/effect/1473_1497.go`
这两组文件当前迁移策略是
- 旧语义中的 `Our` 统一视为当前执行/承载该 effect 的输入侧迁移为 `CarrierInput()`
- 旧语义中的 `Opp` 统一迁移为当前结算上下文里的对位输入侧 `OpponentInput()`
- 暂不在这一轮强行把所有 effect 重写成纯 `Source/Target/Carrier` 三元语义先保证 hostile sub-effect回合类 effect挂在对手身上的限制类 effect 都不再依赖 legacy 字段访问
### 0.2 下一批待迁移队列
高密度遗留文件已继续向后推进`1448-1497` 这两个分段本轮已清理完成
下一轮继续迁移时建议直接对 effect 包执行一次全量扫描仍包含 `Ctx().Our/Opp` grouped file继续往后收口而不是再只盯固定编号段
## 1. 任务目标
将当前战斗系统从每回合双方各 1 个动作的模型改造成支持多上场位多操作者的统一回合模型最终支持以下 3 种战斗模式
1. `1玩家:N精灵:1上场 VS 1玩家:N精灵:1上场`
2. `N玩家:N精灵:N上场 VS N玩家:N精灵:N上场`
3. `1玩家:N精灵:N上场 VS 1玩家:N精灵:N上场`
当前代码只完整支持模式 1模式 2 和模式 3 只做了结构铺垫还没有真正打通
---
## 2. 当前已完成的基础改造
以下结构改造已经落地
- `FightC.Our/Opp` 已改成数组表示战场单位数组不再是单对象
- `input.Input.CurrentPet` 已改成 `CurPet`并且是数组
- `FightC.OurPlayers/OppPlayers` 已加入用于表达操作者数组
- 战斗单位与操作者已解耦
- `Our/Opp` 表示战斗位
- `OurPlayers/OppPlayers` 表示操作这些战斗位的玩家
- `BattlePetEntity` 已支持绑定控制者`ControllerUserID`
- 动作模型已支持
- `ActorIndex`
- `TargetIndex`
- 已提供 indexed 入口
- `UseSkillAt(c, skillID, actorIndex, targetIndex)`
- `ChangePetAt(c, petID, actorIndex)`
- `UseItemAt(c, catchTime, itemID, actorIndex, targetIndex)`
当前默认行为仍等价于
- `actorIndex = 0`
- `targetIndex = 0`
也就是当前模式下仍然是操作和结算 `0` 号单位
---
## 3. 当前未完成的核心问题
### 3.1 回合模型仍然是双动作模型
目前主流程仍然是每回合只处理双方两个动作而不是处理一个动作列表
关键位置
- `logic/service/fight/loop.go`
- `collectPlayerActions(...)` 只收 2 个动作
- `resolveRound(p1Action, p2Action)` 只结算 2 个动作
这意味着
- 模式 2 无法支持双方多个操作者或多个上场位同时行动
- 模式 3 无法支持同一玩家控制多个上场位分别出手
### 3.2 动作提交仍按 `playerID` 去重
当前动作队列逻辑仍以 `playerID` 作为主要识别维度
关键位置
- `logic/service/fight/action.go`
- `submitAction(...)`
这会导致
- 同一玩家在同一回合给多个上场位下达动作时动作会互相覆盖或无法完整保留
这一点对模式 3 是直接阻塞对模式 2 也不够健壮
### 3.3 切宠和当前上场位逻辑仍大量默认使用 `CurPet[0]`
虽然 `CurPet` 已经是数组但主流程中不少逻辑仍固定操作 `0` 号位
典型影响
- 死亡换宠
- 主动换宠
- 当前出手单位检查
- 当前目标单位检查
这部分需要按 `actorIndex` 或上场槽位改造
### 3.4 开战协议仍然只有两个当前单位
当前开战下发协议仍然是双单位结构
关键位置
- `logic/service/fight/info/info.go`
- `FightStartOutboundInfo`
- 仍只有 `Info1` `Info2`
这不适合多上场位模式
### 3.5 公共接口仍是旧的单单位接口
关键位置
- `logic/service/common/fight.go`
- `FightI`
目前接口仍只有
- `UseSkill(c, id)`
- `ChangePet(c, id)`
- `UseItem(c, cacthid, itemid)`
indexed 版本只存在于具体实现 `FightC` 没有进入正式接口层
---
## 4. 当前实现与目标模式的对应关系
### 4.1 模式 1
`1玩家:N精灵:1上场 VS 1玩家:N精灵:1上场`
当前支持
原因
- 当前默认就是操作 `0` 号单位
- 当前默认就是攻击 `0` 号目标
- 当前回合系统仍是每边 1 个动作这与模式 1 一致
### 4.2 模式 2
`N玩家:N精灵:N上场 VS N玩家:N精灵:N上场`
当前不支持
直接原因
- 一回合只收 2 个动作
- 一回合只结算 2 个动作
- 协议仍只同步 2 个当前上场位
### 4.3 模式 3
`1玩家:N精灵:N上场 VS 1玩家:N精灵:N上场`
当前不支持
直接原因
- 同一玩家的多个动作无法作为同回合动作列表完整保留
- 主流程仍不是按动作列表统一排序和执行
---
## 5. 需要完成的工作
### 5.1 改造动作收集模型
将当前每边 1 个动作的模型改成每个可操作上场位 1 个动作的模型
至少需要做到
- 同一玩家可以在同一回合提交多个动作
- 每个动作能区分是哪个上场位发出的
- 每个动作能区分目标上场位
建议将动作唯一键至少扩为
- `playerID`
- `actorIndex`
### 5.2 改造回合结算模型
将当前
- `resolveRound(p1Action, p2Action)`
改成
- `resolveRound(actions []action.BattleActionI)`
并完成
- 动作列表排序
- 按优先级速度等规则统一排序
- 排序后逐个结算
注意
- 当前 effect/node 体系里仍有大量双动作接口不适合一次性全部重写
- 建议先在主流程做兼容层逐步过渡
### 5.3 按槽位处理切宠与死亡换宠
将当前固定 `CurPet[0]` 的逻辑改成按槽位处理
- 主动换宠
- 被动死亡换宠
- 死亡校验
- 出手资格判断
### 5.4 增加开战与战斗同步结构
将当前的双单位同步结构扩成可支持多上场位的结构但是保持协议结构不变现在是固定两个可以改成数组来实现
重点是
- 开战协议
- 当前上场位同步
- 切宠同步
- 可能的回合播报结构
### 5.5 补齐公共接口
indexed 版本能力补进接口层避免只能通过具体实现类型访问
建议新增类似接口
- `UseSkillAt(...)`
- `ChangePetAt(...)`
- `UseItemAt(...)`
---
## 6. 推荐实施顺序
建议按下面顺序推进避免一次性改动面过大
1. 先改动作队列和动作收集逻辑
2. 再改回合结算为动作列表
3. 再改切宠和死亡换宠按槽位处理
4. 最后改协议和正式接口
不建议一开始就全量重写 effect/node 接口因为当前大量效果实现仍假设双动作上下文
---
## 7. 建议重点查看文件
- `logic/service/fight/action.go`
- `logic/service/fight/loop.go`
- `logic/service/fight/fightc.go`
- `logic/service/fight/input.go`
- `logic/service/fight/input/input.go`
- `logic/service/fight/action/BattleAction.go`
- `logic/service/fight/info/info.go`
- `logic/service/common/fight.go`
---
## 8. 完成标准
至少满足以下条件才算这次改造完成
1. 同一玩家可以在同一回合给多个上场位分别提交动作动作不会互相覆盖
2. 双方多个上场位可以在同一回合统一排序并依次结算
3. 攻击目标位可选不再默认只能打对面 `0`
4. 切宠可以按上场槽位处理
5. 模式 1 不回归
6. 代码编译通过
---
## 9. 最低验证要求
至少执行
- `cd /workspace/logic && go build ./...`
- `cd /workspace/logic && go test ./service/fight/effect`
如果本轮改动较大建议再补一轮
- `cd /workspace/logic && go test ./...`
---
## 10. 额外提醒
- 当前仓库工作区可能是脏的不要回滚无关修改
- 这次改造的真正核心不是结构字段改数组而是把回合系统从双动作模型改成动作列表模型
- 已有 `ActorIndex/TargetIndex` 只是入口铺垫不代表多单位模式已经完成

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

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

View File

@@ -12,6 +12,7 @@ import (
"github.com/gogf/gf/v2/util/grand" "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) { func (h Controller) EggGamePlay(data1 *C2S_EGG_GAME_PLAY, c *player.Player) (result *S2C_EGG_GAME_PLAY, err errorcode.ErrorCode) {
switch data1.EggNum { 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 { if data1.EggNum > 10 || data1.EggNum <= 0 {
return nil, errorcode.ErrorCode(errorcode.ErrorCodes.ErrSystemError) return nil, errorcode.ErrorCode(errorcode.ErrorCodes.ErrSystemError)
} }
if r < 0 { if r <= 0 || data1.EggNum > r {
return nil, errorcode.ErrorCode(errorcode.ErrorCodes.ErrGachaTicketsInsufficient) 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) return nil, errorcode.ErrorCode(errorcode.ErrorCodes.ErrGachaTicketsInsufficient)
} }
result = &S2C_EGG_GAME_PLAY{ListInfo: []data.ItemInfo{}} result = &S2C_EGG_GAME_PLAY{ListInfo: []data.ItemInfo{}}
if grand.Meet(int(data1.EggNum), 100) { 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 { for _, item := range addedItems {
result.ListInfo = append(result.ListInfo, data.ItemInfo{ItemId: item.ItemId, ItemCnt: item.ItemCnt}) result.ListInfo = append(result.ListInfo, data.ItemInfo{ItemId: item.ItemId, ItemCnt: item.ItemCnt})
} }
c.Service.Item.UPDATE(400501, int(-data1.EggNum))
return return
} }

View File

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

View File

@@ -7,6 +7,7 @@ import (
"blazing/logic/service/player" "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) { 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{} 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 { type C2s_LEIYI_TRAIN_GET_STATUS struct {
Head common.TomeeHeader `cmd:"2393" struc:"skip"` //玩家登录 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"` Status [10]S2C_LEIYI_TRAIN_GET_STATUS_info `json:"status"`
} }
// S2C_LEIYI_TRAIN_GET_STATUS_info 定义请求或响应数据结构。
type S2C_LEIYI_TRAIN_GET_STATUS_info struct { type S2C_LEIYI_TRAIN_GET_STATUS_info struct {
// Today uint32 // 今日训练HP次数 // Today uint32 // 今日训练HP次数
Current 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 return result, -1
} }
// C2S_2608 定义请求或响应数据结构。
type C2S_2608 struct { type C2S_2608 struct {
Head common.TomeeHeader `cmd:"2608" struc:"skip"` Head common.TomeeHeader `cmd:"2608" struc:"skip"`
} }

View File

@@ -2,7 +2,6 @@ package controller
import ( import (
"blazing/common/socket/errorcode" "blazing/common/socket/errorcode"
"blazing/modules/player/model"
"blazing/logic/service/fight" "blazing/logic/service/fight"
"blazing/logic/service/fight/info" "blazing/logic/service/fight/info"
@@ -18,7 +17,7 @@ func (h Controller) checkFightStatus(c *player.Player) errorcode.ErrorCode {
} }
// OnReadyToFight 准备战斗 // OnReadyToFight 准备战斗
func (h Controller) OnReadyToFight(data *fight.ReadyToFightInboundInfo, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) { func (h Controller) OnReadyToFight(data *ReadyToFightInboundInfo, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
if err := h.checkFightStatus(c); err != 0 { if err := h.checkFightStatus(c); err != 0 {
return nil, err return nil, err
} }
@@ -26,36 +25,108 @@ func (h Controller) OnReadyToFight(data *fight.ReadyToFightInboundInfo, c *playe
return nil, -1 return nil, -1
} }
// UseSkill 使用技能包 // GroupReadyFightFinish 旧组队协议准备完成。
func (h Controller) UseSkill(data *fight.UseSkillInInfo, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) { func (h Controller) GroupReadyFightFinish(data *GroupReadyFightFinishInboundInfo, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
if err := h.checkFightStatus(c); err != 0 { if err := h.checkFightStatus(c); err != 0 {
return nil, err return nil, err
} }
go c.FightC.UseSkill(c, data.SkillId) 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
}
h.dispatchFightActionEnvelope(c, buildLegacyUseSkillEnvelope(data))
return nil, 0
}
// UseSkillAt 组队/多战位技能包cmd=7505
// 目标关系0=对方 1=自己 2=队友。
func (h Controller) UseSkillAt(data *UseSkillAtInboundInfo, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
if err := h.checkFightStatus(c); err != 0 {
return nil, err
}
h.dispatchFightActionEnvelope(c, buildIndexedUseSkillEnvelope(data))
return nil, 0 return nil, 0
} }
// Escape 战斗逃跑 // Escape 战斗逃跑
func (h Controller) Escape(data *fight.EscapeFightInboundInfo, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) { func (h Controller) Escape(data *EscapeFightInboundInfo, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
if err := h.checkFightStatus(c); err != 0 { if err := h.checkFightStatus(c); err != 0 {
return nil, err return nil, err
} }
h.dispatchFightActionEnvelope(c, buildLegacyEscapeEnvelope())
go c.FightC.Over(c, model.BattleOverReason.PlayerEscape)
return nil, 0 return nil, 0
} }
// ChangePet 切换精灵 // ChangePet 切换精灵
func (h Controller) ChangePet(data *fight.ChangePetInboundInfo, c *player.Player) (result *info.ChangePetInfo, err errorcode.ErrorCode) { func (h Controller) ChangePet(data *ChangePetInboundInfo, c *player.Player) (result *info.ChangePetInfo, err errorcode.ErrorCode) {
if err := h.checkFightStatus(c); err != 0 { if err := h.checkFightStatus(c); err != 0 {
return nil, err return nil, err
} }
go c.FightC.ChangePet(c, data.CatchTime) h.dispatchFightActionEnvelope(c, buildLegacyChangeEnvelope(data))
return nil, -1 return nil, -1
} }
// Capture 捕捉精灵 // Capture 捕捉精灵
func (h Controller) Capture(data *fight.CatchMonsterInboundInfo, c *player.Player) (result *info.CatchMonsterOutboundInfo, err errorcode.ErrorCode) { func (h Controller) Capture(data *CatchMonsterInboundInfo, c *player.Player) (result *info.CatchMonsterOutboundInfo, err errorcode.ErrorCode) {
if err := h.checkFightStatus(c); err != 0 { if err := h.checkFightStatus(c); err != 0 {
return nil, err return nil, err
} }
@@ -70,7 +141,7 @@ func (h Controller) Capture(data *fight.CatchMonsterInboundInfo, c *player.Playe
} }
// LoadPercent 加载进度 // LoadPercent 加载进度
func (h Controller) LoadPercent(data *fight.LoadPercentInboundInfo, c *player.Player) (result *info.LoadPercentOutboundInfo, err errorcode.ErrorCode) { func (h Controller) LoadPercent(data *LoadPercentInboundInfo, c *player.Player) (result *info.LoadPercentOutboundInfo, err errorcode.ErrorCode) {
if c.FightC == nil { if c.FightC == nil {
return nil, -1 return nil, -1
} }
@@ -79,7 +150,7 @@ func (h Controller) LoadPercent(data *fight.LoadPercentInboundInfo, c *player.Pl
} }
// UsePetItemInboundInfo 使用宠物道具 // UsePetItemInboundInfo 使用宠物道具
func (h Controller) UsePetItemInboundInfo(data *fight.UsePetItemInboundInfo, c *player.Player) (result *info.UsePetIteminfo, err errorcode.ErrorCode) { func (h Controller) UsePetItemInboundInfo(data *UsePetItemInboundInfo, c *player.Player) (result *info.UsePetIteminfo, err errorcode.ErrorCode) {
if err := h.checkFightStatus(c); err != 0 { if err := h.checkFightStatus(c); err != 0 {
return nil, err return nil, err
} }
@@ -89,15 +160,15 @@ func (h Controller) UsePetItemInboundInfo(data *fight.UsePetItemInboundInfo, c *
} }
} }
go c.FightC.UseItem(c, data.CatchTime, data.ItemId) h.dispatchFightActionEnvelope(c, buildLegacyUseItemEnvelope(data))
return nil, -1 return nil, -1
} }
// FightChat 战斗聊天 // FightChat 战斗聊天
func (h Controller) FightChat(data *fight.ChatInfo, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) { func (h Controller) FightChat(data *ChatInfo, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
if err := h.checkFightStatus(c); err != 0 { if err := h.checkFightStatus(c); err != 0 {
return nil, err return nil, err
} }
go c.FightC.Chat(c, data.Message) h.dispatchFightActionEnvelope(c, buildChatEnvelope(data))
return nil, -1 return nil, -1
} }

View File

@@ -4,7 +4,6 @@ import (
"blazing/common/data" "blazing/common/data"
"blazing/common/data/xmlres" "blazing/common/data/xmlres"
"blazing/common/socket/errorcode" "blazing/common/socket/errorcode"
"strings"
"blazing/logic/service/fight" "blazing/logic/service/fight"
fightinfo "blazing/logic/service/fight/info" fightinfo "blazing/logic/service/fight/info"
@@ -18,13 +17,21 @@ import (
"github.com/gogf/gf/v2/util/grand" "github.com/gogf/gf/v2/util/grand"
) )
const (
rewardItemExpPool = 3
groupBossSlotLimit = 3
)
// PlayerFightBoss 挑战地图boss // PlayerFightBoss 挑战地图boss
func (Controller) PlayerFightBoss(req *fight.ChallengeBossInboundInfo, p *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) { func (Controller) PlayerFightBoss(req *ChallengeBossInboundInfo, p *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
if err = p.CanFight(); err != 0 { if err = p.CanFight(); err != 0 {
return nil, err 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) bossConfigs, err := loadMapBossConfigs(mapNode)
if err != 0 { if err != 0 {
return nil, err return nil, err
@@ -36,14 +43,15 @@ func (Controller) PlayerFightBoss(req *fight.ChallengeBossInboundInfo, p *player
} }
p.Fightinfo.Status = fightinfo.BattleMode.FIGHT_WITH_NPC 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 := player.NewAI_player(monsterInfo)
ai.CanCapture = resolveBossCaptureRate(bossConfigs[0].IsCapture, leadMonsterID) ai.CanCapture = resolveBossCaptureRate(bossConfigs[0].IsCapture, leadMonsterID)
ai.Prop[0] = 2 ai.BossScript = bossConfigs[0].Script
ai.AddBattleProp(0, 2)
var fightC *fight.FightC 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 { if mapNode.WinBonusID == 0 {
return return
} }
@@ -58,13 +66,40 @@ func (Controller) PlayerFightBoss(req *fight.ChallengeBossInboundInfo, p *player
return nil, -1 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 战斗野怪 // OnPlayerFightNpcMonster 战斗野怪
func (Controller) OnPlayerFightNpcMonster(req *fight.FightNpcMonsterInboundInfo, p *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) { func (Controller) OnPlayerFightNpcMonster(req *FightNpcMonsterInboundInfo, p *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
if err = p.CanFight(); err != 0 { if err = p.CanFight(); err != 0 {
return nil, err return nil, err
} }
if req.Number > 9 { if int(req.Number) >= len(p.Data) {
return nil, errorcode.ErrorCodes.ErrSystemError return nil, errorcode.ErrorCodes.ErrPokemonNotHere
} }
refPet := p.Data[req.Number] refPet := p.Data[req.Number]
@@ -79,7 +114,7 @@ func (Controller) OnPlayerFightNpcMonster(req *fight.FightNpcMonsterInboundInfo,
p.Fightinfo.Status = fightinfo.BattleMode.FIGHT_WITH_NPC p.Fightinfo.Status = fightinfo.BattleMode.FIGHT_WITH_NPC
p.Fightinfo.Mode = fightinfo.BattleMode.MULTI_MODE 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) handleNpcFightRewards(p, foi, monster)
}) })
if err != 0 { if err != 0 {
@@ -201,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) { func buildNpcMonsterInfo(refPet player.OgrePetInfo, mapID uint32) (*model.PetInfo, *model.PlayerInfo, errorcode.ErrorCode) {
if refPet.ID == 0 { if refPet.ID == 0 {
return nil, nil, errorcode.ErrorCodes.ErrPokemonNotExists return nil, nil, errorcode.ErrorCodes.ErrPokemonNotHere
} }
monster := model.GenPetInfo( monster := model.GenPetInfo(
@@ -250,7 +285,7 @@ func handleNpcFightRewards(p *player.Player, foi model.FightOverInfo, monster *m
rewards := &fightinfo.S2C_GET_BOSS_MONSTER{} rewards := &fightinfo.S2C_GET_BOSS_MONSTER{}
p.ItemAdd(3, int64(poolexp+addexp)) p.ItemAdd(3, int64(poolexp+addexp))
rewards.ADDitem(3, uint32(poolexp)) rewards.AddItem(rewardItemExpPool, uint32(poolexp))
p.AddPetExp(foi.Winpet, int64(addexp)) p.AddPetExp(foi.Winpet, int64(addexp))
if p.CanGetItem() { if p.CanGetItem() {
@@ -258,7 +293,7 @@ func handleNpcFightRewards(p *player.Player, foi model.FightOverInfo, monster *m
if itemID != 0 { if itemID != 0 {
count := uint32(grand.N(1, 2)) count := uint32(grand.N(1, 2))
if p.ItemAdd(itemID, int64(count)) { if p.ItemAdd(itemID, int64(count)) {
rewards.ADDitem(uint32(itemID), count) rewards.AddItem(uint32(itemID), count)
} }
} }
} }
@@ -268,10 +303,12 @@ func handleNpcFightRewards(p *player.Player, foi model.FightOverInfo, monster *m
xuanID := uint32(400686 + petType) xuanID := uint32(400686 + petType)
count := uint32(grand.N(1, 2)) count := uint32(grand.N(1, 2))
if p.ItemAdd(int64(xuanID), int64(count)) { if p.ItemAdd(int64(xuanID), int64(count)) {
rewards.ADDitem(xuanID, count) rewards.AddItem(xuanID, count)
} }
} }
if rewards.HasReward() {
p.SendPackCmd(8004, rewards) p.SendPackCmd(8004, rewards)
foi.Winpet.AddEV(gconv.Int64s(strings.Fields(petCfg.YieldingEV))) }
foi.Winpet.AddEV(petCfg.YieldingEVValues)
} }

View File

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

View File

@@ -12,7 +12,7 @@ import (
) )
// 接收战斗或者取消战斗的包 // 接收战斗或者取消战斗的包
func (h Controller) OnPlayerHandleFightInvite(data *fight.HandleFightInviteInboundInfo, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) { func (h Controller) OnPlayerHandleFightInvite(data *HandleFightInviteInboundInfo, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
if c.IsArenaPVPLocked() { if c.IsArenaPVPLocked() {
return nil, errorcode.ErrorCodes.ErrSystemError return nil, errorcode.ErrorCodes.ErrSystemError
} }
@@ -55,7 +55,7 @@ func (h Controller) OnPlayerHandleFightInvite(data *fight.HandleFightInviteInbou
return 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) //println("好友对战测试", foi.Reason)
@@ -78,7 +78,7 @@ func (h Controller) OnPlayerHandleFightInvite(data *fight.HandleFightInviteInbou
} }
// 邀请其他人进行战斗 // 邀请其他人进行战斗
func (h Controller) OnPlayerInviteOtherFight(data *fight.InviteToFightInboundInfo, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) { func (h Controller) OnPlayerInviteOtherFight(data *InviteToFightInboundInfo, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
if c.IsArenaPVPLocked() { if c.IsArenaPVPLocked() {
return nil, errorcode.ErrorCodes.ErrSystemError return nil, errorcode.ErrorCodes.ErrSystemError
} }
@@ -103,7 +103,7 @@ func (h Controller) OnPlayerInviteOtherFight(data *fight.InviteToFightInboundInf
} }
// 取消队列 // 取消队列
func (h Controller) OnPlayerCanceledOtherInviteFight(data *fight.InviteFightCancelInboundInfo, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) { func (h Controller) OnPlayerCanceledOtherInviteFight(data *InviteFightCancelInboundInfo, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
atomic.StoreUint32(&c.Fightinfo.Mode, 0) //设置状态为0 atomic.StoreUint32(&c.Fightinfo.Mode, 0) //设置状态为0
return return
} }

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

@@ -36,7 +36,7 @@ type towerChoiceState struct {
} }
// 暗黑门进入boss // 暗黑门进入boss
func (h Controller) FreshOpen(data *fight.C2S_OPEN_DARKPORTAL, c *player.Player) (result *fight.S2C_OPEN_DARKPORTAL, err errorcode.ErrorCode) { func (h Controller) FreshOpen(data *C2S_OPEN_DARKPORTAL, c *player.Player) (result *fight.S2C_OPEN_DARKPORTAL, err errorcode.ErrorCode) {
result = &fight.S2C_OPEN_DARKPORTAL{} result = &fight.S2C_OPEN_DARKPORTAL{}
towerBosses := service.NewTower110Service().Boss(uint32(data.Level)) towerBosses := service.NewTower110Service().Boss(uint32(data.Level))
@@ -57,7 +57,7 @@ func (h Controller) FreshOpen(data *fight.C2S_OPEN_DARKPORTAL, c *player.Player)
} }
// FreshChoiceFightLevel 处理玩家选择挑战模式(试炼之塔或勇者之塔) // FreshChoiceFightLevel 处理玩家选择挑战模式(试炼之塔或勇者之塔)
func (h Controller) FreshChoiceFightLevel(data *fight.C2S_FRESH_CHOICE_FIGHT_LEVEL, c *player.Player) (result *fight.S2C_FreshChoiceLevelRequestInfo, err errorcode.ErrorCode) { func (h Controller) FreshChoiceFightLevel(data *C2S_FRESH_CHOICE_FIGHT_LEVEL, c *player.Player) (result *fight.S2C_FreshChoiceLevelRequestInfo, err errorcode.ErrorCode) {
result = &fight.S2C_FreshChoiceLevelRequestInfo{} result = &fight.S2C_FreshChoiceLevelRequestInfo{}
c.Info.CurrentFreshStage = utils.Max(c.Info.CurrentFreshStage, 1) c.Info.CurrentFreshStage = utils.Max(c.Info.CurrentFreshStage, 1)
c.Info.CurrentStage = utils.Max(c.Info.CurrentStage, 1) c.Info.CurrentStage = utils.Max(c.Info.CurrentStage, 1)
@@ -82,7 +82,8 @@ func (h Controller) FreshChoiceFightLevel(data *fight.C2S_FRESH_CHOICE_FIGHT_LEV
return result, 0 return result, 0
} }
func (h Controller) FreshLeaveFightLevel(data *fight.FRESH_LEAVE_FIGHT_LEVEL, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) { // FreshLeaveFightLevel 处理控制器请求。
func (h Controller) FreshLeaveFightLevel(data *FRESH_LEAVE_FIGHT_LEVEL, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
_ = data _ = data
defer c.GetSpace().EnterMap(c) defer c.GetSpace().EnterMap(c)
@@ -92,7 +93,8 @@ func (h Controller) FreshLeaveFightLevel(data *fight.FRESH_LEAVE_FIGHT_LEVEL, c
return result, 0 return result, 0
} }
func (h Controller) PetTawor(data *fight.StartTwarInboundInfo, c *player.Player) (result *fight.S2C_ChoiceLevelRequestInfo, err errorcode.ErrorCode) { // PetTawor 处理控制器请求。
func (h Controller) PetTawor(data *StartTwarInboundInfo, c *player.Player) (result *fight.S2C_ChoiceLevelRequestInfo, err errorcode.ErrorCode) {
if err = c.CanFight(); err != 0 { if err = c.CanFight(); err != 0 {
return nil, err return nil, err
} }
@@ -110,7 +112,7 @@ func (h Controller) PetTawor(data *fight.StartTwarInboundInfo, c *player.Player)
result = &fight.S2C_ChoiceLevelRequestInfo{CurFightLevel: currentLevel} result = &fight.S2C_ChoiceLevelRequestInfo{CurFightLevel: currentLevel}
appendTowerNextBossPreview(&result.BossID, bossList) appendTowerNextBossPreview(&result.BossID, bossList)
monsterInfo, ok := buildTowerMonsterInfo(currentBoss) monsterInfo, bossScript, ok := buildTowerMonsterInfo(currentBoss)
if !ok { if !ok {
return nil, errorcode.ErrorCodes.ErrPokemonNotExists return nil, errorcode.ErrorCodes.ErrPokemonNotExists
} }
@@ -119,6 +121,7 @@ func (h Controller) PetTawor(data *fight.StartTwarInboundInfo, c *player.Player)
c.Fightinfo.Status = fightinfo.BattleMode.FIGHT_WITH_NPC c.Fightinfo.Status = fightinfo.BattleMode.FIGHT_WITH_NPC
ai := player.NewAI_player(monsterInfo) ai := player.NewAI_player(monsterInfo)
ai.BossScript = bossScript
_, err = fight.NewFight(c, ai, c.GetPetInfo(100), ai.GetPetInfo(0), func(foi model.FightOverInfo) { _, err = fight.NewFight(c, ai, c.GetPetInfo(100), ai.GetPetInfo(0), func(foi model.FightOverInfo) {
if foi.Reason != 0 || foi.WinnerId != c.Info.UserID { if foi.Reason != 0 || foi.WinnerId != c.Info.UserID {
return 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]) bosses := service.NewBossService().Get(towerBoss.BossIds[0])
if len(bosses) == 0 { if len(bosses) == 0 {
return nil, false return nil, "", false
} }
monsterInfo := &model.PlayerInfo{Nick: towerBoss.Name} monsterInfo := &model.PlayerInfo{Nick: towerBoss.Name}
@@ -234,7 +237,7 @@ func buildTowerMonsterInfo(towerBoss configmodel.BaseTowerConfig) (*model.Player
monsterInfo.PetList = append(monsterInfo.PetList, *monster) 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) { func handleTowerFightWin(c *player.Player, cmd uint32, taskID int, currentLevel uint32) {

View File

@@ -1,13 +1,12 @@
package controller package controller
import ( import (
"blazing/common/rpc"
"blazing/common/socket/errorcode" "blazing/common/socket/errorcode"
"blazing/cool"
"blazing/logic/service/common" "blazing/logic/service/common"
"blazing/logic/service/fight" "blazing/logic/service/fight"
"blazing/logic/service/fight/info" "blazing/logic/service/fight/pvp"
"blazing/logic/service/player" "blazing/logic/service/player"
"context"
) )
// 表示"宠物王加入"的入站消息数据 // 表示"宠物王加入"的入站消息数据
@@ -17,15 +16,50 @@ type PetTOPLEVELnboundInfo struct {
} }
// JoINtop 处理控制器请求。
func (h Controller) JoINtop(data *PetTOPLEVELnboundInfo, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) { func (h Controller) JoINtop(data *PetTOPLEVELnboundInfo, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
cool.RedisDo(context.TODO(), "sun:join", info.RPCFightinfo{ err = pvp.JoinPeakQueue(c, data.Mode)
PlayerID: c.Info.UserID, if err != 0 {
Mode: data.Mode, return nil, err
Type: 1, }
}) if Maincontroller.RPCClient == nil || Maincontroller.RPCClient.MatchJoinOrUpdate == nil {
pvp.CancelPeakQueue(c)
// // 类型断言为 UniversalClient return nil, errorcode.ErrorCodes.ErrSystemBusyTryLater
// universalClient, _ := client.(goredis.UniversalClient) }
// repo.NewPlayerRepository(universalClient).AddPlayerToPool(context.TODO(), data.Head.UserID, 1) 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 {
return nil, err
}
return nil, -1 return nil, -1
} }

View File

@@ -1,24 +1,25 @@
package controller package controller
import ( import (
"blazing/common/data"
"blazing/common/socket/errorcode" "blazing/common/socket/errorcode"
"blazing/modules/player/model" "blazing/modules/player/model"
"sync/atomic" "sync/atomic"
"blazing/logic/service/common"
"blazing/logic/service/fight" "blazing/logic/service/fight"
"blazing/logic/service/fight/info" "blazing/logic/service/fight/info"
"blazing/logic/service/player" "blazing/logic/service/player"
"blazing/logic/service/space" "blazing/logic/service/space"
) )
// ARENA_SET_OWENR 定义请求或响应数据结构。
type ARENA_SET_OWENR struct {
Head common.TomeeHeader `cmd:"2417" struc:"skip"`
}
// ArenaSetOwner 处理玩家占据擂台的请求 // ArenaSetOwner 处理玩家占据擂台的请求
// public static const ARENA_SET_OWENR:uint = 2417;
// 如果星际擂台上无人,站到星际擂台的包
// 前端到后端无数据内容 空包
// 后端到前端无数据内容 空包
// ArenaSetOwner 都需要通过2419包广播更新擂台状态 // ArenaSetOwner 都需要通过2419包广播更新擂台状态
func (h Controller) ArenaSetOwner(data *fight.ARENA_SET_OWENR, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) { func (h Controller) ArenaSetOwner(data *ARENA_SET_OWENR, c *player.Player) (result *struct{}, err errorcode.ErrorCode) {
r := c.CanFight() r := c.CanFight()
if r != 0 { if r != 0 {
return nil, r return nil, r
@@ -35,12 +36,15 @@ func (h Controller) ArenaSetOwner(data *fight.ARENA_SET_OWENR, c *player.Player)
return nil, errorcode.ErrorCodes.ErrChampionExists return nil, errorcode.ErrorCodes.ErrChampionExists
} }
// ARENA_FIGHT_OWENR 定义请求或响应数据结构。
type ARENA_FIGHT_OWENR struct {
Head common.TomeeHeader `cmd:"2418" struc:"skip"`
}
// ArenaFightOwner 挑战擂台的包 // ArenaFightOwner 挑战擂台的包
// 前端到后端无数据内容 空包
// 后端到前端无数据内容 空包
// 还是后端主动发送2503的包给双方前端后 等待前端加载完毕 主动发送2404包通知后端开始战斗 // 还是后端主动发送2503的包给双方前端后 等待前端加载完毕 主动发送2404包通知后端开始战斗
// ArenaFightOwner 并不会通知对方是否接受挑战。只要有人挑战就直接进入对战 // ArenaFightOwner 并不会通知对方是否接受挑战。只要有人挑战就直接进入对战
func (h Controller) ArenaFightOwner(data1 *fight.ARENA_FIGHT_OWENR, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) { func (h Controller) ArenaFightOwner(data1 *ARENA_FIGHT_OWENR, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
r := c.CanFight() r := c.CanFight()
if r != 0 { if r != 0 {
@@ -86,12 +90,9 @@ func (h Controller) ArenaFightOwner(data1 *fight.ARENA_FIGHT_OWENR, c *player.Pl
if addev != 0 { if addev != 0 {
c.Info.EVPool += addev c.Info.EVPool += addev
c.SendPackCmd(8004, &info.S2C_GET_BOSS_MONSTER{ //发送EV rewards := &info.S2C_GET_BOSS_MONSTER{}
ItemList: []data.ItemInfo{{ rewards.AddItem(9, uint32(addev))
ItemId: 9, c.SendPackCmd(8004, rewards) //发送EV
ItemCnt: int64(addev),
}},
})
} }
} else { } else {
@@ -102,12 +103,9 @@ func (h Controller) ArenaFightOwner(data1 *fight.ARENA_FIGHT_OWENR, c *player.Pl
if addev != 0 { if addev != 0 {
c.GetSpace().Owner.ARENA_Player.GetInfo().EVPool += addev c.GetSpace().Owner.ARENA_Player.GetInfo().EVPool += addev
c.GetSpace().Owner.ARENA_Player.SendPackCmd(8004, &info.S2C_GET_BOSS_MONSTER{ //发送EV rewards := &info.S2C_GET_BOSS_MONSTER{}
ItemList: []data.ItemInfo{{ rewards.AddItem(9, uint32(addev))
ItemId: 9, c.GetSpace().Owner.ARENA_Player.SendPackCmd(8004, rewards) //发送EV
ItemCnt: int64(addev),
}},
})
} }
} }
@@ -141,17 +139,15 @@ func (h Controller) ArenaFightOwner(data1 *fight.ARENA_FIGHT_OWENR, c *player.Pl
// ArenaGetInfo 获取星际擂台信息的包 进入空间站地图前端会发送请求包 或者 有人站到星际擂台上后 广播回包 // ArenaGetInfo 获取星际擂台信息的包 进入空间站地图前端会发送请求包 或者 有人站到星际擂台上后 广播回包
// 前端到后端无数据内容 // 前端到后端无数据内容
// ArenaGetInfo 后端到前端 // ArenaGetInfo 后端到前端
func (h Controller) ArenaGetInfo(data *fight.ARENA_GET_INFO, c *player.Player) (result *space.ARENA, err errorcode.ErrorCode) { func (h Controller) ArenaGetInfo(data *ARENA_GET_INFO, c *player.Player) (result *space.ARENA, err errorcode.ErrorCode) {
result = &c.GetSpace().Owner result = &c.GetSpace().Owner
return return
} }
// ArenaUpfight 放弃擂台挑战的包 // ArenaUpfight 放弃擂台挑战的包
// 前端到后端无数据内容
// 后端到前端无数据内容
// ArenaUpfight 都需要通过2419包广播更新擂台状态 // ArenaUpfight 都需要通过2419包广播更新擂台状态
func (h Controller) ArenaUpfight(data *fight.ARENA_UPFIGHT, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) { func (h Controller) ArenaUpfight(data *ARENA_UPFIGHT, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
//原子操作,修改擂台状态 //原子操作,修改擂台状态
if atomic.LoadUint32(&c.GetSpace().Owner.UserID) != c.GetInfo().UserID { //说明已经有人了 if atomic.LoadUint32(&c.GetSpace().Owner.UserID) != c.GetInfo().UserID { //说明已经有人了
return nil, errorcode.ErrorCodes.ErrChampionCannotCancel return nil, errorcode.ErrorCodes.ErrChampionCannotCancel
@@ -173,7 +169,7 @@ func (h Controller) ArenaUpfight(data *fight.ARENA_UPFIGHT, c *player.Player) (r
// 后端到前端无数据内容 // 后端到前端无数据内容
// public static const ARENA_OWENR_OUT:uint = 2423; // public static const ARENA_OWENR_OUT:uint = 2423;
// ArenaOwnerAcce 此包不清楚具体怎么触发 但已知此包为后端主动发送。不清楚什么情况下回用到 // ArenaOwnerAcce 此包不清楚具体怎么触发 但已知此包为后端主动发送。不清楚什么情况下回用到
func (h Controller) ArenaOwnerAcce(data *fight.ARENA_OWENR_ACCE, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) { func (h Controller) ArenaOwnerAcce(data *ARENA_OWENR_ACCE, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
s := c.GetSpace() s := c.GetSpace()

View File

@@ -0,0 +1,198 @@
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"`
SelectedCatchTimesLen uint32 `struc:"sizeof=SelectedCatchTimes"`
SelectedCatchTimes []uint32 `json:"selectedCatchTimes"`
BanCatchTimesLen uint32 `struc:"sizeof=BanCatchTimes"`
BanCatchTimes []uint32 `json:"banCatchTimes"`
}
// HandleFightInviteInboundInfo 定义请求或响应数据结构。
type HandleFightInviteInboundInfo struct {
Head common.TomeeHeader `cmd:"2403" struc:"skip"`
UserID uint32 `json:"userId" codec:"userId,uint"`
Flag uint32 `json:"flag" codec:"flag,uint"`
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"`
ActorIndex uint8 `json:"actorIndex"`
TargetIndex uint8 `json:"targetIndex"`
TargetRelation uint8 `json:"targetRelation"`
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"`
ItemId uint32 `description:"使用的物品ID" codec:"itemId"`
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"`
MessageLen uint32 `struc:"sizeof=Message"`
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

@@ -0,0 +1,61 @@
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

@@ -0,0 +1,102 @@
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"`
ProductID uint32 `json:"product_id"`
Count int64 `struc:"uint32"`
}
// ItemListInboundInfo 定义请求或响应数据结构。
type ItemListInboundInfo struct {
Head common.TomeeHeader `cmd:"2605|4475" struc:"skip"`
Param1 uint32
Param2 uint32
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
Nature uint32
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

@@ -0,0 +1,118 @@
package controller
import (
"blazing/logic/service/common"
"blazing/modules/player/model"
)
// EnterMapInboundInfo 定义请求或响应数据结构。
type EnterMapInboundInfo struct {
Head common.TomeeHeader `cmd:"2001" struc:"skip"`
MapType uint32
MapId uint32
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
Point model.Pos `fieldDesc:"直接给坐标xy"`
PathLen uint32 `struc:"sizeof=Path"`
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"`
FitmentsLen uint32 `json:"fitmentsLen" struc:"sizeof=Fitments"`
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

@@ -0,0 +1,114 @@
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"`
PetListLen uint32 `struc:"int32,sizeof=PetList"`
PetList []uint32
BackupPetListLen uint32 `struc:"int32,sizeof=BackupPetList"`
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"`
Reserved uint32 `json:"reserved"`
Reserved1 uint32 `json:"reserved1"`
HasSkill uint32 `json:"hasSkill"`
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"`
Auxcatchtime uint32 `json:"auxcatchtime" msgpack:"auxcatchtime"`
Item1 [4]uint32 `json:"item1" msgpack:"item1"`
GoldItem1 [2]uint32 `json:"gold_item1" msgpack:"gold_item1"`
}

View File

@@ -0,0 +1,111 @@
package controller
import (
"blazing/cool"
"blazing/logic/service/common"
"blazing/modules/player/model"
"context"
"encoding/hex"
"fmt"
"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))
if err != nil {
return false, 0
}
if r.String() != t1 {
return false, 0
}
crc32Table := crc32.MakeTable(crc32.IEEE)
crcValue := crc32.Checksum([]byte(l.Sid), crc32Table)
cool.CacheManager.Remove(context.Background(), fmt.Sprintf("session:%d", l.Head.UserID))
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"`
ShootType uint32 `description:"射击类型 未知 给0" codec:"auto" uint:"true"`
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"`
MessageLen uint32 `struc:"sizeof=Message"`
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

@@ -11,7 +11,7 @@ import (
// 防止封包通过领取来获取道具 // 防止封包通过领取来获取道具
// BuyItem 购买单个道具 // BuyItem 购买单个道具
func (h Controller) BuyItem(data *item.BuyInboundInfo, player *player.Player) (result *item.BuyOutboundInfo, err errorcode.ErrorCode) { func (h Controller) BuyItem(data *BuyInboundInfo, player *player.Player) (result *item.BuyOutboundInfo, err errorcode.ErrorCode) {
result = &item.BuyOutboundInfo{Coins: player.Info.Coins} result = &item.BuyOutboundInfo{Coins: player.Info.Coins}
bought, err := buySeerdouBackpackItem(player, data.ItemId, data.Count) bought, err := buySeerdouBackpackItem(player, data.ItemId, data.Count)
@@ -31,7 +31,7 @@ func (h Controller) BuyItem(data *item.BuyInboundInfo, player *player.Player) (r
} }
// BuyMultipleItems 批量购买道具 // BuyMultipleItems 批量购买道具
func (h Controller) BuyMultipleItems(data *item.BuyMultiInboundInfo, player *player.Player) (result *item.BuyMultiOutboundInfo, err errorcode.ErrorCode) { func (h Controller) BuyMultipleItems(data *BuyMultiInboundInfo, player *player.Player) (result *item.BuyMultiOutboundInfo, err errorcode.ErrorCode) {
for _, itemID := range data.ItemIds { for _, itemID := range data.ItemIds {
bought, buyErr := buySeerdouBackpackItem(player, int64(itemID), 1) bought, buyErr := buySeerdouBackpackItem(player, int64(itemID), 1)
if buyErr == errorcode.ErrorCodes.ErrSunDouInsufficient10016 { if buyErr == errorcode.ErrorCodes.ErrSunDouInsufficient10016 {
@@ -48,7 +48,7 @@ func (h Controller) BuyMultipleItems(data *item.BuyMultiInboundInfo, player *pla
} }
// BuyGoldItem 使用金豆购买商品 // BuyGoldItem 使用金豆购买商品
func (h Controller) BuyGoldItem(data *item.C2S_GOLD_BUY_PRODUCT, player *player.Player) (result *item.S2C_GoldBuyProductInfo, err errorcode.ErrorCode) { func (h Controller) BuyGoldItem(data *C2S_GOLD_BUY_PRODUCT, player *player.Player) (result *item.S2C_GoldBuyProductInfo, err errorcode.ErrorCode) {
pro := service.NewShopService().Get(data.ProductID) pro := service.NewShopService().Get(data.ProductID)
if pro == nil { if pro == nil {
return nil, errorcode.ErrorCodes.ErrTooManyProducts return nil, errorcode.ErrorCodes.ErrTooManyProducts

View File

@@ -4,7 +4,6 @@ import (
"blazing/common/data/xmlres" "blazing/common/data/xmlres"
"blazing/common/socket/errorcode" "blazing/common/socket/errorcode"
"blazing/logic/service/fight" "blazing/logic/service/fight"
"blazing/logic/service/item"
"blazing/logic/service/player" "blazing/logic/service/player"
"github.com/gogf/gf/v2/util/gconv" "github.com/gogf/gf/v2/util/gconv"
@@ -14,15 +13,18 @@ import (
// data: 包含道具ID和数量的输入信息 // data: 包含道具ID和数量的输入信息
// c: 当前玩家对象 // c: 当前玩家对象
// 返回: 空结果和错误码 // 返回: 空结果和错误码
func (h Controller) ItemSale(data *item.C2S_ITEM_SALE, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) { func (h Controller) ItemSale(data *C2S_ITEM_SALE, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
if c.Service.Item.CheakItem(data.ItemId) < int64(data.Amount) { if c.Service.Item.CheakItem(data.ItemId) < int64(data.Amount) {
return nil, errorcode.ErrorCodes.ErrSystemError 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)] itemConfig := xmlres.ItemsMAP[int(data.ItemId)]
if itemConfig.SellPrice != 0 { if itemConfig.SellPrice != 0 {
c.Info.Coins += int64(int64(data.Amount) * int64(itemConfig.SellPrice)) c.Info.Coins += int64(int64(data.Amount) * int64(itemConfig.SellPrice))
} }
c.Service.Item.UPDATE(data.ItemId, -gconv.Int(data.Amount))
return result, 0 return result, 0
} }

View File

@@ -15,13 +15,15 @@ import (
const ( const (
// ItemDefaultLeftTime 道具默认剩余时间(毫秒) // ItemDefaultLeftTime 道具默认剩余时间(毫秒)
ItemDefaultLeftTime = 360000 ItemDefaultLeftTime = 360000
// UniversalNatureItemID 全能性格转化剂Ω
UniversalNatureItemID uint32 = 300136
) )
// GetUserItemList 获取用户道具列表 // GetUserItemList 获取用户道具列表
// data: 包含分页参数的输入信息 // data: 包含分页参数的输入信息
// c: 当前玩家对象 // c: 当前玩家对象
// 返回: 道具列表和错误码 // 返回: 道具列表和错误码
func (h Controller) GetUserItemList(data *item.ItemListInboundInfo, c *player.Player) (result *item.ItemListOutboundInfo, err errorcode.ErrorCode) { func (h Controller) GetUserItemList(data *ItemListInboundInfo, c *player.Player) (result *item.ItemListOutboundInfo, err errorcode.ErrorCode) {
result = &item.ItemListOutboundInfo{ result = &item.ItemListOutboundInfo{
ItemList: c.Service.Item.GetUserItemList(data.Param1, data.Param2, ItemDefaultLeftTime), ItemList: c.Service.Item.GetUserItemList(data.Param1, data.Param2, ItemDefaultLeftTime),
} }
@@ -32,23 +34,39 @@ func (h Controller) GetUserItemList(data *item.ItemListInboundInfo, c *player.Pl
// data: 包含道具ID和宠物捕获时间的输入信息 // data: 包含道具ID和宠物捕获时间的输入信息
// c: 当前玩家对象 // c: 当前玩家对象
// 返回: 使用后的宠物信息和错误码 // 返回: 使用后的宠物信息和错误码
func (h Controller) UsePetItemOutOfFight(data *item.C2S_USE_PET_ITEM_OUT_OF_FIGHT, c *player.Player) (result *item.S2C_USE_PET_ITEM_OUT_OF_FIGHT, err errorcode.ErrorCode) { 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 { if !found {
return nil, errorcode.ErrorCodes.Err10401 return nil, errorcode.ErrorCodes.Err10401
} }
currentPet := slot.PetInfoPtr()
if currentPet == nil {
return nil, errorcode.ErrorCodes.Err10401
}
itemID := uint32(data.ItemID) itemID := uint32(data.ItemID)
if c.Service.Item.CheakItem(itemID) == 0 { if c.Service.Item.CheakItem(itemID) == 0 {
return nil, errorcode.ErrorCodes.ErrInsufficientItems return nil, errorcode.ErrorCodes.ErrInsufficientItems
} }
oldHP := currentPet.Hp
itemCfg, ok := xmlres.ItemsMAP[int(itemID)] itemCfg, ok := xmlres.ItemsMAP[int(itemID)]
if !ok { 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 var errcode errorcode.ErrorCode
switch { switch {
case itemID == 300036: case itemID == 300036:
@@ -75,7 +93,10 @@ func (h Controller) UsePetItemOutOfFight(data *item.C2S_USE_PET_ITEM_OUT_OF_FIGH
return nil, errcode 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{} result = &item.S2C_USE_PET_ITEM_OUT_OF_FIGHT{}
copier.Copy(&result, currentPet) copier.Copy(&result, currentPet)
return result, 0 return result, 0
@@ -118,7 +139,9 @@ func (h Controller) handlexuancaiItem(currentPet *model.PetInfo, c *player.Playe
return errorcode.ErrorCodes.ErrItemUnusable 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 return 0
} }
@@ -166,26 +189,32 @@ func refreshPetPaneKeepHP(currentPet *model.PetInfo, hp uint32) {
// handleRegularPetItem 处理普通宠物道具 // handleRegularPetItem 处理普通宠物道具
func (h Controller) handleRegularPetItem(itemID uint32, currentPet *model.PetInfo) errorcode.ErrorCode { func (h Controller) handleRegularPetItem(itemID uint32, currentPet *model.PetInfo) errorcode.ErrorCode {
handler := item.PetItemRegistry.GetHandler(itemID) return item.PetItemRegistry.Handle(itemID, currentPet)
if handler == nil {
return errorcode.ErrorCodes.ErrItemUnusable
}
if !handler(itemID, currentPet) {
return errorcode.ErrorCodes.ErrItemUnusable
}
return 0
} }
// ResetNature 重置宠物性格 // ResetNature 重置宠物性格
// data: 包含道具ID和宠物捕获时间的输入信息 // data: 包含道具ID和宠物捕获时间的输入信息
// c: 当前玩家对象 // c: 当前玩家对象
// 返回: 无数据和错误码 // 返回: 无数据和错误码
func (h Controller) ResetNature(data *item.C2S_PET_RESET_NATURE, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) { 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 { if !found {
return nil, errorcode.ErrorCodes.Err10401 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 { if c.Service.Item.CheakItem(data.ItemId) <= 0 {
return nil, errorcode.ErrorCodes.ErrInsufficientItems return nil, errorcode.ErrorCodes.ErrInsufficientItems
} }
@@ -193,7 +222,10 @@ func (h Controller) ResetNature(data *item.C2S_PET_RESET_NATURE, c *player.Playe
currentHP := currentPet.Hp currentHP := currentPet.Hp
currentPet.Nature = data.Nature currentPet.Nature = data.Nature
refreshPetPaneKeepHP(currentPet, currentHP) 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 return result, 0
} }
@@ -206,7 +238,7 @@ func (h Controller) ResetNature(data *item.C2S_PET_RESET_NATURE, c *player.Playe
// c: 当前玩家对象 // c: 当前玩家对象
// 返回: 无数据(响应包单独组装)和错误码 // 返回: 无数据(响应包单独组装)和错误码
// 说明根据物品ID区分双倍/三倍经验加速器,使用后扣减道具并更新玩家剩余次数 // 说明根据物品ID区分双倍/三倍经验加速器,使用后扣减道具并更新玩家剩余次数
func (h Controller) UseSpeedupItem(data *item.C2S_USE_SPEEDUP_ITEM, c *player.Player) (result *item.S2C_USE_SPEEDUP_ITEM, err errorcode.ErrorCode) { func (h Controller) UseSpeedupItem(data *C2S_USE_SPEEDUP_ITEM, c *player.Player) (result *item.S2C_USE_SPEEDUP_ITEM, err errorcode.ErrorCode) {
// 1. 校验道具是否存在且数量充足 // 1. 校验道具是否存在且数量充足
itemCount := c.Service.Item.CheakItem(data.ItemID) itemCount := c.Service.Item.CheakItem(data.ItemID)
if itemCount <= 0 { if itemCount <= 0 {
@@ -221,29 +253,38 @@ func (h Controller) UseSpeedupItem(data *item.C2S_USE_SPEEDUP_ITEM, c *player.Pl
if c.Info.TwoTimes != 0 { if c.Info.TwoTimes != 0 {
return nil, errorcode.ErrorCodes.ErrItemInUse return nil, errorcode.ErrorCodes.ErrItemInUse
} }
c.Info.TwoTimes += 50 // 玩家对象新增 TwoTimesExp 字段存储双倍剩余次数
case 300067: case 300067:
if c.Info.TwoTimes != 0 { if c.Info.TwoTimes != 0 {
return nil, errorcode.ErrorCodes.ErrItemInUse return nil, errorcode.ErrorCodes.ErrItemInUse
} }
c.Info.TwoTimes += 25 // 玩家对象新增 TwoTimesExp 字段存储双倍剩余次数
case 300051: // 假设1002是三倍经验加速器道具ID case 300051: // 假设1002是三倍经验加速器道具ID
if c.Info.ThreeTimes != 0 { if c.Info.ThreeTimes != 0 {
return nil, errorcode.ErrorCodes.ErrItemInUse return nil, errorcode.ErrorCodes.ErrItemInUse
} }
c.Info.ThreeTimes += 50 // 玩家对象新增 ThreeTimesExp 字段存储三倍剩余次数
case 300115: case 300115:
if c.Info.ThreeTimes != 0 { if c.Info.ThreeTimes != 0 {
return nil, errorcode.ErrorCodes.ErrItemInUse return nil, errorcode.ErrorCodes.ErrItemInUse
} }
c.Info.ThreeTimes += 30 // 玩家对象新增 ThreeTimesExp 字段存储三倍剩余次数
default: default:
return nil, errorcode.ErrorCodes.ErrSystemError // 未知道具ID return nil, errorcode.ErrorCodes.ErrSystemError // 未知道具ID
} }
// 3. 扣减道具(数量-1 // 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.ThreeTimes = uint32(c.Info.ThreeTimes) // 返回三倍经验剩余次数
result.TwoTimes = uint32(c.Info.TwoTimes) // 返回双倍经验剩余次数 result.TwoTimes = uint32(c.Info.TwoTimes) // 返回双倍经验剩余次数
@@ -263,7 +304,7 @@ func (h Controller) UseSpeedupItem(data *item.C2S_USE_SPEEDUP_ITEM, c *player.Pl
// c: 当前玩家对象 // c: 当前玩家对象
// 返回: 无数据(响应包单独组装)和错误码 // 返回: 无数据(响应包单独组装)和错误码
// 说明:使用后扣减道具并更新玩家能量吸收器剩余次数 // 说明:使用后扣减道具并更新玩家能量吸收器剩余次数
func (h Controller) UseEnergyXishou(data *item.C2S_USE_ENERGY_XISHOU, c *player.Player) (result *item.S2C_USE_ENERGY_XISHOU, err errorcode.ErrorCode) { func (h Controller) UseEnergyXishou(data *C2S_USE_ENERGY_XISHOU, c *player.Player) (result *item.S2C_USE_ENERGY_XISHOU, err errorcode.ErrorCode) {
// 1. 校验道具是否存在且数量充足 // 1. 校验道具是否存在且数量充足
itemCount := c.Service.Item.CheakItem(data.ItemID) itemCount := c.Service.Item.CheakItem(data.ItemID)
if itemCount <= 0 { if itemCount <= 0 {
@@ -274,10 +315,11 @@ func (h Controller) UseEnergyXishou(data *item.C2S_USE_ENERGY_XISHOU, c *player.
} }
// 2. 核心业务逻辑:更新能量吸收器剩余次数 // 2. 核心业务逻辑:更新能量吸收器剩余次数
// 可根据道具ID配置不同的次数加成此处默认+1 // 可根据道具ID配置不同的次数加成此处默认+1
if err := c.Service.Item.UPDATE(data.ItemID, -1); err != nil {
return nil, errorcode.ErrorCodes.ErrInsufficientItems
}
c.Info.EnergyTime += 40 // 玩家对象新增 EnergyTimes 字段存储能量吸收剩余次数 c.Info.EnergyTime += 40 // 玩家对象新增 EnergyTimes 字段存储能量吸收剩余次数
// 3. 扣减道具(数量-1
c.Service.Item.UPDATE(data.ItemID, -1)
result = &item.S2C_USE_ENERGY_XISHOU{ result = &item.S2C_USE_ENERGY_XISHOU{
EnergyTimes: uint32(c.Info.EnergyTime), EnergyTimes: uint32(c.Info.EnergyTime),
} }
@@ -298,7 +340,7 @@ func (h Controller) UseEnergyXishou(data *item.C2S_USE_ENERGY_XISHOU, c *player.
// c: 当前玩家对象 // c: 当前玩家对象
// 返回: 无数据(响应包单独组装)和错误码 // 返回: 无数据(响应包单独组装)和错误码
// 说明使用后扣减道具开启自动战斗flag设为3并更新剩余次数 // 说明使用后扣减道具开启自动战斗flag设为3并更新剩余次数
func (h Controller) UseAutoFightItem(data *item.C2S_USE_AUTO_FIGHT_ITEM, c *player.Player) (result *item.S2C_USE_AUTO_FIGHT_ITEM, err errorcode.ErrorCode) { func (h Controller) UseAutoFightItem(data *C2S_USE_AUTO_FIGHT_ITEM, c *player.Player) (result *item.S2C_USE_AUTO_FIGHT_ITEM, err errorcode.ErrorCode) {
// 1. 校验道具是否存在且数量充足 // 1. 校验道具是否存在且数量充足
itemCount := c.Service.Item.CheakItem(data.ItemID) itemCount := c.Service.Item.CheakItem(data.ItemID)
@@ -308,6 +350,9 @@ func (h Controller) UseAutoFightItem(data *item.C2S_USE_AUTO_FIGHT_ITEM, c *play
if c.Info.AutoFightTime != 0 { if c.Info.AutoFightTime != 0 {
return nil, errorcode.ErrorCodes.ErrItemInUse 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{} result = &item.S2C_USE_AUTO_FIGHT_ITEM{}
// 2. 核心业务逻辑:开启自动战斗 + 更新剩余次数 // 2. 核心业务逻辑:开启自动战斗 + 更新剩余次数
c.Info.AutoFight = 3 // 按需求设置自动战斗flag为3需测试 c.Info.AutoFight = 3 // 按需求设置自动战斗flag为3需测试
@@ -323,8 +368,6 @@ func (h Controller) UseAutoFightItem(data *item.C2S_USE_AUTO_FIGHT_ITEM, c *play
c.Info.AutoFightTime += 50 c.Info.AutoFightTime += 50
} }
result.AutoFightTimes = c.Info.AutoFightTime result.AutoFightTimes = c.Info.AutoFightTime
// 3. 扣减道具(数量-1
c.Service.Item.UPDATE(data.ItemID, -1)
return result, 0 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 ( import (
"blazing/common/data/share" "blazing/common/data/share"
"blazing/cool"
"blazing/common/socket/errorcode" "blazing/common/socket/errorcode"
"blazing/cool"
"blazing/logic/service/user"
"blazing/logic/service/player" "blazing/logic/service/player"
"blazing/logic/service/space" "blazing/logic/service/space"
"blazing/logic/service/user"
"blazing/modules/player/service" "blazing/modules/player/service"
"context" "context"
"time" "time"
@@ -17,8 +14,34 @@ import (
"github.com/panjf2000/gnet/v2" "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 // Login 处理命令: 1001
func (h Controller) Login(data *user.MAIN_LOGIN_IN, c gnet.Conn) (result *user.LoginMSInfo, err errorcode.ErrorCode) { //这个时候player应该是空的 func (h Controller) Login(data *MAIN_LOGIN_IN, c gnet.Conn) (result *user.LoginMSInfo, err errorcode.ErrorCode) { //这个时候player应该是空的
if data.Head.UserID == 0 { if data.Head.UserID == 0 {
defer c.Close() defer c.Close()
@@ -30,12 +53,18 @@ func (h Controller) Login(data *user.MAIN_LOGIN_IN, c gnet.Conn) (result *user.L
defer c.Close() defer c.Close()
return 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() defer c.Close()
return return
} }

View File

@@ -16,7 +16,7 @@ import (
) )
// EnterMap 处理玩家进入地图。 // EnterMap 处理玩家进入地图。
func (h Controller) EnterMap(data *space.InInfo, c *player.Player) (result *info.SimpleInfo, err errorcode.ErrorCode) { func (h Controller) EnterMap(data *EnterMapInboundInfo, c *player.Player) (result *info.SimpleInfo, err errorcode.ErrorCode) {
if c.Info.MapID != data.MapId { if c.Info.MapID != data.MapId {
atomic.StoreUint32(&c.Canmon, 2) atomic.StoreUint32(&c.Canmon, 2)
c.MapNPC.Reset(6 * time.Second) c.MapNPC.Reset(6 * time.Second)
@@ -39,7 +39,8 @@ func (h Controller) EnterMap(data *space.InInfo, c *player.Player) (result *info
return nil, -1 return nil, -1
} }
func (h Controller) GetMapHot(data *maphot.InInfo, c *player.Player) (result *maphot.OutInfo, err errorcode.ErrorCode) { // GetMapHot 处理控制器请求。
func (h Controller) GetMapHot(data *GetMapHotInboundInfo, c *player.Player) (result *maphot.OutInfo, err errorcode.ErrorCode) {
result = &maphot.OutInfo{ result = &maphot.OutInfo{
HotInfos: space.GetMapHot(), HotInfos: space.GetMapHot(),
} }
@@ -47,7 +48,7 @@ func (h Controller) GetMapHot(data *maphot.InInfo, c *player.Player) (result *ma
} }
// LeaveMap 处理玩家离开地图。 // LeaveMap 处理玩家离开地图。
func (h Controller) LeaveMap(data *space.LeaveMapInboundInfo, c *player.Player) (result *info.LeaveMapOutboundInfo, err errorcode.ErrorCode) { func (h Controller) LeaveMap(data *LeaveMapInboundInfo, c *player.Player) (result *info.LeaveMapOutboundInfo, err errorcode.ErrorCode) {
atomic.StoreUint32(&c.Canmon, 0) atomic.StoreUint32(&c.Canmon, 0)
c.GetSpace().LeaveMap(c) // 从当前空间移除玩家。 c.GetSpace().LeaveMap(c) // 从当前空间移除玩家。
@@ -57,7 +58,7 @@ func (h Controller) LeaveMap(data *space.LeaveMapInboundInfo, c *player.Player)
} }
// GetMapPlayerList 获取当前地图内的玩家列表与地图广播信息。 // GetMapPlayerList 获取当前地图内的玩家列表与地图广播信息。
func (h Controller) GetMapPlayerList(data *space.ListMapPlayerInboundInfo, c *player.Player) (result *info.ListMapPlayerOutboundInfo, err errorcode.ErrorCode) { func (h Controller) GetMapPlayerList(data *ListMapPlayerInboundInfo, c *player.Player) (result *info.ListMapPlayerOutboundInfo, err errorcode.ErrorCode) {
result = &info.ListMapPlayerOutboundInfo{ result = &info.ListMapPlayerOutboundInfo{
Player: c.GetSpace().GetInfo(c), Player: c.GetSpace().GetInfo(c),
} }
@@ -71,7 +72,7 @@ func (h Controller) GetMapPlayerList(data *space.ListMapPlayerInboundInfo, c *pl
} }
// AttackBoss 调试扣减当前地图广播BOSS血量。 // AttackBoss 调试扣减当前地图广播BOSS血量。
func (h Controller) AttackBoss(data *space.AttackBossInboundInfo, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) { func (h Controller) AttackBoss(data *AttackBossInboundInfo, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
for i := 0; i < len(c.GetSpace().MapBossSInfo.INFO); i++ { for i := 0; i < len(c.GetSpace().MapBossSInfo.INFO); i++ {
if atomic.LoadInt32(&c.GetSpace().MapBossSInfo.INFO[i].Hp) > 0 { if atomic.LoadInt32(&c.GetSpace().MapBossSInfo.INFO[i].Hp) > 0 {
atomic.AddInt32(&c.GetSpace().MapBossSInfo.INFO[i].Hp, -1) atomic.AddInt32(&c.GetSpace().MapBossSInfo.INFO[i].Hp, -1)

View File

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

View File

@@ -9,73 +9,29 @@ import (
// SavePetBagOrder 保存当前主背包和备用背包顺序 // SavePetBagOrder 保存当前主背包和备用背包顺序
func (h Controller) SavePetBagOrder( func (h Controller) SavePetBagOrder(
data *pet.SavePetBagOrderInboundInfo, data *SavePetBagOrderInboundInfo,
player *player.Player) (result *fight.NullOutboundInfo, player *player.Player) (result *fight.NullOutboundInfo,
err errorcode.ErrorCode) { err errorcode.ErrorCode) {
syncBackupPetList(player) if !player.SavePetBagOrder(data.PetList, data.BackupPetList) {
if len(data.PetList) > 6 || len(data.BackupPetList) > 6 {
return nil, errorcode.ErrorCodes.ErrPokemonIDMismatch return nil, errorcode.ErrorCodes.ErrPokemonIDMismatch
} }
totalPetCount := len(player.Info.PetList) + len(player.Info.BackupPetList)
if len(data.PetList)+len(data.BackupPetList) != totalPetCount {
return nil, errorcode.ErrorCodes.ErrPokemonIDMismatch
}
petMap := buildPetInfoMap(player.Info.PetList, player.Info.BackupPetList)
used := make(map[uint32]struct{}, totalPetCount)
battleList, ok := buildOrderedPetList(data.PetList, petMap, used)
if !ok {
return nil, errorcode.ErrorCodes.ErrPokemonIDMismatch
}
backupList, ok := buildOrderedPetList(data.BackupPetList, petMap, used)
if !ok {
return nil, errorcode.ErrorCodes.ErrPokemonIDMismatch
}
if len(used) != totalPetCount {
return nil, errorcode.ErrorCodes.ErrPokemonIDMismatch
}
player.Info.PetList = battleList
player.Info.BackupPetList = backupList
player.Service.Info.Save(*player.Info) player.Service.Info.Save(*player.Info)
return nil, 0 return nil, 0
} }
// PetRetrieveFromWarehouse 领回仓库精灵 // PetRetrieveFromWarehouse 从放生仓库领回精灵
func (h Controller) PetRetrieveFromWarehouse( func (h Controller) PetRetrieveFromWarehouse(
data *pet.PET_RETRIEVE, player *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) { data *PET_RETRIEVE, player *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
if _, ok := findPetListSlot(player, data.CatchTime); ok { if !player.Service.Pet.UpdateFree(data.CatchTime, 1, 0) {
return nil, 0 return nil, errorcode.ErrorCodes.ErrPokemonIDMismatch
} }
petInfo := player.Service.Pet.PetInfoOneByCatchTime(data.CatchTime)
if petInfo == nil {
return nil, 0
}
syncBackupPetList(player)
changed := false
if len(player.Info.PetList) < 6 {
player.Info.PetList = append(player.Info.PetList, petInfo.Data)
changed = true
} else if len(player.Info.BackupPetList) < 6 {
player.Info.BackupPetList = append(player.Info.BackupPetList, petInfo.Data)
changed = true
}
if changed {
player.Service.Info.Save(*player.Info)
}
return nil, 0 return nil, 0
} }
// TogglePetBagWarehouse 精灵背包仓库切换 // TogglePetBagWarehouse 精灵背包仓库切换
func (h Controller) TogglePetBagWarehouse( func (h Controller) TogglePetBagWarehouse(
data *pet.PetReleaseInboundInfo, data *PetReleaseInboundInfo,
player *player.Player) (result *pet.PetReleaseOutboundInfo, err errorcode.ErrorCode) { player *player.Player) (result *pet.PetReleaseOutboundInfo, err errorcode.ErrorCode) {
result = &pet.PetReleaseOutboundInfo{ result = &pet.PetReleaseOutboundInfo{
Flag: uint32(data.Flag), Flag: uint32(data.Flag),
@@ -85,31 +41,26 @@ func (h Controller) TogglePetBagWarehouse(
return result, errorcode.ErrorCodes.ErrChampionCannotSwitch return result, errorcode.ErrorCodes.ErrChampionCannotSwitch
} }
syncBackupPetList(player)
switch data.Flag { switch data.Flag {
case 0: case 0:
slot, ok := findPetListSlot(player, data.CatchTime) slot, ok := player.FindPetBagSlot(data.CatchTime)
if !ok { if !ok {
return result, errorcode.ErrorCodes.ErrPokemonNotExists return result, errorcode.ErrorCodes.ErrPokemonNotExists
} }
if !slot.isValid() { if !slot.IsValid() {
return result, errorcode.ErrorCodes.ErrPokemonIDMismatch return result, errorcode.ErrorCodes.ErrPokemonIDMismatch
} }
if !player.Service.Pet.Update(slot.info) { if !player.Service.Pet.Update(slot.PetInfo()) {
return result, errorcode.ErrorCodes.ErrSystemError return result, errorcode.ErrorCodes.ErrSystemError
} }
slot.remove() slot.Remove()
if slot.kind == petListKindMain {
player.Service.Info.Save(*player.Info)
}
case 1: case 1:
if len(player.Info.PetList) >= 6 && len(player.Info.BackupPetList) >= 6 { if len(player.Info.PetList) >= 6 && len(player.Info.BackupPetList) >= 6 {
return result, errorcode.ErrorCodes.ErrPokemonIDMismatch return result, errorcode.ErrorCodes.ErrPokemonIDMismatch
} }
if _, ok := findPetListSlot(player, data.CatchTime); ok { if _, ok := player.FindPetBagSlot(data.CatchTime); ok {
return result, 0 return result, 0
} }
@@ -117,77 +68,7 @@ func (h Controller) TogglePetBagWarehouse(
if petInfo == nil { if petInfo == nil {
return result, errorcode.ErrorCodes.ErrPokemonNotExists return result, errorcode.ErrorCodes.ErrPokemonNotExists
} }
if len(player.Info.PetList) < 6 { player.AddPetToAvailableBag(petInfo.Data)
player.Info.PetList = append(player.Info.PetList, petInfo.Data)
} else {
player.Info.BackupPetList = append(player.Info.BackupPetList, petInfo.Data)
}
result.PetInfo = petInfo.Data
}
if len(player.Info.PetList) > 0 {
result.FirstPetTime = player.Info.PetList[0].CatchTime
}
return result, 0
}
// TogglePetBagWarehouseLegacy 旧版精灵背包仓库切换
func (h Controller) TogglePetBagWarehouseLegacy(
data *pet.PetReleaseLegacyInboundInfo,
player *player.Player) (result *pet.PetReleaseOutboundInfo, err errorcode.ErrorCode) {
result = &pet.PetReleaseOutboundInfo{
Flag: uint32(data.Flag),
}
if player.IsArenaSwitchLocked() {
return result, errorcode.ErrorCodes.ErrChampionCannotSwitch
}
switch data.Flag {
case 0:
index, currentPet, ok := player.FindPet(data.CatchTime)
if !ok {
break
}
if index < 0 || index >= len(player.Info.PetList) {
return result, errorcode.ErrorCodes.ErrPokemonIDMismatch
}
if !player.Service.Pet.Update(*currentPet) {
return result, errorcode.ErrorCodes.ErrSystemError
}
player.Info.PetList = append(player.Info.PetList[:index], player.Info.PetList[index+1:]...)
player.Info.BackupPetList = removePetByCatchTime(player.Info.BackupPetList, data.CatchTime)
player.Info.BackupPetList = append(player.Info.BackupPetList, *currentPet)
case 1:
if len(player.Info.PetList) >= 6 {
break
}
if _, _, ok := player.FindPet(data.CatchTime); ok {
player.Info.BackupPetList = removePetByCatchTime(player.Info.BackupPetList, data.CatchTime)
break
}
if index, backupPet, ok := findBackupPet(player, data.CatchTime); ok {
if index < 0 || index >= len(player.Info.BackupPetList) {
return result, errorcode.ErrorCodes.ErrPokemonIDMismatch
}
result.PetInfo = *backupPet
player.Info.PetList = append(player.Info.PetList, *backupPet)
player.Info.BackupPetList = append(player.Info.BackupPetList[:index], player.Info.BackupPetList[index+1:]...)
break
}
petInfo := player.Service.Pet.PetInfoOneByCatchTime(data.CatchTime)
if petInfo == nil {
return result, errorcode.ErrorCodes.ErrPokemonNotExists
}
player.Info.PetList = append(player.Info.PetList, petInfo.Data)
result.PetInfo = petInfo.Data result.PetInfo = petInfo.Data
} }

View File

@@ -7,7 +7,7 @@ import (
) )
// GetPetBargeList 精灵图鉴 // GetPetBargeList 精灵图鉴
func (h Controller) GetPetBargeList(data *pet.PetBargeListInboundInfo, player *player.Player) (result *pet.PetBargeListOutboundInfo, err errorcode.ErrorCode) { func (h Controller) GetPetBargeList(data *PetBargeListInboundInfo, player *player.Player) (result *pet.PetBargeListOutboundInfo, err errorcode.ErrorCode) {
ret := &pet.PetBargeListOutboundInfo{ ret := &pet.PetBargeListOutboundInfo{
PetBargeList: make([]pet.PetBargeListInfo, 0), PetBargeList: make([]pet.PetBargeListInfo, 0),

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,32 +2,9 @@ package controller
import ( import (
"blazing/logic/service/pet" "blazing/logic/service/pet"
"blazing/logic/service/player"
"blazing/modules/player/model" "blazing/modules/player/model"
) )
type petListKind uint8
const (
petListKindMain petListKind = iota
petListKindBackup
)
type petListSlot struct {
list *[]model.PetInfo
index int
info model.PetInfo
kind petListKind
}
func (slot petListSlot) isValid() bool {
return slot.list != nil && slot.index >= 0 && slot.index < len(*slot.list)
}
func (slot petListSlot) remove() {
*slot.list = append((*slot.list)[:slot.index], (*slot.list)[slot.index+1:]...)
}
func buildPetShortInfo(info model.PetInfo) pet.PetShortInfo { func buildPetShortInfo(info model.PetInfo) pet.PetShortInfo {
return pet.PetShortInfo{ return pet.PetShortInfo{
ID: info.ID, ID: info.ID,
@@ -49,163 +26,6 @@ func buildPetListOutboundInfo(petList []model.PetInfo) *pet.GetPetListOutboundIn
return result return result
} }
func removePetByCatchTime(petList []model.PetInfo, catchTime uint32) []model.PetInfo {
for i := range petList {
if petList[i].CatchTime == catchTime {
return append(petList[:i], petList[i+1:]...)
}
}
return petList
}
func buildCatchTimeSet(petLists ...[]model.PetInfo) map[uint32]struct{} {
total := 0
for _, petList := range petLists {
total += len(petList)
}
catchTimes := make(map[uint32]struct{}, total)
for _, petList := range petLists {
for _, petInfo := range petList {
catchTimes[petInfo.CatchTime] = struct{}{}
}
}
return catchTimes
}
func buildPetInfoMap(petLists ...[]model.PetInfo) map[uint32]model.PetInfo {
total := 0
for _, petList := range petLists {
total += len(petList)
}
petMap := make(map[uint32]model.PetInfo, total)
for _, petList := range petLists {
for _, petInfo := range petList {
petMap[petInfo.CatchTime] = petInfo
}
}
return petMap
}
func buildWarehousePetList(player *player.Player) []model.PetInfo {
allPets := player.Service.Pet.PetInfo(0)
if len(allPets) == 0 {
return make([]model.PetInfo, 0)
}
usedCatchTimes := buildCatchTimeSet(player.Info.PetList, player.Info.BackupPetList)
result := make([]model.PetInfo, 0, len(allPets))
for i := range allPets {
catchTime := allPets[i].Data.CatchTime
if _, exists := usedCatchTimes[catchTime]; exists {
continue
}
result = append(result, allPets[i].Data)
}
return result
}
func findBackupPet(player *player.Player, catchTime uint32) (int, *model.PetInfo, bool) {
for i := range player.Info.BackupPetList {
if player.Info.BackupPetList[i].CatchTime == catchTime {
return i, &player.Info.BackupPetList[i], true
}
}
return -1, nil, false
}
func findPetListSlot(player *player.Player, catchTime uint32) (petListSlot, bool) {
if index, petInfo, ok := player.FindPet(catchTime); ok {
return petListSlot{
list: &player.Info.PetList,
index: index,
info: *petInfo,
kind: petListKindMain,
}, true
}
if index, petInfo, ok := findBackupPet(player, catchTime); ok {
return petListSlot{
list: &player.Info.BackupPetList,
index: index,
info: *petInfo,
kind: petListKindBackup,
}, true
}
return petListSlot{}, false
}
func syncBackupPetList(player *player.Player) {
if player.Info.BackupPetList == nil {
player.Info.BackupPetList = make([]model.PetInfo, 0)
return
}
bagPets := player.Service.Pet.PetInfo(0)
if len(bagPets) == 0 {
player.Info.BackupPetList = make([]model.PetInfo, 0)
return
}
bagCatchTimes := make(map[uint32]struct{}, len(bagPets))
for i := range bagPets {
bagCatchTimes[bagPets[i].Data.CatchTime] = struct{}{}
}
mainPetCatchTimes := buildCatchTimeSet(player.Info.PetList)
nextBackupList := make([]model.PetInfo, 0, len(player.Info.BackupPetList))
for _, petInfo := range player.Info.BackupPetList {
if _, inBag := bagCatchTimes[petInfo.CatchTime]; !inBag {
continue
}
if _, inMain := mainPetCatchTimes[petInfo.CatchTime]; inMain {
continue
}
nextBackupList = append(nextBackupList, petInfo)
}
player.Info.BackupPetList = nextBackupList
}
func buildUserBagPetInfo(player *player.Player) *pet.GetUserBagPetInfoOutboundInfo {
syncBackupPetList(player)
result := &pet.GetUserBagPetInfoOutboundInfo{
PetList: make([]model.PetInfo, len(player.Info.PetList)),
BackupPetList: make([]model.PetInfo, len(player.Info.BackupPetList)),
}
copy(result.PetList, player.Info.PetList)
copy(result.BackupPetList, player.Info.BackupPetList)
return result
}
func buildOrderedPetList(
catchTimes []uint32,
petMap map[uint32]model.PetInfo,
used map[uint32]struct{},
) ([]model.PetInfo, bool) {
result := make([]model.PetInfo, 0, len(catchTimes))
for _, catchTime := range catchTimes {
if catchTime == 0 {
return nil, false
}
if _, exists := used[catchTime]; exists {
return nil, false
}
petInfo, exists := petMap[catchTime]
if !exists {
return nil, false
}
used[catchTime] = struct{}{}
result = append(result, petInfo)
}
return result, true
}
func buildPetShowOutboundInfo(userID, flag uint32, info *model.PetInfo) *pet.PetShowOutboundInfo { func buildPetShowOutboundInfo(userID, flag uint32, info *model.PetInfo) *pet.PetShowOutboundInfo {
return &pet.PetShowOutboundInfo{ return &pet.PetShowOutboundInfo{
UserID: userID, UserID: userID,

View File

@@ -6,20 +6,50 @@ import (
"blazing/logic/service/fight" "blazing/logic/service/fight"
"blazing/logic/service/pet" "blazing/logic/service/pet"
"blazing/logic/service/player" "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 将精灵从仓库包中放生 // PetReleaseToWarehouse 将精灵从仓库包中放生
func (h Controller) PetReleaseToWarehouse( func (h Controller) PetReleaseToWarehouse(
data *pet.PET_ROWEI, player *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) { data *PET_ROWEI, player *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
_, _, inBag := player.FindPet(data.CatchTime) _, _, inBag := player.FindPet(data.CatchTime)
_, _, inBackup := findBackupPet(player, data.CatchTime) _, _, inBackup := player.FindBackupPet(data.CatchTime)
freeForbidden := xmlres.PetMAP[int(data.ID)].FreeForbidden freeForbidden := xmlres.PetMAP[int(data.ID)].FreeForbidden
if inBag || inBackup || freeForbidden == 1 { if inBag || inBackup || freeForbidden == 1 {
return nil, errorcode.ErrorCodes.ErrCannotReleaseNonWarehouse return nil, errorcode.ErrorCodes.ErrCannotReleaseNonWarehouse
} }
if !player.Service.Pet.UpdateFree(data.CatchTime, 0, 1) {
if !player.Service.Pet.UpdateFree(data.CatchTime, 1) { return nil, errorcode.ErrorCodes.ErrCannotReleaseNonWarehouse
return nil, errorcode.ErrorCodes.ErrSystemError
} }
return nil, 0 return nil, 0
@@ -27,15 +57,17 @@ func (h Controller) PetReleaseToWarehouse(
// PetOneCure 单体治疗 // PetOneCure 单体治疗
func (h Controller) PetOneCure( func (h Controller) PetOneCure(
data *pet.PetOneCureInboundInfo, player *player.Player) (result *pet.PetOneCureOutboundInfo, err errorcode.ErrorCode) { data *PetOneCureInboundInfo, player *player.Player) (result *pet.PetOneCureOutboundInfo, err errorcode.ErrorCode) {
if player.IsArenaHealLocked() { if player.IsArenaHealLocked() {
return result, errorcode.ErrorCodes.ErrChampionCannotHeal return result, errorcode.ErrorCodes.ErrChampionCannotHeal
} }
_, currentPet, ok := player.FindPet(data.CatchTime) if slot, ok := player.FindPetBagSlot(data.CatchTime); ok {
if ok { currentPet := slot.PetInfoPtr()
if currentPet != nil {
defer currentPet.Cure() defer currentPet.Cure()
} }
}
return &pet.PetOneCureOutboundInfo{ return &pet.PetOneCureOutboundInfo{
CatchTime: data.CatchTime, CatchTime: data.CatchTime,
@@ -44,7 +76,7 @@ func (h Controller) PetOneCure(
// PetFirst 精灵首发 // PetFirst 精灵首发
func (h Controller) PetFirst( func (h Controller) PetFirst(
data *pet.PetDefaultInboundInfo, player *player.Player) (result *pet.PetDefaultOutboundInfo, err errorcode.ErrorCode) { data *PetDefaultInboundInfo, player *player.Player) (result *pet.PetDefaultOutboundInfo, err errorcode.ErrorCode) {
if player.IsArenaSwitchLocked() { if player.IsArenaSwitchLocked() {
return result, errorcode.ErrorCodes.ErrChampionCannotSwitch return result, errorcode.ErrorCodes.ErrChampionCannotSwitch
} }
@@ -61,13 +93,19 @@ func (h Controller) PetFirst(
// SetPetExp 设置宠物经验 // SetPetExp 设置宠物经验
func (h Controller) SetPetExp( func (h Controller) SetPetExp(
data *pet.PetSetExpInboundInfo, data *PetSetExpInboundInfo,
player *player.Player) (result *pet.PetSetExpOutboundInfo, err errorcode.ErrorCode) { player *player.Player) (result *pet.PetSetExpOutboundInfo, err errorcode.ErrorCode) {
_, currentPet, found := player.FindPet(data.CatchTime) slot, found := player.FindPetBagSlot(data.CatchTime)
if !found || currentPet.Level >= 100 { currentPet := slot.PetInfoPtr()
if !found || currentPet == nil || currentPet.Level >= 100 {
return &pet.PetSetExpOutboundInfo{Exp: player.Info.ExpPool}, errorcode.ErrorCodes.ErrSystemError 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 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/pet"
"blazing/logic/service/player" "blazing/logic/service/player"
"blazing/modules/player/model" "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赛尔豆 // SetPetSkill 设置宠物技能消耗50赛尔豆
func (h Controller) SetPetSkill(data *pet.ChangeSkillInfo, c *player.Player) (result *pet.ChangeSkillOutInfo, err errorcode.ErrorCode) { func (h Controller) SetPetSkill(data *ChangeSkillInfo, c *player.Player) (result *pet.ChangeSkillOutInfo, err errorcode.ErrorCode) {
const setSkillCost = 50 const setSkillCost = 50
if !c.GetCoins(setSkillCost) { slot, ok := c.FindPetBagSlot(data.CatchTime)
return nil, errorcode.ErrorCodes.ErrSunDouInsufficient10016 currentPet := slot.PetInfoPtr()
} if !ok || currentPet == nil {
c.Info.Coins -= setSkillCost
_, currentPet, ok := c.FindPet(data.CatchTime)
if !ok {
return nil, errorcode.ErrorCodes.ErrSystemBusy return nil, errorcode.ErrorCodes.ErrSystemBusy
} }
canleaernskill := currentPet.GetLevelRangeCanLearningSkills(1, currentPet.Level)
_, ok = lo.Find(canleaernskill, func(item uint32) bool { canLearnSkillSet := make(map[uint32]struct{})
return item == data.ReplaceSkill for _, skillID := range collectPetLearnableSkillList(currentPet) {
}) canLearnSkillSet[skillID] = struct{}{}
if !ok {
return result, errorcode.ErrorCodes.ErrSystemBusy
} }
_, _, 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 return item.ID == data.ReplaceSkill
}) })
if ok { if ok {
return nil, errorcode.ErrorCodes.ErrSystemBusy return nil, errorcode.ErrorCodes.ErrSystemBusy
} }
// 查找要学习的技能并替换 if data.HasSkill == 0 && len(currentPet.SkillList) >= 4 {
_, targetSkill, ok := utils.FindWithIndex(currentPet.SkillList, func(item model.SkillInfo) bool { 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 return item.ID == data.HasSkill
}) })
if ok {
targetSkill.ID = data.ReplaceSkill 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{ return &pet.ChangeSkillOutInfo{
@@ -57,28 +146,142 @@ func (h Controller) SetPetSkill(data *pet.ChangeSkillInfo, c *player.Player) (re
} }
// SortPetSkills 排序宠物技能消耗50赛尔豆 // SortPetSkills 排序宠物技能消耗50赛尔豆
func (h Controller) SortPetSkills(data *pet.C2S_Skill_Sort, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) { func (h Controller) SortPetSkills(data *C2S_Skill_Sort, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
const skillSortCost = 50 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) { if !c.GetCoins(skillSortCost) {
return nil, errorcode.ErrorCodes.ErrSunDouInsufficient10016 return nil, errorcode.ErrorCodes.ErrSunDouInsufficient10016
} }
c.Info.Coins -= skillSortCost 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 return nil, 0
} }

View File

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

View File

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

View File

@@ -7,7 +7,7 @@ import (
) )
// BuyFitment 购买基地家具 // BuyFitment 购买基地家具
func (h Controller) BuyFitment(data *room.C2S_BUY_FITMENT, c *player.Player) (result *room.S2C_BUY_FITMENT, err errorcode.ErrorCode) { func (h Controller) BuyFitment(data *C2S_BUY_FITMENT, c *player.Player) (result *room.S2C_BUY_FITMENT, err errorcode.ErrorCode) {
result = &room.S2C_BUY_FITMENT{Coins: c.Info.Coins} result = &room.S2C_BUY_FITMENT{Coins: c.Info.Coins}
bought, err := buySeerdouBackpackItem(c, int64(data.ID), int64(data.Count)) bought, err := buySeerdouBackpackItem(c, int64(data.ID), int64(data.Count))

View File

@@ -15,7 +15,7 @@ import (
// data: 包含目标用户ID的输入信息 // data: 包含目标用户ID的输入信息
// c: 当前玩家对象 // c: 当前玩家对象
// 返回: 基地家具信息和错误码 // 返回: 基地家具信息和错误码
func (h Controller) GetFitmentUsing(data *room.FitmentUseringInboundInfo, c *player.Player) (result *room.FitmentUseringOutboundInfo, err errorcode.ErrorCode) { func (h Controller) GetFitmentUsing(data *FitmentUseringInboundInfo, c *player.Player) (result *room.FitmentUseringOutboundInfo, err errorcode.ErrorCode) {
result = &room.FitmentUseringOutboundInfo{UserId: c.Info.UserID, RoomId: data.TargetUserID} result = &room.FitmentUseringOutboundInfo{UserId: c.Info.UserID, RoomId: data.TargetUserID}
result.Fitments = make([]model.FitmentShowInfo, 0) result.Fitments = make([]model.FitmentShowInfo, 0)
result.Fitments = append(result.Fitments, model.FitmentShowInfo{Id: 500001, Status: 1, X: 1, Y: 1, Dir: 1}) result.Fitments = append(result.Fitments, model.FitmentShowInfo{Id: 500001, Status: 1, X: 1, Y: 1, Dir: 1})
@@ -29,7 +29,7 @@ func (h Controller) GetFitmentUsing(data *room.FitmentUseringInboundInfo, c *pla
// data: 包含目标用户ID的输入信息 // data: 包含目标用户ID的输入信息
// c: 当前玩家对象 // c: 当前玩家对象
// 返回: 精灵展示列表和错误码 // 返回: 精灵展示列表和错误码
func (h Controller) GetRoomPetShowInfo(data *room.PetRoomListInboundInfo, c *player.Player) (result *room.PetRoomListOutboundInfo, err errorcode.ErrorCode) { func (h Controller) GetRoomPetShowInfo(data *PetRoomListInboundInfo, c *player.Player) (result *room.PetRoomListOutboundInfo, err errorcode.ErrorCode) {
result = &room.PetRoomListOutboundInfo{} result = &room.PetRoomListOutboundInfo{}
result.Pets = make([]pet.PetShortInfo, 0) result.Pets = make([]pet.PetShortInfo, 0)
roomInfo := c.Service.Room.Get(data.TargetUserID) roomInfo := c.Service.Room.Get(data.TargetUserID)
@@ -51,7 +51,7 @@ func (h Controller) GetRoomPetShowInfo(data *room.PetRoomListInboundInfo, c *pla
// data: 空输入结构 // data: 空输入结构
// c: 当前玩家对象 // c: 当前玩家对象
// 返回: 玩家所有家具列表和错误码 // 返回: 玩家所有家具列表和错误码
func (h Controller) GetAllFurniture(data *room.FitmentAllInboundEmpty, c *player.Player) (result *room.FitmentAllOutboundInfo, err errorcode.ErrorCode) { func (h Controller) GetAllFurniture(data *FitmentAllInboundEmpty, c *player.Player) (result *room.FitmentAllOutboundInfo, err errorcode.ErrorCode) {
result = &room.FitmentAllOutboundInfo{} result = &room.FitmentAllOutboundInfo{}
result.Fitments = make([]room.FitmentItemInfo, 0) result.Fitments = make([]room.FitmentItemInfo, 0)
@@ -75,7 +75,7 @@ func (h Controller) GetAllFurniture(data *room.FitmentAllInboundEmpty, c *player
// data: 包含用户ID和精灵捕获时间的输入信息 // data: 包含用户ID和精灵捕获时间的输入信息
// c: 当前玩家对象 // c: 当前玩家对象
// 返回: 精灵详细信息和错误码 // 返回: 精灵详细信息和错误码
func (h Controller) GetRoomPetInfo(data *room.C2S_RoomPetInfo, c *player.Player) (result *pet.RoomPetInfo, err errorcode.ErrorCode) { func (h Controller) GetRoomPetInfo(data *C2S_RoomPetInfo, c *player.Player) (result *pet.RoomPetInfo, err errorcode.ErrorCode) {
petInfo := c.Service.Pet.PetInfoOneOther(data.UserID, data.CatchTime) petInfo := c.Service.Pet.PetInfoOneOther(data.UserID, data.CatchTime)
result = &pet.RoomPetInfo{} result = &pet.RoomPetInfo{}
copier.CopyWithOption(result, &petInfo.Data, copier.Option{DeepCopy: true}) copier.CopyWithOption(result, &petInfo.Data, copier.Option{DeepCopy: true})

View File

@@ -13,7 +13,7 @@ import (
// data: 包含家具列表的输入信息 // data: 包含家具列表的输入信息
// c: 当前玩家对象 // c: 当前玩家对象
// 返回: 空结果和错误码 // 返回: 空结果和错误码
func (h Controller) SetFitment(data *room.SET_FITMENT, c *player.Player) (result *room.NullInfo, err errorcode.ErrorCode) { func (h Controller) SetFitment(data *SET_FITMENT, c *player.Player) (result *room.NullInfo, err errorcode.ErrorCode) {
c.Service.Room.Set(data.Fitments) c.Service.Room.Set(data.Fitments)
return return
@@ -23,7 +23,7 @@ func (h Controller) SetFitment(data *room.SET_FITMENT, c *player.Player) (result
// data: 包含精灵展示列表的输入信息 // data: 包含精灵展示列表的输入信息
// c: 当前玩家对象 // c: 当前玩家对象
// 返回: 精灵展示列表和错误码 // 返回: 精灵展示列表和错误码
func (h Controller) SetPet(data *room.C2S_PET_ROOM_SHOW, c *player.Player) (result *room.S2C_PET_ROOM_SHOW, err errorcode.ErrorCode) { func (h Controller) SetPet(data *C2S_PET_ROOM_SHOW, c *player.Player) (result *room.S2C_PET_ROOM_SHOW, err errorcode.ErrorCode) {
var showPetCatchTimes []uint32 var showPetCatchTimes []uint32
for _, petShowInfo := range data.PetShowList { for _, petShowInfo := range data.PetShowList {
if petShowInfo.CatchTime != 0 { if petShowInfo.CatchTime != 0 {

View File

@@ -8,6 +8,7 @@ import (
"blazing/logic/service/player" "blazing/logic/service/player"
) )
// SystemTimeInfo 处理控制器请求。
func (h Controller) SystemTimeInfo(data *InInfo, c *player.Player) (result *OutInfo, err errorcode.ErrorCode) { func (h Controller) SystemTimeInfo(data *InInfo, c *player.Player) (result *OutInfo, err errorcode.ErrorCode) {
return &OutInfo{ return &OutInfo{
@@ -23,5 +24,5 @@ type InInfo struct { //这里直接使用组合来实现将传入的原始头部
// OutInfo 表示系统时间的出站消息 // OutInfo 表示系统时间的出站消息
type OutInfo struct { type OutInfo struct {
SystemTime uint32 `json:"systemTime"` // @UInt long类型 SystemTime uint32 `json:"systemTime"`
} }

View File

@@ -16,7 +16,7 @@ import (
// data: 包含射击信息的输入数据 // data: 包含射击信息的输入数据
// player: 当前玩家对象 // player: 当前玩家对象
// 返回: 射击结果和错误码 // 返回: 射击结果和错误码
func (h Controller) PlayerAim(data *user.AimatInboundInfo, player *player.Player) (result *user.AimatOutboundInfo, err errorcode.ErrorCode) { func (h Controller) PlayerAim(data *AimatInboundInfo, player *player.Player) (result *user.AimatOutboundInfo, err errorcode.ErrorCode) {
result = &user.AimatOutboundInfo{ result = &user.AimatOutboundInfo{
ItemId: data.ItemId, ItemId: data.ItemId,
@@ -33,7 +33,7 @@ func (h Controller) PlayerAim(data *user.AimatInboundInfo, player *player.Player
// data: 包含聊天消息的输入数据 // data: 包含聊天消息的输入数据
// player: 当前玩家对象 // player: 当前玩家对象
// 返回: 聊天结果和错误码 // 返回: 聊天结果和错误码
func (h Controller) PlayerChat(data *user.ChatInboundInfo, player *player.Player) (result *user.ChatOutboundInfo, err errorcode.ErrorCode) { func (h Controller) PlayerChat(data *ChatInboundInfo, player *player.Player) (result *user.ChatOutboundInfo, err errorcode.ErrorCode) {
result = &user.ChatOutboundInfo{ result = &user.ChatOutboundInfo{
@@ -50,7 +50,7 @@ func (h Controller) PlayerChat(data *user.ChatInboundInfo, player *player.Player
// data: 包含颜色信息的输入数据 // data: 包含颜色信息的输入数据
// player: 当前玩家对象 // player: 当前玩家对象
// 返回: 颜色更改结果和错误码 // 返回: 颜色更改结果和错误码
func (h Controller) ChangePlayerColor(data *user.ChangeColorInboundInfo, player *player.Player) (result *user.ChangeColorOutboundInfo, err errorcode.ErrorCode) { func (h Controller) ChangePlayerColor(data *ChangeColorInboundInfo, player *player.Player) (result *user.ChangeColorOutboundInfo, err errorcode.ErrorCode) {
const changeColorCost = 50 const changeColorCost = 50
if !player.GetCoins(changeColorCost) { if !player.GetCoins(changeColorCost) {
@@ -76,7 +76,7 @@ func (h Controller) ChangePlayerColor(data *user.ChangeColorInboundInfo, player
// data: 包含涂鸦信息的输入数据 // data: 包含涂鸦信息的输入数据
// player: 当前玩家对象 // player: 当前玩家对象
// 返回: 涂鸦更改结果和错误码 // 返回: 涂鸦更改结果和错误码
func (h Controller) ChangePlayerDoodle(data *user.ChangeDoodleInboundInfo, player *player.Player) (result *user.ChangeDoodleOutboundInfo, err errorcode.ErrorCode) { func (h Controller) ChangePlayerDoodle(data *ChangeDoodleInboundInfo, player *player.Player) (result *user.ChangeDoodleOutboundInfo, err errorcode.ErrorCode) {
const changeDoodleCost = 50 const changeDoodleCost = 50
if !player.GetCoins(changeDoodleCost) { if !player.GetCoins(changeDoodleCost) {
@@ -106,7 +106,7 @@ func (h Controller) ChangePlayerDoodle(data *user.ChangeDoodleInboundInfo, playe
// data: 包含NONO颜色信息的输入数据 // data: 包含NONO颜色信息的输入数据
// player: 当前玩家对象 // player: 当前玩家对象
// 返回: NONO颜色更改结果和错误码 // 返回: NONO颜色更改结果和错误码
func (h Controller) ChangeNONOColor(data *user.ChangeNONOColorInboundInfo, player *player.Player) (result *user.ChangeNONOColorOutboundInfo, err errorcode.ErrorCode) { func (h Controller) ChangeNONOColor(data *ChangeNONOColorInboundInfo, player *player.Player) (result *user.ChangeNONOColorOutboundInfo, err errorcode.ErrorCode) {
//player.Info.Coins -= 200 //player.Info.Coins -= 200
player.Info.NONO.NonoColor = data.Color player.Info.NONO.NonoColor = data.Color
@@ -122,7 +122,7 @@ func (h Controller) ChangeNONOColor(data *user.ChangeNONOColorInboundInfo, playe
// data: 包含跳舞类型信息的输入数据 // data: 包含跳舞类型信息的输入数据
// player: 当前玩家对象 // player: 当前玩家对象
// 返回: 跳舞动作结果和错误码 // 返回: 跳舞动作结果和错误码
func (h Controller) DanceAction(data *user.C2SDanceAction, player *player.Player) (result *user.S2CDanceAction, err errorcode.ErrorCode) { func (h Controller) DanceAction(data *C2SDanceAction, player *player.Player) (result *user.S2CDanceAction, err errorcode.ErrorCode) {
result = &user.S2CDanceAction{ result = &user.S2CDanceAction{
Type: data.Type, Type: data.Type,
@@ -136,7 +136,7 @@ func (h Controller) DanceAction(data *user.C2SDanceAction, player *player.Player
// data: 包含变形信息的输入数据 // data: 包含变形信息的输入数据
// player: 当前玩家对象 // player: 当前玩家对象
// 返回: 变形结果和错误码 // 返回: 变形结果和错误码
func (h Controller) PeopleTransform(data *user.C2SPEOPLE_TRANSFROM, player *player.Player) (result *user.S2CPEOPLE_TRANSFROM, err errorcode.ErrorCode) { func (h Controller) PeopleTransform(data *C2SPEOPLE_TRANSFROM, player *player.Player) (result *user.S2CPEOPLE_TRANSFROM, err errorcode.ErrorCode) {
result = &user.S2CPEOPLE_TRANSFROM{ result = &user.S2CPEOPLE_TRANSFROM{
SuitID: data.SuitID, SuitID: data.SuitID,
@@ -150,7 +150,7 @@ func (h Controller) PeopleTransform(data *user.C2SPEOPLE_TRANSFROM, player *play
// data: 包含服装信息的输入数据 // data: 包含服装信息的输入数据
// player: 当前玩家对象 // player: 当前玩家对象
// 返回: 服装更改结果和错误码 // 返回: 服装更改结果和错误码
func (h Controller) ChangePlayerCloth(data *item.ChangePlayerClothInboundInfo, player *player.Player) (result *item.ChangePlayerClothOutboundInfo, err errorcode.ErrorCode) { func (h Controller) ChangePlayerCloth(data *ChangePlayerClothInboundInfo, player *player.Player) (result *item.ChangePlayerClothOutboundInfo, err errorcode.ErrorCode) {
result = &item.ChangePlayerClothOutboundInfo{ result = &item.ChangePlayerClothOutboundInfo{
UserID: player.Info.UserID, UserID: player.Info.UserID,
@@ -171,7 +171,8 @@ func (h Controller) ChangePlayerCloth(data *item.ChangePlayerClothInboundInfo, p
return return
} }
func (h Controller) ChangePlayerName(data *user.ChangePlayerNameInboundInfo, c *player.Player) (result *user.ChangePlayerNameOutboundInfo, err errorcode.ErrorCode) { // 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"), '*') newNickname := cool.Filter.Replace(strings.Trim(data.Nickname, "\x00"), '*')
c.Info.Nick = newNickname c.Info.Nick = newNickname
@@ -183,7 +184,9 @@ func (h Controller) ChangePlayerName(data *user.ChangePlayerNameInboundInfo, c *
return result, 0 return result, 0
} }
func (h Controller) ChangeTile(data *user.ChangeTitleInboundInfo, c *player.Player) (result *user.ChangeTitleOutboundInfo, err errorcode.ErrorCode) {
// ChangeTile 处理控制器请求。
func (h Controller) ChangeTile(data *ChangeTitleInboundInfo, c *player.Player) (result *user.ChangeTitleOutboundInfo, err errorcode.ErrorCode) {
result = &user.ChangeTitleOutboundInfo{ result = &user.ChangeTitleOutboundInfo{
UserID: c.Info.UserID, UserID: c.Info.UserID,

View File

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

View File

@@ -11,7 +11,7 @@ import (
// data: 包含用户ID列表的输入信息 // data: 包含用户ID列表的输入信息
// c: 当前玩家对象 // c: 当前玩家对象
// 返回: 好友在线信息和错误码 // 返回: 好友在线信息和错误码
func (h Controller) GetOnlineFriends(data *friend.SeeOnlineInboundInfo, c *player.Player) (result *friend.SeeOnlineOutboundInfo, err errorcode.ErrorCode) { func (h Controller) GetOnlineFriends(data *SeeOnlineInboundInfo, c *player.Player) (result *friend.SeeOnlineOutboundInfo, err errorcode.ErrorCode) {
result = &friend.SeeOnlineOutboundInfo{} result = &friend.SeeOnlineOutboundInfo{}
result.Friends = make([]friend.OnlineInfo, 0) result.Friends = make([]friend.OnlineInfo, 0)
return return
@@ -21,7 +21,7 @@ func (h Controller) GetOnlineFriends(data *friend.SeeOnlineInboundInfo, c *playe
// data: 包含要添加好友的用户ID // data: 包含要添加好友的用户ID
// c: 当前玩家对象 // c: 当前玩家对象
// 返回: 无数据内容的响应和错误码 // 返回: 无数据内容的响应和错误码
func (h Controller) FriendAdd(data *friend.FriendAddInboundInfo, c *player.Player) (result fight.NullOutboundInfo, err errorcode.ErrorCode) { func (h Controller) FriendAdd(data *FriendAddInboundInfo, c *player.Player) (result fight.NullOutboundInfo, err errorcode.ErrorCode) {
v, ok := c.GetSpace().User.Load(data.UserID) v, ok := c.GetSpace().User.Load(data.UserID)
@@ -40,7 +40,7 @@ func (h Controller) FriendAdd(data *friend.FriendAddInboundInfo, c *player.Playe
// data: 包含发起好友请求的用户ID和回复标志(1为同意0为拒绝) // data: 包含发起好友请求的用户ID和回复标志(1为同意0为拒绝)
// c: 当前玩家对象 // c: 当前玩家对象
// 返回: 无数据内容的响应和错误码 // 返回: 无数据内容的响应和错误码
func (h Controller) FriendAnswer(data *friend.FriendAnswerInboundInfo, c *player.Player) (result fight.NullOutboundInfo, err errorcode.ErrorCode) { func (h Controller) FriendAnswer(data *FriendAnswerInboundInfo, c *player.Player) (result fight.NullOutboundInfo, err errorcode.ErrorCode) {
v, ok := c.GetSpace().User.Load(data.UserID) v, ok := c.GetSpace().User.Load(data.UserID)
if ok { if ok {
@@ -62,7 +62,7 @@ func (h Controller) FriendAnswer(data *friend.FriendAnswerInboundInfo, c *player
// data: 包含要删除的好友ID // data: 包含要删除的好友ID
// c: 当前玩家对象 // c: 当前玩家对象
// 返回: 无数据内容的响应和错误码 // 返回: 无数据内容的响应和错误码
func (h Controller) FriendRemove(data *friend.FriendRemoveInboundInfo, c *player.Player) (result fight.NullOutboundInfo, err errorcode.ErrorCode) { func (h Controller) FriendRemove(data *FriendRemoveInboundInfo, c *player.Player) (result fight.NullOutboundInfo, err errorcode.ErrorCode) {
c.Service.Friend.Del(data.UserID) c.Service.Friend.Del(data.UserID)
return return
} }

View File

@@ -13,7 +13,7 @@ import (
// data: 包含用户ID的输入信息 // data: 包含用户ID的输入信息
// player: 玩家对象 // player: 玩家对象
// 返回: 模拟用户信息及错误码 // 返回: 模拟用户信息及错误码
func (h Controller) GetUserSimInfo(data *user.SimUserInfoInboundInfo, player *player.Player) (result *user.SimUserInfoOutboundInfo, err errorcode.ErrorCode) { func (h Controller) GetUserSimInfo(data *SimUserInfoInboundInfo, player *player.Player) (result *user.SimUserInfoOutboundInfo, err errorcode.ErrorCode) {
result = &user.SimUserInfoOutboundInfo{} result = &user.SimUserInfoOutboundInfo{}
t, ok := player.GetSpace().UserInfo.Load(data.UserId) t, ok := player.GetSpace().UserInfo.Load(data.UserId)
if ok { if ok {
@@ -33,7 +33,7 @@ func (h Controller) GetUserSimInfo(data *user.SimUserInfoInboundInfo, player *pl
// data: 包含用户ID的输入信息 // data: 包含用户ID的输入信息
// player: 当前玩家对象 // player: 当前玩家对象
// 返回: 包含用户更多信息的输出结果和错误码 // 返回: 包含用户更多信息的输出结果和错误码
func (h Controller) GetUserMoreInfo(data *user.MoreUserInfoInboundInfo, player *player.Player) (result *user.MoreUserInfoOutboundInfo, err errorcode.ErrorCode) { func (h Controller) GetUserMoreInfo(data *MoreUserInfoInboundInfo, player *player.Player) (result *user.MoreUserInfoOutboundInfo, err errorcode.ErrorCode) {
result = &user.MoreUserInfoOutboundInfo{} result = &user.MoreUserInfoOutboundInfo{}
info := player.Service.Info.Person(data.UserId) info := player.Service.Info.Person(data.UserId)
if info == nil { if info == nil {
@@ -49,7 +49,7 @@ func (h Controller) GetUserMoreInfo(data *user.MoreUserInfoInboundInfo, player *
// data: 输入信息(无实际内容) // data: 输入信息(无实际内容)
// player: 当前玩家对象 // player: 当前玩家对象
// 返回: 玩家金币和代币数量及错误码 // 返回: 玩家金币和代币数量及错误码
func (h Controller) GetPlayerGoldCount(data *item.GoldOnlineRemainInboundInfo, player *player.Player) (result *item.GoldOnlineRemainOutboundInfo, err errorcode.ErrorCode) { func (h Controller) GetPlayerGoldCount(data *GoldOnlineRemainInboundInfo, player *player.Player) (result *item.GoldOnlineRemainOutboundInfo, err errorcode.ErrorCode) {
return &item.GoldOnlineRemainOutboundInfo{ return &item.GoldOnlineRemainOutboundInfo{
@@ -62,7 +62,7 @@ func (h Controller) GetPlayerGoldCount(data *item.GoldOnlineRemainInboundInfo, p
// data: 输入信息(无实际内容) // data: 输入信息(无实际内容)
// player: 当前玩家对象 // player: 当前玩家对象
// 返回: 玩家总经验值及错误码 // 返回: 玩家总经验值及错误码
func (h Controller) GetPlayerExp(data *item.ExpTotalRemainInboundInfo, player *player.Player) (result *item.ExpTotalRemainOutboundInfo, err errorcode.ErrorCode) { func (h Controller) GetPlayerExp(data *ExpTotalRemainInboundInfo, player *player.Player) (result *item.ExpTotalRemainOutboundInfo, err errorcode.ErrorCode) {
return &item.ExpTotalRemainOutboundInfo{ return &item.ExpTotalRemainOutboundInfo{

View File

@@ -7,7 +7,8 @@ import (
"blazing/modules/config/service" "blazing/modules/config/service"
) )
func (h Controller) GetTalkCount(data *item.TalkCountInboundInfo, c *player.Player) (result *item.TalkCountOutboundInfo, err errorcode.ErrorCode) { // GetTalkCount 处理控制器请求。
func (h Controller) GetTalkCount(data *TalkCountInboundInfo, c *player.Player) (result *item.TalkCountOutboundInfo, err errorcode.ErrorCode) {
result = &item.TalkCountOutboundInfo{} result = &item.TalkCountOutboundInfo{}
talkCount, ok := c.Service.Talk.Cheak(c.Info.MapID, int(data.ID)) talkCount, ok := c.Service.Talk.Cheak(c.Info.MapID, int(data.ID))
if !ok { if !ok {
@@ -20,7 +21,7 @@ func (h Controller) GetTalkCount(data *item.TalkCountInboundInfo, c *player.Play
//var talkcacche = make(map[string]uint32) //var talkcacche = make(map[string]uint32)
func (h Controller) GetTalkCategory(data *item.TalkCateInboundInfo, c *player.Player) (result *item.DayTalkInfo, err errorcode.ErrorCode) { func (h Controller) GetTalkCategory(data *TalkCateInboundInfo, c *player.Player) (result *item.DayTalkInfo, err errorcode.ErrorCode) {
result = &item.DayTalkInfo{} result = &item.DayTalkInfo{}
result.OutList = make([]item.CateInfo, 0) result.OutList = make([]item.CateInfo, 0)

View File

@@ -11,7 +11,7 @@ import (
) )
// AcceptTask 接受任务 // AcceptTask 接受任务
func (h Controller) AcceptTask(data *task.AcceptTaskInboundInfo, c *player.Player) (result *task.AcceptTaskOutboundInfo, err errorcode.ErrorCode) { func (h Controller) AcceptTask(data *AcceptTaskInboundInfo, c *player.Player) (result *task.AcceptTaskOutboundInfo, err errorcode.ErrorCode) {
//isdaliy := false //isdaliy := false
// if data.Head.CMD != 2201 { //判断是每日任务 // if data.Head.CMD != 2201 { //判断是每日任务
// //isdaliy = true // //isdaliy = true
@@ -28,12 +28,15 @@ func (h Controller) AcceptTask(data *task.AcceptTaskInboundInfo, c *player.Playe
} }
c.Info.SetTask(int(data.TaskId), model.Accepted) c.Info.SetTask(int(data.TaskId), model.Accepted)
c.Service.Task.Exec(uint32(data.TaskId), func(t *model.Task) bool { taskData, taskErr := c.Service.Task.GetTask(uint32(data.TaskId))
t.Data = []uint32{} 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 = &task.AcceptTaskOutboundInfo{}
result.TaskId = data.TaskId result.TaskId = data.TaskId
return result, 0 return result, 0
@@ -43,15 +46,19 @@ func (h Controller) AcceptTask(data *task.AcceptTaskInboundInfo, c *player.Playe
// data: 包含任务ID和任务步骤列表的输入信息 // data: 包含任务ID和任务步骤列表的输入信息
// c: 当前玩家对象 // c: 当前玩家对象
// 返回: 空输出结果和错误码 // 返回: 空输出结果和错误码
func (h Controller) AddTaskBuf(data *task.AddTaskBufInboundInfo, c *player.Player) (result *task.AddTaskBufOutboundInfo, err errorcode.ErrorCode) { func (h Controller) AddTaskBuf(data *AddTaskBufInboundInfo, c *player.Player) (result *task.AddTaskBufOutboundInfo, err errorcode.ErrorCode) {
if c.Info.GetTask(int(data.TaskId)) != model.Accepted { if c.Info.GetTask(int(data.TaskId)) != model.Accepted {
return result, errorcode.ErrorCodes.ErrAwardAlreadyClaimed return result, errorcode.ErrorCodes.ErrAwardAlreadyClaimed
} }
c.Service.Task.Exec(data.TaskId, func(taskEx *model.Task) bool { taskData, taskErr := c.Service.Task.GetTask(data.TaskId)
taskEx.Data = data.TaskList if taskErr != nil {
return true 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 return result, 0
} }
@@ -59,7 +66,7 @@ func (h Controller) AddTaskBuf(data *task.AddTaskBufInboundInfo, c *player.Playe
// data: 包含任务ID的输入信息 // data: 包含任务ID的输入信息
// c: 当前玩家对象 // c: 当前玩家对象
// 返回: 任务完成结果和错误码 // 返回: 任务完成结果和错误码
func (h Controller) CompleteTask(data1 *task.CompleteTaskInboundInfo, c *player.Player) (result *task.CompleteTaskOutboundInfo, err errorcode.ErrorCode) { func (h Controller) CompleteTask(data1 *CompleteTaskInboundInfo, c *player.Player) (result *task.CompleteTaskOutboundInfo, err errorcode.ErrorCode) {
if c.Info.GetTask(int(data1.TaskId)) != model.Accepted { if c.Info.GetTask(int(data1.TaskId)) != model.Accepted {
return result, errorcode.ErrorCodes.ErrAwardAlreadyClaimed return result, errorcode.ErrorCodes.ErrAwardAlreadyClaimed
} }
@@ -70,52 +77,38 @@ func (h Controller) CompleteTask(data1 *task.CompleteTaskInboundInfo, c *player.
// if service.NewTaskService().IsAcceptable(data1.TaskId) == nil { // if service.NewTaskService().IsAcceptable(data1.TaskId) == nil {
// return nil, errorcode.ErrorCodes.ErrSystemError // return nil, errorcode.ErrorCodes.ErrSystemError
// } // }
c.Info.SetTask(int(data1.TaskId), model.Completed)
result = &task.CompleteTaskOutboundInfo{ result = &task.CompleteTaskOutboundInfo{
TaskId: data1.TaskId, TaskId: data1.TaskId,
ItemList: make([]data.ItemInfo, 0), ItemList: make([]data.ItemInfo, 0),
} }
taskInfo := task.GetTaskInfo(int(data1.TaskId), int(data1.OutState)) if _, err = c.ApplyTaskCompletion(data1.TaskId, int(data1.OutState), result); err != 0 {
if taskInfo == nil { return nil, err
return nil, errorcode.ErrorCodes.ErrNeedCompleteTaskForPrize
} }
if taskErr := c.Info.SetTask(int(data1.TaskId), model.Completed); taskErr != nil {
if taskInfo.Pet != nil { return nil, errorcode.ErrorCodes.ErrSystemError
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)
}
} }
return result, 0 //通过PUB/SUB回包 return result, 0 //通过PUB/SUB回包
} }
// GetTaskBuf 获取任务状态 // GetTaskBuf 获取任务状态
func (h Controller) GetTaskBuf(data *task.GetTaskBufInboundInfo, c *player.Player) (result *task.GetTaskBufOutboundInfo, err errorcode.ErrorCode) { func (h Controller) GetTaskBuf(data *GetTaskBufInboundInfo, c *player.Player) (result *task.GetTaskBufOutboundInfo, err errorcode.ErrorCode) {
result = &task.GetTaskBufOutboundInfo{ result = &task.GetTaskBufOutboundInfo{
TaskId: data.TaskId, TaskId: data.TaskId,
} }
c.Service.Task.Exec(data.TaskId, func(te *model.Task) bool {
result.TaskList = te.Data taskData, taskErr := c.Service.Task.GetTask(data.TaskId)
return false if taskErr != nil {
}) return nil, errorcode.ErrorCodes.ErrSystemError
}
result.TaskList = taskData.Data
return result, 0 return result, 0
} }
// DeleteTask 删除任务 // DeleteTask 删除任务
func (h Controller) DeleteTask(data *task.DeleteTaskInboundInfo, c *player.Player) (result *task.DeleteTaskOutboundInfo, err errorcode.ErrorCode) { func (h Controller) DeleteTask(data *DeleteTaskInboundInfo, c *player.Player) (result *task.DeleteTaskOutboundInfo, err errorcode.ErrorCode) {
if c.Info.GetTask(int(data.TaskId)) != model.Accepted { if c.Info.GetTask(int(data.TaskId)) != model.Accepted {
return nil, errorcode.ErrorCodes.ErrSystemError return nil, errorcode.ErrorCodes.ErrSystemError

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