2026-07-02 16:55:34 +08:00
|
|
|
// Package modules - 炼器系统模块
|
|
|
|
|
// 对齐GDD-05 4.7 炼器流程详解 + GDD-27 九 天材地宝系统
|
|
|
|
|
package modules
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"database/sql"
|
|
|
|
|
"encoding/json"
|
|
|
|
|
"math/rand"
|
|
|
|
|
|
|
|
|
|
"github.com/heroiclabs/nakama-common/runtime"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// RegisterForging 注册炼器相关 RPC。
|
|
|
|
|
func RegisterForging(initializer runtime.Initializer) error {
|
2026-07-03 21:34:51 +08:00
|
|
|
rpcs := map[string]func(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, payload string) (string, error){
|
2026-07-02 16:55:34 +08:00
|
|
|
"ForgingService/CraftEquipment": craftEquipment,
|
|
|
|
|
"ForgingService/GetBlueprints": getBlueprints,
|
|
|
|
|
"ForgingService/RepairEquipment": repairEquipment,
|
|
|
|
|
}
|
|
|
|
|
for path, fn := range rpcs {
|
|
|
|
|
if err := initializer.RegisterRpc(path, fn); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --- 请求/响应结构 ---
|
|
|
|
|
|
|
|
|
|
type craftEquipmentReq struct {
|
|
|
|
|
BlueprintID string `json:"blueprint_id"` // 图纸ID
|
|
|
|
|
Materials []string `json:"materials"` // 材料ID列表
|
|
|
|
|
Technique string `json:"technique"` // 刚猛/柔韧/均衡/精密/狂暴
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type getBlueprintsReq struct {
|
|
|
|
|
CharacterID string `json:"character_id"`
|
|
|
|
|
Grade string `json:"grade"` // 可选:筛选品阶
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type repairEquipmentReq struct {
|
|
|
|
|
EquipmentID string `json:"equipment_id"`
|
|
|
|
|
Materials []string `json:"materials"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type blueprintData struct {
|
|
|
|
|
ID string `json:"id"`
|
|
|
|
|
Name string `json:"name"`
|
|
|
|
|
Grade string `json:"grade"` // mortal/yellow/xuan/di/celestial/immortal
|
|
|
|
|
EquipType string `json:"equip_type"` // weapon/armor/accessory
|
|
|
|
|
RequiredMaterials interface{} `json:"required_materials"`
|
|
|
|
|
RequiredLevel int32 `json:"required_level"`
|
|
|
|
|
SuccessRate float64 `json:"success_rate"`
|
|
|
|
|
BaseStats interface{} `json:"base_stats"`
|
|
|
|
|
TechniqueBonus map[string]map[string]float64 `json:"technique_bonus"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type craftedEquipData struct {
|
|
|
|
|
BlueprintID string `json:"blueprint_id"`
|
|
|
|
|
BlueprintName string `json:"blueprint_name"`
|
|
|
|
|
Grade string `json:"grade"`
|
|
|
|
|
EquipType string `json:"equip_type"`
|
|
|
|
|
Success bool `json:"success"`
|
|
|
|
|
Quality string `json:"quality"` // normal/fine/excellent/perfect
|
|
|
|
|
Stats interface{} `json:"stats"`
|
|
|
|
|
Skills interface{} `json:"skills"`
|
|
|
|
|
SetID string `json:"set_id"`
|
|
|
|
|
ExpGain int32 `json:"exp_gain"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --- 图纸配置 ---
|
|
|
|
|
|
|
|
|
|
var defaultBlueprints = map[string]blueprintData{
|
|
|
|
|
"bp_iron_sword": {
|
|
|
|
|
ID: "bp_iron_sword", Name: "铁剑图纸", Grade: "mortal", EquipType: "weapon",
|
|
|
|
|
RequiredMaterials: []string{"mineral_xuantie"},
|
|
|
|
|
RequiredLevel: 1, SuccessRate: 0.95,
|
|
|
|
|
BaseStats: map[string]interface{}{"atk": 10, "def": 0},
|
|
|
|
|
TechniqueBonus: map[string]map[string]float64{
|
|
|
|
|
"strong": {"atk": 1.15, "def": 0.95},
|
|
|
|
|
"flexible": {"atk": 0.95, "def": 1.1},
|
|
|
|
|
"balanced": {"atk": 1.0, "def": 1.0},
|
|
|
|
|
"precise": {"atk": 1.05, "def": 1.05},
|
|
|
|
|
"frenzy": {"atk": 1.2, "def": 0.9, "crit": 0.1},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"bp_steel_armor": {
|
|
|
|
|
ID: "bp_steel_armor", Name: "玄铁甲图纸", Grade: "yellow", EquipType: "armor",
|
|
|
|
|
RequiredMaterials: []string{"mineral_xuantie", "mineral_hantie"},
|
|
|
|
|
RequiredLevel: 2, SuccessRate: 0.85,
|
|
|
|
|
BaseStats: map[string]interface{}{"atk": 0, "def": 25, "hp": 100},
|
|
|
|
|
TechniqueBonus: map[string]map[string]float64{
|
|
|
|
|
"strong": {"atk": 1.0, "def": 1.1},
|
|
|
|
|
"flexible": {"atk": 0.95, "def": 1.15},
|
|
|
|
|
"balanced": {"atk": 1.0, "def": 1.1},
|
|
|
|
|
"precise": {"atk": 1.0, "def": 1.12},
|
|
|
|
|
"frenzy": {"atk": 1.05, "def": 1.0},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"bp_spirit_blade": {
|
|
|
|
|
ID: "bp_spirit_blade", Name: "灵器刀图纸", Grade: "xuan", EquipType: "weapon",
|
|
|
|
|
RequiredMaterials: []string{"mineral_miinyin", "mineral_jingjin"},
|
|
|
|
|
RequiredLevel: 3, SuccessRate: 0.75,
|
|
|
|
|
BaseStats: map[string]interface{}{"atk": 45, "def": 5, "crit": 0.05},
|
|
|
|
|
TechniqueBonus: map[string]map[string]float64{
|
|
|
|
|
"strong": {"atk": 1.2, "def": 0.9},
|
|
|
|
|
"flexible": {"atk": 1.0, "def": 1.1},
|
|
|
|
|
"balanced": {"atk": 1.1, "def": 1.05},
|
|
|
|
|
"precise": {"atk": 1.15, "def": 1.0, "crit": 0.05},
|
|
|
|
|
"frenzy": {"atk": 1.25, "def": 0.85, "crit": 0.15},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"bp_dragon_scale": {
|
|
|
|
|
ID: "bp_dragon_scale", Name: "龙鳞甲图纸", Grade: "di", EquipType: "armor",
|
|
|
|
|
RequiredMaterials: []string{"mineral_longlinshi", "mineral_jingjin"},
|
|
|
|
|
RequiredLevel: 4, SuccessRate: 0.65,
|
|
|
|
|
BaseStats: map[string]interface{}{"atk": 10, "def": 80, "hp": 500, "element_resist": 0.15},
|
|
|
|
|
TechniqueBonus: map[string]map[string]float64{
|
|
|
|
|
"strong": {"atk": 1.1, "def": 1.1},
|
|
|
|
|
"flexible": {"atk": 0.95, "def": 1.2},
|
|
|
|
|
"balanced": {"atk": 1.05, "def": 1.15},
|
|
|
|
|
"precise": {"atk": 1.0, "def": 1.18},
|
|
|
|
|
"frenzy": {"atk": 1.15, "def": 1.0},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"bp_celestial_sword": {
|
|
|
|
|
ID: "bp_celestial_sword", Name: "天品神剑图纸", Grade: "celestial", EquipType: "weapon",
|
|
|
|
|
RequiredMaterials: []string{"mineral_tianwaiyuntie", "mineral_jingjin", "herb_tianlingye"},
|
|
|
|
|
RequiredLevel: 5, SuccessRate: 0.50,
|
|
|
|
|
BaseStats: map[string]interface{}{"atk": 120, "def": 15, "crit": 0.1, "element_dmg": 0.2},
|
|
|
|
|
TechniqueBonus: map[string]map[string]float64{
|
|
|
|
|
"strong": {"atk": 1.25, "def": 0.85},
|
|
|
|
|
"flexible": {"atk": 1.0, "def": 1.15},
|
|
|
|
|
"balanced": {"atk": 1.15, "def": 1.05},
|
|
|
|
|
"precise": {"atk": 1.2, "def": 1.0, "crit": 0.1},
|
|
|
|
|
"frenzy": {"atk": 1.3, "def": 0.8, "crit": 0.2},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"bp_immortal_artifact": {
|
|
|
|
|
ID: "bp_immortal_artifact", Name: "仙器图纸", Grade: "immortal", EquipType: "weapon",
|
|
|
|
|
RequiredMaterials: []string{"mineral_hundunqingjin", "mineral_hundunzhishi", "herb_hundunlingzhi"},
|
|
|
|
|
RequiredLevel: 6, SuccessRate: 0.35,
|
|
|
|
|
BaseStats: map[string]interface{}{"atk": 250, "def": 30, "crit": 0.15, "element_dmg": 0.35, "special": "chaos_damage"},
|
|
|
|
|
TechniqueBonus: map[string]map[string]float64{
|
|
|
|
|
"strong": {"atk": 1.3, "def": 0.8},
|
|
|
|
|
"flexible": {"atk": 1.0, "def": 1.2},
|
|
|
|
|
"balanced": {"atk": 1.15, "def": 1.1},
|
|
|
|
|
"precise": {"atk": 1.2, "def": 1.05, "crit": 0.1},
|
|
|
|
|
"frenzy": {"atk": 1.35, "def": 0.75, "crit": 0.25},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --- RPC 实现 ---
|
|
|
|
|
|
|
|
|
|
func craftEquipment(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 craftEquipmentReq
|
|
|
|
|
if err := json.Unmarshal([]byte(payload), &req); err != nil {
|
|
|
|
|
return errResp(2001, "invalid payload", traceID)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 获取图纸
|
|
|
|
|
bp, ok := defaultBlueprints[req.BlueprintID]
|
|
|
|
|
if !ok {
|
|
|
|
|
return errResp(6101, "blueprint not found", traceID)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 获取角色信息
|
|
|
|
|
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[bp.Grade] {
|
|
|
|
|
return errResp(6102, "realm too low for this blueprint", traceID)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 计算成功率
|
|
|
|
|
successRate := bp.SuccessRate
|
|
|
|
|
|
|
|
|
|
// 手法加成
|
|
|
|
|
if techniqueBonus, ok := bp.TechniqueBonus[req.Technique]; ok {
|
|
|
|
|
if atkBonus, ok := techniqueBonus["atk"]; ok && atkBonus > 1.0 {
|
|
|
|
|
successRate *= 0.95 // 高攻击手法成功率略降
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 限制范围
|
|
|
|
|
if successRate < 0.1 {
|
|
|
|
|
successRate = 0.1
|
|
|
|
|
}
|
|
|
|
|
if successRate > 0.99 {
|
|
|
|
|
successRate = 0.99
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 判定成功/失败
|
|
|
|
|
success := rand.Float64() < successRate
|
|
|
|
|
|
|
|
|
|
var result craftedEquipData
|
|
|
|
|
result.BlueprintID = req.BlueprintID
|
|
|
|
|
result.BlueprintName = bp.Name
|
|
|
|
|
result.Grade = bp.Grade
|
|
|
|
|
result.EquipType = bp.EquipType
|
|
|
|
|
result.Success = success
|
|
|
|
|
|
|
|
|
|
if success {
|
|
|
|
|
// 成功:产出装备
|
|
|
|
|
quality := "normal"
|
|
|
|
|
if rand.Float64() < 0.15 {
|
|
|
|
|
quality = "fine"
|
|
|
|
|
}
|
|
|
|
|
if rand.Float64() < 0.03 {
|
|
|
|
|
quality = "excellent"
|
|
|
|
|
}
|
|
|
|
|
if rand.Float64() < 0.003 {
|
|
|
|
|
quality = "perfect"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
result.Quality = quality
|
|
|
|
|
result.Stats = bp.BaseStats
|
|
|
|
|
result.ExpGain = 15 + int32(realmTier*8)
|
|
|
|
|
|
|
|
|
|
// 根据品质加成属性
|
|
|
|
|
qualityMulti := map[string]float64{
|
|
|
|
|
"normal": 1.0, "fine": 1.1, "excellent": 1.2, "perfect": 1.3,
|
|
|
|
|
}
|
|
|
|
|
multi := qualityMulti[quality]
|
|
|
|
|
|
|
|
|
|
// 应用手法加成
|
|
|
|
|
if techniqueBonus, ok := bp.TechniqueBonus[req.Technique]; ok {
|
|
|
|
|
result.Stats = applyTechniqueBonus(result.Stats, techniqueBonus, multi)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 更新角色经验
|
|
|
|
|
_, err = hhdbPool.Exec(ctx, `
|
|
|
|
|
UPDATE characters SET exp = exp + $1, updated_at = NOW() WHERE id = $2
|
|
|
|
|
`, result.ExpGain, charID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
logger.Error("craft equip update failed: %v", err)
|
|
|
|
|
return errResp(9002, "internal error", traceID)
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// 失败:消耗材料,产出废品
|
|
|
|
|
result.Quality = "normal"
|
|
|
|
|
result.Stats = map[string]interface{}{"atk": 0, "def": 0}
|
|
|
|
|
result.ExpGain = 5
|
|
|
|
|
|
|
|
|
|
_, err = hhdbPool.Exec(ctx, `
|
|
|
|
|
UPDATE characters SET exp = exp + $1, updated_at = NOW() WHERE id = $2
|
|
|
|
|
`, result.ExpGain, charID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
logger.Error("craft equip fail update failed: %v", err)
|
|
|
|
|
return errResp(9002, "internal error", traceID)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return okResp(result, traceID)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func getBlueprints(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 getBlueprintsReq
|
|
|
|
|
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 blueprints []blueprintData
|
|
|
|
|
for _, bp := range defaultBlueprints {
|
|
|
|
|
if realmTier >= gradeRealmReq[bp.Grade] {
|
|
|
|
|
if req.Grade == "" || req.Grade == bp.Grade {
|
|
|
|
|
blueprints = append(blueprints, bp)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return okResp(map[string]interface{}{
|
|
|
|
|
"blueprints": blueprints,
|
|
|
|
|
"count": len(blueprints),
|
|
|
|
|
}, traceID)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func repairEquipment(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 repairEquipmentReq
|
|
|
|
|
if err := json.Unmarshal([]byte(payload), &req); err != nil {
|
|
|
|
|
return errResp(2001, "invalid payload", traceID)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 验证装备归属
|
|
|
|
|
var charID string
|
|
|
|
|
var durability, maxDurability int32
|
|
|
|
|
err := hhdbPool.QueryRow(ctx, `
|
|
|
|
|
SELECT a.character_id, a.durability, a.max_durability
|
|
|
|
|
FROM artifacts a
|
|
|
|
|
JOIN characters c ON a.character_id = c.id
|
|
|
|
|
WHERE a.id = $1 AND c.player_id = $2
|
|
|
|
|
`, req.EquipmentID, uid).Scan(&charID, &durability, &maxDurability)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return errResp(6103, "equipment not found", traceID)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if durability >= maxDurability {
|
|
|
|
|
return errResp(6104, "equipment already at max durability", traceID)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 修复:恢复耐久
|
|
|
|
|
newDurability := durability + int32(len(req.Materials)*10)
|
|
|
|
|
if newDurability > maxDurability {
|
|
|
|
|
newDurability = maxDurability
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_, err = hhdbPool.Exec(ctx, `
|
|
|
|
|
UPDATE artifacts SET durability = $1, updated_at = NOW() WHERE id = $2
|
|
|
|
|
`, newDurability, req.EquipmentID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
logger.Error("repair equipment failed: %v", err)
|
|
|
|
|
return errResp(9002, "internal error", traceID)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return okResp(map[string]interface{}{
|
|
|
|
|
"success": true,
|
|
|
|
|
"repaired_durability": newDurability,
|
|
|
|
|
"max_durability": maxDurability,
|
|
|
|
|
"message": "装备修复成功",
|
|
|
|
|
}, traceID)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --- 辅助函数 ---
|
|
|
|
|
|
|
|
|
|
func applyTechniqueBonus(stats interface{}, techniqueBonus map[string]float64, qualityMulti float64) map[string]interface{} {
|
|
|
|
|
statsMap, ok := stats.(map[string]interface{})
|
|
|
|
|
if !ok {
|
|
|
|
|
return map[string]interface{}{}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
result := make(map[string]interface{})
|
|
|
|
|
for k, v := range statsMap {
|
|
|
|
|
if floatVal, ok := v.(float64); ok {
|
|
|
|
|
bonus := 1.0
|
|
|
|
|
if k == "atk" {
|
|
|
|
|
bonus = techniqueBonus["atk"]
|
|
|
|
|
} else if k == "def" {
|
|
|
|
|
bonus = techniqueBonus["def"]
|
|
|
|
|
} else if k == "crit" {
|
|
|
|
|
if critBonus, ok := techniqueBonus["crit"]; ok {
|
|
|
|
|
bonus = 1.0 + critBonus
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
result[k] = floatVal * bonus * qualityMulti
|
|
|
|
|
} else {
|
|
|
|
|
result[k] = v
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return result
|
|
|
|
|
}
|