lawless/server/modules/artifact.go

361 行
11 KiB
Go

// Package modules - 法宝对战系统模块
// 对齐GDD-03 附.A 法宝对战系统 + GDD-05 4.9 套装系统
package modules
import (
"context"
"database/sql"
"encoding/json"
"errors"
"math/rand"
"github.com/heroiclabs/nakama-common/runtime"
"github.com/jackc/pgx/v5"
)
// RegisterArtifact 注册法宝相关 RPC。
func RegisterArtifact(initializer runtime.Initializer) error {
rpcs := map[string]func(runtime.Initializer) error{
"ArtifactService/RefineArtifact": refineArtifact,
"ArtifactService/EquipArtifact": equipArtifact,
"ArtifactService/ActivateArtifact": activateArtifact,
"ArtifactService/GetArtifactList": getArtifactList,
"ArtifactService/GetSetBonus": getSetBonus,
}
for path, fn := range rpcs {
if err := initializer.RegisterRpc(path, fn); err != nil {
return err
}
}
return nil
}
// --- 请求/响应结构 ---
type refineArtifactReq struct {
ArtifactID string `json:"artifact_id"`
Materials []string `json:"materials"` // 消耗材料ID列表
}
type equipArtifactReq struct {
ArtifactID string `json:"artifact_id"`
SlotName string `json:"slot_name"` // weapon/armor/accessory
}
type activateArtifactReq struct {
ArtifactID string `json:"artifact_id"`
}
type getArtifactListReq struct {
CharacterID string `json:"character_id"`
}
type getSetBonusReq struct {
CharacterID string `json:"character_id"`
}
type artifactData struct {
ID string `json:"id"`
CharacterID string `json:"character_id"`
ItemID string `json:"item_id"`
ArtifactType string `json:"artifact_type"`
Grade string `json:"grade"`
RefinementLevel int32 `json:"refinement_level"`
SpiritLevel int32 `json:"spirit_level"`
SpiritName string `json:"spirit_name"`
Bound bool `json:"bound"`
Stats interface{} `json:"stats"`
Skills interface{} `json:"skills"`
Durability int32 `json:"durability"`
MaxDurability int32 `json:"max_durability"`
SetID string `json:"set_id"`
}
type setBonusData struct {
SetID string `json:"set_id"`
SetName string `json:"set_name"`
PieceCount int32 `json:"piece_count"`
OwnedPieces int32 `json:"owned_pieces"`
Bonus2 interface{} `json:"bonus_2"`
Bonus4 interface{} `json:"bonus_4"`
Bonus6 interface{} `json:"bonus_6"`
IsActive2 bool `json:"is_active_2"`
IsActive4 bool `json:"is_active_4"`
IsActive6 bool `json:"is_active_6"`
}
// --- RPC 实现 ---
func refineArtifact(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 refineArtifactReq
if err := json.Unmarshal([]byte(payload), &req); err != nil {
return errResp(2001, "invalid payload", traceID)
}
// 查询法宝信息
var artifact artifactData
err := hhdbPool.QueryRow(ctx, `
SELECT a.id, a.character_id, a.item_id, a.artifact_type, a.grade,
a.refinement_level, a.spirit_level, COALESCE(a.spirit_name,''),
a.bound, a.stats, a.skills, a.durability, a.max_durability,
COALESCE(a.set_id,'')
FROM artifacts a
JOIN characters c ON a.character_id = c.id
WHERE a.id = $1 AND c.player_id = $2
`, req.ArtifactID, uid).Scan(
&artifact.ID, &artifact.CharacterID, &artifact.ItemID,
&artifact.ArtifactType, &artifact.Grade, &artifact.RefinementLevel,
&artifact.SpiritLevel, &artifact.SpiritName, &artifact.Bound,
&artifact.Stats, &artifact.Skills, &artifact.Durability,
&artifact.MaxDurability, &artifact.SetID,
)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return errResp(5001, "artifact not found", traceID)
}
logger.Error("refine artifact query failed: %v", err)
return errResp(9002, "internal error", traceID)
}
// 检查耐久
if artifact.Durability <= 0 {
return errResp(5002, "artifact durability depleted", traceID)
}
// 检查境界要求
var realmTier int32
err = hhdbPool.QueryRow(ctx, `SELECT realm_tier FROM characters WHERE id = $1`, artifact.CharacterID).Scan(&realmTier)
if err != nil {
return errResp(9002, "internal error", traceID)
}
gradeRealmReq := map[string]int32{
"mortal": 1, "yellow": 2, "xuan": 3, "di": 4, "celestial": 5, "immortal": 6,
}
if realmTier < gradeRealmReq[artifact.Grade] {
return errResp(5003, "realm too low for this artifact grade", traceID)
}
// 计算炼化成功率 (基础85% + 品阶修正 - 炼化等级惩罚)
successRate := 0.85
successRate -= float64(artifact.RefinementLevel) * 0.05
if artifact.RefinementLevel >= 7 {
successRate -= 0.10
}
if rand.Float64() > successRate {
// 炼化失败,消耗材料但不提升
return okResp(map[string]interface{}{
"success": false,
"message": "炼化失败,材料已消耗",
"rate": successRate,
}, traceID)
}
// 炼化成功
newLevel := artifact.RefinementLevel + 1
_, err = hhdbPool.Exec(ctx, `
UPDATE artifacts SET refinement_level = $1, updated_at = NOW() WHERE id = $2
`, newLevel, artifact.ID)
if err != nil {
logger.Error("refine artifact update failed: %v", err)
return errResp(9002, "internal error", traceID)
}
return okResp(map[string]interface{}{
"success": true,
"refinement_level": newLevel,
"message": "炼化成功",
}, traceID)
}
func equipArtifact(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 equipArtifactReq
if err := json.Unmarshal([]byte(payload), &req); err != nil {
return errResp(2001, "invalid payload", traceID)
}
// 验证法宝归属
var charID string
err := hhdbPool.QueryRow(ctx, `
SELECT a.character_id FROM artifacts a
JOIN characters c ON a.character_id = c.id
WHERE a.id = $1 AND c.player_id = $2
`, req.ArtifactID, uid).Scan(&charID)
if err != nil {
return errResp(5001, "artifact not found", traceID)
}
// 装备到指定槽位(替换旧装备)
_, err = hhdbPool.Exec(ctx, `
INSERT INTO equipments (character_id, slot_name, inventory_id, updated_at)
VALUES ($1, $2, NULL, NOW())
ON CONFLICT (character_id, slot_name)
DO UPDATE SET inventory_id = NULL, updated_at = NOW()
`, charID, req.SlotName)
if err != nil {
logger.Error("equip artifact failed: %v", err)
return errResp(9002, "internal error", traceID)
}
return okResp(map[string]interface{}{
"success": true,
"message": "法宝装备成功",
}, traceID)
}
func activateArtifact(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 activateArtifactReq
if err := json.Unmarshal([]byte(payload), &req); err != nil {
return errResp(2001, "invalid payload", traceID)
}
// 验证法宝归属
var charID string
var spiritLevel int32
err := hhdbPool.QueryRow(ctx, `
SELECT a.character_id, a.spirit_level FROM artifacts a
JOIN characters c ON a.character_id = c.id
WHERE a.id = $1 AND c.player_id = $2
`, req.ArtifactID, uid).Scan(&charID, &spiritLevel)
if err != nil {
return errResp(5001, "artifact not found", traceID)
}
if spiritLevel == 0 {
return errResp(5004, "artifact has no spirit", traceID)
}
// 激活法宝(进入战斗状态)
return okResp(map[string]interface{}{
"success": true,
"spirit_level": spiritLevel,
"message": "法宝已激活",
}, traceID)
}
func getArtifactList(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 getArtifactListReq
if err := json.Unmarshal([]byte(payload), &req); err != nil {
return errResp(2001, "invalid payload", traceID)
}
rows, err := hhdbPool.Query(ctx, `
SELECT a.id, a.character_id, a.item_id, a.artifact_type, a.grade,
a.refinement_level, a.spirit_level, COALESCE(a.spirit_name,''),
a.bound, a.stats, a.skills, a.durability, a.max_durability,
COALESCE(a.set_id,'')
FROM artifacts a
JOIN characters c ON a.character_id = c.id
WHERE c.player_id = $1 AND c.id = $2
ORDER BY a.grade DESC, a.refinement_level DESC
`, uid, req.CharacterID)
if err != nil {
logger.Error("get artifact list failed: %v", err)
return errResp(9002, "internal error", traceID)
}
defer rows.Close()
var artifacts []artifactData
for rows.Next() {
var a artifactData
if err := rows.Scan(
&a.ID, &a.CharacterID, &a.ItemID, &a.ArtifactType, &a.Grade,
&a.RefinementLevel, &a.SpiritLevel, &a.SpiritName, &a.Bound,
&a.Stats, &a.Skills, &a.Durability, &a.MaxDurability, &a.SetID,
); err != nil {
continue
}
artifacts = append(artifacts, a)
}
return okResp(map[string]interface{}{
"artifacts": artifacts,
"count": len(artifacts),
}, traceID)
}
func getSetBonus(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 getSetBonusReq
if err := json.Unmarshal([]byte(payload), &req); err != nil {
return errResp(2001, "invalid payload", traceID)
}
// 查询角色拥有的套装部件
rows, err := hhdbPool.Query(ctx, `
SELECT a.set_id, COUNT(*) as piece_count
FROM artifacts a
JOIN characters c ON a.character_id = c.id
WHERE c.player_id = $1 AND c.id = $2 AND a.set_id != ''
GROUP BY a.set_id
`, uid, req.CharacterID)
if err != nil {
logger.Error("get set bonus failed: %v", err)
return errResp(9002, "internal error", traceID)
}
defer rows.Close()
var sets []setBonusData
for rows.Next() {
var s setBonusData
var pieceCount int32
if err := rows.Scan(&s.SetID, &pieceCount); err != nil {
continue
}
s.OwnedPieces = pieceCount
// 查询套装详情
var setName string
var bonus2, bonus4, bonus6 []byte
err = hhdbPool.QueryRow(ctx, `
SELECT name, set_bonus_2, set_bonus_4, set_bonus_6
FROM artifact_sets WHERE id = $1
`, s.SetID).Scan(&setName, &bonus2, &bonus4, &bonus6)
if err != nil {
continue
}
s.SetName = setName
json.Unmarshal(bonus2, &s.Bonus2)
json.Unmarshal(bonus4, &s.Bonus4)
json.Unmarshal(bonus6, &s.Bonus6)
s.IsActive2 = pieceCount >= 2
s.IsActive4 = pieceCount >= 4
s.IsActive6 = pieceCount >= 6
sets = append(sets, s)
}
return okResp(map[string]interface{}{
"sets": sets,
}, traceID)
}