lawless/server/modules/alchemy.go

352 行
10 KiB
Go

此文件含有模棱两可的 Unicode 字符

此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。

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