外观
真伤塔 / 固伤塔 · 技术实现
「玩家设定 →」
1. 代码位置
| 层 | 类 | 职责 |
|---|---|---|
| Controller | controller/DamageTowerController(/api/damage-tower/*) | overview / session / fight / floor-theater 端点(abandon 已删除,docs/48 §4) |
| Controller | controller/DamageTowerSessionController(/api/damage-tower/session/*) | docs/38 旧挂机会话管线遗留(enter / current / settle / summary;前端已不再调用) |
| Controller | controller/PillController(/api/pill-bag/*) | 丹药背包 list / use / use-all / synthesize / tower-settle(补偿路径)/ collection |
| Service | service/DamageTowerService | 单层 fight 与判定+发奖内核 settleFloor(docs/48 §3.3:手动与 idle 共用)、纯判定 judgeFloor(快照墙检测) |
| Service | service/DamageTowerIdleService | 后台挂机会话(docs/48 §3 / §11 v3.1):会话 CRUD、单窗口惰性结算(PUSH→FARM 精确刷满)、停止条件自动关闭(额度满 / 塔顶 / 跨天)、快照与 stopReason 组装、丹药按日结算编排 |
| Service | service/DamageTowerSessionService + service/DamageTowerTrialHandler | docs/38 旧会话管线遗留(复用试炼 v2 EmberTrialDamageService);其 onKill 已对齐 docs/48(每层每日 1 次废止) |
| Service | service/PillService | 丹药背包、服用(U 增量)、保值合成、列表 |
| Service | service/PillGrowthService | M/U 双账本与目标值差额入账 |
| Service | service/PillDailySettlementService | 每日统一结算:claim(PENDING 快照)+ fulfill(原子发奖 SETTLED) |
| domain | domain/DamageTowerRules | 塔规则纯函数:灵砂公式、首通奖励、基准阶/掉阶、通道怪生成、特权倍率、击杀耗时、会话节拍(FARM_ACTION_SECONDS / TOWER_TOP_FLOOR;MAX_CATCHUP_DAYS 已废止) |
| domain | domain/PillRules | 丹药数值:药效曲线 R(M)、目标值、保值合成、掉阶上限 |
| domain | domain/PillGrowth / domain/PillDailySettlement | V113 成长双账本行 / 每日结算快照行 |
| domain | domain/DamageTowerSession | V143 挂机会话行(每玩家每塔:enabled + last_settle_at) |
| repository | repository/DamageTowerRepository、repository/PillRepository、repository/EmberTrialRunRepository | 塔进度/每日记录/会话(getSession/upsertSession)、丹药双账本/结算快照、旧会话 |
| 战斗 | service/CombatService#buildDpsRaceTheater | 纯 DPS 竞速战报(怪物不攻击;胜利逐秒、超时 10 秒采样) |
2. 数据表
| 表 | 关键列 | 说明 |
|---|---|---|
player_damage_tower_progress | player_id、tower_type(FIXED/TRUE)、max_cleared_floor | 双塔首通进度(每塔一行) |
player_damage_tower_daily | player_id、tower_type、floor、battle_date、won | 每日逐层记录(主键含日期,跨天自然重置;won=1 为丹药基准阶与统计数据源,docs/48 起不再做发奖去重)。丹药取 MAX(floor) WHERE won=1(v3.1 修复:原 floor ≤ 200 过滤会让 >200 层玩家拿不到丹药),再按 1000 层夹取 |
player_damage_tower_session | player_id、tower_type、enabled、last_settle_at | V143 后台挂机会话(docs/48 §3.1 / §11 v3.1:每玩家每塔一行,一次当日挂机任务开关 + 惰性结算起点;开启时刻重置起点,禁用期间不推进;命中停止条件由服务端写回 enabled=0) |
player_pill_collection | player_id、pill_item_id、collected | 丹药收集计数(拾取累加;v2.2 无收集里程碑,仅收藏展示;V108 存量) |
player_pill_bonus | player_id、bonus_kind、rate_ppm | 属性丹累计面板加成(V108 存量;v2.2 起由成长账本差额续写) |
player_pill_growth | player_id、pill_kind、mastery_points、consumed_potency、applied_value | V113 丹药无限成长双账本(FOUR_DIM / ATTRIBUTE 各一行,M/U 只增不减) |
player_pill_daily_settlement | player_id、tower_type、reward_date、status、drop_json、result_json | V113 每塔每日一次丹药结算快照(PENDING → SETTLED,幂等 + 审计) |
player_ember_trial_run | 会话管线 | docs/38 旧挂机会话复用试炼 v2 会话(mode = DAMAGE_TOWER_FIXED / DAMAGE_TOWER_TRUE;前端已不再调用) |
3. 配置与常量
- 塔规则:
名称 数值 说明 baseRewardMultiplierBASE_REWARD_MULTIPLIER 1.0D — monthlyCardRewardBonusMONTHLY_CARD_REWARD_BONUS 1.0D — lifetimeCardRewardBonusLIFETIME_CARD_REWARD_BONUS 1.0D — vipRewardBonusPerLevelVIP_REWARD_BONUS_PER_LEVEL 0.1D — namingRewardBonusNAMING_REWARD_BONUS 2.0D — maxRewardMultiplierMAX_REWARD_MULTIPLIER 6.0D — tierBonusFloorsPerStepTIER_BONUS_FLOORS_PER_STEP 7 档位额外灵砂台阶宽度(层/档,v2.8):<b>每 7 层一档</b>—— 旧制为「每 10 层一档 = 30 层境的 ⅓ 境」,新制按同一「⅓ 境」节奏取 {@code ceil(20 / 3) = 7} 层/档(20 层境)。 tierBonusPerStepTIER_BONUS_PER_STEP 3 — tierBonusMaxTIER_BONUS_MAX 30 — firstClearEarlyFloorFIRST_CLEAR_EARLY_FLOOR 10 — firstClearEarlyRewardFIRST_CLEAR_EARLY_REWARD 300L — firstClearMidFloorFIRST_CLEAR_MID_FLOOR 40 — firstClearMidRewardFIRST_CLEAR_MID_REWARD 1_800L — firstClearLadderStartFIRST_CLEAR_LADDER_START 80 — firstClearLadderStepFIRST_CLEAR_LADDER_STEP 40 — firstClearLadderBaseFIRST_CLEAR_LADDER_BASE 6_000L — firstClearLadderIncrementFIRST_CLEAR_LADDER_INCREMENT 1_200L — baseTierFloorsPerStepBASE_TIER_FLOORS_PER_STEP DamageTowerRules.FLOORS_PER_REALM 日结算基准阶台阶(层/阶,v2.8):每 1 境(20 层)+1 阶,与旧制「每 30 层 = 1 境 +1 阶」同构。 (以类名限定引用,避免同类内静态字段前向引用。) pillTierCycleFloorsPILL_TIER_CYCLE_FLOORS DamageTowerRules.MAX_REALM_FLOOR 日结算基准阶的循环层数(docs/36 §1.2 v4.1):每 <b>200</b> 层(= 10 境 × 20 层 = {@link #MAX_REALM_FLOOR})为一个完整的 1~8 阶循环,第 201 层重新从 1 阶起, 不再固定 8 阶。(以类名限定引用,避免同类内静态字段前向引用。) pillBandFloorsPILL_BAND_FLOORS DamageTowerRules.BASE_TIER_FLOORS_PER_STEP 逐档结算的档宽(层/档,docs/36 §1.2 v4.2):与基准阶台阶同宽 ({@link #BASE_TIER_FLOORS_PER_STEP} = 20 层 = 1 境),故<b>每档的基准阶在档内恒定</b>; 塔顶 1000 层 = 50 档。逐档结算即按档为单位各抽一次阶。 dropWeightLowerDROP_WEIGHT_LOWER 30 掉落权重:基准阶 -1(30%)/ 基准阶(50%)/ 基准阶 +1(20%),百分制。 dropWeightBaseDROP_WEIGHT_BASE 50 — dropWeightHigherDROP_WEIGHT_HIGHER 20 — fixedDailyCapPerFloorFIXED_DAILY_CAP_PER_FLOOR 9.4D 灵砂日产曲线斜率(v2.9,2026-09-15 按 200 层模型重跑,docs/36 §6.3-10 毕业天数拉近): 单位「灵砂/层/日」,{@code 日额度 = max(100, round(斜率 × maxClearedFloor))}, 两塔各自独立、互不引用。 <p>斜率由<b>月卡标准轨迹毕业天数</b>反解(docs/37 §3.4 周目标轨迹逐日积分: 日额度收入 + 首通奖励 + 月卡特权 ×8/7,累计达到通道总需求的日子即毕业日, 反解脚本 {@code tools/sand_v29_calc.py}):固伤通道总需求 89,100(词条突破), 目标 ≈90 天毕业(大乘期收口)→ 9.4/层;真伤通道总需求 224,100 (词条突破 89,100 + 称谓石债 135,000),目标 ≈150 天毕业(渡劫三层收口)→ 11.2/层。</p> <p><b>200 层重跑(2026-09-15,与 v2.8 层数重划配套)</b>:v2.9 原斜率 6.3/7.1 按旧 300 层 模型反解(首通总投放 44,100、轨迹爬至 300 层);层数重划后轨迹层号按境界等价压缩 ⅔ (同天数到达层数少 ⅓ → 日额度同步下调),叠加首通总投放降至 33,300,实测毕业日变为 固伤 115 天 / 真伤 199 天,偏离设计目标。按 200 层口径重跑反解后取近值 <b>9.4 / 11.2</b>(精确解 9.3565 / 11.1562),实测毕业第 90 / 150 天、毕业比 1.67 倍 恢复设计目标;补贴段边界随之收窄为固伤 ≤10 层、真伤 ≤8 层。</p> <p>两斜率刻意取近值(真伤高 19%):恢复「两塔同式」的对称产出观感,真伤略高是 对其 2.515 倍需求的部分补偿,剩余差距由更长毕业期吸收——毕业比由 v2.8 的 3.33 倍(60/200 天)压缩至 1.67 倍(90/150 天),是设计而非 bug。 调参只动这两个常量(换目标天数即换斜率,重跑反解脚本)。</p> trueDailyCapPerFloorTRUE_DAILY_CAP_PER_FLOOR 11.2D — sanLimitFixedSAN_LIMIT_FIXED 9305L — sanLimitTrueSAN_LIMIT_TRUE 9306L — minDailySandCapMIN_DAILY_SAND_CAP 100L 灵砂日产上限下限基准(docs/49 §2.5 起步补贴,E2 修复): 100 精确压在首阶词条突破价上,新玩家开服数分钟即可完成首次突破,两塔同值。 补贴段边界:固伤塔 ≤ 10 层、真伤塔 ≤ 8 层(斜率 × 层数达到 100 为止)。 floorsPerRealmFLOORS_PER_REALM 20 — maxRealmFloorMAX_REALM_FLOOR 200 — endlessContentMaxFloorENDLESS_CONTENT_MAX_FLOOR 1000 预留塔顶(docs/54 §4.6,v3.0):<b>1000 层</b>(2026-09-20 随 {@link #DESIGN_MAX_FLOOR} 由 500 提到 1000)。现行无尽层<b>无上界</b>(运行时无夹取), 本常量等于<b>首次给塔加顶</b>;前期不生效,仅供后期接线引用。 endlessContentOpenFloorENDLESS_CONTENT_OPEN_FLOOR 200 正式内容开放层数(docs/54 §4.6,v3.0):前期 = <b>200</b>,即 201+ 仍走现行 无尽层口径(线性额度 + growth 怪 + 递推首通),<b>不是"塔顶"</b>; 后期开放 201–1000 时改为 {@link #ENDLESS_CONTENT_MAX_FLOOR}。 endlessCapGrowthPer10FloorsENDLESS_CAP_GROWTH_PER_10_FLOORS 1.01D 200 层以上日额度的锚点式增速(docs/54 §4.1 曲线 D):每 <b>10 层</b> ×1.01 ({@code cap(L) = cap(200) × 1.01^((L−200)/10)});怪物同期 ×6.88、额度全程仅 ×1.35。 tutorialFloorsPerRealmTUTORIAL_FLOORS_PER_REALM 3 — normalLastFloorInRealmNORMAL_LAST_FLOOR_IN_REALM 13 境内普通层(杂兵 III / HARD)段末层:境内层 <b>4~13</b>。 v2.8 档位带重划(30 层境 → 20 层境)后的显式分界,见 {@link #tierForFloor(int)}。 eliteLastFloorInRealmELITE_LAST_FLOOR_IN_REALM 19 境内精英(精英 IV / ELITE)段末层:境内层 <b>14~19</b>; 境内层 {@link #FLOORS_PER_REALM}(每境末层)为首领 V(BOSS)。 sessionHardSecondsSESSION_HARD_SECONDS 30 * 60 挂机会话硬时长(秒,v1.3):30 分钟兜底,防止无尽层无墙可撞时无限挂机。 会话从第 1 层自动爬起,止步于属性不够的层,或达到本硬时长(docs/38 §1)。 <p>docs/48 §3.1 起新会话按日额度约束、不按时长约束(本常量仅供 docs/38 旧会话路径使用)。</p> farmActionSecondsFARM_ACTION_SECONDS 3 后台挂机会话推进节拍(docs/48 §3.1):3 秒/动作,一个动作 = 推一层 或 刷一层。 maxCatchupDaysMAX_CATCHUP_DAYS 7 — tempAttrHardCapRatioTEMP_ATTR_HARD_CAP_RATIO 0.2D 临时属性越墙上限系数 r(docs/54 §4.7 兼容性清单 #7):硬顶 = 该层怪物所需值 ×(1+r)。 与 {@link #TEMP_ATTR_MULTIPLIER} 同属<b>独立常量域</b>,不复用产出倍率 {@link #privilegeMultiplier(PlayerPrivilege)}(后者 ∈[1,6],语义不同)。 tempAttrMultiplierTEMP_ATTR_MULTIPLIER 1.0D 临时属性权益乘区基准(docs/54 §4.7,兼容性清单 #3/#1):本期只做兼容性预留、 <b>不实现付费</b>,故恒为 1.0——免付费时 {@link TowerTempAttribute#effective()} 逐点等于免费档("m=1 时间守恒"不变量)。后期按形态 A(月卡/VIP → 1.0/1.5/2.0) 或形态 B({@code customOverride} 直填)在此域扩展,<b>不改产出/掉落/面板</b>。 designMonsterBaseDamageDESIGN_MONSTER_BASE_DAMAGE 10_000L — designSegment1GrowthDESIGN_SEGMENT_1_GROWTH 1.38D — designSegment2GrowthDESIGN_SEGMENT_2_GROWTH 1.10D — designSegment3GrowthDESIGN_SEGMENT_3_GROWTH 1.05D — designSegment2AnchorDESIGN_SEGMENT_2_ANCHOR 6_075_665L — designSegment3AnchorDESIGN_SEGMENT_3_ANCHOR 15_758_710L — designMonsterHpPerDamageDESIGN_MONSTER_HP_PER_DAMAGE 2 — designPlayerHpPerDamageDESIGN_PLAYER_HP_PER_DAMAGE DESIGN_MONSTER_HP_PER_DAMAGE — designMaxFloorDESIGN_MAX_FLOOR 1000 — towerTopFloorTOWER_TOP_FLOOR DESIGN_MAX_FLOOR 挂机会话的塔顶(= {@link #DESIGN_MAX_FLOOR} = 1000 层,2026-09-24 新增): 推到该层后<b>自动关闭挂机开关</b>(docs/48 §11 v3.1「挂到最高层自动停止」)。 roundSecondsROUND_SECONDS 1 塔内每回合时长(秒)——<b>每秒一回合</b>(2026-09-20 定案)。 战斗没有时长上限:谁先被打死(血量归零)谁就结束。 monsterActionDelayMillisMONSTER_ACTION_DELAY_MILLIS 100 — minAttackSpeedBonusMIN_ATTACK_SPEED_BONUS 1.0D 攻速加成下限(<b>面板攻速/特权加成</b>:免费 ×1.0 / 月卡 ×3.0 / 满配 ×10.0)。 攻速<b>不改变出手次数</b>(回合固定每秒一回合),而是直接加成固伤/真伤。 towerSessionFloorTickSecondsTOWER_SESSION_FLOOR_TICK_SECONDS 3 扫塔会话的每层推进节拍(秒)。<b>不是战斗时长</b>——塔内胜负由回合赛决定 (每秒一回合、玩家先手、谁先归零谁结束),该值只用于会话推进/止步检测 (扫荡按手动同一难度)。 来源:emberfall-server/src/main/java/com/emberfall/domain/DamageTowerRules.java· commit22d062aff99fabdc373f3b1c82b2a6eff4a034d3 - 丹药可调项集中在
PillRules/DamageTowerRules:MASTERY_LOG_COEFFICIENT、MASTERY_SCALE、BASE_FOUR_DIM_PER_POTENCY、ATTRIBUTE_BASE_DECI_PPM、DAILY_DROP_COUNT、DROP_WEIGHT_*、DAILY_SAND_CAP_MULTIPLIER。 - 会话节拍(docs/48 §3.1/§3.2):
DamageTowerRules.FARM_ACTION_SECONDS = 3(3 秒/动作)、TOWER_TOP_FLOOR = DESIGN_MAX_FLOOR = 1000(推到塔顶即自动关闭挂机);结算锁 TTL 30 秒(DamageTowerIdleService.LOCK_TTL)。MAX_CATCHUP_DAYS(离线回补 7 天)已随 v3.1 废止(标注@Deprecated,仅存规则快照)。 - 错误码:7601 非法楼层(保留);7602 当日已通关 / 7603 放弃冷却(docs/48 退役);7604 会话参数非法(预留)。
- 丹药抽阶 HMAC 密钥:
pill.hmac-secret(未配置时启动 WARN 并使用内置默认值,生产必须显式配置,否则抽阶可被离线推演)。
4. 战斗模型与数值定标(v1.2,对齐余烬试炼场无尽模式)
纯 DPS 竞速:塔怪不攻击,玩家无生命/防御/死亡/治疗参与。
击杀耗时 = ⌈怪物生命 ÷ 玩家对应通道 DPS⌉ (DamageTowerRules.killSeconds)
胜利 = 击杀耗时 ≤ 180 秒(FIGHT_WINDOW_SECONDS)
超时失败 = 写当日 won=0 记录,无掉落,可重试1
2
3
2
3
- 通道隔离:真伤塔
EncounterType.REALM_ABYSS(仅结算真伤)、固伤塔EncounterType.BODY_TEMPERING(仅结算固伤),普攻压 1;玩家通道 DPS 分别取CombatService.expectedTrueDpsOnly/expectedFixedDpsOnly。 - 判定/演出用挑战攻速压缩(
DamageTowerService.challengeStats,2026-09-19 补接 / docs/37 §4.3):双塔属竞速口径,判定与演出统一用withChallengeAttackSpeed()后的属性——1.0 + min(0.3, (面板攻速 − 1.0) × 0.1),即免费 1.0 / 月卡 3.0→1.2 / 满配 10.0→1.3。此前直接用面板攻速,同面板塔内通道 DPS =docs/56基准的 2.5 倍(月卡)/ 8.33 倍(满配冠名),塔顶卡点失效;挂机地图攻速与产出效率不变。overview/ 手动fight/ 挂机惰性结算(共用judge内核)与floor-theater、试炼 handlerflatDps全部同源。 - HP 定标(
DamageTowerRules.channelHp,2026-09-19 刻度重标 / docs/56 §2):round(需求通道DPS(L) × 定标秒数(L))——教学层 = 该境达标表 ×TUTORIAL_DPS_RATIO(0.2)、目标 60 秒;普通层/无尽层 180 秒(= 战斗窗口),于是「窗口内击杀 ⟺ 玩家通道 DPS ≥ 该层需求」。需求 =referenceChannelDps(塔) × 0.980517 × 1.38^((L−199)/10)(DamageTowerRules.designCurveDemandDps):固伤基准 4,766 万 / 真伤基准 1.53 亿(均由本仓封顶常量派生),L199 满配 ×0.981、L200 ×1.013;无尽层 = 需求(L200) ×endlessGrowth(L)。ATK/DEF 沿用挑战档静态表,仅作展示。 - 200 层模型(v2.8,2026-09 产品确认):
FLOORS_PER_REALM = 20、MAX_REALM_FLOOR = 200(旧制 30 / 300);总层数上限与无尽层门槛同步下调,旧进度按新上限钳制。- 层带重划(境内层):1~3 教学(EASY)、4~13 普通(HARD)、14~19 精英(ELITE)、20 首领(BOSS)——旧制为 1~3 / 4~20 / 21~29 / 30。
- 档位台阶
tierBonus:由「每 10 层一档」改为TIER_BONUS_FLOORS_PER_STEP = 7(20 层境的 ⅓ 境),每档 +3、封顶 30;在每境圆满层与旧制逐点相等(新 L20/40/60/80 = 6/15/24/30,旧 L30/60/90/120 同值)。 - 首通锚点重排:10 → 300(第 1 境半程)、40 → 1,800(第 2 境圆满)、递推段
L ≥ 80 且 (L−80) % 40 == 0→6,000 + 1,200×k(80/120/160/200)。数值阶梯不变,落点全部对齐境圆满层;首通总投放 44,100 → 33,300(原 250/300 两档进入无尽层,无尽层不设首通奖励)。 - 丹药基准阶台阶
BASE_TIER_FLOORS_PER_STEP = FLOORS_PER_REALM(每境 +1 阶、8 阶封顶)——若沿用旧 30 层台阶,8 阶将落在 L=211 > 新塔顶 200,永久丹药永远吃不到 8 阶,故必须随层数重划同步重推导。 - 丹药基准阶 200 层循环(v4.1,2026-09-24):
baseTierFor(f)先把层号折到循环内层c = (min(f, MAX_PERMANENT_REWARD_FLOOR) − 1) % PILL_TIER_CYCLE_FLOORS(200) + 1再取台阶——201 层重新从 1 阶起(201~220 = 1 阶、…、341~400 = 8 阶、401 回 1 阶;塔顶 1000 为循环整数倍),1~200 层落点逐点不变;颗数曲线(baseDropCountForFloor的endlessGrowth)不参与循环。抽阶ruleVersion由v2.2升为v4.1(参与 HMAC 载荷),已固化的 PENDING 快照仍按原样发奖。 - 丹药逐档结算(v4.2,2026-09-24):
claim不再只抽「当日最高层那一档」,改为按 f 覆盖的n = DamageTowerRules.pillBandCount(f) = ⌈min(f,1000)/20⌉个档(档宽PILL_BAND_FLOORS = 20= 基准阶台阶,塔顶 50 档)逐档结算:PillRules.allocateAcrossBands(total, n)把 v2.6 的总颗数摊派到各档(余数 Bresenham 均匀铺开,避免偏向高低档),每档按pillBandBaseTier(k) = baseTierFor(k×20)做 30/50/20 抽阶,HMAC 载荷用档末层 + 档内序号。ruleVersion升为v4.2,drop_json增"bands"字段(须排在tiers之前,快照解析取lastIndexOf('"'))。总量守恒(塔顶仍 180 颗),但平均阶数下降 → ΔM 约 −33%(塔顶 1,386 → 926);档位集合取「f 覆盖的档」而非「当日实际 won=1 的层」,避免「每天手动刷 1000 场」成为最优解。
- 战报:
CombatService.buildDpsRaceTheater——胜利逐秒记录、超时按 10 秒采样收尾,怪物全程不反击、玩家满血。
5. 数据流简述
5.1 判定+发奖内核 settleFloor(docs/48 §3.3)
DamageTowerService.settleFloor(playerId, towerType, floor, player, stats, multiplier, battleDate, writeLog):纯 DPS 竞速判定(killSeconds vs 180 秒)→ 灵砂(daily_limit_pool 9305/9306 原子裁剪,整层裁剪)→ 首通(一次性奖励 + 推进最高层)→ won=1 记录 →(可选)逐层战斗日志。手动 fight 与 idle 惰性结算共用,口径永不漂移;失败路径不写库(手动 fight 的败局记录由调用方补写)。
5.2 逐层 fight(兼容/手动路径)
POST /api/damage-tower/fight {towerType, floor}:先触发 idle 惰性结算 → 玩家级分布式锁 + 事务内 → 校验楼层([1, maxCleared+1],7601)→ settleFloor → 战报构建。「每层每日 1 次奖励」规则废止(7602 退役):任意 floor ≤ maxClearedFloor 的层可重复挑战,灵砂仅受当日额度约束。丹药不逐层发放(由 idle 结算按日承接,手动端点 tower-settle 保留为补偿路径)。
手动挑战(2026-09-19 接线):前端首次接上 fight —— 塔页「当层怪物」卡提供「手动挑战这一层」按钮(overview.nextMonster.floor,即 maxCleared+1),ChallengeView 的塔卡片也显示当层属性并可跳转定位(/damage-tower/{type}?floor=N,塔页据此滚动并高亮怪物卡)。挑战范围维持 floor ≤ maxCleared+1(用户确认不放开前探层),因此不做「试深浅」分支:撞墙时当层正是那面打不动的墙,反复挑战即为试深浅,失败不写库、不扣资源。fight 返回的 combat.timeline 与 floor-theater 同源,前端直接喂给塔页同一套播放器重演(manualTheater 标志位复用演出状态机;挂机推进中该按钮置灰,避免楼层漂移触发 7601)。
双塔平衡(v2.4):单层灵砂档位与日产上限按两塔 max 层较大值(已于 2026-09-11 撤销(docs/36 §6.3-8 两塔解耦):DamageTowerRules.rewardFloor/DamageTowerService.symmetricMaxCleared),落后塔低层扫荡拿领先塔档位;首通奖励仍按真实通关层。rewardFloor/symmetricMaxCleared已删除,单层档位 =baseSand(本塔真实层),快照 DTO 的mirrorFloor字段同步移除。
通道消耗权重(docs/49 §2.3)与 v2.7 的:Σ×2曲线权重已于 2026-09-11 撤销(。现行口径(v2.9,2026-09-12,docs/36 §6.3-10 毕业天数拉近):SAND_DEMAND_*/SAND_WEIGHT_*/sandWeight()/minDailySandCap(type)删除);v2.7 现行口径dailySandCap(maxClearedFloor) = max(100, Σ baseSand(1..F) × 2)dailySandCap(maxClearedFloor, towerType) = max(100, round(斜率 × maxClearedFloor)),斜率按塔独立——固伤FIXED_DAILY_CAP_PER_FLOOR = 9.4/层、真伤TRUE_DAILY_CAP_PER_FLOOR = 11.2/层(2026-09-15 按 200 层模型重跑;原 6.3/7.1 系旧 300 层反解值,重划后实测退化至 115/199 天),由各自目标毕业天数反解(月卡标准轨迹 docs/37 §3.4 逐日积分:固伤 ≈90 天 / 真伤 ≈150 天攒齐 89,100 / 224,100,含首通 33,300 与月卡 ×8/7;反解脚本tools/sand_v29_calc.py实测毕业 90/150 天整——v2.8 的 60/200 天目标因 3.33 倍毕业差失衡被同日取代)。起步下限 100 两塔同值(v2.9 补贴段边界固伤 ≤10 层、真伤 ≤8 层);特权privilegedDailySandCap(maxClearedFloor, multiplier, towerType)通道参数与 scale 公式不变(月卡 ×8/7)。两斜率近值——同层两塔日额度接近(真伤高 19% 补偿其 2.515 倍需求),毕业比 1.67 倍由需求侧解释、属设计而非 bug。调参只动两斜率常量(换目标天数即重跑反解脚本,目标比建议保持 1.6~1.8 以维持近值对称)。Redis 故障降级(v2.5):
DistributedLockService.tryLockWithResult返回LockResult(ACQUIRED / BUSY / UNAVAILABLE),idle 结算据此区分「并发占用」与「Redis 故障」——故障时跳过本窗口结算、checkpoint 不推进(恢复后从原起点自动补算,7 天窗口内不丢收益),并在快照IdleSessionSnapshot.settleDegraded/DamageTowerIdleSnapshotDto.settleDegraded置位,前端挂机卡片显示「收益稍后自动补发」提示条。锁服务对 Redis 异常 fail-closed 并保留 WARN 日志。
5.3 后台挂机会话 + 惰性结算(主路径,docs/48 §3)
- 会话模型(v3.1:一次当日挂机任务):
player_damage_tower_session(V143)每玩家每塔一行——enabled挂机开关 +last_settle_at惰性结算起点(开启时刻重置为当前时刻;禁用期间不 accrue)。POST /api/damage-tower/session {towerType, enabled}落开关后立即结算一次并返回快照。命中停止条件时服务端在同一事务内写回enabled=0:①当日灵砂额度刷满 ②推到塔顶TOWER_TOP_FLOOR(1000) ③跨游戏日;次日必须手动重新开启(快照stopReason=TOP_FLOOR/CAPPED/DAY_ROLLOVER/MANUAL)。 - 触发点(任一命中即先结算后返回):
GET /overview、POST /session、POST /fight、POST /floor-theater、GET /player/info(DamageTowerIdleService.settleBothTowers,轻量:仅当 enabled;失败仅告警不阻断)。 - 结算算法(
DamageTowerIdleService.settle):pill-bag:{playerId}分布式锁(TTL 30s,锁被占用时跳过本次——轮询场景防「操作繁忙」)内,只结算一个窗口:起点last_settle_at→ 终点min(now, 起点所在日 24:00)(跨天时stopAfterWindow=true);窗口独立事务(行锁SELECT ... FOR UPDATE):- 阶段一 PUSH:从
maxCleared+1起逐层settleFloor(不问额度——首通进度优先,灵砂按池整层裁剪),首败即墙; - 阶段二 FARM:停在
maxCleared反复刷——grant = min(perAction × actions, remaining)批量精确刷满(部分授发,一次consumeDailyLimit),刷层击杀补 won=1 记录(丹药基准阶数据源);remaining == 0即 CAPPED(D4 当日停,剩余动作作废); - checkpoint:
last_settle_at前移到窗口末尾(窗口 < 3 秒不动,时间碎片下次累计),并按停止条件写回enabled; - 丹药:当日有 won 记录 → 事务内
claim(主键+日期幂等,权益倍率取结算时刻)→ 锁段外fulfill(自行取同一把锁,避免同键重入死锁;PENDING 保留可补偿重试); - 聚合战斗日志:仅在挂机收口(命中停止条件的那一窗)写一条摘要(含最高胜利层 / 推进层数 / 灵砂 / 丹药颗数 / 关闭原因),不逐动作刷屏。
- 阶段一 PUSH:从
- 快照(
IdleSessionSnapshot,§3.5):状态由当前数据推导(无存储态)——CAPPED(池满)> BLOCKED(第 1 层打不过)> WALL_FARMING(前沿不可杀)> PUSHING;wallNeedDps = 该层怪物固伤/真伤;OFF 态附stopReason(关闭原因)。 - 单次结算内玩家属性恒定(墙恒定);跨次结算(养成提升后)自然重新试墙。
5.4 前端(docs/48 §5:服务端是唯一权威)
stores/damageTowerRunStore整体删除——死锁(attachRun vs syncFromStore)与刷新丢状态两类 bug 随之根除;DamageTowerView五状态渲染(OFF / PUSHING / WALL_FARMING / CAPPED / BLOCKED)。- 页面在场且 enabled 时每 3 秒轮询 overview(每次轮询即驱动一次服务端结算);离场停止轮询,离线推进由惰性结算兜底。
- PUSHING:拉 floor-theater 重演最新前沿(加速 ×5 可切),轮询期间错过的层折叠为日志条;WALL_FARMING:层数跳动卡(本地 3 秒 tick,轮询校正)+ 当日额度进度条 + 灵砂飘字;「跳过本层」「放弃战斗」按钮删除。
- 当层怪物卡常显(2026-09-19):
overview.nextMonster由判定内核judge一并派生玩家侧结论(playerChannelDps/killSeconds/beatable/windowSeconds),塔页以独立区块常显(不再只在 OFF 态渲染),挂机任一状态都能看到撞墙层的完整属性与「能否通过」;ChallengeView复用同一 overview 渲染属性行。手动挑战的演出复用塔页播放器(displayingTheater = manualTheater || PUSHING,会话状态卡在演出期间让位),故DamageTowerView内theater/replayFloor/theaterPhase三件套同时服务挂机重演与手动挑战。 - 刷新恢复:会话在服务端,overview 直出当前状态与层数。
5.5 丹药服用与合成
POST /api/pill-bag/use/use-all:扣背包 → 汇总药力 U 增量 →PillGrowthService.applyDelta一次重算目标值并按差额入账(四维丹写基础四维四项、属性丹写 ATTACK/HP/DEFENSE 三行 ppm,不逐颗 round)。POST /api/pill-bag/synthesize {itemId, times}:(k+1)颗 k 阶 +5×k对应灵砂 →k颗 (k+1) 阶(k ≤ 7),容量/素材整体校验 + 事务回滚,不吞素材。POST /api/pill-bag/tower-settle {towerType, rewardDate?}:补偿路径(docs/48 §3.4:服务端已按日自动结算,前端不再调用)。- decompose / decompose-all 已下线(固定 7628 错误,不扣物品)。
6. 已知差异
- docs/38 旧会话管线(
DamageTowerSessionService/DamageTowerSessionController/player_ember_trial_run)保留为遗留路径,前端已不再调用;其SESSION_HARD_SECONDS(30 分钟硬上限)随新会话模型废止。 - 旧
DamageTrialView.vue(伤害试炼旧视图)残留未删,无功能影响。 - 手动 fight 按当日记录 won;idle 结算按窗口日记录;两者共用
daily_limit_pool(游戏时区当日)与丹药结算(主键+日期幂等),天然串行(同pill-bag锁)。 - 固伤塔烙印全伤口径(2026-09-22 已统一):
PlayerService.towerFixedDamage原先固伤塔分支直接返回calculateFinalFixedDamage(不吃烙印全伤),只有真伤塔分支叠abyssAllDamageBonusRate;现两塔统一该乘区(满配 +15%)。挂机会话入场快照该值,存量会话重开后才生效。
7. 相关代码
- 炼体秘境(
BodyTemperingRules:固伤通道与达标 DPS 表) - 破界深渊(
RealmAbyssRules:真伤通道与达标 DPS 表) - 月卡锚点(
ChallengeBalanceConfig.monthCardAnchor:双塔 HP 定标来源2026-09-19 起双塔 HP 改由DamageTowerRules.referenceChannelDps派生,本锚点仅供挑战档展示面板与 201+ 备用) - 试炼 v2 会话管线(
EmberTrialDamageService/TrialDpsSimulator:flatDps短路分支) - 装备淬炼(灵砂 9101/9102 消费端)