lawless/server/modules/realm.go

1065 行
35 KiB
Go

此文件含有模棱两可的 Unicode 字符

此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。

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
}