Compare commits

..

83 Commits

Author SHA1 Message Date
昔念
188d80d41b 1 2026-04-29 10:54:15 +08:00
昔念
07b5271532 1 2026-04-29 10:33:02 +08:00
昔念
29f70513e8 Merge branches 'main' and 'main' of https://cnb.cool/blzing/blazing 2026-04-29 10:15:45 +08:00
xinian
e80e5d526c 更新说明
Some checks failed
ci/woodpecker/push/my-first-workflow Pipeline failed
2026-04-29 04:53:26 +08:00
xinian
99fd21e2fa refactor: 重命名函数为标准命名规范 2026-04-29 04:30:19 +08:00
xinian
4f7f1b5072 refactor: 重构战斗接口以分离控制器和绑定逻辑 2026-04-29 04:01:45 +08:00
xinian
6c76b050b3 refactor: 重构战斗方法调用方式
将战斗控制器的方法调用重构为直接在玩家接口上调用,
以简化代码结构并消除对 FightC 的直接依赖。
2026-04-29 03:39:21 +08:00
xinian
596d4024cc 编辑文件 Dockerfile
All checks were successful
ci/woodpecker/push/my-first-workflow Pipeline was successful
2026-04-29 02:22:33 +08:00
昔念
4673c3163e merge: resolve latest main updates
Some checks failed
ci/woodpecker/push/my-first-workflow Pipeline failed
2026-04-28 19:42:56 +08:00
昔念
52bd4333d9 1 2026-04-28 19:33:27 +08:00
xinian
4b0a17e035 feat: 增加能量珠效果持久化逻辑
Some checks failed
ci/woodpecker/push/my-first-workflow Pipeline failed
2026-04-28 06:03:05 +08:00
xinian
d2b3130414 fix: 修复双方未行动时不触发ActionStart的问题 2026-04-28 05:24:18 +08:00
xinian
48c1a7a463 refactor: 重构技能复制逻辑 2026-04-28 04:15:00 +08:00
xinian
7d49aaa212 refactor: 重构运行时ID组合逻辑
将硬编码的 ID 组合逻辑(100000*OnlineID + Port)提取为通用函数 ComposeRuntimeID,
使用 16 位位移掩码优化,并新增辅助方法与类型转换。同时修复踢人流程中的资源清理问题。
2026-04-28 04:03:13 +08:00
昔念
deae6d371e 1
Some checks failed
ci/woodpecker/push/my-first-workflow Pipeline failed
2026-04-27 19:42:05 +08:00
xinian
45f1485a11 feat: 支持跨服战斗原始cmd/data转发
Some checks failed
ci/woodpecker/push/my-first-workflow Pipeline failed
2026-04-27 06:12:13 +08:00
xinian
20d24428ac feat: 添加SPT进度检查
Some checks failed
ci/woodpecker/push/my-first-workflow Pipeline failed
2026-04-27 04:32:09 +08:00
xinian
ab7fe0639a feat: 添加商城权限自动回收机制
All checks were successful
ci/woodpecker/push/my-first-workflow Pipeline was successful
2026-04-27 01:18:20 +08:00
xinian
ba1a1ffbea refactor: 统一数据库约束维护位置
All checks were successful
ci/woodpecker/push/my-first-workflow Pipeline was successful
2026-04-27 00:57:18 +08:00
xinian
ec1855dfac refactor: 移除模型中的 uniqueIndex 约束 2026-04-27 00:54:18 +08:00
xinian
f97275cb54 1 2026-04-27 00:51:28 +08:00
xinian
6781178f6c refactor: 动态计算商店前置任务等级
All checks were successful
ci/woodpecker/push/my-first-workflow Pipeline was successful
2026-04-26 23:57:40 +08:00
昔念
073db875eb 1
All checks were successful
ci/woodpecker/push/my-first-workflow Pipeline was successful
2026-04-26 15:49:43 +08:00
昔念
949a93b2d5 1 2026-04-26 15:43:11 +08:00
昔念
9171e53e9c 11
Some checks failed
ci/woodpecker/push/my-first-workflow Pipeline failed
2026-04-26 14:49:04 +08:00
昔念
1ff4381617 1
Some checks failed
ci/woodpecker/push/my-first-workflow Pipeline failed
2026-04-26 14:36:49 +08:00
昔念
8e28e030c1 2
Some checks failed
ci/woodpecker/push/my-first-workflow Pipeline failed
2026-04-26 04:57:38 +08:00
昔念
c07e521e4e 1
All checks were successful
ci/woodpecker/push/my-first-workflow Pipeline was successful
2026-04-26 02:33:06 +08:00
昔念
4906197c77 1 2026-04-25 23:05:41 +08:00
昔念
415315c288 1
All checks were successful
ci/woodpecker/push/my-first-workflow Pipeline was successful
2026-04-25 15:55:08 +08:00
昔念
fe9c82fd2d 1
All checks were successful
ci/woodpecker/push/my-first-workflow Pipeline was successful
2026-04-25 15:08:40 +08:00
昔念
cd41b354b9 1
All checks were successful
ci/woodpecker/push/my-first-workflow Pipeline was successful
2026-04-25 13:45:19 +08:00
昔念
9452e36782 1
All checks were successful
ci/woodpecker/push/my-first-workflow Pipeline was successful
2026-04-25 02:28:50 +08:00
昔念
1efc8517e1 1
All checks were successful
ci/woodpecker/push/my-first-workflow Pipeline was successful
2026-04-24 23:02:28 +08:00
昔念
ab2928db1d 1
All checks were successful
ci/woodpecker/push/my-first-workflow Pipeline was successful
2026-04-24 22:37:41 +08:00
昔念
a9999bb93f 1
All checks were successful
ci/woodpecker/push/my-first-workflow Pipeline was successful
2026-04-24 22:11:03 +08:00
昔念
34b65f6399 1 2026-04-24 22:09:56 +08:00
昔念
2ce1057566 1
All checks were successful
ci/woodpecker/push/my-first-workflow Pipeline was successful
2026-04-24 21:57:04 +08:00
昔念
d30028157a 1 2026-04-24 21:55:19 +08:00
昔念
f0a8f521b6 1
All checks were successful
ci/woodpecker/push/my-first-workflow Pipeline was successful
2026-04-24 19:02:13 +08:00
昔念
0ae65cee45 1
All checks were successful
ci/woodpecker/push/my-first-workflow Pipeline was successful
2026-04-24 17:22:25 +08:00
昔念
11bf46c7e4 11
All checks were successful
ci/woodpecker/push/my-first-workflow Pipeline was successful
2026-04-23 23:17:16 +08:00
昔念
c4b5748e5c 1
Some checks failed
ci/woodpecker/push/my-first-workflow Pipeline failed
2026-04-23 23:15:35 +08:00
昔念
1f1fbd09d4 1
Some checks failed
ci/woodpecker/push/my-first-workflow Pipeline failed
2026-04-23 23:07:43 +08:00
昔念
b1ca3df3ae 1
All checks were successful
ci/woodpecker/push/my-first-workflow Pipeline was successful
2026-04-23 21:26:57 +08:00
xinian
57676e998f feat: 新增PVP匹配队列分组和暗黑门关卡前置检查
All checks were successful
ci/woodpecker/push/my-first-workflow Pipeline was successful
2026-04-23 14:48:34 +08:00
昔念
5500684e29 1
All checks were successful
ci/woodpecker/push/my-first-workflow Pipeline was successful
2026-04-23 00:40:01 +08:00
昔念
7fd89800fa 1 2026-04-23 00:39:29 +08:00
xinian
b46a1f442b fix: 修复战斗广播技能PP同步问题
All checks were successful
ci/woodpecker/push/my-first-workflow Pipeline was successful
2026-04-22 14:12:14 +08:00
昔念
eb76c22c41 1
All checks were successful
ci/woodpecker/push/my-first-workflow Pipeline was successful
2026-04-22 01:21:07 +08:00
昔念
a6386daad8 1
All checks were successful
ci/woodpecker/push/my-first-workflow Pipeline was successful
2026-04-22 00:39:41 +08:00
昔念
b59beed45f 1 2026-04-21 23:00:59 +08:00
昔念
77909a5940 1
All checks were successful
ci/woodpecker/push/my-first-workflow Pipeline was successful
2026-04-21 02:27:11 +08:00
昔念
c4d3ab725c 1 2026-04-21 02:16:39 +08:00
昔念
808da76bd0 1
Some checks failed
ci/woodpecker/push/my-first-workflow Pipeline failed
2026-04-21 01:56:06 +08:00
昔念
dcbd9950d3 1
Some checks failed
ci/woodpecker/push/my-first-workflow Pipeline failed
2026-04-21 01:12:26 +08:00
昔念
4b42a64da0 Fix CDK type checks and server naming ownership update 2026-04-21 00:39:12 +08:00
昔念
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
203 changed files with 50780 additions and 2254 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="https://api.jucode.cn/v1" ENV CODEX_BASE_URL="http://sub2api.sflaw.store"
ENV CODEX_MODEL="gpt-5.4" ENV CODEX_MODEL="gpt-5.5"
ENV OPENAI_API_KEY="sk-E0ZZIFNnD0RkhMC9pT2AGMutz9vNy2VLNrgyyobT5voa81pQ" ENV OPENAI_API_KEY="sk-e5d137d0e824adabc0cf674f61a558a2cbafdedf9857743116a3d23dcead1f68"
# ========================================== # ==========================================
# 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,12 +70,14 @@ 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
GO111MODULE: on GO111MODULE: on
GOSUMDB: off GOSUMDB: off
GOMODCACHE: /woodpecker/go/pkg/mod
GOCACHE: /woodpecker/.cache/go-build
commands: commands:
# 2. 清空主源文件关键先删空再写入 # 2. 清空主源文件关键先删空再写入
- > - >
@@ -135,14 +137,15 @@ steps:
- ls -lh ./build/ - ls -lh ./build/
- echo "产物名称:$BIN_NAME" - echo "产物名称:$BIN_NAME"
- echo "✅ Logic服务编译完成" - echo "✅ Logic服务编译完成"
# volumes: volumes:
# - /ext/go/pkg/mod:~/go/pkg/mod # 持久化 Go 模块缓存和编译缓存避免每次流水线都重新下载/重新编译
# - /ext/.cache/go-build:~/.cache/go-build - /ext/woodpecker-cache/go/pkg/mod:/woodpecker/go/pkg/mod
- /ext/woodpecker-cache/.cache/go-build:/woodpecker/.cache/go-build
# ========== 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 +161,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

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

