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