// Package modules - 渡劫系统模块 // 对齐GDD-12 渡劫破镜与境界掉落系统 + T005审阅报告 package modules import ( "context" "database/sql" "encoding/json" "math/rand" "time" "github.com/heroiclabs/nakama-common/runtime" "github.com/jackc/pgx/v5" ) // RegisterTribulation 注册渡劫相关 RPC。 func RegisterTribulation(initializer runtime.Initializer) error { rpcs := map[string]func(runtime.Initializer) error{ "TribulationService/PrepareTribulation": prepareTribulation, "TribulationService/StartTribulation": startTribulation, "TribulationService/GetTribulationInfo": getTribulationInfo, "TribulationService/InviteHelper": inviteHelper, } for path, fn := range rpcs { if err := initializer.RegisterRpc(path, fn); err != nil { return err } } return nil } // --- 请求/响应结构 --- type prepareTribulationReq struct { CharacterID string `json:"character_id"` TargetRealm int32 `json:"target_realm"` // 目标境界tier TargetMinor int32 `json:"target_minor"` // 目标小境界 Pills []string `json:"pills"` // 使用的丹药ID Artifacts []string `json:"artifacts"` // 使用的法宝ID Helpers []string `json:"helpers"` // 护法玩家ID } type startTribulationReq struct { CharacterID string `json:"character_id"` SessionType string `json:"session_type"` // manual/auto } type getTribulationInfoReq struct { CharacterID string `json:"character_id"` } type inviteHelperReq struct { CharacterID string `json:"character_id"` HelperID string `json:"helper_id"` // 被邀请玩家ID } type tribulationInfoData struct { CharacterID string `json:"character_id"` CurrentRealm int32 `json:"current_realm"` CurrentMinor int32 `json:"current_minor"` TargetRealm int32 `json:"target_realm"` TargetMinor int32 `json:"target_minor"` BaseSuccessRate float64 `json:"base_success_rate"` ModifiedRate float64 `json:"modified_rate"` TribulationType string `json:"tribulation_type"` // thunder/fire/heart_devil/human/earth/life/chaos Helpers []string `json:"helpers"` Pills []string `json:"pills"` Artifacts []string `json:"artifacts"` IsMajorBreakthrough bool `json:"is_major_breakthrough"` PreparationStatus string `json:"preparation_status"` } // --- 渡劫类型判定 --- func determineTribulationType(realmTier int32, karmaValue int32, sinValue int32, sanValue int32) string { // 大境界突破(3的倍数)触发三劫连环 if realmTier%3 == 0 && realmTier > 1 { return "triple" // 雷罚→业火→心魔 } // 高罪孽触发天罚天劫 if sinValue >= 500 { return "heavenly_punishment" } // 低SAN触发旧日注视(克苏鲁系) if sanValue < 30 { return "old_gaze" } // 默认雷罚天劫 return "thunder" } func calculateSuccessRate(baseRate float64, pills []string, artifacts []string, karmaValue int32, sinValue int32, dantoxLevel int32, purity float64) float64 { modified := baseRate // 丹药加成 modified += float64(len(pills)) * 0.05 // 法宝加成 modified += float64(len(artifacts)) * 0.03 // 功德加成 if karmaValue >= 1000 { modified += 0.10 } else if karmaValue >= 500 { modified += 0.05 } // 罪孽惩罚 if sinValue >= 2000 { modified -= 0.30 } else if sinValue >= 500 { modified -= 0.15 } // 丹毒惩罚 if dantoxLevel > 100 { modified -= 0.15 } else if dantoxLevel > 60 { modified -= 0.08 } // 纯净度加成 if purity >= 0.9 { modified += 0.15 } else if purity >= 0.7 { modified += 0.08 } else if purity < 0.3 { modified -= 0.15 } // 限制范围 if modified < 0.05 { modified = 0.05 } if modified > 0.95 { modified = 0.95 } return modified } // --- RPC 实现 --- func prepareTribulation(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 prepareTribulationReq if err := json.Unmarshal([]byte(payload), &req); err != nil { return errResp(2001, "invalid payload", traceID) } // 查询角色信息 var charInfo struct { ID string RealmTier int32 MinorRealm int32 KarmaValue int32 SinValue int32 DantoxLevel int32 Purity float64 SanValue int32 } err := hhdbPool.QueryRow(ctx, ` SELECT id, realm_tier, minor_realm, karma_value, crime_score, dantox_level, energy_purity, san_current FROM characters WHERE id = $1 AND player_id = $2 `, req.CharacterID, uid).Scan( &charInfo.ID, &charInfo.RealmTier, &charInfo.MinorRealm, &charInfo.KarmaValue, &charInfo.SinValue, &charInfo.DantoxLevel, &charInfo.Purity, &charInfo.SanValue, ) if err != nil { return errResp(4002, "character not found", traceID) } // 检查是否可渡劫 if charInfo.RealmTier >= 9 { return errResp(5010, "already at max realm", traceID) } // 确定渡劫类型 tribType := determineTribulationType(charInfo.RealmTier, charInfo.KarmaValue, charInfo.SinValue, charInfo.SanValue) // 计算成功率 var realmInfo struct { BaseSuccessRate float64 } err = hhdbPool.QueryRow(ctx, ` SELECT base_success_rate FROM realms WHERE tier = $1 `, charInfo.RealmTier).Scan(&realmInfo.BaseSuccessRate) if err != nil { realmInfo.BaseSuccessRate = 0.50 } modifiedRate := calculateSuccessRate( realmInfo.BaseSuccessRate, req.Pills, req.Artifacts, charInfo.KarmaValue, charInfo.SinValue, charInfo.DantoxLevel, charInfo.Purity, ) isMajor := charInfo.RealmTier%3 == 0 return okResp(tribulationInfoData{ CharacterID: charInfo.ID, CurrentRealm: charInfo.RealmTier, CurrentMinor: charInfo.MinorRealm, TargetRealm: req.TargetRealm, TargetMinor: req.TargetMinor, BaseSuccessRate: realmInfo.BaseSuccessRate, ModifiedRate: modifiedRate, TribulationType: tribType, Helpers: req.Helpers, Pills: req.Pills, Artifacts: req.Artifacts, IsMajorBreakthrough: isMajor, PreparationStatus: "ready", }, traceID) } func startTribulation(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 startTribulationReq if err := json.Unmarshal([]byte(payload), &req); err != nil { return errResp(2001, "invalid payload", traceID) } // 查询角色信息 var charID string var realmTier, minorRealm int32 var karmaValue, sinValue, dantoxLevel, sanCurrent int32 var purity float64 err := hhdbPool.QueryRow(ctx, ` SELECT id, realm_tier, minor_realm, karma_value, crime_score, dantox_level, energy_purity, san_current FROM characters WHERE id = $1 AND player_id = $2 `, req.CharacterID, uid).Scan( &charID, &realmTier, &minorRealm, &karmaValue, &sinValue, &dantoxLevel, &purity, &sanCurrent, ) if err != nil { return errResp(4002, "character not found", traceID) } // 确定渡劫类型 tribType := determineTribulationType(realmTier, karmaValue, sinValue, sanCurrent) // 获取基础成功率 var baseRate float64 err = hhdbPool.QueryRow(ctx, `SELECT base_success_rate FROM realms WHERE tier = $1`, realmTier).Scan(&baseRate) if err != nil { baseRate = 0.50 } modifiedRate := calculateSuccessRate(baseRate, nil, nil, karmaValue, sinValue, dantoxLevel, purity) // 判定渡劫结果 success := rand.Float64() < modifiedRate var result string var penalties map[string]interface{} if success { result = "success" // 成功:境界提升 newMinor := minorRealm + 1 newRealm := realmTier if newMinor > 3 { newMinor = 1 newRealm = realmTier + 1 } _, err = hhdbPool.Exec(ctx, ` UPDATE characters SET realm_tier = $1, minor_realm = $2, updated_at = NOW() WHERE id = $3 `, newRealm, newMinor, charID) if err != nil { logger.Error("tribulation success update failed: %v", err) return errResp(9002, "internal error", traceID) } penalties = map[string]interface{}{ "realm_tier": newRealm, "minor_realm": newMinor, } } else { result = "fail" // 失败:境界掉落 newMinor := minorRealm - 1 newRealm := realmTier if newMinor < 1 { newMinor = 3 newRealm = realmTier - 1 if newRealm < 1 { newRealm = 1 newMinor = 1 } } _, err = hhdbPool.Exec(ctx, ` UPDATE characters SET realm_tier = $1, minor_realm = $2, updated_at = NOW() WHERE id = $3 `, newRealm, newMinor, charID) if err != nil { logger.Error("tribulation fail update failed: %v", err) return errResp(9002, "internal error", traceID) } penalties = map[string]interface{}{ "realm_tier": newRealm, "minor_realm": newMinor, "debuff": "天劫余威:全属性-10%,持续6-24游戏时", } } // 记录渡劫记录 _, err = hhdbPool.Exec(ctx, ` INSERT INTO tribulation_records (character_id, realm_tier, minor_realm, tribulation_type, base_success_rate, modified_success_rate, result, penalties, san_snapshot, created_at) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, NOW()) `, charID, realmTier, minorRealm, tribType, baseRate, modifiedRate, result, penalties, sanCurrent) if err != nil { logger.Error("tribulation record insert failed: %v", err) } return okResp(map[string]interface{}{ "success": success, "result": result, "tribulation_type": tribType, "base_rate": baseRate, "modified_rate": modifiedRate, "penalties": penalties, }, traceID) } func getTribulationInfo(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 getTribulationInfoReq if err := json.Unmarshal([]byte(payload), &req); err != nil { return errResp(2001, "invalid payload", traceID) } // 查询最近的渡劫记录 var record struct { TribulationType string BaseRate float64 ModifiedRate float64 Result string CreatedAt time.Time } err := hhdbPool.QueryRow(ctx, ` SELECT tribulation_type, base_success_rate, modified_success_rate, result, created_at FROM tribulation_records WHERE character_id = $1 ORDER BY created_at DESC LIMIT 1 `, req.CharacterID).Scan( &record.TribulationType, &record.BaseRate, &record.ModifiedRate, &record.Result, &record.CreatedAt, ) if err != nil && err != pgx.ErrNoRows { logger.Error("get tribulation info failed: %v", err) return errResp(9002, "internal error", traceID) } return okResp(map[string]interface{}{ "character_id": req.CharacterID, "last_tribulation": record, }, traceID) } func inviteHelper(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 inviteHelperReq if err := json.Unmarshal([]byte(payload), &req); err != nil { return errResp(2001, "invalid payload", traceID) } // 验证被邀请者存在 var helperName string err := hhdbPool.QueryRow(ctx, ` SELECT name FROM characters WHERE id = $1 AND status = 'active' `, req.HelperID).Scan(&helperName) if err != nil { return errResp(5001, "helper not found", traceID) } // 发送邀请通知(通过Nakama通知系统) _, err = nk.NotificationsSend(ctx, []runtime.NotificationRequest{ { Code: 2001, Content: map[string]interface{}{"type": "tribulation_helper_invite", "inviter_id": req.CharacterID}, Persistent: true, Subject: "渡劫护法邀请", }, }, []string{req.HelperID}) if err != nil { logger.Error("send notification failed: %v", err) } return okResp(map[string]interface{}{ "success": true, "helper_name": helperName, "message": "护法邀请已发送", }, traceID) }