Compare commits

...

49 Commits

Author SHA1 Message Date
昔念
d517c822ef 1
Some checks failed
ci/woodpecker/push/my-first-workflow Pipeline failed
2026-04-20 01:02:57 +08:00
昔念
04038cd16b 1
Some checks failed
ci/woodpecker/push/my-first-workflow Pipeline failed
2026-04-20 00:45:55 +08:00
昔念
ec608d69cd 1
Some checks failed
ci/woodpecker/push/my-first-workflow Pipeline failed
2026-04-19 22:16:35 +08:00
昔念
fd5341da1a 1
Some checks failed
ci/woodpecker/push/my-first-workflow Pipeline failed
2026-04-19 22:05:33 +08:00
昔念
5967414da4 1
All checks were successful
ci/woodpecker/push/my-first-workflow Pipeline was successful
2026-04-18 17:50:42 +08:00
昔念
da118dc826 1
All checks were successful
ci/woodpecker/push/my-first-workflow Pipeline was successful
2026-04-18 16:34:03 +08:00
昔念
823eef00ac 1
Some checks failed
ci/woodpecker/push/my-first-workflow Pipeline failed
2026-04-18 15:30:00 +08:00
昔念
7844c5b76b 1
All checks were successful
ci/woodpecker/push/my-first-workflow Pipeline was successful
2026-04-18 15:22:30 +08:00
昔念
4abd179a23 Switch Woodpecker SSH plugin image to Huawei Cloud mirror
All checks were successful
ci/woodpecker/push/my-first-workflow Pipeline was successful
2026-04-18 13:54:35 +08:00
昔念
a3e88c7357 Switch Woodpecker SSH step to ghcr drone-ssh image
Some checks failed
ci/woodpecker/push/my-first-workflow Pipeline failed
2026-04-18 13:33:18 +08:00
昔念
4e1a9a815f Switch Woodpecker SSH step to plugins/ssh image
Some checks failed
ci/woodpecker/push/my-first-workflow Pipeline failed
2026-04-18 13:22:44 +08:00
昔念
de3ae0bca2 1
Some checks failed
ci/woodpecker/push/my-first-workflow Pipeline failed
2026-04-18 13:09:12 +08:00
昔念
b1ff4d3a2a Switch Woodpecker Go image to DaoCloud mirror
Some checks failed
ci/woodpecker/push/my-first-workflow Pipeline failed
2026-04-18 12:44:59 +08:00
昔念
24b52e14c3 1
Some checks failed
ci/woodpecker/push/my-first-workflow Pipeline failed
2026-04-18 12:35:40 +08:00
昔念
2b92baf530 Use mirror image sources in Woodpecker pipeline 2026-04-18 12:24:34 +08:00
昔念
819d5f667b Set git-sync mode to push for force overwrite sync 2026-04-18 12:13:43 +08:00
昔念
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
90 changed files with 3266 additions and 15311 deletions

View File

@@ -27,5 +27,4 @@ main:
username: ${GIT_USERNAME} username: ${GIT_USERNAME}
password: ${GIT_ACCESS_TOKEN} password: ${GIT_ACCESS_TOKEN}
force: true force: true
sync_mode: push
#sync_mode: rebase

View File

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

View File

@@ -13,7 +13,7 @@ skip_clone: true
steps: steps:
# ========== 1. 替代clone拉取代码核心依赖 ========== # ========== 1. 替代clone拉取代码核心依赖 ==========
prepare: prepare:
image: alpine/git image: docker.1ms.run/alpine/git
environment: environment:
# WOODPECKER_SSH_KEY: # WOODPECKER_SSH_KEY:
# from_secret: WOODPECKER_SSH_KEY # from_secret: WOODPECKER_SSH_KEY
@@ -70,7 +70,7 @@ steps:
# ========== 4. 编译Logic服务完全参考GitHub Actions编译配置 ========== # ========== 4. 编译Logic服务完全参考GitHub Actions编译配置 ==========
build_logic: build_logic:
image: golang:1.25 image: docker.m.daocloud.io/golang:1.25
depends_on: [prepare] depends_on: [prepare]
environment: environment:
CGO_ENABLED: 0 CGO_ENABLED: 0
@@ -142,7 +142,7 @@ steps:
# ========== 6. SCP推送产物依赖编译+配置解析 ========== # ========== 6. SCP推送产物依赖编译+配置解析 ==========
scp-exe-to-servers: # 与fetch-deploy-config同级缩进2个空格 scp-exe-to-servers: # 与fetch-deploy-config同级缩进2个空格
image: appleboy/drone-scp:1.6.2 # 子元素缩进4个空格 image: docker.1ms.run/appleboy/drone-scp:1.6.2 # 子元素缩进4个空格
settings: # 子元素缩进4个空格 settings: # 子元素缩进4个空格
host: &ssh_host 43.248.3.21 host: &ssh_host 43.248.3.21
port: &ssh_port 22 port: &ssh_port 22
@@ -158,7 +158,7 @@ steps:
depends_on: # 子元素缩进4个空格 depends_on: # 子元素缩进4个空格
- build_logic # depends_on内的项缩进6个空格 - build_logic # depends_on内的项缩进6个空格
start-login-logic: start-login-logic:
image: appleboy/drone-ssh:1.6.2 image: swr.cn-north-4.myhuaweicloud.com/ddn-k8s/ghcr.io/appleboy/drone-ssh:1.7.7
depends_on: [scp-exe-to-servers] depends_on: [scp-exe-to-servers]
settings: # 子元素缩进4个空格 settings: # 子元素缩进4个空格
host: *ssh_host host: *ssh_host

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

