package modules import ( "context" "database/sql" "encoding/json" "fmt" "math" "math/rand" "time" "github.com/heroiclabs/nakama-common/runtime" "github.com/honghuang-game/server/config" "github.com/honghuang-game/server/internal/db" "github.com/honghuang-game/server/internal/domain" ) var realmSvc *RealmService // RegisterRealm 注册修炼/境界相关 RPC。 func RegisterRealm(initializer runtime.Initializer) error { realmSvc = NewRealmService(db.NewPgxRealmStore(), config.Global) if err := initializer.RegisterRpc("RealmService/GetRealmProgress", getRealmProgress); err != nil { return err } if err := initializer.RegisterRpc("RealmService/Cultivate", cultivate); err != nil { return err } if err := initializer.RegisterRpc("RealmService/AttemptBreakthrough", attemptBreakthrough); err != nil { return err } if err := initializer.RegisterRpc("RealmService/StartTribulation", startTribulation); err != nil { return err } if err := initializer.RegisterRpc("RealmService/GetTribulationResult", getTribulationResult); err != nil { return err } if err := initializer.RegisterRpc("RealmService/WorldBreak", worldBreak); err != nil { return err } return nil } // RealmService 实现境界/渡劫/破界业务逻辑,便于单元测试 mock 存储层。 type RealmService struct { store db.RealmStore cfg *config.Config randFloat func() float64 } // NewRealmService 创建境界服务实例。 func NewRealmService(store db.RealmStore, cfg *config.Config) *RealmService { return &RealmService{ store: store, cfg: cfg, randFloat: rand.Float64, } } // --------------------------------------------------------------------------- // 请求/响应结构 // --------------------------------------------------------------------------- type realmReq struct { CharacterID string `json:"character_id"` } type cultivateReq struct { CharacterID string `json:"character_id"` StaminaAmount int32 `json:"stamina_amount"` Consumables []materialCostReq `json:"consumables"` } type breakthroughReq struct { CharacterID string `json:"character_id"` TargetMinorRealm int32 `json:"target_minor_realm"` Consumables []materialCostReq `json:"consumables"` HelperIDs []string `json:"helper_ids"` IdempotencyKey string `json:"idempotency_key"` } type tribulationReq struct { CharacterID string `json:"character_id"` TargetRealmTier int32 `json:"target_realm_tier"` TribulationType string `json:"tribulation_type"` Consumables []materialCostReq `json:"consumables"` HelperIDs []string `json:"helper_ids"` } type worldBreakReq struct { CharacterID string `json:"character_id"` TargetWorldTier int32 `json:"target_world_tier"` KeyItemInstanceID string `json:"key_item_instance_id"` Confirm bool `json:"confirm"` } type materialCostReq struct { InventoryID string `json:"inventory_id"` Quantity int32 `json:"quantity"` } type realmPointData struct { RealmTier int32 `json:"realm_tier"` MinorRealm int32 `json:"minor_realm"` } type realmProgressData struct { RealmTier int32 `json:"realm_tier"` MinorRealm int32 `json:"minor_realm"` RealmName string `json:"realm_name"` Exp int64 `json:"exp"` ExpToNext int64 `json:"exp_to_next"` MaxUnlockedWorldTier int32 `json:"max_unlocked_world_tier"` RealmStatus string `json:"realm_status"` BreakthroughReady bool `json:"breakthrough_ready"` TribulationPending bool `json:"tribulation_pending"` StaminaCurrent int32 `json:"stamina_current,omitempty"` StaminaMax int32 `json:"stamina_max,omitempty"` } type cultivateData struct { RealmPointData realmPointData `json:"realm_point"` ExpGained int64 `json:"exp_gained"` ExpToNext int64 `json:"exp_to_next"` BreakthroughReady bool `json:"breakthrough_ready"` StaminaRemaining int32 `json:"stamina_remaining"` EventTriggered string `json:"event_triggered"` EventBonusExp int64 `json:"event_bonus_exp"` } type breakthroughData struct { Success bool `json:"success"` From realmPointData `json:"from"` To realmPointData `json:"to"` ExpConsumed int64 `json:"exp_consumed"` RecordID string `json:"record_id"` SpecialEvent string `json:"special_event"` } type tribulationData struct { TribulationID string `json:"tribulation_id"` Status string `json:"status"` BaseSuccessRate float64 `json:"base_success_rate"` ModifiedSuccessRate float64 `json:"modified_success_rate"` EstimatedTicks int32 `json:"estimated_ticks"` StartAt string `json:"start_at"` Result string `json:"result"` FromRealmTier int32 `json:"from_realm_tier"` ToRealmTier int32 `json:"to_realm_tier"` Penalties interface{} `json:"penalties"` Drops interface{} `json:"drops"` } type worldBreakData struct { Success bool `json:"success"` FromWorldTier int32 `json:"from_world_tier"` ToWorldTier int32 `json:"to_world_tier"` FromRealmTier int32 `json:"from_realm_tier"` ToRealmTier int32 `json:"to_realm_tier"` BreakthroughRecordID string `json:"breakthrough_record_id"` WorldEvent string `json:"world_event"` RuinGenerated bool `json:"ruin_generated"` } // --------------------------------------------------------------------------- // RPC Handlers // --------------------------------------------------------------------------- func getRealmProgress(ctx context.Context, logger runtime.Logger, _db *sql.DB, nk runtime.NakamaModule, payload string) (string, error) { traceID := newTraceID() var req realmReq if err := json.Unmarshal([]byte(payload), &req); err != nil || req.CharacterID == "" { return errResp(3001, "invalid payload", traceID) } resp, code, msg := realmSvc.GetRealmProgress(ctx, req.CharacterID) if code != 0 { return errResp(code, msg, traceID) } logger.Info("RealmService/GetRealmProgress: character_id=%s", req.CharacterID) return okResp(resp, traceID) } func cultivate(ctx context.Context, logger runtime.Logger, _db *sql.DB, nk runtime.NakamaModule, payload string) (string, error) { traceID := newTraceID() var req cultivateReq if err := json.Unmarshal([]byte(payload), &req); err != nil || req.CharacterID == "" { return errResp(3001, "invalid payload", traceID) } resp, code, msg := realmSvc.Cultivate(ctx, req) if code != 0 { return errResp(code, msg, traceID) } logger.Info("RealmService/Cultivate: character_id=%s stamina=%d", req.CharacterID, req.StaminaAmount) return okResp(resp, traceID) } func attemptBreakthrough(ctx context.Context, logger runtime.Logger, _db *sql.DB, nk runtime.NakamaModule, payload string) (string, error) { traceID := newTraceID() var req breakthroughReq if err := json.Unmarshal([]byte(payload), &req); err != nil || req.CharacterID == "" { return errResp(3001, "invalid payload", traceID) } resp, code, msg := realmSvc.AttemptBreakthrough(ctx, req) if code != 0 { return errResp(code, msg, traceID) } logger.Info("RealmService/AttemptBreakthrough: character_id=%s success=%v", req.CharacterID, resp.Success) return okResp(resp, traceID) } func startTribulation(ctx context.Context, logger runtime.Logger, _db *sql.DB, nk runtime.NakamaModule, payload string) (string, error) { traceID := newTraceID() var req tribulationReq if err := json.Unmarshal([]byte(payload), &req); err != nil || req.CharacterID == "" { return errResp(3007, "invalid payload", traceID) } resp, code, msg := realmSvc.StartTribulation(ctx, req) if code != 0 { return errResp(code, msg, traceID) } logger.Info("RealmService/StartTribulation: character_id=%s result=%s", req.CharacterID, resp.Result) return okResp(resp, traceID) } func getTribulationResult(ctx context.Context, logger runtime.Logger, _db *sql.DB, nk runtime.NakamaModule, payload string) (string, error) { traceID := newTraceID() var req tribulationReq if err := json.Unmarshal([]byte(payload), &req); err != nil || req.CharacterID == "" { return errResp(3007, "invalid payload", traceID) } resp, code, msg := realmSvc.GetTribulationResult(ctx, req.CharacterID) if code != 0 { return errResp(code, msg, traceID) } logger.Info("RealmService/GetTribulationResult: character_id=%s", req.CharacterID) return okResp(resp, traceID) } func worldBreak(ctx context.Context, logger runtime.Logger, _db *sql.DB, nk runtime.NakamaModule, payload string) (string, error) { traceID := newTraceID() var req worldBreakReq if err := json.Unmarshal([]byte(payload), &req); err != nil || req.CharacterID == "" { return errResp(3011, "invalid payload", traceID) } if !req.Confirm { return errResp(3011, "confirmation required", traceID) } resp, code, msg := realmSvc.WorldBreak(ctx, req) if code != 0 { return errResp(code, msg, traceID) } logger.Info("RealmService/WorldBreak: character_id=%s to_world=%d", req.CharacterID, req.TargetWorldTier) return okResp(resp, traceID) } // --------------------------------------------------------------------------- // 业务逻辑 // --------------------------------------------------------------------------- func (s *RealmService) getConfigFloat(key string, def float64) float64 { if s.cfg == nil { return def } return s.cfg.GetFloat64(key, def) } func (s *RealmService) getConfigInt(key string, def int) int { if s.cfg == nil { return def } return s.cfg.GetInt(key, def) } func (s *RealmService) expToNext(tier, minor int32) int64 { key := domain.ExpConfigKey(tier, minor) if s.cfg != nil { if v := s.cfg.GetInt(key, 0); v > 0 { return int64(v) } } return domain.DefaultExpToNext(tier, minor) } func (s *RealmService) dailyStaminaMax(realmTier int32) int32 { // 按 GDD-21 境界体力锚点 switch realmTier { case 1: return 120 case 2: return 150 case 3: return 180 case 4: return 210 case 5: return 240 case 6: return 300 } return 120 } func (s *RealmService) expPerStamina(realmTier int32) int64 { // 默认 1 体力 = 100 修为;可按 Nacos 热更 return int64(s.getConfigInt("realm.exp_per_stamina", 100)) } func (s *RealmService) getStamina(baseStats map[string]interface{}) (current, max int32) { max = 120 if v, ok := baseStats["stamina_max"]; ok { max = int32(toFloat64(v)) } if v, ok := baseStats["stamina_current"]; ok { current = int32(toFloat64(v)) } else { current = max } return } func (s *RealmService) setStamina(baseStats map[string]interface{}, current, max int32) { baseStats["stamina_max"] = max baseStats["stamina_current"] = current } func toFloat64(v interface{}) float64 { switch n := v.(type) { case float64: return n case float32: return float64(n) case int: return float64(n) case int32: return float64(n) case int64: return float64(n) } return 0 } // GetRealmProgress 查询角色当前境界、小境界、修为与状态。 func (s *RealmService) GetRealmProgress(ctx context.Context, characterID string) (realmProgressData, int, string) { c, err := s.store.GetCharacter(ctx, characterID) if err != nil { return realmProgressData{}, 3001, "character not found" } cr, err := s.store.EnsureCharacterRealm(ctx, characterID, c.RealmTier, c.MinorRealm) if err != nil { return realmProgressData{}, 3001, "realm data missing" } expToNext := s.expToNext(c.RealmTier, c.MinorRealm) staminaCurrent, staminaMax := s.getStamina(c.BaseStats) return realmProgressData{ RealmTier: c.RealmTier, MinorRealm: c.MinorRealm, RealmName: domain.RealmFullName(c.RealmTier, c.MinorRealm), Exp: cr.ExpInTier, ExpToNext: expToNext, MaxUnlockedWorldTier: c.WorldTier, RealmStatus: c.RealmStatus, BreakthroughReady: cr.ExpInTier >= expToNext && c.RealmStatus == domain.RealmStatusNormal, TribulationPending: c.RealmStatus == domain.RealmStatusTribulationPending, StaminaCurrent: staminaCurrent, StaminaMax: staminaMax, }, 0, "" } // Cultivate 消耗体力/资源增加修为,并概率触发修炼事件。 func (s *RealmService) Cultivate(ctx context.Context, req cultivateReq) (cultivateData, int, string) { c, err := s.store.GetCharacter(ctx, req.CharacterID) if err != nil { return cultivateData{}, 3001, "character not found" } if c.Status != "active" { return cultivateData{}, 3004, "character not active" } if c.RealmStatus == domain.RealmStatusTribulationPending { return cultivateData{}, 3004, "tribulation pending" } staminaCurrent, staminaMax := s.getStamina(c.BaseStats) if req.StaminaAmount <= 0 { req.StaminaAmount = 1 } if staminaCurrent < req.StaminaAmount { return cultivateData{}, 3002, "insufficient stamina" } // 消耗道具(如有) for _, m := range req.Consumables { itemID, qty, err := s.store.GetInventoryItem(ctx, req.CharacterID, m.InventoryID) if err != nil || qty < m.Quantity { return cultivateData{}, 3002, fmt.Sprintf("consumable insufficient: %s", m.InventoryID) } if err := s.store.ConsumeInventoryItem(ctx, req.CharacterID, m.InventoryID, m.Quantity); err != nil { return cultivateData{}, 3002, "failed to consume material" } _ = itemID } // 消耗体力 staminaCurrent -= req.StaminaAmount s.setStamina(c.BaseStats, staminaCurrent, staminaMax) if err := s.store.UpdateCharacterBaseStats(ctx, req.CharacterID, c.BaseStats); err != nil { return cultivateData{}, 9002, "failed to update stamina" } cr, err := s.store.EnsureCharacterRealm(ctx, req.CharacterID, c.RealmTier, c.MinorRealm) if err != nil { return cultivateData{}, 3001, "realm data missing" } expToNext := s.expToNext(c.RealmTier, c.MinorRealm) baseGain := int64(req.StaminaAmount) * s.expPerStamina(c.RealmTier) bonusExp, event := s.applyCultivationEvent(baseGain, expToNext) newExp := cr.ExpInTier + baseGain + bonusExp if newExp > expToNext*2 { newExp = expToNext * 2 // 防止极端溢出 } realmStatus := c.RealmStatus if newExp >= expToNext && realmStatus == domain.RealmStatusNormal { realmStatus = domain.RealmStatusBreakthroughReady } if err := s.store.UpdateCharacterRealm(ctx, req.CharacterID, c.RealmTier, c.MinorRealm, newExp, realmStatus); err != nil { return cultivateData{}, 9002, "failed to update realm" } return cultivateData{ RealmPointData: realmPointData{RealmTier: c.RealmTier, MinorRealm: c.MinorRealm}, ExpGained: baseGain + bonusExp, ExpToNext: expToNext, BreakthroughReady: realmStatus == domain.RealmStatusBreakthroughReady, StaminaRemaining: staminaCurrent, EventTriggered: event, EventBonusExp: bonusExp, }, 0, "" } func (s *RealmService) applyCultivationEvent(baseGain, expToNext int64) (int64, string) { if expToNext <= 0 { return 0, "" } chancePer10Pct := s.getConfigFloat("event.cultivation.trigger_chance_per_10pct", 0.05) progressRatio := float64(baseGain) / float64(expToNext) tens := int(math.Floor(progressRatio * 10)) if tens < 1 { tens = 1 } for i := 0; i < tens; i++ { if s.randFloat() < chancePer10Pct { eventRoll := s.randFloat() switch { case eventRoll < 0.4: return int64(float64(expToNext) * 0.3), "小顿悟·功法进度+30%" case eventRoll < 0.7: return int64(float64(expToNext) * 0.5), "灵气异动·修为暴涨" case eventRoll < 0.9: return 0, "心魔来袭·SAN动荡" default: return 0, "奇遇NPC·待后续领取" } } } return 0, "" } // AttemptBreakthrough 小境界突破(层内初期→中期→圆满)。 func (s *RealmService) AttemptBreakthrough(ctx context.Context, req breakthroughReq) (breakthroughData, int, string) { c, err := s.store.GetCharacter(ctx, req.CharacterID) if err != nil { return breakthroughData{}, 3001, "character not found" } if c.Status != "active" { return breakthroughData{}, 3004, "character not active" } if c.RealmStatus == domain.RealmStatusTribulationPending { return breakthroughData{}, 3004, "tribulation pending" } if req.TargetMinorRealm != c.MinorRealm+1 { return breakthroughData{}, 3003, "invalid target minor realm" } if req.TargetMinorRealm > 3 { return breakthroughData{}, 3003, "already at tier peak" } cr, err := s.store.EnsureCharacterRealm(ctx, req.CharacterID, c.RealmTier, c.MinorRealm) if err != nil { return breakthroughData{}, 3001, "realm data missing" } expToNext := s.expToNext(c.RealmTier, c.MinorRealm) if cr.ExpInTier < expToNext { return breakthroughData{}, 3002, "exp insufficient" } // 消耗材料 if err := s.consumeMaterials(ctx, req.CharacterID, req.Consumables); err != nil { return breakthroughData{}, 3002, err.Error() } // 护法者基础校验 helpers := req.HelperIDs if len(helpers) > 0 { count, err := s.store.CountCharacters(ctx, helpers) if err != nil || count != len(helpers) { return breakthroughData{}, 3005, "helper condition not met" } } successRate := s.calcBreakthroughRate(c, c.RealmTier, false, helpers) rolled := s.randFloat() success := rolled < successRate from := realmPointData{RealmTier: c.RealmTier, MinorRealm: c.MinorRealm} to := from specialEvent := "" if success { to.MinorRealm = req.TargetMinorRealm if err := s.store.UpdateCharacterRealm(ctx, req.CharacterID, to.RealmTier, to.MinorRealm, 0, domain.RealmStatusNormal); err != nil { return breakthroughData{}, 9002, "failed to update realm" } specialEvent = "突破成功" } else { // 小境界突破失败不跨层掉落,但损失部分修为 penaltyExp := cr.ExpInTier / 10 newExp := cr.ExpInTier - penaltyExp if newExp < 0 { newExp = 0 } if err := s.store.UpdateCharacterRealm(ctx, req.CharacterID, c.RealmTier, c.MinorRealm, newExp, domain.RealmStatusNormal); err != nil { return breakthroughData{}, 9002, "failed to update realm" } specialEvent = "突破失败·修为受损" } rec := &db.BreakthroughRecord{ CharacterID: req.CharacterID, FromRealmTier: from.RealmTier, ToRealmTier: to.RealmTier, FromMinorRealm: from.MinorRealm, ToMinorRealm: to.MinorRealm, IsSuccess: success, IsBreakWorldBarrier: false, SourceWorldTier: c.WorldTier, TargetWorldTier: c.WorldTier, } if err := s.store.InsertBreakthroughRecord(ctx, rec); err != nil { return breakthroughData{}, 9002, "failed to record breakthrough" } return breakthroughData{ Success: success, From: from, To: to, ExpConsumed: expToNext, RecordID: "", // record id not returned by current schema insert; left empty intentionally SpecialEvent: specialEvent, }, 0, "" } func (s *RealmService) consumeMaterials(ctx context.Context, characterID string, materials []materialCostReq) error { for _, m := range materials { if m.InventoryID == "" || m.Quantity <= 0 { continue } itemID, qty, err := s.store.GetInventoryItem(ctx, characterID, m.InventoryID) if err != nil || qty < m.Quantity { return fmt.Errorf("material insufficient: %s", m.InventoryID) } has, err := s.store.HasInventoryItem(ctx, characterID, m.InventoryID, itemID, m.Quantity) if err != nil || !has { return fmt.Errorf("material insufficient: %s", m.InventoryID) } if err := s.store.ConsumeInventoryItem(ctx, characterID, m.InventoryID, m.Quantity); err != nil { return fmt.Errorf("failed to consume material: %s", m.InventoryID) } } return nil } func (s *RealmService) calcBreakthroughRate(c *db.Character, tier int32, isBig bool, helpers []string) float64 { base := domain.DefaultTribulationBaseRate(tier, isBig) base = s.getConfigFloat(domain.TribulationBaseRateKey(tier, isBig), base) rate := base // 中期→圆满加成 if !isBig && c.MinorRealm == 2 { rate += s.getConfigFloat("tribulation.mid_to_full_bonus", 0.02) } // 圆满→下境初期惩罚(仅大境界渡劫) if isBig { rate += s.getConfigFloat("tribulation.full_to_next_penalty", -0.03) } // 罪孽惩罚 sinPenalty := math.Min(0.4, float64(c.CrimeScore)/5000.0*0.4) rate -= sinPenalty // 天道值减免 heavenMitigation := math.Min(0.05, float64(c.HeavenlyValue)/50.0*0.01) rate += heavenMitigation // 护法者(每位+2%,上限10%) helperBonus := float64(len(helpers)) * 0.02 if helperBonus > 0.10 { helperBonus = 0.10 } rate += helperBonus cap := s.getConfigFloat("tribulation.success_cap", 0.95) floor := s.getConfigFloat("tribulation.success_floor", 0.05) if rate > cap { rate = cap } if rate < floor { rate = floor } return rate } // StartTribulation 大境界渡劫。当前版本同步结算并返回 completed 状态。 func (s *RealmService) StartTribulation(ctx context.Context, req tribulationReq) (tribulationData, int, string) { c, err := s.store.GetCharacter(ctx, req.CharacterID) if err != nil { return tribulationData{}, 3007, "character not found" } if c.Status != "active" { return tribulationData{}, 3007, "character not active" } if c.RealmStatus == domain.RealmStatusTribulationPending { return tribulationData{}, 3009, "already in tribulation" } if c.MinorRealm != 3 { return tribulationData{}, 3007, "not at minor realm peak" } if req.TargetRealmTier != c.RealmTier+1 { return tribulationData{}, 3008, "invalid target realm tier" } minSAN := int32(s.getConfigInt("tribulation.min_san", 30)) if c.SanCurrent < minSAN { return tribulationData{}, 3010, "SAN too low" } // 消耗材料 if err := s.consumeMaterials(ctx, req.CharacterID, req.Consumables); err != nil { return tribulationData{}, 3007, err.Error() } helpers := req.HelperIDs if len(helpers) > 0 { count, err := s.store.CountCharacters(ctx, helpers) if err != nil || count != len(helpers) { return tribulationData{}, 3005, "helper condition not met" } } tribType := req.TribulationType if tribType == "" { tribType = s.determineTribulationType(c) } successRate := s.calcBreakthroughRate(c, c.RealmTier, true, helpers) recordID, err := s.store.InsertTribulationRecord(ctx, &db.TribulationRecord{ CharacterID: req.CharacterID, RealmTier: req.TargetRealmTier, MinorRealm: 1, TribulationType: tribType, BaseSuccessRate: domain.DefaultTribulationBaseRate(c.RealmTier, true), ModifiedSuccessRate: successRate, HelperIDs: helpers, Result: domain.TribulationResultPending, Penalties: map[string]interface{}{}, DropsOnSuccess: []map[string]interface{}{}, SanSnapshot: c.SanCurrent, }) if err != nil { return tribulationData{}, 9002, "failed to create tribulation record" } // 进入渡劫状态 if err := s.store.UpdateCharacterRealm(ctx, req.CharacterID, c.RealmTier, c.MinorRealm, 0, domain.RealmStatusTribulationPending); err != nil { return tribulationData{}, 9002, "failed to set tribulation status" } // 同步结算(MVP 简化;生产环境可改为异步任务) result, penalties, drops := s.resolveTribulation(c, req.TargetRealmTier, tribType) if err := s.store.UpdateTribulationRecord(ctx, recordID, result, penalties, drops); err != nil { return tribulationData{}, 9002, "failed to update tribulation record" } fromRealm := c.RealmTier toRealm := c.RealmTier toMinor := c.MinorRealm realmStatus := domain.RealmStatusNormal switch result { case domain.TribulationResultSuccess: toRealm = req.TargetRealmTier toMinor = 1 if err := s.store.UpdateCharacterWorldAndRealm(ctx, req.CharacterID, c.WorldTier, toRealm, toMinor, realmStatus); err != nil { return tribulationData{}, 9002, "failed to advance realm" } case domain.TribulationResultFail: // 层内回退:圆满→中期 toMinor = 2 if err := s.store.UpdateCharacterRealm(ctx, req.CharacterID, fromRealm, toMinor, 0, realmStatus); err != nil { return tribulationData{}, 9002, "failed to setback realm" } case domain.TribulationResultBacklash: // 跨层回退或严重受挫 if pType, ok := penalties["type"].(string); ok && pType == "drop_cross_tier" && fromRealm > 1 { toRealm = fromRealm - 1 toMinor = 1 if err := s.store.UpdateCharacterWorldAndRealm(ctx, req.CharacterID, c.WorldTier, toRealm, toMinor, realmStatus); err != nil { return tribulationData{}, 9002, "failed to setback realm" } } else { toMinor = 2 if err := s.store.UpdateCharacterRealm(ctx, req.CharacterID, fromRealm, toMinor, 0, realmStatus); err != nil { return tribulationData{}, 9002, "failed to setback realm" } } case domain.TribulationResultDeath: // 角色进入死亡状态 if err := s.store.UpdateCharacterStatus(ctx, req.CharacterID, "dead"); err != nil { return tribulationData{}, 9002, "failed to set dead status" } } estimatedTicks := int32(120) if s.cfg != nil { estimatedTicks = int32(s.cfg.GetInt("tribulation.estimated_ticks", 120)) } return tribulationData{ TribulationID: recordID, Status: "completed", BaseSuccessRate: domain.DefaultTribulationBaseRate(fromRealm, true), ModifiedSuccessRate: successRate, EstimatedTicks: estimatedTicks, StartAt: time.Now().UTC().Format(time.RFC3339), Result: result, FromRealmTier: fromRealm, ToRealmTier: toRealm, Penalties: penalties, Drops: drops, }, 0, "" } func (s *RealmService) determineTribulationType(c *db.Character) string { if c.SanCurrent < 30 { return domain.TribulationTypeHeartDevil } if c.CrimeScore > 500 { return domain.TribulationTypeQiDeviation } if c.RealmTier >= 6 { return domain.TribulationTypeHeti } return domain.TribulationTypeNormal } func (s *RealmService) resolveTribulation(c *db.Character, targetTier int32, tribType string) (string, map[string]interface{}, []map[string]interface{}) { successRate := s.calcBreakthroughRate(c, c.RealmTier, true, nil) if s.randFloat() < successRate { drops := s.generateTribulationDrops(targetTier) return domain.TribulationResultSuccess, map[string]interface{}{"type": "none"}, drops } // 失败惩罚 result, penalties := s.resolveTribulationFailure(c, c.RealmTier) return result, penalties, nil } func (s *RealmService) resolveTribulationFailure(c *db.Character, tier int32) (string, map[string]interface{}) { dropWithin := s.getConfigFloat("tribulation.drop.l2_within", 0.70) dropCross := s.getConfigFloat("tribulation.drop.l2_cross", 0.20) frustration := s.getConfigFloat("tribulation.drop.frustration", 0.12) if tier >= 4 { dropCross = s.getConfigFloat("tribulation.drop.l4_cross_major", 0.30) remaining := 1.0 - dropCross - frustration if remaining < 0 { remaining = 0 } dropWithin = remaining } roll := s.randFloat() thunderDamage := s.calcTribulationDamage(tier, "thunder") fireDamage := s.calcTribulationDamage(tier, "fire") if roll < dropWithin { return domain.TribulationResultFail, map[string]interface{}{ "type": "drop_within_tier", "minor_realm_loss": 1, "exp_loss_pct": 0.5, "thunder_damage": thunderDamage, "fire_damage": fireDamage, } } if roll < dropWithin+dropCross { return domain.TribulationResultBacklash, map[string]interface{}{ "type": "drop_cross_tier", "realm_tier_loss": 1, "minor_realm_set": 1, "exp_loss_pct": 0.8, "thunder_damage": thunderDamage, "fire_damage": fireDamage, "san_loss": 15, } } return domain.TribulationResultBacklash, map[string]interface{}{ "type": "frustration", "san_loss": 10, "exp_loss_pct": 0.3, "thunder_damage": thunderDamage, "fire_damage": fireDamage, } } func (s *RealmService) calcTribulationDamage(tier int32, dmgType string) float64 { if dmgType == "thunder" { base := s.getConfigFloat("tribulation.thunder.base_pct", 0.115) return base * domain.ThunderLayerCoef(tier) } base := s.getConfigFloat("tribulation.fire.base_pct", 0.045) return base * domain.FireLayerCoef(tier) } func (s *RealmService) generateTribulationDrops(targetTier int32) []map[string]interface{} { // 按目标境界生成天材地宝/心法感悟 drops := []map[string]interface{}{ {"item_id": fmt.Sprintf("heavenly_dew_t%d", targetTier), "quantity": 1}, } if s.randFloat() < 0.3 { drops = append(drops, map[string]interface{}{"item_id": "insight_fragment", "quantity": 1}) } return drops } // GetTribulationResult 查询最近一次渡劫记录。 func (s *RealmService) GetTribulationResult(ctx context.Context, characterID string) (tribulationData, int, string) { rec, err := s.store.GetLatestTribulation(ctx, characterID) if err != nil { return tribulationData{}, 3007, "no tribulation record" } status := "completed" if rec.Result == domain.TribulationResultPending { status = "in_progress" } return tribulationData{ TribulationID: rec.ID, Status: status, BaseSuccessRate: rec.BaseSuccessRate, ModifiedSuccessRate: rec.ModifiedSuccessRate, EstimatedTicks: 0, StartAt: rec.CreatedAt.Format(time.RFC3339), Result: rec.Result, FromRealmTier: rec.RealmTier - 1, ToRealmTier: rec.RealmTier, Penalties: rec.Penalties, Drops: rec.DropsOnSuccess, }, 0, "" } // WorldBreak 破界/晋级到下一世界层级。 func (s *RealmService) WorldBreak(ctx context.Context, req worldBreakReq) (worldBreakData, int, string) { c, err := s.store.GetCharacter(ctx, req.CharacterID) if err != nil { return worldBreakData{}, 3011, "character not found" } if c.Status != "active" { return worldBreakData{}, 3014, "character not active" } if c.RealmStatus == domain.RealmStatusTribulationPending { return worldBreakData{}, 3014, "tribulation pending" } if req.TargetWorldTier != c.WorldTier+1 { return worldBreakData{}, 3011, "target world tier not unlocked" } // 破界要求:境界达到目标层级圆满,或已超越 if !(c.RealmTier >= req.TargetWorldTier && (c.MinorRealm == 3 || c.RealmTier > req.TargetWorldTier)) { return worldBreakData{}, 3013, "realm not perfected" } // 首次 1→2 可不填;后续需要破界钥 if req.TargetWorldTier > 2 { keyItemID := s.getConfigString("realm.world_break_key_item_id", "world_break_key") if req.KeyItemInstanceID == "" { return worldBreakData{}, 3012, "missing world break key" } has, err := s.store.HasInventoryItem(ctx, req.CharacterID, req.KeyItemInstanceID, keyItemID, 1) if err != nil || !has { return worldBreakData{}, 3012, "missing world break key" } if err := s.store.ConsumeInventoryItem(ctx, req.CharacterID, req.KeyItemInstanceID, 1); err != nil { return worldBreakData{}, 3012, "failed to consume key" } } // 跨层携带限制 if err := s.checkCarryLimit(ctx, c, req.TargetWorldTier); err != nil { return worldBreakData{}, 3015, err.Error() } // 应用携带限制:低阶货币超cap部分被系统回收(写审计) if err := s.applyCarryLimit(ctx, c, req.TargetWorldTier); err != nil { return worldBreakData{}, 3015, err.Error() } // 更新世界层级 toRealmTier := c.RealmTier toMinorRealm := c.MinorRealm if err := s.store.UpdateCharacterWorldAndRealm(ctx, req.CharacterID, req.TargetWorldTier, toRealmTier, toMinorRealm, domain.RealmStatusNormal); err != nil { return worldBreakData{}, 9002, "failed to break world" } // 是否生成破界遗迹 ruinGenerated := s.randFloat() < s.ruinSpawnRate(req.TargetWorldTier) rec := &db.BreakthroughRecord{ CharacterID: req.CharacterID, FromRealmTier: c.RealmTier, ToRealmTier: toRealmTier, FromMinorRealm: c.MinorRealm, ToMinorRealm: toMinorRealm, IsSuccess: true, IsBreakWorldBarrier: true, SourceWorldTier: c.WorldTier, TargetWorldTier: req.TargetWorldTier, } if err := s.store.InsertBreakthroughRecord(ctx, rec); err != nil { return worldBreakData{}, 9002, "failed to record world break" } worldEvent := fmt.Sprintf("破界·%s已解锁", s.worldLayerName(req.TargetWorldTier)) if ruinGenerated { worldEvent += "·遗迹显化" } return worldBreakData{ Success: true, FromWorldTier: c.WorldTier, ToWorldTier: req.TargetWorldTier, FromRealmTier: c.RealmTier, ToRealmTier: toRealmTier, BreakthroughRecordID: "", WorldEvent: worldEvent, RuinGenerated: ruinGenerated, }, 0, "" } func (s *RealmService) getConfigString(key, def string) string { if s.cfg == nil { return def } return s.cfg.GetString(key, def) } func (s *RealmService) worldLayerName(tier int32) string { switch tier { case 1: return "种族出生地" case 2: return "洪荒主陆" case 3: return "洪荒腹地" case 4: return "太古秘境" case 5: return "混沌之渊·化神域" case 6: return "混沌之渊·合体域" } return "未知世界" } func (s *RealmService) ruinSpawnRate(tier int32) float64 { key := fmt.Sprintf("world.ruin.spawn_rate_l%d", tier) defaults := map[int32]float64{ 2: 0.01, 3: 0.065, 4: 0.10, 5: 0.15, 6: 0.215, } return s.getConfigFloat(key, defaults[tier]) } // checkCarryLimit 校验跨层携带是否违反硬上限。 func (s *RealmService) checkCarryLimit(ctx context.Context, c *db.Character, targetTier int32) error { balances, err := s.store.GetCurrencyBalances(ctx, c.ID) if err != nil { return err } for currencyCode, amount := range balances { if amount <= 0 { continue } worldTier, err := s.store.GetCurrencyWorldTier(ctx, currencyCode) if err != nil { continue } // 货币层级低于目标-1 时进入硬上限判断 if worldTier < targetTier-1 { cap := s.carryCap(currencyCode, targetTier) if amount > cap { return fmt.Errorf("carry limit exceeded for %s (max %.4f)", currencyCode, cap) } } } return nil } // applyCarryLimit 对超 cap 的低阶货币执行系统回收,并记录审计。 func (s *RealmService) applyCarryLimit(ctx context.Context, c *db.Character, targetTier int32) error { balances, err := s.store.GetCurrencyBalances(ctx, c.ID) if err != nil { return err } for currencyCode, amount := range balances { if amount <= 0 { continue } worldTier, err := s.store.GetCurrencyWorldTier(ctx, currencyCode) if err != nil { continue } if worldTier < targetTier-1 { cap := s.carryCap(currencyCode, targetTier) if amount > cap { removed := amount - cap if err := s.store.SetCurrencyBalance(ctx, c.ID, currencyCode, cap); err != nil { return err } if err := s.store.AuditCurrencyFlow(ctx, c.ID, "character", c.ID, currencyCode, "sink", "world_break_carry_tax", -removed, cap, c.WorldTier, ""); err != nil { return err } } } } return nil } func (s *RealmService) carryCap(currencyCode string, targetTier int32) float64 { // 目标 L2 时低阶货币保留较多;L3+ 仅允许保留少量低阶货币 switch targetTier { case 2: return 50000 case 3: if currencyCode == "copper" || currencyCode == "silver" { return 0 // 铜钱/银两不可带入灵石世界 } return 100 case 4, 5, 6: if currencyCode == "copper" || currencyCode == "silver" { return 0 } return 10 } return math.MaxFloat64 }