package modules import ( "context" "database/sql" "encoding/json" "fmt" "math/rand" "github.com/heroiclabs/nakama-common/runtime" ) // RegisterManual 注册功法相关 RPC。 func RegisterManual(initializer runtime.Initializer) error { if err := initializer.RegisterRpc("ManualService/ListManuals", listManuals); err != nil { return err } if err := initializer.RegisterRpc("ManualService/UpgradeManual", upgradeManual); err != nil { return err } if err := initializer.RegisterRpc("ManualService/SetBuffingManual", setBuffingManual); err != nil { return err } return nil } type manualListReq struct { CharacterID string `json:"character_id"` IsBuffing bool `json:"is_buffing"` Page int32 `json:"page"` PageSize int32 `json:"page_size"` } type upgradeManualReq struct { CharacterID string `json:"character_id"` ManualInstanceID string `json:"manual_instance_id"` Materials []materialCostReq `json:"materials"` UseInsightItem bool `json:"use_insight_item"` IdempotencyKey string `json:"idempotency_key"` } type buffManualReq struct { CharacterID string `json:"character_id"` ManualInstanceID string `json:"manual_instance_id"` SlotIndex int32 `json:"slot_index"` UnsetOthers bool `json:"unset_others"` } type manualItemData struct { ManualInstanceID string `json:"manual_instance_id"` ManualID string `json:"manual_id"` Name string `json:"name"` Category string `json:"category"` Domain string `json:"domain"` Element string `json:"element"` InstanceData interface{} `json:"instance_data"` IsBuffing bool `json:"is_buffing"` CreatedAt string `json:"created_at"` } type manualListData struct { Total int32 `json:"total"` List []manualItemData `json:"list"` } type manualUpgradeData struct { ManualInstanceID string `json:"manual_instance_id"` OldLayer int32 `json:"old_layer"` NewLayer int32 `json:"new_layer"` Success bool `json:"success"` ProficiencyLeft int32 `json:"proficiency_left"` UnlockedSkill interface{} `json:"unlocked_skill"` } type buffSlotData struct { SlotIndex int32 `json:"slot_index"` ManualInstanceID string `json:"manual_instance_id"` BuffSpeedBonus float64 `json:"buff_speed_bonus"` BreakinUntil string `json:"breakin_until"` } func listManuals(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, payload string) (string, error) { traceID := newTraceID() var req manualListReq if err := json.Unmarshal([]byte(payload), &req); err != nil { return errResp(5001, "invalid payload", traceID) } if req.Page < 1 { req.Page = 1 } if req.PageSize < 1 || req.PageSize > 50 { req.PageSize = 20 } offset := (req.Page - 1) * req.PageSize // 查询功法列表 query := ` SELECT cm.id, cm.manual_id, m.name, m.category, m.domain, m.element, cm.instance_data, cm.is_buffing, cm.created_at::text FROM character_manuals cm JOIN manuals m ON cm.manual_id = m.id WHERE cm.character_id = $1 ` args := []interface{}{req.CharacterID} if req.IsBuffing { query += " AND cm.is_buffing = true" } query += " ORDER BY cm.created_at DESC LIMIT $2 OFFSET $3" args = append(args, req.PageSize, offset) rows, err := db.QueryContext(ctx, query, args...) if err != nil { logger.Error("list manuals failed: %v", err) return errResp(9002, "internal error", traceID) } defer rows.Close() var manuals []manualItemData for rows.Next() { var m manualItemData if err := rows.Scan( &m.ManualInstanceID, &m.ManualID, &m.Name, &m.Category, &m.Domain, &m.Element, &m.InstanceData, &m.IsBuffing, &m.CreatedAt, ); err != nil { continue } manuals = append(manuals, m) } // 查询总数 var total int32 err = db.QueryRowContext(ctx, ` SELECT COUNT(*) FROM character_manuals WHERE character_id = $1 `, req.CharacterID).Scan(&total) if err != nil { total = 0 } return okResp(manualListData{Total: total, List: manuals}, traceID) } func upgradeManual(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, payload string) (string, error) { traceID := newTraceID() var req upgradeManualReq if err := json.Unmarshal([]byte(payload), &req); err != nil { return errResp(5002, "invalid payload", traceID) } // 查询功法信息 var currentLayer int32 var maxLayer int32 err := db.QueryRowContext(ctx, ` SELECT cm.instance_data->>'current_layer', m.max_layers FROM character_manuals cm JOIN manuals m ON cm.manual_id = m.id WHERE cm.id = $1 AND cm.character_id = $2 `, req.ManualInstanceID, req.CharacterID).Scan(¤tLayer, &maxLayer) if err != nil { return errResp(5003, "manual not found", traceID) } // 检查层数上限 if currentLayer >= maxLayer { return errResp(5004, "already at max layer", traceID) } // 检查熟练度 var proficiency int32 err = db.QueryRowContext(ctx, ` SELECT COALESCE((instance_data->>'proficiency')::int, 0) FROM character_manuals WHERE id = $1 `, req.ManualInstanceID).Scan(&proficiency) if err != nil { proficiency = 0 } requiredProficiency := currentLayer * 100 if proficiency < requiredProficiency { return errResp(5005, "insufficient proficiency", traceID) } // 计算成功率 successRate := 0.85 if currentLayer >= 5 { successRate -= float64(currentLayer-4) * 0.05 } // 判定成功/失败 success := rand.Float64() < successRate var newLayer int32 if success { newLayer = currentLayer + 1 // 更新功法层数和熟练度 _, err = db.ExecContext(ctx, ` UPDATE character_manuals SET instance_data = jsonb_set( instance_data, '{current_layer}', $1::jsonb ), updated_at = NOW() WHERE id = $2 `, fmt.Sprintf("%d", newLayer), req.ManualInstanceID) if err != nil { logger.Error("upgrade manual failed: %v", err) return errResp(9002, "internal error", traceID) } } logger.Info("ManualService/UpgradeManual: manual=%s success=%v", req.ManualInstanceID, success) return okResp(manualUpgradeData{ ManualInstanceID: req.ManualInstanceID, OldLayer: currentLayer, NewLayer: newLayer, Success: success, }, traceID) } func setBuffingManual(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, payload string) (string, error) { traceID := newTraceID() var req buffManualReq if err := json.Unmarshal([]byte(payload), &req); err != nil { return errResp(5006, "invalid payload", traceID) } // 查询功法信息 var isBuffing bool err := db.QueryRowContext(ctx, ` SELECT is_buffing FROM character_manuals WHERE id = $1 AND character_id = $2 `, req.ManualInstanceID, req.CharacterID).Scan(&isBuffing) if err != nil { return errResp(5003, "manual not found", traceID) } // 检查是否已在加持状态 if isBuffing { return errResp(5007, "already buffing", traceID) } // 更新加持状态 _, err = db.ExecContext(ctx, ` UPDATE character_manuals SET is_buffing = true, updated_at = NOW() WHERE id = $1 AND character_id = $2 `, req.ManualInstanceID, req.CharacterID) if err != nil { logger.Error("set buffing manual failed: %v", err) return errResp(9002, "internal error", traceID) } logger.Info("ManualService/SetBuffingManual success: manual=%s", req.ManualInstanceID) return okResp(buffSlotData{ SlotIndex: req.SlotIndex, ManualInstanceID: req.ManualInstanceID, BuffSpeedBonus: 0.08, }, traceID) }