352 行
10 KiB
Go
352 行
10 KiB
Go
// Package modules - 炼丹系统模块
|
||
// 对齐GDD-05 4.6 炼丹流程详解 + GDD-27 三 天材地宝系统
|
||
package modules
|
||
|
||
import (
|
||
"context"
|
||
"database/sql"
|
||
"encoding/json"
|
||
"math/rand"
|
||
"time"
|
||
|
||
"github.com/heroiclabs/nakama-common/runtime"
|
||
"github.com/jackc/pgx/v5"
|
||
)
|
||
|
||
// RegisterAlchemy 注册炼丹相关 RPC。
|
||
func RegisterAlchemy(initializer runtime.Initializer) error {
|
||
rpcs := map[string]func(runtime.Initializer) error{
|
||
"AlchemyService/CraftPill": craftPill,
|
||
"AlchemyService/GetRecipes": getRecipes,
|
||
"AlchemyService/GetPillList": getPillList,
|
||
}
|
||
for path, fn := range rpcs {
|
||
if err := initializer.RegisterRpc(path, fn); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// --- 请求/响应结构 ---
|
||
|
||
type craftPillReq struct {
|
||
RecipeID string `json:"recipe_id"` // 丹方ID
|
||
Materials []string `json:"materials"` // 药材ID列表
|
||
FireStage string `json:"fire_stage"` // wen/wu/meng/wenlow 文火/武火/猛火/温火
|
||
}
|
||
|
||
type getRecipesReq struct {
|
||
CharacterID string `json:"character_id"`
|
||
Grade string `json:"grade"` // 可选:筛选品阶
|
||
}
|
||
|
||
type getPillListReq struct {
|
||
CharacterID string `json:"character_id"`
|
||
}
|
||
|
||
type pillRecipeData struct {
|
||
ID string `json:"id"`
|
||
Name string `json:"name"`
|
||
Grade string `json:"grade"` // mortal/yellow/xuan/di/celestial/immortal
|
||
RequiredHerbs interface{} `json:"required_herbs"`
|
||
RequiredLevel int32 `json:"required_level"`
|
||
SuccessRate float64 `json:"success_rate"`
|
||
PillEffect interface{} `json:"pill_effect"`
|
||
FireStageBonus map[string]float64 `json:"fire_stage_bonus"`
|
||
}
|
||
|
||
type craftedPillData struct {
|
||
RecipeID string `json:"recipe_id"`
|
||
RecipeName string `json:"recipe_name"`
|
||
Grade string `json:"grade"`
|
||
Success bool `json:"success"`
|
||
Quantity int32 `json:"quantity"`
|
||
Quality string `json:"quality"` // low/mid/high/perfect
|
||
PillEffect interface{} `json:"pill_effect"`
|
||
DantoxCost int32 `json:"dantox_cost"`
|
||
ExpGain int32 `json:"exp_gain"`
|
||
}
|
||
|
||
// --- 丹方配置(从Nacos加载,此处为默认值)---
|
||
|
||
var defaultRecipes = map[string]pillRecipeData{
|
||
"pill_huiqi": {
|
||
ID: "pill_huiqi", Name: "回气丹", Grade: "mortal",
|
||
RequiredHerbs: []string{"herb_qinglingcao"},
|
||
RequiredLevel: 1, SuccessRate: 0.95,
|
||
PillEffect: map[string]interface{}{"type": "energy_restore", "value": 500},
|
||
FireStageBonus: map[string]float64{"wen": 1.0, "wu": 0.9, "meng": 0.7, "wenlow": 1.1},
|
||
},
|
||
"pill_peiyuan": {
|
||
ID: "pill_peiyuan", Name: "培元丹", Grade: "yellow",
|
||
RequiredHerbs: []string{"herb_qianlingzhi", "herb_bixueteng"},
|
||
RequiredLevel: 2, SuccessRate: 0.85,
|
||
PillEffect: map[string]interface{}{"type": "exp_boost", "value": 0.1, "duration": 3600},
|
||
FireStageBonus: map[string]float64{"wen": 1.0, "wu": 0.85, "meng": 0.6, "wenlow": 1.15},
|
||
},
|
||
"pill_jindan": {
|
||
ID: "pill_jindan", Name: "金丹固本丹", Grade: "xuan",
|
||
RequiredHerbs: []string{"herb_jiuyelingzhi", "herb_bixueteng", "mineral_jingjin"},
|
||
RequiredLevel: 3, SuccessRate: 0.75,
|
||
PillEffect: map[string]interface{}{"type": "realm_protect", "value": 0.15},
|
||
FireStageBonus: map[string]float64{"wen": 1.0, "wu": 0.8, "meng": 0.5, "wenlow": 1.2},
|
||
},
|
||
"pill_duhai": {
|
||
ID: "pill_duhai", Name: "渡劫丹", Grade: "di",
|
||
RequiredHerbs: []string{"herb_wannianxuelian", "herb_dixinlingru", "mineral_jingjin"},
|
||
RequiredLevel: 4, SuccessRate: 0.65,
|
||
PillEffect: map[string]interface{}{"type": "tribulation_boost", "success_rate": 0.15, "break_protect": 0.3},
|
||
FireStageBonus: map[string]float64{"wen": 1.0, "wu": 0.75, "meng": 0.4, "wenlow": 1.25},
|
||
},
|
||
"pill_jiuzhuan": {
|
||
ID: "pill_jiuzhuan", Name: "九转还魂丹", Grade: "celestial",
|
||
RequiredHerbs: []string{"herb_jiuzhuanhuanhuncao", "herb_tianlingye", "mineral_jingjin"},
|
||
RequiredLevel: 5, SuccessRate: 0.50,
|
||
PillEffect: map[string]interface{}{"type": "resurrect", "hp_restore": 1.0, "energy_restore": 1.0},
|
||
FireStageBonus: map[string]float64{"wen": 1.0, "wu": 0.7, "meng": 0.3, "wenlow": 1.3},
|
||
},
|
||
"pill_hundun": {
|
||
ID: "pill_hundun", Name: "混沌护体丹", Grade: "immortal",
|
||
RequiredHerbs: []string{"herb_hundunlingzhi", "herb_hundunzhishui", "mineral_hundunqingjin"},
|
||
RequiredLevel: 6, SuccessRate: 0.35,
|
||
PillEffect: map[string]interface{}{"type": "chaos_protect", "all_attr": 0.2, "chaos_resist": 0.3, "san_reduce": 0.5},
|
||
FireStageBonus: map[string]float64{"wen": 1.0, "wu": 0.65, "meng": 0.25, "wenlow": 1.35},
|
||
},
|
||
}
|
||
|
||
// --- RPC 实现 ---
|
||
|
||
func craftPill(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 craftPillReq
|
||
if err := json.Unmarshal([]byte(payload), &req); err != nil {
|
||
return errResp(2001, "invalid payload", traceID)
|
||
}
|
||
|
||
// 获取丹方
|
||
recipe, ok := defaultRecipes[req.RecipeID]
|
||
if !ok {
|
||
return errResp(6001, "recipe not found", traceID)
|
||
}
|
||
|
||
// 获取角色信息
|
||
var charID string
|
||
var realmTier int32
|
||
var dantoxLevel int32
|
||
var purity float64
|
||
err := hhdbPool.QueryRow(ctx, `
|
||
SELECT id, realm_tier, dantox_level, energy_purity
|
||
FROM characters WHERE player_id = $1 AND status = 'active'
|
||
ORDER BY created_at DESC LIMIT 1
|
||
`, uid).Scan(&charID, &realmTier, &dantoxLevel, &purity)
|
||
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[recipe.Grade] {
|
||
return errResp(6002, "realm too low for this recipe", traceID)
|
||
}
|
||
|
||
// 计算成功率
|
||
successRate := recipe.SuccessRate
|
||
|
||
// 火候加成
|
||
if bonus, ok := recipe.FireStageBonus[req.FireStage]; ok {
|
||
successRate *= bonus
|
||
}
|
||
|
||
// 丹毒惩罚
|
||
if dantoxLevel > 100 {
|
||
successRate *= 0.5
|
||
} else if dantoxLevel > 60 {
|
||
successRate *= 0.75
|
||
}
|
||
|
||
// 纯净度加成
|
||
if purity >= 0.9 {
|
||
successRate *= 1.2
|
||
} else if purity >= 0.7 {
|
||
successRate *= 1.1
|
||
}
|
||
|
||
// 限制范围
|
||
if successRate < 0.1 {
|
||
successRate = 0.1
|
||
}
|
||
if successRate > 0.99 {
|
||
successRate = 0.99
|
||
}
|
||
|
||
// 判定成功/失败
|
||
success := rand.Float64() < successRate
|
||
|
||
var result craftedPillData
|
||
result.RecipeID = req.RecipeID
|
||
result.RecipeName = recipe.Name
|
||
result.Grade = recipe.Grade
|
||
result.Success = success
|
||
|
||
if success {
|
||
// 成功:产出丹药
|
||
quantity := int32(1)
|
||
quality := "mid"
|
||
|
||
// 完美火候产出高品质
|
||
if req.FireStage == "wenlow" && rand.Float64() < 0.3 {
|
||
quality = "high"
|
||
quantity = 2
|
||
} else if req.FireStage == "wen" && rand.Float64() < 0.1 {
|
||
quality = "perfect"
|
||
quantity = 1
|
||
}
|
||
|
||
result.Quantity = quantity
|
||
result.Quality = quality
|
||
result.PillEffect = recipe.PillEffect
|
||
result.ExpGain = 10 + int32(realmTier*5)
|
||
|
||
// 增加丹毒
|
||
gradeDantox := map[string]int32{
|
||
"mortal": 3, "yellow": 5, "xuan": 8, "di": 12, "celestial": 18, "immortal": 25,
|
||
}
|
||
result.DantoxCost = gradeDantox[recipe.Grade]
|
||
|
||
// 更新角色丹毒和经验
|
||
_, err = hhdbPool.Exec(ctx, `
|
||
UPDATE characters
|
||
SET dantox_level = LEAST(dantox_level + $1, 200),
|
||
exp = exp + $2,
|
||
updated_at = NOW()
|
||
WHERE id = $3
|
||
`, result.DantoxCost, result.ExpGain, charID)
|
||
if err != nil {
|
||
logger.Error("craft pill update failed: %v", err)
|
||
return errResp(9002, "internal error", traceID)
|
||
}
|
||
} else {
|
||
// 失败:消耗材料,产出废品
|
||
result.Quantity = 0
|
||
result.DantoxCost = 2 // 失败也有少量丹毒
|
||
result.ExpGain = 3
|
||
|
||
_, err = hhdbPool.Exec(ctx, `
|
||
UPDATE characters
|
||
SET dantox_level = LEAST(dantox_level + $1, 200),
|
||
exp = exp + $2,
|
||
updated_at = NOW()
|
||
WHERE id = $3
|
||
`, result.DantoxCost, result.ExpGain, charID)
|
||
if err != nil {
|
||
logger.Error("craft pill fail update failed: %v", err)
|
||
return errResp(9002, "internal error", traceID)
|
||
}
|
||
}
|
||
|
||
return okResp(result, traceID)
|
||
}
|
||
|
||
func getRecipes(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 getRecipesReq
|
||
if err := json.Unmarshal([]byte(payload), &req); err != nil {
|
||
return errResp(2001, "invalid payload", traceID)
|
||
}
|
||
|
||
// 获取角色境界
|
||
var realmTier int32
|
||
err := hhdbPool.QueryRow(ctx, `
|
||
SELECT realm_tier FROM characters WHERE player_id = $1 AND status = 'active'
|
||
ORDER BY created_at DESC LIMIT 1
|
||
`, uid).Scan(&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,
|
||
}
|
||
|
||
var recipes []pillRecipeData
|
||
for _, recipe := range defaultRecipes {
|
||
if realmTier >= gradeRealmReq[recipe.Grade] {
|
||
if req.Grade == "" || req.Grade == recipe.Grade {
|
||
recipes = append(recipes, recipe)
|
||
}
|
||
}
|
||
}
|
||
|
||
return okResp(map[string]interface{}{
|
||
"recipes": recipes,
|
||
"count": len(recipes),
|
||
}, traceID)
|
||
}
|
||
|
||
func getPillList(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)
|
||
}
|
||
|
||
// 获取角色ID
|
||
var charID string
|
||
err := hhdbPool.QueryRow(ctx, `
|
||
SELECT id FROM characters WHERE player_id = $1 AND status = 'active'
|
||
ORDER BY created_at DESC LIMIT 1
|
||
`, uid).Scan(&charID)
|
||
if err != nil {
|
||
return errResp(4002, "character not found", traceID)
|
||
}
|
||
|
||
// 查询背包中的丹药
|
||
rows, err := hhdbPool.Query(ctx, `
|
||
SELECT i.id, i.name, i.category, inv.quantity, inv.instance_data
|
||
FROM inventories inv
|
||
JOIN items i ON inv.item_id = i.id
|
||
WHERE inv.character_id = $1 AND i.category = 'pill'
|
||
`, charID)
|
||
if err != nil {
|
||
logger.Error("get pill list failed: %v", err)
|
||
return errResp(9002, "internal error", traceID)
|
||
}
|
||
defer rows.Close()
|
||
|
||
type pillItem struct {
|
||
ID string `json:"id"`
|
||
Name string `json:"name"`
|
||
Category string `json:"category"`
|
||
Quantity int32 `json:"quantity"`
|
||
Data interface{} `json:"data"`
|
||
}
|
||
|
||
var pills []pillItem
|
||
for rows.Next() {
|
||
var p pillItem
|
||
if err := rows.Scan(&p.ID, &p.Name, &p.Category, &p.Quantity, &p.Data); err != nil {
|
||
continue
|
||
}
|
||
pills = append(pills, p)
|
||
}
|
||
|
||
return okResp(map[string]interface{}{
|
||
"pills": pills,
|
||
"count": len(pills),
|
||
}, traceID)
|
||
}
|