W4 批次:三项设计裁定落地(Boss 战 Plan B / 离线契约 v2 / 长明灯加法化)

Boss 战修复(Plan B,用户拍板):
- HP 公式 15xPxhall_factor -> HP = P x boss_army_seconds_n(S=23.0→28.0 递增,
  每场军列约 23-28 秒清空,战斗时长成为可直接书写的设计旋钮,结构性消除
  高殿 P 抵消导致不可胜的旧 bug);hall_factor 降级为纯叙事/视觉参数
- 新增 souls_par 列(0→90000):开场冲锋 intro_dmg = HP x 10% x clamp(souls/souls_par,0,1),
  上限 20%->10%;删 hp_factor/intro_dmg_souls_factor/intro_dmg_cap_ratio 三列
- 点击伤害 P x κ x combo_mult,κ = (0.010 + 0.052 x click_ratio) x (1 + 0.30 x m06_owned),
  click_ratio = 已购点击升级 / 12(κ 0.010→0.0806,满配点击占比约 20.5%±3pp)
- 防双重计入:L5-12 点击升级 +1% output/s 留全局区(抬 P 同时抬军列 DPS 与点击
  伤害,可接受),不得再计入 κ 分子(recompute 注释固化)

离线契约 v2(P-8,改契约不改表):
- OfflineModel.settle 第三参 offline_mult -> offline_add(加法);各档效率
  min(rate+add, 1.0) 硬顶 100%,离线永不超在线
- 长明灯 x1.15 乘性 -> +5%/级 加法(满级 +30%,各档封顶 100%)
- 0-2h 100% 保持;8h = 6.2 等效小时(77.5% 效率)
- Gate #9 拆 9a/9b/9c:9a FAIL->PASS(0-2h 全额 + 8h∈[5.5,6.5]),
  9b 长明灯满级 8h∈[7.5,8.0] 且 ≤100%,9c 任意配置效率 ≤100%

数据与工具:
- gen_balance_sheets.py 补登 W3 遗留 7 行 tune(防重跑静默丢失),self_check 更新
- boss_battles/upgrades schema 同步(offline_mult->offline_add enum,漏改会违反导入)

测试(runner 7->8 用例,Node 交叉验证 145 断言全过):
- 可胜性哨兵重写为「十殿全可胜」:每殿 S_n<30、军列+点击≥HP、余时∈[2,8]、
  满配余时差≥4、点击占比∈[15,25]、H10≈20.5%±3pp
- κ 四档断言(0.010/0.036/0.062/0.0806,分母 12)
- 新增 test_offline_contract.gd:契约 v2 数值面 + 9a/9b/9c 全 PASS + β 缓议
This commit is contained in:
程基岩(engineering-lead) committed 2026-10-20 17:01:25 +08:00
1 parent 4f650aeb39
commit 08dc5020c3
22 files changed
+738 -233

No files matched your search

