Compare commits
72 Commits
a905954b5c
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2b92baf530 | ||
|
|
819d5f667b | ||
|
|
de6c700bb3 | ||
|
|
3232efd05a | ||
|
|
0c79fee8af | ||
|
|
3d77e146e9 | ||
|
|
a43a25c610 | ||
|
|
3cfde577eb | ||
|
|
85f9c02ced | ||
|
|
9f7fd83626 | ||
|
|
ee8b0a2182 | ||
|
|
6e95e014fa | ||
|
|
61a135b3a7 | ||
|
|
5a81534e84 | ||
|
|
523d835ac0 | ||
|
|
5a7e20efec | ||
|
|
5f47bf0589 | ||
|
|
a58ef20fab | ||
|
|
3999f34f77 | ||
|
|
6f51a2e349 | ||
|
|
de755f8fd0 | ||
|
|
803aa71771 | ||
|
|
4a77066d08 | ||
|
|
c9b5f8569f | ||
|
|
ddbfe91d8b | ||
|
|
74ac6ce940 | ||
|
|
43b0bc2dec | ||
|
|
b953e7831a | ||
|
|
62d93f65e7 | ||
|
|
7dfa9c297e | ||
|
|
f95fd49efd | ||
|
|
ce1a2a3588 | ||
|
|
3739c2a6f9 | ||
|
|
eca7dd86e1 | ||
|
|
e161e3626f | ||
|
|
e1a994ba11 | ||
|
|
82bb99d141 | ||
|
|
f9543a5156 | ||
|
|
174830731c | ||
|
|
3a7f593105 | ||
|
|
f6aa0c3339 | ||
|
|
ecc483a11a | ||
|
|
97c8231b44 | ||
|
|
5f5634d999 | ||
|
|
5bfdb5c32b | ||
|
|
90f1447d48 | ||
|
|
ee3f25438f | ||
|
|
2d8969bed2 | ||
|
|
fa5d50955d | ||
|
|
6574450489 | ||
|
|
0daeb70900 | ||
|
|
061e4f0c51 | ||
|
|
5c76aa7079 | ||
|
|
b327398448 | ||
|
|
d0abb08d5b | ||
|
|
d2cd601802 | ||
|
|
487ee0e726 | ||
|
|
3b35789b47 | ||
|
|
28b6386963 | ||
|
|
1ca0ff344e | ||
|
|
9825944efc | ||
|
|
ca96be3905 | ||
|
|
4b89588c22 | ||
|
|
0051ac0be8 | ||
|
|
918cdeac0e | ||
|
|
13244313f1 | ||
|
|
4ea9864833 | ||
|
|
77057e01b6 | ||
|
|
f030b61645 | ||
|
|
5a44154d30 | ||
|
|
3ee1283a2c | ||
|
|
c3da3162ee |
3
.cnb.yml
3
.cnb.yml
@@ -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
|
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -47,3 +47,4 @@ public/login-linux-amd64
|
|||||||
public/login-login-linux-amd64
|
public/login-login-linux-amd64
|
||||||
public/logic_linux-amd64_1
|
public/logic_linux-amd64_1
|
||||||
.cache/**
|
.cache/**
|
||||||
|
.agents/**
|
||||||
|
|||||||
@@ -18,11 +18,11 @@ ENV GOMODCACHE=/workspace/.cache/gomod
|
|||||||
# ==========================================
|
# ==========================================
|
||||||
# 2. Codex 配置 (更换时修改这里,重新 build)
|
# 2. Codex 配置 (更换时修改这里,重新 build)
|
||||||
# ==========================================
|
# ==========================================
|
||||||
ENV CODEX_BASE_URL="https://www.jnm.lol/v1"
|
ENV CODEX_BASE_URL="https://api.jucode.cn/v1"
|
||||||
|
|
||||||
ENV CODEX_MODEL="gpt-5.4"
|
ENV CODEX_MODEL="gpt-5.4"
|
||||||
|
|
||||||
ENV OPENAI_API_KEY="pk_live__NQFz14yuraSLUY9mXCuQ2Swh1NM9XV4uVOB1qukipw"
|
ENV OPENAI_API_KEY="sk-E0ZZIFNnD0RkhMC9pT2AGMutz9vNy2VLNrgyyobT5voa81pQ"
|
||||||
|
|
||||||
# ==========================================
|
# ==========================================
|
||||||
# 3. 安装系统依赖、Golang、Code-server
|
# 3. 安装系统依赖、Golang、Code-server
|
||||||
|
|||||||
11
.ide/help.md
11
.ide/help.md
@@ -1,12 +1,7 @@
|
|||||||
青氧,十九禁给
|
|
||||||
https://api.aibh.site/console 张晟 2922919493Zs.
|
|
||||||
RUN curl -fsSL https://oss.itbzzb.cn/setup-codex.sh | \
|
|
||||||
YES=1 bash -s -- --base-url https://api.aibh.site \
|
|
||||||
--api-key sk-foAHgsJtmanACECtBlFYZE2z4LkwBboEOYETO3ZdWvCxdmNr \
|
|
||||||
--mirror auto
|
|
||||||
|
|
||||||
|
|
||||||
https://api.gemai.cc/console/token 免费给部分额度 ,还有100块
|
https://api.jucode.cn/
|
||||||
|
fastai.fast 使用谷歌邮箱https://linshiguge.com/白嫖
|
||||||
https://zread.ai/tawer-blog/lmarena-2api/1-overview GLM web2 pai
|
https://zread.ai/tawer-blog/lmarena-2api/1-overview GLM web2 pai
|
||||||
|
|
||||||
https://crazyrouter.com/console 模型最便宜,看看能不能1:10
|
https://crazyrouter.com/console 模型最便宜,看看能不能1:10
|
||||||
@@ -14,7 +9,7 @@ https://crazyrouter.com/console 模型最便宜,看看能不能1:10
|
|||||||
https://agentrouter.org/pricing 签到给,有175
|
https://agentrouter.org/pricing 签到给,有175
|
||||||
|
|
||||||
|
|
||||||
kuaipao.ai 充了十块 cjf19970621 cjf19970621
|
|
||||||
|
|
||||||
充了十块
|
充了十块
|
||||||
使用网址:https://www.jnm.lol
|
使用网址:https://www.jnm.lol
|
||||||
|
|||||||
2
.vscode/launch.json
vendored
2
.vscode/launch.json
vendored
@@ -29,7 +29,7 @@
|
|||||||
"request": "launch",
|
"request": "launch",
|
||||||
"mode": "auto",
|
"mode": "auto",
|
||||||
"cwd": "${workspaceFolder}",
|
"cwd": "${workspaceFolder}",
|
||||||
"args": ["-id=2"],
|
"args": ["-id=99"],
|
||||||
|
|
||||||
"program": "${workspaceFolder}/logic"
|
"program": "${workspaceFolder}/logic"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ skip_clone: true
|
|||||||
steps:
|
steps:
|
||||||
# ========== 1. 替代clone:拉取代码(核心依赖) ==========
|
# ========== 1. 替代clone:拉取代码(核心依赖) ==========
|
||||||
prepare:
|
prepare:
|
||||||
image: alpine/git
|
image: docker.1ms.run/alpine/git
|
||||||
environment:
|
environment:
|
||||||
# WOODPECKER_SSH_KEY:
|
# WOODPECKER_SSH_KEY:
|
||||||
# from_secret: WOODPECKER_SSH_KEY
|
# from_secret: WOODPECKER_SSH_KEY
|
||||||
@@ -70,7 +70,7 @@ steps:
|
|||||||
|
|
||||||
# ========== 4. 编译Logic服务(完全参考GitHub Actions编译配置) ==========
|
# ========== 4. 编译Logic服务(完全参考GitHub Actions编译配置) ==========
|
||||||
build_logic:
|
build_logic:
|
||||||
image: golang:1.25
|
image: docker.1ms.run/golang:1.25
|
||||||
depends_on: [prepare]
|
depends_on: [prepare]
|
||||||
environment:
|
environment:
|
||||||
CGO_ENABLED: 0
|
CGO_ENABLED: 0
|
||||||
@@ -142,23 +142,23 @@ steps:
|
|||||||
|
|
||||||
# ========== 6. SCP推送产物(依赖编译+配置解析) ==========
|
# ========== 6. SCP推送产物(依赖编译+配置解析) ==========
|
||||||
scp-exe-to-servers: # 与fetch-deploy-config同级,缩进2个空格
|
scp-exe-to-servers: # 与fetch-deploy-config同级,缩进2个空格
|
||||||
image: appleboy/drone-scp:1.6.2 # 子元素,缩进4个空格
|
image: docker.1ms.run/appleboy/drone-scp:1.6.2 # 子元素,缩进4个空格
|
||||||
settings: # 子元素,缩进4个空格
|
settings: # 子元素,缩进4个空格
|
||||||
host: &ssh_host 2697v22.mc5173.cn
|
host: &ssh_host 43.248.3.21
|
||||||
port: &ssh_port 16493
|
port: &ssh_port 22
|
||||||
username: &ssh_user root
|
username: &ssh_user root
|
||||||
password: &ssh_pass xIy9PQcBF96C
|
password: &ssh_pass KQv7yzna7BDukK
|
||||||
|
|
||||||
source:
|
source:
|
||||||
- blazing/build/**
|
- blazing/build/**
|
||||||
target: /opt/blazing/
|
target: /ext/blazing/
|
||||||
strip_components: 1 # 统一缩进6个空格
|
strip_components: 1 # 统一缩进6个空格
|
||||||
skip_verify: true # 统一缩进6个空格
|
skip_verify: true # 统一缩进6个空格
|
||||||
timeout: 30s # 统一缩进6个空格
|
timeout: 30s # 统一缩进6个空格
|
||||||
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: docker.1ms.run/appleboy/drone-ssh:1.6.2
|
||||||
depends_on: [scp-exe-to-servers]
|
depends_on: [scp-exe-to-servers]
|
||||||
settings: # 子元素,缩进4个空格
|
settings: # 子元素,缩进4个空格
|
||||||
host: *ssh_host
|
host: *ssh_host
|
||||||
@@ -167,7 +167,7 @@ steps:
|
|||||||
password: *ssh_pass
|
password: *ssh_pass
|
||||||
script:
|
script:
|
||||||
- |
|
- |
|
||||||
cd /opt/blazing/build
|
cd /ext/blazing/build
|
||||||
ls -t login_* 2>/dev/null | head -1
|
ls -t login_* 2>/dev/null | head -1
|
||||||
BIN_NAME=$(ls -t login_* 2>/dev/null | head -1)
|
BIN_NAME=$(ls -t login_* 2>/dev/null | head -1)
|
||||||
echo "BIN_NAME: $BIN_NAME"
|
echo "BIN_NAME: $BIN_NAME"
|
||||||
@@ -201,9 +201,9 @@ steps:
|
|||||||
# 移动logic产物到public目录
|
# 移动logic产物到public目录
|
||||||
LOGIC_BIN=$(ls -t logic_* 2>/dev/null | head -1)
|
LOGIC_BIN=$(ls -t logic_* 2>/dev/null | head -1)
|
||||||
if [ -n "$LOGIC_BIN" ]; then
|
if [ -n "$LOGIC_BIN" ]; then
|
||||||
mkdir -p /opt/blazing/build/public
|
mkdir -p /ext/blazing/build/public
|
||||||
mv $LOGIC_BIN /opt/blazing/build/public/
|
mv $LOGIC_BIN /ext/blazing/build/public/
|
||||||
echo "✅ Logic产物已移动到 /opt/blazing/build/public/ | 文件: $(basename $LOGIC_BIN)"
|
echo "✅ Logic产物已移动到 /ext/blazing/build/public/ | 文件: $(basename $LOGIC_BIN)"
|
||||||
else
|
else
|
||||||
echo "⚠️ 未找到Logic产物"
|
echo "⚠️ 未找到Logic产物"
|
||||||
fi
|
fi
|
||||||
|
|||||||
56
common/contrib/drivers/pgsql/cmd/codexcheck/main.go
Normal file
56
common/contrib/drivers/pgsql/cmd/codexcheck/main.go
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
_ "github.com/lib/pq"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
const dsn = "user=user_YrK4j7 password=password_jSDm76 host=43.248.3.21 port=5432 dbname=bl sslmode=disable timezone=Asia/Shanghai"
|
||||||
|
|
||||||
|
db, err := sql.Open("postgres", dsn)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
var (
|
||||||
|
id int64
|
||||||
|
cdkCode string
|
||||||
|
cdkType int64
|
||||||
|
exchangeRemainCount int64
|
||||||
|
bindUserID int64
|
||||||
|
validEndTime sql.NullTime
|
||||||
|
remark sql.NullString
|
||||||
|
)
|
||||||
|
|
||||||
|
err = db.QueryRow(`
|
||||||
|
select id, cdk_code, type, exchange_remain_count, bind_user_id, valid_end_time, remark
|
||||||
|
from config_gift_cdk
|
||||||
|
where cdk_code = $1
|
||||||
|
`, "nrTbdXFBhKkaTdDk").Scan(
|
||||||
|
&id,
|
||||||
|
&cdkCode,
|
||||||
|
&cdkType,
|
||||||
|
&exchangeRemainCount,
|
||||||
|
&bindUserID,
|
||||||
|
&validEndTime,
|
||||||
|
&remark,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("id=%d\ncdk_code=%s\ntype=%d\nexchange_remain_count=%d\nbind_user_id=%d\nvalid_end_time=%v\nremark=%q\n",
|
||||||
|
id,
|
||||||
|
cdkCode,
|
||||||
|
cdkType,
|
||||||
|
exchangeRemainCount,
|
||||||
|
bindUserID,
|
||||||
|
validEndTime.Time,
|
||||||
|
remark.String,
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -22,8 +22,14 @@ var ctx = context.TODO()
|
|||||||
type Cmd struct {
|
type Cmd struct {
|
||||||
Func reflect.Value //方法函数
|
Func reflect.Value //方法函数
|
||||||
Req reflect.Type //请求体
|
Req reflect.Type //请求体
|
||||||
|
// HeaderFieldIndex 是请求结构体中 TomeeHeader 字段的索引路径。
|
||||||
|
HeaderFieldIndex []int
|
||||||
|
// UseConn 标记第二个参数是否为 gnet.Conn。
|
||||||
|
UseConn bool
|
||||||
// 新增:预缓存的req创建函数(返回结构体指针)
|
// 新增:预缓存的req创建函数(返回结构体指针)
|
||||||
NewReqFunc func() interface{}
|
NewReqFunc func() interface{}
|
||||||
|
// NewReqValue 返回请求结构体指针的 reflect.Value,避免重复构造类型信息。
|
||||||
|
NewReqValue func() reflect.Value
|
||||||
//Res reflect.Value //返回体
|
//Res reflect.Value //返回体
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package cool
|
|||||||
import (
|
import (
|
||||||
_ "blazing/contrib/drivers/pgsql"
|
_ "blazing/contrib/drivers/pgsql"
|
||||||
"blazing/cool/cooldb"
|
"blazing/cool/cooldb"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"github.com/gogf/gf/v2/encoding/gjson"
|
"github.com/gogf/gf/v2/encoding/gjson"
|
||||||
"github.com/gogf/gf/v2/frame/g"
|
"github.com/gogf/gf/v2/frame/g"
|
||||||
@@ -10,6 +11,11 @@ import (
|
|||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
autoMigrateMu sync.Mutex
|
||||||
|
autoMigrateModels []IModel
|
||||||
|
)
|
||||||
|
|
||||||
// 初始化数据库连接供gorm使用
|
// 初始化数据库连接供gorm使用
|
||||||
func InitDB(group string) (*gorm.DB, error) {
|
func InitDB(group string) (*gorm.DB, error) {
|
||||||
// var ctx context.Context
|
// var ctx context.Context
|
||||||
@@ -54,9 +60,33 @@ func getDBbyModel(model IModel) *gorm.DB {
|
|||||||
|
|
||||||
// 根据entity结构体创建表
|
// 根据entity结构体创建表
|
||||||
func CreateTable(model IModel) error {
|
func CreateTable(model IModel) error {
|
||||||
if Config.AutoMigrate {
|
autoMigrateMu.Lock()
|
||||||
|
autoMigrateModels = append(autoMigrateModels, model)
|
||||||
|
autoMigrateMu.Unlock()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RunAutoMigrate 显式执行已注册模型的建表/迁移。
|
||||||
|
func RunAutoMigrate() error {
|
||||||
|
if !Config.AutoMigrate {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
autoMigrateMu.Lock()
|
||||||
|
models := append([]IModel(nil), autoMigrateModels...)
|
||||||
|
autoMigrateMu.Unlock()
|
||||||
|
|
||||||
|
seen := make(map[string]struct{}, len(models))
|
||||||
|
for _, model := range models {
|
||||||
|
key := model.GroupName() + ":" + model.TableName()
|
||||||
|
if _, ok := seen[key]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[key] = struct{}{}
|
||||||
|
|
||||||
db := getDBbyModel(model)
|
db := getDBbyModel(model)
|
||||||
return db.AutoMigrate(model)
|
if err := db.AutoMigrate(model); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,15 +42,15 @@ const (
|
|||||||
maxMatrixSize = 227 // 矩阵维度(覆盖最大属性ID 226)
|
maxMatrixSize = 227 // 矩阵维度(覆盖最大属性ID 226)
|
||||||
)
|
)
|
||||||
|
|
||||||
// 合法单属性ID集合(快速校验)
|
// 合法单属性ID集合(按ID直接索引,避免运行时 map 查找)
|
||||||
var validSingleElementIDs = map[int]bool{
|
var validSingleElementIDs = [maxMatrixSize]bool{
|
||||||
1: true, 2: true, 3: true, 4: true, 5: true, 6: true, 7: true, 8: true, 9: true, 10: true,
|
1: true, 2: true, 3: true, 4: true, 5: true, 6: true, 7: true, 8: true, 9: true, 10: true,
|
||||||
11: true, 12: true, 13: true, 14: true, 15: true, 16: true, 17: true, 18: true, 19: true, 20: true,
|
11: true, 12: true, 13: true, 14: true, 15: true, 16: true, 17: true, 18: true, 19: true, 20: true,
|
||||||
221: true, 222: true, 223: true, 224: true, 225: true, 226: true,
|
221: true, 222: true, 223: true, 224: true, 225: true, 226: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
// 元素名称映射(全属性对应,便于日志输出)
|
// 元素名称映射(按ID直接索引,便于日志输出)
|
||||||
var elementNameMap = map[ElementType]string{
|
var elementNameMap = [maxMatrixSize]string{
|
||||||
ElementTypeGrass: "GRASS",
|
ElementTypeGrass: "GRASS",
|
||||||
ElementTypeWater: "WATER",
|
ElementTypeWater: "WATER",
|
||||||
ElementTypeFire: "FIRE",
|
ElementTypeFire: "FIRE",
|
||||||
@@ -198,46 +198,55 @@ type ElementCombination struct {
|
|||||||
ID int // 组合唯一ID
|
ID int // 组合唯一ID
|
||||||
}
|
}
|
||||||
|
|
||||||
// 全局预加载资源(程序启动时init初始化,运行时直接使用)
|
// 全局预加载资源(程序启动时初始化,运行时只读)
|
||||||
var (
|
var (
|
||||||
// 元素组合池:key=组合ID,value=组合实例(预加载所有合法组合)
|
validCombinationIDs [maxMatrixSize]bool
|
||||||
elementCombinationPool = make(map[int]*ElementCombination, 150) // 128双+26单=154,预分配足够容量
|
elementCombinationPool [maxMatrixSize]ElementCombination
|
||||||
// 单属性克制矩阵(预初始化所有特殊克制关系,默认1.0)
|
dualElementSecondaryPool [maxMatrixSize]ElementType
|
||||||
matrix [maxMatrixSize][maxMatrixSize]float64
|
matrix [maxMatrixSize][maxMatrixSize]float64
|
||||||
|
Calculator *ElementCalculator
|
||||||
)
|
)
|
||||||
|
|
||||||
// init 预加载所有资源(程序启动时执行一次,无并发问题)
|
// init 预加载所有资源(程序启动时执行一次,无并发问题)
|
||||||
func init() {
|
func init() {
|
||||||
// 1. 初始化单属性克制矩阵
|
|
||||||
initFullTableMatrix()
|
initFullTableMatrix()
|
||||||
|
initElementCombinationPool()
|
||||||
|
Calculator = NewElementCalculator()
|
||||||
|
}
|
||||||
|
|
||||||
// 2. 预加载所有单属性组合
|
func initElementCombinationPool() {
|
||||||
for id := range validSingleElementIDs {
|
for id, valid := range validSingleElementIDs {
|
||||||
combo := &ElementCombination{
|
if !valid {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
validCombinationIDs[id] = true
|
||||||
|
elementCombinationPool[id] = ElementCombination{
|
||||||
Primary: ElementType(id),
|
Primary: ElementType(id),
|
||||||
Secondary: nil,
|
|
||||||
ID: id,
|
ID: id,
|
||||||
}
|
}
|
||||||
elementCombinationPool[id] = combo
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 预加载所有双属性组合
|
|
||||||
for dualID, atts := range dualElementMap {
|
for dualID, atts := range dualElementMap {
|
||||||
primaryID, secondaryID := atts[0], atts[1]
|
primaryID, secondaryID := atts[0], atts[1]
|
||||||
// 按ID升序排序,保证组合一致性
|
|
||||||
primary, secondary := ElementType(primaryID), ElementType(secondaryID)
|
primary, secondary := ElementType(primaryID), ElementType(secondaryID)
|
||||||
if primary > secondary {
|
if primary > secondary {
|
||||||
primary, secondary = secondary, primary
|
primary, secondary = secondary, primary
|
||||||
}
|
}
|
||||||
combo := &ElementCombination{
|
|
||||||
|
dualElementSecondaryPool[dualID] = secondary
|
||||||
|
validCombinationIDs[dualID] = true
|
||||||
|
elementCombinationPool[dualID] = ElementCombination{
|
||||||
Primary: primary,
|
Primary: primary,
|
||||||
Secondary: &secondary,
|
Secondary: &dualElementSecondaryPool[dualID],
|
||||||
ID: dualID,
|
ID: dualID,
|
||||||
}
|
}
|
||||||
elementCombinationPool[dualID] = combo
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isValidCombinationID(id int) bool {
|
||||||
|
return id > 0 && id < maxMatrixSize && validCombinationIDs[id]
|
||||||
|
}
|
||||||
|
|
||||||
// IsDual 判断是否为双属性
|
// IsDual 判断是否为双属性
|
||||||
func (ec *ElementCombination) IsDual() bool {
|
func (ec *ElementCombination) IsDual() bool {
|
||||||
return ec.Secondary != nil
|
return ec.Secondary != nil
|
||||||
@@ -245,84 +254,82 @@ func (ec *ElementCombination) IsDual() bool {
|
|||||||
|
|
||||||
// Elements 获取所有属性列表
|
// Elements 获取所有属性列表
|
||||||
func (ec *ElementCombination) Elements() []ElementType {
|
func (ec *ElementCombination) Elements() []ElementType {
|
||||||
if ec.IsDual() {
|
if secondary := ec.Secondary; secondary != nil {
|
||||||
return []ElementType{ec.Primary, *ec.Secondary}
|
return []ElementType{ec.Primary, *secondary}
|
||||||
}
|
}
|
||||||
return []ElementType{ec.Primary}
|
return []ElementType{ec.Primary}
|
||||||
}
|
}
|
||||||
|
|
||||||
// String 友好格式化输出
|
// String 友好格式化输出
|
||||||
func (ec *ElementCombination) String() string {
|
func (ec *ElementCombination) String() string {
|
||||||
primaryName := elementNameMap[ec.Primary]
|
if secondary := ec.Secondary; secondary != nil {
|
||||||
if !ec.IsDual() {
|
return fmt.Sprintf("(%s, %s)", elementNameMap[ec.Primary], elementNameMap[*secondary])
|
||||||
return fmt.Sprintf("(%s)", primaryName)
|
|
||||||
}
|
}
|
||||||
return fmt.Sprintf("(%s, %s)", primaryName, elementNameMap[*ec.Secondary])
|
return fmt.Sprintf("(%s)", elementNameMap[ec.Primary])
|
||||||
}
|
}
|
||||||
|
|
||||||
// ElementCalculator 无锁元素克制计算器(依赖预加载资源)
|
// ElementCalculator 无锁元素克制计算器(所有倍数在初始化阶段预计算)
|
||||||
type ElementCalculator struct {
|
type ElementCalculator struct {
|
||||||
offensiveCache map[string]float64 // 攻击克制缓存(运行时填充,无并发写)
|
offensiveTable [maxMatrixSize][maxMatrixSize]float64
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewElementCalculator 创建计算器实例(仅初始化缓存)
|
// NewElementCalculator 创建计算器实例(构建只读查表缓存)
|
||||||
func NewElementCalculator() *ElementCalculator {
|
func NewElementCalculator() *ElementCalculator {
|
||||||
return &ElementCalculator{
|
c := &ElementCalculator{}
|
||||||
offensiveCache: make(map[string]float64, 4096), // 预分配大容量缓存
|
c.initOffensiveTable()
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ElementCalculator) initOffensiveTable() {
|
||||||
|
for attackerID, valid := range validCombinationIDs {
|
||||||
|
if !valid {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
attacker := &elementCombinationPool[attackerID]
|
||||||
|
for defenderID, valid := range validCombinationIDs {
|
||||||
|
if !valid {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
defender := &elementCombinationPool[defenderID]
|
||||||
|
c.offensiveTable[attackerID][defenderID] = c.calculateMultiplier(attacker, defender)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// getMatrixValue 直接返回矩阵值(修复核心问题:不再将0转换为1)
|
// getMatrixValue 直接返回矩阵值(修复核心问题:不再将0转换为1)
|
||||||
func (c *ElementCalculator) getMatrixValue(attacker, defender ElementType) float64 {
|
func (c *ElementCalculator) getMatrixValue(attacker, defender ElementType) float64 {
|
||||||
return matrix[attacker][defender] // 矩阵默认已初始化1.0,特殊值直接返回
|
return matrix[attacker][defender]
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetCombination 获取元素组合(直接从预加载池读取)
|
// GetCombination 获取元素组合(直接按ID索引)
|
||||||
func (c *ElementCalculator) GetCombination(id int) (*ElementCombination, error) {
|
func (c *ElementCalculator) GetCombination(id int) (*ElementCombination, error) {
|
||||||
combo, exists := elementCombinationPool[id]
|
if !isValidCombinationID(id) {
|
||||||
if !exists {
|
|
||||||
return nil, fmt.Errorf("invalid element combination ID: %d", id)
|
return nil, fmt.Errorf("invalid element combination ID: %d", id)
|
||||||
}
|
}
|
||||||
return combo, nil
|
return &elementCombinationPool[id], nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetOffensiveMultiplier 计算攻击方→防御方的克制倍数(缓存优先)
|
// GetOffensiveMultiplier 计算攻击方→防御方的克制倍数(只读查表)
|
||||||
func (c *ElementCalculator) GetOffensiveMultiplier(attackerID, defenderID int) (float64, error) {
|
func (c *ElementCalculator) GetOffensiveMultiplier(attackerID, defenderID int) (float64, error) {
|
||||||
// 1. 获取预加载的组合实例
|
if !isValidCombinationID(attackerID) {
|
||||||
attacker, err := c.GetCombination(attackerID)
|
return 0, fmt.Errorf("attacker invalid: invalid element combination ID: %d", attackerID)
|
||||||
if err != nil {
|
|
||||||
return 0, fmt.Errorf("attacker invalid: %w", err)
|
|
||||||
}
|
}
|
||||||
defender, err := c.GetCombination(defenderID)
|
if !isValidCombinationID(defenderID) {
|
||||||
if err != nil {
|
return 0, fmt.Errorf("defender invalid: invalid element combination ID: %d", defenderID)
|
||||||
return 0, fmt.Errorf("defender invalid: %w", err)
|
|
||||||
}
|
}
|
||||||
|
return c.offensiveTable[attackerID][defenderID], nil
|
||||||
// 2. 缓存键(全局唯一)
|
|
||||||
cacheKey := fmt.Sprintf("a%d_d%d", attackerID, defenderID)
|
|
||||||
if val, exists := c.offensiveCache[cacheKey]; exists {
|
|
||||||
return val, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. 核心计算+缓存
|
|
||||||
val := c.calculateMultiplier(attacker, defender)
|
|
||||||
c.offensiveCache[cacheKey] = val
|
|
||||||
return val, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// calculateMultiplier 核心克制计算逻辑
|
// calculateMultiplier 核心克制计算逻辑
|
||||||
func (c *ElementCalculator) calculateMultiplier(attacker, defender *ElementCombination) float64 {
|
func (c *ElementCalculator) calculateMultiplier(attacker, defender *ElementCombination) float64 {
|
||||||
// 场景1:单→单
|
|
||||||
if !attacker.IsDual() && !defender.IsDual() {
|
if !attacker.IsDual() && !defender.IsDual() {
|
||||||
return c.getMatrixValue(attacker.Primary, defender.Primary)
|
return c.getMatrixValue(attacker.Primary, defender.Primary)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 场景2:单→双
|
|
||||||
if !attacker.IsDual() {
|
if !attacker.IsDual() {
|
||||||
y1, y2 := defender.Primary, *defender.Secondary
|
y1, y2 := defender.Primary, *defender.Secondary
|
||||||
m1 := c.getMatrixValue(attacker.Primary, y1)
|
m1 := c.getMatrixValue(attacker.Primary, y1)
|
||||||
m2 := c.getMatrixValue(attacker.Primary, y2)
|
m2 := c.getMatrixValue(attacker.Primary, y2)
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case m1 == 2 && m2 == 2:
|
case m1 == 2 && m2 == 2:
|
||||||
return 4.0
|
return 4.0
|
||||||
@@ -333,12 +340,10 @@ func (c *ElementCalculator) calculateMultiplier(attacker, defender *ElementCombi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 场景3:双→单
|
|
||||||
if !defender.IsDual() {
|
if !defender.IsDual() {
|
||||||
return c.calculateDualToSingle(attacker.Primary, *attacker.Secondary, defender.Primary)
|
return c.calculateDualToSingle(attacker.Primary, *attacker.Secondary, defender.Primary)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 场景4:双→双
|
|
||||||
x1, x2 := attacker.Primary, *attacker.Secondary
|
x1, x2 := attacker.Primary, *attacker.Secondary
|
||||||
y1, y2 := defender.Primary, *defender.Secondary
|
y1, y2 := defender.Primary, *defender.Secondary
|
||||||
coeffY1 := c.calculateDualToSingle(x1, x2, y1)
|
coeffY1 := c.calculateDualToSingle(x1, x2, y1)
|
||||||
@@ -350,7 +355,6 @@ func (c *ElementCalculator) calculateMultiplier(attacker, defender *ElementCombi
|
|||||||
func (c *ElementCalculator) calculateDualToSingle(attacker1, attacker2, defender ElementType) float64 {
|
func (c *ElementCalculator) calculateDualToSingle(attacker1, attacker2, defender ElementType) float64 {
|
||||||
k1 := c.getMatrixValue(attacker1, defender)
|
k1 := c.getMatrixValue(attacker1, defender)
|
||||||
k2 := c.getMatrixValue(attacker2, defender)
|
k2 := c.getMatrixValue(attacker2, defender)
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case k1 == 2 && k2 == 2:
|
case k1 == 2 && k2 == 2:
|
||||||
return 4.0
|
return 4.0
|
||||||
@@ -361,60 +365,49 @@ func (c *ElementCalculator) calculateDualToSingle(attacker1, attacker2, defender
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var Calculator = NewElementCalculator()
|
|
||||||
|
|
||||||
// TestAllScenarios 全场景测试(验证预加载和计算逻辑)
|
// TestAllScenarios 全场景测试(验证预加载和计算逻辑)
|
||||||
func TestAllScenarios() {
|
func TestAllScenarios() {
|
||||||
|
|
||||||
// 测试1:单→单(草→水)
|
|
||||||
m1, _ := Calculator.GetOffensiveMultiplier(1, 2)
|
m1, _ := Calculator.GetOffensiveMultiplier(1, 2)
|
||||||
fmt.Println("草→水: %.2f(预期2.0)", m1)
|
fmt.Println("草→水: %.2f(预期2.0)", m1)
|
||||||
if math.Abs(m1-2.0) > 0.001 {
|
if math.Abs(m1-2.0) > 0.001 {
|
||||||
fmt.Println("测试1失败:实际%.2f", m1)
|
fmt.Println("测试1失败:实际%.2f", m1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 测试2:特殊单→单(混沌→虚空)
|
|
||||||
m2, _ := Calculator.GetOffensiveMultiplier(222, 226)
|
m2, _ := Calculator.GetOffensiveMultiplier(222, 226)
|
||||||
fmt.Println("混沌→虚空: %.2f(预期0.0)", m2)
|
fmt.Println("混沌→虚空: %.2f(预期0.0)", m2)
|
||||||
if math.Abs(m2-0.0) > 0.001 {
|
if math.Abs(m2-0.0) > 0.001 {
|
||||||
fmt.Println("测试2失败:实际%.2f", m2)
|
fmt.Println("测试2失败:实际%.2f", m2)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 测试3:单→双(火→冰龙(43))
|
|
||||||
m3, _ := Calculator.GetOffensiveMultiplier(3, 43)
|
m3, _ := Calculator.GetOffensiveMultiplier(3, 43)
|
||||||
fmt.Println("火→冰龙: %.2f(预期1.5)", m3)
|
fmt.Println("火→冰龙: %.2f(预期1.5)", m3)
|
||||||
if math.Abs(m3-1.5) > 0.001 {
|
if math.Abs(m3-1.5) > 0.001 {
|
||||||
fmt.Println("测试3失败:实际%.2f", m3)
|
fmt.Println("测试3失败:实际%.2f", m3)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 测试4:双→特殊单(混沌暗影(92)→神灵(223))
|
|
||||||
m4, _ := Calculator.GetOffensiveMultiplier(92, 223)
|
m4, _ := Calculator.GetOffensiveMultiplier(92, 223)
|
||||||
fmt.Println("混沌暗影→神灵: %.2f(预期1.25)", m4)
|
fmt.Println("混沌暗影→神灵: %.2f(预期1.25)", m4)
|
||||||
if math.Abs(m4-1.25) > 0.001 {
|
if math.Abs(m4-1.25) > 0.001 {
|
||||||
fmt.Println("测试4失败:实际%.2f", m4)
|
fmt.Println("测试4失败:实际%.2f", m4)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 测试5:双→双(虚空邪灵(113)→混沌远古(98))
|
|
||||||
m5, _ := Calculator.GetOffensiveMultiplier(113, 98)
|
m5, _ := Calculator.GetOffensiveMultiplier(113, 98)
|
||||||
fmt.Println("虚空邪灵→混沌远古: %.2f(预期0.875", m5)
|
fmt.Println("虚空邪灵→混沌远古: %.2f(预期0.875", m5)
|
||||||
if math.Abs(m5-0.875) > 0.001 {
|
if math.Abs(m5-0.875) > 0.001 {
|
||||||
fmt.Println("测试5失败:实际%.2f", m5)
|
fmt.Println("测试5失败:实际%.2f", m5)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 测试6:缓存命中
|
|
||||||
m6, _ := Calculator.GetOffensiveMultiplier(113, 98)
|
m6, _ := Calculator.GetOffensiveMultiplier(113, 98)
|
||||||
if math.Abs(m6-m5) > 0.001 {
|
if math.Abs(m6-m5) > 0.001 {
|
||||||
fmt.Println("测试6失败:缓存未命中")
|
fmt.Println("测试6失败:缓存未命中")
|
||||||
}
|
}
|
||||||
|
|
||||||
// 测试7:含无效组合(电→地面)
|
|
||||||
m7, _ := Calculator.GetOffensiveMultiplier(5, 7)
|
m7, _ := Calculator.GetOffensiveMultiplier(5, 7)
|
||||||
fmt.Println("电→地面: %.2f(预期0.0)", m7)
|
fmt.Println("电→地面: %.2f(预期0.0)", m7)
|
||||||
if math.Abs(m7-0.0) > 0.001 {
|
if math.Abs(m7-0.0) > 0.001 {
|
||||||
fmt.Println("测试7失败:实际%.2f", m7)
|
fmt.Println("测试7失败:实际%.2f", m7)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 测试8:双属性含无效(电战斗→地面)
|
|
||||||
m8, _ := Calculator.GetOffensiveMultiplier(35, 7)
|
m8, _ := Calculator.GetOffensiveMultiplier(35, 7)
|
||||||
fmt.Println("电战斗→地面: %.2f(预期0.25)", m8)
|
fmt.Println("电战斗→地面: %.2f(预期0.25)", m8)
|
||||||
if math.Abs(m8-0.25) > 0.001 {
|
if math.Abs(m8-0.25) > 0.001 {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ type EffectArg struct {
|
|||||||
SideEffect []struct {
|
SideEffect []struct {
|
||||||
ID int `json:"ID"`
|
ID int `json:"ID"`
|
||||||
SideEffectArgcount int `json:"SideEffectArgcount"`
|
SideEffectArgcount int `json:"SideEffectArgcount"`
|
||||||
SideEffectArg string `json:"SideEffectArg,omitempty"`
|
SideEffectArg rawFlexibleString `json:"SideEffectArg,omitempty"`
|
||||||
} `json:"SideEffect"`
|
} `json:"SideEffect"`
|
||||||
} `json:"SideEffects"`
|
} `json:"SideEffects"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
|
|
||||||
_ "blazing/common/data/xmlres/packed"
|
_ "blazing/common/data/xmlres/packed"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"os"
|
"fmt"
|
||||||
|
|
||||||
"github.com/ECUST-XX/xml"
|
"github.com/ECUST-XX/xml"
|
||||||
"github.com/gogf/gf/v2/os/gres"
|
"github.com/gogf/gf/v2/os/gres"
|
||||||
@@ -14,22 +14,36 @@ import (
|
|||||||
|
|
||||||
var path string
|
var path string
|
||||||
|
|
||||||
|
func readConfigContent(path string) []byte {
|
||||||
|
return gres.GetContent(path)
|
||||||
|
}
|
||||||
|
|
||||||
func getXml[T any](path string) T {
|
func getXml[T any](path string) T {
|
||||||
|
|
||||||
// 解析XML到结构体
|
// 解析XML到结构体
|
||||||
var xmls T
|
var xmls T
|
||||||
|
|
||||||
t1 := gres.GetContent(path)
|
t1 := readConfigContent(path)
|
||||||
xml.Unmarshal(t1, &xmls)
|
xml.Unmarshal(t1, &xmls)
|
||||||
|
|
||||||
return xmls
|
return xmls
|
||||||
}
|
}
|
||||||
func getJson[T any](path string) T {
|
func getJson[T any](path string) T {
|
||||||
|
|
||||||
// 解析XML到结构体
|
// 解析JSON到结构体
|
||||||
var xmls T
|
var xmls T
|
||||||
t1 := gres.GetContent(path)
|
t1 := readConfigContent(path)
|
||||||
json.Unmarshal(t1, &xmls)
|
if len(t1) == 0 {
|
||||||
|
fmt.Printf("[xmlres] getJson empty content: path=%s\n", path)
|
||||||
|
return xmls
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(t1, &xmls); err != nil {
|
||||||
|
head := string(t1)
|
||||||
|
if len(head) > 300 {
|
||||||
|
head = head[:300]
|
||||||
|
}
|
||||||
|
fmt.Printf("[xmlres] getJson unmarshal failed: path=%s len=%d err=%v head=%q\n", path, len(t1), err, head)
|
||||||
|
}
|
||||||
|
|
||||||
return xmls
|
return xmls
|
||||||
}
|
}
|
||||||
@@ -58,8 +72,6 @@ var (
|
|||||||
|
|
||||||
func Initfile() {
|
func Initfile() {
|
||||||
//gres.Dump()
|
//gres.Dump()
|
||||||
path1, _ := os.Getwd()
|
|
||||||
path = path1 + "/public/config/"
|
|
||||||
path = "config/"
|
path = "config/"
|
||||||
MapConfig = getXml[Maps](path + "210.xml")
|
MapConfig = getXml[Maps](path + "210.xml")
|
||||||
|
|
||||||
@@ -87,10 +99,10 @@ func Initfile() {
|
|||||||
return gconv.Int(m.ProductID)
|
return gconv.Int(m.ProductID)
|
||||||
|
|
||||||
})
|
})
|
||||||
Skill := getXml[MovesTbl](path + "227.xml")
|
skillConfig := getJson[MovesJSON](path + "moves_flash.json")
|
||||||
|
|
||||||
SkillMap = make(map[int]Move, len(Skill.Moves))
|
SkillMap = make(map[int]Move, len(skillConfig.MovesTbl.Moves.Move))
|
||||||
for _, v := range Skill.Moves {
|
for _, v := range skillConfig.MovesTbl.Moves.Move {
|
||||||
v.SideEffectS = ParseSideEffectArgs(v.SideEffect)
|
v.SideEffectS = ParseSideEffectArgs(v.SideEffect)
|
||||||
v.SideEffectArgS = ParseSideEffectArgs(v.SideEffectArg)
|
v.SideEffectArgS = ParseSideEffectArgs(v.SideEffectArg)
|
||||||
SkillMap[v.ID] = v
|
SkillMap[v.ID] = v
|
||||||
|
|||||||
26
common/data/xmlres/json_compat_test.go
Normal file
26
common/data/xmlres/json_compat_test.go
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
package xmlres
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMoveUnmarshalJSONAcceptsNumericName(t *testing.T) {
|
||||||
|
var move Move
|
||||||
|
if err := json.Unmarshal([]byte(`{"ID":10001,"Name":1,"Category":1,"Type":8,"Power":35,"MaxPP":35,"Accuracy":95}`), &move); err != nil {
|
||||||
|
t.Fatalf("unmarshal move failed: %v", err)
|
||||||
|
}
|
||||||
|
if move.Name != "1" {
|
||||||
|
t.Fatalf("expected numeric name to convert to string, got %q", move.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEffectArgUnmarshalJSONAcceptsNumericSideEffectArg(t *testing.T) {
|
||||||
|
var cfg EffectArg
|
||||||
|
if err := json.Unmarshal([]byte(`{"SideEffects":{"SideEffect":[{"ID":1,"SideEffectArgcount":1,"SideEffectArg":3}]}}`), &cfg); err != nil {
|
||||||
|
t.Fatalf("unmarshal effect arg failed: %v", err)
|
||||||
|
}
|
||||||
|
if got := string(cfg.SideEffects.SideEffect[0].SideEffectArg); got != "3" {
|
||||||
|
t.Fatalf("expected numeric side effect arg to convert to string, got %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
@@ -1,6 +1,7 @@
|
|||||||
package xmlres
|
package xmlres
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
@@ -33,52 +34,156 @@ type MovesTbl struct {
|
|||||||
Moves []Move `xml:"Moves>Move"`
|
Moves []Move `xml:"Moves>Move"`
|
||||||
EFF []SideEffect `xml:"SideEffects>SideEffect"`
|
EFF []SideEffect `xml:"SideEffects>SideEffect"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type MovesJSON struct {
|
||||||
|
MovesTbl MovesJSONRoot `json:"MovesTbl"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type MovesJSONRoot struct {
|
||||||
|
Moves struct {
|
||||||
|
Move []Move `json:"Move"`
|
||||||
|
} `json:"Moves"`
|
||||||
|
SideEffects struct {
|
||||||
|
SideEffect []SideEffect `json:"SideEffect"`
|
||||||
|
} `json:"SideEffects"`
|
||||||
|
}
|
||||||
|
|
||||||
type MovesMap struct {
|
type MovesMap struct {
|
||||||
XMLName xml.Name `xml:"MovesTbl"`
|
XMLName xml.Name `xml:"MovesTbl"`
|
||||||
Moves map[int]Move
|
Moves map[int]Move
|
||||||
EFF []SideEffect `xml:"SideEffects>SideEffect"`
|
EFF []SideEffect `xml:"SideEffects>SideEffect"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type rawFlexibleString string
|
||||||
|
|
||||||
|
func (s *rawFlexibleString) UnmarshalJSON(data []byte) error {
|
||||||
|
text := strings.TrimSpace(string(data))
|
||||||
|
if text == "" || text == "null" {
|
||||||
|
*s = ""
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(text) >= 2 && text[0] == '"' && text[len(text)-1] == '"' {
|
||||||
|
var decoded string
|
||||||
|
if err := json.Unmarshal(data, &decoded); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
*s = rawFlexibleString(decoded)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
*s = rawFlexibleString(text)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Move 定义单个技能的结构
|
// Move 定义单个技能的结构
|
||||||
type Move struct {
|
type Move struct {
|
||||||
ID int `xml:"ID,attr"`
|
ID int `xml:"ID,attr" json:"ID"`
|
||||||
Name string `xml:"Name,attr"`
|
Name string `xml:"Name,attr" json:"Name"`
|
||||||
|
|
||||||
Category int `xml:"Category,attr"` //属性
|
Category int `xml:"Category,attr" json:"Category"` //属性
|
||||||
Type int `xml:"Type,attr"` //类型
|
Type int `xml:"Type,attr" json:"Type"` //类型
|
||||||
Power int `xml:"Power,attr"` //威力
|
Power int `xml:"Power,attr" json:"Power"` //威力
|
||||||
MaxPP int `xml:"MaxPP,attr"` //最大PP
|
MaxPP int `xml:"MaxPP,attr" json:"MaxPP"` //最大PP
|
||||||
Accuracy int `xml:"Accuracy,attr"` //命中率
|
Accuracy int `xml:"Accuracy,attr" json:"Accuracy"` //命中率
|
||||||
CritRate int `xml:"CritRate,attr,omitempty"` //暴击率
|
CritRate int `xml:"CritRate,attr,omitempty" json:"CritRate,omitempty"` //暴击率
|
||||||
Priority int `xml:"Priority,attr,omitempty"` //优先级
|
Priority int `xml:"Priority,attr,omitempty" json:"Priority,omitempty"` //优先级
|
||||||
MustHit int `xml:"MustHit,attr,omitempty"` //是否必中
|
MustHit int `xml:"MustHit,attr,omitempty" json:"MustHit,omitempty"` //是否必中
|
||||||
SwapElemType int `xml:"SwapElemType,attr,omitempty"` //技能交换属性
|
SwapElemType int `xml:"SwapElemType,attr,omitempty" json:"SwapElemType,omitempty"` //技能交换属性
|
||||||
CopyElemType int `xml:"CopyElemType,attr,omitempty"` // 技能复制属性
|
CopyElemType int `xml:"CopyElemType,attr,omitempty" json:"CopyElemType,omitempty"` // 技能复制属性
|
||||||
CritAtkFirst int `xml:"CritAtkFirst,attr,omitempty"` // 先出手时必定致命一击
|
CritAtkFirst int `xml:"CritAtkFirst,attr,omitempty" json:"CritAtkFirst,omitempty"` // 先出手时必定致命一击
|
||||||
CritAtkSecond int `xml:"CritAtkSecond,attr,omitempty"` //后出手时必定致命一击
|
CritAtkSecond int `xml:"CritAtkSecond,attr,omitempty" json:"CritAtkSecond,omitempty"` //后出手时必定致命一击
|
||||||
CritSelfHalfHp int `xml:"CritSelfHalfHp,attr,omitempty"` //自身体力低于一半时必定致命一击
|
CritSelfHalfHp int `xml:"CritSelfHalfHp,attr,omitempty" json:"CritSelfHalfHp,omitempty"` //自身体力低于一半时必定致命一击
|
||||||
CritFoeHalfHp int `xml:"CritFoeHalfHp,attr,omitempty"` //对方体力低于一半时必定致命一击
|
CritFoeHalfHp int `xml:"CritFoeHalfHp,attr,omitempty" json:"CritFoeHalfHp,omitempty"` //对方体力低于一半时必定致命一击
|
||||||
DmgBindLv int `xml:"DmgBindLv,attr,omitempty"` //使对方受到的伤害值等于自身的等级
|
DmgBindLv int `xml:"DmgBindLv,attr,omitempty" json:"DmgBindLv,omitempty"` //使对方受到的伤害值等于自身的等级
|
||||||
PwrBindDv int `xml:"PwrBindDv,attr,omitempty"` //威力(power)取决于自身的潜力(个体值)
|
PwrBindDv int `xml:"PwrBindDv,attr,omitempty" json:"PwrBindDv,omitempty"` //威力(power)取决于自身的潜力(个体值)
|
||||||
PwrDouble int `xml:"PwrDouble,attr,omitempty"` //攻击时,若对方处于异常状态, 则威力翻倍;
|
PwrDouble int `xml:"PwrDouble,attr,omitempty" json:"PwrDouble,omitempty"` //攻击时,若对方处于异常状态, 则威力翻倍;
|
||||||
DmgBindHpDv int `xml:"DmgBindHpDv,attr,omitempty"` //使对方受到的伤害值等于自身的体力值
|
DmgBindHpDv int `xml:"DmgBindHpDv,attr,omitempty" json:"DmgBindHpDv,omitempty"` //使对方受到的伤害值等于自身的体力值
|
||||||
SideEffect string `xml:"SideEffect,attr,omitempty"`
|
SideEffect string `xml:"SideEffect,attr,omitempty" json:"SideEffect,omitempty"`
|
||||||
SideEffectArg string `xml:"SideEffectArg,attr,omitempty"`
|
SideEffectArg string `xml:"SideEffectArg,attr,omitempty" json:"SideEffectArg,omitempty"`
|
||||||
SideEffectS []int
|
SideEffectS []int
|
||||||
SideEffectArgS []int
|
SideEffectArgS []int
|
||||||
AtkNum int `xml:"AtkNum,attr,omitempty"`
|
AtkNum int `xml:"AtkNum,attr,omitempty" json:"AtkNum,omitempty"`
|
||||||
AtkType int `xml:"AtkType,attr,omitempty"` // 0:所有人 1:仅己方 2:仅对方 3:仅自己
|
AtkType int `xml:"AtkType,attr,omitempty" json:"AtkType,omitempty"` // 0:所有人 1:仅己方 2:仅对方 3:仅自己
|
||||||
Url string `xml:"Url,attr,omitempty"`
|
Url string `xml:"Url,attr,omitempty" json:"Url,omitempty"`
|
||||||
|
|
||||||
Info string `xml:"info,attr,omitempty"`
|
Info string `xml:"info,attr,omitempty" json:"info,omitempty"`
|
||||||
|
|
||||||
CD *int `xml:"CD,attr"`
|
CD *int `xml:"CD,attr" json:"CD"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Move) UnmarshalJSON(data []byte) error {
|
||||||
|
type moveAlias struct {
|
||||||
|
ID int `json:"ID"`
|
||||||
|
Name rawFlexibleString `json:"Name"`
|
||||||
|
Category int `json:"Category"`
|
||||||
|
Type int `json:"Type"`
|
||||||
|
Power int `json:"Power"`
|
||||||
|
MaxPP int `json:"MaxPP"`
|
||||||
|
Accuracy int `json:"Accuracy"`
|
||||||
|
CritRate int `json:"CritRate,omitempty"`
|
||||||
|
Priority int `json:"Priority,omitempty"`
|
||||||
|
MustHit int `json:"MustHit,omitempty"`
|
||||||
|
SwapElemType int `json:"SwapElemType,omitempty"`
|
||||||
|
CopyElemType int `json:"CopyElemType,omitempty"`
|
||||||
|
CritAtkFirst int `json:"CritAtkFirst,omitempty"`
|
||||||
|
CritAtkSecond int `json:"CritAtkSecond,omitempty"`
|
||||||
|
CritSelfHalfHp int `json:"CritSelfHalfHp,omitempty"`
|
||||||
|
CritFoeHalfHp int `json:"CritFoeHalfHp,omitempty"`
|
||||||
|
DmgBindLv int `json:"DmgBindLv,omitempty"`
|
||||||
|
PwrBindDv int `json:"PwrBindDv,omitempty"`
|
||||||
|
PwrDouble int `json:"PwrDouble,omitempty"`
|
||||||
|
DmgBindHpDv int `json:"DmgBindHpDv,omitempty"`
|
||||||
|
SideEffect rawFlexibleString `json:"SideEffect,omitempty"`
|
||||||
|
SideEffectArg rawFlexibleString `json:"SideEffectArg,omitempty"`
|
||||||
|
AtkNum int `json:"AtkNum,omitempty"`
|
||||||
|
AtkType int `json:"AtkType,omitempty"`
|
||||||
|
Url string `json:"Url,omitempty"`
|
||||||
|
Info string `json:"info,omitempty"`
|
||||||
|
CD *int `json:"CD"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var aux moveAlias
|
||||||
|
if err := json.Unmarshal(data, &aux); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
*m = Move{
|
||||||
|
ID: aux.ID,
|
||||||
|
Name: string(aux.Name),
|
||||||
|
Category: aux.Category,
|
||||||
|
Type: aux.Type,
|
||||||
|
Power: aux.Power,
|
||||||
|
MaxPP: aux.MaxPP,
|
||||||
|
Accuracy: aux.Accuracy,
|
||||||
|
CritRate: aux.CritRate,
|
||||||
|
Priority: aux.Priority,
|
||||||
|
MustHit: aux.MustHit,
|
||||||
|
SwapElemType: aux.SwapElemType,
|
||||||
|
CopyElemType: aux.CopyElemType,
|
||||||
|
CritAtkFirst: aux.CritAtkFirst,
|
||||||
|
CritAtkSecond: aux.CritAtkSecond,
|
||||||
|
CritSelfHalfHp: aux.CritSelfHalfHp,
|
||||||
|
CritFoeHalfHp: aux.CritFoeHalfHp,
|
||||||
|
DmgBindLv: aux.DmgBindLv,
|
||||||
|
PwrBindDv: aux.PwrBindDv,
|
||||||
|
PwrDouble: aux.PwrDouble,
|
||||||
|
DmgBindHpDv: aux.DmgBindHpDv,
|
||||||
|
SideEffect: string(aux.SideEffect),
|
||||||
|
SideEffectArg: string(aux.SideEffectArg),
|
||||||
|
AtkNum: aux.AtkNum,
|
||||||
|
AtkType: aux.AtkType,
|
||||||
|
Url: aux.Url,
|
||||||
|
Info: aux.Info,
|
||||||
|
CD: aux.CD,
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type SideEffect struct {
|
type SideEffect struct {
|
||||||
ID int `xml:"ID,attr"`
|
ID int `xml:"ID,attr" json:"ID"`
|
||||||
Help string `xml:"help,attr"`
|
Help string `xml:"help,attr" json:"help"`
|
||||||
Des string `xml:"des,attr"`
|
Des string `xml:"des,attr" json:"des"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReadHTTPFile 通过HTTP GET请求获取远程文件内容
|
// ReadHTTPFile 通过HTTP GET请求获取远程文件内容
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import (
|
|||||||
"blazing/cool"
|
"blazing/cool"
|
||||||
"blazing/logic/service/fight/pvp"
|
"blazing/logic/service/fight/pvp"
|
||||||
"blazing/logic/service/fight/pvpwire"
|
"blazing/logic/service/fight/pvpwire"
|
||||||
"context"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -15,7 +15,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// ListenFunc 监听函数
|
// ListenFunc 监听函数
|
||||||
// ListenFunc 改造后的 Redis PubSub 监听函数,支持自动重连和心跳保活
|
// ListenFunc 改造后的 Redis PubSub 监听函数,支持自动重连。
|
||||||
|
// 注意:PubSub 连接只负责订阅和接收,避免在同一连接上并发 PING。
|
||||||
func ListenFunc(ctx g.Ctx) {
|
func ListenFunc(ctx g.Ctx) {
|
||||||
if !cool.IsRedisMode {
|
if !cool.IsRedisMode {
|
||||||
panic(gerror.New("集群模式下, 请使用Redis作为缓存"))
|
panic(gerror.New("集群模式下, 请使用Redis作为缓存"))
|
||||||
@@ -25,7 +26,6 @@ func ListenFunc(ctx g.Ctx) {
|
|||||||
const (
|
const (
|
||||||
subscribeTopic = "cool:func" // 订阅的主题
|
subscribeTopic = "cool:func" // 订阅的主题
|
||||||
retryDelay = 10 * time.Second // 连接失败重试间隔
|
retryDelay = 10 * time.Second // 连接失败重试间隔
|
||||||
heartbeatInterval = 30 * time.Second // 心跳保活间隔
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// 外层循环:负责连接断开后的整体重连
|
// 外层循环:负责连接断开后的整体重连
|
||||||
@@ -46,47 +46,25 @@ func ListenFunc(ctx g.Ctx) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 启动心跳保活协程,防止连接因空闲被断开
|
// 2. 订阅主题
|
||||||
heartbeatCtx, heartbeatCancel := context.WithCancel(context.Background())
|
|
||||||
go func() {
|
|
||||||
ticker := time.NewTicker(heartbeatInterval)
|
|
||||||
defer func() {
|
|
||||||
ticker.Stop()
|
|
||||||
heartbeatCancel()
|
|
||||||
}()
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-heartbeatCtx.Done():
|
|
||||||
cool.Logger.Info(ctx, "心跳协程退出")
|
|
||||||
return
|
|
||||||
case <-ticker.C:
|
|
||||||
// 发送 PING 心跳,保持连接活跃
|
|
||||||
_, pingErr := conn.Do(ctx, "PING")
|
|
||||||
if pingErr != nil {
|
|
||||||
cool.Logger.Error(ctx, "Redis 心跳失败,触发重连", "error", pingErr)
|
|
||||||
// 心跳失败时主动关闭连接,触发外层重连
|
|
||||||
_ = conn.Close(ctx)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// 3. 订阅主题
|
|
||||||
_, err = conn.Do(ctx, "subscribe", subscribeTopic)
|
_, err = conn.Do(ctx, "subscribe", subscribeTopic)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
cool.Logger.Error(ctx, "订阅 Redis 主题失败", "topic", subscribeTopic, "error", err)
|
cool.Logger.Error(ctx, "订阅 Redis 主题失败", "topic", subscribeTopic, "error", err)
|
||||||
heartbeatCancel() // 关闭心跳协程
|
|
||||||
_ = conn.Close(ctx)
|
_ = conn.Close(ctx)
|
||||||
time.Sleep(retryDelay)
|
time.Sleep(retryDelay)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
cool.Logger.Info(ctx, "成功订阅 Redis 主题", "topic", subscribeTopic)
|
cool.Logger.Info(ctx, "成功订阅 Redis 主题", "topic", subscribeTopic)
|
||||||
_, err = conn.Do(ctx, "subscribe", "sun:join") //加入队列
|
_, err = conn.Do(ctx, "subscribe", "sun:join") //加入队列
|
||||||
|
if err != nil {
|
||||||
|
cool.Logger.Error(ctx, "订阅 Redis 主题失败", "topic", "sun:join", "error", err)
|
||||||
|
_ = conn.Close(ctx)
|
||||||
|
time.Sleep(retryDelay)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
cool.Logger.Info(ctx, "成功订阅 Redis 主题", "topic", "sun:join")
|
||||||
|
|
||||||
// 4. 循环接收消息
|
// 3. 循环接收消息
|
||||||
connError := false
|
connError := false
|
||||||
for !connError {
|
for !connError {
|
||||||
select {
|
select {
|
||||||
@@ -129,15 +107,15 @@ func ListenFunc(ctx g.Ctx) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. 清理资源,准备重连
|
// 4. 清理资源,准备重连
|
||||||
heartbeatCancel() // 关闭心跳协程
|
|
||||||
_ = conn.Close(ctx) // 关闭当前连接
|
_ = conn.Close(ctx) // 关闭当前连接
|
||||||
// Logger.Warn(ctx, "Redis 连接异常,准备重连", "retry_after", retryDelay)
|
cool.Logger.Info(ctx, "Redis 订阅连接异常,准备重连", "retry_after", retryDelay)
|
||||||
time.Sleep(retryDelay)
|
time.Sleep(retryDelay)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListenFight 完全对齐 ListenFunc 写法,修复收不到消息问题
|
// ListenFight 完全对齐 ListenFunc 写法,修复收不到消息问题。
|
||||||
|
// 注意:PubSub 连接只负责订阅和接收,避免在同一连接上并发 PING。
|
||||||
func ListenFight(ctx g.Ctx) {
|
func ListenFight(ctx g.Ctx) {
|
||||||
if !cool.IsRedisMode {
|
if !cool.IsRedisMode {
|
||||||
panic(gerror.New("集群模式下, 请使用Redis作为缓存"))
|
panic(gerror.New("集群模式下, 请使用Redis作为缓存"))
|
||||||
@@ -146,7 +124,6 @@ func ListenFight(ctx g.Ctx) {
|
|||||||
// 定义常量配置(对齐 ListenFunc 风格)
|
// 定义常量配置(对齐 ListenFunc 风格)
|
||||||
const (
|
const (
|
||||||
retryDelay = 10 * time.Second // 连接失败重试间隔
|
retryDelay = 10 * time.Second // 连接失败重试间隔
|
||||||
heartbeatInterval = 30 * time.Second // 心跳保活间隔
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// 提前拼接订阅主题(避免重复拼接,便于日志打印)
|
// 提前拼接订阅主题(避免重复拼接,便于日志打印)
|
||||||
@@ -175,35 +152,7 @@ func ListenFight(ctx g.Ctx) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 启动心跳保活协程(完全对齐 ListenFunc 逻辑)
|
// 2. 订阅主题(对齐 ListenFunc 的错误处理,替换 panic 为优雅重连)
|
||||||
heartbeatCtx, heartbeatCancel := context.WithCancel(context.Background())
|
|
||||||
go func() {
|
|
||||||
ticker := time.NewTicker(heartbeatInterval)
|
|
||||||
defer func() {
|
|
||||||
ticker.Stop()
|
|
||||||
heartbeatCancel()
|
|
||||||
}()
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-heartbeatCtx.Done():
|
|
||||||
cool.Logger.Info(ctx, "心跳协程退出")
|
|
||||||
return
|
|
||||||
case <-ticker.C:
|
|
||||||
// 发送 PING 心跳,保持连接活跃
|
|
||||||
_, pingErr := conn.Do(ctx, "PING")
|
|
||||||
if pingErr != nil {
|
|
||||||
cool.Logger.Error(ctx, "Redis 心跳失败,触发重连", "error", pingErr)
|
|
||||||
// 心跳失败时主动关闭连接,触发外层重连
|
|
||||||
_ = conn.Close(ctx)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
cool.Logger.Debug(ctx, "Redis 心跳发送成功,连接正常")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// 3. 订阅主题(对齐 ListenFunc 的错误处理,替换 panic 为优雅重连)
|
|
||||||
subscribeTopics := []string{startTopic, pvpServerTopic}
|
subscribeTopics := []string{startTopic, pvpServerTopic}
|
||||||
if cool.Config.GameOnlineID == pvp.CoordinatorOnlineID {
|
if cool.Config.GameOnlineID == pvp.CoordinatorOnlineID {
|
||||||
subscribeTopics = append(subscribeTopics, pvpCoordinatorTopic)
|
subscribeTopics = append(subscribeTopics, pvpCoordinatorTopic)
|
||||||
@@ -213,7 +162,6 @@ func ListenFight(ctx g.Ctx) {
|
|||||||
_, err = conn.Do(ctx, "subscribe", topic)
|
_, err = conn.Do(ctx, "subscribe", topic)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
cool.Logger.Error(ctx, "订阅 Redis 主题失败", "topic", topic, "error", err)
|
cool.Logger.Error(ctx, "订阅 Redis 主题失败", "topic", topic, "error", err)
|
||||||
heartbeatCancel()
|
|
||||||
_ = conn.Close(ctx)
|
_ = conn.Close(ctx)
|
||||||
time.Sleep(retryDelay)
|
time.Sleep(retryDelay)
|
||||||
subscribeFailed = true
|
subscribeFailed = true
|
||||||
@@ -239,7 +187,7 @@ func ListenFight(ctx g.Ctx) {
|
|||||||
// 打印监听提示(保留原有日志)
|
// 打印监听提示(保留原有日志)
|
||||||
fmt.Println("监听战斗", startTopic)
|
fmt.Println("监听战斗", startTopic)
|
||||||
|
|
||||||
// 4. 循环接收消息(完全对齐 ListenFunc 逻辑)
|
// 3. 循环接收消息(完全对齐 ListenFunc 逻辑)
|
||||||
connError := false
|
connError := false
|
||||||
for !connError {
|
for !connError {
|
||||||
select {
|
select {
|
||||||
@@ -281,9 +229,9 @@ func ListenFight(ctx g.Ctx) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. 清理资源,准备重连(完全对齐 ListenFunc)
|
// 4. 清理资源,准备重连(完全对齐 ListenFunc)
|
||||||
heartbeatCancel() // 关闭心跳协程
|
|
||||||
_ = conn.Close(ctx) // 关闭当前连接
|
_ = conn.Close(ctx) // 关闭当前连接
|
||||||
|
cool.Logger.Info(ctx, "Redis 战斗订阅连接异常,准备重连", "retry_after", retryDelay)
|
||||||
time.Sleep(retryDelay)
|
time.Sleep(retryDelay)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
163
common/rpc/pvp_match.go
Normal file
163
common/rpc/pvp_match.go
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
package rpc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"blazing/cool"
|
||||||
|
"blazing/logic/service/fight/pvpwire"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
pvpMatchQueueTTL = 12 * time.Second
|
||||||
|
pvpMatchBanPickSecond = 45
|
||||||
|
)
|
||||||
|
|
||||||
|
type PVPMatchJoinPayload struct {
|
||||||
|
RuntimeServerID uint32 `json:"runtimeServerId"`
|
||||||
|
UserID uint32 `json:"userId"`
|
||||||
|
Nick string `json:"nick"`
|
||||||
|
FightMode uint32 `json:"fightMode"`
|
||||||
|
Status uint32 `json:"status"`
|
||||||
|
CatchTimes []uint32 `json:"catchTimes"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type pvpMatchCoordinator struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
queues map[uint32][]pvpwire.QueuePlayerSnapshot
|
||||||
|
lastSeen map[uint32]time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
var defaultPVPMatchCoordinator = &pvpMatchCoordinator{
|
||||||
|
queues: make(map[uint32][]pvpwire.QueuePlayerSnapshot),
|
||||||
|
lastSeen: make(map[uint32]time.Time),
|
||||||
|
}
|
||||||
|
|
||||||
|
func DefaultPVPMatchCoordinator() *pvpMatchCoordinator {
|
||||||
|
return defaultPVPMatchCoordinator
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *pvpMatchCoordinator) JoinOrUpdate(payload PVPMatchJoinPayload) error {
|
||||||
|
if payload.UserID == 0 || payload.RuntimeServerID == 0 || payload.FightMode == 0 {
|
||||||
|
return fmt.Errorf("invalid pvp match payload: uid=%d server=%d mode=%d", payload.UserID, payload.RuntimeServerID, payload.FightMode)
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
player := pvpwire.QueuePlayerSnapshot{
|
||||||
|
RuntimeServerID: payload.RuntimeServerID,
|
||||||
|
UserID: payload.UserID,
|
||||||
|
Nick: payload.Nick,
|
||||||
|
FightMode: payload.FightMode,
|
||||||
|
Status: payload.Status,
|
||||||
|
JoinedAtUnix: now.Unix(),
|
||||||
|
CatchTimes: append([]uint32(nil), payload.CatchTimes...),
|
||||||
|
}
|
||||||
|
|
||||||
|
var match *pvpwire.MatchFoundPayload
|
||||||
|
|
||||||
|
m.mu.Lock()
|
||||||
|
m.pruneExpiredLocked(now)
|
||||||
|
m.removeUserLocked(payload.UserID)
|
||||||
|
m.lastSeen[payload.UserID] = now
|
||||||
|
|
||||||
|
queue := m.queues[payload.FightMode]
|
||||||
|
if len(queue) > 0 {
|
||||||
|
host := queue[0]
|
||||||
|
queue = queue[1:]
|
||||||
|
m.queues[payload.FightMode] = queue
|
||||||
|
delete(m.lastSeen, host.UserID)
|
||||||
|
delete(m.lastSeen, payload.UserID)
|
||||||
|
|
||||||
|
result := pvpwire.MatchFoundPayload{
|
||||||
|
SessionID: buildPVPMatchSessionID(host.UserID, payload.UserID),
|
||||||
|
Stage: pvpwire.StageBanPick,
|
||||||
|
Host: host,
|
||||||
|
Guest: player,
|
||||||
|
BanPickTimeout: pvpMatchBanPickSecond,
|
||||||
|
}
|
||||||
|
match = &result
|
||||||
|
} else {
|
||||||
|
m.queues[payload.FightMode] = append(queue, player)
|
||||||
|
}
|
||||||
|
m.mu.Unlock()
|
||||||
|
|
||||||
|
if match == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err := publishPVPMatchMessage(pvpwire.ServerTopic(match.Host.RuntimeServerID), pvpwire.MessageTypeMatchFound, *match); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if match.Guest.RuntimeServerID != match.Host.RuntimeServerID {
|
||||||
|
if err := publishPVPMatchMessage(pvpwire.ServerTopic(match.Guest.RuntimeServerID), pvpwire.MessageTypeMatchFound, *match); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *pvpMatchCoordinator) Cancel(userID uint32) {
|
||||||
|
if userID == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
|
||||||
|
delete(m.lastSeen, userID)
|
||||||
|
m.removeUserLocked(userID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *pvpMatchCoordinator) pruneExpiredLocked(now time.Time) {
|
||||||
|
for mode, queue := range m.queues {
|
||||||
|
next := make([]pvpwire.QueuePlayerSnapshot, 0, len(queue))
|
||||||
|
for _, queued := range queue {
|
||||||
|
last := m.lastSeen[queued.UserID]
|
||||||
|
if last.IsZero() || now.Sub(last) > pvpMatchQueueTTL {
|
||||||
|
delete(m.lastSeen, queued.UserID)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
next = append(next, queued)
|
||||||
|
}
|
||||||
|
m.queues[mode] = next
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *pvpMatchCoordinator) removeUserLocked(userID uint32) {
|
||||||
|
for mode, queue := range m.queues {
|
||||||
|
next := make([]pvpwire.QueuePlayerSnapshot, 0, len(queue))
|
||||||
|
for _, queued := range queue {
|
||||||
|
if queued.UserID == userID {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
next = append(next, queued)
|
||||||
|
}
|
||||||
|
m.queues[mode] = next
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func publishPVPMatchMessage(topic, msgType string, body any) error {
|
||||||
|
payload, err := json.Marshal(body)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
envelope, err := json.Marshal(pvpwire.Envelope{
|
||||||
|
Type: msgType,
|
||||||
|
Body: payload,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
conn, err := cool.Redis.Conn(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer conn.Close(context.Background())
|
||||||
|
|
||||||
|
_, err = conn.Do(context.Background(), "publish", topic, envelope)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildPVPMatchSessionID(hostUserID, guestUserID uint32) string {
|
||||||
|
return fmt.Sprintf("xsvr-%d-%d-%d", hostUserID, guestUserID, time.Now().UnixNano())
|
||||||
|
}
|
||||||
@@ -98,6 +98,15 @@ func (*ServerHandler) RegisterLogic(ctx context.Context, id, port uint32) error
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (*ServerHandler) MatchJoinOrUpdate(_ context.Context, payload PVPMatchJoinPayload) error {
|
||||||
|
return DefaultPVPMatchCoordinator().JoinOrUpdate(payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*ServerHandler) MatchCancel(_ context.Context, userID uint32) error {
|
||||||
|
DefaultPVPMatchCoordinator().Cancel(userID)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func CServer() *jsonrpc.RPCServer {
|
func CServer() *jsonrpc.RPCServer {
|
||||||
// create a new server instance
|
// create a new server instance
|
||||||
rpcServer := jsonrpc.NewServer(jsonrpc.WithReverseClient[cool.ClientHandler](""))
|
rpcServer := jsonrpc.NewServer(jsonrpc.WithReverseClient[cool.ClientHandler](""))
|
||||||
@@ -114,6 +123,10 @@ func StartClient(id, port uint32, callback any) *struct {
|
|||||||
Kick func(uint32) error
|
Kick func(uint32) error
|
||||||
|
|
||||||
RegisterLogic func(uint32, uint32) error
|
RegisterLogic func(uint32, uint32) error
|
||||||
|
|
||||||
|
MatchJoinOrUpdate func(PVPMatchJoinPayload) error
|
||||||
|
|
||||||
|
MatchCancel func(uint32) error
|
||||||
} {
|
} {
|
||||||
//cool.Config.File.Domain = "127.0.0.1"
|
//cool.Config.File.Domain = "127.0.0.1"
|
||||||
var rpcaddr = "ws://" + cool.Config.File.Domain + gconv.String(cool.Config.Address) + "/rpc"
|
var rpcaddr = "ws://" + cool.Config.File.Domain + gconv.String(cool.Config.Address) + "/rpc"
|
||||||
@@ -144,6 +157,10 @@ var RPCClient struct {
|
|||||||
|
|
||||||
RegisterLogic func(uint32, uint32) error
|
RegisterLogic func(uint32, uint32) error
|
||||||
|
|
||||||
|
MatchJoinOrUpdate func(PVPMatchJoinPayload) error
|
||||||
|
|
||||||
|
MatchCancel func(uint32) error
|
||||||
|
|
||||||
// UserLogin func(int32, int32) error //用户登录事件
|
// UserLogin func(int32, int32) error //用户登录事件
|
||||||
// UserLogout func(int32, int32) error //用户登出事件
|
// UserLogout func(int32, int32) error //用户登出事件
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,24 +1,29 @@
|
|||||||
package socket
|
package socket
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"blazing/common/socket/codec"
|
||||||
|
"blazing/cool"
|
||||||
|
"blazing/logic/service/player"
|
||||||
|
"blazing/modules/config/service"
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"errors"
|
"errors"
|
||||||
"io"
|
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"blazing/cool"
|
|
||||||
"blazing/logic/service/player"
|
|
||||||
"blazing/modules/config/service"
|
|
||||||
|
|
||||||
"github.com/gogf/gf/v2/frame/g"
|
"github.com/gogf/gf/v2/frame/g"
|
||||||
"github.com/gogf/gf/v2/os/gtime"
|
"github.com/gogf/gf/v2/os/gtime"
|
||||||
"github.com/panjf2000/gnet/v2"
|
"github.com/panjf2000/gnet/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
minPacketLen = 17
|
||||||
|
maxPacketLen = 10 * 1024
|
||||||
|
)
|
||||||
|
|
||||||
func (s *Server) Boot(serverid, port uint32) error {
|
func (s *Server) Boot(serverid, port uint32) error {
|
||||||
// go s.bootws()
|
// go s.bootws()
|
||||||
s.serverid = serverid
|
s.serverid = serverid
|
||||||
@@ -53,36 +58,22 @@ func (s *Server) Stop() error {
|
|||||||
func (s *Server) OnClose(c gnet.Conn, err error) (action gnet.Action) {
|
func (s *Server) OnClose(c gnet.Conn, err error) (action gnet.Action) {
|
||||||
defer func() {
|
defer func() {
|
||||||
if err := recover(); err != nil { // 恢复 panic,err 为 panic 错误值
|
if err := recover(); err != nil { // 恢复 panic,err 为 panic 错误值
|
||||||
// 1. 打印错误信息
|
|
||||||
if t, ok := c.Context().(*player.ClientData); ok {
|
if t, ok := c.Context().(*player.ClientData); ok {
|
||||||
if t.Player != nil {
|
if t.Player != nil {
|
||||||
if t.Player.Info != nil {
|
if t.Player.Info != nil {
|
||||||
cool.Logger.Error(context.TODO(), "OnClose 错误:", cool.Config.ServerInfo.OnlineID, t.Player.Info.UserID, err)
|
cool.Logger.Error(context.TODO(), "OnClose 错误:", cool.Config.ServerInfo.OnlineID, t.Player.Info.UserID, err)
|
||||||
t.Player.Service.Info.Save(*t.Player.Info)
|
go t.Player.SaveOnDisconnect()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
cool.Logger.Error(context.TODO(), "OnClose 错误:", cool.Config.ServerInfo.OnlineID, err)
|
cool.Logger.Error(context.TODO(), "OnClose 错误:", cool.Config.ServerInfo.OnlineID, err)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
// 识别 RST 导致的连接中断(错误信息含 "connection reset")
|
|
||||||
// if err != nil && (strings.Contains(err.Error(), "connection reset") || strings.Contains(err.Error(), "reset by peer")) {
|
|
||||||
// remoteIP := c.RemoteAddr().(*net.TCPAddr).IP.String()
|
|
||||||
|
|
||||||
// log.Printf("RST 攻击检测: 来源 %s, 累计攻击次数 %d", remoteIP)
|
|
||||||
|
|
||||||
// // 防护逻辑:临时封禁异常 IP(可扩展为 IP 黑名单)
|
|
||||||
// // go s.tempBlockIP(remoteIP, 5*time.Minute)
|
|
||||||
// }
|
|
||||||
//fmt.Println(err, c.RemoteAddr().String(), "断开连接")
|
|
||||||
atomic.AddInt64(&cool.Connected, -1)
|
atomic.AddInt64(&cool.Connected, -1)
|
||||||
|
|
||||||
//logging.Infof("conn[%v] disconnected", c.RemoteAddr().String())
|
|
||||||
v, _ := c.Context().(*player.ClientData)
|
v, _ := c.Context().(*player.ClientData)
|
||||||
if v != nil {
|
if v != nil {
|
||||||
v.Close()
|
v.Close()
|
||||||
@@ -90,23 +81,20 @@ func (s *Server) OnClose(c gnet.Conn, err error) (action gnet.Action) {
|
|||||||
v.Player.Save() //保存玩家数据
|
v.Player.Save() //保存玩家数据
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//}
|
|
||||||
//关闭连接
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) OnTick() (delay time.Duration, action gnet.Action) {
|
func (s *Server) OnTick() (delay time.Duration, action gnet.Action) {
|
||||||
g.Log().Async().Info(context.Background(), gtime.Now().ISO8601(), "服务器ID", cool.Config.ServerInfo.OnlineID, "链接数", atomic.LoadInt64(&cool.Connected))
|
g.Log().Async().Info(context.Background(), gtime.Now().ISO8601(), "服务器ID", cool.Config.ServerInfo.OnlineID, "链接数", atomic.LoadInt64(&cool.Connected))
|
||||||
if s.quit && atomic.LoadInt64(&cool.Connected) == 0 {
|
if s.quit && atomic.LoadInt64(&cool.Connected) == 0 {
|
||||||
//执行正常退出逻辑
|
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
return 30 * time.Second, gnet.None
|
return 30 * time.Second, gnet.None
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) OnBoot(eng gnet.Engine) gnet.Action {
|
func (s *Server) OnBoot(eng gnet.Engine) gnet.Action {
|
||||||
s.eng = eng
|
s.eng = eng
|
||||||
|
service.NewServerService().SetServerID(s.serverid, s.port)
|
||||||
service.NewServerService().SetServerID(s.serverid, s.port) //设置当前服务器端口
|
|
||||||
return gnet.None
|
return gnet.None
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,59 +102,68 @@ func (s *Server) OnOpen(conn gnet.Conn) (out []byte, action gnet.Action) {
|
|||||||
if s.network != "tcp" {
|
if s.network != "tcp" {
|
||||||
return nil, gnet.Close
|
return nil, gnet.Close
|
||||||
}
|
}
|
||||||
|
|
||||||
if conn.Context() == nil {
|
if conn.Context() == nil {
|
||||||
conn.SetContext(player.NewClientData(conn)) //注入data
|
conn.SetContext(player.NewClientData(conn))
|
||||||
}
|
}
|
||||||
|
|
||||||
atomic.AddInt64(&cool.Connected, 1)
|
atomic.AddInt64(&cool.Connected, 1)
|
||||||
|
|
||||||
return nil, gnet.None
|
return nil, gnet.None
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) OnTraffic(c gnet.Conn) (action gnet.Action) {
|
func (s *Server) OnTraffic(c gnet.Conn) (action gnet.Action) {
|
||||||
defer func() {
|
defer func() {
|
||||||
if err := recover(); err != nil { // 恢复 panic,err 为 panic 错误值
|
if err := recover(); err != nil {
|
||||||
// 1. 打印错误信息
|
|
||||||
if t, ok := c.Context().(*player.ClientData); ok {
|
if t, ok := c.Context().(*player.ClientData); ok {
|
||||||
if t.Player != nil {
|
if t.Player != nil && t.Player.Info != nil {
|
||||||
if t.Player.Info != nil {
|
|
||||||
cool.Logger.Error(context.TODO(), "OnTraffic 错误:", cool.Config.ServerInfo.OnlineID, t.Player.Info.UserID, err)
|
cool.Logger.Error(context.TODO(), "OnTraffic 错误:", cool.Config.ServerInfo.OnlineID, t.Player.Info.UserID, err)
|
||||||
t.Player.Service.Info.Save(*t.Player.Info)
|
t.Player.Service.Info.Save(*t.Player.Info)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
ws := c.Context().(*player.ClientData).Wsmsg
|
client := c.Context().(*player.ClientData)
|
||||||
if ws.Tcp { //升级失败时候防止缓冲区溢出
|
if s.discorse && !client.IsCrossDomainChecked() {
|
||||||
return s.handleTCP(c)
|
handled, ready, action := handle(c)
|
||||||
|
if action != gnet.None {
|
||||||
|
return action
|
||||||
|
}
|
||||||
|
if handled {
|
||||||
|
client.MarkCrossDomainChecked()
|
||||||
|
return gnet.None
|
||||||
|
}
|
||||||
|
if !ready {
|
||||||
|
return gnet.None
|
||||||
|
}
|
||||||
|
client.MarkCrossDomainChecked()
|
||||||
}
|
}
|
||||||
|
|
||||||
tt, len1 := ws.ReadBufferBytes(c)
|
ws := client.Wsmsg
|
||||||
if tt == gnet.Close {
|
if ws.Tcp {
|
||||||
|
return s.handleTCP(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
readAction, inboundLen := ws.ReadBufferBytes(c)
|
||||||
|
if readAction == gnet.Close {
|
||||||
return gnet.Close
|
return gnet.Close
|
||||||
}
|
}
|
||||||
|
|
||||||
ok, action := ws.Upgrade(c)
|
state, action := ws.Upgrade(c)
|
||||||
if action != gnet.None { //连接断开
|
if action != gnet.None {
|
||||||
return action
|
return action
|
||||||
}
|
}
|
||||||
if !ok { //升级失败,说明是tcp连接
|
if state == player.UpgradeNeedMoreData {
|
||||||
ws.Tcp = true
|
return gnet.None
|
||||||
|
}
|
||||||
return s.handleTCP(c)
|
if state == player.UpgradeUseTCP {
|
||||||
|
return s.handleTCP(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
if inboundLen > 0 {
|
||||||
|
if _, err := c.Discard(inboundLen); err != nil {
|
||||||
|
return gnet.Close
|
||||||
|
}
|
||||||
|
ws.ResetInboundMirror()
|
||||||
}
|
}
|
||||||
// fmt.Println(ws.Buf.Bytes())
|
|
||||||
c.Discard(len1)
|
|
||||||
|
|
||||||
messages, err := ws.Decode(c)
|
messages, err := ws.Decode(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -177,91 +174,93 @@ func (s *Server) OnTraffic(c gnet.Conn) (action gnet.Action) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, msg := range messages {
|
for _, msg := range messages {
|
||||||
|
if !s.onevent(c, msg.Payload) {
|
||||||
s.onevent(c, msg.Payload)
|
return gnet.Close
|
||||||
//t.OnEvent(msg.Payload)
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return gnet.None
|
return gnet.None
|
||||||
}
|
}
|
||||||
|
|
||||||
const maxBodyLen = 10 * 1024 // 业务最大包体长度,按需调整
|
|
||||||
func (s *Server) handleTCP(conn gnet.Conn) (action gnet.Action) {
|
func (s *Server) handleTCP(conn gnet.Conn) (action gnet.Action) {
|
||||||
|
client := conn.Context().(*player.ClientData)
|
||||||
|
if s.discorse && !client.IsCrossDomainChecked() {
|
||||||
|
handled, ready, action := handle(conn)
|
||||||
|
if action != gnet.None {
|
||||||
|
return action
|
||||||
|
}
|
||||||
|
if !ready {
|
||||||
|
return gnet.None
|
||||||
|
}
|
||||||
|
if handled {
|
||||||
|
client.MarkCrossDomainChecked()
|
||||||
|
return gnet.None
|
||||||
|
}
|
||||||
|
client.MarkCrossDomainChecked()
|
||||||
|
}
|
||||||
|
|
||||||
conn.Context().(*player.ClientData).IsCrossDomain.Do(func() { //跨域检测
|
body, err := s.codec.Decode(conn)
|
||||||
handle(conn)
|
|
||||||
})
|
|
||||||
|
|
||||||
// handle(c)
|
|
||||||
// 先读取4字节的包长度
|
|
||||||
lenBuf, err := conn.Peek(4)
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, io.ErrShortBuffer) {
|
if errors.Is(err, codec.ErrIncompletePacket) {
|
||||||
return
|
return gnet.None
|
||||||
}
|
}
|
||||||
return gnet.Close
|
return gnet.Close
|
||||||
}
|
}
|
||||||
|
if !s.onevent(conn, body) {
|
||||||
bodyLen := binary.BigEndian.Uint32(lenBuf)
|
|
||||||
|
|
||||||
if bodyLen > maxBodyLen {
|
|
||||||
return gnet.Close
|
return gnet.Close
|
||||||
}
|
}
|
||||||
|
|
||||||
if conn.InboundBuffered() < int(bodyLen) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// 提取包体
|
|
||||||
body, err := conn.Next(int(bodyLen))
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, io.ErrShortBuffer) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
return gnet.Close
|
|
||||||
}
|
|
||||||
|
|
||||||
s.onevent(conn, body)
|
|
||||||
|
|
||||||
if conn.InboundBuffered() > 0 {
|
if conn.InboundBuffered() > 0 {
|
||||||
if err := conn.Wake(nil); err != nil { // wake up the connection manually to avoid missing the leftover data
|
if err := conn.Wake(nil); err != nil {
|
||||||
|
|
||||||
return gnet.Close
|
return gnet.Close
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return action
|
return action
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// CROSS_DOMAIN 定义跨域策略文件内容
|
|
||||||
const CROSS_DOMAIN = "<?xml version=\"1.0\"?><!DOCTYPE cross-domain-policy><cross-domain-policy><allow-access-from domain=\"*\" to-ports=\"*\" /></cross-domain-policy>\x00"
|
const CROSS_DOMAIN = "<?xml version=\"1.0\"?><!DOCTYPE cross-domain-policy><cross-domain-policy><allow-access-from domain=\"*\" to-ports=\"*\" /></cross-domain-policy>\x00"
|
||||||
|
|
||||||
// TEXT 定义跨域请求的文本格式
|
|
||||||
const TEXT = "<policy-file-request/>\x00"
|
const TEXT = "<policy-file-request/>\x00"
|
||||||
|
|
||||||
func handle(c gnet.Conn) {
|
func handle(c gnet.Conn) (handled bool, ready bool, action gnet.Action) {
|
||||||
|
probeLen := c.InboundBuffered()
|
||||||
|
if probeLen == 0 {
|
||||||
|
return false, false, gnet.None
|
||||||
|
}
|
||||||
|
if probeLen > len(TEXT) {
|
||||||
|
probeLen = len(TEXT)
|
||||||
|
}
|
||||||
|
|
||||||
// 读取数据并检查是否为跨域请求
|
data, err := c.Peek(probeLen)
|
||||||
data, err := c.Peek(len(TEXT))
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Error reading cross-domain request: %v", err)
|
log.Printf("Error reading cross-domain request: %v", err)
|
||||||
return
|
return false, false, gnet.Close
|
||||||
}
|
}
|
||||||
|
if !bytes.Equal(data, []byte(TEXT[:probeLen])) {
|
||||||
if string(data) == TEXT { //判断是否是跨域请求
|
return false, true, gnet.None
|
||||||
//log.Printf("Received cross-domain request from %s", c.RemoteAddr())
|
|
||||||
// 处理跨域请求
|
|
||||||
c.Write([]byte(CROSS_DOMAIN))
|
|
||||||
c.Discard(len(TEXT))
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
if probeLen < len(TEXT) {
|
||||||
//return
|
return false, false, gnet.None
|
||||||
|
}
|
||||||
|
if _, err := c.Write([]byte(CROSS_DOMAIN)); err != nil {
|
||||||
|
return false, true, gnet.Close
|
||||||
|
}
|
||||||
|
if _, err := c.Discard(len(TEXT)); err != nil {
|
||||||
|
return false, true, gnet.Close
|
||||||
|
}
|
||||||
|
return true, true, gnet.None
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) onevent(c gnet.Conn, v []byte) {
|
func (s *Server) onevent(c gnet.Conn, v []byte) bool {
|
||||||
|
if !isValidPacket(v) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
if t, ok := c.Context().(*player.ClientData); ok {
|
if t, ok := c.Context().(*player.ClientData); ok {
|
||||||
t.PushEvent(v, s.workerPool.Submit)
|
t.PushEvent(v, s.workerPool.Submit)
|
||||||
}
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func isValidPacket(v []byte) bool {
|
||||||
|
if len(v) < minPacketLen || len(v) > maxPacketLen {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return binary.BigEndian.Uint32(v[0:4]) == uint32(len(v))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,33 +2,4 @@ module github.com/zmexing/go-sensitive-word
|
|||||||
|
|
||||||
go 1.20
|
go 1.20
|
||||||
|
|
||||||
require (
|
require github.com/orcaman/concurrent-map/v2 v2.0.1
|
||||||
|
|
||||||
github.com/orcaman/concurrent-map/v2 v2.0.1
|
|
||||||
)
|
|
||||||
|
|
||||||
require (
|
|
||||||
github.com/andybalholm/brotli v1.1.0 // indirect
|
|
||||||
github.com/cloudflare/circl v1.3.7 // indirect
|
|
||||||
github.com/go-logr/logr v1.4.2 // indirect
|
|
||||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
|
|
||||||
github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7 // indirect
|
|
||||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
|
||||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
|
||||||
github.com/klauspost/compress v1.17.9 // indirect
|
|
||||||
github.com/onsi/ginkgo/v2 v2.16.0 // indirect
|
|
||||||
github.com/quic-go/qpack v0.4.0 // indirect
|
|
||||||
github.com/quic-go/qtls-go1-20 v0.4.1 // indirect
|
|
||||||
github.com/quic-go/quic-go v0.40.1 // indirect
|
|
||||||
github.com/refraction-networking/utls v1.6.3 // indirect
|
|
||||||
github.com/stretchr/testify v1.11.1 // indirect
|
|
||||||
go.uber.org/mock v0.4.0 // indirect
|
|
||||||
golang.org/x/crypto v0.31.0 // indirect
|
|
||||||
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 // indirect
|
|
||||||
golang.org/x/mod v0.17.0 // indirect
|
|
||||||
golang.org/x/net v0.33.0 // indirect
|
|
||||||
golang.org/x/sys v0.30.0 // indirect
|
|
||||||
golang.org/x/text v0.22.0 // indirect
|
|
||||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
|
|
||||||
google.golang.org/protobuf v1.31.0 // indirect
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -1,55 +1,2 @@
|
|||||||
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
|
|
||||||
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
|
|
||||||
github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU=
|
|
||||||
github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA=
|
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
|
||||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
|
||||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
|
|
||||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
|
|
||||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
|
||||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
|
||||||
github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7 h1:y3N7Bm7Y9/CtpiVkw/ZWj6lSlDF3F74SfKwfTCer72Q=
|
|
||||||
github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik=
|
|
||||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
|
||||||
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
|
|
||||||
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
|
||||||
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
|
|
||||||
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
|
||||||
github.com/imroc/req/v3 v3.42.3 h1:ryPG2AiwouutAopwPxKpWKyxgvO8fB3hts4JXlh3PaE=
|
|
||||||
github.com/imroc/req/v3 v3.42.3/go.mod h1:Axz9Y/a2b++w5/Jht3IhQsdBzrG1ftJd1OJhu21bB2Q=
|
|
||||||
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
|
|
||||||
github.com/onsi/ginkgo/v2 v2.16.0 h1:7q1w9frJDzninhXxjZd+Y/x54XNjG/UlRLIYPZafsPM=
|
|
||||||
github.com/onsi/ginkgo/v2 v2.16.0/go.mod h1:llBI3WDLL9Z6taip6f33H76YcWtJv+7R3HigUjbIBOs=
|
|
||||||
github.com/onsi/gomega v1.30.0 h1:hvMK7xYz4D3HapigLTeGdId/NcfQx1VHMJc60ew99+8=
|
|
||||||
github.com/orcaman/concurrent-map/v2 v2.0.1 h1:jOJ5Pg2w1oeB6PeDurIYf6k9PQ+aTITr/6lP/L/zp6c=
|
github.com/orcaman/concurrent-map/v2 v2.0.1 h1:jOJ5Pg2w1oeB6PeDurIYf6k9PQ+aTITr/6lP/L/zp6c=
|
||||||
github.com/orcaman/concurrent-map/v2 v2.0.1/go.mod h1:9Eq3TG2oBe5FirmYWQfYO5iH1q0Jv47PLaNK++uCdOM=
|
github.com/orcaman/concurrent-map/v2 v2.0.1/go.mod h1:9Eq3TG2oBe5FirmYWQfYO5iH1q0Jv47PLaNK++uCdOM=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
|
||||||
github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo=
|
|
||||||
github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A=
|
|
||||||
github.com/quic-go/qtls-go1-20 v0.4.1 h1:D33340mCNDAIKBqXuAvexTNMUByrYmFYVfKfDN5nfFs=
|
|
||||||
github.com/quic-go/qtls-go1-20 v0.4.1/go.mod h1:X9Nh97ZL80Z+bX/gUXMbipO6OxdiDi58b/fMC9mAL+k=
|
|
||||||
github.com/quic-go/quic-go v0.40.1 h1:X3AGzUNFs0jVuO3esAGnTfvdgvL4fq655WaOi1snv1Q=
|
|
||||||
github.com/quic-go/quic-go v0.40.1/go.mod h1:PeN7kuVJ4xZbxSv/4OX6S1USOX8MJvydwpTx31vx60c=
|
|
||||||
github.com/refraction-networking/utls v1.6.3 h1:MFOfRN35sSx6K5AZNIoESsBuBxS2LCgRilRIdHb6fDc=
|
|
||||||
github.com/refraction-networking/utls v1.6.3/go.mod h1:yil9+7qSl+gBwJqztoQseO6Pr3h62pQoY1lXiNR/FPs=
|
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
|
||||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
|
||||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
|
||||||
go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU=
|
|
||||||
go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
|
|
||||||
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
|
|
||||||
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 h1:LfspQV/FYTatPTr/3HzIcmiUFH7PGP+OQ6mgDYo3yuQ=
|
|
||||||
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc=
|
|
||||||
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
|
|
||||||
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
|
|
||||||
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
|
|
||||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
|
||||||
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
|
||||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
|
|
||||||
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
|
||||||
|
|||||||
224
docs/pvp-login-rpc-match-design-2026-04-09.md
Normal file
224
docs/pvp-login-rpc-match-design-2026-04-09.md
Normal file
@@ -0,0 +1,224 @@
|
|||||||
|
# PVP Match Via RPC, Battle Via Redis
|
||||||
|
|
||||||
|
## 目标
|
||||||
|
|
||||||
|
本次调整先不解决 `login` 更新期间的排队保活和补偿问题,只收敛到一个更简单、可控的方案:
|
||||||
|
|
||||||
|
- 匹配请求走 `logic -> login` 的同步 RPC
|
||||||
|
- 对战过程仍走 `logic` 本地战斗 + Redis 转发战斗指令
|
||||||
|
- `login` 不可用时,`logic` 直接返回“匹配服务不可用”
|
||||||
|
- 前端通过轮询重新发起 / 更新匹配请求,不在后端保留离线补偿队列
|
||||||
|
|
||||||
|
这个方案的核心是:先把“能否立即判断匹配服务可用”做好,不继续依赖 Redis PubSub 做匹配入口。
|
||||||
|
|
||||||
|
## 当前现状
|
||||||
|
|
||||||
|
### 现有匹配入口
|
||||||
|
|
||||||
|
- 前端 `2458` 进入 [logic/controller/fight_巅峰.go](/workspace/logic/controller/fight_巅峰.go#L19)
|
||||||
|
- 当前 `JoINtop` 直接调用 [logic/service/fight/pvp/service.go](/workspace/logic/service/fight/pvp/service.go#L83) 的 `JoinPeakQueue`
|
||||||
|
- `JoinPeakQueue` 当前实现是本地建 `localQueueTicket`,并通过 Redis `publish` 发 `queue_join`
|
||||||
|
|
||||||
|
### 现有跨服协调
|
||||||
|
|
||||||
|
- `logic` 侧订阅 PVP Redis topic 的入口在 [common/rpc/func.go](/workspace/common/rpc/func.go#L153)
|
||||||
|
- PVP 匹配状态当前存在 `logic/service/fight/pvp/service.go` 的 manager 内存里:
|
||||||
|
- `queues`
|
||||||
|
- `lastSeen`
|
||||||
|
- `localQueues`
|
||||||
|
- `sessions`
|
||||||
|
- `userSession`
|
||||||
|
|
||||||
|
### 现有 RPC 能力
|
||||||
|
|
||||||
|
- `logic` 启动时通过 [common/rpc/rpc.go](/workspace/common/rpc/rpc.go#L113) 建立到 `login` 的 RPC client
|
||||||
|
- `login` 的 `/rpc/*` 入口绑定在 [modules/base/middleware/middleware.go](/workspace/modules/base/middleware/middleware.go#L152)
|
||||||
|
- `login` 侧 RPC server 由 [common/rpc/rpc.go](/workspace/common/rpc/rpc.go#L101) 暴露
|
||||||
|
|
||||||
|
### 当前问题
|
||||||
|
|
||||||
|
Redis PubSub 适合“广播消息”,不适合“同步判断服务是否可用”。
|
||||||
|
|
||||||
|
如果继续让匹配入口走 PubSub:
|
||||||
|
|
||||||
|
- `logic` 无法在请求当下知道 `login` 是否真能处理
|
||||||
|
- `login` 更新、重启、未订阅时,匹配请求可能直接丢失
|
||||||
|
- 前端即使轮询,也只是重复投递,不能精确表达“当前匹配服务可用/不可用”
|
||||||
|
|
||||||
|
## 收敛后的职责划分
|
||||||
|
|
||||||
|
### login
|
||||||
|
|
||||||
|
`login` 只负责匹配控制面:
|
||||||
|
|
||||||
|
- 接收 `logic` 发来的同步匹配 RPC
|
||||||
|
- 判断当前匹配服务是否可用
|
||||||
|
- 维护匹配队列
|
||||||
|
- 找到对手后,记录 match 结果
|
||||||
|
- 再通过 Redis 或其他异步方式通知对应 `logic` 开始 Ban/Pick / Battle
|
||||||
|
|
||||||
|
### logic
|
||||||
|
|
||||||
|
`logic` 只负责:
|
||||||
|
|
||||||
|
- 接收前端匹配请求
|
||||||
|
- 同步 RPC 到 `login`
|
||||||
|
- RPC 失败时立即返回“匹配服务不可用”
|
||||||
|
- RPC 成功时返回“排队中”
|
||||||
|
- 收到 match 结果后负责真正 `fight.NewFight(...)`
|
||||||
|
- 对战期间继续使用现有 Redis topic 转发战斗指令
|
||||||
|
|
||||||
|
### Redis
|
||||||
|
|
||||||
|
Redis 只保留在“对战消息面”:
|
||||||
|
|
||||||
|
- `match_found`
|
||||||
|
- `ban_pick_submit`
|
||||||
|
- `battle_command`
|
||||||
|
- `packet_relay`
|
||||||
|
- `session_close`
|
||||||
|
|
||||||
|
也就是说:
|
||||||
|
|
||||||
|
- 匹配入口走 RPC
|
||||||
|
- 对战过程走 Redis
|
||||||
|
|
||||||
|
## 推荐目标链路
|
||||||
|
|
||||||
|
### 1. 前端加入/更新匹配
|
||||||
|
|
||||||
|
前端定期轮询 `logic` 的加入/更新接口。
|
||||||
|
|
||||||
|
`logic` 处理流程:
|
||||||
|
|
||||||
|
1. 校验玩家当前战斗状态
|
||||||
|
2. 同步调用 `login` 的匹配 RPC
|
||||||
|
3. 如果 RPC 成功:返回排队中
|
||||||
|
4. 如果 RPC 失败:清理本地匹配状态,返回匹配服务不可用
|
||||||
|
|
||||||
|
### 2. login 完成匹配
|
||||||
|
|
||||||
|
`login` 维护排队队列和匹配结果,匹配成功后:
|
||||||
|
|
||||||
|
1. 确定 host / guest 所在 `logic`
|
||||||
|
2. 通过 Redis 通知两个 `logic`
|
||||||
|
3. host `logic` 开战
|
||||||
|
4. guest `logic` 设置远端代理并进入 Ban/Pick 或战斗态
|
||||||
|
|
||||||
|
### 3. 对战期间
|
||||||
|
|
||||||
|
继续复用当前 `logic/service/fight/pvp/service.go` 内的 Redis 指令转发模式:
|
||||||
|
|
||||||
|
- 战斗操作通过 Redis topic 转发
|
||||||
|
- host `logic` 维持真实战斗对象
|
||||||
|
- guest `logic` 维持 remote proxy
|
||||||
|
|
||||||
|
## 失败语义
|
||||||
|
|
||||||
|
本阶段不做补偿,不做离线保队列。
|
||||||
|
|
||||||
|
### login 不在线
|
||||||
|
|
||||||
|
如果 `logic -> login` RPC 调用失败:
|
||||||
|
|
||||||
|
- 本次匹配直接失败
|
||||||
|
- `logic` 清理本地匹配状态
|
||||||
|
- 返回前端“匹配服务不可用”
|
||||||
|
|
||||||
|
### 前端轮询停止
|
||||||
|
|
||||||
|
如果前端不再轮询:
|
||||||
|
|
||||||
|
- 视为用户不再持续请求匹配
|
||||||
|
- `logic` 不负责继续保活
|
||||||
|
- 是否从 `login` 队列移除,由 `login` 的超时策略决定
|
||||||
|
|
||||||
|
### login 更新中
|
||||||
|
|
||||||
|
如果 `login` 正在更新:
|
||||||
|
|
||||||
|
- `logic` 的同步 RPC 会失败
|
||||||
|
- 前端当前轮询会收到“匹配服务不可用”
|
||||||
|
- 等 `login` 恢复后,前端下一轮再发起匹配
|
||||||
|
|
||||||
|
这是本阶段明确接受的行为,不在后端做补偿。
|
||||||
|
|
||||||
|
## 最小实现建议
|
||||||
|
|
||||||
|
### 一、先增加 RPC 健康/匹配接口
|
||||||
|
|
||||||
|
在 [common/rpc/rpc.go](/workspace/common/rpc/rpc.go) 增加面向 `logic -> login` 的 RPC 方法。
|
||||||
|
|
||||||
|
建议最小接口:
|
||||||
|
|
||||||
|
- `MatchJoinOrUpdate(PVPMatchJoinPayload) error`
|
||||||
|
- `MatchCancel(userID) error`
|
||||||
|
|
||||||
|
如果需要单独健康检查,也可以加:
|
||||||
|
|
||||||
|
- `MatchPing() error`
|
||||||
|
|
||||||
|
但在最小方案里,`MatchJoinOrUpdate` 自身就可以承担健康检查职责。
|
||||||
|
|
||||||
|
### 二、logic 的匹配入口改为同步 RPC
|
||||||
|
|
||||||
|
改造 [logic/controller/fight_巅峰.go](/workspace/logic/controller/fight_巅峰.go#L19) 和 [logic/service/fight/pvp/service.go](/workspace/logic/service/fight/pvp/service.go#L83):
|
||||||
|
|
||||||
|
- 入口不再直接发布 `queue_join`
|
||||||
|
- 先发 RPC 到 `login`
|
||||||
|
- 成功才更新本地匹配状态
|
||||||
|
- 失败直接返回错误
|
||||||
|
- 取消匹配时通过 `MatchCancel` 做 best-effort 清理
|
||||||
|
|
||||||
|
### 三、保留 Redis 对战链路
|
||||||
|
|
||||||
|
[logic/service/fight/pvp/service.go](/workspace/logic/service/fight/pvp/service.go#L170) 之后的 Redis 消费、match result 处理、Ban/Pick、战斗 relay 不需要一次性重写,可以继续保留。
|
||||||
|
|
||||||
|
调整重点是:
|
||||||
|
|
||||||
|
- 不再让匹配入口依赖 PubSub
|
||||||
|
- 让对战过程继续走 Redis
|
||||||
|
|
||||||
|
## 对前端的要求
|
||||||
|
|
||||||
|
前端不要无脑重复“新 join”,而是按“轮询更新匹配状态”处理。
|
||||||
|
|
||||||
|
建议行为:
|
||||||
|
|
||||||
|
1. 首次点击匹配时发一次加入
|
||||||
|
2. 匹配中每隔 `3~5s` 轮询一次更新
|
||||||
|
3. 如果返回“匹配服务不可用”,前端退出匹配态并提示
|
||||||
|
4. 如果返回“已匹配/进入 Ban/Pick”,前端切换到对应界面
|
||||||
|
|
||||||
|
## 本阶段不做的事
|
||||||
|
|
||||||
|
以下内容明确不在这次最小改造内:
|
||||||
|
|
||||||
|
- `login` 更新期间的排队保活
|
||||||
|
- 持久化消息补偿
|
||||||
|
- `login` 重启后的队列恢复
|
||||||
|
- Redis Stream 化
|
||||||
|
- 多 `login` 实例协调
|
||||||
|
- 匹配服务自动拉起目标 `logic`
|
||||||
|
|
||||||
|
## 后续可选增强
|
||||||
|
|
||||||
|
如果后面要继续提高可用性,可以再逐步演进为:
|
||||||
|
|
||||||
|
1. 匹配入口仍走 RPC
|
||||||
|
2. `login` 内部把队列落 Redis
|
||||||
|
3. 加入 ticket 和续租机制
|
||||||
|
4. login 更新时支持恢复匹配状态
|
||||||
|
|
||||||
|
但这不是当前阶段的目标。
|
||||||
|
|
||||||
|
## 最终收敛结论
|
||||||
|
|
||||||
|
当前阶段建议明确成一句话:
|
||||||
|
|
||||||
|
`匹配走 RPC,对战走 Redis。`
|
||||||
|
|
||||||
|
对应业务语义:
|
||||||
|
|
||||||
|
- 需要立即判断服务可用性的时候,用 RPC
|
||||||
|
- 需要跨服转发战斗消息的时候,用 Redis
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
package controller
|
package controller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"blazing/common/rpc"
|
||||||
"blazing/cool"
|
"blazing/cool"
|
||||||
"blazing/logic/service/common"
|
"blazing/logic/service/common"
|
||||||
"bytes"
|
"bytes"
|
||||||
@@ -16,6 +17,7 @@ import (
|
|||||||
|
|
||||||
"github.com/gogf/gf/v2/os/glog"
|
"github.com/gogf/gf/v2/os/glog"
|
||||||
"github.com/lunixbochs/struc"
|
"github.com/lunixbochs/struc"
|
||||||
|
"github.com/panjf2000/gnet/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Maincontroller 是控制器层共享变量。
|
// Maincontroller 是控制器层共享变量。
|
||||||
@@ -28,6 +30,10 @@ type Controller struct {
|
|||||||
Kick func(uint32) error
|
Kick func(uint32) error
|
||||||
|
|
||||||
RegisterLogic func(uint32, uint32) error
|
RegisterLogic func(uint32, uint32) error
|
||||||
|
|
||||||
|
MatchJoinOrUpdate func(rpc.PVPMatchJoinPayload) error
|
||||||
|
|
||||||
|
MatchCancel func(uint32) error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,19 +51,14 @@ func ParseCmd[T any](data []byte) T {
|
|||||||
// Init 初始化控制器,注册所有cmd处理方法
|
// Init 初始化控制器,注册所有cmd处理方法
|
||||||
// 参数 isGame: 标识是否为游戏服务器(true)或登录服务器(false)
|
// 参数 isGame: 标识是否为游戏服务器(true)或登录服务器(false)
|
||||||
func Init(isGame bool) {
|
func Init(isGame bool) {
|
||||||
// 获取控制器实例的反射值
|
|
||||||
controllerValue := reflect.ValueOf(Maincontroller)
|
controllerValue := reflect.ValueOf(Maincontroller)
|
||||||
|
|
||||||
// 获取控制器类型
|
|
||||||
controllerType := controllerValue.Type()
|
controllerType := controllerValue.Type()
|
||||||
|
|
||||||
// 遍历控制器的所有方法
|
|
||||||
for i := 0; i < controllerType.NumMethod(); i++ {
|
for i := 0; i < controllerType.NumMethod(); i++ {
|
||||||
method := controllerType.Method(i)
|
method := controllerType.Method(i)
|
||||||
methodValue := controllerValue.MethodByName(method.Name)
|
methodValue := controllerValue.Method(i)
|
||||||
methodType := methodValue.Type()
|
methodType := methodValue.Type()
|
||||||
|
|
||||||
// 获取方法第一个参数的类型(请求结构体)
|
|
||||||
if methodType.NumIn() == 0 {
|
if methodType.NumIn() == 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -68,43 +69,46 @@ func Init(isGame bool) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
reqType := reqArgType.Elem()
|
reqType := reqArgType.Elem()
|
||||||
|
binding := getCmdBinding(reqType)
|
||||||
|
|
||||||
// 解析请求结构体中的cmd标签
|
for _, cmd := range binding.cmds {
|
||||||
for _, cmd := range getCmd(reqType) {
|
if cmd == 0 {
|
||||||
if cmd == 0 { // 说明不是有效的注册方法
|
|
||||||
glog.Warning(context.Background(), "方法参数必须包含CMD参数", method.Name, "跳过注册")
|
glog.Warning(context.Background(), "方法参数必须包含CMD参数", method.Name, "跳过注册")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// 根据服务器类型过滤cmd
|
if methodType.NumIn() != 2 {
|
||||||
// 登录服务器只处理小于1000的cmd
|
glog.Warning(context.Background(), "方法参数数量必须为2", method.Name, "跳过注册")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
if !isGame && cmd > 1000 {
|
if !isGame && cmd > 1000 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// 游戏服务器只处理大于等于1000的cmd
|
|
||||||
if isGame && cmd < 1000 {
|
if isGame && cmd < 1000 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// 注册命令处理函数
|
|
||||||
if cool.Config.ServerInfo.IsDebug != 0 {
|
if cool.Config.ServerInfo.IsDebug != 0 {
|
||||||
fmt.Println("注册方法", cmd, method.Name)
|
fmt.Println("注册方法", cmd, method.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
reqTypeForNew := reqType
|
||||||
cmdInfo := cool.Cmd{
|
cmdInfo := cool.Cmd{
|
||||||
Func: methodValue,
|
Func: methodValue,
|
||||||
Req: reqType,
|
Req: reqType,
|
||||||
|
HeaderFieldIndex: append([]int(nil), binding.headerFieldIndex...),
|
||||||
// Res: , // TODO 待实现对不同用户初始化方法以取消全局cmdcache
|
UseConn: methodType.In(1) == connType,
|
||||||
}
|
NewReqFunc: func() interface{} {
|
||||||
// 预编译创建req实例的函数:返回结构体指针
|
|
||||||
reqTypeForNew := reqType
|
|
||||||
cmdInfo.NewReqFunc = func() interface{} {
|
|
||||||
return reflect.New(reqTypeForNew).Interface()
|
return reflect.New(reqTypeForNew).Interface()
|
||||||
|
},
|
||||||
|
NewReqValue: func() reflect.Value {
|
||||||
|
return reflect.New(reqTypeForNew)
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, exists := cool.CmdCache[cmd]; exists { // 方法已存在
|
if _, exists := cool.CmdCache[cmd]; exists {
|
||||||
panic(fmt.Sprintf("命令处理方法已存在,跳过注册 %d %s", cmd, method.Name))
|
panic(fmt.Sprintf("命令处理方法已存在,跳过注册 %d %s", cmd, method.Name))
|
||||||
}
|
}
|
||||||
cool.CmdCache[cmd] = cmdInfo
|
cool.CmdCache[cmd] = cmdInfo
|
||||||
@@ -112,12 +116,20 @@ func Init(isGame bool) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var targetType = reflect.TypeOf(common.TomeeHeader{})
|
var (
|
||||||
var cmdTypeCache sync.Map
|
targetType = reflect.TypeOf(common.TomeeHeader{})
|
||||||
|
connType = reflect.TypeOf((*gnet.Conn)(nil)).Elem()
|
||||||
|
cmdTypeCache sync.Map
|
||||||
|
)
|
||||||
|
|
||||||
// 默认返回值(无匹配字段/解析失败时)
|
// 默认返回值(无匹配字段/解析失败时)
|
||||||
const defaultCmdValue = 0
|
const defaultCmdValue = 0
|
||||||
|
|
||||||
|
type cmdBinding struct {
|
||||||
|
cmds []uint32
|
||||||
|
headerFieldIndex []int
|
||||||
|
}
|
||||||
|
|
||||||
func normalizeStructType(typ reflect.Type) reflect.Type {
|
func normalizeStructType(typ reflect.Type) reflect.Type {
|
||||||
for typ.Kind() == reflect.Ptr {
|
for typ.Kind() == reflect.Ptr {
|
||||||
typ = typ.Elem()
|
typ = typ.Elem()
|
||||||
@@ -125,92 +137,93 @@ func normalizeStructType(typ reflect.Type) reflect.Type {
|
|||||||
return typ
|
return typ
|
||||||
}
|
}
|
||||||
|
|
||||||
// getCmd 从结构体类型中提取绑定的cmd指令(递归查找嵌套结构体,支持值/指针类型的TomeeHeader)
|
// getCmdBinding 从结构体类型中提取绑定的cmd指令和头字段位置。
|
||||||
// 参数 typ: 待解析的结构体类型(支持多层指针)
|
func getCmdBinding(typ reflect.Type) cmdBinding {
|
||||||
// 返回值: 解析到的cmd切片,无匹配/解析失败时返回[defaultCmdValue]
|
|
||||||
func getCmd(typ reflect.Type) []uint32 {
|
|
||||||
typ = normalizeStructType(typ)
|
typ = normalizeStructType(typ)
|
||||||
if cached, ok := cmdTypeCache.Load(typ); ok {
|
if cached, ok := cmdTypeCache.Load(typ); ok {
|
||||||
return cached.([]uint32)
|
return cached.(cmdBinding)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 非结构体类型直接返回默认值
|
|
||||||
if typ.Kind() != reflect.Struct {
|
if typ.Kind() != reflect.Struct {
|
||||||
return []uint32{defaultCmdValue}
|
binding := cmdBinding{cmds: []uint32{defaultCmdValue}}
|
||||||
|
cmdTypeCache.Store(typ, binding)
|
||||||
|
return binding
|
||||||
}
|
}
|
||||||
|
|
||||||
if cmd, ok := findCmd(typ, make(map[reflect.Type]struct{})); ok {
|
if binding, ok := findCmdBinding(typ, make(map[reflect.Type]struct{})); ok {
|
||||||
cmdTypeCache.Store(typ, cmd)
|
cmdTypeCache.Store(typ, binding)
|
||||||
return cmd
|
return binding
|
||||||
}
|
}
|
||||||
|
|
||||||
// 未找到目标字段/所有解析失败,返回默认值
|
binding := cmdBinding{cmds: []uint32{defaultCmdValue}}
|
||||||
defaultCmd := []uint32{defaultCmdValue}
|
cmdTypeCache.Store(typ, binding)
|
||||||
cmdTypeCache.Store(typ, defaultCmd)
|
return binding
|
||||||
return defaultCmd
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func findCmd(typ reflect.Type, visiting map[reflect.Type]struct{}) ([]uint32, bool) {
|
func findCmdBinding(typ reflect.Type, visiting map[reflect.Type]struct{}) (cmdBinding, bool) {
|
||||||
typ = normalizeStructType(typ)
|
typ = normalizeStructType(typ)
|
||||||
if typ.Kind() != reflect.Struct {
|
if typ.Kind() != reflect.Struct {
|
||||||
return nil, false
|
return cmdBinding{}, false
|
||||||
}
|
}
|
||||||
if _, seen := visiting[typ]; seen {
|
if _, seen := visiting[typ]; seen {
|
||||||
return nil, false
|
return cmdBinding{}, false
|
||||||
}
|
}
|
||||||
visiting[typ] = struct{}{}
|
visiting[typ] = struct{}{}
|
||||||
defer delete(visiting, typ)
|
defer delete(visiting, typ)
|
||||||
|
|
||||||
// 遍历结构体字段,查找TomeeHeader字段并解析cmd
|
|
||||||
for i := 0; i < typ.NumField(); i++ {
|
for i := 0; i < typ.NumField(); i++ {
|
||||||
field := typ.Field(i)
|
field := typ.Field(i)
|
||||||
|
|
||||||
// 尝试解析当前字段的cmd标签
|
|
||||||
cmdSlice, isHeader, err := parseCmdTagWithStructField(field)
|
cmdSlice, isHeader, err := parseCmdTagWithStructField(field)
|
||||||
if isHeader && err == nil { // 解析成功,直接返回结果
|
if isHeader && err == nil {
|
||||||
return cmdSlice, true
|
return cmdBinding{
|
||||||
|
cmds: cmdSlice,
|
||||||
|
headerFieldIndex: append([]int(nil), field.Index...),
|
||||||
|
}, true
|
||||||
}
|
}
|
||||||
|
|
||||||
// 递归处理嵌套结构体(值/指针类型)
|
|
||||||
nestedTyp := normalizeStructType(field.Type)
|
nestedTyp := normalizeStructType(field.Type)
|
||||||
if nestedTyp.Kind() == reflect.Struct {
|
if nestedTyp.Kind() != reflect.Struct {
|
||||||
// 递归查找,找到有效cmd则立即返回
|
continue
|
||||||
if nestedCmd, ok := findCmd(nestedTyp, visiting); ok {
|
|
||||||
return nestedCmd, true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, false
|
nestedBinding, ok := findCmdBinding(nestedTyp, visiting)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldIndex := make([]int, 0, len(field.Index)+len(nestedBinding.headerFieldIndex))
|
||||||
|
fieldIndex = append(fieldIndex, field.Index...)
|
||||||
|
fieldIndex = append(fieldIndex, nestedBinding.headerFieldIndex...)
|
||||||
|
nestedBinding.headerFieldIndex = fieldIndex
|
||||||
|
return nestedBinding, true
|
||||||
|
}
|
||||||
|
|
||||||
|
return cmdBinding{}, false
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseCmdTagWithStructField 校验字段是否为TomeeHeader(值/指针)并解析cmd标签
|
// parseCmdTagWithStructField 校验字段是否为TomeeHeader(值/指针)并解析cmd标签
|
||||||
// 参数 field: 结构体字段元信息
|
// 参数 field: 结构体字段元信息
|
||||||
// 返回值: 解析后的cmd切片,是否为目标类型,解析失败错误
|
// 返回值: 解析后的cmd切片,是否为目标类型,解析失败错误
|
||||||
func parseCmdTagWithStructField(field reflect.StructField) ([]uint32, bool, error) {
|
func parseCmdTagWithStructField(field reflect.StructField) ([]uint32, bool, error) {
|
||||||
// 判断字段类型是否为 TomeeHeader 或 *TomeeHeader
|
|
||||||
if field.Type != targetType && !(field.Type.Kind() == reflect.Ptr && field.Type.Elem() == targetType) {
|
if field.Type != targetType && !(field.Type.Kind() == reflect.Ptr && field.Type.Elem() == targetType) {
|
||||||
return nil, false, nil
|
return nil, false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 提取cmd标签
|
|
||||||
cmdStr := field.Tag.Get("cmd")
|
cmdStr := field.Tag.Get("cmd")
|
||||||
if cmdStr == "" {
|
if cmdStr == "" {
|
||||||
return nil, true, fmt.Errorf("field %s cmd tag is empty", field.Name)
|
return nil, true, fmt.Errorf("field %s cmd tag is empty", field.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 高性能解析标签为uint32切片(替代gconv,减少第三方依赖且可控)
|
|
||||||
result := make([]uint32, 0, strings.Count(cmdStr, "|")+1)
|
result := make([]uint32, 0, strings.Count(cmdStr, "|")+1)
|
||||||
remain := cmdStr
|
remain := cmdStr
|
||||||
for idx := 0; ; idx++ {
|
for idx := 0; ; idx++ {
|
||||||
part, next, found := strings.Cut(remain, "|")
|
part, next, found := strings.Cut(remain, "|")
|
||||||
// 去除空白字符(兼容标签中意外的空格)
|
|
||||||
s := strings.TrimSpace(part)
|
s := strings.TrimSpace(part)
|
||||||
if s == "" {
|
if s == "" {
|
||||||
return nil, true, fmt.Errorf("field %s cmd tag part %d is empty", field.Name, idx)
|
return nil, true, fmt.Errorf("field %s cmd tag part %d is empty", field.Name, idx)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 手动解析uint32,比gconv更可控,避免隐式转换问题
|
|
||||||
num, err := strconv.ParseUint(s, 10, 32)
|
num, err := strconv.ParseUint(s, 10, 32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, true, fmt.Errorf("field %s cmd tag part %d parse error: %v (value: %s)",
|
return nil, true, fmt.Errorf("field %s cmd tag part %d parse error: %v (value: %s)",
|
||||||
|
|||||||
@@ -72,16 +72,24 @@ func (h Controller) DASHIbeiR(req *C2s_MASTER_REWARDSR, c *player.Player) (resul
|
|||||||
}
|
}
|
||||||
|
|
||||||
result.ItemList = make([]data.ItemInfo, 0, len(taskInfo.ItemList))
|
result.ItemList = make([]data.ItemInfo, 0, len(taskInfo.ItemList))
|
||||||
c.Service.Task.Exec(masterCupTaskID, func(te *model.Task) bool {
|
taskData, taskErr := c.Service.Task.GetTask(masterCupTaskID)
|
||||||
progress := bitset32.From(te.Data)
|
if taskErr != nil {
|
||||||
if progress.Test(uint(req.ElementType)) {
|
return nil, errorcode.ErrorCode(errorcode.ErrorCodes.ErrSystemError)
|
||||||
err = errorcode.ErrorCode(errorcode.ErrorCodes.ErrAwardAlreadyClaimed)
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
consumeMasterCupItems(c, requiredItems)
|
progress := bitset32.From(taskData.Data)
|
||||||
|
if progress.Test(uint(req.ElementType)) {
|
||||||
|
return nil, errorcode.ErrorCode(errorcode.ErrorCodes.ErrAwardAlreadyClaimed)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := consumeMasterCupItems(c, requiredItems); err != nil {
|
||||||
|
return nil, errorcode.ErrorCode(errorcode.ErrorCodes.ErrInsufficientItems)
|
||||||
|
}
|
||||||
progress.Set(uint(req.ElementType))
|
progress.Set(uint(req.ElementType))
|
||||||
te.Data = progress.Bytes()
|
taskData.Data = progress.Bytes()
|
||||||
|
if taskErr = c.Service.Task.SetTask(taskData); taskErr != nil {
|
||||||
|
return nil, errorcode.ErrorCode(errorcode.ErrorCodes.ErrSystemError)
|
||||||
|
}
|
||||||
|
|
||||||
if taskInfo.Pet != nil {
|
if taskInfo.Pet != nil {
|
||||||
c.Service.Pet.PetAdd(taskInfo.Pet, 0)
|
c.Service.Pet.PetAdd(taskInfo.Pet, 0)
|
||||||
@@ -90,8 +98,6 @@ func (h Controller) DASHIbeiR(req *C2s_MASTER_REWARDSR, c *player.Player) (resul
|
|||||||
}
|
}
|
||||||
|
|
||||||
appendMasterCupRewardItems(c, result, taskInfo.ItemList)
|
appendMasterCupRewardItems(c, result, taskInfo.ItemList)
|
||||||
return true
|
|
||||||
})
|
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -126,10 +132,13 @@ func hasEnoughMasterCupItems(c *player.Player, requiredItems []ItemS) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func consumeMasterCupItems(c *player.Player, requiredItems []ItemS) {
|
func consumeMasterCupItems(c *player.Player, requiredItems []ItemS) error {
|
||||||
for _, item := range requiredItems {
|
for _, item := range requiredItems {
|
||||||
c.Service.Item.UPDATE(item.ItemId, -int(item.ItemCnt))
|
if err := c.Service.Item.UPDATE(item.ItemId, -int(item.ItemCnt)); err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func appendMasterCupRewardItems(c *player.Player, result *S2C_MASTER_REWARDSR, itemList []data.ItemInfo) {
|
func appendMasterCupRewardItems(c *player.Player, result *S2C_MASTER_REWARDSR, itemList []data.ItemInfo) {
|
||||||
|
|||||||
@@ -26,12 +26,11 @@ func (h Controller) EggGamePlay(data1 *C2S_EGG_GAME_PLAY, c *player.Player) (res
|
|||||||
if data1.EggNum > 10 || data1.EggNum <= 0 {
|
if data1.EggNum > 10 || data1.EggNum <= 0 {
|
||||||
return nil, errorcode.ErrorCode(errorcode.ErrorCodes.ErrSystemError)
|
return nil, errorcode.ErrorCode(errorcode.ErrorCodes.ErrSystemError)
|
||||||
}
|
}
|
||||||
if r < 0 {
|
if r <= 0 || data1.EggNum > r {
|
||||||
return nil, errorcode.ErrorCode(errorcode.ErrorCodes.ErrGachaTicketsInsufficient)
|
return nil, errorcode.ErrorCode(errorcode.ErrorCodes.ErrGachaTicketsInsufficient)
|
||||||
}
|
}
|
||||||
if data1.EggNum > r {
|
if err := c.Service.Item.UPDATE(400501, int(-data1.EggNum)); err != nil {
|
||||||
return nil, errorcode.ErrorCode(errorcode.ErrorCodes.ErrGachaTicketsInsufficient)
|
return nil, errorcode.ErrorCode(errorcode.ErrorCodes.ErrGachaTicketsInsufficient)
|
||||||
|
|
||||||
}
|
}
|
||||||
result = &S2C_EGG_GAME_PLAY{ListInfo: []data.ItemInfo{}}
|
result = &S2C_EGG_GAME_PLAY{ListInfo: []data.ItemInfo{}}
|
||||||
if grand.Meet(int(data1.EggNum), 100) {
|
if grand.Meet(int(data1.EggNum), 100) {
|
||||||
@@ -52,8 +51,6 @@ func (h Controller) EggGamePlay(data1 *C2S_EGG_GAME_PLAY, c *player.Player) (res
|
|||||||
for _, item := range addedItems {
|
for _, item := range addedItems {
|
||||||
result.ListInfo = append(result.ListInfo, data.ItemInfo{ItemId: item.ItemId, ItemCnt: item.ItemCnt})
|
result.ListInfo = append(result.ListInfo, data.ItemInfo{ItemId: item.ItemId, ItemCnt: item.ItemCnt})
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Service.Item.UPDATE(400501, int(-data1.EggNum))
|
|
||||||
return
|
return
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ func (h Controller) GET_XUANCAI(data *C2s_GET_XUANCAI, c *player.Player) (result
|
|||||||
|
|
||||||
// 检查该位是否未被选中(避免重复)
|
// 检查该位是否未被选中(避免重复)
|
||||||
if (result.Status & mask) == 0 {
|
if (result.Status & mask) == 0 {
|
||||||
|
result.Status |= mask
|
||||||
itemID := uint32(400686 + randBitIdx + 1)
|
itemID := uint32(400686 + randBitIdx + 1)
|
||||||
selectedItems = append(selectedItems, itemID)
|
selectedItems = append(selectedItems, itemID)
|
||||||
itemMask[itemID] = mask
|
itemMask[itemID] = mask
|
||||||
|
|||||||
@@ -25,6 +25,69 @@ func (h Controller) OnReadyToFight(data *ReadyToFightInboundInfo, c *player.Play
|
|||||||
return nil, -1
|
return nil, -1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GroupReadyFightFinish 旧组队协议准备完成。
|
||||||
|
func (h Controller) GroupReadyFightFinish(data *GroupReadyFightFinishInboundInfo, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
|
||||||
|
if err := h.checkFightStatus(c); err != 0 {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
go c.FightC.ReadyFight(c)
|
||||||
|
return nil, -1
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h Controller) GroupUseSkill(data *GroupUseSkillInboundInfo, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
|
||||||
|
if err := h.checkFightStatus(c); err != 0 {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
targetRelation := fight.SkillTargetOpponent
|
||||||
|
if data.TargetSide == 1 {
|
||||||
|
targetRelation = fight.SkillTargetAlly
|
||||||
|
}
|
||||||
|
h.dispatchFightActionEnvelope(c, fight.NewSkillActionEnvelope(data.SkillId, int(data.ActorIndex), int(data.TargetPos), targetRelation, 0))
|
||||||
|
c.SendPackCmd(7558, nil)
|
||||||
|
return nil, -1
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h Controller) GroupUseItem(data *GroupUseItemInboundInfo, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
|
||||||
|
if err := h.checkFightStatus(c); err != 0 {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
h.dispatchFightActionEnvelope(c, fight.NewItemActionEnvelope(0, data.ItemId, int(data.ActorIndex), int(data.ActorIndex), fight.SkillTargetSelf))
|
||||||
|
return nil, -1
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h Controller) GroupChangePet(data *GroupChangePetInboundInfo, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
|
||||||
|
if err := h.checkFightStatus(c); err != 0 {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
h.dispatchFightActionEnvelope(c, fight.NewChangeActionEnvelope(data.CatchTime, int(data.ActorIndex)))
|
||||||
|
return nil, -1
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h Controller) GroupEscape(data *GroupEscapeInboundInfo, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
|
||||||
|
if err := h.checkFightStatus(c); err != 0 {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if fightC, ok := c.FightC.(*fight.FightC); ok && fightC != nil && fightC.LegacyGroupProtocol {
|
||||||
|
fightC.SendLegacyEscapeSuccess(c, int(data.ActorIndex))
|
||||||
|
}
|
||||||
|
h.dispatchFightActionEnvelope(c, fight.NewEscapeActionEnvelope())
|
||||||
|
return nil, 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h Controller) GroupFightWinClose(data *GroupFightWinCloseInboundInfo, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
|
||||||
|
if c != nil {
|
||||||
|
c.QuitFight()
|
||||||
|
}
|
||||||
|
return nil, -1
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h Controller) GroupFightTimeoutExit(data *GroupFightTimeoutExitInboundInfo, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
|
||||||
|
if c != nil {
|
||||||
|
c.QuitFight()
|
||||||
|
}
|
||||||
|
return nil, -1
|
||||||
|
}
|
||||||
|
|
||||||
// UseSkill 使用技能包
|
// UseSkill 使用技能包
|
||||||
func (h Controller) UseSkill(data *UseSkillInInfo, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
|
func (h Controller) UseSkill(data *UseSkillInInfo, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
|
||||||
if err := h.checkFightStatus(c); err != 0 {
|
if err := h.checkFightStatus(c); err != 0 {
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import (
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
rewardItemExpPool = 3
|
rewardItemExpPool = 3
|
||||||
|
groupBossSlotLimit = 3
|
||||||
)
|
)
|
||||||
|
|
||||||
// PlayerFightBoss 挑战地图boss
|
// PlayerFightBoss 挑战地图boss
|
||||||
@@ -27,7 +28,10 @@ func (Controller) PlayerFightBoss(req *ChallengeBossInboundInfo, p *player.Playe
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
mapNode := service.NewMapNodeService().GetDataNode(p.Info.MapID, req.BossId)
|
mapNode := p.GetSpace().GetMatchedMapNode(req.BossId)
|
||||||
|
if mapNode == nil {
|
||||||
|
return nil, errorcode.ErrorCodes.ErrPokemonNotExists
|
||||||
|
}
|
||||||
bossConfigs, err := loadMapBossConfigs(mapNode)
|
bossConfigs, err := loadMapBossConfigs(mapNode)
|
||||||
if err != 0 {
|
if err != 0 {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -39,7 +43,7 @@ func (Controller) PlayerFightBoss(req *ChallengeBossInboundInfo, p *player.Playe
|
|||||||
}
|
}
|
||||||
|
|
||||||
p.Fightinfo.Status = fightinfo.BattleMode.FIGHT_WITH_NPC
|
p.Fightinfo.Status = fightinfo.BattleMode.FIGHT_WITH_NPC
|
||||||
p.Fightinfo.Mode = fightinfo.BattleMode.MULTI_MODE
|
p.Fightinfo.Mode = resolveMapNodeFightMode(mapNode)
|
||||||
|
|
||||||
ai := player.NewAI_player(monsterInfo)
|
ai := player.NewAI_player(monsterInfo)
|
||||||
ai.CanCapture = resolveBossCaptureRate(bossConfigs[0].IsCapture, leadMonsterID)
|
ai.CanCapture = resolveBossCaptureRate(bossConfigs[0].IsCapture, leadMonsterID)
|
||||||
@@ -47,7 +51,7 @@ func (Controller) PlayerFightBoss(req *ChallengeBossInboundInfo, p *player.Playe
|
|||||||
ai.AddBattleProp(0, 2)
|
ai.AddBattleProp(0, 2)
|
||||||
|
|
||||||
var fightC *fight.FightC
|
var fightC *fight.FightC
|
||||||
fightC, err = fight.NewFight(p, ai, p.GetPetInfo(100), ai.GetPetInfo(0), func(foi model.FightOverInfo) {
|
fightC, err = startMapBossFight(mapNode, p, ai, func(foi model.FightOverInfo) {
|
||||||
if mapNode.WinBonusID == 0 {
|
if mapNode.WinBonusID == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -62,13 +66,40 @@ func (Controller) PlayerFightBoss(req *ChallengeBossInboundInfo, p *player.Playe
|
|||||||
return nil, -1
|
return nil, -1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func startMapBossFight(
|
||||||
|
mapNode *configmodel.MapNode,
|
||||||
|
p *player.Player,
|
||||||
|
ai *player.AI_player,
|
||||||
|
fn func(model.FightOverInfo),
|
||||||
|
) (*fight.FightC, errorcode.ErrorCode) {
|
||||||
|
ourPets := p.GetPetInfo(p.CurrentMapPetLevelLimit())
|
||||||
|
oppPets := ai.GetPetInfo(0)
|
||||||
|
if mapNode != nil && mapNode.IsGroupBoss != 0 {
|
||||||
|
if len(ourPets) > 0 && len(oppPets) > 0 {
|
||||||
|
slotLimit := groupBossSlotLimit
|
||||||
|
if mapNode.PkFlag != 0 {
|
||||||
|
slotLimit = 1
|
||||||
|
}
|
||||||
|
return fight.NewLegacyGroupFightSingleController(p, ai, ourPets, oppPets, slotLimit, fn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fight.NewFight(p, ai, ourPets, oppPets, fn)
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolveMapNodeFightMode(mapNode *configmodel.MapNode) uint32 {
|
||||||
|
if mapNode != nil && mapNode.PkFlag != 0 {
|
||||||
|
return fightinfo.BattleMode.SINGLE_MODE
|
||||||
|
}
|
||||||
|
return fightinfo.BattleMode.MULTI_MODE
|
||||||
|
}
|
||||||
|
|
||||||
// OnPlayerFightNpcMonster 战斗野怪
|
// OnPlayerFightNpcMonster 战斗野怪
|
||||||
func (Controller) OnPlayerFightNpcMonster(req *FightNpcMonsterInboundInfo, p *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
|
func (Controller) OnPlayerFightNpcMonster(req *FightNpcMonsterInboundInfo, p *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
|
||||||
if err = p.CanFight(); err != 0 {
|
if err = p.CanFight(); err != 0 {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if req.Number > 9 {
|
if int(req.Number) >= len(p.Data) {
|
||||||
return nil, errorcode.ErrorCodes.ErrSystemError
|
return nil, errorcode.ErrorCodes.ErrPokemonNotHere
|
||||||
}
|
}
|
||||||
|
|
||||||
refPet := p.Data[req.Number]
|
refPet := p.Data[req.Number]
|
||||||
@@ -83,7 +114,7 @@ func (Controller) OnPlayerFightNpcMonster(req *FightNpcMonsterInboundInfo, p *pl
|
|||||||
p.Fightinfo.Status = fightinfo.BattleMode.FIGHT_WITH_NPC
|
p.Fightinfo.Status = fightinfo.BattleMode.FIGHT_WITH_NPC
|
||||||
p.Fightinfo.Mode = fightinfo.BattleMode.MULTI_MODE
|
p.Fightinfo.Mode = fightinfo.BattleMode.MULTI_MODE
|
||||||
|
|
||||||
_, err = fight.NewFight(p, ai, p.GetPetInfo(100), ai.GetPetInfo(0), func(foi model.FightOverInfo) {
|
_, err = fight.NewFight(p, ai, p.GetPetInfo(p.CurrentMapPetLevelLimit()), ai.GetPetInfo(0), func(foi model.FightOverInfo) {
|
||||||
handleNpcFightRewards(p, foi, monster)
|
handleNpcFightRewards(p, foi, monster)
|
||||||
})
|
})
|
||||||
if err != 0 {
|
if err != 0 {
|
||||||
@@ -205,7 +236,7 @@ func shouldGrantBossWinBonus(fightC *fight.FightC, playerID uint32, bossConfig c
|
|||||||
|
|
||||||
func buildNpcMonsterInfo(refPet player.OgrePetInfo, mapID uint32) (*model.PetInfo, *model.PlayerInfo, errorcode.ErrorCode) {
|
func buildNpcMonsterInfo(refPet player.OgrePetInfo, mapID uint32) (*model.PetInfo, *model.PlayerInfo, errorcode.ErrorCode) {
|
||||||
if refPet.ID == 0 {
|
if refPet.ID == 0 {
|
||||||
return nil, nil, errorcode.ErrorCodes.ErrPokemonNotExists
|
return nil, nil, errorcode.ErrorCodes.ErrPokemonNotHere
|
||||||
}
|
}
|
||||||
|
|
||||||
monster := model.GenPetInfo(
|
monster := model.GenPetInfo(
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ func (h Controller) OnPlayerHandleFightInvite(data *HandleFightInviteInboundInfo
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = fight.NewFight(v, c, v.GetInfo().PetList, c.GetInfo().PetList, func(foi model.FightOverInfo) {
|
_, err = fight.NewFight(v, c, v.GetPetInfo(100), c.GetPetInfo(100), func(foi model.FightOverInfo) {
|
||||||
|
|
||||||
//println("好友对战测试", foi.Reason)
|
//println("好友对战测试", foi.Reason)
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package controller
|
package controller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"blazing/common/rpc"
|
||||||
"blazing/common/socket/errorcode"
|
"blazing/common/socket/errorcode"
|
||||||
"blazing/logic/service/common"
|
"blazing/logic/service/common"
|
||||||
"blazing/logic/service/fight"
|
"blazing/logic/service/fight"
|
||||||
@@ -21,11 +22,35 @@ func (h Controller) JoINtop(data *PetTOPLEVELnboundInfo, c *player.Player) (resu
|
|||||||
if err != 0 {
|
if err != 0 {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if Maincontroller.RPCClient == nil || Maincontroller.RPCClient.MatchJoinOrUpdate == nil {
|
||||||
|
pvp.CancelPeakQueue(c)
|
||||||
|
return nil, errorcode.ErrorCodes.ErrSystemBusyTryLater
|
||||||
|
}
|
||||||
|
fightMode, status, err := pvp.NormalizePeakMode(data.Mode)
|
||||||
|
if err != 0 {
|
||||||
|
pvp.CancelPeakQueue(c)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
joinPayload := rpc.PVPMatchJoinPayload{
|
||||||
|
RuntimeServerID: h.UID,
|
||||||
|
UserID: c.Info.UserID,
|
||||||
|
Nick: c.Info.Nick,
|
||||||
|
FightMode: fightMode,
|
||||||
|
Status: status,
|
||||||
|
CatchTimes: pvp.AvailableCatchTimes(c.GetPetInfo(0)),
|
||||||
|
}
|
||||||
|
if callErr := Maincontroller.RPCClient.MatchJoinOrUpdate(joinPayload); callErr != nil {
|
||||||
|
pvp.CancelPeakQueue(c)
|
||||||
|
return nil, errorcode.ErrorCodes.ErrSystemBusyTryLater
|
||||||
|
}
|
||||||
return nil, -1
|
return nil, -1
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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 {
|
||||||
|
_ = Maincontroller.RPCClient.MatchCancel(c.Info.UserID)
|
||||||
|
}
|
||||||
pvp.CancelPeakQueue(c)
|
pvp.CancelPeakQueue(c)
|
||||||
return nil, -1
|
return nil, -1
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,44 @@ type ReadyToFightInboundInfo struct {
|
|||||||
Head common.TomeeHeader `cmd:"2404" struc:"skip"`
|
Head common.TomeeHeader `cmd:"2404" struc:"skip"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GroupReadyFightFinishInboundInfo 旧组队协议准备完成。
|
||||||
|
type GroupReadyFightFinishInboundInfo struct {
|
||||||
|
Head common.TomeeHeader `cmd:"7556" struc:"skip"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GroupUseSkillInboundInfo struct {
|
||||||
|
Head common.TomeeHeader `cmd:"7558" struc:"skip"`
|
||||||
|
ActorIndex uint8
|
||||||
|
TargetSide uint8
|
||||||
|
TargetPos uint8
|
||||||
|
SkillId uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
type GroupUseItemInboundInfo struct {
|
||||||
|
Head common.TomeeHeader `cmd:"7562" struc:"skip"`
|
||||||
|
ActorIndex uint8
|
||||||
|
ItemId uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
type GroupChangePetInboundInfo struct {
|
||||||
|
Head common.TomeeHeader `cmd:"7563" struc:"skip"`
|
||||||
|
ActorIndex uint8
|
||||||
|
CatchTime uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
type GroupEscapeInboundInfo struct {
|
||||||
|
Head common.TomeeHeader `cmd:"7565" struc:"skip"`
|
||||||
|
ActorIndex uint8
|
||||||
|
}
|
||||||
|
|
||||||
|
type GroupFightWinCloseInboundInfo struct {
|
||||||
|
Head common.TomeeHeader `cmd:"7574" struc:"skip"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GroupFightTimeoutExitInboundInfo struct {
|
||||||
|
Head common.TomeeHeader `cmd:"7587" struc:"skip"`
|
||||||
|
}
|
||||||
|
|
||||||
// EscapeFightInboundInfo 定义请求或响应数据结构。
|
// EscapeFightInboundInfo 定义请求或响应数据结构。
|
||||||
type EscapeFightInboundInfo struct {
|
type EscapeFightInboundInfo struct {
|
||||||
Head common.TomeeHeader `cmd:"2410" struc:"skip"`
|
Head common.TomeeHeader `cmd:"2410" struc:"skip"`
|
||||||
|
|||||||
@@ -93,7 +93,18 @@ type C2S_Skill_Sort struct {
|
|||||||
Skill [4]uint32 `json:"skill_1"`
|
Skill [4]uint32 `json:"skill_1"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// C2S_PetFusion 定义请求或响应数据结构。
|
// GetPetLearnableSkillsInboundInfo 查询当前精灵可学习技能(含额外技能ExtSKill)
|
||||||
|
type GetPetLearnableSkillsInboundInfo struct {
|
||||||
|
Head common.TomeeHeader `cmd:"52312" struc:"skip"`
|
||||||
|
CatchTime uint32 `json:"catchTime"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CommitPetSkillsInboundInfo struct {
|
||||||
|
Head common.TomeeHeader `cmd:"52313" struc:"skip"`
|
||||||
|
CatchTime uint32 `json:"catchTime"`
|
||||||
|
Skill [4]uint32 `json:"skill"`
|
||||||
|
}
|
||||||
|
|
||||||
type C2S_PetFusion struct {
|
type C2S_PetFusion struct {
|
||||||
Head common.TomeeHeader `cmd:"2351" struc:"skip"`
|
Head common.TomeeHeader `cmd:"2351" struc:"skip"`
|
||||||
Mcatchtime uint32 `json:"mcatchtime" msgpack:"mcatchtime"`
|
Mcatchtime uint32 `json:"mcatchtime" msgpack:"mcatchtime"`
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ type MAIN_LOGIN_IN struct {
|
|||||||
Sid []byte `struc:"[16]byte"`
|
Sid []byte `struc:"[16]byte"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// CheakSession 处理控制器请求。
|
// CheakSession 校验登录session。
|
||||||
func (l *MAIN_LOGIN_IN) CheakSession() (bool, uint32) {
|
func (l *MAIN_LOGIN_IN) CheakSession() (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))
|
||||||
|
|||||||
@@ -18,10 +18,13 @@ func (h Controller) ItemSale(data *C2S_ITEM_SALE, c *player.Player) (result *fig
|
|||||||
return nil, errorcode.ErrorCodes.ErrSystemError
|
return nil, errorcode.ErrorCodes.ErrSystemError
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := c.Service.Item.UPDATE(data.ItemId, -gconv.Int(data.Amount)); err != nil {
|
||||||
|
return nil, errorcode.ErrorCodes.ErrInsufficientItems
|
||||||
|
}
|
||||||
|
|
||||||
itemConfig := xmlres.ItemsMAP[int(data.ItemId)]
|
itemConfig := xmlres.ItemsMAP[int(data.ItemId)]
|
||||||
if itemConfig.SellPrice != 0 {
|
if itemConfig.SellPrice != 0 {
|
||||||
c.Info.Coins += int64(int64(data.Amount) * int64(itemConfig.SellPrice))
|
c.Info.Coins += int64(int64(data.Amount) * int64(itemConfig.SellPrice))
|
||||||
}
|
}
|
||||||
c.Service.Item.UPDATE(data.ItemId, -gconv.Int(data.Amount))
|
|
||||||
return result, 0
|
return result, 0
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ import (
|
|||||||
const (
|
const (
|
||||||
// ItemDefaultLeftTime 道具默认剩余时间(毫秒)
|
// ItemDefaultLeftTime 道具默认剩余时间(毫秒)
|
||||||
ItemDefaultLeftTime = 360000
|
ItemDefaultLeftTime = 360000
|
||||||
|
// UniversalNatureItemID 全能性格转化剂Ω
|
||||||
|
UniversalNatureItemID uint32 = 300136
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetUserItemList 获取用户道具列表
|
// GetUserItemList 获取用户道具列表
|
||||||
@@ -33,11 +35,16 @@ func (h Controller) GetUserItemList(data *ItemListInboundInfo, c *player.Player)
|
|||||||
// c: 当前玩家对象
|
// c: 当前玩家对象
|
||||||
// 返回: 使用后的宠物信息和错误码
|
// 返回: 使用后的宠物信息和错误码
|
||||||
func (h Controller) UsePetItemOutOfFight(data *C2S_USE_PET_ITEM_OUT_OF_FIGHT, c *player.Player) (result *item.S2C_USE_PET_ITEM_OUT_OF_FIGHT, err errorcode.ErrorCode) {
|
func (h Controller) UsePetItemOutOfFight(data *C2S_USE_PET_ITEM_OUT_OF_FIGHT, c *player.Player) (result *item.S2C_USE_PET_ITEM_OUT_OF_FIGHT, err errorcode.ErrorCode) {
|
||||||
_, currentPet, found := c.FindPet(data.CatchTime)
|
slot, found := c.FindPetBagSlot(data.CatchTime)
|
||||||
if !found {
|
if !found {
|
||||||
return nil, errorcode.ErrorCodes.Err10401
|
return nil, errorcode.ErrorCodes.Err10401
|
||||||
}
|
}
|
||||||
|
|
||||||
|
currentPet := slot.PetInfoPtr()
|
||||||
|
if currentPet == nil {
|
||||||
|
return nil, errorcode.ErrorCodes.Err10401
|
||||||
|
}
|
||||||
|
|
||||||
itemID := uint32(data.ItemID)
|
itemID := uint32(data.ItemID)
|
||||||
if c.Service.Item.CheakItem(itemID) == 0 {
|
if c.Service.Item.CheakItem(itemID) == 0 {
|
||||||
return nil, errorcode.ErrorCodes.ErrInsufficientItems
|
return nil, errorcode.ErrorCodes.ErrInsufficientItems
|
||||||
@@ -51,7 +58,10 @@ func (h Controller) UsePetItemOutOfFight(data *C2S_USE_PET_ITEM_OUT_OF_FIGHT, c
|
|||||||
return nil, errcode
|
return nil, errcode
|
||||||
}
|
}
|
||||||
refreshPetPaneKeepHP(currentPet, oldHP)
|
refreshPetPaneKeepHP(currentPet, oldHP)
|
||||||
c.Service.Item.UPDATE(itemID, -1)
|
if err := c.Service.Item.UPDATE(itemID, -1); err != nil {
|
||||||
|
return nil, errorcode.ErrorCodes.ErrInsufficientItems
|
||||||
|
}
|
||||||
|
c.Service.Info.Save(*c.Info)
|
||||||
result = &item.S2C_USE_PET_ITEM_OUT_OF_FIGHT{}
|
result = &item.S2C_USE_PET_ITEM_OUT_OF_FIGHT{}
|
||||||
copier.Copy(&result, currentPet)
|
copier.Copy(&result, currentPet)
|
||||||
return result, 0
|
return result, 0
|
||||||
@@ -83,7 +93,10 @@ func (h Controller) UsePetItemOutOfFight(data *C2S_USE_PET_ITEM_OUT_OF_FIGHT, c
|
|||||||
return nil, errcode
|
return nil, errcode
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Service.Item.UPDATE(itemID, -1)
|
if err := c.Service.Item.UPDATE(itemID, -1); err != nil {
|
||||||
|
return nil, errorcode.ErrorCodes.ErrInsufficientItems
|
||||||
|
}
|
||||||
|
c.Service.Info.Save(*c.Info)
|
||||||
result = &item.S2C_USE_PET_ITEM_OUT_OF_FIGHT{}
|
result = &item.S2C_USE_PET_ITEM_OUT_OF_FIGHT{}
|
||||||
copier.Copy(&result, currentPet)
|
copier.Copy(&result, currentPet)
|
||||||
return result, 0
|
return result, 0
|
||||||
@@ -126,7 +139,9 @@ func (h Controller) handlexuancaiItem(currentPet *model.PetInfo, c *player.Playe
|
|||||||
return errorcode.ErrorCodes.ErrItemUnusable
|
return errorcode.ErrorCodes.ErrItemUnusable
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Service.Item.UPDATE(itemid, -100)
|
if err := c.Service.Item.UPDATE(itemid, -100); err != nil {
|
||||||
|
return errorcode.ErrorCodes.ErrInsufficientItems
|
||||||
|
}
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -182,11 +197,24 @@ func (h Controller) handleRegularPetItem(itemID uint32, currentPet *model.PetInf
|
|||||||
// c: 当前玩家对象
|
// c: 当前玩家对象
|
||||||
// 返回: 无数据和错误码
|
// 返回: 无数据和错误码
|
||||||
func (h Controller) ResetNature(data *C2S_PET_RESET_NATURE, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
|
func (h Controller) ResetNature(data *C2S_PET_RESET_NATURE, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
|
||||||
_, currentPet, found := c.FindPet(data.CatchTime)
|
slot, found := c.FindPetBagSlot(data.CatchTime)
|
||||||
if !found {
|
if !found {
|
||||||
return nil, errorcode.ErrorCodes.Err10401
|
return nil, errorcode.ErrorCodes.Err10401
|
||||||
}
|
}
|
||||||
|
|
||||||
|
currentPet := slot.PetInfoPtr()
|
||||||
|
if currentPet == nil {
|
||||||
|
return nil, errorcode.ErrorCodes.Err10401
|
||||||
|
}
|
||||||
|
|
||||||
|
if data.ItemId != UniversalNatureItemID {
|
||||||
|
return nil, errorcode.ErrorCodes.ErrItemUnusable
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := xmlres.NatureRootMap[int(data.Nature)]; !ok {
|
||||||
|
return nil, errorcode.ErrorCodes.ErrItemUnusable
|
||||||
|
}
|
||||||
|
|
||||||
if c.Service.Item.CheakItem(data.ItemId) <= 0 {
|
if c.Service.Item.CheakItem(data.ItemId) <= 0 {
|
||||||
return nil, errorcode.ErrorCodes.ErrInsufficientItems
|
return nil, errorcode.ErrorCodes.ErrInsufficientItems
|
||||||
}
|
}
|
||||||
@@ -194,7 +222,10 @@ func (h Controller) ResetNature(data *C2S_PET_RESET_NATURE, c *player.Player) (r
|
|||||||
currentHP := currentPet.Hp
|
currentHP := currentPet.Hp
|
||||||
currentPet.Nature = data.Nature
|
currentPet.Nature = data.Nature
|
||||||
refreshPetPaneKeepHP(currentPet, currentHP)
|
refreshPetPaneKeepHP(currentPet, currentHP)
|
||||||
c.Service.Item.UPDATE(data.ItemId, -1)
|
if err := c.Service.Item.UPDATE(data.ItemId, -1); err != nil {
|
||||||
|
return nil, errorcode.ErrorCodes.ErrInsufficientItems
|
||||||
|
}
|
||||||
|
c.Service.Info.Save(*c.Info)
|
||||||
return result, 0
|
return result, 0
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -222,29 +253,38 @@ func (h Controller) UseSpeedupItem(data *C2S_USE_SPEEDUP_ITEM, c *player.Player)
|
|||||||
if c.Info.TwoTimes != 0 {
|
if c.Info.TwoTimes != 0 {
|
||||||
return nil, errorcode.ErrorCodes.ErrItemInUse
|
return nil, errorcode.ErrorCodes.ErrItemInUse
|
||||||
}
|
}
|
||||||
c.Info.TwoTimes += 50 // 玩家对象新增 TwoTimesExp 字段存储双倍剩余次数
|
|
||||||
case 300067:
|
case 300067:
|
||||||
if c.Info.TwoTimes != 0 {
|
if c.Info.TwoTimes != 0 {
|
||||||
return nil, errorcode.ErrorCodes.ErrItemInUse
|
return nil, errorcode.ErrorCodes.ErrItemInUse
|
||||||
}
|
}
|
||||||
c.Info.TwoTimes += 25 // 玩家对象新增 TwoTimesExp 字段存储双倍剩余次数
|
|
||||||
case 300051: // 假设1002是三倍经验加速器道具ID
|
case 300051: // 假设1002是三倍经验加速器道具ID
|
||||||
if c.Info.ThreeTimes != 0 {
|
if c.Info.ThreeTimes != 0 {
|
||||||
return nil, errorcode.ErrorCodes.ErrItemInUse
|
return nil, errorcode.ErrorCodes.ErrItemInUse
|
||||||
}
|
}
|
||||||
c.Info.ThreeTimes += 50 // 玩家对象新增 ThreeTimesExp 字段存储三倍剩余次数
|
|
||||||
case 300115:
|
case 300115:
|
||||||
if c.Info.ThreeTimes != 0 {
|
if c.Info.ThreeTimes != 0 {
|
||||||
return nil, errorcode.ErrorCodes.ErrItemInUse
|
return nil, errorcode.ErrorCodes.ErrItemInUse
|
||||||
}
|
}
|
||||||
c.Info.ThreeTimes += 30 // 玩家对象新增 ThreeTimesExp 字段存储三倍剩余次数
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return nil, errorcode.ErrorCodes.ErrSystemError // 未知道具ID
|
return nil, errorcode.ErrorCodes.ErrSystemError // 未知道具ID
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 扣减道具(数量-1)
|
// 3. 扣减道具(数量-1)
|
||||||
c.Service.Item.UPDATE(data.ItemID, -1)
|
if err := c.Service.Item.UPDATE(data.ItemID, -1); err != nil {
|
||||||
|
return nil, errorcode.ErrorCodes.ErrInsufficientItems
|
||||||
|
}
|
||||||
|
|
||||||
|
switch data.ItemID {
|
||||||
|
case 300027: // 假设1001是双倍经验加速器道具ID
|
||||||
|
c.Info.TwoTimes += 50 // 玩家对象新增 TwoTimesExp 字段存储双倍剩余次数
|
||||||
|
case 300067:
|
||||||
|
c.Info.TwoTimes += 25 // 玩家对象新增 TwoTimesExp 字段存储双倍剩余次数
|
||||||
|
case 300051: // 假设1002是三倍经验加速器道具ID
|
||||||
|
c.Info.ThreeTimes += 50 // 玩家对象新增 ThreeTimesExp 字段存储三倍剩余次数
|
||||||
|
case 300115:
|
||||||
|
c.Info.ThreeTimes += 30 // 玩家对象新增 ThreeTimesExp 字段存储三倍剩余次数
|
||||||
|
}
|
||||||
result.ThreeTimes = uint32(c.Info.ThreeTimes) // 返回三倍经验剩余次数
|
result.ThreeTimes = uint32(c.Info.ThreeTimes) // 返回三倍经验剩余次数
|
||||||
result.TwoTimes = uint32(c.Info.TwoTimes) // 返回双倍经验剩余次数
|
result.TwoTimes = uint32(c.Info.TwoTimes) // 返回双倍经验剩余次数
|
||||||
|
|
||||||
@@ -275,10 +315,11 @@ func (h Controller) UseEnergyXishou(data *C2S_USE_ENERGY_XISHOU, c *player.Playe
|
|||||||
}
|
}
|
||||||
// 2. 核心业务逻辑:更新能量吸收器剩余次数
|
// 2. 核心业务逻辑:更新能量吸收器剩余次数
|
||||||
// (注:可根据道具ID配置不同的次数加成,此处默认+1)
|
// (注:可根据道具ID配置不同的次数加成,此处默认+1)
|
||||||
|
if err := c.Service.Item.UPDATE(data.ItemID, -1); err != nil {
|
||||||
|
return nil, errorcode.ErrorCodes.ErrInsufficientItems
|
||||||
|
}
|
||||||
c.Info.EnergyTime += 40 // 玩家对象新增 EnergyTimes 字段存储能量吸收剩余次数
|
c.Info.EnergyTime += 40 // 玩家对象新增 EnergyTimes 字段存储能量吸收剩余次数
|
||||||
|
|
||||||
// 3. 扣减道具(数量-1)
|
|
||||||
c.Service.Item.UPDATE(data.ItemID, -1)
|
|
||||||
result = &item.S2C_USE_ENERGY_XISHOU{
|
result = &item.S2C_USE_ENERGY_XISHOU{
|
||||||
EnergyTimes: uint32(c.Info.EnergyTime),
|
EnergyTimes: uint32(c.Info.EnergyTime),
|
||||||
}
|
}
|
||||||
@@ -309,6 +350,9 @@ func (h Controller) UseAutoFightItem(data *C2S_USE_AUTO_FIGHT_ITEM, c *player.Pl
|
|||||||
if c.Info.AutoFightTime != 0 {
|
if c.Info.AutoFightTime != 0 {
|
||||||
return nil, errorcode.ErrorCodes.ErrItemInUse
|
return nil, errorcode.ErrorCodes.ErrItemInUse
|
||||||
}
|
}
|
||||||
|
if err := c.Service.Item.UPDATE(data.ItemID, -1); err != nil {
|
||||||
|
return nil, errorcode.ErrorCodes.ErrInsufficientItems
|
||||||
|
}
|
||||||
result = &item.S2C_USE_AUTO_FIGHT_ITEM{}
|
result = &item.S2C_USE_AUTO_FIGHT_ITEM{}
|
||||||
// 2. 核心业务逻辑:开启自动战斗 + 更新剩余次数
|
// 2. 核心业务逻辑:开启自动战斗 + 更新剩余次数
|
||||||
c.Info.AutoFight = 3 // 按需求设置自动战斗flag为3(需测试)
|
c.Info.AutoFight = 3 // 按需求设置自动战斗flag为3(需测试)
|
||||||
@@ -324,8 +368,6 @@ func (h Controller) UseAutoFightItem(data *C2S_USE_AUTO_FIGHT_ITEM, c *player.Pl
|
|||||||
c.Info.AutoFightTime += 50
|
c.Info.AutoFightTime += 50
|
||||||
}
|
}
|
||||||
result.AutoFightTimes = c.Info.AutoFightTime
|
result.AutoFightTimes = c.Info.AutoFightTime
|
||||||
// 3. 扣减道具(数量-1)
|
|
||||||
c.Service.Item.UPDATE(data.ItemID, -1)
|
|
||||||
|
|
||||||
return result, 0
|
return result, 0
|
||||||
}
|
}
|
||||||
|
|||||||
60
logic/controller/item_use_test.go
Normal file
60
logic/controller/item_use_test.go
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"blazing/common/data/xmlres"
|
||||||
|
"blazing/logic/service/player"
|
||||||
|
playermodel "blazing/modules/player/model"
|
||||||
|
blservice "blazing/modules/player/service"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestUsePetItemOutOfFightAppliesToBackupPetInMemory(t *testing.T) {
|
||||||
|
petID := firstPetIDForControllerTest(t)
|
||||||
|
backupPet := playermodel.GenPetInfo(petID, 31, 0, 0, 50, nil, 0)
|
||||||
|
if backupPet == nil {
|
||||||
|
t.Fatal("failed to generate backup pet")
|
||||||
|
}
|
||||||
|
if backupPet.MaxHp <= 1 {
|
||||||
|
t.Fatalf("expected generated pet to have max hp > 1, got %d", backupPet.MaxHp)
|
||||||
|
}
|
||||||
|
backupPet.Hp = 1
|
||||||
|
|
||||||
|
testPlayer := player.NewPlayer(nil)
|
||||||
|
testPlayer.Info = &playermodel.PlayerInfo{
|
||||||
|
UserID: 1,
|
||||||
|
PetList: []playermodel.PetInfo{},
|
||||||
|
BackupPetList: []playermodel.PetInfo{*backupPet},
|
||||||
|
}
|
||||||
|
testPlayer.Service = blservice.NewUserService(testPlayer.Info.UserID)
|
||||||
|
|
||||||
|
itemID, recoverHP := firstRecoverHPItemForControllerTest(t)
|
||||||
|
if recoverHP <= 0 {
|
||||||
|
t.Fatalf("expected positive recover hp for item %d, got %d", itemID, recoverHP)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := (Controller{}).UsePetItemOutOfFight(&C2S_USE_PET_ITEM_OUT_OF_FIGHT{
|
||||||
|
CatchTime: backupPet.CatchTime,
|
||||||
|
ItemID: int32(itemID),
|
||||||
|
}, testPlayer)
|
||||||
|
if err != 0 {
|
||||||
|
t.Fatalf("expected backup pet item use to succeed in-memory, got err=%d", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
updatedPet := testPlayer.Info.BackupPetList[0]
|
||||||
|
if updatedPet.Hp <= 1 {
|
||||||
|
t.Fatalf("expected backup pet hp to increase in memory, got hp=%d", updatedPet.Hp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func firstRecoverHPItemForControllerTest(t *testing.T) (uint32, int) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
for id, cfg := range xmlres.ItemsMAP {
|
||||||
|
if cfg.HP > 0 {
|
||||||
|
return uint32(id), cfg.HP
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Fatal("xmlres.ItemsMAP has no HP recovery item")
|
||||||
|
return 0, 0
|
||||||
|
}
|
||||||
@@ -2,14 +2,11 @@ package controller
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"blazing/common/data/share"
|
"blazing/common/data/share"
|
||||||
"blazing/cool"
|
|
||||||
|
|
||||||
"blazing/common/socket/errorcode"
|
"blazing/common/socket/errorcode"
|
||||||
|
"blazing/cool"
|
||||||
"blazing/logic/service/user"
|
|
||||||
|
|
||||||
"blazing/logic/service/player"
|
"blazing/logic/service/player"
|
||||||
"blazing/logic/service/space"
|
"blazing/logic/service/space"
|
||||||
|
"blazing/logic/service/user"
|
||||||
"blazing/modules/player/service"
|
"blazing/modules/player/service"
|
||||||
"context"
|
"context"
|
||||||
"time"
|
"time"
|
||||||
@@ -17,6 +14,32 @@ import (
|
|||||||
"github.com/panjf2000/gnet/v2"
|
"github.com/panjf2000/gnet/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
waitUserOfflineTimeout = 30 * time.Second
|
||||||
|
waitUserOfflineInterval = 200 * time.Millisecond
|
||||||
|
waitUserOfflineKickGap = 5 * time.Second
|
||||||
|
)
|
||||||
|
|
||||||
|
func waitUserOffline(userID uint32, timeout time.Duration) bool {
|
||||||
|
deadline := time.Now().Add(timeout)
|
||||||
|
lastKickAt := time.Now()
|
||||||
|
for {
|
||||||
|
if _, onlineErr := share.ShareManager.GetUserOnline(userID); onlineErr != nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if time.Now().After(deadline) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if time.Since(lastKickAt) >= waitUserOfflineKickGap {
|
||||||
|
if kickErr := Maincontroller.RPCClient.Kick(userID); kickErr != nil {
|
||||||
|
cool.Logger.Error(context.Background(), "补踢失败", userID, kickErr)
|
||||||
|
}
|
||||||
|
lastKickAt = time.Now()
|
||||||
|
}
|
||||||
|
time.Sleep(waitUserOfflineInterval)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Login 处理命令: 1001
|
// Login 处理命令: 1001
|
||||||
func (h Controller) Login(data *MAIN_LOGIN_IN, c gnet.Conn) (result *user.LoginMSInfo, err errorcode.ErrorCode) { //这个时候player应该是空的
|
func (h Controller) Login(data *MAIN_LOGIN_IN, c gnet.Conn) (result *user.LoginMSInfo, err errorcode.ErrorCode) { //这个时候player应该是空的
|
||||||
|
|
||||||
@@ -30,12 +53,18 @@ func (h Controller) Login(data *MAIN_LOGIN_IN, c gnet.Conn) (result *user.LoginM
|
|||||||
defer c.Close()
|
defer c.Close()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
_, erre := share.ShareManager.GetUserOnline(data.Head.UserID)
|
|
||||||
if erre == nil {
|
|
||||||
error := Maincontroller.RPCClient.Kick(data.Head.UserID) //通知其他服务器踢人
|
|
||||||
if error != nil {
|
|
||||||
cool.Logger.Error(context.Background(), "踢人失败", err)
|
|
||||||
|
|
||||||
|
if onlineServerID, onlineErr := share.ShareManager.GetUserOnline(data.Head.UserID); onlineErr == nil {
|
||||||
|
kickErr := Maincontroller.RPCClient.Kick(data.Head.UserID) //通知其他服务器踢人
|
||||||
|
if kickErr != nil {
|
||||||
|
cool.Logger.Error(context.Background(), "踢人失败", data.Head.UserID, onlineServerID, kickErr)
|
||||||
|
err = errorcode.ErrorCodes.ErrSystemBusyTryLater
|
||||||
|
defer c.Close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if ok := waitUserOffline(data.Head.UserID, waitUserOfflineTimeout); !ok {
|
||||||
|
cool.Logger.Error(context.Background(), "等待旧会话下线超时", data.Head.UserID, onlineServerID, waitUserOfflineTimeout)
|
||||||
|
err = errorcode.ErrorCodes.ErrSystemBusyTryLater
|
||||||
defer c.Close()
|
defer c.Close()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ func (h *Controller) SwitchFlying(data *SwitchFlyingInboundInfo, c *player.Playe
|
|||||||
// PlayerPetCure 处理控制器请求。
|
// PlayerPetCure 处理控制器请求。
|
||||||
func (h *Controller) PlayerPetCure(data *PetCureInboundInfo, c *player.Player) (result *nono.PetCureOutboundEmpty, err errorcode.ErrorCode) { //这个时候player应该是空的
|
func (h *Controller) PlayerPetCure(data *PetCureInboundInfo, c *player.Player) (result *nono.PetCureOutboundEmpty, err errorcode.ErrorCode) { //这个时候player应该是空的
|
||||||
_ = data
|
_ = data
|
||||||
|
result = &nono.PetCureOutboundEmpty{}
|
||||||
if c.IsArenaHealLocked() {
|
if c.IsArenaHealLocked() {
|
||||||
return result, errorcode.ErrorCodes.ErrChampionCannotHeal
|
return result, errorcode.ErrorCodes.ErrChampionCannotHeal
|
||||||
}
|
}
|
||||||
@@ -73,6 +74,9 @@ func (h *Controller) PlayerPetCure(data *PetCureInboundInfo, c *player.Player) (
|
|||||||
for i := range c.Info.PetList {
|
for i := range c.Info.PetList {
|
||||||
c.Info.PetList[i].Cure()
|
c.Info.PetList[i].Cure()
|
||||||
}
|
}
|
||||||
|
for i := range c.Info.BackupPetList {
|
||||||
|
c.Info.BackupPetList[i].Cure()
|
||||||
|
}
|
||||||
c.Info.Coins -= nonoPetCureCost
|
c.Info.Coins -= nonoPetCureCost
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -37,7 +37,9 @@ func (h Controller) PetELV(data *C2S_PET_EVOLVTION, c *player.Player) (result *f
|
|||||||
return nil, errorcode.ErrorCodes.ErrInsufficientItemsMulti
|
return nil, errorcode.ErrorCodes.ErrInsufficientItemsMulti
|
||||||
}
|
}
|
||||||
if branch.EvolvItem != 0 {
|
if branch.EvolvItem != 0 {
|
||||||
c.Service.Item.UPDATE(uint32(branch.EvolvItem), -branch.EvolvItemCount)
|
if err := c.Service.Item.UPDATE(uint32(branch.EvolvItem), -branch.EvolvItemCount); err != nil {
|
||||||
|
return nil, errorcode.ErrorCodes.ErrInsufficientItemsMulti
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
currentPet.ID = uint32(branch.MonTo)
|
currentPet.ID = uint32(branch.MonTo)
|
||||||
|
|||||||
@@ -17,11 +17,16 @@ const (
|
|||||||
// c: 当前玩家对象
|
// c: 当前玩家对象
|
||||||
// 返回: 分配结果和错误码
|
// 返回: 分配结果和错误码
|
||||||
func (h Controller) PetEVDiy(data *PetEV, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
|
func (h Controller) PetEVDiy(data *PetEV, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
|
||||||
_, currentPet, found := c.FindPet(data.CacthTime)
|
slot, found := c.FindPetBagSlot(data.CacthTime)
|
||||||
if !found {
|
if !found {
|
||||||
return nil, errorcode.ErrorCodes.Err10401
|
return nil, errorcode.ErrorCodes.Err10401
|
||||||
}
|
}
|
||||||
|
|
||||||
|
currentPet := slot.PetInfoPtr()
|
||||||
|
if currentPet == nil {
|
||||||
|
return nil, errorcode.ErrorCodes.Err10401
|
||||||
|
}
|
||||||
|
|
||||||
var targetTotal uint32
|
var targetTotal uint32
|
||||||
var currentTotal uint32
|
var currentTotal uint32
|
||||||
for i, evValue := range data.EVs {
|
for i, evValue := range data.EVs {
|
||||||
|
|||||||
45
logic/controller/pet_ev_test.go
Normal file
45
logic/controller/pet_ev_test.go
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"blazing/logic/service/player"
|
||||||
|
playermodel "blazing/modules/player/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPetEVDiy_AppliesToBackupPet(t *testing.T) {
|
||||||
|
p := player.NewPlayer(nil)
|
||||||
|
p.Info = &playermodel.PlayerInfo{
|
||||||
|
EVPool: 20,
|
||||||
|
PetList: []playermodel.PetInfo{
|
||||||
|
{CatchTime: 1},
|
||||||
|
},
|
||||||
|
BackupPetList: []playermodel.PetInfo{
|
||||||
|
{
|
||||||
|
CatchTime: 2,
|
||||||
|
Level: 100,
|
||||||
|
Ev: [6]uint32{0, 4, 0, 0, 0, 0},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
data := &PetEV{
|
||||||
|
CacthTime: 2,
|
||||||
|
EVs: [6]uint32{0, 8, 4, 0, 0, 0},
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := (Controller{}).PetEVDiy(data, p)
|
||||||
|
if err != 0 {
|
||||||
|
t.Fatalf("PetEVDiy returned error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
got := p.Info.BackupPetList[0].Ev
|
||||||
|
want := [6]uint32{0, 8, 4, 0, 0, 0}
|
||||||
|
if got != want {
|
||||||
|
t.Fatalf("backup pet EV mismatch, got %v want %v", got, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
if gotPool, wantPool := p.Info.EVPool, int64(12); gotPool != wantPool {
|
||||||
|
t.Fatalf("EVPool mismatch, got %d want %d", gotPool, wantPool)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -65,16 +65,33 @@ func (h Controller) PetFusion(data *C2S_PetFusion, c *player.Player) (result *pe
|
|||||||
return result, errorcode.ErrorCodes.ErrSunDouInsufficient10016
|
return result, errorcode.ErrorCodes.ErrSunDouInsufficient10016
|
||||||
}
|
}
|
||||||
|
|
||||||
consumeItems(c, materialCounts)
|
|
||||||
c.Info.Coins -= petFusionCost
|
|
||||||
|
|
||||||
if resultPetID == 0 {
|
if resultPetID == 0 {
|
||||||
if useOptionalItem(c, data.GoldItem1[:], petFusionFailureItemID) {
|
failedAux := *auxPet
|
||||||
result.CostItemFlag = 1
|
if auxPet.Level > 5 {
|
||||||
} else if auxPet.Level > 5 {
|
failedAux.Downgrade(auxPet.Level - 5)
|
||||||
auxPet.Downgrade(auxPet.Level - 5)
|
|
||||||
} else {
|
} else {
|
||||||
auxPet.Downgrade(1)
|
failedAux.Downgrade(1)
|
||||||
|
}
|
||||||
|
txResult, errCode := c.Service.PetFusionTx(
|
||||||
|
*c.Info,
|
||||||
|
data.Mcatchtime,
|
||||||
|
data.Auxcatchtime,
|
||||||
|
materialCounts,
|
||||||
|
data.GoldItem1[:],
|
||||||
|
petFusionKeepAuxItemID,
|
||||||
|
petFusionFailureItemID,
|
||||||
|
petFusionCost,
|
||||||
|
nil,
|
||||||
|
&failedAux,
|
||||||
|
)
|
||||||
|
if errCode != 0 {
|
||||||
|
return result, errCode
|
||||||
|
}
|
||||||
|
c.Info.Coins -= petFusionCost
|
||||||
|
if txResult.CostItemUsed {
|
||||||
|
result.CostItemFlag = 1
|
||||||
|
} else if txResult.UpdatedAux != nil {
|
||||||
|
*auxPet = *txResult.UpdatedAux
|
||||||
}
|
}
|
||||||
return &pet.PetFusionInfo{}, 0
|
return &pet.PetFusionInfo{}, 0
|
||||||
}
|
}
|
||||||
@@ -101,18 +118,37 @@ func (h Controller) PetFusion(data *C2S_PetFusion, c *player.Player) (result *pe
|
|||||||
newPet.RandomByWeightShiny()
|
newPet.RandomByWeightShiny()
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Service.Pet.PetAdd(newPet, 0)
|
txResult, errCode := c.Service.PetFusionTx(
|
||||||
//println(c.Info.UserID, "进行融合", len(c.Info.PetList), masterPet.ID, auxPet.ID, newPet.ID)
|
*c.Info,
|
||||||
|
data.Mcatchtime,
|
||||||
c.PetDel(data.Mcatchtime)
|
data.Auxcatchtime,
|
||||||
if useOptionalItem(c, data.GoldItem1[:], petFusionKeepAuxItemID) {
|
materialCounts,
|
||||||
result.CostItemFlag = 1
|
data.GoldItem1[:],
|
||||||
} else {
|
petFusionKeepAuxItemID,
|
||||||
c.PetDel(data.Auxcatchtime)
|
petFusionFailureItemID,
|
||||||
|
petFusionCost,
|
||||||
|
newPet,
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
if errCode != 0 {
|
||||||
|
return result, errCode
|
||||||
}
|
}
|
||||||
|
|
||||||
result.ObtainTime = newPet.CatchTime
|
c.Info.Coins -= petFusionCost
|
||||||
result.StarterCpTm = newPet.ID
|
if txResult.CostItemUsed {
|
||||||
|
result.CostItemFlag = 1
|
||||||
|
} else {
|
||||||
|
removePetFromPlayerInfo(c, data.Auxcatchtime)
|
||||||
|
}
|
||||||
|
removePetFromPlayerInfo(c, data.Mcatchtime)
|
||||||
|
|
||||||
|
if txResult.NewPet == nil {
|
||||||
|
return result, errorcode.ErrorCodes.ErrSystemError
|
||||||
|
}
|
||||||
|
c.Info.PetList = append(c.Info.PetList, *txResult.NewPet)
|
||||||
|
|
||||||
|
result.ObtainTime = txResult.NewPet.CatchTime
|
||||||
|
result.StarterCpTm = txResult.NewPet.ID
|
||||||
return result, 0
|
return result, 0
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,21 +185,10 @@ func hasEnoughItems(c *player.Player, itemCounts map[uint32]int) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func consumeItems(c *player.Player, itemCounts map[uint32]int) {
|
func removePetFromPlayerInfo(c *player.Player, catchTime uint32) {
|
||||||
for itemID, count := range itemCounts {
|
index, _, ok := c.FindPet(catchTime)
|
||||||
_ = c.Service.Item.UPDATE(itemID, -count)
|
if !ok {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}
|
c.Info.PetList = append(c.Info.PetList[:index], c.Info.PetList[index+1:]...)
|
||||||
|
|
||||||
func useOptionalItem(c *player.Player, itemIDs []uint32, target uint32) bool {
|
|
||||||
if c.Service.Item.CheakItem(target) <= 0 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
for _, itemID := range itemIDs {
|
|
||||||
if itemID == target {
|
|
||||||
_ = c.Service.Item.UPDATE(target, -1)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,36 +4,41 @@ import (
|
|||||||
"blazing/common/socket/errorcode"
|
"blazing/common/socket/errorcode"
|
||||||
"blazing/logic/service/common"
|
"blazing/logic/service/common"
|
||||||
"blazing/logic/service/pet"
|
"blazing/logic/service/pet"
|
||||||
"blazing/logic/service/player"
|
playersvc "blazing/logic/service/player"
|
||||||
"blazing/modules/player/model"
|
"blazing/modules/player/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetPetInfo 获取精灵信息
|
// GetPetInfo 获取精灵信息
|
||||||
func (h Controller) GetPetInfo(
|
func (h Controller) GetPetInfo(
|
||||||
data *GetPetInfoInboundInfo,
|
data *GetPetInfoInboundInfo,
|
||||||
player *player.Player) (result *model.PetInfo,
|
player *playersvc.Player) (result *model.PetInfo,
|
||||||
err errorcode.ErrorCode) {
|
err errorcode.ErrorCode) {
|
||||||
_, petInfo, found := player.FindPet(data.CatchTime)
|
levelLimit := player.CurrentMapPetLevelLimit()
|
||||||
if found {
|
if slot, found := player.FindPetBagSlot(data.CatchTime); found {
|
||||||
result = petInfo
|
if petInfo := slot.PetInfoPtr(); petInfo != nil {
|
||||||
|
petCopy := playersvc.ApplyPetLevelLimit(*petInfo, levelLimit)
|
||||||
|
result = &petCopy
|
||||||
return result, 0
|
return result, 0
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ret := player.Service.Pet.PetInfoOneByCatchTime(data.CatchTime)
|
ret := player.Service.Pet.PetInfoOneByCatchTime(data.CatchTime)
|
||||||
if ret == nil {
|
if ret == nil {
|
||||||
return nil, errorcode.ErrorCodes.ErrPokemonNotExists
|
return nil, errorcode.ErrorCodes.ErrPokemonNotExists
|
||||||
}
|
}
|
||||||
|
|
||||||
result = &ret.Data
|
petData := ret.Data
|
||||||
|
petData = playersvc.ApplyPetLevelLimit(petData, levelLimit)
|
||||||
|
result = &petData
|
||||||
return result, 0
|
return result, 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetUserBagPetInfo 获取主背包和并列备用精灵列表
|
// GetUserBagPetInfo 获取主背包和并列备用精灵列表
|
||||||
func (h Controller) GetUserBagPetInfo(
|
func (h Controller) GetUserBagPetInfo(
|
||||||
data *GetUserBagPetInfoInboundEmpty,
|
data *GetUserBagPetInfoInboundEmpty,
|
||||||
player *player.Player) (result *pet.GetUserBagPetInfoOutboundInfo,
|
player *playersvc.Player) (result *pet.GetUserBagPetInfoOutboundInfo,
|
||||||
err errorcode.ErrorCode) {
|
err errorcode.ErrorCode) {
|
||||||
return player.GetUserBagPetInfo(), 0
|
return player.GetUserBagPetInfo(player.CurrentMapPetLevelLimit()), 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetPetListInboundEmpty 定义请求或响应数据结构。
|
// GetPetListInboundEmpty 定义请求或响应数据结构。
|
||||||
@@ -44,7 +49,7 @@ type GetPetListInboundEmpty struct {
|
|||||||
// GetPetList 获取当前主背包列表
|
// GetPetList 获取当前主背包列表
|
||||||
func (h Controller) GetPetList(
|
func (h Controller) GetPetList(
|
||||||
data *GetPetListInboundEmpty,
|
data *GetPetListInboundEmpty,
|
||||||
player *player.Player) (result *pet.GetPetListOutboundInfo,
|
player *playersvc.Player) (result *pet.GetPetListOutboundInfo,
|
||||||
err errorcode.ErrorCode) {
|
err errorcode.ErrorCode) {
|
||||||
return buildPetListOutboundInfo(player.Info.PetList), 0
|
return buildPetListOutboundInfo(player.Info.PetList), 0
|
||||||
}
|
}
|
||||||
@@ -57,7 +62,7 @@ type GetPetListFreeInboundEmpty struct {
|
|||||||
// GetPetReleaseList 获取仓库可放生列表
|
// GetPetReleaseList 获取仓库可放生列表
|
||||||
func (h Controller) GetPetReleaseList(
|
func (h Controller) GetPetReleaseList(
|
||||||
data *GetPetListFreeInboundEmpty,
|
data *GetPetListFreeInboundEmpty,
|
||||||
player *player.Player) (result *pet.GetPetListOutboundInfo,
|
player *playersvc.Player) (result *pet.GetPetListOutboundInfo,
|
||||||
err errorcode.ErrorCode) {
|
err errorcode.ErrorCode) {
|
||||||
|
|
||||||
return buildPetListOutboundInfo(player.WarehousePetList()), 0
|
return buildPetListOutboundInfo(player.WarehousePetList()), 0
|
||||||
@@ -66,14 +71,13 @@ func (h Controller) GetPetReleaseList(
|
|||||||
// PlayerShowPet 精灵展示
|
// PlayerShowPet 精灵展示
|
||||||
func (h Controller) PlayerShowPet(
|
func (h Controller) PlayerShowPet(
|
||||||
data *PetShowInboundInfo,
|
data *PetShowInboundInfo,
|
||||||
player *player.Player) (result *pet.PetShowOutboundInfo, err errorcode.ErrorCode) {
|
player *playersvc.Player) (result *pet.PetShowOutboundInfo, err errorcode.ErrorCode) {
|
||||||
result = &pet.PetShowOutboundInfo{
|
result = &pet.PetShowOutboundInfo{
|
||||||
UserID: data.Head.UserID,
|
UserID: data.Head.UserID,
|
||||||
CatchTime: data.CatchTime,
|
CatchTime: data.CatchTime,
|
||||||
Flag: data.Flag,
|
Flag: data.Flag,
|
||||||
}
|
}
|
||||||
|
|
||||||
_, currentPet, ok := player.FindPet(data.CatchTime)
|
|
||||||
if data.Flag == 0 {
|
if data.Flag == 0 {
|
||||||
player.SetPetDisplay(0, nil)
|
player.SetPetDisplay(0, nil)
|
||||||
player.GetSpace().RefreshUserInfo(player)
|
player.GetSpace().RefreshUserInfo(player)
|
||||||
@@ -81,10 +85,16 @@ func (h Controller) PlayerShowPet(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
slot, ok := player.FindPetBagSlot(data.CatchTime)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, errorcode.ErrorCodes.ErrPokemonNotExists
|
return nil, errorcode.ErrorCodes.ErrPokemonNotExists
|
||||||
}
|
}
|
||||||
|
|
||||||
|
currentPet := slot.PetInfoPtr()
|
||||||
|
if currentPet == nil {
|
||||||
|
return nil, errorcode.ErrorCodes.ErrPokemonNotExists
|
||||||
|
}
|
||||||
|
|
||||||
player.SetPetDisplay(data.Flag, currentPet)
|
player.SetPetDisplay(data.Flag, currentPet)
|
||||||
player.GetSpace().RefreshUserInfo(player)
|
player.GetSpace().RefreshUserInfo(player)
|
||||||
result = buildPetShowOutboundInfo(data.Head.UserID, data.Flag, currentPet)
|
result = buildPetShowOutboundInfo(data.Head.UserID, data.Flag, currentPet)
|
||||||
|
|||||||
@@ -6,8 +6,39 @@ import (
|
|||||||
"blazing/logic/service/fight"
|
"blazing/logic/service/fight"
|
||||||
"blazing/logic/service/pet"
|
"blazing/logic/service/pet"
|
||||||
"blazing/logic/service/player"
|
"blazing/logic/service/player"
|
||||||
|
playermodel "blazing/modules/player/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func petSetExpLimit(currentPet *playermodel.PetInfo) int64 {
|
||||||
|
if currentPet == nil || currentPet.Level >= 100 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
simulatedPet := *currentPet
|
||||||
|
allowedExp := simulatedPet.NextLvExp - simulatedPet.Exp
|
||||||
|
if allowedExp < 0 {
|
||||||
|
allowedExp = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
for simulatedPet.Level < 100 && simulatedPet.NextLvExp > 0 {
|
||||||
|
simulatedPet.Level++
|
||||||
|
simulatedPet.Update(true)
|
||||||
|
if simulatedPet.Level >= 100 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
allowedExp += simulatedPet.NextLvExp
|
||||||
|
}
|
||||||
|
|
||||||
|
return allowedExp
|
||||||
|
}
|
||||||
|
|
||||||
|
func minInt64(a, b int64) int64 {
|
||||||
|
if a < b {
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
// PetReleaseToWarehouse 将精灵从仓库包中放生
|
// PetReleaseToWarehouse 将精灵从仓库包中放生
|
||||||
func (h Controller) PetReleaseToWarehouse(
|
func (h Controller) PetReleaseToWarehouse(
|
||||||
data *PET_ROWEI, player *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
|
data *PET_ROWEI, player *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
|
||||||
@@ -17,9 +48,8 @@ func (h Controller) PetReleaseToWarehouse(
|
|||||||
if inBag || inBackup || freeForbidden == 1 {
|
if inBag || inBackup || freeForbidden == 1 {
|
||||||
return nil, errorcode.ErrorCodes.ErrCannotReleaseNonWarehouse
|
return nil, errorcode.ErrorCodes.ErrCannotReleaseNonWarehouse
|
||||||
}
|
}
|
||||||
|
if !player.Service.Pet.UpdateFree(data.CatchTime, 0, 1) {
|
||||||
if !player.Service.Pet.UpdateFree(data.CatchTime, 1) {
|
return nil, errorcode.ErrorCodes.ErrCannotReleaseNonWarehouse
|
||||||
return nil, errorcode.ErrorCodes.ErrSystemError
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, 0
|
return nil, 0
|
||||||
@@ -32,10 +62,12 @@ func (h Controller) PetOneCure(
|
|||||||
return result, errorcode.ErrorCodes.ErrChampionCannotHeal
|
return result, errorcode.ErrorCodes.ErrChampionCannotHeal
|
||||||
}
|
}
|
||||||
|
|
||||||
_, currentPet, ok := player.FindPet(data.CatchTime)
|
if slot, ok := player.FindPetBagSlot(data.CatchTime); ok {
|
||||||
if ok {
|
currentPet := slot.PetInfoPtr()
|
||||||
|
if currentPet != nil {
|
||||||
defer currentPet.Cure()
|
defer currentPet.Cure()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return &pet.PetOneCureOutboundInfo{
|
return &pet.PetOneCureOutboundInfo{
|
||||||
CatchTime: data.CatchTime,
|
CatchTime: data.CatchTime,
|
||||||
@@ -63,11 +95,17 @@ func (h Controller) PetFirst(
|
|||||||
func (h Controller) SetPetExp(
|
func (h Controller) SetPetExp(
|
||||||
data *PetSetExpInboundInfo,
|
data *PetSetExpInboundInfo,
|
||||||
player *player.Player) (result *pet.PetSetExpOutboundInfo, err errorcode.ErrorCode) {
|
player *player.Player) (result *pet.PetSetExpOutboundInfo, err errorcode.ErrorCode) {
|
||||||
_, currentPet, found := player.FindPet(data.CatchTime)
|
slot, found := player.FindPetBagSlot(data.CatchTime)
|
||||||
if !found || currentPet.Level >= 100 {
|
currentPet := slot.PetInfoPtr()
|
||||||
|
if !found || currentPet == nil || currentPet.Level >= 100 {
|
||||||
return &pet.PetSetExpOutboundInfo{Exp: player.Info.ExpPool}, errorcode.ErrorCodes.ErrSystemError
|
return &pet.PetSetExpOutboundInfo{Exp: player.Info.ExpPool}, errorcode.ErrorCodes.ErrSystemError
|
||||||
}
|
}
|
||||||
|
|
||||||
player.AddPetExp(currentPet, data.Exp)
|
allowedExp := petSetExpLimit(currentPet)
|
||||||
|
if allowedExp <= 0 {
|
||||||
|
return &pet.PetSetExpOutboundInfo{Exp: player.Info.ExpPool}, errorcode.ErrorCodes.ErrSystemError
|
||||||
|
}
|
||||||
|
|
||||||
|
player.AddPetExp(currentPet, minInt64(data.Exp, allowedExp))
|
||||||
return &pet.PetSetExpOutboundInfo{Exp: player.Info.ExpPool}, 0
|
return &pet.PetSetExpOutboundInfo{Exp: player.Info.ExpPool}, 0
|
||||||
}
|
}
|
||||||
|
|||||||
75
logic/controller/pet_manage_test.go
Normal file
75
logic/controller/pet_manage_test.go
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"blazing/common/data/xmlres"
|
||||||
|
"blazing/common/socket/errorcode"
|
||||||
|
"blazing/logic/service/player"
|
||||||
|
playermodel "blazing/modules/player/model"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func firstPetIDForControllerTest(t *testing.T) int {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
for id := range xmlres.PetMAP {
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Fatal("xmlres.PetMAP is empty")
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSetPetExpCapsLevelAt100(t *testing.T) {
|
||||||
|
petID := firstPetIDForControllerTest(t)
|
||||||
|
petInfo := playermodel.GenPetInfo(petID, 31, 0, 0, 99, nil, 0)
|
||||||
|
if petInfo == nil {
|
||||||
|
t.Fatal("failed to generate test pet")
|
||||||
|
}
|
||||||
|
|
||||||
|
expPool := petInfo.NextLvExp + 10_000
|
||||||
|
testPlayer := player.NewPlayer(nil)
|
||||||
|
testPlayer.Info = &playermodel.PlayerInfo{
|
||||||
|
ExpPool: expPool,
|
||||||
|
PetList: []playermodel.PetInfo{*petInfo},
|
||||||
|
}
|
||||||
|
|
||||||
|
currentPet := &testPlayer.Info.PetList[0]
|
||||||
|
result, err := (Controller{}).SetPetExp(&PetSetExpInboundInfo{
|
||||||
|
CatchTime: currentPet.CatchTime,
|
||||||
|
Exp: expPool,
|
||||||
|
}, testPlayer)
|
||||||
|
if err != 0 {
|
||||||
|
t.Fatalf("expected SetPetExp to succeed, got err=%d", err)
|
||||||
|
}
|
||||||
|
if currentPet.Level != 100 {
|
||||||
|
t.Fatalf("expected pet level to stop at 100, got %d", currentPet.Level)
|
||||||
|
}
|
||||||
|
if result.Exp != 10_000 {
|
||||||
|
t.Fatalf("expected overflow exp to remain in pool, got %d", result.Exp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSetPetExpRejectsPetAtLevel100(t *testing.T) {
|
||||||
|
petID := firstPetIDForControllerTest(t)
|
||||||
|
petInfo := playermodel.GenPetInfo(petID, 31, 0, 0, 100, nil, 0)
|
||||||
|
if petInfo == nil {
|
||||||
|
t.Fatal("failed to generate test pet")
|
||||||
|
}
|
||||||
|
|
||||||
|
testPlayer := player.NewPlayer(nil)
|
||||||
|
testPlayer.Info = &playermodel.PlayerInfo{
|
||||||
|
ExpPool: 50_000,
|
||||||
|
PetList: []playermodel.PetInfo{*petInfo},
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := (Controller{}).SetPetExp(&PetSetExpInboundInfo{
|
||||||
|
CatchTime: petInfo.CatchTime,
|
||||||
|
Exp: 12_345,
|
||||||
|
}, testPlayer)
|
||||||
|
if err != errorcode.ErrorCodes.ErrSystemError {
|
||||||
|
t.Fatalf("expected level-100 pet to be rejected, got err=%d", err)
|
||||||
|
}
|
||||||
|
if result.Exp != 50_000 {
|
||||||
|
t.Fatalf("expected exp pool to remain unchanged, got %d", result.Exp)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,47 +8,136 @@ import (
|
|||||||
"blazing/logic/service/pet"
|
"blazing/logic/service/pet"
|
||||||
"blazing/logic/service/player"
|
"blazing/logic/service/player"
|
||||||
"blazing/modules/player/model"
|
"blazing/modules/player/model"
|
||||||
|
|
||||||
"github.com/samber/lo"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type GetPetLearnableSkillsOutboundInfo struct {
|
||||||
|
SkillListLen uint32 `struc:"sizeof=SkillList"`
|
||||||
|
SkillList []uint32 `json:"skillList"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func isSameUint32Slice(a []uint32, b []uint32) bool {
|
||||||
|
if len(a) != len(b) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for index := range a {
|
||||||
|
if a[index] != b[index] {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func collectPetLearnableSkillList(currentPet *model.PetInfo) []uint32 {
|
||||||
|
skillSet := make(map[uint32]struct{})
|
||||||
|
skills := make([]uint32, 0)
|
||||||
|
|
||||||
|
appendSkill := func(skillID uint32) {
|
||||||
|
if skillID == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if _, exists := skillSet[skillID]; exists {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
skillSet[skillID] = struct{}{}
|
||||||
|
skills = append(skills, skillID)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, skillID := range currentPet.GetLevelRangeCanLearningSkills(1, currentPet.Level) {
|
||||||
|
appendSkill(skillID)
|
||||||
|
}
|
||||||
|
for _, skillID := range currentPet.ExtSKill {
|
||||||
|
appendSkill(skillID)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, skill := range currentPet.SkillList {
|
||||||
|
delete(skillSet, skill.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
result := make([]uint32, 0, len(skillSet))
|
||||||
|
for _, skillID := range skills {
|
||||||
|
if _, exists := skillSet[skillID]; exists {
|
||||||
|
result = append(result, skillID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPetLearnableSkills 查询当前精灵可学习技能(等级技能 + 额外技能ExtSKill)
|
||||||
|
func (h Controller) GetPetLearnableSkills(
|
||||||
|
data *GetPetLearnableSkillsInboundInfo,
|
||||||
|
c *player.Player,
|
||||||
|
) (result *GetPetLearnableSkillsOutboundInfo, err errorcode.ErrorCode) {
|
||||||
|
slot, ok := c.FindPetBagSlot(data.CatchTime)
|
||||||
|
currentPet := slot.PetInfoPtr()
|
||||||
|
if !ok || currentPet == nil {
|
||||||
|
return nil, errorcode.ErrorCodes.ErrPokemonNotExists
|
||||||
|
}
|
||||||
|
|
||||||
|
return &GetPetLearnableSkillsOutboundInfo{
|
||||||
|
SkillList: collectPetLearnableSkillList(currentPet),
|
||||||
|
}, 0
|
||||||
|
}
|
||||||
|
|
||||||
// SetPetSkill 设置宠物技能,消耗50赛尔豆
|
// SetPetSkill 设置宠物技能,消耗50赛尔豆
|
||||||
func (h Controller) SetPetSkill(data *ChangeSkillInfo, c *player.Player) (result *pet.ChangeSkillOutInfo, err errorcode.ErrorCode) {
|
func (h Controller) SetPetSkill(data *ChangeSkillInfo, c *player.Player) (result *pet.ChangeSkillOutInfo, err errorcode.ErrorCode) {
|
||||||
const setSkillCost = 50
|
const setSkillCost = 50
|
||||||
|
|
||||||
if !c.GetCoins(setSkillCost) {
|
slot, ok := c.FindPetBagSlot(data.CatchTime)
|
||||||
return nil, errorcode.ErrorCodes.ErrSunDouInsufficient10016
|
currentPet := slot.PetInfoPtr()
|
||||||
}
|
if !ok || currentPet == nil {
|
||||||
|
|
||||||
c.Info.Coins -= setSkillCost
|
|
||||||
|
|
||||||
_, currentPet, ok := c.FindPet(data.CatchTime)
|
|
||||||
if !ok {
|
|
||||||
return nil, errorcode.ErrorCodes.ErrSystemBusy
|
return nil, errorcode.ErrorCodes.ErrSystemBusy
|
||||||
}
|
}
|
||||||
canleaernskill := currentPet.GetLevelRangeCanLearningSkills(1, currentPet.Level)
|
|
||||||
|
|
||||||
_, ok = lo.Find(canleaernskill, func(item uint32) bool {
|
canLearnSkillSet := make(map[uint32]struct{})
|
||||||
return item == data.ReplaceSkill
|
for _, skillID := range collectPetLearnableSkillList(currentPet) {
|
||||||
})
|
canLearnSkillSet[skillID] = struct{}{}
|
||||||
|
|
||||||
if !ok {
|
|
||||||
return result, errorcode.ErrorCodes.ErrSystemBusy
|
|
||||||
}
|
}
|
||||||
_, _, ok = utils.FindWithIndex(currentPet.SkillList, func(item model.SkillInfo) bool { //已经存在技能
|
if _, exists := canLearnSkillSet[data.ReplaceSkill]; !exists {
|
||||||
|
return nil, errorcode.ErrorCodes.ErrSystemBusy
|
||||||
|
}
|
||||||
|
|
||||||
|
skillInfo, exists := xmlres.SkillMap[int(data.ReplaceSkill)]
|
||||||
|
if !exists {
|
||||||
|
return nil, errorcode.ErrorCodes.ErrSystemBusy
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _, ok = utils.FindWithIndex(currentPet.SkillList, func(item model.SkillInfo) bool {
|
||||||
return item.ID == data.ReplaceSkill
|
return item.ID == data.ReplaceSkill
|
||||||
})
|
})
|
||||||
if ok {
|
if ok {
|
||||||
return nil, errorcode.ErrorCodes.ErrSystemBusy
|
return nil, errorcode.ErrorCodes.ErrSystemBusy
|
||||||
}
|
}
|
||||||
|
|
||||||
// 查找要学习的技能并替换
|
if data.HasSkill == 0 && len(currentPet.SkillList) >= 4 {
|
||||||
_, targetSkill, ok := utils.FindWithIndex(currentPet.SkillList, func(item model.SkillInfo) bool {
|
return nil, errorcode.ErrorCodes.ErrSystemBusy
|
||||||
|
}
|
||||||
|
|
||||||
|
if data.HasSkill != 0 {
|
||||||
|
_, _, found := utils.FindWithIndex(currentPet.SkillList, func(item model.SkillInfo) bool {
|
||||||
|
return item.ID == data.HasSkill
|
||||||
|
})
|
||||||
|
if !found {
|
||||||
|
return nil, errorcode.ErrorCodes.ErrSystemBusy
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !c.GetCoins(setSkillCost) {
|
||||||
|
return nil, errorcode.ErrorCodes.ErrSunDouInsufficient10016
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Info.Coins -= setSkillCost
|
||||||
|
maxPP := uint32(skillInfo.MaxPP)
|
||||||
|
if data.HasSkill != 0 {
|
||||||
|
_, targetSkill, _ := utils.FindWithIndex(currentPet.SkillList, func(item model.SkillInfo) bool {
|
||||||
return item.ID == data.HasSkill
|
return item.ID == data.HasSkill
|
||||||
})
|
})
|
||||||
if ok {
|
|
||||||
targetSkill.ID = data.ReplaceSkill
|
targetSkill.ID = data.ReplaceSkill
|
||||||
targetSkill.PP = uint32(xmlres.SkillMap[int(targetSkill.ID)].MaxPP)
|
targetSkill.PP = maxPP
|
||||||
|
} else {
|
||||||
|
currentPet.SkillList = append(currentPet.SkillList, model.SkillInfo{
|
||||||
|
ID: data.ReplaceSkill,
|
||||||
|
PP: maxPP,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return &pet.ChangeSkillOutInfo{
|
return &pet.ChangeSkillOutInfo{
|
||||||
@@ -60,25 +149,139 @@ func (h Controller) SetPetSkill(data *ChangeSkillInfo, c *player.Player) (result
|
|||||||
func (h Controller) SortPetSkills(data *C2S_Skill_Sort, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
|
func (h Controller) SortPetSkills(data *C2S_Skill_Sort, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
|
||||||
const skillSortCost = 50
|
const skillSortCost = 50
|
||||||
|
|
||||||
|
slot, ok := c.FindPetBagSlot(data.CapTm)
|
||||||
|
currentPet := slot.PetInfoPtr()
|
||||||
|
if !ok || currentPet == nil {
|
||||||
|
return nil, errorcode.ErrorCodes.ErrPokemonNotExists
|
||||||
|
}
|
||||||
|
|
||||||
|
usedSkillSet := make(map[uint32]struct{})
|
||||||
|
newSkillList := make([]model.SkillInfo, 0, 4)
|
||||||
|
|
||||||
|
for _, skillID := range data.Skill {
|
||||||
|
if skillID == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, used := usedSkillSet[skillID]; used {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
_, skill, found := utils.FindWithIndex(currentPet.SkillList, func(item model.SkillInfo) bool {
|
||||||
|
return item.ID == skillID
|
||||||
|
})
|
||||||
|
if !found {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
newSkillList = append(newSkillList, *skill)
|
||||||
|
usedSkillSet[skillID] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, skill := range currentPet.SkillList {
|
||||||
|
if skill.ID == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, used := usedSkillSet[skill.ID]; used {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
newSkillList = append(newSkillList, skill)
|
||||||
|
usedSkillSet[skill.ID] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(newSkillList) > 4 {
|
||||||
|
newSkillList = newSkillList[:4]
|
||||||
|
}
|
||||||
|
|
||||||
if !c.GetCoins(skillSortCost) {
|
if !c.GetCoins(skillSortCost) {
|
||||||
return nil, errorcode.ErrorCodes.ErrSunDouInsufficient10016
|
return nil, errorcode.ErrorCodes.ErrSunDouInsufficient10016
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Info.Coins -= skillSortCost
|
c.Info.Coins -= skillSortCost
|
||||||
|
|
||||||
_, currentPet, ok := c.FindPet(data.CapTm)
|
|
||||||
if ok {
|
|
||||||
var newSkillList []model.SkillInfo
|
|
||||||
for _, skillID := range data.Skill {
|
|
||||||
_, skill, found := utils.FindWithIndex(currentPet.SkillList, func(item model.SkillInfo) bool {
|
|
||||||
return item.ID == skillID
|
|
||||||
})
|
|
||||||
if found {
|
|
||||||
newSkillList = append(newSkillList, *skill)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
currentPet.SkillList = newSkillList
|
currentPet.SkillList = newSkillList
|
||||||
}
|
|
||||||
|
return nil, 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// CommitPetSkills 按最终技能列表一次性提交学习/替换/排序结果。
|
||||||
|
func (h Controller) CommitPetSkills(
|
||||||
|
data *CommitPetSkillsInboundInfo,
|
||||||
|
c *player.Player,
|
||||||
|
) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) {
|
||||||
|
const setSkillCost = 50
|
||||||
|
const skillSortCost = 50
|
||||||
|
|
||||||
|
slot, ok := c.FindPetBagSlot(data.CatchTime)
|
||||||
|
currentPet := slot.PetInfoPtr()
|
||||||
|
if !ok || currentPet == nil {
|
||||||
|
return nil, errorcode.ErrorCodes.ErrPokemonNotExists
|
||||||
|
}
|
||||||
|
|
||||||
|
currentSkillSet := make(map[uint32]model.SkillInfo, len(currentPet.SkillList))
|
||||||
|
currentSkillOrder := make([]uint32, 0, len(currentPet.SkillList))
|
||||||
|
for _, skill := range currentPet.SkillList {
|
||||||
|
if skill.ID == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
currentSkillSet[skill.ID] = skill
|
||||||
|
currentSkillOrder = append(currentSkillOrder, skill.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
finalSkillIDs := make([]uint32, 0, 4)
|
||||||
|
usedSkillSet := make(map[uint32]struct{}, 4)
|
||||||
|
for _, skillID := range data.Skill {
|
||||||
|
if skillID == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, exists := usedSkillSet[skillID]; exists {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
usedSkillSet[skillID] = struct{}{}
|
||||||
|
finalSkillIDs = append(finalSkillIDs, skillID)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(finalSkillIDs) == 0 {
|
||||||
|
return nil, errorcode.ErrorCodes.ErrSystemBusy
|
||||||
|
}
|
||||||
|
if len(finalSkillIDs) > 4 {
|
||||||
|
finalSkillIDs = finalSkillIDs[:4]
|
||||||
|
}
|
||||||
|
if isSameUint32Slice(currentSkillOrder, finalSkillIDs) {
|
||||||
|
return nil, 0
|
||||||
|
}
|
||||||
|
|
||||||
|
learnableSkillSet := make(map[uint32]struct{})
|
||||||
|
for _, skillID := range collectPetLearnableSkillList(currentPet) {
|
||||||
|
learnableSkillSet[skillID] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
newSkillCount := 0
|
||||||
|
finalSkillList := make([]model.SkillInfo, 0, len(finalSkillIDs))
|
||||||
|
for _, skillID := range finalSkillIDs {
|
||||||
|
if skill, exists := currentSkillSet[skillID]; exists {
|
||||||
|
finalSkillList = append(finalSkillList, skill)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, exists := learnableSkillSet[skillID]; !exists {
|
||||||
|
return nil, errorcode.ErrorCodes.ErrSystemBusy
|
||||||
|
}
|
||||||
|
skillInfo, exists := xmlres.SkillMap[int(skillID)]
|
||||||
|
if !exists {
|
||||||
|
return nil, errorcode.ErrorCodes.ErrSystemBusy
|
||||||
|
}
|
||||||
|
newSkillCount++
|
||||||
|
finalSkillList = append(finalSkillList, model.SkillInfo{
|
||||||
|
ID: skillID,
|
||||||
|
PP: uint32(skillInfo.MaxPP),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
totalCost := int64(newSkillCount * setSkillCost)
|
||||||
|
if newSkillCount == 0 {
|
||||||
|
totalCost += int64(skillSortCost)
|
||||||
|
}
|
||||||
|
if totalCost > 0 && !c.GetCoins(totalCost) {
|
||||||
|
return nil, errorcode.ErrorCodes.ErrSunDouInsufficient10016
|
||||||
|
}
|
||||||
|
c.Info.Coins -= totalCost
|
||||||
|
currentPet.SkillList = finalSkillList
|
||||||
|
|
||||||
return nil, 0
|
return nil, 0
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,15 +17,13 @@ func (h Controller) IsCollect(
|
|||||||
ID: data.Type,
|
ID: data.Type,
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Service.Task.Exec(uint32(1335), func(te *model.Task) bool {
|
taskData, taskErr := c.Service.Task.GetTask(uint32(1335))
|
||||||
|
if taskErr == nil {
|
||||||
r := bitset32.From(te.Data)
|
r := bitset32.From(taskData.Data)
|
||||||
// 分支未完成时,标记完成并发放奖励
|
|
||||||
if r.Test(uint(data.Type)) {
|
if r.Test(uint(data.Type)) {
|
||||||
result.IsCom = 1
|
result.IsCom = 1
|
||||||
}
|
}
|
||||||
return false
|
}
|
||||||
})
|
|
||||||
|
|
||||||
_, ok := lo.Find([]uint32{1, 2, 3, 4, 301}, func(item uint32) bool {
|
_, ok := lo.Find([]uint32{1, 2, 3, 4, 301}, func(item uint32) bool {
|
||||||
return data.Type == item
|
return data.Type == item
|
||||||
@@ -59,14 +57,17 @@ func (h Controller) Collect(
|
|||||||
return data.Type == item
|
return data.Type == item
|
||||||
})
|
})
|
||||||
if res == model.Completed && ok { //这块是为了兼容旧版本
|
if res == model.Completed && ok { //这块是为了兼容旧版本
|
||||||
c.Service.Task.Exec(uint32(1335), func(te *model.Task) bool {
|
taskData, taskErr := c.Service.Task.GetTask(uint32(1335))
|
||||||
|
if taskErr != nil {
|
||||||
r := bitset32.From(te.Data)
|
return nil, errorcode.ErrorCode(errorcode.ErrorCodes.ErrSystemError)
|
||||||
|
}
|
||||||
|
|
||||||
|
r := bitset32.From(taskData.Data)
|
||||||
r.Set(uint(data.Type))
|
r.Set(uint(data.Type))
|
||||||
te.Data = r.Bytes()
|
taskData.Data = r.Bytes()
|
||||||
return true
|
if taskErr = c.Service.Task.SetTask(taskData); taskErr != nil {
|
||||||
})
|
return nil, errorcode.ErrorCode(errorcode.ErrorCodes.ErrSystemError)
|
||||||
|
}
|
||||||
return nil, errorcode.ErrorCode(errorcode.ErrorCodes.ErrSystemError)
|
return nil, errorcode.ErrorCode(errorcode.ErrorCodes.ErrSystemError)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,21 +81,22 @@ func (h Controller) Collect(
|
|||||||
if !lo.Contains(validIDs, data.ID) {
|
if !lo.Contains(validIDs, data.ID) {
|
||||||
return nil, errorcode.ErrorCode(errorcode.ErrorCodes.ErrSystemError)
|
return nil, errorcode.ErrorCode(errorcode.ErrorCodes.ErrSystemError)
|
||||||
}
|
}
|
||||||
c.Service.Task.Exec(uint32(1335), func(te *model.Task) bool {
|
taskData, taskErr := c.Service.Task.GetTask(uint32(1335))
|
||||||
|
if taskErr != nil {
|
||||||
|
return nil, errorcode.ErrorCode(errorcode.ErrorCodes.ErrSystemError)
|
||||||
|
}
|
||||||
|
|
||||||
r := bitset32.From(te.Data)
|
r := bitset32.From(taskData.Data)
|
||||||
// 分支未完成时,标记完成并发放奖励
|
|
||||||
if !r.Test(uint(data.Type)) {
|
if !r.Test(uint(data.Type)) {
|
||||||
r.Set(uint(data.Type))
|
r.Set(uint(data.Type))
|
||||||
te.Data = r.Bytes()
|
taskData.Data = r.Bytes()
|
||||||
r := model.GenPetInfo(int(data.ID), -1, -1, 0, 1, nil, 0)
|
if taskErr = c.Service.Task.SetTask(taskData); taskErr != nil {
|
||||||
c.Service.Pet.PetAdd(r, 0)
|
return nil, errorcode.ErrorCode(errorcode.ErrorCodes.ErrSystemError)
|
||||||
result.CatchTime = r.CatchTime
|
}
|
||||||
|
petInfo := model.GenPetInfo(int(data.ID), -1, -1, 0, 1, nil, 0)
|
||||||
return true
|
c.Service.Pet.PetAdd(petInfo, 0)
|
||||||
|
result.CatchTime = petInfo.CatchTime
|
||||||
}
|
}
|
||||||
return false
|
|
||||||
})
|
|
||||||
|
|
||||||
if result.CatchTime == 0 {
|
if result.CatchTime == 0 {
|
||||||
return nil, errorcode.ErrorCode(errorcode.ErrorCodes.ErrAwardAlreadyClaimed)
|
return nil, errorcode.ErrorCode(errorcode.ErrorCodes.ErrAwardAlreadyClaimed)
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"blazing/logic/service/user"
|
"blazing/logic/service/user"
|
||||||
configservice "blazing/modules/config/service"
|
configservice "blazing/modules/config/service"
|
||||||
playerservice "blazing/modules/player/service"
|
playerservice "blazing/modules/player/service"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -13,10 +14,11 @@ import (
|
|||||||
func (h Controller) CDK(data *C2S_GET_GIFT_COMPLETE, player *logicplayer.Player) (result *user.S2C_GET_GIFT_COMPLETE, err errorcode.ErrorCode) {
|
func (h Controller) CDK(data *C2S_GET_GIFT_COMPLETE, player *logicplayer.Player) (result *user.S2C_GET_GIFT_COMPLETE, err errorcode.ErrorCode) {
|
||||||
result = &user.S2C_GET_GIFT_COMPLETE{}
|
result = &user.S2C_GET_GIFT_COMPLETE{}
|
||||||
|
|
||||||
|
cdkCode := strings.Trim(data.PassText, "\x00")
|
||||||
cdkService := configservice.NewCdkService()
|
cdkService := configservice.NewCdkService()
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
|
|
||||||
r := cdkService.Get(data.PassText)
|
r := cdkService.Get(cdkCode)
|
||||||
if r == nil {
|
if r == nil {
|
||||||
return nil, errorcode.ErrorCodes.ErrMolecularCodeNotExists
|
return nil, errorcode.ErrorCodes.ErrMolecularCodeNotExists
|
||||||
}
|
}
|
||||||
@@ -29,7 +31,7 @@ func (h Controller) CDK(data *C2S_GET_GIFT_COMPLETE, player *logicplayer.Player)
|
|||||||
if !player.Service.Cdk.CanGet(uint32(r.ID)) {
|
if !player.Service.Cdk.CanGet(uint32(r.ID)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !cdkService.Set(data.PassText) {
|
if !cdkService.Set(cdkCode) {
|
||||||
return nil, errorcode.ErrorCodes.ErrMolecularCodeGiftsGone
|
return nil, errorcode.ErrorCodes.ErrMolecularCodeGiftsGone
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,12 +28,15 @@ func (h Controller) AcceptTask(data *AcceptTaskInboundInfo, c *player.Player) (r
|
|||||||
}
|
}
|
||||||
|
|
||||||
c.Info.SetTask(int(data.TaskId), model.Accepted)
|
c.Info.SetTask(int(data.TaskId), model.Accepted)
|
||||||
c.Service.Task.Exec(uint32(data.TaskId), func(t *model.Task) bool {
|
taskData, taskErr := c.Service.Task.GetTask(uint32(data.TaskId))
|
||||||
t.Data = []uint32{}
|
if taskErr != nil {
|
||||||
|
return nil, errorcode.ErrorCodes.ErrSystemError
|
||||||
|
}
|
||||||
|
taskData.Data = []uint32{}
|
||||||
|
if taskErr = c.Service.Task.SetTask(taskData); taskErr != nil {
|
||||||
|
return nil, errorcode.ErrorCodes.ErrSystemError
|
||||||
|
}
|
||||||
|
|
||||||
return true
|
|
||||||
|
|
||||||
})
|
|
||||||
result = &task.AcceptTaskOutboundInfo{}
|
result = &task.AcceptTaskOutboundInfo{}
|
||||||
result.TaskId = data.TaskId
|
result.TaskId = data.TaskId
|
||||||
return result, 0
|
return result, 0
|
||||||
@@ -48,10 +51,14 @@ func (h Controller) AddTaskBuf(data *AddTaskBufInboundInfo, c *player.Player) (r
|
|||||||
if c.Info.GetTask(int(data.TaskId)) != model.Accepted {
|
if c.Info.GetTask(int(data.TaskId)) != model.Accepted {
|
||||||
return result, errorcode.ErrorCodes.ErrAwardAlreadyClaimed
|
return result, errorcode.ErrorCodes.ErrAwardAlreadyClaimed
|
||||||
}
|
}
|
||||||
c.Service.Task.Exec(data.TaskId, func(taskEx *model.Task) bool {
|
taskData, taskErr := c.Service.Task.GetTask(data.TaskId)
|
||||||
taskEx.Data = data.TaskList
|
if taskErr != nil {
|
||||||
return true
|
return nil, errorcode.ErrorCodes.ErrSystemError
|
||||||
})
|
}
|
||||||
|
taskData.Data = data.TaskList
|
||||||
|
if taskErr = c.Service.Task.SetTask(taskData); taskErr != nil {
|
||||||
|
return nil, errorcode.ErrorCodes.ErrSystemError
|
||||||
|
}
|
||||||
return result, 0
|
return result, 0
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,31 +77,16 @@ func (h Controller) CompleteTask(data1 *CompleteTaskInboundInfo, c *player.Playe
|
|||||||
// if service.NewTaskService().IsAcceptable(data1.TaskId) == nil {
|
// if service.NewTaskService().IsAcceptable(data1.TaskId) == nil {
|
||||||
// return nil, errorcode.ErrorCodes.ErrSystemError
|
// return nil, errorcode.ErrorCodes.ErrSystemError
|
||||||
// }
|
// }
|
||||||
c.Info.SetTask(int(data1.TaskId), model.Completed)
|
|
||||||
|
|
||||||
result = &task.CompleteTaskOutboundInfo{
|
result = &task.CompleteTaskOutboundInfo{
|
||||||
TaskId: data1.TaskId,
|
TaskId: data1.TaskId,
|
||||||
ItemList: make([]data.ItemInfo, 0),
|
ItemList: make([]data.ItemInfo, 0),
|
||||||
}
|
}
|
||||||
|
|
||||||
taskInfo := task.GetTaskInfo(int(data1.TaskId), int(data1.OutState))
|
if _, err = c.ApplyTaskCompletion(data1.TaskId, int(data1.OutState), result); err != 0 {
|
||||||
if taskInfo == nil {
|
return nil, err
|
||||||
return nil, errorcode.ErrorCodes.ErrNeedCompleteTaskForPrize
|
|
||||||
}
|
}
|
||||||
|
if taskErr := c.Info.SetTask(int(data1.TaskId), model.Completed); taskErr != nil {
|
||||||
if taskInfo.Pet != nil {
|
return nil, errorcode.ErrorCodes.ErrSystemError
|
||||||
|
|
||||||
c.Service.Pet.PetAdd(taskInfo.Pet, 0)
|
|
||||||
result.CaptureTime = taskInfo.Pet.CatchTime
|
|
||||||
result.PetTypeId = taskInfo.Pet.ID
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, item := range taskInfo.ItemList {
|
|
||||||
success := c.ItemAdd(item.ItemId, item.ItemCnt)
|
|
||||||
if success {
|
|
||||||
result.ItemList = append(result.ItemList, item)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return result, 0 //通过PUB/SUB回包
|
return result, 0 //通过PUB/SUB回包
|
||||||
@@ -105,11 +97,12 @@ func (h Controller) GetTaskBuf(data *GetTaskBufInboundInfo, c *player.Player) (r
|
|||||||
result = &task.GetTaskBufOutboundInfo{
|
result = &task.GetTaskBufOutboundInfo{
|
||||||
TaskId: data.TaskId,
|
TaskId: data.TaskId,
|
||||||
}
|
}
|
||||||
c.Service.Task.Exec(data.TaskId, func(te *model.Task) bool {
|
|
||||||
|
|
||||||
result.TaskList = te.Data
|
taskData, taskErr := c.Service.Task.GetTask(data.TaskId)
|
||||||
return false
|
if taskErr != nil {
|
||||||
})
|
return nil, errorcode.ErrorCodes.ErrSystemError
|
||||||
|
}
|
||||||
|
result.TaskList = taskData.Data
|
||||||
|
|
||||||
return result, 0
|
return result, 0
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ func PprofWeb() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 所有端口都失败时的兜底
|
// 所有端口都失败时的兜底
|
||||||
errMsg := fmt.Sprintf("[FATAL] 端口9909/9910均监听失败,pprof服务启动失败")
|
errMsg := "[FATAL] 端口9909/9910均监听失败,pprof服务启动失败"
|
||||||
fmt.Println(errMsg)
|
fmt.Println(errMsg)
|
||||||
// 可选:根据业务需求决定是否panic
|
// 可选:根据业务需求决定是否panic
|
||||||
// panic(errMsg)
|
// panic(errMsg)
|
||||||
@@ -148,7 +148,7 @@ func monitorMemAndQuit() {
|
|||||||
|
|
||||||
// 4. 超70%阈值,执行优雅退出
|
// 4. 超70%阈值,执行优雅退出
|
||||||
if usedRatio >= memThresholdRatio {
|
if usedRatio >= memThresholdRatio {
|
||||||
log.Fatalf("内存占比达%.1f%%,超过90%阈值,程序开始退出", usedRatio*100)
|
log.Fatalf("内存占比达%.1f%%,超过90%%阈值,程序开始退出", usedRatio*100)
|
||||||
// ########## 可选:这里添加你的优雅清理逻辑 ##########
|
// ########## 可选:这里添加你的优雅清理逻辑 ##########
|
||||||
// 如:关闭数据库连接、释放文件句柄、保存业务状态、推送退出告警等
|
// 如:关闭数据库连接、释放文件句柄、保存业务状态、推送退出告警等
|
||||||
cleanup()
|
cleanup()
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ func (f *FightC) openActionWindow() {
|
|||||||
f.actionMu.Lock()
|
f.actionMu.Lock()
|
||||||
f.acceptActions = true
|
f.acceptActions = true
|
||||||
f.pendingActions = f.pendingActions[:0]
|
f.pendingActions = f.pendingActions[:0]
|
||||||
|
f.pendingHead = 0
|
||||||
f.actionRound.Store(uint32(f.Round))
|
f.actionRound.Store(uint32(f.Round))
|
||||||
f.actionMu.Unlock()
|
f.actionMu.Unlock()
|
||||||
}
|
}
|
||||||
@@ -52,6 +53,7 @@ func (f *FightC) closeActionWindow() {
|
|||||||
f.actionMu.Lock()
|
f.actionMu.Lock()
|
||||||
f.acceptActions = false
|
f.acceptActions = false
|
||||||
f.pendingActions = f.pendingActions[:0]
|
f.pendingActions = f.pendingActions[:0]
|
||||||
|
f.pendingHead = 0
|
||||||
f.actionRound.Store(0)
|
f.actionRound.Store(0)
|
||||||
f.actionMu.Unlock()
|
f.actionMu.Unlock()
|
||||||
|
|
||||||
@@ -73,8 +75,10 @@ func (f *FightC) submitAction(act action.BattleActionI) {
|
|||||||
f.actionMu.Unlock()
|
f.actionMu.Unlock()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
f.compactPendingActionsLocked()
|
||||||
replaceIndex := -1
|
replaceIndex := -1
|
||||||
for i, pending := range f.pendingActions {
|
for i := f.pendingHead; i < len(f.pendingActions); i++ {
|
||||||
|
pending := f.pendingActions[i]
|
||||||
if pending == nil || actionSlotKeyFromAction(pending) != actionSlotKeyFromAction(act) {
|
if pending == nil || actionSlotKeyFromAction(pending) != actionSlotKeyFromAction(act) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -82,6 +86,10 @@ func (f *FightC) submitAction(act action.BattleActionI) {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
if replaceIndex >= 0 {
|
if replaceIndex >= 0 {
|
||||||
|
if f.LegacyGroupProtocol {
|
||||||
|
f.actionMu.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
f.pendingActions[replaceIndex] = act
|
f.pendingActions[replaceIndex] = act
|
||||||
} else {
|
} else {
|
||||||
f.pendingActions = append(f.pendingActions, act)
|
f.pendingActions = append(f.pendingActions, act)
|
||||||
@@ -101,15 +109,23 @@ func (f *FightC) submitAction(act action.BattleActionI) {
|
|||||||
|
|
||||||
func (f *FightC) nextAction() action.BattleActionI {
|
func (f *FightC) nextAction() action.BattleActionI {
|
||||||
f.actionMu.Lock()
|
f.actionMu.Lock()
|
||||||
if len(f.pendingActions) == 0 {
|
if f.pendingHead >= len(f.pendingActions) {
|
||||||
|
f.pendingActions = f.pendingActions[:0]
|
||||||
|
f.pendingHead = 0
|
||||||
f.actionMu.Unlock()
|
f.actionMu.Unlock()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
act := f.pendingActions[0]
|
act := f.pendingActions[f.pendingHead]
|
||||||
copy(f.pendingActions, f.pendingActions[1:])
|
f.pendingActions[f.pendingHead] = nil
|
||||||
f.pendingActions = f.pendingActions[:len(f.pendingActions)-1]
|
f.pendingHead++
|
||||||
hasMore := len(f.pendingActions) > 0
|
hasMore := f.pendingHead < len(f.pendingActions)
|
||||||
|
if !hasMore {
|
||||||
|
f.pendingActions = f.pendingActions[:0]
|
||||||
|
f.pendingHead = 0
|
||||||
|
} else {
|
||||||
|
f.compactPendingActionsLocked()
|
||||||
|
}
|
||||||
notify := f.actionNotify
|
notify := f.actionNotify
|
||||||
f.actionMu.Unlock()
|
f.actionMu.Unlock()
|
||||||
|
|
||||||
@@ -123,6 +139,22 @@ func (f *FightC) nextAction() action.BattleActionI {
|
|||||||
return act
|
return act
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *FightC) compactPendingActionsLocked() {
|
||||||
|
if f.pendingHead == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if f.pendingHead < len(f.pendingActions)/2 && len(f.pendingActions) < cap(f.pendingActions) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
remaining := len(f.pendingActions) - f.pendingHead
|
||||||
|
copy(f.pendingActions, f.pendingActions[f.pendingHead:])
|
||||||
|
for i := remaining; i < len(f.pendingActions); i++ {
|
||||||
|
f.pendingActions[i] = nil
|
||||||
|
}
|
||||||
|
f.pendingActions = f.pendingActions[:remaining]
|
||||||
|
f.pendingHead = 0
|
||||||
|
}
|
||||||
|
|
||||||
// 玩家逃跑/无响应/掉线
|
// 玩家逃跑/无响应/掉线
|
||||||
func (f *FightC) Over(c common.PlayerI, res model.EnumBattleOverReason) {
|
func (f *FightC) Over(c common.PlayerI, res model.EnumBattleOverReason) {
|
||||||
if f.closefight {
|
if f.closefight {
|
||||||
@@ -143,10 +175,13 @@ func (f *FightC) Over(c common.PlayerI, res model.EnumBattleOverReason) {
|
|||||||
// }
|
// }
|
||||||
|
|
||||||
f.overl.Do(func() {
|
f.overl.Do(func() {
|
||||||
f.Reason = res
|
f.Reason = normalizeFightOverReason(res)
|
||||||
if f.GetInputByPlayer(c, true) != nil {
|
if f.GetInputByPlayer(c, true) != nil {
|
||||||
f.WinnerId = f.GetInputByPlayer(c, true).UserID
|
f.WinnerId = f.GetInputByPlayer(c, true).UserID
|
||||||
}
|
}
|
||||||
|
f.FightOverInfo.Reason = f.Reason
|
||||||
|
f.FightOverInfo.WinnerId = f.WinnerId
|
||||||
|
f.closefight = true
|
||||||
|
|
||||||
close(f.quit)
|
close(f.quit)
|
||||||
|
|
||||||
@@ -281,9 +316,19 @@ func (f *FightC) UseItemAt(c common.PlayerI, cacthid, itemid uint32, actorIndex,
|
|||||||
|
|
||||||
// ReadyFight 处理玩家战斗准备逻辑,当满足条件时启动战斗循环
|
// ReadyFight 处理玩家战斗准备逻辑,当满足条件时启动战斗循环
|
||||||
func (f *FightC) ReadyFight(c common.PlayerI) {
|
func (f *FightC) ReadyFight(c common.PlayerI) {
|
||||||
f.Broadcast(func(ff *input.Input) {
|
if f.LegacyGroupProtocol {
|
||||||
|
input := f.GetInputByPlayer(c, false)
|
||||||
ff.Player.SendPackCmd(2404, &info.S2C_2404{UserID: c.GetInfo().UserID})
|
if input == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
input.Finished = true
|
||||||
|
if f.checkBothPlayersReady(c) {
|
||||||
|
f.startBattle(f.FightStartOutboundInfo)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
f.BroadcastPlayers(func(p common.PlayerI) {
|
||||||
|
p.SendPackCmd(2404, &info.S2C_2404{UserID: c.GetInfo().UserID})
|
||||||
})
|
})
|
||||||
// 2. 标记当前玩家已准备完成
|
// 2. 标记当前玩家已准备完成
|
||||||
input := f.GetInputByPlayer(c, false)
|
input := f.GetInputByPlayer(c, false)
|
||||||
@@ -325,7 +370,7 @@ func (f *FightC) collectFightPetInfos(inputs []*input.Input) []info.FightPetInfo
|
|||||||
Hp: currentPet.Info.Hp,
|
Hp: currentPet.Info.Hp,
|
||||||
MaxHp: currentPet.Info.MaxHp,
|
MaxHp: currentPet.Info.MaxHp,
|
||||||
Level: currentPet.Info.Level,
|
Level: currentPet.Info.Level,
|
||||||
Catchable: uint32(fighter.CanCapture),
|
Catchable: fightPetCatchableFlag(fighter.CanCapture),
|
||||||
}
|
}
|
||||||
if fighter.AttackValue != nil {
|
if fighter.AttackValue != nil {
|
||||||
fightInfo.Prop = fighter.AttackValue.Prop
|
fightInfo.Prop = fighter.AttackValue.Prop
|
||||||
@@ -335,6 +380,13 @@ func (f *FightC) collectFightPetInfos(inputs []*input.Input) []info.FightPetInfo
|
|||||||
return infos
|
return infos
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func fightPetCatchableFlag(catchRate int) uint32 {
|
||||||
|
if catchRate > 0 {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
// checkBothPlayersReady 检查PVP战斗中双方是否都已准备完成
|
// checkBothPlayersReady 检查PVP战斗中双方是否都已准备完成
|
||||||
// 参数c为当前准备的玩家,返回true表示双方均准备完成
|
// 参数c为当前准备的玩家,返回true表示双方均准备完成
|
||||||
func (f *FightC) checkBothPlayersReady(currentPlayer common.PlayerI) bool {
|
func (f *FightC) checkBothPlayersReady(currentPlayer common.PlayerI) bool {
|
||||||
@@ -355,8 +407,12 @@ func (f *FightC) startBattle(startInfo info.FightStartOutboundInfo) {
|
|||||||
go f.battleLoop()
|
go f.battleLoop()
|
||||||
|
|
||||||
// 向双方广播战斗开始信息
|
// 向双方广播战斗开始信息
|
||||||
f.Broadcast(func(ff *input.Input) {
|
f.BroadcastPlayers(func(p common.PlayerI) {
|
||||||
ff.Player.SendPackCmd(2504, &startInfo)
|
if f.LegacyGroupProtocol {
|
||||||
|
f.sendLegacyGroupStart(p)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
f.sendFightPacket(p, fightPacketStart, &startInfo)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ type NewSel41 struct {
|
|||||||
NewSel0
|
NewSel0
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *NewSel41) Skill_Use_ex() bool {
|
func (e *NewSel41) Skill_Use() bool {
|
||||||
if e.ID().GetCatchTime() != e.Ctx().Our.CurPet[0].Info.CatchTime {
|
if e.ID().GetCatchTime() != e.Ctx().Our.CurPet[0].Info.CatchTime {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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{})
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -311,7 +311,7 @@ func (e *Effect2194) OnSkill() bool {
|
|||||||
if e.Ctx().Opp.CurPet[0] == nil {
|
if e.Ctx().Opp.CurPet[0] == nil {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
addStatusByID(e.Ctx().Our, e.Ctx().Opp, int(info.PetStatus.DrainedHP))
|
addTimedStatus(e.Ctx().Our, e.Ctx().Opp, int(info.PetStatus.DrainedHP), 4)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ func (e *Effect13) OnSkill() bool {
|
|||||||
if eff == nil {
|
if eff == nil {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
eff.Duration(e.EffectNode.SideEffectArgs[0] - 1)
|
eff.Duration(e.EffectNode.SideEffectArgs[0])
|
||||||
|
|
||||||
e.Ctx().Opp.AddEffect(e.Ctx().Our, eff)
|
e.Ctx().Opp.AddEffect(e.Ctx().Our, eff)
|
||||||
return true
|
return true
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ var effectInfoByID = map[int]string{
|
|||||||
29: "额外附加{0}点固定伤害",
|
29: "额外附加{0}点固定伤害",
|
||||||
31: "",
|
31: "",
|
||||||
32: "使用后{0}回合攻击击中对象要害概率增加1/16",
|
32: "使用后{0}回合攻击击中对象要害概率增加1/16",
|
||||||
33: "消除对手能力提升状态",
|
33: "消除敌方阵营所有强化",
|
||||||
34: "将所受的伤害{0}倍反馈给对手",
|
34: "将所受的伤害{0}倍反馈给对手",
|
||||||
35: "惩罚,对方能力等级越高,此技能威力越大",
|
35: "惩罚,对方能力等级越高,此技能威力越大",
|
||||||
36: "命中时{0}%的概率秒杀对方",
|
36: "命中时{0}%的概率秒杀对方",
|
||||||
@@ -120,7 +120,7 @@ var effectInfoByID = map[int]string{
|
|||||||
164: "{0}回合内若受到攻击则有{1}%概率令对手{2}",
|
164: "{0}回合内若受到攻击则有{1}%概率令对手{2}",
|
||||||
165: "{0}回合内每回合防御和特防等级+{1}",
|
165: "{0}回合内每回合防御和特防等级+{1}",
|
||||||
166: "{0}回合内若对手使用属性攻击则{2}%对手{1}等级{3}",
|
166: "{0}回合内若对手使用属性攻击则{2}%对手{1}等级{3}",
|
||||||
169: "{0}回合内每回合额外附加{1}%概率令对手{2}",
|
169: "{0}回合内每回合额外附加{1}%概率令对方阵营全体{2}",
|
||||||
170: "若先出手,则免疫当回合伤害并回复1/{0}的最大体力值",
|
170: "若先出手,则免疫当回合伤害并回复1/{0}的最大体力值",
|
||||||
171: "{0}回合内自身使用属性技能时能较快出手",
|
171: "{0}回合内自身使用属性技能时能较快出手",
|
||||||
172: "若后出手,则给予对方损伤的1/{0}会回复自己的体力",
|
172: "若后出手,则给予对方损伤的1/{0}会回复自己的体力",
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ func (e *Effect3) Skill_Use() bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Effect 33: 消除对手能力提升状态
|
// Effect 33: 消除敌方阵营所有强化
|
||||||
type Effect33 struct {
|
type Effect33 struct {
|
||||||
node.EffectNode
|
node.EffectNode
|
||||||
Reverse bool
|
Reverse bool
|
||||||
@@ -38,13 +38,17 @@ type Effect33 struct {
|
|||||||
// 执行时逻辑
|
// 执行时逻辑
|
||||||
// ----------------------
|
// ----------------------
|
||||||
func (e *Effect33) Skill_Use() bool {
|
func (e *Effect33) Skill_Use() bool {
|
||||||
|
e.ForEachOpponentSlot(func(target *input.Input) bool {
|
||||||
for i, v := range e.Ctx().Opp.Prop[:] {
|
if target == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
for i, v := range target.Prop[:] {
|
||||||
if v > 0 {
|
if v > 0 {
|
||||||
e.Ctx().Opp.SetProp(e.Ctx().Our, int8(i), 0)
|
target.SetProp(e.Ctx().Our, int8(i), 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -54,8 +58,8 @@ func (e *Effect33) Skill_Use() bool {
|
|||||||
// ----------------------
|
// ----------------------
|
||||||
func init() {
|
func init() {
|
||||||
// {3, false, 0}, // 解除自身能力下降状态
|
// {3, false, 0}, // 解除自身能力下降状态
|
||||||
// {33, true, 0}, // 消除对手能力提升状态{3, false, 0}, // 解除自身能力下降状态
|
// {33, true, 0}, // 消除敌方阵营所有强化{3, false, 0}, // 解除自身能力下降状态
|
||||||
// {33, true, 0}, // 消除对手能力提升状态
|
// {33, true, 0}, // 消除敌方阵营所有强化
|
||||||
input.InitEffect(input.EffectType.Skill, 3, &Effect3{})
|
input.InitEffect(input.EffectType.Skill, 3, &Effect3{})
|
||||||
input.InitEffect(input.EffectType.Skill, 33, &Effect33{})
|
input.InitEffect(input.EffectType.Skill, 33, &Effect33{})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,43 +36,73 @@ func (e *StatusCannotAct) ActionStart(attacker, defender *action.SelectSkillActi
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 疲惫状态:仅限制攻击技能,本回合属性技能仍可正常使用。
|
||||||
|
type StatusTired struct {
|
||||||
|
BaseStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *StatusTired) ActionStart(attacker, defender *action.SelectSkillAction) bool {
|
||||||
|
if e.Ctx().SkillEntity == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return e.Ctx().SkillEntity.Category() == info.Category.STATUS
|
||||||
|
}
|
||||||
|
|
||||||
// 睡眠状态:受击后解除
|
// 睡眠状态:受击后解除
|
||||||
type StatusSleep struct {
|
type StatusSleep struct {
|
||||||
StatusCannotAct
|
StatusCannotAct
|
||||||
hasTriedAct bool // 标记是否尝试过行动
|
hasTriedAct bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// 睡眠在“被攻击且未 miss”后立即解除,而不是等到技能使用后节点。
|
||||||
|
func (e *StatusSleep) DamageSubEx(zone *info.DamageZone) bool {
|
||||||
|
if zone == nil || e.Ctx().SkillEntity == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if e.Ctx().SkillEntity.Category() != info.Category.STATUS {
|
||||||
|
e.Alive(false)
|
||||||
|
}
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// 尝试出手时标记状态
|
|
||||||
func (e *StatusSleep) ActionStart(attacker, defender *action.SelectSkillAction) bool {
|
func (e *StatusSleep) ActionStart(attacker, defender *action.SelectSkillAction) bool {
|
||||||
|
if e.Duration() <= 0 {
|
||||||
|
e.hasTriedAct = false
|
||||||
|
return true
|
||||||
|
}
|
||||||
e.hasTriedAct = true
|
e.hasTriedAct = true
|
||||||
return e.StatusCannotAct.ActionStart(attacker, defender)
|
return e.StatusCannotAct.ActionStart(attacker, defender)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 技能使用后处理:非状态类技能触发后解除睡眠
|
|
||||||
func (e *StatusSleep) Skill_Use_ex() bool {
|
func (e *StatusSleep) Skill_Use_ex() bool {
|
||||||
if !e.hasTriedAct {
|
if !e.hasTriedAct {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
// 技能实体存在且非状态类型技能,解除睡眠
|
if e.Duration() <= 0 && e.Ctx().SkillEntity != nil && e.Ctx().Category() != info.Category.STATUS {
|
||||||
if e.Ctx().SkillEntity != nil && e.Ctx().Category() != info.Category.STATUS {
|
|
||||||
e.Alive(false)
|
e.Alive(false)
|
||||||
}
|
}
|
||||||
|
e.hasTriedAct = false
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (e *StatusSleep) TurnEnd() {
|
||||||
|
e.hasTriedAct = false
|
||||||
|
e.StatusCannotAct.TurnEnd()
|
||||||
|
}
|
||||||
|
|
||||||
// 持续伤害状态基类(中毒、冻伤、烧伤等)
|
// 持续伤害状态基类(中毒、冻伤、烧伤等)
|
||||||
type ContinuousDamage struct {
|
type ContinuousDamage struct {
|
||||||
BaseStatus
|
BaseStatus
|
||||||
isheal bool //是否回血
|
isheal bool //是否回血
|
||||||
}
|
}
|
||||||
|
|
||||||
// 技能命中前触发伤害(1/8最大生命值真实伤害)
|
// 回合开始触发持续伤害,保证吃药/空过回合时也会正常结算。
|
||||||
func (e *ContinuousDamage) ActionStart(attacker, defender *action.SelectSkillAction) bool {
|
func (e *ContinuousDamage) TurnStart(attacker, defender *action.SelectSkillAction) {
|
||||||
carrier := e.CarrierInput()
|
carrier := e.CarrierInput()
|
||||||
source := e.SourceInput()
|
source := e.SourceInput()
|
||||||
opp := e.TargetInput()
|
opp := e.TargetInput()
|
||||||
if carrier == nil {
|
if carrier == nil {
|
||||||
return true
|
return
|
||||||
}
|
}
|
||||||
damage := e.calculateDamage()
|
damage := e.calculateDamage()
|
||||||
|
|
||||||
@@ -81,7 +111,7 @@ func (e *ContinuousDamage) ActionStart(attacker, defender *action.SelectSkillAct
|
|||||||
Damage: damage,
|
Damage: damage,
|
||||||
})
|
})
|
||||||
if len(e.SideEffectArgs) == 0 {
|
if len(e.SideEffectArgs) == 0 {
|
||||||
return true
|
return
|
||||||
}
|
}
|
||||||
// 额外效果
|
// 额外效果
|
||||||
carrier.Damage(source, &info.DamageZone{
|
carrier.Damage(source, &info.DamageZone{
|
||||||
@@ -89,12 +119,11 @@ func (e *ContinuousDamage) ActionStart(attacker, defender *action.SelectSkillAct
|
|||||||
Damage: damage,
|
Damage: damage,
|
||||||
})
|
})
|
||||||
if opp == nil || opp.CurPet[0].GetHP().IntPart() == 0 {
|
if opp == nil || opp.CurPet[0].GetHP().IntPart() == 0 {
|
||||||
return true
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 给对方回血(不受回血限制影响)
|
// 给对方回血(不受回血限制影响)
|
||||||
opp.Heal(carrier, nil, damage)
|
opp.Heal(carrier, nil, damage)
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 计算伤害:最大生命值的1/8
|
// 计算伤害:最大生命值的1/8
|
||||||
@@ -131,15 +160,13 @@ func (e *ParasiticSeed) SwitchOut(in *input.Input) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// 技能命中前触发寄生效果
|
// 回合开始触发寄生效果。寄生属于完整回合流程的一部分,不依赖本回合是否成功出手。
|
||||||
func (e *ParasiticSeed) ActionStartEx(attacker, defender *action.SelectSkillAction) bool {
|
func (e *ParasiticSeed) TurnStart(attacker, defender *action.SelectSkillAction) {
|
||||||
carrier := e.CarrierInput()
|
carrier := e.CarrierInput()
|
||||||
source := e.SourceInput()
|
source := e.SourceInput()
|
||||||
opp := e.TargetInput()
|
|
||||||
if carrier == nil {
|
if carrier == nil {
|
||||||
return true
|
return
|
||||||
}
|
}
|
||||||
// 过滤特定类型单位(假设1是植物类型,使用枚举替代魔法数字)
|
|
||||||
|
|
||||||
damage := alpacadecimal.NewFromInt(int64(carrier.CurPet[0].Info.MaxHp)).
|
damage := alpacadecimal.NewFromInt(int64(carrier.CurPet[0].Info.MaxHp)).
|
||||||
Div(alpacadecimal.NewFromInt(8))
|
Div(alpacadecimal.NewFromInt(8))
|
||||||
@@ -149,13 +176,12 @@ func (e *ParasiticSeed) ActionStartEx(attacker, defender *action.SelectSkillActi
|
|||||||
Type: info.DamageType.True,
|
Type: info.DamageType.True,
|
||||||
Damage: damage,
|
Damage: damage,
|
||||||
})
|
})
|
||||||
if opp == nil || opp.CurPet[0].GetHP().IntPart() == 0 {
|
if source == nil || source.CurPet[0] == nil || source.CurPet[0].GetHP().IntPart() == 0 {
|
||||||
return true
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 给对方回血(不受回血限制影响)
|
// 给寄生种子的施放者回血(不受回血限制影响)
|
||||||
opp.Heal(carrier, nil, damage)
|
source.Heal(carrier, nil, damage)
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Flammable struct {
|
type Flammable struct {
|
||||||
@@ -271,7 +297,6 @@ func init() {
|
|||||||
// 批量注册不能行动的状态
|
// 批量注册不能行动的状态
|
||||||
nonActingStatuses := []info.EnumPetStatus{
|
nonActingStatuses := []info.EnumPetStatus{
|
||||||
info.PetStatus.Paralysis, // 麻痹
|
info.PetStatus.Paralysis, // 麻痹
|
||||||
info.PetStatus.Tired, // 疲惫
|
|
||||||
info.PetStatus.Fear, // 害怕
|
info.PetStatus.Fear, // 害怕
|
||||||
info.PetStatus.Petrified, // 石化
|
info.PetStatus.Petrified, // 石化
|
||||||
}
|
}
|
||||||
@@ -281,6 +306,10 @@ func init() {
|
|||||||
input.InitEffect(input.EffectType.Status, int(status), effect)
|
input.InitEffect(input.EffectType.Status, int(status), effect)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tired := &StatusTired{}
|
||||||
|
tired.Status = info.PetStatus.Tired
|
||||||
|
input.InitEffect(input.EffectType.Status, int(info.PetStatus.Tired), tired)
|
||||||
|
|
||||||
// 注册睡眠状态(使用枚举常量替代硬编码8)
|
// 注册睡眠状态(使用枚举常量替代硬编码8)
|
||||||
input.InitEffect(input.EffectType.Status, int(info.PetStatus.Sleep), &StatusSleep{})
|
input.InitEffect(input.EffectType.Status, int(info.PetStatus.Sleep), &StatusSleep{})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,6 +83,10 @@ func (e *Effect201) OnSkill() bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !carrier.IsMultiInputBattle() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
divisorIndex := len(args) - 1
|
divisorIndex := len(args) - 1
|
||||||
if len(args) > 1 {
|
if len(args) > 1 {
|
||||||
divisorIndex = 1
|
divisorIndex = 1
|
||||||
|
|||||||
91
logic/service/fight/effect/none_test.go
Normal file
91
logic/service/fight/effect/none_test.go
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
package effect
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
fightinfo "blazing/logic/service/fight/info"
|
||||||
|
"blazing/logic/service/fight/input"
|
||||||
|
"blazing/modules/player/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newEffect201TestInput(hp, maxHP uint32) *input.Input {
|
||||||
|
in := &input.Input{
|
||||||
|
CurPet: []*fightinfo.BattlePetEntity{{
|
||||||
|
Info: model.PetInfo{
|
||||||
|
Hp: hp,
|
||||||
|
MaxHp: maxHP,
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
in.AttackValue = fightinfo.NewAttackValue(0)
|
||||||
|
return in
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEffect201HealAllIgnoredInSingleInputBattle(t *testing.T) {
|
||||||
|
carrier := newEffect201TestInput(40, 100)
|
||||||
|
opponent := newEffect201TestInput(60, 100)
|
||||||
|
carrier.Team = []*input.Input{carrier}
|
||||||
|
carrier.OppTeam = []*input.Input{opponent}
|
||||||
|
|
||||||
|
eff := &Effect201{}
|
||||||
|
eff.SetArgs(carrier, 1, 2)
|
||||||
|
eff.EffectNode.EffectContextHolder.Ctx = input.Ctx{
|
||||||
|
LegacySides: input.LegacySides{Our: carrier, Opp: opponent},
|
||||||
|
EffectBinding: input.EffectBinding{Carrier: carrier, Source: carrier},
|
||||||
|
}
|
||||||
|
|
||||||
|
if !eff.OnSkill() {
|
||||||
|
t.Fatalf("expected effect to finish successfully")
|
||||||
|
}
|
||||||
|
if got := carrier.CurrentPet().Info.Hp; got != 40 {
|
||||||
|
t.Fatalf("expected single-input full-team heal to be ignored, got hp %d", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEffect201SingleTargetIgnoredInSingleInputBattle(t *testing.T) {
|
||||||
|
carrier := newEffect201TestInput(40, 100)
|
||||||
|
opponent := newEffect201TestInput(60, 100)
|
||||||
|
carrier.Team = []*input.Input{carrier}
|
||||||
|
carrier.OppTeam = []*input.Input{opponent}
|
||||||
|
|
||||||
|
eff := &Effect201{}
|
||||||
|
eff.SetArgs(carrier, 2)
|
||||||
|
eff.EffectNode.EffectContextHolder.Ctx = input.Ctx{
|
||||||
|
LegacySides: input.LegacySides{Our: carrier, Opp: opponent},
|
||||||
|
EffectBinding: input.EffectBinding{Carrier: carrier, Source: carrier},
|
||||||
|
}
|
||||||
|
|
||||||
|
if !eff.OnSkill() {
|
||||||
|
t.Fatalf("expected effect to finish successfully")
|
||||||
|
}
|
||||||
|
if got := carrier.CurrentPet().Info.Hp; got != 40 {
|
||||||
|
t.Fatalf("expected single-input single-target heal to be ignored, got hp %d", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEffect201HealAllWorksInMultiInputBattle(t *testing.T) {
|
||||||
|
carrier := newEffect201TestInput(40, 100)
|
||||||
|
ally := newEffect201TestInput(10, 80)
|
||||||
|
opponent := newEffect201TestInput(60, 100)
|
||||||
|
carrier.Team = []*input.Input{carrier, ally}
|
||||||
|
carrier.OppTeam = []*input.Input{opponent}
|
||||||
|
ally.Team = carrier.Team
|
||||||
|
ally.OppTeam = carrier.OppTeam
|
||||||
|
|
||||||
|
eff := &Effect201{}
|
||||||
|
eff.SetArgs(carrier, 1, 2)
|
||||||
|
eff.EffectNode.EffectContextHolder.Ctx = input.Ctx{
|
||||||
|
LegacySides: input.LegacySides{Our: carrier, Opp: opponent},
|
||||||
|
EffectBinding: input.EffectBinding{Carrier: carrier, Source: carrier},
|
||||||
|
}
|
||||||
|
|
||||||
|
if !eff.OnSkill() {
|
||||||
|
t.Fatalf("expected effect to finish successfully")
|
||||||
|
}
|
||||||
|
if got := carrier.CurrentPet().Info.Hp; got != 90 {
|
||||||
|
t.Fatalf("expected carrier hp 90 after full-team heal, got %d", got)
|
||||||
|
}
|
||||||
|
if got := ally.CurrentPet().Info.Hp; got != 50 {
|
||||||
|
t.Fatalf("expected ally hp 50 after full-team heal, got %d", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,3 +5,7 @@ import "blazing/logic/service/fight/input"
|
|||||||
func initskill(id int, e input.Effect) {
|
func initskill(id int, e input.Effect) {
|
||||||
input.InitEffect(input.EffectType.Skill, id, e)
|
input.InitEffect(input.EffectType.Skill, id, e)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func initskillFactory(id int, factory func() input.Effect) {
|
||||||
|
input.InitEffectFactory(input.EffectType.Skill, id, factory)
|
||||||
|
}
|
||||||
|
|||||||
@@ -158,7 +158,10 @@ func registerSelfDamageSkillHitEffects() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for effectID, handler := range handlers {
|
for effectID, handler := range handlers {
|
||||||
initskill(effectID, newSkillHitRegistrarEffect(handler))
|
currentHandler := handler
|
||||||
|
initskillFactory(effectID, func() input.Effect {
|
||||||
|
return newSkillHitRegistrarEffect(currentHandler)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -204,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
|
||||||
@@ -223,7 +232,10 @@ func registerSelfDamageOnSkillEffects() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for effectID, handler := range handlers {
|
for effectID, handler := range handlers {
|
||||||
initskill(effectID, newOnSkillRegistrarEffect(handler))
|
currentHandler := handler
|
||||||
|
initskillFactory(effectID, func() input.Effect {
|
||||||
|
return newOnSkillRegistrarEffect(currentHandler)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -235,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{
|
||||||
@@ -247,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
|
||||||
},
|
},
|
||||||
@@ -274,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(),
|
||||||
@@ -305,7 +353,10 @@ func registerSelfDamageSkillUseEffects() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for effectID, handler := range handlers {
|
for effectID, handler := range handlers {
|
||||||
initskill(effectID, newSkillUseRegistrarEffect(handler))
|
currentHandler := handler
|
||||||
|
initskillFactory(effectID, func() input.Effect {
|
||||||
|
return newSkillUseRegistrarEffect(currentHandler)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -339,7 +390,10 @@ func registerSelfDamageComparePreOnSkillEffects() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for effectID, effect := range effects {
|
for effectID, effect := range effects {
|
||||||
initskill(effectID, effect)
|
currentEffect := effect
|
||||||
|
initskillFactory(effectID, func() input.Effect {
|
||||||
|
return newComparePreOnSkillRegistrarEffect(currentEffect.comparePreHandler, currentEffect.onSkillHandler)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
34
logic/service/fight/effect/skill_target_helper.go
Normal file
34
logic/service/fight/effect/skill_target_helper.go
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
40
logic/service/fight/fight_over_payload.go
Normal file
40
logic/service/fight/fight_over_payload.go
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
package fight
|
||||||
|
|
||||||
|
import "blazing/modules/player/model"
|
||||||
|
|
||||||
|
// buildFightOverPayload builds the legacy 2506 payload expected by the flash client.
|
||||||
|
// Regular fight-over packets use a different reason mapping than group fight 7560:
|
||||||
|
// 0=normal end 1=player lost/offline 2=overtime 3=draw 4=system error 5=npc escape.
|
||||||
|
func buildFightOverPayload(over model.FightOverInfo) *model.FightOverInfo {
|
||||||
|
payload := over
|
||||||
|
payload.Reason = model.EnumBattleOverReason(mapUnifiedFightOverReason(over.Reason))
|
||||||
|
return &payload
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeFightOverReason(reason model.EnumBattleOverReason) model.EnumBattleOverReason {
|
||||||
|
if reason == model.BattleOverReason.DefaultEnd {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return reason
|
||||||
|
}
|
||||||
|
|
||||||
|
func mapUnifiedFightOverReason(reason model.EnumBattleOverReason) uint32 {
|
||||||
|
switch normalizeFightOverReason(reason) {
|
||||||
|
case 0, model.BattleOverReason.Cacthok:
|
||||||
|
return 0
|
||||||
|
case model.BattleOverReason.PlayerOffline:
|
||||||
|
return 1
|
||||||
|
case model.BattleOverReason.PlayerOVerTime:
|
||||||
|
return 2
|
||||||
|
case model.BattleOverReason.NOTwind:
|
||||||
|
return 3
|
||||||
|
case model.BattleOverReason.PlayerEscape:
|
||||||
|
return 5
|
||||||
|
default:
|
||||||
|
return 4
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func mapFightOverReasonFor2506(reason model.EnumBattleOverReason) model.EnumBattleOverReason {
|
||||||
|
return model.EnumBattleOverReason(mapUnifiedFightOverReason(reason))
|
||||||
|
}
|
||||||
@@ -3,9 +3,12 @@ package fight
|
|||||||
import (
|
import (
|
||||||
"blazing/common/utils"
|
"blazing/common/utils"
|
||||||
|
|
||||||
|
"blazing/logic/service/common"
|
||||||
"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"
|
||||||
|
|
||||||
@@ -132,7 +135,20 @@ func (f *FightC) getSkillParticipants(skillAction *action.SelectSkillAction) (*i
|
|||||||
if skillAction == nil {
|
if skillAction == nil {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
return f.GetInputByAction(skillAction, false), f.GetInputByAction(skillAction, true)
|
attacker := f.GetInputByAction(skillAction, false)
|
||||||
|
defender := f.GetInputByAction(skillAction, true)
|
||||||
|
if attacker != nil && defender == attacker && shouldResolveOpponentAsTarget(skillAction.SkillEntity) {
|
||||||
|
if opponent, _ := attacker.OpponentSlotAtOrNextLiving(0); opponent != nil {
|
||||||
|
defender = opponent
|
||||||
|
} else if opponent := f.roundOpponentInput(attacker); opponent != nil {
|
||||||
|
defender = opponent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return attacker, defender
|
||||||
|
}
|
||||||
|
|
||||||
|
func shouldResolveOpponentAsTarget(skill *info.SkillEntity) bool {
|
||||||
|
return skill != nil && skill.XML.AtkType == 3
|
||||||
}
|
}
|
||||||
|
|
||||||
// setEffectSkillContext 统一设置技能阶段 effect 上下文。
|
// setEffectSkillContext 统一设置技能阶段 effect 上下文。
|
||||||
@@ -183,20 +199,63 @@ func (f *FightC) collectAttackValues(inputs []*input.Input) []model.AttackValue
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
attackValue := *fighter.AttackValue
|
attackValue := *fighter.AttackValue
|
||||||
|
if attackValue.SkillID == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
attackValue.ActorIndex = uint32(actorIndex)
|
attackValue.ActorIndex = uint32(actorIndex)
|
||||||
values = append(values, attackValue)
|
values = append(values, attackValue)
|
||||||
}
|
}
|
||||||
return values
|
return values
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *FightC) buildAttackValueForBroadcast(fighter *input.Input, fallbackActorIndex int) model.AttackValue {
|
||||||
|
if fighter == nil {
|
||||||
|
return model.AttackValue{}
|
||||||
|
}
|
||||||
|
if fighter.AttackValue == nil {
|
||||||
|
empty := info.NewAttackValue(fighter.UserID)
|
||||||
|
fighter.AttackValue = empty
|
||||||
|
}
|
||||||
|
attackValue := *fighter.AttackValue
|
||||||
|
attackValue.ActorIndex = uint32(fallbackActorIndex)
|
||||||
|
if attackValue.UserID == 0 && fighter.Player != nil && fighter.Player.GetInfo() != nil {
|
||||||
|
attackValue.UserID = fighter.Player.GetInfo().UserID
|
||||||
|
}
|
||||||
|
return attackValue
|
||||||
|
}
|
||||||
|
|
||||||
func (f *FightC) buildNoteUseSkillOutboundInfo() info.NoteUseSkillOutboundInfo {
|
func (f *FightC) buildNoteUseSkillOutboundInfo() info.NoteUseSkillOutboundInfo {
|
||||||
result := info.NoteUseSkillOutboundInfo{}
|
result := info.NoteUseSkillOutboundInfo{}
|
||||||
result.FirstAttackInfo = append(result.FirstAttackInfo, f.collectAttackValues(f.Our)...)
|
if f.First != nil {
|
||||||
result.SecondAttackInfo = append(result.SecondAttackInfo, f.collectAttackValues(f.Opp)...)
|
result.FirstAttackInfo = f.buildAttackValueForBroadcast(f.First, f.First.TeamSlotIndex())
|
||||||
|
}
|
||||||
|
if f.Second != nil {
|
||||||
|
result.SecondAttackInfo = f.buildAttackValueForBroadcast(f.Second, f.Second.TeamSlotIndex())
|
||||||
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *FightC) roundOpponentInput(attacker *input.Input) *input.Input {
|
||||||
|
if attacker == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
for _, opponent := range attacker.OpponentSlots() {
|
||||||
|
if opponent != nil {
|
||||||
|
return opponent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func shouldSkipSecondAction(first, second *input.Input) bool {
|
||||||
|
if first == nil || second == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
firstPet := first.CurrentPet()
|
||||||
|
secondPet := second.CurrentPet()
|
||||||
|
return firstPet == nil || firstPet.Info.Hp <= 0 || secondPet == nil || secondPet.Info.Hp <= 0
|
||||||
|
}
|
||||||
|
|
||||||
// enterturn 处理战斗回合逻辑
|
// enterturn 处理战斗回合逻辑
|
||||||
// 回合有先手方和后手方,同时有攻击方和被攻击方
|
// 回合有先手方和后手方,同时有攻击方和被攻击方
|
||||||
func (f *FightC) enterturn(firstAttack, secondAttack *action.SelectSkillAction) {
|
func (f *FightC) enterturn(firstAttack, secondAttack *action.SelectSkillAction) {
|
||||||
@@ -244,9 +303,11 @@ func (f *FightC) enterturn(firstAttack, secondAttack *action.SelectSkillAction)
|
|||||||
f.First, _ = f.getSkillParticipants(firstAttack)
|
f.First, _ = f.getSkillParticipants(firstAttack)
|
||||||
f.Second, _ = f.getSkillParticipants(secondAttack)
|
f.Second, _ = f.getSkillParticipants(secondAttack)
|
||||||
case firstAttack != nil:
|
case firstAttack != nil:
|
||||||
f.First, f.Second = f.getSkillParticipants(firstAttack)
|
f.First, _ = f.getSkillParticipants(firstAttack)
|
||||||
|
f.Second = f.roundOpponentInput(f.First)
|
||||||
case secondAttack != nil:
|
case secondAttack != nil:
|
||||||
f.First, f.Second = f.getSkillParticipants(secondAttack)
|
f.First, _ = f.getSkillParticipants(secondAttack)
|
||||||
|
f.Second = f.roundOpponentInput(f.First)
|
||||||
}
|
}
|
||||||
if f.First == nil {
|
if f.First == nil {
|
||||||
f.First = f.primaryOur()
|
f.First = f.primaryOur()
|
||||||
@@ -274,25 +335,32 @@ func (f *FightC) enterturn(firstAttack, secondAttack *action.SelectSkillAction)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if firstAttack == nil && secondAttack == nil {
|
skipActionStage := firstAttack == nil && secondAttack == nil
|
||||||
firstAttack, secondAttack = secondAttack, firstAttack //互换先手权
|
|
||||||
f.First, f.Second = f.Second, f.First
|
|
||||||
}
|
|
||||||
var attacker, defender *input.Input
|
var attacker, defender *input.Input
|
||||||
f.TrueFirst = f.First
|
f.TrueFirst = f.First
|
||||||
//开始回合操作
|
//开始回合操作。若双方本回合都未出手,则只走完整回合流程,不进入动作阶段。
|
||||||
for i := 0; i < 2; i++ {
|
for i := 0; !skipActionStage && i < 2; i++ {
|
||||||
var originalSkill *info.SkillEntity //原始技能
|
var originalSkill *info.SkillEntity //原始技能
|
||||||
var currentSkill *info.SkillEntity //当前技能
|
var currentSkill *info.SkillEntity //当前技能
|
||||||
var currentAction *action.SelectSkillAction
|
var currentAction *action.SelectSkillAction
|
||||||
if i == 0 {
|
if i == 0 {
|
||||||
currentAction = firstAttack
|
currentAction = firstAttack
|
||||||
|
if currentAction == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
attacker, defender = f.getSkillParticipants(firstAttack)
|
attacker, defender = f.getSkillParticipants(firstAttack)
|
||||||
originalSkill = f.copySkill(firstAttack)
|
originalSkill = f.copySkill(firstAttack)
|
||||||
//先手阶段,先修复后手效果
|
//先手阶段,先修复后手效果
|
||||||
f.Second.RecoverEffect()
|
f.Second.RecoverEffect()
|
||||||
} else {
|
} else {
|
||||||
currentAction = secondAttack
|
currentAction = secondAttack
|
||||||
|
if currentAction == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if shouldSkipSecondAction(f.First, f.Second) {
|
||||||
|
secondAttack = nil
|
||||||
|
continue
|
||||||
|
}
|
||||||
attacker, defender = f.getSkillParticipants(secondAttack)
|
attacker, defender = f.getSkillParticipants(secondAttack)
|
||||||
originalSkill = f.copySkill(secondAttack)
|
originalSkill = f.copySkill(secondAttack)
|
||||||
//取消后手历史效果
|
//取消后手历史效果
|
||||||
@@ -331,7 +399,6 @@ func (f *FightC) enterturn(firstAttack, secondAttack *action.SelectSkillAction)
|
|||||||
|
|
||||||
}
|
}
|
||||||
//先手权不一定出手
|
//先手权不一定出手
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
f.setActionAttackValue(currentAction)
|
f.setActionAttackValue(currentAction)
|
||||||
|
|
||||||
@@ -416,13 +483,25 @@ func (f *FightC) enterturn(firstAttack, secondAttack *action.SelectSkillAction)
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if f.LegacyGroupProtocol {
|
||||||
|
f.sendLegacyRoundBroadcast(firstAttack, secondAttack)
|
||||||
|
}
|
||||||
|
|
||||||
attackValueResult := f.buildNoteUseSkillOutboundInfo()
|
attackValueResult := f.buildNoteUseSkillOutboundInfo()
|
||||||
//因为切完才能广播,所以必须和回合结束分开结算
|
//因为切完才能广播,所以必须和回合结束分开结算
|
||||||
f.Broadcast(func(fighter *input.Input) {
|
f.BroadcastPlayers(func(p common.PlayerI) {
|
||||||
for _, switchAction := range f.Switch {
|
for _, switchAction := range f.Switch {
|
||||||
if fighter.Player.GetInfo().UserID != switchAction.Reason.UserId {
|
if p.GetInfo().UserID != switchAction.Reason.UserId {
|
||||||
// println("切精灵", switchAction.Reason.UserId, switchAction.Reason.ID)
|
// println("切精灵", switchAction.Reason.UserId, switchAction.Reason.ID)
|
||||||
fighter.Player.SendPackCmd(2407, &switchAction.Reason)
|
if f.LegacyGroupProtocol {
|
||||||
|
switchedInput := f.getInputByUserID(switchAction.Reason.UserId, int(switchAction.Reason.ActorIndex), false)
|
||||||
|
if switchedInput == nil {
|
||||||
|
switchedInput = f.getInputByUserID(switchAction.Reason.UserId, int(switchAction.ActorIndex), false)
|
||||||
|
}
|
||||||
|
f.sendLegacyGroupChangePetSuccess(p, switchedInput, &switchAction.Reason)
|
||||||
|
} else {
|
||||||
|
f.sendFightPacket(p, fightPacketChangePetSuccess, &switchAction.Reason)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -430,14 +509,20 @@ func (f *FightC) enterturn(firstAttack, secondAttack *action.SelectSkillAction)
|
|||||||
if f.closefight && f.Info.Mode == info.BattleMode.PET_MELEE {
|
if f.closefight && f.Info.Mode == info.BattleMode.PET_MELEE {
|
||||||
// f.Broadcast(func(fighter *input.Input) {
|
// f.Broadcast(func(fighter *input.Input) {
|
||||||
// if fighter.UserID != f.WinnerId {
|
// if fighter.UserID != f.WinnerId {
|
||||||
// fighter.Player.SendPackCmd(2505, &attackValueResult)
|
// f.sendFightPacket(fighter.Player, 2505, groupCmdSkillHurt, /&attackValueResult)
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// })
|
// })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if attackValueResult.FirstAttackInfo.UserID != 0 || attackValueResult.SecondAttackInfo.UserID != 0 {
|
||||||
|
f.BroadcastPlayers(func(p common.PlayerI) {
|
||||||
|
if !f.LegacyGroupProtocol {
|
||||||
|
f.sendFightPacket(p, fightPacketSkillResult, &attackValueResult)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
f.Broadcast(func(fighter *input.Input) {
|
f.Broadcast(func(fighter *input.Input) {
|
||||||
fighter.Player.SendPackCmd(2505, &attackValueResult)
|
|
||||||
fighter.CanChange = 0
|
fighter.CanChange = 0
|
||||||
})
|
})
|
||||||
if f.closefight {
|
if f.closefight {
|
||||||
@@ -446,6 +531,12 @@ func (f *FightC) enterturn(firstAttack, secondAttack *action.SelectSkillAction)
|
|||||||
|
|
||||||
}
|
}
|
||||||
func (f *FightC) TURNOVER(cur *input.Input) {
|
func (f *FightC) TURNOVER(cur *input.Input) {
|
||||||
|
var _hasBackup bool
|
||||||
|
if cur == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_hasBackup = cur.HasLivingBench()
|
||||||
|
f.sendLegacySpriteDie(cur, _hasBackup)
|
||||||
|
|
||||||
f.Broadcast(func(ff *input.Input) {
|
f.Broadcast(func(ff *input.Input) {
|
||||||
|
|
||||||
@@ -459,6 +550,9 @@ func (f *FightC) TURNOVER(cur *input.Input) {
|
|||||||
if f.IsWin(f.GetInputByPlayer(cur.Player, true)) { //然后检查是否战斗结束
|
if f.IsWin(f.GetInputByPlayer(cur.Player, true)) { //然后检查是否战斗结束
|
||||||
|
|
||||||
f.FightOverInfo.WinnerId = f.GetInputByPlayer(cur.Player, true).UserID
|
f.FightOverInfo.WinnerId = f.GetInputByPlayer(cur.Player, true).UserID
|
||||||
|
f.FightOverInfo.Reason = normalizeFightOverReason(model.BattleOverReason.DefaultEnd)
|
||||||
|
f.WinnerId = f.FightOverInfo.WinnerId
|
||||||
|
f.Reason = f.FightOverInfo.Reason
|
||||||
|
|
||||||
f.closefight = true
|
f.closefight = true
|
||||||
// break
|
// break
|
||||||
|
|||||||
674
logic/service/fight/group_legacy.go
Normal file
674
logic/service/fight/group_legacy.go
Normal file
@@ -0,0 +1,674 @@
|
|||||||
|
package fight
|
||||||
|
|
||||||
|
import (
|
||||||
|
"blazing/logic/service/common"
|
||||||
|
"blazing/logic/service/fight/action"
|
||||||
|
"blazing/logic/service/fight/info"
|
||||||
|
"blazing/logic/service/fight/input"
|
||||||
|
"blazing/modules/player/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
// <!--
|
||||||
|
// GBTL:
|
||||||
|
// 1. AtkNum:本技能同时攻击数量, 默认:1(不能为0)
|
||||||
|
// 2. AtkType:攻击类型: 0:所有人, 1:仅己方, 2:仅对方, 3:仅自己, 默认:2
|
||||||
|
// -->
|
||||||
|
const (
|
||||||
|
groupCmdReadyToFight uint32 = 7555
|
||||||
|
groupCmdReadyFightFinish uint32 = 7556
|
||||||
|
groupCmdStartFight uint32 = 7557
|
||||||
|
groupCmdUseSkill uint32 = 7558
|
||||||
|
groupCmdSkillHurt uint32 = 7559
|
||||||
|
groupCmdFightOver uint32 = 7560
|
||||||
|
groupCmdSpriteDie uint32 = 7561
|
||||||
|
groupCmdUseItem uint32 = 7562
|
||||||
|
groupCmdChangePet uint32 = 7563
|
||||||
|
groupCmdEscape uint32 = 7565
|
||||||
|
groupCmdBoutDone uint32 = 7566
|
||||||
|
groupCmdChangePetSuc uint32 = 7567
|
||||||
|
groupCmdEscapeSuc uint32 = 7568
|
||||||
|
groupCmdChat uint32 = 7569
|
||||||
|
groupCmdLoadPercent uint32 = 7571
|
||||||
|
groupCmdLoadPercentNotice uint32 = 7572
|
||||||
|
groupCmdSpriteNotice uint32 = 7573
|
||||||
|
groupCmdFightWinClose uint32 = 7574
|
||||||
|
groupCmdFightOvertime uint32 = 7585
|
||||||
|
groupCmdSkillPlayOver uint32 = 7586
|
||||||
|
groupCmdFightTimeoutExit uint32 = 7587
|
||||||
|
groupCmdFightRelation uint32 = 7588
|
||||||
|
groupModelNPC uint32 = 3
|
||||||
|
groupModelBoss uint32 = 4
|
||||||
|
groupModelPlayerSingle uint32 = 5
|
||||||
|
groupModelPlayerMulti uint32 = 6
|
||||||
|
)
|
||||||
|
|
||||||
|
type fightPacketKind uint8
|
||||||
|
|
||||||
|
const (
|
||||||
|
fightPacketReady fightPacketKind = iota
|
||||||
|
fightPacketStart
|
||||||
|
fightPacketSkillResult
|
||||||
|
fightPacketOver
|
||||||
|
fightPacketChangePetSuccess
|
||||||
|
fightPacketUseItem
|
||||||
|
fightPacketChat
|
||||||
|
fightPacketLoadPercentNotice
|
||||||
|
)
|
||||||
|
|
||||||
|
type legacyEscapeSuccessInfo struct {
|
||||||
|
UserID uint32 `struc:"uint32"`
|
||||||
|
Nick string `struc:"[16]byte"`
|
||||||
|
Side uint8 `struc:"uint8"`
|
||||||
|
ActorIndex uint8 `struc:"uint8"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type legacyBoutDoneInfo struct {
|
||||||
|
Round uint32 `struc:"uint32"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type legacySpriteDieInfo struct {
|
||||||
|
Count uint8 `struc:"uint8"`
|
||||||
|
Side uint8 `struc:"uint8"`
|
||||||
|
ActorIndex uint8 `struc:"uint8"`
|
||||||
|
Flag uint8 `struc:"uint8"`
|
||||||
|
HasBackup uint32 `struc:"uint32"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type legacyLegacySpriteDieItem struct {
|
||||||
|
Flag uint8 `struc:"uint8"`
|
||||||
|
Side uint8 `struc:"uint8"`
|
||||||
|
ActorIndex uint8 `struc:"uint8"`
|
||||||
|
Reserve uint8 `struc:"uint8"`
|
||||||
|
HasBackup uint32 `struc:"uint32"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type legacyGroupReadyToFightInfo struct {
|
||||||
|
Model uint32 `struc:"uint32"`
|
||||||
|
GroupOneInfo legacyReadyToFightTeam `struc:""`
|
||||||
|
GroupTwoInfo legacyReadyToFightTeam `struc:""`
|
||||||
|
}
|
||||||
|
|
||||||
|
type legacyReadyToFightTeam struct {
|
||||||
|
InvitorID uint8 `struc:"uint8"`
|
||||||
|
LeaderID uint32 `struc:"uint32"`
|
||||||
|
GroupMembCnt uint8 `struc:"sizeof=GroupList"`
|
||||||
|
GroupList []legacyReadyFightUser `struc:""`
|
||||||
|
}
|
||||||
|
|
||||||
|
type legacyReadyFightUser struct {
|
||||||
|
UserID uint32 `struc:"uint32"`
|
||||||
|
Nick string `struc:"[16]byte"`
|
||||||
|
MonCnt uint32 `struc:"sizeof=MonList"`
|
||||||
|
MonList []legacyReadyFightPet `struc:""`
|
||||||
|
}
|
||||||
|
|
||||||
|
type legacyReadyFightPet struct {
|
||||||
|
ID uint32 `struc:"uint32"`
|
||||||
|
MoveCnt uint32 `struc:"sizeof=MoveList"`
|
||||||
|
MoveList []uint32 `struc:"[]uint32"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type legacyGroupStartInfo struct {
|
||||||
|
IsGank uint8 `struc:"uint8"`
|
||||||
|
GroupOneN uint8 `struc:"sizeof=GroupOne"`
|
||||||
|
GroupOne []legacyGroupStartPet `struc:""`
|
||||||
|
GroupTwoN uint8 `struc:"sizeof=GroupTwo"`
|
||||||
|
GroupTwo []legacyGroupStartPet `struc:""`
|
||||||
|
}
|
||||||
|
|
||||||
|
type legacyGroupStartPet struct {
|
||||||
|
Side uint8 `struc:"uint8"`
|
||||||
|
Pos uint8 `struc:"uint8"`
|
||||||
|
UserID uint32 `struc:"uint32"`
|
||||||
|
IsChange uint8 `struc:"uint8"`
|
||||||
|
PetID uint32 `struc:"uint32"`
|
||||||
|
CatchTime uint32 `struc:"uint32"`
|
||||||
|
Hp uint32 `struc:"uint32"`
|
||||||
|
MaxHp uint32 `struc:"uint32"`
|
||||||
|
Level uint32 `struc:"uint32"`
|
||||||
|
Reserve uint32 `struc:"uint32"`
|
||||||
|
Flag uint32 `struc:"uint32"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type legacyGroupSkillHurtPacket struct {
|
||||||
|
IsGank uint8 `struc:"uint8"`
|
||||||
|
Attack legacyGroupSkillAttackInfo `struc:""`
|
||||||
|
Attacked legacyGroupSkillDefendInfo `struc:""`
|
||||||
|
}
|
||||||
|
|
||||||
|
type legacyGroupSkillAttackInfo struct {
|
||||||
|
IsAttackor uint8 `struc:"uint8"`
|
||||||
|
Side uint8 `struc:"uint8"`
|
||||||
|
Pos uint8 `struc:"uint8"`
|
||||||
|
UserID uint32 `struc:"uint32"`
|
||||||
|
StatusList [20]uint8 `struc:"[20]byte"`
|
||||||
|
Reserve1 uint8 `struc:"uint8"`
|
||||||
|
Reserve2 uint8 `struc:"uint8"`
|
||||||
|
BatLvList [6]uint8 `struc:"[6]byte"`
|
||||||
|
PetID uint32 `struc:"uint32"`
|
||||||
|
MoveID uint32 `struc:"uint32"`
|
||||||
|
Hp uint32 `struc:"uint32"`
|
||||||
|
MaxHp uint32 `struc:"uint32"`
|
||||||
|
MoveCnt uint32 `struc:"sizeof=MoveMap"`
|
||||||
|
MoveMap []legacyGroupSkillMoveInfo `struc:""`
|
||||||
|
Flag uint32 `struc:"uint32"`
|
||||||
|
IsCrit uint32 `struc:"uint32"`
|
||||||
|
EffectName uint32 `struc:"uint32"`
|
||||||
|
AtkTimes uint32 `struc:"uint32"`
|
||||||
|
Dmg int32 `struc:"int32"`
|
||||||
|
ChgHp int32 `struc:"int32"`
|
||||||
|
SideEffectLen uint32 `struc:"uint32"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type legacyGroupSkillDefendInfo struct {
|
||||||
|
IsAttackor uint8 `struc:"uint8"`
|
||||||
|
Side uint8 `struc:"uint8"`
|
||||||
|
Pos uint8 `struc:"uint8"`
|
||||||
|
UserID uint32 `struc:"uint32"`
|
||||||
|
StatusList [20]uint8 `struc:"[20]byte"`
|
||||||
|
Reserve1 uint8 `struc:"uint8"`
|
||||||
|
Reserve2 uint8 `struc:"uint8"`
|
||||||
|
BatLvList [6]uint8 `struc:"[6]byte"`
|
||||||
|
PetID uint32 `struc:"uint32"`
|
||||||
|
MoveID uint32 `struc:"uint32"`
|
||||||
|
Hp uint32 `struc:"uint32"`
|
||||||
|
MaxHp uint32 `struc:"uint32"`
|
||||||
|
MoveCnt uint32 `struc:"sizeof=MoveMap"`
|
||||||
|
MoveMap []legacyGroupSkillMoveInfo `struc:""`
|
||||||
|
Flag uint32 `struc:"uint32"`
|
||||||
|
SideEffectLen uint32 `struc:"uint32"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type legacyGroupSkillMoveInfo struct {
|
||||||
|
MoveID uint32 `struc:"uint32"`
|
||||||
|
PP uint32 `struc:"uint32"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type legacyGroupFightOverInfo struct {
|
||||||
|
IsGank uint8 `struc:"uint8"`
|
||||||
|
Reason uint32 `struc:"uint32"`
|
||||||
|
WinnerID uint32 `struc:"uint32"`
|
||||||
|
Reserve uint32 `struc:"uint32"`
|
||||||
|
TwoTimes uint32 `struc:"uint32"`
|
||||||
|
ThreeTimes uint32 `struc:"uint32"`
|
||||||
|
AutoFightTime uint32 `struc:"uint32"`
|
||||||
|
Reserve2 uint32 `struc:"uint32"`
|
||||||
|
EnergyTime uint32 `struc:"uint32"`
|
||||||
|
LearnTimes uint32 `struc:"uint32"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type legacyGroupChangePetSuccessInfo struct {
|
||||||
|
Side uint8 `struc:"uint8"`
|
||||||
|
Pos uint8 `struc:"uint8"`
|
||||||
|
UserID uint32 `struc:"uint32"`
|
||||||
|
PetID uint32 `struc:"uint32"`
|
||||||
|
CatchTime uint32 `struc:"uint32"`
|
||||||
|
Level uint32 `struc:"uint32"`
|
||||||
|
Hp uint32 `struc:"uint32"`
|
||||||
|
MaxHp uint32 `struc:"uint32"`
|
||||||
|
SkinID uint32 `struc:"uint32"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func groupModelByFight(f *FightC) uint32 {
|
||||||
|
if f == nil {
|
||||||
|
return groupModelBoss
|
||||||
|
}
|
||||||
|
switch {
|
||||||
|
case f.Info.Status == 0:
|
||||||
|
return groupModelNPC
|
||||||
|
case f.Info.Status == 1 && f.Info.Mode == 1:
|
||||||
|
return groupModelPlayerSingle
|
||||||
|
case f.Info.Status == 1:
|
||||||
|
return groupModelPlayerMulti
|
||||||
|
default:
|
||||||
|
return groupModelBoss
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FightC) fightPacketCmd(kind fightPacketKind) uint32 {
|
||||||
|
switch kind {
|
||||||
|
case fightPacketReady:
|
||||||
|
return 2503
|
||||||
|
case fightPacketStart:
|
||||||
|
return 2504
|
||||||
|
case fightPacketSkillResult:
|
||||||
|
return 2505
|
||||||
|
case fightPacketOver:
|
||||||
|
return 2506
|
||||||
|
case fightPacketChangePetSuccess:
|
||||||
|
if f != nil && f.LegacyGroupProtocol {
|
||||||
|
return groupCmdChangePetSuc
|
||||||
|
}
|
||||||
|
return 2407
|
||||||
|
case fightPacketUseItem:
|
||||||
|
if f != nil && f.LegacyGroupProtocol {
|
||||||
|
return groupCmdUseItem
|
||||||
|
}
|
||||||
|
return 2406
|
||||||
|
case fightPacketChat:
|
||||||
|
if f != nil && f.LegacyGroupProtocol {
|
||||||
|
return groupCmdChat
|
||||||
|
}
|
||||||
|
return 50002
|
||||||
|
case fightPacketLoadPercentNotice:
|
||||||
|
if f != nil && f.LegacyGroupProtocol {
|
||||||
|
return groupCmdLoadPercentNotice
|
||||||
|
}
|
||||||
|
return 2441
|
||||||
|
default:
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FightC) sendFightPacket(player common.PlayerI, kind fightPacketKind, payload any) {
|
||||||
|
if player == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cmd := f.fightPacketCmd(kind)
|
||||||
|
if cmd == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
player.SendPackCmd(cmd, payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FightC) sendLegacyGroupReady(player common.PlayerI) {
|
||||||
|
if f == nil || !f.LegacyGroupProtocol || player == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
player.SendPackCmd(groupCmdReadyToFight, f.buildLegacyGroupReadyInfo())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FightC) buildLegacyGroupReadyInfo() *legacyGroupReadyToFightInfo {
|
||||||
|
return &legacyGroupReadyToFightInfo{
|
||||||
|
Model: groupModelByFight(f),
|
||||||
|
GroupOneInfo: f.buildLegacyReadyTeam(f.OurPlayers, f.Our),
|
||||||
|
GroupTwoInfo: f.buildLegacyReadyTeam(f.OppPlayers, f.Opp),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FightC) buildLegacyReadyTeam(players []common.PlayerI, inputs []*input.Input) legacyReadyToFightTeam {
|
||||||
|
team := legacyReadyToFightTeam{InvitorID: 1}
|
||||||
|
users := make([]legacyReadyFightUser, 0, len(players))
|
||||||
|
for _, p := range players {
|
||||||
|
if p == nil || p.GetInfo() == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
users = append(users, legacyReadyFightUser{
|
||||||
|
UserID: p.GetInfo().UserID,
|
||||||
|
Nick: p.GetInfo().Nick,
|
||||||
|
MonList: collectLegacyReadyPetsByController(inputs, p.GetInfo().UserID),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if len(users) == 0 {
|
||||||
|
if fallback := firstNonNilInput(inputs); fallback != nil && fallback.Player != nil && fallback.Player.GetInfo() != nil {
|
||||||
|
info := fallback.Player.GetInfo()
|
||||||
|
users = append(users, legacyReadyFightUser{
|
||||||
|
UserID: info.UserID,
|
||||||
|
Nick: info.Nick,
|
||||||
|
MonList: collectLegacyReadyPetsByController(inputs, info.UserID),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for idx := range users {
|
||||||
|
users[idx].MonCnt = uint32(len(users[idx].MonList))
|
||||||
|
}
|
||||||
|
if len(users) > 0 {
|
||||||
|
team.LeaderID = users[0].UserID
|
||||||
|
}
|
||||||
|
team.GroupList = users
|
||||||
|
return team
|
||||||
|
}
|
||||||
|
|
||||||
|
func collectLegacyReadyPetsByController(inputs []*input.Input, controllerID uint32) []legacyReadyFightPet {
|
||||||
|
pets := make([]legacyReadyFightPet, 0, 6)
|
||||||
|
for _, in := range inputs {
|
||||||
|
if in == nil || !in.ControlledBy(controllerID) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
currentPet := in.CurrentPet()
|
||||||
|
if currentPet == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
pets = append(pets, buildLegacyReadyFightPet(currentPet))
|
||||||
|
}
|
||||||
|
return pets
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildLegacyReadyFightPet(pet *info.BattlePetEntity) legacyReadyFightPet {
|
||||||
|
result := legacyReadyFightPet{}
|
||||||
|
if pet == nil {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
moves := make([]uint32, 0, len(pet.Info.SkillList))
|
||||||
|
for _, skill := range pet.Info.SkillList {
|
||||||
|
if skill.ID == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
moves = append(moves, skill.ID)
|
||||||
|
}
|
||||||
|
result.ID = pet.Info.ID
|
||||||
|
result.MoveList = moves
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func firstNonNilInput(inputs []*input.Input) *input.Input {
|
||||||
|
for _, in := range inputs {
|
||||||
|
if in != nil {
|
||||||
|
return in
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FightC) sendLegacyGroupStart(player common.PlayerI) {
|
||||||
|
if f == nil || !f.LegacyGroupProtocol || player == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
player.SendPackCmd(groupCmdStartFight, f.buildLegacyGroupStartInfo())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FightC) buildLegacyGroupStartInfo() *legacyGroupStartInfo {
|
||||||
|
return &legacyGroupStartInfo{
|
||||||
|
IsGank: 0,
|
||||||
|
GroupOne: f.collectLegacyGroupStartPets(f.Our, 1),
|
||||||
|
GroupTwo: f.collectLegacyGroupStartPets(f.Opp, 2),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FightC) collectLegacyGroupStartPets(inputs []*input.Input, side uint8) []legacyGroupStartPet {
|
||||||
|
ret := make([]legacyGroupStartPet, 0, len(inputs))
|
||||||
|
for pos, in := range inputs {
|
||||||
|
if in == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
currentPet := in.CurrentPet()
|
||||||
|
if currentPet == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
userID := uint32(0)
|
||||||
|
if in.Player != nil && in.Player.GetInfo() != nil {
|
||||||
|
userID = in.Player.GetInfo().UserID
|
||||||
|
}
|
||||||
|
ret = append(ret, legacyGroupStartPet{
|
||||||
|
Side: side,
|
||||||
|
Pos: uint8(pos),
|
||||||
|
UserID: userID,
|
||||||
|
IsChange: 0,
|
||||||
|
PetID: currentPet.Info.ID,
|
||||||
|
CatchTime: currentPet.Info.CatchTime,
|
||||||
|
Hp: currentPet.Info.Hp,
|
||||||
|
MaxHp: currentPet.Info.MaxHp,
|
||||||
|
Level: currentPet.Info.Level,
|
||||||
|
Reserve: 0,
|
||||||
|
Flag: 1,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FightC) sendLegacyGroupOver(player common.PlayerI, over *model.FightOverInfo) {
|
||||||
|
if f == nil || !f.LegacyGroupProtocol || player == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
player.SendPackCmd(groupCmdFightOver, f.buildLegacyGroupOverInfo(over))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FightC) buildLegacyGroupOverInfo(over *model.FightOverInfo) *legacyGroupFightOverInfo {
|
||||||
|
result := &legacyGroupFightOverInfo{}
|
||||||
|
if over != nil {
|
||||||
|
result.Reason = resolveLegacyGroupFightOverReason(over)
|
||||||
|
result.WinnerID = over.WinnerId
|
||||||
|
}
|
||||||
|
if our := f.primaryOurPlayer(); our != nil && our.GetInfo() != nil {
|
||||||
|
playerInfo := our.GetInfo()
|
||||||
|
result.TwoTimes = uint32(playerInfo.TwoTimes)
|
||||||
|
result.ThreeTimes = uint32(playerInfo.ThreeTimes)
|
||||||
|
result.AutoFightTime = playerInfo.AutoFightTime
|
||||||
|
result.EnergyTime = uint32(playerInfo.EnergyTime)
|
||||||
|
result.LearnTimes = playerInfo.LearnTimes
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func mapLegacyGroupFightOverReason(reason model.EnumBattleOverReason) uint32 {
|
||||||
|
return mapUnifiedFightOverReason(reason)
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolveLegacyGroupFightOverReason(over *model.FightOverInfo) uint32 {
|
||||||
|
if over == nil {
|
||||||
|
return mapUnifiedFightOverReason(0)
|
||||||
|
}
|
||||||
|
if over.WinnerId != 0 {
|
||||||
|
return mapUnifiedFightOverReason(0)
|
||||||
|
}
|
||||||
|
return mapLegacyGroupFightOverReason(over.Reason)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FightC) sendLegacyGroupChangePetSuccess(player common.PlayerI, in *input.Input, reason *info.ChangePetInfo) {
|
||||||
|
if f == nil || !f.LegacyGroupProtocol || player == nil || in == nil || reason == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
player.SendPackCmd(groupCmdChangePetSuc, f.buildLegacyGroupChangePetSuccessInfo(in, reason))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FightC) buildLegacyGroupChangePetSuccessInfo(in *input.Input, reason *info.ChangePetInfo) *legacyGroupChangePetSuccessInfo {
|
||||||
|
result := &legacyGroupChangePetSuccessInfo{}
|
||||||
|
if in == nil || reason == nil {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
if !f.isOurPlayerID(in.UserID) {
|
||||||
|
result.Side = 2
|
||||||
|
} else {
|
||||||
|
result.Side = 1
|
||||||
|
}
|
||||||
|
result.Pos = uint8(in.TeamSlotIndex())
|
||||||
|
result.UserID = reason.UserId
|
||||||
|
result.PetID = reason.ID
|
||||||
|
result.CatchTime = reason.CatchTime
|
||||||
|
result.Level = reason.Level
|
||||||
|
result.Hp = reason.Hp
|
||||||
|
result.MaxHp = reason.MaxHp
|
||||||
|
if currentPet := in.CurrentPet(); currentPet != nil {
|
||||||
|
result.SkinID = currentPet.Info.SkinID
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FightC) SendLegacyEscapeSuccess(player common.PlayerI, actorIndex int) {
|
||||||
|
if f == nil || !f.LegacyGroupProtocol || player == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
side := uint8(1)
|
||||||
|
if !f.isOurPlayerID(player.GetInfo().UserID) {
|
||||||
|
side = 2
|
||||||
|
}
|
||||||
|
payload := legacyEscapeSuccessInfo{
|
||||||
|
UserID: player.GetInfo().UserID,
|
||||||
|
Nick: player.GetInfo().Nick,
|
||||||
|
Side: side,
|
||||||
|
ActorIndex: uint8(actorIndex),
|
||||||
|
}
|
||||||
|
f.BroadcastPlayers(func(p common.PlayerI) {
|
||||||
|
p.SendPackCmd(groupCmdEscapeSuc, &payload)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FightC) sendLegacyRoundBroadcast(firstAttack, secondAttack *action.SelectSkillAction) {
|
||||||
|
if f == nil || !f.LegacyGroupProtocol {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if f.legacySkillExecuted(firstAttack) {
|
||||||
|
f.sendLegacyGroupSkillHurt(firstAttack)
|
||||||
|
}
|
||||||
|
if f.legacySkillExecuted(secondAttack) {
|
||||||
|
f.sendLegacyGroupSkillHurt(secondAttack)
|
||||||
|
}
|
||||||
|
f.sendLegacyGroupBoutDone()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FightC) legacySkillExecuted(skillAction *action.SelectSkillAction) bool {
|
||||||
|
if f == nil || skillAction == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
attacker := f.GetInputByAction(skillAction, false)
|
||||||
|
return attacker != nil && attacker.AttackValue != nil && attacker.AttackValue.SkillID != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FightC) sendLegacyGroupSkillHurt(skillAction *action.SelectSkillAction) {
|
||||||
|
if f == nil || !f.LegacyGroupProtocol || skillAction == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
packet := f.buildLegacyGroupSkillHurtPacket(skillAction)
|
||||||
|
if packet == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
f.BroadcastPlayers(func(p common.PlayerI) {
|
||||||
|
p.SendPackCmd(groupCmdSkillHurt, packet)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FightC) buildLegacyGroupSkillHurtPacket(skillAction *action.SelectSkillAction) *legacyGroupSkillHurtPacket {
|
||||||
|
attacker := f.GetInputByAction(skillAction, false)
|
||||||
|
defender := f.GetInputByAction(skillAction, true)
|
||||||
|
if attacker == nil || defender == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &legacyGroupSkillHurtPacket{
|
||||||
|
IsGank: 0,
|
||||||
|
Attack: f.buildLegacyGroupSkillAttackInfo(skillAction, attacker),
|
||||||
|
Attacked: f.buildLegacyGroupSkillDefendInfo(defender),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FightC) fillLegacyGroupSkillCommonFields(
|
||||||
|
self *input.Input,
|
||||||
|
isAttackor uint8,
|
||||||
|
statusList *[20]uint8,
|
||||||
|
batLvList *[6]uint8,
|
||||||
|
) (side uint8, pos uint8, userID uint32, petID uint32, hp uint32, maxHP uint32, moveMap []legacyGroupSkillMoveInfo, flag uint32) {
|
||||||
|
if self == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !f.isOurPlayerID(self.UserID) {
|
||||||
|
side = 2
|
||||||
|
} else {
|
||||||
|
side = 1
|
||||||
|
}
|
||||||
|
pos = uint8(self.TeamSlotIndex())
|
||||||
|
userID = self.UserID
|
||||||
|
attackValue := self.AttackValue
|
||||||
|
if attackValue == nil {
|
||||||
|
attackValue = info.NewAttackValue(self.UserID)
|
||||||
|
}
|
||||||
|
for i := 0; i < len(attackValue.Status) && i < 20; i++ {
|
||||||
|
statusList[i] = uint8(attackValue.Status[i])
|
||||||
|
}
|
||||||
|
for i := 0; i < len(attackValue.Prop) && i < len(batLvList); i++ {
|
||||||
|
batLvList[i] = uint8(attackValue.Prop[i])
|
||||||
|
}
|
||||||
|
currentPet := self.CurrentPet()
|
||||||
|
if currentPet != nil {
|
||||||
|
petID = currentPet.Info.ID
|
||||||
|
hp = currentPet.Info.Hp
|
||||||
|
maxHP = currentPet.Info.MaxHp
|
||||||
|
moveMap = collectLegacyGroupSkillMoves(currentPet.Info.SkillList)
|
||||||
|
} else {
|
||||||
|
hp = clampLegacyInt32ToUint32(attackValue.RemainHp)
|
||||||
|
maxHP = attackValue.MaxHp
|
||||||
|
moveMap = collectLegacyGroupSkillMoves(attackValue.SkillList)
|
||||||
|
}
|
||||||
|
flag = attackValue.State
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FightC) buildLegacyGroupSkillAttackInfo(skillAction *action.SelectSkillAction, self *input.Input) legacyGroupSkillAttackInfo {
|
||||||
|
result := legacyGroupSkillAttackInfo{}
|
||||||
|
if self == nil {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
result.IsAttackor = 0
|
||||||
|
result.Side, result.Pos, result.UserID, result.PetID, result.Hp, result.MaxHp, result.MoveMap, result.Flag =
|
||||||
|
f.fillLegacyGroupSkillCommonFields(self, result.IsAttackor, &result.StatusList, &result.BatLvList)
|
||||||
|
attackValue := self.AttackValue
|
||||||
|
if attackValue == nil {
|
||||||
|
attackValue = info.NewAttackValue(self.UserID)
|
||||||
|
}
|
||||||
|
if attackValue.SkillID != 0 {
|
||||||
|
result.MoveID = attackValue.SkillID
|
||||||
|
} else if skillAction != nil && skillAction.SkillEntity != nil {
|
||||||
|
result.MoveID = uint32(skillAction.SkillEntity.XML.ID)
|
||||||
|
}
|
||||||
|
result.IsCrit = attackValue.IsCritical
|
||||||
|
result.EffectName = attackValue.State
|
||||||
|
result.AtkTimes = 1
|
||||||
|
result.Dmg = int32(attackValue.LostHp)
|
||||||
|
result.ChgHp = attackValue.GainHp
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FightC) buildLegacyGroupSkillDefendInfo(self *input.Input) legacyGroupSkillDefendInfo {
|
||||||
|
result := legacyGroupSkillDefendInfo{}
|
||||||
|
if self == nil {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
result.IsAttackor = 1
|
||||||
|
result.Side, result.Pos, result.UserID, result.PetID, result.Hp, result.MaxHp, result.MoveMap, result.Flag =
|
||||||
|
f.fillLegacyGroupSkillCommonFields(self, result.IsAttackor, &result.StatusList, &result.BatLvList)
|
||||||
|
result.MoveID = 0
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func collectLegacyGroupSkillMoves(skills []model.SkillInfo) []legacyGroupSkillMoveInfo {
|
||||||
|
moves := make([]legacyGroupSkillMoveInfo, 0, len(skills))
|
||||||
|
for _, skill := range skills {
|
||||||
|
if skill.ID == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
moves = append(moves, legacyGroupSkillMoveInfo{
|
||||||
|
MoveID: skill.ID,
|
||||||
|
PP: skill.PP,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return moves
|
||||||
|
}
|
||||||
|
|
||||||
|
func clampLegacyInt32ToUint32(v int32) uint32 {
|
||||||
|
if v < 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return uint32(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FightC) sendLegacyGroupBoutDone() {
|
||||||
|
if f == nil || !f.LegacyGroupProtocol {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
payload := legacyBoutDoneInfo{Round: f.Round}
|
||||||
|
f.BroadcastPlayers(func(p common.PlayerI) {
|
||||||
|
p.SendPackCmd(groupCmdBoutDone, &payload)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FightC) sendLegacySpriteDie(in *input.Input, hasBackup bool) {
|
||||||
|
if f == nil || !f.LegacyGroupProtocol || in == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
side := uint8(1)
|
||||||
|
if !f.isOurPlayerID(in.UserID) {
|
||||||
|
side = 2
|
||||||
|
}
|
||||||
|
var data uint32
|
||||||
|
if hasBackup {
|
||||||
|
data = 1
|
||||||
|
}
|
||||||
|
payload := legacySpriteDieInfo{
|
||||||
|
Count: 1,
|
||||||
|
Side: side,
|
||||||
|
ActorIndex: uint8(in.TeamSlotIndex()),
|
||||||
|
Flag: 1,
|
||||||
|
HasBackup: data,
|
||||||
|
}
|
||||||
|
f.BroadcastPlayers(func(p common.PlayerI) {
|
||||||
|
p.SendPackCmd(groupCmdSpriteDie, &payload)
|
||||||
|
})
|
||||||
|
}
|
||||||
61
logic/service/fight/group_legacy_test.go
Normal file
61
logic/service/fight/group_legacy_test.go
Normal 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])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,11 +10,23 @@ type PlayerCaptureContext struct {
|
|||||||
Guarantees map[int]int // 按分母分组的保底分子 map[denominator]numerator
|
Guarantees map[int]int // 按分母分组的保底分子 map[denominator]numerator
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func NewPlayerCaptureContext() *PlayerCaptureContext {
|
||||||
|
return &PlayerCaptureContext{
|
||||||
|
Denominator: 1000,
|
||||||
|
DecayFactor: 0.10,
|
||||||
|
MinDecayNum: 1,
|
||||||
|
Guarantees: make(map[int]int),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Roll 通用概率判定(带共享保底) 返回成功,基础,保底
|
// Roll 通用概率判定(带共享保底) 返回成功,基础,保底
|
||||||
func (c *PlayerCaptureContext) Roll(numerator, denominator int) (bool, float64, float64) {
|
func (c *PlayerCaptureContext) Roll(numerator, denominator int) (bool, float64, float64) {
|
||||||
if denominator <= 0 {
|
if denominator <= 0 {
|
||||||
return false, 0, 0
|
return false, 0, 0
|
||||||
}
|
}
|
||||||
|
if c == nil {
|
||||||
|
c = NewPlayerCaptureContext()
|
||||||
|
}
|
||||||
|
|
||||||
base := float64(numerator) / float64(denominator)
|
base := float64(numerator) / float64(denominator)
|
||||||
bonusNumerator := c.getGuaranteeNumerator(denominator)
|
bonusNumerator := c.getGuaranteeNumerator(denominator)
|
||||||
@@ -41,14 +53,29 @@ func (c *PlayerCaptureContext) Roll(numerator, denominator int) (bool, float64,
|
|||||||
|
|
||||||
// 保底操作
|
// 保底操作
|
||||||
func (c *PlayerCaptureContext) getGuaranteeNumerator(denominator int) int {
|
func (c *PlayerCaptureContext) getGuaranteeNumerator(denominator int) int {
|
||||||
|
if c == nil || c.Guarantees == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
if num, ok := c.Guarantees[denominator]; ok {
|
if num, ok := c.Guarantees[denominator]; ok {
|
||||||
return num
|
return num
|
||||||
}
|
}
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
func (c *PlayerCaptureContext) increaseGuarantee(denominator int) {
|
func (c *PlayerCaptureContext) increaseGuarantee(denominator int) {
|
||||||
|
if c == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if c.Guarantees == nil {
|
||||||
|
c.Guarantees = make(map[int]int)
|
||||||
|
}
|
||||||
c.Guarantees[denominator]++
|
c.Guarantees[denominator]++
|
||||||
}
|
}
|
||||||
func (c *PlayerCaptureContext) resetGuarantee(denominator int) {
|
func (c *PlayerCaptureContext) resetGuarantee(denominator int) {
|
||||||
|
if c == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if c.Guarantees == nil {
|
||||||
|
c.Guarantees = make(map[int]int)
|
||||||
|
}
|
||||||
c.Guarantees[denominator] = 0
|
c.Guarantees[denominator] = 0
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -196,10 +196,8 @@ type PropDict struct {
|
|||||||
|
|
||||||
// NoteUseSkillOutboundInfo 战斗技能使用通知的出站信息结构体
|
// NoteUseSkillOutboundInfo 战斗技能使用通知的出站信息结构体
|
||||||
type NoteUseSkillOutboundInfo struct {
|
type NoteUseSkillOutboundInfo struct {
|
||||||
FirstAttackInfoLen uint32 `struc:"sizeof=FirstAttackInfo"`
|
FirstAttackInfo model.AttackValue // 本轮先手方精灵在释放技能结束后的状态
|
||||||
FirstAttackInfo []model.AttackValue // 本轮先手方精灵在释放技能结束后的状态
|
SecondAttackInfo model.AttackValue // 本轮后手方精灵在释放技能结束后的状态
|
||||||
SecondAttackInfoLen uint32 `struc:"sizeof=SecondAttackInfo"`
|
|
||||||
SecondAttackInfo []model.AttackValue // 本轮后手方精灵在释放技能结束后的状态
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type FightStartOutboundInfo struct {
|
type FightStartOutboundInfo struct {
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ type FightC struct {
|
|||||||
info.FightStartOutboundInfo
|
info.FightStartOutboundInfo
|
||||||
Info info.Fightinfo
|
Info info.Fightinfo
|
||||||
IsReady bool
|
IsReady bool
|
||||||
|
LegacyGroupProtocol bool
|
||||||
ownerID uint32 // 战斗发起者ID
|
ownerID uint32 // 战斗发起者ID
|
||||||
Our []*input.Input // 我方战斗位
|
Our []*input.Input // 我方战斗位
|
||||||
Opp []*input.Input // 敌方战斗位
|
Opp []*input.Input // 敌方战斗位
|
||||||
@@ -38,6 +39,7 @@ type FightC struct {
|
|||||||
actionNotify chan struct{}
|
actionNotify chan struct{}
|
||||||
acceptActions bool
|
acceptActions bool
|
||||||
pendingActions []action.BattleActionI // 待处理动作队列,同一战斗位最多保留一个动作
|
pendingActions []action.BattleActionI // 待处理动作队列,同一战斗位最多保留一个动作
|
||||||
|
pendingHead int
|
||||||
actionRound atomic.Uint32
|
actionRound atomic.Uint32
|
||||||
|
|
||||||
quit chan struct{}
|
quit chan struct{}
|
||||||
@@ -227,14 +229,37 @@ func (f *FightC) getInputByController(userID uint32, isOpposite bool) *input.Inp
|
|||||||
func (f *FightC) expectedActionSlots() map[actionSlotKey]struct{} {
|
func (f *FightC) expectedActionSlots() map[actionSlotKey]struct{} {
|
||||||
slots := make(map[actionSlotKey]struct{}, len(f.Our)+len(f.Opp))
|
slots := make(map[actionSlotKey]struct{}, len(f.Our)+len(f.Opp))
|
||||||
for _, slot := range f.SideSlots(SideOur) {
|
for _, slot := range f.SideSlots(SideOur) {
|
||||||
|
if f.slotNeedsAction(slot.Input) {
|
||||||
slots[newActionSlotKey(slot.ControllerUserID, slot.SlotIndex)] = struct{}{}
|
slots[newActionSlotKey(slot.ControllerUserID, slot.SlotIndex)] = struct{}{}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
for _, slot := range f.SideSlots(SideOpp) {
|
for _, slot := range f.SideSlots(SideOpp) {
|
||||||
|
if f.slotNeedsAction(slot.Input) {
|
||||||
slots[newActionSlotKey(slot.ControllerUserID, slot.SlotIndex)] = struct{}{}
|
slots[newActionSlotKey(slot.ControllerUserID, slot.SlotIndex)] = struct{}{}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return slots
|
return slots
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *FightC) sideHasActionableSlots(side int) bool {
|
||||||
|
for _, slot := range f.SideSlots(side) {
|
||||||
|
if f.slotNeedsAction(slot.Input) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FightC) slotNeedsAction(in *input.Input) bool {
|
||||||
|
if in == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if current := in.CurrentPet(); current != nil && current.Info.Hp > 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return in.HasLivingBench()
|
||||||
|
}
|
||||||
|
|
||||||
func (f *FightC) setActionAttackValue(act action.BattleActionI) {
|
func (f *FightC) setActionAttackValue(act action.BattleActionI) {
|
||||||
if act == nil {
|
if act == nil {
|
||||||
return
|
return
|
||||||
@@ -245,6 +270,9 @@ func (f *FightC) setActionAttackValue(act action.BattleActionI) {
|
|||||||
}
|
}
|
||||||
attacker.AttackValue.ActorIndex = uint32(act.GetActorIndex())
|
attacker.AttackValue.ActorIndex = uint32(act.GetActorIndex())
|
||||||
targetIndex, _ := DecodeTargetIndex(act.GetTargetIndex())
|
targetIndex, _ := DecodeTargetIndex(act.GetTargetIndex())
|
||||||
|
if _, resolvedIndex, ok := f.resolveActionTarget(act); ok && resolvedIndex >= 0 {
|
||||||
|
targetIndex = resolvedIndex
|
||||||
|
}
|
||||||
attacker.AttackValue.TargetIndex = uint32(targetIndex)
|
attacker.AttackValue.TargetIndex = uint32(targetIndex)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -285,6 +313,25 @@ func (f *FightC) GetInputByPlayerAt(c common.PlayerI, actorIndex int, isOpposite
|
|||||||
return f.getInputByUserID(c.GetInfo().UserID, actorIndex, isOpposite)
|
return f.getInputByUserID(c.GetInfo().UserID, actorIndex, isOpposite)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *FightC) resolveActionTarget(c action.BattleActionI) (*input.Input, int, bool) {
|
||||||
|
if c == nil {
|
||||||
|
return nil, -1, false
|
||||||
|
}
|
||||||
|
attacker := f.getInputByUserID(c.GetPlayerID(), c.GetActorIndex(), false)
|
||||||
|
if attacker == nil {
|
||||||
|
return nil, -1, false
|
||||||
|
}
|
||||||
|
encodedTargetIndex := c.GetTargetIndex()
|
||||||
|
targetIndex, targetIsOpposite := DecodeTargetIndex(encodedTargetIndex)
|
||||||
|
if !targetIsOpposite {
|
||||||
|
return attacker.TeamSlotAt(targetIndex), targetIndex, false
|
||||||
|
}
|
||||||
|
if target, resolvedIndex := attacker.OpponentSlotAtOrNextLiving(targetIndex); target != nil {
|
||||||
|
return target, resolvedIndex, true
|
||||||
|
}
|
||||||
|
return attacker.OpponentSlotAt(targetIndex), targetIndex, true
|
||||||
|
}
|
||||||
|
|
||||||
func (f *FightC) GetInputByAction(c action.BattleActionI, isOpposite bool) *input.Input {
|
func (f *FightC) GetInputByAction(c action.BattleActionI, isOpposite bool) *input.Input {
|
||||||
if c == nil {
|
if c == nil {
|
||||||
if isOpposite {
|
if isOpposite {
|
||||||
@@ -296,8 +343,8 @@ func (f *FightC) GetInputByAction(c action.BattleActionI, isOpposite bool) *inpu
|
|||||||
if !isOpposite {
|
if !isOpposite {
|
||||||
return f.getInputByUserID(c.GetPlayerID(), index, false)
|
return f.getInputByUserID(c.GetPlayerID(), index, false)
|
||||||
}
|
}
|
||||||
targetIndex, targetIsOpposite := DecodeTargetIndex(c.GetTargetIndex())
|
target, _, _ := f.resolveActionTarget(c)
|
||||||
return f.getInputByUserID(c.GetPlayerID(), targetIndex, targetIsOpposite)
|
return target
|
||||||
}
|
}
|
||||||
|
|
||||||
// 玩家使用技能
|
// 玩家使用技能
|
||||||
@@ -329,8 +376,16 @@ func (f *FightC) GetOpp(c common.PlayerI) *input.Input {
|
|||||||
|
|
||||||
// // 获取随机数
|
// // 获取随机数
|
||||||
func (f *FightC) IsFirst(play common.PlayerI) bool {
|
func (f *FightC) IsFirst(play common.PlayerI) bool {
|
||||||
|
if f == nil || play == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if f.TrueFirst != nil && f.TrueFirst.Player != nil {
|
||||||
return f.TrueFirst.Player == play
|
return f.TrueFirst.Player == play
|
||||||
|
}
|
||||||
|
if f.First != nil && f.First.Player != nil {
|
||||||
|
return f.First.Player == play
|
||||||
|
}
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
func (f *FightC) GetRound() uint32 {
|
func (f *FightC) GetRound() uint32 {
|
||||||
|
|
||||||
@@ -338,7 +393,7 @@ func (f *FightC) GetRound() uint32 {
|
|||||||
}
|
}
|
||||||
func (f *FightC) Chat(c common.PlayerI, msg string) {
|
func (f *FightC) Chat(c common.PlayerI, msg string) {
|
||||||
|
|
||||||
f.GetInputByPlayer(c, true).Player.SendPackCmd(50002, &user.ChatOutboundInfo{
|
f.sendFightPacket(f.GetInputByPlayer(c, true).Player, fightPacketChat, &user.ChatOutboundInfo{
|
||||||
SenderId: c.GetInfo().UserID,
|
SenderId: c.GetInfo().UserID,
|
||||||
SenderNickname: c.GetInfo().Nick,
|
SenderNickname: c.GetInfo().Nick,
|
||||||
Message: utils.RemoveLast(msg),
|
Message: utils.RemoveLast(msg),
|
||||||
@@ -353,7 +408,7 @@ func (f *FightC) LoadPercent(c common.PlayerI, percent int32) {
|
|||||||
if f.Info.Status == info.BattleMode.FIGHT_WITH_NPC {
|
if f.Info.Status == info.BattleMode.FIGHT_WITH_NPC {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
f.GetInputByPlayer(c, true).Player.SendPackCmd(2441, &info.LoadPercentOutboundInfo{
|
f.sendFightPacket(f.GetInputByPlayer(c, true).Player, fightPacketLoadPercentNotice, &info.LoadPercentOutboundInfo{
|
||||||
Id: c.GetInfo().UserID,
|
Id: c.GetInfo().UserID,
|
||||||
Percent: uint32(percent),
|
Percent: uint32(percent),
|
||||||
})
|
})
|
||||||
@@ -467,6 +522,27 @@ func (f *FightC) Broadcast(t func(ff *input.Input)) {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *FightC) BroadcastPlayers(t func(common.PlayerI)) {
|
||||||
|
if f == nil || t == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
seen := make(map[uint32]struct{}, len(f.OurPlayers)+len(f.OppPlayers))
|
||||||
|
visit := func(players []common.PlayerI) {
|
||||||
|
for _, p := range players {
|
||||||
|
if p == nil || p.GetInfo() == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, ok := seen[p.GetInfo().UserID]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[p.GetInfo().UserID] = struct{}{}
|
||||||
|
t(p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
visit(f.OurPlayers)
|
||||||
|
visit(f.OppPlayers)
|
||||||
|
}
|
||||||
|
|
||||||
func (f *FightC) GetOverChan() chan struct{} {
|
func (f *FightC) GetOverChan() chan struct{} {
|
||||||
return f.over
|
return f.over
|
||||||
|
|
||||||
|
|||||||
@@ -41,6 +41,10 @@ func getItemBonus(itemID uint32) float64 {
|
|||||||
// -1是保底模式,0是锁定模式,》0是衰减模式
|
// -1是保底模式,0是锁定模式,》0是衰减模式
|
||||||
// Capture 执行捕捉 ,捕捉精灵,使用的道具,模式
|
// Capture 执行捕捉 ,捕捉精灵,使用的道具,模式
|
||||||
func (our *Input) Capture(pet *info.BattlePetEntity, ItemID uint32, ownerpet int) (bool, CaptureDetails) {
|
func (our *Input) Capture(pet *info.BattlePetEntity, ItemID uint32, ownerpet int) (bool, CaptureDetails) {
|
||||||
|
captureCtx := our.Player.GetPlayerCaptureContext()
|
||||||
|
if captureCtx == nil {
|
||||||
|
captureCtx = info.NewPlayerCaptureContext()
|
||||||
|
}
|
||||||
|
|
||||||
if getItemBonus(ItemID) >= 255 {
|
if getItemBonus(ItemID) >= 255 {
|
||||||
return true, CaptureDetails{
|
return true, CaptureDetails{
|
||||||
@@ -69,12 +73,12 @@ func (our *Input) Capture(pet *info.BattlePetEntity, ItemID uint32, ownerpet int
|
|||||||
|
|
||||||
// 计算基础捕捉率
|
// 计算基础捕捉率
|
||||||
baseRate := our.calcBaseRate(pet, ItemID)
|
baseRate := our.calcBaseRate(pet, ItemID)
|
||||||
denominator := our.Player.GetPlayerCaptureContext().Denominator
|
denominator := captureCtx.Denominator
|
||||||
numerator := int(baseRate * float64(denominator))
|
numerator := int(baseRate * float64(denominator))
|
||||||
|
|
||||||
// 衰减模式
|
// 衰减模式
|
||||||
if ownerpet > 0 {
|
if ownerpet > 0 {
|
||||||
decay := math.Pow(1-our.Player.GetPlayerCaptureContext().DecayFactor, float64(ownerpet))
|
decay := math.Pow(1-captureCtx.DecayFactor, float64(ownerpet))
|
||||||
baseRate *= decay
|
baseRate *= decay
|
||||||
if baseRate < 0.01 {
|
if baseRate < 0.01 {
|
||||||
baseRate = 0.01 // 最低1%成功率
|
baseRate = 0.01 // 最低1%成功率
|
||||||
@@ -83,7 +87,7 @@ func (our *Input) Capture(pet *info.BattlePetEntity, ItemID uint32, ownerpet int
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 走统一保底判定
|
// 走统一保底判定
|
||||||
success, basePct, bonusPct := our.Player.Roll(numerator, denominator)
|
success, basePct, bonusPct := captureCtx.Roll(numerator, denominator)
|
||||||
|
|
||||||
return success, CaptureDetails{
|
return success, CaptureDetails{
|
||||||
Success: success,
|
Success: success,
|
||||||
@@ -98,8 +102,12 @@ func (our *Input) Capture(pet *info.BattlePetEntity, ItemID uint32, ownerpet int
|
|||||||
|
|
||||||
// calcBaseA 按公式计算a值
|
// calcBaseA 按公式计算a值
|
||||||
func (our *Input) calcBaseA(pet *info.BattlePetEntity, ItemID uint32) int {
|
func (our *Input) calcBaseA(pet *info.BattlePetEntity, ItemID uint32) int {
|
||||||
|
captureCtx := our.Player.GetPlayerCaptureContext()
|
||||||
|
if captureCtx == nil {
|
||||||
|
captureCtx = info.NewPlayerCaptureContext()
|
||||||
|
}
|
||||||
catchRate := gconv.Int(pet.CatchRate)
|
catchRate := gconv.Int(pet.CatchRate)
|
||||||
catchRate = (catchRate * our.Player.GetPlayerCaptureContext().Denominator) / 1000 // 归一化到1000分母
|
catchRate = (catchRate * captureCtx.Denominator) / 1000 // 归一化到1000分母
|
||||||
if catchRate < 3 {
|
if catchRate < 3 {
|
||||||
catchRate = 3
|
catchRate = 3
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,11 +19,15 @@ func Shuffle[T any](slice []T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (our *Input) GetAction() {
|
func (our *Input) GetAction() {
|
||||||
|
actorIndex := our.TeamSlotIndex()
|
||||||
|
targetIndex := our.RandomOpponentSlotIndex()
|
||||||
|
target := our.OpponentSlotAt(targetIndex)
|
||||||
|
|
||||||
next := our.Exec(func(t Effect) bool {
|
next := our.Exec(func(t Effect) bool {
|
||||||
return t.HookAction()
|
return t.HookAction()
|
||||||
})
|
})
|
||||||
|
|
||||||
scriptCtx := buildBossHookActionContext(our, next)
|
scriptCtx := buildBossHookActionContext(our, target, next)
|
||||||
if aiPlayer, ok := our.Player.(*player.AI_player); ok && aiPlayer.BossScript != "" {
|
if aiPlayer, ok := our.Player.(*player.AI_player); ok && aiPlayer.BossScript != "" {
|
||||||
scriptBoss := &configmodel.BossConfig{Script: aiPlayer.BossScript}
|
scriptBoss := &configmodel.BossConfig{Script: aiPlayer.BossScript}
|
||||||
nextByScript, err := scriptBoss.RunHookActionScript(scriptCtx)
|
nextByScript, err := scriptBoss.RunHookActionScript(scriptCtx)
|
||||||
@@ -37,18 +41,18 @@ func (our *Input) GetAction() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if applyBossScriptAction(our, scriptCtx) {
|
if applyBossScriptAction(our, scriptCtx, actorIndex, targetIndex) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
selfPet := our.FightC.GetCurrPET(our.Player)
|
selfPet := our.FightC.GetCurrPETAt(our.Player, 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.ChangePet(our.Player, v.Info.CatchTime)
|
our.FightC.ChangePetAt(our.Player, v.Info.CatchTime, actorIndex)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -68,9 +72,12 @@ func (our *Input) GetAction() {
|
|||||||
if !s.CanUse() {
|
if !s.CanUse() {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
s.DamageValue = our.CalculatePower(our.Opp, s)
|
if target == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
s.DamageValue = our.CalculatePower(target, s)
|
||||||
|
|
||||||
oppPet := our.Opp.CurrentPet()
|
oppPet := target.CurrentPet()
|
||||||
if oppPet == nil {
|
if oppPet == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -99,13 +106,13 @@ func (our *Input) GetAction() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if usedskill != nil {
|
if usedskill != nil {
|
||||||
our.FightC.UseSkill(our.Player, uint32(usedskill.XML.ID))
|
our.FightC.UseSkillAt(our.Player, uint32(usedskill.XML.ID), actorIndex, targetIndex)
|
||||||
} else {
|
} else {
|
||||||
our.FightC.UseSkill(our.Player, 0)
|
our.FightC.UseSkillAt(our.Player, 0, actorIndex, targetIndex)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildBossHookActionContext(our *Input, hookAction bool) *configmodel.BossHookActionContext {
|
func buildBossHookActionContext(our, opponent *Input, hookAction bool) *configmodel.BossHookActionContext {
|
||||||
ctx := &configmodel.BossHookActionContext{
|
ctx := &configmodel.BossHookActionContext{
|
||||||
HookAction: hookAction,
|
HookAction: hookAction,
|
||||||
Action: "auto",
|
Action: "auto",
|
||||||
@@ -150,8 +157,8 @@ func buildBossHookActionContext(our *Input, hookAction bool) *configmodel.BossHo
|
|||||||
if our.AttackValue != nil {
|
if our.AttackValue != nil {
|
||||||
ctx.OurAttack = convertAttackValue(our.AttackValue)
|
ctx.OurAttack = convertAttackValue(our.AttackValue)
|
||||||
}
|
}
|
||||||
if our.Opp != nil {
|
if opponent != nil {
|
||||||
if oppPet := our.Opp.CurrentPet(); oppPet != nil {
|
if oppPet := opponent.CurrentPet(); oppPet != nil {
|
||||||
ctx.Opp = &configmodel.BossHookPetContext{
|
ctx.Opp = &configmodel.BossHookPetContext{
|
||||||
PetID: oppPet.Info.ID,
|
PetID: oppPet.Info.ID,
|
||||||
CatchTime: oppPet.Info.CatchTime,
|
CatchTime: oppPet.Info.CatchTime,
|
||||||
@@ -159,8 +166,8 @@ func buildBossHookActionContext(our *Input, hookAction bool) *configmodel.BossHo
|
|||||||
MaxHp: oppPet.Info.MaxHp,
|
MaxHp: oppPet.Info.MaxHp,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if our.Opp.AttackValue != nil {
|
if opponent.AttackValue != nil {
|
||||||
ctx.OppAttack = convertAttackValue(our.Opp.AttackValue)
|
ctx.OppAttack = convertAttackValue(opponent.AttackValue)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -195,7 +202,7 @@ func convertAttackValue(src *playermodel.AttackValue) *configmodel.BossHookAttac
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func applyBossScriptAction(our *Input, ctx *configmodel.BossHookActionContext) bool {
|
func applyBossScriptAction(our *Input, ctx *configmodel.BossHookActionContext, actorIndex, targetIndex int) bool {
|
||||||
if our == nil || ctx == nil {
|
if our == nil || ctx == nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -204,10 +211,10 @@ func applyBossScriptAction(our *Input, ctx *configmodel.BossHookActionContext) b
|
|||||||
case "", "auto":
|
case "", "auto":
|
||||||
return false
|
return false
|
||||||
case "skill", "use_skill", "useskill":
|
case "skill", "use_skill", "useskill":
|
||||||
our.FightC.UseSkill(our.Player, ctx.SkillID)
|
our.FightC.UseSkillAt(our.Player, ctx.SkillID, actorIndex, targetIndex)
|
||||||
return true
|
return true
|
||||||
case "switch", "change_pet", "changepet":
|
case "switch", "change_pet", "changepet":
|
||||||
our.FightC.ChangePet(our.Player, ctx.CatchTime)
|
our.FightC.ChangePetAt(our.Player, ctx.CatchTime, actorIndex)
|
||||||
return true
|
return true
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ var EffectType = enum.New[struct {
|
|||||||
}]()
|
}]()
|
||||||
|
|
||||||
var NodeM = make(map[int64]Effect, 0)
|
var NodeM = make(map[int64]Effect, 0)
|
||||||
|
var NodeFactoryM = make(map[int64]func() Effect, 0)
|
||||||
|
|
||||||
func InitEffect(etype EnumEffectType, id int, t Effect) {
|
func InitEffect(etype EnumEffectType, id int, t Effect) {
|
||||||
pr := EffectIDCombiner{}
|
pr := EffectIDCombiner{}
|
||||||
@@ -41,6 +42,13 @@ func InitEffect(etype EnumEffectType, id int, t Effect) {
|
|||||||
|
|
||||||
NodeM[pr.EffectID()] = t
|
NodeM[pr.EffectID()] = t
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func InitEffectFactory(etype EnumEffectType, id int, factory func() Effect) {
|
||||||
|
pr := EffectIDCombiner{}
|
||||||
|
pr.Combine(etype, 0, gconv.Uint16(id))
|
||||||
|
|
||||||
|
NodeFactoryM[pr.EffectID()] = factory
|
||||||
|
}
|
||||||
func GeteffectIDs(etype EnumEffectType) []uint32 {
|
func GeteffectIDs(etype EnumEffectType) []uint32 {
|
||||||
|
|
||||||
var ret []uint32 = make([]uint32, 0)
|
var ret []uint32 = make([]uint32, 0)
|
||||||
@@ -60,6 +68,19 @@ func geteffect[T int | byte | uint16](etype EnumEffectType, id T) Effect {
|
|||||||
pr := EffectIDCombiner{}
|
pr := EffectIDCombiner{}
|
||||||
pr.Combine(etype, 0, gconv.Uint16(id))
|
pr.Combine(etype, 0, gconv.Uint16(id))
|
||||||
|
|
||||||
|
if factory, ok := NodeFactoryM[pr.EffectID()]; ok {
|
||||||
|
eff := factory()
|
||||||
|
if eff == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
eff.ID(pr)
|
||||||
|
if etype == EffectType.Status {
|
||||||
|
eff.CanStack(true)
|
||||||
|
eff.Duration(grand.N(1, 2))
|
||||||
|
}
|
||||||
|
return eff
|
||||||
|
}
|
||||||
|
|
||||||
//todo 获取前GetEffect
|
//todo 获取前GetEffect
|
||||||
ret, ok := NodeM[pr.EffectID()]
|
ret, ok := NodeM[pr.EffectID()]
|
||||||
if ok {
|
if ok {
|
||||||
@@ -107,18 +128,14 @@ func (our *Input) GetProp(id int) alpacadecimal.Decimal {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (our *Input) GetEffect(etype EnumEffectType, id int) Effect {
|
func (our *Input) GetEffect(etype EnumEffectType, id int) Effect {
|
||||||
var ret []Effect
|
|
||||||
pr := EffectIDCombiner{}
|
pr := EffectIDCombiner{}
|
||||||
pr.Combine(etype, 0, gconv.Uint16(id))
|
pr.Combine(etype, 0, gconv.Uint16(id))
|
||||||
|
our.ensureEffectIndex()
|
||||||
for _, v := range our.Effects {
|
bucket := our.effectsByBase[pr.Base]
|
||||||
if v.ID().Base == pr.Base && v.Alive() {
|
for i := len(bucket) - 1; i >= 0; i-- {
|
||||||
ret = append(ret, v)
|
if bucket[i] != nil && bucket[i].Alive() {
|
||||||
|
return bucket[i]
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
|
||||||
if len(ret) > 0 {
|
|
||||||
return ret[len(ret)-1]
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -178,6 +195,7 @@ func (our *Input) AddEffect(in *Input, e Effect) Effect {
|
|||||||
if e == nil {
|
if e == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
our.ensureEffectIndex()
|
||||||
ctx := e.Ctx()
|
ctx := e.Ctx()
|
||||||
if ctx != nil {
|
if ctx != nil {
|
||||||
if ctx.Source == nil {
|
if ctx.Source == nil {
|
||||||
@@ -204,7 +222,7 @@ func (our *Input) AddEffect(in *Input, e Effect) Effect {
|
|||||||
//TODO 先激活
|
//TODO 先激活
|
||||||
//fmt.Println("产生回合数", e.ID(), e.Duration())
|
//fmt.Println("产生回合数", e.ID(), e.Duration())
|
||||||
// 如果已有同 ID 的效果,尝试叠加
|
// 如果已有同 ID 的效果,尝试叠加
|
||||||
for _, v := range our.Effects {
|
for _, v := range our.effectsByBase[e.ID().Base] {
|
||||||
if v == e {
|
if v == e {
|
||||||
return nil //完全相同,跳过执行
|
return nil //完全相同,跳过执行
|
||||||
}
|
}
|
||||||
@@ -219,7 +237,7 @@ func (our *Input) AddEffect(in *Input, e Effect) Effect {
|
|||||||
if !v.CanStack() { //说明进行了替换
|
if !v.CanStack() { //说明进行了替换
|
||||||
v.Alive(false) //不允许叠层,取消效果
|
v.Alive(false) //不允许叠层,取消效果
|
||||||
e.Duration(utils.Max(e.Duration(), v.Duration()))
|
e.Duration(utils.Max(e.Duration(), v.Duration()))
|
||||||
our.Effects = append(our.Effects, e)
|
our.appendEffect(e)
|
||||||
return v //这里把V替换掉了
|
return v //这里把V替换掉了
|
||||||
} else {
|
} else {
|
||||||
//默认给叠一层
|
//默认给叠一层
|
||||||
@@ -237,7 +255,7 @@ func (our *Input) AddEffect(in *Input, e Effect) Effect {
|
|||||||
}
|
}
|
||||||
//无限叠加,比如能力提升类buff
|
//无限叠加,比如能力提升类buff
|
||||||
// 如果没有同 ID 的效果,直接添加
|
// 如果没有同 ID 的效果,直接添加
|
||||||
our.Effects = append(our.Effects, e)
|
our.appendEffect(e)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -309,6 +327,34 @@ func (our *Input) CancelTurn(in *Input) {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (our *Input) ensureEffectIndex() {
|
||||||
|
if our == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if our.effectsByBase == nil {
|
||||||
|
our.effectsByBase = make(map[int64][]Effect, len(our.Effects))
|
||||||
|
}
|
||||||
|
if our.indexedEffects > len(our.Effects) {
|
||||||
|
our.effectsByBase = make(map[int64][]Effect, len(our.Effects))
|
||||||
|
our.indexedEffects = 0
|
||||||
|
}
|
||||||
|
for our.indexedEffects < len(our.Effects) {
|
||||||
|
effect := our.Effects[our.indexedEffects]
|
||||||
|
if effect != nil {
|
||||||
|
our.effectsByBase[effect.ID().Base] = append(our.effectsByBase[effect.ID().Base], effect)
|
||||||
|
}
|
||||||
|
our.indexedEffects++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (our *Input) appendEffect(effect Effect) {
|
||||||
|
if our == nil || effect == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
our.Effects = append(our.Effects, effect)
|
||||||
|
our.ensureEffectIndex()
|
||||||
|
}
|
||||||
|
|
||||||
// // 消除全部 断回合效果,但是我放下场的时候应该断掉所有的回合类效果
|
// // 消除全部 断回合效果,但是我放下场的时候应该断掉所有的回合类效果
|
||||||
// func (our *Input) CancelAll() {
|
// func (our *Input) CancelAll() {
|
||||||
// our.Effects = make([]Effect, 0)
|
// our.Effects = make([]Effect, 0)
|
||||||
|
|||||||
@@ -77,15 +77,27 @@ func (our *Input) Heal(in *Input, ac action.BattleActionI, value alpacadecimal.D
|
|||||||
|
|
||||||
if healValue >= 0 {
|
if healValue >= 0 {
|
||||||
currentPet.Info.ModelHP(int64(healValue))
|
currentPet.Info.ModelHP(int64(healValue))
|
||||||
|
if our.AttackValue != nil {
|
||||||
|
our.AttackValue.RemainHp = int32(currentPet.Info.Hp)
|
||||||
|
our.AttackValue.MaxHp = currentPet.Info.MaxHp
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
damage := uint32(-healValue)
|
damage := uint32(-healValue)
|
||||||
if damage >= currentPet.Info.Hp {
|
if damage >= currentPet.Info.Hp {
|
||||||
currentPet.Info.Hp = 0
|
currentPet.Info.Hp = 0
|
||||||
|
if our.AttackValue != nil {
|
||||||
|
our.AttackValue.RemainHp = 0
|
||||||
|
our.AttackValue.MaxHp = currentPet.Info.MaxHp
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
currentPet.Info.Hp -= damage
|
currentPet.Info.Hp -= damage
|
||||||
|
if our.AttackValue != nil {
|
||||||
|
our.AttackValue.RemainHp = int32(currentPet.Info.Hp)
|
||||||
|
our.AttackValue.MaxHp = currentPet.Info.MaxHp
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
func (our *Input) HealPP(value int) {
|
func (our *Input) HealPP(value int) {
|
||||||
@@ -227,6 +239,10 @@ func (our *Input) Damage(in *Input, sub *info.DamageZone) {
|
|||||||
} else {
|
} else {
|
||||||
currentPet.Info.Hp = currentPet.Info.Hp - uint32(sub.Damage.IntPart())
|
currentPet.Info.Hp = currentPet.Info.Hp - uint32(sub.Damage.IntPart())
|
||||||
}
|
}
|
||||||
|
if our.AttackValue != nil {
|
||||||
|
our.AttackValue.RemainHp = int32(currentPet.Info.Hp)
|
||||||
|
our.AttackValue.MaxHp = currentPet.Info.MaxHp
|
||||||
|
}
|
||||||
|
|
||||||
//todo 待实现死亡effet
|
//todo 待实现死亡effet
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,13 @@ import (
|
|||||||
"github.com/jinzhu/copier"
|
"github.com/jinzhu/copier"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var statusBonuses = map[info.EnumPetStatus]float64{
|
||||||
|
info.PetStatus.Paralysis: 1.5,
|
||||||
|
info.PetStatus.Poisoned: 1.5,
|
||||||
|
info.PetStatus.Sleep: 2.0,
|
||||||
|
// /info.BattleStatus.Frozen: 2.0,
|
||||||
|
}
|
||||||
|
|
||||||
type Input struct {
|
type Input struct {
|
||||||
CanChange uint32 //是否可以死亡切换CanChange
|
CanChange uint32 //是否可以死亡切换CanChange
|
||||||
// CanAction bool //是否可以行动
|
// CanAction bool //是否可以行动
|
||||||
@@ -30,6 +37,8 @@ type Input struct {
|
|||||||
Effects []Effect //effects 实际上全局就是effect无限回合 //effects容器 技能的
|
Effects []Effect //effects 实际上全局就是effect无限回合 //effects容器 技能的
|
||||||
EffectCache []Effect //这里是命中前执行的容器,也就是命中前执行的所有逻辑相关,理论上一个effect被激活,就应该同时将其他的effect取消激活
|
EffectCache []Effect //这里是命中前执行的容器,也就是命中前执行的所有逻辑相关,理论上一个effect被激活,就应该同时将其他的effect取消激活
|
||||||
EffectLost []Effect
|
EffectLost []Effect
|
||||||
|
effectsByBase map[int64][]Effect
|
||||||
|
indexedEffects int
|
||||||
// 删掉伤害记录,可以在回调中记录,而不是每次调用记录
|
// 删掉伤害记录,可以在回调中记录,而不是每次调用记录
|
||||||
*model.AttackValue
|
*model.AttackValue
|
||||||
FightC common.FightI
|
FightC common.FightI
|
||||||
@@ -57,6 +66,7 @@ func NewInput(c common.FightI, p common.PlayerI) *Input {
|
|||||||
ret := &Input{FightC: c, Player: p}
|
ret := &Input{FightC: c, 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)
|
||||||
|
|
||||||
// t := Geteffect(EffectType.Damage, 0)
|
// t := Geteffect(EffectType.Damage, 0)
|
||||||
// t.Effect.SetArgs(ret)
|
// t.Effect.SetArgs(ret)
|
||||||
@@ -115,6 +125,27 @@ func (our *Input) BenchPets() []*info.BattlePetEntity {
|
|||||||
return bench
|
return bench
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (our *Input) HasLivingBench() bool {
|
||||||
|
if our == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
current := our.CurrentPet()
|
||||||
|
currentCatchTime := uint32(0)
|
||||||
|
if current != nil {
|
||||||
|
currentCatchTime = current.Info.CatchTime
|
||||||
|
}
|
||||||
|
for _, pet := range our.AllPet {
|
||||||
|
if pet == nil || pet.Info.Hp == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if current != nil && pet.Info.CatchTime == currentCatchTime {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func (our *Input) OpponentSlots() []*Input {
|
func (our *Input) OpponentSlots() []*Input {
|
||||||
if our == nil {
|
if our == nil {
|
||||||
return nil
|
return nil
|
||||||
@@ -299,20 +330,12 @@ func (our *Input) GetPet(id uint32) (ii *info.BattlePetEntity, Reason info.Chang
|
|||||||
// GetStatusBonus 获取最高的状态倍率
|
// GetStatusBonus 获取最高的状态倍率
|
||||||
// 遍历状态数组,返回存在的状态中最高的倍率(无状态则返回1.0)
|
// 遍历状态数组,返回存在的状态中最高的倍率(无状态则返回1.0)
|
||||||
func (our *Input) GetStatusBonus() float64 {
|
func (our *Input) GetStatusBonus() float64 {
|
||||||
// 异常状态倍率映射表(状态索引 -> 倍率)
|
|
||||||
var statusBonuses = map[info.EnumPetStatus]float64{
|
|
||||||
info.PetStatus.Paralysis: 1.5,
|
|
||||||
info.PetStatus.Poisoned: 1.5,
|
|
||||||
info.PetStatus.Sleep: 2.0,
|
|
||||||
// /info.BattleStatus.Frozen: 2.0,
|
|
||||||
}
|
|
||||||
maxBonus := 1.0 // 默认无状态倍率
|
maxBonus := 1.0 // 默认无状态倍率
|
||||||
|
|
||||||
for statusIdx := 0; statusIdx < 20; statusIdx++ {
|
for statusIdx := 0; statusIdx < 20; statusIdx++ {
|
||||||
t := our.InitEffect(EffectType.Status, statusIdx)
|
t := our.GetEffect(EffectType.Status, statusIdx)
|
||||||
|
|
||||||
// 检查状态是否存在(数组中值为1表示存在该状态)
|
if t != nil && t.Alive() {
|
||||||
if t != nil && t.Stack() > 0 {
|
|
||||||
if bonus, exists := statusBonuses[info.EnumPetStatus(statusIdx)]; exists && bonus > maxBonus {
|
if bonus, exists := statusBonuses[info.EnumPetStatus(statusIdx)]; exists && bonus > maxBonus {
|
||||||
maxBonus = bonus
|
maxBonus = bonus
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,41 @@
|
|||||||
package input
|
package input
|
||||||
|
|
||||||
|
import "github.com/gogf/gf/v2/util/grand"
|
||||||
|
|
||||||
|
func compactSlots(slots []*Input) []*Input {
|
||||||
|
if len(slots) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
ret := make([]*Input, 0, len(slots))
|
||||||
|
for _, slot := range slots {
|
||||||
|
if slot == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ret = append(ret, slot)
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
func uniqueControllerCount(slots []*Input) int {
|
||||||
|
if len(slots) == 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
seen := make(map[uint32]struct{}, len(slots))
|
||||||
|
count := 0
|
||||||
|
for _, slot := range slots {
|
||||||
|
if slot == nil || slot.Player == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
userID := slot.Player.GetInfo().UserID
|
||||||
|
if _, ok := seen[userID]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[userID] = struct{}{}
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
|
||||||
// TeamSlots 返回当前输入所在阵营的全部有效战斗位视图。
|
// TeamSlots 返回当前输入所在阵营的全部有效战斗位视图。
|
||||||
func (our *Input) TeamSlots() []*Input {
|
func (our *Input) TeamSlots() []*Input {
|
||||||
if our == nil {
|
if our == nil {
|
||||||
@@ -8,14 +44,20 @@ func (our *Input) TeamSlots() []*Input {
|
|||||||
if len(our.Team) == 0 {
|
if len(our.Team) == 0 {
|
||||||
return []*Input{our}
|
return []*Input{our}
|
||||||
}
|
}
|
||||||
slots := make([]*Input, 0, len(our.Team))
|
return compactSlots(our.Team)
|
||||||
for _, teammate := range our.Team {
|
}
|
||||||
if teammate == nil {
|
|
||||||
continue
|
// TeamSlotIndex 返回当前输入在本阵营中的原始站位下标。
|
||||||
|
func (our *Input) TeamSlotIndex() int {
|
||||||
|
if our == nil || len(our.Team) == 0 {
|
||||||
|
return 0
|
||||||
}
|
}
|
||||||
slots = append(slots, teammate)
|
for idx, teammate := range our.Team {
|
||||||
|
if teammate == our {
|
||||||
|
return idx
|
||||||
}
|
}
|
||||||
return slots
|
}
|
||||||
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// Teammates 返回队友列表,不包含自己。
|
// Teammates 返回队友列表,不包含自己。
|
||||||
@@ -53,3 +95,158 @@ func (our *Input) LivingTeammates() []*Input {
|
|||||||
func (our *Input) HasLivingTeammate() bool {
|
func (our *Input) HasLivingTeammate() bool {
|
||||||
return len(our.LivingTeammates()) > 0
|
return len(our.LivingTeammates()) > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TeamInputCount 返回当前阵营有效 input 数量。
|
||||||
|
func (our *Input) TeamInputCount() int {
|
||||||
|
return len(our.TeamSlots())
|
||||||
|
}
|
||||||
|
|
||||||
|
// OpponentInputCount 返回敌方阵营有效 input 数量。
|
||||||
|
func (our *Input) OpponentInputCount() int {
|
||||||
|
if our == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
if len(our.OppTeam) == 0 {
|
||||||
|
if our.Opp != nil {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return len(compactSlots(our.OppTeam))
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsMultiInputSide 判断当前阵营是否为多 input。
|
||||||
|
func (our *Input) IsMultiInputSide() bool {
|
||||||
|
return our.TeamInputCount() > 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsMultiInputBattle 判断当前战斗是否包含多 input 站位。
|
||||||
|
func (our *Input) IsMultiInputBattle() bool {
|
||||||
|
if our == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return our.TeamInputCount() > 1 || our.OpponentInputCount() > 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// TeamControllerCount 返回当前阵营实际操作者数量。
|
||||||
|
func (our *Input) TeamControllerCount() int {
|
||||||
|
if our == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return uniqueControllerCount(our.TeamSlots())
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsSingleControllerMultiInputSide 判断当前阵营是否为“单人控制的多 input”。
|
||||||
|
func (our *Input) IsSingleControllerMultiInputSide() bool {
|
||||||
|
return our.IsMultiInputSide() && our.TeamControllerCount() <= 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// TeamSlotAt 返回指定己方站位。
|
||||||
|
func (our *Input) TeamSlotAt(index int) *Input {
|
||||||
|
if our == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if index >= 0 && index < len(our.Team) {
|
||||||
|
return our.Team[index]
|
||||||
|
}
|
||||||
|
if index == 0 {
|
||||||
|
return our
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// OpponentSlotAt 返回指定敌方站位。
|
||||||
|
func (our *Input) OpponentSlotAt(index int) *Input {
|
||||||
|
if our == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if index >= 0 && index < len(our.OppTeam) {
|
||||||
|
return our.OppTeam[index]
|
||||||
|
}
|
||||||
|
if index == 0 {
|
||||||
|
return our.Opp
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func nextLivingSlotIndex(slots []*Input, start int) int {
|
||||||
|
if len(slots) == 0 {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
if start < 0 {
|
||||||
|
start = 0
|
||||||
|
}
|
||||||
|
if start >= len(slots) {
|
||||||
|
start = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
availableIndex := -1
|
||||||
|
for offset := 0; offset < len(slots); offset++ {
|
||||||
|
idx := (start + offset) % len(slots)
|
||||||
|
slot := slots[idx]
|
||||||
|
if slot == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if availableIndex < 0 {
|
||||||
|
availableIndex = idx
|
||||||
|
}
|
||||||
|
current := slot.CurrentPet()
|
||||||
|
if current != nil && current.Info.Hp > 0 {
|
||||||
|
return idx
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return availableIndex
|
||||||
|
}
|
||||||
|
|
||||||
|
// OpponentSlotAtOrNextLiving 返回指定敌方站位;若该目标已死亡,则顺延到下一只存活精灵。
|
||||||
|
func (our *Input) OpponentSlotAtOrNextLiving(index int) (*Input, int) {
|
||||||
|
if our == nil {
|
||||||
|
return nil, -1
|
||||||
|
}
|
||||||
|
if len(our.OppTeam) == 0 {
|
||||||
|
return our.OpponentSlotAt(index), index
|
||||||
|
}
|
||||||
|
resolvedIndex := nextLivingSlotIndex(our.OppTeam, index)
|
||||||
|
if resolvedIndex < 0 {
|
||||||
|
return nil, -1
|
||||||
|
}
|
||||||
|
return our.OppTeam[resolvedIndex], resolvedIndex
|
||||||
|
}
|
||||||
|
|
||||||
|
// RandomOpponentSlotIndex 返回一个可用的敌方站位下标,优先从存活站位中随机。
|
||||||
|
func (our *Input) RandomOpponentSlotIndex() int {
|
||||||
|
if our == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
if len(our.OppTeam) == 0 {
|
||||||
|
if our.Opp != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
living := make([]int, 0, len(our.OppTeam))
|
||||||
|
available := make([]int, 0, len(our.OppTeam))
|
||||||
|
for idx, opponent := range our.OppTeam {
|
||||||
|
if opponent == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
available = append(available, idx)
|
||||||
|
current := opponent.CurrentPet()
|
||||||
|
if current != nil && current.Info.Hp > 0 {
|
||||||
|
living = append(living, idx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
candidates := living
|
||||||
|
if len(candidates) == 0 {
|
||||||
|
candidates = available
|
||||||
|
}
|
||||||
|
if len(candidates) == 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
if len(candidates) == 1 {
|
||||||
|
return candidates[0]
|
||||||
|
}
|
||||||
|
return candidates[grand.Intn(len(candidates))]
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,10 +3,32 @@ package input
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"blazing/common/socket/errorcode"
|
||||||
|
"blazing/logic/service/common"
|
||||||
fightinfo "blazing/logic/service/fight/info"
|
fightinfo "blazing/logic/service/fight/info"
|
||||||
|
spaceinfo "blazing/logic/service/space/info"
|
||||||
"blazing/modules/player/model"
|
"blazing/modules/player/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type teamTestPlayer struct {
|
||||||
|
info model.PlayerInfo
|
||||||
|
fightInfo fightinfo.Fightinfo
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *teamTestPlayer) ApplyPetDisplayInfo(*spaceinfo.SimpleInfo) {}
|
||||||
|
func (p *teamTestPlayer) GetPlayerCaptureContext() *fightinfo.PlayerCaptureContext { return nil }
|
||||||
|
func (p *teamTestPlayer) Roll(int, int) (bool, float64, float64) { return false, 0, 0 }
|
||||||
|
func (p *teamTestPlayer) Getfightinfo() fightinfo.Fightinfo { return p.fightInfo }
|
||||||
|
func (p *teamTestPlayer) ItemAdd(int64, int64) bool { return false }
|
||||||
|
func (p *teamTestPlayer) GetInfo() *model.PlayerInfo { return &p.info }
|
||||||
|
func (p *teamTestPlayer) InvitePlayer(common.PlayerI) {}
|
||||||
|
func (p *teamTestPlayer) SetFightC(common.FightI) {}
|
||||||
|
func (p *teamTestPlayer) QuitFight() {}
|
||||||
|
func (p *teamTestPlayer) MessWin(bool) {}
|
||||||
|
func (p *teamTestPlayer) CanFight() errorcode.ErrorCode { return 0 }
|
||||||
|
func (p *teamTestPlayer) SendPackCmd(uint32, any) {}
|
||||||
|
func (p *teamTestPlayer) GetPetInfo(uint32) []model.PetInfo { return nil }
|
||||||
|
|
||||||
func TestLivingTeammatesFiltersSelfAndDeadSlots(t *testing.T) {
|
func TestLivingTeammatesFiltersSelfAndDeadSlots(t *testing.T) {
|
||||||
owner := &Input{CurPet: []*fightinfo.BattlePetEntity{{Info: model.PetInfo{Hp: 10}}}}
|
owner := &Input{CurPet: []*fightinfo.BattlePetEntity{{Info: model.PetInfo{Hp: 10}}}}
|
||||||
aliveMate := &Input{CurPet: []*fightinfo.BattlePetEntity{{Info: model.PetInfo{Hp: 5}}}}
|
aliveMate := &Input{CurPet: []*fightinfo.BattlePetEntity{{Info: model.PetInfo{Hp: 5}}}}
|
||||||
@@ -28,3 +50,68 @@ func TestLivingTeammatesFiltersSelfAndDeadSlots(t *testing.T) {
|
|||||||
t.Fatalf("expected owner to detect living teammate")
|
t.Fatalf("expected owner to detect living teammate")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestTeamSlotIndexKeepsOriginalSlot(t *testing.T) {
|
||||||
|
first := &Input{}
|
||||||
|
second := &Input{}
|
||||||
|
third := &Input{}
|
||||||
|
|
||||||
|
team := []*Input{first, second, third}
|
||||||
|
first.Team = team
|
||||||
|
second.Team = team
|
||||||
|
third.Team = team
|
||||||
|
|
||||||
|
if got := third.TeamSlotIndex(); got != 2 {
|
||||||
|
t.Fatalf("expected slot index 2, got %d", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRandomOpponentSlotIndexPrefersLivingTarget(t *testing.T) {
|
||||||
|
owner := &Input{}
|
||||||
|
deadOpp := &Input{CurPet: []*fightinfo.BattlePetEntity{{Info: model.PetInfo{Hp: 0}}}}
|
||||||
|
liveOpp := &Input{CurPet: []*fightinfo.BattlePetEntity{{Info: model.PetInfo{Hp: 12}}}}
|
||||||
|
|
||||||
|
owner.OppTeam = []*Input{deadOpp, liveOpp}
|
||||||
|
|
||||||
|
if got := owner.RandomOpponentSlotIndex(); got != 1 {
|
||||||
|
t.Fatalf("expected living opponent slot 1, got %d", got)
|
||||||
|
}
|
||||||
|
if got := owner.OpponentSlotAt(1); got != liveOpp {
|
||||||
|
t.Fatalf("expected opponent slot 1 to return live opponent")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInputModeHelpers(t *testing.T) {
|
||||||
|
playerA := &teamTestPlayer{info: model.PlayerInfo{UserID: 1001}}
|
||||||
|
playerB := &teamTestPlayer{info: model.PlayerInfo{UserID: 1002}}
|
||||||
|
|
||||||
|
solo := &Input{}
|
||||||
|
soloMate := &Input{}
|
||||||
|
groupMate := &Input{}
|
||||||
|
|
||||||
|
solo.Player = playerA
|
||||||
|
soloMate.Player = playerA
|
||||||
|
groupMate.Player = playerB
|
||||||
|
|
||||||
|
solo.Team = []*Input{solo}
|
||||||
|
solo.OppTeam = []*Input{{}}
|
||||||
|
if solo.IsMultiInputBattle() {
|
||||||
|
t.Fatalf("expected single-input battle to be false")
|
||||||
|
}
|
||||||
|
|
||||||
|
solo.Team = []*Input{solo, soloMate}
|
||||||
|
if !solo.IsMultiInputBattle() {
|
||||||
|
t.Fatalf("expected multi-input battle to be true")
|
||||||
|
}
|
||||||
|
if !solo.IsSingleControllerMultiInputSide() {
|
||||||
|
t.Fatalf("expected same-controller double side to be single-controller multi-input")
|
||||||
|
}
|
||||||
|
|
||||||
|
solo.Team = []*Input{solo, groupMate}
|
||||||
|
if solo.TeamControllerCount() != 2 {
|
||||||
|
t.Fatalf("expected group side controller count 2, got %d", solo.TeamControllerCount())
|
||||||
|
}
|
||||||
|
if solo.IsSingleControllerMultiInputSide() {
|
||||||
|
t.Fatalf("expected grouped side not to be single-controller multi-input")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"blazing/cool"
|
"blazing/cool"
|
||||||
"blazing/modules/player/model"
|
"blazing/modules/player/model"
|
||||||
"context"
|
"context"
|
||||||
|
"runtime/debug"
|
||||||
"sort"
|
"sort"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
|
|
||||||
@@ -71,6 +72,27 @@ func (f *FightC) battleLoop() {
|
|||||||
|
|
||||||
f.Round++
|
f.Round++
|
||||||
|
|
||||||
|
if !f.sideHasActionableSlots(SideOur) {
|
||||||
|
if player := f.primaryOppPlayer(); player != nil {
|
||||||
|
f.WinnerId = player.GetInfo().UserID
|
||||||
|
}
|
||||||
|
f.Reason = normalizeFightOverReason(model.BattleOverReason.DefaultEnd)
|
||||||
|
f.FightOverInfo.WinnerId = f.WinnerId
|
||||||
|
f.FightOverInfo.Reason = f.Reason
|
||||||
|
f.closefight = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if !f.sideHasActionableSlots(SideOpp) {
|
||||||
|
if player := f.primaryOurPlayer(); player != nil {
|
||||||
|
f.WinnerId = player.GetInfo().UserID
|
||||||
|
}
|
||||||
|
f.Reason = normalizeFightOverReason(model.BattleOverReason.DefaultEnd)
|
||||||
|
f.FightOverInfo.WinnerId = f.WinnerId
|
||||||
|
f.FightOverInfo.Reason = f.Reason
|
||||||
|
f.closefight = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
expectedSlots := f.expectedActionSlots()
|
expectedSlots := f.expectedActionSlots()
|
||||||
actions := f.collectPlayerActions(expectedSlots)
|
actions := f.collectPlayerActions(expectedSlots)
|
||||||
if f.closefight {
|
if f.closefight {
|
||||||
@@ -151,11 +173,14 @@ func (f *FightC) battleLoop() {
|
|||||||
|
|
||||||
//大乱斗,给个延迟
|
//大乱斗,给个延迟
|
||||||
//<-time.After(1000)
|
//<-time.After(1000)
|
||||||
f.Broadcast(func(ff *input.Input) {
|
f.BroadcastPlayers(func(p common.PlayerI) {
|
||||||
|
if f.LegacyGroupProtocol {
|
||||||
|
f.sendLegacyGroupOver(p, &f.FightOverInfo)
|
||||||
|
} else {
|
||||||
|
f.sendFightPacket(p, fightPacketOver, buildFightOverPayload(f.FightOverInfo))
|
||||||
|
}
|
||||||
|
|
||||||
ff.Player.SendPackCmd(2506, &f.FightOverInfo)
|
p.QuitFight()
|
||||||
|
|
||||||
ff.Player.QuitFight()
|
|
||||||
|
|
||||||
//待退出玩家战斗状态
|
//待退出玩家战斗状态
|
||||||
})
|
})
|
||||||
@@ -179,7 +204,7 @@ func (f *FightC) collectPlayerActions(expectedSlots map[actionSlotKey]struct{})
|
|||||||
defer f.closeActionWindow()
|
defer f.closeActionWindow()
|
||||||
|
|
||||||
if f.Info.Status == info.BattleMode.FIGHT_WITH_NPC {
|
if f.Info.Status == info.BattleMode.FIGHT_WITH_NPC {
|
||||||
go f.Opp[0].GetAction()
|
f.triggerNPCActions()
|
||||||
}
|
}
|
||||||
|
|
||||||
waitr := time.Duration(f.waittime)*time.Millisecond*10 + 30*time.Second
|
waitr := time.Duration(f.waittime)*time.Millisecond*10 + 30*time.Second
|
||||||
@@ -239,7 +264,11 @@ func (f *FightC) collectPlayerActions(expectedSlots map[actionSlotKey]struct{})
|
|||||||
ret.Reason = reason
|
ret.Reason = reason
|
||||||
ret.Reason.ActorIndex = uint32(ret.ActorIndex)
|
ret.Reason.ActorIndex = uint32(ret.ActorIndex)
|
||||||
|
|
||||||
selfinput.Player.SendPackCmd(2407, &ret.Reason)
|
if f.LegacyGroupProtocol {
|
||||||
|
f.sendLegacyGroupChangePetSuccess(selfinput.Player, selfinput, &ret.Reason)
|
||||||
|
} else {
|
||||||
|
f.sendFightPacket(selfinput.Player, fightPacketChangePetSuccess, &ret.Reason)
|
||||||
|
}
|
||||||
|
|
||||||
f.Switch[key] = ret
|
f.Switch[key] = ret
|
||||||
|
|
||||||
@@ -260,9 +289,13 @@ func (f *FightC) collectPlayerActions(expectedSlots map[actionSlotKey]struct{})
|
|||||||
selfinput.CanChange = 0
|
selfinput.CanChange = 0
|
||||||
if f.Info.Status == info.BattleMode.FIGHT_WITH_NPC && paction.GetPlayerID() == 0 {
|
if f.Info.Status == info.BattleMode.FIGHT_WITH_NPC && paction.GetPlayerID() == 0 {
|
||||||
f.Switch = make(map[actionSlotKey]*action.ActiveSwitchAction)
|
f.Switch = make(map[actionSlotKey]*action.ActiveSwitchAction)
|
||||||
f.Our[0].Player.SendPackCmd(2407, &ret.Reason)
|
if f.LegacyGroupProtocol {
|
||||||
|
f.sendLegacyGroupChangePetSuccess(f.Our[0].Player, selfinput, &ret.Reason)
|
||||||
|
} else {
|
||||||
|
f.sendFightPacket(f.Our[0].Player, fightPacketChangePetSuccess, &ret.Reason)
|
||||||
|
}
|
||||||
//println("AI出手死切")
|
//println("AI出手死切")
|
||||||
go f.Opp[0].GetAction() //boss出手后获取出招
|
f.triggerNPCActions() // boss出手后获取出招
|
||||||
|
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
@@ -311,7 +344,11 @@ func (f *FightC) handleTimeout(expectedSlots map[actionSlotKey]struct{}, actions
|
|||||||
}
|
}
|
||||||
player := f.getPlayerByID(key.PlayerID)
|
player := f.getPlayerByID(key.PlayerID)
|
||||||
if player != nil {
|
if player != nil {
|
||||||
f.UseSkillAt(player, 0, key.ActorIndex, 0)
|
targetIndex := 0
|
||||||
|
if self := f.getInputByUserID(key.PlayerID, key.ActorIndex, false); self != nil {
|
||||||
|
targetIndex = self.RandomOpponentSlotIndex()
|
||||||
|
}
|
||||||
|
f.UseSkillAt(player, 0, key.ActorIndex, targetIndex)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
@@ -353,6 +390,22 @@ func (f *FightC) handleTimeout(expectedSlots map[actionSlotKey]struct{}, actions
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *FightC) triggerNPCActions() {
|
||||||
|
for slot, opponent := range f.Opp {
|
||||||
|
if opponent == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
go func(slot int, opponent *input.Input) {
|
||||||
|
defer func() {
|
||||||
|
if err := recover(); err != nil {
|
||||||
|
cool.Logger.Error(context.Background(), "fight npc action panic", f.ownerID, slot, err, string(debug.Stack()))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
opponent.GetAction()
|
||||||
|
}(slot, opponent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func flattenActionMap(actions map[actionSlotKey]action.BattleActionI) []action.BattleActionI {
|
func flattenActionMap(actions map[actionSlotKey]action.BattleActionI) []action.BattleActionI {
|
||||||
flattened := make([]action.BattleActionI, 0, len(actions))
|
flattened := make([]action.BattleActionI, 0, len(actions))
|
||||||
for _, act := range actions {
|
for _, act := range actions {
|
||||||
@@ -556,12 +609,12 @@ func (f *FightC) handleItemAction(a *action.UseItemAction) {
|
|||||||
case gconv.Int(item.HP) != 0:
|
case gconv.Int(item.HP) != 0:
|
||||||
addhp := item.HP
|
addhp := item.HP
|
||||||
source.Heal(source, a, alpacadecimal.NewFromInt(int64(addhp)))
|
source.Heal(source, a, alpacadecimal.NewFromInt(int64(addhp)))
|
||||||
f.Broadcast(func(ff *input.Input) {
|
f.BroadcastPlayers(func(p common.PlayerI) {
|
||||||
currentPet := source.PrimaryCurPet()
|
currentPet := source.PrimaryCurPet()
|
||||||
if currentPet == nil {
|
if currentPet == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ff.Player.SendPackCmd(2406, &info.UsePetIteminfo{
|
f.sendFightPacket(p, fightPacketUseItem, &info.UsePetIteminfo{
|
||||||
UserID: source.UserID,
|
UserID: source.UserID,
|
||||||
ChangeHp: int32(addhp),
|
ChangeHp: int32(addhp),
|
||||||
ItemID: uint32(item.ID),
|
ItemID: uint32(item.ID),
|
||||||
@@ -571,12 +624,12 @@ func (f *FightC) handleItemAction(a *action.UseItemAction) {
|
|||||||
})
|
})
|
||||||
case gconv.Int(item.PP) != 0:
|
case gconv.Int(item.PP) != 0:
|
||||||
source.HealPP(item.PP)
|
source.HealPP(item.PP)
|
||||||
f.Broadcast(func(ff *input.Input) {
|
f.BroadcastPlayers(func(p common.PlayerI) {
|
||||||
currentPet := source.PrimaryCurPet()
|
currentPet := source.PrimaryCurPet()
|
||||||
if currentPet == nil {
|
if currentPet == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ff.Player.SendPackCmd(2406, &info.UsePetIteminfo{
|
f.sendFightPacket(p, fightPacketUseItem, &info.UsePetIteminfo{
|
||||||
UserID: source.UserID,
|
UserID: source.UserID,
|
||||||
|
|
||||||
ItemID: uint32(item.ID),
|
ItemID: uint32(item.ID),
|
||||||
@@ -599,7 +652,7 @@ func (f *FightC) handleSkillActions(a1, a2 action.BattleActionI) {
|
|||||||
|
|
||||||
switch {
|
switch {
|
||||||
case s1 == nil || s1.SkillEntity == nil:
|
case s1 == nil || s1.SkillEntity == nil:
|
||||||
if s2.SkillEntity != nil {
|
if s2 != nil && s2.SkillEntity != nil {
|
||||||
if s2.XML.CD != nil {
|
if s2.XML.CD != nil {
|
||||||
f.waittime = *s2.XML.CD
|
f.waittime = *s2.XML.CD
|
||||||
}
|
}
|
||||||
@@ -608,7 +661,7 @@ func (f *FightC) handleSkillActions(a1, a2 action.BattleActionI) {
|
|||||||
f.enterturn(s2, nil)
|
f.enterturn(s2, nil)
|
||||||
// fmt.Println("1 空过 2玩家执行技能:", s2.PlayerID, s2.Info.ID)
|
// fmt.Println("1 空过 2玩家执行技能:", s2.PlayerID, s2.Info.ID)
|
||||||
case s2 == nil || s2.SkillEntity == nil:
|
case s2 == nil || s2.SkillEntity == nil:
|
||||||
if s1.SkillEntity != nil {
|
if s1 != nil && s1.SkillEntity != nil {
|
||||||
if s1.XML.CD != nil {
|
if s1.XML.CD != nil {
|
||||||
f.waittime = *s1.XML.CD
|
f.waittime = *s1.XML.CD
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,6 +47,153 @@ func NewFightSingleControllerN(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ArrangePetsBySlotLimit 按站位上限切分宠物。
|
||||||
|
// 规则:
|
||||||
|
// 1. 前 slotLimit 只存活宠物优先占据出战位。
|
||||||
|
// 2. 其余宠物按 1..slotLimit 轮转挂到对应站位作为后备。
|
||||||
|
// 3. 每个站位最多保留 6 只宠物。
|
||||||
|
func ArrangePetsBySlotLimit(pets []model.PetInfo, slotLimit int) [][]model.PetInfo {
|
||||||
|
var (
|
||||||
|
alivePets []model.PetInfo
|
||||||
|
slots [][]model.PetInfo
|
||||||
|
idx int
|
||||||
|
)
|
||||||
|
for _, pet := range pets {
|
||||||
|
if pet.Hp == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
alivePets = append(alivePets, pet)
|
||||||
|
}
|
||||||
|
if len(alivePets) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if slotLimit <= 0 {
|
||||||
|
slotLimit = 1
|
||||||
|
}
|
||||||
|
if slotLimit > len(alivePets) {
|
||||||
|
slotLimit = len(alivePets)
|
||||||
|
}
|
||||||
|
slots = make([][]model.PetInfo, 0, slotLimit)
|
||||||
|
for i := 0; i < slotLimit; i++ {
|
||||||
|
slots = append(slots, []model.PetInfo{alivePets[i]})
|
||||||
|
}
|
||||||
|
for _, pet := range alivePets[slotLimit:] {
|
||||||
|
for step := 0; step < len(slots); step++ {
|
||||||
|
slotIdx := (idx + step) % len(slots)
|
||||||
|
if len(slots[slotIdx]) >= 6 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
slots[slotIdx] = append(slots[slotIdx], pet)
|
||||||
|
idx = (slotIdx + 1) % len(slots)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return slots
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExpandPlayersWithSlotLimit 将“每位玩家的宠物列表”按站位限制展开为扁平站位列表。
|
||||||
|
// 例如:
|
||||||
|
// 1. slotLimit=1: 每位玩家占 1 个站位,其余为该站位后备。
|
||||||
|
// 2. slotLimit=3: 每位玩家最多展开 3 个站位,每个站位带各自后备。
|
||||||
|
func ExpandPlayersWithSlotLimit(
|
||||||
|
players []common.PlayerI,
|
||||||
|
petsByPlayer [][]model.PetInfo,
|
||||||
|
slotLimit int,
|
||||||
|
) ([]common.PlayerI, [][]model.PetInfo, errorcode.ErrorCode) {
|
||||||
|
var (
|
||||||
|
flatPlayers []common.PlayerI
|
||||||
|
flatSlots [][]model.PetInfo
|
||||||
|
)
|
||||||
|
if len(players) == 0 || len(players) != len(petsByPlayer) {
|
||||||
|
return nil, nil, errorcode.ErrorCodes.ErrSystemBusyTryLater
|
||||||
|
}
|
||||||
|
for idx, p := range players {
|
||||||
|
if p == nil {
|
||||||
|
return nil, nil, errorcode.ErrorCodes.ErrSystemBusyTryLater
|
||||||
|
}
|
||||||
|
slots := ArrangePetsBySlotLimit(petsByPlayer[idx], slotLimit)
|
||||||
|
for _, slotPets := range slots {
|
||||||
|
flatPlayers = append(flatPlayers, p)
|
||||||
|
flatSlots = append(flatSlots, slotPets)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(flatPlayers) == 0 || len(flatPlayers) != len(flatSlots) {
|
||||||
|
return nil, nil, errorcode.ErrorCodes.ErrSystemBusyTryLater
|
||||||
|
}
|
||||||
|
return flatPlayers, flatSlots, 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewFightSingleController 使用站位限制规则创建单人控制多站位战斗。
|
||||||
|
func NewFightSingleController(
|
||||||
|
ourController common.PlayerI,
|
||||||
|
oppController common.PlayerI,
|
||||||
|
ourPets []model.PetInfo,
|
||||||
|
oppPets []model.PetInfo,
|
||||||
|
slotLimit int,
|
||||||
|
fn func(model.FightOverInfo),
|
||||||
|
) (*FightC, errorcode.ErrorCode) {
|
||||||
|
return NewFightSingleControllerN(
|
||||||
|
ourController,
|
||||||
|
oppController,
|
||||||
|
ArrangePetsBySlotLimit(ourPets, slotLimit),
|
||||||
|
ArrangePetsBySlotLimit(oppPets, slotLimit),
|
||||||
|
fn,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewLegacyGroupFightSingleControllerN 创建旧组队协议的单人控制多站位战斗。
|
||||||
|
func NewLegacyGroupFightSingleControllerN(
|
||||||
|
ourController common.PlayerI,
|
||||||
|
oppController common.PlayerI,
|
||||||
|
ourPetsBySlot [][]model.PetInfo,
|
||||||
|
oppPetsBySlot [][]model.PetInfo,
|
||||||
|
fn func(model.FightOverInfo),
|
||||||
|
) (*FightC, errorcode.ErrorCode) {
|
||||||
|
if ourController == nil || oppController == nil {
|
||||||
|
return nil, errorcode.ErrorCodes.ErrSystemBusyTryLater
|
||||||
|
}
|
||||||
|
fightInfo := ourController.Getfightinfo()
|
||||||
|
|
||||||
|
ourInputs, err := buildSideInputsByController(ourController, ourPetsBySlot, fightInfo.Mode)
|
||||||
|
if err > 0 {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
oppInputs, err := buildSideInputsByController(oppController, oppPetsBySlot, fightInfo.Mode)
|
||||||
|
if err > 0 {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return NewFightWithOptions(
|
||||||
|
WithFightInputs(ourInputs, oppInputs),
|
||||||
|
WithFightPlayersOnSide(
|
||||||
|
[]common.PlayerI{ourController},
|
||||||
|
[]common.PlayerI{oppController},
|
||||||
|
),
|
||||||
|
WithInputControllerBinding(InputControllerBindingSingle),
|
||||||
|
WithLegacyGroupProtocol(true),
|
||||||
|
WithFightCallback(fn),
|
||||||
|
WithFightInfo(fightInfo),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewLegacyGroupFightSingleController 使用站位限制规则创建旧组队协议战斗。
|
||||||
|
func NewLegacyGroupFightSingleController(
|
||||||
|
ourController common.PlayerI,
|
||||||
|
oppController common.PlayerI,
|
||||||
|
ourPets []model.PetInfo,
|
||||||
|
oppPets []model.PetInfo,
|
||||||
|
slotLimit int,
|
||||||
|
fn func(model.FightOverInfo),
|
||||||
|
) (*FightC, errorcode.ErrorCode) {
|
||||||
|
return NewLegacyGroupFightSingleControllerN(
|
||||||
|
ourController,
|
||||||
|
oppController,
|
||||||
|
ArrangePetsBySlotLimit(ourPets, slotLimit),
|
||||||
|
ArrangePetsBySlotLimit(oppPets, slotLimit),
|
||||||
|
fn,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// NewFightPerSlotControllerN 创建 N 打战斗(多人各控制一个站位)。
|
// NewFightPerSlotControllerN 创建 N 打战斗(多人各控制一个站位)。
|
||||||
// ourPlayers/oppPlayers 与 ourPetsBySlot/oppPetsBySlot 按站位一一对应。
|
// ourPlayers/oppPlayers 与 ourPetsBySlot/oppPetsBySlot 按站位一一对应。
|
||||||
func NewFightPerSlotControllerN(
|
func NewFightPerSlotControllerN(
|
||||||
@@ -82,25 +229,32 @@ func NewFightPerSlotControllerN(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewFightPerPlayerControllers 使用“每位玩家 + 站位限制”创建多人战斗。
|
||||||
|
func NewFightPerPlayerControllers(
|
||||||
|
ourPlayers []common.PlayerI,
|
||||||
|
oppPlayers []common.PlayerI,
|
||||||
|
ourPetsByPlayer [][]model.PetInfo,
|
||||||
|
oppPetsByPlayer [][]model.PetInfo,
|
||||||
|
slotLimit int,
|
||||||
|
fn func(model.FightOverInfo),
|
||||||
|
) (*FightC, errorcode.ErrorCode) {
|
||||||
|
flatOurPlayers, flatOurSlots, err := ExpandPlayersWithSlotLimit(ourPlayers, ourPetsByPlayer, slotLimit)
|
||||||
|
if err > 0 {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
flatOppPlayers, flatOppSlots, err := ExpandPlayersWithSlotLimit(oppPlayers, oppPetsByPlayer, slotLimit)
|
||||||
|
if err > 0 {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return NewFightPerSlotControllerN(flatOurPlayers, flatOppPlayers, flatOurSlots, flatOppSlots, fn)
|
||||||
|
}
|
||||||
|
|
||||||
// 创建新战斗,邀请方和被邀请方,或者玩家和野怪方
|
// 创建新战斗,邀请方和被邀请方,或者玩家和野怪方
|
||||||
func NewFight(p1, p2 common.PlayerI, b1, b2 []model.PetInfo, fn func(model.FightOverInfo)) (*FightC, errorcode.ErrorCode) {
|
func NewFight(p1, p2 common.PlayerI, b1, b2 []model.PetInfo, fn func(model.FightOverInfo)) (*FightC, errorcode.ErrorCode) {
|
||||||
if p1 == nil || p2 == nil {
|
if p1 == nil || p2 == nil {
|
||||||
return nil, errorcode.ErrorCodes.ErrSystemBusyTryLater
|
return nil, errorcode.ErrorCodes.ErrSystemBusyTryLater
|
||||||
}
|
}
|
||||||
fightInfo := p1.Getfightinfo()
|
return NewFightSingleController(p1, p2, b1, b2, 1, fn)
|
||||||
ourInput, err := buildInputFromPets(p1, b1, fightInfo.Mode)
|
|
||||||
if err > 0 {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
oppInput, err := buildInputFromPets(p2, b2, fightInfo.Mode)
|
|
||||||
if err > 0 {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return NewFightWithOptions(
|
|
||||||
WithFightInputs([]*input.Input{ourInput}, []*input.Input{oppInput}),
|
|
||||||
WithFightCallback(fn),
|
|
||||||
WithFightInfo(fightInfo),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// buildFight 基于已准备好的双方 Inputs 构建战斗实例。
|
// buildFight 基于已准备好的双方 Inputs 构建战斗实例。
|
||||||
@@ -116,6 +270,7 @@ func buildFight(opts *fightBuildOptions) (*FightC, errorcode.ErrorCode) {
|
|||||||
|
|
||||||
f := &FightC{}
|
f := &FightC{}
|
||||||
f.ownerID = opts.owner.GetInfo().UserID
|
f.ownerID = opts.owner.GetInfo().UserID
|
||||||
|
f.LegacyGroupProtocol = opts.legacyGroupProtocol
|
||||||
f.OurPlayers = opts.ourPlayers
|
f.OurPlayers = opts.ourPlayers
|
||||||
f.OppPlayers = opts.oppPlayers
|
f.OppPlayers = opts.oppPlayers
|
||||||
f.Switch = make(map[actionSlotKey]*action.ActiveSwitchAction)
|
f.Switch = make(map[actionSlotKey]*action.ActiveSwitchAction)
|
||||||
@@ -142,9 +297,6 @@ func buildFight(opts *fightBuildOptions) (*FightC, errorcode.ErrorCode) {
|
|||||||
f.bindInputFightContext(f.Our, f.Opp)
|
f.bindInputFightContext(f.Our, f.Opp)
|
||||||
f.linkTeamViews()
|
f.linkTeamViews()
|
||||||
|
|
||||||
f.ReadyInfo.OurInfo, f.ReadyInfo.OurPetList = initfightready(f.primaryOur())
|
|
||||||
f.ReadyInfo.OpponentInfo, f.ReadyInfo.OpponentPetList = initfightready(f.primaryOpp())
|
|
||||||
|
|
||||||
loadtime := 120 * time.Second
|
loadtime := 120 * time.Second
|
||||||
if f.Info.Status == info.BattleMode.FIGHT_WITH_NPC {
|
if f.Info.Status == info.BattleMode.FIGHT_WITH_NPC {
|
||||||
if opp := f.primaryOpp(); opp != nil {
|
if opp := f.primaryOpp(); opp != nil {
|
||||||
@@ -158,10 +310,17 @@ func buildFight(opts *fightBuildOptions) (*FightC, errorcode.ErrorCode) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
f.ReadyInfo.OurInfo, f.ReadyInfo.OurPetList = initfightready(f.primaryOur())
|
||||||
|
f.ReadyInfo.OpponentInfo, f.ReadyInfo.OpponentPetList = initfightready(f.primaryOpp())
|
||||||
f.FightStartOutboundInfo = f.buildFightStartInfo()
|
f.FightStartOutboundInfo = f.buildFightStartInfo()
|
||||||
|
|
||||||
f.Broadcast(func(ff *input.Input) {
|
f.BroadcastPlayers(func(p common.PlayerI) {
|
||||||
ff.Player.SendPackCmd(2503, &f.ReadyInfo)
|
if f.LegacyGroupProtocol {
|
||||||
|
f.sendLegacyGroupReady(p)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
f.sendFightPacket(p, fightPacketReady, &f.ReadyInfo)
|
||||||
})
|
})
|
||||||
|
|
||||||
cool.Cron.AfterFunc(loadtime, func() {
|
cool.Cron.AfterFunc(loadtime, func() {
|
||||||
@@ -179,9 +338,13 @@ func buildFight(opts *fightBuildOptions) (*FightC, errorcode.ErrorCode) {
|
|||||||
case !our.Finished:
|
case !our.Finished:
|
||||||
f.WinnerId = opp.Player.GetInfo().UserID
|
f.WinnerId = opp.Player.GetInfo().UserID
|
||||||
}
|
}
|
||||||
f.Broadcast(func(ff *input.Input) {
|
f.BroadcastPlayers(func(p common.PlayerI) {
|
||||||
ff.Player.SendPackCmd(2506, &f.FightOverInfo)
|
if f.LegacyGroupProtocol {
|
||||||
ff.Player.QuitFight()
|
f.sendLegacyGroupOver(p, &f.FightOverInfo)
|
||||||
|
} else {
|
||||||
|
f.sendFightPacket(p, fightPacketOver, buildFightOverPayload(f.FightOverInfo))
|
||||||
|
}
|
||||||
|
p.QuitFight()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ type fightBuildOptions struct {
|
|||||||
fightInfo *info.Fightinfo
|
fightInfo *info.Fightinfo
|
||||||
|
|
||||||
controllerBinding int
|
controllerBinding int
|
||||||
|
legacyGroupProtocol bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// defaultFightBuildOptions 返回建战默认参数。
|
// defaultFightBuildOptions 返回建战默认参数。
|
||||||
@@ -89,6 +90,13 @@ func WithInputControllerBinding(mode int) FightOption {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithLegacyGroupProtocol 设置是否使用旧组队协议 CMD。
|
||||||
|
func WithLegacyGroupProtocol(enabled bool) FightOption {
|
||||||
|
return func(opts *fightBuildOptions) {
|
||||||
|
opts.legacyGroupProtocol = enabled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func NewFightWithOptions(opts ...FightOption) (*FightC, errorcode.ErrorCode) {
|
func NewFightWithOptions(opts ...FightOption) (*FightC, errorcode.ErrorCode) {
|
||||||
buildOpts := defaultFightBuildOptions()
|
buildOpts := defaultFightBuildOptions()
|
||||||
for _, opt := range opts {
|
for _, opt := range opts {
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ type EffectNode struct {
|
|||||||
canStack bool // 最大叠加层数 ,正常都是不允许叠加的,除了衰弱特殊效果 ,异常和能力的叠层
|
canStack bool // 最大叠加层数 ,正常都是不允许叠加的,除了衰弱特殊效果 ,异常和能力的叠层
|
||||||
isFirst bool
|
isFirst bool
|
||||||
SideEffectArgs []int // 附加效果参数
|
SideEffectArgs []int // 附加效果参数
|
||||||
|
cachedArgs []alpacadecimal.Decimal
|
||||||
// owner bool //是否作用自身
|
// owner bool //是否作用自身
|
||||||
Success bool // 是否执行成功 成功XXX,失败XXX
|
Success bool // 是否执行成功 成功XXX,失败XXX
|
||||||
arget bool // 传出作用对象,默认0是自身,1是作用于对面
|
arget bool // 传出作用对象,默认0是自身,1是作用于对面
|
||||||
@@ -240,18 +241,22 @@ func (e *EffectNode) SetArgs(t *input.Input, a ...int) {
|
|||||||
e.Input = t
|
e.Input = t
|
||||||
if len(a) > 0 {
|
if len(a) > 0 {
|
||||||
e.SideEffectArgs = a
|
e.SideEffectArgs = a
|
||||||
|
e.cachedArgs = e.cachedArgs[:0]
|
||||||
|
for _, v := range a {
|
||||||
|
e.cachedArgs = append(e.cachedArgs, alpacadecimal.NewFromInt(int64(v)))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
func (e *EffectNode) Args() []alpacadecimal.Decimal {
|
func (e *EffectNode) Args() []alpacadecimal.Decimal {
|
||||||
var ret []alpacadecimal.Decimal
|
if len(e.cachedArgs) == len(e.SideEffectArgs) {
|
||||||
|
return e.cachedArgs
|
||||||
for _, v := range e.SideEffectArgs {
|
|
||||||
ret = append(ret, alpacadecimal.NewFromInt(int64(v)))
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
e.cachedArgs = e.cachedArgs[:0]
|
||||||
return ret
|
for _, v := range e.SideEffectArgs {
|
||||||
|
e.cachedArgs = append(e.cachedArgs, alpacadecimal.NewFromInt(int64(v)))
|
||||||
|
}
|
||||||
|
return e.cachedArgs
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ const (
|
|||||||
queueTTL = 12 * time.Second
|
queueTTL = 12 * time.Second
|
||||||
banPickTimeout = 45 * time.Second
|
banPickTimeout = 45 * time.Second
|
||||||
banPickStartCmd = 2461
|
banPickStartCmd = 2461
|
||||||
|
battleLevelCap = 100
|
||||||
)
|
)
|
||||||
|
|
||||||
type localQueueTicket struct {
|
type localQueueTicket struct {
|
||||||
@@ -91,28 +92,8 @@ func JoinPeakQueue(p *player.Player, requestedMode uint32) errorcode.ErrorCode {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
m := Default()
|
|
||||||
runtimeServerID := localRuntimeServerID()
|
|
||||||
ticket := &localQueueTicket{
|
|
||||||
playerID: p.Info.UserID,
|
|
||||||
runtimeServerID: runtimeServerID,
|
|
||||||
fightMode: fightMode,
|
|
||||||
status: status,
|
|
||||||
stop: make(chan struct{}),
|
|
||||||
}
|
|
||||||
|
|
||||||
m.mu.Lock()
|
|
||||||
if old := m.localQueues[p.Info.UserID]; old != nil {
|
|
||||||
old.Stop()
|
|
||||||
}
|
|
||||||
m.localQueues[p.Info.UserID] = ticket
|
|
||||||
delete(m.userSession, p.Info.UserID)
|
|
||||||
m.mu.Unlock()
|
|
||||||
|
|
||||||
p.Fightinfo.Mode = fightMode
|
p.Fightinfo.Mode = fightMode
|
||||||
p.Fightinfo.Status = status
|
p.Fightinfo.Status = status
|
||||||
|
|
||||||
go m.queueHeartbeatLoop(p, ticket)
|
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,15 +109,19 @@ func CancelPeakQueue(p *player.Player) {
|
|||||||
m.mu.Unlock()
|
m.mu.Unlock()
|
||||||
if ticket != nil {
|
if ticket != nil {
|
||||||
ticket.Stop()
|
ticket.Stop()
|
||||||
_ = publishServerMessage(pvpwire.CoordinatorTopicPrefix, pvpwire.MessageTypeQueueCancel, pvpwire.QueueCancelPayload{
|
|
||||||
RuntimeServerID: ticket.runtimeServerID,
|
|
||||||
UserID: ticket.playerID,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
atomic.StoreUint32(&p.Fightinfo.Mode, 0)
|
atomic.StoreUint32(&p.Fightinfo.Mode, 0)
|
||||||
atomic.StoreUint32(&p.Fightinfo.Status, 0)
|
atomic.StoreUint32(&p.Fightinfo.Status, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func NormalizePeakMode(requested uint32) (fightMode uint32, status uint32, err errorcode.ErrorCode) {
|
||||||
|
return normalizePeakMode(requested)
|
||||||
|
}
|
||||||
|
|
||||||
|
func AvailableCatchTimes(pets []model.PetInfo) []uint32 {
|
||||||
|
return filterAvailableCatchTimes(pets)
|
||||||
|
}
|
||||||
|
|
||||||
func SubmitBanPick(p *player.Player, selected, banned []uint32) errorcode.ErrorCode {
|
func SubmitBanPick(p *player.Player, selected, banned []uint32) errorcode.ErrorCode {
|
||||||
if p == nil {
|
if p == nil {
|
||||||
return errorcode.ErrorCodes.ErrSystemBusyTryLater
|
return errorcode.ErrorCodes.ErrSystemBusyTryLater
|
||||||
@@ -665,7 +650,11 @@ func resolveBattlePets(catchTimes []uint32, limit int) []model.PetInfo {
|
|||||||
if pet == nil || pet.Data.ID == 0 {
|
if pet == nil || pet.Data.ID == 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
result = append(result, pet.Data)
|
petInfo := pet.Data
|
||||||
|
if petInfo.Level > battleLevelCap {
|
||||||
|
petInfo.Level = battleLevelCap
|
||||||
|
}
|
||||||
|
result = append(result, petInfo)
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|||||||
60
logic/service/fight/slot_alloc_test.go
Normal file
60
logic/service/fight/slot_alloc_test.go
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
package fight
|
||||||
|
|
||||||
|
import (
|
||||||
|
"blazing/logic/service/common"
|
||||||
|
"blazing/modules/player/model"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestArrangePetsBySlotLimit(t *testing.T) {
|
||||||
|
pets := []model.PetInfo{
|
||||||
|
{ID: 1, Hp: 10},
|
||||||
|
{ID: 2, Hp: 10},
|
||||||
|
{ID: 3, Hp: 10},
|
||||||
|
{ID: 4, Hp: 10},
|
||||||
|
{ID: 5, Hp: 10},
|
||||||
|
{ID: 6, Hp: 10},
|
||||||
|
}
|
||||||
|
|
||||||
|
slots := ArrangePetsBySlotLimit(pets, 3)
|
||||||
|
if len(slots) != 3 {
|
||||||
|
t.Fatalf("expected 3 slots, got %d", len(slots))
|
||||||
|
}
|
||||||
|
if len(slots[0]) != 2 || slots[0][0].ID != 1 || slots[0][1].ID != 4 {
|
||||||
|
t.Fatalf("slot 0 mismatch: %+v", slots[0])
|
||||||
|
}
|
||||||
|
if len(slots[1]) != 2 || slots[1][0].ID != 2 || slots[1][1].ID != 5 {
|
||||||
|
t.Fatalf("slot 1 mismatch: %+v", slots[1])
|
||||||
|
}
|
||||||
|
if len(slots[2]) != 2 || slots[2][0].ID != 3 || slots[2][1].ID != 6 {
|
||||||
|
t.Fatalf("slot 2 mismatch: %+v", slots[2])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExpandPlayersWithSlotLimit(t *testing.T) {
|
||||||
|
players := []common.PlayerI{&stubPlayer{}, &stubPlayer{}}
|
||||||
|
petsByPlayer := [][]model.PetInfo{
|
||||||
|
{
|
||||||
|
{ID: 1, Hp: 10},
|
||||||
|
{ID: 2, Hp: 10},
|
||||||
|
{ID: 3, Hp: 10},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
{ID: 11, Hp: 10},
|
||||||
|
{ID: 12, Hp: 10},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
flatPlayers, flatSlots, err := ExpandPlayersWithSlotLimit(players, petsByPlayer, 1)
|
||||||
|
if err != 0 {
|
||||||
|
t.Fatalf("unexpected err: %v", err)
|
||||||
|
}
|
||||||
|
if len(flatPlayers) != 2 || len(flatSlots) != 2 {
|
||||||
|
t.Fatalf("unexpected flatten result: players=%d slots=%d", len(flatPlayers), len(flatSlots))
|
||||||
|
}
|
||||||
|
if len(flatSlots[0]) != 3 || flatSlots[0][0].ID != 1 || flatSlots[0][1].ID != 2 || flatSlots[0][2].ID != 3 {
|
||||||
|
t.Fatalf("player0 slot mismatch: %+v", flatSlots[0])
|
||||||
|
}
|
||||||
|
if len(flatSlots[1]) != 2 || flatSlots[1][0].ID != 11 || flatSlots[1][1].ID != 12 {
|
||||||
|
t.Fatalf("player1 slot mismatch: %+v", flatSlots[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
|
|
||||||
type stubPlayer struct {
|
type stubPlayer struct {
|
||||||
info model.PlayerInfo
|
info model.PlayerInfo
|
||||||
|
sentCmds []uint32
|
||||||
}
|
}
|
||||||
|
|
||||||
func (*stubPlayer) ApplyPetDisplayInfo(*spaceinfo.SimpleInfo) {}
|
func (*stubPlayer) ApplyPetDisplayInfo(*spaceinfo.SimpleInfo) {}
|
||||||
@@ -26,7 +27,7 @@ func (*stubPlayer) SetFightC(common.FightI) {}
|
|||||||
func (*stubPlayer) QuitFight() {}
|
func (*stubPlayer) QuitFight() {}
|
||||||
func (*stubPlayer) MessWin(bool) {}
|
func (*stubPlayer) MessWin(bool) {}
|
||||||
func (*stubPlayer) CanFight() errorcode.ErrorCode { return 0 }
|
func (*stubPlayer) CanFight() errorcode.ErrorCode { return 0 }
|
||||||
func (*stubPlayer) SendPackCmd(uint32, any) {}
|
func (p *stubPlayer) SendPackCmd(cmd uint32, _ any) { p.sentCmds = append(p.sentCmds, cmd) }
|
||||||
func (*stubPlayer) GetPetInfo(uint32) []model.PetInfo { return nil }
|
func (*stubPlayer) GetPetInfo(uint32) []model.PetInfo { return nil }
|
||||||
|
|
||||||
func TestFightActionEnvelopeEncodedTargetIndex(t *testing.T) {
|
func TestFightActionEnvelopeEncodedTargetIndex(t *testing.T) {
|
||||||
@@ -111,3 +112,36 @@ func TestBuildFightStateStartEnvelope(t *testing.T) {
|
|||||||
t.Fatalf("unexpected right fighter snapshot: %+v", envelope.Right[0])
|
t.Fatalf("unexpected right fighter snapshot: %+v", envelope.Right[0])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBuildNoteUseSkillOutboundInfoUsesActionOrder(t *testing.T) {
|
||||||
|
ourPlayer := &stubPlayer{info: model.PlayerInfo{UserID: 1001}}
|
||||||
|
oppPlayer := &stubPlayer{info: model.PlayerInfo{UserID: 2002}}
|
||||||
|
|
||||||
|
our := input.NewInput(nil, ourPlayer)
|
||||||
|
our.InitAttackValue()
|
||||||
|
our.AttackValue.SkillID = 111
|
||||||
|
our.AttackValue.RemainHp = 80
|
||||||
|
our.AttackValue.MaxHp = 100
|
||||||
|
|
||||||
|
opp := input.NewInput(nil, oppPlayer)
|
||||||
|
opp.InitAttackValue()
|
||||||
|
opp.AttackValue.SkillID = 222
|
||||||
|
opp.AttackValue.RemainHp = 70
|
||||||
|
opp.AttackValue.MaxHp = 100
|
||||||
|
|
||||||
|
fc := &FightC{
|
||||||
|
Our: []*input.Input{our},
|
||||||
|
Opp: []*input.Input{opp},
|
||||||
|
First: opp,
|
||||||
|
Second: our,
|
||||||
|
}
|
||||||
|
|
||||||
|
result := fc.buildNoteUseSkillOutboundInfo()
|
||||||
|
|
||||||
|
if result.FirstAttackInfo.UserID != 2002 || result.FirstAttackInfo.SkillID != 222 {
|
||||||
|
t.Fatalf("expected first attack info to belong to acting opponent, got %+v", result.FirstAttackInfo)
|
||||||
|
}
|
||||||
|
if result.SecondAttackInfo.UserID != 1001 || result.SecondAttackInfo.SkillID != 111 {
|
||||||
|
t.Fatalf("expected second attack info to keep the idle side placeholder, got %+v", result.SecondAttackInfo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -23,3 +23,9 @@ type C2S_Skill_Sort struct {
|
|||||||
Skill [4]uint32 `json:"skill_1"` // 技能1(对应C# uint skill_1)
|
Skill [4]uint32 `json:"skill_1"` // 技能1(对应C# uint skill_1)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type CommitPetSkillsInfo struct {
|
||||||
|
Head common.TomeeHeader `cmd:"52313" struc:"skip"`
|
||||||
|
CatchTime uint32 `json:"catchTime"`
|
||||||
|
Skill [4]uint32 `json:"skill"`
|
||||||
|
}
|
||||||
|
|||||||
@@ -51,6 +51,9 @@ func (p *Player) IsMatch(t configmodel.Event) bool {
|
|||||||
if len(p.Info.PetList) == 0 {
|
if len(p.Info.PetList) == 0 {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
if p.Info.PetList[0].Hp == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
firstPetID := int32(p.Info.PetList[0].ID)
|
firstPetID := int32(p.Info.PetList[0].ID)
|
||||||
_, ok := lo.Find(t.FirstSprites, func(item int32) bool {
|
_, ok := lo.Find(t.FirstSprites, func(item int32) bool {
|
||||||
|
|||||||
49
logic/service/player/Monster_test.go
Normal file
49
logic/service/player/Monster_test.go
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
package player
|
||||||
|
|
||||||
|
import (
|
||||||
|
configmodel "blazing/modules/config/model"
|
||||||
|
playermodel "blazing/modules/player/model"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestIsMatchFirstSpritesRequiresLivingLeadPet(t *testing.T) {
|
||||||
|
player := &Player{
|
||||||
|
baseplayer: baseplayer{
|
||||||
|
Info: &playermodel.PlayerInfo{
|
||||||
|
PetList: []playermodel.PetInfo{
|
||||||
|
{ID: 1001, Hp: 0},
|
||||||
|
{ID: 2002, Hp: 100},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
event := configmodel.Event{
|
||||||
|
FirstSprites: []int32{1001},
|
||||||
|
}
|
||||||
|
|
||||||
|
if player.IsMatch(event) {
|
||||||
|
t.Fatalf("expected dead lead pet to fail FirstSprites match")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsMatchFirstSpritesAcceptsLivingLeadPet(t *testing.T) {
|
||||||
|
player := &Player{
|
||||||
|
baseplayer: baseplayer{
|
||||||
|
Info: &playermodel.PlayerInfo{
|
||||||
|
PetList: []playermodel.PetInfo{
|
||||||
|
{ID: 1001, Hp: 100},
|
||||||
|
{ID: 2002, Hp: 100},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
event := configmodel.Event{
|
||||||
|
FirstSprites: []int32{1001},
|
||||||
|
}
|
||||||
|
|
||||||
|
if !player.IsMatch(event) {
|
||||||
|
t.Fatalf("expected living lead pet to pass FirstSprites match")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,16 @@
|
|||||||
package player
|
package player
|
||||||
|
|
||||||
|
import "blazing/modules/player/model"
|
||||||
|
|
||||||
type AI_player struct {
|
type AI_player struct {
|
||||||
baseplayer
|
baseplayer
|
||||||
|
|
||||||
CanCapture int
|
CanCapture int
|
||||||
BossScript string
|
BossScript string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *AI_player) GetPetInfo(_ uint32) []model.PetInfo {
|
||||||
|
ret := make([]model.PetInfo, 0, len(p.Info.PetList))
|
||||||
|
ret = append(ret, p.Info.PetList...)
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import (
|
|||||||
"blazing/logic/service/fight/info"
|
"blazing/logic/service/fight/info"
|
||||||
spaceinfo "blazing/logic/service/space/info"
|
spaceinfo "blazing/logic/service/space/info"
|
||||||
"blazing/modules/player/model"
|
"blazing/modules/player/model"
|
||||||
|
|
||||||
|
"github.com/gogf/gf/v2/util/grand"
|
||||||
)
|
)
|
||||||
|
|
||||||
type baseplayer struct {
|
type baseplayer struct {
|
||||||
@@ -30,17 +32,19 @@ func (p *baseplayer) GetInfo() *model.PlayerInfo {
|
|||||||
return p.Info
|
return p.Info
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ApplyPetLevelLimit(pet model.PetInfo, limitlevel uint32) model.PetInfo {
|
||||||
|
originalHP := pet.Hp
|
||||||
|
pet.CalculatePetPane(limitlevel)
|
||||||
|
pet.Hp = utils.Min(originalHP, pet.MaxHp)
|
||||||
|
return pet
|
||||||
|
}
|
||||||
|
|
||||||
func (p *baseplayer) GetPetInfo(limitlevel uint32) []model.PetInfo {
|
func (p *baseplayer) GetPetInfo(limitlevel uint32) []model.PetInfo {
|
||||||
|
|
||||||
var ret []model.PetInfo
|
ret := make([]model.PetInfo, 0, len(p.Info.PetList))
|
||||||
|
|
||||||
for _, pet := range p.Info.PetList {
|
for _, pet := range p.Info.PetList {
|
||||||
if limitlevel > 0 {
|
ret = append(ret, ApplyPetLevelLimit(pet, limitlevel))
|
||||||
pet.Level = utils.Min(pet.Level, limitlevel)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
ret = append(ret, pet)
|
|
||||||
}
|
}
|
||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
@@ -85,6 +89,21 @@ func (f *baseplayer) GetPlayerCaptureContext() *info.PlayerCaptureContext {
|
|||||||
return f.PlayerCaptureContext
|
return f.PlayerCaptureContext
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *baseplayer) Roll(numerator, denominator int) (bool, float64, float64) {
|
||||||
|
if denominator <= 0 {
|
||||||
|
return false, 0, 0
|
||||||
|
}
|
||||||
|
if numerator < 0 {
|
||||||
|
numerator = 0
|
||||||
|
}
|
||||||
|
if numerator > denominator {
|
||||||
|
numerator = denominator
|
||||||
|
}
|
||||||
|
|
||||||
|
base := float64(numerator) / float64(denominator) * 100
|
||||||
|
return grand.Intn(denominator) < numerator, base, 0
|
||||||
|
}
|
||||||
|
|
||||||
// FindPet 根据捕捉时间查找宠物
|
// FindPet 根据捕捉时间查找宠物
|
||||||
// 返回值: (索引, 宠物信息, 是否找到)
|
// 返回值: (索引, 宠物信息, 是否找到)
|
||||||
func (f *baseplayer) FindPet(catchTime uint32) (int, *model.PetInfo, bool) {
|
func (f *baseplayer) FindPet(catchTime uint32) (int, *model.PetInfo, bool) {
|
||||||
|
|||||||
@@ -1,23 +1,11 @@
|
|||||||
package player
|
package player
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"blazing/logic/service/fight/info"
|
|
||||||
"blazing/logic/service/task"
|
|
||||||
"blazing/modules/player/model"
|
"blazing/modules/player/model"
|
||||||
|
|
||||||
"github.com/pointernil/bitset32"
|
"github.com/pointernil/bitset32"
|
||||||
)
|
)
|
||||||
|
|
||||||
// 辅助函数:获取任务奖励,封装逻辑便于复用和统一检查
|
|
||||||
// 返回nil表示无奖励
|
|
||||||
func (p *Player) getTaskGift(taskID int, ot int) *task.TaskResult {
|
|
||||||
// 防御性检查:taskID非法时直接返回nil
|
|
||||||
if taskID <= 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return task.GetTaskInfo(taskID, ot)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SptCompletedTask 完成任务(单分支)
|
// SptCompletedTask 完成任务(单分支)
|
||||||
// 优化点:仅当奖励存在时,才完成任务并发放奖励
|
// 优化点:仅当奖励存在时,才完成任务并发放奖励
|
||||||
func (p *Player) SptCompletedTask(taskID int, ot int) {
|
func (p *Player) SptCompletedTask(taskID int, ot int) {
|
||||||
@@ -29,15 +17,17 @@ func (p *Player) SptCompletedTask(taskID int, ot int) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 核心逻辑:先检查奖励是否存在,无奖励则直接返回(不完成任务)
|
if !p.canCompleteTaskReward(taskID, ot) {
|
||||||
gift := p.getTaskGift(taskID, ot)
|
return
|
||||||
if gift == nil {
|
}
|
||||||
|
|
||||||
|
granted, err := p.ApplyTaskCompletion(uint32(taskID), ot, nil)
|
||||||
|
if err != 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 奖励存在时,才标记任务完成 + 发放奖励
|
|
||||||
p.Info.SetTask(taskID, model.Completed)
|
p.Info.SetTask(taskID, model.Completed)
|
||||||
p.bossgive(taskID, ot)
|
p.SendTaskCompletionBonus(uint32(taskID), granted)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TawerCompletedTask 完成塔类任务(多分支)
|
// TawerCompletedTask 完成塔类任务(多分支)
|
||||||
@@ -48,71 +38,34 @@ func (p *Player) TawerCompletedTask(taskID int, ot int) {
|
|||||||
}
|
}
|
||||||
// 处理默认分支(ot=-1):仅奖励存在时才完成主任务
|
// 处理默认分支(ot=-1):仅奖励存在时才完成主任务
|
||||||
if p.Info.GetTask(taskID) != model.Completed {
|
if p.Info.GetTask(taskID) != model.Completed {
|
||||||
defaultGift := p.getTaskGift(taskID, -1)
|
if p.canCompleteTaskReward(taskID, -1) {
|
||||||
if defaultGift != nil { // 奖励存在才标记主任务完成
|
granted, err := p.ApplyTaskCompletion(uint32(taskID), -1, nil)
|
||||||
|
if err == 0 {
|
||||||
p.Info.SetTask(taskID, model.Completed)
|
p.Info.SetTask(taskID, model.Completed)
|
||||||
p.bossgive(taskID, -1)
|
p.SendTaskCompletionBonus(uint32(taskID), granted)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理指定分支(ot):仅奖励存在时才标记分支完成并发奖
|
// 处理指定分支(ot):仅奖励存在时才标记分支完成并发奖
|
||||||
p.Service.Task.Exec(uint32(taskID), func(te *model.Task) bool {
|
taskData, err := p.Service.Task.GetTask(uint32(taskID))
|
||||||
|
if err != nil {
|
||||||
// 核心检查:指定分支的奖励是否存在
|
|
||||||
branchGift := p.getTaskGift(taskID, ot)
|
|
||||||
if branchGift == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// 初始化分支数据
|
|
||||||
if te.Data == nil {
|
|
||||||
te.Data = []uint32{}
|
|
||||||
}
|
|
||||||
|
|
||||||
r := bitset32.From(te.Data)
|
|
||||||
// 分支未完成时,标记完成并发放奖励
|
|
||||||
if !r.Test(uint(ot)) {
|
|
||||||
r.Set(uint(ot))
|
|
||||||
p.bossgive(taskID, ot)
|
|
||||||
te.Data = r.Bytes()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// bossgive 发放任务奖励(逻辑保持不变,仅补充注释)
|
|
||||||
func (p *Player) bossgive(taskID int, ot int) {
|
|
||||||
gift := p.getTaskGift(taskID, ot)
|
|
||||||
if gift == nil {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
res := &info.S2C_GET_BOSS_MONSTER{
|
if !p.canCompleteTaskReward(taskID, ot) {
|
||||||
BonusID: uint32(taskID),
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 发放宠物奖励
|
r := bitset32.From(taskData.Data)
|
||||||
if gift.Pet != nil {
|
if !r.Test(uint(ot)) {
|
||||||
p.Service.Pet.PetAdd(gift.Pet, 0)
|
r.Set(uint(ot))
|
||||||
res.PetID = gift.Pet.ID
|
granted, rewardErr := p.ApplyTaskCompletion(uint32(taskID), ot, nil)
|
||||||
res.CaptureTm = gift.Pet.CatchTime
|
if rewardErr != 0 {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
p.SendTaskCompletionBonus(uint32(taskID), granted)
|
||||||
// 发放道具奖励(仅成功添加的道具才返回给前端)
|
taskData.Data = r.Bytes()
|
||||||
for _, item := range gift.ItemList {
|
_ = p.Service.Task.SetTask(taskData)
|
||||||
if success := p.ItemAdd(item.ItemId, item.ItemCnt); success {
|
|
||||||
res.AddItemInfo(item)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 发放称号奖励
|
|
||||||
if gift.Title != 0 {
|
|
||||||
p.GiveTitle(gift.Title)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 发送奖励通知给前端
|
|
||||||
if res.HasReward() {
|
|
||||||
p.SendPackCmd(8004, res)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,11 @@ import (
|
|||||||
"github.com/panjf2000/gnet/v2"
|
"github.com/panjf2000/gnet/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
minPacketLen = 17
|
||||||
|
maxPacketLen = 10 * 1024
|
||||||
|
)
|
||||||
|
|
||||||
// getUnderlyingValue 递归解析reflect.Value,解包指针、interface{}到底层具体类型
|
// getUnderlyingValue 递归解析reflect.Value,解包指针、interface{}到底层具体类型
|
||||||
func getUnderlyingValue(val reflect.Value) (reflect.Value, error) {
|
func getUnderlyingValue(val reflect.Value) (reflect.Value, error) {
|
||||||
for {
|
for {
|
||||||
@@ -48,6 +53,44 @@ func getUnderlyingValue(val reflect.Value) (reflect.Value, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func setFieldByIndex(root reflect.Value, index []int, value reflect.Value) bool {
|
||||||
|
current := root
|
||||||
|
for pos, idx := range index {
|
||||||
|
if current.Kind() == reflect.Ptr {
|
||||||
|
if current.IsNil() {
|
||||||
|
current.Set(reflect.New(current.Type().Elem()))
|
||||||
|
}
|
||||||
|
current = current.Elem()
|
||||||
|
}
|
||||||
|
|
||||||
|
if current.Kind() != reflect.Struct || idx < 0 || idx >= current.NumField() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
field := current.Field(idx)
|
||||||
|
if pos == len(index)-1 {
|
||||||
|
if !field.CanSet() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if value.Type().AssignableTo(field.Type()) {
|
||||||
|
field.Set(value)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if field.Kind() == reflect.Ptr && value.Type().AssignableTo(field.Type().Elem()) {
|
||||||
|
ptr := reflect.New(field.Type().Elem())
|
||||||
|
ptr.Elem().Set(value)
|
||||||
|
field.Set(ptr)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
current = field
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// XORDecryptU 原地执行异或解密,避免额外分配和拷贝。
|
// XORDecryptU 原地执行异或解密,避免额外分配和拷贝。
|
||||||
func XORDecryptU(encryptedData []byte, key uint32) []byte {
|
func XORDecryptU(encryptedData []byte, key uint32) []byte {
|
||||||
if len(encryptedData) == 0 {
|
if len(encryptedData) == 0 {
|
||||||
@@ -106,6 +149,12 @@ func (h *ClientData) PushEvent(v []byte, submit func(task func()) error) {
|
|||||||
if h == nil || h.IsClosed() {
|
if h == nil || h.IsClosed() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if len(v) < minPacketLen || len(v) > maxPacketLen {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if binary.BigEndian.Uint32(v[0:4]) != uint32(len(v)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
var header common.TomeeHeader
|
var header common.TomeeHeader
|
||||||
header.Len = binary.BigEndian.Uint32(v[0:4])
|
header.Len = binary.BigEndian.Uint32(v[0:4])
|
||||||
@@ -182,45 +231,38 @@ func (h *ClientData) OnEvent(data common.TomeeHeader) {
|
|||||||
return //TODO 待实现cmd未注册
|
return //TODO 待实现cmd未注册
|
||||||
}
|
}
|
||||||
|
|
||||||
params := []reflect.Value{}
|
var ptrValue reflect.Value
|
||||||
|
if cmdlister.NewReqValue != nil {
|
||||||
|
ptrValue = cmdlister.NewReqValue()
|
||||||
|
} else {
|
||||||
|
ptrValue = reflect.New(cmdlister.Req)
|
||||||
|
}
|
||||||
|
|
||||||
//funct := cmdlister.Type().NumIn()
|
|
||||||
|
|
||||||
// 如果需要可设置的变量(用于修改值),创建指针并解引用
|
|
||||||
ptrValue := reflect.New(cmdlister.Req)
|
|
||||||
|
|
||||||
// fmt.Println(tt1)
|
|
||||||
if data.Res != nil {
|
if data.Res != nil {
|
||||||
tt1 := ptrValue.Elem().Addr().Interface()
|
err := struc.Unpack(bytes.NewBuffer(data.Res), ptrValue.Interface())
|
||||||
err := struc.Unpack(bytes.NewBuffer(data.Res), tt1)
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
||||||
cool.Logger.Error(context.Background(), data.UserID, data.CMD, "解包失败,", err, hex.EncodeToString(data.Res))
|
cool.Logger.Error(context.Background(), data.UserID, data.CMD, "解包失败,", err, hex.EncodeToString(data.Res))
|
||||||
//fmt.Println(data.UserID, data.CMD, "解包失败,", hex.EncodeToString(data.Data))
|
|
||||||
data.Result = uint32(errorcode.ErrorCodes.ErrSystemProcessingError)
|
data.Result = uint32(errorcode.ErrorCodes.ErrSystemProcessingError)
|
||||||
h.SendPack(data.Pack(nil))
|
h.SendPack(data.Pack(nil))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ptrValue1 := ptrValue.Elem().Addr()
|
if !setFieldByIndex(ptrValue.Elem(), cmdlister.HeaderFieldIndex, reflect.ValueOf(data)) {
|
||||||
// 设置 Name 字段
|
cool.Logger.Warning(context.Background(), data.UserID, data.CMD, "设置请求头失败")
|
||||||
nameField := ptrValue.Elem().Field(0) //首个为header
|
return
|
||||||
nameField.Set(reflect.ValueOf(data))
|
|
||||||
|
|
||||||
if data.CMD > 1001 { //if cmdlister.Type().In(1) == reflect.TypeOf(&Player{}) {
|
|
||||||
//t := GetPlayer(c, data.UserID)
|
|
||||||
|
|
||||||
// fmt.Println(data.CMD, "接收 变量的地址 ", &t.Info, t.Info.UserID)
|
|
||||||
|
|
||||||
params = append(params, ptrValue1, reflect.ValueOf(h.Player))
|
|
||||||
} else {
|
|
||||||
|
|
||||||
params = append(params, ptrValue1, reflect.ValueOf(h.Conn))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ret := cmdlister.Func.Call(params)
|
var params [2]reflect.Value
|
||||||
|
params[0] = ptrValue
|
||||||
|
if cmdlister.UseConn {
|
||||||
|
params[1] = reflect.ValueOf(h.Conn)
|
||||||
|
} else {
|
||||||
|
params[1] = reflect.ValueOf(h.Player)
|
||||||
|
}
|
||||||
|
|
||||||
|
ret := cmdlister.Func.Call(params[:])
|
||||||
|
|
||||||
if len(ret) <= 0 { //如果判断没有参数,那就说明这个包没有返回参数
|
if len(ret) <= 0 { //如果判断没有参数,那就说明这个包没有返回参数
|
||||||
return
|
return
|
||||||
@@ -249,13 +291,13 @@ func (h *ClientData) OnEvent(data common.TomeeHeader) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ClientData struct {
|
type ClientData struct {
|
||||||
IsCrossDomain sync.Once //是否跨域过
|
|
||||||
Player *Player //客户实体
|
Player *Player //客户实体
|
||||||
ERROR_CONNUT int
|
ERROR_CONNUT int
|
||||||
Wsmsg *WsCodec
|
Wsmsg *WsCodec
|
||||||
Conn gnet.Conn
|
Conn gnet.Conn
|
||||||
LF *lockfree.Lockfree[common.TomeeHeader]
|
LF *lockfree.Lockfree[common.TomeeHeader]
|
||||||
closed int32
|
closed int32
|
||||||
|
crossDomainChecked uint32
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *ClientData) IsClosed() bool {
|
func (p *ClientData) IsClosed() bool {
|
||||||
@@ -271,6 +313,14 @@ func (p *ClientData) Close() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *ClientData) IsCrossDomainChecked() bool {
|
||||||
|
return atomic.LoadUint32(&p.crossDomainChecked) == 1
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ClientData) MarkCrossDomainChecked() {
|
||||||
|
atomic.StoreUint32(&p.crossDomainChecked, 1)
|
||||||
|
}
|
||||||
|
|
||||||
func (p *ClientData) GetPlayer(userid uint32) *Player { //TODO 这里待优化,可能存在内存泄漏问题
|
func (p *ClientData) GetPlayer(userid uint32) *Player { //TODO 这里待优化,可能存在内存泄漏问题
|
||||||
if p.Player == nil {
|
if p.Player == nil {
|
||||||
p.Player = NewPlayer(p.Conn)
|
p.Player = NewPlayer(p.Conn)
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ func (player *Player) WarehousePetList() []model.PetInfo {
|
|||||||
return make([]model.PetInfo, 0)
|
return make([]model.PetInfo, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
result := make([]model.PetInfo, 0, len(allPets))
|
result := make([]model.PetInfo, 0, len(allPets))
|
||||||
|
|
||||||
return result
|
return result
|
||||||
@@ -25,32 +24,35 @@ func (player *Player) WarehousePetList() []model.PetInfo {
|
|||||||
|
|
||||||
// AddPetExp 添加宠物经验
|
// AddPetExp 添加宠物经验
|
||||||
func (p *Player) AddPetExp(petInfo *model.PetInfo, addExp int64) {
|
func (p *Player) AddPetExp(petInfo *model.PetInfo, addExp int64) {
|
||||||
if addExp < 0 {
|
if petInfo == nil || addExp <= 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
panelLimit := p.CurrentMapPetLevelLimit()
|
||||||
|
if petInfo.Level > 100 {
|
||||||
|
currentHP := petInfo.Hp
|
||||||
|
petInfo.Update(false)
|
||||||
|
petInfo.CalculatePetPane(panelLimit)
|
||||||
|
petInfo.Hp = utils.Min(currentHP, petInfo.MaxHp)
|
||||||
|
}
|
||||||
addExp = utils.Min(addExp, p.Info.ExpPool)
|
addExp = utils.Min(addExp, p.Info.ExpPool)
|
||||||
|
if addExp <= 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
originalLevel := petInfo.Level
|
originalLevel := petInfo.Level
|
||||||
exp := int64(petInfo.Exp) + addExp
|
allocatedExp := addExp
|
||||||
p.Info.ExpPool -= addExp //减去已使用的经验
|
p.Info.ExpPool -= addExp //减去已使用的经验
|
||||||
gainedExp := exp //已获得的经验
|
|
||||||
for exp >= int64(petInfo.NextLvExp) {
|
|
||||||
|
|
||||||
|
currentExp := petInfo.Exp + addExp
|
||||||
|
for currentExp >= petInfo.NextLvExp && petInfo.NextLvExp > 0 {
|
||||||
petInfo.Level++
|
petInfo.Level++
|
||||||
|
|
||||||
exp -= int64(petInfo.LvExp)
|
|
||||||
petInfo.Update(true)
|
petInfo.Update(true)
|
||||||
if originalLevel < 100 && petInfo.Level == 100 { //升到100了
|
currentExp -= petInfo.LvExp
|
||||||
p.Info.ExpPool += exp //减去已使用的经验
|
|
||||||
gainedExp -= exp
|
|
||||||
exp = 0
|
|
||||||
break //停止升级
|
|
||||||
}
|
}
|
||||||
|
petInfo.Exp = currentExp
|
||||||
|
|
||||||
}
|
|
||||||
petInfo.Exp = (exp)
|
|
||||||
// 重新计算面板
|
// 重新计算面板
|
||||||
if originalLevel != petInfo.Level {
|
if originalLevel != petInfo.Level {
|
||||||
petInfo.CalculatePetPane(100)
|
petInfo.CalculatePetPane(panelLimit)
|
||||||
|
|
||||||
petInfo.Cure()
|
petInfo.Cure()
|
||||||
p.Info.PetMaxLevel = utils.Max(petInfo.Level, p.Info.PetMaxLevel)
|
p.Info.PetMaxLevel = utils.Max(petInfo.Level, p.Info.PetMaxLevel)
|
||||||
@@ -82,7 +84,7 @@ func (p *Player) AddPetExp(petInfo *model.PetInfo, addExp int64) {
|
|||||||
var petUpdateInfo info.UpdatePropInfo
|
var petUpdateInfo info.UpdatePropInfo
|
||||||
|
|
||||||
copier.Copy(&petUpdateInfo, petInfo)
|
copier.Copy(&petUpdateInfo, petInfo)
|
||||||
petUpdateInfo.Exp = uint32(gainedExp)
|
petUpdateInfo.Exp = uint32(allocatedExp)
|
||||||
updateOutbound.Data = append(updateOutbound.Data, petUpdateInfo)
|
updateOutbound.Data = append(updateOutbound.Data, petUpdateInfo)
|
||||||
p.SendPack(header.Pack(updateOutbound)) //准备包由各自发,因为协议不一样
|
p.SendPack(header.Pack(updateOutbound)) //准备包由各自发,因为协议不一样
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,13 @@ func (slot PetBagSlot) PetInfo() model.PetInfo {
|
|||||||
return slot.info
|
return slot.info
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (slot PetBagSlot) PetInfoPtr() *model.PetInfo {
|
||||||
|
if !slot.IsValid() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &(*slot.list)[slot.index]
|
||||||
|
}
|
||||||
|
|
||||||
func (slot PetBagSlot) IsMainBag() bool {
|
func (slot PetBagSlot) IsMainBag() bool {
|
||||||
return slot.main
|
return slot.main
|
||||||
}
|
}
|
||||||
@@ -99,12 +106,20 @@ func validatePetBagOrder(
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func buildLimitedPetList(petList []model.PetInfo, limitlevel uint32) []model.PetInfo {
|
||||||
|
result := make([]model.PetInfo, 0, len(petList))
|
||||||
|
for _, petInfo := range petList {
|
||||||
|
result = append(result, ApplyPetLevelLimit(petInfo, limitlevel))
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
// GetUserBagPetInfo 返回主背包和并列备用精灵列表。
|
// GetUserBagPetInfo 返回主背包和并列备用精灵列表。
|
||||||
func (p *Player) GetUserBagPetInfo() *pet.GetUserBagPetInfoOutboundInfo {
|
func (p *Player) GetUserBagPetInfo(limitlevel uint32) *pet.GetUserBagPetInfoOutboundInfo {
|
||||||
|
|
||||||
result := &pet.GetUserBagPetInfoOutboundInfo{
|
result := &pet.GetUserBagPetInfoOutboundInfo{
|
||||||
PetList: p.Info.PetList,
|
PetList: buildLimitedPetList(p.Info.PetList, limitlevel),
|
||||||
BackupPetList: p.Info.BackupPetList,
|
BackupPetList: buildLimitedPetList(p.Info.BackupPetList, limitlevel),
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|||||||
157
logic/service/player/pet_test.go
Normal file
157
logic/service/player/pet_test.go
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
package player
|
||||||
|
|
||||||
|
import (
|
||||||
|
"blazing/common/data/xmlres"
|
||||||
|
playermodel "blazing/modules/player/model"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func firstPetIDForTest(t *testing.T) int {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
for id := range xmlres.PetMAP {
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Fatal("xmlres.PetMAP is empty")
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAddPetExpAllowsLevelBeyond100WhilePanelStaysCapped(t *testing.T) {
|
||||||
|
petID := firstPetIDForTest(t)
|
||||||
|
petInfo := playermodel.GenPetInfo(petID, 31, 0, 0, 100, nil, 0)
|
||||||
|
expectedPanel := playermodel.GenPetInfo(petID, 31, 0, 0, 100, nil, 0)
|
||||||
|
if petInfo == nil {
|
||||||
|
t.Fatalf("failed to generate test pet")
|
||||||
|
}
|
||||||
|
if expectedPanel == nil {
|
||||||
|
t.Fatalf("failed to generate expected test pet")
|
||||||
|
}
|
||||||
|
|
||||||
|
player := &Player{
|
||||||
|
baseplayer: baseplayer{
|
||||||
|
Info: &playermodel.PlayerInfo{
|
||||||
|
ExpPool: 1_000_000,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
player.AddPetExp(petInfo, petInfo.NextLvExp+10_000)
|
||||||
|
|
||||||
|
if petInfo.Level <= 100 {
|
||||||
|
t.Fatalf("expected pet level to continue beyond 100, got %d", petInfo.Level)
|
||||||
|
}
|
||||||
|
if petInfo.MaxHp != expectedPanel.MaxHp {
|
||||||
|
t.Fatalf("expected max hp to stay capped at 100-level panel, got %d want %d", petInfo.MaxHp, expectedPanel.MaxHp)
|
||||||
|
}
|
||||||
|
if petInfo.Prop != expectedPanel.Prop {
|
||||||
|
t.Fatalf("expected props to stay capped at 100-level panel, got %+v want %+v", petInfo.Prop, expectedPanel.Prop)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAddPetExpRecalculatesPanelForLevelAbove100(t *testing.T) {
|
||||||
|
petID := firstPetIDForTest(t)
|
||||||
|
petInfo := playermodel.GenPetInfo(petID, 31, 0, 0, 100, nil, 0)
|
||||||
|
expectedPanel := playermodel.GenPetInfo(petID, 31, 0, 0, 100, nil, 0)
|
||||||
|
if petInfo == nil {
|
||||||
|
t.Fatalf("failed to generate test pet")
|
||||||
|
}
|
||||||
|
if expectedPanel == nil {
|
||||||
|
t.Fatalf("failed to generate expected test pet")
|
||||||
|
}
|
||||||
|
petInfo.Level = 101
|
||||||
|
petInfo.Exp = 7
|
||||||
|
petInfo.MaxHp = 1
|
||||||
|
petInfo.Hp = 999999
|
||||||
|
|
||||||
|
player := &Player{
|
||||||
|
baseplayer: baseplayer{
|
||||||
|
Info: &playermodel.PlayerInfo{
|
||||||
|
ExpPool: 50_000,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
player.AddPetExp(petInfo, 12_345)
|
||||||
|
|
||||||
|
if petInfo.Level < 101 {
|
||||||
|
t.Fatalf("expected level above 100 to be preserved, got %d", petInfo.Level)
|
||||||
|
}
|
||||||
|
if petInfo.MaxHp != expectedPanel.MaxHp {
|
||||||
|
t.Fatalf("expected max hp to be recalculated using level 100 cap, got %d want %d", petInfo.MaxHp, expectedPanel.MaxHp)
|
||||||
|
}
|
||||||
|
if petInfo.Hp != petInfo.MaxHp {
|
||||||
|
t.Fatalf("expected hp to be clamped to recalculated max hp, got hp=%d maxHp=%d", petInfo.Hp, petInfo.MaxHp)
|
||||||
|
}
|
||||||
|
if player.Info.ExpPool != 50_000-12_345 {
|
||||||
|
t.Fatalf("expected exp pool to be consumed normally, got %d", player.Info.ExpPool)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAddPetExpSmallRewardDoesNotJumpToMaxLevel(t *testing.T) {
|
||||||
|
petID := firstPetIDForTest(t)
|
||||||
|
petInfo := playermodel.GenPetInfo(petID, 31, 0, 0, 20, nil, 0)
|
||||||
|
if petInfo == nil {
|
||||||
|
t.Fatalf("failed to generate test pet")
|
||||||
|
}
|
||||||
|
|
||||||
|
player := &Player{
|
||||||
|
baseplayer: baseplayer{
|
||||||
|
Info: &playermodel.PlayerInfo{
|
||||||
|
ExpPool: 1_000_000,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
addExp := int64(100)
|
||||||
|
originalLevel := petInfo.Level
|
||||||
|
nextLevelNeed := petInfo.NextLvExp - petInfo.Exp
|
||||||
|
if addExp >= nextLevelNeed {
|
||||||
|
t.Fatalf("test setup invalid: addExp=%d should be smaller than next level need=%d", addExp, nextLevelNeed)
|
||||||
|
}
|
||||||
|
|
||||||
|
player.AddPetExp(petInfo, addExp)
|
||||||
|
|
||||||
|
if petInfo.Level != originalLevel {
|
||||||
|
t.Fatalf("expected level to stay at %d, got %d", originalLevel, petInfo.Level)
|
||||||
|
}
|
||||||
|
if petInfo.Exp != addExp {
|
||||||
|
t.Fatalf("expected current exp to increase by %d, got %d", addExp, petInfo.Exp)
|
||||||
|
}
|
||||||
|
if player.Info.ExpPool != 1_000_000-addExp {
|
||||||
|
t.Fatalf("expected exp pool to decrease by %d, got %d", addExp, player.Info.ExpPool)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAddPetExpUsesDynamicPerLevelRequirement(t *testing.T) {
|
||||||
|
petID := firstPetIDForTest(t)
|
||||||
|
petInfo := playermodel.GenPetInfo(petID, 31, 0, 0, 20, nil, 0)
|
||||||
|
if petInfo == nil {
|
||||||
|
t.Fatalf("failed to generate test pet")
|
||||||
|
}
|
||||||
|
|
||||||
|
initialNeed := petInfo.NextLvExp
|
||||||
|
if initialNeed <= 0 {
|
||||||
|
t.Fatalf("expected positive exp requirement, got %d", initialNeed)
|
||||||
|
}
|
||||||
|
|
||||||
|
player := &Player{
|
||||||
|
baseplayer: baseplayer{
|
||||||
|
Info: &playermodel.PlayerInfo{
|
||||||
|
ExpPool: 1_000_000,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
player.AddPetExp(petInfo, initialNeed)
|
||||||
|
|
||||||
|
if petInfo.Level != 21 {
|
||||||
|
t.Fatalf("expected pet level to become 21, got %d", petInfo.Level)
|
||||||
|
}
|
||||||
|
if petInfo.Exp != 0 {
|
||||||
|
t.Fatalf("expected level-up to reset current level exp, got %d", petInfo.Exp)
|
||||||
|
}
|
||||||
|
if petInfo.NextLvExp == initialNeed {
|
||||||
|
t.Fatalf("expected next level exp to change after leveling, still %d", petInfo.NextLvExp)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"blazing/logic/service/fight/info"
|
"blazing/logic/service/fight/info"
|
||||||
"blazing/logic/service/space"
|
"blazing/logic/service/space"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -116,6 +117,10 @@ type Player struct {
|
|||||||
Hash uint32
|
Hash uint32
|
||||||
|
|
||||||
ArenaFlags uint32
|
ArenaFlags uint32
|
||||||
|
|
||||||
|
logoutMu sync.Mutex
|
||||||
|
logoutDone chan struct{}
|
||||||
|
logoutSaved bool
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -223,6 +228,21 @@ func (p *Player) GetSpace() *space.Space {
|
|||||||
return space.GetSpace(p.Info.MapID)
|
return space.GetSpace(p.Info.MapID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *Player) CurrentMapPetLevelLimit() uint32 {
|
||||||
|
if p == nil {
|
||||||
|
return 100
|
||||||
|
}
|
||||||
|
currentSpace := p.GetSpace()
|
||||||
|
if currentSpace != nil && currentSpace.IsLevelBreakMap {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return 100
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Player) IsCurrentMapLevelBreak() bool {
|
||||||
|
return p != nil && p.CurrentMapPetLevelLimit() == 0
|
||||||
|
}
|
||||||
|
|
||||||
// CanFight 检查玩家是否可以进行战斗
|
// CanFight 检查玩家是否可以进行战斗
|
||||||
// 0无战斗,1PVP,2,BOOS,3PVE
|
// 0无战斗,1PVP,2,BOOS,3PVE
|
||||||
func (p *Player) CanFight() errorcode.ErrorCode {
|
func (p *Player) CanFight() errorcode.ErrorCode {
|
||||||
@@ -414,7 +434,15 @@ func (p *Player) ItemAdd(ItemId, ItemCnt int64) (result bool) {
|
|||||||
p.SendPack(t1.Pack(nil)) //准备包由各自发,因为协议不一样
|
p.SendPack(t1.Pack(nil)) //准备包由各自发,因为协议不一样
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
p.Service.Item.UPDATE(uint32(ItemId), gconv.Int(ItemCnt))
|
if err := p.Service.Item.UPDATE(uint32(ItemId), gconv.Int(ItemCnt)); err != nil {
|
||||||
|
cool.Logger.Error(context.TODO(), "item add update failed", p.Info.UserID, ItemId, ItemCnt, err)
|
||||||
|
|
||||||
|
t1 := common.NewTomeeHeader(2601, p.Info.UserID)
|
||||||
|
t1.Result = uint32(errorcode.ErrorCodes.ErrSystemError200007)
|
||||||
|
|
||||||
|
p.SendPack(t1.Pack(nil)) //准备包由各自发,因为协议不一样
|
||||||
|
return false
|
||||||
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -447,14 +475,16 @@ func (player1 *Player) Kick(isquit bool) {
|
|||||||
// --- 新增超时机制核心代码 ---
|
// --- 新增超时机制核心代码 ---
|
||||||
// 设定超时时间(可根据业务需求调整,这里以3秒为例)
|
// 设定超时时间(可根据业务需求调整,这里以3秒为例)
|
||||||
const kickTimeout = 10 * time.Second
|
const kickTimeout = 10 * time.Second
|
||||||
|
timeout := false
|
||||||
select {
|
select {
|
||||||
case <-CloseChan:
|
case <-CloseChan:
|
||||||
// 正常流程:连接关闭回调已执行,CloseChan 被关闭
|
// 正常流程:连接关闭回调已执行,CloseChan 被关闭
|
||||||
case <-time.After(kickTimeout):
|
case <-time.After(kickTimeout):
|
||||||
player1.Save()
|
timeout = true
|
||||||
service.NewBaseSysLogService().RecordKick(uint32(player1.Info.UserID), fmt.Errorf("踢人操作超时(超时时间:%v)", kickTimeout).Error())
|
}
|
||||||
// 超时处理:避免永久阻塞,可添加日志便于排查问题
|
player1.SaveOnDisconnect()
|
||||||
// 注意:这里不会中断 CloseWithCallback 的执行,仅解除当前协程的阻塞
|
if timeout {
|
||||||
|
service.NewBaseSysLogService().RecordKick(uint32(player1.Info.UserID), fmt.Sprintf("踢人操作超时(超时时间:%v)", kickTimeout))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,13 +15,13 @@ import (
|
|||||||
|
|
||||||
// Save 保存玩家数据
|
// Save 保存玩家数据
|
||||||
func (p *Player) Save() {
|
func (p *Player) Save() {
|
||||||
cool.CacheManager.Remove(context.TODO(), fmt.Sprintf("player:%d", p.Info.UserID))
|
if p == nil || p.Info == nil {
|
||||||
|
|
||||||
if p.Info == nil {
|
|
||||||
return
|
return
|
||||||
|
|
||||||
}
|
}
|
||||||
|
cacheCtx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
cool.CacheManager.Remove(cacheCtx, fmt.Sprintf("player:%d", p.Info.UserID))
|
||||||
newtime := time.Now().Unix()
|
newtime := time.Now().Unix()
|
||||||
p.Info.TimeToday = p.Info.TimeToday + newtime - int64(p.Logintime) //保存电池时间
|
p.Info.TimeToday = p.Info.TimeToday + newtime - int64(p.Logintime) //保存电池时间
|
||||||
// p.Info.FightTime = p.Info.FightTime + (newtime - int64(p.Logintime))
|
// p.Info.FightTime = p.Info.FightTime + (newtime - int64(p.Logintime))
|
||||||
@@ -52,7 +52,11 @@ func (p *Player) Save() {
|
|||||||
|
|
||||||
}
|
}
|
||||||
p.IsLogin = false
|
p.IsLogin = false
|
||||||
p.Service.Info.Save(*p.Info)
|
if p.Service != nil && p.Service.Info != nil {
|
||||||
|
p.Service.Info.SaveUntilSuccess(*p.Info)
|
||||||
|
} else {
|
||||||
|
cool.Logger.Error(context.TODO(), "player save skipped: service not ready", p.Info.UserID)
|
||||||
|
}
|
||||||
space.GetSpace(p.Info.MapID).LeaveMap(p)
|
space.GetSpace(p.Info.MapID).LeaveMap(p)
|
||||||
|
|
||||||
p.MapNPC.Stop() //停止刷怪
|
p.MapNPC.Stop() //停止刷怪
|
||||||
@@ -61,6 +65,46 @@ func (p *Player) Save() {
|
|||||||
share.ShareManager.DeleteUserOnline(p.Info.UserID) //设置用户登录服务器
|
share.ShareManager.DeleteUserOnline(p.Info.UserID) //设置用户登录服务器
|
||||||
|
|
||||||
}
|
}
|
||||||
|
func (p *Player) SaveOnDisconnect() {
|
||||||
|
if p == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
p.logoutMu.Lock()
|
||||||
|
if p.logoutSaved {
|
||||||
|
p.logoutMu.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if p.logoutDone != nil {
|
||||||
|
done := p.logoutDone
|
||||||
|
p.logoutMu.Unlock()
|
||||||
|
<-done
|
||||||
|
return
|
||||||
|
}
|
||||||
|
done := make(chan struct{})
|
||||||
|
p.logoutDone = done
|
||||||
|
p.logoutMu.Unlock()
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if recoverErr := recover(); recoverErr != nil {
|
||||||
|
if p.Info != nil {
|
||||||
|
cool.Logger.Error(context.TODO(), "SaveOnDisconnect panic", p.Info.UserID, recoverErr)
|
||||||
|
} else {
|
||||||
|
cool.Logger.Error(context.TODO(), "SaveOnDisconnect panic", recoverErr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p.logoutMu.Lock()
|
||||||
|
p.logoutSaved = true
|
||||||
|
if p.logoutDone == done {
|
||||||
|
close(done)
|
||||||
|
p.logoutDone = nil
|
||||||
|
}
|
||||||
|
p.logoutMu.Unlock()
|
||||||
|
}()
|
||||||
|
|
||||||
|
p.Save()
|
||||||
|
}
|
||||||
func (p *Player) CanGet() bool {
|
func (p *Player) CanGet() bool {
|
||||||
if p.Info.TimeToday >= p.Info.TimeLimit {
|
if p.Info.TimeToday >= p.Info.TimeLimit {
|
||||||
return false
|
return false
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user