@@ -2,6 +2,7 @@ package cool
import ( import (
"context" "context"
"reflect"
"strings" "strings"
"blazing/cool/coolconfig" "blazing/cool/coolconfig"
@@ -158,13 +159,34 @@ func (c *Controller) Page(ctx context.Context, req *PageReq) (res *BaseRes, err
// 注册控制器到路由 // 注册控制器到路由
func RegisterController(c IController) { func RegisterController(c IController) {
var ctx = context.Background() var ctx = context.Background()
var sController = &Controller{} var sController *Controller
rv := reflect.ValueOf(c)
if rv.IsValid() && rv.Kind() == reflect.Ptr {
ev := rv.Elem()
if ev.IsValid() {
field := ev.FieldByName("Controller")
if field.IsValid() && !field.IsNil() {
if ctrl, ok := field.Interface().(*Controller); ok && ctrl != nil {
sController = ctrl
}
}
}
}
if sController == nil {
sController = &Controller{}
gconv.Struct(c, &sController) gconv.Struct(c, &sController)
}
if coolconfig.Config.Eps { if coolconfig.Config.Eps {
model := sController.Service.GetModel() model := sController.Service.GetModel()
tableName := ""
if model != nil {
tableName = strings.TrimSpace(model.TableName())
}
if tableName != "" && tableName != "this_table_should_not_exist" {
columns := getModelInfo(ctx, sController.Prefix, model) columns := getModelInfo(ctx, sController.Prefix, model)
ModelInfo[sController.Prefix] = columns ModelInfo[sController.Prefix] = columns
} }
}
g.Server().Group( g.Server().Group(
sController.Prefix, func(group *ghttp.RouterGroup) { sController.Prefix, func(group *ghttp.RouterGroup) {
group.Middleware(MiddlewareHandlerResponse) group.Middleware(MiddlewareHandlerResponse)

View File

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

View File

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

View File

@@ -6,9 +6,6 @@ import (
_ "blazing/common/data/xmlres/packed" _ "blazing/common/data/xmlres/packed"
"encoding/json" "encoding/json"
"fmt" "fmt"
"os"
"path/filepath"
"strings"
"github.com/ECUST-XX/xml" "github.com/ECUST-XX/xml"
"github.com/gogf/gf/v2/os/gres" "github.com/gogf/gf/v2/os/gres"
@@ -16,27 +13,9 @@ import (
) )
var path string var path string
var diskConfigPath string
func readConfigContent(path string) []byte { func readConfigContent(path string) []byte {
content := gres.GetContent(path) return gres.GetContent(path)
if len(content) > 0 {
return content
}
if diskConfigPath == "" {
return content
}
diskPath := filepath.Join(diskConfigPath, strings.TrimPrefix(path, "config/"))
data, err := os.ReadFile(diskPath)
if err != nil {
fmt.Printf("[xmlres] readConfigContent fallback failed: path=%s disk=%s err=%v\n", path, diskPath, err)
return content
}
fmt.Printf("[xmlres] readConfigContent fallback hit: path=%s disk=%s len=%d\n", path, diskPath, len(data))
return data
} }
func getXml[T any](path string) T { func getXml[T any](path string) T {
@@ -93,9 +72,6 @@ var (
func Initfile() { func Initfile() {
//gres.Dump() //gres.Dump()
path1, _ := os.Getwd()
diskConfigPath = filepath.Join(path1, "public", "config")
path = path1 + "/public/config/"
path = "config/" path = "config/"
MapConfig = getXml[Maps](path + "210.xml") MapConfig = getXml[Maps](path + "210.xml")

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

@@ -115,7 +115,7 @@ type Move struct {
func (m *Move) UnmarshalJSON(data []byte) error { func (m *Move) UnmarshalJSON(data []byte) error {
type moveAlias struct { type moveAlias struct {
ID int `json:"ID"` ID int `json:"ID"`
Name string `json:"Name"` Name rawFlexibleString `json:"Name"`
Category int `json:"Category"` Category int `json:"Category"`
Type int `json:"Type"` Type int `json:"Type"`
Power int `json:"Power"` Power int `json:"Power"`
@@ -150,7 +150,7 @@ func (m *Move) UnmarshalJSON(data []byte) error {
*m = Move{ *m = Move{
ID: aux.ID, ID: aux.ID,
Name: aux.Name, Name: string(aux.Name),
Category: aux.Category, Category: aux.Category,
Type: aux.Type, Type: aux.Type,
Power: aux.Power, Power: aux.Power,

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 { if ws.Tcp {
return s.handleTCP(c) return s.handleTCP(c)
} }

View File

@@ -0,0 +1,73 @@
-- 初始化/修复 SPT 配置表PostgreSQL
-- 用法 sun 数据库执行本文件
BEGIN;
CREATE TABLE IF NOT EXISTS config_spt (
id BIGSERIAL PRIMARY KEY,
"createTime" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
"updateTime" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMPTZ NULL,
is_enable INTEGER NOT NULL DEFAULT 1,
remark VARCHAR(255) NOT NULL DEFAULT '',
task_id INTEGER NOT NULL,
title VARCHAR(64) NOT NULL DEFAULT '',
pet_id INTEGER NOT NULL DEFAULT 0,
online INTEGER NOT NULL DEFAULT 1,
level INTEGER NOT NULL DEFAULT 1,
enter_id INTEGER NOT NULL DEFAULT 0,
description TEXT NOT NULL DEFAULT ''
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_config_spt_task_id ON config_spt(task_id);
ALTER TABLE config_spt DROP COLUMN IF EXISTS seat_id;
INSERT INTO config_spt
(task_id, title, pet_id, online, level, enter_id, description, is_enable, remark)
VALUES
(301,'蘑菇怪',47,1,1,12,'生活在克洛斯星被艾里逊的液氮冻伤而发狂使用火焰喷射器可以使它安静下来制服它可以获得草系精灵小蘑菇',1,'破除防护罩'),
(302,'钢牙鲨',34,1,1,21,'海洋星海底的危险怪兽据说它躲藏的洞穴中有制作黑武士装的黑晶矿石记住到海底一定要穿上耐压的潜水套装',1,''),
(303,'里奥斯',42,1,2,17,'海盗艾里逊在火山被它困住战胜它有机会获得火系精灵胡里亚在火山你会用到喷水装的',1,'扑灭火焰屏障'),
(304,'阿克希亚',50,1,4,40,'塞西利亚星的守护者正义的精灵圣兽它是不可战胜的千年来一直等待着宿命的对手',1,''),
(305,'提亚斯',69,1,3,27,'云霄星出现了一只极具攻击性的变异精灵拥有很多蛋的它虽然想要努力呵护自己的孩子却力不从心看来需要大家帮帮忙啊',1,''),
(306,'雷伊',70,1,3,32,'赫尔卡星天空中划过一道闪电映出了一个酷似精灵的黑影它全身被电流包围从它的神态中可以看出它正等待来自各方的挑战',1,'雷雨天'),
(307,'纳多雷',88,1,3,106,'在双子阿尔法星上特派队遇见了一只巨大的精灵经过多次挑战后它仍然丝毫无损赛尔们是否有办法战胜这只精灵呢',1,''),
(308,'雷纳多',113,1,3,49,'彪悍的雷纳多盘踞在双子贝塔星上和双子阿尔法星的纳多雷遥相对应守护着星球上所有精灵的',1,''),
(309,'尤纳斯',132,1,4,314,'黑暗之门的制造者拥有能够抵御一切的暗影屏障和所有能量来源的黑暗之核',1,''),
(310,'魔狮迪露',187,1,4,53,'魔狮迪露具有神秘的力量能使自己的体力突破界限但同时也会受到未知的惩罚',1,''),
(311,'哈莫雷特',216,1,5,60,'拥有无比巨大的身躯集水火草三种原能为一身龙系的神秘力量使它所向无敌失忆的它似乎还有很多谜团',1,''),
(312,'奈尼芬多',264,1,4,325,'奈尼芬多是爱迪星的守护者凄美的歌声连月亮都为之倾倒据说只有音乐的力量才能够唤醒它',1,''),
(316,'厄尔塞拉',421,1,5,61,'浑身散发着各色光芒任何邪恶在她的光芒下消散无形',1,''),
(50,'卡特斯',169,1,2,110,'作为暗黑武斗场的试炼精灵守护着试炼之门它的气度和风度非同一般杀气重重很难对付',1,''),
(51,'魔牙鲨',171,1,3,503,'暗黑第一门的魔牙鲨被赋予了传说中的暗黑斗气隐藏在暗影中攻击时它的能力可以被放大增强',1,''),
(53,'贝鲁基德',174,1,3,504,'暗黑第二门的贝鲁基德暗黑火焰环绕周身凶悍的外表下藏着善战勇敢的心',1,''),
(55,'巴弗洛',177,1,3,505,'勇猛凶横的巴弗洛把守着暗黑武斗场-霹雳闪电般的羽翼攻击震荡心胸的音乐攻击让人防不胜防',1,''),
(56,'奇拉塔顿',183,1,3,505,'勇敢的奇拉塔顿驻守在暗黑武斗场-门的那一边身为大地之子的它驾驭着反物质能量纵横无敌',1,''),
(59,'西萨拉斯',195,1,3,506,'拥有强大反物质电力的暗黑-门守护者雷霆之刃震撼寰宇天地电流之剑穿透空间阻隔威慑四方',1,''),
(60,'克林卡修',192,1,4,506,'暗黑-门守护者冰雪灵兽克林卡修冰雪之爪具有猛烈的攻击力果敢不张扬的个性让它成为忍者般的精灵',1,''),
(76,'卡库',222,1,4,507,'暗黑-门是武学之门守门精灵不张扬不蛮横步步为营每出一招都会致命',1,''),
(77,'赫德卡',224,1,4,507,'驻守暗黑V-II门的铁血赫德卡有着铜墙铁壁的防守能力有着超级强力的电光炮它的防御之门你能够开启吗',1,''),
(78,'伊兰罗尼',227,1,5,507,'守护暗黑-门的伊兰罗尼是优雅得体的淑女擅长在裙摆飘飘光芒闪耀间使出杀手',1,''),
(117,'斯加尔卡',356,1,5,508,'暗黑VI-I门的守护者擅长使用暗黑电能的家伙比起折磨对手的身体斯加尔卡更喜欢震慑对手的心灵',1,''),
(118,'艾尔伊洛',297,1,5,508,'暗黑VI-II门的守护者历经了炼狱洗礼的艾尔伊洛开始崇尚爽快的攻击方式喜欢凭借精湛的技巧近距地伤害对手',1,''),
(119,'布林克克',359,1,5,508,'暗黑VI-III门的守护者拥有海妖之力的庇护体内充满着混沌的能量企图吞噬整个海洋',1,''),
(502,'魔花使者',438,1,5,509,'暗黑VII-I门的守护者比恩特的进化形态浑身散发着反物质世界中的黑暗气息散发出来的毒粉是它的致命武器',1,''),
(503,'莫尔加斯',441,1,5,509,'暗黑VII-II门的守护者莫鲁格尔的进化形态受到了反物质世界的影响浑身被黑暗所包围拥有极强的防御能力所有攻击在它面前都显得非常渺小',1,''),
(504,'萨诺拉斯',435,1,5,509,'暗黑VII-III门的守护者萨诺的进化形态经过岩浆洗礼的皮肤拥有独特的降温功能即使在极其炎热的环境下依然不受影响',1,''),
(606,'帕多尼',656,1,5,510,'暗黑-门的守护者浑身充斥着暗黑能量暗黑能量会随着它的歌神散发出来',1,''),
(607,'加洛德',659,1,5,510,'暗黑-门的守护暗黑能量的注入使它的脾气变得暴躁擅长与对手近身搏斗浑身的尖刺催生出的植物都是它进攻的利器',1,''),
(608,'萨多拉尼',661,1,5,510,'暗黑-门的守护将暗黑能量融入自身肢体变得非常结实有力虽然体积很小但是却拥有了堪比巨龙的神力',1,'')
ON CONFLICT (task_id) DO UPDATE
SET
title = EXCLUDED.title,
pet_id = EXCLUDED.pet_id,
online = EXCLUDED.online,
level = EXCLUDED.level,
enter_id = EXCLUDED.enter_id,
description = EXCLUDED.description,
is_enable = EXCLUDED.is_enable,
remark = EXCLUDED.remark,
"updateTime" = NOW();
COMMIT;

View File

@@ -82,7 +82,9 @@ func (h Controller) DASHIbeiR(req *C2s_MASTER_REWARDSR, c *player.Player) (resul
return nil, errorcode.ErrorCode(errorcode.ErrorCodes.ErrAwardAlreadyClaimed) return nil, errorcode.ErrorCode(errorcode.ErrorCodes.ErrAwardAlreadyClaimed)
} }
consumeMasterCupItems(c, requiredItems) if err := consumeMasterCupItems(c, requiredItems); err != nil {
return nil, errorcode.ErrorCode(errorcode.ErrorCodes.ErrInsufficientItems)
}
progress.Set(uint(req.ElementType)) progress.Set(uint(req.ElementType))
taskData.Data = progress.Bytes() taskData.Data = progress.Bytes()
if taskErr = c.Service.Task.SetTask(taskData); taskErr != nil { if taskErr = c.Service.Task.SetTask(taskData); taskErr != nil {
@@ -130,11 +132,14 @@ func hasEnoughMasterCupItems(c *player.Player, requiredItems []ItemS) bool {
return true return true
} }
func consumeMasterCupItems(c *player.Player, requiredItems []ItemS) { func consumeMasterCupItems(c *player.Player, requiredItems []ItemS) error {
for _, item := range requiredItems { for _, item := range requiredItems {
c.Service.Item.UPDATE(item.ItemId, -int(item.ItemCnt)) if err := c.Service.Item.UPDATE(item.ItemId, -int(item.ItemCnt)); err != nil {
return err
} }
} }
return nil
}
func appendMasterCupRewardItems(c *player.Player, result *S2C_MASTER_REWARDSR, itemList []data.ItemInfo) { func appendMasterCupRewardItems(c *player.Player, result *S2C_MASTER_REWARDSR, itemList []data.ItemInfo) {
for _, item := range itemList { for _, item := range itemList {

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

View File

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

View File

@@ -72,7 +72,7 @@ func startMapBossFight(
ai *player.AI_player, ai *player.AI_player,
fn func(model.FightOverInfo), fn func(model.FightOverInfo),
) (*fight.FightC, errorcode.ErrorCode) { ) (*fight.FightC, errorcode.ErrorCode) {
ourPets := p.GetPetInfo(100) ourPets := p.GetPetInfo(p.CurrentMapPetLevelLimit())
oppPets := ai.GetPetInfo(0) oppPets := ai.GetPetInfo(0)
if mapNode != nil && mapNode.IsGroupBoss != 0 { if mapNode != nil && mapNode.IsGroupBoss != 0 {
if len(ourPets) > 0 && len(oppPets) > 0 { if len(ourPets) > 0 && len(oppPets) > 0 {
@@ -98,8 +98,8 @@ func (Controller) OnPlayerFightNpcMonster(req *FightNpcMonsterInboundInfo, p *pl
if err = p.CanFight(); err != 0 { if err = p.CanFight(); err != 0 {
return nil, err return nil, err
} }
if req.Number > 9 { if int(req.Number) >= len(p.Data) {
return nil, errorcode.ErrorCodes.ErrSystemError return nil, errorcode.ErrorCodes.ErrPokemonNotHere
} }
refPet := p.Data[req.Number] refPet := p.Data[req.Number]
@@ -114,7 +114,7 @@ func (Controller) OnPlayerFightNpcMonster(req *FightNpcMonsterInboundInfo, p *pl
p.Fightinfo.Status = fightinfo.BattleMode.FIGHT_WITH_NPC p.Fightinfo.Status = fightinfo.BattleMode.FIGHT_WITH_NPC
p.Fightinfo.Mode = fightinfo.BattleMode.MULTI_MODE p.Fightinfo.Mode = fightinfo.BattleMode.MULTI_MODE
_, err = fight.NewFight(p, ai, p.GetPetInfo(100), ai.GetPetInfo(0), func(foi model.FightOverInfo) { _, err = fight.NewFight(p, ai, p.GetPetInfo(p.CurrentMapPetLevelLimit()), ai.GetPetInfo(0), func(foi model.FightOverInfo) {
handleNpcFightRewards(p, foi, monster) handleNpcFightRewards(p, foi, monster)
}) })
if err != 0 { if err != 0 {
@@ -236,7 +236,7 @@ func shouldGrantBossWinBonus(fightC *fight.FightC, playerID uint32, bossConfig c
func buildNpcMonsterInfo(refPet player.OgrePetInfo, mapID uint32) (*model.PetInfo, *model.PlayerInfo, errorcode.ErrorCode) { func buildNpcMonsterInfo(refPet player.OgrePetInfo, mapID uint32) (*model.PetInfo, *model.PlayerInfo, errorcode.ErrorCode) {
if refPet.ID == 0 { if refPet.ID == 0 {
return nil, nil, errorcode.ErrorCodes.ErrPokemonNotExists return nil, nil, errorcode.ErrorCodes.ErrPokemonNotHere
} }
monster := model.GenPetInfo( monster := model.GenPetInfo(

View File

@@ -13,7 +13,6 @@ import (
"sync/atomic" "sync/atomic"
"github.com/gogf/gf/v2/util/gconv"
"github.com/jinzhu/copier" "github.com/jinzhu/copier"
) )
@@ -207,32 +206,8 @@ func buildTowerMonsterInfo(towerBoss configmodel.BaseTowerConfig) (*model.Player
monsterInfo := &model.PlayerInfo{Nick: towerBoss.Name} monsterInfo := &model.PlayerInfo{Nick: towerBoss.Name}
for i, boss := range bosses { for i, boss := range bosses {
monster := model.GenPetInfo(int(boss.MonID), 24, int(boss.Nature), 0, int(boss.Lv), nil, 0) monster := model.GenPetInfo(int(boss.MonID), 24, int(boss.Nature), 0, int(boss.Lv), nil, 0)
if boss.Hp != 0 { monster.ConfigBoss(boss.PetBaseConfig)
monster.Hp = uint32(boss.Hp) appendPetEffects(monster, boss.Effect)
monster.MaxHp = uint32(boss.Hp)
}
for statIdx, prop := range boss.Prop {
if prop != 0 {
monster.Prop[statIdx] = prop
}
}
for skillIdx := 0; skillIdx < len(monster.SkillList) && skillIdx < len(boss.SKill); skillIdx++ {
if boss.SKill[skillIdx] != 0 {
monster.SkillList[skillIdx].ID = boss.SKill[skillIdx]
}
}
effects := service.NewEffectService().Args(boss.Effect)
for _, effect := range effects {
monster.EffectInfo = append(monster.EffectInfo, model.PetEffectInfo{
Idx: uint16(effect.ID),
EID: gconv.Uint16(effect.Eid),
Args: gconv.Ints(effect.Args),
})
}
monster.CatchTime = uint32(i) monster.CatchTime = uint32(i)
monsterInfo.PetList = append(monsterInfo.PetList, *monster) monsterInfo.PetList = append(monsterInfo.PetList, *monster)
} }

View File

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

View File

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

View File

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

View File

@@ -19,20 +19,13 @@ func (h Controller) SavePetBagOrder(
return nil, 0 return nil, 0
} }
// PetRetrieveFromWarehouse 领回仓库精灵 // PetRetrieveFromWarehouse 从放生仓库领回精灵
func (h Controller) PetRetrieveFromWarehouse( func (h Controller) PetRetrieveFromWarehouse(
data *PET_RETRIEVE, player *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) { data *PET_RETRIEVE, player *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
if _, ok := player.FindPetBagSlot(data.CatchTime); ok { if !player.Service.Pet.UpdateFree(data.CatchTime, 1, 0) {
return nil, 0 return nil, errorcode.ErrorCodes.ErrPokemonIDMismatch
} }
petInfo := player.Service.Pet.PetInfoOneByCatchTime(data.CatchTime)
if petInfo == nil {
return nil, 0
}
player.AddPetToAvailableBag(petInfo.Data)
return nil, 0 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 return nil, errorcode.ErrorCodes.ErrInsufficientItemsMulti
} }
if branch.EvolvItem != 0 { if branch.EvolvItem != 0 {
c.Service.Item.UPDATE(uint32(branch.EvolvItem), -branch.EvolvItemCount) if err := c.Service.Item.UPDATE(uint32(branch.EvolvItem), -branch.EvolvItemCount); err != nil {
return nil, errorcode.ErrorCodes.ErrInsufficientItemsMulti
}
} }
currentPet.ID = uint32(branch.MonTo) currentPet.ID = uint32(branch.MonTo)

View File

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

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

View File

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

View File

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

View File

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

View File

@@ -67,8 +67,9 @@ func (h Controller) GetPetLearnableSkills(
data *GetPetLearnableSkillsInboundInfo, data *GetPetLearnableSkillsInboundInfo,
c *player.Player, c *player.Player,
) (result *GetPetLearnableSkillsOutboundInfo, err errorcode.ErrorCode) { ) (result *GetPetLearnableSkillsOutboundInfo, err errorcode.ErrorCode) {
_, currentPet, ok := c.FindPet(data.CatchTime) slot, ok := c.FindPetBagSlot(data.CatchTime)
if !ok { currentPet := slot.PetInfoPtr()
if !ok || currentPet == nil {
return nil, errorcode.ErrorCodes.ErrPokemonNotExists return nil, errorcode.ErrorCodes.ErrPokemonNotExists
} }
@@ -81,8 +82,9 @@ func (h Controller) GetPetLearnableSkills(
func (h Controller) SetPetSkill(data *ChangeSkillInfo, c *player.Player) (result *pet.ChangeSkillOutInfo, err errorcode.ErrorCode) { func (h Controller) SetPetSkill(data *ChangeSkillInfo, c *player.Player) (result *pet.ChangeSkillOutInfo, err errorcode.ErrorCode) {
const setSkillCost = 50 const setSkillCost = 50
_, currentPet, ok := c.FindPet(data.CatchTime) slot, ok := c.FindPetBagSlot(data.CatchTime)
if !ok { currentPet := slot.PetInfoPtr()
if !ok || currentPet == nil {
return nil, errorcode.ErrorCodes.ErrSystemBusy return nil, errorcode.ErrorCodes.ErrSystemBusy
} }
@@ -147,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) { func (h Controller) SortPetSkills(data *C2S_Skill_Sort, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
const skillSortCost = 50 const skillSortCost = 50
_, currentPet, ok := c.FindPet(data.CapTm) slot, ok := c.FindPetBagSlot(data.CapTm)
if !ok { currentPet := slot.PetInfoPtr()
if !ok || currentPet == nil {
return nil, errorcode.ErrorCodes.ErrPokemonNotExists return nil, errorcode.ErrorCodes.ErrPokemonNotExists
} }
@@ -205,8 +208,9 @@ func (h Controller) CommitPetSkills(
const setSkillCost = 50 const setSkillCost = 50
const skillSortCost = 50 const skillSortCost = 50
_, currentPet, ok := c.FindPet(data.CatchTime) slot, ok := c.FindPetBagSlot(data.CatchTime)
if !ok { currentPet := slot.PetInfoPtr()
if !ok || currentPet == nil {
return nil, errorcode.ErrorCodes.ErrPokemonNotExists return nil, errorcode.ErrorCodes.ErrPokemonNotExists
} }

View File

@@ -4,6 +4,7 @@ import (
"blazing/common/socket/errorcode" "blazing/common/socket/errorcode"
logicplayer "blazing/logic/service/player" logicplayer "blazing/logic/service/player"
"blazing/logic/service/user" "blazing/logic/service/user"
baseservice "blazing/modules/base/service"
configservice "blazing/modules/config/service" configservice "blazing/modules/config/service"
playerservice "blazing/modules/player/service" playerservice "blazing/modules/player/service"
"strings" "strings"
@@ -14,6 +15,11 @@ import (
func (h Controller) CDK(data *C2S_GET_GIFT_COMPLETE, player *logicplayer.Player) (result *user.S2C_GET_GIFT_COMPLETE, err errorcode.ErrorCode) { func (h Controller) CDK(data *C2S_GET_GIFT_COMPLETE, player *logicplayer.Player) (result *user.S2C_GET_GIFT_COMPLETE, err errorcode.ErrorCode) {
result = &user.S2C_GET_GIFT_COMPLETE{} result = &user.S2C_GET_GIFT_COMPLETE{}
userInfo := baseservice.NewBaseSysUserService().GetPerson(data.Head.UserID)
if userInfo == nil || userInfo.QQ == 0 {
return nil, errorcode.ErrorCodes.ErrCannotPerformAction
}
cdkCode := strings.Trim(data.PassText, "\x00") cdkCode := strings.Trim(data.PassText, "\x00")
cdkService := configservice.NewCdkService() cdkService := configservice.NewCdkService()
now := time.Now() now := time.Now()
@@ -35,7 +41,12 @@ func (h Controller) CDK(data *C2S_GET_GIFT_COMPLETE, player *logicplayer.Player)
return nil, errorcode.ErrorCodes.ErrMolecularCodeGiftsGone return nil, errorcode.ErrorCodes.ErrMolecularCodeGiftsGone
} }
reward, grantErr := playerservice.NewCdkService(data.Head.UserID).GrantConfigReward(uint32(r.ID)) reward, grantErr := playerservice.NewCdkService(data.Head.UserID).GrantConfigReward(
uint32(r.ID),
func(itemID uint32, count int64) bool {
return player.ItemAdd(int64(itemID), count)
},
)
if grantErr != nil { if grantErr != nil {
return nil, errorcode.ErrorCodes.ErrSystemError return nil, errorcode.ErrorCodes.ErrSystemError
} }

View File

@@ -175,7 +175,7 @@ func (f *FightC) Over(c common.PlayerI, res model.EnumBattleOverReason) {
// } // }
f.overl.Do(func() { f.overl.Do(func() {
f.Reason = res f.Reason = normalizeFightOverReason(res)
if f.GetInputByPlayer(c, true) != nil { if f.GetInputByPlayer(c, true) != nil {
f.WinnerId = f.GetInputByPlayer(c, true).UserID f.WinnerId = f.GetInputByPlayer(c, true).UserID
} }
@@ -370,7 +370,7 @@ func (f *FightC) collectFightPetInfos(inputs []*input.Input) []info.FightPetInfo
Hp: currentPet.Info.Hp, Hp: currentPet.Info.Hp,
MaxHp: currentPet.Info.MaxHp, MaxHp: currentPet.Info.MaxHp,
Level: currentPet.Info.Level, Level: currentPet.Info.Level,
Catchable: uint32(fighter.CanCapture), Catchable: fightPetCatchableFlag(fighter.CanCapture),
} }
if fighter.AttackValue != nil { if fighter.AttackValue != nil {
fightInfo.Prop = fighter.AttackValue.Prop fightInfo.Prop = fighter.AttackValue.Prop
@@ -380,6 +380,13 @@ func (f *FightC) collectFightPetInfos(inputs []*input.Input) []info.FightPetInfo
return infos return infos
} }
func fightPetCatchableFlag(catchRate int) uint32 {
if catchRate > 0 {
return 1
}
return 0
}
// checkBothPlayersReady 检查PVP战斗中双方是否都已准备完成 // checkBothPlayersReady 检查PVP战斗中双方是否都已准备完成
// 参数c为当前准备的玩家返回true表示双方均准备完成 // 参数c为当前准备的玩家返回true表示双方均准备完成
func (f *FightC) checkBothPlayersReady(currentPlayer common.PlayerI) bool { func (f *FightC) checkBothPlayersReady(currentPlayer common.PlayerI) bool {

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,7 @@
package effect package effect
import ( import (
"blazing/logic/service/fight/input"
"blazing/logic/service/fight/node" "blazing/logic/service/fight/node"
) )
@@ -41,14 +42,16 @@ type Effect5 struct {
// 技能触发时调用 // 技能触发时调用
// ----------------------------------------------------------- // -----------------------------------------------------------
func (e *Effect5) Skill_Use() bool { func (e *Effect5) Skill_Use() bool {
// 概率判定 // 概率判定
ok, _, _ := e.Input.Player.Roll(e.SideEffectArgs[1], 100) ok, _, _ := e.Input.Player.Roll(e.SideEffectArgs[1], 100)
if !ok { if !ok {
return true 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 return true
} }

View File

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

View File

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

View File

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

View File

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

View File

@@ -83,6 +83,10 @@ func (e *Effect201) OnSkill() bool {
return true return true
} }
if !carrier.IsMultiInputBattle() {
return true
}
divisorIndex := len(args) - 1 divisorIndex := len(args) - 1
if len(args) > 1 { if len(args) > 1 {
divisorIndex = 1 divisorIndex = 1
@@ -110,10 +114,6 @@ func (e *Effect201) OnSkill() bool {
return true return true
} }
if !carrier.IsMultiInputBattle() {
return true
}
team := carrier.Team team := carrier.Team
if len(team) == 0 { if len(team) == 0 {
team = []*input.Input{carrier} team = []*input.Input{carrier}

View File

@@ -42,6 +42,27 @@ func TestEffect201HealAllIgnoredInSingleInputBattle(t *testing.T) {
} }
} }
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) { func TestEffect201HealAllWorksInMultiInputBattle(t *testing.T) {
carrier := newEffect201TestInput(40, 100) carrier := newEffect201TestInput(40, 100)
ally := newEffect201TestInput(10, 80) ally := newEffect201TestInput(10, 80)

View File

@@ -207,11 +207,17 @@ func registerSelfDamageOnSkillEffects() {
}) })
} }
e.Ctx().Opp.Damage(e.Ctx().Our, &info.DamageZone{ 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, Type: info.DamageType.Fixed,
Damage: opponentDamage, Damage: opponentDamage,
}) })
return true return true
})
return true
}, },
556: func(e *onSkillRegistrarEffect) bool { 556: func(e *onSkillRegistrarEffect) bool {
currentHP := e.Ctx().Our.CurPet[0].Info.Hp currentHP := e.Ctx().Our.CurPet[0].Info.Hp
@@ -241,11 +247,17 @@ func registerSelfDamageSkillUseEffects() {
Type: info.DamageType.Fixed, Type: info.DamageType.Fixed,
Damage: damage, Damage: damage,
}) })
e.Ctx().Opp.Damage(e.Ctx().Our, &info.DamageZone{ 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, Type: info.DamageType.Fixed,
Damage: damage, Damage: damage,
}) })
return true return true
})
return true
}, },
112: func(e *skillUseRegistrarEffect) bool { 112: func(e *skillUseRegistrarEffect) bool {
e.Ctx().Our.Damage(e.Ctx().Our, &info.DamageZone{ e.Ctx().Our.Damage(e.Ctx().Our, &info.DamageZone{
@@ -253,9 +265,23 @@ func registerSelfDamageSkillUseEffects() {
Damage: alpacadecimal.NewFromInt(int64(e.Ctx().Our.CurPet[0].Info.MaxHp)), Damage: alpacadecimal.NewFromInt(int64(e.Ctx().Our.CurPet[0].Info.MaxHp)),
}) })
damage := int64(grand.N(250, 300)) damage := int64(grand.N(250, 300))
e.Ctx().Opp.Damage(e.Ctx().Our, &info.DamageZone{ 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, Type: info.DamageType.Fixed,
Damage: alpacadecimal.Min(alpacadecimal.NewFromInt(damage), e.Ctx().Opp.CurPet[0].GetHP().Sub(alpacadecimal.NewFromInt(1))), Damage: alpacadecimal.Min(alpacadecimal.NewFromInt(damage), remainHP),
})
return true
}) })
return true return true
}, },
@@ -280,28 +306,44 @@ func registerSelfDamageSkillUseEffects() {
randomDamage = grand.N(minDamage, maxDamage) randomDamage = grand.N(minDamage, maxDamage)
} }
remainHP := 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 { if remainHP.Cmp(alpacadecimal.Zero) <= 0 {
return true return true
} }
damage := alpacadecimal.Min(alpacadecimal.NewFromInt(int64(randomDamage)), remainHP) damage := alpacadecimal.Min(alpacadecimal.NewFromInt(int64(randomDamage)), remainHP)
e.Ctx().Opp.Damage(e.Ctx().Our, &info.DamageZone{ target.Damage(e.Ctx().Our, &info.DamageZone{
Type: info.DamageType.Fixed, Type: info.DamageType.Fixed,
Damage: damage, Damage: damage,
}) })
return true return true
})
return true
}, },
1380: func(e *skillUseRegistrarEffect) bool { 1380: func(e *skillUseRegistrarEffect) bool {
if len(e.Args()) < 3 { if len(e.Args()) < 3 {
return true return true
} }
applyAllPropDown(e.Ctx().Our, e.Ctx().Opp, int8(e.Args()[0].IntPart())) 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())) sub := e.Ctx().Our.InitEffect(input.EffectType.Sub, 1380, int(e.Args()[1].IntPart()), int(e.Args()[2].IntPart()))
if sub != nil { if sub != nil {
e.Ctx().Opp.AddEffect(e.Ctx().Our, sub) target.AddEffect(e.Ctx().Our, sub)
} }
return true
})
e.Ctx().Our.Damage(e.Ctx().Our, &info.DamageZone{ e.Ctx().Our.Damage(e.Ctx().Our, &info.DamageZone{
Type: info.DamageType.Fixed, Type: info.DamageType.Fixed,
Damage: e.Ctx().Our.CurPet[0].GetHP(), Damage: e.Ctx().Our.CurPet[0].GetHP(),

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

@@ -7,12 +7,21 @@ import "blazing/modules/player/model"
// 0=normal end 1=player lost/offline 2=overtime 3=draw 4=system error 5=npc escape. // 0=normal end 1=player lost/offline 2=overtime 3=draw 4=system error 5=npc escape.
func buildFightOverPayload(over model.FightOverInfo) *model.FightOverInfo { func buildFightOverPayload(over model.FightOverInfo) *model.FightOverInfo {
payload := over payload := over
payload.Reason = mapFightOverReasonFor2506(over.Reason) payload.Reason = model.EnumBattleOverReason(mapUnifiedFightOverReason(over.Reason))
return &payload return &payload
} }
func mapFightOverReasonFor2506(reason model.EnumBattleOverReason) model.EnumBattleOverReason { func normalizeFightOverReason(reason model.EnumBattleOverReason) model.EnumBattleOverReason {
switch reason { 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: case model.BattleOverReason.PlayerOffline:
return 1 return 1
case model.BattleOverReason.PlayerOVerTime: case model.BattleOverReason.PlayerOVerTime:
@@ -20,12 +29,12 @@ func mapFightOverReasonFor2506(reason model.EnumBattleOverReason) model.EnumBatt
case model.BattleOverReason.NOTwind: case model.BattleOverReason.NOTwind:
return 3 return 3
case model.BattleOverReason.PlayerEscape: case model.BattleOverReason.PlayerEscape:
// Player-initiated escape is handled by 2410 on the flash side; 2506 should return 5
// still land in a non-error bucket instead of "system error".
return 1
case model.BattleOverReason.Cacthok, model.BattleOverReason.DefaultEnd:
return 0
default: default:
return 4 return 4
} }
} }
func mapFightOverReasonFor2506(reason model.EnumBattleOverReason) model.EnumBattleOverReason {
return model.EnumBattleOverReason(mapUnifiedFightOverReason(reason))
}

View File

@@ -7,6 +7,8 @@ import (
"blazing/logic/service/fight/action" "blazing/logic/service/fight/action"
"blazing/logic/service/fight/info" "blazing/logic/service/fight/info"
"blazing/logic/service/fight/input" "blazing/logic/service/fight/input"
_ "blazing/logic/service/fight/itemover"
_ "blazing/logic/service/fight/rule"
"blazing/modules/player/model" "blazing/modules/player/model"
"reflect" "reflect"
@@ -133,7 +135,20 @@ func (f *FightC) getSkillParticipants(skillAction *action.SelectSkillAction) (*i
if skillAction == nil { if skillAction == nil {
return nil, 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 上下文。 // setEffectSkillContext 统一设置技能阶段 effect 上下文。
@@ -184,17 +199,46 @@ func (f *FightC) collectAttackValues(inputs []*input.Input) []model.AttackValue
continue continue
} }
attackValue := *fighter.AttackValue attackValue := *fighter.AttackValue
if attackValue.SkillID == 0 {
continue
}
attackValue.ActorIndex = uint32(actorIndex) attackValue.ActorIndex = uint32(actorIndex)
values = append(values, attackValue) values = append(values, attackValue)
} }
return values 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
}
if currentPet := fighter.CurrentPet(); currentPet != nil {
// 始终以当前战斗态回填血量和技能 PP避免广播沿用旧缓存。
attackValue.RemainHp = int32(currentPet.Info.Hp)
attackValue.MaxHp = currentPet.Info.MaxHp
attackValue.SkillList = append(attackValue.SkillList[:0], currentPet.Info.SkillList...)
attackValue.SkillListLen = uint32(len(attackValue.SkillList))
}
return attackValue
}
func (f *FightC) buildNoteUseSkillOutboundInfo() info.NoteUseSkillOutboundInfo { func (f *FightC) buildNoteUseSkillOutboundInfo() info.NoteUseSkillOutboundInfo {
result := info.NoteUseSkillOutboundInfo{} result := info.NoteUseSkillOutboundInfo{}
result.FirstAttackInfo = append(result.FirstAttackInfo, f.collectAttackValues(f.Our)...) if f.First != nil {
result.SecondAttackInfo = append(result.SecondAttackInfo, f.collectAttackValues(f.Opp)...) result.FirstAttackInfo = f.buildAttackValueForBroadcast(f.First, f.First.TeamSlotIndex())
}
if f.Second != nil {
result.SecondAttackInfo = f.buildAttackValueForBroadcast(f.Second, f.Second.TeamSlotIndex())
}
return result return result
} }
@@ -210,6 +254,15 @@ func (f *FightC) roundOpponentInput(attacker *input.Input) *input.Input {
return nil 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 处理战斗回合逻辑 // enterturn 处理战斗回合逻辑
// 回合有先手方和后手方,同时有攻击方和被攻击方 // 回合有先手方和后手方,同时有攻击方和被攻击方
func (f *FightC) enterturn(firstAttack, secondAttack *action.SelectSkillAction) { func (f *FightC) enterturn(firstAttack, secondAttack *action.SelectSkillAction) {
@@ -289,25 +342,32 @@ func (f *FightC) enterturn(firstAttack, secondAttack *action.SelectSkillAction)
} }
} }
if firstAttack == nil && secondAttack == nil { skipActionStage := firstAttack == nil && secondAttack == nil
firstAttack, secondAttack = secondAttack, firstAttack //互换先手权
f.First, f.Second = f.Second, f.First
}
var attacker, defender *input.Input var attacker, defender *input.Input
f.TrueFirst = f.First f.TrueFirst = f.First
//开始回合操作 //开始回合操作。若双方本回合都未出手,则只走完整回合流程,不进入动作阶段。
for i := 0; i < 2; i++ { for i := 0; !skipActionStage && i < 2; i++ {
var originalSkill *info.SkillEntity //原始技能 var originalSkill *info.SkillEntity //原始技能
var currentSkill *info.SkillEntity //当前技能 var currentSkill *info.SkillEntity //当前技能
var currentAction *action.SelectSkillAction var currentAction *action.SelectSkillAction
if i == 0 { if i == 0 {
currentAction = firstAttack currentAction = firstAttack
if currentAction == nil {
continue
}
attacker, defender = f.getSkillParticipants(firstAttack) attacker, defender = f.getSkillParticipants(firstAttack)
originalSkill = f.copySkill(firstAttack) originalSkill = f.copySkill(firstAttack)
//先手阶段,先修复后手效果 //先手阶段,先修复后手效果
f.Second.RecoverEffect() f.Second.RecoverEffect()
} else { } else {
currentAction = secondAttack currentAction = secondAttack
if currentAction == nil {
continue
}
if shouldSkipSecondAction(f.First, f.Second) {
secondAttack = nil
continue
}
attacker, defender = f.getSkillParticipants(secondAttack) attacker, defender = f.getSkillParticipants(secondAttack)
originalSkill = f.copySkill(secondAttack) originalSkill = f.copySkill(secondAttack)
//取消后手历史效果 //取消后手历史效果
@@ -462,11 +522,13 @@ func (f *FightC) enterturn(firstAttack, secondAttack *action.SelectSkillAction)
// }) // })
return return
} }
if attackValueResult.FirstAttackInfo.UserID != 0 || attackValueResult.SecondAttackInfo.UserID != 0 {
f.BroadcastPlayers(func(p common.PlayerI) { f.BroadcastPlayers(func(p common.PlayerI) {
if !f.LegacyGroupProtocol { if !f.LegacyGroupProtocol {
f.sendFightPacket(p, fightPacketSkillResult, &attackValueResult) f.sendFightPacket(p, fightPacketSkillResult, &attackValueResult)
} }
}) })
}
f.Broadcast(func(fighter *input.Input) { f.Broadcast(func(fighter *input.Input) {
fighter.CanChange = 0 fighter.CanChange = 0
}) })
@@ -495,9 +557,9 @@ func (f *FightC) TURNOVER(cur *input.Input) {
if f.IsWin(f.GetInputByPlayer(cur.Player, true)) { //然后检查是否战斗结束 if f.IsWin(f.GetInputByPlayer(cur.Player, true)) { //然后检查是否战斗结束
f.FightOverInfo.WinnerId = f.GetInputByPlayer(cur.Player, true).UserID 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.WinnerId = f.FightOverInfo.WinnerId
f.Reason = model.BattleOverReason.DefaultEnd f.Reason = f.FightOverInfo.Reason
f.closefight = true f.closefight = true
// break // break

View File

@@ -8,6 +8,11 @@ import (
"blazing/modules/player/model" "blazing/modules/player/model"
) )
// <!--
// GBTL:
// 1. AtkNum:本技能同时攻击数量, 默认:1(不能为0)
// 2. AtkType:攻击类型: 0:所有人, 1:仅己方, 2:仅对方, 3:仅自己, 默认:2
// -->
const ( const (
groupCmdReadyToFight uint32 = 7555 groupCmdReadyToFight uint32 = 7555
groupCmdReadyFightFinish uint32 = 7556 groupCmdReadyFightFinish uint32 = 7556
@@ -426,38 +431,15 @@ func (f *FightC) buildLegacyGroupOverInfo(over *model.FightOverInfo) *legacyGrou
} }
func mapLegacyGroupFightOverReason(reason model.EnumBattleOverReason) uint32 { func mapLegacyGroupFightOverReason(reason model.EnumBattleOverReason) uint32 {
switch reason { return mapUnifiedFightOverReason(reason)
case model.BattleOverReason.PlayerOffline:
return 2
case model.BattleOverReason.PlayerOVerTime:
return 3
case model.BattleOverReason.NOTwind:
return 4
case model.BattleOverReason.DefaultEnd:
return 1
case model.BattleOverReason.PlayerEscape:
return 6
default:
return 5
}
} }
func resolveLegacyGroupFightOverReason(over *model.FightOverInfo) uint32 { func resolveLegacyGroupFightOverReason(over *model.FightOverInfo) uint32 {
if over == nil { if over == nil {
return 5 return mapUnifiedFightOverReason(0)
}
switch over.Reason {
case model.BattleOverReason.PlayerOffline:
return 2
case model.BattleOverReason.PlayerOVerTime:
return 3
case model.BattleOverReason.PlayerEscape:
return 6
case model.BattleOverReason.NOTwind:
return 4
} }
if over.WinnerId != 0 { if over.WinnerId != 0 {
return 1 return mapUnifiedFightOverReason(0)
} }
return mapLegacyGroupFightOverReason(over.Reason) return mapLegacyGroupFightOverReason(over.Reason)
} }
@@ -515,15 +497,23 @@ func (f *FightC) sendLegacyRoundBroadcast(firstAttack, secondAttack *action.Sele
if f == nil || !f.LegacyGroupProtocol { if f == nil || !f.LegacyGroupProtocol {
return return
} }
if firstAttack != nil { if f.legacySkillExecuted(firstAttack) {
f.sendLegacyGroupSkillHurt(firstAttack) f.sendLegacyGroupSkillHurt(firstAttack)
} }
if secondAttack != nil { if f.legacySkillExecuted(secondAttack) {
f.sendLegacyGroupSkillHurt(secondAttack) f.sendLegacyGroupSkillHurt(secondAttack)
} }
f.sendLegacyGroupBoutDone() 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) { func (f *FightC) sendLegacyGroupSkillHurt(skillAction *action.SelectSkillAction) {
if f == nil || !f.LegacyGroupProtocol || skillAction == nil { if f == nil || !f.LegacyGroupProtocol || skillAction == nil {
return return
@@ -603,10 +593,10 @@ func (f *FightC) buildLegacyGroupSkillAttackInfo(skillAction *action.SelectSkill
if attackValue == nil { if attackValue == nil {
attackValue = info.NewAttackValue(self.UserID) attackValue = info.NewAttackValue(self.UserID)
} }
if skillAction != nil && skillAction.SkillEntity != nil { if attackValue.SkillID != 0 {
result.MoveID = uint32(skillAction.SkillEntity.XML.ID)
} else {
result.MoveID = attackValue.SkillID result.MoveID = attackValue.SkillID
} else if skillAction != nil && skillAction.SkillEntity != nil {
result.MoveID = uint32(skillAction.SkillEntity.XML.ID)
} }
result.IsCrit = attackValue.IsCritical result.IsCrit = attackValue.IsCritical
result.EffectName = attackValue.State result.EffectName = attackValue.State

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 战斗技能使用通知的出站信息结构体 // NoteUseSkillOutboundInfo 战斗技能使用通知的出站信息结构体
type NoteUseSkillOutboundInfo struct { type NoteUseSkillOutboundInfo struct {
FirstAttackInfoLen uint32 `struc:"sizeof=FirstAttackInfo"` FirstAttackInfo model.AttackValue // 本轮先手方精灵在释放技能结束后的状态
FirstAttackInfo []model.AttackValue // 本轮手方精灵在释放技能结束后的状态 SecondAttackInfo model.AttackValue // 本轮手方精灵在释放技能结束后的状态
SecondAttackInfoLen uint32 `struc:"sizeof=SecondAttackInfo"`
SecondAttackInfo []model.AttackValue // 本轮后手方精灵在释放技能结束后的状态
} }
type FightStartOutboundInfo struct { type FightStartOutboundInfo struct {

View File

@@ -76,7 +76,7 @@ func (f *FightC) battleLoop() {
if player := f.primaryOppPlayer(); player != nil { if player := f.primaryOppPlayer(); player != nil {
f.WinnerId = player.GetInfo().UserID f.WinnerId = player.GetInfo().UserID
} }
f.Reason = model.BattleOverReason.DefaultEnd f.Reason = normalizeFightOverReason(model.BattleOverReason.DefaultEnd)
f.FightOverInfo.WinnerId = f.WinnerId f.FightOverInfo.WinnerId = f.WinnerId
f.FightOverInfo.Reason = f.Reason f.FightOverInfo.Reason = f.Reason
f.closefight = true f.closefight = true
@@ -86,7 +86,7 @@ func (f *FightC) battleLoop() {
if player := f.primaryOurPlayer(); player != nil { if player := f.primaryOurPlayer(); player != nil {
f.WinnerId = player.GetInfo().UserID f.WinnerId = player.GetInfo().UserID
} }
f.Reason = model.BattleOverReason.DefaultEnd f.Reason = normalizeFightOverReason(model.BattleOverReason.DefaultEnd)
f.FightOverInfo.WinnerId = f.WinnerId f.FightOverInfo.WinnerId = f.WinnerId
f.FightOverInfo.Reason = f.Reason f.FightOverInfo.Reason = f.Reason
f.closefight = true f.closefight = true
@@ -586,6 +586,7 @@ func (f *FightC) handleItemAction(a *action.UseItemAction) {
if r <= 0 { if r <= 0 {
return return
} }
source.Player.(*player.Player).Service.Item.UPDATE(a.ItemID, -1) source.Player.(*player.Player).Service.Item.UPDATE(a.ItemID, -1)
switch { switch {

View File

@@ -297,9 +297,6 @@ func buildFight(opts *fightBuildOptions) (*FightC, errorcode.ErrorCode) {
f.bindInputFightContext(f.Our, f.Opp) f.bindInputFightContext(f.Our, f.Opp)
f.linkTeamViews() f.linkTeamViews()
f.ReadyInfo.OurInfo, f.ReadyInfo.OurPetList = initfightready(f.primaryOur())
f.ReadyInfo.OpponentInfo, f.ReadyInfo.OpponentPetList = initfightready(f.primaryOpp())
loadtime := 120 * time.Second loadtime := 120 * time.Second
if f.Info.Status == info.BattleMode.FIGHT_WITH_NPC { if f.Info.Status == info.BattleMode.FIGHT_WITH_NPC {
if opp := f.primaryOpp(); opp != nil { if opp := f.primaryOpp(); opp != nil {
@@ -313,6 +310,9 @@ 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() f.FightStartOutboundInfo = f.buildFightStartInfo()
f.BroadcastPlayers(func(p common.PlayerI) { f.BroadcastPlayers(func(p common.PlayerI) {

View File

@@ -13,6 +13,7 @@ import (
type stubPlayer struct { type stubPlayer struct {
info model.PlayerInfo info model.PlayerInfo
sentCmds []uint32
} }
func (*stubPlayer) ApplyPetDisplayInfo(*spaceinfo.SimpleInfo) {} func (*stubPlayer) ApplyPetDisplayInfo(*spaceinfo.SimpleInfo) {}
@@ -26,7 +27,7 @@ func (*stubPlayer) SetFightC(common.FightI) {}
func (*stubPlayer) QuitFight() {} func (*stubPlayer) QuitFight() {}
func (*stubPlayer) MessWin(bool) {} func (*stubPlayer) MessWin(bool) {}
func (*stubPlayer) CanFight() errorcode.ErrorCode { return 0 } 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 (*stubPlayer) GetPetInfo(uint32) []model.PetInfo { return nil }
func TestFightActionEnvelopeEncodedTargetIndex(t *testing.T) { func TestFightActionEnvelopeEncodedTargetIndex(t *testing.T) {
@@ -111,3 +112,70 @@ func TestBuildFightStateStartEnvelope(t *testing.T) {
t.Fatalf("unexpected right fighter snapshot: %+v", envelope.Right[0]) 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)
}
}
func TestBuildNoteUseSkillOutboundInfoUsesCurrentPetSkillPP(t *testing.T) {
player := &stubPlayer{info: model.PlayerInfo{UserID: 1001}}
fighter := input.NewInput(nil, player)
fighter.InitAttackValue()
fighter.AttackValue.SkillID = 300
fighter.AttackValue.SkillList = []model.SkillInfo{{ID: 300, PP: 1}}
fighter.AttackValue.SkillListLen = 1
fighter.AttackValue.RemainHp = 50
fighter.AttackValue.MaxHp = 100
currentPet := fightinfo.CreateBattlePetEntity(model.PetInfo{
ID: 11,
Name: "Alpha",
Level: 20,
Hp: 50,
MaxHp: 100,
CatchTime: 101,
SkillList: []model.SkillInfo{{ID: 300, PP: 0}},
})
currentPet.BindController(player.info.UserID)
fighter.SetCurPetAt(0, currentPet)
fc := &FightC{First: fighter}
result := fc.buildNoteUseSkillOutboundInfo()
if len(result.FirstAttackInfo.SkillList) != 1 {
t.Fatalf("expected one skill in broadcast snapshot, got %+v", result.FirstAttackInfo.SkillList)
}
if result.FirstAttackInfo.SkillList[0].PP != 0 {
t.Fatalf("expected broadcast PP to come from current pet state, got %+v", result.FirstAttackInfo.SkillList)
}
}

View File

@@ -51,6 +51,9 @@ func (p *Player) IsMatch(t configmodel.Event) bool {
if len(p.Info.PetList) == 0 { if len(p.Info.PetList) == 0 {
return false return false
} }
if p.Info.PetList[0].Hp == 0 {
return false
}
firstPetID := int32(p.Info.PetList[0].ID) firstPetID := int32(p.Info.PetList[0].ID)
_, ok := lo.Find(t.FirstSprites, func(item int32) bool { _, 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 package player
import "blazing/modules/player/model"
type AI_player struct { type AI_player struct {
baseplayer baseplayer
CanCapture int CanCapture int
BossScript string 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 return p.Info
} }
func (p *baseplayer) GetPetInfo(limitlevel uint32) []model.PetInfo { func ApplyPetLevelLimit(pet model.PetInfo, limitlevel uint32) model.PetInfo {
originalHP := pet.Hp
var ret []model.PetInfo pet.CalculatePetPane(limitlevel)
pet.Hp = utils.Min(originalHP, pet.MaxHp)
for _, pet := range p.Info.PetList { return pet
if limitlevel > 0 {
pet.Level = utils.Min(pet.Level, limitlevel)
} }
ret = append(ret, pet) func (p *baseplayer) GetPetInfo(limitlevel uint32) []model.PetInfo {
ret := make([]model.PetInfo, 0, len(p.Info.PetList))
for _, pet := range p.Info.PetList {
ret = append(ret, ApplyPetLevelLimit(pet, limitlevel))
} }
return ret return ret
} }

View File

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

View File

@@ -25,6 +25,13 @@ func (slot PetBagSlot) PetInfo() model.PetInfo {
return slot.info return slot.info
} }
func (slot PetBagSlot) PetInfoPtr() *model.PetInfo {
if !slot.IsValid() {
return nil
}
return &(*slot.list)[slot.index]
}
func (slot PetBagSlot) IsMainBag() bool { func (slot PetBagSlot) IsMainBag() bool {
return slot.main return slot.main
} }
@@ -99,12 +106,20 @@ func validatePetBagOrder(
return true 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 返回主背包和并列备用精灵列表。 // GetUserBagPetInfo 返回主背包和并列备用精灵列表。
func (p *Player) GetUserBagPetInfo() *pet.GetUserBagPetInfoOutboundInfo { func (p *Player) GetUserBagPetInfo(limitlevel uint32) *pet.GetUserBagPetInfoOutboundInfo {
result := &pet.GetUserBagPetInfoOutboundInfo{ result := &pet.GetUserBagPetInfoOutboundInfo{
PetList: p.Info.PetList, PetList: buildLimitedPetList(p.Info.PetList, limitlevel),
BackupPetList: p.Info.BackupPetList, BackupPetList: buildLimitedPetList(p.Info.BackupPetList, limitlevel),
} }
return result 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) 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 检查玩家是否可以进行战斗 // CanFight 检查玩家是否可以进行战斗
// 0无战斗1PVP2,BOOS,3PVE // 0无战斗1PVP2,BOOS,3PVE
func (p *Player) CanFight() errorcode.ErrorCode { func (p *Player) CanFight() errorcode.ErrorCode {
@@ -419,7 +434,15 @@ func (p *Player) ItemAdd(ItemId, ItemCnt int64) (result bool) {
p.SendPack(t1.Pack(nil)) //准备包由各自发,因为协议不一样 p.SendPack(t1.Pack(nil)) //准备包由各自发,因为协议不一样
return false 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 return true
} }

View File

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

View File

@@ -30,6 +30,9 @@ var (
// service.NewServerService().SetServerScreen(0, "sss") // service.NewServerService().SetServerScreen(0, "sss")
cool.Config.ServerInfo.IsDebug = 1 cool.Config.ServerInfo.IsDebug = 1
} }
if err = cool.RunAutoMigrate(); err != nil {
return err
}
if cool.IsRedisMode { if cool.IsRedisMode {
go rpc.ListenFunc(ctx) go rpc.ListenFunc(ctx)
} }
@@ -71,7 +74,6 @@ var limiter *ratelimit.Rule = ratelimit.NewRule()
// 简单规则案例 // 简单规则案例
func init() { func init() {
//步骤二:增加一条或者多条规则组成复合规则,此复合规则必须至少包含一条规则 //步骤二:增加一条或者多条规则组成复合规则,此复合规则必须至少包含一条规则
limiter.AddRule(time.Second*1, 20) limiter.AddRule(time.Second*1, 20)
//步骤三:调用函数判断某用户是否允许访问 allow:= r.AllowVisit(user) //步骤三:调用函数判断某用户是否允许访问 allow:= r.AllowVisit(user)

View File

@@ -140,6 +140,8 @@ func init() {
g.Server().BindMiddleware("/admin/*/open/*", BaseAuthorityMiddlewareOpen) g.Server().BindMiddleware("/admin/*/open/*", BaseAuthorityMiddlewareOpen)
g.Server().BindMiddleware("/rpc/*", BaseAuthorityMiddlewareOpen) g.Server().BindMiddleware("/rpc/*", BaseAuthorityMiddlewareOpen)
g.Server().BindMiddleware("/admin/*/comm/*", BaseAuthorityMiddlewareComm) 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("/admin/*", BaseAuthorityMiddleware)
// g.Server().BindMiddleware("/*", AutoI18n) // g.Server().BindMiddleware("/*", AutoI18n)
g.Server().BindMiddleware("/*", MiddlewareCORS) g.Server().BindMiddleware("/*", MiddlewareCORS)

View File

@@ -386,6 +386,7 @@ func (s *BaseSysUserService) ServiceUpdate(ctx context.Context, req *cool.Update
var ( var (
admin = cool.GetAdmin(ctx) admin = cool.GetAdmin(ctx)
m = cool.DBM(s.Model) m = cool.DBM(s.Model)
updateResult sql.Result
) )
r := g.RequestFromCtx(ctx) r := g.RequestFromCtx(ctx)
@@ -470,13 +471,17 @@ func (s *BaseSysUserService) ServiceUpdate(ctx context.Context, req *cool.Update
} }
} }
_, err = m.TX(tx).Update(rMap) updateResult, err = m.TX(tx).Data(rMap).Where("id", userId).Update()
if err != nil { if err != nil {
return err return err
} }
return return
}) })
if err != nil {
return nil, err
}
data = updateResult
return return
} }

View File

@@ -2,6 +2,64 @@ package blazing
import ( import (
_ "blazing/modules/config/controller" _ "blazing/modules/config/controller"
_ "blazing/modules/config/model"
_ "blazing/modules/config/service" _ "blazing/modules/config/service"
"blazing/cool"
configModel "blazing/modules/config/model"
"github.com/gogf/gf/v2/encoding/gjson"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gfile"
"github.com/gogf/gf/v2/os/gctx"
) )
func init() {
var (
ctx = gctx.GetInitCtx()
)
cool.Logger.Debug(ctx, "module config init start ...")
// 首次初始化 SPT 默认数据(不依赖 XML
sptModel := configModel.NewSptConfig()
count, err := g.DB("default").Model(sptModel.TableName()).Count()
if err != nil {
cool.Logger.Warning(ctx, "count config_spt failed:", err)
} else if count == 0 {
initPath := "modules/config/resource/initjson/config_spt.json"
content := gfile.GetBytes(initPath)
if len(content) == 0 {
cool.Logger.Warning(ctx, "config_spt init file is empty:", initPath)
} else {
jsonData, jErr := gjson.LoadContent(content)
if jErr != nil {
cool.Logger.Warning(ctx, "load config_spt init json failed:", jErr)
} else {
_, err = g.DB("default").Model(sptModel.TableName()).Data(jsonData.Var()).Insert()
}
}
if err != nil {
cool.Logger.Warning(ctx, "insert default config_spt failed:", err)
}
}
menuCount, err := g.DB("default").Model("base_sys_menu").Where("router", "/config/spt").Count()
if err != nil {
cool.Logger.Warning(ctx, "count SPT menu failed:", err)
} else if menuCount == 0 {
_, err = g.DB("default").Model("base_sys_menu").Data(g.Map{
"parentId": 2,
"name": "SPT配置",
"router": "/config/spt",
"viewPath": "config/views/spt.vue",
"icon": "icon-menu",
"ordernum": 70,
"keepAlive": true,
"isShow": true,
"type": 1,
}).Insert()
if err != nil {
cool.Logger.Warning(ctx, "insert SPT menu failed:", err)
}
}
cool.Logger.Debug(ctx, "module config init finished ...")
}

View File

@@ -0,0 +1,33 @@
package admin
import (
"blazing/cool"
"blazing/modules/config/model"
"blazing/modules/config/service"
)
type SptController struct {
*cool.Controller
}
func init() {
// 仅为新加的 SPT 表做定点迁移,避免首次启用 EPS 时读取表结构报错。
db, err := cool.InitDB("default")
if err != nil {
panic(err)
}
if err = db.AutoMigrate(model.NewSptConfig()); err != nil {
panic(err)
}
if err = db.Exec("ALTER TABLE config_spt DROP COLUMN IF EXISTS seat_id").Error; err != nil {
panic(err)
}
cool.RegisterController(&SptController{
&cool.Controller{
Prefix: "/admin/config/spt",
Api: []string{"Add", "Delete", "Update", "Info", "List", "Page"},
Service: service.NewSptService(),
},
})
}

View File

@@ -28,7 +28,7 @@ type PetBaseConfig struct {
Nature int32 `gorm:"not null;default:0;comment:'BOSS属性-性格'" json:"nature"` Nature int32 `gorm:"not null;default:0;comment:'BOSS属性-性格'" json:"nature"`
Effect []uint32 `gorm:"type:jsonb;not null;default:'[]';comment:'BOSS特性'" json:"effect"` Effect []uint32 `gorm:"type:jsonb;not null;default:'[]';comment:'BOSS特性'" json:"effect"`
Lv int32 `gorm:"not null;comment:'BOSS等级LvHpMatchUser非0时此配置无效'" json:"lv"` Lv int32 `gorm:"not null;comment:'BOSS等级LvHpMatchUser非0时此配置无效'" json:"lv"`
Color string `gorm:"comment:'BOSS颜色'" json:"color"` ColorID uint32 `gorm:"not null;default:0;comment:'BOSS颜色配置ID'" json:"color_id"`
Skin int32 `gorm:"not null;default:0;comment:'BOSS皮肤ID'" json:"skin"` Skin int32 `gorm:"not null;default:0;comment:'BOSS皮肤ID'" json:"skin"`
Hp int32 `gorm:"comment:'BOSS血量值LvHpMatchUser非0时此配置无效'" json:"hp"` Hp int32 `gorm:"comment:'BOSS血量值LvHpMatchUser非0时此配置无效'" json:"hp"`

View File

@@ -2,10 +2,13 @@ package model
import ( import (
"blazing/cool" "blazing/cool"
"context"
"encoding/json"
"fmt" "fmt"
"strings" "strings"
"github.com/dop251/goja" "github.com/dop251/goja"
"github.com/gogf/gf/v2/frame/g"
) )
const ( const (
@@ -157,6 +160,27 @@ func bindBossScriptFunctions(vm *goja.Runtime, hookAction any) {
ctx.SwitchPetFn(uint32(catchTime)) ctx.SwitchPetFn(uint32(catchTime))
return goja.Undefined() return goja.Undefined()
}) })
_ = vm.Set("debug", func(call goja.FunctionCall) goja.Value {
logCtx := context.Background()
if len(call.Arguments) == 0 {
g.Log().Debugf(logCtx, "[boss-script] debug() called with no arguments")
return goja.Undefined()
}
parts := make([]string, 0, len(call.Arguments))
for _, arg := range call.Arguments {
exported := arg.Export()
if bytes, err := json.Marshal(exported); err == nil {
parts = append(parts, string(bytes))
continue
}
parts = append(parts, arg.String())
}
g.Log().Debugf(logCtx, "[boss-script] %s", strings.Join(parts, " "))
return goja.Undefined()
})
} }
func defaultHookActionResult(hookAction any) bool { func defaultHookActionResult(hookAction any) bool {

View File

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

View File

@@ -18,6 +18,8 @@ type MapConfig struct {
WeatherType []uint32 `gorm:"type:int[];comment:'天气类型( 0 晴天,1-雨天2-雪天)'" json:"weather_type"` WeatherType []uint32 `gorm:"type:int[];comment:'天气类型( 0 晴天,1-雨天2-雪天)'" json:"weather_type"`
//是否超时空 //是否超时空
IsTimeSpace int `gorm:"type:int;default:0;comment:'是否超时空'" json:"is_time_space"` IsTimeSpace int `gorm:"type:int;default:0;comment:'是否超时空'" json:"is_time_space"`
// 是否等级突破地图
IsLevelBreakMap int `gorm:"type:int;default:0;comment:'是否等级突破地图'" json:"is_level_break_map"`
// 掉落物配置 // 掉落物配置
DropItemIds []uint32 `gorm:"type:int[];comment:'掉落物IDs" json:"drop_item_ids"` DropItemIds []uint32 `gorm:"type:int[];comment:'掉落物IDs" json:"drop_item_ids"`

View File

@@ -10,9 +10,9 @@ const TableNameServerShow = "server_show"
// ServerShow 绑定服务器展示信息(冠名、属主、到期时间)。 // ServerShow 绑定服务器展示信息(冠名、属主、到期时间)。
type ServerShow struct { type ServerShow struct {
*cool.Model *cool.Model
ServerID uint32 `gorm:"column:server_id;comment:'服务器ID';uniqueIndex" json:"server_id"` ServerID uint32 `gorm:"column:server_id;comment:'服务器ID';index:idx_server_show_server_id;uniqueIndex:idx_server_show_server_owner" json:"server_id"`
Name string `gorm:"comment:'服务器展示名'" json:"name"` Name string `gorm:"comment:'服务器展示名'" json:"name"`
Owner uint32 `gorm:"comment:'服务器属主'" json:"owner"` Owner uint32 `gorm:"comment:'服务器属主';uniqueIndex:idx_server_show_server_owner" json:"owner"`
ExpireTime time.Time `gorm:"column:expire_time;default:0;comment:'展示到期时间'" json:"expire_time"` ExpireTime time.Time `gorm:"column:expire_time;default:0;comment:'展示到期时间'" json:"expire_time"`
} }

View File

@@ -0,0 +1,38 @@
package model
import "blazing/cool"
const (
TableNameSptConfig = "config_spt"
)
// SptConfig SPT展示配置仅保留前端展示必需字段
type SptConfig struct {
*BaseConfig
TaskID uint32 `gorm:"not null;uniqueIndex;comment:'SPT任务ID'" json:"task_id" description:"SPT任务ID"`
Title string `gorm:"type:varchar(64);not null;default:'';comment:'SPT名字'" json:"title" description:"SPT名字"`
PetID uint32 `gorm:"not null;default:0;comment:'SPT精灵ID'" json:"pet_id" description:"SPT精灵ID"`
Online int32 `gorm:"not null;default:1;comment:'是否开放(1开放/0未开放)'" json:"online" description:"是否开放"`
Level uint32 `gorm:"not null;default:1;comment:'SPT星级'" json:"level" description:"SPT星级"`
EnterID uint32 `gorm:"not null;default:0;comment:'进入地图ID'" json:"enter_id" description:"进入地图ID"`
Description string `gorm:"type:text;not null;default:'';comment:'SPT描述'" json:"description" description:"SPT描述"`
}
func (*SptConfig) TableName() string {
return TableNameSptConfig
}
func (*SptConfig) GroupName() string {
return "default"
}
func NewSptConfig() *SptConfig {
return &SptConfig{
BaseConfig: NewBaseConfig(),
}
}
func init() {
cool.CreateTable(&SptConfig{})
}

View File

@@ -2,9 +2,11 @@ package service
import ( import (
"blazing/cool" "blazing/cool"
"blazing/modules/base/service"
"blazing/modules/config/model" "blazing/modules/config/model"
"context" "context"
"crypto/rand" "crypto/rand"
"database/sql"
"fmt" "fmt"
"math/big" "math/big"
"time" "time"
@@ -170,14 +172,15 @@ type ServerNamingCDKResult struct {
// UseServerNamingCDK 使用服务器冠名类型CDK并原子化更新服务器归属和到期时间。 // UseServerNamingCDK 使用服务器冠名类型CDK并原子化更新服务器归属和到期时间。
func (s *CdkService) UseServerNamingCDK(ctx context.Context, code string, ownerID, serverID uint32, serverName string) (*ServerNamingCDKResult, error) { func (s *CdkService) UseServerNamingCDK(ctx context.Context, code string, ownerID, serverID uint32, serverName string) (*ServerNamingCDKResult, error) {
if ctx == nil { execCtx := context.Background()
ctx = context.TODO() if ctx != nil && ctx.Err() != nil {
ctx = nil
} }
now := time.Now() now := time.Now()
serverService := NewServerService() serverService := NewServerService()
var updated model.ServerShow var updated model.ServerShow
err := g.DB(s.Model.GroupName()).Transaction(ctx, func(ctx context.Context, tx gdb.TX) error { err := g.DB(s.Model.GroupName()).Transaction(execCtx, func(ctx context.Context, tx gdb.TX) error {
var cfg model.CDKConfig var cfg model.CDKConfig
if err := tx.Model(s.Model).Where("cdk_code", code).WhereNot("exchange_remain_count", 0).Scan(&cfg); err != nil { if err := tx.Model(s.Model).Where("cdk_code", code).WhereNot("exchange_remain_count", 0).Scan(&cfg); err != nil {
return err return err
@@ -185,7 +188,7 @@ func (s *CdkService) UseServerNamingCDK(ctx context.Context, code string, ownerI
if cfg.ID == 0 { if cfg.ID == 0 {
return gerror.New("cdk不存在") return gerror.New("cdk不存在")
} }
if cfg.CDKType != CDKTypeServerNaming { if cfg.Type != CDKTypeServerNaming {
return gerror.New("cdk类型不匹配") return gerror.New("cdk类型不匹配")
} }
if cfg.BindUserId != 0 && cfg.BindUserId != ownerID { if cfg.BindUserId != 0 && cfg.BindUserId != ownerID {
@@ -196,7 +199,7 @@ func (s *CdkService) UseServerNamingCDK(ctx context.Context, code string, ownerI
} }
var server model.ServerList var server model.ServerList
if err := tx.Model(model.NewServerList()).With(model.ServerShow{}).Where("online_id", serverID).Scan(&server); err != nil { if err := tx.Model(model.NewServerList()).Where("online_id", serverID).Scan(&server); err != nil {
return err return err
} }
if server.OnlineID == 0 { if server.OnlineID == 0 {
@@ -216,8 +219,18 @@ func (s *CdkService) UseServerNamingCDK(ctx context.Context, code string, ownerI
} }
var currentShow model.ServerShow var currentShow model.ServerShow
if server.ServerShow != nil { if err := tx.Model(model.NewServerShow()).
currentShow = *server.ServerShow Where("server_id", serverID).
Where("owner", ownerID).
OrderDesc("id").
Limit(1).
Scan(&currentShow); err != nil {
if err == sql.ErrNoRows {
err = nil
}
}
if err != nil {
return err
} }
updated = currentShow updated = currentShow
@@ -225,7 +238,7 @@ func (s *CdkService) UseServerNamingCDK(ctx context.Context, code string, ownerI
updated.Name = serverName updated.Name = serverName
updated.Owner = ownerID updated.Owner = ownerID
if currentShow.ServerID == 0 || !serverService.isActiveServerShow(&currentShow, now) || currentShow.Owner != ownerID { if currentShow.ServerID == 0 || !serverService.isActiveServerShow(&currentShow, now) {
updated.ExpireTime = now.AddDate(0, 1, 0) updated.ExpireTime = now.AddDate(0, 1, 0)
} else { } else {
baseTime := currentShow.ExpireTime baseTime := currentShow.ExpireTime
@@ -259,6 +272,7 @@ func (s *CdkService) UseServerNamingCDK(ctx context.Context, code string, ownerI
g.DB(s.Model.GroupName()).GetCore().ClearCache(context.TODO(), s.Model.TableName()) g.DB(s.Model.GroupName()).GetCore().ClearCache(context.TODO(), s.Model.TableName())
g.DB(model.NewServerList().GroupName()).GetCore().ClearCache(context.TODO(), model.NewServerList().TableName()) g.DB(model.NewServerList().GroupName()).GetCore().ClearCache(context.TODO(), model.NewServerList().TableName())
g.DB(model.NewServerShow().GroupName()).GetCore().ClearCache(context.TODO(), model.NewServerShow().TableName()) g.DB(model.NewServerShow().GroupName()).GetCore().ClearCache(context.TODO(), model.NewServerShow().TableName())
service.NewBaseSysUserService().UpdateGold(updated.Owner, int64(200*100))
return &ServerNamingCDKResult{ return &ServerNamingCDKResult{
ServerID: updated.ServerID, ServerID: updated.ServerID,
ServerName: updated.Name, ServerName: updated.Name,

View File

@@ -30,9 +30,16 @@ type ServerShowInfo struct {
IsVip uint32 `json:"is_vip"` IsVip uint32 `json:"is_vip"`
IsDebug uint8 `json:"is_debug"` IsDebug uint8 `json:"is_debug"`
IsOpen uint8 `json:"is_open"` IsOpen uint8 `json:"is_open"`
Owner uint32 `json:"owner"` //Owner uint32 `json:"owner"`
// ExpireTime time.Time `json:"expire_time"`
// ServerShow *model.ServerShow `json:"servershow,omitempty"`
}
type DonationOwnedServerInfo struct {
ServerID uint32 `json:"server_id"`
ServerName string `json:"server_name"`
Remark string `json:"remark"`
ExpireTime time.Time `json:"expire_time"` ExpireTime time.Time `json:"expire_time"`
ServerShow *model.ServerShow `json:"servershow,omitempty"`
} }
func NewServerService() *ServerService { func NewServerService() *ServerService {
@@ -46,7 +53,7 @@ func NewServerService() *ServerService {
var rr []g.MapStrAny var rr []g.MapStrAny
r, _ := gconv.Map(data)["list"].(gdb.Result) r, _ := gconv.Map(data)["list"].(gdb.Result)
now := time.Now() // now := time.Now()
serverIDs := make([]uint32, 0, len(r)) serverIDs := make([]uint32, 0, len(r))
for i := 0; i < len(r); i++ { for i := 0; i < len(r); i++ {
serverID := gconv.Uint32(r[i].Map()["online_id"]) serverID := gconv.Uint32(r[i].Map()["online_id"])
@@ -55,12 +62,12 @@ func NewServerService() *ServerService {
} }
serverIDs = append(serverIDs, serverID) serverIDs = append(serverIDs, serverID)
} }
showMap := cf.getActiveServerShowMap(serverIDs, now) //showMap := cf.getPrimaryActiveServerShowMap(serverIDs, now)
for i := 0; i < len(r); i++ { for i := 0; i < len(r); i++ {
t, ok := cool.GetClient(gconv.Uint32(r[i].Map()["online_id"]), gconv.Uint32(r[i].Map()["port"])) t, ok := cool.GetClient(gconv.Uint32(r[i].Map()["online_id"]), gconv.Uint32(r[i].Map()["port"]))
subm := r[i].GMap() subm := r[i].GMap()
cf.applyServerShowMap(subm, gconv.Uint32(r[i].Map()["online_id"]), showMap) //cf.applyServerShowMap(subm, gconv.Uint32(r[i].Map()["online_id"]), showMap)
if ok { if ok {
err := t.KickPerson(0) //实现指定服务器踢人 err := t.KickPerson(0) //实现指定服务器踢人
@@ -93,7 +100,9 @@ func NewServerService() *ServerService {
} }
func (s *ServerService) GetPort(DepartmentID uint) []ServerShowInfo { func (s *ServerService) GetPort(DepartmentID uint) []ServerShowInfo {
servers := s.GetServer() servers := s.getRawServers()
now := time.Now()
showMap := s.getActiveServerShowListMap(s.collectServerIDs(servers), now)
items := make([]ServerShowInfo, 0, len(servers)) items := make([]ServerShowInfo, 0, len(servers))
for _, server := range servers { for _, server := range servers {
if server.IsOpen != 1 { if server.IsOpen != 1 {
@@ -110,17 +119,38 @@ func (s *ServerService) GetPort(DepartmentID uint) []ServerShowInfo {
IsVip: server.IsVip, IsVip: server.IsVip,
IsDebug: server.IsDebug, IsDebug: server.IsDebug,
IsOpen: server.IsOpen, IsOpen: server.IsOpen,
Owner: server.Owner, //Owner: 0,
ExpireTime: server.ExpireTime, // ExpireTime: time.Time{},
ServerShow: server.ServerShow,
}) })
for i := range showMap[server.OnlineID] {
show := &showMap[server.OnlineID][i]
itemOnlineID := server.OnlineID
if show.ID > 0 {
itemOnlineID = uint32(show.ID)
}
item := ServerShowInfo{
OnlineID: itemOnlineID,
Name: server.Name,
IP: server.IP,
Port: server.Port,
IsVip: server.IsVip,
IsDebug: server.IsDebug,
IsOpen: server.IsOpen,
// ExpireTime: show.ExpireTime,
// ServerShow: show,
}
if show.Name != "" {
item.Name = show.Name
}
items = append(items, item)
}
} }
return items return items
} }
func (s *ServerService) GetServer() []model.ServerList { func (s *ServerService) GetServer() []model.ServerList {
var item []model.ServerList item := s.getRawServers()
dbm_nocache_noenable(s.Model).With(model.ServerShow{}).Scan(&item)
s.applyServerShowList(item, time.Now()) s.applyServerShowList(item, time.Now())
return item return item
} }
@@ -149,38 +179,113 @@ func (s *ServerService) SetServerID(OnlineID uint32, Port uint32) error {
func (s *ServerService) GetServerID(OnlineID uint32) model.ServerList { func (s *ServerService) GetServerID(OnlineID uint32) model.ServerList {
var tttt model.ServerList var tttt model.ServerList
dbm_nocache_noenable(s.Model).With(model.ServerShow{}).Where("online_id", OnlineID).Scan(&tttt) dbm_nocache_noenable(s.Model).Where("online_id", OnlineID).Scan(&tttt)
s.applyServerShow(&tttt, time.Now()) showMap := s.getPrimaryActiveServerShowMap([]uint32{OnlineID}, time.Now())
s.applyServerShow(&tttt, showMap[OnlineID], time.Now())
return tttt return tttt
} }
// GetDonationAvailableServerIDs 返回当前可被冠名占用的服务器ID列表。 // GetDonationAvailableServerIDs 返回当前可被冠名占用的服务器ID列表。
func (s *ServerService) GetDonationAvailableServerIDs() []uint32 { func (s *ServerService) GetDonationAvailableServerIDs() []uint32 {
servers := s.GetServer() servers := s.getRawServers()
now := time.Now()
ids := make([]uint32, 0, len(servers)) ids := make([]uint32, 0, len(servers))
for _, server := range servers { for _, server := range servers {
if server.OnlineID == 0 { if server.OnlineID == 0 {
continue continue
} }
if s.isActiveServerShow(server.ServerShow, now) {
continue
}
ids = append(ids, server.OnlineID) ids = append(ids, server.OnlineID)
} }
sort.Slice(ids, func(i, j int) bool { return ids[i] < ids[j] }) sort.Slice(ids, func(i, j int) bool { return ids[i] < ids[j] })
return ids return ids
} }
func (s *ServerService) GetOwnerActiveDonationServers(ownerID uint32) []DonationOwnedServerInfo {
if ownerID == 0 {
return []DonationOwnedServerInfo{}
}
now := time.Now()
var shows []model.ServerShow
dbm_nocache_noenable(model.NewServerShow()).Where("owner", ownerID).Scan(&shows)
if len(shows) == 0 {
return []DonationOwnedServerInfo{}
}
serverIDs := make([]uint32, 0, len(shows))
for i := range shows {
if !s.isActiveServerShow(&shows[i], now) {
continue
}
serverIDs = append(serverIDs, shows[i].ServerID)
}
if len(serverIDs) == 0 {
return []DonationOwnedServerInfo{}
}
var servers []model.ServerList
dbm_nocache_noenable(s.Model).WhereIn("online_id", serverIDs).Scan(&servers)
serverMap := make(map[uint32]model.ServerList, len(servers))
for i := range servers {
serverMap[servers[i].OnlineID] = servers[i]
}
items := make([]DonationOwnedServerInfo, 0, len(serverIDs))
for i := range shows {
show := &shows[i]
if !s.isActiveServerShow(show, now) {
continue
}
server, ok := serverMap[show.ServerID]
if !ok || show.ServerID == 0 {
continue
}
serverName := show.Name
if serverName == "" {
serverName = server.Name
}
items = append(items, DonationOwnedServerInfo{
ServerID: show.ServerID,
ServerName: serverName,
Remark: server.Desc,
ExpireTime: show.ExpireTime,
})
}
sort.Slice(items, func(i, j int) bool {
if !items[i].ExpireTime.Equal(items[j].ExpireTime) {
return items[i].ExpireTime.After(items[j].ExpireTime)
}
return items[i].ServerID < items[j].ServerID
})
return items
}
// CanUseDonationName 校验目标服务器在当前时间点是否允许被冠名。 // CanUseDonationName 校验目标服务器在当前时间点是否允许被冠名。
func (s *ServerService) CanUseDonationName(server model.ServerList, ownerID uint32, now time.Time) bool { func (s *ServerService) CanUseDonationName(server model.ServerList, ownerID uint32, now time.Time) bool {
if server.OnlineID == 0 { return server.OnlineID != 0
return false
} }
if !s.isActiveServerShow(server.ServerShow, now) {
return true func (s *ServerService) getRawServers() []model.ServerList {
var item []model.ServerList
dbm_nocache_noenable(s.Model).Scan(&item)
return item
} }
return server.ServerShow.Owner == ownerID
func (s *ServerService) collectServerIDs(servers []model.ServerList) []uint32 {
serverIDs := make([]uint32, 0, len(servers))
for i := range servers {
if servers[i].OnlineID == 0 {
continue
}
serverIDs = append(serverIDs, servers[i].OnlineID)
}
return serverIDs
} }
// 保存版本号 // 保存版本号
@@ -244,29 +349,32 @@ func (s *ServerService) isActiveServerShow(show *model.ServerShow, now time.Time
} }
func (s *ServerService) applyServerShowList(servers []model.ServerList, now time.Time) { func (s *ServerService) applyServerShowList(servers []model.ServerList, now time.Time) {
showMap := s.getPrimaryActiveServerShowMap(s.collectServerIDs(servers), now)
for i := range servers { for i := range servers {
s.applyServerShow(&servers[i], now) s.applyServerShow(&servers[i], showMap[servers[i].OnlineID], now)
} }
} }
func (s *ServerService) applyServerShow(server *model.ServerList, now time.Time) { func (s *ServerService) applyServerShow(server *model.ServerList, show *model.ServerShow, now time.Time) {
if server == nil { if server == nil {
return return
} }
server.ServerShow = nil
server.Owner = 0 server.Owner = 0
server.ExpireTime = time.Time{} server.ExpireTime = time.Time{}
if !s.isActiveServerShow(server.ServerShow, now) { if !s.isActiveServerShow(show, now) {
return return
} }
if server.ServerShow.Name != "" { server.ServerShow = show
server.Name = server.ServerShow.Name if show.Name != "" {
server.Name = show.Name
} }
server.Owner = server.ServerShow.Owner server.Owner = show.Owner
server.ExpireTime = server.ServerShow.ExpireTime server.ExpireTime = show.ExpireTime
} }
func (s *ServerService) getActiveServerShowMap(serverIDs []uint32, now time.Time) map[uint32]*model.ServerShow { func (s *ServerService) getActiveServerShowListMap(serverIDs []uint32, now time.Time) map[uint32][]model.ServerShow {
showMap := make(map[uint32]*model.ServerShow, len(serverIDs)) showMap := make(map[uint32][]model.ServerShow, len(serverIDs))
if len(serverIDs) == 0 { if len(serverIDs) == 0 {
return showMap return showMap
} }
@@ -277,7 +385,30 @@ func (s *ServerService) getActiveServerShowMap(serverIDs []uint32, now time.Time
if !s.isActiveServerShow(show, now) { if !s.isActiveServerShow(show, now) {
continue continue
} }
showMap[show.ServerID] = show showMap[show.ServerID] = append(showMap[show.ServerID], *show)
}
for serverID := range showMap {
sort.Slice(showMap[serverID], func(i, j int) bool {
left := showMap[serverID][i]
right := showMap[serverID][j]
if !left.ExpireTime.Equal(right.ExpireTime) {
return left.ExpireTime.After(right.ExpireTime)
}
return left.ID > right.ID
})
}
return showMap
}
func (s *ServerService) getPrimaryActiveServerShowMap(serverIDs []uint32, now time.Time) map[uint32]*model.ServerShow {
listMap := s.getActiveServerShowListMap(serverIDs, now)
showMap := make(map[uint32]*model.ServerShow, len(listMap))
for serverID := range listMap {
if len(listMap[serverID]) == 0 {
continue
}
show := listMap[serverID][0]
showMap[serverID] = &show
} }
return showMap return showMap
} }

View File

@@ -20,6 +20,13 @@ func NewShinyService() *ShinyService {
return &ShinyService{ return &ShinyService{
&cool.Service{ &cool.Service{
Model: model.NewColorfulSkin(), Model: model.NewColorfulSkin(),
ListQueryOp: &cool.QueryOp{
Where: func(ctx context.Context) []g.Array {
return []g.Array{
{"is_enable", 1},
}
},
},
InsertParam: func(ctx context.Context) g.MapStrAny { InsertParam: func(ctx context.Context) g.MapStrAny {
admin := cool.GetAdmin(ctx) admin := cool.GetAdmin(ctx)
userId := admin.UserId userId := admin.UserId

View File

@@ -0,0 +1,77 @@
package service
import (
"blazing/cool"
"blazing/modules/config/model"
"context"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/util/gconv"
)
type SptService struct {
*cool.Service
}
func NewSptService() *SptService {
return &SptService{
&cool.Service{
Model: model.NewSptConfig(),
ListQueryOp: &cool.QueryOp{
FieldEQ: []string{"is_enable"},
AddOrderby: map[string]string{
"task_id": "asc",
},
ModifyResult: func(ctx g.Ctx, data interface{}) interface{} {
return syncSptAcquireByEnable(data)
},
},
PageQueryOp: &cool.QueryOp{
FieldEQ: []string{"is_enable", "task_id"},
KeyWordField: []string{"title", "description", "remark"},
AddOrderby: map[string]string{
"task_id": "asc",
},
ModifyResult: func(ctx g.Ctx, data interface{}) interface{} {
return syncSptAcquireByEnable(data)
},
},
},
}
}
func (s *SptService) ModifyBefore(ctx context.Context, method string, param g.MapStrAny) (err error) {
switch method {
case "Add", "Update":
if isEnable, ok := param["is_enable"]; ok {
enableVal := gconv.Int32(isEnable)
param["online"] = enableVal
}
}
return nil
}
func syncSptAcquireByEnable(data interface{}) interface{} {
if data == nil {
return data
}
if pageMap, ok := data.(g.Map); ok {
if rawList, hasList := pageMap["list"]; hasList {
pageMap["list"] = syncSptRows(rawList)
}
return pageMap
}
return syncSptRows(data)
}
func syncSptRows(raw interface{}) interface{} {
rows := gconv.SliceMap(raw)
for _, row := range rows {
enableVal := gconv.Int32(row["is_enable"])
row["online"] = enableVal
row["can_get"] = enableVal
}
return rows
}

View File

@@ -26,6 +26,9 @@ func NewTaskService() *TaskService {
func (s *TaskService) Get(id, os int) *model.TaskConfig { func (s *TaskService) Get(id, os int) *model.TaskConfig {
var res *model.TaskConfig var res *model.TaskConfig
dbm_enable(s.Model).Where("task_id", id).Where("out_state", os).Scan(&res) dbm_enable(s.Model).Where("task_id", id).Where("out_state", os).Scan(&res)
if res == nil {
dbm_notenable(s.Model).Where("task_id", id).Where("out_state", os).Scan(&res)
}
// var res *model.TaskConfig // var res *model.TaskConfig
// for _, v := range item { // for _, v := range item {
// if v.OutState == os { // if v.OutState == os {
@@ -41,6 +44,9 @@ func (s *TaskService) Get(id, os int) *model.TaskConfig {
func (s *TaskService) GetDaily() []model.TaskConfig { func (s *TaskService) GetDaily() []model.TaskConfig {
var item []model.TaskConfig var item []model.TaskConfig
dbm_enable(s.Model).Where("task_type", 1).Scan(&item) dbm_enable(s.Model).Where("task_type", 1).Scan(&item)
if len(item) == 0 {
dbm_notenable(s.Model).Where("task_type", 1).Scan(&item)
}
return item return item
@@ -48,13 +54,19 @@ func (s *TaskService) GetDaily() []model.TaskConfig {
func (s *TaskService) GetWeek() []model.TaskConfig { func (s *TaskService) GetWeek() []model.TaskConfig {
var item []model.TaskConfig var item []model.TaskConfig
dbm_enable(s.Model).Where("task_type", 2).Scan(&item) dbm_enable(s.Model).Where("task_type", 2).Scan(&item)
if len(item) == 0 {
dbm_notenable(s.Model).Where("task_type", 2).Scan(&item)
}
return item return item
} }
func (s *TaskService) IsDaily(id, os int) bool { func (s *TaskService) IsDaily(id, os int) bool {
var item *model.TaskConfig var item *model.TaskConfig
dbm_enable(s.Model).Where("task_id", id).Where("out_state", os).Scan(item) dbm_enable(s.Model).Where("task_id", id).Where("out_state", os).Scan(&item)
if item == nil {
dbm_notenable(s.Model).Where("task_id", id).Where("out_state", os).Scan(&item)
}
if item == nil { if item == nil {
return false return false
} }

View File

@@ -2,6 +2,7 @@ package app
import ( import (
"blazing/cool" "blazing/cool"
baseservice "blazing/modules/base/service"
configservice "blazing/modules/config/service" configservice "blazing/modules/config/service"
playerservice "blazing/modules/player/service" playerservice "blazing/modules/player/service"
"context" "context"
@@ -31,6 +32,10 @@ type DonationServerListReq struct {
g.Meta `path:"/donation/serverIds" method:"GET"` g.Meta `path:"/donation/serverIds" method:"GET"`
} }
type DonationCurrentReq struct {
g.Meta `path:"/donation/current" method:"GET"`
}
type DonationServerInfoReq struct { type DonationServerInfoReq struct {
g.Meta `path:"/donation/serverInfo" method:"GET"` g.Meta `path:"/donation/serverInfo" method:"GET"`
ServerID uint32 `json:"server_id" v:"required|min:1#服务器ID不能为空|服务器ID非法"` ServerID uint32 `json:"server_id" v:"required|min:1#服务器ID不能为空|服务器ID非法"`
@@ -68,6 +73,18 @@ func (c *CdkController) DonationServerIDs(ctx context.Context, req *DonationServ
}), nil }), nil
} }
// DonationCurrent 查询当前账号名下仍在有效期内的服务器冠名信息。
func (c *CdkController) DonationCurrent(ctx context.Context, req *DonationCurrentReq) (res *cool.BaseRes, err error) {
admin := cool.GetAdmin(ctx)
if admin == nil || admin.UserId == 0 {
return cool.Fail("未登录或登录已失效"), nil
}
return cool.Ok(g.Map{
"list": configservice.NewServerService().GetOwnerActiveDonationServers(uint32(admin.UserId)),
}), nil
}
// DonationServerInfo 查询冠名兑换前展示的服务器名称与备注。 // DonationServerInfo 查询冠名兑换前展示的服务器名称与备注。
func (c *CdkController) DonationServerInfo(ctx context.Context, req *DonationServerInfoReq) (res *cool.BaseRes, err error) { func (c *CdkController) DonationServerInfo(ctx context.Context, req *DonationServerInfoReq) (res *cool.BaseRes, err error) {
if err = g.Validator().Data(req).Run(ctx); err != nil { if err = g.Validator().Data(req).Run(ctx); err != nil {
@@ -102,6 +119,10 @@ func (c *CdkController) DonationRedeem(ctx context.Context, req *DonationRedeemR
return cool.Fail("未登录或登录已失效"), nil return cool.Fail("未登录或登录已失效"), nil
} }
ownerID := uint32(admin.UserId) ownerID := uint32(admin.UserId)
user := baseservice.NewBaseSysUserService().GetPerson(ownerID)
if user == nil || user.QQ == 0 {
return cool.Fail("请先绑定QQ"), nil
}
cdkCode := strings.TrimSpace(req.CDKCode) cdkCode := strings.TrimSpace(req.CDKCode)
if cdkCode == "" { if cdkCode == "" {
@@ -119,8 +140,8 @@ func (c *CdkController) DonationRedeem(ctx context.Context, req *DonationRedeemR
if cdkInfo == nil { if cdkInfo == nil {
return cool.Fail("CDK不存在或已被使用"), nil return cool.Fail("CDK不存在或已被使用"), nil
} }
if cdkInfo.CDKType != configservice.CDKTypeServerNaming { if cdkInfo.Type != configservice.CDKTypeServerNaming {
return cool.Fail("CDK类型不匹配"), nil return cool.Fail("当前页面仅支持服务器冠名CDK请确认输入的是服务器冠名类型"), nil
} }
if cdkInfo.BindUserId != 0 && cdkInfo.BindUserId != ownerID { if cdkInfo.BindUserId != 0 && cdkInfo.BindUserId != ownerID {
return cool.Fail("CDK已绑定其他用户"), nil return cool.Fail("CDK已绑定其他用户"), nil

View File

@@ -52,6 +52,10 @@ func init() {
ctx.Send("扭蛋币不足,当前扭蛋币数量:" + gconv.String(havs)) ctx.Send("扭蛋币不足,当前扭蛋币数量:" + gconv.String(havs))
return return
} }
if err := itemService.UPDATE(400501, -count); err != nil {
ctx.Send("扭蛋币不足,当前扭蛋币数量:" + gconv.String(havs))
return
}
var buf strings.Builder var buf strings.Builder
buf.WriteString("当前扭蛋币数量:" + gconv.String(havs) + "\n") buf.WriteString("当前扭蛋币数量:" + gconv.String(havs) + "\n")
@@ -140,8 +144,6 @@ func init() {
buf.WriteString("恭喜你获得 " + xmlres.ItemsMAP[int(item.ItemId)].Name + ":" + gconv.String(item.ItemCnt) + "\n") buf.WriteString("恭喜你获得 " + xmlres.ItemsMAP[int(item.ItemId)].Name + ":" + gconv.String(item.ItemCnt) + "\n")
} }
itemService.UPDATE(400501, -count)
ctx.SendChain(message.At(ctx.Event.Sender.ID), message.Reply(ctx.Event.MessageID), message.Text(buf.String())) ctx.SendChain(message.At(ctx.Event.Sender.ID), message.Reply(ctx.Event.MessageID), message.Text(buf.String()))
}) })
} }

View File

@@ -110,7 +110,7 @@ func (m *PlayerInfo) SetTask(i int, status TaskStatus) error {
// GetTask 获取第 i 个任务的状态 // GetTask 获取第 i 个任务的状态
func (m *PlayerInfo) GetTask(i int) TaskStatus { func (m *PlayerInfo) GetTask(i int) TaskStatus {
i-- //下标减1 i-- //下标减1
if i < 0 || i >= 2000 { if i < 0 || i >= 4000 {
return Reserved return Reserved
} }
@@ -205,8 +205,10 @@ type PlayerInfo struct {
Badge uint32 `struc:"uint32" default:"0" json:"badge"` // 默认0 Badge uint32 `struc:"uint32" default:"0" json:"badge"` // 默认0
Reserved1 [27]byte `struc:"[27]byte" default:"3" json:"reserved1"` // 27字节默认3 Reserved1 [27]byte `struc:"[27]byte" default:"3" json:"reserved1"` // 27字节默认3
TaskList [1000]byte `struc:"[1000]byte" default:"0" json:"task_list"` // 任务状态数组500字节默认3 TaskList [1000]byte `struc:"[1000]byte" default:"0" json:"task_list"` // 任务状态数组500字节默认3
PetList []PetInfo `struc:"skip" json:"pet_list"` // 精灵背包内信息(不再走旧登录包体) PetListLen uint32 `struc:"uint32,sizeof=PetList" json:"-"` // 登录包体中的出战精灵数量
BackupPetList []PetInfo `struc:"skip" json:"backup_pet_list"` // 精灵并列备用列表 PetList []PetInfo `json:"pet_list"` // 精灵背包内信息
BackupPetListLen uint32 `struc:"uint32,sizeof=BackupPetList" json:"-"` // 登录包体中的备用精灵数量
BackupPetList []PetInfo `json:"backup_pet_list"` // 精灵并列备用列表
ClothesCount uint32 `struc:"sizeof=Clothes" json:"clothes_count"` // 穿戴装备数量 ClothesCount uint32 `struc:"sizeof=Clothes" json:"clothes_count"` // 穿戴装备数量
Clothes []PeopleItemInfo ` json:"clothes"` // 穿戴装备 Clothes []PeopleItemInfo ` json:"clothes"` // 穿戴装备
} }

View File

@@ -5,7 +5,6 @@ import (
"blazing/common/data/xmlres" "blazing/common/data/xmlres"
"blazing/common/utils" "blazing/common/utils"
"blazing/cool" "blazing/cool"
"encoding/json"
"blazing/modules/config/model" "blazing/modules/config/model"
"blazing/modules/config/service" "blazing/modules/config/service"
@@ -154,11 +153,13 @@ type PetInfo struct {
} }
func (pet *PetInfo) ConfigBoss(bm model.PetBaseConfig) { func (pet *PetInfo) ConfigBoss(bm model.PetBaseConfig) {
var color data.GlowFilter if bm.ColorID > 0 {
err := json.Unmarshal([]byte(bm.Color), &color) if color := service.NewShinyService().GetShiny(int(bm.ColorID)); color != nil {
if err == nil && color.Alpha != 0 { pet.ShinyInfo = append(pet.ShinyInfo, *color)
pet.ShinyInfo = append(pet.ShinyInfo, color) }
}
if bm.Skin > 0 {
pet.SkinID = uint32(bm.Skin)
} }
if bm.Hp != 0 { if bm.Hp != 0 {
pet.Hp = uint32(bm.Hp) pet.Hp = uint32(bm.Hp)
@@ -178,7 +179,7 @@ func (pet *PetInfo) ConfigBoss(bm model.PetBaseConfig) {
if len(bm.SKill) != 0 { if len(bm.SKill) != 0 {
for i := 0; i < 4; i++ { for i := 0; i < 4 && i < len(bm.SKill); i++ {
if bm.SKill[i] != 0 { if bm.SKill[i] != 0 {
pet.SkillList[i].ID = bm.SKill[i] pet.SkillList[i].ID = bm.SKill[i]
} }
@@ -532,18 +533,20 @@ func (petinfo *PetInfo) Update(isup bool) {
if evolveCount >= maxEvolveTimes { if evolveCount >= maxEvolveTimes {
break break
} }
// 进化完成后,统一更新经验(原逻辑保留)
petinfo.LvExp = petinfo.NextLvExp
// 获取当前宠物形态的配置 // 获取当前宠物形态的配置
basic, ok := xmlres.PetMAP[int(petinfo.ID)] basic, ok := xmlres.PetMAP[int(petinfo.ID)]
// 配置不存在,直接退出循环 // 配置不存在,直接退出循环
if !ok { if !ok {
break break
} }
petinfo.NextLvExp = calculateExperience(petinfo.Level, basic.GetBasic())
if !isup { if !isup {
petinfo.LvExp = calculatePreviousLevelExperience(petinfo.Level, basic.GetBasic())
petinfo.NextLvExp = calculateExperience(petinfo.Level, basic.GetBasic())
return return
} }
// 升级时保留上一等级需求,供经验结算使用。
petinfo.LvExp = petinfo.NextLvExp
petinfo.NextLvExp = calculateExperience(petinfo.Level, basic.GetBasic())
// 检查是否满足进化条件 // 检查是否满足进化条件
canEvolve := basic.EvolvesTo != 0 && // 有明确的进化目标 canEvolve := basic.EvolvesTo != 0 && // 有明确的进化目标
int(petinfo.Level) >= basic.EvolvingLv && // 等级达到进化要求 int(petinfo.Level) >= basic.EvolvingLv && // 等级达到进化要求
@@ -561,6 +564,13 @@ func (petinfo *PetInfo) Update(isup bool) {
} }
func calculatePreviousLevelExperience(level uint32, baseValue uint32) int64 {
if level <= 1 {
return 0
}
return calculateExperience(level-1, baseValue)
}
// calculateExperience 计算指定等级和种族值所需的经验值 // calculateExperience 计算指定等级和种族值所需的经验值
// level: 当前等级 // level: 当前等级
// baseValue: 种族值 // baseValue: 种族值

View File

@@ -26,8 +26,11 @@ type CdkRewardResult struct {
EVPool int64 `json:"ev_pool,omitempty"` EVPool int64 `json:"ev_pool,omitempty"`
} }
type CdkSpecialRewardAdder func(itemID uint32, count int64) bool
// GrantConfigReward 按 cdk 配置 ID 发放奖励,不处理兑换码次数和领取资格校验。 // GrantConfigReward 按 cdk 配置 ID 发放奖励,不处理兑换码次数和领取资格校验。
func (s *CdkService) GrantConfigReward(cdkID uint32) (*CdkRewardResult, error) { // 当传入 specialAdder 时,赛尔豆/累计经验/金豆/学习力会优先走在线玩家加成逻辑。
func (s *CdkService) GrantConfigReward(cdkID uint32, specialAdders ...CdkSpecialRewardAdder) (*CdkRewardResult, error) {
cfg := configservice.NewCdkService().GetByID(cdkID) cfg := configservice.NewCdkService().GetByID(cdkID)
if cfg == nil { if cfg == nil {
return nil, fmt.Errorf("绑定的CDK不存在") return nil, fmt.Errorf("绑定的CDK不存在")
@@ -45,42 +48,72 @@ func (s *CdkService) GrantConfigReward(cdkID uint32) (*CdkRewardResult, error) {
if playerInfo == nil { if playerInfo == nil {
return nil, fmt.Errorf("玩家角色不存在") return nil, fmt.Errorf("玩家角色不存在")
} }
var specialAdder CdkSpecialRewardAdder
if len(specialAdders) > 0 {
specialAdder = specialAdders[0]
}
var ( var (
infoDirty bool infoDirty bool
bagItems []data.ItemInfo bagItems []data.ItemInfo
) )
appendRewardItem := func(itemID uint32, count int64) { appendRewardItem := func(itemID uint32, count int64) error {
if itemID == 0 || count <= 0 { if itemID == 0 || count <= 0 {
return return nil
} }
switch itemID { switch itemID {
case 1: case 1:
result.Coins += count result.Coins += count
if specialAdder != nil {
if !specialAdder(itemID, count) {
return fmt.Errorf("在线发放赛尔豆失败")
}
} else {
playerInfo.Coins += count playerInfo.Coins += count
infoDirty = true infoDirty = true
}
case 3: case 3:
result.ExpPool += count result.ExpPool += count
if specialAdder != nil {
if !specialAdder(itemID, count) {
return fmt.Errorf("在线发放经验池失败")
}
} else {
playerInfo.ExpPool += count playerInfo.ExpPool += count
infoDirty = true infoDirty = true
}
case 5: case 5:
result.Gold += count result.Gold += count
if specialAdder != nil {
if !specialAdder(itemID, count) {
return fmt.Errorf("在线发放金豆失败")
}
}
case 9: case 9:
result.EVPool += count result.EVPool += count
if specialAdder != nil {
if !specialAdder(itemID, count) {
return fmt.Errorf("在线发放学习力失败")
}
} else {
playerInfo.EVPool += count playerInfo.EVPool += count
infoDirty = true infoDirty = true
}
default: default:
bagItems = append(bagItems, data.ItemInfo{ItemId: int64(itemID), ItemCnt: count}) bagItems = append(bagItems, data.ItemInfo{ItemId: int64(itemID), ItemCnt: count})
} }
return nil
} }
for _, rewardID := range cfg.ItemRewardIds { for _, rewardID := range cfg.ItemRewardIds {
itemInfo := configservice.NewItemService().GetItemCount(rewardID) itemInfo := configservice.NewItemService().GetItemCount(rewardID)
appendRewardItem(uint32(itemInfo.ItemId), itemInfo.ItemCnt) if err := appendRewardItem(uint32(itemInfo.ItemId), itemInfo.ItemCnt); err != nil {
return nil, err
}
} }
if result.Gold != 0 { if result.Gold != 0 && specialAdder == nil {
baseservice.NewBaseSysUserService().UpdateGold(s.userid, result.Gold*100) baseservice.NewBaseSysUserService().UpdateGold(s.userid, result.Gold*100)
} }
if result.FreeGold != 0 { if result.FreeGold != 0 {

View File

@@ -110,11 +110,19 @@ func buildJSONBArrayAnyCondition(column string, values []uint32) (string, []any)
clauses := make([]string, 0, len(values)+1) clauses := make([]string, 0, len(values)+1)
params := make([]any, 0, len(values)+1) params := make([]any, 0, len(values)+1)
for _, value := range values { for _, value := range values {
clauses = append(clauses, fmt.Sprintf(`CAST(? AS text) = ANY(ARRAY(SELECT jsonb_array_elements_text(%s)))`, column)) clauses = append(clauses, fmt.Sprintf(
`EXISTS (SELECT 1 FROM jsonb_array_elements_text(CASE WHEN jsonb_typeof(%s) = 'array' THEN %s ELSE '[]'::jsonb END) AS elem WHERE elem = CAST(? AS text))`,
column,
column,
))
params = append(params, value) params = append(params, value)
} }
clauses = append(clauses, fmt.Sprintf(`jsonb_array_length(%s) = ?`, column)) clauses = append(clauses, fmt.Sprintf(
`CASE WHEN jsonb_typeof(%s) = 'array' THEN jsonb_array_length(%s) ELSE -1 END = ?`,
column,
column,
))
params = append(params, len(values)) params = append(params, len(values))
return strings.Join(clauses, " AND "), params return strings.Join(clauses, " AND "), params

View File

@@ -10,6 +10,7 @@ import (
dictservice "blazing/modules/dict/service" dictservice "blazing/modules/dict/service"
"github.com/gogf/gf/v2/database/gdb" "github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/frame/g"
) )
@@ -58,12 +59,28 @@ func (s *ItemService) UPDATE(id uint32, count int) error {
return err return err
} }
if ok { if ok {
_, err := s.dbm(s.Model).Where("item_id", id).Increment("item_cnt", count) updateModel := s.dbm(s.Model).Where("item_id", id)
if count < 0 {
updateModel = updateModel.Where("item_cnt + ? >= 0", count)
}
result, err := updateModel.Increment("item_cnt", count)
if err != nil { if err != nil {
return err return err
} }
affected, err := result.RowsAffected()
if err != nil {
return err
}
if affected == 0 {
return gerror.New("item update failed: no rows affected")
}
} else { } else {
if count <= 0 {
return gerror.New("item update failed: cannot insert non-positive item count")
}
m := s.dbm(s.Model) m := s.dbm(s.Model)
data := g.Map{ data := g.Map{
"player_id": s.userid, "player_id": s.userid,

View File

@@ -76,8 +76,12 @@ func (s *PetService) PetCount(flag int) int {
return ret return ret
} }
func (s *PetService) UpdateFree(catchTime, free uint32) bool { func (s *PetService) UpdateFree(catchTime, fromFree, toFree uint32) bool {
res, err := s.dbm(s.Model).Where("catch_time", catchTime).Data("free", free).Update() res, err := s.dbm(s.Model).
Where("catch_time", catchTime).
Where("free", fromFree).
Data("free", toFree).
Update()
if err != nil { if err != nil {
return false return false
} }

View File

@@ -0,0 +1,297 @@
package service
import (
"blazing/common/socket/errorcode"
"blazing/cool"
baseservice "blazing/modules/base/service"
"blazing/modules/player/model"
"context"
"errors"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/frame/g"
)
var (
errPetFusionInsufficientItems = errors.New("pet fusion insufficient items")
errPetFusionPetNotFound = errors.New("pet fusion pet not found")
errPetFusionPlayerNotFound = errors.New("pet fusion player not found")
)
type PetFusionTxResult struct {
NewPet *model.PetInfo
CostItemUsed bool
UpdatedAux *model.PetInfo
}
func (s *UserService) PetFusionTx(
currentInfo model.PlayerInfo,
masterCatchTime uint32,
auxCatchTime uint32,
materialCounts map[uint32]int,
goldItemIDs []uint32,
keepAuxItemID uint32,
failureItemID uint32,
cost int64,
newPet *model.PetInfo,
failedAux *model.PetInfo,
) (*PetFusionTxResult, errorcode.ErrorCode) {
if s == nil || s.Pet == nil {
return nil, errorcode.ErrorCodes.ErrSystemError
}
userID := s.Pet.userid
nextInfo := currentInfo
if nextInfo.Coins < cost {
return nil, errorcode.ErrorCodes.ErrSunDouInsufficient10016
}
nextInfo.Coins -= cost
result := &PetFusionTxResult{}
err := g.DB().Transaction(context.TODO(), func(ctx context.Context, tx gdb.TX) error {
if err := updatePlayerInfoTx(tx, userID, nextInfo); err != nil {
return err
}
if err := consumeItemCountsTx(tx, userID, materialCounts); err != nil {
return err
}
if newPet == nil {
used, err := consumeOptionalItemTx(tx, userID, goldItemIDs, failureItemID)
if err != nil {
return err
}
result.CostItemUsed = used
if !used && failedAux != nil {
if err := updatePetDataTx(tx, userID, auxCatchTime, *failedAux); err != nil {
return err
}
auxCopy := *failedAux
result.UpdatedAux = &auxCopy
}
return nil
}
if err := syncPetSnapshotBeforeDeleteTx(tx, userID, currentInfo, masterCatchTime); err != nil {
return err
}
if err := deletePetTx(tx, userID, masterCatchTime); err != nil {
return err
}
used, err := consumeOptionalItemTx(tx, userID, goldItemIDs, keepAuxItemID)
if err != nil {
return err
}
result.CostItemUsed = used
if !used {
if err := syncPetSnapshotBeforeDeleteTx(tx, userID, currentInfo, auxCatchTime); err != nil {
return err
}
if err := deletePetTx(tx, userID, auxCatchTime); err != nil {
return err
}
}
catchTime, err := addPetTx(tx, s.Pet, newPet, 0)
if err != nil {
return err
}
petCopy := *newPet
petCopy.CatchTime = catchTime
result.NewPet = &petCopy
return nil
})
if err == nil {
return result, 0
}
switch {
case errors.Is(err, errPetFusionInsufficientItems):
return nil, errorcode.ErrorCodes.ErrInsufficientItems
case errors.Is(err, errPetFusionPetNotFound):
return nil, errorcode.ErrorCodes.ErrPokemonNotFusionReady2
case errors.Is(err, errPetFusionPlayerNotFound):
return nil, errorcode.ErrorCodes.ErrSystemError
default:
cool.Logger.Error(context.TODO(), "pet fusion tx failed", userID, err)
return nil, errorcode.ErrorCodes.ErrSystemError
}
}
func updatePlayerInfoTx(tx gdb.TX, userID uint32, info model.PlayerInfo) error {
res, err := tx.Model(model.NewPlayer()).Where("player_id", userID).Data("data", info).Update()
if err != nil {
return err
}
affected, err := res.RowsAffected()
if err != nil {
return err
}
if affected == 0 {
return errPetFusionPlayerNotFound
}
return nil
}
func consumeItemCountsTx(tx gdb.TX, userID uint32, itemCounts map[uint32]int) error {
for itemID, count := range itemCounts {
if err := updateItemCountTx(tx, userID, itemID, -count); err != nil {
return err
}
}
return nil
}
func consumeOptionalItemTx(tx gdb.TX, userID uint32, itemIDs []uint32, target uint32) (bool, error) {
for _, itemID := range itemIDs {
if itemID != target {
continue
}
if err := updateItemCountTx(tx, userID, target, -1); err != nil {
return false, err
}
return true, nil
}
return false, nil
}
func updateItemCountTx(tx gdb.TX, userID uint32, id uint32, count int) error {
if cool.Config.ServerInfo.IsVip != 0 && count < 0 {
return nil
}
if id == 0 {
return nil
}
baseModel := tx.Model(model.NewPlayerBag()).
Where("player_id", userID).
Where("is_vip", cool.Config.ServerInfo.IsVip)
ok, err := baseModel.Where("item_id", id).Exist()
if err != nil {
return err
}
if ok {
updateModel := tx.Model(model.NewPlayerBag()).
Where("player_id", userID).
Where("is_vip", cool.Config.ServerInfo.IsVip).
Where("item_id", id)
if count < 0 {
updateModel = updateModel.Where("item_cnt + ? >= 0", count)
}
result, err := updateModel.Increment("item_cnt", count)
if err != nil {
return err
}
affected, err := result.RowsAffected()
if err != nil {
return err
}
if affected == 0 {
return errPetFusionInsufficientItems
}
return nil
}
if count <= 0 {
return errPetFusionInsufficientItems
}
_, err = tx.Model(model.NewPlayerBag()).Data(g.Map{
"player_id": userID,
"item_id": id,
"item_cnt": count,
"is_vip": cool.Config.ServerInfo.IsVip,
}).Insert()
return err
}
func updatePetDataTx(tx gdb.TX, userID uint32, catchTime uint32, pet model.PetInfo) error {
res, err := tx.Model(model.NewPet()).
Where("player_id", userID).
Where("is_vip", cool.Config.ServerInfo.IsVip).
Where("catch_time", catchTime).
Data("data", pet).
Update()
if err != nil {
return err
}
affected, err := res.RowsAffected()
if err != nil {
return err
}
if affected == 0 {
return errPetFusionPetNotFound
}
return nil
}
func syncPetSnapshotBeforeDeleteTx(tx gdb.TX, userID uint32, info model.PlayerInfo, catchTime uint32) error {
pet, ok := findPetDataSnapshotInPlayerInfo(info, catchTime)
if !ok {
return nil
}
return updatePetDataTx(tx, userID, catchTime, pet)
}
func findPetDataSnapshotInPlayerInfo(info model.PlayerInfo, catchTime uint32) (model.PetInfo, bool) {
for i := range info.PetList {
if info.PetList[i].CatchTime == catchTime {
return info.PetList[i], true
}
}
for i := range info.BackupPetList {
if info.BackupPetList[i].CatchTime == catchTime {
return info.BackupPetList[i], true
}
}
return model.PetInfo{}, false
}
func deletePetTx(tx gdb.TX, userID uint32, catchTime uint32) error {
res, err := tx.Model(model.NewPet()).
Where("player_id", userID).
Where("is_vip", cool.Config.ServerInfo.IsVip).
Where("catch_time", catchTime).
Delete()
if err != nil {
return err
}
affected, err := res.RowsAffected()
if err != nil {
return err
}
if affected == 0 {
return errPetFusionPetNotFound
}
return nil
}
func addPetTx(tx gdb.TX, petService *PetService, petInfo *model.PetInfo, saleCount uint32) (uint32, error) {
if petInfo == nil {
return 0, nil
}
catchTime, err := petService.nextCatchTime(tx.Model(baseservice.NewBaseSysUserService().Model))
if err != nil {
return 0, err
}
petCopy := *petInfo
petCopy.CatchTime = catchTime
playerPet := model.Pet{
PlayerID: petService.userid,
Data: petCopy,
CatchTime: catchTime,
Free: 0,
SaleCount: saleCount,
}
playerPet.IsVip = cool.Config.ServerInfo.IsVip
if _, err := tx.Model(model.NewPet()).Insert(playerPet); err != nil {
return 0, err
}
return catchTime, nil
}

View File

@@ -0,0 +1,50 @@
package service
import (
"blazing/modules/player/model"
"testing"
)
func TestFindPetDataSnapshotInPlayerInfo(t *testing.T) {
t.Run("pet list", func(t *testing.T) {
want := model.PetInfo{CatchTime: 1001, Level: 55}
info := model.PlayerInfo{
PetList: []model.PetInfo{want},
}
got, ok := findPetDataSnapshotInPlayerInfo(info, want.CatchTime)
if !ok {
t.Fatal("expected pet snapshot in pet list")
}
if got.CatchTime != want.CatchTime || got.Level != want.Level {
t.Fatalf("unexpected pet snapshot: %+v", got)
}
})
t.Run("backup pet list", func(t *testing.T) {
want := model.PetInfo{CatchTime: 2002, Level: 66}
info := model.PlayerInfo{
BackupPetList: []model.PetInfo{want},
}
got, ok := findPetDataSnapshotInPlayerInfo(info, want.CatchTime)
if !ok {
t.Fatal("expected pet snapshot in backup pet list")
}
if got.CatchTime != want.CatchTime || got.Level != want.Level {
t.Fatalf("unexpected pet snapshot: %+v", got)
}
})
t.Run("not found", func(t *testing.T) {
info := model.PlayerInfo{
PetList: []model.PetInfo{{CatchTime: 3003}},
BackupPetList: []model.PetInfo{{CatchTime: 4004}},
}
_, ok := findPetDataSnapshotInPlayerInfo(info, 9999)
if ok {
t.Fatal("expected missing pet snapshot")
}
})
}

File diff suppressed because it is too large Load Diff