@@ -1,10 +1,11 @@
package coolconfig package coolconfig
import ( import (
"os"
"strings"
"time" "time"
"github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/util/gconv"
) )
// cool config // cool config
@@ -21,7 +22,7 @@ type sConfig struct {
} }
type ServerList struct { type ServerList struct {
OnlineID uint32 `gorm:"column:online_id;comment:'在线ID';uniqueIndex" json:"online_id"` OnlineID uint32 `gorm:"column:online_id;comment:'在线ID'" json:"online_id"`
//服务器名称Desc //服务器名称Desc
Name string `gorm:"comment:'服务器名称'" json:"name"` Name string `gorm:"comment:'服务器名称'" json:"name"`
IP string `gorm:"type:string;comment:'服务器IP'" json:"ip"` IP string `gorm:"type:string;comment:'服务器IP'" json:"ip"`
@@ -48,7 +49,11 @@ type ServerList struct {
} }
func (s *ServerList) GetID() string { func (s *ServerList) GetID() string {
return gconv.String(100000*s.OnlineID + s.Port) return RuntimeIDString(s.OnlineID, s.Port)
}
func (s *ServerList) RuntimeID() uint32 {
return ComposeRuntimeID(s.OnlineID, s.Port)
} }
// OSS相关配置 // OSS相关配置
@@ -72,7 +77,7 @@ type file struct {
func newConfig() *sConfig { func newConfig() *sConfig {
var ctx g.Ctx var ctx g.Ctx
config := &sConfig{ config := &sConfig{
AutoMigrate: GetCfgWithDefault(ctx, "blazing.autoMigrate", g.NewVar(false)).Bool(), AutoMigrate: hasDebugArg(),
Name: GetCfgWithDefault(ctx, "server.name", g.NewVar("")).String(), Name: GetCfgWithDefault(ctx, "server.name", g.NewVar("")).String(),
Eps: GetCfgWithDefault(ctx, "blazing.eps", g.NewVar(false)).Bool(), Eps: GetCfgWithDefault(ctx, "blazing.eps", g.NewVar(false)).Bool(),
@@ -97,6 +102,19 @@ func newConfig() *sConfig {
return config return config
} }
func hasDebugArg() bool {
for _, arg := range os.Args[1:] {
if arg == "-debug" || arg == "--debug" {
return true
}
if strings.HasPrefix(arg, "-debug=") || strings.HasPrefix(arg, "--debug=") {
value := strings.TrimSpace(strings.SplitN(arg, "=", 2)[1])
return value != "" && value != "0" && !strings.EqualFold(value, "false")
}
}
return false
}
// qiniu 七牛云配置 // qiniu 七牛云配置
type qiniu struct { type qiniu struct {
AccessKey string `json:"ak"` AccessKey string `json:"ak"`

View File

@@ -0,0 +1,22 @@
package coolconfig
import "github.com/gogf/gf/v2/util/gconv"
const runtimeIDPortBits = 16
const runtimeIDPortMask uint32 = 0xFFFF
// ComposeRuntimeID 将 serverID 和 port 组合成运行时复合 ID。
// 高 16 位保存 serverID低 16 位保存 port。
func ComposeRuntimeID(serverID, port uint32) uint32 {
return ((serverID & runtimeIDPortMask) << runtimeIDPortBits) | (port & runtimeIDPortMask)
}
// SplitRuntimeID 将运行时复合 ID 拆分为 serverID 和 port。
func SplitRuntimeID(runtimeID uint32) (serverID, port uint32) {
return runtimeID >> runtimeIDPortBits, runtimeID & runtimeIDPortMask
}
// RuntimeIDString 返回运行时复合 ID 的字符串形式。
func RuntimeIDString(serverID, port uint32) string {
return gconv.String(ComposeRuntimeID(serverID, port))
}

View File

@@ -63,9 +63,15 @@ func CreateTable(model IModel) error {
autoMigrateMu.Lock() autoMigrateMu.Lock()
autoMigrateModels = append(autoMigrateModels, model) autoMigrateModels = append(autoMigrateModels, model)
autoMigrateMu.Unlock() autoMigrateMu.Unlock()
if !Config.AutoMigrate {
return nil return nil
} }
db := getDBbyModel(model)
return db.AutoMigrate(model)
}
// RunAutoMigrate 显式执行已注册模型的建表/迁移。 // RunAutoMigrate 显式执行已注册模型的建表/迁移。
func RunAutoMigrate() error { func RunAutoMigrate() error {
if !Config.AutoMigrate { if !Config.AutoMigrate {

View File

@@ -1,25 +1,27 @@
package cool package cool
import "blazing/cool/coolconfig"
// 存值示例 // 存值示例
func AddClient(id uint32, client *ClientHandler) { func AddClient(id uint32, client *ClientHandler) {
// 普通mapClientmap[id] = client // 普通mapClientmap[id] = client
Clientmap.Store(id, client) // sync.Map存值 Clientmap.Store(id, client) // sync.Map存值
} }
// 清理指定clientuid=100000*onlineID+port // 清理指定 client高 16 位 serverID低 16 位 port
func DeleteClientOnly(uid uint32) { func DeleteClientOnly(uid uint32) {
Clientmap.Delete(uid) Clientmap.Delete(uid)
} }
// 清理指定clientonlineID+port // 清理指定 client由 serverID 和 port 组合
func DeleteClient(id, port uint32) { func DeleteClient(id, port uint32) {
Clientmap.Delete(100000*id + port) Clientmap.Delete(coolconfig.ComposeRuntimeID(id, port))
} }
// 取值示例 // 取值示例
func GetClient(id, port uint32) (*ClientHandler, bool) { func GetClient(id, port uint32) (*ClientHandler, bool) {
// 普通mapclient, ok := Clientmap[id] // 普通mapclient, ok := Clientmap[id]
val, ok := Clientmap.Load(100000*id + port) // sync.Map取值 val, ok := Clientmap.Load(coolconfig.ComposeRuntimeID(id, port)) // sync.Map取值
if !ok { if !ok {
return nil, false return nil, false
} }

View File

@@ -1,346 +1,128 @@
package element package element
// 初始化全属性克制矩阵(经双向结果验证) import (
_ "embed"
"encoding/json"
"fmt"
"strings"
)
//go:embed data/skillTypes.json
var skillTypesJSON []byte
//go:embed data/typesRelation.json
var typesRelationJSON []byte
type skillTypesFile struct {
Root struct {
Item []skillTypeItem `json:"item"`
} `json:"root"`
}
type skillTypeItem struct {
ID int `json:"id"`
CN string `json:"cn"`
Att string `json:"att"`
IsDou int `json:"is_dou"`
EN []string `json:"en"`
}
type typeRelationFile struct {
Root struct {
Relation []typeRelationItem `json:"relation"`
} `json:"root"`
}
type typeRelationItem struct {
Type string `json:"type"`
Opponent []typeRelationOpponent `json:"opponent"`
}
type typeRelationOpponent struct {
Type string `json:"type"`
Multiple float64 `json:"multiple"`
}
// 初始化全属性克制矩阵。数据来自 seer-types-relation默认关系为 1.0。
func initFullTableMatrix() { func initFullTableMatrix() {
// 初始化17×17矩阵默认系数1.0
// 定义属性克制矩阵索引对应属性ID值为克制倍数
// 矩阵维度227x227覆盖所有属性ID1-20、221-226
// 第一步所有克制关系默认1.0(未特殊配置的都是普通关系)
for i := 0; i < maxMatrixSize; i++ { for i := 0; i < maxMatrixSize; i++ {
for j := 0; j < maxMatrixSize; j++ { for j := 0; j < maxMatrixSize; j++ {
matrix[i][j] = 1.0 matrix[i][j] = 1.0
} }
} }
// 初始化矩阵默认值为1.0以下仅列出非1.0的特殊值) enToID := initSkillTypes()
initTypeRelations(enToID)
// 1. 草grass对其他属性的克制 }
matrix[1][1] = 0.5 // 草->草
matrix[1][2] = 2 // 草->水 func initSkillTypes() map[string]int {
matrix[1][3] = 0.5 // 草->火 var cfg skillTypesFile
matrix[1][4] = 0.5 // 草->飞行 mustUnmarshalJSON(skillTypesJSON, &cfg, "skillTypes.json")
matrix[1][6] = 0.5 // 草->机械
matrix[1][7] = 2 // 草->地面 enToID := make(map[string]int, len(cfg.Root.Item))
matrix[1][12] = 2 // 草->光 for _, item := range cfg.Root.Item {
matrix[1][16] = 0.5 // 草->圣灵 mustValidElementID(item.ID, "skill type")
matrix[1][18] = 0.5 // 草->远古 if len(item.EN) == 0 {
matrix[1][222] = 0.5 // 草->混沌 panic(fmt.Sprintf("skill type %d has no en name", item.ID))
matrix[1][223] = 0.5 // 草->神灵 }
// 2. 水water对其他属性的克制 elementNameMap[item.ID] = strings.ToUpper(strings.Join(item.EN, "_"))
matrix[2][1] = 0.5 // 水->草 if item.IsDou == 0 {
matrix[2][2] = 0.5 // 水->水 validSingleElementIDs[item.ID] = true
matrix[2][3] = 2 // 水->火 enToID[item.EN[0]] = item.ID
matrix[2][7] = 2 // 水->地面 }
matrix[2][16] = 0.5 // 水->圣灵 }
matrix[2][20] = 0.5 // 水->自然
matrix[2][222] = 0.5 // 水->混沌 for _, item := range cfg.Root.Item {
matrix[2][223] = 0.5 // 水->神灵 if item.IsDou == 0 {
continue
// 3. 火fire对其他属性的克制 }
matrix[3][1] = 2 // 火->草
matrix[3][2] = 0.5 // 火->水 if len(item.EN) != 2 {
matrix[3][3] = 0.5 // 火->火 panic(fmt.Sprintf("dual skill type %d must have two en names", item.ID))
matrix[3][6] = 2 // 火->机械 }
matrix[3][9] = 2 // 火->冰 primaryID, ok := enToID[item.EN[0]]
matrix[3][16] = 0.5 // 火->圣灵 if !ok {
matrix[3][20] = 0.5 // 火->自然 panic(fmt.Sprintf("dual skill type %d references unknown primary type %q", item.ID, item.EN[0]))
matrix[3][222] = 0.5 // 火->混沌 }
matrix[3][223] = 0.5 // 火->神灵 secondaryID, ok := enToID[item.EN[1]]
if !ok {
// 4. 飞行flying对其他属性的克制 panic(fmt.Sprintf("dual skill type %d references unknown secondary type %q", item.ID, item.EN[1]))
matrix[4][1] = 2 // 飞行->草 }
matrix[4][5] = 0.5 // 飞行->电 dualElementMap[item.ID] = [2]int{primaryID, secondaryID}
matrix[4][6] = 0.5 // 飞行->机械 }
matrix[4][11] = 2 // 飞行->战斗
matrix[4][17] = 0.5 // 飞行->次元 return enToID
matrix[4][19] = 0.5 // 飞行->邪灵 }
matrix[4][20] = 0.5 // 飞行->自然
matrix[4][222] = 0.5 // 飞行->混沌 func initTypeRelations(enToID map[string]int) {
matrix[4][225] = 2 // 飞行->虫 var cfg typeRelationFile
mustUnmarshalJSON(typesRelationJSON, &cfg, "typesRelation.json")
// 5. 电electric对其他属性的克制
matrix[5][1] = 0.5 // 电->草 for _, relation := range cfg.Root.Relation {
matrix[5][2] = 2 // 电->水 attackerID, ok := enToID[relation.Type]
matrix[5][4] = 2 // 电->飞行 if !ok {
matrix[5][5] = 0.5 // 电->电 panic(fmt.Sprintf("type relation references unknown attacker type %q", relation.Type))
matrix[5][7] = 0 // 电->地面(无效) }
matrix[5][13] = 2 // 电->暗影
matrix[5][14] = 0.5 // 电->神秘 for _, opponent := range relation.Opponent {
matrix[5][16] = 0.5 // 电->圣灵 defenderID, ok := enToID[opponent.Type]
matrix[5][17] = 2 // 电->次元 if !ok {
matrix[5][20] = 0.5 // 电->自然 panic(fmt.Sprintf("type relation references unknown defender type %q", opponent.Type))
matrix[5][222] = 2 // 电->混沌 }
matrix[5][223] = 0.5 // 电->神灵 matrix[attackerID][defenderID] = opponent.Multiple
matrix[5][226] = 2 // 电->虚空 }
}
// 6. 机械steel对其他属性的克制你提供的示例已包含 }
matrix[6][2] = 0.5 // 机械->水
matrix[6][3] = 0.5 // 机械->火 func mustUnmarshalJSON(data []byte, target any, name string) {
matrix[6][5] = 0.5 // 机械->电 if err := json.Unmarshal(data, target); err != nil {
matrix[6][6] = 0.5 // 机械->机械 panic(fmt.Sprintf("parse %s: %v", name, err))
matrix[6][9] = 2 // 机械->冰 }
matrix[6][11] = 2 // 机械->战斗 }
matrix[6][17] = 0.5 // 机械->次元
matrix[6][18] = 2 // 机械->远古 func mustValidElementID(id int, kind string) {
matrix[6][19] = 2 // 机械->邪灵 if id <= 0 || id >= maxMatrixSize {
matrix[6][223] = 2 // 机械->神灵 panic(fmt.Sprintf("%s id out of range: %d", kind, id))
}
// 7. 地面ground对其他属性的克制
matrix[7][1] = 0.5 // 地面->草
matrix[7][3] = 2 // 地面->火
matrix[7][4] = 0 // 地面->飞行(无效)
matrix[7][5] = 2 // 地面->电
matrix[7][6] = 2 // 地面->机械
matrix[7][10] = 0.5 // 地面->超能
matrix[7][13] = 0.5 // 地面->暗影
matrix[7][15] = 0.5 // 地面->龙
matrix[7][16] = 0.5 // 地面->圣灵
matrix[7][20] = 0.5 // 地面->自然
matrix[7][221] = 2 // 地面->王
matrix[7][223] = 0.5 // 地面->神灵
matrix[7][224] = 2 // 地面->轮回
matrix[7][225] = 0.5 // 地面->虫
// 8. 普通normal对其他属性的克制全为1.0,无特殊值)
// 9. 冰ice对其他属性的克制
matrix[9][1] = 2 // 冰->草
matrix[9][2] = 0.5 // 冰->水
matrix[9][3] = 0.5 // 冰->火
matrix[9][4] = 2 // 冰->飞行
matrix[9][6] = 0.5 // 冰->机械
matrix[9][7] = 2 // 冰->地面
matrix[9][9] = 0.5 // 冰->冰
matrix[9][16] = 0.5 // 冰->圣灵
matrix[9][17] = 2 // 冰->次元
matrix[9][18] = 2 // 冰->远古
matrix[9][222] = 0.5 // 冰->混沌
matrix[9][223] = 0.5 // 冰->神灵
matrix[9][224] = 2 // 冰->轮回
matrix[9][225] = 2 // 冰->虫
// 10. 超能psychic对其他属性的克制
matrix[10][6] = 0.5 // 超能->机械
matrix[10][10] = 0.5 // 超能->超能
matrix[10][11] = 2 // 超能->战斗
matrix[10][12] = 0 // 超能->光(无效)
matrix[10][14] = 2 // 超能->神秘
matrix[10][20] = 2 // 超能->自然
matrix[10][225] = 0.5 // 超能->虫
// 11. 战斗fight对其他属性的克制
matrix[11][6] = 2 // 战斗->机械
matrix[11][9] = 2 // 战斗->冰
matrix[11][10] = 0.5 // 战斗->超能
matrix[11][11] = 0.5 // 战斗->战斗
matrix[11][13] = 0.5 // 战斗->暗影
matrix[11][15] = 2 // 战斗->龙
matrix[11][16] = 2 // 战斗->圣灵
matrix[11][19] = 0.5 // 战斗->邪灵
matrix[11][221] = 0.5 // 战斗->王
// 12. 光light对其他属性的克制
matrix[12][1] = 0 // 光->草(无效)
matrix[12][6] = 0.5 // 光->机械
matrix[12][9] = 0.5 // 光->冰
matrix[12][10] = 2 // 光->超能
matrix[12][12] = 0.5 // 光->光
matrix[12][13] = 2 // 光->暗影
matrix[12][16] = 0.5 // 光->圣灵
matrix[12][19] = 0.5 // 光->邪灵
matrix[12][20] = 0.5 // 光->自然
matrix[12][223] = 0.5 // 光->神灵
matrix[12][224] = 0.5 // 光->轮回
matrix[12][225] = 2 // 光->虫
matrix[12][226] = 0.5 // 光->虚空
// 13. 暗影dark对其他属性的克制
matrix[13][6] = 0.5 // 暗影->机械
matrix[13][9] = 0.5 // 暗影->冰
matrix[13][10] = 2 // 暗影->超能
matrix[13][12] = 0.5 // 暗影->光
matrix[13][13] = 2 // 暗影->暗影
matrix[13][16] = 0.5 // 暗影->圣灵
matrix[13][17] = 2 // 暗影->次元
matrix[13][19] = 0.5 // 暗影->邪灵
matrix[13][223] = 0.5 // 暗影->神灵
// 14. 神秘myth对其他属性的克制
matrix[14][5] = 2 // 神秘->电
matrix[14][7] = 0.5 // 神秘->地面
matrix[14][11] = 0.5 // 神秘->战斗
matrix[14][14] = 2 // 神秘->神秘
matrix[14][16] = 2 // 神秘->圣灵
matrix[14][19] = 0.5 // 神秘->邪灵
matrix[14][20] = 2 // 神秘->自然
matrix[14][221] = 2 // 神秘->王
matrix[14][222] = 0.5 // 神秘->混沌
matrix[14][223] = 2 // 神秘->神灵
matrix[14][224] = 2 // 神秘->轮回
matrix[14][225] = 0.5 // 神秘->虫
// 15. 龙dragon对其他属性的克制
matrix[15][1] = 0.5 // 龙->草
matrix[15][2] = 0.5 // 龙->水
matrix[15][3] = 0.5 // 龙->火
matrix[15][5] = 0.5 // 龙->电
matrix[15][9] = 2 // 龙->冰
matrix[15][15] = 2 // 龙->龙
matrix[15][16] = 2 // 龙->圣灵
matrix[15][18] = 0.5 // 龙->远古
matrix[15][19] = 2 // 龙->邪灵
matrix[15][225] = 0.5 // 龙->虫
// 16. 圣灵saint对其他属性的克制
matrix[16][1] = 2 // 圣灵->草
matrix[16][2] = 2 // 圣灵->水
matrix[16][3] = 2 // 圣灵->火
matrix[16][5] = 2 // 圣灵->电
matrix[16][9] = 2 // 圣灵->冰
matrix[16][11] = 0.5 // 圣灵->战斗
matrix[16][14] = 0.5 // 圣灵->神秘
matrix[16][15] = 0.5 // 圣灵->龙
matrix[16][18] = 2 // 圣灵->远古
matrix[16][224] = 0.5 // 圣灵->轮回
matrix[16][226] = 2 // 圣灵->虚空
// 17. 次元dimension对其他属性的克制
matrix[17][4] = 2 // 次元->飞行
matrix[17][6] = 2 // 次元->机械
matrix[17][9] = 0.5 // 次元->冰
matrix[17][10] = 2 // 次元->超能
matrix[17][13] = 0 // 次元->暗影(无效)
matrix[17][19] = 2 // 次元->邪灵
matrix[17][20] = 2 // 次元->自然
matrix[17][221] = 0.5 // 次元->王
matrix[17][222] = 0.5 // 次元->混沌
matrix[17][223] = 0.5 // 次元->神灵
matrix[17][224] = 0.5 // 次元->轮回
matrix[17][225] = 2 // 次元->虫
matrix[17][226] = 2 // 次元->虚空
// 18. 远古ancient对其他属性的克制
matrix[18][1] = 2 // 远古->草
matrix[18][4] = 2 // 远古->飞行
matrix[18][6] = 0.5 // 远古->机械
matrix[18][9] = 0.5 // 远古->冰
matrix[18][14] = 2 // 远古->神秘
matrix[18][15] = 2 // 远古->龙
matrix[18][221] = 0.5 // 远古->王
matrix[18][224] = 0.5 // 远古->轮回
matrix[18][226] = 2 // 远古->虚空
// 19. 邪灵demon对其他属性的克制
matrix[19][6] = 0.5 // 邪灵->机械
matrix[19][9] = 0.5 // 邪灵->冰
matrix[19][10] = 0.5 // 邪灵->超能
matrix[19][12] = 2 // 邪灵->光
matrix[19][13] = 2 // 邪灵->暗影
matrix[19][14] = 2 // 邪灵->神秘
matrix[19][16] = 0.5 // 邪灵->圣灵
matrix[19][17] = 2 // 邪灵->次元
matrix[19][20] = 2 // 邪灵->自然
matrix[19][221] = 0.5 // 邪灵->王
matrix[19][222] = 0.5 // 邪灵->混沌
matrix[19][223] = 0 // 邪灵->神灵(无效)
matrix[19][224] = 0.5 // 邪灵->轮回
// 20. 自然nature对其他属性的克制
matrix[20][1] = 2 // 自然->草
matrix[20][2] = 2 // 自然->水
matrix[20][3] = 2 // 自然->火
matrix[20][4] = 2 // 自然->飞行
matrix[20][5] = 2 // 自然->电
matrix[20][6] = 0.5 // 自然->机械
matrix[20][7] = 2 // 自然->地面
matrix[20][10] = 0.5 // 自然->超能
matrix[20][11] = 0.5 // 自然->战斗
matrix[20][12] = 2 // 自然->光
matrix[20][13] = 0.5 // 自然->暗影
matrix[20][14] = 0.5 // 自然->神秘
matrix[20][17] = 0.5 // 自然->次元
matrix[20][19] = 0.5 // 自然->邪灵
matrix[20][221] = 2 // 自然->王
matrix[20][222] = 0.5 // 自然->混沌
matrix[20][224] = 2 // 自然->轮回
matrix[20][226] = 0.5 // 自然->虚空
// 21. 王kingID=221对其他属性的克制
matrix[221][10] = 0.5 // 王 -> 超能
matrix[221][11] = 2 // 王 -> 战斗
matrix[221][13] = 2 // 王 -> 暗影
matrix[221][17] = 2 // 王 -> 次元
matrix[221][19] = 2 // 王 -> 邪灵
matrix[221][20] = 0.5 // 王 -> 自然
matrix[221][225] = 0.5 // 王 -> 虫
// 22. 混沌chaosID=222对其他属性的克制
matrix[222][4] = 2 // 混沌 -> 飞行
matrix[222][5] = 0.5 // 混沌 -> 电
matrix[222][6] = 0.5 // 混沌 -> 机械
matrix[222][9] = 2 // 混沌 -> 冰
matrix[222][11] = 0.5 // 混沌 -> 战斗
matrix[222][14] = 2 // 混沌 -> 神秘
matrix[222][17] = 2 // 混沌 -> 次元
matrix[222][19] = 2 // 混沌 -> 邪灵
matrix[222][20] = 2 // 混沌 -> 自然
matrix[222][222] = 1 // 混沌 -> 混沌默认1.0,显式标注)
matrix[222][223] = 2 // 混沌 -> 神灵
matrix[222][224] = 0.5 // 混沌 -> 轮回
matrix[222][225] = 2 // 混沌 -> 虫
matrix[222][226] = 0 // 混沌 -> 虚空(无效)
// 23. 神灵deityID=223对其他属性的克制
matrix[223][1] = 2 // 神灵 -> 草
matrix[223][2] = 2 // 神灵 -> 水
matrix[223][3] = 2 // 神灵 -> 火
matrix[223][5] = 2 // 神灵 -> 电
matrix[223][6] = 0.5 // 神灵 -> 机械
matrix[223][9] = 2 // 神灵 -> 冰
matrix[223][11] = 0.5 // 神灵 -> 战斗
matrix[223][15] = 0.5 // 神灵 -> 龙
matrix[223][18] = 2 // 神灵 -> 远古
matrix[223][19] = 2 // 神灵 -> 邪灵
matrix[223][222] = 2 // 神灵 -> 混沌
matrix[223][223] = 1 // 神灵 -> 神灵默认1.0,显式标注)
// 24. 轮回samsaraID=224对其他属性的克制
matrix[224][9] = 0.5 // 轮回 -> 冰
matrix[224][10] = 0.5 // 轮回 -> 超能
matrix[224][12] = 2 // 轮回 -> 光
matrix[224][13] = 2 // 轮回 -> 暗影
matrix[224][16] = 2 // 轮回 -> 圣灵
matrix[224][17] = 2 // 轮回 -> 次元
matrix[224][19] = 2 // 轮回 -> 邪灵
matrix[224][20] = 0.5 // 轮回 -> 自然
matrix[224][222] = 2 // 轮回 -> 混沌
matrix[224][224] = 1 // 轮回 -> 轮回默认1.0,显式标注)
matrix[224][226] = 0.5 // 轮回 -> 虚空
// 25. 虫insectID=225对其他属性的克制
matrix[225][1] = 2 // 虫 -> 草
matrix[225][2] = 0.5 // 虫 -> 水
matrix[225][3] = 0.5 // 虫 -> 火
matrix[225][7] = 2 // 虫 -> 地面
matrix[225][9] = 0.5 // 虫 -> 冰
matrix[225][11] = 2 // 虫 -> 战斗
matrix[225][12] = 0.5 // 虫 -> 光
matrix[225][222] = 2 // 虫 -> 混沌
matrix[225][224] = 1 // 虫 -> 轮回默认1.0,显式标注)
matrix[225][225] = 2 // 虫 -> 虫
// 26. 虚空voidID=226对其他属性的克制
matrix[226][4] = 0.5 // 虚空 -> 飞行
matrix[226][10] = 2 // 虚空 -> 超能
matrix[226][11] = 2 // 虚空 -> 战斗
matrix[226][12] = 2 // 虚空 -> 光
matrix[226][13] = 0.5 // 虚空 -> 暗影
matrix[226][14] = 2 // 虚空 -> 神秘
matrix[226][16] = 0.5 // 虚空 -> 圣灵
matrix[226][17] = 0.5 // 虚空 -> 次元
matrix[226][20] = 2 // 虚空 -> 自然
matrix[226][222] = 1 // 虚空 -> 混沌默认1.0,显式标注)
matrix[226][224] = 2 // 虚空 -> 轮回
matrix[226][226] = 1 // 虚空 -> 虚空默认1.0,显式标注)
} }

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -42,154 +42,14 @@ const (
maxMatrixSize = 227 // 矩阵维度覆盖最大属性ID 226 maxMatrixSize = 227 // 矩阵维度覆盖最大属性ID 226
) )
// 合法单属性ID集合按ID直接索引避免运行时 map 查找) // 属性配置由 data/skillTypes.json 初始化,数据来自 seer-types-relation。
var validSingleElementIDs = [maxMatrixSize]bool{ var validSingleElementIDs [maxMatrixSize]bool
1: true, 2: true, 3: true, 4: true, 5: true, 6: true, 7: true, 8: true, 9: true, 10: true,
11: true, 12: true, 13: true, 14: true, 15: true, 16: true, 17: true, 18: true, 19: true, 20: true,
221: true, 222: true, 223: true, 224: true, 225: true, 226: true,
}
// 元素名称映射按ID直接索引便于日志输出 // 元素名称映射按ID直接索引便于日志输出
var elementNameMap = [maxMatrixSize]string{ var elementNameMap [maxMatrixSize]string
ElementTypeGrass: "GRASS",
ElementTypeWater: "WATER",
ElementTypeFire: "FIRE",
ElementTypeFlying: "FLYING",
ElementTypeElectric: "ELECTRIC",
ElementTypeSteel: "STEEL",
ElementTypeGround: "GROUND",
ElementTypeNormal: "NORMAL",
ElementTypeIce: "ICE",
ElementTypePsychic: "PSYCHIC",
ElementTypeFighting: "FIGHTING",
ElementTypeLight: "LIGHT",
ElementTypeDark: "DARK",
ElementTypeMythic: "MYTHIC",
ElementTypeDragon: "DRAGON",
ElementTypeSaint: "SAINT",
ElementTypeDimension: "DIMENSION",
ElementTypeAncient: "ANCIENT",
ElementTypeDemon: "DEMON",
ElementTypeNature: "NATURE",
ElementTypeKing: "KING",
ElementTypeChaos: "CHAOS",
ElementTypeDeity: "DEITY",
ElementTypeSamsara: "SAMSARA",
ElementTypeInsect: "INSECT",
ElementTypeVoid: "VOID",
}
// 双属性映射key=双属性IDvalue=组成的两个单属性ID // 双属性映射key=双属性IDvalue=组成的两个单属性ID
var dualElementMap = map[int][2]int{ var dualElementMap = map[int][2]int{}
21: {1, 10}, // 草 超能
22: {1, 11}, // 草 战斗
23: {1, 13}, // 草 暗影
24: {2, 10}, // 水 超能
25: {2, 13}, // 水 暗影
26: {2, 15}, // 水 龙
27: {3, 4}, // 火 飞行
28: {3, 15}, // 火 龙
29: {3, 10}, // 火 超能
30: {4, 10}, // 飞行 超能
31: {12, 4}, // 光 飞行
32: {4, 15}, // 飞行 龙
33: {5, 3}, // 电 火
34: {5, 9}, // 电 冰
35: {5, 11}, // 电 战斗
36: {13, 5}, // 暗影 电
37: {6, 7}, // 机械 地面
38: {6, 10}, // 机械 超能
39: {6, 15}, // 机械 龙
40: {7, 15}, // 地面 龙
41: {11, 7}, // 战斗 地面
42: {7, 13}, // 地面 暗影
43: {9, 15}, // 冰 龙
44: {9, 12}, // 冰 光
45: {9, 13}, // 冰 暗影
46: {10, 9}, // 超能 冰
47: {11, 3}, // 战斗 火
48: {11, 13}, // 战斗 暗影
49: {12, 14}, // 光 神秘
50: {13, 14}, // 暗影 神秘
51: {14, 10}, // 神秘 超能
52: {16, 12}, // 圣灵 光
53: {4, 14}, // 飞行 神秘
54: {7, 10}, // 地面 超能
55: {13, 15}, // 暗影 龙
56: {16, 13}, // 圣灵 暗影
57: {18, 11}, // 远古 战斗
58: {3, 14}, // 火 神秘
59: {12, 11}, // 光 战斗
60: {14, 11}, // 神秘 战斗
61: {17, 11}, // 次元 战斗
62: {19, 14}, // 邪灵 神秘
63: {18, 15}, // 远古 龙
64: {12, 17}, // 光 次元
65: {18, 16}, // 远古 圣灵
66: {2, 11}, // 水 战斗
67: {5, 15}, // 电 龙
68: {12, 3}, // 光 火
69: {12, 13}, // 光 暗影
70: {19, 15}, // 邪灵 龙
71: {18, 14}, // 远古 神秘
72: {6, 17}, // 机械 次元
73: {11, 15}, // 战斗 龙
74: {11, 20}, // 战斗 自然
75: {19, 6}, // 邪灵 机械
76: {5, 17}, // 电 次元
77: {18, 3}, // 远古 火
78: {16, 11}, // 圣灵 战斗
79: {16, 17}, // 圣灵 次元
80: {16, 5}, // 圣灵 电
81: {18, 7}, // 远古 地面
82: {18, 1}, // 远古 草
83: {20, 15}, // 自然 龙
84: {9, 14}, // 冰 神秘
85: {4, 13}, // 飞行 暗影
86: {9, 3}, // 冰 火
87: {9, 4}, // 冰 飞行
88: {20, 16}, // 自然 圣灵
89: {222, 16}, // 混沌 圣灵
90: {18, 19}, // 远古 邪灵
91: {20, 9}, // 自然 冰
92: {222, 13}, // 混沌 暗影
93: {222, 11}, // 混沌 战斗
94: {222, 10}, // 混沌 超能
95: {16, 10}, // 圣灵 超能
96: {222, 7}, // 混沌 地面
97: {13, 19}, // 暗影 邪灵
98: {222, 18}, // 混沌 远古
99: {222, 19}, // 混沌 邪灵
100: {16, 7}, // 圣灵 地面
101: {3, 13}, // 火 暗影
102: {12, 10}, // 光 超能
103: {6, 11}, // 机械 战斗
104: {4, 5}, // 飞行 电
105: {222, 4}, // 混沌 飞行
106: {222, 15}, // 混沌 龙
107: {222, 3}, // 混沌 火
108: {16, 3}, // 圣灵 火
109: {7, 14}, // 地面 神秘
110: {222, 17}, // 混沌 次元
111: {222, 9}, // 混沌 冰
112: {20, 14}, // 自然 神秘
113: {226, 19}, // 虚空 邪灵
114: {226, 222}, // 虚空 混沌
115: {16, 224}, // 圣灵 轮回
116: {2, 17}, // 水 次元
117: {16, 14}, // 圣灵 神秘
118: {6, 14}, // 机械 神秘
119: {2, 14}, // 水 神秘
120: {17, 15}, // 次元 龙
121: {20, 10}, // 自然 超能
122: {5, 6}, // 电 机械
123: {14, 224}, // 神秘 轮回
124: {2, 6}, // 水 机械
125: {3, 6}, // 火 机械
126: {1, 6}, // 草 机械
127: {18, 5}, // 远古 电
128: {16, 4}, // 圣灵 飞行
}
// 元素组合结构体 // 元素组合结构体
type ElementCombination struct { type ElementCombination struct {

View File

@@ -0,0 +1,56 @@
package element
import (
"math"
"testing"
)
func TestGetOffensiveMultiplierFromSeerTypesRelation(t *testing.T) {
tests := []struct {
name string
attackerID int
defenderID int
want float64
}{
{name: "single to single", attackerID: 1, defenderID: 2, want: 2},
{name: "immunity stays zero", attackerID: 5, defenderID: 7, want: 0},
{name: "single to double", attackerID: 3, defenderID: 43, want: 1.5},
{name: "double to single", attackerID: 35, defenderID: 7, want: 0.25},
{name: "double to double", attackerID: 92, defenderID: 223, want: 1.25},
{name: "new dual type ancient steel", attackerID: 129, defenderID: 223, want: 1.5},
{name: "new dual type ancient light", attackerID: 130, defenderID: 226, want: 1.25},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Calculator.GetOffensiveMultiplier(tt.attackerID, tt.defenderID)
if err != nil {
t.Fatalf("GetOffensiveMultiplier() error = %v", err)
}
if math.Abs(got-tt.want) > 0.001 {
t.Fatalf("GetOffensiveMultiplier() = %v, want %v", got, tt.want)
}
})
}
}
func TestGetCombinationIncludesSeerTypesRelationDualTypes(t *testing.T) {
tests := []struct {
id int
primary ElementType
secondary ElementType
}{
{id: 129, primary: ElementTypeSteel, secondary: ElementTypeAncient},
{id: 130, primary: ElementTypeLight, secondary: ElementTypeAncient},
}
for _, tt := range tests {
got, err := Calculator.GetCombination(tt.id)
if err != nil {
t.Fatalf("GetCombination(%d) error = %v", tt.id, err)
}
if got.Primary != tt.primary || got.Secondary == nil || *got.Secondary != tt.secondary {
t.Fatalf("GetCombination(%d) = %#v, want primary %v secondary %v", tt.id, got, tt.primary, tt.secondary)
}
}
}

File diff suppressed because one or more lines are too long

View File

@@ -21,17 +21,26 @@ type PVPMatchJoinPayload struct {
Nick string `json:"nick"` Nick string `json:"nick"`
FightMode uint32 `json:"fightMode"` FightMode uint32 `json:"fightMode"`
Status uint32 `json:"status"` Status uint32 `json:"status"`
IsVip uint32 `json:"isVip"`
IsDebug uint8 `json:"isDebug"`
CatchTimes []uint32 `json:"catchTimes"` CatchTimes []uint32 `json:"catchTimes"`
TianxuanPetIDs []uint32 `json:"tianxuanPetIds"`
}
type pvpMatchQueueKey struct {
FightMode uint32
IsVip uint32
IsDebug uint8
} }
type pvpMatchCoordinator struct { type pvpMatchCoordinator struct {
mu sync.Mutex mu sync.Mutex
queues map[uint32][]pvpwire.QueuePlayerSnapshot queues map[pvpMatchQueueKey][]pvpwire.QueuePlayerSnapshot
lastSeen map[uint32]time.Time lastSeen map[uint32]time.Time
} }
var defaultPVPMatchCoordinator = &pvpMatchCoordinator{ var defaultPVPMatchCoordinator = &pvpMatchCoordinator{
queues: make(map[uint32][]pvpwire.QueuePlayerSnapshot), queues: make(map[pvpMatchQueueKey][]pvpwire.QueuePlayerSnapshot),
lastSeen: make(map[uint32]time.Time), lastSeen: make(map[uint32]time.Time),
} }
@@ -51,8 +60,11 @@ func (m *pvpMatchCoordinator) JoinOrUpdate(payload PVPMatchJoinPayload) error {
Nick: payload.Nick, Nick: payload.Nick,
FightMode: payload.FightMode, FightMode: payload.FightMode,
Status: payload.Status, Status: payload.Status,
IsVip: payload.IsVip,
IsDebug: payload.IsDebug,
JoinedAtUnix: now.Unix(), JoinedAtUnix: now.Unix(),
CatchTimes: append([]uint32(nil), payload.CatchTimes...), CatchTimes: append([]uint32(nil), payload.CatchTimes...),
TianxuanPetIDs: append([]uint32(nil), payload.TianxuanPetIDs...),
} }
var match *pvpwire.MatchFoundPayload var match *pvpwire.MatchFoundPayload
@@ -62,11 +74,12 @@ func (m *pvpMatchCoordinator) JoinOrUpdate(payload PVPMatchJoinPayload) error {
m.removeUserLocked(payload.UserID) m.removeUserLocked(payload.UserID)
m.lastSeen[payload.UserID] = now m.lastSeen[payload.UserID] = now
queue := m.queues[payload.FightMode] queueKey := newPVPMatchQueueKey(player)
queue := m.queues[queueKey]
if len(queue) > 0 { if len(queue) > 0 {
host := queue[0] host := queue[0]
queue = queue[1:] queue = queue[1:]
m.queues[payload.FightMode] = queue m.queues[queueKey] = queue
delete(m.lastSeen, host.UserID) delete(m.lastSeen, host.UserID)
delete(m.lastSeen, payload.UserID) delete(m.lastSeen, payload.UserID)
@@ -79,7 +92,7 @@ func (m *pvpMatchCoordinator) JoinOrUpdate(payload PVPMatchJoinPayload) error {
} }
match = &result match = &result
} else { } else {
m.queues[payload.FightMode] = append(queue, player) m.queues[queueKey] = append(queue, player)
} }
m.mu.Unlock() m.mu.Unlock()
@@ -109,7 +122,7 @@ func (m *pvpMatchCoordinator) Cancel(userID uint32) {
} }
func (m *pvpMatchCoordinator) pruneExpiredLocked(now time.Time) { func (m *pvpMatchCoordinator) pruneExpiredLocked(now time.Time) {
for mode, queue := range m.queues { for key, queue := range m.queues {
next := make([]pvpwire.QueuePlayerSnapshot, 0, len(queue)) next := make([]pvpwire.QueuePlayerSnapshot, 0, len(queue))
for _, queued := range queue { for _, queued := range queue {
last := m.lastSeen[queued.UserID] last := m.lastSeen[queued.UserID]
@@ -119,12 +132,12 @@ func (m *pvpMatchCoordinator) pruneExpiredLocked(now time.Time) {
} }
next = append(next, queued) next = append(next, queued)
} }
m.queues[mode] = next m.queues[key] = next
} }
} }
func (m *pvpMatchCoordinator) removeUserLocked(userID uint32) { func (m *pvpMatchCoordinator) removeUserLocked(userID uint32) {
for mode, queue := range m.queues { for key, queue := range m.queues {
next := make([]pvpwire.QueuePlayerSnapshot, 0, len(queue)) next := make([]pvpwire.QueuePlayerSnapshot, 0, len(queue))
for _, queued := range queue { for _, queued := range queue {
if queued.UserID == userID { if queued.UserID == userID {
@@ -132,7 +145,15 @@ func (m *pvpMatchCoordinator) removeUserLocked(userID uint32) {
} }
next = append(next, queued) next = append(next, queued)
} }
m.queues[mode] = next m.queues[key] = next
}
}
func newPVPMatchQueueKey(player pvpwire.QueuePlayerSnapshot) pvpMatchQueueKey {
return pvpMatchQueueKey{
FightMode: player.FightMode,
IsVip: player.IsVip,
IsDebug: player.IsDebug,
} }
} }

View File

@@ -3,10 +3,13 @@ package rpc
import ( import (
"blazing/common/data/share" "blazing/common/data/share"
"blazing/cool" "blazing/cool"
"blazing/cool/coolconfig"
"context" "context"
"fmt" "fmt"
"log" "log"
"net/url"
"strings"
"time" "time"
config "blazing/modules/config/service" config "blazing/modules/config/service"
@@ -19,6 +22,19 @@ import (
type ServerHandler struct{} type ServerHandler struct{}
const kickForwardTimeout = 3 * time.Second const kickForwardTimeout = 3 * time.Second
const ClientCallTimeout = 5 * time.Second
// A 服强关留下僵尸在线状态B 服可以通过 login 清理后登录。
// login 服不可用B 服不会放行,仍提示系统忙。
func isDisconnectedLogicReverseClientError(err error) bool {
if err == nil {
return false
}
errText := err.Error()
return strings.Contains(errText, "websocket routine exiting") ||
strings.Contains(errText, "sendRequest failed") ||
strings.Contains(errText, "closed out channel")
}
// 实现踢人 // 实现踢人
func (*ServerHandler) Kick(_ context.Context, userid uint32) error { func (*ServerHandler) Kick(_ context.Context, userid uint32) error {
@@ -57,6 +73,11 @@ func (*ServerHandler) Kick(_ context.Context, userid uint32) error {
cool.DeleteClientOnly(useid2) cool.DeleteClientOnly(useid2)
return nil return nil
} }
if isDisconnectedLogicReverseClientError(callErr) {
_ = share.ShareManager.DeleteUserOnline(userid)
cool.DeleteClientOnly(useid2)
return nil
}
// 仍在线则返回失败,不按成功处理 // 仍在线则返回失败,不按成功处理
return callErr return callErr
@@ -79,22 +100,7 @@ func (*ServerHandler) Kick(_ context.Context, userid uint32) error {
// 注册logic服务器 // 注册logic服务器
func (*ServerHandler) RegisterLogic(ctx context.Context, id, port uint32) error { func (*ServerHandler) RegisterLogic(ctx context.Context, id, port uint32) error {
fmt.Println("注册logic服务器", id, port) fmt.Println("注册logic服务器", id, port)
return registerReverseLogicClient(ctx, id, port)
//TODO 待修复滚动更新可能导致的玩家可以同时在旧服务器和新服务器同时在线的bug
revClient, ok := jsonrpc.ExtractReverseClient[cool.ClientHandler](ctx)
if !ok {
return fmt.Errorf("no reverse client")
}
t := config.NewServerService().GetServerID((id))
aa, ok := cool.GetClient(t.OnlineID, t.Port)
if ok && aa != nil { //如果已经存在且这个端口已经被存过
aa.QuitSelf(0)
}
cool.AddClient(100000*id+port, &revClient)
//Refurh()
return nil
} }
@@ -109,7 +115,7 @@ func (*ServerHandler) MatchCancel(_ context.Context, userID uint32) error {
func CServer() *jsonrpc.RPCServer { func CServer() *jsonrpc.RPCServer {
// create a new server instance // create a new server instance
rpcServer := jsonrpc.NewServer(jsonrpc.WithReverseClient[cool.ClientHandler]("")) rpcServer := jsonrpc.NewServer(jsonrpc.WithReverseClientSetup[cool.ClientHandler]("", setupLogicReverseClient))
rpcServer.Register("", &ServerHandler{}) rpcServer.Register("", &ServerHandler{})
@@ -120,32 +126,35 @@ func CServer() *jsonrpc.RPCServer {
var closer jsonrpc.ClientCloser var closer jsonrpc.ClientCloser
func StartClient(id, port uint32, callback any) *struct { func StartClient(id, port uint32, callback any) *struct {
Kick func(uint32) error Kick func(context.Context, uint32) error
RegisterLogic func(uint32, uint32) error RegisterLogic func(context.Context, uint32, uint32) error
MatchJoinOrUpdate func(PVPMatchJoinPayload) error MatchJoinOrUpdate func(context.Context, PVPMatchJoinPayload) error
MatchCancel func(uint32) error MatchCancel func(context.Context, uint32) error
} { } {
//cool.Config.File.Domain = "127.0.0.1" cool.Config.File.Domain = "127.0.0.1"
var rpcaddr = "ws://" + cool.Config.File.Domain + gconv.String(cool.Config.Address) + "/rpc" u := url.URL{
Scheme: "ws",
Host: cool.Config.File.Domain + gconv.String(cool.Config.Address),
Path: "/rpc",
}
q := u.Query()
q.Set("logic_id", gconv.String(id))
q.Set("logic_port", gconv.String(port))
u.RawQuery = q.Encode()
rpcaddr := u.String()
closer1, err := jsonrpc.NewMergeClient(context.Background(), closer1, err := jsonrpc.NewMergeClient(context.Background(),
rpcaddr, "", []interface{}{ rpcaddr, "", []interface{}{
&RPCClient, &RPCClient,
}, nil, jsonrpc.WithClientHandler("", callback), }, nil, jsonrpc.WithClientHandler("", callback),
jsonrpc.WithReconnFun(func() { RPCClient.RegisterLogic(id, port) }),
) )
if err != nil { if err != nil {
log.Fatalf("Failed to create client: %v", err) log.Fatalf("Failed to create client: %v", err)
} }
//if port != 0 { //注册logic
defer RPCClient.RegisterLogic(id, port)
//}
closer = closer1 closer = closer1
return &RPCClient return &RPCClient
@@ -153,14 +162,55 @@ func StartClient(id, port uint32, callback any) *struct {
// Setup RPCClient with reverse call handler // Setup RPCClient with reverse call handler
var RPCClient struct { var RPCClient struct {
Kick func(uint32) error //踢人 Kick func(context.Context, uint32) error //踢人
RegisterLogic func(uint32, uint32) error RegisterLogic func(context.Context, uint32, uint32) error
MatchJoinOrUpdate func(PVPMatchJoinPayload) error MatchJoinOrUpdate func(context.Context, PVPMatchJoinPayload) error
MatchCancel func(uint32) error MatchCancel func(context.Context, uint32) error
// UserLogin func(int32, int32) error //用户登录事件 // UserLogin func(int32, int32) error //用户登录事件
// UserLogout func(int32, int32) error //用户登出事件 // UserLogout func(int32, int32) error //用户登出事件
} }
func setupLogicReverseClient(ctx context.Context, revClient cool.ClientHandler) error {
_ = revClient
req, ok := jsonrpc.GetHTTPRequest(ctx)
if !ok || req == nil {
return fmt.Errorf("missing websocket request context")
}
id := gconv.Uint32(req.URL.Query().Get("logic_id"))
port := gconv.Uint32(req.URL.Query().Get("logic_port"))
if id == 0 || port == 0 {
return fmt.Errorf("missing logic identity in websocket query: id=%d port=%d", id, port)
}
if err := registerReverseLogicClient(ctx, id, port); err != nil {
return err
}
key := coolconfig.ComposeRuntimeID(id, port)
go func() {
<-ctx.Done()
cool.DeleteClientOnly(key)
}()
return nil
}
func registerReverseLogicClient(ctx context.Context, id, port uint32) error {
//TODO 待修复滚动更新可能导致的玩家可以同时在旧服务器和新服务器同时在线的bug
revClient, ok := jsonrpc.ExtractReverseClient[cool.ClientHandler](ctx)
if !ok {
return fmt.Errorf("no reverse client")
}
t := config.NewServerService().GetServerID(id)
aa, ok := cool.GetClient(t.OnlineID, t.Port)
if ok && aa != nil { //如果已经存在且这个端口已经被存过
aa.QuitSelf(0)
}
cool.AddClient(coolconfig.ComposeRuntimeID(id, port), &revClient)
return nil
}

View File

@@ -18,7 +18,7 @@ import (
) )
type RPCfight struct { type RPCfight struct {
fightmap *csmap.CsMap[int, common.FightI] fightmap *csmap.CsMap[int, common.FightControllerI]
zs *zset.ZSet[uint32, *model.PVP] zs *zset.ZSet[uint32, *model.PVP]
} }
@@ -69,7 +69,7 @@ func (r *RPCfight) cancel(pvp info.RPCFightinfo) {
///定义map,存储用户对战斗容器的映射,便于外部传入时候进行直接操作 ///定义map,存储用户对战斗容器的映射,便于外部传入时候进行直接操作
var fightmap = RPCfight{ var fightmap = RPCfight{
fightmap: csmap.New[int, common.FightI](), fightmap: csmap.New[int, common.FightControllerI](),
zs: zset.New[uint32, *model.PVP](func(a, b *model.PVP) bool { zs: zset.New[uint32, *model.PVP](func(a, b *model.PVP) bool {
return a.Less(b) return a.Less(b)
}), }),

View File

@@ -78,7 +78,7 @@ func (s *Server) OnClose(c gnet.Conn, err error) (action gnet.Action) {
if v != nil { if v != nil {
v.Close() v.Close()
if v.Player != nil { if v.Player != nil {
v.Player.Save() //保存玩家数据 v.Player.SaveOnDisconnect() //保存玩家数据
} }
} }
return return

View File

@@ -115,6 +115,10 @@ var ErrorCodes = enum.New[struct {
ErrPokemonLevelTooLow ErrorCode `enum:"13007"` ErrPokemonLevelTooLow ErrorCode `enum:"13007"`
// 不能展示背包里的精灵! // 不能展示背包里的精灵!
ErrCannotShowBagPokemon ErrorCode `enum:"13017"` ErrCannotShowBagPokemon ErrorCode `enum:"13017"`
// 基地展示精灵数量已达上限!
ErrRoomShowPetLimit ErrorCode `enum:"13019"`
// 该精灵不在仓库中,无法设为基地展示!
ErrPetNotInWarehouse ErrorCode `enum:"13021"`
// 你今天已经被吃掉过一回了,明天再来吧! // 你今天已经被吃掉过一回了,明天再来吧!
ErrAlreadyEatenToday ErrorCode `enum:"17018"` ErrAlreadyEatenToday ErrorCode `enum:"17018"`
// 该道具已经在使用中,无法重复使用。 // 该道具已经在使用中,无法重复使用。

View File

@@ -344,7 +344,7 @@ func websocketClient(ctx context.Context, addr string, namespace string, outs []
} }
func (c *client) setupRequestChan() chan clientRequest { func (c *client) setupRequestChan() chan clientRequest {
requests := make(chan clientRequest) requests := make(chan clientRequest, 1024)
c.doRequest = func(ctx context.Context, cr clientRequest) (clientResponse, error) { c.doRequest = func(ctx context.Context, cr clientRequest) (clientResponse, error) {
select { select {

View File

@@ -75,11 +75,7 @@ func WithTracer(l Tracer) ServerOption {
} }
} }
// WithReverseClient will allow extracting reverse client on **WEBSOCKET** calls. func buildReverseClient[RP any](c *ServerConfig, ctx context.Context, conn *wsConn, namespace string, onConnect func(context.Context, RP) error) (context.Context, error) {
// RP is a proxy-struct type, much like the one passed to NewClient.
func WithReverseClient[RP any](namespace string) ServerOption {
return func(c *ServerConfig) {
c.reverseClientBuilder = func(ctx context.Context, conn *wsConn) (context.Context, error) {
cl := client{ cl := client{
namespace: namespace, namespace: namespace,
paramEncoders: map[reflect.Type]ParamEncoder{}, paramEncoders: map[reflect.Type]ParamEncoder{},
@@ -101,7 +97,31 @@ func WithReverseClient[RP any](namespace string) ServerOption {
return nil, xerrors.Errorf("provide reverse client calls: %w", err) return nil, xerrors.Errorf("provide reverse client calls: %w", err)
} }
return context.WithValue(ctx, jsonrpcReverseClient{reflect.TypeOf(calls).Elem()}, calls), nil ctx = context.WithValue(ctx, jsonrpcReverseClient{reflect.TypeOf(calls).Elem()}, calls)
if onConnect != nil {
if err := onConnect(ctx, *calls); err != nil {
return nil, err
}
}
return ctx, nil
}
// WithReverseClient will allow extracting reverse client on **WEBSOCKET** calls.
// RP is a proxy-struct type, much like the one passed to NewClient.
func WithReverseClient[RP any](namespace string) ServerOption {
return func(c *ServerConfig) {
c.reverseClientBuilder = func(ctx context.Context, conn *wsConn) (context.Context, error) {
return buildReverseClient[RP](c, ctx, conn, namespace, nil)
}
}
}
// WithReverseClientSetup behaves like WithReverseClient, and also runs onConnect
// once the reverse client has been created for the websocket connection.
func WithReverseClientSetup[RP any](namespace string, onConnect func(context.Context, RP) error) ServerOption {
return func(c *ServerConfig) {
c.reverseClientBuilder = func(ctx context.Context, conn *wsConn) (context.Context, error) {
return buildReverseClient[RP](c, ctx, conn, namespace, onConnect)
} }
} }
} }

View File

@@ -45,6 +45,16 @@ func GetConnectionType(ctx context.Context) ConnectionType {
return ConnectionTypeUnknown return ConnectionTypeUnknown
} }
// GetHTTPRequest returns the original HTTP request when the context comes from RPCServer.
func GetHTTPRequest(ctx context.Context) (*http.Request, bool) {
v := ctx.Value(httpRequestCtxKey{})
if v == nil {
return nil, false
}
req, ok := v.(*http.Request)
return req, ok
}
// RPCServer provides a jsonrpc 2.0 http server handler // RPCServer provides a jsonrpc 2.0 http server handler
type RPCServer struct { type RPCServer struct {
*handler *handler
@@ -75,6 +85,8 @@ var upgrader = websocket.Upgrader{
} }
func (s *RPCServer) handleWS(ctx context.Context, w http.ResponseWriter, r *http.Request) { func (s *RPCServer) handleWS(ctx context.Context, w http.ResponseWriter, r *http.Request) {
ctx = context.WithValue(ctx, httpRequestCtxKey{}, r)
// TODO: allow setting // TODO: allow setting
// (note that we still are mostly covered by jwt tokens) // (note that we still are mostly covered by jwt tokens)
w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Origin", "*")
@@ -181,3 +193,5 @@ func (s *RPCServer) AliasMethod(alias, original string) {
} }
var _ error = &JSONRPCError{} var _ error = &JSONRPCError{}
type httpRequestCtxKey struct{}

View File

@@ -661,7 +661,9 @@ func (c *wsConn) tryReconnect(ctx context.Context) bool {
c.writeLk.Unlock() c.writeLk.Unlock()
go c.nextMessage() go c.nextMessage()
c.reconfun() if c.reconfun != nil {
go c.reconfun()
}
}() }()
return true return true

View File

@@ -0,0 +1,81 @@
# 巅峰之战天选池当前逻辑说明
更新时间2026-04-26
## 1. 入口职责
- `point_1`巅峰之战主页面展示当前玩家已配置的天选精灵
- `point_2`天选池投票页面只保存玩家本周投票
- `point_3`天选精灵配置页面配置玩家自己的天选精灵属性
## 2. 投票数据
投票表是 `config_peak_tianxuan_vote`
字段口径
- `week_index`日期周格式为 ISO 年周例如 `202617`
- `player_id`投票玩家
- `pet_id`投票精灵
约束口径
- 每个玩家每个日期周只能投 1 只精灵
- 同周重复投票时更新原记录
- 票数统计按 `week_index + pet_id` 聚合
## 3. 投票池
`point_2` 的候选池来自乱斗精灵池 `config_boss_melee` `mon_id` 去重
当前不再因为上周出现过就从候选池移除上周投票结果只作为展示和后续配置参考
页面会展示
- 当前日期周投票池
- 上一日期周投票统计
- 当前玩家本周已投的精灵
## 4. 天选配置
天选配置表是 `config_peak_tianxuan`
这张表现在表示玩家额外拥有的天选精灵配置参考 `player_pet` 的归属思路不再按周保存
核心字段
- `player_id`
- `pet_id`
- `display_order`
- `preset_name`
- `level`
- `nature`
- `hp`
- `max_hp`
- `attack`
- `defence`
- `sp_attack`
- `sp_defence`
- `speed`
- `skin_id`
- `effect_ids`
- `skill_ids`
- `remark`
- `is_enable`
约束口径
- 同一玩家不能重复配置同一只天选精灵
- 不再校验本周不能和上周重复
- 不再要求 `week_index`
## 5. BP 使用
BP 不直接读取投票记录
进入 BP 时读取双方各自的 `config_peak_tianxuan` 配置
- 我方看到自己的天选配置
- 对方看到对方自己的天选配置
投票结果后续可用于决定哪些精灵开放给玩家配置但配置本身是玩家维度的数据

View File

@@ -0,0 +1,42 @@
# 巅峰之战天选池投票设计
## 规则补全
- 参与规则
- 白银及以下至少 1 100 级以上精灵进行 `1v1`
- 黄金铂金紫晶至少 3 100 级以上精灵进行 `3v3`
- 钻石及以上至少 6 100 级以上精灵进行 `6v6`
- 段位规则
- 初始段位为青铜初始分 `800`
- `1000` 白银`1200` 黄金`1500` 铂金`1800` 紫晶`2100` 钻石`3000` 大师`4000` 王者
- 王者榜前 100 自动晋升至尊王者
- 天选池规则
- 每周开放一组天选池候选精灵
- 玩家在进入巅峰匹配前先从本周天选池中投票选择自己的天选备选
- 白银及以下可选 `1` 紫晶及以下可选 `2` 钻石及以上可选 `3`
- 这些投票结果跟随匹配快照一起进入后续 BP 流程
- BP 规则
- 钻石以下默认无 Ban/Pick 强制要求但前后端结构统一预留天选池字段
- 钻石及以上进入正式 BP
- BP 展示区应包含
- 出战备战精灵
- 天选精灵
- 双方互 Ban / Pick
- BP 完成后再进入正式对战
- 奖励规则
- 本次仅补结构与文档位结算奖励赛季奖励周奖励仍按后续配置表落地
## 本次后端结构预留
- `2458` 巅峰加入请求增加 `TianxuanPetIDs`
- 匹配快照 `QueuePlayerSnapshot` 增加 `TianxuanPetIDs`
- `CrossServerBanPickStartOutboundInfo` 增加
- `TianxuanSelectableCount`
- `MyTianxuanPets`
- `OpponentTianxuanPets`
## 当前实现边界
- 本次先完成开始界面投票 + 匹配快照透传 + BP 启动数据预留
- 不直接改现有 `banpick.vue`
- 天选精灵的正式上场生成战斗侧实体化和完整 BP 消费交给后续 BP 页面与战斗链路接入

View File

@@ -0,0 +1,257 @@
# PVP 跨服战斗消息 `cmd/data` 转发改造说明
## 背景
巅峰赛跨服战斗原本已经有 Redis pub/sub 通道但战斗中的客户端操作转发仍然偏间接
1. 本服 controller 先把客户端请求翻译成 `FightI` 调用
2. `RemoteFightProxy` 再把 `FightI` 调用翻译成 `battle_command`
3. 宿主服收到 `battle_command` 再翻译回 `FightI` 调用
这样一来跨服战斗链路和本地战斗链路之间多了一层动作语义映射调试时不容易直接看到前端到底发了哪个 cmd带了什么 data
本次改造只处理战斗中的消息转发不改匹配/加入队列逻辑
## 保持不变的部分
- 巅峰赛加入和取消匹配仍然走原有 RPC
- `MatchJoinOrUpdate`
- `MatchCancel`
- Ban/PickMatchFoundSessionClose PVP 服务内消息仍兼容旧外层格式
- `battle_command` 旧语义消息仍保留作为兼容兜底不强制一次性切掉
对应原因
- 匹配加入需要同步返回结果RPC 更直接
- 现网如果已有旧 envelope 或旧 battle command 发送方不能直接断
## 改造目标
把跨服战斗中的客户端操作转发改成直接发送
```json
{
"cmd": "battle_client_command",
"data": {
"sessionId": "xsvr-...",
"userId": 10001,
"cmd": 2405,
"data": {
"SkillId": 1234
}
}
}
```
核心含义
- 外层 `cmd`PVP 内部 Redis 消息类型
- 内层 `data.cmd`真实客户端战斗协议 cmd
- 内层 `data.data` cmd 对应的 JSON 业务字段
这样宿主服拿到消息后可以直接按真实战斗 cmd 处理而不是先理解一层额外的跨服动作协议
## 本次代码落点
### 1. 新增 `cmd/data` envelope 能力
文件
- `logic/service/fight/pvpwire/types.go`
改动
- `Envelope` 新增
- `Cmd string`
- `Data json.RawMessage`
- 保留旧字段
- `Type string`
- `Body []byte`
- 新增辅助方法
- `NewEnvelope`
- `MessageCmd`
- `MessageData`
作用
- 新消息可以直接用 `cmd/data`
- 旧消息仍可通过 `type/body` 解析
### 2. controller 层遇到远端跨服战斗时直接转发原始战斗 cmd
文件
- `logic/controller/fight_unified.go`
- `logic/controller/fight_base.go`
改动
- `fight_unified.go` 增加 `relayRemoteFightCommand`
- `c.FightC` `*pvp.RemoteFightProxy` 不再走本地 `dispatchFightActionEnvelope` 分发而是直接转发当前请求的
- `data.Head.CMD`
- 当前请求结构体 `data`
已接入的战斗入口包括
- `2404` 准备
- `7556` 旧组队准备
- `2405` 单战位技能
- `7505` 多战位技能
- `7558` 旧组队技能
- `2406` 使用道具
- `7562` 旧组队道具
- `2407` 切宠
- `7563` 旧组队切宠
- `2410` 逃跑
- `7565` 旧组队逃跑
- `2441` 加载进度
- `50002` 战斗聊天
### 3. RemoteFightProxy 新增直接转发客户端命令能力
文件
- `logic/service/fight/pvp/proxy.go`
改动
- 新增 `RelayClientCommand(cmd uint32, data any) bool`
- 新增 `marshalClientCommandData(data any) ([]byte, error)`
说明
- `marshalClientCommandData` 会把请求结构体转成 JSON
- 会删除 `Head/head` 字段避免把协议头二进制字段一起塞进跨服消息
- 这样发出去的 `data.data` 只保留业务字段
### 4. PVP 宿主服新增 `battle_client_command` 处理
文件
- `logic/service/fight/pvp/service.go`
- `logic/service/fight/pvpwire/types.go`
改动
- 新增消息类型
- `MessageTypeBattleClientCommand`
- 新增载荷
- `BattleClientCommandPayload`
- `handleRedisMessage` 增加 `battle_client_command` 分支
- 新增 `handleBattleClientCommand`
处理方式
- 宿主服根据内层真实 `cmd` 解出 `data`
- 再直接调用当前 `FightI` / `FightC`
当前支持的映射
- `2404` / `7556` -> `ReadyFight`
- `2405` / `7505` / `7558` -> `UseSkillAt`
- `2406` / `7562` -> `UseItemAt`
- `2407` / `7563` -> `ChangePetAt`
- `2410` / `7565` -> `Over(PlayerEscape)`
- `2441` -> `LoadPercent`
- `50002` -> `Chat`
### 5. 战斗发包转发也统一改成 `cmd/data`
文件
- `logic/service/player/rpc.go`
改动
- `PacketRelayPayload` 外层 envelope `type/body` 改为 `cmd/data`
原因
- 它本质也是跨服战斗中的消息转发
- 和新的 `battle_client_command` 保持一致便于抓包和日志排查
## 当前实际链路
### 客户端操作转发
1. 客户端发战斗包到本服 controller
2. controller 判断当前 `FightC` 是否为 `RemoteFightProxy`
3. 如果是
- 直接发布 `battle_client_command`
- 内层携带真实 `cmd` JSON `data`
4. 宿主服 `pvp.service` 收到后按 `cmd` 分发回战斗逻辑
### 战斗结果/下行发包转发
1. 宿主服在 `RPC_player.SendPackCmd` 中组包
2. 发布 `packet_relay` 消息外层使用 `cmd/data`
3. 客服端所在服收到后解包并发回真实客户端
## 兼容策略
为了避免一次性改动过大这次保留了两层兼容
### 1. envelope 兼容
`Envelope` 同时支持
- 新格式`cmd/data`
- 旧格式`type/body`
`handleRedisMessage` 统一通过
- `MessageCmd()`
- `MessageData()`
读取消息
### 2. `battle_command` 兼容
`RemoteFightProxy` 旧的
- `UseSkill`
- `UseSkillAt`
- `UseItem`
- `ChangePet`
- `Chat`
这些方法仍然保留并继续发送原来的 `battle_command`
新接入的 controller 优先走 `battle_client_command`旧路径仍可兜底
## 这次改造的收益
### 好处
- 跨服战斗消息更直观日志里能直接看到真实客户端 cmd
- controller 到宿主服之间不需要再先翻译成一套额外动作协议
- 宿主服调试时更容易复现前端输入
- 新老消息可并存改造风险相对可控
### 仍然保留的复杂度
- 宿主服仍然需要针对不同战斗 cmd 做一次 decode 和分发
- 这是必要复杂度因为真实战斗入口本来就有多种协议包
## 验证
本次已执行
- `cd logic && go test ./service/fight/pvp ./service/fight/pvpwire`
- `cd logic && go test -c ./controller`
结果
- `pvp``pvpwire` 包通过
- `controller` 可以完成编译检查
补充说明
- `go test ./controller ./service/player ...` 在当前环境会因为依赖初始化读取 `/proc/sys/kernel/osrelease` 失败而 panic这属于运行环境问题不是这次改动引入的编译错误
## 后续建议
如果后面继续收敛这块逻辑可以按下面顺序做
1. `battle_command` 的旧语义层逐步下线只保留 `battle_client_command`
2. `handleBattleClientCommand` 里的 cmd 映射抽成独立表避免 `switch` 继续膨胀
3. 如果后续战斗协议继续统一可考虑把 controller 入站结构和跨服转发 decode 共享同一套注册表

View File

@@ -0,0 +1,276 @@
# RPC 阻塞并发承载与重连复用检查
日期2026-04-27
## 1. 检查范围
本次主要检查以下两层
- 业务 RPC 封装`common/rpc/rpc.go`
- 底层 websocket JSON-RPC`common/utils/go-jsonrpc/client.go`
- 底层 websocket 重连循环`common/utils/go-jsonrpc/websocket.go`
- 业务调用点`logic/controller/login_main.go``logic/controller/fight_巅峰.go`
另外同步看了本轮已有的跨服战斗转发改动
- `logic/service/fight/pvp/proxy.go`
- `logic/service/fight/pvp/service.go`
- `logic/service/fight/pvpwire/types.go`
- `logic/service/player/rpc.go`
## 2. 结论
### 2.1 现在的 RPC 会不会阻塞
当前业务侧 `Kick``MatchJoinOrUpdate``MatchCancel``RegisterLogic` 都是同步 RPC 调用
调用 goroutine 会一直等到底层返回响应连接错误或者调用方自己的 `context` 超时
原先的问题是
- 这些业务 RPC 方法没有 `context.Context` 入参
- 调用方没法给单次 RPC 设置超时
- 一旦对端卡住或网络异常调用方可能长期挂住
这会直接影响
- 登录踢人流程
- 巅峰匹配加入/取消
- 重连后的逻辑服重新注册
### 2.2 并发会不会顶不住
结论是底层支持多路并发但原实现存在明显背压点
底层 `go-jsonrpc` 的设计不是单请求单连接而是
- 一个 `RPCClient`
- 一个 websocket 连接
- 多个请求共用一个 `requests` 通道
- 通过请求 ID 做响应分发
所以它本身支持并发复用同一条连接
但原实现有两个风险
1. `client.setupRequestChan()` 里的 `requests` 是无缓冲通道
`handleWsConn` 主循环发送不过来时调用方会在写入请求通道这一步被卡住
2. 业务调用没有统一超时
即使底层连接还能用某个慢 RPC 也可能把业务 goroutine 长时间挂住
这不代表完全扛不住但高并发下会更容易出现请求堆积和业务侧等待放大
### 2.3 重连后 URL 会不会复用
会复用
当前 websocket client 在初始化时把地址保存在 `connFactory` 重连时走的还是同一个 `addr`
- `common/utils/go-jsonrpc/client.go`
- `websocketClient(...)` 中构造 `connFactory`
- `common/utils/go-jsonrpc/websocket.go`
- `tryReconnect(...)` 中再次调用 `c.connFactory()`
也就是说
- 重连不是只发一次注册 RPC 就结束
- 重连后不是一次性临时连接
- 而是替换 `wsConn.conn` 为新连接
- 后续 RPC 仍然继续复用同一个 `RPCClient` 和同一个目标 URL
### 2.4 重连后是不是必须再发一次注册 RPC
现在不是了
当前实现已经改成
- 客户端在 websocket 建连 URL 上直接带 `logic_id` / `logic_port`
- 服务端在握手阶段创建 reverse client 立刻根据 URL 参数完成 logic 注册
- 连接断开时再根据同一身份清理 `cool.Clientmap`
这样重连时
- 仍然走同一个 URL
- 新连接在握手阶段就知道 client 身份
- 不需要再依赖重连成功后的二次 `RegisterLogic(id, port)` RPC
保留 `RegisterLogic` 只是兼容已有接口不再是重连链路的必要步骤
## 3. 本轮已做修改
### 3.1 给业务 RPC 增加显式超时能力
修改文件
- `common/rpc/rpc.go`
- `logic/controller/Controller.go`
- `logic/controller/login_main.go`
- `logic/controller/fight_巅峰.go`
改动内容
- `Kick`
- `RegisterLogic`
- `MatchJoinOrUpdate`
- `MatchCancel`
统一改成带 `context.Context` 的签名
新增
- `common/rpc/rpc.go`
- `ClientCallTimeout = 5 * time.Second`
调用侧现在会显式设置超时避免业务 goroutine 无限等待
### 3.2 连接握手阶段直接注册 logic 身份
修改文件
- `common/rpc/rpc.go`
- `common/utils/go-jsonrpc/server.go`
- `common/utils/go-jsonrpc/options_server.go`
行为调整
- 客户端建连 URL 直接携带 `logic_id` / `logic_port`
- 服务端握手时把原始 `*http.Request` 放入 RPC 上下文
- reverse client 建好后立即读取 URL 参数并注册 logic client
- 连接关闭时按相同 key 自动清理 `cool.Clientmap`
这样重连后不需要额外补发一次注册 RPC
### 3.3 底层请求通道增加缓冲
修改文件
- `common/utils/go-jsonrpc/client.go`
改动
- `requests := make(chan clientRequest, 1024)`
目的
- 调用方 goroutine 立刻卡在请求投递这个点往后挪
- `handleWsConn` 主循环留一个有限缓冲区
这不是彻底消除背压只是把最硬的无缓冲阻塞改掉
### 3.4 重连回调不再承担注册职责
修改文件
- `common/rpc/rpc.go`
改动
- 去掉 `StartClient(...)` 中依赖 `WithReconnFun(...)` 做补注册的逻辑
目的
- 谁是这个 logic client变成握手时就已确定的连接属性
- 避免重连后再发一笔注册 RPC
## 4. 这轮修改后的判断
### 4.1 RPC 还会不会阻塞
但现在阻塞是有边界的同步等待不是无上限死等
也就是
- 业务仍然是同步 RPC 模式
- 但调用方现在有明确超时
- 超时后能返回错误不会无限挂住
### 4.2 并发有没有改善
有改善但不是彻底做成高吞吐 RPC 网关
现在比原来更稳的点
- 业务调用有超时
- 请求投递通道有缓冲
- 重连时不再额外补发一次注册 RPC
仍然保留的现实限制
- 单条 websocket 连接仍然只有一个写口
- `handleWsConn` 仍是单主循环
- 极端并发下仍会出现排队只是不会像原来那样更早卡死
## 5. 关于URL 复用的最终确认
最终确认如下
1. `RPCClient` 建立时会把目标 `rpcaddr` 固定到 `connFactory`
2. 这个 `rpcaddr` 现在已经带上 `logic_id` / `logic_port`
3. 连接断开后`tryReconnect(...)` 继续使用这个 `connFactory`
4. 新连接建立后服务端在握手阶段直接按 URL 参数注册 reverse client
5. `RPCClient` 后续 RPC 继续走新连接
所以这里是
- 复用同一个目标 URL
- 复用同一个 `RPCClient`
- 复用同一个请求分发模型
不是
- 重连后靠额外发一次 `RegisterLogic`
- 才让后续 RPC 可用
现在身份识别和注册已经前置到连接握手本身
## 6. 还没解决的风险
### 6.1 连接级串行写仍然存在
虽然请求可以并发入队但真正写 websocket 还是串行的
如果将来 login RPC 量继续上升还是可能需要继续做
- 更细的调用隔离
- 独立连接池
- 或把部分强同步调用改为异步消息
### 6.2 1024 缓冲不是容量上限方案
当前只是经验值不是经过压测得出的最终值
如果峰值比预期高还可能继续积压
### 6.3 业务上仍然是同步等待模式
比如
- 登录踢人
- 匹配加入
仍然依赖 RPC 成功/失败来推进
只是现在不会无限挂死但高峰期延迟仍可能直接体现在业务响应上
## 7. 验证情况
已完成
- `gofmt` 已执行
- `go test ./rpc` in `common` 通过
- `go test -run '^$' .` in `common/utils/go-jsonrpc` 通过
受环境限制未完整确认
- `common/utils/go-jsonrpc` 全量测试在 sandbox 下需要本地监听端口
- `logic` 模块测试受当前环境 `/proc/sys/kernel/osrelease` 读取失败影响无法作为本轮改动的有效回归结论
另外`common` 模块全量 `go test ./...` 还会被仓库内已有的 `fmt.Println("%.2f")` 这类历史问题拦住与本次 RPC 改动无关
## 8. 建议的下一步
如果后面还要继续收敛这块建议优先级如下
1. login 侧关键 RPC 增加更明确的耗时日志和超时日志
2. `requests` 队列积压增加指标或告警
3. 评估 `Kick` 和匹配 RPC 是否需要拆连接
4. 如果 login 压力继续上涨再考虑把部分同步入口改成异步投递 + 状态查询

View File

@@ -0,0 +1,37 @@
-- base_sys_user_role 角色授权去重
-- 只处理未软删除的有效授权软删除历史记录不参与去重
-- 保留每组有效 userId + roleId id 最小的一条删除其余重复记录
-- 1. 执行前查看重复数据
SELECT
"userId",
"roleId",
COUNT(*) AS cnt,
MIN(id) AS keep_id,
ARRAY_AGG(id ORDER BY id) AS ids
FROM base_sys_user_role
WHERE deleted_at IS NULL
GROUP BY "userId", "roleId"
HAVING COUNT(*) > 1
ORDER BY cnt DESC, "userId", "roleId";
-- 2. 删除重复数据
DELETE FROM base_sys_user_role a
USING base_sys_user_role b
WHERE a."userId" = b."userId"
AND a."roleId" = b."roleId"
AND a.deleted_at IS NULL
AND b.deleted_at IS NULL
AND a.id > b.id;
-- 3. 执行后复查应返回 0
SELECT
"userId",
"roleId",
COUNT(*) AS cnt
FROM base_sys_user_role
WHERE deleted_at IS NULL
GROUP BY "userId", "roleId"
HAVING COUNT(*) > 1;
-- 唯一约束统一在 约束类.sql 中维护

10
help/ftp.md Normal file
View File

@@ -0,0 +1,10 @@
FTP地址38.102.84.92 [ 端口21 ]
FTP用户名m1584920
FTP密码7WKimiwDH5RL2SLs
空间容量10G
有效期至2027-04-211
文件上传之后其URL地址是
https://m1584920.772988.xyz/文件名+文件格式,如果文件名有中文,请注意编码问题,如果你不懂这个就遇到再跟我说。
PS如果需要绑定你的域名手续费30

View File

@@ -0,0 +1,14 @@
-- server_show 冠名索引修复
-- 目的允许同一服务器存在多个不同玩家的冠名记录
-- 同服同属主唯一约束统一在 约束类.sql 中维护
BEGIN;
-- 历史上 server_id 被建成了单列唯一约束/唯一索引会拦截同服多冠名
ALTER TABLE server_show DROP CONSTRAINT IF EXISTS idx_server_show_server_id;
DROP INDEX IF EXISTS idx_server_show_server_id;
-- 保留普通查询索引
CREATE INDEX IF NOT EXISTS idx_server_show_server_id ON server_show (server_id);
COMMIT;

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 ''
);
-- task_id 唯一约束统一在 约束类.sql 中维护
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) WHERE deleted_at IS NULL 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

@@ -1,42 +1,254 @@
-- 唯一约束修复
-- 规则所有带 deleted_at 的业务唯一约束只约束未软删除记录
-- 玩家+物品+VIP状态 联合唯一 -- 玩家+物品+VIP状态 联合唯一
ALTER TABLE player_item ALTER TABLE player_item
ADD CONSTRAINT uk_player_item_player_item_vip DROP CONSTRAINT IF EXISTS uk_player_item_player_item_vip;
UNIQUE (player_id, item_id, is_vip); DROP INDEX IF EXISTS uk_player_item_player_item_vip;
CREATE UNIQUE INDEX IF NOT EXISTS uk_player_item_player_item_vip
ON player_item (player_id, item_id, is_vip)
WHERE deleted_at IS NULL;
-- 玩家+挖矿 联合唯一 -- 玩家+挖矿 联合唯一
CREATE UNIQUE INDEX uk_talk_player ON player_talk (talk_id, player_id); ALTER TABLE player_talk
DROP CONSTRAINT IF EXISTS uk_talk_player;
DROP INDEX IF EXISTS uk_talk_player;
CREATE UNIQUE INDEX IF NOT EXISTS uk_talk_player
ON player_talk (talk_id, player_id)
WHERE deleted_at IS NULL;
-- 玩家+任务 联合唯一 -- 玩家+任务 联合唯一
CREATE UNIQUE INDEX uk_player_task ON player_task (player_id, task_id); ALTER TABLE player_task
DROP CONSTRAINT IF EXISTS uk_player_task;
DROP INDEX IF EXISTS uk_player_task;
CREATE UNIQUE INDEX IF NOT EXISTS uk_player_task
ON player_task (player_id, task_id)
WHERE deleted_at IS NULL;
-- 玩家+称号 联合唯一 -- 玩家+称号 联合唯一
CREATE UNIQUE INDEX uk_player_title ON player_title (player_id, is_vip) WHERE deleted_at IS NULL; ALTER TABLE player_title
DROP CONSTRAINT IF EXISTS uk_player_title;
DROP INDEX IF EXISTS uk_player_title;
CREATE UNIQUE INDEX IF NOT EXISTS uk_player_title
ON player_title (player_id, is_vip)
WHERE deleted_at IS NULL;
-- 玩家+精灵 联合唯一 -- 玩家+精灵 联合唯一
CREATE UNIQUE INDEX uk_player_pet ON player_pet (player_id, is_vip, catch_time) WHERE deleted_at IS NULL; ALTER TABLE player_pet
DROP CONSTRAINT IF EXISTS uk_player_pet;
DROP INDEX IF EXISTS uk_player_pet;
CREATE UNIQUE INDEX IF NOT EXISTS uk_player_pet
ON player_pet (player_id, is_vip, catch_time)
WHERE deleted_at IS NULL;
-- 玩家+CDK 联合唯一 -- 玩家+CDK 联合唯一
CREATE UNIQUE INDEX uk_player_cdk_log ALTER TABLE player_cdk_log
DROP CONSTRAINT IF EXISTS uk_player_cdk_log;
DROP INDEX IF EXISTS uk_player_cdk_log;
CREATE UNIQUE INDEX IF NOT EXISTS uk_player_cdk_log
ON player_cdk_log (player_id, code_id, is_vip) ON player_cdk_log (player_id, code_id, is_vip)
WHERE deleted_at IS NULL; WHERE deleted_at IS NULL;
-- 玩家孵蛋 联合唯一 -- 玩家孵蛋 联合唯一
CREATE UNIQUE INDEX uk_player_egg ALTER TABLE player_egg
DROP CONSTRAINT IF EXISTS uk_player_egg;
DROP INDEX IF EXISTS uk_player_egg;
CREATE UNIQUE INDEX IF NOT EXISTS uk_player_egg
ON player_egg (player_id, is_vip) ON player_egg (player_id, is_vip)
WHERE deleted_at IS NULL; WHERE deleted_at IS NULL;
---PVP索引
CREATE UNIQUE INDEX uk_player_pvp -- PVP索引
ALTER TABLE player_pvp
DROP CONSTRAINT IF EXISTS uk_player_pvp;
DROP INDEX IF EXISTS uk_player_pvp;
CREATE UNIQUE INDEX IF NOT EXISTS uk_player_pvp
ON player_pvp (player_id, season) ON player_pvp (player_id, season)
WHERE deleted_at IS NULL; WHERE deleted_at IS NULL;
-- 签到 -- 签到
CREATE UNIQUE INDEX uk_player_sign_in_log ALTER TABLE player_sign_in_log
DROP CONSTRAINT IF EXISTS uk_player_sign_in_log;
DROP INDEX IF EXISTS uk_player_sign_in_log;
CREATE UNIQUE INDEX IF NOT EXISTS uk_player_sign_in_log
ON player_sign_in_log (player_id, sign_in_id, is_vip) ON player_sign_in_log (player_id, sign_in_id, is_vip)
WHERE deleted_at IS NULL; WHERE deleted_at IS NULL;
-- 房间索引 -- 房间索引
CREATE UNIQUE INDEX uk_player_room_house ALTER TABLE player_room_house
DROP CONSTRAINT IF EXISTS uk_player_room_house;
DROP INDEX IF EXISTS uk_player_room_house;
CREATE UNIQUE INDEX IF NOT EXISTS uk_player_room_house
ON player_room_house (player_id, is_vip) ON player_room_house (player_id, is_vip)
WHERE deleted_at IS NULL; WHERE deleted_at IS NULL;
-- 集市权限角色 联合唯一
-- 先清理有效重复授权保留每组 userId + roleId id 最小的一条
DELETE FROM base_sys_user_role a
USING base_sys_user_role b
WHERE a."userId" = b."userId"
AND a."roleId" = b."roleId"
AND a.deleted_at IS NULL
AND b.deleted_at IS NULL
AND a.id > b.id;
ALTER TABLE base_sys_user_role
DROP CONSTRAINT IF EXISTS uk_base_sys_user_role_user_role;
DROP INDEX IF EXISTS uk_base_sys_user_role_user_role;
CREATE UNIQUE INDEX IF NOT EXISTS uk_base_sys_user_role_user_role
ON base_sys_user_role ("userId", "roleId")
WHERE deleted_at IS NULL;
-- CDK配置 编号唯一
ALTER TABLE config_gift_cdk
DROP CONSTRAINT IF EXISTS idx_config_gift_cdk_cdk_code;
DROP INDEX IF EXISTS idx_config_gift_cdk_cdk_code;
CREATE UNIQUE INDEX IF NOT EXISTS idx_config_gift_cdk_cdk_code
ON config_gift_cdk (cdk_code)
WHERE deleted_at IS NULL;
-- 战斗规则 规则索引唯一
ALTER TABLE config_fight_rule
DROP CONSTRAINT IF EXISTS idx_rule_idx;
DROP INDEX IF EXISTS idx_rule_idx;
CREATE UNIQUE INDEX IF NOT EXISTS idx_rule_idx
ON config_fight_rule (rule_idx)
WHERE deleted_at IS NULL;
-- 天选配置 玩家+精灵唯一
ALTER TABLE config_peak_tianxuan
DROP CONSTRAINT IF EXISTS idx_peak_tianxuan_player_pet;
DROP INDEX IF EXISTS idx_peak_tianxuan_player_pet;
CREATE UNIQUE INDEX IF NOT EXISTS idx_peak_tianxuan_player_pet
ON config_peak_tianxuan (player_id, pet_id)
WHERE deleted_at IS NULL;
-- 天选投票 周期+玩家唯一
ALTER TABLE config_peak_tianxuan_vote
DROP CONSTRAINT IF EXISTS idx_peak_tianxuan_vote_week_player;
DROP INDEX IF EXISTS idx_peak_tianxuan_vote_week_player;
CREATE UNIQUE INDEX IF NOT EXISTS idx_peak_tianxuan_vote_week_player
ON config_peak_tianxuan_vote (week_index, player_id)
WHERE deleted_at IS NULL;
-- 服务器冠名 同服同属主唯一
ALTER TABLE server_show
DROP CONSTRAINT IF EXISTS idx_server_show_server_owner;
DROP INDEX IF EXISTS idx_server_show_server_owner;
CREATE UNIQUE INDEX IF NOT EXISTS idx_server_show_server_owner
ON server_show (server_id, owner)
WHERE deleted_at IS NULL;
-- 商店 商品ID唯一
ALTER TABLE config_shop
DROP CONSTRAINT IF EXISTS idx_config_shop_product_id;
DROP INDEX IF EXISTS idx_config_shop_product_id;
CREATE UNIQUE INDEX IF NOT EXISTS idx_config_shop_product_id
ON config_shop (product_id)
WHERE deleted_at IS NULL;
-- 签到配置 签到类别+阶段唯一
ALTER TABLE config_sign_in
DROP CONSTRAINT IF EXISTS idx_sign_type_stage;
DROP INDEX IF EXISTS idx_sign_type_stage;
CREATE UNIQUE INDEX IF NOT EXISTS idx_sign_type_stage
ON config_sign_in (sign_type, stage_days)
WHERE deleted_at IS NULL;
-- 签到配置 CDK唯一
ALTER TABLE config_sign_in
DROP CONSTRAINT IF EXISTS idx_config_sign_in_cdk_id;
DROP INDEX IF EXISTS idx_config_sign_in_cdk_id;
CREATE UNIQUE INDEX IF NOT EXISTS idx_config_sign_in_cdk_id
ON config_sign_in (cdk_id)
WHERE deleted_at IS NULL;
-- SPT配置 任务ID唯一
ALTER TABLE config_spt
DROP CONSTRAINT IF EXISTS idx_config_spt_task_id;
DROP INDEX IF EXISTS idx_config_spt_task_id;
CREATE UNIQUE INDEX IF NOT EXISTS idx_config_spt_task_id
ON config_spt (task_id)
WHERE deleted_at IS NULL;
-- 爬塔配置 层级唯一
ALTER TABLE config_tower_1
DROP CONSTRAINT IF EXISTS idx_config_tower_1_tower_level;
DROP INDEX IF EXISTS idx_config_tower_1_tower_level;
CREATE UNIQUE INDEX IF NOT EXISTS idx_config_tower_1_tower_level
ON config_tower_1 (tower_level)
WHERE deleted_at IS NULL;
ALTER TABLE config_tower_110
DROP CONSTRAINT IF EXISTS idx_config_tower_110_tower_level;
DROP INDEX IF EXISTS idx_config_tower_110_tower_level;
CREATE UNIQUE INDEX IF NOT EXISTS idx_config_tower_110_tower_level
ON config_tower_110 (tower_level)
WHERE deleted_at IS NULL;
ALTER TABLE config_tower_500
DROP CONSTRAINT IF EXISTS idx_config_tower_500_tower_level;
DROP INDEX IF EXISTS idx_config_tower_500_tower_level;
CREATE UNIQUE INDEX IF NOT EXISTS idx_config_tower_500_tower_level
ON config_tower_500 (tower_level)
WHERE deleted_at IS NULL;
ALTER TABLE config_tower_600
DROP CONSTRAINT IF EXISTS idx_config_tower_600_tower_level;
DROP INDEX IF EXISTS idx_config_tower_600_tower_level;
CREATE UNIQUE INDEX IF NOT EXISTS idx_config_tower_600_tower_level
ON config_tower_600 (tower_level)
WHERE deleted_at IS NULL;
-- 玩家信息 角色ID唯一
ALTER TABLE player_info
DROP CONSTRAINT IF EXISTS idx_player_info_player_id;
DROP INDEX IF EXISTS idx_player_info_player_id;
CREATE UNIQUE INDEX IF NOT EXISTS idx_player_info_player_id
ON player_info (player_id)
WHERE deleted_at IS NULL;

View File

@@ -27,13 +27,13 @@ var Maincontroller = &Controller{} //注入service
type Controller struct { type Controller struct {
UID uint32 UID uint32
RPCClient *struct { RPCClient *struct {
Kick func(uint32) error Kick func(context.Context, uint32) error
RegisterLogic func(uint32, uint32) error RegisterLogic func(context.Context, uint32, uint32) error
MatchJoinOrUpdate func(rpc.PVPMatchJoinPayload) error MatchJoinOrUpdate func(context.Context, rpc.PVPMatchJoinPayload) error
MatchCancel func(uint32) error MatchCancel func(context.Context, uint32) error
} }
} }

View File

@@ -43,8 +43,8 @@ var masterCupRequiredItems = map[uint32][]ItemS{
}, },
} }
// DASHIbei 处理控制器请求。 // GetMasterCupRewards 处理控制器请求。
func (h Controller) DASHIbei(req *C2s_MASTER_REWARDS, c *player.Player) (result *S2C_MASTER_REWARDS, err errorcode.ErrorCode) { func (h Controller) GetMasterCupRewards(req *C2s_MASTER_REWARDS, c *player.Player) (result *S2C_MASTER_REWARDS, err errorcode.ErrorCode) {
_ = req _ = req
result = &S2C_MASTER_REWARDS{} result = &S2C_MASTER_REWARDS{}
items := c.Service.Item.Get(masterCupRewardItemMin, masterCupRewardItemMax) items := c.Service.Item.Get(masterCupRewardItemMin, masterCupRewardItemMax)
@@ -53,8 +53,8 @@ func (h Controller) DASHIbei(req *C2s_MASTER_REWARDS, c *player.Player) (result
return return
} }
// DASHIbeiR 处理控制器请求。 // ClaimMasterCupReward 处理控制器请求。
func (h Controller) DASHIbeiR(req *C2s_MASTER_REWARDSR, c *player.Player) (result *S2C_MASTER_REWARDSR, err errorcode.ErrorCode) { func (h Controller) ClaimMasterCupReward(req *C2s_MASTER_REWARDSR, c *player.Player) (result *S2C_MASTER_REWARDSR, err errorcode.ErrorCode) {
result = &S2C_MASTER_REWARDSR{} result = &S2C_MASTER_REWARDSR{}
requiredItems, ok := masterCupRequiredItems[req.ElementType] requiredItems, ok := masterCupRequiredItems[req.ElementType]

View File

@@ -11,9 +11,9 @@ import (
"github.com/gogf/gf/v2/util/grand" "github.com/gogf/gf/v2/util/grand"
) )
// Draw15To10WithBitSet 15抽10返回标记抽取结果的uint32位1表示选中 // drawTenOfFifteenBitset 15抽10返回标记抽取结果的uint32位1表示选中
// 规则uint32的第n位0≤n≤14=1 → 选中第n+1号元素 // 规则uint32的第n位0≤n≤14=1 → 选中第n+1号元素
func Draw15To10WithBitSet() uint32 { func drawTenOfFifteenBitset() uint32 {
// 初始化随机数生成器 // 初始化随机数生成器
r := rand.New(rand.NewSource(time.Now().UnixNano())) r := rand.New(rand.NewSource(time.Now().UnixNano()))
@@ -37,8 +37,8 @@ func Draw15To10WithBitSet() uint32 {
return resultBits return resultBits
} }
// GET_XUANCAI 处理控制器请求。 // ClaimXuanCaiShards 处理控制器请求。
func (h Controller) GET_XUANCAI(data *C2s_GET_XUANCAI, c *player.Player) (result *S2C_GET_XUANCAI, err errorcode.ErrorCode) { func (h Controller) ClaimXuanCaiShards(data *C2s_GET_XUANCAI, c *player.Player) (result *S2C_GET_XUANCAI, err errorcode.ErrorCode) {
result = &S2C_GET_XUANCAI{} result = &S2C_GET_XUANCAI{}
selectedCount := 0 // 已选中的数量 selectedCount := 0 // 已选中的数量
res := c.Info.GetTask(13) //第一期 res := c.Info.GetTask(13) //第一期

View File

@@ -10,19 +10,17 @@ import (
) )
// 进入超时空隧道 // 进入超时空隧道
func (h Controller) TimeMap(data *C2s_SP, c *player.Player) (result *S2C_SP, err errorcode.ErrorCode) { func (h Controller) GetTimeTunnelMaps(data *C2s_SP, c *player.Player) (result *S2C_SP, err errorcode.ErrorCode) {
result = &S2C_SP{} result = &S2C_SP{}
mapPitService := service.NewMapPitService()
maps := service.NewMapService().GetTimeMap() maps := service.NewMapService().GetTimeMap()
result.MapList = make([]ServerInfo, len(maps)) result.MapList = make([]ServerInfo, len(maps))
for i, v := range maps { for i, mapInfo := range maps {
result.MapList[i].ID = v.MapID result.MapList[i].ID = mapInfo.MapID
result.MapList[i].DropItemIds = v.DropItemIds result.MapList[i].DropItemIds = mapInfo.DropItemIds
pits := service.NewMapPitService().GetDataALL(v.MapID) pits := mapPitService.GetDataALL(mapInfo.MapID)
for _, pit := range pits {
for _, v := range pits { result.MapList[i].Pet = append(result.MapList[i].Pet, pit.RefreshID...)
result.MapList[i].Pet = append(result.MapList[i].Pet, v.RefreshID...)
} }
result.MapList[i].Pet = lo.Union(result.MapList[i].Pet) result.MapList[i].Pet = lo.Union(result.MapList[i].Pet)
} }

View File

@@ -12,7 +12,7 @@ import (
// data: 空输入结构 // data: 空输入结构
// c: 当前玩家对象 // c: 当前玩家对象
// 返回: 捕捉结果消耗的EV值和错误码 // 返回: 捕捉结果消耗的EV值和错误码
func (h Controller) HanLiuQiang(data *C2S_2608, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) { func (h Controller) ClaimColdFlowGunReward(data *C2S_2608, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
if c.ItemAdd(100245, 1) { if c.ItemAdd(100245, 1) {
return return

View File

@@ -21,7 +21,10 @@ func (h Controller) OnReadyToFight(data *ReadyToFightInboundInfo, c *player.Play
if err := h.checkFightStatus(c); err != 0 { if err := h.checkFightStatus(c); err != 0 {
return nil, err return nil, err
} }
go c.FightC.ReadyFight(c) if h.relayRemoteFightCommand(c, data.Head.CMD, data) {
return nil, -1
}
go c.ReadyFight()
return nil, -1 return nil, -1
} }
@@ -30,7 +33,10 @@ func (h Controller) GroupReadyFightFinish(data *GroupReadyFightFinishInboundInfo
if err := h.checkFightStatus(c); err != 0 { if err := h.checkFightStatus(c); err != 0 {
return nil, err return nil, err
} }
go c.FightC.ReadyFight(c) if h.relayRemoteFightCommand(c, data.Head.CMD, data) {
return nil, -1
}
go c.ReadyFight()
return nil, -1 return nil, -1
} }
@@ -38,6 +44,9 @@ func (h Controller) GroupUseSkill(data *GroupUseSkillInboundInfo, c *player.Play
if err := h.checkFightStatus(c); err != 0 { if err := h.checkFightStatus(c); err != 0 {
return nil, err return nil, err
} }
if h.relayRemoteFightCommand(c, data.Head.CMD, data) {
return nil, -1
}
targetRelation := fight.SkillTargetOpponent targetRelation := fight.SkillTargetOpponent
if data.TargetSide == 1 { if data.TargetSide == 1 {
targetRelation = fight.SkillTargetAlly targetRelation = fight.SkillTargetAlly
@@ -51,6 +60,9 @@ func (h Controller) GroupUseItem(data *GroupUseItemInboundInfo, c *player.Player
if err := h.checkFightStatus(c); err != 0 { if err := h.checkFightStatus(c); err != 0 {
return nil, err return nil, err
} }
if h.relayRemoteFightCommand(c, data.Head.CMD, data) {
return nil, -1
}
h.dispatchFightActionEnvelope(c, fight.NewItemActionEnvelope(0, data.ItemId, int(data.ActorIndex), int(data.ActorIndex), fight.SkillTargetSelf)) h.dispatchFightActionEnvelope(c, fight.NewItemActionEnvelope(0, data.ItemId, int(data.ActorIndex), int(data.ActorIndex), fight.SkillTargetSelf))
return nil, -1 return nil, -1
} }
@@ -59,6 +71,9 @@ func (h Controller) GroupChangePet(data *GroupChangePetInboundInfo, c *player.Pl
if err := h.checkFightStatus(c); err != 0 { if err := h.checkFightStatus(c); err != 0 {
return nil, err return nil, err
} }
if h.relayRemoteFightCommand(c, data.Head.CMD, data) {
return nil, -1
}
h.dispatchFightActionEnvelope(c, fight.NewChangeActionEnvelope(data.CatchTime, int(data.ActorIndex))) h.dispatchFightActionEnvelope(c, fight.NewChangeActionEnvelope(data.CatchTime, int(data.ActorIndex)))
return nil, -1 return nil, -1
} }
@@ -67,7 +82,10 @@ func (h Controller) GroupEscape(data *GroupEscapeInboundInfo, c *player.Player)
if err := h.checkFightStatus(c); err != 0 { if err := h.checkFightStatus(c); err != 0 {
return nil, err return nil, err
} }
if fightC, ok := c.FightC.(*fight.FightC); ok && fightC != nil && fightC.LegacyGroupProtocol { if h.relayRemoteFightCommand(c, data.Head.CMD, data) {
return nil, -1
}
if fightC, ok := c.GetFightController().(*fight.FightC); ok && fightC != nil && fightC.LegacyGroupProtocol {
fightC.SendLegacyEscapeSuccess(c, int(data.ActorIndex)) fightC.SendLegacyEscapeSuccess(c, int(data.ActorIndex))
} }
h.dispatchFightActionEnvelope(c, fight.NewEscapeActionEnvelope()) h.dispatchFightActionEnvelope(c, fight.NewEscapeActionEnvelope())
@@ -93,6 +111,9 @@ func (h Controller) UseSkill(data *UseSkillInInfo, c *player.Player) (result *fi
if err := h.checkFightStatus(c); err != 0 { if err := h.checkFightStatus(c); err != 0 {
return nil, err return nil, err
} }
if h.relayRemoteFightCommand(c, data.Head.CMD, data) {
return nil, 0
}
h.dispatchFightActionEnvelope(c, buildLegacyUseSkillEnvelope(data)) h.dispatchFightActionEnvelope(c, buildLegacyUseSkillEnvelope(data))
return nil, 0 return nil, 0
} }
@@ -103,6 +124,9 @@ func (h Controller) UseSkillAt(data *UseSkillAtInboundInfo, c *player.Player) (r
if err := h.checkFightStatus(c); err != 0 { if err := h.checkFightStatus(c); err != 0 {
return nil, err return nil, err
} }
if h.relayRemoteFightCommand(c, data.Head.CMD, data) {
return nil, 0
}
h.dispatchFightActionEnvelope(c, buildIndexedUseSkillEnvelope(data)) h.dispatchFightActionEnvelope(c, buildIndexedUseSkillEnvelope(data))
return nil, 0 return nil, 0
} }
@@ -112,6 +136,9 @@ func (h Controller) Escape(data *EscapeFightInboundInfo, c *player.Player) (resu
if err := h.checkFightStatus(c); err != 0 { if err := h.checkFightStatus(c); err != 0 {
return nil, err return nil, err
} }
if h.relayRemoteFightCommand(c, data.Head.CMD, data) {
return nil, 0
}
h.dispatchFightActionEnvelope(c, buildLegacyEscapeEnvelope()) h.dispatchFightActionEnvelope(c, buildLegacyEscapeEnvelope())
return nil, 0 return nil, 0
} }
@@ -121,6 +148,9 @@ func (h Controller) ChangePet(data *ChangePetInboundInfo, c *player.Player) (res
if err := h.checkFightStatus(c); err != 0 { if err := h.checkFightStatus(c); err != 0 {
return nil, err return nil, err
} }
if h.relayRemoteFightCommand(c, data.Head.CMD, data) {
return nil, -1
}
h.dispatchFightActionEnvelope(c, buildLegacyChangeEnvelope(data)) h.dispatchFightActionEnvelope(c, buildLegacyChangeEnvelope(data))
return nil, -1 return nil, -1
} }
@@ -132,11 +162,11 @@ func (h Controller) Capture(data *CatchMonsterInboundInfo, c *player.Player) (re
} }
if c.GetSpace().IsTime { if c.GetSpace().IsTime {
if data.CapsuleId < 300009 { if data.CapsuleId < 300009 {
go c.FightC.UseSkill(c, 0) go c.UseSkill(0)
return nil, -1 return nil, -1
} }
} }
go c.FightC.Capture(c, data.CapsuleId) go c.Capture(data.CapsuleId)
return nil, -1 return nil, -1
} }
@@ -145,18 +175,24 @@ func (h Controller) LoadPercent(data *LoadPercentInboundInfo, c *player.Player)
if c.FightC == nil { if c.FightC == nil {
return nil, -1 return nil, -1
} }
go c.FightC.LoadPercent(c, int32(data.Percent)) if h.relayRemoteFightCommand(c, data.Head.CMD, data) {
return nil, -1
}
go c.LoadPercent(int32(data.Percent))
return nil, -1 return nil, -1
} }
// UsePetItemInboundInfo 使用宠物道具 // UsePetItemInFight 使用宠物道具
func (h Controller) UsePetItemInboundInfo(data *UsePetItemInboundInfo, c *player.Player) (result *info.UsePetIteminfo, err errorcode.ErrorCode) { func (h Controller) UsePetItemInFight(data *UsePetItemInboundInfo, c *player.Player) (result *info.UsePetIteminfo, err errorcode.ErrorCode) {
if err := h.checkFightStatus(c); err != 0 { if err := h.checkFightStatus(c); err != 0 {
return nil, err return nil, err
} }
if h.relayRemoteFightCommand(c, data.Head.CMD, data) {
return nil, -1
}
if c.GetSpace().IsTime { if c.GetSpace().IsTime {
if data.ItemId < 300009 { if data.ItemId < 300009 {
go c.FightC.UseSkill(c, 0) go c.UseSkill(0)
} }
} }
@@ -169,6 +205,9 @@ func (h Controller) FightChat(data *ChatInfo, c *player.Player) (result *fight.N
if err := h.checkFightStatus(c); err != 0 { if err := h.checkFightStatus(c); err != 0 {
return nil, err return nil, err
} }
if h.relayRemoteFightCommand(c, data.Head.CMD, data) {
return nil, -1
}
h.dispatchFightActionEnvelope(c, buildChatEnvelope(data)) h.dispatchFightActionEnvelope(c, buildChatEnvelope(data))
return nil, -1 return nil, -1
} }

View File

@@ -13,7 +13,7 @@ import (
//大乱斗 //大乱斗
func (h Controller) PetMelee(data *StartPetWarInboundInfo, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) { func (h Controller) JoinPetMelee(data *StartPetWarInboundInfo, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
c.Fightinfo.Mode = info.BattleMode.PET_MELEE c.Fightinfo.Mode = info.BattleMode.PET_MELEE
c.Fightinfo.Status = info.BattleMode.PET_MELEE c.Fightinfo.Status = info.BattleMode.PET_MELEE
@@ -71,8 +71,8 @@ func (h Controller) PetMelee(data *StartPetWarInboundInfo, c *player.Player) (re
return return
} }
// PetKing 处理控制器请求。 // JoinPetKing 处理控制器请求。
func (h Controller) PetKing(data *PetKingJoinInboundInfo, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) { func (h Controller) JoinPetKing(data *PetKingJoinInboundInfo, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
c.Fightinfo.Status = info.BattleMode.PET_TOPLEVEL c.Fightinfo.Status = info.BattleMode.PET_TOPLEVEL
// ElementTypeNumbers 是控制器层共享变量。 // ElementTypeNumbers 是控制器层共享变量。

View File

@@ -4,9 +4,21 @@ import (
"blazing/modules/player/model" "blazing/modules/player/model"
"blazing/logic/service/fight" "blazing/logic/service/fight"
"blazing/logic/service/fight/pvp"
"blazing/logic/service/player" "blazing/logic/service/player"
) )
func (h Controller) relayRemoteFightCommand(c *player.Player, cmd uint32, data any) bool {
if c == nil || c.FightC == nil || cmd == 0 {
return false
}
remote, ok := c.GetFightController().(*pvp.RemoteFightProxy)
if !ok || remote == nil {
return false
}
return remote.RelayClientCommand(cmd, data)
}
// dispatchFightActionEnvelope 把控制器层收到的统一动作结构分发回现有 FightI 接口。 // dispatchFightActionEnvelope 把控制器层收到的统一动作结构分发回现有 FightI 接口。
func (h Controller) dispatchFightActionEnvelope(c *player.Player, envelope fight.FightActionEnvelope) { func (h Controller) dispatchFightActionEnvelope(c *player.Player, envelope fight.FightActionEnvelope) {
if c == nil || c.FightC == nil { if c == nil || c.FightC == nil {
@@ -15,15 +27,15 @@ func (h Controller) dispatchFightActionEnvelope(c *player.Player, envelope fight
switch envelope.ActionType { switch envelope.ActionType {
case fight.FightActionTypeSkill: case fight.FightActionTypeSkill:
go c.FightC.UseSkillAt(c, envelope.SkillID, envelope.ActorIndex, envelope.EncodedTargetIndex()) go c.UseSkillAt(envelope.SkillID, envelope.ActorIndex, envelope.EncodedTargetIndex())
case fight.FightActionTypeItem: case fight.FightActionTypeItem:
go c.FightC.UseItemAt(c, envelope.CatchTime, envelope.ItemID, envelope.ActorIndex, envelope.EncodedTargetIndex()) go c.UseItemAt(envelope.CatchTime, envelope.ItemID, envelope.ActorIndex, envelope.EncodedTargetIndex())
case fight.FightActionTypeChange: case fight.FightActionTypeChange:
go c.FightC.ChangePetAt(c, envelope.CatchTime, envelope.ActorIndex) go c.ChangePetAt(envelope.CatchTime, envelope.ActorIndex)
case fight.FightActionTypeEscape: case fight.FightActionTypeEscape:
go c.FightC.Over(c, model.BattleOverReason.PlayerEscape) go c.Over(model.BattleOverReason.PlayerEscape)
case fight.FightActionTypeChat: case fight.FightActionTypeChat:
go c.FightC.Chat(c, envelope.Chat) go c.Chat(envelope.Chat)
} }
} }

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"
) )
@@ -36,7 +35,7 @@ type towerChoiceState struct {
} }
// 暗黑门进入boss // 暗黑门进入boss
func (h Controller) FreshOpen(data *C2S_OPEN_DARKPORTAL, c *player.Player) (result *fight.S2C_OPEN_DARKPORTAL, err errorcode.ErrorCode) { func (h Controller) OpenDarkPortal(data *C2S_OPEN_DARKPORTAL, c *player.Player) (result *fight.S2C_OPEN_DARKPORTAL, err errorcode.ErrorCode) {
result = &fight.S2C_OPEN_DARKPORTAL{} result = &fight.S2C_OPEN_DARKPORTAL{}
towerBosses := service.NewTower110Service().Boss(uint32(data.Level)) towerBosses := service.NewTower110Service().Boss(uint32(data.Level))
@@ -56,8 +55,8 @@ func (h Controller) FreshOpen(data *C2S_OPEN_DARKPORTAL, c *player.Player) (resu
return result, 0 return result, 0
} }
// FreshChoiceFightLevel 处理玩家选择挑战模式(试炼之塔或勇者之塔) // ChooseTowerFightLevel 处理玩家选择挑战模式(试炼之塔或勇者之塔)
func (h Controller) FreshChoiceFightLevel(data *C2S_FRESH_CHOICE_FIGHT_LEVEL, c *player.Player) (result *fight.S2C_FreshChoiceLevelRequestInfo, err errorcode.ErrorCode) { func (h Controller) ChooseTowerFightLevel(data *C2S_FRESH_CHOICE_FIGHT_LEVEL, c *player.Player) (result *fight.S2C_FreshChoiceLevelRequestInfo, err errorcode.ErrorCode) {
result = &fight.S2C_FreshChoiceLevelRequestInfo{} result = &fight.S2C_FreshChoiceLevelRequestInfo{}
c.Info.CurrentFreshStage = utils.Max(c.Info.CurrentFreshStage, 1) c.Info.CurrentFreshStage = utils.Max(c.Info.CurrentFreshStage, 1)
c.Info.CurrentStage = utils.Max(c.Info.CurrentStage, 1) c.Info.CurrentStage = utils.Max(c.Info.CurrentStage, 1)
@@ -82,8 +81,8 @@ func (h Controller) FreshChoiceFightLevel(data *C2S_FRESH_CHOICE_FIGHT_LEVEL, c
return result, 0 return result, 0
} }
// FreshLeaveFightLevel 处理控制器请求。 // LeaveTowerFightLevel 处理控制器请求。
func (h Controller) FreshLeaveFightLevel(data *FRESH_LEAVE_FIGHT_LEVEL, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) { func (h Controller) LeaveTowerFightLevel(data *FRESH_LEAVE_FIGHT_LEVEL, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
_ = data _ = data
defer c.GetSpace().EnterMap(c) defer c.GetSpace().EnterMap(c)
@@ -93,8 +92,8 @@ func (h Controller) FreshLeaveFightLevel(data *FRESH_LEAVE_FIGHT_LEVEL, c *playe
return result, 0 return result, 0
} }
// PetTawor 处理控制器请求。 // StartTowerFight 处理控制器请求。
func (h Controller) PetTawor(data *StartTwarInboundInfo, c *player.Player) (result *fight.S2C_ChoiceLevelRequestInfo, err errorcode.ErrorCode) { func (h Controller) StartTowerFight(data *StartTwarInboundInfo, c *player.Player) (result *fight.S2C_ChoiceLevelRequestInfo, err errorcode.ErrorCode) {
if err = c.CanFight(); err != 0 { if err = c.CanFight(); err != 0 {
return nil, err return nil, err
} }
@@ -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

@@ -3,10 +3,12 @@ package controller
import ( import (
"blazing/common/rpc" "blazing/common/rpc"
"blazing/common/socket/errorcode" "blazing/common/socket/errorcode"
"blazing/cool"
"blazing/logic/service/common" "blazing/logic/service/common"
"blazing/logic/service/fight" "blazing/logic/service/fight"
"blazing/logic/service/fight/pvp" "blazing/logic/service/fight/pvp"
"blazing/logic/service/player" "blazing/logic/service/player"
"context"
) )
// 表示"宠物王加入"的入站消息数据 // 表示"宠物王加入"的入站消息数据
@@ -14,10 +16,12 @@ type PetTOPLEVELnboundInfo struct {
Head common.TomeeHeader `cmd:"2458" struc:"skip"` Head common.TomeeHeader `cmd:"2458" struc:"skip"`
Mode uint32 //巅峰赛对战模式 19 = 普通模式单精灵 20 = 普通模式多精灵 Mode uint32 //巅峰赛对战模式 19 = 普通模式单精灵 20 = 普通模式多精灵
TianxuanPetIDsLen uint32 `struc:"sizeof=TianxuanPetIDs"`
TianxuanPetIDs []uint32 `json:"tianxuanPetIds"`
} }
// JoINtop 处理控制器请求。 // JoinPeakQueue 处理控制器请求。
func (h Controller) JoINtop(data *PetTOPLEVELnboundInfo, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) { func (h Controller) JoinPeakQueue(data *PetTOPLEVELnboundInfo, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
err = pvp.JoinPeakQueue(c, data.Mode) err = pvp.JoinPeakQueue(c, data.Mode)
if err != 0 { if err != 0 {
return nil, err return nil, err
@@ -37,9 +41,13 @@ func (h Controller) JoINtop(data *PetTOPLEVELnboundInfo, c *player.Player) (resu
Nick: c.Info.Nick, Nick: c.Info.Nick,
FightMode: fightMode, FightMode: fightMode,
Status: status, Status: status,
IsVip: cool.Config.ServerInfo.IsVip,
IsDebug: cool.Config.ServerInfo.IsDebug,
CatchTimes: pvp.AvailableCatchTimes(c.GetPetInfo(0)), CatchTimes: pvp.AvailableCatchTimes(c.GetPetInfo(0)),
} }
if callErr := Maincontroller.RPCClient.MatchJoinOrUpdate(joinPayload); callErr != nil { ctx, cancel := context.WithTimeout(context.Background(), rpc.ClientCallTimeout)
defer cancel()
if callErr := Maincontroller.RPCClient.MatchJoinOrUpdate(ctx, joinPayload); callErr != nil {
pvp.CancelPeakQueue(c) pvp.CancelPeakQueue(c)
return nil, errorcode.ErrorCodes.ErrSystemBusyTryLater return nil, errorcode.ErrorCodes.ErrSystemBusyTryLater
} }
@@ -49,7 +57,9 @@ func (h Controller) JoINtop(data *PetTOPLEVELnboundInfo, c *player.Player) (resu
// CancelPeakQueue 处理控制器请求。 // CancelPeakQueue 处理控制器请求。
func (h Controller) CancelPeakQueue(data *PeakQueueCancelInboundInfo, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) { func (h Controller) CancelPeakQueue(data *PeakQueueCancelInboundInfo, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
if Maincontroller.RPCClient != nil && Maincontroller.RPCClient.MatchCancel != nil { if Maincontroller.RPCClient != nil && Maincontroller.RPCClient.MatchCancel != nil {
_ = Maincontroller.RPCClient.MatchCancel(c.Info.UserID) ctx, cancel := context.WithTimeout(context.Background(), rpc.ClientCallTimeout)
_ = Maincontroller.RPCClient.MatchCancel(ctx, c.Info.UserID)
cancel()
} }
pvp.CancelPeakQueue(c) pvp.CancelPeakQueue(c)
return nil, -1 return nil, -1

View File

@@ -145,9 +145,9 @@ func (h Controller) ArenaGetInfo(data *ARENA_GET_INFO, c *player.Player) (result
return return
} }
// ArenaUpfight 放弃擂台挑战的包 // LeaveArena 放弃擂台挑战的包
// ArenaUpfight 都需要通过2419包广播更新擂台状态 // LeaveArena 都需要通过2419包广播更新擂台状态
func (h Controller) ArenaUpfight(data *ARENA_UPFIGHT, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) { func (h Controller) LeaveArena(data *ARENA_UPFIGHT, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
//原子操作,修改擂台状态 //原子操作,修改擂台状态
if atomic.LoadUint32(&c.GetSpace().Owner.UserID) != c.GetInfo().UserID { //说明已经有人了 if atomic.LoadUint32(&c.GetSpace().Owner.UserID) != c.GetInfo().UserID { //说明已经有人了
return nil, errorcode.ErrorCodes.ErrChampionCannotCancel return nil, errorcode.ErrorCodes.ErrChampionCannotCancel
@@ -164,12 +164,12 @@ func (h Controller) ArenaUpfight(data *ARENA_UPFIGHT, c *player.Player) (result
} }
// ArenaOwnerAcce 此包为擂台战对战结束后 胜方前端会发送给后端 具体作用为通知后端发送2419包更新擂台信息。 // ConfirmArenaFightResult 此包为擂台战对战结束后 胜方前端会发送给后端 具体作用为通知后端发送2419包更新擂台信息。
// 前端到后端无数据内容 // 前端到后端无数据内容
// 后端到前端无数据内容 // 后端到前端无数据内容
// public static const ARENA_OWENR_OUT:uint = 2423; // public static const ARENA_OWENR_OUT:uint = 2423;
// ArenaOwnerAcce 此包不清楚具体怎么触发 但已知此包为后端主动发送。不清楚什么情况下回用到 // ConfirmArenaFightResult 此包不清楚具体怎么触发 但已知此包为后端主动发送。不清楚什么情况下回用到
func (h Controller) ArenaOwnerAcce(data *ARENA_OWENR_ACCE, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) { func (h Controller) ConfirmArenaFightResult(data *ARENA_OWENR_ACCE, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
s := c.GetSpace() s := c.GetSpace()

View File

@@ -87,6 +87,14 @@ type C2S_RoomPetInfo struct {
CatchTime uint32 `json:"catchTime"` CatchTime uint32 `json:"catchTime"`
} }
// C2S_RoomPetShowToggle 基地展示精灵添加/移除请求
type C2S_RoomPetShowToggle struct {
Head common.TomeeHeader `cmd:"2326" struc:"skip"`
CatchTime uint32 `json:"catchTime"`
PetID uint32 `json:"petID"`
Flag uint32 `json:"flag"` // 1=添加展示, 0=移除展示
}
// C2S_BUY_FITMENT 定义请求或响应数据结构。 // C2S_BUY_FITMENT 定义请求或响应数据结构。
type C2S_BUY_FITMENT struct { type C2S_BUY_FITMENT struct {
Head common.TomeeHeader `cmd:"10004" struc:"skip"` Head common.TomeeHeader `cmd:"10004" struc:"skip"`

View File

@@ -16,8 +16,8 @@ type MAIN_LOGIN_IN struct {
Sid []byte `struc:"[16]byte"` Sid []byte `struc:"[16]byte"`
} }
// CheakSession 校验登录session。 // CheckSession 校验登录session。
func (l *MAIN_LOGIN_IN) CheakSession() (bool, uint32) { func (l *MAIN_LOGIN_IN) CheckSession() (bool, uint32) {
t1 := hex.EncodeToString(l.Sid) t1 := hex.EncodeToString(l.Sid)
r, err := cool.CacheManager.Get(context.Background(), fmt.Sprintf("session:%d", l.Head.UserID)) r, err := cool.CacheManager.Get(context.Background(), fmt.Sprintf("session:%d", l.Head.UserID))
if err != nil { if err != nil {

View File

@@ -58,13 +58,7 @@ 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)
if err := c.Service.Item.UPDATE(itemID, -1); err != nil { return finishUsePetItemOutOfFight(c, itemID, currentPet)
return nil, errorcode.ErrorCodes.ErrInsufficientItems
}
c.Service.Info.Save(*c.Info)
result = &item.S2C_USE_PET_ITEM_OUT_OF_FIGHT{}
copier.Copy(&result, currentPet)
return result, 0
} }
var errcode errorcode.ErrorCode var errcode errorcode.ErrorCode
@@ -75,7 +69,7 @@ func (h Controller) UsePetItemOutOfFight(data *C2S_USE_PET_ITEM_OUT_OF_FIGHT, c
refreshPetPaneKeepHP(currentPet, oldHP) refreshPetPaneKeepHP(currentPet, oldHP)
} }
case itemID == 300212: case itemID == 300212:
errcode = h.handlexuancaiItem(currentPet, c) errcode = h.handleXuanCaiItem(currentPet, c)
case itemCfg.Bonus != 0: case itemCfg.Bonus != 0:
errcode = errorcode.ErrorCodes.ErrItemUnusable errcode = errorcode.ErrorCodes.ErrItemUnusable
case itemCfg.HP != 0: case itemCfg.HP != 0:
@@ -93,11 +87,15 @@ func (h Controller) UsePetItemOutOfFight(data *C2S_USE_PET_ITEM_OUT_OF_FIGHT, c
return nil, errcode return nil, errcode
} }
return finishUsePetItemOutOfFight(c, itemID, currentPet)
}
func finishUsePetItemOutOfFight(c *player.Player, itemID uint32, currentPet *model.PetInfo) (*item.S2C_USE_PET_ITEM_OUT_OF_FIGHT, errorcode.ErrorCode) {
if err := c.Service.Item.UPDATE(itemID, -1); err != nil { if err := c.Service.Item.UPDATE(itemID, -1); err != nil {
return nil, errorcode.ErrorCodes.ErrInsufficientItems return nil, errorcode.ErrorCodes.ErrInsufficientItems
} }
c.Service.Info.Save(*c.Info) 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
} }
@@ -123,23 +121,28 @@ func (h Controller) handleNeuronItem(currentPet *model.PetInfo, c *player.Player
} }
// 炫彩碎片 处理神300212 // 炫彩碎片 处理神300212
func (h Controller) handlexuancaiItem(currentPet *model.PetInfo, c *player.Player) errorcode.ErrorCode { func (h Controller) handleXuanCaiItem(currentPet *model.PetInfo, c *player.Player) errorcode.ErrorCode {
r, _ := element.Calculator.GetCombination(int(xmlres.PetMAP[int(currentPet.ID)].Type)) petCfg, ok := xmlres.PetMAP[int(currentPet.ID)]
if !ok {
return errorcode.ErrorCodes.ErrSystemError
}
r, _ := element.Calculator.GetCombination(int(petCfg.Type))
if r.Secondary != nil { if r.Secondary != nil {
return errorcode.ErrorCodes.ErrItemUnusable return errorcode.ErrorCodes.ErrItemUnusable
} }
itemid := uint32(currentPet.Type()) + 400686 shardItemID := uint32(currentPet.Type()) + 400686
items := c.Service.Item.CheakItem(itemid) items := c.Service.Item.CheakItem(shardItemID)
if items < 100 { if items < 100 {
return errorcode.ErrorCodes.ErrInsufficientItems return errorcode.ErrorCodes.ErrInsufficientItems
} }
ok := currentPet.FixShiny() ok = currentPet.FixShiny()
if !ok { if !ok {
return errorcode.ErrorCodes.ErrItemUnusable return errorcode.ErrorCodes.ErrItemUnusable
} }
if err := c.Service.Item.UPDATE(itemid, -100); err != nil { if err := c.Service.Item.UPDATE(shardItemID, -100); err != nil {
return errorcode.ErrorCodes.ErrInsufficientItems return errorcode.ErrorCodes.ErrInsufficientItems
} }
return 0 return 0
@@ -299,12 +302,12 @@ func (h Controller) UseSpeedupItem(data *C2S_USE_SPEEDUP_ITEM, c *player.Player)
// 能量吸收器相关方法 // 能量吸收器相关方法
// ============================== // ==============================
// UseEnergyXishou 使用能量吸收器 // UseEnergyAbsorber 使用能量吸收器
// data: 包含使用的能量吸收器物品ID的输入信息 // data: 包含使用的能量吸收器物品ID的输入信息
// c: 当前玩家对象 // c: 当前玩家对象
// 返回: 无数据(响应包单独组装)和错误码 // 返回: 无数据(响应包单独组装)和错误码
// 说明:使用后扣减道具并更新玩家能量吸收器剩余次数 // 说明:使用后扣减道具并更新玩家能量吸收器剩余次数
func (h Controller) UseEnergyXishou(data *C2S_USE_ENERGY_XISHOU, c *player.Player) (result *item.S2C_USE_ENERGY_XISHOU, err errorcode.ErrorCode) { func (h Controller) UseEnergyAbsorber(data *C2S_USE_ENERGY_XISHOU, c *player.Player) (result *item.S2C_USE_ENERGY_XISHOU, err errorcode.ErrorCode) {
// 1. 校验道具是否存在且数量充足 // 1. 校验道具是否存在且数量充足
itemCount := c.Service.Item.CheakItem(data.ItemID) itemCount := c.Service.Item.CheakItem(data.ItemID)
if itemCount <= 0 { if itemCount <= 0 {

View File

@@ -46,6 +46,50 @@ func TestUsePetItemOutOfFightAppliesToBackupPetInMemory(t *testing.T) {
} }
} }
func TestUsePetItemOutOfFightPersistsEnergyOrbEffectInfo(t *testing.T) {
petID := firstPetIDForControllerTest(t)
petInfo := playermodel.GenPetInfo(petID, 31, 0, 0, 50, nil, 0)
if petInfo == nil {
t.Fatal("failed to generate test pet")
}
testPlayer := player.NewPlayer(nil)
testPlayer.Info = &playermodel.PlayerInfo{
UserID: 10001,
PetList: []playermodel.PetInfo{*petInfo},
}
testPlayer.Service = blservice.NewUserService(testPlayer.Info.UserID)
itemID, effectCfg := firstEnergyOrbItemForControllerTest(t)
if err := testPlayer.Service.Item.UPDATE(itemID, 1); err != nil {
t.Fatalf("failed to seed energy orb item %d: %v", itemID, err)
}
_, err := (Controller{}).UsePetItemOutOfFight(&C2S_USE_PET_ITEM_OUT_OF_FIGHT{
CatchTime: petInfo.CatchTime,
ItemID: int32(itemID),
}, testPlayer)
if err != 0 {
t.Fatalf("expected energy orb use to succeed, got err=%d", err)
}
storedPet := testPlayer.Info.PetList[0]
if len(storedPet.EffectInfo) == 0 {
t.Fatalf("expected pet effect info to persist after using item %d", itemID)
}
last := storedPet.EffectInfo[len(storedPet.EffectInfo)-1]
if last.ItemID != itemID {
t.Fatalf("expected stored item id %d, got %d", itemID, last.ItemID)
}
if last.Status != 2 {
t.Fatalf("expected energy orb status 2, got %d", last.Status)
}
if last.EID != uint16(atoiOrZero(effectCfg.Eid)) {
t.Fatalf("expected effect id %s, got %d", effectCfg.Eid, last.EID)
}
}
func firstRecoverHPItemForControllerTest(t *testing.T) (uint32, int) { func firstRecoverHPItemForControllerTest(t *testing.T) (uint32, int) {
t.Helper() t.Helper()
@@ -58,3 +102,36 @@ func firstRecoverHPItemForControllerTest(t *testing.T) (uint32, int) {
t.Fatal("xmlres.ItemsMAP has no HP recovery item") t.Fatal("xmlres.ItemsMAP has no HP recovery item")
return 0, 0 return 0, 0
} }
func firstEnergyOrbItemForControllerTest(t *testing.T) (uint32, xmlres.NewSeIdx) {
t.Helper()
for id, itemCfg := range xmlres.ItemsMAP {
if itemCfg.NewSeIdx == 0 {
continue
}
effectCfg, ok := xmlres.EffectMAP[itemCfg.NewSeIdx]
if !ok {
continue
}
if effectCfg.Stat == "2" && effectCfg.ItemId != nil {
return uint32(id), effectCfg
}
}
t.Fatal("xmlres.ItemsMAP has no energy orb item")
return 0, xmlres.NewSeIdx{}
}
func atoiOrZero(value string) int {
result := 0
for _, ch := range value {
if ch < '0' || ch > '9' {
return result
}
result = result*10 + int(ch-'0')
}
return result
}

View File

@@ -2,6 +2,7 @@ package controller
import ( import (
"blazing/common/data/share" "blazing/common/data/share"
"blazing/common/rpc"
"blazing/common/socket/errorcode" "blazing/common/socket/errorcode"
"blazing/cool" "blazing/cool"
"blazing/logic/service/player" "blazing/logic/service/player"
@@ -31,7 +32,10 @@ func waitUserOffline(userID uint32, timeout time.Duration) bool {
return false return false
} }
if time.Since(lastKickAt) >= waitUserOfflineKickGap { if time.Since(lastKickAt) >= waitUserOfflineKickGap {
if kickErr := Maincontroller.RPCClient.Kick(userID); kickErr != nil { ctx, cancel := context.WithTimeout(context.Background(), rpc.ClientCallTimeout)
kickErr := Maincontroller.RPCClient.Kick(ctx, userID)
cancel()
if kickErr != nil {
cool.Logger.Error(context.Background(), "补踢失败", userID, kickErr) cool.Logger.Error(context.Background(), "补踢失败", userID, kickErr)
} }
lastKickAt = time.Now() lastKickAt = time.Now()
@@ -47,7 +51,7 @@ func (h Controller) Login(data *MAIN_LOGIN_IN, c gnet.Conn) (result *user.LoginM
defer c.Close() defer c.Close()
return return
} }
isSessionValid, hashcode := data.CheakSession() isSessionValid, hashcode := data.CheckSession()
if !isSessionValid { if !isSessionValid {
defer c.Close() defer c.Close()
@@ -55,7 +59,9 @@ func (h Controller) Login(data *MAIN_LOGIN_IN, c gnet.Conn) (result *user.LoginM
} }
if onlineServerID, onlineErr := share.ShareManager.GetUserOnline(data.Head.UserID); onlineErr == nil { if onlineServerID, onlineErr := share.ShareManager.GetUserOnline(data.Head.UserID); onlineErr == nil {
kickErr := Maincontroller.RPCClient.Kick(data.Head.UserID) //通知其他服务器踢人 ctx, cancel := context.WithTimeout(context.Background(), rpc.ClientCallTimeout)
kickErr := Maincontroller.RPCClient.Kick(ctx, data.Head.UserID) //通知其他服务器踢人
cancel()
if kickErr != nil { if kickErr != nil {
cool.Logger.Error(context.Background(), "踢人失败", data.Head.UserID, onlineServerID, kickErr) cool.Logger.Error(context.Background(), "踢人失败", data.Head.UserID, onlineServerID, kickErr)
err = errorcode.ErrorCodes.ErrSystemBusyTryLater err = errorcode.ErrorCodes.ErrSystemBusyTryLater

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

@@ -18,8 +18,8 @@ func (h Controller) GetPetBargeList(data *PetBargeListInboundInfo, player *playe
ret.PetBargeList = append(ret.PetBargeList, pet.PetBargeListInfo{ ret.PetBargeList = append(ret.PetBargeList, pet.PetBargeListInfo{
PetId: uint32(v.Args[0]), PetId: uint32(v.Args[0]),
EnCntCnt: 1, EnCntCnt: 1,
IsCatched: uint32(v.Results[0]), IsCatched: uint32(v.Results[1]),
IsKilled: uint32(v.Results[1]), IsKilled: uint32(v.Results[0]),
}) })
} }

View File

@@ -12,8 +12,8 @@ import (
"github.com/jinzhu/copier" "github.com/jinzhu/copier"
) )
// PetELV 处理控制器请求。 // EvolvePet 处理控制器请求。
func (h Controller) PetELV(data *C2S_PET_EVOLVTION, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) { func (h Controller) EvolvePet(data *C2S_PET_EVOLVTION, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
_, currentPet, found := c.FindPet(data.CacthTime) _, currentPet, found := c.FindPet(data.CacthTime)
if !found { if !found {
return nil, errorcode.ErrorCodes.Err10401 return nil, errorcode.ErrorCodes.Err10401

View File

@@ -12,11 +12,11 @@ const (
petEVMaxSingle uint32 = 255 petEVMaxSingle uint32 = 255
) )
// PetEVDiy 自定义分配宠物努力值EV // CustomizePetEV 自定义分配宠物努力值EV
// data: 包含宠物捕获时间和EV分配数据的输入信息 // data: 包含宠物捕获时间和EV分配数据的输入信息
// c: 当前玩家对象 // c: 当前玩家对象
// 返回: 分配结果和错误码 // 返回: 分配结果和错误码
func (h Controller) PetEVDiy(data *PetEV, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) { func (h Controller) CustomizePetEV(data *PetEV, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
slot, found := c.FindPetBagSlot(data.CacthTime) slot, found := c.FindPetBagSlot(data.CacthTime)
if !found { if !found {
return nil, errorcode.ErrorCodes.Err10401 return nil, errorcode.ErrorCodes.Err10401

View File

@@ -7,7 +7,7 @@ import (
playermodel "blazing/modules/player/model" playermodel "blazing/modules/player/model"
) )
func TestPetEVDiy_AppliesToBackupPet(t *testing.T) { func TestCustomizePetEV_AppliesToBackupPet(t *testing.T) {
p := player.NewPlayer(nil) p := player.NewPlayer(nil)
p.Info = &playermodel.PlayerInfo{ p.Info = &playermodel.PlayerInfo{
EVPool: 20, EVPool: 20,
@@ -28,9 +28,9 @@ func TestPetEVDiy_AppliesToBackupPet(t *testing.T) {
EVs: [6]uint32{0, 8, 4, 0, 0, 0}, EVs: [6]uint32{0, 8, 4, 0, 0, 0},
} }
_, err := (Controller{}).PetEVDiy(data, p) _, err := (Controller{}).CustomizePetEV(data, p)
if err != 0 { if err != 0 {
t.Fatalf("PetEVDiy returned error: %v", err) t.Fatalf("CustomizePetEV returned error: %v", err)
} }
got := p.Info.BackupPetList[0].Ev got := p.Info.BackupPetList[0].Ev

View File

@@ -85,9 +85,10 @@ func (h Controller) PlayerShowPet(
return return
} }
slot, ok := player.FindPetBagSlot(data.CatchTime) // 仅允许背包精灵跟随:仓库中的精灵不允许跟随
if !ok { slot, found := player.FindPetBagSlot(data.CatchTime)
return nil, errorcode.ErrorCodes.ErrPokemonNotExists if !found {
return nil, errorcode.ErrorCodes.ErrCannotShowBagPokemon
} }
currentPet := slot.PetInfoPtr() currentPet := slot.PetInfoPtr()

View File

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

View File

@@ -40,7 +40,7 @@ func (ctl Controller) GetBreedPet(
} }
result = &pet.S2C_GET_BREED_PET{} result = &pet.S2C_GET_BREED_PET{}
compatibleFemaleIDs := buildBreedPetIDSet(service.NewEggService().GetData(malePet.ID)) compatibleFemaleIDs := buildBreedPetIDSet(breedConfigService.GetData(malePet.ID))
if len(compatibleFemaleIDs) == 0 { if len(compatibleFemaleIDs) == 0 {
return result, 0 return result, 0
} }
@@ -116,8 +116,12 @@ func buildBreedPetIDSet(ids []int32) map[uint32]struct{} {
} }
func canBreedPair(maleID, femaleID uint32) bool { func canBreedPair(maleID, femaleID uint32) bool {
_, ok := buildBreedPetIDSet(service.NewEggService().GetData(maleID))[femaleID] for _, id := range breedConfigService.GetData(maleID) {
return ok if uint32(id) == femaleID {
return true
}
}
return false
} }
// GetEggList 处理控制器请求。 // GetEggList 处理控制器请求。
@@ -144,7 +148,10 @@ const (
breedCost = 1000 breedCost = 1000
) )
var limiter *ratelimit.Rule = ratelimit.NewRule() var (
breedConfigService = service.NewEggService()
limiter = ratelimit.NewRule()
)
// 简单规则案例 // 简单规则案例
func init() { func init() {

View File

@@ -3,6 +3,7 @@ package controller
import ( import (
"blazing/common/socket/errorcode" "blazing/common/socket/errorcode"
"blazing/modules/player/model" "blazing/modules/player/model"
"blazing/modules/player/service"
"blazing/logic/service/pet" "blazing/logic/service/pet"
"blazing/logic/service/player" "blazing/logic/service/player"
@@ -17,10 +18,9 @@ import (
// 返回: 基地家具信息和错误码 // 返回: 基地家具信息和错误码
func (h Controller) GetFitmentUsing(data *FitmentUseringInboundInfo, c *player.Player) (result *room.FitmentUseringOutboundInfo, err errorcode.ErrorCode) { func (h Controller) GetFitmentUsing(data *FitmentUseringInboundInfo, c *player.Player) (result *room.FitmentUseringOutboundInfo, err errorcode.ErrorCode) {
result = &room.FitmentUseringOutboundInfo{UserId: c.Info.UserID, RoomId: data.TargetUserID} result = &room.FitmentUseringOutboundInfo{UserId: c.Info.UserID, RoomId: data.TargetUserID}
result.Fitments = make([]model.FitmentShowInfo, 0)
result.Fitments = append(result.Fitments, model.FitmentShowInfo{Id: 500001, Status: 1, X: 1, Y: 1, Dir: 1})
roomInfo := c.Service.Room.Get(data.TargetUserID) roomInfo := c.Service.Room.Get(data.TargetUserID)
result.Fitments = make([]model.FitmentShowInfo, 0, len(roomInfo.PlacedItems)+1)
result.Fitments = append(result.Fitments, model.FitmentShowInfo{Id: 500001, Status: 1, X: 1, Y: 1, Dir: 1})
result.Fitments = append(result.Fitments, roomInfo.PlacedItems...) result.Fitments = append(result.Fitments, roomInfo.PlacedItems...)
return return
} }
@@ -31,18 +31,46 @@ func (h Controller) GetFitmentUsing(data *FitmentUseringInboundInfo, c *player.P
// 返回: 精灵展示列表和错误码 // 返回: 精灵展示列表和错误码
func (h Controller) GetRoomPetShowInfo(data *PetRoomListInboundInfo, c *player.Player) (result *room.PetRoomListOutboundInfo, err errorcode.ErrorCode) { func (h Controller) GetRoomPetShowInfo(data *PetRoomListInboundInfo, c *player.Player) (result *room.PetRoomListOutboundInfo, err errorcode.ErrorCode) {
result = &room.PetRoomListOutboundInfo{} result = &room.PetRoomListOutboundInfo{}
result.Pets = make([]pet.PetShortInfo, 0) showPets := service.NewPetService(data.TargetUserID).GetShowPets()
roomInfo := c.Service.Room.Get(data.TargetUserID) result.Pets = make([]pet.PetShortInfo, 0, len(showPets))
for _, catchTime := range roomInfo.ShowPokemon { for i := range showPets {
petInfo := c.Service.Pet.PetInfoOneOther(data.TargetUserID, catchTime)
if petInfo.Data.ID == 0 {
continue
}
var petShortInfo pet.PetShortInfo var petShortInfo pet.PetShortInfo
copier.Copy(&petShortInfo, &petInfo.Data) copier.Copy(&petShortInfo, &showPets[i].Data)
if petInfo.ID != 0 {
result.Pets = append(result.Pets, petShortInfo) result.Pets = append(result.Pets, petShortInfo)
} }
return
}
// SetRoomPetShowInfo 设置基地展示精灵并返回最新展示列表cmd:2323
func (h Controller) SetRoomPetShowInfo(data *C2S_PET_ROOM_SHOW, c *player.Player) (result *room.S2C_PET_ROOM_SHOW, err errorcode.ErrorCode) {
result = &room.S2C_PET_ROOM_SHOW{}
catchTimes := make([]uint32, 0, len(data.PetShowList))
seen := make(map[uint32]struct{}, len(data.PetShowList))
for _, item := range data.PetShowList {
ct := uint32(item.CatchTime)
if ct == 0 {
continue
}
if _, ok := seen[ct]; ok {
continue
}
seen[ct] = struct{}{}
catchTimes = append(catchTimes, ct)
}
petSvc := service.NewPetService(c.Info.UserID)
if !petSvc.SetShowCatchTimes(catchTimes) {
err = errorcode.ErrorCodes.ErrSystemError
return
}
showPets := petSvc.GetShowPets()
result.PetShowList = make([]pet.PetShortInfo, 0, len(showPets))
for i := range showPets {
var petShortInfo pet.PetShortInfo
copier.Copy(&petShortInfo, &showPets[i].Data)
result.PetShowList = append(result.PetShowList, petShortInfo)
} }
return return
} }
@@ -53,10 +81,10 @@ func (h Controller) GetRoomPetShowInfo(data *PetRoomListInboundInfo, c *player.P
// 返回: 玩家所有家具列表和错误码 // 返回: 玩家所有家具列表和错误码
func (h Controller) GetAllFurniture(data *FitmentAllInboundEmpty, c *player.Player) (result *room.FitmentAllOutboundInfo, err errorcode.ErrorCode) { func (h Controller) GetAllFurniture(data *FitmentAllInboundEmpty, c *player.Player) (result *room.FitmentAllOutboundInfo, err errorcode.ErrorCode) {
result = &room.FitmentAllOutboundInfo{} result = &room.FitmentAllOutboundInfo{}
result.Fitments = make([]room.FitmentItemInfo, 0)
items := c.Service.Item.Get(500000, 600000) items := c.Service.Item.Get(500000, 600000)
roomData := c.Service.Room.Get(c.Info.UserID) roomData := c.Service.Room.Get(c.Info.UserID)
result.Fitments = make([]room.FitmentItemInfo, 0, len(items))
for _, item := range items { for _, item := range items {
var itemInfo room.FitmentItemInfo var itemInfo room.FitmentItemInfo
itemInfo.Id = item.ItemId itemInfo.Id = item.ItemId
@@ -77,6 +105,9 @@ func (h Controller) GetAllFurniture(data *FitmentAllInboundEmpty, c *player.Play
// 返回: 精灵详细信息和错误码 // 返回: 精灵详细信息和错误码
func (h Controller) GetRoomPetInfo(data *C2S_RoomPetInfo, c *player.Player) (result *pet.RoomPetInfo, err errorcode.ErrorCode) { func (h Controller) GetRoomPetInfo(data *C2S_RoomPetInfo, c *player.Player) (result *pet.RoomPetInfo, err errorcode.ErrorCode) {
petInfo := c.Service.Pet.PetInfoOneOther(data.UserID, data.CatchTime) petInfo := c.Service.Pet.PetInfoOneOther(data.UserID, data.CatchTime)
if petInfo == nil {
return nil, errorcode.ErrorCodes.ErrPokemonNotExists
}
result = &pet.RoomPetInfo{} result = &pet.RoomPetInfo{}
copier.CopyWithOption(result, &petInfo.Data, copier.Option{DeepCopy: true}) copier.CopyWithOption(result, &petInfo.Data, copier.Option{DeepCopy: true})
result.OwnerId = data.UserID result.OwnerId = data.UserID

View File

@@ -2,11 +2,8 @@ package controller
import ( import (
"blazing/common/socket/errorcode" "blazing/common/socket/errorcode"
"blazing/logic/service/pet"
"blazing/logic/service/player" "blazing/logic/service/player"
"blazing/logic/service/room" "blazing/logic/service/room"
"github.com/jinzhu/copier"
) )
// SetFitment 设置基地家具摆放 // SetFitment 设置基地家具摆放
@@ -18,29 +15,3 @@ func (h Controller) SetFitment(data *SET_FITMENT, c *player.Player) (result *roo
c.Service.Room.Set(data.Fitments) c.Service.Room.Set(data.Fitments)
return return
} }
// SetPet 设置基地展示的精灵
// data: 包含精灵展示列表的输入信息
// c: 当前玩家对象
// 返回: 精灵展示列表和错误码
func (h Controller) SetPet(data *C2S_PET_ROOM_SHOW, c *player.Player) (result *room.S2C_PET_ROOM_SHOW, err errorcode.ErrorCode) {
var showPetCatchTimes []uint32
for _, petShowInfo := range data.PetShowList {
if petShowInfo.CatchTime != 0 {
showPetCatchTimes = append(showPetCatchTimes, petShowInfo.CatchTime)
}
}
c.Service.Room.Show(showPetCatchTimes)
result = &room.S2C_PET_ROOM_SHOW{}
result.PetShowList = make([]pet.PetShortInfo, len(showPetCatchTimes))
for _, catchTime := range showPetCatchTimes {
petInfo := c.Service.Pet.PetInfoOneByCatchTime(catchTime)
if petInfo == nil {
continue
}
var petShortInfo pet.PetShortInfo
copier.Copy(&petShortInfo, &petInfo.Data)
result.PetShowList = append(result.PetShowList, petShortInfo)
}
return
}

View File

@@ -102,11 +102,11 @@ func (h Controller) ChangePlayerDoodle(data *ChangeDoodleInboundInfo, player *pl
return return
} }
// ChangeNONOColor 修改NONO颜色 // ChangeNonoColor 修改NONO颜色
// data: 包含NONO颜色信息的输入数据 // data: 包含NONO颜色信息的输入数据
// player: 当前玩家对象 // player: 当前玩家对象
// 返回: NONO颜色更改结果和错误码 // 返回: NONO颜色更改结果和错误码
func (h Controller) ChangeNONOColor(data *ChangeNONOColorInboundInfo, player *player.Player) (result *user.ChangeNONOColorOutboundInfo, err errorcode.ErrorCode) { func (h Controller) ChangeNonoColor(data *ChangeNONOColorInboundInfo, player *player.Player) (result *user.ChangeNONOColorOutboundInfo, err errorcode.ErrorCode) {
//player.Info.Coins -= 200 //player.Info.Coins -= 200
player.Info.NONO.NonoColor = data.Color player.Info.NONO.NonoColor = data.Color
@@ -185,8 +185,8 @@ func (h Controller) ChangePlayerName(data *ChangePlayerNameInboundInfo, c *playe
return result, 0 return result, 0
} }
// ChangeTile 处理控制器请求。 // ChangeTitle 处理控制器请求。
func (h Controller) ChangeTile(data *ChangeTitleInboundInfo, c *player.Player) (result *user.ChangeTitleOutboundInfo, err errorcode.ErrorCode) { func (h Controller) ChangeTitle(data *ChangeTitleInboundInfo, c *player.Player) (result *user.ChangeTitleOutboundInfo, err errorcode.ErrorCode) {
result = &user.ChangeTitleOutboundInfo{ result = &user.ChangeTitleOutboundInfo{
UserID: c.Info.UserID, UserID: c.Info.UserID,

View File

@@ -4,16 +4,22 @@ 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"
"time" "time"
) )
// CDK 处理控制器请求。 // RedeemGiftCode 处理控制器请求。
func (h Controller) CDK(data *C2S_GET_GIFT_COMPLETE, player *logicplayer.Player) (result *user.S2C_GET_GIFT_COMPLETE, err errorcode.ErrorCode) { func (h Controller) RedeemGiftCode(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()
@@ -22,6 +28,9 @@ func (h Controller) CDK(data *C2S_GET_GIFT_COMPLETE, player *logicplayer.Player)
if r == nil { if r == nil {
return nil, errorcode.ErrorCodes.ErrMolecularCodeNotExists return nil, errorcode.ErrorCodes.ErrMolecularCodeNotExists
} }
if r.Type != configservice.CDKTypeReward {
return nil, errorcode.ErrorCodes.ErrMolecularCodeNotExists
}
if r.BindUserId != 0 && r.BindUserId != data.Head.UserID { if r.BindUserId != 0 && r.BindUserId != data.Head.UserID {
return nil, errorcode.ErrorCodes.ErrMolecularCodeFrozen return nil, errorcode.ErrorCodes.ErrMolecularCodeFrozen
} }
@@ -35,7 +44,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

@@ -81,7 +81,7 @@ func Start() {
controller.Maincontroller.RPCClient = rpcClient //将RPC赋值Start controller.Maincontroller.RPCClient = rpcClient //将RPC赋值Start
controller.Maincontroller.UID = gconv.Uint32(cool.Config.ServerInfo.GetID()) //赋值服务器ID controller.Maincontroller.UID = cool.Config.ServerInfo.RuntimeID() //赋值服务器复合ID
controller.Init(true) controller.Init(true)
xmlres.Initfile() xmlres.Initfile()

View File

@@ -5,8 +5,8 @@ import (
"blazing/modules/player/model" "blazing/modules/player/model"
) )
// FightI 定义 common 服务层依赖的战斗操作接口。 // FightControllerI 定义底层战斗控制器接口。
type FightI interface { type FightControllerI interface {
Over(c PlayerI, id model.EnumBattleOverReason) //逃跑 Over(c PlayerI, id model.EnumBattleOverReason) //逃跑
UseSkill(c PlayerI, id uint32) //使用技能 UseSkill(c PlayerI, id uint32) //使用技能
UseSkillAt(c PlayerI, id uint32, actorIndex, targetIndex int) UseSkillAt(c PlayerI, id uint32, actorIndex, targetIndex int)
@@ -27,3 +27,110 @@ type FightI interface {
GetOverChan() chan struct{} GetOverChan() chan struct{}
GetAttackValue(bool) *model.AttackValue GetAttackValue(bool) *model.AttackValue
} }
// FightI 是绑定到玩家后的战斗操作接口。
type FightI interface {
Over(model.EnumBattleOverReason)
UseSkill(uint32)
UseSkillAt(uint32, int, int)
GetCurrPET() *info.BattlePetEntity
GetCurrPETAt(int) *info.BattlePetEntity
GetOverInfo() model.FightOverInfo
Ownerid() uint32
ReadyFight()
ChangePet(uint32)
ChangePetAt(uint32, int)
Capture(uint32)
LoadPercent(int32)
UseItem(uint32, uint32)
UseItemAt(uint32, uint32, int, int)
Chat(string)
IsFirst() bool
GetOverChan() chan struct{}
GetAttackValue(bool) *model.AttackValue
}
type boundFight struct {
controller FightControllerI
player PlayerI
}
func BindFight(controller FightControllerI, player PlayerI) FightI {
if controller == nil {
return nil
}
return &boundFight{controller: controller, player: player}
}
func (f *boundFight) Over(reason model.EnumBattleOverReason) {
f.controller.Over(f.player, reason)
}
func (f *boundFight) UseSkill(id uint32) {
f.controller.UseSkill(f.player, id)
}
func (f *boundFight) UseSkillAt(id uint32, actorIndex, targetIndex int) {
f.controller.UseSkillAt(f.player, id, actorIndex, targetIndex)
}
func (f *boundFight) GetCurrPET() *info.BattlePetEntity {
return f.controller.GetCurrPET(f.player)
}
func (f *boundFight) GetCurrPETAt(actorIndex int) *info.BattlePetEntity {
return f.controller.GetCurrPETAt(f.player, actorIndex)
}
func (f *boundFight) GetOverInfo() model.FightOverInfo {
return f.controller.GetOverInfo()
}
func (f *boundFight) Ownerid() uint32 {
return f.controller.Ownerid()
}
func (f *boundFight) ReadyFight() {
f.controller.ReadyFight(f.player)
}
func (f *boundFight) ChangePet(id uint32) {
f.controller.ChangePet(f.player, id)
}
func (f *boundFight) ChangePetAt(id uint32, actorIndex int) {
f.controller.ChangePetAt(f.player, id, actorIndex)
}
func (f *boundFight) Capture(id uint32) {
f.controller.Capture(f.player, id)
}
func (f *boundFight) LoadPercent(percent int32) {
f.controller.LoadPercent(f.player, percent)
}
func (f *boundFight) UseItem(catchTime, itemID uint32) {
f.controller.UseItem(f.player, catchTime, itemID)
}
func (f *boundFight) UseItemAt(catchTime, itemID uint32, actorIndex, targetIndex int) {
f.controller.UseItemAt(f.player, catchTime, itemID, actorIndex, targetIndex)
}
func (f *boundFight) Chat(msg string) {
f.controller.Chat(f.player, msg)
}
func (f *boundFight) IsFirst() bool {
return f.controller.IsFirst(f.player)
}
func (f *boundFight) GetOverChan() chan struct{} {
return f.controller.GetOverChan()
}
func (f *boundFight) GetAttackValue(isOpponent bool) *model.AttackValue {
return f.controller.GetAttackValue(isOpponent)
}

View File

@@ -1,9 +1,11 @@
package common package common
import ( import (
"blazing/cool"
"bytes" "bytes"
"context"
"encoding/binary" "encoding/binary"
"fmt" "reflect"
"github.com/lunixbochs/struc" "github.com/lunixbochs/struc"
) )
@@ -39,7 +41,14 @@ func (h *TomeeHeader) Pack(data any) []byte {
var data1 bytes.Buffer var data1 bytes.Buffer
err := struc.Pack(&data1, data) err := struc.Pack(&data1, data)
if err != nil { if err != nil {
fmt.Println(err) cool.Logger.Error(context.Background(),
"struc pack failed",
"cmd", h.CMD,
"userID", h.UserID,
"result", h.Result,
"payloadType", packetPayloadType(data),
"err", err,
)
} }
if len(data1.Bytes()) == 0 { if len(data1.Bytes()) == 0 {
@@ -89,3 +98,10 @@ func (h *TomeeHeader) packHeaderWithData(data []byte) []byte {
return buf return buf
} }
func packetPayloadType(data any) string {
if data == nil {
return "<nil>"
}
return reflect.TypeOf(data).String()
}

View File

@@ -17,10 +17,25 @@ type PlayerI interface {
ItemAdd(ItemId, ItemCnt int64) (result bool) ItemAdd(ItemId, ItemCnt int64) (result bool)
GetInfo() *model.PlayerInfo GetInfo() *model.PlayerInfo
InvitePlayer(PlayerI) InvitePlayer(PlayerI)
SetFightC(FightI) SetFightC(FightControllerI)
QuitFight() QuitFight()
MessWin(bool) MessWin(bool)
CanFight() errorcode.ErrorCode CanFight() errorcode.ErrorCode
SendPackCmd(uint32, any) SendPackCmd(uint32, any)
GetPetInfo(limitlevel uint32) []model.PetInfo GetPetInfo(limitlevel uint32) []model.PetInfo
Over(model.EnumBattleOverReason)
UseSkill(uint32)
UseSkillAt(uint32, int, int)
GetCurrPET() *info.BattlePetEntity
GetCurrPETAt(int) *info.BattlePetEntity
ReadyFight()
ChangePet(uint32)
ChangePetAt(uint32, int)
Capture(uint32)
LoadPercent(int32)
UseItem(uint32, uint32)
UseItemAt(uint32, uint32, int, int)
Chat(string)
IsFirst() bool
} }

View File

@@ -253,7 +253,7 @@ func (f *FightC) UseSkillAt(c common.PlayerI, id uint32, actorIndex, targetIndex
// } // }
for _, v := range currentPet.Skills { for _, v := range currentPet.Skills {
if v.XML.ID == int(id) { if v.XML.ID == int(id) {
ret.SkillEntity = v ret.SkillEntity = info.CreateSkill(v.Info, currentPet)
break break
} }

View File

@@ -0,0 +1,67 @@
package fight
import (
"testing"
"blazing/logic/service/common"
fightinfo "blazing/logic/service/fight/info"
"blazing/logic/service/fight/input"
"blazing/modules/player/model"
)
func TestEnterTurnRunsActionStartWhenNoOneActs(t *testing.T) {
ourPlayer := &stubPlayer{info: model.PlayerInfo{UserID: 1001}}
oppPlayer := &stubPlayer{info: model.PlayerInfo{UserID: 2002}}
our := input.NewInput(nil, ourPlayer)
our.InitAttackValue()
ourPet := fightinfo.CreateBattlePetEntity(model.PetInfo{
ID: 11,
Name: "Alpha",
Level: 20,
Hp: 100,
MaxHp: 100,
CatchTime: 101,
})
ourPet.BindController(ourPlayer.info.UserID)
our.SetCurPetAt(0, ourPet)
our.Team = []*input.Input{our}
opp := input.NewInput(nil, oppPlayer)
opp.InitAttackValue()
oppPet := fightinfo.CreateBattlePetEntity(model.PetInfo{
ID: 22,
Name: "Beta",
Level: 20,
Hp: 80,
MaxHp: 80,
CatchTime: 202,
})
oppPet.BindController(oppPlayer.info.UserID)
opp.SetCurPetAt(0, oppPet)
opp.Team = []*input.Input{opp}
our.Opp = opp
opp.Opp = our
our.OppTeam = []*input.Input{opp}
opp.OppTeam = []*input.Input{our}
fc := &FightC{
Our: []*input.Input{our},
Opp: []*input.Input{opp},
}
our.FightC = common.BindFight(fc, ourPlayer)
opp.FightC = common.BindFight(fc, oppPlayer)
poison := opp.InitEffect(input.EffectType.Status, int(fightinfo.PetStatus.Poisoned))
if poison == nil {
t.Fatalf("expected poisoned status effect to be registered")
}
opp.AddEffect(opp, poison)
fc.enterturn(nil, nil)
if got := opp.CurrentPet().Info.Hp; got != 70 {
t.Fatalf("expected poisoned defender to lose 10 HP during action-start phase, got %d", got)
}
}

View File

@@ -31,6 +31,13 @@ func (e *NewSel0) IsOwner() bool {
return e.ID().GetCatchTime() == source.CurPet[0].Info.CatchTime return e.ID().GetCatchTime() == source.CurPet[0].Info.CatchTime
} }
func (e *NewSel0) CurrentSkillHit() bool {
if e.Ctx().SkillEntity == nil {
return false
}
return e.Ctx().SkillEntity.AttackTime != 0
}
// 免疫"能力(battle_lv)下降" // 免疫"能力(battle_lv)下降"
type NewSel1 struct { type NewSel1 struct {
NewSel0 NewSel0

View File

@@ -17,7 +17,7 @@ func (e *NewSel13) HookAction() bool {
} }
r := e.Ctx().Our.FightC.GetOverInfo() r := e.Ctx().Our.FightC.GetOverInfo()
if r.Round == uint32(e.Args()[0].IntPart()) { if r.Round == uint32(e.Args()[0].IntPart()) {
e.Ctx().Our.FightC.Over(e.Ctx().Our.Player, model.BattleOverReason.PlayerEscape) e.Ctx().Our.Player.Over(model.BattleOverReason.PlayerEscape)
return false //阻止技能释放 return false //阻止技能释放
} }

View File

@@ -13,13 +13,13 @@ type NewSel38 struct {
NewSel0 NewSel0
} }
func (e *NewSel39) DamageAdd(t *info.DamageZone) bool { func (e *NewSel38) DamageAdd(t *info.DamageZone) bool {
if e.ID().GetCatchTime() != e.Ctx().Our.CurPet[0].Info.CatchTime { if e.ID().GetCatchTime() != e.Ctx().Our.CurPet[0].Info.CatchTime {
return true return true
} }
t.Damage = t.Damage.Add(t.Damage.Mul(alpacadecimal.NewFromInt(int64(e.Args()[0].IntPart())))) t.Damage = t.Damage.Add(t.Damage.Mul(e.Args()[0].Div(alpacadecimal.NewFromInt(100))))
return true return true
} }

View File

@@ -28,6 +28,9 @@ func (e *NewSel49) Action_end_ex() bool {
if e.Ctx().SkillEntity == nil { if e.Ctx().SkillEntity == nil {
return true return true
} }
if !e.CurrentSkillHit() {
return true
}
if e.Ctx().SkillEntity.Category() == info.Category.PHYSICAL { if e.Ctx().SkillEntity.Category() == info.Category.PHYSICAL {
e.attackType = 1 e.attackType = 1

View File

@@ -13,8 +13,7 @@ type NewSel53 struct {
} }
func (e *NewSel53) TurnStart(fattack *action.SelectSkillAction, sattack *action.SelectSkillAction) { func (e *NewSel53) TurnStart(fattack *action.SelectSkillAction, sattack *action.SelectSkillAction) {
//魂印特性有不在场的情况,绑定时候将精灵和特性绑定 if !e.IsOwner() || e.Ctx().Our.CurPet[0] == nil || e.Ctx().Opp.CurPet[0] == nil {
if e.ID().GetCatchTime() != e.Ctx().Our.CurPet[0].Info.CatchTime {
return return
} }
@@ -23,12 +22,12 @@ func (e *NewSel53) TurnStart(fattack *action.SelectSkillAction, sattack *action.
healAmount := maxHP.Mul(e.Args()[0]).Div(alpacadecimal.NewFromInt(100)) healAmount := maxHP.Mul(e.Args()[0]).Div(alpacadecimal.NewFromInt(100))
// 恢复我方HP // 恢复我方HP
e.Ctx().Our.Heal(e.Ctx().Our, nil, healAmount) e.Ctx().Our.Heal(e.Ctx().Our, &action.SelectSkillAction{}, healAmount)
// 恢复敌方HP // 恢复敌方HP
oppMaxHP := e.Ctx().Opp.CurPet[0].GetMaxHP() oppMaxHP := e.Ctx().Opp.CurPet[0].GetMaxHP()
oppHealAmount := oppMaxHP.Mul(e.Args()[0]).Div(alpacadecimal.NewFromInt(100)) oppHealAmount := oppMaxHP.Mul(e.Args()[0]).Div(alpacadecimal.NewFromInt(100))
e.Ctx().Opp.Heal(e.Ctx().Opp, nil, oppHealAmount) e.Ctx().Opp.Heal(e.Ctx().Opp, &action.SelectSkillAction{}, oppHealAmount)
} }
func init() { func init() {

View File

@@ -22,6 +22,9 @@ func (e *NewSel6) Action_end_ex() bool {
if e.Ctx().SkillEntity.Category() != info.Category.PHYSICAL { if e.Ctx().SkillEntity.Category() != info.Category.PHYSICAL {
return true return true
} }
if !e.CurrentSkillHit() {
return true
}
// 3. 概率判定Args()[1]为触发概率) // 3. 概率判定Args()[1]为触发概率)
success, _, _ := e.Input.Player.Roll(int(e.Args()[1].IntPart()), 100) success, _, _ := e.Input.Player.Roll(int(e.Args()[1].IntPart()), 100)

View File

@@ -23,6 +23,9 @@ func (e *NewSel74) Action_end_ex() bool {
if e.Ctx().SkillEntity.Category() == info.Category.STATUS { if e.Ctx().SkillEntity.Category() == info.Category.STATUS {
return true return true
} }
if !e.CurrentSkillHit() {
return true
}
// 检查概率是否触发 // 检查概率是否触发
success, _, _ := e.Input.Player.Roll(int(e.Args()[0].IntPart()), 100) success, _, _ := e.Input.Player.Roll(int(e.Args()[0].IntPart()), 100)

View File

@@ -23,6 +23,9 @@ func (e *NewSel78) Action_end_ex() bool {
if e.Ctx().SkillEntity.Category() != info.Category.SPECIAL { if e.Ctx().SkillEntity.Category() != info.Category.SPECIAL {
return true return true
} }
if !e.CurrentSkillHit() {
return true
}
// 检查概率是否触发 // 检查概率是否触发
success, _, _ := e.Input.Player.Roll(int(e.Args()[1].IntPart()), 100) success, _, _ := e.Input.Player.Roll(int(e.Args()[1].IntPart()), 100)

View File

@@ -94,6 +94,11 @@ type CrossServerBanPickPetInfo struct {
MaxHp uint32 `json:"maxHp"` MaxHp uint32 `json:"maxHp"`
} }
type CrossServerBanPickTianxuanPetInfo struct {
PetID uint32 `json:"petId"`
Name string `struc:"[16]byte" json:"name"`
}
type CrossServerBanPickStartOutboundInfo struct { type CrossServerBanPickStartOutboundInfo struct {
SessionIDLen uint32 `struc:"sizeof=SessionID"` SessionIDLen uint32 `struc:"sizeof=SessionID"`
SessionID string `json:"sessionId"` SessionID string `json:"sessionId"`
@@ -106,12 +111,19 @@ type CrossServerBanPickStartOutboundInfo struct {
TimeoutSeconds uint32 `json:"timeoutSeconds"` TimeoutSeconds uint32 `json:"timeoutSeconds"`
SelectableCount uint32 `json:"selectableCount"` SelectableCount uint32 `json:"selectableCount"`
TianxuanSelectableCount uint32 `json:"tianxuanSelectableCount"`
MyPetsLen uint32 `struc:"sizeof=MyPets"` MyPetsLen uint32 `struc:"sizeof=MyPets"`
MyPets []CrossServerBanPickPetInfo `json:"myPets"` MyPets []CrossServerBanPickPetInfo `json:"myPets"`
MyTianxuanPetsLen uint32 `struc:"sizeof=MyTianxuanPets"`
MyTianxuanPets []CrossServerBanPickTianxuanPetInfo `json:"myTianxuanPets"`
OpponentPetsLen uint32 `struc:"sizeof=OpponentPets"` OpponentPetsLen uint32 `struc:"sizeof=OpponentPets"`
OpponentPets []CrossServerBanPickPetInfo `json:"opponentPets"` OpponentPets []CrossServerBanPickPetInfo `json:"opponentPets"`
OpponentTianxuanPetsLen uint32 `struc:"sizeof=OpponentTianxuanPets"`
OpponentTianxuanPets []CrossServerBanPickTianxuanPetInfo `json:"opponentTianxuanPets"`
} }
// HandleFightInviteInboundInfo 处理战斗邀请的入站消息 // HandleFightInviteInboundInfo 处理战斗邀请的入站消息

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

@@ -94,7 +94,7 @@ func (e *Effect1395) SkillHit() bool {
if e.Ctx().SkillEntity == nil || e.Ctx().SkillEntity.Category() == info.Category.STATUS { if e.Ctx().SkillEntity == nil || e.Ctx().SkillEntity.Category() == info.Category.STATUS {
return true return true
} }
if !e.Ctx().Our.FightC.IsFirst(e.Ctx().Our.Player) { if !e.Ctx().Our.Player.IsFirst() {
return true return true
} }

View File

@@ -185,7 +185,7 @@ func (e *Effect1527) SkillHit() bool {
if e.Ctx().Our == nil || e.Ctx().Our.FightC == nil { if e.Ctx().Our == nil || e.Ctx().Our.FightC == nil {
return true return true
} }
if e.Ctx().Our.FightC.IsFirst(e.Ctx().Our.Player) { if e.Ctx().Our.Player.IsFirst() {
return true return true
} }

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

@@ -444,7 +444,7 @@ func (e *Effect2278) Skill_Use() bool {
return true return true
} }
damage := marked.GetHP().Mul(alpacadecimal.NewFromInt(percent)).Div(hundred) damage := marked.GetMaxHP().Mul(alpacadecimal.NewFromInt(percent)).Div(hundred)
if damage.Cmp(alpacadecimal.Zero) > 0 { if damage.Cmp(alpacadecimal.Zero) > 0 {
e.Ctx().Opp.Damage(e.Ctx().Our, &info.DamageZone{ e.Ctx().Opp.Damage(e.Ctx().Our, &info.DamageZone{
Type: info.DamageType.Percent, Type: info.DamageType.Percent,

View File

@@ -14,17 +14,24 @@ type Effect85 struct {
// 执行时逻辑 // 执行时逻辑
// ---------------------- // ----------------------
func (e *Effect85) OnSkill() bool { func (e *Effect85) OnSkill() bool {
carrier := e.CarrierInput()
for i, v := range e.Ctx().Opp.Prop[:] { opp := e.OpponentInput()
if v > 0 { if carrier == nil || opp == nil {
e.Ctx().Our.SetProp(e.Ctx().Our, int8(i), v)
e.Ctx().Opp.SetProp(e.Ctx().Our, int8(i), 0)
}
}
return true return true
} }
e.transferPositiveProps(carrier, opp)
return true
}
// transferPositiveProps 使用显式入参执行业务逻辑,避免嵌套结算时再从 Ctx 取到漂移后的对象。
func (e *Effect85) transferPositiveProps(carrier, opp *input.Input) {
for i, v := range opp.Prop[:] {
if v > 0 {
carrier.SetProp(carrier, int8(i), v)
opp.SetProp(carrier, int8(i), 0)
}
}
}
// ---------------------- // ----------------------
// 注册所有效果 // 注册所有效果

View File

@@ -27,7 +27,7 @@ type Effect116 struct {
func (e *Effect116) Skill_Use() bool { func (e *Effect116) Skill_Use() bool {
if e.Input.FightC.IsFirst(e.Input.Player) { if e.Input.Player.IsFirst() {
e.Ctx().Our.Heal(e.Ctx().Our, &action.SelectSkillAction{}, e.Ctx().Our.SumDamage.Div(alpacadecimal.NewFromInt(5))) e.Ctx().Our.Heal(e.Ctx().Our, &action.SelectSkillAction{}, e.Ctx().Our.SumDamage.Div(alpacadecimal.NewFromInt(5)))
} }

View File

@@ -27,7 +27,7 @@ type Effect117 struct {
func (e *Effect117) OnSkill() bool { func (e *Effect117) OnSkill() bool {
if e.Input.FightC.IsFirst(e.Input.Player) { if e.Input.Player.IsFirst() {
// 概率判定 // 概率判定
ok, _, _ := e.Input.Player.Roll(e.EffectNode.SideEffectArgs[1], 100) ok, _, _ := e.Input.Player.Roll(e.EffectNode.SideEffectArgs[1], 100)
if !ok { if !ok {

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

@@ -27,7 +27,7 @@ func (e *Effect52) SkillHit_ex() bool {
return true return true
} }
if !e.Input.FightC.IsFirst(e.Ctx().Our.Player) { if !e.Ctx().Our.Player.IsFirst() {
return true return true
} }
e.Ctx().SkillEntity.SetMiss() e.Ctx().SkillEntity.SetMiss()

View File

@@ -5,6 +5,7 @@ import (
"blazing/logic/service/fight/info" "blazing/logic/service/fight/info"
"blazing/logic/service/fight/input" "blazing/logic/service/fight/input"
"blazing/logic/service/fight/node" "blazing/logic/service/fight/node"
"fmt"
) )
// Effect 724: {0}回合内受到攻击{1}%恢复1/{2}体力 // Effect 724: {0}回合内受到攻击{1}%恢复1/{2}体力
@@ -60,7 +61,7 @@ func (e *Effect726) SkillHit_ex() bool {
if skill == nil || skill.Category() == info.Category.STATUS { if skill == nil || skill.Category() == info.Category.STATUS {
return true return true
} }
if !e.Ctx().Opp.FightC.IsFirst(e.Ctx().Opp.Player) { if !e.Ctx().Opp.Player.IsFirst() {
return true return true
} }
@@ -85,6 +86,7 @@ func (e *Effect727) Action_end() bool {
return true return true
} }
fmt.Printf("[Effect727] Action_end RESET: Our.Prop %v -> LastTurnEndProp %v\n", e.Ctx().Our.Prop, e.Ctx().Our.LastTurnEndProp)
e.Ctx().Our.AttackValue.Prop = e.Ctx().Our.LastTurnEndProp e.Ctx().Our.AttackValue.Prop = e.Ctx().Our.LastTurnEndProp
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

@@ -1,7 +1,6 @@
package effect package effect
import ( import (
"blazing/logic/service/fight/action"
"blazing/logic/service/fight/input" "blazing/logic/service/fight/input"
) )
@@ -13,23 +12,72 @@ func init() {
t := &Effect91{} t := &Effect91{}
input.InitEffect(input.EffectType.Skill, 91, t) input.InitEffect(input.EffectType.Skill, 91, t)
input.InitEffect(input.EffectType.Sub, 91, &Effect91Sub{})
} }
// Effect 91: {0}回合内对手的状态变化会同时作用在自己身上 // Effect 91: {0}回合内对手的状态变化会同时作用在自己身上
type Effect91 struct { type Effect91 struct {
RoundEffectSideArg0Base RoundEffectSideArg0Base
can bool
} }
// 默认添加回合 func (e *Effect91) Skill_Use() bool {
sub := e.Ctx().Our.InitEffect(input.EffectType.Sub, 91, e.SideEffectArgs...)
func (e *Effect91) TurnStart(fattack *action.SelectSkillAction, sattack *action.SelectSkillAction) { if sub != nil {
e.Ctx().Opp.AddEffect(e.Ctx().Our, sub)
for i, v := range e.Ctx().Opp.Prop[:] { }
return true
e.Ctx().Our.SetProp(e.Ctx().Our, int8(i), v)
} }
type Effect91Sub struct {
RoundEffectSideArg0Base
}
func (e *Effect91Sub) PropBefer(_ *input.Input, prop int8, level int8) bool {
if prop < 0 || int(prop) >= len(e.Ctx().Our.Prop) {
return true
}
// 当前效果挂在“对手”身上:当对手能力变化时,将实际变化量同步给来源方。
target := e.Ctx().Our
current := target.Prop[prop]
actualDelta := calcPropActualDelta(current, level)
if actualDelta == 0 {
return true
}
owner := e.SourceInput()
if owner == nil || owner == target {
return true
}
owner.SetProp(owner, prop, actualDelta)
return true
}
func calcPropActualDelta(current, change int8) int8 {
switch {
case change < 0:
if current <= -6 {
return 0
}
next := current + change
if next < -6 {
next = -6
}
return next - current
case change > 0:
if current >= 6 {
return 0
}
next := current + change
if next > 6 {
next = 6
}
return next - current
default:
if current == 0 {
return 0
}
return -current
}
} }

View File

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

@@ -44,10 +44,10 @@ func init() {
return o.CurPet[0].Info.Hp < (o.CurPet[0].Info.MaxHp / 2) return o.CurPet[0].Info.Hp < (o.CurPet[0].Info.MaxHp / 2)
}) })
registerStatusFunc(30, func(i, o *input.Input) bool { registerStatusFunc(30, func(i, o *input.Input) bool {
return !i.FightC.IsFirst(i.Player) return !i.Player.IsFirst()
}) })
registerStatusFunc(40, func(i, o *input.Input) bool { registerStatusFunc(40, func(i, o *input.Input) bool {
return i.FightC.IsFirst(i.Player) return i.Player.IsFirst()
}) })
registerStatusFunc(64, func(i, o *input.Input) bool { registerStatusFunc(64, func(i, o *input.Input) bool {
if i.StatEffect_Exist(info.PetStatus.Burned) { if i.StatEffect_Exist(info.PetStatus.Burned) {

View File

@@ -36,58 +36,21 @@ 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
}
// 睡眠在“被攻击且未 miss”后立即解除而不是等到技能使用后节点。
func (e *StatusSleep) DamageSubEx(zone *info.DamageZone) bool {
if zone == nil || e.Ctx().SkillEntity == nil {
return true
}
if e.Ctx().SkillEntity.Category() != info.Category.STATUS {
e.Alive(false)
}
return true
}
func (e *StatusSleep) ActionStart(attacker, defender *action.SelectSkillAction) bool {
if e.Duration() <= 0 {
e.hasTriedAct = false
return true
}
e.hasTriedAct = true
return e.StatusCannotAct.ActionStart(attacker, defender)
} }
func (e *StatusSleep) Skill_Use_ex() bool { func (e *StatusSleep) Skill_Use_ex() bool {
if !e.hasTriedAct {
return true if e.Ctx().SkillEntity != nil {
} if e.Ctx().SkillEntity.AttackTime != 0 && e.Ctx().Category() != info.Category.STATUS {
if e.Duration() <= 0 && e.Ctx().SkillEntity != nil && e.Ctx().Category() != info.Category.STATUS {
e.Alive(false) e.Alive(false)
} }
e.hasTriedAct = false
return true
} }
func (e *StatusSleep) TurnEnd() { return true
e.hasTriedAct = false
e.StatusCannotAct.TurnEnd()
} }
// 持续伤害状态基类(中毒、冻伤、烧伤等) // 持续伤害状态基类(中毒、冻伤、烧伤等)
@@ -96,13 +59,13 @@ type ContinuousDamage struct {
isheal bool //是否回血 isheal bool //是否回血
} }
// 回合开始触发持续伤害,保证吃药/空过回合时也会正常结算。 // 行动开始触发持续伤害:当中招方轮到自己行动时结算。
func (e *ContinuousDamage) TurnStart(attacker, defender *action.SelectSkillAction) { func (e *ContinuousDamage) ActionStart(attacker, defender *action.SelectSkillAction) bool {
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 return true
} }
damage := e.calculateDamage() damage := e.calculateDamage()
@@ -111,7 +74,7 @@ func (e *ContinuousDamage) TurnStart(attacker, defender *action.SelectSkillActio
Damage: damage, Damage: damage,
}) })
if len(e.SideEffectArgs) == 0 { if len(e.SideEffectArgs) == 0 {
return return true
} }
// 额外效果 // 额外效果
carrier.Damage(source, &info.DamageZone{ carrier.Damage(source, &info.DamageZone{
@@ -119,11 +82,12 @@ func (e *ContinuousDamage) TurnStart(attacker, defender *action.SelectSkillActio
Damage: damage, Damage: damage,
}) })
if opp == nil || opp.CurPet[0].GetHP().IntPart() == 0 { if opp == nil || opp.CurPet[0].GetHP().IntPart() == 0 {
return return true
} }
// 给对方回血(不受回血限制影响) // 给对方回血(不受回血限制影响)
opp.Heal(carrier, nil, damage) opp.Heal(carrier, nil, damage)
return true
} }
// 计算伤害最大生命值的1/8 // 计算伤害最大生命值的1/8
@@ -299,6 +263,7 @@ func init() {
info.PetStatus.Paralysis, // 麻痹 info.PetStatus.Paralysis, // 麻痹
info.PetStatus.Fear, // 害怕 info.PetStatus.Fear, // 害怕
info.PetStatus.Petrified, // 石化 info.PetStatus.Petrified, // 石化
info.PetStatus.Tired, // 疲惫
} }
for _, status := range nonActingStatuses { for _, status := range nonActingStatuses {
effect := &StatusCannotAct{} effect := &StatusCannotAct{}
@@ -306,10 +271,6 @@ 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

@@ -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,11 +7,12 @@ 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"
"github.com/alpacahq/alpacadecimal" "github.com/alpacahq/alpacadecimal"
"github.com/barkimedes/go-deepcopy"
"github.com/gogf/gf/v2/util/grand" "github.com/gogf/gf/v2/util/grand"
) )
@@ -115,25 +116,24 @@ func IsNil(x interface{}) bool {
return rv.Kind() == reflect.Ptr && rv.IsNil() return rv.Kind() == reflect.Ptr && rv.IsNil()
} }
// copySkill 复制技能实体
func (f *FightC) copySkill(action *action.SelectSkillAction) *info.SkillEntity {
if action == nil {
return nil
}
if action.SkillEntity == nil {
return nil
}
originalSkill, _ := deepcopy.Anything(action.SkillEntity) //备份技能
originalSkill.(*info.SkillEntity).Accuracy = action.SkillEntity.Accuracy //拷贝后命中丢失
return originalSkill.(*info.SkillEntity)
}
func (f *FightC) getSkillParticipants(skillAction *action.SelectSkillAction) (*input.Input, *input.Input) { func (f *FightC) getSkillParticipants(skillAction *action.SelectSkillAction) (*input.Input, *input.Input) {
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 上下文。
@@ -232,6 +232,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) {
@@ -311,50 +320,63 @@ func (f *FightC) enterturn(firstAttack, secondAttack *action.SelectSkillAction)
} }
} }
skipActionStage := firstAttack == nil && secondAttack == nil
var attacker, defender *input.Input
f.TrueFirst = f.First f.TrueFirst = f.First
//开始回合操作。若双方本回合都未出手,则只走完整回合流程,不进入动作阶段。 // 开始回合操作。无论本回合是否真的出手,双方都需要经过自己的“行动前”阶段。
for i := 0; !skipActionStage && i < 2; i++ { for i := 0; i < 2; i++ {
var originalSkill *info.SkillEntity //原始技能 currentAction := firstAttack
var currentSkill *info.SkillEntity //当前技能 attacker := f.First
var currentAction *action.SelectSkillAction defender := f.Second
if i == 0 { if i == 0 {
currentAction = firstAttack
if currentAction == nil {
continue
}
attacker, defender = f.getSkillParticipants(firstAttack)
originalSkill = f.copySkill(firstAttack)
// 先手阶段,先修复后手效果 // 先手阶段,先修复后手效果
if f.Second != nil {
f.Second.RecoverEffect() f.Second.RecoverEffect()
}
} else { } else {
currentAction = secondAttack if shouldSkipSecondAction(f.First, f.Second) {
if currentAction == nil { secondAttack = nil
continue continue
} }
attacker, defender = f.getSkillParticipants(secondAttack) currentAction = secondAttack
originalSkill = f.copySkill(secondAttack) attacker = f.Second
defender = f.First
// 取消后手历史效果 // 取消后手历史效果
if f.Second != nil {
f.Second.ReactvieEffect() f.Second.ReactvieEffect()
} }
if attacker == nil {
attacker = f.First
}
if defender == nil {
defender = f.Second
} }
currentSkill = originalSkill if currentAction != nil {
if actAttacker, actDefender := f.getSkillParticipants(currentAction); actAttacker != nil {
attacker = actAttacker
if actDefender != nil {
defender = actDefender
}
}
}
if attacker == nil {
continue
}
var currentSkill *info.SkillEntity
if currentAction != nil {
currentSkill = currentAction.SkillEntity
}
if defender != nil {
defender.ExecWithOpponent(attacker, func(effect input.Effect) bool { //这个是能否使用技能 defender.ExecWithOpponent(attacker, func(effect input.Effect) bool { //这个是能否使用技能
f.setEffectSkillContext(effect, currentSkill, defender) f.setEffectSkillContext(effect, currentSkill, defender)
return effect.ActionStartEx(firstAttack, secondAttack) return effect.ActionStartEx(firstAttack, secondAttack)
}) })
}
canUseSkill := attacker.ExecWithOpponent(defender, func(effect input.Effect) bool { //这个是能否使用技能 canUseSkill := attacker.ExecWithOpponent(defender, func(effect input.Effect) bool { //这个是能否使用技能
f.setEffectSkillContext(effect, currentSkill, defender) f.setEffectSkillContext(effect, currentSkill, defender)
return effect.ActionStart(firstAttack, secondAttack) return effect.ActionStart(firstAttack, secondAttack)
}) })
if currentAction == nil {
continue
}
attackerPet := attacker.CurrentPet() attackerPet := attacker.CurrentPet()
defenderPet := defender.CurrentPet() defenderPet := defender.CurrentPet()
canUse := canUseSkill && action.CanUse(currentSkill) && attacker != nil && attackerPet != nil && attackerPet.Info.Hp > 0 canUse := canUseSkill && action.CanUse(currentSkill) && attacker != nil && attackerPet != nil && attackerPet.Info.Hp > 0
@@ -378,7 +400,6 @@ func (f *FightC) enterturn(firstAttack, secondAttack *action.SelectSkillAction)
effect.IsFirst(true) effect.IsFirst(true)
} }
f.processSkillAttack(attacker, defender, currentSkill) f.processSkillAttack(attacker, defender, currentSkill)
currentSkill = originalSkill //还原技能
_, skill, ok := utils.FindWithIndex(attackerPet.Info.SkillList, func(item model.SkillInfo) bool { _, skill, ok := utils.FindWithIndex(attackerPet.Info.SkillList, func(item model.SkillInfo) bool {
return item.ID == currentSkill.Info.ID return item.ID == currentSkill.Info.ID

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
@@ -492,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
@@ -580,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

@@ -145,8 +145,8 @@ func (f *FightC) bindInputFightContext(inputGroups ...[]*input.Input) {
if fighter == nil { if fighter == nil {
continue continue
} }
fighter.FightC = f
if fighter.Player != nil { if fighter.Player != nil {
fighter.FightC = common.BindFight(f, fighter.Player)
fighter.Player.SetFightC(f) fighter.Player.SetFightC(f)
} }
} }

View File

@@ -45,14 +45,14 @@ func (our *Input) GetAction() {
return return
} }
selfPet := our.FightC.GetCurrPETAt(our.Player, actorIndex) selfPet := our.Player.GetCurrPETAt(actorIndex)
if selfPet == nil { if selfPet == nil {
return return
} }
if selfPet.Info.Hp <= 0 { if selfPet.Info.Hp <= 0 {
for _, v := range our.AllPet { for _, v := range our.AllPet {
if v.Info.Hp > 0 { if v.Info.Hp > 0 {
our.FightC.ChangePetAt(our.Player, v.Info.CatchTime, actorIndex) our.Player.ChangePetAt(v.Info.CatchTime, actorIndex)
return return
} }
} }
@@ -106,9 +106,9 @@ func (our *Input) GetAction() {
} }
if usedskill != nil { if usedskill != nil {
our.FightC.UseSkillAt(our.Player, uint32(usedskill.XML.ID), actorIndex, targetIndex) our.Player.UseSkillAt(uint32(usedskill.XML.ID), actorIndex, targetIndex)
} else { } else {
our.FightC.UseSkillAt(our.Player, 0, actorIndex, targetIndex) our.Player.UseSkillAt(0, actorIndex, targetIndex)
} }
} }
@@ -132,7 +132,7 @@ func buildBossHookActionContext(our, opponent *Input, hookAction bool) *configmo
overInfo := our.FightC.GetOverInfo() overInfo := our.FightC.GetOverInfo()
ctx.Round = overInfo.Round ctx.Round = overInfo.Round
ctx.IsFirst = our.FightC.IsFirst(our.Player) ctx.IsFirst = our.Player.IsFirst()
if selfPet := our.CurrentPet(); selfPet != nil { if selfPet := our.CurrentPet(); selfPet != nil {
ctx.Our = &configmodel.BossHookPetContext{ ctx.Our = &configmodel.BossHookPetContext{
@@ -211,10 +211,10 @@ func applyBossScriptAction(our *Input, ctx *configmodel.BossHookActionContext, a
case "", "auto": case "", "auto":
return false return false
case "skill", "use_skill", "useskill": case "skill", "use_skill", "useskill":
our.FightC.UseSkillAt(our.Player, ctx.SkillID, actorIndex, targetIndex) our.Player.UseSkillAt(ctx.SkillID, actorIndex, targetIndex)
return true return true
case "switch", "change_pet", "changepet": case "switch", "change_pet", "changepet":
our.FightC.ChangePetAt(our.Player, ctx.CatchTime, actorIndex) our.Player.ChangePetAt(ctx.CatchTime, actorIndex)
return true return true
default: default:
return false return false

View File

@@ -27,11 +27,11 @@ func (our *Input) CalculateCrit(opp *Input, skill *info.SkillEntity) {
CritRate := utils.Max(skill.XML.CritRate, 1) CritRate := utils.Max(skill.XML.CritRate, 1)
//CritAtkFirst: 先出手时必定致命一击; 默认: 0 //CritAtkFirst: 先出手时必定致命一击; 默认: 0
if skill.XML.CritAtkFirst != 0 && our.FightC.IsFirst(our.Player) { if skill.XML.CritAtkFirst != 0 && our.Player.IsFirst() {
CritRate = 16 CritRate = 16
} }
//CritAtkSecond: 后出手时必定致命一击; 默认: 0 //CritAtkSecond: 后出手时必定致命一击; 默认: 0
if skill.XML.CritAtkSecond != 0 && !our.FightC.IsFirst(our.Player) { if skill.XML.CritAtkSecond != 0 && !our.Player.IsFirst() {
CritRate = 16 CritRate = 16
} }
// CritSelfHalfHp: 自身体力低于一半时必定致命一击; 默认: 0 // CritSelfHalfHp: 自身体力低于一半时必定致命一击; 默认: 0
@@ -107,6 +107,10 @@ func (our *Input) HealPP(value int) {
} }
currentPet.Info.HealPP(value) currentPet.Info.HealPP(value)
if our.AttackValue != nil {
our.AttackValue.SkillList = append(our.AttackValue.SkillList[:0], currentPet.Info.SkillList...)
our.AttackValue.SkillListLen = uint32(len(our.AttackValue.SkillList))
}
} }
func (our *Input) DelPP(value int) { func (our *Input) DelPP(value int) {
@@ -123,6 +127,10 @@ func (our *Input) DelPP(value int) {
} }
} }
if our.AttackValue != nil {
our.AttackValue.SkillList = append(our.AttackValue.SkillList[:0], currentPet.Info.SkillList...)
our.AttackValue.SkillListLen = uint32(len(our.AttackValue.SkillList))
}
} }
@@ -298,7 +306,7 @@ func (our *Input) CalculatePower(deftype *Input, skill *info.SkillEntity) alpaca
} }
if skill.XML.PwrBindDv == 2 { if skill.XML.PwrBindDv == 2 {
skill.XML.Power = int(ourPet.Info.Hp/3 + ourPet.Info.Dv) skill.XML.Power = int(ourPet.Info.MaxHp/3 + ourPet.Info.Dv)
} }
} }
@@ -309,10 +317,7 @@ func (our *Input) CalculatePower(deftype *Input, skill *info.SkillEntity) alpaca
} }
} }
if skill.XML.DmgBindHpDv != 0 {
skill.XML.Power = int(ourPet.Info.Hp/2 + ourPet.Info.Dv)
}
// 5. 基础伤害公式:等级因子 * 威力因子 * 攻击 / 防御 / 50 + 2 // 5. 基础伤害公式:等级因子 * 威力因子 * 攻击 / 防御 / 50 + 2
baseDamage := levelFactor. baseDamage := levelFactor.
Div(alpacadecimal.NewFromInt(50)). Div(alpacadecimal.NewFromInt(50)).
@@ -333,6 +338,10 @@ func (our *Input) CalculatePower(deftype *Input, skill *info.SkillEntity) alpaca
Mul(skill.Criticalrandom()) //随机波动 Mul(skill.Criticalrandom()) //随机波动
//println(baseDamage.IntPart(), damage.IntPart(), attackDec.IntPart(), defenseDec.IntPart(), "技能伤害") //println(baseDamage.IntPart(), damage.IntPart(), attackDec.IntPart(), defenseDec.IntPart(), "技能伤害")
if skill.XML.DmgBindHpDv != 0 {
damage = alpacadecimal.NewFromInt(int64(ourPet.Info.Hp/2 + ourPet.Info.Dv))
}
return damage return damage
} }

View File

@@ -62,8 +62,8 @@ type Input struct {
//First bool //是否先手 //First bool //是否先手
} }
func NewInput(c common.FightI, p common.PlayerI) *Input { func NewInput(c common.FightControllerI, p common.PlayerI) *Input {
ret := &Input{FightC: c, Player: p} ret := &Input{FightC: common.BindFight(c, p), Player: p}
ret.Effects = make([]Effect, 0) ret.Effects = make([]Effect, 0)
ret.CurPet = make([]*info.BattlePetEntity, 0) ret.CurPet = make([]*info.BattlePetEntity, 0)
ret.effectsByBase = make(map[int64][]Effect) ret.effectsByBase = make(map[int64][]Effect)
@@ -284,7 +284,9 @@ func (our *Input) GenInfo() {
} }
our.RemainHp = int32(currentPet.Info.Hp) our.RemainHp = int32(currentPet.Info.Hp)
our.SkillList = currentPet.Info.SkillList our.MaxHp = currentPet.Info.MaxHp
our.SkillList = append(our.SkillList[:0], currentPet.Info.SkillList...)
our.SkillListLen = uint32(len(our.SkillList))
// f.Second.SkillList = f.Second.CurPet.Info.SkillList // f.Second.SkillList = f.Second.CurPet.Info.SkillList
// f.Second.RemainHp = int32(f.Second.CurPet.Info.Hp) // f.Second.RemainHp = int32(f.Second.CurPet.Info.Hp)

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