# TDD-06 离线挂机结算系统设计 > 文档类型:技术设计文档(Technical Design Document) > 版本:1.1 > 日期:2026-07-02 > 关联文档:TDD-04《数据库表结构设计》、TDD-05《API接口设计》、GDD-03《战斗系统设计》、GDD-06《经济系统设计》、GDD-07《帮派门派社交系统设计》、GDD-22《开放世界随机事件》、GDD-23《能量体系与功法相性设计》 --- ## 1. 文档信息 | 项目 | 说明 | |------|------| | 目标 | 为《洪荒大陆》挂机手游定义离线挂机结算系统的服务端技术方案,覆盖离线时间快进、资源批量结算、ATB战斗离线结算、弟子代挂、游历事件触发、上线结算面板等核心模块。 | | 读者 | 服务端开发(Nakama/Go)、数值策划、测试 | | 技术栈 | Nakama 3.x + Go插件 + PostgreSQL 16 + Valkey + Nacos 2.x | | 核心约束 | 无任务系统、无赛季重置、概率/机遇驱动、文字战报、ATB行动条、功法加持、能量体系(非体力) | | 游戏时间 | 现实:游戏 = 1:3 | --- ## 2. 系统总览 ### 2.1 离线结算的核心循环 ``` 玩家下线 ↓ 记录 last_online_at + 当前状态快照 ↓ 服务端定时任务扫描离线玩家(每 5 分钟) ↓ 时间快进算法:计算离线期间的事件队列 ↓ 批量结算:资源产出 / 弟子代挂 / 游历事件 / 战斗 ↓ 生成结算结果 JSON(压缩存储到 Valkey) ↓ 玩家上线 ↓ 拉取结算结果 → 展示结算面板 → 应用状态变更到 DB ``` ### 2.2 设计原则 | 原则 | 说明 | |------|------| | **服务端权威** | 所有离线结算逻辑在服务端完成,客户端只负责展示结算面板 | | **延迟结算** | 离线期间只记录事件队列,玩家上线时才批量执行结算(减少 DB 写入压力) | | **时间快进** | 离线时间按游戏时间 1:3 换算后,以固定步长快进模拟事件触发 | | **幂等性** | 结算结果带版本号,重复登录不会重复结算 | | **上限兜底** | 背包满/货币上限/弟子异常等边界情况在结算时统一处理 | --- ## 3. 离线期间事件触发的服务端模拟逻辑 ### 3.1 时间快进算法 离线期间不实时模拟,而是玩家上线时通过"时间快进"算法一次性计算离线期间的所有事件。 ``` 算法:OfflineTimeWarp(character_id, last_online_at, now) 输入: last_online_at — 玩家最后在线时间(timestamptz) now — 当前时间(timestamptz) 输出: event_queue — 离线期间触发的事件列表 步骤: 1. 计算离线时长(现实时间) offline_duration_real = now - last_online_at offline_duration_game = offline_duration_real × 3 // 现实:游戏 = 1:3 2. 限制最大离线结算时长(按境界递增,防止超长离线导致结算爆炸) max_offline_hours = 按角色境界动态确定(见下方境界递增表) if offline_duration_real > max_offline_hours: offline_duration_real = max_offline_hours offline_duration_game = max_offline_hours × 3 境界递增离线上限表: 炼气期 → 72 现实小时(默认基准,约3天) 筑基期 → 96 现实小时(约4天) 金丹期 → 120 现实小时(约5天,新默认上限) 元婴期及以上 → 无上限(完全离线结算,不限制时长) // 通过 Nacos 配置可覆盖: // offline.max_settle_hours.default = 120(默认上限,金丹期) // offline.max_settle_hours.qi_refining = 72 // offline.max_settle_hours.foundation = 96 // offline.max_settle_hours.golden_core = 120 // offline.max_settle_hours.nascent_soul = 0(0表示无上限) 3. 确定时间步长(tick_interval) // 每个 tick 代表一段游戏时间,期间可能触发 0~N 个事件 tick_interval_game = 1 游戏小时 // 可通过 Nacos 配置 4. 加载玩家离线前状态快照 snapshot = load_character_snapshot(character_id) // 包含:境界、属性、装备、功法、弟子列表、当前挂机状态 5. 加载区域事件池 zone_events = load_zone_event_pool(snapshot.zone_id, snapshot.world_tier) // 来自 GDD-22 的区域事件配置 6. 时间快进循环 current_game_time = last_online_at × 3 // 转为游戏时间 end_game_time = now × 3 event_queue = [] while current_game_time < end_game_time: // 6a. 检查定时触发事件 for event in zone_events: if event.trigger_type == "periodic": if current_game_time % event.period == 0: event_queue.append(Event(event, current_game_time)) // 6b. 概率触发事件(游历/挂机遭遇) for event in zone_events: if event.trigger_type == "probability": roll = random(0, 1) // 概率衰减:连续触发同类事件时概率递减 decay = get_fatigue_decay(character_id, event.category) effective_rate = event.base_rate × decay if roll < effective_rate: event_queue.append(Event(event, current_game_time)) // 6c. 弟子代挂事件检查 for disciple in snapshot.disciples: if disciple.status == "dispatched": disciple_tick_result = simulate_disciple_tick(disciple, current_game_time) event_queue.append(disciple_tick_result) // 6d. 推进时间 current_game_time += tick_interval_game 7. 返回 event_queue ``` ### 3.2 事件队列结构 ```go type OfflineEvent struct { EventID string `json:"event_id"` EventType string `json:"event_type"` // combat / gather / disciple / encounter / resource TriggerTime int64 `json:"trigger_time"` // 游戏时间戳 ZoneID string `json:"zone_id"` WorldTier int `json:"world_tier"` RealmTier int `json:"realm_tier"` Payload map[string]interface{} `json:"payload"` // 事件特定数据 Resolved bool `json:"resolved"` // 是否已结算 Result map[string]interface{} `json:"result"` // 结算结果 } ``` ### 3.3 事件队列回放 ```go // 玩家上线时调用 func SettleOfflineEvents(characterID string) (*SettlementReport, error) { // 1. 加载事件队列 events := loadEventQueue(characterID) // 2. 按时间顺序回放 report := &SettlementReport{CharacterID: characterID} for _, event := range events { if event.Resolved { continue // 已结算跳过(幂等) } switch event.EventType { case "combat": result := resolveOfflineCombat(event) report.Battles = append(report.Battles, result) case "gather": result := resolveOfflineGather(event) report.Resources = append(report.Resources, result) case "disciple": result := resolveDiscipleEvent(event) report.DiscipleEvents = append(report.DiscipleEvents, result) case "encounter": result := resolveEncounter(event) report.Encounters = append(report.Encounters, result) } event.Resolved = true saveEventResult(event) } // 3. 批量应用状态变更 applySettlementToDatabase(report) // 4. 压缩存储结算报告 saveCompressedReport(characterID, report) return report, nil } ``` --- ## 4. 离线产出计算公式 ### 4.1 挂机资源产出(接入 GDD-06 经济参数) 离线挂机产出遵循 GDD-06 §5.3 的"单位时间/风险期望产出参考"。 ``` 离线挂机资源产出公式: base_output = zone_base_rate × realm_multiplier × game_hours actual_output = base_output × quality_modifier × fatigue_decay × facility_bonus 参数说明: zone_base_rate — 当前区域基础产出率(来自 GDD-08-附录A) realm_multiplier — 境界倍率(炼气 1.0 / 筑基 1.5 / 金丹 2.5 / 元婴 4.0 / 化神 6.5 / 合体 10.0) game_hours — 离线游戏时长(现实小时 × 3) quality_modifier — 弟子/装备品质修正(1.0~2.5) fatigue_decay — 疲劳衰减系数(连续挂机同一区域,每游戏小时 -5%,最低 0.3) facility_bonus — 门派/领地设施加成(1.0~2.2) ``` **接入 GDD-06 各境界产出基线**: | 境界 | 1 游戏小时基础产出 | 离线挂机上限(现实 24h) | |------|-------------------|------------------------| | 炼气期 | 3~6 铜钱 或 低品灵石碎片×0.2 | 216~432 铜钱 | | 筑基期 | 15~30 铜钱 或 灵石×0.3~0.5 | 1080~2160 铜钱 | | 金丹期 | 灵石(中品)×0.4~0.8 | 28.8~57.6 中品灵石 | | 元婴期 | 魂晶/仙晶碎片×0.3~0.6 | 21.6~43.2 碎片 | | 化神期 | 仙晶×0.2~0.4 | 14.4~28.8 仙晶 | | 合体期 | 仙晶(上品)×0.15~0.3 | 10.8~21.6 上品仙晶 | ### 4.2 弟子代挂产出(接入 GDD-07 弟子系统) ``` 弟子代挂产出公式: disciple_output = base_output × disciple_quality × race_match × skill_match × guild_facility 参数说明: base_output — 同 §4.1 的区域基础产出 disciple_quality — 弟子品质系数(凡品 0.8 / 良品 1.0 / 优品 1.3 / 极品 1.8 / 仙品 2.5) race_match — 种族匹配系数(地精挖矿 1.3 / 矮人锻造 1.2 / 其他 1.0) skill_match — 生活技能匹配系数(匹配 1.5 / 不匹配 1.0) guild_facility — 门派设施等级加成(Lv1 1.0 / Lv2 1.2 / Lv3 1.45 / Lv4 1.8 / Lv5 2.2) ``` **弟子品质与产出对照表(GDD-07 §2.3.2)**: | 品质 | 出现概率 | 效率加成 | 特殊能力 | |------|---------|---------|---------| | 凡品 | 55% | 80% | 无 | | 良品 | 30% | 100% | 小幅降低意外率 | | 优品 | 12% | 130% | 可学习一种生活技能加成 | | 极品 | 2.8% | 180% | 有概率触发双倍产出 | | 仙品 | 0.2% | 250% | 可独立触发小型奇遇 | ### 4.3 游历事件产出 ``` 游历事件产出公式: explore_output = event_reward × risk_multiplier × time_efficiency × sect_bonus 参数说明: event_reward — 事件基础奖励(来自 GDD-22 事件池配置) risk_multiplier — 风险倍率(闲逛 1.0 / 历练 1.3 / 秘境 1.8 / 古迹 2.5 / 禁地 4.0) time_efficiency — 时间效率(离线游历按实际游戏时长折算,非满效率) sect_bonus — 系统门派游历加成(门派等级 × 2.5%,最高 25%) ``` --- ## 5. 离线 ATB 战斗结算(接入 GDD-03 战斗公式) ### 5.1 离线战斗触发条件 离线期间战斗由以下场景触发: - 游历途中遭遇野怪/精英/Boss(GDD-03 §6) - 弟子代挂遭遇战斗 - 被其他玩家 PVP 挑战(离线战书默认接受,GDD-03 ✅14) ### 5.2 服务端完整 ATB 战斗计算 离线战斗采用与在线战斗完全相同的 ATB 引擎,服务端一次性完整计算。 ```go // 离线ATB战斗结算(接入 GDD-03 §三 ATB 机制) func SimulateOfflineBattle(attacker, defender *CombatUnit, context BattleContext) *BattleResult { // 1. 初始化战斗状态 battle := &BattleState{ Units: []*CombatUnit{attacker, defender}, Tick: 0, MaxTicks: 3000, // ✅10 战斗行动时间上限 MaxActions: 50, // ✅10 单方最多50次行动 } // 2. 初始化行动条(先手/伏击判定) // GDD-03 §3.1:普通遭遇双方初始 0;先手方初始 50 attacker.ATBGauge = context.AttackerInitGauge // 0 或 50 defender.ATBGauge = context.DefenderInitGauge // 0 或 50 // 3. 加载技能候选池 attacker.Skills = loadActiveSkills(attacker.CharacterID) defender.Skills = loadActiveSkills(defender.CharacterID) // 4. ATB 主循环 report := &BattleReport{Rounds: []RoundLog{}} for battle.Tick < battle.MaxTicks { battle.Tick++ // 4a. 行动条填充(GDD-03 §3.1) for _, unit := range battle.Units { if unit.HP > 0 { increment := unit.Speed * 0.1 // ATB_BASE_COEFFICIENT = 0.1 unit.ATBGauge += increment } } // 4b. 检查可行动单位 readyUnits := []*CombatUnit{} for _, unit := range battle.Units { if unit.ATBGauge >= 100 && unit.HP > 0 { readyUnits = append(readyUnits, unit) } } // 4c. 同 tick 满条冲突判定(GDD-03 §3.1) if len(readyUnits) > 1 { sort.Slice(readyUnits, func(i, j int) bool { vi := readyUnits[i].Speed * (1 + randomFloat(-0.1, 0.1)) vj := readyUnits[j].Speed * (1 + randomFloat(-0.1, 0.1)) return vi > vj }) } // 4d. 逐个执行行动 for _, unit := range readyUnits { target := getOpponent(battle, unit) // 控制状态检查 if unit.HasStatus("stun") { report.Rounds = append(report.Rounds, RoundLog{ Tick: battle.Tick, Actor: unit.ID, Action: "stunned", Message: "眩晕中无法行动", }) unit.ATBGauge = 0 continue } // 逃跑判定(GDD-03 §3.8 ✅32-✅34) // 玩家预设血量/能量/SAN阈值,达到阈值后每次ATB满自动尝试逃跑 if unit.IsPlayer && shouldAttemptEscape(unit, context.EscapeConfig) { escapeRate := calculateEscapeRate(unit, target, context) if random(100) < escapeRate { // 逃跑成功:本场战利品清零,无死亡惩罚 report.Rounds = append(report.Rounds, RoundLog{ Tick: battle.Tick, Actor: unit.ID, Action: "escape_success", Message: "成功逃离战斗", }) result := &BattleResult{ Winner: "escape", EndCondition: "escape_success", LootCleared: true, // ✅33 逃跑成功战利品清零 DeathPenalty: false, // ✅33 无死亡惩罚 } report.Result = result return result } else { // 逃跑失败:无惩罚,继续战斗 report.Rounds = append(report.Rounds, RoundLog{ Tick: battle.Tick, Actor: unit.ID, Action: "escape_fail", Message: "逃跑失败", }) unit.ATBGauge = 0 continue } } // 技能选择(GDD-03 §3.3 ✅24 触发率机制) skill := selectSkill(unit, target, battle) // 命中判定 hitChance := 100 - target.DodgeRate + unit.HitBonus if random(100) > hitChance { report.Rounds = append(report.Rounds, RoundLog{ Tick: battle.Tick, Actor: unit.ID, Skill: skill.ID, Action: "miss", Message: "身形一晃,闪过了攻击", }) unit.ATBGauge = 0 continue } // 伤害计算(GDD-03 §2.2) damage := calculateDamage(unit, target, skill, context) // 暴击判定(GDD-03 §2.2 ✅18) critRate := unit.Luck * 0.15 + skill.CritBonus if critRate > 60 { critRate = 60 } // 上限60% isCrit := random(100) < critRate if isCrit { critMultiplier := 1.5 // 基础×1.5 if critMultiplier > 2.0 { critMultiplier = 2.0 } // 上限×2.0 damage = damage * critMultiplier } // 应用伤害 target.HP -= damage if target.HP < 0 { target.HP = 0 } // 记录战报 report.Rounds = append(report.Rounds, RoundLog{ Tick: battle.Tick, Actor: unit.ID, Skill: skill.ID, Damage: damage, IsCrit: isCrit, HPAfter: map[string]int{attacker.ID: attacker.HP, defender.ID: defender.HP}, }) // 行动条归零 unit.ATBGauge = 0 // 行动次数计数 unit.ActionCount++ if unit.ActionCount >= battle.MaxActions { goto endBattle } // 死亡判定 if target.HP <= 0 { goto endBattle } // 天赋触发判定(GDD-03 §4.2) checkAndTriggerTalents(unit, target, battle, report) } // 4e. DOT/状态效果结算 processStatusEffects(battle, report) // 4f. 战斗结束条件检查(GDD-03 §3.7) if attacker.HP <= 0 || defender.HP <= 0 { break } } endBattle: // 5. 判定胜负(GDD-03 §3.7 超时处理) result := determineWinner(battle, context) report.Result = result return result } // 逃跑判定辅助函数(GDD-03 §3.8 ✅32-✅34) // shouldAttemptEscape 判断是否应该尝试逃跑 func shouldAttemptEscape(unit *CombatUnit, config EscapeConfig) bool { // 检查血量阈值 if config.HPThreshold > 0 { hpPercent := float64(unit.HP) / float64(unit.MaxHP) * 100 if hpPercent <= config.HPThreshold { return true } } // 检查能量阈值 if config.EPThreshold > 0 { epPercent := float64(unit.EP) / float64(unit.MaxEP) * 100 if epPercent <= config.EPThreshold { return true } } // 检查SAN阈值 if config.SANThreshold > 0 && unit.SAN > 0 { if unit.SAN <= config.SANThreshold { return true } } return false } // calculateEscapeRate 计算逃跑成功率(GDD-03 §3.8.3 ✅34) func calculateEscapeRate(escaper, chaser *CombatUnit, context BattleContext) float64 { // 基础率:35% baseRate := 35.0 // 速度差系数:己方速度/对方速度(上限2.0,下限0.5) speedRatio := float64(escaper.Speed) / float64(chaser.Speed) if speedRatio > 2.0 { speedRatio = 2.0 } if speedRatio < 0.5 { speedRatio = 0.5 } // 状态修正 statusMod := 1.0 if escaper.HasStatus("stun") || escaper.HasStatus("root") { return 0 // 眩晕/定身无法逃跑 } if escaper.HasStatus("slow") { statusMod = 0.7 } if escaper.HasStatus("haste") { statusMod = 1.3 } // 道具修正 itemMod := 1.0 if context.HasItem("escape_talisman") { itemMod = 1.5 } if context.HasItem("teleport_talisman") { return 100 // 瞬移符直接成功 } finalRate := baseRate * speedRatio * statusMod * itemMod if finalRate > 100 { finalRate = 100 } return finalRate } ``` ### 5.3 战斗伤害计算公式(接入 GDD-03 §2.2) ```go // 物理伤害(GDD-03 §2.2) func calculatePhysicalDamage(attacker, defender *CombatUnit, skill *Skill) float64 { baseDamage := float64(attacker.Str)*1.0 + attacker.WeaponAtk // 攻击系数=1.0 skillDamage := baseDamage * skill.DamageCoeff // 亲和度+共鸣修正(GDD-03 §4.4 ✅20) affinityMod := 1.0 + skill.AffinityMod resonanceMod := 1.0 + skill.ResonanceMod skillDamage = skillDamage * affinityMod * resonanceMod // 物理减伤率(GDD-03 §2.2) defCoeff := 0.5 defConst := getDefenseConstant(defender.WorldTier) // Tier1:100 / Tier2:150 / ... / Tier6:640 phyReduce := float64(defender.Vit)*defCoeff / (float64(defender.Vit)*defCoeff + float64(defConst)) finalDamage := skillDamage * (1 - phyReduce) // 阵营伤害修正(GDD-03 §2.3 ✅12) alignmentMod := getAlignmentModifier(skill.Alignment, defender.Alignment) finalDamage = finalDamage * alignmentMod // 状态修正 finalDamage = finalDamage * attacker.StatusAtkMod return math.Max(finalDamage, 1) // 最低1点伤害 } // 法术伤害(GDD-03 §2.2) func calculateMagicalDamage(attacker, defender *CombatUnit, skill *Skill) float64 { baseDamage := float64(attacker.Spi)*1.0 + attacker.ArtifactAtk // 法攻系数=1.0 skillDamage := baseDamage * skill.DamageCoeff // 亲和度+共鸣修正 affinityMod := 1.0 + skill.AffinityMod resonanceMod := 1.0 + skill.ResonanceMod skillDamage = skillDamage * affinityMod * resonanceMod // 法术减伤率 magCoeff := 0.5 magConst := getDefenseConstant(defender.WorldTier) // 与防御常数一致 magReduce := float64(defender.Spi)*magCoeff / (float64(defender.Spi)*magCoeff + float64(magConst)) finalDamage := skillDamage * (1 - magReduce) // 元素克制修正(GDD-03 §2.4 ✅19) elementMod := getElementModifier(skill.Element, defender.Element) finalDamage = finalDamage * elementMod // 阵营伤害修正 alignmentMod := getAlignmentModifier(skill.Alignment, defender.Alignment) finalDamage = finalDamage * alignmentMod return math.Max(finalDamage, 1) } ``` ### 5.4 离线战斗性能优化 离线战斗与在线战斗共用同一套 ATB 引擎,但有以下优化: | 优化项 | 说明 | |--------|------| | **跳过战报文案渲染** | 离线计算只生成结构化数据,不生成文字战报文案;文案在客户端展示时即时生成 | | **批量预加载** | 一次性加载角色战斗快照 + 技能列表 + 天赋列表,减少 DB 查询 | | **计算结果缓存** | 战斗结果缓存到 Valkey,上线时直接读取 | | **异步结算** | 玩家上线时先返回"结算中"状态,后台 Goroutine 异步完成计算 | ### 5.5 离线战斗场景与惩罚对照(接入 GDD-03 §6.2) | 战斗场景 | 失败惩罚 | 离线处理 | |----------|---------|---------| | 普通野怪战败 | 不触发死亡惩罚;产出 -60% | 事件标记为 `combat_loss_normal` | | 精英怪战败 | 轻度死亡惩罚(进度 -10%) | 事件标记为 `combat_loss_elite`,应用轻度惩罚 | | Boss 战败 | 完整死亡惩罚(进度 -20%~30% + 纯度 + 道伤) | 事件标记为 `combat_loss_boss`,应用完整惩罚 | | PVP 失败 | 完整死亡惩罚 | 事件标记为 `pvp_loss`,应用完整惩罚 | --- ## 6. 离线上限与溢出处理 ### 6.1 背包满处理 ```go // 离线产出溢出处理 func handleInventoryOverflow(characterID string, items []ItemDrop) { inventory := loadInventory(characterID) maxSlots := inventory.MaxSlots // 由境界/装备决定 for _, item := range items { if inventory.UsedSlots >= maxSlots { // 背包满:转入邮件系统 sendToMail(characterID, item, MailType_OfflineOverflow, "离线产出溢出") continue } // 可堆叠物品尝试合并 if existing := inventory.FindStackable(item.ItemID); existing != nil { remaining := existing.MaxStack - existing.Quantity if remaining >= item.Quantity { existing.Quantity += item.Quantity } else { existing.Quantity = existing.MaxStack overflow := item.Quantity - remaining sendToMail(characterID, Item{ItemID: item.ItemID, Quantity: overflow}, MailType_OfflineOverflow, "离线产出溢出") } } else { inventory.AddItem(item) } } } ``` ### 6.2 货币上限处理 ```go // 离线货币产出上限处理(接入 GDD-06 经济参数) func handleCurrencyOverflow(characterID string, currencyCode string, amount decimal.Decimal) { balance := loadCurrencyBalance(characterID, currencyCode) maxBalance := getCurrencyMaxBalance(currencyCode) // 由 Nacos 配置 if balance.Amount + amount > maxBalance { overflow := (balance.Amount + amount) - maxBalance balance.Amount = maxBalance // 溢出部分按比例折算为低一档货币或转入邮件 converted := convertOverflowCurrency(currencyCode, overflow) if converted != nil { addCurrency(characterID, converted.Code, converted.Amount) } } else { balance.Amount += amount } // 写入经济审计日志(TDD-04 §5.4) writeEconomyAudit(characterID, currencyCode, "faucet", "offline_settle", amount) } ``` ### 6.3 弟子状态异常处理 | 异常状态 | 处理方式 | |----------|---------| | 弟子死亡 | 标记 `status = "dead"`,记录 `died_at`,生成墓碑数据(GDD-07),不产出后续收益 | | 弟子叛逃 | 标记 `status = "deserted"`,已产出保留,后续产出取消 | | 弟子受伤 | 标记临时 debuff,产出效率 -30%~50%,持续 N 游戏小时 | | 弟子顿悟 | 标记 `status = "insight"`,产出效率 +50%,持续 N 游戏小时,可能解锁新技能 | --- ## 7. 上线结算面板数据结构 ### 7.1 结算报告 JSON Schema ```json { "$schema": "http://json-schema.org/draft-07/schema#", "title": "OfflineSettlementReport", "type": "object", "required": ["report_id", "character_id", "offline_duration", "settlement_time", "summary"], "properties": { "report_id": { "type": "string", "description": "结算报告唯一ID(UUID)" }, "character_id": { "type": "string", "description": "角色ID" }, "settlement_version": { "type": "integer", "description": "结算版本号,用于幂等校验" }, "offline_duration": { "type": "object", "properties": { "real_seconds": { "type": "integer", "description": "离线现实秒数" }, "game_hours": { "type": "number", "description": "离线游戏时长(小时)" }, "capped": { "type": "boolean", "description": "是否触及上限截断" } } }, "settlement_time": { "type": "string", "format": "date-time", "description": "结算执行时间" }, "summary": { "type": "object", "description": "收益汇总", "properties": { "total_currency": { "type": "array", "items": { "type": "object", "properties": { "currency_code": { "type": "string" }, "amount": { "type": "string", "description": "Decimal字符串" }, "source": { "type": "string", "enum": ["gather", "disciple", "combat_drop", "encounter"] } } } }, "total_items": { "type": "array", "items": { "type": "object", "properties": { "item_id": { "type": "string" }, "item_name": { "type": "string" }, "quantity": { "type": "integer" }, "rarity": { "type": "string" } } } }, "overflow_items_count": { "type": "integer", "description": "溢出到邮件的物品数" }, "exp_gained": { "type": "string", "description": "修为增长" }, "energy_change": { "type": "string", "description": "能量变化" } } }, "battles": { "type": "array", "description": "战斗摘要列表", "items": { "type": "object", "properties": { "battle_id": { "type": "string" }, "battle_type": { "type": "string", "enum": ["expedition_pve", "pvp", "disciple_combat"] }, "trigger_time": { "type": "string", "format": "date-time" }, "opponent_name": { "type": "string" }, "opponent_race": { "type": "string" }, "opponent_realm": { "type": "string" }, "result": { "type": "string", "enum": ["win", "lose", "draw"] }, "end_condition": { "type": "string", "enum": ["hp_zero", "timeout", "surrender"] }, "rounds_count": { "type": "integer", "description": "行动次数" }, "damage_dealt": { "type": "string" }, "damage_taken": { "type": "string" }, "death_penalty_applied": { "type": "boolean" }, "drops_summary": { "type": "object", "properties": { "currency": { "type": "array" }, "items": { "type": "array" } } } } } }, "disciple_events": { "type": "array", "description": "弟子代挂事件列表", "items": { "type": "object", "properties": { "disciple_id": { "type": "string" }, "disciple_name": { "type": "string" }, "disciple_quality": { "type": "string" }, "event_type": { "type": "string", "enum": ["gather", "combat", "insight", "death", "desertion", "treasure"] }, "trigger_time": { "type": "string", "format": "date-time" }, "result": { "type": "string", "enum": ["success", "fail", "death", "insight"] }, "rewards": { "type": "object" }, "message": { "type": "string", "description": "叙事化描述" } } } }, "encounters": { "type": "array", "description": "游历事件列表", "items": { "type": "object", "properties": { "encounter_id": { "type": "string" }, "encounter_type": { "type": "string" }, "trigger_time": { "type": "string", "format": "date-time" }, "zone_name": { "type": "string" }, "title": { "type": "string" }, "description": { "type": "string" }, "auto_choice": { "type": "string", "description": "离线自动选择的分支" }, "choice_reason": { "type": "string", "description": "选择策略说明" }, "result": { "type": "object" } } } }, "state_changes": { "type": "object", "description": "角色状态变更", "properties": { "hp_change": { "type": "string" }, "energy_change": { "type": "string" }, "san_change": { "type": "string" }, "crime_score_change": { "type": "string" }, "realm_exp_change": { "type": "string" }, "death_occurred": { "type": "boolean" }, "tribulation_triggered": { "type": "boolean" } } } } } ``` ### 7.2 结算面板 API ```protobuf // gRPC 接口定义 service OfflineSettlementService { // 拉取离线结算报告(玩家上线时调用) rpc GetSettlementReport (GetSettlementReq) returns (SettlementReportResp); // 确认结算(客户端展示完面板后调用,应用状态变更) rpc ConfirmSettlement (ConfirmSettlementReq) returns (ConfirmSettlementResp); // 查询历史结算报告 rpc ListSettlementHistory (ListSettlementReq) returns (ListSettlementResp); } message GetSettlementReq { string character_id = 1; } message SettlementReportResp { int32 code = 1; string message = 2; SettlementReport data = 3; } message SettlementReport { string report_id = 1; int64 settlement_version = 2; OfflineDuration offline_duration = 3; SettlementSummary summary = 4; repeated BattleSummary battles = 5; repeated DiscipleEventSummary disciple_events = 6; repeated EncounterSummary encounters = 7; StateChanges state_changes = 8; string compressed_full_report = 9; // gzip 压缩的完整战报 JSON } ``` --- ## 8. 弟子代挂结算(接入 GDD-07 弟子系统) ### 8.1 弟子代挂结算流程 ``` 弟子代挂结算流程: 1. 加载弟子列表(status = "dispatched") 2. 计算离线游戏时长 3. 逐弟子结算: a. 检查派遣目标(自建门派资源点 / 帮派领地 / 历练点) b. 计算基础产出(§4.2 公式) c. 随机事件判定(弟子遭遇/顿悟/死亡) d. 累计产出到结算报告 4. 批量更新弟子状态 ``` ### 8.2 弟子品质对产出的影响 ```go // 弟子代挂产出计算 func calculateDiscipleOutput(disciple *Disciple, zone *Zone, gameHours float64) *OutputResult { // 基础产出 baseRate := zone.BaseOutputRate // 品质系数(GDD-07 §2.3.2) qualityCoeff := map[string]float64{ "common": 0.8, "fine": 1.0, "excellent": 1.3, "perfect": 1.8, "immortal": 2.5, }[disciple.Quality] // 种族匹配系数 raceCoeff := calculateRaceMatch(disciple.RaceID, zone.ZoneType) // 生活技能匹配 skillCoeff := 1.0 if hasMatchingSkill(disciple, zone) { skillCoeff = 1.5 } // 门派设施加成 facilityCoeff := getGuildFacilityBonus(disciple.GuildID, zone) // 极品/仙品双倍产出概率(GDD-07 §2.3.2) doubleChance := 0.0 if disciple.Quality == "perfect" { doubleChance = 0.15 // 15% 概率双倍 } else if disciple.Quality == "immortal" { doubleChance = 0.25 // 25% 概率双倍 } // 计算总产出 totalOutput := baseRate * qualityCoeff * raceCoeff * skillCoeff * facilityCoeff * gameHours // 随机波动 ±20% totalOutput = totalOutput * randomFloat(0.8, 1.2) // 双倍产出判定 if randomFloat(0, 1) < doubleChance { totalOutput *= 2 } return &OutputResult{Amount: totalOutput} } ``` ### 8.3 弟子死亡判定(接入 GDD-07 ✅T4 / GDD-13) ```go // 弟子离线死亡判定 func checkDiscipleDeath(disciple *Disciple, missionType string, difficulty int) bool { // 基础死亡率(随难度递增) baseDeathRate := map[string]float64{ "gathering": 0.001, // 采集:0.1% "training": 0.005, // 历练:0.5% "mercenary": 0.02, // 佣兵委托:2% "sect_proxy": 0.01, // 门派代挂:1% }[missionType] // 难度修正 difficultyMod := 1.0 + float64(difficulty-1)*0.5 // 每星 +50% // 品质修正(高品质弟子生存率更高) qualityMod := map[string]float64{ "common": 1.2, // 凡品 +20% 死亡率 "fine": 1.0, "excellent": 0.8, // 优品 -20% "perfect": 0.6, // 极品 -40% "immortal": 0.4, // 仙品 -60% }[disciple.Quality] // 保险道具修正 insuranceMod := 1.0 if disciple.InsuranceItemID != "" { insuranceMod = 0.3 // 有保险道具,死亡率 -70% } // 弟子死亡率修正字段(培养/装备可降低) deathRateMod := 1.0 - disciple.DeathRateModifier finalDeathRate := baseDeathRate * difficultyMod * qualityMod * insuranceMod * deathRateMod // 每游戏小时判定一次 return randomFloat(0, 1) < finalDeathRate } ``` ### 8.4 弟子事件池 | 事件类型 | 触发概率 | 效果 | 叙事示例 | |----------|---------|------|---------| | 正常产出 | 85% | 获得资源 | "弟子在矿脉中采集到灵石×N" | | 发现宝藏 | 3% | 额外稀有材料 | "弟子在洞窟深处发现一株千年灵草" | | 顿悟 | 5% | 弟子属性/技能提升 | "弟子在采集时顿悟,棍法熟练度+8%" | | 遭遇战斗 | 4% | 胜利获额外奖励,失败受伤 | "弟子遭遇妖兽袭击,奋力击退" | | 受伤 | 2% | 产出效率临时下降 | "弟子不慎中毒,需要休养" | | 死亡 | 0.1%~2% | 弟子永久损失 | "弟子在禁地中殒落..." | | 叛逃 | 0.05% | 弟子永久损失(仅低忠诚度) | "弟子心生异志,携物资潜逃" | --- ## 9. 游历事件离线触发(接入 GDD-22) ### 9.1 随机事件池抽取 ```go // 离线游历事件抽取 func pickOfflineEncounter(character *Character, zone *Zone, gameTime int64) *Encounter { // 加载区域事件池(GDD-22 §三) eventPool := loadZoneEventPool(zone.ID) // 过滤不适用离线的事件 validEvents := filterEvents(eventPool, func(e Event) bool { return e.OfflineEligible && character.RealmTier >= e.MinRealmTier }) // 计算每个事件的权重 weightedEvents := []WeightedEvent{} for _, event := range validEvents { weight := event.BaseWeight // 时段权重(GDD-03 ✅5:昼夜增益按游戏时间判定) timeMod := getTimeModifier(gameTime, event.TimePreference) weight *= timeMod // 天气/环境权重 envMod := getEnvironmentModifier(zone.Weather, event.ElementPreference) weight *= envMod // 疲劳衰减(同一区域连续探索) fatigueMod := getFatigueDecay(character.ID, event.Category) weight *= fatigueMod // 卜算/感知类天赋加成 if character.HasTalent("divination") { weight *= 1.3 } weightedEvents = append(weightedEvents, WeightedEvent{Event: event, Weight: weight}) } // 加权随机抽取 selected := weightedRandomSelect(weightedEvents) return selected } ``` ### 9.2 分支选择策略 离线状态下玩家无法实时决策,系统按预设策略自动选择: ```go // 离线自动分支选择策略 func autoSelectBranch(encounter *Encounter, character *Character) string { // 策略优先级: // 1. 安全优先(默认策略):选择风险最低的选项 // 2. 收益优先:选择预期收益最高的选项 // 3. 玩家预设:玩家上线时可设置离线偏好 strategy := character.OfflineStrategy // "safe" / "greedy" / "balanced" switch strategy { case "safe": return selectSafestOption(encounter.Options, character) case "greedy": return selectHighestRewardOption(encounter.Options, character) case "balanced": return selectBalancedOption(encounter.Options, character) default: return selectSafestOption(encounter.Options, character) } } // 安全优先选择 func selectSafestOption(options []EncounterOption, character *Character) string { safest := options[0] for _, opt := range options[1:] { if opt.RiskLevel < safest.RiskLevel { safest = opt } } return safest.ID } // 收益优先选择 func selectHighestRewardOption(options []EncounterOption, character *Character) string { best := options[0] bestEV := calculateExpectedValue(best, character) for _, opt := range options[1:] { ev := calculateExpectedValue(opt, character) if ev > bestEV { best = opt bestEV = ev } } return best.ID } // 期望值计算 func calculateExpectedValue(option EncounterOption, character *Character) float64 { rewardEV := option.RewardValue * option.SuccessRate riskEV := option.RiskValue * (1 - option.SuccessRate) * option.DeathPenaltyWeight return rewardEV - riskEV } ``` ### 9.3 离线事件类型与处理 | 事件类型 | 离线处理方式 | 接入文档 | |----------|-------------|---------| | 灵气异动 | 自动吸纳(安全策略)或标记(收益策略) | GDD-22 §三 | | 古修残魂 | 默认超度(最安全),不选择夺舍 | GDD-22 §三 | | 行脚商人 | 默认无视(避免被抢) | GDD-22 §三 | | 心魔低语 | 默认压制(最安全) | GDD-22 §三 | | 同族求救 | 默认无视(避免风险) | GDD-22 §三 | | 天降宝箱 | 默认开启 | GDD-22 §三 | | 遭遇战斗 | 自动按 ATB 引擎结算 | GDD-03 §六 | | 采集点 | 自动采集(消耗能量) | GDD-06 §三 | --- ## 10. 性能优化 ### 10.1 批量结算的并发模型 ``` 玩家上线触发结算 ↓ Goroutine Pool(N=CPU核心数) ├── Goroutine 1: 挂机资源结算 ├── Goroutine 2: 弟子代挂结算 ├── Goroutine 3: 游历事件结算 ├── Goroutine 4: 战斗结算(可能多个) └── Goroutine 5: 状态变更合并 ↓ Channel 汇总结果 ↓ 批量写入 DB(单事务) ``` ```go // 并发结算模型 func SettleOfflineConcurrent(characterID string) (*SettlementReport, error) { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() // 并发执行各模块结算 var ( gatherResult *GatherResult discipleResult *DiscipleResult encounterResult *EncounterResult battleResults []*BattleResult wg sync.WaitGroup mu sync.Mutex errs []error ) wg.Add(4) // 挂机资源结算 go func() { defer wg.Done() r, err := settleOfflineGather(ctx, characterID) mu.Lock() if err != nil { errs = append(errs, err) } else { gatherResult = r } mu.Unlock() }() // 弟子代挂结算 go func() { defer wg.Done() r, err := settleOfflineDisciples(ctx, characterID) mu.Lock() if err != nil { errs = append(errs, err) } else { discipleResult = r } mu.Unlock() }() // 游历事件结算 go func() { defer wg.Done() r, err := settleOfflineEncounters(ctx, characterID) mu.Lock() if err != nil { errs = append(errs, err) } else { encounterResult = r } mu.Unlock() }() // 战斗结算(可能包含多场战斗) go func() { defer wg.Done() results, err := settleOfflineBattles(ctx, characterID) mu.Lock() if err != nil { errs = append(errs, err) } else { battleResults = results } mu.Unlock() }() wg.Wait() if len(errs) > 0 { return nil, fmt.Errorf("settlement errors: %v", errs) } // 合并结果 report := mergeResults(gatherResult, discipleResult, encounterResult, battleResults) // 批量写入 DB(单事务) err := applySettlementInTransaction(ctx, characterID, report) if err != nil { return nil, err } return report, nil } ``` ### 10.2 数据库写入优化 | 优化策略 | 说明 | |----------|------| | **单事务批量写入** | 所有结算结果在一个事务中写入,减少事务开销 | | **UPSERT 代替 INSERT+UPDATE** | 货币余额、背包物品使用 `INSERT ... ON CONFLICT UPDATE` | | **延迟写入经济审计** | 经济审计日志先缓存到 Valkey,定时批量刷入 DB(每 5 分钟) | | **分区表利用** | `battle_logs` 按周分区,`economy_audit_logs` 按月分区,写入只命中当前分区 | | **连接池调优** | 结算高峰期使用独立连接池,避免影响在线请求 | ```go // 单事务批量写入 func applySettlementInTransaction(ctx context.Context, characterID string, report *SettlementReport) error { tx, err := db.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelReadCommitted}) if err != nil { return err } defer tx.Rollback() // 1. 更新角色状态 _, err = tx.ExecContext(ctx, ` UPDATE characters SET exp = exp + $1, san_current = GREATEST(0, LEAST(san_max, san_current + $2)), crime_score = GREATEST(0, crime_score + $3), last_online_at = NOW(), updated_at = NOW() WHERE id = $4 `, report.ExpGained, report.SanChange, report.CrimeChange, characterID) // 2. 批量更新货币余额(UPSERT) for _, curr := range report.TotalCurrency { _, err = tx.ExecContext(ctx, ` INSERT INTO currency_balances (character_id, currency_code, amount, total_earned, updated_at) VALUES ($1, $2, $3, $3, NOW()) ON CONFLICT (character_id, currency_code) DO UPDATE SET amount = currency_balances.amount + EXCLUDED.amount, total_earned = currency_balances.total_earned + EXCLUDED.total_earned, updated_at = NOW() `, characterID, curr.CurrencyCode, curr.Amount) } // 3. 批量插入背包物品 for _, item := range report.TotalItems { _, err = tx.ExecContext(ctx, ` INSERT INTO inventories (id, character_id, item_id, slot_type, quantity, instance_data, acquired_at, created_at) VALUES (gen_random_uuid(), $1, $2, 'bag', $3, $4, NOW(), NOW()) `, characterID, item.ItemID, item.Quantity, item.InstanceData) } // 4. 更新弟子状态 for _, de := range report.DiscipleEvents { if de.EventType == "death" { _, err = tx.ExecContext(ctx, ` UPDATE disciples SET status = 'dead', died_at = NOW(), tombstone_data = $1 WHERE id = $2 `, de.TombstoneData, de.DiscipleID) } } // 5. 写入战斗记录 for _, battle := range report.Battles { _, err = tx.ExecContext(ctx, ` INSERT INTO battles (id, battle_type, world_tier, realm_tier, attacker_id, defender_id, status, result_summary, created_at) VALUES ($1, $2, $3, $4, $5, $6, 'completed', $7, $8) `, battle.ID, battle.Type, battle.WorldTier, battle.RealmTier, battle.AttackerID, battle.DefenderID, battle.ResultSummary, battle.CreatedAt) } // 6. 写入经济审计日志 for _, audit := range report.EconomyAudits { _, err = tx.ExecContext(ctx, ` INSERT INTO economy_audit_logs (character_id, entity_type, entity_id, currency_code, flow_type, reason_code, amount, balance_after, world_tier, created_at) VALUES ($1, 'character', $1, $2, 'faucet', 'offline_settle', $3, $4, $5, NOW()) `, characterID, audit.CurrencyCode, audit.Amount, audit.BalanceAfter, audit.WorldTier) } // 7. 标记结算完成 _, err = tx.ExecContext(ctx, ` UPDATE characters SET offline_settle_version = offline_settle_version + 1 WHERE id = $1 `, characterID) return tx.Commit() } ``` ### 10.3 结算结果压缩存储 ```go // 结算结果压缩存储到 Valkey func saveCompressedReport(characterID string, report *SettlementReport) error { // JSON 序列化 jsonBytes, err := json.Marshal(report) if err != nil { return err } // gzip 压缩 var buf bytes.Buffer gz := gzip.NewWriter(&buf) if _, err := gz.Write(jsonBytes); err != nil { return err } gz.Close() // 存储到 Valkey,TTL = 7 天 key := fmt.Sprintf("offline_settle:%s:%d", characterID, report.SettlementVersion) return valkey.Set(ctx, key, buf.Bytes(), 7*24*time.Hour) } // 从 Valkey 读取并解压 func loadCompressedReport(characterID string, version int64) (*SettlementReport, error) { key := fmt.Sprintf("offline_settle:%s:%d", characterID, version) data, err := valkey.Get(ctx, key).Bytes() if err != nil { return nil, err } // gzip 解压 gz, err := gzip.NewReader(bytes.NewReader(data)) if err != nil { return nil, err } defer gz.Close() var report SettlementReport if err := json.NewDecoder(gz).Decode(&report); err != nil { return nil, err } return &report, nil } ``` ### 10.4 性能指标与监控 | 指标 | 目标值 | 监控方式 | |------|--------|---------| | 单次结算耗时(24h 离线) | < 500ms | Prometheus histogram | | 单次结算耗时(120h 离线) | < 3s | Prometheus histogram | | 结算并发数 | 100/s(峰值) | Grafana dashboard | | DB 写入延迟 | < 100ms | PostgreSQL slow query log | | Valkey 缓存命中率 | > 95% | Valkey INFO | | 结算失败率 | < 0.1% | AlertManager | --- ## 11. 已确认决策记录 | # | 决策 | 来源 | |---|------|------| | ✅O01 | 离线结算采用"延迟结算"模式:离线期间只记录事件队列,上线时批量结算 | 本文确认 | | ✅O02 | 最大离线结算时长按境界递增:炼气72h→筑基96h→金丹120h→元婴+无上限;默认上限120h,通过 Nacos 可配 | 本文确认 | | ✅O03 | 离线战斗使用与在线完全相同的 ATB 引擎,服务端权威计算 | GDD-03 ✅1 | | ✅O04 | 离线游历事件自动选择分支,默认"安全优先"策略,玩家可设置偏好 | GDD-22 三 | | ✅O05 | 弟子离线死亡判定每游戏小时执行一次,死亡率与难度/品质/保险相关 | GDD-07 ✅T4 | | ✅O06 | 结算结果压缩存储到 Valkey,TTL 7 天,支持幂等重复拉取 | 本文确认 | | ✅O07 | 背包满时溢出物品转入邮件系统,货币溢出按比例折算低档货币 | TDD-04 | | ✅O08 | 结算面板展示战报摘要(不含完整行动序列),完整战报按需加载 | TDD-05 | | ✅O09 | 离线产出遵循 GDD-06 经济参数,受疲劳衰减/区域警觉值约束 | GDD-06 §5.3 | | ✅O10 | 弟子代挂产出按品质/种族/技能/设施四维系数计算 | GDD-07 §2.3.2 | | ✅O11 | 离线 PVP 战书默认接受(GDD-03 ✅14),被挑战方上线后查看战报 | GDD-03 §8.2 | | ✅O12 | 结算版本号机制保证幂等性,重复登录不会重复结算 | 本文确认 | --- ## 12. 验收标准 | # | 验收条目 | 测试方法 | |---|----------|----------| | 1 | 玩家离线 24 现实小时后上线,结算面板正确显示挂机资源/弟子产出/事件列表 | 离线 24h mock + 上线验证 | | 2 | 离线战斗结果与在线相同条件下手动触发的战斗结果一致(相同随机种子) | 相同参数对比测试 | | 3 | 背包满时溢出物品正确转入邮件,货币溢出正确折算 | 背包填满后离线验证 | | 4 | 弟子离线死亡概率符合 GDD-07 设定(品质/难度/保险修正) | 10000 次蒙特卡洛模拟 | | 5 | 离线游历事件按"安全优先"策略自动选择,不选择高风险选项 | 事件池 mock 验证 | | 6 | 结算结果幂等:同一结算版本重复调用不产生额外收益 | 重复调用验证 | | 7 | 72h 离线结算耗时 < 2s(单角色) | 性能基准测试 | | 8 | 100 并发结算请求下系统稳定,无 DB 死锁 | 压力测试 | | 9 | 结算报告 JSON Schema 验证通过 | JSON Schema 校验工具 | | 10 | 离线期间的经济审计日志完整记录(每笔产出/消耗) | 查询 economy_audit_logs | | 11 | 弟子品质/数量上限符合 GDD-07 ✅T4(炼气2→合体10,弟子居+2) | 边界值测试 | | 12 | 离线 PVP 被挑战默认接受,上线后战报正确展示 | PVP mock 测试 | | 13 | Valkey 缓存 TTL 过期后结算报告正确降级到 DB 查询 | TTL 过期测试 | | 14 | 结算版本号递增正确,旧版本报告不再重复应用 | 版本号递增测试 | --- ## 13. 数据库新增/变更(补充 TDD-04) ### 13.1 离线结算相关新增字段 #### characters 表新增字段 | 字段 | 类型 | 说明 | |------|------|------| | offline_settle_version | bigint | 离线结算版本号,每次结算 +1 | | offline_strategy | varchar(16) | 离线事件选择策略:safe / greedy / balanced,默认 safe | | last_settle_at | timestamptz | 上次结算时间 | #### 新增表:offline_event_queue | 字段 | 类型 | 约束 | 说明 | |------|------|------|------| | id | uuid | PK | | | character_id | uuid | FK, IX | | | event_type | varchar(32) | IX | combat / gather / disciple / encounter | | trigger_time_game | bigint | IX | 触发时间(游戏时间戳) | | payload | jsonb | | 事件数据 | | resolved | boolean | IX | 是否已结算 | | result | jsonb | | 结算结果 | | settle_version | bigint | | 关联的结算版本 | | created_at | timestamptz | IX | | **分区**:按 `created_at` 日分区,保留 7 天。 #### 新增表:offline_settlement_reports | 字段 | 类型 | 约束 | 说明 | |------|------|------|------| | id | uuid | PK | | | character_id | uuid | FK, IX | | | settle_version | bigint | UQ(character_id, settle_version) | 结算版本 | | offline_duration_real_sec | int | | 离线现实秒数 | | offline_duration_game_hours | numeric(10,2) | | 离线游戏时长 | | summary | jsonb | | 收益汇总(压缩) | | compressed_report | bytea | | gzip 压缩的完整报告 | | created_at | timestamptz | IX | | --- ## 14. Nacos 动态配置项 | 配置键 | 类型 | 默认值 | 说明 | |--------|------|--------|------| | `offline.max_settle_hours.default` | int | 120 | 默认最大离线结算时长(现实小时,金丹期) | | `offline.max_settle_hours.qi_refining` | int | 72 | 炼气期离线上限(现实小时) | | `offline.max_settle_hours.foundation` | int | 96 | 筑基期离线上限(现实小时) | | `offline.max_settle_hours.golden_core` | int | 120 | 金丹期离线上限(现实小时) | | `offline.max_settle_hours.nascent_soul` | int | 0 | 元婴期+离线上限(0=无上限) | | `offline.tick_interval_game_hours` | float | 1.0 | 时间快进步长(游戏小时) | | `offline.event_check_interval` | int | 300 | 定时扫描离线玩家间隔(秒) | | `offline.gather.fatigue_decay_per_hour` | float | 0.05 | 挂机疲劳衰减率(每游戏小时) | | `offline.gather.fatigue_min` | float | 0.3 | 疲劳衰减最低系数 | | `offline.disciple.death_rate_base.gathering` | float | 0.001 | 弟子采集基础死亡率 | | `offline.disciple.death_rate_base.mercenary` | float | 0.02 | 弟子佣兵基础死亡率 | | `offline.encounter.safe_strategy_weight` | float | 0.8 | 安全策略权重 | | `offline.encounter.greedy_strategy_weight` | float | 0.3 | 收益策略权重 | | `offline.battle.max_ticks` | int | 3000 | 战斗最大行动时间 | | `offline.battle.max_actions` | int | 50 | 单方最大行动次数 | | `offline.report.cache_ttl_hours` | int | 168 | 结算报告缓存 TTL(小时) | --- ## 15. 版本记录 | 版本 | 日期 | 修订内容 | 作者 | |------|------|----------|------| | 1.1 | 2026-07-02 | 离线结算上限按境界递增:炼气72h→筑基96h→金丹120h→元婴+无上限;默认上限从72h调整为120h;Nacos配置拆分为按境界独立配置 | Claude Code | | 1.0 | 2026-07-02 | 初始版本:时间快进算法、离线产出公式、ATB战斗结算、弟子代挂、游历事件、上线结算面板、性能优化、数据库变更、Nacos配置 | Claude Code | --- *TDD-06 v1.1 | 2026-07-02 | 离线结算上限按境界递增调整:炼气72h→筑基96h→金丹120h→元婴+无上限 | 前序:v1.0*