lawless/server/modules/formation.go

271 行
7.7 KiB
Go

// Package modules - 阵法系统模块
// 对齐GDD-03 附.C + GDD-27 二 阵法深化系统
package modules
import (
"context"
"database/sql"
"encoding/json"
"errors"
"math/rand"
"github.com/heroiclabs/nakama-common/runtime"
"github.com/jackc/pgx/v5"
)
// RegisterFormation 注册阵法相关 RPC。
func RegisterFormation(initializer runtime.Initializer) error {
rpcs := map[string]func(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, payload string) (string, error){
"FormationService/DeployFormation": deployFormation,
"FormationService/BreakFormation": breakFormation,
"FormationService/GetFormations": getFormations,
}
for path, fn := range rpcs {
if err := initializer.RegisterRpc(path, fn); err != nil {
return err
}
}
return nil
}
type deployFormationReq struct {
FormationType string `json:"formation_type"` // attack/defense/control/encapsulate/support
Grade string `json:"grade"` // mortal/yellow/xuan/di/celestial/immortal
Materials []string `json:"materials"` // 阵旗/材料ID列表
}
type breakFormationReq struct {
FormationID string `json:"formation_id"`
}
type getFormationsReq struct {
CharacterID string `json:"character_id"`
LocationType string `json:"location_type"` // character/guild/zone
}
type formationData struct {
ID string `json:"id"`
FormationType string `json:"formation_type"`
Grade string `json:"grade"`
Level int32 `json:"level"`
Stats interface{} `json:"stats"`
IsActive bool `json:"is_active"`
LocationType string `json:"location_type"`
}
// 阵法品阶 → 成功率/效果系数映射
var formationGradeCoeff = map[string]struct {
successRate float64
effectCoef float64
}{
"mortal": {0.95, 1.0},
"yellow": {0.85, 1.3},
"xuan": {0.75, 1.8},
"di": {0.65, 2.5},
"celestial": {0.55, 3.5},
"immortal": {0.45, 5.0},
}
func deployFormation(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, payload string) (string, error) {
traceID := newTraceID()
uid := userIDFromCtx(ctx)
if uid == "" {
return errResp(1001, "missing token", traceID)
}
var req deployFormationReq
if err := json.Unmarshal([]byte(payload), &req); err != nil {
return errResp(2001, "invalid payload", traceID)
}
// 获取角色ID
var charID string
var realmTier int32
err := hhdbPool.QueryRow(ctx, `
SELECT id, realm_tier FROM characters WHERE player_id = $1 AND status = 'active'
ORDER BY created_at DESC LIMIT 1
`, uid).Scan(&charID, &realmTier)
if err != nil {
return errResp(4002, "character not found", traceID)
}
// 检查境界要求
gradeRealmReq := map[string]int32{
"mortal": 1, "yellow": 2, "xuan": 3, "di": 4, "celestial": 5, "immortal": 6,
}
if realmTier < gradeRealmReq[req.Grade] {
return errResp(5003, "realm too low for this formation grade", traceID)
}
// 检查是否在战斗中(战斗中不可布阵)
var inBattle bool
err = hhdbPool.QueryRow(ctx, `
SELECT EXISTS(
SELECT 1 FROM battles
WHERE (attacker_id = $1 OR defender_id = $1)
AND status = 'in_progress'
)
`, charID).Scan(&inBattle)
if err == nil && inBattle {
return errResp(5004, "cannot deploy formation in battle", traceID)
}
// 计算布阵成功率
coeff, ok := formationGradeCoeff[req.Grade]
if !ok {
return errResp(2001, "invalid grade", traceID)
}
successRate := coeff.successRate
if rand.Float64() > successRate {
return okResp(map[string]interface{}{
"success": false,
"message": "布阵失败,材料已消耗",
"rate": successRate,
}, traceID)
}
// 创建阵法
formationID := genUUID()
_, err = hhdbPool.Exec(ctx, `
INSERT INTO formations (id, character_id, formation_type, name, grade, level, stats, is_active, created_at)
VALUES ($1, $2, $3, $4, $5, 1, '{}', true, NOW())
`, formationID, charID, req.FormationType, req.FormationType+"阵", req.Grade)
if err != nil {
logger.Error("deploy formation insert failed: %v", err)
return errResp(9002, "internal error", traceID)
}
return okResp(map[string]interface{}{
"success": true,
"formation_id": formationID,
"formation_type": req.FormationType,
"grade": req.Grade,
"effect_coef": coeff.effectCoef,
"message": "布阵成功",
}, traceID)
}
func breakFormation(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, payload string) (string, error) {
traceID := newTraceID()
uid := userIDFromCtx(ctx)
if uid == "" {
return errResp(1001, "missing token", traceID)
}
var req breakFormationReq
if err := json.Unmarshal([]byte(payload), &req); err != nil {
return errResp(2001, "invalid payload", traceID)
}
// 验证阵法归属
var charID string
err := hhdbPool.QueryRow(ctx, `
SELECT f.character_id FROM formations f
JOIN characters c ON f.character_id = c.id
WHERE f.id = $1 AND c.player_id = $2
`, req.FormationID, uid).Scan(&charID)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return errResp(5001, "formation not found", traceID)
}
return errResp(9002, "internal error", traceID)
}
// 破阵判定(基于悟性)
var wisdom float64
err = hhdbPool.QueryRow(ctx, `
SELECT (base_stats->>'悟')::float FROM characters WHERE id = $1
`, charID).Scan(&wisdom)
if err != nil {
wisdom = 15.0 // 默认值
}
breakRate := 0.3 + wisdom*0.01 // 基础30% + 悟性加成
if breakRate > 0.95 {
breakRate = 0.95
}
success := rand.Float64() < breakRate
if success {
// 破阵成功,删除阵法
_, err = hhdbPool.Exec(ctx, `DELETE FROM formations WHERE id = $1`, req.FormationID)
if err != nil {
return errResp(9002, "internal error", traceID)
}
return okResp(map[string]interface{}{
"success": true,
"message": "破阵成功",
}, traceID)
}
// 破阵失败,获得debuff
return okResp(map[string]interface{}{
"success": false,
"message": "破阵失败,受到阵法反噬",
"debuff": map[string]interface{}{
"type": "formation_backlash",
"duration": 1800, // 30游戏秒
"penalty": "全属性-5%",
},
}, traceID)
}
func getFormations(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, payload string) (string, error) {
traceID := newTraceID()
uid := userIDFromCtx(ctx)
if uid == "" {
return errResp(1001, "missing token", traceID)
}
var req getFormationsReq
if err := json.Unmarshal([]byte(payload), &req); err != nil {
return errResp(2001, "invalid payload", traceID)
}
var rows pgx.Rows
var err error
switch req.LocationType {
case "character":
rows, err = hhdbPool.Query(ctx, `
SELECT f.id, f.formation_type, f.grade, f.level, f.stats, f.is_active, 'character'
FROM formations f
JOIN characters c ON f.character_id = c.id
WHERE c.player_id = $1 AND c.id = $2 AND f.zone_id IS NULL AND f.guild_id IS NULL
ORDER BY f.grade DESC
`, uid, req.CharacterID)
case "guild":
rows, err = hhdbPool.Query(ctx, `
SELECT f.id, f.formation_type, f.grade, f.level, f.stats, f.is_active, 'guild'
FROM formations f
JOIN characters c ON f.character_id = c.id
JOIN guild_members gm ON gm.character_id = c.id
WHERE c.player_id = $1 AND f.guild_id IS NOT NULL
ORDER BY f.grade DESC
`, uid)
default:
return errResp(2001, "invalid location_type", traceID)
}
if err != nil {
logger.Error("get formations failed: %v", err)
return errResp(9002, "internal error", traceID)
}
defer rows.Close()
var formations []formationData
for rows.Next() {
var f formationData
if err := rows.Scan(&f.ID, &f.FormationType, &f.Grade, &f.Level, &f.Stats, &f.IsActive, &f.LocationType); err != nil {
continue
}
formations = append(formations, f)
}
return okResp(map[string]interface{}{
"formations": formations,
"count": len(formations),
}, traceID)
}