+28
View File
@@ -267,6 +267,34 @@
"num": 3,
"text": "",
"desc_cn": "[B§7] 事件系统解锁后首个事件的延迟分钟数(给玩家缓冲,避免刚解锁就来)"
},
{
"id": "tune_boss_kappa_base",
"kind": "number",
"num": 0.01,
"text": "",
"desc_cn": "[systems-spec §1.2 B2] 殿王战点击伤害 κ 基数(零点击投资时 κ = 0.010;W4 方案 B)"
},
{
"id": "tune_boss_kappa_slope",
"kind": "number",
"num": 0.052,
"text": "",
"desc_cn": "[systems-spec §1.2 B2] κ 随法器线进度斜率:κ = (base + slope × 已购点击升级/12) × (1 + m06阶×0.30);满配 κ = 0.0806"
},
{
"id": "tune_boss_intro_cap",
"kind": "number",
"num": 0.1,
"text": "",
"desc_cn": "[systems-spec §1.2 B3] 开场冲锋伤害封顶 = HP × 本值(0.10;旧公式 20% 已废止)"
},
{
"id": "tune_boss_intro_enabled",
"kind": "number",
"num": 1,
"text": "",
"desc_cn": "[systems-spec §1.2 B3] 开场冲锋开关(1 = 启用;门 #11 首轮数据回来前默认 1)"
}
]
}
+22 -33
View File
@@ -5,11 +5,10 @@
"id",
"hall_id",
"hall_factor",
"hp_factor",
"boss_army_seconds",
"souls_par",
"army_dps_factor",
"intro_sec",
"intro_dmg_souls_factor",
"intro_dmg_cap_ratio",
"active_sec",
"combo_window_sec",
"combo_step",
@@ -22,11 +21,10 @@
"id": "boss_hall_01",
"hall_id": "hall_01_qinguang",
"hall_factor": 1,
"hp_factor": 15,
"boss_army_seconds": 23,
"souls_par": 0,
"army_dps_factor": 1,
"intro_sec": 3,
"intro_dmg_souls_factor": 0.5,
"intro_dmg_cap_ratio": 0.2,
"active_sec": 30,
"combo_window_sec": 0.6,
"combo_step": 0.02,
@@ -38,11 +36,10 @@
"id": "boss_hall_02",
"hall_id": "hall_02_chujiang",
"hall_factor": 1.1,
"hp_factor": 15,
"boss_army_seconds": 23.5,
"souls_par": 2,
"army_dps_factor": 1,
"intro_sec": 3,
"intro_dmg_souls_factor": 0.5,
"intro_dmg_cap_ratio": 0.2,
"active_sec": 30,
"combo_window_sec": 0.6,
"combo_step": 0.02,
@@ -54,11 +51,10 @@
"id": "boss_hall_03",
"hall_id": "hall_03_songdi",
"hall_factor": 1.2,
"hp_factor": 15,
"boss_army_seconds": 24,
"souls_par": 18,
"army_dps_factor": 1,
"intro_sec": 3,
"intro_dmg_souls_factor": 0.5,
"intro_dmg_cap_ratio": 0.2,
"active_sec": 30,
"combo_window_sec": 0.6,
"combo_step": 0.02,
@@ -70,11 +66,10 @@
"id": "boss_hall_04",
"hall_id": "hall_04_wuguan",
"hall_factor": 1.3,
"hp_factor": 15,
"boss_army_seconds": 24.5,
"souls_par": 120,
"army_dps_factor": 1,
"intro_sec": 3,
"intro_dmg_souls_factor": 0.5,
"intro_dmg_cap_ratio": 0.2,
"active_sec": 30,
"combo_window_sec": 0.6,
"combo_step": 0.02,
@@ -86,11 +81,10 @@
"id": "boss_hall_05",
"hall_id": "hall_05_yama",
"hall_factor": 1.4,
"hp_factor": 15,
"boss_army_seconds": 25,
"souls_par": 600,
"army_dps_factor": 1,
"intro_sec": 3,
"intro_dmg_souls_factor": 0.5,
"intro_dmg_cap_ratio": 0.2,
"active_sec": 30,
"combo_window_sec": 0.6,
"combo_step": 0.02,
@@ -102,11 +96,10 @@
"id": "boss_hall_06",
"hall_id": "hall_06_biancheng",
"hall_factor": 1.6,
"hp_factor": 15,
"boss_army_seconds": 25.5,
"souls_par": 2400,
"army_dps_factor": 1,
"intro_sec": 3,
"intro_dmg_souls_factor": 0.5,
"intro_dmg_cap_ratio": 0.2,
"active_sec": 30,
"combo_window_sec": 0.6,
"combo_step": 0.02,
@@ -118,11 +111,10 @@
"id": "boss_hall_07",
"hall_id": "hall_07_taishan",
"hall_factor": 1.8,
"hp_factor": 15,
"boss_army_seconds": 26,
"souls_par": 7200,
"army_dps_factor": 1,
"intro_sec": 3,
"intro_dmg_souls_factor": 0.5,
"intro_dmg_cap_ratio": 0.2,
"active_sec": 30,
"combo_window_sec": 0.6,
"combo_step": 0.02,
@@ -134,11 +126,10 @@
"id": "boss_hall_08",
"hall_id": "hall_08_dushi",
"hall_factor": 2,
"hp_factor": 15,
"boss_army_seconds": 26.5,
"souls_par": 21000,
"army_dps_factor": 1,
"intro_sec": 3,
"intro_dmg_souls_factor": 0.5,
"intro_dmg_cap_ratio": 0.2,
"active_sec": 30,
"combo_window_sec": 0.6,
"combo_step": 0.02,
@@ -150,11 +141,10 @@
"id": "boss_hall_09",
"hall_id": "hall_09_pingdeng",
"hall_factor": 2.1,
"hp_factor": 15,
"boss_army_seconds": 27,
"souls_par": 48000,
"army_dps_factor": 1,
"intro_sec": 3,
"intro_dmg_souls_factor": 0.5,
"intro_dmg_cap_ratio": 0.2,
"active_sec": 30,
"combo_window_sec": 0.6,
"combo_step": 0.02,
@@ -166,11 +156,10 @@
"id": "boss_hall_10",
"hall_id": "hall_10_zhuanlun",
"hall_factor": 2.2,
"hp_factor": 15,
"boss_army_seconds": 28,
"souls_par": 90000,
"army_dps_factor": 1,
"intro_sec": 3,
"intro_dmg_souls_factor": 0.5,
"intro_dmg_cap_ratio": 0.2,
"active_sec": 30,
"combo_window_sec": 0.6,
"combo_step": 0.02,
+18 -18
View File
@@ -1348,11 +1348,11 @@
"type": "offline",
"target_gen": "none",
"unlock_hall": "hall_04_wuguan",
"effect_type": "offline_mult",
"effect_param": 1.15,
"effect_type": "offline_add",
"effect_param": 0.05,
"requires": "none",
"cost": 2000,
"desc_cn": "离线效率 ×1.15(作用于衰减表全部时段;6 级满 ×2.31)"
"desc_cn": "离线效率 +5%(加法,直接加在衰减表各档速率上;6 级满 +30%,各档硬封顶 100%)"
},
{
"id": "up_offline_2",
@@ -1361,11 +1361,11 @@
"type": "offline",
"target_gen": "none",
"unlock_hall": "hall_04_wuguan",
"effect_type": "offline_mult",
"effect_param": 1.15,
"effect_type": "offline_add",
"effect_param": 0.05,
"requires": "none",
"cost": 20000,
"desc_cn": "离线效率 ×1.15(作用于衰减表全部时段;6 级满 ×2.31)"
"desc_cn": "离线效率 +5%(加法,直接加在衰减表各档速率上;6 级满 +30%,各档硬封顶 100%)"
},
{
"id": "up_offline_3",
@@ -1374,11 +1374,11 @@
"type": "offline",
"target_gen": "none",
"unlock_hall": "hall_04_wuguan",
"effect_type": "offline_mult",
"effect_param": 1.15,
"effect_type": "offline_add",
"effect_param": 0.05,
"requires": "none",
"cost": 200000,
"desc_cn": "离线效率 ×1.15(作用于衰减表全部时段;6 级满 ×2.31)"
"desc_cn": "离线效率 +5%(加法,直接加在衰减表各档速率上;6 级满 +30%,各档硬封顶 100%)"
},
{
"id": "up_offline_4",
@@ -1387,11 +1387,11 @@
"type": "offline",
"target_gen": "none",
"unlock_hall": "hall_04_wuguan",
"effect_type": "offline_mult",
"effect_param": 1.15,
"effect_type": "offline_add",
"effect_param": 0.05,
"requires": "none",
"cost": 2000000,
"desc_cn": "离线效率 ×1.15(作用于衰减表全部时段;6 级满 ×2.31)"
"desc_cn": "离线效率 +5%(加法,直接加在衰减表各档速率上;6 级满 +30%,各档硬封顶 100%)"
},
{
"id": "up_offline_5",
@@ -1400,11 +1400,11 @@
"type": "offline",
"target_gen": "none",
"unlock_hall": "hall_04_wuguan",
"effect_type": "offline_mult",
"effect_param": 1.15,
"effect_type": "offline_add",
"effect_param": 0.05,
"requires": "none",
"cost": 20000000,
"desc_cn": "离线效率 ×1.15(作用于衰减表全部时段;6 级满 ×2.31)"
"desc_cn": "离线效率 +5%(加法,直接加在衰减表各档速率上;6 级满 +30%,各档硬封顶 100%)"
},
{
"id": "up_offline_6",
@@ -1413,11 +1413,11 @@
"type": "offline",
"target_gen": "none",
"unlock_hall": "hall_04_wuguan",
"effect_type": "offline_mult",
"effect_param": 1.15,
"effect_type": "offline_add",
"effect_param": 0.05,
"requires": "none",
"cost": 200000000,
"desc_cn": "离线效率 ×1.15(作用于衰减表全部时段;6 级满 ×2.31)"
"desc_cn": "离线效率 +5%(加法,直接加在衰减表各档速率上;6 级满 +30%,各档硬封顶 100%)"
}
]
}
+16 -12
View File
@@ -1,8 +1,8 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "boss_battles.schema.json",
"title": "boss_battles 表 schema v0(十场殿王战参数,数值抄录自 Balance Bible v1.0 §5)",
"description": "架构 §3.2 CSV 规范落盘。HP = hp_factor × 进入时产出/s × hall_factor([B§5]:强度跟随产出,任何水平 ~30s 的仗;hp_factor=15 为 [B§11 差异#4] 修复值,GDD 旧值 50 已废止)。军列 DPS = 产出/s × army_dps_factor。点击伤害 = 点击价值 × 点击区倍率 × 连击(combo_step/层,combo_cap 封顶)。INTRO 开场伤害 = min(超度值×intro_dmg_souls_factor, HP×intro_dmg_cap_ratio)。通过奖励 = 阴德爆发 HP×reward_hp_ratio。",
"title": "boss_battles 表 schema v0(十场殿王战参数,数值抄录自 Balance Bible v1.0 §5 + systems-spec §1.2 方案 B)",
"description": "架构 §3.2 CSV 规范落盘。W4 方案 B(systems-spec §1.2,用户 2026-10-20 拍板):HP = 进入时产出/s × boss_army_seconds(S_n,军列单推秒数,可直读的设计旋钮);军列 DPS = 产出/s × army_dps_factor;点击伤害 = 产出/s × κ × combo_mult(κ = (tune_boss_kappa_base + tune_boss_kappa_slope × 已购点击升级数/12) × (1 + meta_06阶 × 0.30),满配 0.0806);INTRO 开场伤害 = HP × tune_boss_intro_cap × clamp(超度值/souls_par, 0, 1)(tune_boss_intro_enabled 开关);通过奖励 = 阴德爆发 HP×reward_hp_ratio。hall_factor 保留但降级为纯叙事/视觉参数(血条长度观感与 UI 文案),不参与任何伤害计算。已删除列:hp_factor / intro_dmg_souls_factor / intro_dmg_cap_ratio(旧公式参数,方案 B 后为死旋钮,见 provenance P-16/P-19)。",
"x-supported-keywords": [
"type", "properties", "required", "items", "enum", "pattern",
"minimum", "maximum", "minLength", "minItems", "maxItems",
@@ -17,7 +17,7 @@
"columns": {
"type": "array",
"items": { "type": "string" },
"minItems": 14
"minItems": 13
},
"rows": {
"type": "array",
@@ -26,8 +26,8 @@
"items": {
"type": "object",
"required": [
"id", "hall_id", "hall_factor", "hp_factor", "army_dps_factor",
"intro_sec", "intro_dmg_souls_factor", "intro_dmg_cap_ratio",
"id", "hall_id", "hall_factor", "boss_army_seconds", "souls_par",
"army_dps_factor", "intro_sec",
"active_sec", "combo_window_sec", "combo_step", "combo_cap",
"retry_cooldown_sec", "reward_hp_ratio"
],
@@ -42,18 +42,22 @@
"type": "number",
"minimum": 1,
"maximum": 3,
"description": "殿系数([B§5] H1-H10 = 1.0/1.1/1.2/1.3/1.4/1.6/1.8/2.0/2.1/2.2)"
"description": "殿系数([B§5] H1-H10 = 1.0/1.1/1.2/1.3/1.4/1.6/1.8/2.0/2.1/2.2)。W4 起降级为纯叙事/视觉参数(血条长度观感与 UI 文案,如\"此殿厚重系数 2.2\"),不参与伤害计算;调难度只改 boss_army_seconds"
},
"hp_factor": {
"boss_army_seconds": {
"type": "number",
"const": 15,
"description": "HP 系数([B§5]:15×产出/s×殿系数;[B§11] 由 GDD 50 修正)。W3 补齐缺失的 type:此前未声明 type 时 SheetImporter 按 string 处理,build 产物出现 \"15\" 字符串(且依赖 Godot Variant 的类型宽松相等才能通过 const 校验),是不该存在的隐式假设。"
"minimum": 1,
"maximum": 60,
"description": "军列单推秒数 S_n(systems-spec §1.2 B1:23.0/23.5/24.0/24.5/25.0/25.5/26.0/26.5/27.0/28.0)。HP = 进入时产出/s × 本值;军列 DPS = 产出/s × 1.0 ⇒ 纯军列耗时 = S_n 秒(恒等式)。必须 < active_sec(十殿全可胜)"
},
"souls_par": {
"type": "integer",
"minimum": 0,
"description": "开场冲锋超度基准(systems-spec §1.2 B3:0/2/18/120/600/2400/7200/21000/48000/90000)。intro_dmg = HP × tune_boss_intro_cap × clamp(超度值/本值, 0, 1);0 = 该殿无开场加成"
},
"army_dps_factor": { "type": "number", "minimum": 0, "description": "军列 DPS = 产出/s × 本值([B§5] 1.0)" },
"intro_sec": { "type": "integer", "minimum": 0, "description": "INTRO 冲锋过场秒数(3s)" },
"intro_dmg_souls_factor": { "type": "number", "minimum": 0, "description": "开场伤害 = 超度值 × 本值(0.5)" },
"intro_dmg_cap_ratio": { "type": "number", "minimum": 0, "maximum": 1, "description": "开场伤害封顶 = HP × 本值(0.2)" },
"active_sec": { "type": "integer", "minimum": 1, "description": "ACTIVE 主动阶段秒数(30s)" },
"active_sec": { "type": "integer", "minimum": 1, "description": "ACTIVE 主动阶段秒数(30s;用户锁定恒 30 不动)" },
"combo_window_sec": { "type": "number", "minimum": 0, "description": "连击窗口(0.6s)" },
"combo_step": { "type": "number", "minimum": 0, "description": "连击每层加成(+2%)" },
"combo_cap": { "type": "number", "minimum": 0, "maximum": 1, "description": "连击封顶(+60%)" },
+3 -3
View File
@@ -53,13 +53,13 @@
},
"effect_type": {
"type": "string",
"enum": ["gen_output_add", "click_mult", "click_output_pct", "offline_mult", "unlock"],
"description": "gen_output_add=班底产出加法(+param);click_mult=点击价值×param;click_output_pct=点击附加产出/s 的 param 比例;offline_mult=离线效率×param;unlock=纯解锁(param=0)"
"enum": ["gen_output_add", "click_mult", "click_output_pct", "offline_add", "unlock"],
"description": "gen_output_add=班底产出加法(+param);click_mult=点击价值×param;click_output_pct=点击附加产出/s 的 param 比例(进全局区,P-15);offline_add=离线效率 +param(加法加在衰减表各档速率上,各档硬顶 100%;W4 契约 v2 取代旧 offline_mult ×1.15 乘性链,systems-spec §2.5);unlock=纯解锁(param=0)"
},
"effect_param": {
"type": "number",
"minimum": 0,
"description": "效果参数(见 effect_type);efficiency=1(+100%),click_mult=2,click_output_pct=0.01,offline_mult=1.15,unlock=0"
"description": "效果参数(见 effect_type);efficiency=1(+100%),click_mult=2,click_output_pct=0.01,offline_add=0.05(长明灯每级 +5%,6 级满 +30%),unlock=0"
},
"requires": {
"type": "string",
+4
View File
@@ -36,3 +36,7 @@ tune_cam_follow_tau,number,0.35,,[W3 相机] 进度锚定跟随的指数平滑
tune_cam_zoom_tau,number,0.3,,[W3 相机] 缩放与殿王战推近的指数平滑时间常数(秒)
tune_boss_preview_min,number,15,,[B§6] 殿王战战前预告提前分钟数(与 H9-H10 prestige 预告同口径)
tune_event_first_min,number,3,,[B§7] 事件系统解锁后首个事件的延迟分钟数(给玩家缓冲,避免刚解锁就来)
tune_boss_kappa_base,number,0.01,,[systems-spec §1.2 B2] 殿王战点击伤害 κ 基数(零点击投资时 κ = 0.010;W4 方案 B)
tune_boss_kappa_slope,number,0.052,,[systems-spec §1.2 B2] κ 随法器线进度斜率:κ = (base + slope × 已购点击升级/12) × (1 + m06阶×0.30);满配 κ = 0.0806
tune_boss_intro_cap,number,0.1,,[systems-spec §1.2 B3] 开场冲锋伤害封顶 = HP × 本值(0.10;旧公式 20% 已废止)
tune_boss_intro_enabled,number,1,,[systems-spec §1.2 B3] 开场冲锋开关(1 = 启用;门 #11 首轮数据回来前默认 1)
1 id kind num text desc_cn
36 tune_cam_zoom_tau number 0.3 [W3 相机] 缩放与殿王战推近的指数平滑时间常数(秒)
37 tune_boss_preview_min number 15 [B§6] 殿王战战前预告提前分钟数(与 H9-H10 prestige 预告同口径)
38 tune_event_first_min number 3 [B§7] 事件系统解锁后首个事件的延迟分钟数(给玩家缓冲,避免刚解锁就来)
39 tune_boss_kappa_base number 0.01 [systems-spec §1.2 B2] 殿王战点击伤害 κ 基数(零点击投资时 κ = 0.010;W4 方案 B)
40 tune_boss_kappa_slope number 0.052 [systems-spec §1.2 B2] κ 随法器线进度斜率:κ = (base + slope × 已购点击升级/12) × (1 + m06阶×0.30);满配 κ = 0.0806
41 tune_boss_intro_cap number 0.1 [systems-spec §1.2 B3] 开场冲锋伤害封顶 = HP × 本值(0.10;旧公式 20% 已废止)
42 tune_boss_intro_enabled number 1 [systems-spec §1.2 B3] 开场冲锋开关(1 = 启用;门 #11 首轮数据回来前默认 1)
+11 -11
View File
@@ -1,11 +1,11 @@
id,hall_id,hall_factor,hp_factor,army_dps_factor,intro_sec,intro_dmg_souls_factor,intro_dmg_cap_ratio,active_sec,combo_window_sec,combo_step,combo_cap,retry_cooldown_sec,reward_hp_ratio
boss_hall_01,hall_01_qinguang,1,15,1,3,0.5,0.2,30,0.6,0.02,0.6,60,0.2
boss_hall_02,hall_02_chujiang,1.1,15,1,3,0.5,0.2,30,0.6,0.02,0.6,60,0.2
boss_hall_03,hall_03_songdi,1.2,15,1,3,0.5,0.2,30,0.6,0.02,0.6,60,0.2
boss_hall_04,hall_04_wuguan,1.3,15,1,3,0.5,0.2,30,0.6,0.02,0.6,60,0.2
boss_hall_05,hall_05_yama,1.4,15,1,3,0.5,0.2,30,0.6,0.02,0.6,60,0.2
boss_hall_06,hall_06_biancheng,1.6,15,1,3,0.5,0.2,30,0.6,0.02,0.6,60,0.2
boss_hall_07,hall_07_taishan,1.8,15,1,3,0.5,0.2,30,0.6,0.02,0.6,60,0.2
boss_hall_08,hall_08_dushi,2,15,1,3,0.5,0.2,30,0.6,0.02,0.6,60,0.2
boss_hall_09,hall_09_pingdeng,2.1,15,1,3,0.5,0.2,30,0.6,0.02,0.6,60,0.2
boss_hall_10,hall_10_zhuanlun,2.2,15,1,3,0.5,0.2,30,0.6,0.02,0.6,60,0.2
id,hall_id,hall_factor,boss_army_seconds,souls_par,army_dps_factor,intro_sec,active_sec,combo_window_sec,combo_step,combo_cap,retry_cooldown_sec,reward_hp_ratio
boss_hall_01,hall_01_qinguang,1,23,0,1,3,30,0.6,0.02,0.6,60,0.2
boss_hall_02,hall_02_chujiang,1.1,23.5,2,1,3,30,0.6,0.02,0.6,60,0.2
boss_hall_03,hall_03_songdi,1.2,24,18,1,3,30,0.6,0.02,0.6,60,0.2
boss_hall_04,hall_04_wuguan,1.3,24.5,120,1,3,30,0.6,0.02,0.6,60,0.2
boss_hall_05,hall_05_yama,1.4,25,600,1,3,30,0.6,0.02,0.6,60,0.2
boss_hall_06,hall_06_biancheng,1.6,25.5,2400,1,3,30,0.6,0.02,0.6,60,0.2
boss_hall_07,hall_07_taishan,1.8,26,7200,1,3,30,0.6,0.02,0.6,60,0.2
boss_hall_08,hall_08_dushi,2,26.5,21000,1,3,30,0.6,0.02,0.6,60,0.2
boss_hall_09,hall_09_pingdeng,2.1,27,48000,1,3,30,0.6,0.02,0.6,60,0.2
boss_hall_10,hall_10_zhuanlun,2.2,28,90000,1,3,30,0.6,0.02,0.6,60,0.2
1 id hall_id hall_factor hp_factor boss_army_seconds souls_par army_dps_factor intro_dmg_souls_factor intro_sec intro_dmg_cap_ratio active_sec combo_window_sec combo_step combo_cap retry_cooldown_sec reward_hp_ratio
2 boss_hall_01 hall_01_qinguang 1 15 23 0 1 0.5 3 0.2 30 0.6 0.02 0.6 60 0.2
3 boss_hall_02 hall_02_chujiang 1.1 15 23.5 2 1 0.5 3 0.2 30 0.6 0.02 0.6 60 0.2
4 boss_hall_03 hall_03_songdi 1.2 15 24 18 1 0.5 3 0.2 30 0.6 0.02 0.6 60 0.2
5 boss_hall_04 hall_04_wuguan 1.3 15 24.5 120 1 0.5 3 0.2 30 0.6 0.02 0.6 60 0.2
6 boss_hall_05 hall_05_yama 1.4 15 25 600 1 0.5 3 0.2 30 0.6 0.02 0.6 60 0.2
7 boss_hall_06 hall_06_biancheng 1.6 15 25.5 2400 1 0.5 3 0.2 30 0.6 0.02 0.6 60 0.2
8 boss_hall_07 hall_07_taishan 1.8 15 26 7200 1 0.5 3 0.2 30 0.6 0.02 0.6 60 0.2
9 boss_hall_08 hall_08_dushi 2 15 26.5 21000 1 0.5 3 0.2 30 0.6 0.02 0.6 60 0.2
10 boss_hall_09 hall_09_pingdeng 2.1 15 27 48000 1 0.5 3 0.2 30 0.6 0.02 0.6 60 0.2
11 boss_hall_10 hall_10_zhuanlun 2.2 15 28 90000 1 0.5 3 0.2 30 0.6 0.02 0.6 60 0.2
+6 -6
View File
@@ -101,9 +101,9 @@ up_gate_prestige,开通 · 轮回预告,Unlock: Rebirth Preview,gate_system,none
up_gate_boss_retry,开通 · 殿王再战,Unlock: Boss Rematch,gate_system,none,hall_07_taishan,unlock,0,none,60000000,系统闸门:纯解锁零效果([B§2.3];成本外推 = 殿门阈值÷2,P-5)
up_gate_bulkmax,开通 · 批量购置(×尽),Unlock: Bulk Buy Max,gate_system,none,hall_08_dushi,unlock,0,none,175000000,系统闸门:纯解锁零效果([B§2.3];成本外推 = 殿门阈值÷2,P-5)
up_gate_enthrone,开通 · 敕封预告,Unlock: Enthronement Preview,gate_system,none,hall_09_pingdeng,unlock,0,none,400000000,系统闸门:纯解锁零效果([B§2.3];成本外推 = 殿门阈值÷2,P-5)
up_offline_1,离线收益 · 沉淀 I,Offline Income · Settling I,offline,none,hall_04_wuguan,offline_mult,1.15,none,2000,离线效率 ×1.15(作用于衰减表全部时段;6 级满 ×2.31)
up_offline_2,离线收益 · 沉淀 II,Offline Income · Settling II,offline,none,hall_04_wuguan,offline_mult,1.15,none,20000,离线效率 ×1.15(作用于衰减表全部时段;6 级满 ×2.31)
up_offline_3,离线收益 · 沉淀 III,Offline Income · Settling III,offline,none,hall_04_wuguan,offline_mult,1.15,none,200000,离线效率 ×1.15(作用于衰减表全部时段;6 级满 ×2.31)
up_offline_4,离线收益 · 沉淀 IV,Offline Income · Settling IV,offline,none,hall_04_wuguan,offline_mult,1.15,none,2000000,离线效率 ×1.15(作用于衰减表全部时段;6 级满 ×2.31)
up_offline_5,离线收益 · 沉淀 V,Offline Income · Settling V,offline,none,hall_04_wuguan,offline_mult,1.15,none,20000000,离线效率 ×1.15(作用于衰减表全部时段;6 级满 ×2.31)
up_offline_6,离线收益 · 沉淀 VI,Offline Income · Settling VI,offline,none,hall_04_wuguan,offline_mult,1.15,none,200000000,离线效率 ×1.15(作用于衰减表全部时段;6 级满 ×2.31)
up_offline_1,离线收益 · 沉淀 I,Offline Income · Settling I,offline,none,hall_04_wuguan,offline_add,0.05,none,2000,离线效率 +5%(加法,直接加在衰减表各档速率上;6 级满 +30%,各档硬封顶 100%)
up_offline_2,离线收益 · 沉淀 II,Offline Income · Settling II,offline,none,hall_04_wuguan,offline_add,0.05,none,20000,离线效率 +5%(加法,直接加在衰减表各档速率上;6 级满 +30%,各档硬封顶 100%)
up_offline_3,离线收益 · 沉淀 III,Offline Income · Settling III,offline,none,hall_04_wuguan,offline_add,0.05,none,200000,离线效率 +5%(加法,直接加在衰减表各档速率上;6 级满 +30%,各档硬封顶 100%)
up_offline_4,离线收益 · 沉淀 IV,Offline Income · Settling IV,offline,none,hall_04_wuguan,offline_add,0.05,none,2000000,离线效率 +5%(加法,直接加在衰减表各档速率上;6 级满 +30%,各档硬封顶 100%)
up_offline_5,离线收益 · 沉淀 V,Offline Income · Settling V,offline,none,hall_04_wuguan,offline_add,0.05,none,20000000,离线效率 +5%(加法,直接加在衰减表各档速率上;6 级满 +30%,各档硬封顶 100%)
up_offline_6,离线收益 · 沉淀 VI,Offline Income · Settling VI,offline,none,hall_04_wuguan,offline_add,0.05,none,200000000,离线效率 +5%(加法,直接加在衰减表各档速率上;6 级满 +30%,各档硬封顶 100%)
1 id name_cn name_en type target_gen unlock_hall effect_type effect_param requires cost desc_cn
101 up_gate_boss_retry 开通 · 殿王再战 Unlock: Boss Rematch gate_system none hall_07_taishan unlock 0 none 60000000 系统闸门:纯解锁零效果([B§2.3];成本外推 = 殿门阈值÷2,P-5)
102 up_gate_bulkmax 开通 · 批量购置(×尽) Unlock: Bulk Buy Max gate_system none hall_08_dushi unlock 0 none 175000000 系统闸门:纯解锁零效果([B§2.3];成本外推 = 殿门阈值÷2,P-5)
103 up_gate_enthrone 开通 · 敕封预告 Unlock: Enthronement Preview gate_system none hall_09_pingdeng unlock 0 none 400000000 系统闸门:纯解锁零效果([B§2.3];成本外推 = 殿门阈值÷2,P-5)
104 up_offline_1 离线收益 · 沉淀 I Offline Income · Settling I offline none hall_04_wuguan offline_mult offline_add 1.15 0.05 none 2000 离线效率 ×1.15(作用于衰减表全部时段;6 级满 ×2.31) 离线效率 +5%(加法,直接加在衰减表各档速率上;6 级满 +30%,各档硬封顶 100%)
105 up_offline_2 离线收益 · 沉淀 II Offline Income · Settling II offline none hall_04_wuguan offline_mult offline_add 1.15 0.05 none 20000 离线效率 ×1.15(作用于衰减表全部时段;6 级满 ×2.31) 离线效率 +5%(加法,直接加在衰减表各档速率上;6 级满 +30%,各档硬封顶 100%)
106 up_offline_3 离线收益 · 沉淀 III Offline Income · Settling III offline none hall_04_wuguan offline_mult offline_add 1.15 0.05 none 200000 离线效率 ×1.15(作用于衰减表全部时段;6 级满 ×2.31) 离线效率 +5%(加法,直接加在衰减表各档速率上;6 级满 +30%,各档硬封顶 100%)
107 up_offline_4 离线收益 · 沉淀 IV Offline Income · Settling IV offline none hall_04_wuguan offline_mult offline_add 1.15 0.05 none 2000000 离线效率 ×1.15(作用于衰减表全部时段;6 级满 ×2.31) 离线效率 +5%(加法,直接加在衰减表各档速率上;6 级满 +30%,各档硬封顶 100%)
108 up_offline_5 离线收益 · 沉淀 V Offline Income · Settling V offline none hall_04_wuguan offline_mult offline_add 1.15 0.05 none 20000000 离线效率 ×1.15(作用于衰减表全部时段;6 级满 ×2.31) 离线效率 +5%(加法,直接加在衰减表各档速率上;6 级满 +30%,各档硬封顶 100%)
109 up_offline_6 离线收益 · 沉淀 VI Offline Income · Settling VI offline none hall_04_wuguan offline_mult offline_add 1.15 0.05 none 200000000 离线效率 ×1.15(作用于衰减表全部时段;6 级满 ×2.31) 离线效率 +5%(加法,直接加在衰减表各档速率上;6 级满 +30%,各档硬封顶 100%)
+4 -3
View File
@@ -158,11 +158,12 @@ func _load_bosses(rows: Array) -> void:
d.id = str(r.get("id", ""))
d.hall_id = str(r.get("hall_id", ""))
d.hall_factor = float(r.get("hall_factor", 1))
d.hp_factor = float(r.get("hp_factor", 15))
# W4 方案 B(systems-spec §1.2):HP = 产出/s × boss_army_seconds;
# 旧 hp_factor / intro_dmg_souls_factor / intro_dmg_cap_ratio 列已删除。
d.boss_army_seconds = float(r.get("boss_army_seconds", 23.0))
d.souls_par = int(r.get("souls_par", 0))
d.army_dps_factor = float(r.get("army_dps_factor", 1))
d.intro_sec = float(r.get("intro_sec", 3))
d.intro_dmg_souls_factor = float(r.get("intro_dmg_souls_factor", 0.5))
d.intro_dmg_cap_ratio = float(r.get("intro_dmg_cap_ratio", 0.2))
d.active_sec = float(r.get("active_sec", 30))
d.combo_window_sec = float(r.get("combo_window_sec", 0.6))
d.combo_step = float(r.get("combo_step", 0.02))
+24 -14
View File
@@ -2,30 +2,32 @@ class_name BossDef
extends RefCounted
# ============================================================================
# BossDef —— 殿王战参数数据行(架构 §4.3;[B§5])
# BossDef —— 殿王战参数数据行(架构 §4.3;[B§5] + systems-spec §1.2 方案 B)
#
# 公式([B§5],参数全部来自本行,代码零硬编码):
# HP = hp_factor × 进入时产出/s × hall_factor (15×产出/s×殿系数 → ~30s 的仗)
# 公式(W4 方案 B,用户 2026-10-20 拍板;参数全部来自本行 + balance_tuning,代码零硬编码):
# HP = 进入时产出/s × boss_army_seconds (S_n:军列单推秒数,可直读旋钮)
# 军列 DPS = 进入时产出/s × army_dps_factor (阴兵在打,不是你一个人)
# 点击伤害 = 点击价值 × 连击(combo_step/层,combo_cap 封顶)
# INTRO = intro_sec 秒冲锋过场;开场伤害 = min(超度×intro_dmg_souls_factor, HP×intro_dmg_cap_ratio)
# 点击伤害 = 进入时产出/s × κ × combo_mult (BossBattle 计算 κ)
# INTRO = intro_sec 秒冲锋过场;开场伤害 = HP × intro_cap × clamp(超度/souls_par, 0, 1)
# ACTIVE = active_sec 秒主动阶段
# 通过奖励 = 阴德爆发 = HP × reward_hp_ratio
# 失败 = retry_cooldown_sec 冷却可再战(H7 前败即等推 → up_gate_boss_retry)
#
# 注:hp_factor=15 是 [B§11 差异#4] 对 GDD 50× 的修复值(50× 按 DPS=产出/s
# 推演 30s 打不完)。架构文档 §1.2/§4.3 中残留的 "50×" 为陈旧引用,以本表与
# Balance Bible v1.0 §5 为准。
# 方案 B 记(systems-spec §1.2,取代旧式 HP = 15×产出/s×hall_factor):
# - 旧式使产出/s 在分子分母被约掉 ⇒ 战斗时长与玩家强度脱钩,h>2.0 时数学不可胜(D-0);
# - 新式把"这场仗军列单推几秒"变成 CSV 里直写的数字,同类 bug 结构上不可能复发;
# - hall_factor **降级为纯叙事/视觉参数**(血条观感 / UI 文案),不参与伤害计算;
# - hp_factor / intro_dmg_souls_factor / intro_dmg_cap_ratio 三列随旧公式一并删除
# (留列即成误导性死旋钮;登记见 provenance P-16 / P-19)。
# ============================================================================
var id: String # "boss_hall_01"
var hall_id: String # halls.csv 行 ID
var hall_factor: float # 殿系数 1.0→2.2
var hp_factor: float # HP 系数(15)
var hall_factor: float # 殿系数 1.0→2.2(**纯叙事/视觉,不参与计算**)
var boss_army_seconds: float # 军列单推秒数 S_n(23.0→28.0)
var souls_par: int # 开场冲锋超度基准(0/2/18/…/90000;0 = 无开场加成)
var army_dps_factor: float # 军列 DPS 系数(1.0)
var intro_sec: float # INTRO 秒数(3)
var intro_dmg_souls_factor: float # 开场伤害 = 超度 × 本值(0.5)
var intro_dmg_cap_ratio: float # 开场伤害封顶 = HP × 本值(0.2)
var active_sec: float # ACTIVE 秒数(30)
var combo_window_sec: float # 连击窗口(0.6s)
var combo_step: float # 连击每层 +2%
@@ -34,6 +36,14 @@ var retry_cooldown_sec: float # 失败冷却(60s)
var reward_hp_ratio: float # 通过阴德爆发 = HP × 本值(0.2)
## 进入时 HP(架构 §4.3 start() 口径)
## 进入时 HP(方案 B:产出/s × S_n;架构 §4.3 start() 口径)
func hp_at(output_per_sec: float) -> float:
return hp_factor * output_per_sec * hall_factor
return boss_army_seconds * output_per_sec
## 开场冲锋伤害(方案 B3:HP × cap × clamp(超度/souls_par, 0, 1);cap 由 tune_boss_intro_cap 给)
## souls_par = 0(H1)→ 无开场加成(防 0 除);cap ≤ 0 或 enabled = false 时调用方直接跳过。
func intro_factor(souls_saved: int) -> float:
if souls_par <= 0:
return 0.0
return clampf(float(souls_saved) / float(souls_par), 0.0, 1.0)
+13 -6
View File
@@ -491,6 +491,11 @@ func recompute() -> void:
for gid in eff:
_mult.set_gen_eff(gid, eff[gid])
# 全局区加法总和([B§1]:宿慧×3% + 法器附加 + 冤煞层 + 敕封 25% + m08 + m12)
# ⚠ 防双重计入约束(systems-spec §1.2 B2 / 第三章 P-10 耦合条,用户 2026-10-20 拍板):
# L5-12 的 click_output_pct(8×+1% 产出/s)进全局区 ⇒ 提升产出/s ⇒ **同时**提升
# 军列 DPS 与殿王战点击伤害(click_dmg = P × κ)。这是可接受的(点击树本就该是
# boss 战投资),但 **不得**再把它计入 κ 的分子——κ 只按"已购点击升级条数/12"
# (BossBattle.click_line_ratio)计算,杜绝二次放大。改此函数前先读 systems-spec §1.2。
var gadd := float(suihui) * _suihui_global_pct
gadd += float(wrath_layers) * _wrath_bonus
if enthroned:
@@ -527,16 +532,18 @@ func gen_index_of(gen_id: String) -> int:
return -1
## 当前离线倍率([B§2.4] ×1.15/级链 × m05 加法;P-13 已标注)
func offline_mult() -> float:
var m := 1.0
## 当前离线加法加成(契约 v2 / systems-spec §2.5,替代旧 ×1.15 乘性链 offline_mult):
## 长明灯 6 条每级 +5%(加法)+ m05 每阶 +25%(加法),直接加在衰减表各档速率上,
## 各档由 OfflineModel 硬封顶 100%。6 条满 = +30% ⇒ 8h 离线 = 8.0h(效率 100%)。
func offline_add() -> float:
var add := 0.0
for u in data.upgrades:
if u.type == "offline" and bought.has(u.id):
m *= u.effect_param
add += u.effect_param
var m05 := data.meta("meta_05")
if m05 != null:
m *= 1.0 + m05.effect_param * float(int(meta_ranks.get("meta_05", 0)))
return m
add += m05.effect_param * float(int(meta_ranks.get("meta_05", 0)))
return add
## 事件瞬时区写入(game/EventDirector 调用;独立区不进全局乘区)
+20 -14
View File
@@ -2,22 +2,26 @@ class_name OfflineModel
extends RefCounted
# ============================================================================
# OfflineModel —— 离线收益衰减表 + 时钟钳制(架构 §4.2/§6.5;[B§8])
# OfflineModel —— 离线收益衰减表 + 长明灯加法 + 硬顶(架构 §4.2/§6.5;[B§8] + 契约 v2)
#
# 衰减表([B§8],参数来自 balance_tuning.csv):
# 衰减表([B§8],参数来自 balance_tuning.csv;**W4 裁定一字不改**):
# 0-2h 100% / 2-8h 70% / 8-24h 40% / >24h 10%;硬上限 72h 封顶
# × 离线升级(最多 ×2.31);时钟回拨钳制为 0(负收益记 0)
# 长明灯(systems-spec §2.5 / P-13,用户 2026-10-20 拍板):**加法 +5%/级,6 级满 +30%**
# ——直接加在衰减表各档速率上;**各档速率硬封顶 100%**(离线永不得优于在线)。
# (旧实现为 ×1.15 乘性链 6 级 ×2.31:8h 离线效率 179% > 在线,摧毁在线动机,已废止。)
#
# 契约校准点([B§8]):离线 8h ≈ 在线 1.5-2h 有效进度(门 #9 断言 ∈[1.5,2.5])。
# ⚠ 已知不一致(provenance P-8):按上表直接求值,8h 离线 = 2h×100% + 6h×70%
# = 6.2h 在线等效 → 比率 6.2 ∉ [1.5,2.5]。表与契约二者其一需文策渊裁定
# (候选:2-8h 降至 ~8% 以下,或契约改 [4,7])。本类按表实现,门 #9 如实报 FAIL。
# 契约 v2(systems-spec §2.4,替换 [B§8] 的"8h≈在线 1.5-2h"):
# - 效率指标(α,静态可算):8h = 6.2 等效小时(77.5%);24h = 12.6h;72h 封顶 = 17.4h(24.2%)
# - 进度指标(β,sim 标定):∈[1.5, 4.0],待 sim 首轮(门 #9-b 口径)
# - 硬顶:任何配置(含长明灯满级 + m05)离线等效效率 ≤ 100%
#
# 静态纯函数:sim 门 #9 与游戏载入时共用(零漂移)。
# ============================================================================
## 分带结算:把秒数拆进各衰减带,返回 {seconds, merit, effective_hours}
static func settle(seconds: float, output_per_sec: float, offline_mult: float,
## offline_add = 长明灯加法总和(up_offline 6 条 × 0.05 + m05 阶 × 0.25,EconomyState.offline_add);
## 各档最终速率 = min(表速率 + offline_add, 1.0)(硬顶 100%)。
static func settle(seconds: float, output_per_sec: float, offline_add: float,
band1_h: float, band1_rate: float, band2_h: float, band2_rate: float,
band3_h: float, band3_rate: float, band4_rate: float, cap_h: float) -> Dictionary:
var out := {"seconds": 0.0, "merit": 0.0, "effective_hours": 0.0}
@@ -35,23 +39,25 @@ static func settle(seconds: float, output_per_sec: float, offline_mult: float,
if remaining <= 0.0:
break
var take := minf(remaining, span)
effective += take * rates[i]
# 长明灯加法 + 各档硬顶 100%(契约 v2:离线不得优于在线)
effective += take * minf(rates[i] + offline_add, 1.0)
remaining -= take
prev = bounds[i]
if remaining > 0.0:
effective += remaining * band4_rate
effective += remaining * minf(band4_rate + offline_add, 1.0)
out["seconds"] = s
out["effective_hours"] = effective / 3600.0
out["merit"] = effective * output_per_sec * offline_mult
out["merit"] = effective * output_per_sec
return out
## 离线 8h ÷ 在线 1h 的等效比率(门 #9 直接用;offline_mult=1 口径)
## 离线 8h ÷ 在线 1h 的等效比率(历史口径;offline_add=0 时 = 6.2)。
## 契约 v2 起门 #9 不再用比率判 PASS/FAIL,改判等效小时与效率硬顶(见 SimGates.gate_offline_v2)。
static func ratio_8h_over_1h(band1_h: float, band1_rate: float, band2_h: float,
band2_rate: float, band3_h: float, band3_rate: float, band4_rate: float) -> float:
var r := settle(8.0 * 3600.0, 1.0, 1.0, band1_h, band1_rate, band2_h, band2_rate,
var r := settle(8.0 * 3600.0, 1.0, 0.0, band1_h, band1_rate, band2_h, band2_rate,
band3_h, band3_rate, band4_rate, 1e9)
var one := settle(3600.0, 1.0, 1.0, band1_h, band1_rate, band2_h, band2_rate,
var one := settle(3600.0, 1.0, 0.0, band1_h, band1_rate, band2_h, band2_rate,
band3_h, band3_rate, band4_rate, 1e9)
if one["merit"] <= 0.0:
return 0.0
+6 -2
View File
@@ -9,11 +9,15 @@ extends Resource
# click 点击升级(click_mult=价值×param;click_output_pct=附加产出/s 的 param 比例)
# gate_hire 班底入职文书(纯解锁 target_gen;成本 = C0×2/3)
# gate_system 系统闸门(纯解锁系统;成本 = 殿门阈值÷2,工程外推 P-5)
# offline 离线效率(offline_mult=离线效率×param)
# offline 离线效率(offline_add=离线效率 +param,加法加在各档速率上;W4 契约 v2)
# meta 宿慧商店永久商品(meta_upgrades.csv;max_rank>1 可重复)
#
# 成本口径:效率 = round(C0×r^(10+4n))(0 起始级号第 10+4n 级价);点击/离线 =
# [B§2.2/§2.4] 钉值;入职文书 = round(C0×2/3)(g03=800 钉值复现)。
#
# W4 变更(systems-spec §2.5,用户 2026-10-20 拍板):offline 的 effect_type 由
# offline_mult(每级 ×1.15 乘性,6 级 ×2.31 → 8h 离线效率 179% > 在线,品类红线)
# 改为 offline_add(每级 +5% 加法,6 级 +30%;各档速率由 OfflineModel 硬顶 100%)。
# ============================================================================
@export var id: String # "up_eff_gen_01_youhun_1" / "meta_01"
@@ -22,7 +26,7 @@ extends Resource
@export var type: String # efficiency/click/gate_hire/gate_system/offline/meta
@export var target_gen: String # efficiency/gate_hire 指向班底 ID;其余 "none"/""
@export var unlock_hall: String # 出现条件:通过哪殿后可见/可购;"none" = 开局
@export var effect_type: String # gen_output_add/click_mult/click_output_pct/offline_mult/unlock/…
@export var effect_type: String # gen_output_add/click_mult/click_output_pct/offline_add/unlock/…
@export var effect_param: float # 效果参数(语义见 effect_type)
@export var cost: int # 阴德成本(meta 行为宿慧成本,见 cost_is_suihui)
@export var cost_is_suihui: bool = false # true = meta 行(cost 单位为宿慧)
+57 -10
View File
@@ -34,8 +34,8 @@ static func evaluate_report(report: Dictionary, data: BalanceData) -> Dictionary
g[6] = _gate_no_wall(report)
# #8 回收期 tier2-6 ∈[120,293]s 且全表单调(数据静态门)
g[8] = _gate_payback(data)
# #9 离线 8h÷1h ∈[1.5,2.5](P-8:按表求值预计 FAIL,如实报)
g[9] = _gate_offline(data)
# #9 离线契约 v2(P-8 裁定:拆 a/b/c 三条;a 条由 FAIL 转 PASS)
g[9] = gate_offline_v2(data)
# #10 终局 ≤1e14
var peak := float(report.get("peak_lifetime", 0.0))
g[10] = {"pass": peak <= BigNumber.RED_LINE,
@@ -203,14 +203,61 @@ static func _gate_payback(data: BalanceData) -> Dictionary:
return {"pass": ok, "detail": detail}
static func _gate_offline(data: BalanceData) -> Dictionary:
var r := OfflineModel.ratio_8h_over_1h(
data.tune_num("tune_offline_band1_h", 2.0), data.tune_num("tune_offline_band1_rate", 1.0),
data.tune_num("tune_offline_band2_h", 8.0), data.tune_num("tune_offline_band2_rate", 0.7),
data.tune_num("tune_offline_band3_h", 24.0), data.tune_num("tune_offline_band3_rate", 0.4),
data.tune_num("tune_offline_band4_rate", 0.1))
return {"pass": r >= 1.5 and r <= 2.5,
"detail": "离线 8h÷在线 1h = %.2f(契约 [1.5,2.5];P-8 已知不一致,按表求值)" % r}
## 门 #9 离线契约 v2(systems-spec §2.4 裁定,用户 2026-10-20 拍板;替代旧 [1.5,2.5] 比率门)
## 拆三条子检查(旧门的矛盾 = 契约语义错误:α 产出量口径下 1.5-2.5 数学不可达):
## 9a(静态):0-2h 全额(band1 = 100%)且 8h 等效小时 ∈ [5.5, 6.5](无长明灯)→ PASS(原 FAIL 转 PASS)
## 9b(静态):长明灯满级(加法 +30%)后 8h 等效 ∈ [7.5, 8.0],效率 ≤ 100% → PASS
## 9c(静态):任意加成/任意时长等效 ≤ 实际(效率硬顶 ≤ 100%)→ PASS
## 另:进度口径 β(离线 8h 推进 ÷ 在线 1h 推进 ∈ [1.5, 4.0])需 sim 首轮标定,
## 记为 deferred,不在本三条内(systems-spec §2.4 门 #9-b 原条目)。
static func gate_offline_v2(data: BalanceData) -> Dictionary:
var b1h := data.tune_num("tune_offline_band1_h", 2.0)
var b1r := data.tune_num("tune_offline_band1_rate", 1.0)
var b2h := data.tune_num("tune_offline_band2_h", 8.0)
var b2r := data.tune_num("tune_offline_band2_rate", 0.7)
var b3h := data.tune_num("tune_offline_band3_h", 24.0)
var b3r := data.tune_num("tune_offline_band3_rate", 0.4)
var b4r := data.tune_num("tune_offline_band4_rate", 0.1)
var caph := data.tune_num("tune_offline_cap_h", 72.0)
# 长明灯最大加法 = 6 条 up_offline 的 effect_param 之和 + m05 满阶
var lamp_max := 0.0
for u in data.upgrades:
if u.type == "offline":
lamp_max += u.effect_param
var m05 := data.meta("meta_05")
if m05 != null:
lamp_max += m05.effect_param * float(m05.max_rank)
# --- 9a:0-2h 全额 + 8h 等效 ∈ [5.5, 6.5](契约 v2:6.2h / 77.5%) ---
var r8 := OfflineModel.settle(8.0 * 3600.0, 1.0, 0.0, b1h, b1r, b2h, b2r, b3h, b3r, b4r, caph)
var eff8 := float(r8["effective_hours"])
var ok_a := is_equal_approx(b1r, 1.0) and eff8 >= 5.5 and eff8 <= 6.5
var g_a := {"pass": ok_a,
"detail": "0-2h = %.0f%%;8h 等效 %.2fh(无长明灯;目标 [5.5,6.5])" % [b1r * 100.0, eff8]}
# --- 9b:长明灯满级 8h ∈ [7.5, 8.0] 且效率 ≤ 100% ---
var r8l := OfflineModel.settle(8.0 * 3600.0, 1.0, lamp_max, b1h, b1r, b2h, b2r, b3h, b3r, b4r, caph)
var eff8l := float(r8l["effective_hours"])
var ok_b := eff8l >= 7.5 and eff8l <= 8.0 and eff8l <= 8.0 + 1e-9
var g_b := {"pass": ok_b,
"detail": "长明灯满级(加法 +%.0f%%)8h 等效 %.2fh;效率 %.1f%%(目标 ∈[7.5,8.0] 且 ≤100%%)" % [
lamp_max * 100.0, eff8l, eff8l / 8.0 * 100.0]}
# --- 9c:效率硬顶 ≤100%(任意配置/任意时长) ---
var ok_c := true
var parts: Array = []
for dur_h in [1.0, 8.0, 24.0, 72.0, 100.0]:
var rr := OfflineModel.settle(dur_h * 3600.0, 1.0, lamp_max, b1h, b1r, b2h, b2r, b3h, b3r, b4r, caph)
var eff := float(rr["effective_hours"])
var elapsed := float(rr["seconds"]) / 3600.0
if eff > elapsed + 1e-9:
ok_c = false
parts.append("%sh=%.2fh(%.0f%%)" % [str(dur_h), eff, eff / elapsed * 100.0 if elapsed > 0.0 else 0.0])
var g_c := {"pass": ok_c,
"detail": "效率硬顶:%s(全部 ≤100%%;超出 %sh 封顶)" % [",".join(parts), str(caph)]}
var pass := ok_a and ok_b and ok_c
return {"pass": pass,
"detail": "契约 v2:a %s / b %s / c %s(P-8 已裁定:改契约不改表)" % [
"PASS" if ok_a else "FAIL", "PASS" if ok_b else "FAIL", "PASS" if ok_c else "FAIL"],
"sub": {"9a": g_a, "9b": g_b, "9c": g_c},
"deferred_beta": "进度口径 β(∈[1.5,4.0])待 sim 首轮标定"}
static func _join_times(reports: Array) -> String:
+56 -12
View File
@@ -2,13 +2,16 @@ class_name BossBattle
extends RefCounted
# ============================================================================
# BossBattle —— 殿王战逻辑状态机(架构 §4.3;Balance Bible [B§5] 的 W3 落地)
# BossBattle —— 殿王战逻辑状态机(架构 §4.3;[B§5] + systems-spec §1.2 方案 B,W4)
#
# 公式全部来自 boss_battles.csv 的 BossDef(代码零硬编码):
# HP = hp_factor(15) × 进入时产出/s × hall_factor(1.0→2.2)
# 公式全部来自 boss_battles.csv 的 BossDef + balance_tuning.csv(代码零硬编码):
# HP = 进入时产出/s × boss_army_seconds(S_n) 【方案 B1:23.0→28.0】
# 军列 DPS = 进入时产出/s × army_dps_factor(1.0)
# 点击伤害 = 进入时点击价值 × (1 + min(连击层数×combo_step, combo_cap)) [上限 +60%]
# INTRO = intro_sec(3s) 军列冲锋;开场伤害 = min(超度×0.5, HP×0.2)
# 点击伤害 = 进入时产出/s × κ × combo_mult 【方案 B2】
# κ = (kappa_base + kappa_slope × click_ratio) × (1 + m06阶 × m06_param)
# click_ratio = 已购点击升级条数 / 点击树总条数(12)
# INTRO = intro_sec(3s) 军列冲锋;开场伤害 = HP × intro_cap × clamp(超度值/souls_par, 0, 1)
# 【方案 B3:上限从旧 20% 降到 10%;开关 tune_boss_intro_enabled】
# ACTIVE = active_sec(30s);HP≤0 胜 / 超时败
# 通过奖励 = 阴德爆发 = HP × reward_hp_ratio(0.2)(进本轮累计口径 → 可推殿门)
# 失败 = retry_cooldown_sec(60s) 冷却可再战
@@ -42,10 +45,14 @@ var timer: float = 0.0
var cooldown: float = 0.0
var win: bool = false
var entry_output: float = 0.0
var entry_click: float = 0.0
var entry_kappa: float = 0.0 # W4 方案 B2:进入时锁定的点击伤害系数 κ
var last_reward: float = 0.0
var cleared: Dictionary = {} # boss id -> true(同一轮回内不重复挑战)
# W4 方案 B3:开场冲锋开关与封顶(balance_tuning;start 时缓存一次)
var _intro_enabled: bool = true
var _intro_cap: float = 0.10
func _init(p_data: BalanceData) -> void:
data = p_data
@@ -65,6 +72,35 @@ func combo_mult() -> float:
return 1.0 + minf(float(combo) * def.combo_step, def.combo_cap)
## 法器线进度(已购点击升级条数 / 点击树总条数;UI 可直接显示为"法器大成 x/12")
## 分母取整棵点击树(type == "click" 的数据行数,现为 12)——**防双重计入约束**:
## 点击 L5-12 的 "+1% 产出/s" 已在全局区提升产出/s(从而同时提升军列 DPS 与点击伤害),
## **不得**再把它加进 κ 的分子(systems-spec §1.2 B2 / 第三章 P-10 耦合条)。
func click_line_ratio(econ: EconomyState) -> float:
var owned := 0
var total := 0
for u in data.upgrades:
if u.type != "click":
continue
total += 1
if econ.bought.has(u.id):
owned += 1
if total <= 0:
return 0.0
return float(owned) / float(total)
## 殿王战点击伤害系数 κ(方案 B2;start 时锁定,战斗中不随购买变动)
func _compute_kappa(econ: EconomyState) -> float:
var base := data.tune_num("tune_boss_kappa_base", 0.010)
var slope := data.tune_num("tune_boss_kappa_slope", 0.052)
var m06 := data.meta("meta_06")
var m06_mult := 1.0
if m06 != null:
m06_mult = 1.0 + m06.effect_param * float(int(econ.meta_ranks.get("meta_06", 0)))
return (base + slope * click_line_ratio(econ)) * m06_mult
## 当前可挑战的殿王:已过该殿门 + 未通关 + 不在冷却/进行中
## 原因串要**准确**:RESOLVED(结算面板挂着)与 INTRO/ACTIVE(真在打)是两回事,
## UI 与单测都靠它区分"等玩家点确认"和"别打断战斗",不能一律回"战斗中"。
@@ -119,6 +155,8 @@ func view(econ: EconomyState = null) -> Dictionary:
"timer": timer,
"combo": combo,
"combo_mult": combo_mult(),
"kappa": entry_kappa,
"click_dmg": entry_output * entry_kappa * combo_mult(),
"cooldown": cooldown,
"can_retry": can_retry,
"win": win,
@@ -154,7 +192,9 @@ func start(econ: EconomyState, hall_index: int) -> Dictionary:
def = d
hall_no = hall.no
entry_output = out
entry_click = econ.click_value()
entry_kappa = _compute_kappa(econ)
_intro_enabled = data.tune_num("tune_boss_intro_enabled", 1.0) >= 0.5
_intro_cap = data.tune_num("tune_boss_intro_cap", 0.10)
hp_max = def.hp_at(entry_output)
hp = hp_max
combo = 0
@@ -201,12 +241,14 @@ func tick(dt: float, econ: EconomyState) -> void:
## 玩家挥刃(ACTIVE 阶段有效);返回本次伤害,0 = 未生效
## 方案 B2:click_dmg = 进入时产出/s × κ × combo_mult(点击伤害随产出缩放,
## 修复"点击线在终局归零"的支柱级缺陷 D-2;满配点击占总伤害 ~20.5%)。
func register_click(econ: EconomyState) -> float:
if phase != Phase.ACTIVE or def == null:
return 0.0
combo += 1
combo_left = def.combo_window_sec
var dmg := entry_click * combo_mult()
var dmg := entry_output * entry_kappa * combo_mult()
hp = maxf(0.0, hp - dmg)
if hp <= 0.0:
_resolve(econ, true)
@@ -245,10 +287,12 @@ func reset_for_new_run() -> void:
# ----------------------------------------------------------------------------
func _apply_intro_damage(econ: EconomyState) -> void:
# 开场伤害 = min(超度值 × intro_dmg_souls_factor, HP × intro_dmg_cap_ratio)([B§5])
var raw := float(econ.souls_saved()) * def.intro_dmg_souls_factor
var capv := hp_max * def.intro_dmg_cap_ratio
hp = maxf(0.0, hp - minf(raw, capv))
# 方案 B3(systems-spec §1.2):intro_dmg = HP × intro_cap × clamp(超度值/souls_par, 0, 1)
# 旧式 min(超度×0.5, HP×20%) 实值仅为 HP 的 0.07%-0.83%,"战前攒超度"从未兑现(D-1)。
if not _intro_enabled or _intro_cap <= 0.0:
return
var dmg := hp_max * _intro_cap * def.intro_factor(econ.souls_saved())
hp = maxf(0.0, hp - dmg)
func _resolve(econ: EconomyState, win_flag: bool) -> void:
+83 -15
View File
@@ -48,9 +48,19 @@ gen_balance_sheets.py —— Balance Bible v1.0 → CSV 落表 + build JSON 生
未敕封不可买诏书 → 永不受敕)。GDD §3.4 触发链 = 买诏书 + 再败转轮王
→ 受敕;本版改为 "enthrone_ready"(转世≥2 且 宿慧≥60,[G§3.4] 解锁条件),
gen12 愿景档仍以 enthroned 旗标为最终解锁(EconomyState 闸门规则)
P-13 离线升级 [B§2.4] "每级 ×1.15" 为乘性链(6 级 ×2.31),与三乘区
"加法优先"纪律字面冲突——离线倍率不在三显式乘区内,按 [B] 表值实现,
provenance 标注待裁定
P-13 离线升级 [B§2.4] "每级 ×1.15" 乘性链(6 级 ×2.31)——**W4 已裁定
(systems-spec §2.5 / 第三章 P-13,用户拍板)**:改为每级 +5% 加法
(effect_type offline_add;6 级满 +30%),各档速率硬封顶 100%。
乘性链会使 8h 离线效率 179% > 在线,摧毁在线动机(品类红线)
P-16 ~ P-19(W4 起,方案 B 一组,用户 2026-10-20 拍板):
殿王战 HP = 产出/s × boss_army_seconds(S_n = 23.0→28.0,P-16);
hall_factor 降级为纯叙事参数、值不动(P-17);
点击伤害 = P × κ × combo_mult,κ = (0.010 + 0.052 × 已购点击升级/12)
× (1 + m06阶 × 0.30),满配 κ = 0.0806(P-18);
开场冲锋 = HP × 0.10 × clamp(超度/souls_par, 0, 1)(P-19)。
全部依据 design/gdd/2026-10-20-F-systems-spec.md §1.2
P-24(W4 补登)W3 的 7 行 balance_tuning(相机 5 行 + boss_preview/event_first)
原为 CSV 手改、生成器漏登——本次一并补登,防重跑丢行
P-14 tune_souls_div=10000 新增([B§6]"超度 = floor(本轮累计阴德÷10,000)",
叙事计数器不设闸门);GDD §4.1 的超度事件计数口路由 W3 事件系统承接
P-15 法器附加(点击 L5-12,CSV click_output_pct)乘区口径:[B§1] 总公式与
@@ -119,9 +129,19 @@ HALLS = [
("hall_10_zhuanlun",10, "转轮王", "King of the Wheel", "轮回道(六道环)","Wheel of Rebirth", 180, 1500000000, "none", "none", "pal_hall_10", "true"),
]
# [B§5] 殿系数 H1-H10
# [B§5] 殿系数 H1-H10(**W4 起降级为纯叙事/视觉参数,不参与伤害计算**;
# 计算改由 boss_army_seconds 承担,systems-spec §1.2 B1 / 第五章差异 #2)
BOSS_FACTORS = [1.0, 1.1, 1.2, 1.3, 1.4, 1.6, 1.8, 2.0, 2.1, 2.2]
# [systems-spec §1.2 B1] 军列单推秒数 S_n(方案 B 重参数化):
# HP = 进入时产出/s × boss_army_seconds;军列 DPS = 产出/s × 1.0
# ⇒ 纯军列单推耗时 = S_n 秒(恒等式,与玩家强度脱钩的问题在结构上不可能复发)
BOSS_ARMY_SECONDS = [23.0, 23.5, 24.0, 24.5, 25.0, 25.5, 26.0, 26.5, 27.0, 28.0]
# [systems-spec §1.2 B3] 开场冲锋超度基准(intro_dmg = HP × 0.10 × clamp(超度值/基准, 0, 1))
# 初值 = 该殿典型超度值 × 60%;H1 = 0 ⇒ 该殿无开场加成(防 0 除)
SOULS_PAR = [0, 2, 18, 120, 600, 2400, 7200, 21000, 48000, 90000]
# ---------------------------------------------------------------------------
# [B§2.1] 效率升级 72 条(12 班底 × 6 级)
# 效果:该班底产出 +100%(加法,进班底乘区;6 级满 = ×7 < ×8 软上限)
@@ -154,7 +174,9 @@ SYSTEM_GATES = [
("up_gate_enthrone", "开通 · 敕封预告", "Unlock: Enthronement Preview", "hall_09_pingdeng", 400000000),
]
# [B§2.4] 离线升级 6 条(×1.15/级)
# [systems-spec §2.5 / 第三章 P-13] 离线升级 6 条:每级 +5% **加法**
# (直接加在衰减表各档速率上,各档硬封顶 100%;替代 [B§2.4] 的 ×1.15 乘性链。
# 乘性链 6 级 = ×2.31 会使 8h 离线效率达 179% > 在线,摧毁在线动机——品类红线)
OFFLINE_COSTS = [2000, 20000, 200000, 2000000, 20000000, 200000000]
# ---------------------------------------------------------------------------
@@ -213,6 +235,19 @@ TUNING = [
("tune_event_timeout_default","text", 0, "opt_a", "[B§7] 冤魂申诉超时默认 = 超度(opt_a)"),
("tune_souls_div", "number", 10000, "", "[B§6] 超度亡魂 = floor(本轮累计阴德 ÷ 10,000)(叙事计数器,不设闸门;P-14)"),
("tune_enthrone_suihui_req","number", 60, "", "[G§3.4] 阴司敕封解锁条件:宿慧 ≥60([B§4.4] 仅给 8-12h 体感,采信 GDD 数值)"),
# ---- W3 增补行(原为 CSV 手改、生成器漏登;W4 补登,防重跑丢行,见 provenance P-20) ----
("tune_cam_peek_max_px", "number", 320, "", "[W3 相机] 偷看(peek)偏移的世界像素上限;超出即夹到此值(架构 §4.3 偷看回弹)"),
("tune_cam_peek_spring", "number", 42, "", "[W3 相机] 偷看回弹弹簧刚度 k(越大回弹越快)"),
("tune_cam_peek_damp", "number", 11, "", "[W3 相机] 偷看回弹阻尼 c;近临界阻尼约 2*sqrt(k)≈13,取略欠阻尼以保留手感"),
("tune_cam_follow_tau", "number", 0.35, "", "[W3 相机] 进度锚定跟随的指数平滑时间常数(秒)"),
("tune_cam_zoom_tau", "number", 0.3, "", "[W3 相机] 缩放与殿王战推近的指数平滑时间常数(秒)"),
("tune_boss_preview_min", "number", 15, "", "[B§6] 殿王战战前预告提前分钟数(与 H9-H10 prestige 预告同口径)"),
("tune_event_first_min", "number", 3, "", "[B§7] 事件系统解锁后首个事件的延迟分钟数(给玩家缓冲,避免刚解锁就来)"),
# ---- W4 方案 B 调参(systems-spec §1.2;用户 2026-10-20 拍板) ----
("tune_boss_kappa_base", "number", 0.01, "", "[systems-spec §1.2 B2] 殿王战点击伤害 κ 基数(零点击投资时 κ = 0.010;W4 方案 B)"),
("tune_boss_kappa_slope", "number", 0.052, "", "[systems-spec §1.2 B2] κ 随法器线进度斜率:κ = (base + slope × 已购点击升级/12) × (1 + m06阶×0.30);满配 κ = 0.0806"),
("tune_boss_intro_cap", "number", 0.1, "", "[systems-spec §1.2 B3] 开场冲锋伤害封顶 = HP × 本值(0.10;旧公式 20% 已废止)"),
("tune_boss_intro_enabled","number", 1, "", "[systems-spec §1.2 B3] 开场冲锋开关(1 = 启用;门 #11 首轮数据回来前默认 1)"),
]
# ---------------------------------------------------------------------------
@@ -274,8 +309,8 @@ UPGRADE_COLS = ["id", "name_cn", "name_en", "type", "target_gen", "unlock_hall",
PALETTE_COLS = ["id", "hall_id", "name_cn", "ink_hex", "bone_hex", "cinnabar_hex",
"indigo_hex", "accent_hex", "accent_name_cn", "flow", "lut_index",
"ink_pct", "bone_pct", "cinnabar_max_pct", "indigo_max_pct"]
BOSS_COLS = ["id", "hall_id", "hall_factor", "hp_factor", "army_dps_factor",
"intro_sec", "intro_dmg_souls_factor", "intro_dmg_cap_ratio",
BOSS_COLS = ["id", "hall_id", "hall_factor", "boss_army_seconds", "souls_par",
"army_dps_factor", "intro_sec",
"active_sec", "combo_window_sec", "combo_step", "combo_cap",
"retry_cooldown_sec", "reward_hp_ratio"]
EVENT_COLS = ["id", "name_cn", "name_en", "trigger_min_min", "trigger_max_min",
@@ -370,9 +405,9 @@ def build_rows():
"name_cn": "离线收益 · 沉淀 %s" % ROMAN[i],
"name_en": "Offline Income · Settling %s" % ROMAN[i],
"type": "offline", "target_gen": "none", "unlock_hall": "hall_04_wuguan",
"effect_type": "offline_mult", "effect_param": 1.15, "requires": "none",
"effect_type": "offline_add", "effect_param": 0.05, "requires": "none",
"cost": OFFLINE_COSTS[i],
"desc_cn": "离线效率 ×1.15(作用于衰减表全部时段;6 级满 ×2.31)",
"desc_cn": "离线效率 +5%(加法,直接加在衰减表各档速率上;6 级满 +30%,各档硬封顶 100%)",
})
sheets["upgrades"] = (UPGRADE_COLS, rows)
@@ -404,14 +439,21 @@ def build_rows():
})
sheets["hall_palettes"] = (PALETTE_COLS, rows)
# --- boss_battles([B§5]) -------------------------------------------------
# --- boss_battles([B§5] + systems-spec §1.2 方案 B 重参数化) --------------
# hall_factor 保留但降级为纯叙事/视觉参数(血条观感与 UI 文案),不参与伤害计算;
# HP = 进入时产出/s × boss_army_seconds;开场冲锋 = HP×0.10×clamp(超度/souls_par)
# (cap 与开关走 balance_tuning:tune_boss_intro_cap / tune_boss_intro_enabled)。
# 已删除列:hp_factor(旧 HP 公式系数,方案 B 后无意义——留列即成误导性死旋钮)、
# intro_dmg_souls_factor / intro_dmg_cap_ratio(旧开场公式参数,被上式取代)。
rows = []
for i, (hid, no, *_rest) in enumerate(HALLS):
rows.append({
"id": "boss_hall_%02d" % no, "hall_id": hid,
"hall_factor": BOSS_FACTORS[i],
"hp_factor": 15, "army_dps_factor": 1.0,
"intro_sec": 3, "intro_dmg_souls_factor": 0.5, "intro_dmg_cap_ratio": 0.2,
"boss_army_seconds": BOSS_ARMY_SECONDS[i],
"souls_par": SOULS_PAR[i],
"army_dps_factor": 1.0,
"intro_sec": 3,
"active_sec": 30, "combo_window_sec": 0.6, "combo_step": 0.02,
"combo_cap": 0.6, "retry_cooldown_sec": 60, "reward_hp_ratio": 0.2,
})
@@ -503,12 +545,28 @@ def self_check(sheets) -> None:
for g in gens:
if g["unlock_hall"] != "none":
assert g["unlock_hall"] in hall_ids, "dangling hall %s" % g["unlock_hall"]
# boss_battles 10 行 + [B§5] 殿系数
# boss_battles 10 行 + 方案 B 参数(systems-spec §1.2;用户 2026-10-20 拍板)
bosses = sheets["boss_battles"][1]
assert len(bosses) == 10
for b, h in zip(bosses, halls):
expect_seconds = [23.0, 23.5, 24.0, 24.5, 25.0, 25.5, 26.0, 26.5, 27.0, 28.0]
expect_par = [0, 2, 18, 120, 600, 2400, 7200, 21000, 48000, 90000]
for b, h, s, p in zip(bosses, halls, expect_seconds, expect_par):
assert b["hall_id"] == h["id"]
assert b["hp_factor"] == 15 # [B§11 差异 #4]:50→15
assert b["boss_army_seconds"] == s, "S_n %s" % b["id"] # 军列单推秒数
assert b["souls_par"] == p, "souls_par %s" % b["id"] # 超度基准
assert b["active_sec"] == 30 # 恒 30s 不动
# S_n 单调不降(爬坡 23→28)+ 全殿 < 30s(十殿全可胜,D-0 修复)
for i in range(1, len(expect_seconds)):
assert expect_seconds[i] > expect_seconds[i - 1], "S_n 单调性 @H%d" % (i + 1)
assert max(expect_seconds) < 30, "S_n 必须 < active_sec(无必败殿)"
# hall_factor 降级为纯叙事参数:原值 1.0→2.2 一字不改(systems-spec 第五章 #2)
for b, hf in zip(bosses, BOSS_FACTORS):
assert b["hall_factor"] == hf, "hall_factor 叙事值不动 %s" % b["id"]
# 长明灯加法口径(systems-spec §2.5 / P-13):6 × 0.05 = +30%
offs = [u for u in ups if u["type"] == "offline"]
assert len(offs) == 6
for u in offs:
assert u["effect_param"] == 0.05, "长明灯加法每级 +0.05 %s" % u["id"]
# 冤煞成本公式 spot check(round(8×n^1.8) 由 core 代码求值,此处验公式输入)
tune = {t["id"]: t for t in sheets["balance_tuning"][1]}
assert tune["tune_wrath_layers"]["num"] == 22
@@ -531,6 +589,16 @@ def self_check(sheets) -> None:
assert tune["tune_global_zone_cap"]["num"] == 64
assert tune["tune_restart_curve"]["text"] == "1:0.6:0.4:0.3"
assert tune["tune_event_timeout_default"]["text"] == "opt_a"
# W4 方案 B 调参 4 行(systems-spec §1.2;用户 2026-10-20 拍板)
assert tune["tune_boss_kappa_base"]["num"] == 0.01
assert tune["tune_boss_kappa_slope"]["num"] == 0.052
assert tune["tune_boss_intro_cap"]["num"] == 0.1
assert tune["tune_boss_intro_enabled"]["num"] == 1
# 离线衰减表一字不改(systems-spec §2.4:错的是契约不是表)
assert tune["tune_offline_band1_rate"]["num"] == 1.0
assert tune["tune_offline_band2_rate"]["num"] == 0.7
assert tune["tune_offline_band3_rate"]["num"] == 0.4
assert tune["tune_offline_band4_rate"]["num"] == 0.1
# meta 12 条
metas = sheets["meta_upgrades"][1]
assert len(metas) == 12
+6 -1
View File
@@ -19,7 +19,12 @@ func run(a, data) -> void:
a.near(data.hall_by_no(1).gate_merit, 2000.0, 0.5, "H1 门 2K")
a.near(data.hall_by_no(10).gate_merit, 1.5e9, 0.5, "H10 门 1.5B")
a.true_(data.hall_by_no(10).is_prestige_gate, "H10 = prestige 门")
a.near(data.boss_for_hall("hall_01_qinguang").hp_factor, 15.0, 1e-9, "殿王 HP 系数 15([B§5])")
a.near(data.boss_for_hall("hall_01_qinguang").boss_army_seconds, 23.0, 1e-9,
"H1 殿王军列单推秒数 S_1 = 23(方案 B;旧 HP 系数 15 已废止)")
a.near(data.boss_for_hall("hall_10_zhuanlun").boss_army_seconds, 28.0, 1e-9,
"H10 殿王军列单推秒数 S_10 = 28")
a.eq(data.boss_for_hall("hall_01_qinguang").souls_par, 0, "H1 超度基准 0(无开场加成)")
a.eq(data.boss_for_hall("hall_10_zhuanlun").souls_par, 90000, "H10 超度基准 90000")
# 敕封诏书(P-12:requires = enthrone_ready)
var edict := data.upgrade("up_gate_tianzi")
a.true_(edict != null, "敕封诏书存在")
+235 -62
View File
@@ -1,16 +1,24 @@
extends RefCounted
# 用例:W3 殿王战逻辑状态机(game/BossBattle;boss_battles.csv 十场)
# 口径全部来自数据行([B§5]):HP = 15 × 进入时产出/s × 殿系数(1.0→2.2)。
# 本类不用随机 → 结果可复现(W4 起 sim 门 #11 直接复用)。
# 用例:W4 殿王战逻辑状态机(game/BossBattle;boss_battles.csv 十场;方案 B)
# 口径全部来自数据行(systems-spec §1.2,用户 2026-10-20 拍板):
# HP = 进入时产出/s × boss_army_seconds(S_n);点击伤害 = 产出/s × κ × combo_mult;
# 开场冲锋 = HP × 0.10 × clamp(超度/souls_par, 0, 1)。
# 本类不用随机 → 结果可复现(W4 sim 门 #11 直接复用)。
#
# W4 变更说明:原"可胜性事实记录"哨兵(8 座可胜 / 2 座必超时 / 点击天花板 25.6)
# 钉的是旧 HP 公式下的**缺陷事实**;方案 B 落地后按 systems-spec 第七章第 1 条
# 重写为"十殿全可胜 + S_n + κ + 修订门 #11 五判据"的**设计意图断言**。
func run(a, data) -> void:
_test_hp_formula(a, data)
_test_win(a, data)
_test_intro_damage(a, data)
_test_combo_cap(a, data)
_test_lose_and_retry(a, data)
_test_availability(a, data)
_test_winnability_doc(a, data)
_test_kappa_scaling(a, data)
_test_winnability_b(a, data)
func _new_econ(data, merit_run: float, gen0_level: int = 10) -> EconomyState:
@@ -21,31 +29,83 @@ func _new_econ(data, merit_run: float, gen0_level: int = 10) -> EconomyState:
return e
# ---- HP = 15 × 产出/s × 殿系数([B§5],GDD 旧值 50× 已废止) ----
## 满配点击:12 条点击升级全购 + m06 斩鬼刃 1 阶(直接置位 + recompute,同旧用例对
## up_gate_boss_retry 的手法;购买合法性由 test_economy_state 覆盖)。
func _full_click_config(e: EconomyState, data) -> void:
for u in data.upgrades:
if u.type == "click":
e.bought[u.id] = true
e.meta_ranks["meta_06"] = 1
e.recompute()
## 战斗模拟器(dt=0.1;click_every = 点击间隔秒,0 = 挂机;keep_alive = 每 tick 前补满 HP,
## 防止 dummy 提前死亡,用于测满时长内的军列+点击伤害容量)。返回实测字典。
func _run_battle(b: BossBattle, e: EconomyState, max_sec: float,
click_every: float = 0.0, keep_alive: bool = false) -> Dictionary:
var out := {"win": false, "active_sec": 0.0, "army_dmg": 0.0,
"click_dmg": 0.0, "clicks": 0}
var dt := 0.1
var steps := int(round(max_sec / dt))
var click_acc := 0.0
for _i in steps:
if b.phase == BossBattle.Phase.RESOLVED:
break
if keep_alive:
b.hp = b.hp_max
b.tick(dt, e)
if b.phase == BossBattle.Phase.ACTIVE:
out.army_dmg += e.output_per_sec() * dt
out.active_sec += dt
if click_every > 0.0:
click_acc += dt
while click_acc >= click_every - 1e-9:
click_acc -= click_every
var dmg := b.register_click(e)
out.click_dmg += dmg
out.clicks += 1
if keep_alive and b.phase == BossBattle.Phase.RESOLVED:
# 测试手法:点击击杀 dummy 后拉回 ACTIVE 续打(只动 phase/hp,
# 不动任何公式;save_soul 的阴德副作用对本断言无影响)。
b.phase = BossBattle.Phase.ACTIVE
b.win = false
b.hp = b.hp_max
out.win = bool(b.view().get("win", false))
return out
# ---- HP = 产出/s × boss_army_seconds(方案 B1;hall_factor 降级为纯叙事参数) ----
func _test_hp_formula(a, data) -> void:
var e := _new_econ(data, 2000.0) # 过 H1
a.eq(e.halls_passed, 1, "H1 门达成(2000 阴德)")
var out := e.output_per_sec()
a.near(out, 1.0, 1e-6, "基线产出 1.0/s")
var expect_s := [23.0, 23.5, 24.0, 24.5, 25.0, 25.5, 26.0, 26.5, 27.0, 28.0]
var expect_h := [1.0, 1.1, 1.2, 1.3, 1.4, 1.6, 1.8, 2.0, 2.1, 2.2]
for i in data.halls.size():
var h: HallDef = data.halls[i]
var def := data.boss_for_hall(h.id)
a.true_(def != null, "殿 %s 有 boss 行" % h.id)
if def == null:
continue
a.near(float(def.hp_factor), 15.0, 1e-9, "%s:hp_factor = 15" % h.id)
a.near(def.hp_at(out), 15.0 * out * float(def.hall_factor), 1e-6,
"%s:HP = 15 × 产出/s × 殿系数" % h.id)
# H1 殿系数 1.0 → HP = 15;H10 = 2.2 → 2.2 倍
a.near(def.boss_army_seconds, expect_s[i], 1e-9,
"%s:S_n = %.1f(军列单推秒数)" % [h.id, expect_s[i]])
a.near(def.hp_at(out), expect_s[i] * out, 1e-6,
"%s:HP = 产出/s × S_n" % h.id)
a.near(float(def.hall_factor), expect_h[i], 1e-9,
"%s:hall_factor = %.1f(纯叙事/视觉参数,值不动)" % [h.id, expect_h[i]])
a.true_(def.boss_army_seconds < float(def.active_sec),
"%s:S_n < active_sec(无必败殿)" % h.id)
# H1:产出 1/s → HP 23;H10:产出 1/s → HP 28(旧式 15/33 已废止)
var d1 := data.boss_for_hall("hall_01_qinguang")
var d10 := data.boss_for_hall("hall_10_zhuanlun")
a.near(float(d1.hall_factor), 1.0, 1e-9, "H1 殿系数 1.0")
a.near(float(d10.hall_factor), 2.2, 1e-9, "H10 殿系数 2.2")
a.near(d1.hp_at(out), 15.0, 1e-6, "H1:产出 1/s → HP 15")
a.near(d10.hp_at(out), 33.0, 1e-6, "H10:产出 1/s → HP 33")
a.near(d1.hp_at(out), 23.0, 1e-6, "H1:产出 1/s → HP 23")
a.near(d10.hp_at(out), 28.0, 1e-6, "H10:产出 1/s → HP 28")
# HP 与 hall_factor 无关(叙事参数不参与计算的证明)
a.near(d10.hp_at(100.0), 2800.0, 1e-6, "H10:产出 100/s → HP 2800(不含殿系数 2.2)")
# ---- 军列 DPS = 产出/s × 1.0;30s 内打完 → 胜 + 阴德爆发 HP×0.2 ----
# ---- 军列 DPS = 产出/s × 1.0;S_n 秒打完 → 胜 + 阴德爆发 HP×0.2 ----
func _test_win(a, data) -> void:
var e := _new_econ(data, 2000.0)
var b := BossBattle.new(data)
@@ -54,25 +114,25 @@ func _test_win(a, data) -> void:
a.eq(int(info.get("hall_index", -1)), 0, "可挑战的殿 index = 0")
var r := b.start(e, 0)
a.true_(bool(r.get("ok", false)), "开战")
a.near(float(r.get("hp_max", 0.0)), 15.0, 1e-6, "进入时锁定 HP = 15")
a.near(float(r.get("hp_max", 0.0)), 23.0, 1e-6, "进入时锁定 HP = 23(P=1/s × S_1=23)")
a.eq(b.phase, BossBattle.Phase.INTRO, "先走 INTRO(3s 军列冲锋)")
# 超度值为 0 → 开场伤害 0(min(0×0.5, HP×0.2))
a.near(b.hp, 15.0, 1e-6, "开场伤害 = 0(超度值 0)")
# 超度值为 0 且 H1 souls_par = 0 → 开场伤害 0
a.near(b.hp, 23.0, 1e-6, "开场伤害 = 0(超度值 0 / H1 基准 0)")
# 走完 INTRO
for _i in 31:
b.tick(0.1, e)
a.eq(b.phase, BossBattle.Phase.ACTIVE, "3s 后进入 ACTIVE")
a.near(float(b.view().get("timer", 0.0)), 30.0, 1.0, "ACTIVE 计时 30s 起步")
# 军列 DPS = 1.0 → 15s 打完
# 军列 DPS = 1.0 → 23s 打完(S_1)
var before := e.merit_run_total
for _i in 152:
for _i in 240:
b.tick(0.1, e)
if b.phase == BossBattle.Phase.RESOLVED:
break
a.eq(b.phase, BossBattle.Phase.RESOLVED, "HP 归零 → RESOLVED")
a.true_(bool(b.view().get("win", false)), "判定为胜")
a.near(float(b.view().get("reward", 0.0)), 15.0 * 0.2, 1e-6, "奖励 = HP × 0.2 = 3")
a.near(e.merit_run_total, before + 15.0 * 0.2, 1e-6, "阴德爆发进本轮累计口径")
a.near(float(b.view().get("reward", 0.0)), 23.0 * 0.2, 1e-6, "奖励 = HP × 0.2 = 4.6")
a.near(e.merit_run_total, before + 23.0 * 0.2, 1e-6, "阴德爆发进本轮累计口径")
# RESOLVED(结算面板挂着)与"真在打"要能区分:原因串不许一律是"战斗中"
var during: Dictionary = b.available(e)
a.false_(bool(during.get("ok", false)), "结算未确认 → 不可开新战")
@@ -86,10 +146,40 @@ func _test_win(a, data) -> void:
a.true_(bool(b.available(e).get("ok", false)), "新轮回后重新可挑战")
# ---- 连击:+2%/层,封顶 +60% ----
# 注意:本用例必须让 HP 大到"100 刀砍不死",否则第 14 刀就把 boss 打死(HP=15×产出/s,
# 而点击价值恒为 1.0),战斗提前进 RESOLVED,连击永远涨不到封顶 —— 这是用例设计陷阱,
# 不是被测代码的 bug。故这里把军列拉到 200 级(产出 20/s → HP 300 >> 100 刀总伤 ≈151)。
# ---- 开场冲锋 v2(方案 B3):HP × 0.10 × clamp(超度/souls_par, 0, 1) ----
func _test_intro_damage(a, data) -> void:
a.near(data.tune_num("tune_boss_intro_cap", 0.0), 0.10, 1e-9, "intro_cap = 0.10(旧 20% 已废止)")
a.near(data.tune_num("tune_boss_intro_enabled", 0.0), 1.0, 1e-9, "intro_enabled = 1(默认启用)")
# H5(souls_par = 600):超度 600 → clamp = 1 → 开场伤害 = 10% HP
var e := _new_econ(data, 6000000.0) # 超度 = floor(6e6/1e4) = 600
a.eq(e.souls_saved(), 600, "超度值 600")
var b := BossBattle.new(data)
var r := b.start(e, 4) # H5 殿王
a.true_(bool(r.get("ok", false)), "H5 开战")
a.near(float(r.get("hp_max", 0.0)), 25.0, 1e-6, "H5 HP = 25(P=1/s × S_5=25)")
for _i in 31:
b.tick(0.1, e)
a.eq(b.phase, BossBattle.Phase.ACTIVE, "进入 ACTIVE")
a.near(b.hp, 25.0 * 0.9, 1e-6, "开场伤害 = HP × 10% × clamp(600/600) = 2.5")
# 超度 300 → clamp = 0.5 → 开场伤害 = 5% HP
var e2 := _new_econ(data, 3000000.0)
var b2 := BossBattle.new(data)
b2.start(e2, 4)
for _i in 31:
b2.tick(0.1, e2)
a.near(b2.hp, 25.0 * 0.95, 1e-6, "开场伤害 = HP × 10% × clamp(300/600) = 1.25")
# H1(souls_par = 0)→ 无开场加成(防 0 除),即使超度很大
var e3 := _new_econ(data, 6000000.0)
var b3 := BossBattle.new(data)
b3.start(e3, 0)
for _i in 31:
b3.tick(0.1, e3)
a.near(b3.hp, 23.0, 1e-6, "H1 基准 0 → 开场伤害 0")
# ---- 连击:+2%/层,封顶 +60%;点击伤害 = P × κ × 连击 ----
# 用例前提:HP 大到"100 刀砍不死"。κ(零投资) = 0.010,P = 20/s(军列 200 级)
# ⇒ HP = 23 × 20 = 460 >> 100 刀总伤 ≈ 20 × 0.010 × ~130 ≈ 26。
func _test_combo_cap(a, data) -> void:
var e := _new_econ(data, 2000.0, 200)
var b := BossBattle.new(data)
@@ -97,12 +187,14 @@ func _test_combo_cap(a, data) -> void:
for _i in 31:
b.tick(0.1, e)
a.eq(b.phase, BossBattle.Phase.ACTIVE, "进入 ACTIVE")
a.true_(b.hp_max > 100.0 * e.click_value() * 1.6,
a.near(b.entry_kappa, 0.010, 1e-9, "零投资 κ = 0.010(tune_boss_kappa_base)")
a.true_(b.hp_max > 100.0 * e.output_per_sec() * b.entry_kappa * 1.6,
"用例前提:HP 足够撑过 100 刀(HP=%s)" % NumberFormat.short(b.hp_max))
a.near(b.combo_mult(), 1.0, 1e-9, "初始连击 ×1.0")
var dmg1 := b.register_click(e) # 第 1 刀:1 层 → ×1.02
a.eq(b.combo, 1, "连击计数 1")
a.near(b.combo_mult(), 1.02, 1e-6, "1 层 → +2%")
a.near(dmg1, e.click_value() * 1.02, 1e-6, "伤害 = 点击价值 × 连击")
a.near(dmg1, e.output_per_sec() * 0.010 * 1.02, 1e-6, "伤害 = 产出/s × κ × 连击")
# 连点 100 下(窗口内)→ 封顶 +60%
for _i in 99:
b.register_click(e)
@@ -173,39 +265,120 @@ func _test_availability(a, data) -> void:
a.false_(bool(b2.start(e2, 99).get("ok", false)), "殿序越界被拒")
# ---- 可胜性结构(**事实记录**,不是"应当如此"的断言) ----
# 推导(与产出/s 无关,故对任何存档都成立):
# 纯军列耗时 = HP ÷ (产出/s × army_dps_factor)
# = (hp_factor × 产出/s × hall_factor) ÷ (产出/s × army_dps_factor)
# = hp_factor × hall_factor ÷ army_dps_factor ← 产出/s 被约掉了
# 于是:殿系数 > active_sec ÷ (hp_factor ÷ army_dps_factor) = 30 ÷ 15 = 2.0 的殿,
# 军列单推必定超时;而缺口 ∝ 产出/s,点击伤害却是常数(不随产出/s 增长)
# → **玩家越强,缺口越大,越打不过**。这是 W3 交付时点名上报文策渊的平衡缺陷,
# 本用例把当前数据的事实钉死:一旦有人改 hall_factor / active_sec / hp_factor,
# 这里会立刻变红,提醒重新评估可胜性。修数值的权力在文策渊,不在本文件。
func _test_winnability_doc(a, data) -> void:
var ratio: float = float(data.bosses[0].hp_factor) / float(data.bosses[0].army_dps_factor)
a.near(ratio, 15.0, 1e-9, "hp_factor ÷ army_dps_factor = 15(纯军列耗时的基准秒数)")
var hard: Array = []
var easy: Array = []
for d in data.bosses:
var t := float(d.hp_factor) * float(d.hall_factor) / float(d.army_dps_factor)
var label := "%s(%.1fs)" % [d.hall_id, t]
if t > float(d.active_sec):
hard.append(label)
else:
easy.append(label)
# 事实:殿系数 ≤2.0 的殿(H1-H8)军列可胜;>2.0 的殿(H9/H10)必超时
a.eq(easy.size(), 8, "军列单推可胜的殿 = 8 座:%s" % ", ".join(easy))
a.eq(hard.size(), 2, "军列单推必超时的殿 = 2 座:%s(上报文策渊的平衡缺陷)" % ", ".join(hard))
# 点击侧天花板(常数,不随产出/s 增长)—— 后期补不上缺口的根源。
# 2^升级数 不写死:从 upgrades.csv 里数 click_mult 型的行数(数据行纪律)。
var click_up_count := 0
# ---- 点击伤害缩放 κ(方案 B2):零投资 0.010 / 满配 0.0806 / 分母 12 / DPS 占比 20.5% ----
func _test_kappa_scaling(a, data) -> void:
# 点击树 = 12 条(type == "click" 的数据行数;防双重计入:L5-12 的 +1% 产出/s 走全局区,
# 不进 κ 分子——见 economy_state.recompute 注释)
var click_total := 0
for u in data.upgrades:
if u.effect_type == "click_mult":
click_up_count += 1
var click_cap := data.tune_num("tune_click_base", 1.0) \
* pow(2.0, float(click_up_count)) * (1.0 + float(data.bosses[0].combo_cap))
a.near(click_cap, 25.6, 1e-6,
"点击单次伤害天花板 = 基数1 × 2^%d(点击升级) × 1.6(连击封顶) = 25.6(常数)" % click_up_count)
# 若哪天把点击伤害改成随产出/s 增长,上面这条会红 → 说明可胜性结构已变,需重评
if u.type == "click":
click_total += 1
a.eq(click_total, 12, "点击树 12 条(κ 的 click_ratio 分母)")
# 零投资
var e := _new_econ(data, 2000.0)
var b := BossBattle.new(data)
b.start(e, 0)
a.near(b.entry_kappa, 0.010, 1e-9, "零投资 κ = 0.010")
a.near(b.click_line_ratio(e), 0.0, 1e-9, "法器线进度 0/12")
# 半线(6/12,无 m06):κ = 0.010 + 0.052×0.5 = 0.036
var e2 := _new_econ(data, 2000.0)
var n := 0
for u in data.upgrades:
if u.type == "click":
e2.bought[u.id] = true
n += 1
if n >= 6:
break
e2.recompute()
var b2 := BossBattle.new(data)
b2.start(e2, 0)
a.near(b2.click_line_ratio(e2), 0.5, 1e-9, "法器线进度 6/12 = 0.5")
a.near(b2.entry_kappa, 0.036, 1e-9, "半线 κ = 0.036")
# 满配(12/12 + m06):κ = 0.062 × 1.30 = 0.0806
var e3 := _new_econ(data, 2000.0)
_full_click_config(e3, data)
var b3 := BossBattle.new(data)
b3.start(e3, 0)
a.near(b3.click_line_ratio(e3), 1.0, 1e-9, "法器线进度 12/12 = 1.0")
a.near(b3.entry_kappa, 0.0806, 1e-9, "满配 κ = (0.010+0.052) × 1.30 = 0.0806")
# 封顶连击下的点击 DPS 占比 = 2κ×1.6 / (1 + 2κ×1.6) = 20.5%(spec §1.3 的"兵七成我三成")
var kappa := 0.0806
var click_dps := 2.0 * kappa * 1.6 # 2 次/s × κ × 连击封顶
a.near(click_dps / (1.0 + click_dps), 0.205, 0.005,
"封顶连击点击 DPS 占比 = 20.5%(军列 79.5% / 点击 20.5%)")
# m06 单独验证:0 阶 → ×1.0;1 阶 → ×1.30
var e4 := _new_econ(data, 2000.0)
for u in data.upgrades:
if u.type == "click":
e4.bought[u.id] = true
e4.recompute()
var b4 := BossBattle.new(data)
b4.start(e4, 0)
a.near(b4.entry_kappa, 0.062, 1e-9, "12/12 无 m06 → κ = 0.062")
# ---- 可胜性结构(方案 B 后重写;替代旧"8 胜 / 2 必超时 / 天花板 25.6"事实哨兵) ----
# 断言 = systems-spec §1.4 修订门 #11 五判据 + 用户 W4 要求:
# a) 十殿胜率 100%(挂机单推即过) b) 挂机余时 ∈ [2,8]s
# c) 满配余时 ∈ [7,12]s d) 余时差(满配−挂机)≥ 4s
# e) 点击伤害占比 ∈ [15%,25%] + 满配 30s 容量:军列+点击 ≥ HP
func _test_winnability_b(a, data) -> void:
var expect_s := [23.0, 23.5, 24.0, 24.5, 25.0, 25.5, 26.0, 26.5, 27.0, 28.0]
for i in data.halls.size():
var h: HallDef = data.halls[i]
var s_n: float = expect_s[i]
# --- 挂机单推(0 点击;merit_run = 0 ⇒ 超度 0 ⇒ 开场 0,纯军列口径) ---
var e_idle := _new_econ(data, 0.0, 1000) # 产出 100/s
var b_idle := BossBattle.new(data)
var ri: Dictionary = b_idle.start(e_idle, i)
a.true_(bool(ri.get("ok", false)), "%s:挂机开局" % h.id)
var ri_run := _run_battle(b_idle, e_idle, 31.0, 0.0)
a.true_(ri_run.win, "%s:挂机单推可胜(十殿全可胜,a 判据)" % h.id)
a.near(ri_run.active_sec, s_n, 0.15,
"%s:挂机耗时 = S_n = %.1fs(恒等式)" % [h.id, s_n])
a.true_(ri_run.active_sec < 30.0, "%s:挂机不超时(富余 %.1fs)" % [h.id, 30.0 - s_n])
var idle_left := 30.0 - ri_run.active_sec
a.true_(idle_left >= 2.0 - 0.15 and idle_left <= 8.0 + 0.15,
"%s:挂机余时 %.1fs ∈ [2,8](b 判据)" % [h.id, idle_left])
# --- 满配点击(12/12 + m06,2 次/s) ---
var e_full := _new_econ(data, 0.0, 1000)
_full_click_config(e_full, data)
var b_full := BossBattle.new(data)
b_full.start(e_full, i)
var rf := _run_battle(b_full, e_full, 31.0, 0.5)
a.true_(rf.win, "%s:满配点击可胜" % h.id)
var full_left := 30.0 - rf.active_sec
a.true_(full_left >= 7.0 and full_left <= 12.0,
"%s:满配余时 %.1fs ∈ [7,12](c 判据)" % [h.id, full_left])
var margin := full_left - idle_left
a.true_(margin >= 4.0,
"%s:余时差 %.1fs ≥ 4s(d 判据:点击线有决定性意义)" % [h.id, margin])
var share := rf.click_dmg / float(b_full.hp_max)
a.true_(share >= 0.15 and share <= 0.25,
"%s:点击伤害占比 %.1f%% ∈ [15%%,25%%](e 判据)" % [h.id, share * 100.0])
# --- 满配 30s 伤害容量:军列 + 点击 ≥ HP(keep_alive 实测,无提前死亡) ---
var e_cap := _new_econ(data, 0.0, 1000)
_full_click_config(e_cap, data)
var b_cap := BossBattle.new(data)
b_cap.start(e_cap, i)
var rc := _run_battle(b_cap, e_cap, 30.0, 0.5, true)
var cap_ratio := (rc.army_dmg + rc.click_dmg) / float(b_cap.hp_max)
a.true_(cap_ratio >= 1.0,
"%s:30s 军列+点击容量 = HP × %.2f ≥ 1(余量充足)" % [h.id, cap_ratio])
# 挂机容量同样 ≥ 1(纯军列 30s 也能磨死,只是压哨)
var e_cap2 := _new_econ(data, 0.0, 1000)
var b_cap2 := BossBattle.new(data)
b_cap2.start(e_cap2, i)
var rc2 := _run_battle(b_cap2, e_cap2, 30.0, 0.0, true)
var cap_ratio2 := rc2.army_dmg / float(b_cap2.hp_max)
a.true_(cap_ratio2 >= 1.0,
"%s:30s 纯军列容量 = HP × %.2f ≥ 1" % [h.id, cap_ratio2])
# H10 点击占比 ≈ 20.5% ±3pp(spec 表头值;实测为累计口径 ~18%,见 provenance P-25 说明)
var e10 := _new_econ(data, 0.0, 1000)
_full_click_config(e10, data)
var b10 := BossBattle.new(data)
b10.start(e10, 9)
var r10 := _run_battle(b10, e10, 31.0, 0.5)
var share10 := r10.click_dmg / float(b10.hp_max)
a.true_(share10 >= 0.175 and share10 <= 0.235,
"H10:满配点击占总伤害 %.1f%% ≈ 20.5%%(容差 ±3 个百分点)" % (share10 * 100.0))
+28 -8
View File
@@ -40,19 +40,39 @@ func run(a, _data) -> void:
a.true_(PrestigeModel.enthrone_available(2, 2), "敕封:转世 2 可")
a.true_(not PrestigeModel.enthrone_available(1, 2), "敕封:转世 1 不可")
# ---- OfflineModel 分带 + 72h 封顶 + 时钟回拨 ----
var r1: Dictionary = OfflineModel.settle(3600.0, 10.0, 1.0, 2.0, 1.0, 8.0, 0.7, 24.0, 0.4, 0.1, 72.0)
# ---- OfflineModel 分带 + 72h 封顶 + 时钟回拨 + 长明灯加法 + 硬顶 ----
# W4 契约 v2(systems-spec §2.4/§2.5):第三参数由 offline_mult(乘性)改为
# offline_add(加法,直接加在各档速率上,各档硬顶 100%)。
var r1: Dictionary = OfflineModel.settle(3600.0, 10.0, 0.0, 2.0, 1.0, 8.0, 0.7, 24.0, 0.4, 0.1, 72.0)
a.near(r1["effective_hours"], 1.0, 1e-9, "离线 1h = 1h 等效")
a.near(r1["merit"], 36000.0, 1e-6, "离线 1h × 10/s")
var r8: Dictionary = OfflineModel.settle(8.0 * 3600.0, 10.0, 1.0, 2.0, 1.0, 8.0, 0.7, 24.0, 0.4, 0.1, 72.0)
a.near(r8["effective_hours"], 6.2, 1e-9, "离线 8h = 6.2h 等效(P-8)")
var r8: Dictionary = OfflineModel.settle(8.0 * 3600.0, 10.0, 0.0, 2.0, 1.0, 8.0, 0.7, 24.0, 0.4, 0.1, 72.0)
a.near(r8["effective_hours"], 6.2, 1e-9, "离线 8h = 6.2h 等效(契约 v2:77.5%)")
a.near(r8["merit"], 223200.0, 1e-6, "离线 8h 收益")
var r100: Dictionary = OfflineModel.settle(100.0 * 3600.0, 10.0, 1.0, 2.0, 1.0, 8.0, 0.7, 24.0, 0.4, 0.1, 72.0)
a.near(r100["effective_hours"], 17.4, 1e-9, "离线 100h → 72h 封顶 = 17.4h 等效")
var rneg: Dictionary = OfflineModel.settle(-5.0, 10.0, 1.0, 2.0, 1.0, 8.0, 0.7, 24.0, 0.4, 0.1, 72.0)
var r24: Dictionary = OfflineModel.settle(24.0 * 3600.0, 10.0, 0.0, 2.0, 1.0, 8.0, 0.7, 24.0, 0.4, 0.1, 72.0)
a.near(r24["effective_hours"], 12.6, 1e-9, "离线 24h = 12.6h 等效(契约 v2)")
var r100: Dictionary = OfflineModel.settle(100.0 * 3600.0, 10.0, 0.0, 2.0, 1.0, 8.0, 0.7, 24.0, 0.4, 0.1, 72.0)
a.near(r100["effective_hours"], 17.4, 1e-9, "离线 100h → 72h 封顶 = 17.4h 等效(24.2%)")
var rneg: Dictionary = OfflineModel.settle(-5.0, 10.0, 0.0, 2.0, 1.0, 8.0, 0.7, 24.0, 0.4, 0.1, 72.0)
a.near(rneg["merit"], 0.0, 1e-9, "时钟回拨 → 0")
a.near(OfflineModel.ratio_8h_over_1h(2.0, 1.0, 8.0, 0.7, 24.0, 0.4, 0.1), 6.2, 1e-9,
"门 #9 比率 6.2(契约 [1.5,2.5] 预计 FAIL,P-8)")
"历史口径比率 6.2(契约 v2 起门 #9 改判等效小时,见 SimGates.gate_offline_v2)")
# 长明灯加法:+30%(6 级)⇒ 2-8h 档 70%+30% = 100%(硬顶)⇒ 8h = 8.0h(效率 100%)
var r8l: Dictionary = OfflineModel.settle(8.0 * 3600.0, 10.0, 0.30, 2.0, 1.0, 8.0, 0.7, 24.0, 0.4, 0.1, 72.0)
a.near(r8l["effective_hours"], 8.0, 1e-9, "长明灯满级 8h = 8.0h 等效(硬顶 100%)")
a.near(r8l["merit"], 288000.0, 1e-6, "长明灯满级 8h 收益")
# 加法不越过硬顶:+55%(6 级 + m05)仍 = 8.0h;8-24h 档 40%+30% = 70%
var r8m: Dictionary = OfflineModel.settle(8.0 * 3600.0, 10.0, 0.55, 2.0, 1.0, 8.0, 0.7, 24.0, 0.4, 0.1, 72.0)
a.near(r8m["effective_hours"], 8.0, 1e-9, "加法 +55% 仍钳 8.0h(0-2h/2-8h 双档顶格)")
var r24l: Dictionary = OfflineModel.settle(24.0 * 3600.0, 10.0, 0.30, 2.0, 1.0, 8.0, 0.7, 24.0, 0.4, 0.1, 72.0)
a.near(r24l["effective_hours"], 2.0 + 6.0 + 16.0 * 0.7, 1e-9, "长明灯满级 24h = 19.2h(8-24h 档 70%)")
# 效率硬顶:任意加成下等效 ≤ 实际(离线不得优于在线)
for dur_h in [1.0, 8.0, 24.0, 72.0]:
for add in [0.0, 0.30, 0.55, 5.0]:
var rr: Dictionary = OfflineModel.settle(dur_h * 3600.0, 10.0, add,
2.0, 1.0, 8.0, 0.7, 24.0, 0.4, 0.1, 72.0)
a.true_(float(rr["effective_hours"]) <= dur_h + 1e-9,
"效率硬顶:%gh 离线 加法 +%g ⇒ 等效 %.2fh ≤ %gh" % [dur_h, add, float(rr["effective_hours"]), dur_h])
# ---- BigNumber 红线 ----
a.true_(BigNumber.assert_within(1e13, "test"), "1e13 不过红线")
+12 -3
View File
@@ -143,7 +143,7 @@ func _test_enthrone(a, data) -> void:
a.near(e.output_per_sec(), 1073741824.0 * 4.25, 1.0, "gen12 1 级产出")
# ---- 事件瞬时区 + 离线倍率 ----
# ---- 事件瞬时区 + 离线加法(契约 v2:长明灯 ×2.31 → 加法 +30%) ----
func _test_event_and_offline(a, data) -> void:
var e := EconomyState.new(data)
e.gen_levels[0] = 10
@@ -151,9 +151,18 @@ func _test_event_and_offline(a, data) -> void:
e.set_event_mult(5.0)
a.near(e.output_per_sec(), base_out * 5.0, 1e-9, "事件 ×5 瞬时区")
e.set_event_mult(1.0)
a.near(e.offline_mult(), 1.0, 1e-9, "无长明灯 → ×1")
a.near(e.offline_add(), 0.0, 1e-9, "无长明灯 → 加法 +0")
e.halls_passed = 4 # 离线升级出现条件
e.merit = 1e9
var r: Dictionary = e.buy_upgrade("up_offline_1")
a.true_(bool(r.get("ok", false)), "买长明灯 I")
a.near(e.offline_mult(), 1.15, 1e-9, "离线 ×1.15(P-13 乘性链)")
a.near(e.offline_add(), 0.05, 1e-9, "长明灯 I → 加法 +5%(旧 ×1.15 乘性已废止)")
for i in range(2, 7):
var ri: Dictionary = e.buy_upgrade("up_offline_%d" % i)
a.true_(bool(ri.get("ok", false)), "买长明灯 %d" % i)
a.near(e.offline_add(), 0.30, 1e-9, "长明灯 6 级满 = 加法 +30%(各档速率硬顶 100%)")
# m05 阴司人脉(每阶 +25% 加法,max_rank 1)
e.suihui = 100
var rm: Dictionary = e.buy_meta("meta_05")
a.true_(bool(rm.get("ok", false)), "买阴司人脉")
a.near(e.offline_add(), 0.55, 1e-9, "+ m05 = 加法 +55%(仍由 OfflineModel 钳各档 ≤100%)")
+84
View File
@@ -0,0 +1,84 @@
extends RefCounted
# 用例:W4 离线契约 v2(systems-spec §2.4/§2.5,用户 2026-10-20 拍板)
# 契约 v2:8h = 6.2 等效小时(效率 77.5%);24h = 12.6h;72h 封顶 = 17.4h(24.2%);
# 长明灯 6 条 × +5% 加法(满 +30%,各档速率硬顶 100%);效率硬顶 ≤ 100%。
# 门 #9 拆三条子检查(a 由 FAIL 转 PASS):9a 契约效率 / 9b 长明灯上限 / 9c 硬顶。
func run(a, data) -> void:
_test_contract_v2(a, data)
_test_gate_offline_v2(a, data)
func _settle(a, data, hours: float, add: float) -> float:
var r: Dictionary = OfflineModel.settle(hours * 3600.0, 1.0, add,
data.tune_num("tune_offline_band1_h", 2.0), data.tune_num("tune_offline_band1_rate", 1.0),
data.tune_num("tune_offline_band2_h", 8.0), data.tune_num("tune_offline_band2_rate", 0.7),
data.tune_num("tune_offline_band3_h", 24.0), data.tune_num("tune_offline_band3_rate", 0.4),
data.tune_num("tune_offline_band4_rate", 0.1),
data.tune_num("tune_offline_cap_h", 72.0))
return float(r["effective_hours"])
# ---- 契约 v2 数值面 ----
func _test_contract_v2(a, data) -> void:
# 衰减表一字不改(systems-spec §2.4:错的是契约不是表)
a.near(data.tune_num("tune_offline_band1_rate", 0.0), 1.0, 1e-9, "0-2h = 100%")
a.near(data.tune_num("tune_offline_band2_rate", 0.0), 0.7, 1e-9, "2-8h = 70%")
a.near(data.tune_num("tune_offline_band3_rate", 0.0), 0.4, 1e-9, "8-24h = 40%")
a.near(data.tune_num("tune_offline_band4_rate", 0.0), 0.1, 1e-9, ">24h = 10%")
a.near(data.tune_num("tune_offline_cap_h", 0.0), 72.0, 1e-9, "72h 封顶")
# 8h = 6.2 等效小时(77.5%)∈ [5.5, 7.0](用户口径)/ [5.5, 6.5](spec 口径,双重满足)
var e8 := _settle(a, data, 8.0, 0.0)
a.near(e8, 6.2, 1e-9, "8h 离线 = 6.2h 等效(契约 v2)")
a.true_(e8 >= 5.5 and e8 <= 7.0, "8h 等效 ∈ [5.5, 7.0](用户构造口径)")
a.true_(e8 >= 5.5 and e8 <= 6.5, "8h 等效 ∈ [5.5, 6.5](systems-spec §2.4 口径)")
a.near(e8 / 8.0, 0.775, 1e-9, "8h 效率 = 77.5%")
# 24h = 12.6h;72h 封顶 = 17.4h(24.2%)
a.near(_settle(a, data, 24.0, 0.0), 12.6, 1e-9, "24h 离线 = 12.6h 等效")
a.near(_settle(a, data, 72.0, 0.0), 17.4, 1e-9, "72h 封顶 = 17.4h 等效")
a.near(_settle(a, data, 72.0, 0.0) / 72.0, 17.4 / 72.0, 1e-9, "72h 效率 ≈ 24.2%")
a.near(_settle(a, data, 100.0, 0.0), 17.4, 1e-9, "100h 也被 72h 硬顶钳到 17.4h")
# 长明灯:数据面 = 6 条 × 0.05 加法(effect_type offline_add)
var lamp := 0.0
var lamp_rows := 0
for u in data.upgrades:
if u.type == "offline":
lamp += u.effect_param
lamp_rows += 1
a.eq(lamp_rows, 6, "长明灯 6 条")
a.near(lamp, 0.30, 1e-9, "长明灯满级 = 加法 +30%(旧 ×2.31 乘性已废止)")
# 长明灯满级:8h = 8.0h,效率恰好 100%(硬顶允许的最大值)
var e8l := _settle(a, data, 8.0, lamp)
a.near(e8l, 8.0, 1e-9, "长明灯满级 8h = 8.0h 等效")
a.near(e8l / 8.0, 1.0, 1e-9, "效率 = 100%(≤ 硬顶,离线不优于在线)")
# + m05(+25%)也不越过硬顶
var m05 := data.meta("meta_05")
var m05_add := 0.0
if m05 != null:
m05_add = m05.effect_param * float(m05.max_rank)
a.near(_settle(a, data, 8.0, lamp + m05_add), 8.0, 1e-9, "+ m05(+25%)仍钳 8.0h")
# 效率硬顶:任意配置/任意时长 ≤ 100%
for dur_h in [1.0, 8.0, 24.0, 72.0]:
for add in [0.0, lamp, lamp + m05_add, 5.0]:
var eff := _settle(a, data, dur_h, add)
a.true_(eff <= dur_h + 1e-9,
"效率硬顶:%sh + 加法 %.2f ⇒ %.2fh ≤ %sh" % [str(dur_h), add, eff, str(dur_h)])
# ---- 门 #9 三条子检查(a 由 FAIL 转 PASS) ----
func _test_gate_offline_v2(a, data) -> void:
var g: Dictionary = SimGates.gate_offline_v2(data)
a.true_(bool(g.get("pass", false)), "门 #9 三条子检查全过(a 由 FAIL 转 PASS)")
a.true_(str(g.get("detail", "")).contains("契约 v2"), "detail 标注契约 v2")
var sub: Dictionary = g.get("sub", {})
var g9a: Dictionary = sub.get("9a", {})
var g9b: Dictionary = sub.get("9b", {})
var g9c: Dictionary = sub.get("9c", {})
a.true_(bool(g9a.get("pass", false)),
"9a PASS:%s" % str(g9a.get("detail", "")))
a.true_(bool(g9b.get("pass", false)),
"9b PASS:%s" % str(g9b.get("detail", "")))
a.true_(bool(g9c.get("pass", false)),
"9c PASS:%s" % str(g9c.get("detail", "")))
a.true_(str(g.get("deferred_beta", "")).contains("β"), "进度口径 β 标记为待 sim 标定")
+2
View File
@@ -43,6 +43,8 @@ const CASE_FILES: Array[String] = [
"res://tests/unit/cases/test_camera_data.gd",
"res://tests/unit/cases/test_event_director.gd",
"res://tests/unit/cases/test_boss_battle.gd",
# W4 新增:离线契约 v2 + 门 #9 三条子检查(P-8 裁定落地)
"res://tests/unit/cases/test_offline_contract.gd",
]