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)
|
|||
|
|
}
|