Compare commits

...

61 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
136 changed files with 6551 additions and 363523 deletions

3
.gitignore vendored
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

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

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

View File

@@ -98,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 {
// create a new server instance
rpcServer := jsonrpc.NewServer(jsonrpc.WithReverseClient[cool.ClientHandler](""))
@@ -114,6 +123,10 @@ func StartClient(id, port uint32, callback any) *struct {
Kick func(uint32) error
RegisterLogic func(uint32, uint32) error
MatchJoinOrUpdate func(PVPMatchJoinPayload) error
MatchCancel func(uint32) error
} {
//cool.Config.File.Domain = "127.0.0.1"
var rpcaddr = "ws://" + cool.Config.File.Domain + gconv.String(cool.Config.Address) + "/rpc"
@@ -144,6 +157,10 @@ var RPCClient struct {
RegisterLogic func(uint32, uint32) error
MatchJoinOrUpdate func(PVPMatchJoinPayload) error
MatchCancel func(uint32) error
// UserLogin func(int32, int32) error //用户登录事件
// UserLogout func(int32, int32) error //用户登出事件
}

View File

@@ -121,7 +121,23 @@ func (s *Server) OnTraffic(c gnet.Conn) (action gnet.Action) {
}
}()
ws := c.Context().(*player.ClientData).Wsmsg
client := c.Context().(*player.ClientData)
if s.discorse && !client.IsCrossDomainChecked() {
handled, ready, action := handle(c)
if action != gnet.None {
return action
}
if handled {
client.MarkCrossDomainChecked()
return gnet.None
}
if !ready {
return gnet.None
}
client.MarkCrossDomainChecked()
}
ws := client.Wsmsg
if ws.Tcp {
return s.handleTCP(c)
}

View File

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

View File

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

View File

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

@@ -4,6 +4,7 @@
package controller
import (
"blazing/common/rpc"
"blazing/cool"
"blazing/logic/service/common"
"bytes"
@@ -29,6 +30,10 @@ type Controller struct {
Kick func(uint32) error
RegisterLogic func(uint32, uint32) error
MatchJoinOrUpdate func(rpc.PVPMatchJoinPayload) error
MatchCancel func(uint32) error
}
}

View File

@@ -72,26 +72,32 @@ func (h Controller) DASHIbeiR(req *C2s_MASTER_REWARDSR, c *player.Player) (resul
}
result.ItemList = make([]data.ItemInfo, 0, len(taskInfo.ItemList))
c.Service.Task.Exec(masterCupTaskID, func(te *model.Task) bool {
progress := bitset32.From(te.Data)
if progress.Test(uint(req.ElementType)) {
err = errorcode.ErrorCode(errorcode.ErrorCodes.ErrAwardAlreadyClaimed)
return false
}
taskData, taskErr := c.Service.Task.GetTask(masterCupTaskID)
if taskErr != nil {
return nil, errorcode.ErrorCode(errorcode.ErrorCodes.ErrSystemError)
}
consumeMasterCupItems(c, requiredItems)
progress.Set(uint(req.ElementType))
te.Data = progress.Bytes()
progress := bitset32.From(taskData.Data)
if progress.Test(uint(req.ElementType)) {
return nil, errorcode.ErrorCode(errorcode.ErrorCodes.ErrAwardAlreadyClaimed)
}
if taskInfo.Pet != nil {
c.Service.Pet.PetAdd(taskInfo.Pet, 0)
result.CaptureTime = taskInfo.Pet.CatchTime
result.PetTypeId = taskInfo.Pet.ID
}
if err := consumeMasterCupItems(c, requiredItems); err != nil {
return nil, errorcode.ErrorCode(errorcode.ErrorCodes.ErrInsufficientItems)
}
progress.Set(uint(req.ElementType))
taskData.Data = progress.Bytes()
if taskErr = c.Service.Task.SetTask(taskData); taskErr != nil {
return nil, errorcode.ErrorCode(errorcode.ErrorCodes.ErrSystemError)
}
appendMasterCupRewardItems(c, result, taskInfo.ItemList)
return true
})
if taskInfo.Pet != nil {
c.Service.Pet.PetAdd(taskInfo.Pet, 0)
result.CaptureTime = taskInfo.Pet.CatchTime
result.PetTypeId = taskInfo.Pet.ID
}
appendMasterCupRewardItems(c, result, taskInfo.ItemList)
return
}
@@ -126,10 +132,13 @@ func hasEnoughMasterCupItems(c *player.Player, requiredItems []ItemS) bool {
return true
}
func consumeMasterCupItems(c *player.Player, requiredItems []ItemS) {
func consumeMasterCupItems(c *player.Player, requiredItems []ItemS) error {
for _, item := range requiredItems {
c.Service.Item.UPDATE(item.ItemId, -int(item.ItemCnt))
if err := c.Service.Item.UPDATE(item.ItemId, -int(item.ItemCnt)); err != nil {
return err
}
}
return nil
}
func appendMasterCupRewardItems(c *player.Player, result *S2C_MASTER_REWARDSR, itemList []data.ItemInfo) {

View File

@@ -26,12 +26,11 @@ func (h Controller) EggGamePlay(data1 *C2S_EGG_GAME_PLAY, c *player.Player) (res
if data1.EggNum > 10 || data1.EggNum <= 0 {
return nil, errorcode.ErrorCode(errorcode.ErrorCodes.ErrSystemError)
}
if r < 0 {
if r <= 0 || data1.EggNum > r {
return nil, errorcode.ErrorCode(errorcode.ErrorCodes.ErrGachaTicketsInsufficient)
}
if data1.EggNum > r {
if err := c.Service.Item.UPDATE(400501, int(-data1.EggNum)); err != nil {
return nil, errorcode.ErrorCode(errorcode.ErrorCodes.ErrGachaTicketsInsufficient)
}
result = &S2C_EGG_GAME_PLAY{ListInfo: []data.ItemInfo{}}
if grand.Meet(int(data1.EggNum), 100) {
@@ -52,8 +51,6 @@ func (h Controller) EggGamePlay(data1 *C2S_EGG_GAME_PLAY, c *player.Player) (res
for _, item := range addedItems {
result.ListInfo = append(result.ListInfo, data.ItemInfo{ItemId: item.ItemId, ItemCnt: item.ItemCnt})
}
c.Service.Item.UPDATE(400501, int(-data1.EggNum))
return
}

View File

@@ -57,6 +57,7 @@ func (h Controller) GET_XUANCAI(data *C2s_GET_XUANCAI, c *player.Player) (result
// 检查该位是否未被选中(避免重复)
if (result.Status & mask) == 0 {
result.Status |= mask
itemID := uint32(400686 + randBitIdx + 1)
selectedItems = append(selectedItems, itemID)
itemMask[itemID] = mask

View File

@@ -43,7 +43,8 @@ func (h Controller) GroupUseSkill(data *GroupUseSkillInboundInfo, c *player.Play
targetRelation = fight.SkillTargetAlly
}
h.dispatchFightActionEnvelope(c, fight.NewSkillActionEnvelope(data.SkillId, int(data.ActorIndex), int(data.TargetPos), targetRelation, 0))
return nil, 0
c.SendPackCmd(7558, nil)
return nil, -1
}
func (h Controller) GroupUseItem(data *GroupUseItemInboundInfo, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
@@ -73,6 +74,20 @@ func (h Controller) GroupEscape(data *GroupEscapeInboundInfo, c *player.Player)
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 {

View File

@@ -43,7 +43,7 @@ func (Controller) PlayerFightBoss(req *ChallengeBossInboundInfo, p *player.Playe
}
p.Fightinfo.Status = fightinfo.BattleMode.FIGHT_WITH_NPC
p.Fightinfo.Mode = fightinfo.BattleMode.MULTI_MODE
p.Fightinfo.Mode = resolveMapNodeFightMode(mapNode)
ai := player.NewAI_player(monsterInfo)
ai.CanCapture = resolveBossCaptureRate(bossConfigs[0].IsCapture, leadMonsterID)
@@ -72,55 +72,25 @@ func startMapBossFight(
ai *player.AI_player,
fn func(model.FightOverInfo),
) (*fight.FightC, errorcode.ErrorCode) {
ourPets := p.GetPetInfo(100)
ourPets := p.GetPetInfo(p.CurrentMapPetLevelLimit())
oppPets := ai.GetPetInfo(0)
if mapNode != nil && mapNode.IsGroupBoss != 0 {
ourSlots := buildGroupBossPetSlots(ourPets, groupBossSlotLimit)
oppSlots := buildGroupBossPetSlots(oppPets, groupBossSlotLimit)
if len(ourSlots) > 0 && len(oppSlots) > 0 {
return fight.NewLegacyGroupFightSingleControllerN(p, ai, ourSlots, oppSlots, fn)
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 buildGroupBossPetSlots(pets []model.PetInfo, slotLimit int) [][]model.PetInfo {
if len(pets) == 0 {
return nil
func resolveMapNodeFightMode(mapNode *configmodel.MapNode) uint32 {
if mapNode != nil && mapNode.PkFlag != 0 {
return fightinfo.BattleMode.SINGLE_MODE
}
slots := make([][]model.PetInfo, 0, slotLimit)
for _, pet := range pets {
if pet.Hp == 0 {
continue
}
if slotLimit <= 0 {
slotLimit = 3
}
if len(slots) < slotLimit {
slots = append(slots, []model.PetInfo{pet})
continue
}
break
}
if len(slots) == 0 {
return nil
}
var idx int = 0
for _, pet := range pets[len(slots):] {
if pet.Hp == 0 {
continue
}
for step := 0; step < len(slots); step++ {
slotIdx := (idx + step) % len(slots)
if len(slots[slotIdx]) < 6 {
slots[slotIdx] = append(slots[slotIdx], pet)
idx = (slotIdx + 1) % len(slots)
break
}
}
}
return slots
return fightinfo.BattleMode.MULTI_MODE
}
// OnPlayerFightNpcMonster 战斗野怪
@@ -128,8 +98,8 @@ func (Controller) OnPlayerFightNpcMonster(req *FightNpcMonsterInboundInfo, p *pl
if err = p.CanFight(); err != 0 {
return nil, err
}
if req.Number > 9 {
return nil, errorcode.ErrorCodes.ErrSystemError
if int(req.Number) >= len(p.Data) {
return nil, errorcode.ErrorCodes.ErrPokemonNotHere
}
refPet := p.Data[req.Number]
@@ -144,7 +114,7 @@ func (Controller) OnPlayerFightNpcMonster(req *FightNpcMonsterInboundInfo, p *pl
p.Fightinfo.Status = fightinfo.BattleMode.FIGHT_WITH_NPC
p.Fightinfo.Mode = fightinfo.BattleMode.MULTI_MODE
_, err = fight.NewFight(p, ai, p.GetPetInfo(100), ai.GetPetInfo(0), func(foi model.FightOverInfo) {
_, err = fight.NewFight(p, ai, p.GetPetInfo(p.CurrentMapPetLevelLimit()), ai.GetPetInfo(0), func(foi model.FightOverInfo) {
handleNpcFightRewards(p, foi, monster)
})
if err != 0 {
@@ -266,7 +236,7 @@ func shouldGrantBossWinBonus(fightC *fight.FightC, playerID uint32, bossConfig c
func buildNpcMonsterInfo(refPet player.OgrePetInfo, mapID uint32) (*model.PetInfo, *model.PlayerInfo, errorcode.ErrorCode) {
if refPet.ID == 0 {
return nil, nil, errorcode.ErrorCodes.ErrPokemonNotExists
return nil, nil, errorcode.ErrorCodes.ErrPokemonNotHere
}
monster := model.GenPetInfo(

View File

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

View File

@@ -49,6 +49,14 @@ type GroupEscapeInboundInfo struct {
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"`

View File

@@ -99,6 +99,12 @@ type GetPetLearnableSkillsInboundInfo struct {
CatchTime uint32 `json:"catchTime"`
}
type CommitPetSkillsInboundInfo struct {
Head common.TomeeHeader `cmd:"52313" struc:"skip"`
CatchTime uint32 `json:"catchTime"`
Skill [4]uint32 `json:"skill"`
}
type C2S_PetFusion struct {
Head common.TomeeHeader `cmd:"2351" struc:"skip"`
Mcatchtime uint32 `json:"mcatchtime" msgpack:"mcatchtime"`

View File

@@ -16,7 +16,7 @@ type MAIN_LOGIN_IN struct {
Sid []byte `struc:"[16]byte"`
}
// CheakSession 处理控制器请求
// 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))

View File

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

View File

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

View File

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

View File

@@ -64,6 +64,7 @@ func (h *Controller) SwitchFlying(data *SwitchFlyingInboundInfo, c *player.Playe
// PlayerPetCure 处理控制器请求。
func (h *Controller) PlayerPetCure(data *PetCureInboundInfo, c *player.Player) (result *nono.PetCureOutboundEmpty, err errorcode.ErrorCode) { //这个时候player应该是空的
_ = data
result = &nono.PetCureOutboundEmpty{}
if c.IsArenaHealLocked() {
return result, errorcode.ErrorCodes.ErrChampionCannotHeal
}
@@ -73,6 +74,9 @@ func (h *Controller) PlayerPetCure(data *PetCureInboundInfo, c *player.Player) (
for i := range c.Info.PetList {
c.Info.PetList[i].Cure()
}
for i := range c.Info.BackupPetList {
c.Info.BackupPetList[i].Cure()
}
c.Info.Coins -= nonoPetCureCost
return
}

View File

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

View File

@@ -37,7 +37,9 @@ func (h Controller) PetELV(data *C2S_PET_EVOLVTION, c *player.Player) (result *f
return nil, errorcode.ErrorCodes.ErrInsufficientItemsMulti
}
if branch.EvolvItem != 0 {
c.Service.Item.UPDATE(uint32(branch.EvolvItem), -branch.EvolvItemCount)
if err := c.Service.Item.UPDATE(uint32(branch.EvolvItem), -branch.EvolvItemCount); err != nil {
return nil, errorcode.ErrorCodes.ErrInsufficientItemsMulti
}
}
currentPet.ID = uint32(branch.MonTo)

View File

@@ -17,11 +17,16 @@ const (
// c: 当前玩家对象
// 返回: 分配结果和错误码
func (h Controller) PetEVDiy(data *PetEV, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
_, currentPet, found := c.FindPet(data.CacthTime)
slot, found := c.FindPetBagSlot(data.CacthTime)
if !found {
return nil, errorcode.ErrorCodes.Err10401
}
currentPet := slot.PetInfoPtr()
if currentPet == nil {
return nil, errorcode.ErrorCodes.Err10401
}
var targetTotal uint32
var currentTotal uint32
for i, evValue := range data.EVs {

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

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

View File

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

View File

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

View File

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

View File

@@ -15,6 +15,18 @@ type GetPetLearnableSkillsOutboundInfo struct {
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)
@@ -55,8 +67,9 @@ func (h Controller) GetPetLearnableSkills(
data *GetPetLearnableSkillsInboundInfo,
c *player.Player,
) (result *GetPetLearnableSkillsOutboundInfo, err errorcode.ErrorCode) {
_, currentPet, ok := c.FindPet(data.CatchTime)
if !ok {
slot, ok := c.FindPetBagSlot(data.CatchTime)
currentPet := slot.PetInfoPtr()
if !ok || currentPet == nil {
return nil, errorcode.ErrorCodes.ErrPokemonNotExists
}
@@ -69,8 +82,9 @@ func (h Controller) GetPetLearnableSkills(
func (h Controller) SetPetSkill(data *ChangeSkillInfo, c *player.Player) (result *pet.ChangeSkillOutInfo, err errorcode.ErrorCode) {
const setSkillCost = 50
_, currentPet, ok := c.FindPet(data.CatchTime)
if !ok {
slot, ok := c.FindPetBagSlot(data.CatchTime)
currentPet := slot.PetInfoPtr()
if !ok || currentPet == nil {
return nil, errorcode.ErrorCodes.ErrSystemBusy
}
@@ -135,8 +149,9 @@ func (h Controller) SetPetSkill(data *ChangeSkillInfo, c *player.Player) (result
func (h Controller) SortPetSkills(data *C2S_Skill_Sort, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
const skillSortCost = 50
_, currentPet, ok := c.FindPet(data.CapTm)
if !ok {
slot, ok := c.FindPetBagSlot(data.CapTm)
currentPet := slot.PetInfoPtr()
if !ok || currentPet == nil {
return nil, errorcode.ErrorCodes.ErrPokemonNotExists
}
@@ -184,3 +199,89 @@ func (h Controller) SortPetSkills(data *C2S_Skill_Sort, c *player.Player) (resul
return nil, 0
}
// CommitPetSkills 按最终技能列表一次性提交学习/替换/排序结果。
func (h Controller) CommitPetSkills(
data *CommitPetSkillsInboundInfo,
c *player.Player,
) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
const setSkillCost = 50
const skillSortCost = 50
slot, ok := c.FindPetBagSlot(data.CatchTime)
currentPet := slot.PetInfoPtr()
if !ok || currentPet == nil {
return nil, errorcode.ErrorCodes.ErrPokemonNotExists
}
currentSkillSet := make(map[uint32]model.SkillInfo, len(currentPet.SkillList))
currentSkillOrder := make([]uint32, 0, len(currentPet.SkillList))
for _, skill := range currentPet.SkillList {
if skill.ID == 0 {
continue
}
currentSkillSet[skill.ID] = skill
currentSkillOrder = append(currentSkillOrder, skill.ID)
}
finalSkillIDs := make([]uint32, 0, 4)
usedSkillSet := make(map[uint32]struct{}, 4)
for _, skillID := range data.Skill {
if skillID == 0 {
continue
}
if _, exists := usedSkillSet[skillID]; exists {
continue
}
usedSkillSet[skillID] = struct{}{}
finalSkillIDs = append(finalSkillIDs, skillID)
}
if len(finalSkillIDs) == 0 {
return nil, errorcode.ErrorCodes.ErrSystemBusy
}
if len(finalSkillIDs) > 4 {
finalSkillIDs = finalSkillIDs[:4]
}
if isSameUint32Slice(currentSkillOrder, finalSkillIDs) {
return nil, 0
}
learnableSkillSet := make(map[uint32]struct{})
for _, skillID := range collectPetLearnableSkillList(currentPet) {
learnableSkillSet[skillID] = struct{}{}
}
newSkillCount := 0
finalSkillList := make([]model.SkillInfo, 0, len(finalSkillIDs))
for _, skillID := range finalSkillIDs {
if skill, exists := currentSkillSet[skillID]; exists {
finalSkillList = append(finalSkillList, skill)
continue
}
if _, exists := learnableSkillSet[skillID]; !exists {
return nil, errorcode.ErrorCodes.ErrSystemBusy
}
skillInfo, exists := xmlres.SkillMap[int(skillID)]
if !exists {
return nil, errorcode.ErrorCodes.ErrSystemBusy
}
newSkillCount++
finalSkillList = append(finalSkillList, model.SkillInfo{
ID: skillID,
PP: uint32(skillInfo.MaxPP),
})
}
totalCost := int64(newSkillCount * setSkillCost)
if newSkillCount == 0 {
totalCost += int64(skillSortCost)
}
if totalCost > 0 && !c.GetCoins(totalCost) {
return nil, errorcode.ErrorCodes.ErrSunDouInsufficient10016
}
c.Info.Coins -= totalCost
currentPet.SkillList = finalSkillList
return nil, 0
}

View File

@@ -17,15 +17,13 @@ func (h Controller) IsCollect(
ID: data.Type,
}
c.Service.Task.Exec(uint32(1335), func(te *model.Task) bool {
r := bitset32.From(te.Data)
// 分支未完成时,标记完成并发放奖励
taskData, taskErr := c.Service.Task.GetTask(uint32(1335))
if taskErr == nil {
r := bitset32.From(taskData.Data)
if r.Test(uint(data.Type)) {
result.IsCom = 1
}
return false
})
}
_, ok := lo.Find([]uint32{1, 2, 3, 4, 301}, func(item uint32) bool {
return data.Type == item
@@ -59,14 +57,17 @@ func (h Controller) Collect(
return data.Type == item
})
if res == model.Completed && ok { //这块是为了兼容旧版本
c.Service.Task.Exec(uint32(1335), func(te *model.Task) bool {
taskData, taskErr := c.Service.Task.GetTask(uint32(1335))
if taskErr != nil {
return nil, errorcode.ErrorCode(errorcode.ErrorCodes.ErrSystemError)
}
r := bitset32.From(te.Data)
r.Set(uint(data.Type))
te.Data = r.Bytes()
return true
})
r := bitset32.From(taskData.Data)
r.Set(uint(data.Type))
taskData.Data = r.Bytes()
if taskErr = c.Service.Task.SetTask(taskData); taskErr != nil {
return nil, errorcode.ErrorCode(errorcode.ErrorCodes.ErrSystemError)
}
return nil, errorcode.ErrorCode(errorcode.ErrorCodes.ErrSystemError)
}
@@ -80,21 +81,22 @@ func (h Controller) Collect(
if !lo.Contains(validIDs, data.ID) {
return nil, errorcode.ErrorCode(errorcode.ErrorCodes.ErrSystemError)
}
c.Service.Task.Exec(uint32(1335), func(te *model.Task) bool {
taskData, taskErr := c.Service.Task.GetTask(uint32(1335))
if taskErr != nil {
return nil, errorcode.ErrorCode(errorcode.ErrorCodes.ErrSystemError)
}
r := bitset32.From(te.Data)
// 分支未完成时,标记完成并发放奖励
if !r.Test(uint(data.Type)) {
r.Set(uint(data.Type))
te.Data = r.Bytes()
r := model.GenPetInfo(int(data.ID), -1, -1, 0, 1, nil, 0)
c.Service.Pet.PetAdd(r, 0)
result.CatchTime = r.CatchTime
return true
r := bitset32.From(taskData.Data)
if !r.Test(uint(data.Type)) {
r.Set(uint(data.Type))
taskData.Data = r.Bytes()
if taskErr = c.Service.Task.SetTask(taskData); taskErr != nil {
return nil, errorcode.ErrorCode(errorcode.ErrorCodes.ErrSystemError)
}
return false
})
petInfo := model.GenPetInfo(int(data.ID), -1, -1, 0, 1, nil, 0)
c.Service.Pet.PetAdd(petInfo, 0)
result.CatchTime = petInfo.CatchTime
}
if result.CatchTime == 0 {
return nil, errorcode.ErrorCode(errorcode.ErrorCodes.ErrAwardAlreadyClaimed)

View File

@@ -6,6 +6,7 @@ import (
"blazing/logic/service/user"
configservice "blazing/modules/config/service"
playerservice "blazing/modules/player/service"
"strings"
"time"
)
@@ -13,10 +14,11 @@ import (
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{}
cdkCode := strings.Trim(data.PassText, "\x00")
cdkService := configservice.NewCdkService()
now := time.Now()
r := cdkService.Get(data.PassText)
r := cdkService.Get(cdkCode)
if r == nil {
return nil, errorcode.ErrorCodes.ErrMolecularCodeNotExists
}
@@ -29,7 +31,7 @@ func (h Controller) CDK(data *C2S_GET_GIFT_COMPLETE, player *logicplayer.Player)
if !player.Service.Cdk.CanGet(uint32(r.ID)) {
return
}
if !cdkService.Set(data.PassText) {
if !cdkService.Set(cdkCode) {
return nil, errorcode.ErrorCodes.ErrMolecularCodeGiftsGone
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,9 +3,12 @@ package fight
import (
"blazing/common/utils"
"blazing/logic/service/common"
"blazing/logic/service/fight/action"
"blazing/logic/service/fight/info"
"blazing/logic/service/fight/input"
_ "blazing/logic/service/fight/itemover"
_ "blazing/logic/service/fight/rule"
"blazing/modules/player/model"
"reflect"
@@ -132,7 +135,20 @@ func (f *FightC) getSkillParticipants(skillAction *action.SelectSkillAction) (*i
if skillAction == nil {
return nil, nil
}
return f.GetInputByAction(skillAction, false), f.GetInputByAction(skillAction, true)
attacker := f.GetInputByAction(skillAction, false)
defender := f.GetInputByAction(skillAction, true)
if attacker != nil && defender == attacker && shouldResolveOpponentAsTarget(skillAction.SkillEntity) {
if opponent, _ := attacker.OpponentSlotAtOrNextLiving(0); opponent != nil {
defender = opponent
} else if opponent := f.roundOpponentInput(attacker); opponent != nil {
defender = opponent
}
}
return attacker, defender
}
func shouldResolveOpponentAsTarget(skill *info.SkillEntity) bool {
return skill != nil && skill.XML.AtkType == 3
}
// setEffectSkillContext 统一设置技能阶段 effect 上下文。
@@ -183,20 +199,63 @@ func (f *FightC) collectAttackValues(inputs []*input.Input) []model.AttackValue
continue
}
attackValue := *fighter.AttackValue
if attackValue.SkillID == 0 {
continue
}
attackValue.ActorIndex = uint32(actorIndex)
values = append(values, attackValue)
}
return values
}
func (f *FightC) buildAttackValueForBroadcast(fighter *input.Input, fallbackActorIndex int) model.AttackValue {
if fighter == nil {
return model.AttackValue{}
}
if fighter.AttackValue == nil {
empty := info.NewAttackValue(fighter.UserID)
fighter.AttackValue = empty
}
attackValue := *fighter.AttackValue
attackValue.ActorIndex = uint32(fallbackActorIndex)
if attackValue.UserID == 0 && fighter.Player != nil && fighter.Player.GetInfo() != nil {
attackValue.UserID = fighter.Player.GetInfo().UserID
}
return attackValue
}
func (f *FightC) buildNoteUseSkillOutboundInfo() info.NoteUseSkillOutboundInfo {
result := info.NoteUseSkillOutboundInfo{}
result.FirstAttackInfo = append(result.FirstAttackInfo, f.collectAttackValues(f.Our)...)
result.SecondAttackInfo = append(result.SecondAttackInfo, f.collectAttackValues(f.Opp)...)
if f.First != nil {
result.FirstAttackInfo = f.buildAttackValueForBroadcast(f.First, f.First.TeamSlotIndex())
}
if f.Second != nil {
result.SecondAttackInfo = f.buildAttackValueForBroadcast(f.Second, f.Second.TeamSlotIndex())
}
return result
}
func (f *FightC) roundOpponentInput(attacker *input.Input) *input.Input {
if attacker == nil {
return nil
}
for _, opponent := range attacker.OpponentSlots() {
if opponent != nil {
return opponent
}
}
return nil
}
func shouldSkipSecondAction(first, second *input.Input) bool {
if first == nil || second == nil {
return false
}
firstPet := first.CurrentPet()
secondPet := second.CurrentPet()
return firstPet == nil || firstPet.Info.Hp <= 0 || secondPet == nil || secondPet.Info.Hp <= 0
}
// enterturn 处理战斗回合逻辑
// 回合有先手方和后手方,同时有攻击方和被攻击方
func (f *FightC) enterturn(firstAttack, secondAttack *action.SelectSkillAction) {
@@ -244,9 +303,11 @@ func (f *FightC) enterturn(firstAttack, secondAttack *action.SelectSkillAction)
f.First, _ = f.getSkillParticipants(firstAttack)
f.Second, _ = f.getSkillParticipants(secondAttack)
case firstAttack != nil:
f.First, f.Second = f.getSkillParticipants(firstAttack)
f.First, _ = f.getSkillParticipants(firstAttack)
f.Second = f.roundOpponentInput(f.First)
case secondAttack != nil:
f.First, f.Second = f.getSkillParticipants(secondAttack)
f.First, _ = f.getSkillParticipants(secondAttack)
f.Second = f.roundOpponentInput(f.First)
}
if f.First == nil {
f.First = f.primaryOur()
@@ -274,25 +335,32 @@ func (f *FightC) enterturn(firstAttack, secondAttack *action.SelectSkillAction)
}
}
if firstAttack == nil && secondAttack == nil {
firstAttack, secondAttack = secondAttack, firstAttack //互换先手权
f.First, f.Second = f.Second, f.First
}
skipActionStage := firstAttack == nil && secondAttack == nil
var attacker, defender *input.Input
f.TrueFirst = f.First
//开始回合操作
for i := 0; i < 2; i++ {
//开始回合操作。若双方本回合都未出手,则只走完整回合流程,不进入动作阶段。
for i := 0; !skipActionStage && i < 2; i++ {
var originalSkill *info.SkillEntity //原始技能
var currentSkill *info.SkillEntity //当前技能
var currentAction *action.SelectSkillAction
if i == 0 {
currentAction = firstAttack
if currentAction == nil {
continue
}
attacker, defender = f.getSkillParticipants(firstAttack)
originalSkill = f.copySkill(firstAttack)
//先手阶段,先修复后手效果
f.Second.RecoverEffect()
} else {
currentAction = secondAttack
if currentAction == nil {
continue
}
if shouldSkipSecondAction(f.First, f.Second) {
secondAttack = nil
continue
}
attacker, defender = f.getSkillParticipants(secondAttack)
originalSkill = f.copySkill(secondAttack)
//取消后手历史效果
@@ -331,7 +399,6 @@ func (f *FightC) enterturn(firstAttack, secondAttack *action.SelectSkillAction)
}
//先手权不一定出手
} else {
f.setActionAttackValue(currentAction)
@@ -422,11 +489,19 @@ func (f *FightC) enterturn(firstAttack, secondAttack *action.SelectSkillAction)
attackValueResult := f.buildNoteUseSkillOutboundInfo()
//因为切完才能广播,所以必须和回合结束分开结算
f.Broadcast(func(fighter *input.Input) {
f.BroadcastPlayers(func(p common.PlayerI) {
for _, switchAction := range f.Switch {
if fighter.Player.GetInfo().UserID != switchAction.Reason.UserId {
if p.GetInfo().UserID != switchAction.Reason.UserId {
// println("切精灵", switchAction.Reason.UserId, switchAction.Reason.ID)
fighter.Player.SendPackCmd(2407, &switchAction.Reason)
if f.LegacyGroupProtocol {
switchedInput := f.getInputByUserID(switchAction.Reason.UserId, int(switchAction.Reason.ActorIndex), false)
if switchedInput == nil {
switchedInput = f.getInputByUserID(switchAction.Reason.UserId, int(switchAction.ActorIndex), false)
}
f.sendLegacyGroupChangePetSuccess(p, switchedInput, &switchAction.Reason)
} else {
f.sendFightPacket(p, fightPacketChangePetSuccess, &switchAction.Reason)
}
}
}
})
@@ -434,14 +509,20 @@ func (f *FightC) enterturn(firstAttack, secondAttack *action.SelectSkillAction)
if f.closefight && f.Info.Mode == info.BattleMode.PET_MELEE {
// f.Broadcast(func(fighter *input.Input) {
// if fighter.UserID != f.WinnerId {
// fighter.Player.SendPackCmd(2505, &attackValueResult)
// f.sendFightPacket(fighter.Player, 2505, groupCmdSkillHurt, /&attackValueResult)
// }
// })
return
}
if attackValueResult.FirstAttackInfo.UserID != 0 || attackValueResult.SecondAttackInfo.UserID != 0 {
f.BroadcastPlayers(func(p common.PlayerI) {
if !f.LegacyGroupProtocol {
f.sendFightPacket(p, fightPacketSkillResult, &attackValueResult)
}
})
}
f.Broadcast(func(fighter *input.Input) {
fighter.Player.SendPackCmd(2505, &attackValueResult)
fighter.CanChange = 0
})
if f.closefight {
@@ -454,12 +535,7 @@ func (f *FightC) TURNOVER(cur *input.Input) {
if cur == nil {
return
}
for _, pet := range cur.BenchPets() {
if pet != nil && pet.Info.Hp > 0 {
_hasBackup = true
break
}
}
_hasBackup = cur.HasLivingBench()
f.sendLegacySpriteDie(cur, _hasBackup)
f.Broadcast(func(ff *input.Input) {
@@ -474,9 +550,9 @@ func (f *FightC) TURNOVER(cur *input.Input) {
if f.IsWin(f.GetInputByPlayer(cur.Player, true)) { //然后检查是否战斗结束
f.FightOverInfo.WinnerId = f.GetInputByPlayer(cur.Player, true).UserID
f.FightOverInfo.Reason = model.BattleOverReason.DefaultEnd
f.FightOverInfo.Reason = normalizeFightOverReason(model.BattleOverReason.DefaultEnd)
f.WinnerId = f.FightOverInfo.WinnerId
f.Reason = model.BattleOverReason.DefaultEnd
f.Reason = f.FightOverInfo.Reason
f.closefight = true
// break

View File

@@ -6,10 +6,13 @@ import (
"blazing/logic/service/fight/info"
"blazing/logic/service/fight/input"
"blazing/modules/player/model"
"bytes"
"encoding/binary"
)
// <!--
// GBTL:
// 1. AtkNum:本技能同时攻击数量, 默认:1(不能为0)
// 2. AtkType:攻击类型: 0:所有人, 1:仅己方, 2:仅对方, 3:仅自己, 默认:2
// -->
const (
groupCmdReadyToFight uint32 = 7555
groupCmdReadyFightFinish uint32 = 7556
@@ -39,6 +42,173 @@ const (
groupModelPlayerMulti uint32 = 6
)
type fightPacketKind uint8
const (
fightPacketReady fightPacketKind = iota
fightPacketStart
fightPacketSkillResult
fightPacketOver
fightPacketChangePetSuccess
fightPacketUseItem
fightPacketChat
fightPacketLoadPercentNotice
)
type legacyEscapeSuccessInfo struct {
UserID uint32 `struc:"uint32"`
Nick string `struc:"[16]byte"`
Side uint8 `struc:"uint8"`
ActorIndex uint8 `struc:"uint8"`
}
type legacyBoutDoneInfo struct {
Round uint32 `struc:"uint32"`
}
type legacySpriteDieInfo struct {
Count uint8 `struc:"uint8"`
Side uint8 `struc:"uint8"`
ActorIndex uint8 `struc:"uint8"`
Flag uint8 `struc:"uint8"`
HasBackup uint32 `struc:"uint32"`
}
type legacyLegacySpriteDieItem struct {
Flag uint8 `struc:"uint8"`
Side uint8 `struc:"uint8"`
ActorIndex uint8 `struc:"uint8"`
Reserve uint8 `struc:"uint8"`
HasBackup uint32 `struc:"uint32"`
}
type legacyGroupReadyToFightInfo struct {
Model uint32 `struc:"uint32"`
GroupOneInfo legacyReadyToFightTeam `struc:""`
GroupTwoInfo legacyReadyToFightTeam `struc:""`
}
type legacyReadyToFightTeam struct {
InvitorID uint8 `struc:"uint8"`
LeaderID uint32 `struc:"uint32"`
GroupMembCnt uint8 `struc:"sizeof=GroupList"`
GroupList []legacyReadyFightUser `struc:""`
}
type legacyReadyFightUser struct {
UserID uint32 `struc:"uint32"`
Nick string `struc:"[16]byte"`
MonCnt uint32 `struc:"sizeof=MonList"`
MonList []legacyReadyFightPet `struc:""`
}
type legacyReadyFightPet struct {
ID uint32 `struc:"uint32"`
MoveCnt uint32 `struc:"sizeof=MoveList"`
MoveList []uint32 `struc:"[]uint32"`
}
type legacyGroupStartInfo struct {
IsGank uint8 `struc:"uint8"`
GroupOneN uint8 `struc:"sizeof=GroupOne"`
GroupOne []legacyGroupStartPet `struc:""`
GroupTwoN uint8 `struc:"sizeof=GroupTwo"`
GroupTwo []legacyGroupStartPet `struc:""`
}
type legacyGroupStartPet struct {
Side uint8 `struc:"uint8"`
Pos uint8 `struc:"uint8"`
UserID uint32 `struc:"uint32"`
IsChange uint8 `struc:"uint8"`
PetID uint32 `struc:"uint32"`
CatchTime uint32 `struc:"uint32"`
Hp uint32 `struc:"uint32"`
MaxHp uint32 `struc:"uint32"`
Level uint32 `struc:"uint32"`
Reserve uint32 `struc:"uint32"`
Flag uint32 `struc:"uint32"`
}
type legacyGroupSkillHurtPacket struct {
IsGank uint8 `struc:"uint8"`
Attack legacyGroupSkillAttackInfo `struc:""`
Attacked legacyGroupSkillDefendInfo `struc:""`
}
type legacyGroupSkillAttackInfo struct {
IsAttackor uint8 `struc:"uint8"`
Side uint8 `struc:"uint8"`
Pos uint8 `struc:"uint8"`
UserID uint32 `struc:"uint32"`
StatusList [20]uint8 `struc:"[20]byte"`
Reserve1 uint8 `struc:"uint8"`
Reserve2 uint8 `struc:"uint8"`
BatLvList [6]uint8 `struc:"[6]byte"`
PetID uint32 `struc:"uint32"`
MoveID uint32 `struc:"uint32"`
Hp uint32 `struc:"uint32"`
MaxHp uint32 `struc:"uint32"`
MoveCnt uint32 `struc:"sizeof=MoveMap"`
MoveMap []legacyGroupSkillMoveInfo `struc:""`
Flag uint32 `struc:"uint32"`
IsCrit uint32 `struc:"uint32"`
EffectName uint32 `struc:"uint32"`
AtkTimes uint32 `struc:"uint32"`
Dmg int32 `struc:"int32"`
ChgHp int32 `struc:"int32"`
SideEffectLen uint32 `struc:"uint32"`
}
type legacyGroupSkillDefendInfo struct {
IsAttackor uint8 `struc:"uint8"`
Side uint8 `struc:"uint8"`
Pos uint8 `struc:"uint8"`
UserID uint32 `struc:"uint32"`
StatusList [20]uint8 `struc:"[20]byte"`
Reserve1 uint8 `struc:"uint8"`
Reserve2 uint8 `struc:"uint8"`
BatLvList [6]uint8 `struc:"[6]byte"`
PetID uint32 `struc:"uint32"`
MoveID uint32 `struc:"uint32"`
Hp uint32 `struc:"uint32"`
MaxHp uint32 `struc:"uint32"`
MoveCnt uint32 `struc:"sizeof=MoveMap"`
MoveMap []legacyGroupSkillMoveInfo `struc:""`
Flag uint32 `struc:"uint32"`
SideEffectLen uint32 `struc:"uint32"`
}
type legacyGroupSkillMoveInfo struct {
MoveID uint32 `struc:"uint32"`
PP uint32 `struc:"uint32"`
}
type legacyGroupFightOverInfo struct {
IsGank uint8 `struc:"uint8"`
Reason uint32 `struc:"uint32"`
WinnerID uint32 `struc:"uint32"`
Reserve uint32 `struc:"uint32"`
TwoTimes uint32 `struc:"uint32"`
ThreeTimes uint32 `struc:"uint32"`
AutoFightTime uint32 `struc:"uint32"`
Reserve2 uint32 `struc:"uint32"`
EnergyTime uint32 `struc:"uint32"`
LearnTimes uint32 `struc:"uint32"`
}
type legacyGroupChangePetSuccessInfo struct {
Side uint8 `struc:"uint8"`
Pos uint8 `struc:"uint8"`
UserID uint32 `struc:"uint32"`
PetID uint32 `struc:"uint32"`
CatchTime uint32 `struc:"uint32"`
Level uint32 `struc:"uint32"`
Hp uint32 `struc:"uint32"`
MaxHp uint32 `struc:"uint32"`
SkinID uint32 `struc:"uint32"`
}
func groupModelByFight(f *FightC) uint32 {
if f == nil {
return groupModelBoss
@@ -55,374 +225,450 @@ func groupModelByFight(f *FightC) uint32 {
}
}
func writeUint8(buf *bytes.Buffer, v uint8) {
_ = buf.WriteByte(v)
}
func writeUint32(buf *bytes.Buffer, v uint32) {
_ = binary.Write(buf, binary.BigEndian, v)
}
func writeInt32(buf *bytes.Buffer, v int32) {
_ = binary.Write(buf, binary.BigEndian, v)
}
func writeFixedString16(buf *bytes.Buffer, s string) {
raw := make([]byte, 16)
copy(raw, []byte(s))
_, _ = buf.Write(raw)
}
func buildPacket(cmd uint32, userID uint32, payload []byte) []byte {
header := common.NewTomeeHeader(cmd, userID)
totalLen := uint32(17 + len(payload))
header.Len = totalLen
buf := make([]byte, totalLen)
binary.BigEndian.PutUint32(buf[0:4], totalLen)
buf[4] = header.Version
binary.BigEndian.PutUint32(buf[5:9], cmd)
binary.BigEndian.PutUint32(buf[9:13], userID)
binary.BigEndian.PutUint32(buf[13:17], 0)
copy(buf[17:], payload)
return buf
}
func (f *FightC) sendLegacyGroupReady() {
f.Broadcast(func(ff *input.Input) {
if ff == nil || ff.Player == nil {
return
func (f *FightC) fightPacketCmd(kind fightPacketKind) uint32 {
switch kind {
case fightPacketReady:
return 2503
case fightPacketStart:
return 2504
case fightPacketSkillResult:
return 2505
case fightPacketOver:
return 2506
case fightPacketChangePetSuccess:
if f != nil && f.LegacyGroupProtocol {
return groupCmdChangePetSuc
}
sendLegacyPacket(ff.Player, groupCmdReadyToFight, f.buildLegacyGroupReadyPayload())
})
return 2407
case fightPacketUseItem:
if f != nil && f.LegacyGroupProtocol {
return groupCmdUseItem
}
return 2406
case fightPacketChat:
if f != nil && f.LegacyGroupProtocol {
return groupCmdChat
}
return 50002
case fightPacketLoadPercentNotice:
if f != nil && f.LegacyGroupProtocol {
return groupCmdLoadPercentNotice
}
return 2441
default:
return 0
}
}
func (f *FightC) sendFightPacket(player common.PlayerI, kind fightPacketKind, payload any) {
if player == nil {
return
}
cmd := f.fightPacketCmd(kind)
if cmd == 0 {
return
}
player.SendPackCmd(cmd, payload)
}
func (f *FightC) sendLegacyGroupReady(player common.PlayerI) {
if f == nil || !f.LegacyGroupProtocol || player == nil {
return
}
player.SendPackCmd(groupCmdReadyToFight, f.buildLegacyGroupReadyInfo())
}
func (f *FightC) buildLegacyGroupReadyInfo() *legacyGroupReadyToFightInfo {
return &legacyGroupReadyToFightInfo{
Model: groupModelByFight(f),
GroupOneInfo: f.buildLegacyReadyTeam(f.OurPlayers, f.Our),
GroupTwoInfo: f.buildLegacyReadyTeam(f.OppPlayers, f.Opp),
}
}
func (f *FightC) buildLegacyReadyTeam(players []common.PlayerI, inputs []*input.Input) legacyReadyToFightTeam {
team := legacyReadyToFightTeam{InvitorID: 1}
users := make([]legacyReadyFightUser, 0, len(players))
for _, p := range players {
if p == nil || p.GetInfo() == nil {
continue
}
users = append(users, legacyReadyFightUser{
UserID: p.GetInfo().UserID,
Nick: p.GetInfo().Nick,
MonList: collectLegacyReadyPetsByController(inputs, p.GetInfo().UserID),
})
}
if len(users) == 0 {
if fallback := firstNonNilInput(inputs); fallback != nil && fallback.Player != nil && fallback.Player.GetInfo() != nil {
info := fallback.Player.GetInfo()
users = append(users, legacyReadyFightUser{
UserID: info.UserID,
Nick: info.Nick,
MonList: collectLegacyReadyPetsByController(inputs, info.UserID),
})
}
}
for idx := range users {
users[idx].MonCnt = uint32(len(users[idx].MonList))
}
if len(users) > 0 {
team.LeaderID = users[0].UserID
}
team.GroupList = users
return team
}
func collectLegacyReadyPetsByController(inputs []*input.Input, controllerID uint32) []legacyReadyFightPet {
pets := make([]legacyReadyFightPet, 0, 6)
for _, in := range inputs {
if in == nil || !in.ControlledBy(controllerID) {
continue
}
currentPet := in.CurrentPet()
if currentPet == nil {
continue
}
pets = append(pets, buildLegacyReadyFightPet(currentPet))
}
return pets
}
func buildLegacyReadyFightPet(pet *info.BattlePetEntity) legacyReadyFightPet {
result := legacyReadyFightPet{}
if pet == nil {
return result
}
moves := make([]uint32, 0, len(pet.Info.SkillList))
for _, skill := range pet.Info.SkillList {
if skill.ID == 0 {
continue
}
moves = append(moves, skill.ID)
}
result.ID = pet.Info.ID
result.MoveList = moves
return result
}
func firstNonNilInput(inputs []*input.Input) *input.Input {
for _, in := range inputs {
if in != nil {
return in
}
}
return nil
}
func (f *FightC) sendLegacyGroupStart(player common.PlayerI) {
if player == nil {
if f == nil || !f.LegacyGroupProtocol || player == nil {
return
}
sendLegacyPacket(player, groupCmdStartFight, f.buildLegacyGroupStartPayload())
player.SendPackCmd(groupCmdStartFight, f.buildLegacyGroupStartInfo())
}
func (f *FightC) buildLegacyGroupStartInfo() *legacyGroupStartInfo {
return &legacyGroupStartInfo{
IsGank: 0,
GroupOne: f.collectLegacyGroupStartPets(f.Our, 1),
GroupTwo: f.collectLegacyGroupStartPets(f.Opp, 2),
}
}
func (f *FightC) collectLegacyGroupStartPets(inputs []*input.Input, side uint8) []legacyGroupStartPet {
ret := make([]legacyGroupStartPet, 0, len(inputs))
for pos, in := range inputs {
if in == nil {
continue
}
currentPet := in.CurrentPet()
if currentPet == nil {
continue
}
userID := uint32(0)
if in.Player != nil && in.Player.GetInfo() != nil {
userID = in.Player.GetInfo().UserID
}
ret = append(ret, legacyGroupStartPet{
Side: side,
Pos: uint8(pos),
UserID: userID,
IsChange: 0,
PetID: currentPet.Info.ID,
CatchTime: currentPet.Info.CatchTime,
Hp: currentPet.Info.Hp,
MaxHp: currentPet.Info.MaxHp,
Level: currentPet.Info.Level,
Reserve: 0,
Flag: 1,
})
}
return ret
}
func (f *FightC) sendLegacyGroupOver(player common.PlayerI, over *model.FightOverInfo) {
if player == nil {
if f == nil || !f.LegacyGroupProtocol || player == nil {
return
}
sendLegacyPacket(player, groupCmdFightOver, f.buildLegacyGroupOverPayload(over))
player.SendPackCmd(groupCmdFightOver, f.buildLegacyGroupOverInfo(over))
}
func sendLegacyPacket(player common.PlayerI, cmd uint32, payload []byte) {
if player == nil {
return
}
if sender, ok := player.(interface{ SendPack([]byte) error }); ok {
_ = sender.SendPack(buildPacket(cmd, player.GetInfo().UserID, payload))
}
}
func (f *FightC) buildLegacyGroupReadyPayload() []byte {
var (
buf bytes.Buffer
users [][]*input.Input
)
writeUint32(&buf, groupModelByFight(f))
users = [][]*input.Input{f.Our, f.Opp}
for sideIndex, sideInputs := range users {
writeUint8(&buf, 1)
var leaderID uint32
if len(sideInputs) > 0 && sideInputs[0] != nil && sideInputs[0].Player != nil {
leaderID = sideInputs[0].Player.GetInfo().UserID
}
writeUint32(&buf, leaderID)
writeUint8(&buf, 1)
if leaderID == 0 {
writeUint32(&buf, 0)
writeFixedString16(&buf, "boss")
} else {
writeUint32(&buf, leaderID)
writeFixedString16(&buf, sideInputs[0].Player.GetInfo().Nick)
}
writeUint32(&buf, uint32(len(sideInputs)))
for _, slot := range sideInputs {
if slot == nil || slot.CurrentPet() == nil {
continue
}
currentPet := slot.CurrentPet()
writeUint32(&buf, currentPet.Info.ID)
writeUint32(&buf, uint32(len(currentPet.Info.SkillList)))
for _, skill := range currentPet.Info.SkillList {
writeUint32(&buf, skill.ID)
}
}
if sideIndex == 0 && len(sideInputs) == 0 {
writeUint32(&buf, 0)
writeFixedString16(&buf, "")
writeUint32(&buf, 0)
}
}
return buf.Bytes()
}
func (f *FightC) buildLegacyGroupStartPayload() []byte {
var (
buf bytes.Buffer
sides [][]*input.Input
)
writeUint8(&buf, 0)
sides = [][]*input.Input{f.Our, f.Opp}
for sideIndex, sideInputs := range sides {
writeUint8(&buf, uint8(len(sideInputs)))
for pos, slot := range sideInputs {
if slot == nil || slot.CurrentPet() == nil {
continue
}
currentPet := slot.CurrentPet()
writeUint8(&buf, uint8(sideIndex+1))
writeUint8(&buf, uint8(pos))
if slot.Player != nil {
writeUint32(&buf, slot.Player.GetInfo().UserID)
} else {
writeUint32(&buf, 0)
}
writeUint8(&buf, 0)
writeUint32(&buf, currentPet.Info.ID)
writeUint32(&buf, currentPet.Info.CatchTime)
writeUint32(&buf, currentPet.Info.Hp)
writeUint32(&buf, currentPet.Info.MaxHp)
writeUint32(&buf, currentPet.Info.Level)
writeUint32(&buf, 0)
writeUint32(&buf, 1)
}
}
return buf.Bytes()
}
func (f *FightC) buildLegacyGroupOverPayload(over *model.FightOverInfo) []byte {
var (
buf bytes.Buffer
winnerID uint32
endReason uint32
playerInfo *model.PlayerInfo
)
func (f *FightC) buildLegacyGroupOverInfo(over *model.FightOverInfo) *legacyGroupFightOverInfo {
result := &legacyGroupFightOverInfo{}
if over != nil {
winnerID = over.WinnerId
endReason = uint32(over.Reason)
result.Reason = resolveLegacyGroupFightOverReason(over)
result.WinnerID = over.WinnerId
}
if our := f.primaryOurPlayer(); our != nil {
playerInfo = our.GetInfo()
if our := f.primaryOurPlayer(); our != nil && our.GetInfo() != nil {
playerInfo := our.GetInfo()
result.TwoTimes = uint32(playerInfo.TwoTimes)
result.ThreeTimes = uint32(playerInfo.ThreeTimes)
result.AutoFightTime = playerInfo.AutoFightTime
result.EnergyTime = uint32(playerInfo.EnergyTime)
result.LearnTimes = playerInfo.LearnTimes
}
writeUint8(&buf, 0)
writeUint32(&buf, endReason)
writeUint32(&buf, winnerID)
writeUint32(&buf, 0)
if playerInfo != nil {
writeUint32(&buf, uint32(playerInfo.TwoTimes))
writeUint32(&buf, uint32(playerInfo.ThreeTimes))
writeUint32(&buf, playerInfo.AutoFightTime)
writeUint32(&buf, 0)
writeUint32(&buf, uint32(playerInfo.EnergyTime))
writeUint32(&buf, playerInfo.LearnTimes)
return result
}
func mapLegacyGroupFightOverReason(reason model.EnumBattleOverReason) uint32 {
return mapUnifiedFightOverReason(reason)
}
func resolveLegacyGroupFightOverReason(over *model.FightOverInfo) uint32 {
if over == nil {
return mapUnifiedFightOverReason(0)
}
if over.WinnerId != 0 {
return mapUnifiedFightOverReason(0)
}
return mapLegacyGroupFightOverReason(over.Reason)
}
func (f *FightC) sendLegacyGroupChangePetSuccess(player common.PlayerI, in *input.Input, reason *info.ChangePetInfo) {
if f == nil || !f.LegacyGroupProtocol || player == nil || in == nil || reason == nil {
return
}
player.SendPackCmd(groupCmdChangePetSuc, f.buildLegacyGroupChangePetSuccessInfo(in, reason))
}
func (f *FightC) buildLegacyGroupChangePetSuccessInfo(in *input.Input, reason *info.ChangePetInfo) *legacyGroupChangePetSuccessInfo {
result := &legacyGroupChangePetSuccessInfo{}
if in == nil || reason == nil {
return result
}
if !f.isOurPlayerID(in.UserID) {
result.Side = 2
} else {
for i := 0; i < 6; i++ {
writeUint32(&buf, 0)
}
result.Side = 1
}
return buf.Bytes()
result.Pos = uint8(in.TeamSlotIndex())
result.UserID = reason.UserId
result.PetID = reason.ID
result.CatchTime = reason.CatchTime
result.Level = reason.Level
result.Hp = reason.Hp
result.MaxHp = reason.MaxHp
if currentPet := in.CurrentPet(); currentPet != nil {
result.SkinID = currentPet.Info.SkinID
}
return result
}
func (f *FightC) SendLegacyEscapeSuccess(player common.PlayerI, actorIndex int) {
if f == nil || !f.LegacyGroupProtocol || player == nil {
return
}
payload := f.buildLegacyEscapePayload(player, actorIndex)
f.Broadcast(func(ff *input.Input) {
if ff == nil || ff.Player == nil {
return
}
sendLegacyPacket(ff.Player, groupCmdEscapeSuc, payload)
})
}
func (f *FightC) buildLegacyEscapePayload(player common.PlayerI, actorIndex int) []byte {
var buf bytes.Buffer
side := uint8(1)
if !f.isOurPlayerID(player.GetInfo().UserID) {
side = 2
}
writeUint32(&buf, player.GetInfo().UserID)
writeFixedString16(&buf, player.GetInfo().Nick)
writeUint8(&buf, side)
writeUint8(&buf, uint8(actorIndex))
return buf.Bytes()
payload := legacyEscapeSuccessInfo{
UserID: player.GetInfo().UserID,
Nick: player.GetInfo().Nick,
Side: side,
ActorIndex: uint8(actorIndex),
}
f.BroadcastPlayers(func(p common.PlayerI) {
p.SendPackCmd(groupCmdEscapeSuc, &payload)
})
}
func (f *FightC) sendLegacyRoundBroadcast(firstAttack, secondAttack *action.SelectSkillAction) {
if f == nil || !f.LegacyGroupProtocol {
return
}
if firstAttack != nil {
if f.legacySkillExecuted(firstAttack) {
f.sendLegacyGroupSkillHurt(firstAttack)
}
if secondAttack != nil {
if f.legacySkillExecuted(secondAttack) {
f.sendLegacyGroupSkillHurt(secondAttack)
}
f.sendLegacyGroupBoutDone()
}
func (f *FightC) legacySkillExecuted(skillAction *action.SelectSkillAction) bool {
if f == nil || skillAction == nil {
return false
}
attacker := f.GetInputByAction(skillAction, false)
return attacker != nil && attacker.AttackValue != nil && attacker.AttackValue.SkillID != 0
}
func (f *FightC) sendLegacyGroupSkillHurt(skillAction *action.SelectSkillAction) {
var payload []byte
if skillAction == nil {
if f == nil || !f.LegacyGroupProtocol || skillAction == nil {
return
}
payload = f.buildLegacyGroupSkillHurtPayload(skillAction)
if len(payload) == 0 {
packet := f.buildLegacyGroupSkillHurtPacket(skillAction)
if packet == nil {
return
}
f.Broadcast(func(ff *input.Input) {
if ff == nil || ff.Player == nil {
return
}
sendLegacyPacket(ff.Player, groupCmdSkillHurt, payload)
f.BroadcastPlayers(func(p common.PlayerI) {
p.SendPackCmd(groupCmdSkillHurt, packet)
})
}
func (f *FightC) buildLegacyGroupSkillHurtPayload(skillAction *action.SelectSkillAction) []byte {
var buf bytes.Buffer
func (f *FightC) buildLegacyGroupSkillHurtPacket(skillAction *action.SelectSkillAction) *legacyGroupSkillHurtPacket {
attacker := f.GetInputByAction(skillAction, false)
defender := f.GetInputByAction(skillAction, true)
if attacker == nil || defender == nil || attacker.AttackValue == nil || defender.AttackValue == nil {
if attacker == nil || defender == nil {
return nil
}
writeUint8(&buf, 0)
f.writeLegacySkillHurtInfo(&buf, skillAction, attacker, defender, true)
f.writeLegacySkillHurtInfo(&buf, skillAction, defender, attacker, false)
return buf.Bytes()
return &legacyGroupSkillHurtPacket{
IsGank: 0,
Attack: f.buildLegacyGroupSkillAttackInfo(skillAction, attacker),
Attacked: f.buildLegacyGroupSkillDefendInfo(defender),
}
}
func (f *FightC) writeLegacySkillHurtInfo(buf *bytes.Buffer, skillAction *action.SelectSkillAction, self *input.Input, opponent *input.Input, isAttacker bool) {
var (
moveID uint32
attackVal *model.AttackValue
currentPet *info.BattlePetEntity
side uint8
pos uint8
)
if self == nil || buf == nil {
func (f *FightC) fillLegacyGroupSkillCommonFields(
self *input.Input,
isAttackor uint8,
statusList *[20]uint8,
batLvList *[6]uint8,
) (side uint8, pos uint8, userID uint32, petID uint32, hp uint32, maxHP uint32, moveMap []legacyGroupSkillMoveInfo, flag uint32) {
if self == nil {
return
}
if self.AttackValue == nil {
attackVal = info.NewAttackValue(self.UserID)
} else {
attackVal = self.AttackValue
}
currentPet = self.CurrentPet()
side = 1
if !f.isOurPlayerID(self.UserID) {
side = 2
} else {
side = 1
}
pos = uint8(self.TeamSlotIndex())
moveID = attackVal.SkillID
if isAttacker {
if skillAction != nil && skillAction.SkillEntity != nil {
moveID = uint32(skillAction.SkillEntity.XML.ID)
}
writeUint8(buf, 0)
} else {
writeUint8(buf, 1)
moveID = 0
userID = self.UserID
attackValue := self.AttackValue
if attackValue == nil {
attackValue = info.NewAttackValue(self.UserID)
}
writeUint8(buf, side)
writeUint8(buf, pos)
writeUint32(buf, self.UserID)
for i := 0; i < 20; i++ {
writeUint8(buf, uint8(attackVal.Status[i]))
for i := 0; i < len(attackValue.Status) && i < 20; i++ {
statusList[i] = uint8(attackValue.Status[i])
}
writeUint8(buf, 0)
writeUint8(buf, 0)
for i := 0; i < 6; i++ {
writeUint8(buf, uint8(attackVal.Prop[i]))
for i := 0; i < len(attackValue.Prop) && i < len(batLvList); i++ {
batLvList[i] = uint8(attackValue.Prop[i])
}
currentPet := self.CurrentPet()
if currentPet != nil {
writeUint32(buf, currentPet.Info.ID)
petID = currentPet.Info.ID
hp = currentPet.Info.Hp
maxHP = currentPet.Info.MaxHp
moveMap = collectLegacyGroupSkillMoves(currentPet.Info.SkillList)
} else {
writeUint32(buf, 0)
hp = clampLegacyInt32ToUint32(attackValue.RemainHp)
maxHP = attackValue.MaxHp
moveMap = collectLegacyGroupSkillMoves(attackValue.SkillList)
}
writeUint32(buf, moveID)
if currentPet != nil {
writeUint32(buf, currentPet.Info.Hp)
writeUint32(buf, currentPet.Info.MaxHp)
writeUint32(buf, uint32(len(currentPet.Info.SkillList)))
for _, skill := range currentPet.Info.SkillList {
writeUint32(buf, skill.ID)
writeUint32(buf, skill.PP)
}
} else {
writeUint32(buf, uint32(maxInt32(attackVal.RemainHp)))
writeUint32(buf, attackVal.MaxHp)
writeUint32(buf, uint32(len(attackVal.SkillList)))
for _, skill := range attackVal.SkillList {
writeUint32(buf, skill.ID)
writeUint32(buf, skill.PP)
flag = attackValue.State
return
}
func (f *FightC) buildLegacyGroupSkillAttackInfo(skillAction *action.SelectSkillAction, self *input.Input) legacyGroupSkillAttackInfo {
result := legacyGroupSkillAttackInfo{}
if self == nil {
return result
}
result.IsAttackor = 0
result.Side, result.Pos, result.UserID, result.PetID, result.Hp, result.MaxHp, result.MoveMap, result.Flag =
f.fillLegacyGroupSkillCommonFields(self, result.IsAttackor, &result.StatusList, &result.BatLvList)
attackValue := self.AttackValue
if attackValue == nil {
attackValue = info.NewAttackValue(self.UserID)
}
if attackValue.SkillID != 0 {
result.MoveID = attackValue.SkillID
} else if skillAction != nil && skillAction.SkillEntity != nil {
result.MoveID = uint32(skillAction.SkillEntity.XML.ID)
}
result.IsCrit = attackValue.IsCritical
result.EffectName = attackValue.State
result.AtkTimes = 1
result.Dmg = int32(attackValue.LostHp)
result.ChgHp = attackValue.GainHp
return result
}
func (f *FightC) buildLegacyGroupSkillDefendInfo(self *input.Input) legacyGroupSkillDefendInfo {
result := legacyGroupSkillDefendInfo{}
if self == nil {
return result
}
result.IsAttackor = 1
result.Side, result.Pos, result.UserID, result.PetID, result.Hp, result.MaxHp, result.MoveMap, result.Flag =
f.fillLegacyGroupSkillCommonFields(self, result.IsAttackor, &result.StatusList, &result.BatLvList)
result.MoveID = 0
return result
}
func collectLegacyGroupSkillMoves(skills []model.SkillInfo) []legacyGroupSkillMoveInfo {
moves := make([]legacyGroupSkillMoveInfo, 0, len(skills))
for _, skill := range skills {
if skill.ID == 0 {
continue
}
moves = append(moves, legacyGroupSkillMoveInfo{
MoveID: skill.ID,
PP: skill.PP,
})
}
writeUint32(buf, attackVal.State)
if isAttacker {
writeUint32(buf, attackVal.IsCritical)
writeUint32(buf, attackVal.State)
writeUint32(buf, 1)
writeInt32(buf, int32(attackVal.LostHp))
writeInt32(buf, attackVal.GainHp)
return moves
}
func clampLegacyInt32ToUint32(v int32) uint32 {
if v < 0 {
return 0
}
writeUint32(buf, 0)
return uint32(v)
}
func (f *FightC) sendLegacyGroupBoutDone() {
var buf bytes.Buffer
if f == nil || !f.LegacyGroupProtocol {
return
}
writeUint32(&buf, f.Round)
f.Broadcast(func(ff *input.Input) {
if ff == nil || ff.Player == nil {
return
}
sendLegacyPacket(ff.Player, groupCmdBoutDone, buf.Bytes())
payload := legacyBoutDoneInfo{Round: f.Round}
f.BroadcastPlayers(func(p common.PlayerI) {
p.SendPackCmd(groupCmdBoutDone, &payload)
})
}
func (f *FightC) sendLegacySpriteDie(in *input.Input, hasBackup bool) {
var (
buf bytes.Buffer
side uint8
data uint32
)
if f == nil || !f.LegacyGroupProtocol || in == nil {
return
}
side = 1
side := uint8(1)
if !f.isOurPlayerID(in.UserID) {
side = 2
}
var data uint32
if hasBackup {
data = 1
}
writeUint8(&buf, 1)
writeUint8(&buf, side)
writeUint8(&buf, uint8(in.TeamSlotIndex()))
writeUint8(&buf, 1)
writeUint32(&buf, data)
f.Broadcast(func(ff *input.Input) {
if ff == nil || ff.Player == nil {
return
}
sendLegacyPacket(ff.Player, groupCmdSpriteDie, buf.Bytes())
payload := legacySpriteDieInfo{
Count: 1,
Side: side,
ActorIndex: uint8(in.TeamSlotIndex()),
Flag: 1,
HasBackup: data,
}
f.BroadcastPlayers(func(p common.PlayerI) {
p.SendPackCmd(groupCmdSpriteDie, &payload)
})
}
func maxInt32(v int32) int32 {
if v < 0 {
return 0
}
return v
}

View File

@@ -0,0 +1,61 @@
package fight
import (
"testing"
"blazing/logic/service/fight/action"
fightinfo "blazing/logic/service/fight/info"
"blazing/logic/service/fight/input"
"blazing/modules/player/model"
)
func TestSendLegacyRoundBroadcastSkipsUnexecutedAction(t *testing.T) {
ourPlayer := &stubPlayer{info: model.PlayerInfo{UserID: 1001}}
oppPlayer := &stubPlayer{info: model.PlayerInfo{UserID: 2002}}
our := input.NewInput(nil, ourPlayer)
our.InitAttackValue()
our.AttackValue.SkillID = 111
our.SetCurPetAt(0, fightinfo.CreateBattlePetEntity(model.PetInfo{
ID: 11,
Hp: 80,
MaxHp: 100,
CatchTime: 101,
}))
opp := input.NewInput(nil, oppPlayer)
opp.InitAttackValue()
opp.SetCurPetAt(0, fightinfo.CreateBattlePetEntity(model.PetInfo{
ID: 22,
Hp: 0,
MaxHp: 100,
CatchTime: 202,
}))
fc := &FightC{
Our: []*input.Input{our},
Opp: []*input.Input{opp},
LegacyGroupProtocol: true,
}
firstAttack := &action.SelectSkillAction{
BaseAction: action.BaseAction{PlayerID: ourPlayer.info.UserID, ActorIndex: 0, TargetIndex: 0},
}
secondAttack := &action.SelectSkillAction{
BaseAction: action.BaseAction{PlayerID: oppPlayer.info.UserID, ActorIndex: 0, TargetIndex: 0},
}
fc.sendLegacyRoundBroadcast(firstAttack, secondAttack)
for _, player := range []*stubPlayer{ourPlayer, oppPlayer} {
if len(player.sentCmds) != 2 {
t.Fatalf("expected one skill packet plus bout done, got %v", player.sentCmds)
}
if player.sentCmds[0] != groupCmdSkillHurt {
t.Fatalf("expected first packet to be skill hurt, got %d", player.sentCmds[0])
}
if player.sentCmds[1] != groupCmdBoutDone {
t.Fatalf("expected second packet to be bout done, got %d", player.sentCmds[1])
}
}
}

View File

@@ -196,10 +196,8 @@ type PropDict struct {
// NoteUseSkillOutboundInfo 战斗技能使用通知的出站信息结构体
type NoteUseSkillOutboundInfo struct {
FirstAttackInfoLen uint32 `struc:"sizeof=FirstAttackInfo"`
FirstAttackInfo []model.AttackValue // 本轮手方精灵在释放技能结束后的状态
SecondAttackInfoLen uint32 `struc:"sizeof=SecondAttackInfo"`
SecondAttackInfo []model.AttackValue // 本轮后手方精灵在释放技能结束后的状态
FirstAttackInfo model.AttackValue // 本轮先手方精灵在释放技能结束后的状态
SecondAttackInfo model.AttackValue // 本轮手方精灵在释放技能结束后的状态
}
type FightStartOutboundInfo struct {

View File

@@ -39,6 +39,7 @@ type FightC struct {
actionNotify chan struct{}
acceptActions bool
pendingActions []action.BattleActionI // 待处理动作队列,同一战斗位最多保留一个动作
pendingHead int
actionRound atomic.Uint32
quit chan struct{}
@@ -250,20 +251,13 @@ func (f *FightC) sideHasActionableSlots(side int) bool {
}
func (f *FightC) slotNeedsAction(in *input.Input) bool {
var bench []*info.BattlePetEntity
if in == nil {
return false
}
if current := in.CurrentPet(); current != nil && current.Info.Hp > 0 {
return true
}
bench = in.BenchPets()
for _, pet := range bench {
if pet != nil && pet.Info.Hp > 0 {
return true
}
}
return false
return in.HasLivingBench()
}
func (f *FightC) setActionAttackValue(act action.BattleActionI) {
@@ -276,6 +270,9 @@ func (f *FightC) setActionAttackValue(act action.BattleActionI) {
}
attacker.AttackValue.ActorIndex = uint32(act.GetActorIndex())
targetIndex, _ := DecodeTargetIndex(act.GetTargetIndex())
if _, resolvedIndex, ok := f.resolveActionTarget(act); ok && resolvedIndex >= 0 {
targetIndex = resolvedIndex
}
attacker.AttackValue.TargetIndex = uint32(targetIndex)
}
@@ -316,6 +313,25 @@ func (f *FightC) GetInputByPlayerAt(c common.PlayerI, actorIndex int, isOpposite
return f.getInputByUserID(c.GetInfo().UserID, actorIndex, isOpposite)
}
func (f *FightC) resolveActionTarget(c action.BattleActionI) (*input.Input, int, bool) {
if c == nil {
return nil, -1, false
}
attacker := f.getInputByUserID(c.GetPlayerID(), c.GetActorIndex(), false)
if attacker == nil {
return nil, -1, false
}
encodedTargetIndex := c.GetTargetIndex()
targetIndex, targetIsOpposite := DecodeTargetIndex(encodedTargetIndex)
if !targetIsOpposite {
return attacker.TeamSlotAt(targetIndex), targetIndex, false
}
if target, resolvedIndex := attacker.OpponentSlotAtOrNextLiving(targetIndex); target != nil {
return target, resolvedIndex, true
}
return attacker.OpponentSlotAt(targetIndex), targetIndex, true
}
func (f *FightC) GetInputByAction(c action.BattleActionI, isOpposite bool) *input.Input {
if c == nil {
if isOpposite {
@@ -327,8 +343,8 @@ func (f *FightC) GetInputByAction(c action.BattleActionI, isOpposite bool) *inpu
if !isOpposite {
return f.getInputByUserID(c.GetPlayerID(), index, false)
}
targetIndex, targetIsOpposite := DecodeTargetIndex(c.GetTargetIndex())
return f.getInputByUserID(c.GetPlayerID(), targetIndex, targetIsOpposite)
target, _, _ := f.resolveActionTarget(c)
return target
}
// 玩家使用技能
@@ -377,7 +393,7 @@ func (f *FightC) GetRound() uint32 {
}
func (f *FightC) Chat(c common.PlayerI, msg string) {
f.GetInputByPlayer(c, true).Player.SendPackCmd(50002, &user.ChatOutboundInfo{
f.sendFightPacket(f.GetInputByPlayer(c, true).Player, fightPacketChat, &user.ChatOutboundInfo{
SenderId: c.GetInfo().UserID,
SenderNickname: c.GetInfo().Nick,
Message: utils.RemoveLast(msg),
@@ -392,7 +408,7 @@ func (f *FightC) LoadPercent(c common.PlayerI, percent int32) {
if f.Info.Status == info.BattleMode.FIGHT_WITH_NPC {
return
}
f.GetInputByPlayer(c, true).Player.SendPackCmd(2441, &info.LoadPercentOutboundInfo{
f.sendFightPacket(f.GetInputByPlayer(c, true).Player, fightPacketLoadPercentNotice, &info.LoadPercentOutboundInfo{
Id: c.GetInfo().UserID,
Percent: uint32(percent),
})
@@ -506,6 +522,27 @@ func (f *FightC) Broadcast(t func(ff *input.Input)) {
}
func (f *FightC) BroadcastPlayers(t func(common.PlayerI)) {
if f == nil || t == nil {
return
}
seen := make(map[uint32]struct{}, len(f.OurPlayers)+len(f.OppPlayers))
visit := func(players []common.PlayerI) {
for _, p := range players {
if p == nil || p.GetInfo() == nil {
continue
}
if _, ok := seen[p.GetInfo().UserID]; ok {
continue
}
seen[p.GetInfo().UserID] = struct{}{}
t(p)
}
}
visit(f.OurPlayers)
visit(f.OppPlayers)
}
func (f *FightC) GetOverChan() chan struct{} {
return f.over

View File

@@ -32,6 +32,7 @@ var EffectType = enum.New[struct {
}]()
var NodeM = make(map[int64]Effect, 0)
var NodeFactoryM = make(map[int64]func() Effect, 0)
func InitEffect(etype EnumEffectType, id int, t Effect) {
pr := EffectIDCombiner{}
@@ -41,6 +42,13 @@ func InitEffect(etype EnumEffectType, id int, t Effect) {
NodeM[pr.EffectID()] = t
}
func InitEffectFactory(etype EnumEffectType, id int, factory func() Effect) {
pr := EffectIDCombiner{}
pr.Combine(etype, 0, gconv.Uint16(id))
NodeFactoryM[pr.EffectID()] = factory
}
func GeteffectIDs(etype EnumEffectType) []uint32 {
var ret []uint32 = make([]uint32, 0)
@@ -60,6 +68,19 @@ func geteffect[T int | byte | uint16](etype EnumEffectType, id T) Effect {
pr := EffectIDCombiner{}
pr.Combine(etype, 0, gconv.Uint16(id))
if factory, ok := NodeFactoryM[pr.EffectID()]; ok {
eff := factory()
if eff == nil {
return nil
}
eff.ID(pr)
if etype == EffectType.Status {
eff.CanStack(true)
eff.Duration(grand.N(1, 2))
}
return eff
}
//todo 获取前GetEffect
ret, ok := NodeM[pr.EffectID()]
if ok {
@@ -107,18 +128,14 @@ func (our *Input) GetProp(id int) alpacadecimal.Decimal {
}
func (our *Input) GetEffect(etype EnumEffectType, id int) Effect {
var ret []Effect
pr := EffectIDCombiner{}
pr.Combine(etype, 0, gconv.Uint16(id))
for _, v := range our.Effects {
if v.ID().Base == pr.Base && v.Alive() {
ret = append(ret, v)
our.ensureEffectIndex()
bucket := our.effectsByBase[pr.Base]
for i := len(bucket) - 1; i >= 0; i-- {
if bucket[i] != nil && bucket[i].Alive() {
return bucket[i]
}
}
if len(ret) > 0 {
return ret[len(ret)-1]
}
return nil
}
@@ -178,6 +195,7 @@ func (our *Input) AddEffect(in *Input, e Effect) Effect {
if e == nil {
return nil
}
our.ensureEffectIndex()
ctx := e.Ctx()
if ctx != nil {
if ctx.Source == nil {
@@ -204,7 +222,7 @@ func (our *Input) AddEffect(in *Input, e Effect) Effect {
//TODO 先激活
//fmt.Println("产生回合数", e.ID(), e.Duration())
// 如果已有同 ID 的效果,尝试叠加
for _, v := range our.Effects {
for _, v := range our.effectsByBase[e.ID().Base] {
if v == e {
return nil //完全相同,跳过执行
}
@@ -219,7 +237,7 @@ func (our *Input) AddEffect(in *Input, e Effect) Effect {
if !v.CanStack() { //说明进行了替换
v.Alive(false) //不允许叠层,取消效果
e.Duration(utils.Max(e.Duration(), v.Duration()))
our.Effects = append(our.Effects, e)
our.appendEffect(e)
return v //这里把V替换掉了
} else {
//默认给叠一层
@@ -237,7 +255,7 @@ func (our *Input) AddEffect(in *Input, e Effect) Effect {
}
//无限叠加比如能力提升类buff
// 如果没有同 ID 的效果,直接添加
our.Effects = append(our.Effects, e)
our.appendEffect(e)
return nil
}
@@ -309,6 +327,34 @@ func (our *Input) CancelTurn(in *Input) {
}
func (our *Input) ensureEffectIndex() {
if our == nil {
return
}
if our.effectsByBase == nil {
our.effectsByBase = make(map[int64][]Effect, len(our.Effects))
}
if our.indexedEffects > len(our.Effects) {
our.effectsByBase = make(map[int64][]Effect, len(our.Effects))
our.indexedEffects = 0
}
for our.indexedEffects < len(our.Effects) {
effect := our.Effects[our.indexedEffects]
if effect != nil {
our.effectsByBase[effect.ID().Base] = append(our.effectsByBase[effect.ID().Base], effect)
}
our.indexedEffects++
}
}
func (our *Input) appendEffect(effect Effect) {
if our == nil || effect == nil {
return
}
our.Effects = append(our.Effects, effect)
our.ensureEffectIndex()
}
// // 消除全部 断回合效果,但是我放下场的时候应该断掉所有的回合类效果
// func (our *Input) CancelAll() {
// our.Effects = make([]Effect, 0)

View File

@@ -15,6 +15,13 @@ import (
"github.com/jinzhu/copier"
)
var statusBonuses = map[info.EnumPetStatus]float64{
info.PetStatus.Paralysis: 1.5,
info.PetStatus.Poisoned: 1.5,
info.PetStatus.Sleep: 2.0,
// /info.BattleStatus.Frozen: 2.0,
}
type Input struct {
CanChange uint32 //是否可以死亡切换CanChange
// CanAction bool //是否可以行动
@@ -27,9 +34,11 @@ type Input struct {
CanCapture int
Finished bool //是否加载完成
// info.BattleActionI
Effects []Effect //effects 实际上全局就是effect无限回合 //effects容器 技能的
EffectCache []Effect //这里是命中前执行的容器,也就是命中前执行的所有逻辑相关,理论上一个effect被激活,就应该同时将其他的effect取消激活
EffectLost []Effect
Effects []Effect //effects 实际上全局就是effect无限回合 //effects容器 技能的
EffectCache []Effect //这里是命中前执行的容器,也就是命中前执行的所有逻辑相关,理论上一个effect被激活,就应该同时将其他的effect取消激活
EffectLost []Effect
effectsByBase map[int64][]Effect
indexedEffects int
// 删掉伤害记录,可以在回调中记录,而不是每次调用记录
*model.AttackValue
FightC common.FightI
@@ -57,6 +66,7 @@ func NewInput(c common.FightI, p common.PlayerI) *Input {
ret := &Input{FightC: c, Player: p}
ret.Effects = make([]Effect, 0)
ret.CurPet = make([]*info.BattlePetEntity, 0)
ret.effectsByBase = make(map[int64][]Effect)
// t := Geteffect(EffectType.Damage, 0)
// t.Effect.SetArgs(ret)
@@ -115,6 +125,27 @@ func (our *Input) BenchPets() []*info.BattlePetEntity {
return bench
}
func (our *Input) HasLivingBench() bool {
if our == nil {
return false
}
current := our.CurrentPet()
currentCatchTime := uint32(0)
if current != nil {
currentCatchTime = current.Info.CatchTime
}
for _, pet := range our.AllPet {
if pet == nil || pet.Info.Hp == 0 {
continue
}
if current != nil && pet.Info.CatchTime == currentCatchTime {
continue
}
return true
}
return false
}
func (our *Input) OpponentSlots() []*Input {
if our == nil {
return nil
@@ -299,20 +330,12 @@ func (our *Input) GetPet(id uint32) (ii *info.BattlePetEntity, Reason info.Chang
// GetStatusBonus 获取最高的状态倍率
// 遍历状态数组返回存在的状态中最高的倍率无状态则返回1.0
func (our *Input) GetStatusBonus() float64 {
// 异常状态倍率映射表(状态索引 -> 倍率)
var statusBonuses = map[info.EnumPetStatus]float64{
info.PetStatus.Paralysis: 1.5,
info.PetStatus.Poisoned: 1.5,
info.PetStatus.Sleep: 2.0,
// /info.BattleStatus.Frozen: 2.0,
}
maxBonus := 1.0 // 默认无状态倍率
for statusIdx := 0; statusIdx < 20; statusIdx++ {
t := our.InitEffect(EffectType.Status, statusIdx)
t := our.GetEffect(EffectType.Status, statusIdx)
// 检查状态是否存在数组中值为1表示存在该状态
if t != nil && t.Stack() > 0 {
if t != nil && t.Alive() {
if bonus, exists := statusBonuses[info.EnumPetStatus(statusIdx)]; exists && bonus > maxBonus {
maxBonus = bonus
}

View File

@@ -2,6 +2,40 @@ package input
import "github.com/gogf/gf/v2/util/grand"
func compactSlots(slots []*Input) []*Input {
if len(slots) == 0 {
return nil
}
ret := make([]*Input, 0, len(slots))
for _, slot := range slots {
if slot == nil {
continue
}
ret = append(ret, slot)
}
return ret
}
func uniqueControllerCount(slots []*Input) int {
if len(slots) == 0 {
return 0
}
seen := make(map[uint32]struct{}, len(slots))
count := 0
for _, slot := range slots {
if slot == nil || slot.Player == nil {
continue
}
userID := slot.Player.GetInfo().UserID
if _, ok := seen[userID]; ok {
continue
}
seen[userID] = struct{}{}
count++
}
return count
}
// TeamSlots 返回当前输入所在阵营的全部有效战斗位视图。
func (our *Input) TeamSlots() []*Input {
if our == nil {
@@ -10,14 +44,7 @@ func (our *Input) TeamSlots() []*Input {
if len(our.Team) == 0 {
return []*Input{our}
}
slots := make([]*Input, 0, len(our.Team))
for _, teammate := range our.Team {
if teammate == nil {
continue
}
slots = append(slots, teammate)
}
return slots
return compactSlots(our.Team)
}
// TeamSlotIndex 返回当前输入在本阵营中的原始站位下标。
@@ -69,6 +96,65 @@ func (our *Input) HasLivingTeammate() bool {
return len(our.LivingTeammates()) > 0
}
// TeamInputCount 返回当前阵营有效 input 数量。
func (our *Input) TeamInputCount() int {
return len(our.TeamSlots())
}
// OpponentInputCount 返回敌方阵营有效 input 数量。
func (our *Input) OpponentInputCount() int {
if our == nil {
return 0
}
if len(our.OppTeam) == 0 {
if our.Opp != nil {
return 1
}
return 0
}
return len(compactSlots(our.OppTeam))
}
// IsMultiInputSide 判断当前阵营是否为多 input。
func (our *Input) IsMultiInputSide() bool {
return our.TeamInputCount() > 1
}
// IsMultiInputBattle 判断当前战斗是否包含多 input 站位。
func (our *Input) IsMultiInputBattle() bool {
if our == nil {
return false
}
return our.TeamInputCount() > 1 || our.OpponentInputCount() > 1
}
// TeamControllerCount 返回当前阵营实际操作者数量。
func (our *Input) TeamControllerCount() int {
if our == nil {
return 0
}
return uniqueControllerCount(our.TeamSlots())
}
// IsSingleControllerMultiInputSide 判断当前阵营是否为“单人控制的多 input”。
func (our *Input) IsSingleControllerMultiInputSide() bool {
return our.IsMultiInputSide() && our.TeamControllerCount() <= 1
}
// TeamSlotAt 返回指定己方站位。
func (our *Input) TeamSlotAt(index int) *Input {
if our == nil {
return nil
}
if index >= 0 && index < len(our.Team) {
return our.Team[index]
}
if index == 0 {
return our
}
return nil
}
// OpponentSlotAt 返回指定敌方站位。
func (our *Input) OpponentSlotAt(index int) *Input {
if our == nil {
@@ -83,6 +169,50 @@ func (our *Input) OpponentSlotAt(index int) *Input {
return nil
}
func nextLivingSlotIndex(slots []*Input, start int) int {
if len(slots) == 0 {
return -1
}
if start < 0 {
start = 0
}
if start >= len(slots) {
start = 0
}
availableIndex := -1
for offset := 0; offset < len(slots); offset++ {
idx := (start + offset) % len(slots)
slot := slots[idx]
if slot == nil {
continue
}
if availableIndex < 0 {
availableIndex = idx
}
current := slot.CurrentPet()
if current != nil && current.Info.Hp > 0 {
return idx
}
}
return availableIndex
}
// OpponentSlotAtOrNextLiving 返回指定敌方站位;若该目标已死亡,则顺延到下一只存活精灵。
func (our *Input) OpponentSlotAtOrNextLiving(index int) (*Input, int) {
if our == nil {
return nil, -1
}
if len(our.OppTeam) == 0 {
return our.OpponentSlotAt(index), index
}
resolvedIndex := nextLivingSlotIndex(our.OppTeam, index)
if resolvedIndex < 0 {
return nil, -1
}
return our.OppTeam[resolvedIndex], resolvedIndex
}
// RandomOpponentSlotIndex 返回一个可用的敌方站位下标,优先从存活站位中随机。
func (our *Input) RandomOpponentSlotIndex() int {
if our == nil {

View File

@@ -3,10 +3,32 @@ package input
import (
"testing"
"blazing/common/socket/errorcode"
"blazing/logic/service/common"
fightinfo "blazing/logic/service/fight/info"
spaceinfo "blazing/logic/service/space/info"
"blazing/modules/player/model"
)
type teamTestPlayer struct {
info model.PlayerInfo
fightInfo fightinfo.Fightinfo
}
func (p *teamTestPlayer) ApplyPetDisplayInfo(*spaceinfo.SimpleInfo) {}
func (p *teamTestPlayer) GetPlayerCaptureContext() *fightinfo.PlayerCaptureContext { return nil }
func (p *teamTestPlayer) Roll(int, int) (bool, float64, float64) { return false, 0, 0 }
func (p *teamTestPlayer) Getfightinfo() fightinfo.Fightinfo { return p.fightInfo }
func (p *teamTestPlayer) ItemAdd(int64, int64) bool { return false }
func (p *teamTestPlayer) GetInfo() *model.PlayerInfo { return &p.info }
func (p *teamTestPlayer) InvitePlayer(common.PlayerI) {}
func (p *teamTestPlayer) SetFightC(common.FightI) {}
func (p *teamTestPlayer) QuitFight() {}
func (p *teamTestPlayer) MessWin(bool) {}
func (p *teamTestPlayer) CanFight() errorcode.ErrorCode { return 0 }
func (p *teamTestPlayer) SendPackCmd(uint32, any) {}
func (p *teamTestPlayer) GetPetInfo(uint32) []model.PetInfo { return nil }
func TestLivingTeammatesFiltersSelfAndDeadSlots(t *testing.T) {
owner := &Input{CurPet: []*fightinfo.BattlePetEntity{{Info: model.PetInfo{Hp: 10}}}}
aliveMate := &Input{CurPet: []*fightinfo.BattlePetEntity{{Info: model.PetInfo{Hp: 5}}}}
@@ -58,3 +80,38 @@ func TestRandomOpponentSlotIndexPrefersLivingTarget(t *testing.T) {
t.Fatalf("expected opponent slot 1 to return live opponent")
}
}
func TestInputModeHelpers(t *testing.T) {
playerA := &teamTestPlayer{info: model.PlayerInfo{UserID: 1001}}
playerB := &teamTestPlayer{info: model.PlayerInfo{UserID: 1002}}
solo := &Input{}
soloMate := &Input{}
groupMate := &Input{}
solo.Player = playerA
soloMate.Player = playerA
groupMate.Player = playerB
solo.Team = []*Input{solo}
solo.OppTeam = []*Input{{}}
if solo.IsMultiInputBattle() {
t.Fatalf("expected single-input battle to be false")
}
solo.Team = []*Input{solo, soloMate}
if !solo.IsMultiInputBattle() {
t.Fatalf("expected multi-input battle to be true")
}
if !solo.IsSingleControllerMultiInputSide() {
t.Fatalf("expected same-controller double side to be single-controller multi-input")
}
solo.Team = []*Input{solo, groupMate}
if solo.TeamControllerCount() != 2 {
t.Fatalf("expected group side controller count 2, got %d", solo.TeamControllerCount())
}
if solo.IsSingleControllerMultiInputSide() {
t.Fatalf("expected grouped side not to be single-controller multi-input")
}
}

View File

@@ -76,7 +76,7 @@ func (f *FightC) battleLoop() {
if player := f.primaryOppPlayer(); player != nil {
f.WinnerId = player.GetInfo().UserID
}
f.Reason = model.BattleOverReason.DefaultEnd
f.Reason = normalizeFightOverReason(model.BattleOverReason.DefaultEnd)
f.FightOverInfo.WinnerId = f.WinnerId
f.FightOverInfo.Reason = f.Reason
f.closefight = true
@@ -86,7 +86,7 @@ func (f *FightC) battleLoop() {
if player := f.primaryOurPlayer(); player != nil {
f.WinnerId = player.GetInfo().UserID
}
f.Reason = model.BattleOverReason.DefaultEnd
f.Reason = normalizeFightOverReason(model.BattleOverReason.DefaultEnd)
f.FightOverInfo.WinnerId = f.WinnerId
f.FightOverInfo.Reason = f.Reason
f.closefight = true
@@ -173,14 +173,14 @@ func (f *FightC) battleLoop() {
//大乱斗,给个延迟
//<-time.After(1000)
f.Broadcast(func(ff *input.Input) {
f.BroadcastPlayers(func(p common.PlayerI) {
if f.LegacyGroupProtocol {
f.sendLegacyGroupOver(ff.Player, &f.FightOverInfo)
f.sendLegacyGroupOver(p, &f.FightOverInfo)
} else {
ff.Player.SendPackCmd(2506, &f.FightOverInfo)
f.sendFightPacket(p, fightPacketOver, buildFightOverPayload(f.FightOverInfo))
}
ff.Player.QuitFight()
p.QuitFight()
//待退出玩家战斗状态
})
@@ -264,7 +264,11 @@ func (f *FightC) collectPlayerActions(expectedSlots map[actionSlotKey]struct{})
ret.Reason = reason
ret.Reason.ActorIndex = uint32(ret.ActorIndex)
selfinput.Player.SendPackCmd(2407, &ret.Reason)
if f.LegacyGroupProtocol {
f.sendLegacyGroupChangePetSuccess(selfinput.Player, selfinput, &ret.Reason)
} else {
f.sendFightPacket(selfinput.Player, fightPacketChangePetSuccess, &ret.Reason)
}
f.Switch[key] = ret
@@ -285,7 +289,11 @@ func (f *FightC) collectPlayerActions(expectedSlots map[actionSlotKey]struct{})
selfinput.CanChange = 0
if f.Info.Status == info.BattleMode.FIGHT_WITH_NPC && paction.GetPlayerID() == 0 {
f.Switch = make(map[actionSlotKey]*action.ActiveSwitchAction)
f.Our[0].Player.SendPackCmd(2407, &ret.Reason)
if f.LegacyGroupProtocol {
f.sendLegacyGroupChangePetSuccess(f.Our[0].Player, selfinput, &ret.Reason)
} else {
f.sendFightPacket(f.Our[0].Player, fightPacketChangePetSuccess, &ret.Reason)
}
//println("AI出手死切")
f.triggerNPCActions() // boss出手后获取出招
@@ -601,12 +609,12 @@ func (f *FightC) handleItemAction(a *action.UseItemAction) {
case gconv.Int(item.HP) != 0:
addhp := item.HP
source.Heal(source, a, alpacadecimal.NewFromInt(int64(addhp)))
f.Broadcast(func(ff *input.Input) {
f.BroadcastPlayers(func(p common.PlayerI) {
currentPet := source.PrimaryCurPet()
if currentPet == nil {
return
}
ff.Player.SendPackCmd(2406, &info.UsePetIteminfo{
f.sendFightPacket(p, fightPacketUseItem, &info.UsePetIteminfo{
UserID: source.UserID,
ChangeHp: int32(addhp),
ItemID: uint32(item.ID),
@@ -616,12 +624,12 @@ func (f *FightC) handleItemAction(a *action.UseItemAction) {
})
case gconv.Int(item.PP) != 0:
source.HealPP(item.PP)
f.Broadcast(func(ff *input.Input) {
f.BroadcastPlayers(func(p common.PlayerI) {
currentPet := source.PrimaryCurPet()
if currentPet == nil {
return
}
ff.Player.SendPackCmd(2406, &info.UsePetIteminfo{
f.sendFightPacket(p, fightPacketUseItem, &info.UsePetIteminfo{
UserID: source.UserID,
ItemID: uint32(item.ID),

View File

@@ -47,6 +47,100 @@ func NewFightSingleControllerN(
)
}
// ArrangePetsBySlotLimit 按站位上限切分宠物。
// 规则:
// 1. 前 slotLimit 只存活宠物优先占据出战位。
// 2. 其余宠物按 1..slotLimit 轮转挂到对应站位作为后备。
// 3. 每个站位最多保留 6 只宠物。
func ArrangePetsBySlotLimit(pets []model.PetInfo, slotLimit int) [][]model.PetInfo {
var (
alivePets []model.PetInfo
slots [][]model.PetInfo
idx int
)
for _, pet := range pets {
if pet.Hp == 0 {
continue
}
alivePets = append(alivePets, pet)
}
if len(alivePets) == 0 {
return nil
}
if slotLimit <= 0 {
slotLimit = 1
}
if slotLimit > len(alivePets) {
slotLimit = len(alivePets)
}
slots = make([][]model.PetInfo, 0, slotLimit)
for i := 0; i < slotLimit; i++ {
slots = append(slots, []model.PetInfo{alivePets[i]})
}
for _, pet := range alivePets[slotLimit:] {
for step := 0; step < len(slots); step++ {
slotIdx := (idx + step) % len(slots)
if len(slots[slotIdx]) >= 6 {
continue
}
slots[slotIdx] = append(slots[slotIdx], pet)
idx = (slotIdx + 1) % len(slots)
break
}
}
return slots
}
// ExpandPlayersWithSlotLimit 将“每位玩家的宠物列表”按站位限制展开为扁平站位列表。
// 例如:
// 1. slotLimit=1: 每位玩家占 1 个站位,其余为该站位后备。
// 2. slotLimit=3: 每位玩家最多展开 3 个站位,每个站位带各自后备。
func ExpandPlayersWithSlotLimit(
players []common.PlayerI,
petsByPlayer [][]model.PetInfo,
slotLimit int,
) ([]common.PlayerI, [][]model.PetInfo, errorcode.ErrorCode) {
var (
flatPlayers []common.PlayerI
flatSlots [][]model.PetInfo
)
if len(players) == 0 || len(players) != len(petsByPlayer) {
return nil, nil, errorcode.ErrorCodes.ErrSystemBusyTryLater
}
for idx, p := range players {
if p == nil {
return nil, nil, errorcode.ErrorCodes.ErrSystemBusyTryLater
}
slots := ArrangePetsBySlotLimit(petsByPlayer[idx], slotLimit)
for _, slotPets := range slots {
flatPlayers = append(flatPlayers, p)
flatSlots = append(flatSlots, slotPets)
}
}
if len(flatPlayers) == 0 || len(flatPlayers) != len(flatSlots) {
return nil, nil, errorcode.ErrorCodes.ErrSystemBusyTryLater
}
return flatPlayers, flatSlots, 0
}
// NewFightSingleController 使用站位限制规则创建单人控制多站位战斗。
func NewFightSingleController(
ourController common.PlayerI,
oppController common.PlayerI,
ourPets []model.PetInfo,
oppPets []model.PetInfo,
slotLimit int,
fn func(model.FightOverInfo),
) (*FightC, errorcode.ErrorCode) {
return NewFightSingleControllerN(
ourController,
oppController,
ArrangePetsBySlotLimit(ourPets, slotLimit),
ArrangePetsBySlotLimit(oppPets, slotLimit),
fn,
)
}
// NewLegacyGroupFightSingleControllerN 创建旧组队协议的单人控制多站位战斗。
func NewLegacyGroupFightSingleControllerN(
ourController common.PlayerI,
@@ -82,6 +176,24 @@ func NewLegacyGroupFightSingleControllerN(
)
}
// NewLegacyGroupFightSingleController 使用站位限制规则创建旧组队协议战斗。
func NewLegacyGroupFightSingleController(
ourController common.PlayerI,
oppController common.PlayerI,
ourPets []model.PetInfo,
oppPets []model.PetInfo,
slotLimit int,
fn func(model.FightOverInfo),
) (*FightC, errorcode.ErrorCode) {
return NewLegacyGroupFightSingleControllerN(
ourController,
oppController,
ArrangePetsBySlotLimit(ourPets, slotLimit),
ArrangePetsBySlotLimit(oppPets, slotLimit),
fn,
)
}
// NewFightPerSlotControllerN 创建 N 打战斗(多人各控制一个站位)。
// ourPlayers/oppPlayers 与 ourPetsBySlot/oppPetsBySlot 按站位一一对应。
func NewFightPerSlotControllerN(
@@ -117,25 +229,32 @@ func NewFightPerSlotControllerN(
)
}
// NewFightPerPlayerControllers 使用“每位玩家 + 站位限制”创建多人战斗。
func NewFightPerPlayerControllers(
ourPlayers []common.PlayerI,
oppPlayers []common.PlayerI,
ourPetsByPlayer [][]model.PetInfo,
oppPetsByPlayer [][]model.PetInfo,
slotLimit int,
fn func(model.FightOverInfo),
) (*FightC, errorcode.ErrorCode) {
flatOurPlayers, flatOurSlots, err := ExpandPlayersWithSlotLimit(ourPlayers, ourPetsByPlayer, slotLimit)
if err > 0 {
return nil, err
}
flatOppPlayers, flatOppSlots, err := ExpandPlayersWithSlotLimit(oppPlayers, oppPetsByPlayer, slotLimit)
if err > 0 {
return nil, err
}
return NewFightPerSlotControllerN(flatOurPlayers, flatOppPlayers, flatOurSlots, flatOppSlots, fn)
}
// 创建新战斗,邀请方和被邀请方,或者玩家和野怪方
func NewFight(p1, p2 common.PlayerI, b1, b2 []model.PetInfo, fn func(model.FightOverInfo)) (*FightC, errorcode.ErrorCode) {
if p1 == nil || p2 == nil {
return nil, errorcode.ErrorCodes.ErrSystemBusyTryLater
}
fightInfo := p1.Getfightinfo()
ourInput, err := buildInputFromPets(p1, b1, fightInfo.Mode)
if err > 0 {
return nil, err
}
oppInput, err := buildInputFromPets(p2, b2, fightInfo.Mode)
if err > 0 {
return nil, err
}
return NewFightWithOptions(
WithFightInputs([]*input.Input{ourInput}, []*input.Input{oppInput}),
WithFightCallback(fn),
WithFightInfo(fightInfo),
)
return NewFightSingleController(p1, p2, b1, b2, 1, fn)
}
// buildFight 基于已准备好的双方 Inputs 构建战斗实例。
@@ -178,9 +297,6 @@ func buildFight(opts *fightBuildOptions) (*FightC, errorcode.ErrorCode) {
f.bindInputFightContext(f.Our, f.Opp)
f.linkTeamViews()
f.ReadyInfo.OurInfo, f.ReadyInfo.OurPetList = initfightready(f.primaryOur())
f.ReadyInfo.OpponentInfo, f.ReadyInfo.OpponentPetList = initfightready(f.primaryOpp())
loadtime := 120 * time.Second
if f.Info.Status == info.BattleMode.FIGHT_WITH_NPC {
if opp := f.primaryOpp(); opp != nil {
@@ -194,15 +310,18 @@ func buildFight(opts *fightBuildOptions) (*FightC, errorcode.ErrorCode) {
}
}
}
f.ReadyInfo.OurInfo, f.ReadyInfo.OurPetList = initfightready(f.primaryOur())
f.ReadyInfo.OpponentInfo, f.ReadyInfo.OpponentPetList = initfightready(f.primaryOpp())
f.FightStartOutboundInfo = f.buildFightStartInfo()
if f.LegacyGroupProtocol {
f.sendLegacyGroupReady()
} else {
f.Broadcast(func(ff *input.Input) {
ff.Player.SendPackCmd(2503, &f.ReadyInfo)
})
}
f.BroadcastPlayers(func(p common.PlayerI) {
if f.LegacyGroupProtocol {
f.sendLegacyGroupReady(p)
return
}
f.sendFightPacket(p, fightPacketReady, &f.ReadyInfo)
})
cool.Cron.AfterFunc(loadtime, func() {
our := f.primaryOur()
@@ -219,13 +338,13 @@ func buildFight(opts *fightBuildOptions) (*FightC, errorcode.ErrorCode) {
case !our.Finished:
f.WinnerId = opp.Player.GetInfo().UserID
}
f.Broadcast(func(ff *input.Input) {
f.BroadcastPlayers(func(p common.PlayerI) {
if f.LegacyGroupProtocol {
f.sendLegacyGroupOver(ff.Player, &f.FightOverInfo)
f.sendLegacyGroupOver(p, &f.FightOverInfo)
} else {
ff.Player.SendPackCmd(2506, &f.FightOverInfo)
f.sendFightPacket(p, fightPacketOver, buildFightOverPayload(f.FightOverInfo))
}
ff.Player.QuitFight()
p.QuitFight()
})
}
})

View File

@@ -24,6 +24,7 @@ type EffectNode struct {
canStack bool // 最大叠加层数 ,正常都是不允许叠加的,除了衰弱特殊效果 ,异常和能力的叠层
isFirst bool
SideEffectArgs []int // 附加效果参数
cachedArgs []alpacadecimal.Decimal
// owner bool //是否作用自身
Success bool // 是否执行成功 成功XXX失败XXX
arget bool // 传出作用对象,默认0是自身,1是作用于对面
@@ -240,18 +241,22 @@ func (e *EffectNode) SetArgs(t *input.Input, a ...int) {
e.Input = t
if len(a) > 0 {
e.SideEffectArgs = a
e.cachedArgs = e.cachedArgs[:0]
for _, v := range a {
e.cachedArgs = append(e.cachedArgs, alpacadecimal.NewFromInt(int64(v)))
}
}
}
func (e *EffectNode) Args() []alpacadecimal.Decimal {
var ret []alpacadecimal.Decimal
for _, v := range e.SideEffectArgs {
ret = append(ret, alpacadecimal.NewFromInt(int64(v)))
if len(e.cachedArgs) == len(e.SideEffectArgs) {
return e.cachedArgs
}
return ret
e.cachedArgs = e.cachedArgs[:0]
for _, v := range e.SideEffectArgs {
e.cachedArgs = append(e.cachedArgs, alpacadecimal.NewFromInt(int64(v)))
}
return e.cachedArgs
}

View File

@@ -92,28 +92,8 @@ func JoinPeakQueue(p *player.Player, requestedMode uint32) errorcode.ErrorCode {
return err
}
m := Default()
runtimeServerID := localRuntimeServerID()
ticket := &localQueueTicket{
playerID: p.Info.UserID,
runtimeServerID: runtimeServerID,
fightMode: fightMode,
status: status,
stop: make(chan struct{}),
}
m.mu.Lock()
if old := m.localQueues[p.Info.UserID]; old != nil {
old.Stop()
}
m.localQueues[p.Info.UserID] = ticket
delete(m.userSession, p.Info.UserID)
m.mu.Unlock()
p.Fightinfo.Mode = fightMode
p.Fightinfo.Status = status
go m.queueHeartbeatLoop(p, ticket)
return 0
}
@@ -129,15 +109,19 @@ func CancelPeakQueue(p *player.Player) {
m.mu.Unlock()
if ticket != nil {
ticket.Stop()
_ = publishServerMessage(pvpwire.CoordinatorTopicPrefix, pvpwire.MessageTypeQueueCancel, pvpwire.QueueCancelPayload{
RuntimeServerID: ticket.runtimeServerID,
UserID: ticket.playerID,
})
}
atomic.StoreUint32(&p.Fightinfo.Mode, 0)
atomic.StoreUint32(&p.Fightinfo.Status, 0)
}
func NormalizePeakMode(requested uint32) (fightMode uint32, status uint32, err errorcode.ErrorCode) {
return normalizePeakMode(requested)
}
func AvailableCatchTimes(pets []model.PetInfo) []uint32 {
return filterAvailableCatchTimes(pets)
}
func SubmitBanPick(p *player.Player, selected, banned []uint32) errorcode.ErrorCode {
if p == nil {
return errorcode.ErrorCodes.ErrSystemBusyTryLater

View File

@@ -0,0 +1,60 @@
package fight
import (
"blazing/logic/service/common"
"blazing/modules/player/model"
"testing"
)
func TestArrangePetsBySlotLimit(t *testing.T) {
pets := []model.PetInfo{
{ID: 1, Hp: 10},
{ID: 2, Hp: 10},
{ID: 3, Hp: 10},
{ID: 4, Hp: 10},
{ID: 5, Hp: 10},
{ID: 6, Hp: 10},
}
slots := ArrangePetsBySlotLimit(pets, 3)
if len(slots) != 3 {
t.Fatalf("expected 3 slots, got %d", len(slots))
}
if len(slots[0]) != 2 || slots[0][0].ID != 1 || slots[0][1].ID != 4 {
t.Fatalf("slot 0 mismatch: %+v", slots[0])
}
if len(slots[1]) != 2 || slots[1][0].ID != 2 || slots[1][1].ID != 5 {
t.Fatalf("slot 1 mismatch: %+v", slots[1])
}
if len(slots[2]) != 2 || slots[2][0].ID != 3 || slots[2][1].ID != 6 {
t.Fatalf("slot 2 mismatch: %+v", slots[2])
}
}
func TestExpandPlayersWithSlotLimit(t *testing.T) {
players := []common.PlayerI{&stubPlayer{}, &stubPlayer{}}
petsByPlayer := [][]model.PetInfo{
{
{ID: 1, Hp: 10},
{ID: 2, Hp: 10},
{ID: 3, Hp: 10},
},
{
{ID: 11, Hp: 10},
{ID: 12, Hp: 10},
},
}
flatPlayers, flatSlots, err := ExpandPlayersWithSlotLimit(players, petsByPlayer, 1)
if err != 0 {
t.Fatalf("unexpected err: %v", err)
}
if len(flatPlayers) != 2 || len(flatSlots) != 2 {
t.Fatalf("unexpected flatten result: players=%d slots=%d", len(flatPlayers), len(flatSlots))
}
if len(flatSlots[0]) != 3 || flatSlots[0][0].ID != 1 || flatSlots[0][1].ID != 2 || flatSlots[0][2].ID != 3 {
t.Fatalf("player0 slot mismatch: %+v", flatSlots[0])
}
if len(flatSlots[1]) != 2 || flatSlots[1][0].ID != 11 || flatSlots[1][1].ID != 12 {
t.Fatalf("player1 slot mismatch: %+v", flatSlots[1])
}
}

View File

@@ -12,7 +12,8 @@ import (
)
type stubPlayer struct {
info model.PlayerInfo
info model.PlayerInfo
sentCmds []uint32
}
func (*stubPlayer) ApplyPetDisplayInfo(*spaceinfo.SimpleInfo) {}
@@ -26,7 +27,7 @@ func (*stubPlayer) SetFightC(common.FightI) {}
func (*stubPlayer) QuitFight() {}
func (*stubPlayer) MessWin(bool) {}
func (*stubPlayer) CanFight() errorcode.ErrorCode { return 0 }
func (*stubPlayer) SendPackCmd(uint32, any) {}
func (p *stubPlayer) SendPackCmd(cmd uint32, _ any) { p.sentCmds = append(p.sentCmds, cmd) }
func (*stubPlayer) GetPetInfo(uint32) []model.PetInfo { return nil }
func TestFightActionEnvelopeEncodedTargetIndex(t *testing.T) {
@@ -111,3 +112,36 @@ func TestBuildFightStateStartEnvelope(t *testing.T) {
t.Fatalf("unexpected right fighter snapshot: %+v", envelope.Right[0])
}
}
func TestBuildNoteUseSkillOutboundInfoUsesActionOrder(t *testing.T) {
ourPlayer := &stubPlayer{info: model.PlayerInfo{UserID: 1001}}
oppPlayer := &stubPlayer{info: model.PlayerInfo{UserID: 2002}}
our := input.NewInput(nil, ourPlayer)
our.InitAttackValue()
our.AttackValue.SkillID = 111
our.AttackValue.RemainHp = 80
our.AttackValue.MaxHp = 100
opp := input.NewInput(nil, oppPlayer)
opp.InitAttackValue()
opp.AttackValue.SkillID = 222
opp.AttackValue.RemainHp = 70
opp.AttackValue.MaxHp = 100
fc := &FightC{
Our: []*input.Input{our},
Opp: []*input.Input{opp},
First: opp,
Second: our,
}
result := fc.buildNoteUseSkillOutboundInfo()
if result.FirstAttackInfo.UserID != 2002 || result.FirstAttackInfo.SkillID != 222 {
t.Fatalf("expected first attack info to belong to acting opponent, got %+v", result.FirstAttackInfo)
}
if result.SecondAttackInfo.UserID != 1001 || result.SecondAttackInfo.SkillID != 111 {
t.Fatalf("expected second attack info to keep the idle side placeholder, got %+v", result.SecondAttackInfo)
}
}

View File

@@ -23,3 +23,9 @@ type C2S_Skill_Sort struct {
Skill [4]uint32 `json:"skill_1"` // 技能1对应C# uint skill_1
}
type CommitPetSkillsInfo struct {
Head common.TomeeHeader `cmd:"52313" struc:"skip"`
CatchTime uint32 `json:"catchTime"`
Skill [4]uint32 `json:"skill"`
}

View File

@@ -51,6 +51,9 @@ func (p *Player) IsMatch(t configmodel.Event) bool {
if len(p.Info.PetList) == 0 {
return false
}
if p.Info.PetList[0].Hp == 0 {
return false
}
firstPetID := int32(p.Info.PetList[0].ID)
_, ok := lo.Find(t.FirstSprites, func(item int32) bool {

View File

@@ -0,0 +1,49 @@
package player
import (
configmodel "blazing/modules/config/model"
playermodel "blazing/modules/player/model"
"testing"
)
func TestIsMatchFirstSpritesRequiresLivingLeadPet(t *testing.T) {
player := &Player{
baseplayer: baseplayer{
Info: &playermodel.PlayerInfo{
PetList: []playermodel.PetInfo{
{ID: 1001, Hp: 0},
{ID: 2002, Hp: 100},
},
},
},
}
event := configmodel.Event{
FirstSprites: []int32{1001},
}
if player.IsMatch(event) {
t.Fatalf("expected dead lead pet to fail FirstSprites match")
}
}
func TestIsMatchFirstSpritesAcceptsLivingLeadPet(t *testing.T) {
player := &Player{
baseplayer: baseplayer{
Info: &playermodel.PlayerInfo{
PetList: []playermodel.PetInfo{
{ID: 1001, Hp: 100},
{ID: 2002, Hp: 100},
},
},
},
}
event := configmodel.Event{
FirstSprites: []int32{1001},
}
if !player.IsMatch(event) {
t.Fatalf("expected living lead pet to pass FirstSprites match")
}
}

View File

@@ -1,8 +1,16 @@
package player
import "blazing/modules/player/model"
type AI_player struct {
baseplayer
CanCapture int
BossScript string
}
func (p *AI_player) GetPetInfo(_ uint32) []model.PetInfo {
ret := make([]model.PetInfo, 0, len(p.Info.PetList))
ret = append(ret, p.Info.PetList...)
return ret
}

View File

@@ -32,17 +32,19 @@ func (p *baseplayer) GetInfo() *model.PlayerInfo {
return p.Info
}
func ApplyPetLevelLimit(pet model.PetInfo, limitlevel uint32) model.PetInfo {
originalHP := pet.Hp
pet.CalculatePetPane(limitlevel)
pet.Hp = utils.Min(originalHP, pet.MaxHp)
return pet
}
func (p *baseplayer) GetPetInfo(limitlevel uint32) []model.PetInfo {
var ret []model.PetInfo
ret := make([]model.PetInfo, 0, len(p.Info.PetList))
for _, pet := range p.Info.PetList {
if limitlevel > 0 {
pet.Level = utils.Min(pet.Level, limitlevel)
}
ret = append(ret, pet)
ret = append(ret, ApplyPetLevelLimit(pet, limitlevel))
}
return ret
}

View File

@@ -1,23 +1,11 @@
package player
import (
"blazing/logic/service/fight/info"
"blazing/logic/service/task"
"blazing/modules/player/model"
"github.com/pointernil/bitset32"
)
// 辅助函数:获取任务奖励,封装逻辑便于复用和统一检查
// 返回nil表示无奖励
func (p *Player) getTaskGift(taskID int, ot int) *task.TaskResult {
// 防御性检查taskID非法时直接返回nil
if taskID <= 0 {
return nil
}
return task.GetTaskInfo(taskID, ot)
}
// SptCompletedTask 完成任务(单分支)
// 优化点:仅当奖励存在时,才完成任务并发放奖励
func (p *Player) SptCompletedTask(taskID int, ot int) {
@@ -29,15 +17,17 @@ func (p *Player) SptCompletedTask(taskID int, ot int) {
return
}
// 2. 核心逻辑:先检查奖励是否存在,无奖励则直接返回(不完成任务)
gift := p.getTaskGift(taskID, ot)
if gift == nil {
if !p.canCompleteTaskReward(taskID, ot) {
return
}
granted, err := p.ApplyTaskCompletion(uint32(taskID), ot, nil)
if err != 0 {
return
}
// 3. 奖励存在时,才标记任务完成 + 发放奖励
p.Info.SetTask(taskID, model.Completed)
p.bossgive(taskID, ot)
p.SendTaskCompletionBonus(uint32(taskID), granted)
}
// TawerCompletedTask 完成塔类任务(多分支)
@@ -48,71 +38,34 @@ func (p *Player) TawerCompletedTask(taskID int, ot int) {
}
// 处理默认分支ot=-1仅奖励存在时才完成主任务
if p.Info.GetTask(taskID) != model.Completed {
defaultGift := p.getTaskGift(taskID, -1)
if defaultGift != nil { // 奖励存在才标记主任务完成
p.Info.SetTask(taskID, model.Completed)
p.bossgive(taskID, -1)
if p.canCompleteTaskReward(taskID, -1) {
granted, err := p.ApplyTaskCompletion(uint32(taskID), -1, nil)
if err == 0 {
p.Info.SetTask(taskID, model.Completed)
p.SendTaskCompletionBonus(uint32(taskID), granted)
}
}
}
// 处理指定分支ot仅奖励存在时才标记分支完成并发奖
p.Service.Task.Exec(uint32(taskID), func(te *model.Task) bool {
// 核心检查:指定分支的奖励是否存在
branchGift := p.getTaskGift(taskID, ot)
if branchGift == nil {
return false
}
// 初始化分支数据
if te.Data == nil {
te.Data = []uint32{}
}
r := bitset32.From(te.Data)
// 分支未完成时,标记完成并发放奖励
if !r.Test(uint(ot)) {
r.Set(uint(ot))
p.bossgive(taskID, ot)
te.Data = r.Bytes()
return true
}
return false
})
}
// bossgive 发放任务奖励(逻辑保持不变,仅补充注释)
func (p *Player) bossgive(taskID int, ot int) {
gift := p.getTaskGift(taskID, ot)
if gift == nil {
taskData, err := p.Service.Task.GetTask(uint32(taskID))
if err != nil {
return
}
res := &info.S2C_GET_BOSS_MONSTER{
BonusID: uint32(taskID),
if !p.canCompleteTaskReward(taskID, ot) {
return
}
// 发放宠物奖励
if gift.Pet != nil {
p.Service.Pet.PetAdd(gift.Pet, 0)
res.PetID = gift.Pet.ID
res.CaptureTm = gift.Pet.CatchTime
}
// 发放道具奖励(仅成功添加的道具才返回给前端)
for _, item := range gift.ItemList {
if success := p.ItemAdd(item.ItemId, item.ItemCnt); success {
res.AddItemInfo(item)
r := bitset32.From(taskData.Data)
if !r.Test(uint(ot)) {
r.Set(uint(ot))
granted, rewardErr := p.ApplyTaskCompletion(uint32(taskID), ot, nil)
if rewardErr != 0 {
return
}
}
// 发放称号奖励
if gift.Title != 0 {
p.GiveTitle(gift.Title)
}
// 发送奖励通知给前端
if res.HasReward() {
p.SendPackCmd(8004, res)
p.SendTaskCompletionBonus(uint32(taskID), granted)
taskData.Data = r.Bytes()
_ = p.Service.Task.SetTask(taskData)
}
}

View File

@@ -17,7 +17,6 @@ func (player *Player) WarehousePetList() []model.PetInfo {
return make([]model.PetInfo, 0)
}
result := make([]model.PetInfo, 0, len(allPets))
return result
@@ -25,32 +24,35 @@ func (player *Player) WarehousePetList() []model.PetInfo {
// AddPetExp 添加宠物经验
func (p *Player) AddPetExp(petInfo *model.PetInfo, addExp int64) {
if addExp < 0 {
if petInfo == nil || addExp <= 0 {
return
}
addExp = utils.Min(addExp, p.Info.ExpPool)
originalLevel := petInfo.Level
exp := int64(petInfo.Exp) + addExp
p.Info.ExpPool -= addExp //减去已使用的经验
gainedExp := exp //已获得的经验
for exp >= int64(petInfo.NextLvExp) {
petInfo.Level++
exp -= int64(petInfo.LvExp)
petInfo.Update(true)
if originalLevel < 100 && petInfo.Level == 100 { //升到100了
p.Info.ExpPool += exp //减去已使用的经验
gainedExp -= exp
exp = 0
break //停止升级
}
panelLimit := p.CurrentMapPetLevelLimit()
if petInfo.Level > 100 {
currentHP := petInfo.Hp
petInfo.Update(false)
petInfo.CalculatePetPane(panelLimit)
petInfo.Hp = utils.Min(currentHP, petInfo.MaxHp)
}
petInfo.Exp = (exp)
addExp = utils.Min(addExp, p.Info.ExpPool)
if addExp <= 0 {
return
}
originalLevel := petInfo.Level
allocatedExp := addExp
p.Info.ExpPool -= addExp //减去已使用的经验
currentExp := petInfo.Exp + addExp
for currentExp >= petInfo.NextLvExp && petInfo.NextLvExp > 0 {
petInfo.Level++
petInfo.Update(true)
currentExp -= petInfo.LvExp
}
petInfo.Exp = currentExp
// 重新计算面板
if originalLevel != petInfo.Level {
petInfo.CalculatePetPane(100)
petInfo.CalculatePetPane(panelLimit)
petInfo.Cure()
p.Info.PetMaxLevel = utils.Max(petInfo.Level, p.Info.PetMaxLevel)
@@ -82,7 +84,7 @@ func (p *Player) AddPetExp(petInfo *model.PetInfo, addExp int64) {
var petUpdateInfo info.UpdatePropInfo
copier.Copy(&petUpdateInfo, petInfo)
petUpdateInfo.Exp = uint32(gainedExp)
petUpdateInfo.Exp = uint32(allocatedExp)
updateOutbound.Data = append(updateOutbound.Data, petUpdateInfo)
p.SendPack(header.Pack(updateOutbound)) //准备包由各自发,因为协议不一样

View File

@@ -25,6 +25,13 @@ func (slot PetBagSlot) PetInfo() model.PetInfo {
return slot.info
}
func (slot PetBagSlot) PetInfoPtr() *model.PetInfo {
if !slot.IsValid() {
return nil
}
return &(*slot.list)[slot.index]
}
func (slot PetBagSlot) IsMainBag() bool {
return slot.main
}
@@ -99,12 +106,20 @@ func validatePetBagOrder(
return true
}
func buildLimitedPetList(petList []model.PetInfo, limitlevel uint32) []model.PetInfo {
result := make([]model.PetInfo, 0, len(petList))
for _, petInfo := range petList {
result = append(result, ApplyPetLevelLimit(petInfo, limitlevel))
}
return result
}
// GetUserBagPetInfo 返回主背包和并列备用精灵列表。
func (p *Player) GetUserBagPetInfo() *pet.GetUserBagPetInfoOutboundInfo {
func (p *Player) GetUserBagPetInfo(limitlevel uint32) *pet.GetUserBagPetInfoOutboundInfo {
result := &pet.GetUserBagPetInfoOutboundInfo{
PetList: p.Info.PetList,
BackupPetList: p.Info.BackupPetList,
PetList: buildLimitedPetList(p.Info.PetList, limitlevel),
BackupPetList: buildLimitedPetList(p.Info.BackupPetList, limitlevel),
}
return result

View File

@@ -0,0 +1,157 @@
package player
import (
"blazing/common/data/xmlres"
playermodel "blazing/modules/player/model"
"testing"
)
func firstPetIDForTest(t *testing.T) int {
t.Helper()
for id := range xmlres.PetMAP {
return id
}
t.Fatal("xmlres.PetMAP is empty")
return 0
}
func TestAddPetExpAllowsLevelBeyond100WhilePanelStaysCapped(t *testing.T) {
petID := firstPetIDForTest(t)
petInfo := playermodel.GenPetInfo(petID, 31, 0, 0, 100, nil, 0)
expectedPanel := playermodel.GenPetInfo(petID, 31, 0, 0, 100, nil, 0)
if petInfo == nil {
t.Fatalf("failed to generate test pet")
}
if expectedPanel == nil {
t.Fatalf("failed to generate expected test pet")
}
player := &Player{
baseplayer: baseplayer{
Info: &playermodel.PlayerInfo{
ExpPool: 1_000_000,
},
},
}
player.AddPetExp(petInfo, petInfo.NextLvExp+10_000)
if petInfo.Level <= 100 {
t.Fatalf("expected pet level to continue beyond 100, got %d", petInfo.Level)
}
if petInfo.MaxHp != expectedPanel.MaxHp {
t.Fatalf("expected max hp to stay capped at 100-level panel, got %d want %d", petInfo.MaxHp, expectedPanel.MaxHp)
}
if petInfo.Prop != expectedPanel.Prop {
t.Fatalf("expected props to stay capped at 100-level panel, got %+v want %+v", petInfo.Prop, expectedPanel.Prop)
}
}
func TestAddPetExpRecalculatesPanelForLevelAbove100(t *testing.T) {
petID := firstPetIDForTest(t)
petInfo := playermodel.GenPetInfo(petID, 31, 0, 0, 100, nil, 0)
expectedPanel := playermodel.GenPetInfo(petID, 31, 0, 0, 100, nil, 0)
if petInfo == nil {
t.Fatalf("failed to generate test pet")
}
if expectedPanel == nil {
t.Fatalf("failed to generate expected test pet")
}
petInfo.Level = 101
petInfo.Exp = 7
petInfo.MaxHp = 1
petInfo.Hp = 999999
player := &Player{
baseplayer: baseplayer{
Info: &playermodel.PlayerInfo{
ExpPool: 50_000,
},
},
}
player.AddPetExp(petInfo, 12_345)
if petInfo.Level < 101 {
t.Fatalf("expected level above 100 to be preserved, got %d", petInfo.Level)
}
if petInfo.MaxHp != expectedPanel.MaxHp {
t.Fatalf("expected max hp to be recalculated using level 100 cap, got %d want %d", petInfo.MaxHp, expectedPanel.MaxHp)
}
if petInfo.Hp != petInfo.MaxHp {
t.Fatalf("expected hp to be clamped to recalculated max hp, got hp=%d maxHp=%d", petInfo.Hp, petInfo.MaxHp)
}
if player.Info.ExpPool != 50_000-12_345 {
t.Fatalf("expected exp pool to be consumed normally, got %d", player.Info.ExpPool)
}
}
func TestAddPetExpSmallRewardDoesNotJumpToMaxLevel(t *testing.T) {
petID := firstPetIDForTest(t)
petInfo := playermodel.GenPetInfo(petID, 31, 0, 0, 20, nil, 0)
if petInfo == nil {
t.Fatalf("failed to generate test pet")
}
player := &Player{
baseplayer: baseplayer{
Info: &playermodel.PlayerInfo{
ExpPool: 1_000_000,
},
},
}
addExp := int64(100)
originalLevel := petInfo.Level
nextLevelNeed := petInfo.NextLvExp - petInfo.Exp
if addExp >= nextLevelNeed {
t.Fatalf("test setup invalid: addExp=%d should be smaller than next level need=%d", addExp, nextLevelNeed)
}
player.AddPetExp(petInfo, addExp)
if petInfo.Level != originalLevel {
t.Fatalf("expected level to stay at %d, got %d", originalLevel, petInfo.Level)
}
if petInfo.Exp != addExp {
t.Fatalf("expected current exp to increase by %d, got %d", addExp, petInfo.Exp)
}
if player.Info.ExpPool != 1_000_000-addExp {
t.Fatalf("expected exp pool to decrease by %d, got %d", addExp, player.Info.ExpPool)
}
}
func TestAddPetExpUsesDynamicPerLevelRequirement(t *testing.T) {
petID := firstPetIDForTest(t)
petInfo := playermodel.GenPetInfo(petID, 31, 0, 0, 20, nil, 0)
if petInfo == nil {
t.Fatalf("failed to generate test pet")
}
initialNeed := petInfo.NextLvExp
if initialNeed <= 0 {
t.Fatalf("expected positive exp requirement, got %d", initialNeed)
}
player := &Player{
baseplayer: baseplayer{
Info: &playermodel.PlayerInfo{
ExpPool: 1_000_000,
},
},
}
player.AddPetExp(petInfo, initialNeed)
if petInfo.Level != 21 {
t.Fatalf("expected pet level to become 21, got %d", petInfo.Level)
}
if petInfo.Exp != 0 {
t.Fatalf("expected level-up to reset current level exp, got %d", petInfo.Exp)
}
if petInfo.NextLvExp == initialNeed {
t.Fatalf("expected next level exp to change after leveling, still %d", petInfo.NextLvExp)
}
}

View File

@@ -228,6 +228,21 @@ func (p *Player) GetSpace() *space.Space {
return space.GetSpace(p.Info.MapID)
}
func (p *Player) CurrentMapPetLevelLimit() uint32 {
if p == nil {
return 100
}
currentSpace := p.GetSpace()
if currentSpace != nil && currentSpace.IsLevelBreakMap {
return 0
}
return 100
}
func (p *Player) IsCurrentMapLevelBreak() bool {
return p != nil && p.CurrentMapPetLevelLimit() == 0
}
// CanFight 检查玩家是否可以进行战斗
// 0无战斗1PVP2,BOOS,3PVE
func (p *Player) CanFight() errorcode.ErrorCode {
@@ -419,7 +434,15 @@ func (p *Player) ItemAdd(ItemId, ItemCnt int64) (result bool) {
p.SendPack(t1.Pack(nil)) //准备包由各自发,因为协议不一样
return false
}
p.Service.Item.UPDATE(uint32(ItemId), gconv.Int(ItemCnt))
if err := p.Service.Item.UPDATE(uint32(ItemId), gconv.Int(ItemCnt)); err != nil {
cool.Logger.Error(context.TODO(), "item add update failed", p.Info.UserID, ItemId, ItemCnt, err)
t1 := common.NewTomeeHeader(2601, p.Info.UserID)
t1.Result = uint32(errorcode.ErrorCodes.ErrSystemError200007)
p.SendPack(t1.Pack(nil)) //准备包由各自发,因为协议不一样
return false
}
return true
}

View File

@@ -0,0 +1,222 @@
package player
import (
"blazing/common/data"
"blazing/common/socket/errorcode"
fightinfo "blazing/logic/service/fight/info"
tasklogic "blazing/logic/service/task"
configmodel "blazing/modules/config/model"
configservice "blazing/modules/config/service"
playermodel "blazing/modules/player/model"
"sync"
)
// TaskCompletionContext 封装任务完成时的上下文。
// 这里除了保留任务配置和默认奖励,也给自定义任务完成逻辑暴露了返回包与开关位,
// 用来兼容“固定发物品/精灵”之外的奖励场景。
// 这套扩展最初是为任务发放特训技能、皮肤而补上的:
// 特训奖励不能完全按静态表直发,需要结合额外条件做特判,
// 例如通过挖矿/对话进度限制特训次数,满足条件后再允许完成任务。
type TaskCompletionContext struct {
TaskID uint32
OutState int
Config *configmodel.TaskConfig
Reward *tasklogic.TaskResult
Result *tasklogic.CompleteTaskOutboundInfo
SkipDefaultReward bool
}
// TaskCompletionHandler 定义任务完成前的自定义处理器。
// 处理器可用于补充校验、写入额外奖励,或在任务完全走自定义发奖时跳过默认奖励流程。
type TaskCompletionHandler func(*Player, *TaskCompletionContext) errorcode.ErrorCode
// taskCompletionRegistry 按任务 ID 维护自定义完成处理器。
// 默认任务仍然走 task 配表里的固定奖励;只有存在特判需求的任务才在这里注册。
var taskCompletionRegistry = struct {
sync.RWMutex
handlers map[uint32]TaskCompletionHandler
}{
handlers: make(map[uint32]TaskCompletionHandler),
}
// taskRewardGrantResult 汇总本次任务实际发放的奖励,
// 便于后续统一推送给前端展示。
type taskRewardGrantResult struct {
Pet *playermodel.PetInfo
Items []data.ItemInfo
}
// RegisterTaskCompletionHandler 注册任务完成时的自定义处理器。
// 用于覆盖“任务奖励固定为物品和精灵”的旧模型,让指定任务在完成前后插入额外逻辑。
// 当前这套机制主要服务于特训技能、皮肤等特殊奖励,以及需要额外次数/进度校验的任务。
func RegisterTaskCompletionHandler(taskID uint32, handler TaskCompletionHandler) {
if taskID == 0 || handler == nil {
return
}
taskCompletionRegistry.Lock()
taskCompletionRegistry.handlers[taskID] = handler
taskCompletionRegistry.Unlock()
}
// RegisterTaskTalkLimitHandler 注册一个基于挖矿/采集对话进度的完成限制。
// 历史上特训任务需要通过挖矿次数限制可领取次数,因此复用了 Talk 进度作为准入条件。
// 当指定 talkID 的进度不足 needCount 时,任务不能完成也不能领奖。
func RegisterTaskTalkLimitHandler(taskID, talkID, needCount uint32) {
RegisterTaskCompletionHandler(taskID, func(p *Player, _ *TaskCompletionContext) errorcode.ErrorCode {
if p == nil || p.Service == nil || p.Service.Talk == nil {
return errorcode.ErrorCodes.ErrSystemError
}
currentCount, ok := p.Service.Talk.Progress(int(talkID))
if !ok || currentCount < needCount {
return errorcode.ErrorCodes.ErrNeedCompleteTaskForPrize
}
return 0
})
}
func (p *Player) getTaskGift(taskID int, outState int) *tasklogic.TaskResult {
if taskID <= 0 {
return nil
}
return tasklogic.GetTaskInfo(taskID, outState)
}
// hasTaskCompletionHandler 判断任务是否存在自定义完成处理器。
func hasTaskCompletionHandler(taskID uint32) bool {
taskCompletionRegistry.RLock()
_, ok := taskCompletionRegistry.handlers[taskID]
taskCompletionRegistry.RUnlock()
return ok
}
// getTaskCompletionHandler 获取任务的自定义完成处理器。
func getTaskCompletionHandler(taskID uint32) TaskCompletionHandler {
taskCompletionRegistry.RLock()
handler := taskCompletionRegistry.handlers[taskID]
taskCompletionRegistry.RUnlock()
return handler
}
// canCompleteTaskReward 判断任务是否具备可执行的奖励逻辑。
// 只要存在默认奖励,或已注册自定义处理器,就允许进入完成流程。
func (p *Player) canCompleteTaskReward(taskID, outState int) bool {
if taskID <= 0 {
return false
}
return p.getTaskGift(taskID, outState) != nil || hasTaskCompletionHandler(uint32(taskID))
}
// ApplyTaskCompletion 执行任务完成时的奖励发放入口。
// 流程分两层:
// 1. 先执行自定义处理器,处理特训/皮肤/额外次数校验等特殊逻辑;
// 2. 若未要求跳过默认奖励,再回落到原有的物品/精灵发奖逻辑。
func (p *Player) ApplyTaskCompletion(taskID uint32, outState int, result *tasklogic.CompleteTaskOutboundInfo) (*taskRewardGrantResult, errorcode.ErrorCode) {
if p == nil {
return nil, errorcode.ErrorCodes.ErrSystemError
}
ctx := &TaskCompletionContext{
TaskID: taskID,
OutState: outState,
Config: configservice.NewTaskService().Get(int(taskID), outState),
Reward: tasklogic.GetTaskInfo(int(taskID), outState),
Result: result,
}
if ctx.Reward == nil && !hasTaskCompletionHandler(taskID) {
return nil, errorcode.ErrorCodes.ErrNeedCompleteTaskForPrize
}
if handler := getTaskCompletionHandler(taskID); handler != nil {
if err := handler(p, ctx); err != 0 {
return nil, err
}
}
if ctx.SkipDefaultReward {
if result != nil {
result.ItemLen = uint32(len(result.ItemList))
}
return &taskRewardGrantResult{Items: make([]data.ItemInfo, 0)}, 0
}
if ctx.Reward == nil {
return nil, errorcode.ErrorCodes.ErrNeedCompleteTaskForPrize
}
return p.grantTaskReward(ctx.Reward, result), 0
}
// grantTaskReward 发放 task 配表里的默认奖励。
// 这里仍负责原有的固定奖励模型:物品、精灵、称号,以及配置里声明的任务宠奖励。
func (p *Player) grantTaskReward(reward *tasklogic.TaskResult, result *tasklogic.CompleteTaskOutboundInfo) *taskRewardGrantResult {
granted := &taskRewardGrantResult{
Items: make([]data.ItemInfo, 0),
}
if reward == nil {
if result != nil {
result.ItemLen = uint32(len(result.ItemList))
}
return granted
}
if reward.Pet != nil {
p.Service.Pet.PetAdd(reward.Pet, 0)
granted.Pet = reward.Pet
if result != nil {
result.CaptureTime = reward.Pet.CatchTime
result.PetTypeId = reward.Pet.ID
}
}
for _, item := range reward.ItemList {
if !p.ItemAdd(item.ItemId, item.ItemCnt) {
continue
}
granted.Items = append(granted.Items, item)
if result != nil {
result.ItemList = append(result.ItemList, item)
}
}
if reward.Title != 0 {
p.GiveTitle(reward.Title)
}
if reward.RewardPetID != 0 {
p.GrantTaskPetRewards(reward.RewardPetID, reward.TrainSkillIDs, reward.SkinIDs)
}
if result != nil {
result.ItemLen = uint32(len(result.ItemList))
}
return granted
}
// SendTaskCompletionBonus 将任务奖励转换为旧的奖励展示协议并推送给前端。
func (p *Player) SendTaskCompletionBonus(bonusID uint32, granted *taskRewardGrantResult) {
if p == nil {
return
}
res := &fightinfo.S2C_GET_BOSS_MONSTER{
BonusID: bonusID,
}
if granted != nil && granted.Pet != nil {
res.PetID = granted.Pet.ID
res.CaptureTm = granted.Pet.CatchTime
}
if granted != nil {
for _, item := range granted.Items {
res.AddItemInfo(item)
}
}
if res.HasReward() {
p.SendPackCmd(8004, res)
}
}

View File

@@ -0,0 +1,115 @@
package player
import "blazing/modules/player/model"
func applyTaskRewardToPetInfo(pet *model.PetInfo, trainSkillIDs, skinIDs []uint32) bool {
if pet == nil {
return false
}
changed := false
mergedSkills := mergeTaskRewardIDs(pet.ExtSKill, trainSkillIDs)
if len(mergedSkills) != len(pet.ExtSKill) {
pet.ExtSKill = mergedSkills
changed = true
}
mergedSkins := mergeTaskRewardIDs(pet.ExtSkin, skinIDs)
if len(mergedSkins) != len(pet.ExtSkin) {
pet.ExtSkin = mergedSkins
changed = true
}
if pet.SkinID == 0 {
for _, skinID := range mergedSkins {
if skinID == 0 {
continue
}
pet.SkinID = skinID
changed = true
break
}
}
return changed
}
func mergeTaskRewardIDs(dst []uint32, src []uint32) []uint32 {
if len(src) == 0 {
return dst
}
seen := make(map[uint32]struct{}, len(dst)+len(src))
result := make([]uint32, 0, len(dst)+len(src))
for _, id := range dst {
if id == 0 {
continue
}
if _, ok := seen[id]; ok {
continue
}
seen[id] = struct{}{}
result = append(result, id)
}
for _, id := range src {
if id == 0 {
continue
}
if _, ok := seen[id]; ok {
continue
}
seen[id] = struct{}{}
result = append(result, id)
}
return result
}
func (p *Player) GrantTaskPetRewards(targetPetID uint32, trainSkillIDs, skinIDs []uint32) bool {
if p == nil || targetPetID == 0 || (len(trainSkillIDs) == 0 && len(skinIDs) == 0) {
return false
}
changed := false
for i := range p.Info.PetList {
if p.Info.PetList[i].ID != targetPetID {
continue
}
if applyTaskRewardToPetInfo(&p.Info.PetList[i], trainSkillIDs, skinIDs) {
changed = true
}
}
for i := range p.Info.BackupPetList {
if p.Info.BackupPetList[i].ID != targetPetID {
continue
}
if applyTaskRewardToPetInfo(&p.Info.BackupPetList[i], trainSkillIDs, skinIDs) {
changed = true
}
}
if p.Service == nil || p.Service.Pet == nil {
return changed
}
allPets := p.Service.Pet.PetInfo(0)
for i := range allPets {
if allPets[i].Data.ID != targetPetID {
continue
}
if !applyTaskRewardToPetInfo(&allPets[i].Data, trainSkillIDs, skinIDs) {
continue
}
allPets[i].Data.CatchTime = allPets[i].CatchTime
if p.Service.Pet.Update(allPets[i].Data) {
changed = true
}
}
return changed
}

View File

@@ -42,10 +42,11 @@ type Space struct {
WeatherType []uint32
TimeBoss info.S2C_2022
IsTime bool
DropItemIds []uint32
PitS *csmap.CsMap[int, []model.MapPit]
MapNodeS *csmap.CsMap[uint32, *model.MapNode]
IsTime bool
IsLevelBreakMap bool
DropItemIds []uint32
PitS *csmap.CsMap[int, []model.MapPit]
MapNodeS *csmap.CsMap[uint32, *model.MapNode]
}
func NewSpace() *Space {
@@ -185,6 +186,9 @@ func (ret *Space) init() {
if r.IsTimeSpace != 0 {
ret.IsTime = true
}
if r.IsLevelBreakMap != 0 {
ret.IsLevelBreakMap = true
}
ret.MapBossSInfo = info.MapModelBroadcastInfo{}
ret.MapBossSInfo.INFO = make([]info.MapModelBroadcastEntry, 0)

View File

@@ -10,8 +10,11 @@ import (
type TaskResult struct {
Pet *model.PetInfo `json:"petTypeId" description:"发放的精灵ID"` // 发放的精灵ID
ItemList []data.ItemInfo `json:"itemList" description:"发放物品的数组"` // 发放物品的数组,
Title uint32 `json:"title" description:"称号奖励"`
ItemList []data.ItemInfo `json:"itemList" description:"发放物品的数组"` // 发放物品的数组,
Title uint32 `json:"title" description:"称号奖励"`
RewardPetID uint32 `json:"rewardPetId" description:"宠物相关奖励目标精灵ID"`
TrainSkillIDs []uint32 `json:"trainSkillIds" description:"特训技能奖励"`
SkinIDs []uint32 `json:"skinIds" description:"皮肤奖励"`
}
func GetTaskInfo(id, ot int) *TaskResult {
@@ -26,6 +29,12 @@ func GetTaskInfo(id, ot int) *TaskResult {
if pet != nil {
ret.Pet = model.GenPetInfo(int(pet.MonID), int(pet.DV), int(pet.Nature), int(pet.Effect), int(pet.Lv), nil, 0)
}
ret.RewardPetID = r.RewardPetID
ret.TrainSkillIDs = append(ret.TrainSkillIDs, r.TrainSkillIDs...)
ret.SkinIDs = append(ret.SkinIDs, r.SkinIDs...)
if ret.Pet != nil {
applyTaskRewardPetExtras(ret.Pet, ret.TrainSkillIDs, ret.SkinIDs)
}
for _, itemID := range r.ItemRewardIds {
iteminfo := service.NewItemService().GetItemCount(itemID)
@@ -39,3 +48,59 @@ func GetTaskInfo(id, ot int) *TaskResult {
return ret
}
func applyTaskRewardPetExtras(pet *model.PetInfo, trainSkillIDs, skinIDs []uint32) {
if pet == nil {
return
}
if merged := mergeTaskRewardIDs(pet.ExtSKill, trainSkillIDs); len(merged) != len(pet.ExtSKill) {
pet.ExtSKill = merged
}
mergedSkins := mergeTaskRewardIDs(pet.ExtSkin, skinIDs)
if len(mergedSkins) != len(pet.ExtSkin) {
pet.ExtSkin = mergedSkins
}
if pet.SkinID == 0 {
for _, skinID := range mergedSkins {
if skinID != 0 {
pet.SkinID = skinID
break
}
}
}
}
func mergeTaskRewardIDs(dst []uint32, src []uint32) []uint32 {
if len(src) == 0 {
return dst
}
seen := make(map[uint32]struct{}, len(dst)+len(src))
result := make([]uint32, 0, len(dst)+len(src))
for _, id := range dst {
if id == 0 {
continue
}
if _, ok := seen[id]; ok {
continue
}
seen[id] = struct{}{}
result = append(result, id)
}
for _, id := range src {
if id == 0 {
continue
}
if _, ok := seen[id]; ok {
continue
}
seen[id] = struct{}{}
result = append(result, id)
}
return result
}

View File

@@ -12,10 +12,10 @@ type C2S_GET_GIFT_COMPLETE struct {
// S2C_GET_GIFT_COMPLETE 礼品兑换完成协议
// 后端到前端
type S2C_GET_GIFT_COMPLETE struct {
Flag uint32 `json:"flag"` // 0 代表sdk已被使用 1 表示兑换成功 如果返回0 那么后续数组不需要返回
Flag uint32 `json:"flag"`
Tile uint32 `json:"tile"`
GiftListLen uint32 `struc:"sizeof=GiftList"`
GiftList []GiftInfo `json:"giftList"` // 物品id数组
GiftList []GiftInfo `json:"giftList"`
PetGiftLen uint32 `struc:"sizeof=PetGift"`
PetGift []PetGiftInfo `json:"petGiftList"`
}
@@ -25,6 +25,7 @@ type GiftInfo struct {
GiftID int64 `struc:"uint32"`
Count int64 `struc:"uint32"`
}
type PetGiftInfo struct {
PetID uint32 `json:"petID"`
CacthTime uint32 `json:"catchTime"`

View File

@@ -1,94 +1,97 @@
package cmd
import (
"blazing/common/rpc"
"blazing/cool"
"context"
"time"
"github.com/yudeguang/ratelimit"
i18n "blazing/modules/base/middleware"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/net/ghttp"
"github.com/gogf/gf/v2/os/gcmd"
"github.com/gogf/gf/v2/os/gfile"
"github.com/xiaoqidun/qqwry"
)
var (
Main = gcmd.Command{
Name: "main",
Usage: "main",
Brief: "start http server",
Func: func(ctx context.Context, parser *gcmd.Parser) (err error) {
r := parser.GetOpt("debug", false)
if r.Bool() {
g.DB().SetDebug(true)
// service.NewServerService().SetServerScreen(0, "sss")
cool.Config.ServerInfo.IsDebug = 1
}
if cool.IsRedisMode {
go rpc.ListenFunc(ctx)
}
// // 从文件加载IP数据库
if err := qqwry.LoadFile("public/qqwry.ipdb"); err != nil {
panic(err)
}
//go robot()
//go reg()
go startrobot()
s := g.Server()
s.Use(Limiter, ghttp.MiddlewareHandlerResponse)
s.EnableAdmin()
s.SetServerAgent(cool.Config.Name)
s.BindHookHandler("/*", ghttp.HookBeforeServe, beforeServeHook)
// runtime.SetMutexProfileFraction(1) // (非必需)开启对锁调用的跟踪
// runtime.SetBlockProfileRate(1) // (非必需)开启对阻塞操作的跟踪
// s.EnablePProf()
// 如果存在 data/cool-admin-vue/dist 目录,则设置为主目录
if gfile.IsDir("public") {
s.SetServerRoot("public")
}
// i18n 信息
s.BindHandler("/i18n", i18n.I18nInfo)
// g.Server().BindMiddleware("/*", MiddlewareCORS)
s.Run()
return nil
},
}
)
func beforeServeHook(r *ghttp.Request) {
//glog.Debugf(r.GetCtx(), "beforeServeHook [is file:%v] URI:%s", r.IsFileRequest(), r.RequestURI)
r.Response.CORSDefault()
}
// var limiter = rate.NewLimiter(rate.Limit(150), 50)
var limiter *ratelimit.Rule = ratelimit.NewRule()
// 简单规则案例
func init() {
//步骤二:增加一条或者多条规则组成复合规则,此复合规则必须至少包含一条规则
limiter.AddRule(time.Second*1, 20)
//步骤三:调用函数判断某用户是否允许访问 allow:= r.AllowVisit(user)
}
// Limiter is a middleware that implements rate limiting for all HTTP requests.
// It returns HTTP 429 (Too Many Requests) when the rate limit is exceeded.
func Limiter(r *ghttp.Request) {
// 3. 为任意键 "some-key" 获取一个速率限制器
// - rate.Limit(2): 表示速率为 "每秒2个请求"
// - 2: 表示桶的容量 (Burst)允许瞬时处理2个请求
ip := r.GetClientIp()
if !limiter.AllowVisitByIP4(ip) {
r.Response.WriteStatusExit(429) // Return 429 Too Many Requests
r.ExitAll()
}
r.Middleware.Next()
}
package cmd
import (
"blazing/common/rpc"
"blazing/cool"
"context"
"time"
"github.com/yudeguang/ratelimit"
i18n "blazing/modules/base/middleware"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/net/ghttp"
"github.com/gogf/gf/v2/os/gcmd"
"github.com/gogf/gf/v2/os/gfile"
"github.com/xiaoqidun/qqwry"
)
var (
Main = gcmd.Command{
Name: "main",
Usage: "main",
Brief: "start http server",
Func: func(ctx context.Context, parser *gcmd.Parser) (err error) {
r := parser.GetOpt("debug", false)
if r.Bool() {
g.DB().SetDebug(true)
// service.NewServerService().SetServerScreen(0, "sss")
cool.Config.ServerInfo.IsDebug = 1
}
if err = cool.RunAutoMigrate(); err != nil {
return err
}
if cool.IsRedisMode {
go rpc.ListenFunc(ctx)
}
// // 从文件加载IP数据库
if err := qqwry.LoadFile("public/qqwry.ipdb"); err != nil {
panic(err)
}
//go robot()
//go reg()
go startrobot()
s := g.Server()
s.Use(Limiter, ghttp.MiddlewareHandlerResponse)
s.EnableAdmin()
s.SetServerAgent(cool.Config.Name)
s.BindHookHandler("/*", ghttp.HookBeforeServe, beforeServeHook)
// runtime.SetMutexProfileFraction(1) // (非必需)开启对锁调用的跟踪
// runtime.SetBlockProfileRate(1) // (非必需)开启对阻塞操作的跟踪
// s.EnablePProf()
// 如果存在 data/cool-admin-vue/dist 目录,则设置为主目录
if gfile.IsDir("public") {
s.SetServerRoot("public")
}
// i18n 信息
s.BindHandler("/i18n", i18n.I18nInfo)
// g.Server().BindMiddleware("/*", MiddlewareCORS)
s.Run()
return nil
},
}
)
func beforeServeHook(r *ghttp.Request) {
//glog.Debugf(r.GetCtx(), "beforeServeHook [is file:%v] URI:%s", r.IsFileRequest(), r.RequestURI)
r.Response.CORSDefault()
}
// var limiter = rate.NewLimiter(rate.Limit(150), 50)
var limiter *ratelimit.Rule = ratelimit.NewRule()
// 简单规则案例
func init() {
//步骤二:增加一条或者多条规则组成复合规则,此复合规则必须至少包含一条规则
limiter.AddRule(time.Second*1, 20)
//步骤三:调用函数判断某用户是否允许访问 allow:= r.AllowVisit(user)
}
// Limiter is a middleware that implements rate limiting for all HTTP requests.
// It returns HTTP 429 (Too Many Requests) when the rate limit is exceeded.
func Limiter(r *ghttp.Request) {
// 3. 为任意键 "some-key" 获取一个速率限制器
// - rate.Limit(2): 表示速率为 "每秒2个请求"
// - 2: 表示桶的容量 (Burst)允许瞬时处理2个请求
ip := r.GetClientIp()
if !limiter.AllowVisitByIP4(ip) {
r.Response.WriteStatusExit(429) // Return 429 Too Many Requests
r.ExitAll()
}
r.Middleware.Next()
}

View File

@@ -1,86 +1,86 @@
server:
name: "blazing server"
address: ":15948" #前端服务器+rpc地址
openapiPath: "/api.json"
swaggerPath: "/swagger"
clientMaxBodySize:
20971520 # 20MB in bytes 20*1024*1024
# 平滑重启特性
graceful: true # 是否开启平滑重启特性开启时将会在本地增加10000的本地TCP端口用于进程间通信默认false
gracefulTimeout: 2 # 父进程在平滑重启后多少秒退出默认2秒若请求耗时大于该值可能会导致请求中断
gracefulShutdownTimeout: 5 # 关闭Server时如果存在正在执行的HTTP请求Server等待多少秒才执行强行关闭
logger:
level: "all"
stdout: true
database:
default:
type: "pgsql"
host: "43.248.3.21"
port: "5432"
user: "user_YrK4j7"
pass: "password_jSDm76"
name: "bl"
debug: false
timezone: "Asia/Shanghai"
createdAt: "createTime"
updatedAt: "updateTime"
#timeMaintainDisabled: false # (可选)是否完全关闭时间更新特性为true时CreatedAt/UpdatedAt/DeletedAt都将失效
# deletedAt: "deleteTime"
# default:
# type: "mysql"
# host: "159.75.107.160"
# port: "3306"
# user: "blazing"
# pass: "zjjSrsBMrB5RmjE7"
# name: "blazing"
# charset: "utf8mb4"
# timezone: "Asia/Shanghai"
# debug: true
# createdAt: "createTime"
# updatedAt: "updateTime"
# deletedAt: "deleteTime"
# baseConfig:
# type: "sqlite"
# link: "base-config.sqlite"
# extra: busy_timeout=5000
# createdAt: "createTime"
# updatedAt: "updateTime"
# debug: true
# Redis 配置示例
redis:
cool:
address: "43.248.3.21:6379"
db: 0
pass: "redis_TxYnSy"
blazing:
autoMigrate: true
eps: true
file:
mode: "local" # local | minio | oss
#前端上传地址,因为放弃本地,所以这个弃用了 ,现在被当成rpc地址
domain: "61.147.247.41"
# oss配置项兼容 minio oss 需要配置bucket公开读
oss:
endpoint: "192.168.192.110:9000"
accessKeyID: "accessKeyID"
secretAccessKey: "secretAccessKey"
bucketName: "blazing"
useSSL: false #minio用到
location: "us-east-1" #minio用到
modules:
base:
jwt:
sso: false
secret: "sun-base88776655"
token:
expire: 7200 # 2*3600
refreshExpire: 1296000 # 24*3600*15
middleware:
authority:
enable: true
log:
enable: true
server:
name: "blazing server"
address: ":15948" #前端服务器+rpc地址
openapiPath: "/api.json"
swaggerPath: "/swagger"
clientMaxBodySize:
20971520 # 20MB in bytes 20*1024*1024
# 平滑重启特性
# graceful: true # 是否开启平滑重启特性开启时将会在本地增加10000的本地TCP端口用于进程间通信默认false
# gracefulTimeout: 2 # 父进程在平滑重启后多少秒退出默认2秒若请求耗时大于该值可能会导致请求中断
# gracefulShutdownTimeout: 5 # 关闭Server时如果存在正在执行的HTTP请求Server等待多少秒才执行强行关闭
logger:
level: "all"
stdout: true
database:
default:
type: "pgsql"
host: "43.248.3.21"
port: "5432"
user: "user_YrK4j7"
pass: "password_jSDm76"
name: "bl"
debug: false
timezone: "Asia/Shanghai"
createdAt: "createTime"
updatedAt: "updateTime"
#timeMaintainDisabled: false # (可选)是否完全关闭时间更新特性为true时CreatedAt/UpdatedAt/DeletedAt都将失效
# deletedAt: "deleteTime"
# default:
# type: "mysql"
# host: "159.75.107.160"
# port: "3306"
# user: "blazing"
# pass: "zjjSrsBMrB5RmjE7"
# name: "blazing"
# charset: "utf8mb4"
# timezone: "Asia/Shanghai"
# debug: true
# createdAt: "createTime"
# updatedAt: "updateTime"
# deletedAt: "deleteTime"
# baseConfig:
# type: "sqlite"
# link: "base-config.sqlite"
# extra: busy_timeout=5000
# createdAt: "createTime"
# updatedAt: "updateTime"
# debug: true
# Redis 配置示例
redis:
cool:
address: "43.248.3.21:6379"
db: 0
pass: "redis_TxYnSy"
blazing:
autoMigrate: true
eps: true
file:
mode: "local" # local | minio | oss
#前端上传地址,因为放弃本地,所以这个弃用了 ,现在被当成rpc地址
domain: "43.248.3.21"
# oss配置项兼容 minio oss 需要配置bucket公开读
oss:
endpoint: "192.168.192.110:9000"
accessKeyID: "accessKeyID"
secretAccessKey: "secretAccessKey"
bucketName: "blazing"
useSSL: false #minio用到
location: "us-east-1" #minio用到
modules:
base:
jwt:
sso: false
secret: "sun-base88776655"
token:
expire: 7200 # 2*3600
refreshExpire: 1296000 # 24*3600*15
middleware:
authority:
enable: true
log:
enable: true

View File

@@ -16,7 +16,6 @@ import (
playerservice "blazing/modules/player/service"
"github.com/deatil/go-cryptobin/cryptobin/crypto"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gtime"
)
@@ -56,8 +55,13 @@ type ReqShopReq struct {
func (c *BaseSysUserController) ReqShop(ctx context.Context, req *ReqShopReq) (res *cool.BaseRes, err error) {
t := cool.GetAdmin(ctx)
if !playerservice.NewTaskService(uint32(t.UserId)).CanShop() {
return cool.Fail("不满足申请条件"), nil
user := service.NewBaseSysUserService().GetPerson(uint32(t.UserId))
if user == nil || user.QQ == 0 {
return cool.Fail("请先绑定QQ"), nil
}
if err := playerservice.NewTaskService(uint32(t.UserId)).ShopRequirementError(); err != nil {
return cool.Fail(err.Error()), nil
}
cool.DBM(&model.BaseSysUserRole{}).Data("roleId", "27", "userId", t.UserId).Save()
res = cool.Ok(nil)
@@ -118,8 +122,8 @@ type SessionRes struct {
UserID int `json:"userid"`
Session string `json:"session"`
Server gdb.List `json:"server"`
PetID []int `json:"petid"`
Server []config.ServerShowInfo `json:"server"`
PetID []int `json:"petid"`
}
type RegReq struct {

View File

@@ -1,211 +1,213 @@
package middleware
import (
"blazing/common/rpc"
"blazing/cool"
"blazing/modules/base/config"
"blazing/modules/config/service"
"encoding/json"
"fmt"
"math/rand"
"net/http"
"net/http/httputil"
"net/url"
"path/filepath"
"strconv"
"time"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/net/ghttp"
"github.com/gogf/gf/v2/os/gtime"
"github.com/gogf/gf/v2/util/gconv"
"github.com/golang-jwt/jwt/v4"
"github.com/google/uuid"
"github.com/lxzan/gws"
)
const UpStream = "http://43.248.3.21:45632/"
func MiddlewareCORS(r *ghttp.Request) {
r.Response.CORSDefault()
corsOptions := r.Response.DefaultCORSOptions()
corsOptions.AllowDomain = []string{"*", "localhost", "tauri.localhost"}
if !r.Response.CORSAllowedOrigin(corsOptions) {
r.Response.WriteStatus(http.StatusForbidden)
return
}
r.Response.CORS(corsOptions)
if r.Response.Request.Method == "OPTIONS" {
r.Response.WriteStatus(http.StatusOK)
return
}
// fmt.Println(r.Response.Header())
//g.Dump(r.Response.Server.SetConfig(gtt))
//r.Response.Header().Del("Server") // 删除Server头
// r.Response.Header().Set("Server", "blazing")
r.Middleware.Next()
}
func StartServerProxy() {
s := g.Server()
// Parse the upstream URL
u, _ := url.Parse(UpStream)
// Create a new reverse proxy instance
proxy := httputil.NewSingleHostReverseProxy(u)
// Configure error handling for proxy failures
proxy.ErrorHandler = func(writer http.ResponseWriter, request *http.Request, e error) {
writer.WriteHeader(http.StatusBadGateway)
}
s.BindHandler("/bbs/api/fof/upload", func(r *ghttp.Request) {
// 1. 调用上传方法仍返回单个URL字符串不改动Upload方法
urlStr, err := cool.File().Upload(r.Context())
// 2. 错误处理返回标准化错误JSON
if err != nil {
r.Response.Header().Set("Content-Type", "application/json; charset=utf-8")
r.Response.WriteHeader(http.StatusBadRequest)
json.NewEncoder(r.Response.Writer).Encode(map[string]interface{}{
"code": 1,
"msg": err.Error(),
"data": nil,
})
return
}
// 3. 基于返回的URL构造完整的JSON结构体和示例完全一致
// 解析URL中的文件名和路径从urlStr中提取
baseName := filepath.Base(urlStr) // 提取文件名如13e8d062-xxx.jpg
dir := gtime.Now().Format("Y-m-d") // 日期目录2026-02-07
path := fmt.Sprintf("%s/%s", dir, baseName) // 拼接path字段
rand.Seed(time.Now().UnixNano())
randomID := strconv.Itoa(rand.Intn(1000)) // 模拟ID如54
uuidStr := uuid.New().String() // 生成UUID
humanSize := "743kB" // 模拟易读大小(可根据实际需求优化)
fileSize := int64(760783) // 模拟文件大小(字节)
// 构造和示例完全一致的响应结构体
fullResponse := map[string]interface{}{
"data": []map[string]interface{}{
{
"type": "files",
"id": randomID,
"attributes": map[string]interface{}{
"baseName": baseName,
"path": path,
"url": urlStr, // 用Upload返回的URL
"type": "image/jpeg", // 模拟MIME类型
"size": fileSize,
"humanSize": humanSize,
"createdAt": nil, // null
"uuid": uuidStr,
"tag": "just-url",
"hidden": false,
"bbcode": `[img]` + urlStr + `[/img]`, // 和URL一致
"shared": false,
"canViewInfo": false,
"canHide": true,
"canDelete": true,
},
},
},
}
// 4. 输出完整JSON响应
r.Response.Header().Set("Content-Type", "application/json; charset=utf-8")
if err := json.NewEncoder(r.Response.Writer).Encode(fullResponse); err != nil {
// 兜底错误
fmt.Fprintf(r.Response.Writer, `{"code":1,"msg":"响应生成失败:%s","data":null}`, err.Error())
}
})
// Handle all requests with path prefix "/proxy/*"
s.BindHandler("/bbs/*url", func(r *ghttp.Request) {
var (
//originalPath = r.Request.URL.Path
proxyToPath = "/" + r.Get("url").String()
)
// Rewrite the request path
r.Request.URL.Path = proxyToPath
// Log the proxy operation
//g.Log().Infof(r.Context(), `proxy:"%s" -> backend:"%s"`, originalPath, proxyToPath)
// Ensure request body can be read multiple times if needed
r.MakeBodyRepeatableRead(false)
// Forward the request to the backend server
proxy.ServeHTTP(r.Response.Writer, r.Request)
})
}
func init() {
if config.Config.Middleware.Authority.Enable {
g.Server().BindMiddleware("/admin/*/open/*", BaseAuthorityMiddlewareOpen)
g.Server().BindMiddleware("/rpc/*", BaseAuthorityMiddlewareOpen)
g.Server().BindMiddleware("/admin/*/comm/*", BaseAuthorityMiddlewareComm)
g.Server().BindMiddleware("/admin/*", BaseAuthorityMiddleware)
// g.Server().BindMiddleware("/*", AutoI18n)
g.Server().BindMiddleware("/*", MiddlewareCORS)
}
if config.Config.Middleware.Log.Enable {
g.Server().BindMiddleware("/admin/*", BaseLog)
}
StartServerProxy()
tt := rpc.CServer()
g.Server().BindHandler("/rpc/*", func(r *ghttp.Request) {
tt.ServeHTTP(r.Response.Writer, r.Request)
})
g.Server().Use(CompressMiddleware)
g.Server().BindHandler("/server/*", func(r *ghttp.Request) {
servert := new(ServerHandler)
id := gconv.Uint16(r.URL.Query().Get("id"))
servert.isinstall = gconv.Uint32(r.URL.Query().Get("isinstall"))
servert.ServerList = service.NewServerService().StartUPdate(id, int(servert.isinstall))
upgrader := gws.NewUpgrader(servert, &gws.ServerOption{
Authorize: func(_ *http.Request, _ gws.SessionStorage) bool {
tokenString := r.URL.Query().Get("Authorization")
token, err := jwt.ParseWithClaims(tokenString, &cool.Claims{}, func(_ *jwt.Token) (interface{}, error) {
return []byte(config.Config.Jwt.Secret), nil
})
if err != nil {
return false
}
if !token.Valid {
return false
}
admin := token.Claims.(*cool.Claims)
if admin.UserId != 10001 {
return false
}
// var name = r.URL.Query().Get("name")
// if name == "" {
// return false
// }
// t, _ := service.NewBaseSysUserService().Person(admin.UserID)
//Loger.Debug(context.TODO(), t.Mimi)
// session.Store("name", t.Mimi)
//session.Store("key", r.Header.Get("Sec-WebSocket-Key"))
return true
},
})
socket, err := upgrader.Upgrade(r.Response.Writer, r.Request)
if err != nil {
fmt.Println(err)
return
}
// ants.Submit(func() {
// socket.ReadLoop()
// })
// ants.Submit(func() { socket.ReadLoop() })
go socket.ReadLoop()
})
}
package middleware
import (
"blazing/common/rpc"
"blazing/cool"
"blazing/modules/base/config"
"blazing/modules/config/service"
"encoding/json"
"fmt"
"math/rand"
"net/http"
"net/http/httputil"
"net/url"
"path/filepath"
"strconv"
"time"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/net/ghttp"
"github.com/gogf/gf/v2/os/gtime"
"github.com/gogf/gf/v2/util/gconv"
"github.com/golang-jwt/jwt/v4"
"github.com/google/uuid"
"github.com/lxzan/gws"
)
const UpStream = "http://43.248.3.21:45632/"
func MiddlewareCORS(r *ghttp.Request) {
r.Response.CORSDefault()
corsOptions := r.Response.DefaultCORSOptions()
corsOptions.AllowDomain = []string{"*", "localhost", "tauri.localhost"}
if !r.Response.CORSAllowedOrigin(corsOptions) {
r.Response.WriteStatus(http.StatusForbidden)
return
}
r.Response.CORS(corsOptions)
if r.Response.Request.Method == "OPTIONS" {
r.Response.WriteStatus(http.StatusOK)
return
}
// fmt.Println(r.Response.Header())
//g.Dump(r.Response.Server.SetConfig(gtt))
//r.Response.Header().Del("Server") // 删除Server头
// r.Response.Header().Set("Server", "blazing")
r.Middleware.Next()
}
func StartServerProxy() {
s := g.Server()
// Parse the upstream URL
u, _ := url.Parse(UpStream)
// Create a new reverse proxy instance
proxy := httputil.NewSingleHostReverseProxy(u)
// Configure error handling for proxy failures
proxy.ErrorHandler = func(writer http.ResponseWriter, request *http.Request, e error) {
writer.WriteHeader(http.StatusBadGateway)
}
s.BindHandler("/bbs/api/fof/upload", func(r *ghttp.Request) {
// 1. 调用上传方法仍返回单个URL字符串不改动Upload方法
urlStr, err := cool.File().Upload(r.Context())
// 2. 错误处理返回标准化错误JSON
if err != nil {
r.Response.Header().Set("Content-Type", "application/json; charset=utf-8")
r.Response.WriteHeader(http.StatusBadRequest)
json.NewEncoder(r.Response.Writer).Encode(map[string]interface{}{
"code": 1,
"msg": err.Error(),
"data": nil,
})
return
}
// 3. 基于返回的URL构造完整的JSON结构体和示例完全一致
// 解析URL中的文件名和路径从urlStr中提取
baseName := filepath.Base(urlStr) // 提取文件名如13e8d062-xxx.jpg
dir := gtime.Now().Format("Y-m-d") // 日期目录2026-02-07
path := fmt.Sprintf("%s/%s", dir, baseName) // 拼接path字段
rand.Seed(time.Now().UnixNano())
randomID := strconv.Itoa(rand.Intn(1000)) // 模拟ID如54
uuidStr := uuid.New().String() // 生成UUID
humanSize := "743kB" // 模拟易读大小(可根据实际需求优化)
fileSize := int64(760783) // 模拟文件大小(字节)
// 构造和示例完全一致的响应结构体
fullResponse := map[string]interface{}{
"data": []map[string]interface{}{
{
"type": "files",
"id": randomID,
"attributes": map[string]interface{}{
"baseName": baseName,
"path": path,
"url": urlStr, // 用Upload返回的URL
"type": "image/jpeg", // 模拟MIME类型
"size": fileSize,
"humanSize": humanSize,
"createdAt": nil, // null
"uuid": uuidStr,
"tag": "just-url",
"hidden": false,
"bbcode": `[img]` + urlStr + `[/img]`, // 和URL一致
"shared": false,
"canViewInfo": false,
"canHide": true,
"canDelete": true,
},
},
},
}
// 4. 输出完整JSON响应
r.Response.Header().Set("Content-Type", "application/json; charset=utf-8")
if err := json.NewEncoder(r.Response.Writer).Encode(fullResponse); err != nil {
// 兜底错误
fmt.Fprintf(r.Response.Writer, `{"code":1,"msg":"响应生成失败:%s","data":null}`, err.Error())
}
})
// Handle all requests with path prefix "/proxy/*"
s.BindHandler("/bbs/*url", func(r *ghttp.Request) {
var (
//originalPath = r.Request.URL.Path
proxyToPath = "/" + r.Get("url").String()
)
// Rewrite the request path
r.Request.URL.Path = proxyToPath
// Log the proxy operation
//g.Log().Infof(r.Context(), `proxy:"%s" -> backend:"%s"`, originalPath, proxyToPath)
// Ensure request body can be read multiple times if needed
r.MakeBodyRepeatableRead(false)
// Forward the request to the backend server
proxy.ServeHTTP(r.Response.Writer, r.Request)
})
}
func init() {
if config.Config.Middleware.Authority.Enable {
g.Server().BindMiddleware("/admin/*/open/*", BaseAuthorityMiddlewareOpen)
g.Server().BindMiddleware("/rpc/*", BaseAuthorityMiddlewareOpen)
g.Server().BindMiddleware("/admin/*/comm/*", BaseAuthorityMiddlewareComm)
g.Server().BindMiddleware("/seer/game/cdk/*", BaseAuthorityMiddlewareComm)
g.Server().BindMiddleware("/seer/game/cdk/*", BaseAuthorityMiddleware)
g.Server().BindMiddleware("/admin/*", BaseAuthorityMiddleware)
// g.Server().BindMiddleware("/*", AutoI18n)
g.Server().BindMiddleware("/*", MiddlewareCORS)
}
if config.Config.Middleware.Log.Enable {
g.Server().BindMiddleware("/admin/*", BaseLog)
}
StartServerProxy()
tt := rpc.CServer()
g.Server().BindHandler("/rpc/*", func(r *ghttp.Request) {
tt.ServeHTTP(r.Response.Writer, r.Request)
})
g.Server().Use(CompressMiddleware)
g.Server().BindHandler("/server/*", func(r *ghttp.Request) {
servert := new(ServerHandler)
id := gconv.Uint16(r.URL.Query().Get("id"))
servert.isinstall = gconv.Uint32(r.URL.Query().Get("isinstall"))
servert.ServerList = service.NewServerService().StartUPdate(id, int(servert.isinstall))
upgrader := gws.NewUpgrader(servert, &gws.ServerOption{
Authorize: func(_ *http.Request, _ gws.SessionStorage) bool {
tokenString := r.URL.Query().Get("Authorization")
token, err := jwt.ParseWithClaims(tokenString, &cool.Claims{}, func(_ *jwt.Token) (interface{}, error) {
return []byte(config.Config.Jwt.Secret), nil
})
if err != nil {
return false
}
if !token.Valid {
return false
}
admin := token.Claims.(*cool.Claims)
if admin.UserId != 10001 {
return false
}
// var name = r.URL.Query().Get("name")
// if name == "" {
// return false
// }
// t, _ := service.NewBaseSysUserService().Person(admin.UserID)
//Loger.Debug(context.TODO(), t.Mimi)
// session.Store("name", t.Mimi)
//session.Store("key", r.Header.Get("Sec-WebSocket-Key"))
return true
},
})
socket, err := upgrader.Upgrade(r.Response.Writer, r.Request)
if err != nil {
fmt.Println(err)
return
}
// ants.Submit(func() {
// socket.ReadLoop()
// })
// ants.Submit(func() { socket.ReadLoop() })
go socket.ReadLoop()
})
}

File diff suppressed because it is too large Load Diff

View File

@@ -3,6 +3,9 @@ package admin
import (
"blazing/cool"
"blazing/modules/config/service"
"context"
"github.com/gogf/gf/v2/frame/g"
)
type CdkController struct {
@@ -20,3 +23,16 @@ func init() {
},
})
}
type BatchGenerateReq struct {
g.Meta `path:"/batchGenerate" method:"POST"`
Count int `json:"count" v:"required|min:1#请输入正确的生成数量"`
}
func (c *CdkController) BatchGenerate(ctx context.Context, req *BatchGenerateReq) (res *cool.BaseRes, err error) {
data, err := service.NewCdkService().BatchGenerate(ctx, req.Count)
if err != nil {
return nil, err
}
return cool.Ok(data), nil
}

View File

@@ -16,6 +16,7 @@ type CDKConfig struct {
// 核心字段
CDKCode string `gorm:"not null;size:16;uniqueIndex;comment:'CDK编号唯一标识用于玩家兑换'" json:"cdk_code" description:"CDK编号"`
Type uint32 `gorm:"column:type;not null;default:0;comment:'CDK类型:0普通奖励,1服务器冠名'" json:"type" description:"CDK类型"`
//cdk可兑换次数where不等于0
ExchangeRemainCount int64 `gorm:"not null;default:1;comment:'CDK剩余可兑换次数不能为0才允许兑换支持查询where !=0'" json:"exchange_remain_count" description:"剩余可兑换次数"`

View File

@@ -71,6 +71,10 @@ func roundFloat32(val float32, decimals int) float32 {
return float32(math.Round(float64(val*shift))) / shift
}
func randomFloat32Range(r *rand.Rand, min, max float32) float32 {
return min + float32(r.Float64())*(max-min)
}
// 预处理矩阵:自动修正越界参数,确保合规
func ProcessOffspringMatrix(mat [4][5]float32) [4][5]float32 {
var processedMat [4][5]float32
@@ -118,71 +122,62 @@ func calculateMatrixHash(mm MonsterMatrix) string {
return hex.EncodeToString(h.Sum(nil))
}
// GenerateRandomParentMatrix 调整随机参数范围,解决暗色问题,贴合你的示例
// GenerateRandomParentMatrix 生成色域更大的父矩阵,同时抬高黑场避免整只精灵发黑
func GenerateRandomParentMatrix() MonsterMatrix {
// 1. 保留高熵种子逻辑(确保唯一性,不变)
salt := "player-matrix-random-seed"
now := time.Now().UnixNano()
randomNum := rand.Int63()
seed := now ^ randomNum ^ int64(hashString(salt))
r := rand.New(rand.NewSource(seed))
// 2. 调整随机参数范围(贴合你的示例,解决暗色问题)
// 对比你的示例:亮度大多在[-30,30],对比度[0.7,1.7],饱和度[0.4,1.4],偏移[-10,10]
params := struct {
brightness float32 // 从[-125,125]调整为[-30,60],避免过低亮度
contrast float32 // 从[0.4,2.2]调整为[0.7,1.7],贴合你的示例
saturation float32 // 从[0.1,2.0]调整为[0.4,1.4],避免低饱和度灰暗
hueRotate float32 // 从[-180,180]调整为[-50,50],减少极端色相导致的暗沉
rOffset float32 // 从[-50,50]调整为[-10,10],避免亮度偏移过大
gOffset float32 // 同上
bOffset float32 // 同上
}{
brightness: float32(r.Float64()*90 - 30), // [-30, 60](贴合示例亮度范围,减少暗色)
contrast: float32(r.Float64()*1.0 + 0.7), // [0.7, 1.7](与你的示例完全匹配)
saturation: float32(r.Float64()*1.0 + 0.4), // [0.4, 1.4](与你的示例完全匹配)
hueRotate: float32(r.Float64()*100 - 50), // [-50, 50](避免极端色相)
rOffset: float32(r.Float64()*20 - 10), // [-10, 10](小幅亮度偏移,贴合示例)
gOffset: float32(r.Float64()*20 - 10), // 同上
bOffset: float32(r.Float64()*20 - 10), // 同上
matrix := newIdentityMatrix()
dominant := r.Intn(3)
accent := (dominant + 1 + r.Intn(2)) % 3
baseLift := randomFloat32Range(r, 10, 24)
for row := 0; row < 3; row++ {
for col := 0; col < 3; col++ {
value := randomFloat32Range(r, -0.22, 0.22)
if row == col {
value = randomFloat32Range(r, 0.8, 1.45)
}
if row == dominant && col == dominant {
value += randomFloat32Range(r, 0.35, 0.7)
}
if row == accent && col == accent {
value += randomFloat32Range(r, 0.1, 0.3)
}
if row == dominant && col != row {
value += randomFloat32Range(r, -0.12, 0.28)
}
matrix[row][col] = clampFloat32(roundFloat32(value, FloatPrecision), MatrixMinVal, MatrixMaxVal)
}
}
// 3. 矩阵计算逻辑不变(确保与你的示例参数分布一致)
matrix := newIdentityMatrix()
contrast := params.contrast
matrix[0][0] *= contrast
matrix[1][1] *= contrast
matrix[2][2] *= contrast
matrix[0][4] = baseLift + randomFloat32Range(r, -5, 10)
matrix[1][4] = baseLift + randomFloat32Range(r, -5, 10)
matrix[2][4] = baseLift + randomFloat32Range(r, -5, 10)
matrix[dominant][4] += randomFloat32Range(r, 8, 18)
matrix[accent][4] += randomFloat32Range(r, 2, 8)
sat := params.saturation
grayR, grayG, grayB := float32(0.299)*(1-sat), float32(0.587)*(1-sat), float32(0.114)*(1-sat)
matrix[0][0] = grayR + sat*matrix[0][0]
matrix[0][1], matrix[0][2] = grayG, grayB
matrix[1][0] = grayR
matrix[1][1] = grayG + sat*matrix[1][1]
matrix[1][2] = grayB
matrix[2][0] = grayR
matrix[2][1] = grayG
matrix[2][2] = grayB + sat*matrix[2][2]
avgBrightness := (matrix[0][4] + matrix[1][4] + matrix[2][4]) / 3
if avgBrightness < 12 {
lift := 12 - avgBrightness + randomFloat32Range(r, 0, 6)
for row := 0; row < 3; row++ {
matrix[row][4] += lift
}
}
if matrix[dominant][4] < 16 {
matrix[dominant][4] = 16 + randomFloat32Range(r, 0, 10)
}
angle := params.hueRotate * float32(math.Pi) / 180
cos, sin := float32(math.Cos(float64(angle))), float32(math.Sin(float64(angle)))
r1, g1, b1 := matrix[0][0], matrix[0][1], matrix[0][2]
r2, g2, b2 := matrix[1][0], matrix[1][1], matrix[1][2]
for row := 0; row < 3; row++ {
for col := 0; col < 3; col++ {
matrix[row][col] = clampFloat32(roundFloat32(matrix[row][col], FloatPrecision), MatrixMinVal, MatrixMaxVal)
}
matrix[row][4] = clampFloat32(roundFloat32(matrix[row][4], FloatPrecision), BrightnessMinVal, BrightnessMaxVal)
}
matrix[0][0] = r1*cos - r2*sin
matrix[0][1] = r1*sin + r2*cos
matrix[1][0] = g1*cos - g2*sin
matrix[1][1] = g1*sin + g2*cos
matrix[2][0] = b1*cos - b2*sin
matrix[2][1] = b1*sin + b2*cos
// 亮度偏移:范围缩小后,避免过暗/过亮
matrix[0][4] = params.brightness + params.rOffset
matrix[1][4] = params.brightness + params.gOffset
matrix[2][4] = params.brightness + params.bOffset
// 4. 封装为MonsterMatrix不变
parent := MonsterMatrix{
Matrix: matrix,
Hash: "",

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