feat: W2 经济核心+数据管线+灰盒 — Balance Bible v1.0 八表逐值落地,EconomyState/sim 14 门/灰盒主场景/单测(GDScript 待本机引擎验证)
This commit is contained in:
1 parent
856326a766
commit
24e5b0c4cc
58 files changed
+7099
-22
No files matched your search
@@ -0,0 +1,223 @@
|
||||
{
|
||||
"sheet": "balance_tuning",
|
||||
"schema_version": 0,
|
||||
"columns": [
|
||||
"id",
|
||||
"kind",
|
||||
"num",
|
||||
"text",
|
||||
"desc_cn"
|
||||
],
|
||||
"rows": [
|
||||
{
|
||||
"id": "tune_click_base",
|
||||
"kind": "number",
|
||||
"num": 1,
|
||||
"text": "",
|
||||
"desc_cn": "[B§2.2] 点击基础值 = 1(与 gen1 成本 15 配对 → 15 次点击首购)"
|
||||
},
|
||||
{
|
||||
"id": "tune_click_zone_cap",
|
||||
"kind": "number",
|
||||
"num": 16,
|
||||
"text": "",
|
||||
"desc_cn": "[B§1/§2.2] 点击区软上限 ×16(4 级 ×2 满)"
|
||||
},
|
||||
{
|
||||
"id": "tune_gen_zone_cap",
|
||||
"kind": "number",
|
||||
"num": 8,
|
||||
"text": "",
|
||||
"desc_cn": "[B§1] 班底乘区软上限 ×8(6 级效率满 = ×7,留 1 级余量)"
|
||||
},
|
||||
{
|
||||
"id": "tune_global_zone_cap",
|
||||
"kind": "number",
|
||||
"num": 64,
|
||||
"text": "",
|
||||
"desc_cn": "[B§1] 全局乘区软上限 ×64"
|
||||
},
|
||||
{
|
||||
"id": "tune_offline_band1_h",
|
||||
"kind": "number",
|
||||
"num": 2,
|
||||
"text": "",
|
||||
"desc_cn": "[B§8] 离线 0-2h 速率 100%"
|
||||
},
|
||||
{
|
||||
"id": "tune_offline_band1_rate",
|
||||
"kind": "number",
|
||||
"num": 1,
|
||||
"text": "",
|
||||
"desc_cn": "[B§8] 离线 0-2h 速率"
|
||||
},
|
||||
{
|
||||
"id": "tune_offline_band2_h",
|
||||
"kind": "number",
|
||||
"num": 8,
|
||||
"text": "",
|
||||
"desc_cn": "[B§8] 离线 2-8h 速率 70%"
|
||||
},
|
||||
{
|
||||
"id": "tune_offline_band2_rate",
|
||||
"kind": "number",
|
||||
"num": 0.7,
|
||||
"text": "",
|
||||
"desc_cn": "[B§8] 离线 2-8h 速率"
|
||||
},
|
||||
{
|
||||
"id": "tune_offline_band3_h",
|
||||
"kind": "number",
|
||||
"num": 24,
|
||||
"text": "",
|
||||
"desc_cn": "[B§8] 离线 8-24h 速率 40%"
|
||||
},
|
||||
{
|
||||
"id": "tune_offline_band3_rate",
|
||||
"kind": "number",
|
||||
"num": 0.4,
|
||||
"text": "",
|
||||
"desc_cn": "[B§8] 离线 8-24h 速率"
|
||||
},
|
||||
{
|
||||
"id": "tune_offline_band4_rate",
|
||||
"kind": "number",
|
||||
"num": 0.1,
|
||||
"text": "",
|
||||
"desc_cn": "[B§8] 离线 >24h 速率 10%"
|
||||
},
|
||||
{
|
||||
"id": "tune_offline_cap_h",
|
||||
"kind": "number",
|
||||
"num": 72,
|
||||
"text": "",
|
||||
"desc_cn": "[B§8] 离线硬上限 72h 封顶"
|
||||
},
|
||||
{
|
||||
"id": "tune_wrath_layers",
|
||||
"kind": "number",
|
||||
"num": 22,
|
||||
"text": "",
|
||||
"desc_cn": "[B§3.2] 冤煞层总数 22"
|
||||
},
|
||||
{
|
||||
"id": "tune_wrath_cost_base",
|
||||
"kind": "number",
|
||||
"num": 8,
|
||||
"text": "",
|
||||
"desc_cn": "[B§3.2] 第 n 层成本 = round(8 × n^1.8)"
|
||||
},
|
||||
{
|
||||
"id": "tune_wrath_cost_exp",
|
||||
"kind": "number",
|
||||
"num": 1.8,
|
||||
"text": "",
|
||||
"desc_cn": "[B§3.2] 冤煞层成本指数"
|
||||
},
|
||||
{
|
||||
"id": "tune_wrath_bonus",
|
||||
"kind": "number",
|
||||
"num": 0.25,
|
||||
"text": "",
|
||||
"desc_cn": "[B§3.2] 每层全局 +25%(加法,进全局乘区)"
|
||||
},
|
||||
{
|
||||
"id": "tune_wrath_penalty",
|
||||
"kind": "number",
|
||||
"num": 0.03,
|
||||
"text": "",
|
||||
"desc_cn": "[B§3.2] 每层宿慧获取削减 +3%"
|
||||
},
|
||||
{
|
||||
"id": "tune_wrath_penalty_cap",
|
||||
"kind": "number",
|
||||
"num": 0.3,
|
||||
"text": "",
|
||||
"desc_cn": "[B§3.2] 宿慧削减封顶 30%"
|
||||
},
|
||||
{
|
||||
"id": "tune_suihui_div",
|
||||
"kind": "number",
|
||||
"num": 1000000,
|
||||
"text": "",
|
||||
"desc_cn": "[B§4.1] 宿慧 = floor(√(本轮累计阴德/1e6) × 5)"
|
||||
},
|
||||
{
|
||||
"id": "tune_suihui_mult",
|
||||
"kind": "number",
|
||||
"num": 5,
|
||||
"text": "",
|
||||
"desc_cn": "[B§4.1] 宿慧公式乘数"
|
||||
},
|
||||
{
|
||||
"id": "tune_suihui_global_pct",
|
||||
"kind": "number",
|
||||
"num": 0.03,
|
||||
"text": "",
|
||||
"desc_cn": "[B§1/§4.2] 每级宿慧全局产出 +3%(加法,进全局乘区)"
|
||||
},
|
||||
{
|
||||
"id": "tune_enthrone_bonus",
|
||||
"kind": "number",
|
||||
"num": 0.25,
|
||||
"text": "",
|
||||
"desc_cn": "[B§4.4] 阴司敕封自动全局恩典 +25%(与 m08 商品字面重叠,见 provenance P-4)"
|
||||
},
|
||||
{
|
||||
"id": "tune_skip_interval",
|
||||
"kind": "number",
|
||||
"num": 5,
|
||||
"text": "",
|
||||
"desc_cn": "[B§4.2] 每 5 级宿慧 → 前 3 档班底各 +25 级(m04 可加快为 4)"
|
||||
},
|
||||
{
|
||||
"id": "tune_skip_gens",
|
||||
"kind": "number",
|
||||
"num": 3,
|
||||
"text": "",
|
||||
"desc_cn": "[B§4.2] 跳过机制覆盖前 3 档班底"
|
||||
},
|
||||
{
|
||||
"id": "tune_skip_levels",
|
||||
"kind": "number",
|
||||
"num": 25,
|
||||
"text": "",
|
||||
"desc_cn": "[B§4.2] 跳过机制每档 +25 级"
|
||||
},
|
||||
{
|
||||
"id": "tune_enthrone_rebirths",
|
||||
"kind": "number",
|
||||
"num": 2,
|
||||
"text": "",
|
||||
"desc_cn": "[B§4.4] 阴司敕封触发:转世 ≥2(约第 2-3 轮,累计 8-12h 为其 pacing 后果)"
|
||||
},
|
||||
{
|
||||
"id": "tune_restart_curve",
|
||||
"kind": "text",
|
||||
"num": 0,
|
||||
"text": "1:0.6:0.4:0.3",
|
||||
"desc_cn": "[B§4.3] 重开曲线目标(第 5 轮触底 ≥0.25×);模拟器门 #13 断言"
|
||||
},
|
||||
{
|
||||
"id": "tune_event_timeout_default",
|
||||
"kind": "text",
|
||||
"num": 0,
|
||||
"text": "opt_a",
|
||||
"desc_cn": "[B§7] 冤魂申诉超时默认 = 超度(opt_a)"
|
||||
},
|
||||
{
|
||||
"id": "tune_souls_div",
|
||||
"kind": "number",
|
||||
"num": 10000,
|
||||
"text": "",
|
||||
"desc_cn": "[B§6] 超度亡魂 = floor(本轮累计阴德 ÷ 10,000)(叙事计数器,不设闸门;P-14)"
|
||||
},
|
||||
{
|
||||
"id": "tune_enthrone_suihui_req",
|
||||
"kind": "number",
|
||||
"num": 60,
|
||||
"text": "",
|
||||
"desc_cn": "[G§3.4] 阴司敕封解锁条件:宿慧 ≥60([B§4.4] 仅给 8-12h 体感,采信 GDD 数值)"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
{
|
||||
"sheet": "boss_battles",
|
||||
"schema_version": 0,
|
||||
"columns": [
|
||||
"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"
|
||||
],
|
||||
"rows": [
|
||||
{
|
||||
"id": "boss_hall_01",
|
||||
"hall_id": "hall_01_qinguang",
|
||||
"hall_factor": 1,
|
||||
"hp_factor": 15,
|
||||
"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,
|
||||
"combo_cap": 0.6,
|
||||
"retry_cooldown_sec": 60,
|
||||
"reward_hp_ratio": 0.2
|
||||
},
|
||||
{
|
||||
"id": "boss_hall_02",
|
||||
"hall_id": "hall_02_chujiang",
|
||||
"hall_factor": 1.1,
|
||||
"hp_factor": 15,
|
||||
"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,
|
||||
"combo_cap": 0.6,
|
||||
"retry_cooldown_sec": 60,
|
||||
"reward_hp_ratio": 0.2
|
||||
},
|
||||
{
|
||||
"id": "boss_hall_03",
|
||||
"hall_id": "hall_03_songdi",
|
||||
"hall_factor": 1.2,
|
||||
"hp_factor": 15,
|
||||
"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,
|
||||
"combo_cap": 0.6,
|
||||
"retry_cooldown_sec": 60,
|
||||
"reward_hp_ratio": 0.2
|
||||
},
|
||||
{
|
||||
"id": "boss_hall_04",
|
||||
"hall_id": "hall_04_wuguan",
|
||||
"hall_factor": 1.3,
|
||||
"hp_factor": 15,
|
||||
"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,
|
||||
"combo_cap": 0.6,
|
||||
"retry_cooldown_sec": 60,
|
||||
"reward_hp_ratio": 0.2
|
||||
},
|
||||
{
|
||||
"id": "boss_hall_05",
|
||||
"hall_id": "hall_05_yama",
|
||||
"hall_factor": 1.4,
|
||||
"hp_factor": 15,
|
||||
"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,
|
||||
"combo_cap": 0.6,
|
||||
"retry_cooldown_sec": 60,
|
||||
"reward_hp_ratio": 0.2
|
||||
},
|
||||
{
|
||||
"id": "boss_hall_06",
|
||||
"hall_id": "hall_06_biancheng",
|
||||
"hall_factor": 1.6,
|
||||
"hp_factor": 15,
|
||||
"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,
|
||||
"combo_cap": 0.6,
|
||||
"retry_cooldown_sec": 60,
|
||||
"reward_hp_ratio": 0.2
|
||||
},
|
||||
{
|
||||
"id": "boss_hall_07",
|
||||
"hall_id": "hall_07_taishan",
|
||||
"hall_factor": 1.8,
|
||||
"hp_factor": 15,
|
||||
"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,
|
||||
"combo_cap": 0.6,
|
||||
"retry_cooldown_sec": 60,
|
||||
"reward_hp_ratio": 0.2
|
||||
},
|
||||
{
|
||||
"id": "boss_hall_08",
|
||||
"hall_id": "hall_08_dushi",
|
||||
"hall_factor": 2,
|
||||
"hp_factor": 15,
|
||||
"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,
|
||||
"combo_cap": 0.6,
|
||||
"retry_cooldown_sec": 60,
|
||||
"reward_hp_ratio": 0.2
|
||||
},
|
||||
{
|
||||
"id": "boss_hall_09",
|
||||
"hall_id": "hall_09_pingdeng",
|
||||
"hall_factor": 2.1,
|
||||
"hp_factor": 15,
|
||||
"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,
|
||||
"combo_cap": 0.6,
|
||||
"retry_cooldown_sec": 60,
|
||||
"reward_hp_ratio": 0.2
|
||||
},
|
||||
{
|
||||
"id": "boss_hall_10",
|
||||
"hall_id": "hall_10_zhuanlun",
|
||||
"hall_factor": 2.2,
|
||||
"hp_factor": 15,
|
||||
"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,
|
||||
"combo_cap": 0.6,
|
||||
"retry_cooldown_sec": 60,
|
||||
"reward_hp_ratio": 0.2
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
{
|
||||
"sheet": "events",
|
||||
"schema_version": 0,
|
||||
"columns": [
|
||||
"id",
|
||||
"name_cn",
|
||||
"name_en",
|
||||
"trigger_min_min",
|
||||
"trigger_max_min",
|
||||
"duration_sec",
|
||||
"type",
|
||||
"p_mult",
|
||||
"p_clicks",
|
||||
"p_window_sec",
|
||||
"p_penalty",
|
||||
"p_penalty_dur_sec",
|
||||
"opt_a_type",
|
||||
"opt_a_param",
|
||||
"opt_b_type",
|
||||
"opt_b_param",
|
||||
"flavor_cn"
|
||||
],
|
||||
"rows": [
|
||||
{
|
||||
"id": "ev_01_baigui",
|
||||
"name_cn": "百鬼夜行",
|
||||
"name_en": "Night of the Hundred Ghosts",
|
||||
"trigger_min_min": 8,
|
||||
"trigger_max_min": 15,
|
||||
"duration_sec": 60,
|
||||
"type": "output_mult",
|
||||
"p_mult": 5,
|
||||
"p_clicks": 0,
|
||||
"p_window_sec": 0,
|
||||
"p_penalty": 0,
|
||||
"p_penalty_dur_sec": 0,
|
||||
"opt_a_type": "none",
|
||||
"opt_a_param": 0,
|
||||
"opt_b_type": "none",
|
||||
"opt_b_param": 0,
|
||||
"flavor_cn": "百鬼倾巢而出,阴阳秩序暂时失序"
|
||||
},
|
||||
{
|
||||
"id": "ev_02_naoxiang",
|
||||
"name_cn": "阴兵闹饷",
|
||||
"name_en": "Ghost Mutiny",
|
||||
"trigger_min_min": 10,
|
||||
"trigger_max_min": 18,
|
||||
"duration_sec": 15,
|
||||
"type": "click_challenge",
|
||||
"p_mult": 0,
|
||||
"p_clicks": 10,
|
||||
"p_window_sec": 15,
|
||||
"p_penalty": 0.5,
|
||||
"p_penalty_dur_sec": 60,
|
||||
"opt_a_type": "none",
|
||||
"opt_a_param": 0,
|
||||
"opt_b_type": "none",
|
||||
"opt_b_param": 0,
|
||||
"flavor_cn": "鬼卒也要三牲祭,不然哗变"
|
||||
},
|
||||
{
|
||||
"id": "ev_03_shensu",
|
||||
"name_cn": "冤魂申诉",
|
||||
"name_en": "Grievance of the Wronged",
|
||||
"trigger_min_min": 6,
|
||||
"trigger_max_min": 12,
|
||||
"duration_sec": 45,
|
||||
"type": "choice",
|
||||
"p_mult": 0,
|
||||
"p_clicks": 15,
|
||||
"p_window_sec": 40,
|
||||
"p_penalty": 0,
|
||||
"p_penalty_dur_sec": 0,
|
||||
"opt_a_type": "merit_burst_sec",
|
||||
"opt_a_param": 30,
|
||||
"opt_b_type": "wrath_gain",
|
||||
"opt_b_param": 0,
|
||||
"flavor_cn": "他跪在地上喊冤枉。你选择……"
|
||||
},
|
||||
{
|
||||
"id": "ev_04_tiangou",
|
||||
"name_cn": "天狗食月",
|
||||
"name_en": "Heavenly Dog Eats the Moon",
|
||||
"trigger_min_min": 20,
|
||||
"trigger_max_min": 35,
|
||||
"duration_sec": 30,
|
||||
"type": "click_output_mult",
|
||||
"p_mult": 2,
|
||||
"p_clicks": 10,
|
||||
"p_window_sec": 0,
|
||||
"p_penalty": 0,
|
||||
"p_penalty_dur_sec": 0,
|
||||
"opt_a_type": "none",
|
||||
"opt_a_param": 0,
|
||||
"opt_b_type": "none",
|
||||
"opt_b_param": 0,
|
||||
"flavor_cn": "天狗噬月,阴气最盛——动手"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -52,7 +52,7 @@
|
||||
"display_en": "Death-Scribe Clerks",
|
||||
"base_cost": 12000,
|
||||
"cost_ratio": 1.15,
|
||||
"base_rate": 50,
|
||||
"base_rate": 64,
|
||||
"unlock_hall": "hall_02_chujiang",
|
||||
"unlock_gate": "up_gate_biliao",
|
||||
"sprite_row": "res://assets/sprites/units/gen_04_biliao"
|
||||
@@ -63,7 +63,7 @@
|
||||
"display_en": "Meng Po's Soup Stall",
|
||||
"base_cost": 120000,
|
||||
"cost_ratio": 1.16,
|
||||
"base_rate": 400,
|
||||
"base_rate": 512,
|
||||
"unlock_hall": "hall_03_songdi",
|
||||
"unlock_gate": "up_gate_tangpu",
|
||||
"sprite_row": "res://assets/sprites/units/gen_05_tangpu"
|
||||
@@ -74,7 +74,7 @@
|
||||
"display_en": "Refugee Camp of the Wrongfully Dead",
|
||||
"base_cost": 1200000,
|
||||
"cost_ratio": 1.16,
|
||||
"base_rate": 3000,
|
||||
"base_rate": 4096,
|
||||
"unlock_hall": "hall_04_wuguan",
|
||||
"unlock_gate": "up_gate_nanmin",
|
||||
"sprite_row": "res://assets/sprites/units/gen_06_nanmin"
|
||||
@@ -85,7 +85,7 @@
|
||||
"display_en": "Clerks of the Nether Court",
|
||||
"base_cost": 12000000,
|
||||
"cost_ratio": 1.17,
|
||||
"base_rate": 24000,
|
||||
"base_rate": 32768,
|
||||
"unlock_hall": "hall_05_yama",
|
||||
"unlock_gate": "up_gate_shuban",
|
||||
"sprite_row": "res://assets/sprites/units/gen_07_shuban"
|
||||
@@ -96,7 +96,7 @@
|
||||
"display_en": "Armory of the Dark Capital",
|
||||
"base_cost": 120000000,
|
||||
"cost_ratio": 1.17,
|
||||
"base_rate": 180000,
|
||||
"base_rate": 262144,
|
||||
"unlock_hall": "hall_06_biancheng",
|
||||
"unlock_gate": "up_gate_bingfang",
|
||||
"sprite_row": "res://assets/sprites/units/gen_08_bingfang"
|
||||
@@ -107,7 +107,7 @@
|
||||
"display_en": "Sleeper Agents in the Ten Courts",
|
||||
"base_cost": 1200000000,
|
||||
"cost_ratio": 1.18,
|
||||
"base_rate": 1400000,
|
||||
"base_rate": 2097152,
|
||||
"unlock_hall": "hall_07_taishan",
|
||||
"unlock_gate": "up_gate_wodi",
|
||||
"sprite_row": "res://assets/sprites/units/gen_09_wodi"
|
||||
@@ -118,7 +118,7 @@
|
||||
"display_en": "Rift Walkers of the Wheel",
|
||||
"base_cost": 12000000000,
|
||||
"cost_ratio": 1.18,
|
||||
"base_rate": 11000000,
|
||||
"base_rate": 16777216,
|
||||
"unlock_hall": "hall_08_dushi",
|
||||
"unlock_gate": "up_gate_xingzhe",
|
||||
"sprite_row": "res://assets/sprites/units/gen_10_xingzhe"
|
||||
@@ -129,7 +129,7 @@
|
||||
"display_en": "Soul Guides of the Six Paths",
|
||||
"base_cost": 120000000000,
|
||||
"cost_ratio": 1.19,
|
||||
"base_rate": 85000000,
|
||||
"base_rate": 134217728,
|
||||
"unlock_hall": "hall_09_pingdeng",
|
||||
"unlock_gate": "up_gate_yinhun",
|
||||
"sprite_row": "res://assets/sprites/units/gen_11_yinhun"
|
||||
@@ -140,7 +140,7 @@
|
||||
"display_en": "Emperor of the Netherworld",
|
||||
"base_cost": 1200000000000,
|
||||
"cost_ratio": 1.2,
|
||||
"base_rate": 650000000,
|
||||
"base_rate": 1073741824,
|
||||
"unlock_hall": "none",
|
||||
"unlock_gate": "up_gate_tianzi",
|
||||
"sprite_row": "res://assets/sprites/units/gen_12_tianzi"
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
{
|
||||
"sheet": "hall_palettes",
|
||||
"schema_version": 0,
|
||||
"columns": [
|
||||
"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"
|
||||
],
|
||||
"rows": [
|
||||
{
|
||||
"id": "pal_hall_01",
|
||||
"hall_id": "hall_01_qinguang",
|
||||
"name_cn": "冷靛",
|
||||
"ink_hex": "#1A1A1A",
|
||||
"bone_hex": "#F2EDE4",
|
||||
"cinnabar_hex": "#B4402C",
|
||||
"indigo_hex": "#2C4A6E",
|
||||
"accent_hex": "#3D5A80",
|
||||
"accent_name_cn": "冷靛",
|
||||
"flow": "bleed_left",
|
||||
"lut_index": 1,
|
||||
"ink_pct": 65,
|
||||
"bone_pct": 25,
|
||||
"cinnabar_max_pct": 8,
|
||||
"indigo_max_pct": 7
|
||||
},
|
||||
{
|
||||
"id": "pal_hall_02",
|
||||
"hall_id": "hall_02_chujiang",
|
||||
"name_cn": "冰青",
|
||||
"ink_hex": "#1A1A1A",
|
||||
"bone_hex": "#F2EDE4",
|
||||
"cinnabar_hex": "#B4402C",
|
||||
"indigo_hex": "#2C4A6E",
|
||||
"accent_hex": "#8FC1C9",
|
||||
"accent_name_cn": "冰青",
|
||||
"flow": "seep_down",
|
||||
"lut_index": 2,
|
||||
"ink_pct": 65,
|
||||
"bone_pct": 25,
|
||||
"cinnabar_max_pct": 8,
|
||||
"indigo_max_pct": 7
|
||||
},
|
||||
{
|
||||
"id": "pal_hall_03",
|
||||
"hall_id": "hall_03_songdi",
|
||||
"name_cn": "暗红",
|
||||
"ink_hex": "#1A1A1A",
|
||||
"bone_hex": "#F2EDE4",
|
||||
"cinnabar_hex": "#B4402C",
|
||||
"indigo_hex": "#2C4A6E",
|
||||
"accent_hex": "#7E2E2E",
|
||||
"accent_name_cn": "暗红",
|
||||
"flow": "bleed_left",
|
||||
"lut_index": 3,
|
||||
"ink_pct": 65,
|
||||
"bone_pct": 25,
|
||||
"cinnabar_max_pct": 8,
|
||||
"indigo_max_pct": 7
|
||||
},
|
||||
{
|
||||
"id": "pal_hall_04",
|
||||
"hall_id": "hall_04_wuguan",
|
||||
"name_cn": "锈橙",
|
||||
"ink_hex": "#1A1A1A",
|
||||
"bone_hex": "#F2EDE4",
|
||||
"cinnabar_hex": "#B4402C",
|
||||
"indigo_hex": "#2C4A6E",
|
||||
"accent_hex": "#A85C32",
|
||||
"accent_name_cn": "锈橙",
|
||||
"flow": "seep_down",
|
||||
"lut_index": 4,
|
||||
"ink_pct": 65,
|
||||
"bone_pct": 25,
|
||||
"cinnabar_max_pct": 8,
|
||||
"indigo_max_pct": 7
|
||||
},
|
||||
{
|
||||
"id": "pal_hall_05",
|
||||
"hall_id": "hall_05_yama",
|
||||
"name_cn": "玄黑",
|
||||
"ink_hex": "#1A1A1A",
|
||||
"bone_hex": "#F2EDE4",
|
||||
"cinnabar_hex": "#B4402C",
|
||||
"indigo_hex": "#2C4A6E",
|
||||
"accent_hex": "#141414",
|
||||
"accent_name_cn": "玄黑",
|
||||
"flow": "bleed_left",
|
||||
"lut_index": 5,
|
||||
"ink_pct": 65,
|
||||
"bone_pct": 25,
|
||||
"cinnabar_max_pct": 8,
|
||||
"indigo_max_pct": 7
|
||||
},
|
||||
{
|
||||
"id": "pal_hall_06",
|
||||
"hall_id": "hall_06_biancheng",
|
||||
"name_cn": "铅灰",
|
||||
"ink_hex": "#1A1A1A",
|
||||
"bone_hex": "#F2EDE4",
|
||||
"cinnabar_hex": "#B4402C",
|
||||
"indigo_hex": "#2C4A6E",
|
||||
"accent_hex": "#6E7276",
|
||||
"accent_name_cn": "铅灰",
|
||||
"flow": "seep_down",
|
||||
"lut_index": 6,
|
||||
"ink_pct": 65,
|
||||
"bone_pct": 25,
|
||||
"cinnabar_max_pct": 8,
|
||||
"indigo_max_pct": 7
|
||||
},
|
||||
{
|
||||
"id": "pal_hall_07",
|
||||
"hall_id": "hall_07_taishan",
|
||||
"name_cn": "烬红",
|
||||
"ink_hex": "#1A1A1A",
|
||||
"bone_hex": "#F2EDE4",
|
||||
"cinnabar_hex": "#B4402C",
|
||||
"indigo_hex": "#2C4A6E",
|
||||
"accent_hex": "#8C3A2B",
|
||||
"accent_name_cn": "烬红",
|
||||
"flow": "bleed_left",
|
||||
"lut_index": 7,
|
||||
"ink_pct": 65,
|
||||
"bone_pct": 25,
|
||||
"cinnabar_max_pct": 8,
|
||||
"indigo_max_pct": 7
|
||||
},
|
||||
{
|
||||
"id": "pal_hall_08",
|
||||
"hall_id": "hall_08_dushi",
|
||||
"name_cn": "墨绿",
|
||||
"ink_hex": "#1A1A1A",
|
||||
"bone_hex": "#F2EDE4",
|
||||
"cinnabar_hex": "#B4402C",
|
||||
"indigo_hex": "#2C4A6E",
|
||||
"accent_hex": "#2F4A3E",
|
||||
"accent_name_cn": "墨绿",
|
||||
"flow": "seep_down",
|
||||
"lut_index": 8,
|
||||
"ink_pct": 65,
|
||||
"bone_pct": 25,
|
||||
"cinnabar_max_pct": 8,
|
||||
"indigo_max_pct": 7
|
||||
},
|
||||
{
|
||||
"id": "pal_hall_09",
|
||||
"hall_id": "hall_09_pingdeng",
|
||||
"name_cn": "黛紫",
|
||||
"ink_hex": "#1A1A1A",
|
||||
"bone_hex": "#F2EDE4",
|
||||
"cinnabar_hex": "#B4402C",
|
||||
"indigo_hex": "#2C4A6E",
|
||||
"accent_hex": "#4A3A5C",
|
||||
"accent_name_cn": "黛紫",
|
||||
"flow": "bleed_left",
|
||||
"lut_index": 9,
|
||||
"ink_pct": 65,
|
||||
"bone_pct": 25,
|
||||
"cinnabar_max_pct": 8,
|
||||
"indigo_max_pct": 7
|
||||
},
|
||||
{
|
||||
"id": "pal_hall_10",
|
||||
"hall_id": "hall_10_zhuanlun",
|
||||
"name_cn": "骨白+金",
|
||||
"ink_hex": "#1A1A1A",
|
||||
"bone_hex": "#F2EDE4",
|
||||
"cinnabar_hex": "#B4402C",
|
||||
"indigo_hex": "#2C4A6E",
|
||||
"accent_hex": "#C9A227",
|
||||
"accent_name_cn": "暗金",
|
||||
"flow": "seep_down",
|
||||
"lut_index": 10,
|
||||
"ink_pct": 65,
|
||||
"bone_pct": 25,
|
||||
"cinnabar_max_pct": 8,
|
||||
"indigo_max_pct": 7
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
{
|
||||
"sheet": "halls",
|
||||
"schema_version": 0,
|
||||
"columns": [
|
||||
"id",
|
||||
"no",
|
||||
"king_cn",
|
||||
"king_en",
|
||||
"motif_cn",
|
||||
"motif_en",
|
||||
"target_min",
|
||||
"gate_merit",
|
||||
"unlock_gen",
|
||||
"unlock_gate",
|
||||
"palette_id",
|
||||
"is_prestige_gate"
|
||||
],
|
||||
"rows": [
|
||||
{
|
||||
"id": "hall_01_qinguang",
|
||||
"no": 1,
|
||||
"king_cn": "秦广王",
|
||||
"king_en": "King Qinguang",
|
||||
"motif_cn": "孽镜台(照心)",
|
||||
"motif_en": "Mirror of Karma",
|
||||
"target_min": 25,
|
||||
"gate_merit": 2000,
|
||||
"unlock_gen": "gen_03_guizu",
|
||||
"unlock_gate": "up_gate_guizu",
|
||||
"palette_id": "pal_hall_01",
|
||||
"is_prestige_gate": "false"
|
||||
},
|
||||
{
|
||||
"id": "hall_02_chujiang",
|
||||
"no": 2,
|
||||
"king_cn": "楚江王",
|
||||
"king_en": "King Chujiang",
|
||||
"motif_cn": "寒冰狱",
|
||||
"motif_en": "Ice Hell",
|
||||
"target_min": 42,
|
||||
"gate_merit": 30000,
|
||||
"unlock_gen": "gen_04_biliao",
|
||||
"unlock_gate": "up_gate_biliao",
|
||||
"palette_id": "pal_hall_02",
|
||||
"is_prestige_gate": "false"
|
||||
},
|
||||
{
|
||||
"id": "hall_03_songdi",
|
||||
"no": 3,
|
||||
"king_cn": "宋帝王",
|
||||
"king_en": "King Songdi",
|
||||
"motif_cn": "剥皮亭",
|
||||
"motif_en": "Flaying Pavilion",
|
||||
"target_min": 62,
|
||||
"gate_merit": 300000,
|
||||
"unlock_gen": "gen_05_tangpu",
|
||||
"unlock_gate": "up_gate_tangpu",
|
||||
"palette_id": "pal_hall_03",
|
||||
"is_prestige_gate": "false"
|
||||
},
|
||||
{
|
||||
"id": "hall_04_wuguan",
|
||||
"no": 4,
|
||||
"king_cn": "五官王",
|
||||
"king_en": "King Wuguan",
|
||||
"motif_cn": "沸汤/毒蛇",
|
||||
"motif_en": "Scalding Cauldron",
|
||||
"target_min": 84,
|
||||
"gate_merit": 2000000,
|
||||
"unlock_gen": "gen_06_nanmin",
|
||||
"unlock_gate": "up_gate_nanmin",
|
||||
"palette_id": "pal_hall_04",
|
||||
"is_prestige_gate": "false"
|
||||
},
|
||||
{
|
||||
"id": "hall_05_yama",
|
||||
"no": 5,
|
||||
"king_cn": "阎罗王",
|
||||
"king_en": "King Yama",
|
||||
"motif_cn": "叫唤地狱/铁床",
|
||||
"motif_en": "Wailing Hell",
|
||||
"target_min": 105,
|
||||
"gate_merit": 10000000,
|
||||
"unlock_gen": "gen_07_shuban",
|
||||
"unlock_gate": "up_gate_shuban",
|
||||
"palette_id": "pal_hall_05",
|
||||
"is_prestige_gate": "false"
|
||||
},
|
||||
{
|
||||
"id": "hall_06_biancheng",
|
||||
"no": 6,
|
||||
"king_cn": "卞城王",
|
||||
"king_en": "King Biancheng",
|
||||
"motif_cn": "石碾",
|
||||
"motif_en": "Stone Roller",
|
||||
"target_min": 125,
|
||||
"gate_merit": 40000000,
|
||||
"unlock_gen": "gen_08_bingfang",
|
||||
"unlock_gate": "up_gate_bingfang",
|
||||
"palette_id": "pal_hall_06",
|
||||
"is_prestige_gate": "false"
|
||||
},
|
||||
{
|
||||
"id": "hall_07_taishan",
|
||||
"no": 7,
|
||||
"king_cn": "泰山王",
|
||||
"king_en": "King Taishan",
|
||||
"motif_cn": "热油火坑",
|
||||
"motif_en": "Boiling Oil Pit",
|
||||
"target_min": 143,
|
||||
"gate_merit": 120000000,
|
||||
"unlock_gen": "gen_09_wodi",
|
||||
"unlock_gate": "up_gate_wodi",
|
||||
"palette_id": "pal_hall_07",
|
||||
"is_prestige_gate": "false"
|
||||
},
|
||||
{
|
||||
"id": "hall_08_dushi",
|
||||
"no": 8,
|
||||
"king_cn": "都市王",
|
||||
"king_en": "King Dushi",
|
||||
"motif_cn": "泥犁/大石",
|
||||
"motif_en": "Avici Stone",
|
||||
"target_min": 160,
|
||||
"gate_merit": 350000000,
|
||||
"unlock_gen": "gen_10_xingzhe",
|
||||
"unlock_gate": "up_gate_xingzhe",
|
||||
"palette_id": "pal_hall_08",
|
||||
"is_prestige_gate": "false"
|
||||
},
|
||||
{
|
||||
"id": "hall_09_pingdeng",
|
||||
"no": 9,
|
||||
"king_cn": "平等王",
|
||||
"king_en": "King Pingdeng",
|
||||
"motif_cn": "拔舌/铁树",
|
||||
"motif_en": "Tongue-Ripping Iron Tree",
|
||||
"target_min": 175,
|
||||
"gate_merit": 800000000,
|
||||
"unlock_gen": "gen_11_yinhun",
|
||||
"unlock_gate": "up_gate_yinhun",
|
||||
"palette_id": "pal_hall_09",
|
||||
"is_prestige_gate": "false"
|
||||
},
|
||||
{
|
||||
"id": "hall_10_zhuanlun",
|
||||
"no": 10,
|
||||
"king_cn": "转轮王",
|
||||
"king_en": "King of the Wheel",
|
||||
"motif_cn": "轮回道(六道环)",
|
||||
"motif_en": "Wheel of Rebirth",
|
||||
"target_min": 180,
|
||||
"gate_merit": 1500000000,
|
||||
"unlock_gen": "none",
|
||||
"unlock_gate": "none",
|
||||
"palette_id": "pal_hall_10",
|
||||
"is_prestige_gate": "true"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
{
|
||||
"sheet": "meta_upgrades",
|
||||
"schema_version": 0,
|
||||
"columns": [
|
||||
"id",
|
||||
"name_cn",
|
||||
"name_en",
|
||||
"effect_type",
|
||||
"target_id",
|
||||
"effect_param",
|
||||
"cost_suihui",
|
||||
"max_rank",
|
||||
"condition",
|
||||
"desc_cn"
|
||||
],
|
||||
"rows": [
|
||||
{
|
||||
"id": "meta_01",
|
||||
"name_cn": "枕旧梦",
|
||||
"name_en": "Pillow of Old Dreams",
|
||||
"effect_type": "start_levels",
|
||||
"target_id": "gen_01_youhun",
|
||||
"effect_param": 10,
|
||||
"cost_suihui": 3,
|
||||
"max_rank": 1,
|
||||
"condition": "none",
|
||||
"desc_cn": "新轮回开局自带游魂 ×10 级"
|
||||
},
|
||||
{
|
||||
"id": "meta_02",
|
||||
"name_cn": "携阴兵",
|
||||
"name_en": "Bring Ghost Soldiers",
|
||||
"effect_type": "start_levels",
|
||||
"target_id": "gen_02_yinbing",
|
||||
"effect_param": 10,
|
||||
"cost_suihui": 5,
|
||||
"max_rank": 1,
|
||||
"condition": "none",
|
||||
"desc_cn": "开局自带阴兵 ×10 级"
|
||||
},
|
||||
{
|
||||
"id": "meta_03",
|
||||
"name_cn": "宿慧通透",
|
||||
"name_en": "Perspicacious Wisdom",
|
||||
"effect_type": "suihui_gain_pct",
|
||||
"target_id": "",
|
||||
"effect_param": 0.15,
|
||||
"cost_suihui": 8,
|
||||
"max_rank": 3,
|
||||
"condition": "none",
|
||||
"desc_cn": "宿慧获取 +15%(每阶)"
|
||||
},
|
||||
{
|
||||
"id": "meta_04",
|
||||
"name_cn": "天生熟练",
|
||||
"name_en": "Born Skilled",
|
||||
"effect_type": "skip_interval",
|
||||
"target_id": "",
|
||||
"effect_param": 4,
|
||||
"cost_suihui": 12,
|
||||
"max_rank": 1,
|
||||
"condition": "none",
|
||||
"desc_cn": "跳过机制从每 5 级加快为每 4 级"
|
||||
},
|
||||
{
|
||||
"id": "meta_05",
|
||||
"name_cn": "阴司人脉",
|
||||
"name_en": "Netherworld Connections",
|
||||
"effect_type": "offline_pct",
|
||||
"target_id": "",
|
||||
"effect_param": 0.25,
|
||||
"cost_suihui": 6,
|
||||
"max_rank": 1,
|
||||
"condition": "none",
|
||||
"desc_cn": "离线效率 +25%"
|
||||
},
|
||||
{
|
||||
"id": "meta_06",
|
||||
"name_cn": "斩鬼刃",
|
||||
"name_en": "Ghost-Slaying Blade",
|
||||
"effect_type": "boss_click_pct",
|
||||
"target_id": "",
|
||||
"effect_param": 0.3,
|
||||
"cost_suihui": 10,
|
||||
"max_rank": 1,
|
||||
"condition": "none",
|
||||
"desc_cn": "殿王战点击伤害 +30%"
|
||||
},
|
||||
{
|
||||
"id": "meta_07",
|
||||
"name_cn": "心安理得",
|
||||
"name_en": "Easy Conscience",
|
||||
"effect_type": "wrath_penalty_set",
|
||||
"target_id": "",
|
||||
"effect_param": 0.15,
|
||||
"cost_suihui": 15,
|
||||
"max_rank": 1,
|
||||
"condition": "none",
|
||||
"desc_cn": "冤气对宿慧的削减 30%→15%"
|
||||
},
|
||||
{
|
||||
"id": "meta_08",
|
||||
"name_cn": "敕封恩典",
|
||||
"name_en": "Grace of Enthronement",
|
||||
"effect_type": "global_pct",
|
||||
"target_id": "",
|
||||
"effect_param": 0.25,
|
||||
"cost_suihui": 20,
|
||||
"max_rank": 2,
|
||||
"condition": "none",
|
||||
"desc_cn": "全局 +25%(每阶)"
|
||||
},
|
||||
{
|
||||
"id": "meta_09",
|
||||
"name_cn": "阴阳眼",
|
||||
"name_en": "Yin-Yang Eye",
|
||||
"effect_type": "event_freq_pct",
|
||||
"target_id": "",
|
||||
"effect_param": 0.2,
|
||||
"cost_suihui": 10,
|
||||
"max_rank": 1,
|
||||
"condition": "none",
|
||||
"desc_cn": "事件触发频率 +20%"
|
||||
},
|
||||
{
|
||||
"id": "meta_10",
|
||||
"name_cn": "超度加速",
|
||||
"name_en": "Salvation Acceleration",
|
||||
"effect_type": "souls_mult",
|
||||
"target_id": "",
|
||||
"effect_param": 2,
|
||||
"cost_suihui": 8,
|
||||
"max_rank": 1,
|
||||
"condition": "none",
|
||||
"desc_cn": "超度计数获取 ×2"
|
||||
},
|
||||
{
|
||||
"id": "meta_11",
|
||||
"name_cn": "老马识途",
|
||||
"name_en": "Old Horse Knows the Way",
|
||||
"effect_type": "bulk_unlock",
|
||||
"target_id": "",
|
||||
"effect_param": 0,
|
||||
"cost_suihui": 5,
|
||||
"max_rank": 1,
|
||||
"condition": "none",
|
||||
"desc_cn": "开局即可用批量 ×10"
|
||||
},
|
||||
{
|
||||
"id": "meta_12",
|
||||
"name_cn": "幽冥权柄",
|
||||
"name_en": "Authority of the Netherworld",
|
||||
"effect_type": "conditional_global_pct",
|
||||
"target_id": "gen_12_tianzi",
|
||||
"effect_param": 0.1,
|
||||
"cost_suihui": 25,
|
||||
"max_rank": 1,
|
||||
"condition": "gen12_unlocked",
|
||||
"desc_cn": "愿景档(gen12)解锁后全局再 +10%"
|
||||
}
|
||||
]
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "balance_tuning.schema.json",
|
||||
"title": "balance_tuning 表 schema v0(全局调参数,Balance Bible v1.0 §1/§3.2/§4/§8)",
|
||||
"description": "架构 §2.1 已列名;容纳七张内容表无归属行的全局参数:三乘区软上限/点击基础值([B§1/§2.2])、离线衰减表 + 72h 封顶([B§8])、冤煞层 22 层公式参数([B§3.2])、宿慧公式/跳过机制/敕封触发/重开曲线([B§4])。kind=number 时 num 有效;kind=text 时 text 有效(另一列填 0/空串)。新增参数 = 加行(稳定 ID)。",
|
||||
"x-supported-keywords": [
|
||||
"type", "properties", "required", "items", "enum", "pattern",
|
||||
"minimum", "maximum", "minLength", "minItems", "maxItems",
|
||||
"additionalProperties", "const"
|
||||
],
|
||||
"x-unique": ["id"],
|
||||
"type": "object",
|
||||
"required": ["sheet", "schema_version", "columns", "rows"],
|
||||
"properties": {
|
||||
"sheet": { "const": "balance_tuning" },
|
||||
"schema_version": { "const": 0 },
|
||||
"columns": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" },
|
||||
"minItems": 5
|
||||
},
|
||||
"rows": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["id", "kind", "num", "text", "desc_cn"],
|
||||
"properties": {
|
||||
"id": { "type": "string", "pattern": "^tune_[a-z0-9_]+$", "description": "稳定字符串 ID(永不重编号)" },
|
||||
"kind": { "type": "string", "enum": ["number", "text"] },
|
||||
"num": { "type": "number", "minimum": 0, "description": "kind=number 时的值;kind=text 时填 0" },
|
||||
"text": { "type": "string", "description": "kind=text 时的值(如重开曲线 1:0.6:0.4:0.3);kind=number 时为空串" },
|
||||
"desc_cn": { "type": "string", "minLength": 1 }
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
{
|
||||
"$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。",
|
||||
"x-supported-keywords": [
|
||||
"type", "properties", "required", "items", "enum", "pattern",
|
||||
"minimum", "maximum", "minLength", "minItems", "maxItems",
|
||||
"additionalProperties", "const"
|
||||
],
|
||||
"x-unique": ["id", "hall_id"],
|
||||
"type": "object",
|
||||
"required": ["sheet", "schema_version", "columns", "rows"],
|
||||
"properties": {
|
||||
"sheet": { "const": "boss_battles" },
|
||||
"schema_version": { "const": 0 },
|
||||
"columns": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" },
|
||||
"minItems": 14
|
||||
},
|
||||
"rows": {
|
||||
"type": "array",
|
||||
"minItems": 10,
|
||||
"maxItems": 10,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"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"
|
||||
],
|
||||
"properties": {
|
||||
"id": { "type": "string", "pattern": "^boss_hall_[0-9]{2}$" },
|
||||
"hall_id": {
|
||||
"type": "string",
|
||||
"pattern": "^hall_[0-9]{2}_[a-z]+$",
|
||||
"description": "halls.csv 行 ID(每殿恰一场)"
|
||||
},
|
||||
"hall_factor": {
|
||||
"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)"
|
||||
},
|
||||
"hp_factor": {
|
||||
"const": 15,
|
||||
"description": "HP 系数([B§5]:15×产出/s×殿系数;[B§11] 由 GDD 50 修正)"
|
||||
},
|
||||
"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)" },
|
||||
"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%)" },
|
||||
"retry_cooldown_sec": { "type": "integer", "minimum": 0, "description": "失败后再战冷却(60s;H7 前败即等推 → up_gate_boss_retry)" },
|
||||
"reward_hp_ratio": { "type": "number", "minimum": 0, "maximum": 1, "description": "通过奖励阴德爆发 = HP × 本值(0.2)" }
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "events.schema.json",
|
||||
"title": "events 表 schema v0(4 事件,数值抄录自 Balance Bible v1.0 §7)",
|
||||
"description": "架构 §3.2 CSV 规范落盘。事件倍率走独立瞬时区,不进全局乘区([B§0.3/§7])。参数列语义(按 type 取用):p_mult=output_mult 全产出倍率 / click_output_mult 产出倍率部分;p_clicks=click_challenge 需点击次数 / click_output_mult 点击倍率 / choice 冤气增益下限;p_window_sec=click_challenge 窗口秒 / choice 冤气增益上限(随殿序 lerp);p_penalty+p_penalty_dur_sec=click_challenge 失败惩罚;opt_a_*/opt_b_*=choice 二选一(merit_burst_sec=阴德爆发秒数 / wrath_gain=冤气增益)。出现前提 = up_gate_events(过 H2,[B§2.3])。冤魂申诉超时默认 opt_a(超度)。",
|
||||
"x-supported-keywords": [
|
||||
"type", "properties", "required", "items", "enum", "pattern",
|
||||
"minimum", "maximum", "minLength", "minItems", "maxItems",
|
||||
"additionalProperties", "const"
|
||||
],
|
||||
"x-unique": ["id"],
|
||||
"type": "object",
|
||||
"required": ["sheet", "schema_version", "columns", "rows"],
|
||||
"properties": {
|
||||
"sheet": { "const": "events" },
|
||||
"schema_version": { "const": 0 },
|
||||
"columns": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" },
|
||||
"minItems": 17
|
||||
},
|
||||
"rows": {
|
||||
"type": "array",
|
||||
"minItems": 4,
|
||||
"maxItems": 4,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id", "name_cn", "name_en", "trigger_min_min", "trigger_max_min",
|
||||
"duration_sec", "type", "p_mult", "p_clicks", "p_window_sec",
|
||||
"p_penalty", "p_penalty_dur_sec", "opt_a_type", "opt_a_param",
|
||||
"opt_b_type", "opt_b_param", "flavor_cn"
|
||||
],
|
||||
"properties": {
|
||||
"id": { "type": "string", "pattern": "^ev_[0-9]{2}_[a-z]+$" },
|
||||
"name_cn": { "type": "string", "minLength": 1 },
|
||||
"name_en": { "type": "string", "minLength": 1, "description": "临时译名,待文策渊 glossary" },
|
||||
"trigger_min_min": { "type": "integer", "minimum": 1, "description": "触发间隔下限(分钟)" },
|
||||
"trigger_max_min": { "type": "integer", "minimum": 1, "description": "触发间隔上限(分钟,含)" },
|
||||
"duration_sec": { "type": "integer", "minimum": 1, "description": "持续秒数(冤魂申诉为非阻塞决策窗)" },
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["output_mult", "click_challenge", "choice", "click_output_mult"]
|
||||
},
|
||||
"p_mult": { "type": "number", "minimum": 0 },
|
||||
"p_clicks": { "type": "integer", "minimum": 0 },
|
||||
"p_window_sec": { "type": "number", "minimum": 0 },
|
||||
"p_penalty": { "type": "number", "minimum": 0, "maximum": 1, "description": "click_challenge 失败惩罚倍率(0.5 = -50%)" },
|
||||
"p_penalty_dur_sec": { "type": "integer", "minimum": 0 },
|
||||
"opt_a_type": { "type": "string", "enum": ["none", "merit_burst_sec", "wrath_gain"] },
|
||||
"opt_a_param": { "type": "number", "minimum": 0 },
|
||||
"opt_b_type": { "type": "string", "enum": ["none", "merit_burst_sec", "wrath_gain"] },
|
||||
"opt_b_param": { "type": "number", "minimum": 0 },
|
||||
"flavor_cn": { "type": "string", "minLength": 1 }
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "generators.schema.json",
|
||||
"title": "generators 表 schema v0(十二班底发生器,数值抄录自 GDD §3.1)",
|
||||
"description": "架构 §3.2 CSV 规范落盘。数值列禁千分位;引用列填目标 ID 或 none;ID 一旦分配永不重编号。v0 仅做单表校验:unlock_hall/unlock_gate 引用的 halls.csv / upgrades.csv 目标表 W2 落盘,届时激活跨表引用完整性校验。",
|
||||
"title": "generators 表 schema v0(十二班底发生器,数值抄录自 Balance Bible v1.0 §1)",
|
||||
"description": "架构 §3.2 CSV 规范落盘。数值列禁千分位;引用列填目标 ID 或 none;ID 一旦分配永不重编号。速率 = 8 的纯幂阶梯(gen1=0.1;gen_n=8^(n-2),[B§1]/[B§11 差异#1] 修复 GDD 6.25× 异常档);成本 gen1→gen2 为 8×(点击养成档,设计意图),gen2 起恒 10×。v0 仅做单表校验:unlock_hall/unlock_gate 引用的 halls.csv / upgrades.csv 目标表已落盘(W2),跨表引用完整性由 DataRegistry 载入时检查。",
|
||||
"x-supported-keywords": [
|
||||
"type", "properties", "required", "items", "enum", "pattern",
|
||||
"minimum", "maximum", "minLength", "minItems", "maxItems",
|
||||
@@ -68,7 +68,7 @@
|
||||
"unlock_gate": {
|
||||
"type": "string",
|
||||
"pattern": "^(none|up_gate_[a-z0-9_]+)$",
|
||||
"description": "路引闸门 ID(upgrades.csv 行 ID)或 none;gen_12 的敕封前置(宿慧≥60 且转世≥2)属 upgrades.csv 行内规则,W2 建模"
|
||||
"description": "路引闸门 ID(upgrades.csv 行 ID)或 none;gen_12 的敕封前置 = up_gate_tianzi 需转世≥2([B§4.4],W2 已建模为 upgrades.csv 行内条件)"
|
||||
},
|
||||
"sprite_row": {
|
||||
"type": "string",
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "hall_palettes.schema.json",
|
||||
"title": "hall_palettes 表 schema v0(per-hall 色板行,架构 §5.3 + Balance Bible v1.0 §6 色板列)",
|
||||
"description": "架构 §3.2 CSV 规范落盘。一行 = 一殿:水墨四色(墨/骨白/朱砂/靛)+ 该殿色温 accent + 流向向量 + LUT 索引 + 四色面积纪律百分比。hex 为工程起始值,W5 look-dev 冻结评审按截图直方图校准(架构 §10.1 评审清单 #1)。lut_generator.tres 消费本表。",
|
||||
"x-supported-keywords": [
|
||||
"type", "properties", "required", "items", "enum", "pattern",
|
||||
"minimum", "maximum", "minLength", "minItems", "maxItems",
|
||||
"additionalProperties", "const"
|
||||
],
|
||||
"x-unique": ["id", "hall_id", "lut_index"],
|
||||
"type": "object",
|
||||
"required": ["sheet", "schema_version", "columns", "rows"],
|
||||
"properties": {
|
||||
"sheet": { "const": "hall_palettes" },
|
||||
"schema_version": { "const": 0 },
|
||||
"columns": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" },
|
||||
"minItems": 15
|
||||
},
|
||||
"rows": {
|
||||
"type": "array",
|
||||
"minItems": 10,
|
||||
"maxItems": 10,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"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"
|
||||
],
|
||||
"properties": {
|
||||
"id": { "type": "string", "pattern": "^pal_hall_[0-9]{2}$" },
|
||||
"hall_id": {
|
||||
"type": "string",
|
||||
"pattern": "^hall_[0-9]{2}_[a-z]+$",
|
||||
"description": "halls.csv 行 ID(每殿恰一行)"
|
||||
},
|
||||
"name_cn": { "type": "string", "minLength": 1, "description": "色板行名([B§6]:冷靛/冰青/暗红/锈橙/玄黑/铅灰/烬红/墨绿/黛紫/骨白+金)" },
|
||||
"ink_hex": { "type": "string", "pattern": "^#[0-9A-Fa-f]{6}$", "description": "墨(默认 #1A1A1A,架构 §7.3 管线规格)" },
|
||||
"bone_hex": { "type": "string", "pattern": "^#[0-9A-Fa-f]{6}$", "description": "骨白(纸底)" },
|
||||
"cinnabar_hex": { "type": "string", "pattern": "^#[0-9A-Fa-f]{6}$", "description": "朱砂(强调,面积 ≤8%)" },
|
||||
"indigo_hex": { "type": "string", "pattern": "^#[0-9A-Fa-f]{6}$", "description": "靛(次强调,面积 ≤7%;阴德方印编码用)" },
|
||||
"accent_hex": { "type": "string", "pattern": "^#[0-9A-Fa-f]{6}$", "description": "该殿色温 accent(工程起始值,W5 校准)" },
|
||||
"accent_name_cn": { "type": "string", "minLength": 1 },
|
||||
"flow": {
|
||||
"type": "string",
|
||||
"enum": ["seep_down", "bleed_left"],
|
||||
"description": "纸纹流向向量:自上而下渗 vs 向左横溢洇(架构 §5.3;工程起始值,W5 校准)"
|
||||
},
|
||||
"lut_index": { "type": "integer", "minimum": 1, "maximum": 10, "description": "LUT 索引(assets/luts/hall_XX.png)" },
|
||||
"ink_pct": { "type": "integer", "minimum": 0, "maximum": 100, "description": "墨面积目标%([B] 65)" },
|
||||
"bone_pct": { "type": "integer", "minimum": 0, "maximum": 100, "description": "骨白面积目标%([B] 25)" },
|
||||
"cinnabar_max_pct": { "type": "integer", "minimum": 0, "maximum": 100, "description": "朱砂面积硬顶([B] ≤8)" },
|
||||
"indigo_max_pct": { "type": "integer", "minimum": 0, "maximum": 100, "description": "靛面积硬顶([B] ≤7)" }
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "halls.schema.json",
|
||||
"title": "halls 表 schema v0(十殿 band 数据行,数值抄录自 Balance Bible v1.0 §6)",
|
||||
"description": "架构 §3.2 CSV 规范落盘。每殿 = 一个横向 band(2-3 屏宽 @1920,四层组装)。殿门 = 单计数器闸门(本轮累计阴德 merit_run ≥ gate_merit,[B§0.1]);is_prestige_gate=true 的行是轮回转世触发门(仅 H10)。unlock_gen/unlock_gate = 过殿后解锁批。",
|
||||
"x-supported-keywords": [
|
||||
"type", "properties", "required", "items", "enum", "pattern",
|
||||
"minimum", "maximum", "minLength", "minItems", "maxItems",
|
||||
"additionalProperties", "const"
|
||||
],
|
||||
"x-unique": ["id"],
|
||||
"type": "object",
|
||||
"required": ["sheet", "schema_version", "columns", "rows"],
|
||||
"properties": {
|
||||
"sheet": { "const": "halls" },
|
||||
"schema_version": { "const": 0 },
|
||||
"columns": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" },
|
||||
"minItems": 12
|
||||
},
|
||||
"rows": {
|
||||
"type": "array",
|
||||
"minItems": 10,
|
||||
"maxItems": 10,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id", "no", "king_cn", "king_en", "motif_cn", "motif_en",
|
||||
"target_min", "gate_merit", "unlock_gen", "unlock_gate",
|
||||
"palette_id", "is_prestige_gate"
|
||||
],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"pattern": "^hall_[0-9]{2}_[a-z]+$",
|
||||
"description": "稳定字符串 ID(W1 已分配);永不重编号(年更=加行)"
|
||||
},
|
||||
"no": { "type": "integer", "minimum": 1, "maximum": 10 },
|
||||
"king_cn": { "type": "string", "minLength": 1 },
|
||||
"king_en": { "type": "string", "minLength": 1, "description": "临时译名,待文策渊 glossary" },
|
||||
"motif_cn": { "type": "string", "minLength": 1, "description": "刑典母题(剪影+文字暗示路线,GDD §0.3 IP 红线)" },
|
||||
"motif_en": { "type": "string", "minLength": 1 },
|
||||
"target_min": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"description": "到达目标时长(分钟,[B§6] pacing 表:25→180min);模拟器门 #3/#5/#14 对照"
|
||||
},
|
||||
"gate_merit": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"description": "殿门条件:本轮累计阴德(单计数器;[B§6] 2K→1.5B)"
|
||||
},
|
||||
"unlock_gen": {
|
||||
"type": "string",
|
||||
"pattern": "^(none|gen_[0-9]{2}_[a-z0-9]+)$",
|
||||
"description": "过殿解锁的班底 ID;H10 = none(prestige 门)"
|
||||
},
|
||||
"unlock_gate": {
|
||||
"type": "string",
|
||||
"pattern": "^(none|up_gate_[a-z0-9_]+)$",
|
||||
"description": "过殿出现的路引闸门 ID(upgrades.csv);H10 = none"
|
||||
},
|
||||
"palette_id": {
|
||||
"type": "string",
|
||||
"pattern": "^pal_hall_[0-9]{2}$",
|
||||
"description": "hall_palettes.csv 行 ID(per-hall 色板 + 流向向量)"
|
||||
},
|
||||
"is_prestige_gate": {
|
||||
"type": "boolean",
|
||||
"description": "true = 轮回转世触发门([B§4.1]:仅 H10 转轮王)"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "meta_upgrades.schema.json",
|
||||
"title": "meta_upgrades 表 schema v0(12 条宿慧商店,数值抄录自 Balance Bible v1.0 §2.5)",
|
||||
"description": "架构 §3.2 CSV 规范落盘。货币 = 宿慧(跨轮回永久,永不离场——身份资产层)。max_rank>1 可重复购买(每阶独立计费)。condition=gen12_unlocked 表示效果仅 gen12 解锁后生效(meta_12)。effect_type 语义:start_levels=新轮回开局 target_gen 自带 param 级;suihui_gain_pct=宿慧获取 +param;skip_interval=跳过机制间隔改为 param 级;offline_pct=离线效率 +param;boss_click_pct=殿王战点击伤害 +param;wrath_penalty_set=冤气削减设为 param(替代默认 0.3);global_pct=全局 +param/阶;event_freq_pct=事件频率 +param;souls_mult=超度计数 ×param;bulk_unlock=开局批量×10;conditional_global_pct=条件全局 +param。",
|
||||
"x-supported-keywords": [
|
||||
"type", "properties", "required", "items", "enum", "pattern",
|
||||
"minimum", "maximum", "minLength", "minItems", "maxItems",
|
||||
"additionalProperties", "const"
|
||||
],
|
||||
"x-unique": ["id"],
|
||||
"type": "object",
|
||||
"required": ["sheet", "schema_version", "columns", "rows"],
|
||||
"properties": {
|
||||
"sheet": { "const": "meta_upgrades" },
|
||||
"schema_version": { "const": 0 },
|
||||
"columns": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" },
|
||||
"minItems": 10
|
||||
},
|
||||
"rows": {
|
||||
"type": "array",
|
||||
"minItems": 12,
|
||||
"maxItems": 12,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id", "name_cn", "name_en", "effect_type", "target_id",
|
||||
"effect_param", "cost_suihui", "max_rank", "condition", "desc_cn"
|
||||
],
|
||||
"properties": {
|
||||
"id": { "type": "string", "pattern": "^meta_[0-9]{2}$" },
|
||||
"name_cn": { "type": "string", "minLength": 1 },
|
||||
"name_en": { "type": "string", "minLength": 1, "description": "临时译名,待文策渊 glossary" },
|
||||
"effect_type": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"start_levels", "suihui_gain_pct", "skip_interval", "offline_pct",
|
||||
"boss_click_pct", "wrath_penalty_set", "global_pct", "event_freq_pct",
|
||||
"souls_mult", "bulk_unlock", "conditional_global_pct"
|
||||
]
|
||||
},
|
||||
"target_id": {
|
||||
"type": "string",
|
||||
"pattern": "^(|gen_[0-9]{2}_[a-z0-9]+)$",
|
||||
"description": "start_levels/conditional_global_pct 的目标班底 ID;其余为空串"
|
||||
},
|
||||
"effect_param": { "type": "number", "minimum": 0 },
|
||||
"cost_suihui": { "type": "integer", "minimum": 1, "description": "宿慧成本([B§2.5] 钉值 3-25)" },
|
||||
"max_rank": { "type": "integer", "minimum": 1, "description": "可购买阶数上限(m03=3、m08=2,其余 1)" },
|
||||
"condition": {
|
||||
"type": "string",
|
||||
"enum": ["none", "gen12_unlocked"],
|
||||
"description": "效果生效条件"
|
||||
},
|
||||
"desc_cn": { "type": "string", "minLength": 1 }
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "upgrades.schema.json",
|
||||
"title": "upgrades 表 schema v0(108 条升级,数值抄录自 Balance Bible v1.0 §2)",
|
||||
"description": "架构 §3.2 CSV 规范落盘。配比 [B§2]:效率 72(12 班底×6 级,+100%/级加法,成本 C0×r^(10+4n) 取整)+ 点击 12(L1-4 ×2;L5-12 +1% 产出/s)+ 路引 18(10 入职文书 + 8 系统闸门,纯解锁零效果)+ 离线 6(×1.15/级)。与 meta_upgrades.csv 合计 120。ID 稳定永不重编号;display_en 为临时译名待文策渊 glossary。requires=enthrone_ready 表示敕封动作就绪(转世≥2 且宿慧≥60)后才可购(仅 up_gate_tianzi,provenance P-12;W2 初版误置 enthroned 会造成死锁,已修)。",
|
||||
"x-supported-keywords": [
|
||||
"type", "properties", "required", "items", "enum", "pattern",
|
||||
"minimum", "maximum", "minLength", "minItems", "maxItems",
|
||||
"additionalProperties", "const"
|
||||
],
|
||||
"x-unique": ["id"],
|
||||
"type": "object",
|
||||
"required": ["sheet", "schema_version", "columns", "rows"],
|
||||
"properties": {
|
||||
"sheet": { "const": "upgrades" },
|
||||
"schema_version": { "const": 0 },
|
||||
"columns": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" },
|
||||
"minItems": 11
|
||||
},
|
||||
"rows": {
|
||||
"type": "array",
|
||||
"minItems": 108,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id", "name_cn", "name_en", "type", "target_gen", "unlock_hall",
|
||||
"effect_type", "effect_param", "requires", "cost", "desc_cn"
|
||||
],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"pattern": "^up_[a-z0-9_]+$",
|
||||
"description": "稳定字符串 ID:up_eff_<gen_id>_<1-6> / up_click_<1-12> / up_offline_<1-6> / up_gate_<name>(入职文书 ID 与 generators.unlock_gate 引用一致,W1 已分配)"
|
||||
},
|
||||
"name_cn": { "type": "string", "minLength": 1 },
|
||||
"name_en": { "type": "string", "minLength": 1, "description": "临时译名,待文策渊 glossary" },
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["efficiency", "click", "gate_hire", "gate_system", "offline"],
|
||||
"description": "efficiency=班底效率;click=点击;gate_hire=班底入职文书(target_gen 解锁);gate_system=系统闸门;offline=离线效率"
|
||||
},
|
||||
"target_gen": {
|
||||
"type": "string",
|
||||
"pattern": "^(none|gen_[0-9]{2}_[a-z0-9]+)$",
|
||||
"description": "efficiency/gate_hire 指向的班底 ID(generators.csv);其余 none"
|
||||
},
|
||||
"unlock_hall": {
|
||||
"type": "string",
|
||||
"pattern": "^(none|hall_[0-9]{2}_[a-z]+)$",
|
||||
"description": "出现条件:通过哪殿后可见/可购;efficiency 随所属班底殿门,offline 随 H4(离线收益系统闸门),click 无门"
|
||||
},
|
||||
"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)"
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"requires": {
|
||||
"type": "string",
|
||||
"enum": ["none", "enthrone_ready", "enthroned"],
|
||||
"description": "购买前置条件:none=无;enthrone_ready=转世≥2 且宿慧≥60(敕封动作就绪,仅 up_gate_tianzi,provenance P-12);enthroned=需已受敕(预留枚举,现无行使用)"
|
||||
},
|
||||
"cost": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"description": "阴德成本(禁千分位)。效率 = round(C0×r^(10+4n));入职文书 = round(C0×2/3)(g03=800 钉值复现);系统闸门 = 殿门阈值÷2(工程外推, provenance P-5);点击/离线 = [B§2.2/§2.4] 钉值"
|
||||
},
|
||||
"desc_cn": { "type": "string", "minLength": 1 }
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
id,kind,num,text,desc_cn
|
||||
tune_click_base,number,1,,[B§2.2] 点击基础值 = 1(与 gen1 成本 15 配对 → 15 次点击首购)
|
||||
tune_click_zone_cap,number,16,,[B§1/§2.2] 点击区软上限 ×16(4 级 ×2 满)
|
||||
tune_gen_zone_cap,number,8,,[B§1] 班底乘区软上限 ×8(6 级效率满 = ×7,留 1 级余量)
|
||||
tune_global_zone_cap,number,64,,[B§1] 全局乘区软上限 ×64
|
||||
tune_offline_band1_h,number,2,,[B§8] 离线 0-2h 速率 100%
|
||||
tune_offline_band1_rate,number,1,,[B§8] 离线 0-2h 速率
|
||||
tune_offline_band2_h,number,8,,[B§8] 离线 2-8h 速率 70%
|
||||
tune_offline_band2_rate,number,0.7,,[B§8] 离线 2-8h 速率
|
||||
tune_offline_band3_h,number,24,,[B§8] 离线 8-24h 速率 40%
|
||||
tune_offline_band3_rate,number,0.4,,[B§8] 离线 8-24h 速率
|
||||
tune_offline_band4_rate,number,0.1,,[B§8] 离线 >24h 速率 10%
|
||||
tune_offline_cap_h,number,72,,[B§8] 离线硬上限 72h 封顶
|
||||
tune_wrath_layers,number,22,,[B§3.2] 冤煞层总数 22
|
||||
tune_wrath_cost_base,number,8,,[B§3.2] 第 n 层成本 = round(8 × n^1.8)
|
||||
tune_wrath_cost_exp,number,1.8,,[B§3.2] 冤煞层成本指数
|
||||
tune_wrath_bonus,number,0.25,,[B§3.2] 每层全局 +25%(加法,进全局乘区)
|
||||
tune_wrath_penalty,number,0.03,,[B§3.2] 每层宿慧获取削减 +3%
|
||||
tune_wrath_penalty_cap,number,0.3,,[B§3.2] 宿慧削减封顶 30%
|
||||
tune_suihui_div,number,1000000,,[B§4.1] 宿慧 = floor(√(本轮累计阴德/1e6) × 5)
|
||||
tune_suihui_mult,number,5,,[B§4.1] 宿慧公式乘数
|
||||
tune_suihui_global_pct,number,0.03,,[B§1/§4.2] 每级宿慧全局产出 +3%(加法,进全局乘区)
|
||||
tune_enthrone_bonus,number,0.25,,[B§4.4] 阴司敕封自动全局恩典 +25%(与 m08 商品字面重叠,见 provenance P-4)
|
||||
tune_skip_interval,number,5,,[B§4.2] 每 5 级宿慧 → 前 3 档班底各 +25 级(m04 可加快为 4)
|
||||
tune_skip_gens,number,3,,[B§4.2] 跳过机制覆盖前 3 档班底
|
||||
tune_skip_levels,number,25,,[B§4.2] 跳过机制每档 +25 级
|
||||
tune_enthrone_rebirths,number,2,,[B§4.4] 阴司敕封触发:转世 ≥2(约第 2-3 轮,累计 8-12h 为其 pacing 后果)
|
||||
tune_restart_curve,text,0,1:0.6:0.4:0.3,[B§4.3] 重开曲线目标(第 5 轮触底 ≥0.25×);模拟器门 #13 断言
|
||||
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 数值)
|
||||
|
@@ -0,0 +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
|
||||
|
@@ -0,0 +1,5 @@
|
||||
id,name_cn,name_en,trigger_min_min,trigger_max_min,duration_sec,type,p_mult,p_clicks,p_window_sec,p_penalty,p_penalty_dur_sec,opt_a_type,opt_a_param,opt_b_type,opt_b_param,flavor_cn
|
||||
ev_01_baigui,百鬼夜行,Night of the Hundred Ghosts,8,15,60,output_mult,5,0,0,0,0,none,0,none,0,百鬼倾巢而出,阴阳秩序暂时失序
|
||||
ev_02_naoxiang,阴兵闹饷,Ghost Mutiny,10,18,15,click_challenge,0,10,15,0.5,60,none,0,none,0,鬼卒也要三牲祭,不然哗变
|
||||
ev_03_shensu,冤魂申诉,Grievance of the Wronged,6,12,45,choice,0,15,40,0,0,merit_burst_sec,30,wrath_gain,0,他跪在地上喊冤枉。你选择……
|
||||
ev_04_tiangou,天狗食月,Heavenly Dog Eats the Moon,20,35,30,click_output_mult,2,10,0,0,0,none,0,none,0,天狗噬月,阴气最盛——动手
|
||||
|
@@ -2,12 +2,12 @@
|
||||
gen_01_youhun,游魂,Wandering Souls,15,1.13,0.1,none,none,res://assets/sprites/units/gen_01_youhun
|
||||
gen_02_yinbing,阴兵,Ghost Soldiers,120,1.14,1,none,none,res://assets/sprites/units/gen_02_yinbing
|
||||
gen_03_guizu,鬼卒班,Ghost Constables,1200,1.15,8,hall_01_qinguang,up_gate_guizu,res://assets/sprites/units/gen_03_guizu
|
||||
gen_04_biliao,勾魂笔僚,Death-Scribe Clerks,12000,1.15,50,hall_02_chujiang,up_gate_biliao,res://assets/sprites/units/gen_04_biliao
|
||||
gen_05_tangpu,孟婆汤铺,Meng Po's Soup Stall,120000,1.16,400,hall_03_songdi,up_gate_tangpu,res://assets/sprites/units/gen_05_tangpu
|
||||
gen_06_nanmin,枉死城难民团,Refugee Camp of the Wrongfully Dead,1200000,1.16,3000,hall_04_wuguan,up_gate_nanmin,res://assets/sprites/units/gen_06_nanmin
|
||||
gen_07_shuban,阴律司书办,Clerks of the Nether Court,12000000,1.17,24000,hall_05_yama,up_gate_shuban,res://assets/sprites/units/gen_07_shuban
|
||||
gen_08_bingfang,幽都兵坊,Armory of the Dark Capital,120000000,1.17,180000,hall_06_biancheng,up_gate_bingfang,res://assets/sprites/units/gen_08_bingfang
|
||||
gen_09_wodi,十殿卧底,Sleeper Agents in the Ten Courts,1200000000,1.18,1400000,hall_07_taishan,up_gate_wodi,res://assets/sprites/units/gen_09_wodi
|
||||
gen_10_xingzhe,轮回裂隙行者,Rift Walkers of the Wheel,12000000000,1.18,11000000,hall_08_dushi,up_gate_xingzhe,res://assets/sprites/units/gen_10_xingzhe
|
||||
gen_11_yinhun,六道引魂人,Soul Guides of the Six Paths,120000000000,1.19,85000000,hall_09_pingdeng,up_gate_yinhun,res://assets/sprites/units/gen_11_yinhun
|
||||
gen_12_tianzi,幽冥天子,Emperor of the Netherworld,1200000000000,1.20,650000000,none,up_gate_tianzi,res://assets/sprites/units/gen_12_tianzi
|
||||
gen_04_biliao,勾魂笔僚,Death-Scribe Clerks,12000,1.15,64,hall_02_chujiang,up_gate_biliao,res://assets/sprites/units/gen_04_biliao
|
||||
gen_05_tangpu,孟婆汤铺,Meng Po's Soup Stall,120000,1.16,512,hall_03_songdi,up_gate_tangpu,res://assets/sprites/units/gen_05_tangpu
|
||||
gen_06_nanmin,枉死城难民团,Refugee Camp of the Wrongfully Dead,1200000,1.16,4096,hall_04_wuguan,up_gate_nanmin,res://assets/sprites/units/gen_06_nanmin
|
||||
gen_07_shuban,阴律司书办,Clerks of the Nether Court,12000000,1.17,32768,hall_05_yama,up_gate_shuban,res://assets/sprites/units/gen_07_shuban
|
||||
gen_08_bingfang,幽都兵坊,Armory of the Dark Capital,120000000,1.17,262144,hall_06_biancheng,up_gate_bingfang,res://assets/sprites/units/gen_08_bingfang
|
||||
gen_09_wodi,十殿卧底,Sleeper Agents in the Ten Courts,1200000000,1.18,2097152,hall_07_taishan,up_gate_wodi,res://assets/sprites/units/gen_09_wodi
|
||||
gen_10_xingzhe,轮回裂隙行者,Rift Walkers of the Wheel,12000000000,1.18,16777216,hall_08_dushi,up_gate_xingzhe,res://assets/sprites/units/gen_10_xingzhe
|
||||
gen_11_yinhun,六道引魂人,Soul Guides of the Six Paths,120000000000,1.19,134217728,hall_09_pingdeng,up_gate_yinhun,res://assets/sprites/units/gen_11_yinhun
|
||||
gen_12_tianzi,幽冥天子,Emperor of the Netherworld,1200000000000,1.2,1073741824,none,up_gate_tianzi,res://assets/sprites/units/gen_12_tianzi
|
||||
|
@@ -0,0 +1,11 @@
|
||||
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
|
||||
pal_hall_01,hall_01_qinguang,冷靛,#1A1A1A,#F2EDE4,#B4402C,#2C4A6E,#3D5A80,冷靛,bleed_left,1,65,25,8,7
|
||||
pal_hall_02,hall_02_chujiang,冰青,#1A1A1A,#F2EDE4,#B4402C,#2C4A6E,#8FC1C9,冰青,seep_down,2,65,25,8,7
|
||||
pal_hall_03,hall_03_songdi,暗红,#1A1A1A,#F2EDE4,#B4402C,#2C4A6E,#7E2E2E,暗红,bleed_left,3,65,25,8,7
|
||||
pal_hall_04,hall_04_wuguan,锈橙,#1A1A1A,#F2EDE4,#B4402C,#2C4A6E,#A85C32,锈橙,seep_down,4,65,25,8,7
|
||||
pal_hall_05,hall_05_yama,玄黑,#1A1A1A,#F2EDE4,#B4402C,#2C4A6E,#141414,玄黑,bleed_left,5,65,25,8,7
|
||||
pal_hall_06,hall_06_biancheng,铅灰,#1A1A1A,#F2EDE4,#B4402C,#2C4A6E,#6E7276,铅灰,seep_down,6,65,25,8,7
|
||||
pal_hall_07,hall_07_taishan,烬红,#1A1A1A,#F2EDE4,#B4402C,#2C4A6E,#8C3A2B,烬红,bleed_left,7,65,25,8,7
|
||||
pal_hall_08,hall_08_dushi,墨绿,#1A1A1A,#F2EDE4,#B4402C,#2C4A6E,#2F4A3E,墨绿,seep_down,8,65,25,8,7
|
||||
pal_hall_09,hall_09_pingdeng,黛紫,#1A1A1A,#F2EDE4,#B4402C,#2C4A6E,#4A3A5C,黛紫,bleed_left,9,65,25,8,7
|
||||
pal_hall_10,hall_10_zhuanlun,骨白+金,#1A1A1A,#F2EDE4,#B4402C,#2C4A6E,#C9A227,暗金,seep_down,10,65,25,8,7
|
||||
|
@@ -0,0 +1,11 @@
|
||||
id,no,king_cn,king_en,motif_cn,motif_en,target_min,gate_merit,unlock_gen,unlock_gate,palette_id,is_prestige_gate
|
||||
hall_01_qinguang,1,秦广王,King Qinguang,孽镜台(照心),Mirror of Karma,25,2000,gen_03_guizu,up_gate_guizu,pal_hall_01,false
|
||||
hall_02_chujiang,2,楚江王,King Chujiang,寒冰狱,Ice Hell,42,30000,gen_04_biliao,up_gate_biliao,pal_hall_02,false
|
||||
hall_03_songdi,3,宋帝王,King Songdi,剥皮亭,Flaying Pavilion,62,300000,gen_05_tangpu,up_gate_tangpu,pal_hall_03,false
|
||||
hall_04_wuguan,4,五官王,King Wuguan,沸汤/毒蛇,Scalding Cauldron,84,2000000,gen_06_nanmin,up_gate_nanmin,pal_hall_04,false
|
||||
hall_05_yama,5,阎罗王,King Yama,叫唤地狱/铁床,Wailing Hell,105,10000000,gen_07_shuban,up_gate_shuban,pal_hall_05,false
|
||||
hall_06_biancheng,6,卞城王,King Biancheng,石碾,Stone Roller,125,40000000,gen_08_bingfang,up_gate_bingfang,pal_hall_06,false
|
||||
hall_07_taishan,7,泰山王,King Taishan,热油火坑,Boiling Oil Pit,143,120000000,gen_09_wodi,up_gate_wodi,pal_hall_07,false
|
||||
hall_08_dushi,8,都市王,King Dushi,泥犁/大石,Avici Stone,160,350000000,gen_10_xingzhe,up_gate_xingzhe,pal_hall_08,false
|
||||
hall_09_pingdeng,9,平等王,King Pingdeng,拔舌/铁树,Tongue-Ripping Iron Tree,175,800000000,gen_11_yinhun,up_gate_yinhun,pal_hall_09,false
|
||||
hall_10_zhuanlun,10,转轮王,King of the Wheel,轮回道(六道环),Wheel of Rebirth,180,1500000000,none,none,pal_hall_10,true
|
||||
|
@@ -0,0 +1,13 @@
|
||||
id,name_cn,name_en,effect_type,target_id,effect_param,cost_suihui,max_rank,condition,desc_cn
|
||||
meta_01,枕旧梦,Pillow of Old Dreams,start_levels,gen_01_youhun,10,3,1,none,新轮回开局自带游魂 ×10 级
|
||||
meta_02,携阴兵,Bring Ghost Soldiers,start_levels,gen_02_yinbing,10,5,1,none,开局自带阴兵 ×10 级
|
||||
meta_03,宿慧通透,Perspicacious Wisdom,suihui_gain_pct,,0.15,8,3,none,宿慧获取 +15%(每阶)
|
||||
meta_04,天生熟练,Born Skilled,skip_interval,,4,12,1,none,跳过机制从每 5 级加快为每 4 级
|
||||
meta_05,阴司人脉,Netherworld Connections,offline_pct,,0.25,6,1,none,离线效率 +25%
|
||||
meta_06,斩鬼刃,Ghost-Slaying Blade,boss_click_pct,,0.3,10,1,none,殿王战点击伤害 +30%
|
||||
meta_07,心安理得,Easy Conscience,wrath_penalty_set,,0.15,15,1,none,冤气对宿慧的削减 30%→15%
|
||||
meta_08,敕封恩典,Grace of Enthronement,global_pct,,0.25,20,2,none,全局 +25%(每阶)
|
||||
meta_09,阴阳眼,Yin-Yang Eye,event_freq_pct,,0.2,10,1,none,事件触发频率 +20%
|
||||
meta_10,超度加速,Salvation Acceleration,souls_mult,,2,8,1,none,超度计数获取 ×2
|
||||
meta_11,老马识途,Old Horse Knows the Way,bulk_unlock,,0,5,1,none,开局即可用批量 ×10
|
||||
meta_12,幽冥权柄,Authority of the Netherworld,conditional_global_pct,gen_12_tianzi,0.1,25,1,gen12_unlocked,愿景档(gen12)解锁后全局再 +10%
|
||||
|
@@ -0,0 +1,109 @@
|
||||
id,name_cn,name_en,type,target_gen,unlock_hall,effect_type,effect_param,requires,cost,desc_cn
|
||||
up_eff_gen_01_youhun_1,游魂 · 历练 I,Wandering Souls · Training I,efficiency,gen_01_youhun,none,gen_output_add,1,none,83,该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)
|
||||
up_eff_gen_01_youhun_2,游魂 · 历练 II,Wandering Souls · Training II,efficiency,gen_01_youhun,none,gen_output_add,1,none,135,该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)
|
||||
up_eff_gen_01_youhun_3,游魂 · 历练 III,Wandering Souls · Training III,efficiency,gen_01_youhun,none,gen_output_add,1,none,221,该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)
|
||||
up_eff_gen_01_youhun_4,游魂 · 历练 IV,Wandering Souls · Training IV,efficiency,gen_01_youhun,none,gen_output_add,1,none,360,该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)
|
||||
up_eff_gen_01_youhun_5,游魂 · 历练 V,Wandering Souls · Training V,efficiency,gen_01_youhun,none,gen_output_add,1,none,587,该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)
|
||||
up_eff_gen_01_youhun_6,游魂 · 历练 VI,Wandering Souls · Training VI,efficiency,gen_01_youhun,none,gen_output_add,1,none,957,该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)
|
||||
up_eff_gen_02_yinbing_1,阴兵 · 历练 I,Ghost Soldiers · Training I,efficiency,gen_02_yinbing,none,gen_output_add,1,none,751,该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)
|
||||
up_eff_gen_02_yinbing_2,阴兵 · 历练 II,Ghost Soldiers · Training II,efficiency,gen_02_yinbing,none,gen_output_add,1,none,1269,该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)
|
||||
up_eff_gen_02_yinbing_3,阴兵 · 历练 III,Ghost Soldiers · Training III,efficiency,gen_02_yinbing,none,gen_output_add,1,none,2143,该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)
|
||||
up_eff_gen_02_yinbing_4,阴兵 · 历练 IV,Ghost Soldiers · Training IV,efficiency,gen_02_yinbing,none,gen_output_add,1,none,3620,该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)
|
||||
up_eff_gen_02_yinbing_5,阴兵 · 历练 V,Ghost Soldiers · Training V,efficiency,gen_02_yinbing,none,gen_output_add,1,none,6114,该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)
|
||||
up_eff_gen_02_yinbing_6,阴兵 · 历练 VI,Ghost Soldiers · Training VI,efficiency,gen_02_yinbing,none,gen_output_add,1,none,10326,该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)
|
||||
up_eff_gen_03_guizu_1,鬼卒班 · 历练 I,Ghost Constables · Training I,efficiency,gen_03_guizu,hall_01_qinguang,gen_output_add,1,none,8491,该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)
|
||||
up_eff_gen_03_guizu_2,鬼卒班 · 历练 II,Ghost Constables · Training II,efficiency,gen_03_guizu,hall_01_qinguang,gen_output_add,1,none,14851,该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)
|
||||
up_eff_gen_03_guizu_3,鬼卒班 · 历练 III,Ghost Constables · Training III,efficiency,gen_03_guizu,hall_01_qinguang,gen_output_add,1,none,25974,该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)
|
||||
up_eff_gen_03_guizu_4,鬼卒班 · 历练 IV,Ghost Constables · Training IV,efficiency,gen_03_guizu,hall_01_qinguang,gen_output_add,1,none,45428,该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)
|
||||
up_eff_gen_03_guizu_5,鬼卒班 · 历练 V,Ghost Constables · Training V,efficiency,gen_03_guizu,hall_01_qinguang,gen_output_add,1,none,79454,该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)
|
||||
up_eff_gen_03_guizu_6,鬼卒班 · 历练 VI,Ghost Constables · Training VI,efficiency,gen_03_guizu,hall_01_qinguang,gen_output_add,1,none,138966,该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)
|
||||
up_eff_gen_04_biliao_1,勾魂笔僚 · 历练 I,Death-Scribe Clerks · Training I,efficiency,gen_04_biliao,hall_02_chujiang,gen_output_add,1,none,84908,该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)
|
||||
up_eff_gen_04_biliao_2,勾魂笔僚 · 历练 II,Death-Scribe Clerks · Training II,efficiency,gen_04_biliao,hall_02_chujiang,gen_output_add,1,none,148505,该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)
|
||||
up_eff_gen_04_biliao_3,勾魂笔僚 · 历练 III,Death-Scribe Clerks · Training III,efficiency,gen_04_biliao,hall_02_chujiang,gen_output_add,1,none,259737,该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)
|
||||
up_eff_gen_04_biliao_4,勾魂笔僚 · 历练 IV,Death-Scribe Clerks · Training IV,efficiency,gen_04_biliao,hall_02_chujiang,gen_output_add,1,none,454282,该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)
|
||||
up_eff_gen_04_biliao_5,勾魂笔僚 · 历练 V,Death-Scribe Clerks · Training V,efficiency,gen_04_biliao,hall_02_chujiang,gen_output_add,1,none,794541,该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)
|
||||
up_eff_gen_04_biliao_6,勾魂笔僚 · 历练 VI,Death-Scribe Clerks · Training VI,efficiency,gen_04_biliao,hall_02_chujiang,gen_output_add,1,none,1389658,该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)
|
||||
up_eff_gen_05_tangpu_1,孟婆汤铺 · 历练 I,Meng Po's Soup Stall · Training I,efficiency,gen_05_tangpu,hall_03_songdi,gen_output_add,1,none,958502,该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)
|
||||
up_eff_gen_05_tangpu_2,孟婆汤铺 · 历练 II,Meng Po's Soup Stall · Training II,efficiency,gen_05_tangpu,hall_03_songdi,gen_output_add,1,none,1735502,该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)
|
||||
up_eff_gen_05_tangpu_3,孟婆汤铺 · 历练 III,Meng Po's Soup Stall · Training III,efficiency,gen_05_tangpu,hall_03_songdi,gen_output_add,1,none,3142368,该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)
|
||||
up_eff_gen_05_tangpu_4,孟婆汤铺 · 历练 IV,Meng Po's Soup Stall · Training IV,efficiency,gen_05_tangpu,hall_03_songdi,gen_output_add,1,none,5689695,该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)
|
||||
up_eff_gen_05_tangpu_5,孟婆汤铺 · 历练 V,Meng Po's Soup Stall · Training V,efficiency,gen_05_tangpu,hall_03_songdi,gen_output_add,1,none,10301985,该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)
|
||||
up_eff_gen_05_tangpu_6,孟婆汤铺 · 历练 VI,Meng Po's Soup Stall · Training VI,efficiency,gen_05_tangpu,hall_03_songdi,gen_output_add,1,none,18653180,该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)
|
||||
up_eff_gen_06_nanmin_1,枉死城难民团 · 历练 I,Refugee Camp of the Wrongfully Dead · Training I,efficiency,gen_06_nanmin,hall_04_wuguan,gen_output_add,1,none,9585022,该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)
|
||||
up_eff_gen_06_nanmin_2,枉死城难民团 · 历练 II,Refugee Camp of the Wrongfully Dead · Training II,efficiency,gen_06_nanmin,hall_04_wuguan,gen_output_add,1,none,17355017,该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)
|
||||
up_eff_gen_06_nanmin_3,枉死城难民团 · 历练 III,Refugee Camp of the Wrongfully Dead · Training III,efficiency,gen_06_nanmin,hall_04_wuguan,gen_output_add,1,none,31423678,该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)
|
||||
up_eff_gen_06_nanmin_4,枉死城难民团 · 历练 IV,Refugee Camp of the Wrongfully Dead · Training IV,efficiency,gen_06_nanmin,hall_04_wuguan,gen_output_add,1,none,56896947,该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)
|
||||
up_eff_gen_06_nanmin_5,枉死城难民团 · 历练 V,Refugee Camp of the Wrongfully Dead · Training V,efficiency,gen_06_nanmin,hall_04_wuguan,gen_output_add,1,none,103019852,该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)
|
||||
up_eff_gen_06_nanmin_6,枉死城难民团 · 历练 VI,Refugee Camp of the Wrongfully Dead · Training VI,efficiency,gen_06_nanmin,hall_04_wuguan,gen_output_add,1,none,186531799,该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)
|
||||
up_eff_gen_07_shuban_1,阴律司书办 · 历练 I,Clerks of the Nether Court · Training I,efficiency,gen_07_shuban,hall_05_yama,gen_output_add,1,none,108089451,该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)
|
||||
up_eff_gen_07_shuban_2,阴律司书办 · 历练 II,Clerks of the Nether Court · Training II,efficiency,gen_07_shuban,hall_05_yama,gen_output_add,1,none,202547440,该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)
|
||||
up_eff_gen_07_shuban_3,阴律司书办 · 历练 III,Clerks of the Nether Court · Training III,efficiency,gen_07_shuban,hall_05_yama,gen_output_add,1,none,379551056,该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)
|
||||
up_eff_gen_07_shuban_4,阴律司书办 · 历练 IV,Clerks of the Nether Court · Training IV,efficiency,gen_07_shuban,hall_05_yama,gen_output_add,1,none,711235870,该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)
|
||||
up_eff_gen_07_shuban_5,阴律司书办 · 历练 V,Clerks of the Nether Court · Training V,efficiency,gen_07_shuban,hall_05_yama,gen_output_add,1,none,1332775800,该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)
|
||||
up_eff_gen_07_shuban_6,阴律司书办 · 历练 VI,Clerks of the Nether Court · Training VI,efficiency,gen_07_shuban,hall_05_yama,gen_output_add,1,none,2497471526,该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)
|
||||
up_eff_gen_08_bingfang_1,幽都兵坊 · 历练 I,Armory of the Dark Capital · Training I,efficiency,gen_08_bingfang,hall_06_biancheng,gen_output_add,1,none,1080894509,该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)
|
||||
up_eff_gen_08_bingfang_2,幽都兵坊 · 历练 II,Armory of the Dark Capital · Training II,efficiency,gen_08_bingfang,hall_06_biancheng,gen_output_add,1,none,2025474395,该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)
|
||||
up_eff_gen_08_bingfang_3,幽都兵坊 · 历练 III,Armory of the Dark Capital · Training III,efficiency,gen_08_bingfang,hall_06_biancheng,gen_output_add,1,none,3795510563,该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)
|
||||
up_eff_gen_08_bingfang_4,幽都兵坊 · 历练 IV,Armory of the Dark Capital · Training IV,efficiency,gen_08_bingfang,hall_06_biancheng,gen_output_add,1,none,7112358700,该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)
|
||||
up_eff_gen_08_bingfang_5,幽都兵坊 · 历练 V,Armory of the Dark Capital · Training V,efficiency,gen_08_bingfang,hall_06_biancheng,gen_output_add,1,none,13327758001,该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)
|
||||
up_eff_gen_08_bingfang_6,幽都兵坊 · 历练 VI,Armory of the Dark Capital · Training VI,efficiency,gen_08_bingfang,hall_06_biancheng,gen_output_add,1,none,24974715257,该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)
|
||||
up_eff_gen_09_wodi_1,十殿卧底 · 历练 I,Sleeper Agents in the Ten Courts · Training I,efficiency,gen_09_wodi,hall_07_taishan,gen_output_add,1,none,12176692765,该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)
|
||||
up_eff_gen_09_wodi_2,十殿卧底 · 历练 II,Sleeper Agents in the Ten Courts · Training II,efficiency,gen_09_wodi,hall_07_taishan,gen_output_add,1,none,23607901124,该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)
|
||||
up_eff_gen_09_wodi_3,十殿卧底 · 历练 III,Sleeper Agents in the Ten Courts · Training III,efficiency,gen_09_wodi,hall_07_taishan,gen_output_add,1,none,45770473659,该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)
|
||||
up_eff_gen_09_wodi_4,十殿卧底 · 历练 IV,Sleeper Agents in the Ten Courts · Training IV,efficiency,gen_09_wodi,hall_07_taishan,gen_output_add,1,none,88738776396,该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)
|
||||
up_eff_gen_09_wodi_5,十殿卧底 · 历练 V,Sleeper Agents in the Ten Courts · Training V,efficiency,gen_09_wodi,hall_07_taishan,gen_output_add,1,none,172044766126,该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)
|
||||
up_eff_gen_09_wodi_6,十殿卧底 · 历练 VI,Sleeper Agents in the Ten Courts · Training VI,efficiency,gen_09_wodi,hall_07_taishan,gen_output_add,1,none,333556566289,该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)
|
||||
up_eff_gen_10_xingzhe_1,轮回裂隙行者 · 历练 I,Rift Walkers of the Wheel · Training I,efficiency,gen_10_xingzhe,hall_08_dushi,gen_output_add,1,none,121766927654,该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)
|
||||
up_eff_gen_10_xingzhe_2,轮回裂隙行者 · 历练 II,Rift Walkers of the Wheel · Training II,efficiency,gen_10_xingzhe,hall_08_dushi,gen_output_add,1,none,236079011240,该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)
|
||||
up_eff_gen_10_xingzhe_3,轮回裂隙行者 · 历练 III,Rift Walkers of the Wheel · Training III,efficiency,gen_10_xingzhe,hall_08_dushi,gen_output_add,1,none,457704736595,该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)
|
||||
up_eff_gen_10_xingzhe_4,轮回裂隙行者 · 历练 IV,Rift Walkers of the Wheel · Training IV,efficiency,gen_10_xingzhe,hall_08_dushi,gen_output_add,1,none,887387763957,该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)
|
||||
up_eff_gen_10_xingzhe_5,轮回裂隙行者 · 历练 V,Rift Walkers of the Wheel · Training V,efficiency,gen_10_xingzhe,hall_08_dushi,gen_output_add,1,none,1720447661255,该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)
|
||||
up_eff_gen_10_xingzhe_6,轮回裂隙行者 · 历练 VI,Rift Walkers of the Wheel · Training VI,efficiency,gen_10_xingzhe,hall_08_dushi,gen_output_add,1,none,3335565662885,该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)
|
||||
up_eff_gen_11_yinhun_1,六道引魂人 · 历练 I,Soul Guides of the Six Paths · Training I,efficiency,gen_11_yinhun,hall_09_pingdeng,gen_output_add,1,none,1370372723145,该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)
|
||||
up_eff_gen_11_yinhun_2,六道引魂人 · 历练 II,Soul Guides of the Six Paths · Training II,efficiency,gen_11_yinhun,hall_09_pingdeng,gen_output_add,1,none,2748062154037,该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)
|
||||
up_eff_gen_11_yinhun_3,六道引魂人 · 历练 III,Soul Guides of the Six Paths · Training III,efficiency,gen_11_yinhun,hall_09_pingdeng,gen_output_add,1,none,5510796789008,该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)
|
||||
up_eff_gen_11_yinhun_4,六道引魂人 · 历练 IV,Soul Guides of the Six Paths · Training IV,efficiency,gen_11_yinhun,hall_09_pingdeng,gen_output_add,1,none,11051016879340,该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)
|
||||
up_eff_gen_11_yinhun_5,六道引魂人 · 历练 V,Soul Guides of the Six Paths · Training V,efficiency,gen_11_yinhun,hall_09_pingdeng,gen_output_add,1,none,22161037458512,该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)
|
||||
up_eff_gen_11_yinhun_6,六道引魂人 · 历练 VI,Soul Guides of the Six Paths · Training VI,efficiency,gen_11_yinhun,hall_09_pingdeng,gen_output_add,1,none,44440397349832,该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)
|
||||
up_eff_gen_12_tianzi_1,幽冥天子 · 历练 I,Emperor of the Netherworld · Training I,efficiency,gen_12_tianzi,none,gen_output_add,1,none,15407021574586,该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)
|
||||
up_eff_gen_12_tianzi_2,幽冥天子 · 历练 II,Emperor of the Netherworld · Training II,efficiency,gen_12_tianzi,none,gen_output_add,1,none,31947999937062,该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)
|
||||
up_eff_gen_12_tianzi_3,幽冥天子 · 历练 III,Emperor of the Netherworld · Training III,efficiency,gen_12_tianzi,none,gen_output_add,1,none,66247372669492,该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)
|
||||
up_eff_gen_12_tianzi_4,幽冥天子 · 历练 IV,Emperor of the Netherworld · Training IV,efficiency,gen_12_tianzi,none,gen_output_add,1,none,137370551967459,该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)
|
||||
up_eff_gen_12_tianzi_5,幽冥天子 · 历练 V,Emperor of the Netherworld · Training V,efficiency,gen_12_tianzi,none,gen_output_add,1,none,284851576559723,该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)
|
||||
up_eff_gen_12_tianzi_6,幽冥天子 · 历练 VI,Emperor of the Netherworld · Training VI,efficiency,gen_12_tianzi,none,gen_output_add,1,none,590668229154242,该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)
|
||||
up_click_1,指诀 · 倍力 I,Gesture · Force I,click,none,none,click_mult,2,none,50,点击价值 ×2(点击区,软上限 ×16)
|
||||
up_click_2,指诀 · 倍力 II,Gesture · Force II,click,none,none,click_mult,2,none,500,点击价值 ×2(点击区,软上限 ×16)
|
||||
up_click_3,指诀 · 倍力 III,Gesture · Force III,click,none,none,click_mult,2,none,5000,点击价值 ×2(点击区,软上限 ×16)
|
||||
up_click_4,指诀 · 倍力 IV,Gesture · Force IV,click,none,none,click_mult,2,none,50000,点击价值 ×2(点击区,软上限 ×16)
|
||||
up_click_5,指诀 · 引阴 I,Gesture · Soul-Draw 1,click,none,none,click_output_pct,0.01,none,100000,点击附加 +1% 当前产出/s(仪式与殿王战弹药)
|
||||
up_click_6,指诀 · 引阴 II,Gesture · Soul-Draw 2,click,none,none,click_output_pct,0.01,none,1000000,点击附加 +1% 当前产出/s(仪式与殿王战弹药)
|
||||
up_click_7,指诀 · 引阴 III,Gesture · Soul-Draw 3,click,none,none,click_output_pct,0.01,none,10000000,点击附加 +1% 当前产出/s(仪式与殿王战弹药)
|
||||
up_click_8,指诀 · 引阴 IV,Gesture · Soul-Draw 4,click,none,none,click_output_pct,0.01,none,100000000,点击附加 +1% 当前产出/s(仪式与殿王战弹药)
|
||||
up_click_9,指诀 · 引阴 V,Gesture · Soul-Draw 5,click,none,none,click_output_pct,0.01,none,1000000000,点击附加 +1% 当前产出/s(仪式与殿王战弹药)
|
||||
up_click_10,指诀 · 引阴 VI,Gesture · Soul-Draw 6,click,none,none,click_output_pct,0.01,none,10000000000,点击附加 +1% 当前产出/s(仪式与殿王战弹药)
|
||||
up_click_11,指诀 · 引阴 VII,Gesture · Soul-Draw 7,click,none,none,click_output_pct,0.01,none,100000000000,点击附加 +1% 当前产出/s(仪式与殿王战弹药)
|
||||
up_click_12,指诀 · 引阴 VIII,Gesture · Soul-Draw 8,click,none,none,click_output_pct,0.01,none,1000000000000,点击附加 +1% 当前产出/s(仪式与殿王战弹药)
|
||||
up_gate_guizu,鬼卒班 · 入职文书,Ghost Constables · Hiring Letter,gate_hire,gen_03_guizu,hall_01_qinguang,unlock,0,none,800,编制闸门:购入后该班底可购置(需先过对应殿门)
|
||||
up_gate_biliao,勾魂笔僚 · 入职文书,Death-Scribe Clerks · Hiring Letter,gate_hire,gen_04_biliao,hall_02_chujiang,unlock,0,none,8000,编制闸门:购入后该班底可购置(需先过对应殿门)
|
||||
up_gate_tangpu,孟婆汤铺 · 入职文书,Meng Po's Soup Stall · Hiring Letter,gate_hire,gen_05_tangpu,hall_03_songdi,unlock,0,none,80000,编制闸门:购入后该班底可购置(需先过对应殿门)
|
||||
up_gate_nanmin,枉死城难民团 · 入职文书,Refugee Camp of the Wrongfully Dead · Hiring Letter,gate_hire,gen_06_nanmin,hall_04_wuguan,unlock,0,none,800000,编制闸门:购入后该班底可购置(需先过对应殿门)
|
||||
up_gate_shuban,阴律司书办 · 入职文书,Clerks of the Nether Court · Hiring Letter,gate_hire,gen_07_shuban,hall_05_yama,unlock,0,none,8000000,编制闸门:购入后该班底可购置(需先过对应殿门)
|
||||
up_gate_bingfang,幽都兵坊 · 入职文书,Armory of the Dark Capital · Hiring Letter,gate_hire,gen_08_bingfang,hall_06_biancheng,unlock,0,none,80000000,编制闸门:购入后该班底可购置(需先过对应殿门)
|
||||
up_gate_wodi,十殿卧底 · 入职文书,Sleeper Agents in the Ten Courts · Hiring Letter,gate_hire,gen_09_wodi,hall_07_taishan,unlock,0,none,800000000,编制闸门:购入后该班底可购置(需先过对应殿门)
|
||||
up_gate_xingzhe,轮回裂隙行者 · 入职文书,Rift Walkers of the Wheel · Hiring Letter,gate_hire,gen_10_xingzhe,hall_08_dushi,unlock,0,none,8000000000,编制闸门:购入后该班底可购置(需先过对应殿门)
|
||||
up_gate_yinhun,六道引魂人 · 入职文书,Soul Guides of the Six Paths · Hiring Letter,gate_hire,gen_11_yinhun,hall_09_pingdeng,unlock,0,none,80000000000,编制闸门:购入后该班底可购置(需先过对应殿门)
|
||||
up_gate_tianzi,幽冥天子 · 敕封诏书,Enthronement Edict,gate_hire,gen_12_tianzi,none,unlock,0,enthrone_ready,800000000000,编制闸门:购入后该班底可购置(需先过对应殿门)
|
||||
up_gate_wrath,开通 · 冤气系统,Unlock: Wrath System,gate_system,none,hall_01_qinguang,unlock,0,none,1000,系统闸门:纯解锁零效果([B§2.3];成本外推 = 殿门阈值÷2,P-5)
|
||||
up_gate_events,开通 · 事件系统,Unlock: Event System,gate_system,none,hall_02_chujiang,unlock,0,none,15000,系统闸门:纯解锁零效果([B§2.3];成本外推 = 殿门阈值÷2,P-5)
|
||||
up_gate_bulk10,开通 · 批量购置(×10),Unlock: Bulk Buy ×10,gate_system,none,hall_03_songdi,unlock,0,none,150000,系统闸门:纯解锁零效果([B§2.3];成本外推 = 殿门阈值÷2,P-5)
|
||||
up_gate_offline,开通 · 离线收益,Unlock: Offline Income,gate_system,none,hall_04_wuguan,unlock,0,none,1000000,系统闸门:纯解锁零效果([B§2.3];成本外推 = 殿门阈值÷2,P-5)
|
||||
up_gate_prestige,开通 · 轮回预告,Unlock: Rebirth Preview,gate_system,none,hall_06_biancheng,unlock,0,none,20000000,系统闸门:纯解锁零效果([B§2.3];成本外推 = 殿门阈值÷2,P-5)
|
||||
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)
|
||||
|
+6
-1
@@ -14,10 +14,15 @@ config_version=5
|
||||
|
||||
config/name="WRATH OF THE DEAD"
|
||||
config/description="Wrongfully dead? Raise a ghost army and fight your way back to the living world."
|
||||
run/main_scene=""
|
||||
run/main_scene="res://src/presentation/stage/stage_root.tscn"
|
||||
config/features=PackedStringArray("4.7", "GL Compatibility")
|
||||
config/icon="res://icon.svg"
|
||||
|
||||
[autoload]
|
||||
|
||||
DataRegistry="*res://src/game/data_registry.gd"
|
||||
GameState="*res://src/game/game_state.gd"
|
||||
|
||||
[display]
|
||||
|
||||
window/size/viewport_width=1280
|
||||
|
||||
@@ -0,0 +1,336 @@
|
||||
class_name BalanceData
|
||||
extends RefCounted
|
||||
|
||||
# ============================================================================
|
||||
# BalanceData —— data/build/*.json 载入器(架构 §3.1 管线第三段)
|
||||
#
|
||||
# 单向数据管线的运行时入口:
|
||||
# data/sheets/*.csv ──SheetImporter──► data/build/*.json ──本类──► def 对象
|
||||
# sim(core/)与游戏(game/DataRegistry)读同一份文件 = 零漂移(架构 §3.1 铁律②)
|
||||
#
|
||||
# 纪律:
|
||||
# - core 层:无 Node/无场景树依赖;sim 与单测可无头构造(架构 §2.2)
|
||||
# - 只读:游戏永不写 data/(铁律①)
|
||||
# - 载入即做跨表引用完整性检查(check_references),违规进 load_errors
|
||||
#
|
||||
# 知识缺口:4.7.x 超出训练数据——只用 4.x 稳定 API(FileAccess/JSON/DirAccess)。
|
||||
# ============================================================================
|
||||
|
||||
const SHEET_FILES: Array[String] = [
|
||||
"generators", "upgrades", "halls", "hall_palettes",
|
||||
"boss_battles", "events", "meta_upgrades", "balance_tuning",
|
||||
]
|
||||
|
||||
var generators: Array[GeneratorDef] = []
|
||||
var upgrades: Array[UpgradeDef] = [] # 108 条非 meta 升级
|
||||
var meta_upgrades: Array[UpgradeDef] = [] # 12 条宿慧商店(type=meta)
|
||||
var halls: Array[HallDef] = []
|
||||
var palettes: Array[PaletteDef] = []
|
||||
var bosses: Array[BossDef] = []
|
||||
var events: Array[EventDef] = []
|
||||
|
||||
var load_errors: Array[String] = []
|
||||
|
||||
var _gen_by_id := {}
|
||||
var _up_by_id := {}
|
||||
var _meta_by_id := {}
|
||||
var _hall_by_id := {}
|
||||
var _hall_by_no := {}
|
||||
var _boss_by_hall := {}
|
||||
var _palette_by_hall := {}
|
||||
var _event_by_id := {}
|
||||
var _tune := {} # id -> {"kind": String, "num": float, "text": String}
|
||||
|
||||
|
||||
## 从目录载入全部表(缺表 = load_errors,不静默)
|
||||
static func load_from_dir(dir: String) -> BalanceData:
|
||||
var bd := BalanceData.new()
|
||||
for sheet in SHEET_FILES:
|
||||
var path := "%s/%s.json" % [dir, sheet]
|
||||
if not FileAccess.file_exists(path):
|
||||
bd.load_errors.append("缺少 build 产物:%s(先运行 SheetImporter import_all)" % path)
|
||||
continue
|
||||
var text := FileAccess.get_file_as_string(path)
|
||||
var doc: Variant = JSON.parse_string(text)
|
||||
if not (doc is Dictionary):
|
||||
bd.load_errors.append("JSON 解析失败:%s" % path)
|
||||
continue
|
||||
var rows: Variant = doc.get("rows", [])
|
||||
if not (rows is Array):
|
||||
bd.load_errors.append("%s:缺少 rows 数组" % path)
|
||||
continue
|
||||
match sheet:
|
||||
"generators": bd._load_generators(rows)
|
||||
"upgrades": bd._load_upgrades(rows)
|
||||
"halls": bd._load_halls(rows)
|
||||
"hall_palettes": bd._load_palettes(rows)
|
||||
"boss_battles": bd._load_bosses(rows)
|
||||
"events": bd._load_events(rows)
|
||||
"meta_upgrades": bd._load_meta(rows)
|
||||
"balance_tuning": bd._load_tuning(rows)
|
||||
bd._build_indexes()
|
||||
bd.load_errors.append_array(bd.check_references())
|
||||
return bd
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# 各表载入(字段名与 CSV 列同名;JSON 数字按 int/float 到达,统一 float() 收敛)
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
func _load_generators(rows: Array) -> void:
|
||||
for r in rows:
|
||||
var d := GeneratorDef.new()
|
||||
d.id = str(r.get("id", ""))
|
||||
d.display_cn = str(r.get("display_cn", ""))
|
||||
d.display_en = str(r.get("display_en", ""))
|
||||
d.base_cost = float(r.get("base_cost", 0))
|
||||
d.cost_ratio = float(r.get("cost_ratio", 1))
|
||||
d.base_rate = float(r.get("base_rate", 0))
|
||||
d.unlock_hall = str(r.get("unlock_hall", "none"))
|
||||
d.unlock_gate = str(r.get("unlock_gate", "none"))
|
||||
d.sprite_row = str(r.get("sprite_row", ""))
|
||||
generators.append(d)
|
||||
|
||||
|
||||
func _load_upgrades(rows: Array) -> void:
|
||||
for r in rows:
|
||||
var d := UpgradeDef.new()
|
||||
d.id = str(r.get("id", ""))
|
||||
d.name_cn = str(r.get("name_cn", ""))
|
||||
d.name_en = str(r.get("name_en", ""))
|
||||
d.type = str(r.get("type", ""))
|
||||
d.target_gen = str(r.get("target_gen", "none"))
|
||||
d.unlock_hall = str(r.get("unlock_hall", "none"))
|
||||
d.effect_type = str(r.get("effect_type", ""))
|
||||
d.effect_param = float(r.get("effect_param", 0))
|
||||
d.requires = str(r.get("requires", "none"))
|
||||
d.cost = int(r.get("cost", 0))
|
||||
d.desc_cn = str(r.get("desc_cn", ""))
|
||||
upgrades.append(d)
|
||||
|
||||
|
||||
func _load_halls(rows: Array) -> void:
|
||||
for r in rows:
|
||||
var d := HallDef.new()
|
||||
d.id = str(r.get("id", ""))
|
||||
d.no = int(r.get("no", 0))
|
||||
d.king_cn = str(r.get("king_cn", ""))
|
||||
d.king_en = str(r.get("king_en", ""))
|
||||
d.motif_cn = str(r.get("motif_cn", ""))
|
||||
d.motif_en = str(r.get("motif_en", ""))
|
||||
d.target_min = float(r.get("target_min", 0))
|
||||
d.gate_merit = float(r.get("gate_merit", 0))
|
||||
d.unlock_gen = str(r.get("unlock_gen", "none"))
|
||||
d.unlock_gate = str(r.get("unlock_gate", "none"))
|
||||
d.palette_id = str(r.get("palette_id", ""))
|
||||
d.is_prestige_gate = bool(r.get("is_prestige_gate", false))
|
||||
halls.append(d)
|
||||
|
||||
|
||||
func _load_palettes(rows: Array) -> void:
|
||||
for r in rows:
|
||||
var d := PaletteDef.new()
|
||||
d.id = str(r.get("id", ""))
|
||||
d.hall_id = str(r.get("hall_id", ""))
|
||||
d.name_cn = str(r.get("name_cn", ""))
|
||||
d.ink_hex = str(r.get("ink_hex", "#1A1A1A"))
|
||||
d.bone_hex = str(r.get("bone_hex", "#F2EDE4"))
|
||||
d.cinnabar_hex = str(r.get("cinnabar_hex", "#B4402C"))
|
||||
d.indigo_hex = str(r.get("indigo_hex", "#2C4A6E"))
|
||||
d.accent_hex = str(r.get("accent_hex", "#1A1A1A"))
|
||||
d.accent_name_cn = str(r.get("accent_name_cn", ""))
|
||||
d.flow = str(r.get("flow", "seep_down"))
|
||||
d.lut_index = int(r.get("lut_index", 0))
|
||||
d.ink_pct = int(r.get("ink_pct", 65))
|
||||
d.bone_pct = int(r.get("bone_pct", 25))
|
||||
d.cinnabar_max_pct = int(r.get("cinnabar_max_pct", 8))
|
||||
d.indigo_max_pct = int(r.get("indigo_max_pct", 7))
|
||||
palettes.append(d)
|
||||
|
||||
|
||||
func _load_bosses(rows: Array) -> void:
|
||||
for r in rows:
|
||||
var d := BossDef.new()
|
||||
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))
|
||||
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))
|
||||
d.combo_cap = float(r.get("combo_cap", 0.6))
|
||||
d.retry_cooldown_sec = float(r.get("retry_cooldown_sec", 60))
|
||||
d.reward_hp_ratio = float(r.get("reward_hp_ratio", 0.2))
|
||||
bosses.append(d)
|
||||
|
||||
|
||||
func _load_events(rows: Array) -> void:
|
||||
for r in rows:
|
||||
var d := EventDef.new()
|
||||
d.id = str(r.get("id", ""))
|
||||
d.name_cn = str(r.get("name_cn", ""))
|
||||
d.name_en = str(r.get("name_en", ""))
|
||||
d.trigger_min_min = float(r.get("trigger_min_min", 1))
|
||||
d.trigger_max_min = float(r.get("trigger_max_min", 1))
|
||||
d.duration_sec = float(r.get("duration_sec", 0))
|
||||
d.type = str(r.get("type", ""))
|
||||
d.p_mult = float(r.get("p_mult", 0))
|
||||
d.p_clicks = float(r.get("p_clicks", 0))
|
||||
d.p_window_sec = float(r.get("p_window_sec", 0))
|
||||
d.p_penalty = float(r.get("p_penalty", 0))
|
||||
d.p_penalty_dur_sec = float(r.get("p_penalty_dur_sec", 0))
|
||||
d.opt_a_type = str(r.get("opt_a_type", "none"))
|
||||
d.opt_a_param = float(r.get("opt_a_param", 0))
|
||||
d.opt_b_type = str(r.get("opt_b_type", "none"))
|
||||
d.opt_b_param = float(r.get("opt_b_param", 0))
|
||||
d.flavor_cn = str(r.get("flavor_cn", ""))
|
||||
events.append(d)
|
||||
|
||||
|
||||
func _load_meta(rows: Array) -> void:
|
||||
for r in rows:
|
||||
var d := UpgradeDef.new()
|
||||
d.id = str(r.get("id", ""))
|
||||
d.name_cn = str(r.get("name_cn", ""))
|
||||
d.name_en = str(r.get("name_en", ""))
|
||||
d.type = "meta"
|
||||
d.target_gen = str(r.get("target_id", ""))
|
||||
d.unlock_hall = "none"
|
||||
d.effect_type = str(r.get("effect_type", ""))
|
||||
d.effect_param = float(r.get("effect_param", 0))
|
||||
d.requires = str(r.get("requires", "none"))
|
||||
d.cost = int(r.get("cost_suihui", 0))
|
||||
d.cost_is_suihui = true
|
||||
d.max_rank = int(r.get("max_rank", 1))
|
||||
d.condition = str(r.get("condition", "none"))
|
||||
d.desc_cn = str(r.get("desc_cn", ""))
|
||||
meta_upgrades.append(d)
|
||||
|
||||
|
||||
func _load_tuning(rows: Array) -> void:
|
||||
for r in rows:
|
||||
var id := str(r.get("id", ""))
|
||||
if id == "":
|
||||
continue
|
||||
_tune[id] = {
|
||||
"kind": str(r.get("kind", "number")),
|
||||
"num": float(r.get("num", 0)),
|
||||
"text": str(r.get("text", "")),
|
||||
}
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# 索引与查询
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
func _build_indexes() -> void:
|
||||
for d in generators:
|
||||
_gen_by_id[d.id] = d
|
||||
for d in upgrades:
|
||||
_up_by_id[d.id] = d
|
||||
for d in meta_upgrades:
|
||||
_meta_by_id[d.id] = d
|
||||
for d in halls:
|
||||
_hall_by_id[d.id] = d
|
||||
_hall_by_no[d.no] = d
|
||||
for d in bosses:
|
||||
_boss_by_hall[d.hall_id] = d
|
||||
for d in palettes:
|
||||
_palette_by_hall[d.hall_id] = d
|
||||
for d in events:
|
||||
_event_by_id[d.id] = d
|
||||
|
||||
|
||||
func generator(id: String) -> GeneratorDef:
|
||||
return _gen_by_id.get(id)
|
||||
|
||||
|
||||
func upgrade(id: String) -> UpgradeDef:
|
||||
return _up_by_id.get(id)
|
||||
|
||||
|
||||
func meta(id: String) -> UpgradeDef:
|
||||
return _meta_by_id.get(id)
|
||||
|
||||
|
||||
func hall(id_or_no: Variant) -> HallDef:
|
||||
if id_or_no is int:
|
||||
return _hall_by_no.get(id_or_no)
|
||||
return _hall_by_id.get(str(id_or_no))
|
||||
|
||||
|
||||
func hall_by_no(no: int) -> HallDef:
|
||||
return _hall_by_no.get(no)
|
||||
|
||||
|
||||
func boss_for_hall(hall_id: String) -> BossDef:
|
||||
return _boss_by_hall.get(hall_id)
|
||||
|
||||
|
||||
func palette_for_hall(hall_id: String) -> PaletteDef:
|
||||
return _palette_by_hall.get(hall_id)
|
||||
|
||||
|
||||
func event(id: String) -> EventDef:
|
||||
return _event_by_id.get(id)
|
||||
|
||||
|
||||
## 全局调参数(balance_tuning.csv);缺失返回 default 并记 load_errors
|
||||
func tune_num(key: String, default_value: float) -> float:
|
||||
var row: Variant = _tune.get(key)
|
||||
if row == null:
|
||||
load_errors.append("tune 缺失:%s(用默认 %s)" % [key, str(default_value)])
|
||||
return default_value
|
||||
return float(row.num)
|
||||
|
||||
|
||||
func tune_text(key: String, default_value: String) -> String:
|
||||
var row: Variant = _tune.get(key)
|
||||
if row == null:
|
||||
load_errors.append("tune 缺失:%s(用默认 %s)" % [key, default_value])
|
||||
return default_value
|
||||
return str(row.text)
|
||||
|
||||
|
||||
func has_tune(key: String) -> bool:
|
||||
return _tune.has(key)
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# 跨表引用完整性(schema 管单表;跨表在这里查)
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
func check_references() -> Array[String]:
|
||||
var errs: Array[String] = []
|
||||
for g in generators:
|
||||
if g.unlock_hall != "none" and _hall_by_id.get(g.unlock_hall) == null:
|
||||
errs.append("generators.%s.unlock_hall 引用了不存在的殿:%s" % [g.id, g.unlock_hall])
|
||||
if g.unlock_gate != "none" and _up_by_id.get(g.unlock_gate) == null:
|
||||
errs.append("generators.%s.unlock_gate 引用了不存在的路引:%s" % [g.id, g.unlock_gate])
|
||||
for u in upgrades:
|
||||
if u.target_gen != "none" and _gen_by_id.get(u.target_gen) == null:
|
||||
errs.append("upgrades.%s.target_gen 引用了不存在的班底:%s" % [u.id, u.target_gen])
|
||||
if u.unlock_hall != "none" and _hall_by_id.get(u.unlock_hall) == null:
|
||||
errs.append("upgrades.%s.unlock_hall 引用了不存在的殿:%s" % [u.id, u.unlock_hall])
|
||||
for h in halls:
|
||||
if h.unlock_gen != "none" and _gen_by_id.get(h.unlock_gen) == null:
|
||||
errs.append("halls.%s.unlock_gen 引用了不存在的班底:%s" % [h.id, h.unlock_gen])
|
||||
if h.unlock_gate != "none" and _up_by_id.get(h.unlock_gate) == null:
|
||||
errs.append("halls.%s.unlock_gate 引用了不存在的路引:%s" % [h.id, h.unlock_gate])
|
||||
if _palette_by_hall.get(h.id) == null:
|
||||
errs.append("halls.%s 缺少色板行(hall_palettes.csv)" % h.id)
|
||||
for b in bosses:
|
||||
if _hall_by_id.get(b.hall_id) == null:
|
||||
errs.append("boss_battles.%s.hall_id 引用了不存在的殿:%s" % [b.id, b.hall_id])
|
||||
for m in meta_upgrades:
|
||||
if m.target_gen != "" and _gen_by_id.get(m.target_gen) == null:
|
||||
errs.append("meta_upgrades.%s.target_id 引用了不存在的班底:%s" % [m.id, m.target_gen])
|
||||
# 每殿恰一场殿王战
|
||||
for h in halls:
|
||||
if _boss_by_hall.get(h.id) == null:
|
||||
errs.append("殿 %s 缺少 boss_battles 行" % h.id)
|
||||
return errs
|
||||
@@ -0,0 +1,39 @@
|
||||
class_name BossDef
|
||||
extends RefCounted
|
||||
|
||||
# ============================================================================
|
||||
# BossDef —— 殿王战参数数据行(架构 §4.3;[B§5])
|
||||
#
|
||||
# 公式([B§5],参数全部来自本行,代码零硬编码):
|
||||
# HP = hp_factor × 进入时产出/s × hall_factor (15×产出/s×殿系数 → ~30s 的仗)
|
||||
# 军列 DPS = 进入时产出/s × army_dps_factor (阴兵在打,不是你一个人)
|
||||
# 点击伤害 = 点击价值 × 连击(combo_step/层,combo_cap 封顶)
|
||||
# INTRO = intro_sec 秒冲锋过场;开场伤害 = min(超度×intro_dmg_souls_factor, HP×intro_dmg_cap_ratio)
|
||||
# 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 为准。
|
||||
# ============================================================================
|
||||
|
||||
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 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%
|
||||
var combo_cap: float # 连击封顶 +60%
|
||||
var retry_cooldown_sec: float # 失败冷却(60s)
|
||||
var reward_hp_ratio: float # 通过阴德爆发 = HP × 本值(0.2)
|
||||
|
||||
|
||||
## 进入时 HP(架构 §4.3 start() 口径)
|
||||
func hp_at(output_per_sec: float) -> float:
|
||||
return hp_factor * output_per_sec * hall_factor
|
||||
@@ -0,0 +1,37 @@
|
||||
class_name EventDef
|
||||
extends RefCounted
|
||||
|
||||
# ============================================================================
|
||||
# EventDef —— 事件数据行(架构 §4.4;[B§7])
|
||||
#
|
||||
# 事件倍率走独立瞬时区,不进全局乘区([B§0.3/§7])。出现前提 =
|
||||
# up_gate_events(过 H2)。参数列按 type 取用:
|
||||
# output_mult p_mult = 全产出倍率(百鬼夜行 ×5)
|
||||
# click_challenge p_clicks = 窗口内需点击次数;p_window_sec = 窗口;
|
||||
# p_penalty = 失败惩罚倍率(-50%);p_penalty_dur_sec = 持续
|
||||
# (失败目标 = 随机已解锁班底,工程裁定 P-9)
|
||||
# choice opt_a = 超度(merit_burst_sec:阴德 = 产出/s × 秒数);
|
||||
# opt_b = 吞噬(wrath_gain:冤气 = lerp(p_clicks, p_window_sec,
|
||||
# 殿序进度));超时默认 opt_a([B§7])
|
||||
# click_output_mult p_clicks = 点击倍率;p_mult = 产出倍率(天狗食月 ×10/×2)
|
||||
#
|
||||
# EventDirector 运行时为 W3 交付;本文件 + events.csv 先行落定数据。
|
||||
# ============================================================================
|
||||
|
||||
var id: String # "ev_01_baigui"
|
||||
var name_cn: String
|
||||
var name_en: String # 临时译名,待文策渊 glossary
|
||||
var trigger_min_min: float # 触发间隔下限(分钟)
|
||||
var trigger_max_min: float # 触发间隔上限(分钟,含)
|
||||
var duration_sec: float # 持续秒数(choice 为非阻塞决策窗)
|
||||
var type: String # output_mult/click_challenge/choice/click_output_mult
|
||||
var p_mult: float
|
||||
var p_clicks: float
|
||||
var p_window_sec: float
|
||||
var p_penalty: float
|
||||
var p_penalty_dur_sec: float
|
||||
var opt_a_type: String # none/merit_burst_sec/wrath_gain
|
||||
var opt_a_param: float
|
||||
var opt_b_type: String
|
||||
var opt_b_param: float
|
||||
var flavor_cn: String
|
||||
@@ -0,0 +1,23 @@
|
||||
class_name HallDef
|
||||
extends RefCounted
|
||||
|
||||
# ============================================================================
|
||||
# HallDef —— 十殿 band 数据行(架构 §2.1/§4.3;[B§6])
|
||||
#
|
||||
# 殿门 = 单计数器闸门:本轮累计阴德 merit_run ≥ gate_merit([B§0.1],
|
||||
# 挂机也能过;与殿王战解耦——[B§5] 注)。is_prestige_gate=true 的行
|
||||
# (仅 H10 转轮王)是轮回转世触发门([B§4.1])。
|
||||
# ============================================================================
|
||||
|
||||
var id: String # "hall_01_qinguang"(W1 已分配,永不重编号)
|
||||
var no: int # 1-10
|
||||
var king_cn: String # 殿王名(秦广王…)
|
||||
var king_en: String # 临时译名,待文策渊 glossary
|
||||
var motif_cn: String # 刑典母题(剪影+文字暗示路线)
|
||||
var motif_en: String
|
||||
var target_min: float # 到达目标时长(分钟,[B§6] pacing 表 25→180)
|
||||
var gate_merit: float # 殿门条件:本轮累计阴德(2K→1.5B)
|
||||
var unlock_gen: String # 过殿解锁班底 ID 或 "none"
|
||||
var unlock_gate: String # 过殿出现路引 ID 或 "none"
|
||||
var palette_id: String # hall_palettes.csv 行 ID
|
||||
var is_prestige_gate: bool # true = 轮回触发门(仅 H10)
|
||||
@@ -0,0 +1,32 @@
|
||||
class_name PaletteDef
|
||||
extends RefCounted
|
||||
|
||||
# ============================================================================
|
||||
# PaletteDef —— per-hall 色板数据行(架构 §5.3;[B§6 色板列])
|
||||
#
|
||||
# 水墨四色面积纪律([B]):墨 ~65% / 骨白 ~25% / 朱砂 ≤8% / 靛 ≤7%,
|
||||
# 由 W5 look-dev 冻结评审的截图直方图检查执行(架构 §10.1 评审 #1)。
|
||||
# hex 为工程起始值(provenance P-10),lut_generator 消费本表生成
|
||||
# assets/luts/hall_XX.png;换殿 = 换一行数据 + shader 换一张 LUT(ADR-3)。
|
||||
# ============================================================================
|
||||
|
||||
var id: String # "pal_hall_01"
|
||||
var hall_id: String # halls.csv 行 ID(每殿恰一行)
|
||||
var name_cn: String # 色板行名(冷靛/冰青/…/骨白+金)
|
||||
var ink_hex: String # 墨(#1A1A1A)
|
||||
var bone_hex: String # 骨白(纸底)
|
||||
var cinnabar_hex: String # 朱砂(强调)
|
||||
var indigo_hex: String # 靛(次强调;阴德方印编码)
|
||||
var accent_hex: String # 该殿色温
|
||||
var accent_name_cn: String
|
||||
var flow: String # seep_down(自上而下渗)/ bleed_left(向左横溢洶)
|
||||
var lut_index: int
|
||||
var ink_pct: int # 面积目标%
|
||||
var bone_pct: int
|
||||
var cinnabar_max_pct: int # 面积硬顶
|
||||
var indigo_max_pct: int
|
||||
|
||||
|
||||
## "#RRGGBB" → Color(供 shader/LUT 生成用)
|
||||
func to_color(hex: String) -> Color:
|
||||
return Color.html(hex)
|
||||
@@ -0,0 +1,635 @@
|
||||
class_name EconomyState
|
||||
extends RefCounted
|
||||
|
||||
# ============================================================================
|
||||
# EconomyState —— 权威经济状态(架构 §4.2;GDD §3 / Balance Bible v1.0 下称 [B§x])
|
||||
#
|
||||
# 唯一权威状态持有者(core 侧):游戏(game/GameState)与模拟器(core/sim)
|
||||
# 驱动同一份 tick()——这就是"模拟器与游戏零漂移"的机制保证(架构 §4.2)。
|
||||
#
|
||||
# 纪律(架构 §2.2 core 作用域):
|
||||
# - 纯 GDScript 逻辑:无 Node / 无场景树 / 无 Autoload 依赖
|
||||
# - tick() 零分配:只碰预分配 typed 数组与浮点(UI 层 10Hz 拉快照兜底显示)
|
||||
# - 数值全部来自 BalanceData(data/build/*.json);代码零硬编码
|
||||
# - 购买是唯一写入口:校验 + 扣费 + 乘区重算都在这里完成(命令模式)
|
||||
#
|
||||
# 三乘区([B§1],MultiplierSet 钳制):
|
||||
# output/s = Σ_g(rate_g × n_g) × M_gen(g) × M_global × 事件瞬时倍率
|
||||
# 事件倍率是独立瞬时区(set_event_mult),不进全局乘区([B§1/§5])。
|
||||
#
|
||||
# W2 范围声明(诚实标注):
|
||||
# - 殿王战 / 事件系统为 W3+;prestige 触发暂以"H10 殿门达成"为代理
|
||||
# ([B§5] 殿门与殿王战解耦,W3 换真 boss 胜负判定)
|
||||
# - 冤气入口(事件吞噬/天狗/放弃超度,[B§3.2])随 W3 事件到位;
|
||||
# 本类已实现完整 devour / 冤煞层 API 供 sim 门 #12 与 W3 接线
|
||||
# ============================================================================
|
||||
|
||||
## 购买结果 ok 值
|
||||
const OK := "ok"
|
||||
|
||||
var data: BalanceData # 定义表(只读;game/sim 注入)
|
||||
|
||||
# ---- 货币与计数器([B§3.1] 阴德为主货币;本轮累计 = 殿门闸门计数器)----
|
||||
var merit: float = 0.0 # 可花费阴德
|
||||
var wrath: float = 0.0 # 冤气(第二货币)
|
||||
var merit_run_total: float = 0.0 # 本轮累计阴德(monotonic;殿门/宿慧口径)
|
||||
var merit_lifetime: float = 0.0 # 历史累计(红线观测用)
|
||||
|
||||
# ---- 班底级数(预分配 typed 数组,长度 = 数据行数 12)----
|
||||
var gen_levels: Array[int] = []
|
||||
|
||||
# ---- 已购升级(id -> true)与宿慧商店阶数(id -> rank)----
|
||||
var bought: Dictionary = {}
|
||||
var meta_ranks: Dictionary = {}
|
||||
|
||||
# ---- meta 状态(跨轮回保留:[B§4.1])----
|
||||
var suihui: int = 0 # 宿慧
|
||||
var rebirths: int = 0 # 转世次数
|
||||
var enthroned: bool = false # 阴司敕封旗标([B§4.4]/[G§3.4])
|
||||
var devour_count: int = 0 # 吞噬次数(成就/叙事计数)
|
||||
|
||||
# ---- 进度 ----
|
||||
var halls_passed: int = 0 # 0-10(按殿序单调)
|
||||
var wrath_layers: int = 0 # 怨煞层([B§3.2] 22 层上限)
|
||||
|
||||
# ---- 事件瞬时区(game/EventDirector 写入;W2 默认 1.0)----
|
||||
var event_mult: float = 1.0
|
||||
|
||||
# ---- 调参数缓存(init 时从 data 读取一次;禁硬编码)----
|
||||
var _click_base: float = 1.0
|
||||
var _suihui_div: float = 1e6
|
||||
var _suihui_mult: float = 5.0
|
||||
var _suihui_global_pct: float = 0.03
|
||||
var _enthrone_bonus: float = 0.25
|
||||
var _skip_interval: int = 5
|
||||
var _skip_gens: int = 3
|
||||
var _skip_levels: int = 25
|
||||
var _enthrone_rebirths: int = 2
|
||||
var _enthrone_suihui_req: int = 60
|
||||
var _souls_div: float = 10000.0
|
||||
var _wrath_bonus: float = 0.25
|
||||
var _wrath_penalty: float = 0.03
|
||||
var _wrath_penalty_cap: float = 0.3
|
||||
|
||||
var _mult: MultiplierSet # 三乘区计算器(输入由 recompute 写入)
|
||||
|
||||
|
||||
func _init(p_data: BalanceData) -> void:
|
||||
data = p_data
|
||||
_mult = MultiplierSet.new(
|
||||
data.tune_num("tune_click_zone_cap", 16.0),
|
||||
data.tune_num("tune_gen_zone_cap", 8.0),
|
||||
data.tune_num("tune_global_zone_cap", 64.0))
|
||||
_click_base = data.tune_num("tune_click_base", 1.0)
|
||||
_suihui_div = data.tune_num("tune_suihui_div", 1e6)
|
||||
_suihui_mult = data.tune_num("tune_suihui_mult", 5.0)
|
||||
_suihui_global_pct = data.tune_num("tune_suihui_global_pct", 0.03)
|
||||
_enthrone_bonus = data.tune_num("tune_enthrone_bonus", 0.25)
|
||||
_skip_interval = int(data.tune_num("tune_skip_interval", 5.0))
|
||||
_skip_gens = int(data.tune_num("tune_skip_gens", 3.0))
|
||||
_skip_levels = int(data.tune_num("tune_skip_levels", 25.0))
|
||||
_enthrone_rebirths = int(data.tune_num("tune_enthrone_rebirths", 2.0))
|
||||
_enthrone_suihui_req = int(data.tune_num("tune_enthrone_suihui_req", 60.0))
|
||||
_souls_div = data.tune_num("tune_souls_div", 10000.0)
|
||||
_wrath_bonus = data.tune_num("tune_wrath_bonus", 0.25)
|
||||
_wrath_penalty = data.tune_num("tune_wrath_penalty", 0.03)
|
||||
_wrath_penalty_cap = data.tune_num("tune_wrath_penalty_cap", 0.3)
|
||||
gen_levels.resize(data.generators.size())
|
||||
recompute()
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# 产出([B§1] 总公式;tick 与快照共用)
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
## 全局乘区当前值(快照/调试用)
|
||||
func global_mult() -> float:
|
||||
return _mult.global_mult()
|
||||
|
||||
|
||||
## 单班底乘区当前值
|
||||
func gen_mult(gen_index: int) -> float:
|
||||
return _mult.gen_mult(data.generators[gen_index].id)
|
||||
|
||||
|
||||
## 总产出/s(含事件瞬时区)
|
||||
func output_per_sec() -> float:
|
||||
var sum := 0.0
|
||||
for i in data.generators.size():
|
||||
var lv: int = gen_levels[i]
|
||||
if lv <= 0:
|
||||
continue
|
||||
sum += data.generators[i].base_rate * float(lv) * _mult.gen_mult(data.generators[i].id)
|
||||
return sum * _mult.global_mult() * event_mult
|
||||
|
||||
|
||||
## 点击价值([B§2.2]:基础值 × 点击区倍率;法器附加走全局区,P-15)
|
||||
func click_value() -> float:
|
||||
return _click_base * _mult.click_mult()
|
||||
|
||||
|
||||
## 执行一次点击:加阴德(累计口径单调)+ 过殿轮询;返回获得量
|
||||
func click() -> float:
|
||||
var gain := click_value()
|
||||
if gain <= 0.0:
|
||||
return 0.0
|
||||
merit += gain
|
||||
merit_run_total += gain
|
||||
merit_lifetime += gain
|
||||
poll_halls()
|
||||
return gain
|
||||
|
||||
|
||||
## 超度亡魂叙事计数器([B§6]:floor(本轮累计阴德 ÷ 10,000);不设闸门)
|
||||
func souls_saved() -> int:
|
||||
return int(floor(merit_run_total / _souls_div))
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# tick(零分配;game 10Hz 累加器步进,sim 虚拟时钟驱动同一函数)
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
func tick(dt: float) -> void:
|
||||
if dt <= 0.0:
|
||||
return
|
||||
var gain := output_per_sec() * dt
|
||||
if gain > 0.0:
|
||||
merit += gain
|
||||
merit_run_total += gain
|
||||
merit_lifetime += gain
|
||||
BigNumber.assert_within(merit_lifetime, "EconomyState.tick")
|
||||
poll_halls()
|
||||
|
||||
|
||||
## 殿门轮询(单计数器闸门,[B§0.1]):本轮累计阴德 ≥ gate_merit → 过殿。
|
||||
## 返回本 tick 新通过的殿数(0 = 无;game 层据此发仪式信号)。
|
||||
func poll_halls() -> int:
|
||||
var newly := 0
|
||||
while halls_passed < data.halls.size():
|
||||
var h := data.halls[halls_passed]
|
||||
if merit_run_total < h.gate_merit:
|
||||
break
|
||||
halls_passed += 1
|
||||
newly += 1
|
||||
return newly
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# 购买命令(唯一写入口;全部返回 {ok=bool, reason=String, ...})
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
## 班底是否已解锁(双闸:过殿 = 许可 + 路引 = 编制,[B§2.3] 设计意图)
|
||||
func generator_unlocked(gen_index: int) -> bool:
|
||||
if gen_index < 0 or gen_index >= data.generators.size():
|
||||
return false
|
||||
var g := data.generators[gen_index]
|
||||
if g.unlock_hall != "none" and not hall_passed_id(g.unlock_hall):
|
||||
return false
|
||||
if g.unlock_gate != "none":
|
||||
if not bought.has(g.unlock_gate):
|
||||
return false
|
||||
# P-12:gen12 愿景档的闸门(敕封诏书,requires=enthrone_ready)
|
||||
# 只把"受敕动作"备好;班底本身以 enthroned 旗标为最终解锁
|
||||
# ([B§1] gen12 行"阴司敕封后" + [G§3.4] 效果 a)
|
||||
var gd := data.upgrade(g.unlock_gate)
|
||||
if gd != null and gd.requires == "enthrone_ready" and not enthroned:
|
||||
return false
|
||||
return true
|
||||
|
||||
|
||||
## 班底第 level_index 级(0 起始)成本
|
||||
func generator_cost(gen_index: int, level_index: int) -> float:
|
||||
return data.generators[gen_index].cost_of_level(level_index)
|
||||
|
||||
|
||||
## 从当前级数起连买 count 级的总成本;count = -1 → 最大可买
|
||||
func generator_bulk_cost(gen_index: int, count: int) -> float:
|
||||
var g := data.generators[gen_index]
|
||||
if count < 0:
|
||||
return g.cost_range(gen_levels[gen_index], max_affordable(gen_index))
|
||||
return g.cost_range(gen_levels[gen_index], count)
|
||||
|
||||
|
||||
## 当前阴德下该班底还能买几级(几何级数逐级累加;12 档内开销可忽略)
|
||||
func max_affordable(gen_index: int) -> int:
|
||||
var g := data.generators[gen_index]
|
||||
var n := 0
|
||||
var cost := 0.0
|
||||
var lv := gen_levels[gen_index]
|
||||
while n < 10000: # 安全阀:防 cost_ratio≈1 时无限循环
|
||||
var next := cost + g.cost_of_level(lv + n)
|
||||
if next > merit:
|
||||
break
|
||||
cost = next
|
||||
n += 1
|
||||
return n
|
||||
|
||||
|
||||
## 买 count 级(count = -1 → 最大可买);返回 {ok, bought, spent, reason}
|
||||
func buy_generator(gen_index: int, count: int) -> Dictionary:
|
||||
if gen_index < 0 or gen_index >= data.generators.size():
|
||||
return {"ok": false, "bought": 0, "spent": 0.0, "reason": "班底索引越界"}
|
||||
if not generator_unlocked(gen_index):
|
||||
return {"ok": false, "bought": 0, "spent": 0.0, "reason": "班底未解锁"}
|
||||
var g := data.generators[gen_index]
|
||||
var n := count
|
||||
if n < 0:
|
||||
n = max_affordable(gen_index)
|
||||
if n <= 0:
|
||||
return {"ok": false, "bought": 0, "spent": 0.0, "reason": "阴德不足"}
|
||||
var cost := g.cost_range(gen_levels[gen_index], n)
|
||||
if cost > merit:
|
||||
# 部分成交:按实际可买数扣(bulk 语义,UI 显示实购数)
|
||||
n = max_affordable(gen_index)
|
||||
if n <= 0:
|
||||
return {"ok": false, "bought": 0, "spent": 0.0, "reason": "阴德不足"}
|
||||
cost = g.cost_range(gen_levels[gen_index], n)
|
||||
merit -= cost
|
||||
gen_levels[gen_index] += n
|
||||
return {"ok": true, "bought": n, "spent": cost, "reason": OK}
|
||||
|
||||
|
||||
## 升级是否可见(unlock_hall 达成;"none" = 开局)
|
||||
func upgrade_visible(def: UpgradeDef) -> bool:
|
||||
if def.unlock_hall == "none":
|
||||
return true
|
||||
return hall_passed_id(def.unlock_hall)
|
||||
|
||||
|
||||
## 购买前置(requires 枚举,schema 同步)
|
||||
func requires_ok(def: UpgradeDef) -> bool:
|
||||
match def.requires:
|
||||
"none":
|
||||
return true
|
||||
"enthrone_ready":
|
||||
return enthrone_ready()
|
||||
"enthroned":
|
||||
return enthroned
|
||||
return false
|
||||
|
||||
|
||||
## 非 meta 升级是否可购
|
||||
func upgrade_available(def: UpgradeDef) -> bool:
|
||||
return upgrade_visible(def) and requires_ok(def) \
|
||||
and not bought.has(def.id) and merit >= float(def.cost)
|
||||
|
||||
|
||||
## 买一条升级;返回 {ok, reason}
|
||||
func buy_upgrade(id: String) -> Dictionary:
|
||||
var def := data.upgrade(id)
|
||||
if def == null:
|
||||
return {"ok": false, "reason": "升级不存在:%s" % id}
|
||||
if bought.has(id):
|
||||
return {"ok": false, "reason": "已购入"}
|
||||
if not upgrade_visible(def):
|
||||
return {"ok": false, "reason": "未达出现条件"}
|
||||
if not requires_ok(def):
|
||||
return {"ok": false, "reason": "未满足前置"}
|
||||
if merit < float(def.cost):
|
||||
return {"ok": false, "reason": "阴德不足"}
|
||||
merit -= float(def.cost)
|
||||
bought[id] = true
|
||||
recompute()
|
||||
return {"ok": true, "reason": OK}
|
||||
|
||||
|
||||
## 宿慧商店商品是否可购(阶数/meta 条件)
|
||||
func meta_available(def: UpgradeDef) -> bool:
|
||||
var rank: int = meta_ranks.get(def.id, 0)
|
||||
if rank >= def.max_rank:
|
||||
return false
|
||||
if suihui < def.cost:
|
||||
return false
|
||||
# m12 条件:gen12 解锁后才生效(condition = gen12_unlocked)
|
||||
if def.condition == "gen12_unlocked" and not generator_unlocked(gen_index_of("gen_12_tianzi")):
|
||||
return false
|
||||
return true
|
||||
|
||||
|
||||
## 买一阶宿慧商品;返回 {ok, reason, rank}
|
||||
func buy_meta(id: String) -> Dictionary:
|
||||
var def := data.meta(id)
|
||||
if def == null:
|
||||
return {"ok": false, "reason": "宿慧商品不存在:%s" % id, "rank": 0}
|
||||
if not meta_available(def):
|
||||
return {"ok": false, "reason": "不可购(阶数满/宿慧不足/条件未达)", "rank": int(meta_ranks.get(id, 0))}
|
||||
suihui -= def.cost
|
||||
var rank: int = int(meta_ranks.get(id, 0)) + 1
|
||||
meta_ranks[id] = rank
|
||||
recompute()
|
||||
return {"ok": true, "reason": OK, "rank": rank}
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# 冤气线([B§3.2];入口 = 事件三出口,W3 接线;sim 门 #12 直接用)
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
## 吞噬入口(事件调用):冤气 += lump;计数 +1
|
||||
func devour(lump: float) -> void:
|
||||
if lump <= 0.0:
|
||||
return
|
||||
wrath += lump
|
||||
devour_count += 1
|
||||
|
||||
|
||||
## 超度仪式入口(事件调用):阴德 lump(进累计口径,单调,[B§3.1])
|
||||
func save_soul(lump: float) -> void:
|
||||
if lump <= 0.0:
|
||||
return
|
||||
merit += lump
|
||||
merit_run_total += lump
|
||||
merit_lifetime += lump
|
||||
poll_halls()
|
||||
|
||||
|
||||
## 下一层怨煞成本([B§3.2]:round(8 × n^1.8),n 从 1 起)
|
||||
func wrath_layer_cost() -> int:
|
||||
return PrestigeModel.wrath_layer_cost(wrath_layers + 1,
|
||||
data.tune_num("tune_wrath_cost_base", 8.0),
|
||||
data.tune_num("tune_wrath_cost_exp", 1.8))
|
||||
|
||||
|
||||
## 买一层怨煞;返回 {ok, reason, layers}
|
||||
func buy_wrath_layer() -> Dictionary:
|
||||
var max_layers := int(data.tune_num("tune_wrath_layers", 22.0))
|
||||
if wrath_layers >= max_layers:
|
||||
return {"ok": false, "reason": "怨煞层已满(%d 层)" % max_layers, "layers": wrath_layers}
|
||||
var cost := wrath_layer_cost()
|
||||
if wrath < float(cost):
|
||||
return {"ok": false, "reason": "冤气不足", "layers": wrath_layers}
|
||||
wrath -= float(cost)
|
||||
wrath_layers += 1
|
||||
recompute()
|
||||
return {"ok": true, "reason": OK, "layers": wrath_layers}
|
||||
|
||||
|
||||
## 冤气对宿慧的削减率(每层 +3% 封顶 30%;m07 改 15%,[B§3.2])
|
||||
func wrath_penalty() -> float:
|
||||
var override_cap := 0.0
|
||||
var m07 := data.meta("meta_07")
|
||||
if m07 != null and meta_ranks.get("meta_07", 0) > 0:
|
||||
override_cap = m07.effect_param
|
||||
return PrestigeModel.wrath_penalty(wrath_layers, _wrath_penalty, _wrath_penalty_cap, override_cap)
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Prestige:轮回转世 + 阴司敕封([B§4] / [G§3.4])
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
## 本轮宿慧收益([B§4.1] 公式;m03 加成 + 冤气削减)
|
||||
func suihui_gain() -> int:
|
||||
var gain_pct := 0.0
|
||||
var m03 := data.meta("meta_03")
|
||||
if m03 != null:
|
||||
gain_pct = m03.effect_param * float(int(meta_ranks.get("meta_03", 0)))
|
||||
return PrestigeModel.suihui_gain(merit_run_total, _suihui_div, _suihui_mult,
|
||||
gain_pct, wrath_penalty())
|
||||
|
||||
|
||||
## 轮回是否可用(W2 代理 = H10 殿门达成;W3 换殿王战胜败判定,[B§5] 解耦备注)
|
||||
func prestige_available() -> bool:
|
||||
var h10 := data.hall_by_no(10)
|
||||
return h10 != null and merit_run_total >= h10.gate_merit
|
||||
|
||||
|
||||
## 敕封动作就绪([G§3.4]:转世 ≥2 且 宿慧 ≥60)
|
||||
func enthrone_ready() -> bool:
|
||||
return rebirths >= _enthrone_rebirths and suihui >= _enthrone_suihui_req
|
||||
|
||||
|
||||
## 受敕是否可用(诏书已购 + H10 达成 + 未受敕)
|
||||
func enthrone_available() -> bool:
|
||||
return not enthroned and bought.has("up_gate_tianzi") and prestige_available()
|
||||
|
||||
|
||||
## 执行轮回转世:结算宿慧 → 重置本轮 → 应用跳过/开局加成 → 转世 +1
|
||||
func do_prestige() -> Dictionary:
|
||||
if not prestige_available():
|
||||
return {"ok": false, "reason": "未达 H10 殿门"}
|
||||
var gain := suihui_gain()
|
||||
suihui += gain
|
||||
rebirths += 1
|
||||
# 重置本轮([B§4.1]:班底/升级/货币全清;保留宿慧/宿慧商店/成就/冤案卡册)
|
||||
merit = 0.0
|
||||
wrath = 0.0
|
||||
merit_run_total = 0.0
|
||||
wrath_layers = 0
|
||||
devour_count = 0
|
||||
bought.clear()
|
||||
# meta_ranks 不清空(宿慧商店跨轮回保留,[B§4.1])
|
||||
for i in gen_levels.size():
|
||||
gen_levels[i] = 0
|
||||
halls_passed = 0
|
||||
_apply_rebirth_start_levels()
|
||||
recompute()
|
||||
return {"ok": true, "reason": OK, "suihui_gain": gain, "rebirths": rebirths}
|
||||
|
||||
|
||||
## 执行阴司敕封([B§4.4]/[G§3.4]):永久旗标 + 全局恩典 + gen12 解锁
|
||||
func do_enthrone() -> Dictionary:
|
||||
if enthroned:
|
||||
return {"ok": false, "reason": "已受敕"}
|
||||
if not enthrone_available():
|
||||
return {"ok": false, "reason": "条件未达(诏书/H10)"}
|
||||
enthroned = true
|
||||
recompute()
|
||||
return {"ok": true, "reason": OK}
|
||||
|
||||
|
||||
## 新一世开局自带级数([B§4.2] 跳过机制 + m01/m02 开局加成;P-11 叠加口径)
|
||||
func _apply_rebirth_start_levels() -> void:
|
||||
var interval := _skip_interval
|
||||
var m04 := data.meta("meta_04")
|
||||
if m04 != null and meta_ranks.get("meta_04", 0) > 0:
|
||||
interval = int(m04.effect_param)
|
||||
var skips := PrestigeModel.skip_count(suihui, interval)
|
||||
var bonus := skips * _skip_levels
|
||||
var cover := int(mini(float(_skip_gens), float(gen_levels.size())))
|
||||
for i in cover:
|
||||
gen_levels[i] += bonus
|
||||
# m01/m02:开局自带游魂/阴兵 ×10 级(start_levels 效果)
|
||||
for mid in ["meta_01", "meta_02"]:
|
||||
var m := data.meta(mid)
|
||||
if m == null or meta_ranks.get(mid, 0) <= 0:
|
||||
continue
|
||||
var gi := gen_index_of(m.target_gen)
|
||||
if gi >= 0:
|
||||
gen_levels[gi] += int(m.effect_param)
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# 乘区重算(购买/轮回/敕封后调用;非热路径)
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
func recompute() -> void:
|
||||
# 点击区:L1-4 为 ×2/级(计数);L5-12 法器附加进全局区(P-15,[B§1] 公式口径)
|
||||
var click_levels := 0
|
||||
for u in data.upgrades:
|
||||
if bought.has(u.id) and u.effect_type == "click_mult":
|
||||
click_levels += 1
|
||||
_mult.set_click_levels(click_levels, 0.0, 0.01)
|
||||
# 班底区:per-gen 效率升级数
|
||||
var eff := {}
|
||||
for u in data.upgrades:
|
||||
if u.type != "efficiency" or not bought.has(u.id):
|
||||
continue
|
||||
eff[u.target_gen] = int(eff.get(u.target_gen, 0)) + 1
|
||||
for gid in eff:
|
||||
_mult.set_gen_eff(gid, eff[gid])
|
||||
# 全局区加法总和([B§1]:宿慧×3% + 法器附加 + 冤煞层 + 敕封 25% + m08 + m12)
|
||||
var gadd := float(suihui) * _suihui_global_pct
|
||||
gadd += float(wrath_layers) * _wrath_bonus
|
||||
if enthroned:
|
||||
gadd += _enthrone_bonus
|
||||
for u in data.upgrades:
|
||||
if u.effect_type == "click_output_pct" and bought.has(u.id):
|
||||
gadd += u.effect_param
|
||||
var m08 := data.meta("meta_08")
|
||||
if m08 != null:
|
||||
gadd += m08.effect_param * float(int(meta_ranks.get("meta_08", 0)))
|
||||
var m12 := data.meta("meta_12")
|
||||
if m12 != null and meta_ranks.get("meta_12", 0) > 0:
|
||||
var gi := gen_index_of("gen_12_tianzi")
|
||||
if gi >= 0 and generator_unlocked(gi):
|
||||
gadd += m12.effect_param
|
||||
_mult.set_global_add(gadd)
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# 查询辅助
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
## 殿门是否已通过(按殿序单调计数器,[B§0.1])
|
||||
func hall_passed_id(hall_id: String) -> bool:
|
||||
var h := data.hall(hall_id)
|
||||
return h != null and h.no <= halls_passed
|
||||
|
||||
|
||||
## 班底 ID → 索引(-1 = 不存在)
|
||||
func gen_index_of(gen_id: String) -> int:
|
||||
for i in data.generators.size():
|
||||
if data.generators[i].id == gen_id:
|
||||
return i
|
||||
return -1
|
||||
|
||||
|
||||
## 当前离线倍率([B§2.4] ×1.15/级链 × m05 加法;P-13 已标注)
|
||||
func offline_mult() -> float:
|
||||
var m := 1.0
|
||||
for u in data.upgrades:
|
||||
if u.type == "offline" and bought.has(u.id):
|
||||
m *= 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
|
||||
|
||||
|
||||
## 事件瞬时区写入(game/EventDirector 调用;独立区不进全局乘区)
|
||||
func set_event_mult(m: float) -> void:
|
||||
event_mult = maxf(0.0, m)
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# 快照(10Hz;UI 拉取。W2 为 Dictionary;W3 换预分配 RefCounted 结构,
|
||||
# 字段与脏检查纪律见架构 §4.2——当前 UI 层自己做脏检查兜底)
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
func snapshot() -> Dictionary:
|
||||
return {
|
||||
"merit": merit,
|
||||
"output_per_sec": output_per_sec(),
|
||||
"click_value": click_value(),
|
||||
"wrath": wrath,
|
||||
"wrath_layers": wrath_layers,
|
||||
"suihui": suihui,
|
||||
"suihui_gain": suihui_gain(),
|
||||
"rebirths": rebirths,
|
||||
"enthroned": enthroned,
|
||||
"halls_passed": halls_passed,
|
||||
"souls_saved": souls_saved(),
|
||||
"merit_run_total": merit_run_total,
|
||||
"global_mult": global_mult(),
|
||||
"gen_levels": gen_levels.duplicate(),
|
||||
"prestige_available": prestige_available(),
|
||||
"enthrone_ready": enthone_ready(),
|
||||
"enthrone_available": enthrone_available(),
|
||||
"wrath_layer_cost": wrath_layer_cost(),
|
||||
"gens": _gens_view(),
|
||||
"upgrades": _upgrades_view(),
|
||||
"metas": _metas_view(),
|
||||
"next_hall": _next_hall_view(),
|
||||
}
|
||||
|
||||
|
||||
## W2 便利视图字段(架构 §4.2 快照纪律的 W2 落地:UI 只读快照 + 发命令)。
|
||||
## W3 换预分配 EconomySnapshot + UI 脏检查;此处 10Hz 重建小字典可接受。
|
||||
func _gens_view() -> Array:
|
||||
var out := []
|
||||
for i in data.generators.size():
|
||||
var g := data.generators[i]
|
||||
var lv := gen_levels[i]
|
||||
out.append({
|
||||
"id": g.id,
|
||||
"name_cn": g.display_cn,
|
||||
"level": lv,
|
||||
"cost_next": g.cost_of_level(lv),
|
||||
"unlocked": generator_unlocked(i),
|
||||
"affordable": merit >= g.cost_of_level(lv),
|
||||
# 不含事件瞬时区(事件是瞬时表演,槽位显示稳态产出)
|
||||
"output": g.base_rate * float(lv) * _mult.gen_mult(g.id) * _mult.global_mult(),
|
||||
})
|
||||
return out
|
||||
|
||||
|
||||
func _upgrades_view() -> Array:
|
||||
var out := []
|
||||
for u in data.upgrades:
|
||||
if bought.has(u.id) or not upgrade_visible(u):
|
||||
continue
|
||||
out.append({
|
||||
"id": u.id,
|
||||
"name_cn": u.name_cn,
|
||||
"type": u.type,
|
||||
"cost": u.cost,
|
||||
"desc_cn": u.desc_cn,
|
||||
"available": upgrade_available(u),
|
||||
"requires": u.requires,
|
||||
})
|
||||
return out
|
||||
|
||||
|
||||
func _metas_view() -> Array:
|
||||
var out := []
|
||||
for m in data.meta_upgrades:
|
||||
out.append({
|
||||
"id": m.id,
|
||||
"name_cn": m.name_cn,
|
||||
"cost": m.cost,
|
||||
"rank": int(meta_ranks.get(m.id, 0)),
|
||||
"max_rank": m.max_rank,
|
||||
"available": meta_available(m),
|
||||
"desc_cn": m.desc_cn,
|
||||
"condition": m.condition,
|
||||
})
|
||||
return out
|
||||
|
||||
|
||||
## 下一座未过殿的门进度(UI"距 XX 还差 X"梯度条,[B§6])
|
||||
func _next_hall_view() -> Dictionary:
|
||||
if halls_passed >= data.halls.size():
|
||||
return {}
|
||||
var h := data.halls[halls_passed]
|
||||
var out := output_per_sec()
|
||||
var eta := 0.0
|
||||
if out > 0.0 and merit_run_total < h.gate_merit:
|
||||
eta = (h.gate_merit - merit_run_total) / out
|
||||
return {
|
||||
"no": h.no,
|
||||
"king_cn": h.king_cn,
|
||||
"motif_cn": h.motif_cn,
|
||||
"gate_merit": h.gate_merit,
|
||||
"have": merit_run_total,
|
||||
"eta_sec": eta,
|
||||
"target_min": h.target_min,
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
class_name GeneratorDef
|
||||
extends Resource
|
||||
|
||||
# ============================================================================
|
||||
# GeneratorDef —— 班底(发生器)数据行 → 实例(架构 §3.3 签名级)
|
||||
#
|
||||
# 字段与 data/sheets/generators.csv 列一一对应;CSV 数值经 BalanceData
|
||||
# 注入。.tres 导出为 W3 动作(SheetImporter.export_tres 届时启用);
|
||||
# 现阶段 BalanceData 用代码构造本 Resource(字段同名,无缝衔接)。
|
||||
#
|
||||
# 曲线规则([B§1],模拟器逐行断言):
|
||||
# - 成本 gen1→gen2 为 8×(点击养成档,设计意图);gen2 起恒 10×
|
||||
# - 速率 gen1=0.1;gen_n=8^(n-2)(8 的纯幂阶梯,double 精确表示)
|
||||
# - 单班底产出对级数线性(rate×n);成本指数 → 边际递减 → "买新不买旧"
|
||||
# ============================================================================
|
||||
|
||||
@export var id: String # "gen_01_youhun"
|
||||
@export var display_cn: String
|
||||
@export var display_en: String
|
||||
@export var base_cost: float # C0([B§1];1 级成本)
|
||||
@export var cost_ratio: float # r(1.13→1.20)
|
||||
@export var base_rate: float # rate/s(每级产出)
|
||||
@export var unlock_hall: String # 殿门闸门 ID 或 "none"
|
||||
@export var unlock_gate: String # 路引闸门 ID 或 "none"
|
||||
@export var sprite_row: String # ★ ADR-3:唯一允许的逐资产代码 = sprite 路径前缀
|
||||
|
||||
|
||||
## 第 level_index 级(0 起始)的购买成本;level 0 = base_cost
|
||||
func cost_of_level(level_index: int) -> float:
|
||||
return base_cost * pow(cost_ratio, float(level_index))
|
||||
|
||||
|
||||
## 从 from_level 起连买 count 级的总成本(几何级数和)
|
||||
func cost_range(from_level: int, count: int) -> float:
|
||||
if count <= 0:
|
||||
return 0.0
|
||||
var first := cost_of_level(from_level)
|
||||
var last := cost_of_level(from_level + count - 1)
|
||||
return first * (pow(cost_ratio, float(count)) - 1.0) / (cost_ratio - 1.0) if cost_ratio > 1.0 else first * count
|
||||
|
||||
|
||||
## 首级回本时间 = C0 / rate([B§1] 门 #8 断言 tier2-6 ∈[120,293]s)
|
||||
func payback_seconds() -> float:
|
||||
if base_rate <= 0.0:
|
||||
return INF
|
||||
return base_cost / base_rate
|
||||
|
||||
|
||||
## 在持有 level 级、班底乘区 mult 下的产出/s(不含全局乘区)
|
||||
func output_at(level: int, gen_mult: float) -> float:
|
||||
return base_rate * float(level) * gen_mult
|
||||
@@ -0,0 +1,83 @@
|
||||
class_name MultiplierSet
|
||||
extends RefCounted
|
||||
|
||||
# ============================================================================
|
||||
# MultiplierSet —— 三显式乘区(架构 §4.2;[B§1] 产出总公式)
|
||||
#
|
||||
# output_per_sec = Σ_g (rate_g × n_g) × M_generator(g) × M_global
|
||||
#
|
||||
# 乘区([B§0.3] 加法叠加优先;乘性只存在于三个显式乘区且各有软上限):
|
||||
# 点击区 M_click = 2^click_mult_levels 软上限 ×16(4 级满)
|
||||
# 班底区 M_gen(g) = 1 + 效率升级数(加法 +100%/级) 软上限 ×8(6 级满 = ×7)
|
||||
# 全局区 M_global = 1 + Σ(宿慧×3% + 冤煞层×25% + 敕封 25% + m08 阶×25%
|
||||
# + m12 条件 10% + 法器附加) 软上限 ×64
|
||||
# 事件倍率 = 独立瞬时区,不与上述叠加(EconomyState.tick 单独乘)
|
||||
#
|
||||
# ⚠ P-15(provenance):点击 L5-12(法器附加,CSV effect_type=click_output_pct)
|
||||
# 的口径裁定——[B§1] 总公式与 GDD §3.2 均把"法器附加"列入 M_global(全局区),
|
||||
# 与 [B§2.2] 表 prose"点击附加 +1% 当前产出/s"(点击区口径)冲突。
|
||||
# 工程裁定采信公式层(全局区):EconomyState.recompute 将 click_output_pct
|
||||
# 参数求和进 set_global_add;本类的 click_output_pct 输入保持 0,
|
||||
# click_value 第二项暂为 dead。若文策渊裁定改回点击区,只需改 recompute
|
||||
# 一行(set_click_levels 传计数与步长)——两解都保留在本类能力内。
|
||||
#
|
||||
# 本类是纯计算器:输入由 EconomyState 在购买/重生时写入(set_*),
|
||||
# evaluate 只读钳制。软上限数值来自 balance_tuning.csv(禁硬编码)。
|
||||
# ============================================================================
|
||||
|
||||
var click_zone_cap: float = 16.0
|
||||
var gen_zone_cap: float = 8.0
|
||||
var global_zone_cap: float = 64.0
|
||||
|
||||
var _click_mult_levels: int = 0 # 0-4(L1-4 点击 ×2)
|
||||
var _click_output_pct: float = 0.0 # Σ(L5-12) × 0.01
|
||||
var _gen_eff := {} # gen_id -> int(已购效率升级数 0-6)
|
||||
var _global_add: float = 0.0 # 全局加法总和(EconomyState 组装)
|
||||
|
||||
|
||||
func _init(p_click_cap: float = 16.0, p_gen_cap: float = 8.0, p_global_cap: float = 64.0) -> void:
|
||||
click_zone_cap = p_click_cap
|
||||
gen_zone_cap = p_gen_cap
|
||||
global_zone_cap = p_global_cap
|
||||
|
||||
|
||||
## 点击区输入(EconomyState 在购买点击升级后调用)
|
||||
func set_click_levels(mult_levels: int, output_pct: float, pct_step: float) -> void:
|
||||
_click_mult_levels = mult_levels
|
||||
_click_output_pct = output_pct * pct_step
|
||||
|
||||
|
||||
## 班底区输入(单班底效率升级数)
|
||||
func set_gen_eff(gen_id: String, count: int) -> void:
|
||||
_gen_eff[gen_id] = count
|
||||
|
||||
|
||||
## 全局区加法总和(EconomyState 组装宿慧/冤煞/敕封/meta 后调用)
|
||||
func set_global_add(total_add: float) -> void:
|
||||
_global_add = total_add
|
||||
|
||||
|
||||
## 点击区倍率(钳制 ×16)
|
||||
func click_mult() -> float:
|
||||
return minf(pow(2.0, float(_click_mult_levels)), click_zone_cap)
|
||||
|
||||
|
||||
## 点击附加比例(+1%×级数;无软上限——[B§2.2] 只给点击区 ×16 上限)
|
||||
func click_output_pct() -> float:
|
||||
return _click_output_pct
|
||||
|
||||
|
||||
## 单班底乘区(钳制 ×8)
|
||||
func gen_mult(gen_id: String) -> float:
|
||||
var eff: Variant = _gen_eff.get(gen_id, 0)
|
||||
return minf(1.0 + float(eff), gen_zone_cap)
|
||||
|
||||
|
||||
## 全局乘区(钳制 ×64)
|
||||
func global_mult() -> float:
|
||||
return minf(1.0 + _global_add, global_zone_cap)
|
||||
|
||||
|
||||
## 点击价值 = 基础值 × 点击区倍率 + 产出/s × 点击附加比例([B§2.2])
|
||||
func click_value(click_base: float, output_per_sec: float) -> float:
|
||||
return click_base * click_mult() + output_per_sec * _click_output_pct
|
||||
@@ -0,0 +1,58 @@
|
||||
class_name OfflineModel
|
||||
extends RefCounted
|
||||
|
||||
# ============================================================================
|
||||
# OfflineModel —— 离线收益衰减表 + 时钟钳制(架构 §4.2/§6.5;[B§8])
|
||||
#
|
||||
# 衰减表([B§8],参数来自 balance_tuning.csv):
|
||||
# 0-2h 100% / 2-8h 70% / 8-24h 40% / >24h 10%;硬上限 72h 封顶
|
||||
# × 离线升级(最多 ×2.31);时钟回拨钳制为 0(负收益记 0)
|
||||
#
|
||||
# 契约校准点([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。
|
||||
#
|
||||
# 静态纯函数:sim 门 #9 与游戏载入时共用(零漂移)。
|
||||
# ============================================================================
|
||||
|
||||
## 分带结算:把秒数拆进各衰减带,返回 {seconds, merit, effective_hours}
|
||||
static func settle(seconds: float, output_per_sec: float, offline_mult: 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}
|
||||
var s := seconds
|
||||
if s <= 0.0:
|
||||
return out # 时钟回拨/零时长 → 0
|
||||
s = minf(s, cap_h * 3600.0) # 72h 硬顶
|
||||
var effective := 0.0
|
||||
var remaining := s
|
||||
var bounds := [band1_h * 3600.0, band2_h * 3600.0, band3_h * 3600.0]
|
||||
var rates := [band1_rate, band2_rate, band3_rate]
|
||||
var prev := 0.0
|
||||
for i in bounds.size():
|
||||
var span: float = bounds[i] - prev
|
||||
if remaining <= 0.0:
|
||||
break
|
||||
var take := minf(remaining, span)
|
||||
effective += take * rates[i]
|
||||
remaining -= take
|
||||
prev = bounds[i]
|
||||
if remaining > 0.0:
|
||||
effective += remaining * band4_rate
|
||||
out["seconds"] = s
|
||||
out["effective_hours"] = effective / 3600.0
|
||||
out["merit"] = effective * output_per_sec * offline_mult
|
||||
return out
|
||||
|
||||
|
||||
## 离线 8h ÷ 在线 1h 的等效比率(门 #9 直接用;offline_mult=1 口径)
|
||||
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,
|
||||
band3_h, band3_rate, band4_rate, 1e9)
|
||||
var one := settle(3600.0, 1.0, 1.0, band1_h, band1_rate, band2_h, band2_rate,
|
||||
band3_h, band3_rate, band4_rate, 1e9)
|
||||
if one["merit"] <= 0.0:
|
||||
return 0.0
|
||||
return float(r["merit"]) / float(one["merit"])
|
||||
@@ -0,0 +1,61 @@
|
||||
class_name PrestigeModel
|
||||
extends RefCounted
|
||||
|
||||
# ============================================================================
|
||||
# PrestigeModel —— 轮回转世纯函数(架构 §4.2;[B§4])
|
||||
#
|
||||
# 公式([B§4],参数全部来自 balance_tuning.csv,代码零硬编码):
|
||||
# 宿慧获得 = floor(√(本轮累计阴德 / div) × mult) × (1 + 宿慧获取加成)
|
||||
# × (1 - 冤气削减) 削减封顶 30%(m07 可降至 15%)
|
||||
# 跳过机制:每 skip_interval 级宿慧 → 前 skip_gens 档班底各 +skip_levels 级
|
||||
# (m04 把间隔 5 改为 4;工程裁定叠加,见 provenance P-11)
|
||||
# 敕封触发:转世 ≥ enthrone_rebirths([B§4.4] 约第 2-3 轮)
|
||||
# 重开曲线目标:1 : 0.6 : 0.4 : 0.3,第 5 轮起触底 ≥0.25×(门 #13 断言)
|
||||
#
|
||||
# 静态纯函数:sim 与游戏共用(零漂移);无状态、无分配(除返回值)。
|
||||
# ============================================================================
|
||||
|
||||
const RESTART_TARGETS: Array[float] = [1.0, 0.6, 0.4, 0.3]
|
||||
const RESTART_FLOOR := 0.25
|
||||
|
||||
|
||||
## 宿慧获得([B§4.1];gain_pct = m03 阶×0.15;penalty = 冤煞削减,已含封顶)
|
||||
static func suihui_gain(merit_run: float, div: float, mult: float, gain_pct: float, penalty: float) -> int:
|
||||
if merit_run <= 0.0 or div <= 0.0:
|
||||
return 0
|
||||
var raw := floor(sqrt(merit_run / div) * mult)
|
||||
var adjusted := raw * (1.0 + gain_pct) * (1.0 - penalty)
|
||||
return int(maxi(0, int(adjusted)))
|
||||
|
||||
|
||||
## 冤煞层成本([B§3.2]:第 n 层 = round(base × n^exp),n 从 1 起)
|
||||
static func wrath_layer_cost(layer_index: int, base: float, exp: float) -> int:
|
||||
var n := float(maxi(1, layer_index))
|
||||
return int(round(base * pow(n, exp)))
|
||||
|
||||
|
||||
## 冤气对宿慧的削减率(每层 +3%,封顶 30%;m07 改为 15%)
|
||||
static func wrath_penalty(layers: int, per_layer: float, cap: float, override_cap: float) -> float:
|
||||
var effective_cap := cap
|
||||
if override_cap > 0.0:
|
||||
effective_cap = minf(cap, override_cap)
|
||||
return minf(float(layers) * per_layer, effective_cap)
|
||||
|
||||
|
||||
## 跳过机制:本级宿慧应获得的跳过次数([B§4.2])
|
||||
static func skip_count(suihui: int, interval: int) -> int:
|
||||
if interval <= 0:
|
||||
return 0
|
||||
return int(suihui / interval)
|
||||
|
||||
|
||||
## 重开曲线目标倍率(第 rebirth_index 轮,0 起;第 5 轮起触底 RESTART_FLOOR)
|
||||
static func restart_target(rebirth_index: int) -> float:
|
||||
if rebirth_index < RESTART_TARGETS.size():
|
||||
return RESTART_TARGETS[rebirth_index]
|
||||
return RESTART_FLOOR
|
||||
|
||||
|
||||
## 敕封是否可用([B§4.4])
|
||||
static func enthrone_available(rebirths: int, min_rebirths: int) -> bool:
|
||||
return rebirths >= min_rebirths
|
||||
@@ -0,0 +1,42 @@
|
||||
class_name UpgradeDef
|
||||
extends Resource
|
||||
|
||||
# ============================================================================
|
||||
# UpgradeDef —— 升级数据行 → 实例(架构 §3.3;[B§2] 108 条 + meta 12 条)
|
||||
#
|
||||
# type 枚举([B§2] 五类型 + meta):
|
||||
# efficiency 班底效率(target_gen 产出 +effect_param,加法;6 级满 = ×7 < ×8 软上限)
|
||||
# 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)
|
||||
# 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 钉值复现)。
|
||||
# ============================================================================
|
||||
|
||||
@export var id: String # "up_eff_gen_01_youhun_1" / "meta_01"
|
||||
@export var name_cn: String
|
||||
@export var name_en: String
|
||||
@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_param: float # 效果参数(语义见 effect_type)
|
||||
@export var cost: int # 阴德成本(meta 行为宿慧成本,见 cost_is_suihui)
|
||||
@export var cost_is_suihui: bool = false # true = meta 行(cost 单位为宿慧)
|
||||
@export var max_rank: int = 1 # meta 可重复购买阶数;其余 1
|
||||
@export var requires: String = "none" # 购买前置:none / enthroned(仅 up_gate_tianzi)
|
||||
@export var condition: String = "none" # meta 效果条件:none / gen12_unlocked
|
||||
@export var desc_cn: String
|
||||
|
||||
|
||||
## 是否一次性购入(非 meta)
|
||||
func is_one_shot() -> bool:
|
||||
return type != "meta"
|
||||
|
||||
|
||||
## 是否纯解锁类(零数值效果)
|
||||
func is_unlock() -> bool:
|
||||
return type == "gate_hire" or type == "gate_system"
|
||||
@@ -0,0 +1,36 @@
|
||||
class_name BigNumber
|
||||
extends RefCounted
|
||||
|
||||
# ============================================================================
|
||||
# BigNumber —— 大数红线工具(架构 §4.2)
|
||||
#
|
||||
# 口径:double 全程(MVP 终局 ≤1e14 已锁定,技术评估 §3.1 预留的大数系统路径
|
||||
# 不启用)。本类只做两件事:
|
||||
# ① 红线断言:sim 门 #10 与 CI 用;超出 = push_error + 返回 false
|
||||
# ② 科学计数辅助:日志/报告用
|
||||
#
|
||||
# 纪律:core 层无 Node 依赖;零热路径分配(仅静态函数)。
|
||||
# 知识缺口:4.7.x 超出训练数据——本文件只用 4.x 稳定 API。
|
||||
# ============================================================================
|
||||
|
||||
## 终局红线([B§10] 门 #10:终局 ≤1e14)
|
||||
const RED_LINE := 1e14
|
||||
|
||||
|
||||
## 断言 value 未超红线;超了返回 false 并 push_error(带上下文)。
|
||||
static func assert_within(value: float, context: String = "") -> bool:
|
||||
if value > RED_LINE:
|
||||
push_error("BigNumber 超红线:%.3g > 1e14(上下文:%s)" % [value, context])
|
||||
return false
|
||||
return true
|
||||
|
||||
|
||||
## 科学计数串(报告/日志用;游戏内显示走 NumberFormat.short)
|
||||
static func sci(value: float, digits: int = 3) -> String:
|
||||
if value == 0.0:
|
||||
return "0"
|
||||
var sign := "" if value > 0.0 else "-"
|
||||
var v := absf(value)
|
||||
var exp := int(floor(log(v) / log(10.0)))
|
||||
var mant := v / pow(10.0, exp)
|
||||
return "%s%.*f e%d" % [sign, digits, mant, exp]
|
||||
@@ -0,0 +1,78 @@
|
||||
class_name NumberFormat
|
||||
extends RefCounted
|
||||
|
||||
# ============================================================================
|
||||
# NumberFormat —— 数字显示格式化(架构 §4.2 / GDD §7.2②)
|
||||
#
|
||||
# 口径:
|
||||
# - 游戏内短格式:<1000 原样整数;之后 K/M/B/T/Qa/Qi/Sx/Sp/Oc/No/Dc + aa/ab…
|
||||
# ([B§10] 终局 ≤1e14 → 实际只用到 T/aa 区间, ladder 预留到 Dc+)
|
||||
# - tabular-nums 对齐由字体层保证(JetBrains Mono,架构 §5.5);本类只产字符串
|
||||
# - 时间格式:秒 → "1h 25m" / "25m 30s" / "42s"(殿门预计时间 UI 用)
|
||||
#
|
||||
# 纪律:core 层无 Node 依赖;静态函数;零分配仅指调用方复用(字符串必然分配,
|
||||
# 热点路径由 UI 层脏检查兜底——架构 §4.2 快照纪律)。
|
||||
# ============================================================================
|
||||
|
||||
const SUFFIXES: Array[String] = [
|
||||
"", "K", "M", "B", "T",
|
||||
"Qa", "Qi", "Sx", "Sp", "Oc", "No", "Dc",
|
||||
"UDc", "DDc", "TDc", "QaDc", "QiDc",
|
||||
]
|
||||
|
||||
## aa/ab/ac… 双字母梯(第 18 档起;MVP 不会到达,预留)
|
||||
static func _letter_pair(index: int) -> String:
|
||||
var a := char("a".unicode_at(0) + index / 26)
|
||||
var b := char("a".unicode_at(0) + index % 26)
|
||||
return "%s%s" % [a, b]
|
||||
|
||||
|
||||
## 短格式:1234 → "1.23K";15 → "15";1.5e9 → "1.50B"
|
||||
static func short(value: float, decimals: int = 2) -> String:
|
||||
var v := absf(value)
|
||||
if v < 1000.0:
|
||||
return str(int(round(value)))
|
||||
var tier := int(floor(log(v) / log(1000.0)))
|
||||
if tier >= SUFFIXES.size():
|
||||
# 超出 ladder:退到 aa/ab 双字母(index 从 0 起继续 SUFFIXES 之后)
|
||||
var extra := tier - SUFFIXES.size()
|
||||
var scaled := v / pow(1000.0, tier)
|
||||
return "%.*f%s" % [decimals, scaled, _letter_pair(extra)]
|
||||
var scaled := v / pow(1000.0, tier)
|
||||
return "%.*f%s" % [decimals, scaled, SUFFIXES[tier]]
|
||||
|
||||
|
||||
## 整数短格式(无小数点):用于 tabular 对齐要求更严的槽位
|
||||
static func short_int(value: float) -> String:
|
||||
var v := absf(value)
|
||||
if v < 1000.0:
|
||||
return str(int(round(value)))
|
||||
var tier := int(floor(log(v) / log(1000.0)))
|
||||
if tier >= SUFFIXES.size():
|
||||
return short(value, 0)
|
||||
return "%d%s" % [int(round(v / pow(1000.0, tier))), SUFFIXES[tier]]
|
||||
|
||||
|
||||
## 秒 → "1h 25m" / "25m 30s" / "42s" / "1.2s"
|
||||
static func duration(seconds: float) -> String:
|
||||
var s := absf(seconds)
|
||||
if s >= 3600.0:
|
||||
var h := int(s / 3600.0)
|
||||
var m := int((s - h * 3600.0) / 60.0)
|
||||
if m == 0:
|
||||
return "%dh" % h
|
||||
return "%dh %dm" % [h, m]
|
||||
if s >= 60.0:
|
||||
var m2 := int(s / 60.0)
|
||||
var s2 := int(s - m2 * 60.0)
|
||||
if s2 == 0:
|
||||
return "%dm" % m2
|
||||
return "%dm %ds" % [m2, s2]
|
||||
if s >= 10.0:
|
||||
return "%ds" % int(round(s))
|
||||
return "%.1fs" % s
|
||||
|
||||
|
||||
## 分钟(整数)→ "2h 5m" / "42m"(殿门 target_min 显示)
|
||||
static func minutes(minutes_v: float) -> String:
|
||||
return duration(minutes_v * 60.0)
|
||||
@@ -0,0 +1,221 @@
|
||||
class_name SimGates
|
||||
extends RefCounted
|
||||
|
||||
# ============================================================================
|
||||
# SimGates —— 14 条模拟器验收门(架构 §9.3;GDD §12 / 草案 §2.7)
|
||||
#
|
||||
# 输入 = Simulator.run 报告(与游戏同一份 data/build/*.json 驱动)。
|
||||
# W2 状态:#11(殿王战胜率)deferred——BossBattle 逻辑 W3 落地后接入;
|
||||
# #12 用"策略级吞噬对照"近似(事件流 W3 接入后换真口径)。
|
||||
# 硬门(W6 起 CI 红):#5 主线时长 / #10 数值安全;其余 informational(§7.2)。
|
||||
# ============================================================================
|
||||
|
||||
const HARD_GATES: Array[int] = [5, 10] # W6 起 CI 硬门(架构 §7.2)
|
||||
|
||||
|
||||
## 单报告可评的门(#1-6 / #8-10 / #14)
|
||||
static func evaluate_report(report: Dictionary, data: BalanceData) -> Dictionary:
|
||||
var g := {}
|
||||
# #1 首购点击 ∈[10,20]
|
||||
var c1 := int(report.get("clicks_before_first_purchase", 0))
|
||||
g[1] = {"pass": c1 >= 10 and c1 <= 20, "detail": "首购点击 %d(目标 [10,20])" % c1}
|
||||
# #2 前 5 分钟购买 ∈[3,6]
|
||||
var c2 := int(report.get("purchases_first_5min", 0))
|
||||
g[2] = {"pass": c2 >= 3 and c2 <= 6, "detail": "前 5min 购买 %d 次(目标 [3,6])" % c2}
|
||||
# #3 解锁时间表(前 30min ±50%,之后 ±30%)
|
||||
g[3] = _gate_hall_table(report)
|
||||
# #4 购买间隔无 >60min 塌陷
|
||||
g[4] = _gate_purchase_gap(report)
|
||||
# #5 主线 ∈[2h,5h](到 H10)
|
||||
var h10 := _hall_time(report, 10)
|
||||
g[5] = {"pass": h10 >= 7200.0 and h10 <= 18000.0,
|
||||
"detail": "到 H10 %.0f min(目标 120-300min)" % (h10 / 60.0)}
|
||||
# #6 无卡墙:15min 窗口挂机产出 ≥ 最便宜可见升级 50%
|
||||
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)
|
||||
# #10 终局 ≤1e14
|
||||
var peak := float(report.get("peak_lifetime", 0.0))
|
||||
g[10] = {"pass": peak <= BigNumber.RED_LINE,
|
||||
"detail": "峰值累计 %.3g(红线 1e14)" % peak}
|
||||
# #11 殿王战(W3 deferred)
|
||||
g[11] = {"pass": true, "detail": "deferred:BossBattle 逻辑 W3 接入后评估", "deferred": true}
|
||||
# #14 殿门标定 ∈[80%,120%]
|
||||
g[14] = _gate_hall_calibration(report)
|
||||
return g
|
||||
|
||||
|
||||
## 跨策略门 #7(主导策略极差 <1.5×):入参 = 各策略报告数组
|
||||
static func evaluate_cross(reports: Array) -> Dictionary:
|
||||
var times: Array = []
|
||||
for r in reports:
|
||||
var t := _hall_time(r, 10)
|
||||
if t > 0.0:
|
||||
times.append(t)
|
||||
if times.size() < 2:
|
||||
return {"pass": false, "detail": "有效完成策略 <2(%d)" % times.size()}
|
||||
var lo := INF
|
||||
var hi := 0.0
|
||||
for t in times:
|
||||
lo = minf(lo, float(t))
|
||||
hi = maxf(hi, float(t))
|
||||
var spread := hi / lo if lo > 0.0 else INF
|
||||
return {"pass": spread < 1.5, "detail": "H10 时长极差 %.2f×(%s;目标 <1.5×)" % [
|
||||
spread, _join_times(reports)]}
|
||||
|
||||
|
||||
## 门 #12(怨气/宿慧取舍真实):吞噬轮 vs 纯阴德轮对照
|
||||
static func evaluate_wrath_tradeoff(pure_report: Dictionary, devour_report: Dictionary) -> Dictionary:
|
||||
var a := _hall_time(pure_report, 10)
|
||||
var b := _hall_time(devour_report, 10)
|
||||
if a <= 0.0 or b <= 0.0:
|
||||
return {"pass": false, "detail": "对照轮未完成(pure %.0fmin / devour %.0fmin)" % [a / 60.0, b / 60.0]}
|
||||
var ratio := b / a
|
||||
# 取舍"真实"= 两轮都能完成且时长有可感知差异(≥5%)
|
||||
var differ := absf(ratio - 1.0) >= 0.05
|
||||
return {"pass": differ and ratio > 0.3 and ratio < 3.0,
|
||||
"detail": "devour/pure = %.2f×(%.0fmin vs %.0fmin)" % [ratio, b / 60.0, a / 60.0]}
|
||||
|
||||
|
||||
## 门 #13(重开曲线):rebirths=4 的报告;各轮 H10 间隔比 ≈ 1:0.6:0.4:0.3
|
||||
static func evaluate_restart(report: Dictionary) -> Dictionary:
|
||||
var rebirths: Array = report.get("rebirths", [])
|
||||
if rebirths.size() < 2:
|
||||
return {"pass": false, "detail": "转世次数不足(%d)" % rebirths.size()}
|
||||
var intervals: Array = []
|
||||
var prev := 0.0
|
||||
for r in rebirths:
|
||||
intervals.append(float(r.at_sec) - prev)
|
||||
prev = float(r.at_sec)
|
||||
var base := float(intervals[0])
|
||||
if base <= 0.0:
|
||||
return {"pass": false, "detail": "首轮时长为 0"}
|
||||
var targets := PrestigeModel.RESTART_TARGETS
|
||||
var ok := true
|
||||
var parts: Array = []
|
||||
for i in intervals.size():
|
||||
var ratio := float(intervals[i]) / base
|
||||
var want: float = targets[i] if i < targets.size() else PrestigeModel.RESTART_FLOOR
|
||||
# 容差:前 4 轮 ±40%,第 5 轮起只查触底 ≥0.25×
|
||||
var good: bool
|
||||
if i < targets.size():
|
||||
good = absf(ratio - want) <= 0.4 * want
|
||||
else:
|
||||
good = ratio <= PrestigeModel.RESTART_FLOOR * 1.2
|
||||
if not good:
|
||||
ok = false
|
||||
parts.append("R%d=%.2f(目标 %.2f)" % [i + 1, ratio, want])
|
||||
return {"pass": ok, "detail": ";".join(parts)}
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# 内部
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
static func _hall_time(report: Dictionary, no: int) -> float:
|
||||
for h in report.get("halls", []):
|
||||
if int(h.no) == no:
|
||||
return float(h.at_sec)
|
||||
return 0.0
|
||||
|
||||
|
||||
static func _gate_hall_table(report: Dictionary) -> Dictionary:
|
||||
var ok := true
|
||||
var parts: Array = []
|
||||
for h in report.get("halls", []):
|
||||
var target_sec := float(h.target_min) * 60.0
|
||||
if target_sec <= 0.0:
|
||||
continue
|
||||
var ratio := float(h.at_sec) / target_sec
|
||||
var tol := 0.5 if target_sec <= 1800.0 else 0.3 # 目标 ≤30min 的殿 ±50%
|
||||
if ratio < 1.0 - tol or ratio > 1.0 + tol:
|
||||
ok = false
|
||||
parts.append("H%d %.0f/%.0fmin" % [int(h.no), float(h.at_sec) / 60.0, target_sec / 60.0])
|
||||
return {"pass": ok, "detail": ";".join(parts) if not parts.is_empty() else "无殿门事件"}
|
||||
|
||||
|
||||
static func _gate_hall_calibration(report: Dictionary) -> Dictionary:
|
||||
var ok := true
|
||||
var parts: Array = []
|
||||
for h in report.get("halls", []):
|
||||
var target_sec := float(h.target_min) * 60.0
|
||||
if target_sec <= 0.0:
|
||||
continue
|
||||
var ratio := float(h.at_sec) / target_sec
|
||||
if ratio < 0.8 or ratio > 1.2:
|
||||
ok = false
|
||||
parts.append("H%d %.0f%%" % [int(h.no), ratio * 100.0])
|
||||
return {"pass": ok, "detail": ";".join(parts) if not parts.is_empty() else "无殿门事件"}
|
||||
|
||||
|
||||
static func _gate_purchase_gap(report: Dictionary) -> Dictionary:
|
||||
var purchases: Array = report.get("purchases", [])
|
||||
if purchases.size() < 2:
|
||||
return {"pass": true, "detail": "购买事件 <2(跳过)"}
|
||||
var max_gap := 0.0
|
||||
var prev_t := float(purchases[0].t)
|
||||
for i in range(1, purchases.size()):
|
||||
var t := float(purchases[i].t)
|
||||
max_gap = maxf(max_gap, t - prev_t)
|
||||
prev_t = t
|
||||
return {"pass": max_gap <= 3600.0, "detail": "最大购买间隔 %.0f min(上限 60min)" % (max_gap / 60.0)}
|
||||
|
||||
|
||||
static func _gate_no_wall(report: Dictionary) -> Dictionary:
|
||||
var timeline: Array = report.get("timeline", [])
|
||||
if timeline.size() < 2:
|
||||
return {"pass": false, "detail": "时间线不足"}
|
||||
var worst := INF
|
||||
var worst_t := 0.0
|
||||
for s in timeline:
|
||||
var output := float(s.output)
|
||||
var cheap: Variant = s.get("min_upgrade_cost", 0.0)
|
||||
if typeof(cheap) != TYPE_FLOAT and typeof(cheap) != TYPE_INT:
|
||||
continue
|
||||
if float(cheap) <= 0.0 or is_inf(float(cheap)):
|
||||
continue # 无可见升级(全购完/未解锁)→ 不构成卡墙
|
||||
var income_15m := output * 900.0
|
||||
var ratio := income_15m / (0.5 * float(cheap))
|
||||
if ratio < worst:
|
||||
worst = ratio
|
||||
worst_t = float(s.t)
|
||||
return {"pass": worst >= 1.0,
|
||||
"detail": "最差窗口 %.2f×(t=%.0fmin;≥1.0 = 15min 挂机 ≥ 最便宜升级 50%%)" % [worst, worst_t / 60.0]}
|
||||
|
||||
|
||||
static func _gate_payback(data: BalanceData) -> Dictionary:
|
||||
var paybacks: Array = []
|
||||
for g in data.generators:
|
||||
paybacks.append(g.payback_seconds())
|
||||
var ok := true
|
||||
var detail := ""
|
||||
for i in range(1, 6): # tier2-6(0-indexed 1..5)
|
||||
var pb := float(paybacks[i])
|
||||
if pb < 120.0 or pb > 293.0:
|
||||
ok = false
|
||||
detail += "t%d %.0fs;" % [i + 1, pb]
|
||||
for i in range(1, paybacks.size() - 1):
|
||||
if float(paybacks[i]) >= float(paybacks[i + 1]):
|
||||
ok = false
|
||||
detail += "非单调@t%d;" % (i + 1)
|
||||
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}
|
||||
|
||||
|
||||
static func _join_times(reports: Array) -> String:
|
||||
var parts: Array = []
|
||||
for r in reports:
|
||||
var t := _hall_time(r, 10)
|
||||
parts.append("%s %.0fmin" % [str(r.get("strategy", "?")), t / 60.0])
|
||||
return ",".join(parts)
|
||||
@@ -0,0 +1,85 @@
|
||||
extends SceneTree
|
||||
|
||||
# ============================================================================
|
||||
# sim_main.gd —— 无头平衡模拟器入口(架构 §10.3;探针 P11)
|
||||
#
|
||||
# 运行方式(CI 第 4 步 / 本地):
|
||||
# godot --headless --path . -s res://src/core/sim/sim_main.gd
|
||||
# 输出:tests/sim_reports/report-<date>.json(14 门 + 里程碑快照,git 跟踪)
|
||||
#
|
||||
# W2 口径(架构 §7.2):sim 报告 informational;W6 起门 #5/#10 设硬门。
|
||||
# 因此本入口退出码恒 0(除非崩溃);硬门状态写在报告 hard_gates 字段。
|
||||
# ============================================================================
|
||||
|
||||
func _initialize() -> void:
|
||||
var data := BalanceData.load_from_dir("res://data/build/")
|
||||
if not data.load_errors.is_empty():
|
||||
for e in data.load_errors:
|
||||
printerr("sim_main:数据载入错误:%s" % e)
|
||||
quit(2)
|
||||
return
|
||||
var report := {
|
||||
"date": _date_stamp(),
|
||||
"engine_note": "Godot 4.7.x headless;EconomyState 与游戏同源",
|
||||
"runs": [],
|
||||
"gates": {},
|
||||
"cross": {},
|
||||
"restart": {},
|
||||
"wrath_tradeoff": {},
|
||||
}
|
||||
# 三策略主线(4h 预算;H10 目标 3h)
|
||||
var reports: Array = []
|
||||
for strategy in [NoviceStrategy.new(), GreedyStrategy.new(), RandomStrategy.new(7)]:
|
||||
var r := Simulator.run(data, strategy, {"max_sec": 4.0 * 3600.0})
|
||||
reports.append(r)
|
||||
report.runs.append(r)
|
||||
# 每报告门(各策略全量评)
|
||||
for r in reports:
|
||||
report.gates[str(r.strategy)] = SimGates.evaluate_report(r, data)
|
||||
# 主口径合并:门 #1/#2 为新手行为门,采 novice 报告;其余采 greedy 报告
|
||||
var novice_gates: Dictionary = report.gates.get("novice", {})
|
||||
var greedy_gates: Dictionary = report.gates.get("greedy", {})
|
||||
var primary: Dictionary = greedy_gates.duplicate()
|
||||
primary[1] = novice_gates.get(1, greedy_gates.get(1, {}))
|
||||
primary[2] = novice_gates.get(2, greedy_gates.get(2, {}))
|
||||
report.primary = primary
|
||||
# 跨策略门 #7
|
||||
report.cross = SimGates.evaluate_cross(reports)
|
||||
# 门 #13 重开曲线(greedy 5 轮,6h 预算)
|
||||
var restart_report := Simulator.run(data, GreedyStrategy.new(),
|
||||
{"max_sec": 6.0 * 3600.0, "rebirths": 4})
|
||||
report.restart = SimGates.evaluate_restart(restart_report)
|
||||
report.runs.append(restart_report)
|
||||
# 门 #12 怨气取舍对照(greedy devour vs pure,各 4h)
|
||||
var pure := Simulator.run(data, GreedyStrategy.new(), {"max_sec": 4.0 * 3600.0})
|
||||
var devour := Simulator.run(data, GreedyStrategy.new(),
|
||||
{"max_sec": 4.0 * 3600.0, "devour": true})
|
||||
report.wrath_tradeoff = SimGates.evaluate_wrath_tradeoff(pure, devour)
|
||||
# 硬门状态汇总(W6 起 CI 红;W2 informational)
|
||||
var hard := {}
|
||||
for gate_no in SimGates.HARD_GATES:
|
||||
var g: Dictionary = primary.get(gate_no, {})
|
||||
hard[str(gate_no)] = bool(g.get("pass", false))
|
||||
report.hard_gates = hard
|
||||
var path := "res://tests/sim_reports/report-%s.json" % _date_stamp()
|
||||
var f := FileAccess.open(path, FileAccess.WRITE)
|
||||
if f == null:
|
||||
printerr("sim_main:无法写报告 %s(Error %d)" % [path, FileAccess.get_open_error()])
|
||||
quit(3)
|
||||
return
|
||||
f.store_string(JSON.stringify(report, "\t"))
|
||||
f.close()
|
||||
# 摘要行(CI 日志可读;详细看 JSON)
|
||||
var fails: Array = []
|
||||
for k in primary:
|
||||
var gv: Dictionary = primary[k]
|
||||
if not bool(gv.get("pass", false)) and not bool(gv.get("deferred", false)):
|
||||
fails.append(k)
|
||||
print("sim_main:报告已写 %s;主口径未过门 = %s;硬门 = %s" % [
|
||||
path, str(fails), str(hard)])
|
||||
quit(0)
|
||||
|
||||
|
||||
func _date_stamp() -> String:
|
||||
var d := Time.get_datetime_dict_from_system()
|
||||
return "%04d-%02d-%02d" % [d.year, d.month, d.day]
|
||||
@@ -0,0 +1,124 @@
|
||||
class_name Simulator
|
||||
extends RefCounted
|
||||
|
||||
# ============================================================================
|
||||
# Simulator —— 无头平衡模拟器(架构 §10.3;W2 骨架)
|
||||
#
|
||||
# 与游戏零漂移的机制:驱动同一个 EconomyState.tick()(虚拟时钟 1s/步),
|
||||
# 读同一份 data/build/*.json(BalanceData.load_from_dir)。
|
||||
#
|
||||
# W2 范围:策略 novice/greedy/random;事件/殿王战未接入(W3),
|
||||
# 因此门 #11 在 gates.gd 标记 deferred;门 #12 用"策略级吞噬对照"近似
|
||||
# (wrath 入口 devour API 已就绪,W3 换真事件流)。
|
||||
# ============================================================================
|
||||
|
||||
const SNAPSHOT_INTERVAL := 60 # 里程碑快照间隔(秒)
|
||||
|
||||
|
||||
## 跑一个策略。opts:{max_sec, seed, rebirths, devour}
|
||||
## 返回报告 Dictionary( gates.gd 的输入)。
|
||||
static func run(data: BalanceData, strategy: SimStrategy, opts: Dictionary = {}) -> Dictionary:
|
||||
var max_sec := float(opts.get("max_sec", 4.0 * 3600.0))
|
||||
var rebirths_max := int(opts.get("rebirths", 0))
|
||||
var devour := bool(opts.get("devour", false))
|
||||
var econ := EconomyState.new(data)
|
||||
var t := 0.0
|
||||
var halls: Array = []
|
||||
var purchases: Array = []
|
||||
var rebirths: Array = []
|
||||
var timeline: Array = []
|
||||
var clicks := 0
|
||||
var clicks_before_first := 0
|
||||
var first_purchase_sec := -1.0
|
||||
var prev_bought := 0
|
||||
var prev_levels_sum := 0
|
||||
var prev_halls := 0
|
||||
var abort_reason := "max_sec"
|
||||
while t < max_sec:
|
||||
econ.tick(1.0)
|
||||
t += 1.0
|
||||
# 策略点击(act 内含;此处仅计数用近似:click_rate × 秒)
|
||||
clicks += int(ceil(strategy.click_rate))
|
||||
strategy.act(econ)
|
||||
if devour:
|
||||
# W2 近似吞噬流:按 GDD §3.3a 口径,每 60s 一次 45×sqrt(产出/s)
|
||||
if int(t) % 60 == 0:
|
||||
econ.devour(45.0 * sqrt(maxf(0.0, econ.output_per_sec())))
|
||||
econ.buy_wrath_layer()
|
||||
# 殿门事件
|
||||
if econ.halls_passed > prev_halls:
|
||||
for no in range(prev_halls + 1, econ.halls_passed + 1):
|
||||
halls.append({"no": no, "at_sec": t,
|
||||
"target_min": data.hall_by_no(no).target_min if data.hall_by_no(no) else 0.0})
|
||||
prev_halls = econ.halls_passed
|
||||
# 购买事件(升级购入 + 班底级数增长)
|
||||
if econ.bought.size() > prev_bought:
|
||||
if first_purchase_sec < 0.0:
|
||||
first_purchase_sec = t
|
||||
clicks_before_first = clicks
|
||||
purchases.append({"t": t, "kind": "upgrade", "count": econ.bought.size() - prev_bought})
|
||||
prev_bought = econ.bought.size()
|
||||
var lv_sum := 0
|
||||
for lv in econ.gen_levels:
|
||||
lv_sum += lv
|
||||
if lv_sum > prev_levels_sum:
|
||||
if first_purchase_sec < 0.0:
|
||||
first_purchase_sec = t
|
||||
clicks_before_first = clicks
|
||||
purchases.append({"t": t, "kind": "generator", "count": lv_sum - prev_levels_sum})
|
||||
prev_levels_sum = lv_sum
|
||||
# 里程碑快照
|
||||
if int(t) % SNAPSHOT_INTERVAL == 0:
|
||||
timeline.append({
|
||||
"t": t, "merit": econ.merit, "run_total": econ.merit_run_total,
|
||||
"output": econ.output_per_sec(), "levels": econ.gen_levels.duplicate(),
|
||||
"halls": econ.halls_passed, "suihui": econ.suihui,
|
||||
"min_upgrade_cost": _min_visible_upgrade_cost(econ),
|
||||
})
|
||||
# prestige(H10 达成 → 转世,重计时钟)
|
||||
if rebirths_max > 0 and econ.prestige_available() and rebirths.size() < rebirths_max:
|
||||
var r := econ.do_prestige()
|
||||
rebirths.append({"at_sec": t, "suihui_gain": int(r.get("suihui_gain", 0))})
|
||||
prev_halls = 0
|
||||
prev_bought = 0
|
||||
prev_levels_sum = 0
|
||||
if econ.halls_passed >= data.halls.size():
|
||||
abort_reason = "h10_reached"
|
||||
return {
|
||||
"strategy": strategy.strategy_name,
|
||||
"duration_sec": t,
|
||||
"abort_reason": abort_reason,
|
||||
"halls": halls,
|
||||
"purchases": purchases,
|
||||
"rebirths": rebirths,
|
||||
"timeline": timeline,
|
||||
"clicks_total": clicks,
|
||||
"clicks_before_first_purchase": clicks_before_first,
|
||||
"first_purchase_sec": first_purchase_sec,
|
||||
"purchases_first_5min": _count_before(purchases, 300.0),
|
||||
"peak_lifetime": econ.merit_lifetime,
|
||||
"end": {
|
||||
"merit": econ.merit, "output": econ.output_per_sec(),
|
||||
"levels": econ.gen_levels.duplicate(), "suihui": econ.suihui,
|
||||
"rebirths": econ.rebirths, "enthroned": econ.enthroned,
|
||||
"wrath_layers": econ.wrath_layers, "souls_saved": econ.souls_saved(),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
static func _count_before(purchases: Array, sec: float) -> int:
|
||||
var n := 0
|
||||
for p in purchases:
|
||||
if float(p.t) <= sec:
|
||||
n += int(p.count)
|
||||
return n
|
||||
|
||||
|
||||
## 当前最便宜"可见且未购"升级成本(门 #6 无卡墙检查用;INF = 无)
|
||||
static func _min_visible_upgrade_cost(econ: EconomyState) -> float:
|
||||
var best := INF
|
||||
for u in econ.data.upgrades:
|
||||
if econ.bought.has(u.id) or not econ.upgrade_visible(u):
|
||||
continue
|
||||
best = minf(best, float(u.cost))
|
||||
return best
|
||||
@@ -0,0 +1,67 @@
|
||||
class_name GreedyStrategy
|
||||
extends SimStrategy
|
||||
|
||||
# ============================================================================
|
||||
# GreedyStrategy —— 回本最快策略(架构 §10.3)
|
||||
#
|
||||
# 行为模型:每秒点 1 次;每决策点比较
|
||||
# ① 可见升级的"回本"(效率:cost / 该班底当前产出)
|
||||
# ② 各班底下一级的回本(cost / Δoutput)
|
||||
# 买最小者;bulk ×10 解锁后一次买 10 级(仍按回本选班底)。
|
||||
# 门 #7(主导策略极差 <1.5×)的对照臂。
|
||||
# ============================================================================
|
||||
|
||||
func _init() -> void:
|
||||
strategy_name = "greedy"
|
||||
click_rate = 1.0
|
||||
|
||||
|
||||
func act(econ: EconomyState) -> void:
|
||||
econ.click()
|
||||
var bulk10 := econ.bought.has("up_gate_bulk10") or econ.meta_ranks.get("meta_11", 0) > 0
|
||||
var count := 10 if bulk10 else 1
|
||||
var best_kind := ""
|
||||
var best_id := ""
|
||||
var best_gi := -1
|
||||
var best_pb := INF
|
||||
var gm := econ.global_mult()
|
||||
# 升级回本(效率类:产出增量 = 该班底当前产出 × param)
|
||||
for u in econ.data.upgrades:
|
||||
if econ.bought.has(u.id) or not econ.upgrade_visible(u) or not econ.requires_ok(u):
|
||||
continue
|
||||
if econ.merit < float(u.cost):
|
||||
continue
|
||||
var pb := INF
|
||||
if u.effect_type == "gen_output_add":
|
||||
var gi := econ.gen_index_of(u.target_gen)
|
||||
if gi < 0:
|
||||
continue
|
||||
var g := econ.data.generators[gi]
|
||||
var d_out := g.base_rate * float(econ.gen_levels[gi]) * u.effect_param * gm
|
||||
pb = float(u.cost) / d_out if d_out > 0.0 else INF
|
||||
else:
|
||||
# 点击/闸门/离线类:给一个小回本估值(点击区×2 ≈ 点击收益翻倍)
|
||||
pb = float(u.cost) / maxf(1.0, econ.click_value() * 2.0)
|
||||
if pb < best_pb:
|
||||
best_pb = pb
|
||||
best_kind = "upgrade"
|
||||
best_id = u.id
|
||||
best_gi = -1
|
||||
# 班底下一级回本
|
||||
var best_gen := best_payback_generator(econ)
|
||||
if best_gen >= 0:
|
||||
var g := econ.data.generators[best_gen]
|
||||
var cost := g.cost_range(econ.gen_levels[best_gen], count)
|
||||
if econ.merit >= cost:
|
||||
var d_out2 := g.base_rate * float(count) * econ.gen_mult(best_gen) * gm
|
||||
var pb := cost / d_out2 if d_out2 > 0.0 else INF
|
||||
if pb < best_pb:
|
||||
best_pb = pb
|
||||
best_kind = "generator"
|
||||
best_id = g.id
|
||||
best_gi = best_gen
|
||||
match best_kind:
|
||||
"upgrade":
|
||||
econ.buy_upgrade(best_id)
|
||||
"generator":
|
||||
econ.buy_generator(best_gi, count)
|
||||
@@ -0,0 +1,25 @@
|
||||
class_name NoviceStrategy
|
||||
extends SimStrategy
|
||||
|
||||
# ============================================================================
|
||||
# NoviceStrategy —— 新手策略(架构 §10.3:只点 + 买最便宜)
|
||||
#
|
||||
# 行为模型:每秒点 click_rate 次;有钱就买当前最便宜的可购目标
|
||||
#(班底级数 / 可见升级)。不规划、不攒钱、不用 bulk。
|
||||
# 模拟器门 #1(首购点击 ∈[10,20])与 #2(前 5 分钟购买 ∈[3,6])的主载体。
|
||||
# ============================================================================
|
||||
|
||||
func _init() -> void:
|
||||
strategy_name = "novice"
|
||||
click_rate = 2.0
|
||||
|
||||
|
||||
func act(econ: EconomyState) -> void:
|
||||
for _i in int(click_rate):
|
||||
econ.click()
|
||||
var t := cheapest_target(econ)
|
||||
match t.kind:
|
||||
"upgrade":
|
||||
econ.buy_upgrade(t.id)
|
||||
"generator":
|
||||
econ.buy_generator(t.gen_index, 1)
|
||||
@@ -0,0 +1,40 @@
|
||||
class_name RandomStrategy
|
||||
extends SimStrategy
|
||||
|
||||
# ============================================================================
|
||||
# RandomStrategy —— 随机策略( seeded ;门 #7 对照臂 + 无主导策略验证)
|
||||
#
|
||||
# 行为模型:每秒点 1 次;在可购目标里按 seed 随机挑一个买。
|
||||
# ============================================================================
|
||||
|
||||
var _rng := RandomNumberGenerator.new()
|
||||
|
||||
|
||||
func _init(seed_value: int = 7) -> void:
|
||||
strategy_name = "random"
|
||||
click_rate = 1.0
|
||||
_rng.seed = seed_value
|
||||
|
||||
|
||||
func act(econ: EconomyState) -> void:
|
||||
econ.click()
|
||||
# 收集可购目标后随机选(目标数 ≤ ~120,每秒一次可接受)
|
||||
var targets: Array = []
|
||||
for u in econ.data.upgrades:
|
||||
if econ.bought.has(u.id) or not econ.upgrade_visible(u) or not econ.requires_ok(u):
|
||||
continue
|
||||
if econ.merit >= float(u.cost):
|
||||
targets.append({"kind": "upgrade", "id": u.id, "gen_index": -1})
|
||||
for i in econ.data.generators.size():
|
||||
if not econ.generator_unlocked(i):
|
||||
continue
|
||||
if econ.merit >= econ.data.generators[i].cost_of_level(econ.gen_levels[i]):
|
||||
targets.append({"kind": "generator", "id": econ.data.generators[i].id, "gen_index": i})
|
||||
if targets.is_empty():
|
||||
return
|
||||
var pick: Dictionary = targets[_rng.randi_range(0, targets.size() - 1)]
|
||||
match pick.kind:
|
||||
"upgrade":
|
||||
econ.buy_upgrade(pick.id)
|
||||
"generator":
|
||||
econ.buy_generator(pick.gen_index, 1)
|
||||
@@ -0,0 +1,57 @@
|
||||
class_name SimStrategy
|
||||
extends RefCounted
|
||||
|
||||
# ============================================================================
|
||||
# SimStrategy —— 平衡模拟器策略基类(架构 §10.3)
|
||||
#
|
||||
# 契约:simulator 每模拟 1 秒调用一次 act(econ);策略可点击/购买/吞噬。
|
||||
# 策略只通过 EconomyState 公共 API 操作——与游戏命令同源(零漂移)。
|
||||
# ============================================================================
|
||||
|
||||
var strategy_name := "base"
|
||||
var click_rate := 1.0 # 每秒点击次数(novice 2.0 / greedy 1.0)
|
||||
|
||||
|
||||
func act(_econ: EconomyState) -> void:
|
||||
pass
|
||||
|
||||
|
||||
## 工具:买当前"最便宜"的可购目标(novice/random 共用)
|
||||
func cheapest_target(econ: EconomyState) -> Dictionary:
|
||||
var best := {"kind": "", "id": "", "gen_index": -1, "cost": INF}
|
||||
for u in econ.data.upgrades:
|
||||
if econ.bought.has(u.id) or not econ.upgrade_visible(u):
|
||||
continue
|
||||
if not econ.requires_ok(u):
|
||||
continue
|
||||
var c := float(u.cost)
|
||||
if c < best.cost and econ.merit >= c:
|
||||
best = {"kind": "upgrade", "id": u.id, "gen_index": -1, "cost": c}
|
||||
for i in econ.data.generators.size():
|
||||
if not econ.generator_unlocked(i):
|
||||
continue
|
||||
var c := econ.data.generators[i].cost_of_level(econ.gen_levels[i])
|
||||
if c < best.cost and econ.merit >= c:
|
||||
best = {"kind": "generator", "id": econ.data.generators[i].id, "gen_index": i, "cost": c}
|
||||
return best
|
||||
|
||||
|
||||
## 工具:买回本最快的一级班底(greedy 用;payback = Δcost / Δoutput)
|
||||
func best_payback_generator(econ: EconomyState) -> int:
|
||||
var best_i := -1
|
||||
var best_pb := INF
|
||||
var gm := econ.global_mult()
|
||||
for i in econ.data.generators.size():
|
||||
if not econ.generator_unlocked(i):
|
||||
continue
|
||||
var g := econ.data.generators[i]
|
||||
var lv := econ.gen_levels[i]
|
||||
var cost := g.cost_of_level(lv)
|
||||
var d_out := g.base_rate * econ.gen_mult(i) * gm
|
||||
if d_out <= 0.0 or econ.merit < cost:
|
||||
continue
|
||||
var pb := cost / d_out
|
||||
if pb < best_pb:
|
||||
best_pb = pb
|
||||
best_i = i
|
||||
return best_i
|
||||
@@ -0,0 +1,89 @@
|
||||
extends Node
|
||||
|
||||
# ============================================================================
|
||||
# DataRegistry —— Autoload;data/build/*.json → 定义的唯一查询入口
|
||||
# (架构 §2.3/§3.1;DataRegistry 只读,热重载入口)
|
||||
#
|
||||
# 依赖方向:game → core(BalanceData);本类不持有任何游戏状态。
|
||||
# 用法:任何层 `DataRegistry.generator("gen_03_guizu")`;
|
||||
# sim 与单测也可直接 new BalanceData().load_from_dir()(零漂移)。
|
||||
#
|
||||
# 热重载(架构 §3.6):dev 构建设置面板"重载数据"钮 → reload();
|
||||
# 运行时(export 构建)禁热重载——tools/ 不打包,data/ 只读。
|
||||
# ============================================================================
|
||||
|
||||
const BUILD_DIR := "res://data/build/"
|
||||
|
||||
var data: BalanceData
|
||||
var load_ok: bool = false
|
||||
|
||||
signal data_reloaded(errors: int)
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
ensure_loaded()
|
||||
|
||||
|
||||
## 幂等载入(GameState._ready 不依赖 autoload 顺序,显式调用)
|
||||
func ensure_loaded() -> void:
|
||||
if load_ok:
|
||||
return
|
||||
load_all()
|
||||
|
||||
|
||||
func load_all() -> void:
|
||||
data = BalanceData.load_from_dir(BUILD_DIR)
|
||||
load_ok = data.load_errors.is_empty()
|
||||
if not load_ok:
|
||||
for e in data.load_errors:
|
||||
push_error("DataRegistry:%s" % e)
|
||||
push_error("DataRegistry:载入存在 %d 条错误(详见上列)" % data.load_errors.size())
|
||||
data_reloaded.emit(data.load_errors.size())
|
||||
|
||||
|
||||
## dev 热重载:重新读盘(GameState 监听后重建 EconomyState 由调用方负责)
|
||||
func reload() -> void:
|
||||
load_ok = false
|
||||
load_all()
|
||||
|
||||
|
||||
# ---- 查询透传(薄封装;真实索引逻辑在 core/BalanceData)----
|
||||
|
||||
func generator(id: String) -> GeneratorDef:
|
||||
return data.generator(id)
|
||||
|
||||
|
||||
func upgrade(id: String) -> UpgradeDef:
|
||||
return data.upgrade(id)
|
||||
|
||||
|
||||
func meta(id: String) -> UpgradeDef:
|
||||
return data.meta(id)
|
||||
|
||||
|
||||
func hall(id_or_no: Variant) -> HallDef:
|
||||
return data.hall(id_or_no)
|
||||
|
||||
|
||||
func hall_by_no(no: int) -> HallDef:
|
||||
return data.hall_by_no(no)
|
||||
|
||||
|
||||
func boss_for_hall(hall_id: String) -> BossDef:
|
||||
return data.boss_for_hall(hall_id)
|
||||
|
||||
|
||||
func palette_for_hall(hall_id: String) -> PaletteDef:
|
||||
return data.palette_for_hall(hall_id)
|
||||
|
||||
|
||||
func event(id: String) -> EventDef:
|
||||
return data.event(id)
|
||||
|
||||
|
||||
func tune_num(key: String, default_value: float) -> float:
|
||||
return data.tune_num(key, default_value)
|
||||
|
||||
|
||||
func tune_text(key: String, default_value: String) -> String:
|
||||
return data.tune_text(key, default_value)
|
||||
@@ -0,0 +1,138 @@
|
||||
extends Node
|
||||
|
||||
# ============================================================================
|
||||
# GameState —— Autoload;唯一权威状态容器 + 系统 tick(架构 §2.3/§4.1)
|
||||
#
|
||||
# 职责:
|
||||
# - 持有 EconomyState(core 权威经济状态的唯一实例)
|
||||
# - _process 累加器步进:经济固定 10Hz(架构 §4.2),与渲染帧率解耦
|
||||
# - 命令 API(UI 唯一写入口):cmd_click / cmd_buy_* / cmd_prestige …
|
||||
# - 快照发布:10Hz snapshot_changed 信号(presentation 拉取)
|
||||
#
|
||||
# 纪律:
|
||||
# - game/ 不直连 Steam(W6 SteamService);不碰渲染(presentation 的活)
|
||||
# - UI 永不直接写 econ;一切经 cmd_*(校验+扣费在 core 完成)
|
||||
# - W2 不含事件导演/解锁导演/殿王战(W3+);殿门轮询在 EconomyState.tick 内
|
||||
# ============================================================================
|
||||
|
||||
const STEP := 0.1 # 经济固定步长 10Hz(架构 §4.2)
|
||||
|
||||
var econ: EconomyState
|
||||
var _acc: float = 0.0
|
||||
var _last_halls_passed: int = 0
|
||||
|
||||
signal snapshot_changed(snap: Dictionary) # 10Hz
|
||||
signal purchase_result(ok: bool, reason: String)
|
||||
signal hall_passed(hall_no: int)
|
||||
signal prestige_done(suihui_gain: int, rebirths: int)
|
||||
signal enthrone_done()
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
DataRegistry.ensure_loaded()
|
||||
econ = EconomyState.new(DataRegistry.data)
|
||||
_last_halls_passed = econ.halls_passed
|
||||
snapshot_changed.emit(econ.snapshot())
|
||||
|
||||
|
||||
func _process(_dt: float) -> void:
|
||||
_acc += _dt
|
||||
# 掉帧保护:单帧最多追 10 步(防后台切回巨额追帧)
|
||||
var steps := 0
|
||||
while _acc >= STEP and steps < 10:
|
||||
econ.tick(STEP)
|
||||
_acc -= STEP
|
||||
steps += 1
|
||||
if steps >= 10:
|
||||
_acc = 0.0
|
||||
_emit_hall_events()
|
||||
snapshot_changed.emit(econ.snapshot())
|
||||
|
||||
|
||||
func _emit_hall_events() -> void:
|
||||
while _last_halls_passed < econ.halls_passed:
|
||||
_last_halls_passed += 1
|
||||
hall_passed.emit(_last_halls_passed)
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# 命令 API(UI 唯一写入口)
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
## 点击(返回获得阴德;UI 可做飘字)
|
||||
func cmd_click() -> float:
|
||||
var gain := econ.click()
|
||||
_emit_hall_events()
|
||||
return gain
|
||||
|
||||
|
||||
## 买班底 count 级(-1 = 最大可买;bulk 解锁校验在 bulk_unlocked)
|
||||
func cmd_buy_generator(gen_index: int, count: int) -> Dictionary:
|
||||
var r := econ.buy_generator(gen_index, count)
|
||||
purchase_result.emit(bool(r.get("ok", false)), str(r.get("reason", "")))
|
||||
return r
|
||||
|
||||
|
||||
func cmd_buy_upgrade(id: String) -> Dictionary:
|
||||
var r := econ.buy_upgrade(id)
|
||||
purchase_result.emit(bool(r.get("ok", false)), str(r.get("reason", "")))
|
||||
return r
|
||||
|
||||
|
||||
func cmd_buy_meta(id: String) -> Dictionary:
|
||||
var r := econ.buy_meta(id)
|
||||
purchase_result.emit(bool(r.get("ok", false)), str(r.get("reason", "")))
|
||||
return r
|
||||
|
||||
|
||||
func cmd_buy_wrath_layer() -> Dictionary:
|
||||
var r := econ.buy_wrath_layer()
|
||||
purchase_result.emit(bool(r.get("ok", false)), str(r.get("reason", "")))
|
||||
return r
|
||||
|
||||
|
||||
## 事件入口(W3 EventDirector 接线;API 先行)
|
||||
func cmd_devour(lump: float) -> void:
|
||||
econ.devour(lump)
|
||||
|
||||
|
||||
func cmd_save_soul(lump: float) -> void:
|
||||
econ.save_soul(lump)
|
||||
|
||||
|
||||
func cmd_prestige() -> Dictionary:
|
||||
var r := econ.do_prestige()
|
||||
if bool(r.get("ok", false)):
|
||||
_last_halls_passed = 0
|
||||
prestige_done.emit(int(r.get("suihui_gain", 0)), int(r.get("rebirths", 0)))
|
||||
else:
|
||||
purchase_result.emit(false, str(r.get("reason", "")))
|
||||
return r
|
||||
|
||||
|
||||
func cmd_enthrone() -> Dictionary:
|
||||
var r := econ.do_enthrone()
|
||||
if bool(r.get("ok", false)):
|
||||
enthrone_done.emit()
|
||||
else:
|
||||
purchase_result.emit(false, str(r.get("reason", "")))
|
||||
return r
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# bulk 解锁查询([B§2.3] 系统闸门 + m11;UI 按钮显隐用)
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
func bulk_unlocked(mode: int) -> bool:
|
||||
if mode <= 1:
|
||||
return true
|
||||
if mode == 10:
|
||||
return econ.bought.has("up_gate_bulk10") or econ.meta_ranks.get("meta_11", 0) > 0
|
||||
if mode < 0: # max
|
||||
return econ.bought.has("up_gate_bulkmax")
|
||||
return false
|
||||
|
||||
|
||||
## 当前快照(UI _draw/刷新时拉取;10Hz 信号已广播,此为按需补拉)
|
||||
func get_snapshot() -> Dictionary:
|
||||
return econ.snapshot()
|
||||
@@ -0,0 +1,171 @@
|
||||
class_name HudRoot
|
||||
extends VBoxContainer
|
||||
|
||||
# ============================================================================
|
||||
# HudRoot —— 灰盒 HUD:升级列表 + 宿慧商店 + 轮回/敕封/怨煞(W2)
|
||||
#
|
||||
# 纪律(架构 §4.5):只读快照 + 发命令;UI 不持有游戏状态(仅视图本地态)。
|
||||
# 形态(§10.4):默认 Godot 主题 + 系统字体;按钮即行;零皮肤零图标。
|
||||
# 行池按需创建、快照驱动刷新;不可见/已购的行隐藏(不销毁,避免 10Hz 重建)。
|
||||
# ============================================================================
|
||||
|
||||
signal buy_upgrade_requested(id: String)
|
||||
signal buy_meta_requested(id: String)
|
||||
signal buy_wrath_layer_requested()
|
||||
signal prestige_requested()
|
||||
signal enthrone_requested()
|
||||
|
||||
var _upgrade_rows: Dictionary = {} # id -> {btn, label, last_text}
|
||||
var _upgrade_box: VBoxContainer
|
||||
var _meta_rows: Dictionary = {}
|
||||
var _meta_box: VBoxContainer
|
||||
var _prestige_btn: Button
|
||||
var _enthrone_btn: Button
|
||||
var _wrath_btn: Button
|
||||
var _suihui_label: Label
|
||||
var _wrath_label: Label
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
add_theme_constant_override("separation", 8)
|
||||
var split := HSplitContainer.new()
|
||||
split.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
add_child(split)
|
||||
# ---- 左:升级列表 ----
|
||||
var left_scroll := ScrollContainer.new()
|
||||
left_scroll.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
left_scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
split.add_child(left_scroll)
|
||||
_upgrade_box = VBoxContainer.new()
|
||||
_upgrade_box.size_flags_vertical = Control.SIZE_SHRINK_BEGIN
|
||||
left_scroll.add_child(_upgrade_box)
|
||||
var up_title := Label.new()
|
||||
up_title.text = "升级(操练/法器/路引/长明灯)"
|
||||
up_title.add_theme_font_size_override("font_size", 18)
|
||||
_upgrade_box.add_child(up_title)
|
||||
# ---- 右:宿慧/轮回 ----
|
||||
var right_scroll := ScrollContainer.new()
|
||||
right_scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
right_scroll.custom_minimum_size = Vector2(360, 0)
|
||||
split.add_child(right_scroll)
|
||||
_meta_box = VBoxContainer.new()
|
||||
_meta_box.size_flags_vertical = Control.SIZE_SHRINK_BEGIN
|
||||
right_scroll.add_child(_meta_box)
|
||||
var meta_title := Label.new()
|
||||
meta_title.text = "宿慧商店 / 轮回"
|
||||
meta_title.add_theme_font_size_override("font_size", 18)
|
||||
_meta_box.add_child(meta_title)
|
||||
_suihui_label = Label.new()
|
||||
_suihui_label.add_theme_font_size_override("font_size", 16)
|
||||
_meta_box.add_child(_suihui_label)
|
||||
_wrath_label = Label.new()
|
||||
_wrath_label.add_theme_font_size_override("font_size", 14)
|
||||
_meta_box.add_child(_wrath_label)
|
||||
_prestige_btn = Button.new()
|
||||
_prestige_btn.pressed.connect(func(): prestige_requested.emit())
|
||||
_meta_box.add_child(_prestige_btn)
|
||||
_enthrone_btn = Button.new()
|
||||
_enthrone_btn.pressed.connect(func(): enthrone_requested.emit())
|
||||
_meta_box.add_child(_enthrone_btn)
|
||||
_wrath_btn = Button.new()
|
||||
_wrath_btn.pressed.connect(func(): buy_wrath_layer_requested.emit())
|
||||
_meta_box.add_child(_wrath_btn)
|
||||
|
||||
|
||||
## 快照刷新
|
||||
func refresh(snap: Dictionary) -> void:
|
||||
# 升级行池
|
||||
var visible_ids := {}
|
||||
for u in snap.get("upgrades", []):
|
||||
var id := str(u.get("id", ""))
|
||||
visible_ids[id] = true
|
||||
var row: Variant = _upgrade_rows.get(id)
|
||||
if row == null:
|
||||
row = _create_upgrade_row(id)
|
||||
var d: Dictionary = row
|
||||
var avail := bool(u.get("available", false))
|
||||
var text := "%s · %s" % [
|
||||
str(u.get("name_cn", "")),
|
||||
NumberFormat.short_int(float(u.get("cost", 0.0)))]
|
||||
var btn: Button = d.btn
|
||||
var label: Label = d.label
|
||||
if label.text != text:
|
||||
label.text = text
|
||||
btn.tooltip_text = str(u.get("desc_cn", ""))
|
||||
btn.disabled = not avail
|
||||
btn.visible = true
|
||||
for id in _upgrade_rows:
|
||||
if not visible_ids.has(id):
|
||||
var d2: Dictionary = _upgrade_rows[id]
|
||||
(d2.btn as Button).visible = false
|
||||
# 宿慧/轮回区
|
||||
_suihui_label.text = "宿慧 %d(转世 %d 次%s)" % [
|
||||
int(snap.get("suihui", 0)), int(snap.get("rebirths", 0)),
|
||||
" · 已敕封" if bool(snap.get("enthroned", false)) else ""]
|
||||
_wrath_label.text = "冤气 %s · 怨煞 %d 层(全局 ×%.2f)" % [
|
||||
NumberFormat.short(float(snap.get("wrath", 0.0))),
|
||||
int(snap.get("wrath_layers", 0)),
|
||||
float(snap.get("global_mult", 1.0))]
|
||||
_prestige_btn.disabled = not bool(snap.get("prestige_available", false))
|
||||
_prestige_btn.text = "轮回转世(+%d 宿慧)" % int(snap.get("suihui_gain", 0))
|
||||
var ent_avail := bool(snap.get("enthrone_available", false))
|
||||
_enthrone_btn.visible = ent_avail or bool(snap.get("enthroned", false))
|
||||
_enthrone_btn.disabled = not ent_avail
|
||||
_enthrone_btn.text = "已受敕" if bool(snap.get("enthroned", false)) else "受敕(阴司敕封)"
|
||||
_wrath_btn.disabled = float(snap.get("wrath", 0.0)) < float(snap.get("wrath_layer_cost", INF))
|
||||
_wrath_btn.text = "叠怨煞层(%s 冤气)" % NumberFormat.short_int(float(snap.get("wrath_layer_cost", 0.0)))
|
||||
# 宿慧商品行池
|
||||
for m in snap.get("metas", []):
|
||||
var mid := str(m.get("id", ""))
|
||||
var mrow: Variant = _meta_rows.get(mid)
|
||||
if mrow == null:
|
||||
mrow = _create_meta_row(mid)
|
||||
var md: Dictionary = mrow
|
||||
var mbtn: Button = md.btn
|
||||
var mlabel: Label = md.label
|
||||
var rank := int(m.get("rank", 0))
|
||||
var max_rank := int(m.get("max_rank", 1))
|
||||
var mtext := "%s%s · %s 宿慧" % [
|
||||
str(m.get("name_cn", "")),
|
||||
" ×%d" % rank if max_rank > 1 else "",
|
||||
NumberFormat.short_int(float(m.get("cost", 0.0)))]
|
||||
if mlabel.text != mtext:
|
||||
mlabel.text = mtext
|
||||
mbtn.tooltip_text = str(m.get("desc_cn", ""))
|
||||
mbtn.disabled = not bool(m.get("available", false))
|
||||
mbtn.visible = rank < max_rank
|
||||
|
||||
|
||||
func _create_upgrade_row(id: String) -> Dictionary:
|
||||
var btn := Button.new()
|
||||
btn.focus_mode = Control.FOCUS_NONE
|
||||
btn.alignment = HorizontalAlignment.LEFT
|
||||
btn.pressed.connect(func(): buy_upgrade_requested.emit(id))
|
||||
var label := Label.new()
|
||||
label.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
label.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
label.offset_left = 6.0
|
||||
label.add_theme_font_size_override("font_size", 13)
|
||||
label.autowrap_mode = TextServer.AUTOWRAP_OFF
|
||||
btn.add_child(label)
|
||||
_upgrade_box.add_child(btn)
|
||||
var d := {"btn": btn, "label": label}
|
||||
_upgrade_rows[id] = d
|
||||
return d
|
||||
|
||||
|
||||
func _create_meta_row(id: String) -> Dictionary:
|
||||
var btn := Button.new()
|
||||
btn.focus_mode = Control.FOCUS_NONE
|
||||
btn.alignment = HorizontalAlignment.LEFT
|
||||
btn.pressed.connect(func(): buy_meta_requested.emit(id))
|
||||
var label := Label.new()
|
||||
label.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
label.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
label.offset_left = 6.0
|
||||
label.add_theme_font_size_override("font_size", 13)
|
||||
btn.add_child(label)
|
||||
_meta_box.add_child(btn)
|
||||
var d := {"btn": btn, "label": label}
|
||||
_meta_rows[id] = d
|
||||
return d
|
||||
@@ -0,0 +1,122 @@
|
||||
class_name ArmyBand
|
||||
extends HBoxContainer
|
||||
|
||||
# ============================================================================
|
||||
# ArmyBand —— 军列带 12 槽灰盒版(架构 §4.3/§10.2 的 W2 落地;§10.4 灰盒规范)
|
||||
#
|
||||
# 12 槽数据驱动(图标位置 = 级数/成本/状态;GDD §7.1):
|
||||
# 左键 = 买 ×1;右键 = 买 ×10(up_gate_bulk10 或 m11 解锁后);
|
||||
# Shift+左键 = 买最大(up_gate_bulkmax 解锁后)。
|
||||
# 三级分层可视化(GDD §2.4):可购 = 骨白边;锁定 = 40% 透明 + "未解锁"。
|
||||
# W3 换 MultiMesh 像素阴兵实例(探针 P10);本版零精灵、100% 可弃。
|
||||
# ============================================================================
|
||||
|
||||
signal buy_requested(gen_index: int, count: int)
|
||||
|
||||
const BONE := Color("F2EDE4")
|
||||
const CINNABAR := Color("B4402C")
|
||||
|
||||
var _slots: Array[Control] = []
|
||||
var _fills: Array[ColorRect] = []
|
||||
var _frames: Array[ColorRect] = []
|
||||
var _name_labels: Array[Label] = []
|
||||
var _level_labels: Array[Label] = []
|
||||
var _cost_labels: Array[Label] = []
|
||||
var _out_labels: Array[Label] = []
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
alignment = BoxContainer.ALIGNMENT_BEGIN
|
||||
add_theme_constant_override("separation", 6)
|
||||
for i in DataRegistry.data.generators.size():
|
||||
_build_slot(i, DataRegistry.data.generators[i])
|
||||
|
||||
|
||||
func _build_slot(index: int, def: GeneratorDef) -> void:
|
||||
var btn := Button.new()
|
||||
btn.custom_minimum_size = Vector2(96, 132)
|
||||
btn.tooltip_text = "%s:左键×1 / 右键×10 / Shift+左键最大" % def.display_cn
|
||||
btn.set_meta("gen_index", index)
|
||||
btn.focus_mode = Control.FOCUS_NONE
|
||||
btn.gui_input.connect(_on_slot_gui_input.bind(btn))
|
||||
var vbox := VBoxContainer.new()
|
||||
vbox.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
vbox.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
vbox.offset_left = 4.0
|
||||
vbox.offset_top = 2.0
|
||||
vbox.offset_right = -4.0
|
||||
vbox.offset_bottom = -2.0
|
||||
btn.add_child(vbox)
|
||||
# 色块(一色一档,灰阶 ramp;§10.4"干净地丑")
|
||||
var frame := ColorRect.new()
|
||||
frame.color = BONE
|
||||
frame.custom_minimum_size = Vector2(0, 4)
|
||||
vbox.add_child(frame)
|
||||
var fill := ColorRect.new()
|
||||
fill.custom_minimum_size = Vector2(0, 34)
|
||||
fill.color = Color.from_hsv(0.08, 0.06, 0.16 + 0.05 * index)
|
||||
vbox.add_child(fill)
|
||||
var name_l := Label.new()
|
||||
name_l.add_theme_font_size_override("font_size", 13)
|
||||
vbox.add_child(name_l)
|
||||
var lv_l := Label.new()
|
||||
lv_l.add_theme_font_size_override("font_size", 13)
|
||||
vbox.add_child(lv_l)
|
||||
var cost_l := Label.new()
|
||||
cost_l.add_theme_font_size_override("font_size", 12)
|
||||
vbox.add_child(cost_l)
|
||||
var out_l := Label.new()
|
||||
out_l.add_theme_font_size_override("font_size", 11)
|
||||
vbox.add_child(out_l)
|
||||
add_child(btn)
|
||||
_slots.append(btn)
|
||||
_fills.append(fill)
|
||||
_frames.append(frame)
|
||||
_name_labels.append(name_l)
|
||||
_level_labels.append(lv_l)
|
||||
_cost_labels.append(cost_l)
|
||||
_out_labels.append(out_l)
|
||||
|
||||
|
||||
func _on_slot_gui_input(event: InputEvent, btn: Control) -> void:
|
||||
if not (event is InputEventMouseButton):
|
||||
return
|
||||
var mb := event as InputEventMouseButton
|
||||
if not mb.pressed:
|
||||
return
|
||||
var index := int(btn.get_meta("gen_index"))
|
||||
if mb.button_index == MOUSE_BUTTON_LEFT:
|
||||
if mb.shift_pressed:
|
||||
buy_requested.emit(index, -1) # 最大
|
||||
else:
|
||||
buy_requested.emit(index, 1)
|
||||
elif mb.button_index == MOUSE_BUTTON_RIGHT:
|
||||
buy_requested.emit(index, 10)
|
||||
|
||||
|
||||
## 快照刷新(只改文本;Godot Label 内部对同文本不重排)
|
||||
func refresh(snap: Dictionary) -> void:
|
||||
var gens: Array = snap.get("gens", [])
|
||||
for i in _slots.size():
|
||||
if i >= gens.size():
|
||||
_slots[i].visible = false
|
||||
continue
|
||||
var g: Dictionary = gens[i]
|
||||
var unlocked := bool(g.get("unlocked", false))
|
||||
var affordable := bool(g.get("affordable", false))
|
||||
_name_labels[i].text = str(g.get("name_cn", ""))
|
||||
if unlocked:
|
||||
_level_labels[i].text = "Lv %d" % int(g.get("level", 0))
|
||||
_cost_labels[i].text = NumberFormat.short_int(float(g.get("cost_next", 0.0)))
|
||||
_out_labels[i].text = "%s/s" % NumberFormat.short(float(g.get("output", 0.0)))
|
||||
else:
|
||||
_level_labels[i].text = "—"
|
||||
_cost_labels[i].text = "未解锁"
|
||||
_out_labels[i].text = ""
|
||||
# 三级分层:锁定 40% + 剪影感(纯色压暗);可购骨白边;其余无边框
|
||||
_slots[i].modulate = Color(1, 1, 1, 1) if unlocked else Color(1, 1, 1, 0.4)
|
||||
_frames[i].color = BONE if (unlocked and affordable) else Color("6E6E6E")
|
||||
if unlocked:
|
||||
_fills[i].color = Color.from_hsv(0.08, 0.06, 0.16 + 0.05 * i)
|
||||
else:
|
||||
_fills[i].color = Color("2A2A2A")
|
||||
@@ -0,0 +1,87 @@
|
||||
class_name HallBand
|
||||
extends Control
|
||||
|
||||
# ============================================================================
|
||||
# HallBand —— 单殿灰盒色带(架构 §4.3 的 W2 灰盒版;§10.4 灰盒规范)
|
||||
#
|
||||
# 形态:纯色块 + 文字(殿号/殿王/殿门阈值/状态)。色相取自 hall_palettes.csv
|
||||
# 的 accent_hex(数据驱动:换殿 = 换一行数据 + 换一个色,零代码)。
|
||||
# 状态:locked = 40% 透明 + "未解锁";current = 朱砂边;passed = 骨白边。
|
||||
# 禁止:任何 sprite/半成品美术/shader(§10.4 退出条件 = 100% 可弃)。
|
||||
# ============================================================================
|
||||
|
||||
const INK := Color("1A1A1A")
|
||||
const BONE := Color("F2EDE4")
|
||||
const CINNABAR := Color("B4402C")
|
||||
|
||||
var _fill: ColorRect
|
||||
var _frame: ColorRect
|
||||
var _labels: VBoxContainer
|
||||
var _state_label: Label
|
||||
|
||||
static func make(def: HallDef, palette: PaletteDef) -> HallBand:
|
||||
var band := HallBand.new()
|
||||
band.custom_minimum_size = Vector2(112, 96)
|
||||
band._build(def, palette)
|
||||
return band
|
||||
|
||||
|
||||
func _build(def: HallDef, palette: PaletteDef) -> void:
|
||||
# 外框(边色随状态换)+ 内填充(殿 accent 色)
|
||||
_frame = ColorRect.new()
|
||||
_frame.color = INK
|
||||
_frame.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
add_child(_frame)
|
||||
_fill = ColorRect.new()
|
||||
_fill.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
_fill.offset_left = 3.0
|
||||
_fill.offset_top = 3.0
|
||||
_fill.offset_right = -3.0
|
||||
_fill.offset_bottom = -3.0
|
||||
var accent := INK
|
||||
if palette != null and palette.accent_hex != "":
|
||||
accent = Color(palette.accent_hex)
|
||||
_fill.color = accent
|
||||
add_child(_fill)
|
||||
_labels = VBoxContainer.new()
|
||||
_labels.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
_labels.offset_left = 8.0
|
||||
_labels.offset_top = 6.0
|
||||
_labels.offset_right = -8.0
|
||||
_labels.offset_bottom = -6.0
|
||||
add_child(_labels)
|
||||
var title := Label.new()
|
||||
title.text = "H%d %s" % [def.no, def.king_cn]
|
||||
title.add_theme_font_size_override("font_size", 16)
|
||||
title.add_theme_color_override("font_color", BONE)
|
||||
_labels.add_child(title)
|
||||
var gate := Label.new()
|
||||
gate.text = "门 %s" % NumberFormat.short_int(def.gate_merit)
|
||||
gate.add_theme_font_size_override("font_size", 13)
|
||||
gate.add_theme_color_override("font_color", BONE)
|
||||
_labels.add_child(gate)
|
||||
var target := Label.new()
|
||||
target.text = "~%s" % NumberFormat.minutes(def.target_min)
|
||||
target.add_theme_font_size_override("font_size", 12)
|
||||
target.add_theme_color_override("font_color", BONE)
|
||||
_labels.add_child(target)
|
||||
_state_label = Label.new()
|
||||
_state_label.add_theme_font_size_override("font_size", 12)
|
||||
_state_label.add_theme_color_override("font_color", BONE)
|
||||
_labels.add_child(_state_label)
|
||||
|
||||
|
||||
## 状态刷新(快照驱动;passed/current/locked 三态,GDD §2.4 三级分层)
|
||||
func set_state(passed: bool, current: bool, have: float, need: float) -> void:
|
||||
if passed:
|
||||
_frame.color = BONE
|
||||
modulate = Color(1, 1, 1, 1)
|
||||
_state_label.text = "已过"
|
||||
return
|
||||
modulate = Color(1, 1, 1, 0.4) if not current else Color(1, 1, 1, 1)
|
||||
_frame.color = CINNABAR if current else INK
|
||||
if current:
|
||||
var pct := 0.0 if need <= 0.0 else clampf(have / need, 0.0, 1.0) * 100.0
|
||||
_state_label.text = "%.0f%%" % pct
|
||||
else:
|
||||
_state_label.text = "未解锁"
|
||||
@@ -0,0 +1,239 @@
|
||||
extends Node
|
||||
|
||||
# ============================================================================
|
||||
# stage_root.gd —— 灰盒主场景(W2 可玩性验证版;架构 §10.4 灰盒规范)
|
||||
#
|
||||
# 验证循环(W3 出口标准的提前落地):点 → 买 → 涨 → 解锁 → prestige 全循环,
|
||||
# 零美术资产。布局即 §10.2 纸面原型的程序化落地:
|
||||
# 顶栏(阴德/产出/点击/冤气/宿慧/转世/超度)|十殿色带|军列带 12 槽|
|
||||
# 升级列表 + 宿慧商店(含轮回/敕封/怨煞)|日志行
|
||||
#
|
||||
# 纪律:只读 GameState 快照 + 发命令;本场景不持有任何游戏状态。
|
||||
# ============================================================================
|
||||
|
||||
const INK := Color("1A1A1A")
|
||||
const BONE := Color("F2EDE4")
|
||||
const CINNABAR := Color("B4402C")
|
||||
|
||||
var _merit_label: Label
|
||||
var _rate_label: Label
|
||||
var _click_btn: Button
|
||||
var _wrath_label: Label
|
||||
var _suihui_label: Label
|
||||
var _rebirth_label: Label
|
||||
var _souls_label: Label
|
||||
var _progress_label: Label
|
||||
var _log_label: Label
|
||||
var _army: ArmyBand
|
||||
var _hud: HudRoot
|
||||
var _hall_bands: Array[HallBand] = []
|
||||
var _progress_shown := ""
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
if DataRegistry.data == null:
|
||||
push_error("stage_root:DataRegistry 无数据")
|
||||
return
|
||||
_build_ui()
|
||||
_connect()
|
||||
|
||||
|
||||
func _build_ui() -> void:
|
||||
var margin := MarginContainer.new()
|
||||
margin.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
margin.add_theme_constant_override("margin_all", 8)
|
||||
add_child(margin)
|
||||
var root_v := VBoxContainer.new()
|
||||
root_v.add_theme_constant_override("separation", 6)
|
||||
margin.add_child(root_v)
|
||||
|
||||
# ---- 顶栏 ----
|
||||
var top := HBoxContainer.new()
|
||||
top.add_theme_constant_override("separation", 18)
|
||||
root_v.add_child(top)
|
||||
top.add_child(_stat("阴德", 14))
|
||||
_merit_label = _big_label(26)
|
||||
top.add_child(_merit_label)
|
||||
top.add_child(_stat("产出/s", 14))
|
||||
_rate_label = _big_label(18)
|
||||
top.add_child(_rate_label)
|
||||
_click_btn = Button.new()
|
||||
_click_btn.custom_minimum_size = Vector2(150, 56)
|
||||
_click_btn.add_theme_font_size_override("font_size", 22)
|
||||
_click_btn.pressed.connect(_on_click_pressed)
|
||||
top.add_child(_click_btn)
|
||||
top.add_child(_stat("冤气", 14))
|
||||
_wrath_label = _big_label(16)
|
||||
top.add_child(_wrath_label)
|
||||
top.add_child(_stat("宿慧", 14))
|
||||
_suihui_label = _big_label(16)
|
||||
top.add_child(_suihui_label)
|
||||
top.add_child(_stat("转世", 14))
|
||||
_rebirth_label = _big_label(16)
|
||||
top.add_child(_rebirth_label)
|
||||
top.add_child(_stat("超度", 14))
|
||||
_souls_label = _big_label(16)
|
||||
top.add_child(_souls_label)
|
||||
|
||||
# ---- 十殿色带(数据行 → 色带;§10.4:10 条色带 + 殿名文字)----
|
||||
var hall_row := HBoxContainer.new()
|
||||
hall_row.add_theme_constant_override("separation", 4)
|
||||
hall_row.custom_minimum_size = Vector2(0, 100)
|
||||
root_v.add_child(hall_row)
|
||||
for h in DataRegistry.data.halls:
|
||||
var pal := DataRegistry.data.palette_for_hall(h.id)
|
||||
var band := HallBand.make(h, pal)
|
||||
hall_row.add_child(band)
|
||||
_hall_bands.append(band)
|
||||
|
||||
# ---- 军列带 12 槽 ----
|
||||
_army = ArmyBand.new()
|
||||
_army.custom_minimum_size = Vector2(0, 140)
|
||||
root_v.add_child(_army)
|
||||
|
||||
# ---- HUD(升级 + 宿慧商店)----
|
||||
_hud = HudRoot.new()
|
||||
root_v.add_child(_hud)
|
||||
|
||||
# ---- 进度行(目标梯度:下一座殿门,[B§6]"距 XX 还差 X")----
|
||||
_progress_label = Label.new()
|
||||
_progress_label.add_theme_font_size_override("font_size", 14)
|
||||
_progress_label.add_theme_color_override("font_color", BONE)
|
||||
root_v.add_child(_progress_label)
|
||||
|
||||
# ---- 日志行 ----
|
||||
_log_label = Label.new()
|
||||
_log_label.add_theme_font_size_override("font_size", 13)
|
||||
_log_label.add_theme_color_override("font_color", BONE)
|
||||
_log_label.text = "灰盒版:点→买→涨→解锁→轮回。左键×1 / 右键×10 / Shift+左键最大。"
|
||||
root_v.add_child(_log_label)
|
||||
# 全局墨色底(灰盒唯一"视觉"决定:高对比)
|
||||
var bg := ColorRect.new()
|
||||
bg.color = INK
|
||||
bg.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
bg.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
bg.z_index = -10
|
||||
add_child(bg)
|
||||
|
||||
|
||||
func _stat(text: String, size: int) -> Label:
|
||||
var l := Label.new()
|
||||
l.text = text
|
||||
l.add_theme_font_size_override("font_size", size)
|
||||
l.add_theme_color_override("font_color", BONE)
|
||||
return l
|
||||
|
||||
|
||||
func _big_label(size: int) -> Label:
|
||||
var l := Label.new()
|
||||
l.add_theme_font_size_override("font_size", size)
|
||||
l.add_theme_color_override("font_color", BONE)
|
||||
l.custom_minimum_size = Vector2(110, 0)
|
||||
return l
|
||||
|
||||
|
||||
func _connect() -> void:
|
||||
_army.buy_requested.connect(_on_buy_generator)
|
||||
_hud.buy_upgrade_requested.connect(_on_buy_upgrade)
|
||||
_hud.buy_meta_requested.connect(_on_buy_meta)
|
||||
_hud.buy_wrath_layer_requested.connect(func(): GameState.cmd_buy_wrath_layer())
|
||||
_hud.prestige_requested.connect(func(): GameState.cmd_prestige())
|
||||
_hud.enthrone_requested.connect(func(): GameState.cmd_enthrone())
|
||||
GameState.snapshot_changed.connect(_on_snapshot)
|
||||
GameState.hall_passed.connect(_on_hall_passed)
|
||||
GameState.prestige_done.connect(_on_prestige_done)
|
||||
GameState.enthrone_done.connect(_on_enthrone_done)
|
||||
_on_snapshot(GameState.get_snapshot())
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# 命令下行(UI 唯一写入口;校验与扣费在 core/game)
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
func _on_click_pressed() -> void:
|
||||
var gain := GameState.cmd_click()
|
||||
_log_label.text = "指诀一掐,阴德 +%s。" % NumberFormat.short(gain)
|
||||
|
||||
|
||||
func _on_buy_generator(gen_index: int, count: int) -> void:
|
||||
if not GameState.bulk_unlocked(count if count > 0 else -1) and count != 1:
|
||||
_log_label.text = "批量购置未解锁(路引·批量 or 老马识途)。"
|
||||
return
|
||||
var r := GameState.cmd_buy_generator(gen_index, count)
|
||||
if bool(r.get("ok", false)):
|
||||
var gens: Array = GameState.get_snapshot().get("gens", [])
|
||||
var name := str((gens[gen_index] as Dictionary).get("name_cn", "")) if gen_index < gens.size() else ""
|
||||
_log_label.text = "%s 入列 ×%d(-%s 阴德)。" % [name, int(r.get("bought", 0)),
|
||||
NumberFormat.short(float(r.get("spent", 0.0)))]
|
||||
else:
|
||||
_log_label.text = "购不得:%s" % str(r.get("reason", ""))
|
||||
|
||||
|
||||
func _on_buy_upgrade(id: String) -> void:
|
||||
var r := GameState.cmd_buy_upgrade(id)
|
||||
_log_label.text = "已购 %s。" % id if bool(r.get("ok", false)) else "购不得:%s" % str(r.get("reason", ""))
|
||||
|
||||
|
||||
func _on_buy_meta(id: String) -> void:
|
||||
var r := GameState.cmd_buy_meta(id)
|
||||
_log_label.text = "宿慧觉醒 %s(阶 %d)。" % [id, int(r.get("rank", 0))] if bool(r.get("ok", false)) \
|
||||
else "购不得:%s" % str(r.get("reason", ""))
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# 快照上行(10Hz;只读)
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
func _on_snapshot(snap: Dictionary) -> void:
|
||||
_merit_label.text = NumberFormat.short(float(snap.get("merit", 0.0)))
|
||||
_rate_label.text = "%s/s" % NumberFormat.short(float(snap.get("output_per_sec", 0.0)))
|
||||
_click_btn.text = "点击 +%s" % NumberFormat.short(float(snap.get("click_value", 0.0)))
|
||||
_wrath_label.text = "%s(%d 层)" % [NumberFormat.short(float(snap.get("wrath", 0.0))),
|
||||
int(snap.get("wrath_layers", 0))]
|
||||
_suihui_label.text = str(int(snap.get("suihui", 0)))
|
||||
_rebirth_label.text = str(int(snap.get("rebirths", 0)))
|
||||
_souls_label.text = NumberFormat.short_int(float(snap.get("souls_saved", 0)))
|
||||
var nh: Dictionary = snap.get("next_hall", {})
|
||||
if nh.is_empty():
|
||||
_progress_label_text("十殿已穿——轮回转世,或受敕自立。")
|
||||
else:
|
||||
var eta := float(nh.get("eta_sec", 0.0))
|
||||
_progress_label_text("下一目标:H%d %s 门 %s(持有 %s,约 %s)" % [
|
||||
int(nh.get("no", 0)), str(nh.get("king_cn", "")),
|
||||
NumberFormat.short_int(float(nh.get("gate_merit", 0.0))),
|
||||
NumberFormat.short_int(float(nh.get("have", 0.0))),
|
||||
NumberFormat.duration(eta) if eta > 0.0 else "—"])
|
||||
_army.refresh(snap)
|
||||
_hud.refresh(snap)
|
||||
_refresh_hall_bands(snap)
|
||||
|
||||
|
||||
func _progress_label_text(t: String) -> void:
|
||||
if _progress_shown != t:
|
||||
_progress_shown = t
|
||||
_progress_label.text = t
|
||||
|
||||
|
||||
func _refresh_hall_bands(snap: Dictionary) -> void:
|
||||
var passed := int(snap.get("halls_passed", 0))
|
||||
var nh: Dictionary = snap.get("next_hall", {})
|
||||
for i in _hall_bands.size():
|
||||
var is_passed := i < passed
|
||||
var is_current := i == passed
|
||||
var have := float(nh.get("have", 0.0)) if is_current else 0.0
|
||||
var need := DataRegistry.data.halls[i].gate_merit if is_passed else \
|
||||
float(nh.get("gate_merit", 0.0))
|
||||
_hall_bands[i].set_state(is_passed, is_current, have, need)
|
||||
|
||||
|
||||
func _on_hall_passed(hall_no: int) -> void:
|
||||
var h := DataRegistry.data.hall_by_no(hall_no)
|
||||
_log_label.text = "殿门开:H%d %s。" % [hall_no, h.king_cn if h else ""]
|
||||
|
||||
|
||||
func _on_prestige_done(gain: int, rebirths: int) -> void:
|
||||
_log_label.text = "轮回转世(第 %d 世):+%d 宿慧。" % [rebirths, gain]
|
||||
|
||||
|
||||
func _on_enthrone_done() -> void:
|
||||
_log_label.text = "阴司敕封:幽冥天子可用了。"
|
||||
@@ -0,0 +1,6 @@
|
||||
[gd_scene load_steps=2 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://src/presentation/stage/stage_root.gd" id="1_stage"]
|
||||
|
||||
[node name="StageRoot" type="Node"]
|
||||
script = ExtResource("1_stage")
|
||||
@@ -0,0 +1,645 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
r"""
|
||||
gen_balance_sheets.py —— Balance Bible v1.0 → CSV 落表 + build JSON 生成
|
||||
============================================================================
|
||||
用途(架构 §3.1 数据管线第一段):
|
||||
design/gdd/2026-10-19-F-balance-and-levels.md(Balance Bible v1.0,下称 [B§x])
|
||||
→ data/sheets/*.csv(策划维护源,git 跟踪)
|
||||
→ data/build/*.json(SheetImporter 产物;本脚本以与 SheetImporter
|
||||
_canonical_json 逐字节一致的序列化器预生成,供 CI 漂移门;
|
||||
用户首次跑 `godot --headless -s res://src/tools/import_main.gd`
|
||||
后 `git diff --exit-code -- data/build/` 必须为空)
|
||||
|
||||
铁律:
|
||||
- 数值列禁千分位/禁科学计数法(SheetImporter._convert 的正则只接受
|
||||
^[+-]?([0-9]+(\.[0-9]+)?|\.[0-9]+)$)
|
||||
- 稳定字符串 ID:永不重编号、永不复用(W1 已分配的 ID 原样保留)
|
||||
- 本脚本只写 data/sheets/ 与 data/build/,不碰 src/
|
||||
|
||||
表清单(8 张):
|
||||
[B§12] 七张内容表:generators / upgrades / halls / hall_palettes /
|
||||
boss_battles / events / meta_upgrades
|
||||
+ balance_tuning(架构 §2.1 已列名;容纳 [B§3.2 冤煞层 / §4 prestige 参数 /
|
||||
§8 离线衰减表 / §1 三乘区软上限] —— 这些全局参数在七张内容表中无归属行,
|
||||
按数据驱动红线"零硬编码"必须入表)
|
||||
|
||||
[B] prose 与表格不一致处的采信规则(详见 docs/engineering/2026-10-19-f-w2-csv-provenance.md):
|
||||
P-1 速率 prose "gen2 起 = 8^(n-1)" 与表值(gen2=1=8^0)差一;采信表值 → rate=8^(n-2)
|
||||
P-2 效率升级成本 prose "第 14/18/…/34 级" 与公式 r^(10+4n) 差一;采信公式
|
||||
(0 起始级号:第 14 级 = C0×r^14,公式自洽)
|
||||
P-3 效率升级成本为实数 → 取整到最近整数(工程取整规则,[B] 未规定)
|
||||
P-4 敕封([B§4.4] "全局恩典+25%")与 m08 宿慧商品(同名 +25%×2)字面重叠;
|
||||
按 [B] 字面两者都实现(auto +25% + m08 每阶 +25%),已在 provenance 标注
|
||||
待模拟器门 #7/#12 与文策渊裁定
|
||||
P-5 系统闸门成本 [B] 未给值 → 工程外推 = 对应殿门阈值 ÷2
|
||||
P-6 入职文书成本 [B] 仅钉 g03=800 → 公式 = C0×2/3(对 g03 精确复现 800)
|
||||
P-7 入职文书/系统闸门的 display_en 为临时译名([B§11 差异 #6]:英文名延后)
|
||||
P-8 离线契约([B§8]"8h≈在线1.5-2h")与衰减表求值矛盾(8h÷1h=6.2)——
|
||||
按表实现,门 #9 如实报 FAIL,待文策渊裁定(改表或改契约区间)
|
||||
P-9 [B§2.3] 8 条系统闸门的身份与 GDD §3.2"机制路引清单"(鬼门关/枉死城/
|
||||
望乡台/孽镜/幽都/轮回隙/幽冥/天子)不一致——[B] 为数值母本,采信 [B];
|
||||
GDD 清单中的统计页/冤案卡册等入口去向待文策渊在 W3 事件范围一并裁定
|
||||
P-10 [B§2.2] 点击 L5-12 = 8×(+1% 产出/s),GDD §3.2 同行为 4× (+5/10/15/10%,
|
||||
满 +40%)——采信 [B] 表值(8×1%=+8%),差异已标注待门 #7 标定后裁定
|
||||
P-11 跳过机制口径:[B§4.2]"每 5 级宿慧 → 前 3 档各 25 级"按
|
||||
floor(宿慧/间隔)×25 级叠加实现(m04 间隔 5→4;工程裁定叠加语义)
|
||||
P-12 up_gate_tianzi(敕封诏书)requires:W2 初版误置 "enthroned"(死锁:
|
||||
未敕封不可买诏书 → 永不受敕)。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-14 tune_souls_div=10000 新增([B§6]"超度 = floor(本轮累计阴德÷10,000)",
|
||||
叙事计数器不设闸门);GDD §4.1 的超度事件计数口路由 W3 事件系统承接
|
||||
P-15 法器附加(点击 L5-12,CSV click_output_pct)乘区口径:[B§1] 总公式与
|
||||
GDD §3.2 均列入 M_global(全局区),[B§2.2] 表 prose 为"点击附加 +1%
|
||||
当前产出/s"(点击区口径)——采信公式层(全局区),EconomyState.recompute
|
||||
求和进 set_global_add;MultiplierSet 点击区输入保持 0(两解随时可换)
|
||||
|
||||
用法:
|
||||
/c/Users/89268/.workbuddy/binaries/python/envs/default/Scripts/python.exe \\
|
||||
src/tools/gen_balance_sheets.py
|
||||
无 stdout 输出;自检失败会以非零退出码中止(不写半成品)。
|
||||
"""
|
||||
|
||||
import math
|
||||
import os
|
||||
import sys
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 路径(相对本脚本位置解析,与 cwd 无关)
|
||||
# ---------------------------------------------------------------------------
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
PROJECT_ROOT = os.path.abspath(os.path.join(HERE, "..", ".."))
|
||||
SHEETS_DIR = os.path.join(PROJECT_ROOT, "data", "sheets")
|
||||
BUILD_DIR = os.path.join(PROJECT_ROOT, "data", "build")
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# [B§1] 班底总表(12 档)。id/display 沿用 W1 已分配稳定值;C0/r 为 [B§1] 表值。
|
||||
# rate:gen1=0.1;gen_n (n>=2) = 8^(n-2)([B§1] 表值 = 8 的纯幂阶梯,见 P-1)
|
||||
# ---------------------------------------------------------------------------
|
||||
GENERATORS = [
|
||||
# (id, display_cn, display_en[临时], C0, r, unlock_hall, unlock_gate)
|
||||
("gen_01_youhun", "游魂", "Wandering Souls", 15, 1.13, "none", "none"),
|
||||
("gen_02_yinbing", "阴兵", "Ghost Soldiers", 120, 1.14, "none", "none"),
|
||||
("gen_03_guizu", "鬼卒班", "Ghost Constables", 1200, 1.15, "hall_01_qinguang", "up_gate_guizu"),
|
||||
("gen_04_biliao", "勾魂笔僚", "Death-Scribe Clerks", 12000, 1.15, "hall_02_chujiang", "up_gate_biliao"),
|
||||
("gen_05_tangpu", "孟婆汤铺", "Meng Po's Soup Stall", 120000, 1.16, "hall_03_songdi", "up_gate_tangpu"),
|
||||
("gen_06_nanmin", "枉死城难民团", "Refugee Camp of the Wrongfully Dead", 1200000, 1.16, "hall_04_wuguan", "up_gate_nanmin"),
|
||||
("gen_07_shuban", "阴律司书办", "Clerks of the Nether Court", 12000000, 1.17, "hall_05_yama", "up_gate_shuban"),
|
||||
("gen_08_bingfang", "幽都兵坊", "Armory of the Dark Capital", 120000000, 1.17, "hall_06_biancheng","up_gate_bingfang"),
|
||||
("gen_09_wodi", "十殿卧底", "Sleeper Agents in the Ten Courts",1200000000,1.18, "hall_07_taishan", "up_gate_wodi"),
|
||||
("gen_10_xingzhe", "轮回裂隙行者", "Rift Walkers of the Wheel", 12000000000, 1.18, "hall_08_dushi", "up_gate_xingzhe"),
|
||||
("gen_11_yinhun", "六道引魂人", "Soul Guides of the Six Paths", 120000000000, 1.19, "hall_09_pingdeng","up_gate_yinhun"),
|
||||
("gen_12_tianzi", "幽冥天子", "Emperor of the Netherworld", 1200000000000, 1.20, "none", "up_gate_tianzi"),
|
||||
]
|
||||
|
||||
def gen_rate(n: int) -> float:
|
||||
"""[B§1] 表值:gen1=0.1;gen_n (n>=2)=8^(n-2)。"""
|
||||
if n == 1:
|
||||
return 0.1
|
||||
return float(8 ** (n - 2))
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# [B§6] 十殿总表。gate_merit = 殿门条件(本轮累计阴德,单计数器 [B§0.1])
|
||||
# ---------------------------------------------------------------------------
|
||||
HALLS = [
|
||||
# (id, no, king_cn, king_en[临时], motif_cn, motif_en[临时], target_min, gate_merit, unlock_gen, unlock_gate, palette_id, is_prestige_gate)
|
||||
("hall_01_qinguang", 1, "秦广王", "King Qinguang", "孽镜台(照心)", "Mirror of Karma", 25, 2000, "gen_03_guizu", "up_gate_guizu", "pal_hall_01", "false"),
|
||||
("hall_02_chujiang", 2, "楚江王", "King Chujiang", "寒冰狱", "Ice Hell", 42, 30000, "gen_04_biliao", "up_gate_biliao", "pal_hall_02", "false"),
|
||||
("hall_03_songdi", 3, "宋帝王", "King Songdi", "剥皮亭", "Flaying Pavilion", 62, 300000, "gen_05_tangpu", "up_gate_tangpu", "pal_hall_03", "false"),
|
||||
("hall_04_wuguan", 4, "五官王", "King Wuguan", "沸汤/毒蛇", "Scalding Cauldron", 84, 2000000, "gen_06_nanmin", "up_gate_nanmin", "pal_hall_04", "false"),
|
||||
("hall_05_yama", 5, "阎罗王", "King Yama", "叫唤地狱/铁床", "Wailing Hell", 105, 10000000, "gen_07_shuban", "up_gate_shuban", "pal_hall_05", "false"),
|
||||
("hall_06_biancheng",6, "卞城王", "King Biancheng","石碾", "Stone Roller", 125, 40000000, "gen_08_bingfang","up_gate_bingfang","pal_hall_06", "false"),
|
||||
("hall_07_taishan", 7, "泰山王", "King Taishan", "热油火坑", "Boiling Oil Pit", 143, 120000000, "gen_09_wodi", "up_gate_wodi", "pal_hall_07", "false"),
|
||||
("hall_08_dushi", 8, "都市王", "King Dushi", "泥犁/大石", "Avici Stone", 160, 350000000, "gen_10_xingzhe", "up_gate_xingzhe", "pal_hall_08", "false"),
|
||||
("hall_09_pingdeng", 9, "平等王", "King Pingdeng", "拔舌/铁树", "Tongue-Ripping Iron Tree",175, 800000000, "gen_11_yinhun", "up_gate_yinhun", "pal_hall_09", "false"),
|
||||
("hall_10_zhuanlun",10, "转轮王", "King of the Wheel", "轮回道(六道环)","Wheel of Rebirth", 180, 1500000000, "none", "none", "pal_hall_10", "true"),
|
||||
]
|
||||
|
||||
# [B§5] 殿系数 H1-H10
|
||||
BOSS_FACTORS = [1.0, 1.1, 1.2, 1.3, 1.4, 1.6, 1.8, 2.0, 2.1, 2.2]
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# [B§2.1] 效率升级 72 条(12 班底 × 6 级)
|
||||
# 效果:该班底产出 +100%(加法,进班底乘区;6 级满 = ×7 < ×8 软上限)
|
||||
# 成本:cost_n(g) = C0_g × r_g^(10+4n),n=1..6,取整(P-2/P-3)
|
||||
# 命名:up_eff_genNN_n(架构 §3.2 ID 示例口径);显示名 = 班底 · 历练 I-VI
|
||||
# ---------------------------------------------------------------------------
|
||||
ROMAN = ["I", "II", "III", "IV", "V", "VI"]
|
||||
|
||||
def eff_upgrade_cost(c0: float, r: float, n: int) -> int:
|
||||
return int(round(c0 * (r ** (10 + 4 * n))))
|
||||
|
||||
# [B§2.2] 点击升级 12 条
|
||||
CLICK_MULT_COSTS = [50, 500, 5000, 50000] # L1-4:点击价值 ×2
|
||||
CLICK_ADD_COSTS = [100000, 1000000, 10000000, 100000000,
|
||||
1000000000, 10000000000, 100000000000, 1000000000000] # L5-12:+1% 产出/s
|
||||
CLICK_MULT_NAMES = ["指诀 · 倍力 I", "指诀 · 倍力 II", "指诀 · 倍力 III", "指诀 · 倍力 IV"]
|
||||
CLICK_ADD_NAMES = ["指诀 · 引阴 I", "指诀 · 引阴 II", "指诀 · 引阴 III", "指诀 · 引阴 IV",
|
||||
"指诀 · 引阴 V", "指诀 · 引阴 VI", "指诀 · 引阴 VII", "指诀 · 引阴 VIII"]
|
||||
|
||||
# [B§2.3] 系统闸门 8 条(成本外推 = 对应殿门阈值 ÷2,P-5)
|
||||
SYSTEM_GATES = [
|
||||
# (id, name_cn, name_en[临时], unlock_hall, cost)
|
||||
("up_gate_wrath", "开通 · 冤气系统", "Unlock: Wrath System", "hall_01_qinguang", 1000),
|
||||
("up_gate_events", "开通 · 事件系统", "Unlock: Event System", "hall_02_chujiang", 15000),
|
||||
("up_gate_bulk10", "开通 · 批量购置(×10)", "Unlock: Bulk Buy ×10", "hall_03_songdi", 150000),
|
||||
("up_gate_offline", "开通 · 离线收益", "Unlock: Offline Income", "hall_04_wuguan", 1000000),
|
||||
("up_gate_prestige", "开通 · 轮回预告", "Unlock: Rebirth Preview", "hall_06_biancheng",20000000),
|
||||
("up_gate_boss_retry", "开通 · 殿王再战", "Unlock: Boss Rematch", "hall_07_taishan", 60000000),
|
||||
("up_gate_bulkmax", "开通 · 批量购置(×尽)", "Unlock: Bulk Buy Max", "hall_08_dushi", 175000000),
|
||||
("up_gate_enthrone", "开通 · 敕封预告", "Unlock: Enthronement Preview", "hall_09_pingdeng", 400000000),
|
||||
]
|
||||
|
||||
# [B§2.4] 离线升级 6 条(×1.15/级)
|
||||
OFFLINE_COSTS = [2000, 20000, 200000, 2000000, 20000000, 200000000]
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# [B§2.5] Prestige 升级 12 条(宿慧商店,跨轮回永久)
|
||||
# effect_type / target / param 口径见 economy 代码与 schema description
|
||||
# ---------------------------------------------------------------------------
|
||||
META_UPGRADES = [
|
||||
# (id, name_cn, name_en[临时], effect_type, target_id, param, cost, max_rank, condition, desc_cn)
|
||||
("meta_01", "枕旧梦", "Pillow of Old Dreams", "start_levels", "gen_01_youhun", 10, 3, 1, "none", "新轮回开局自带游魂 ×10 级"),
|
||||
("meta_02", "携阴兵", "Bring Ghost Soldiers", "start_levels", "gen_02_yinbing",10, 5, 1, "none", "开局自带阴兵 ×10 级"),
|
||||
("meta_03", "宿慧通透", "Perspicacious Wisdom","suihui_gain_pct", "", 0.15, 8, 3, "none", "宿慧获取 +15%(每阶)"),
|
||||
("meta_04", "天生熟练", "Born Skilled", "skip_interval", "", 4, 12, 1, "none", "跳过机制从每 5 级加快为每 4 级"),
|
||||
("meta_05", "阴司人脉", "Netherworld Connections","offline_pct", "", 0.25, 6, 1, "none", "离线效率 +25%"),
|
||||
("meta_06", "斩鬼刃", "Ghost-Slaying Blade", "boss_click_pct", "", 0.30, 10, 1, "none", "殿王战点击伤害 +30%"),
|
||||
("meta_07", "心安理得", "Easy Conscience", "wrath_penalty_set", "", 0.15, 15, 1, "none", "冤气对宿慧的削减 30%→15%"),
|
||||
("meta_08", "敕封恩典", "Grace of Enthronement","global_pct", "", 0.25, 20, 2, "none", "全局 +25%(每阶)"),
|
||||
("meta_09", "阴阳眼", "Yin-Yang Eye", "event_freq_pct", "", 0.20, 10, 1, "none", "事件触发频率 +20%"),
|
||||
("meta_10", "超度加速", "Salvation Acceleration","souls_mult", "", 2, 8, 1, "none", "超度计数获取 ×2"),
|
||||
("meta_11", "老马识途", "Old Horse Knows the Way","bulk_unlock", "", 0, 5, 1, "none", "开局即可用批量 ×10"),
|
||||
("meta_12", "幽冥权柄", "Authority of the Netherworld","conditional_global_pct","gen_12_tianzi", 0.10, 25, 1, "gen12_unlocked", "愿景档(gen12)解锁后全局再 +10%"),
|
||||
]
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# [B§3.2] 冤煞层 22 层参数 / [B§8] 离线衰减表 / [B§4] prestige 参数 / [B§1] 软上限
|
||||
# → balance_tuning.csv(key-value 行;架构 §2.1)
|
||||
# ---------------------------------------------------------------------------
|
||||
TUNING = [
|
||||
# (id, kind, num, text, desc_cn)
|
||||
("tune_click_base", "number", 1, "", "[B§2.2] 点击基础值 = 1(与 gen1 成本 15 配对 → 15 次点击首购)"),
|
||||
("tune_click_zone_cap", "number", 16, "", "[B§1/§2.2] 点击区软上限 ×16(4 级 ×2 满)"),
|
||||
("tune_gen_zone_cap", "number", 8, "", "[B§1] 班底乘区软上限 ×8(6 级效率满 = ×7,留 1 级余量)"),
|
||||
("tune_global_zone_cap", "number", 64, "", "[B§1] 全局乘区软上限 ×64"),
|
||||
("tune_offline_band1_h", "number", 2, "", "[B§8] 离线 0-2h 速率 100%"),
|
||||
("tune_offline_band1_rate","number", 1.0, "", "[B§8] 离线 0-2h 速率"),
|
||||
("tune_offline_band2_h", "number", 8, "", "[B§8] 离线 2-8h 速率 70%"),
|
||||
("tune_offline_band2_rate","number", 0.7, "", "[B§8] 离线 2-8h 速率"),
|
||||
("tune_offline_band3_h", "number", 24, "", "[B§8] 离线 8-24h 速率 40%"),
|
||||
("tune_offline_band3_rate","number", 0.4, "", "[B§8] 离线 8-24h 速率"),
|
||||
("tune_offline_band4_rate","number", 0.1, "", "[B§8] 离线 >24h 速率 10%"),
|
||||
("tune_offline_cap_h", "number", 72, "", "[B§8] 离线硬上限 72h 封顶"),
|
||||
("tune_wrath_layers", "number", 22, "", "[B§3.2] 冤煞层总数 22"),
|
||||
("tune_wrath_cost_base", "number", 8, "", "[B§3.2] 第 n 层成本 = round(8 × n^1.8)"),
|
||||
("tune_wrath_cost_exp", "number", 1.8, "", "[B§3.2] 冤煞层成本指数"),
|
||||
("tune_wrath_bonus", "number", 0.25, "", "[B§3.2] 每层全局 +25%(加法,进全局乘区)"),
|
||||
("tune_wrath_penalty", "number", 0.03, "", "[B§3.2] 每层宿慧获取削减 +3%"),
|
||||
("tune_wrath_penalty_cap", "number", 0.3, "", "[B§3.2] 宿慧削减封顶 30%"),
|
||||
("tune_suihui_div", "number", 1000000, "", "[B§4.1] 宿慧 = floor(√(本轮累计阴德/1e6) × 5)"),
|
||||
("tune_suihui_mult", "number", 5, "", "[B§4.1] 宿慧公式乘数"),
|
||||
("tune_suihui_global_pct", "number", 0.03, "", "[B§1/§4.2] 每级宿慧全局产出 +3%(加法,进全局乘区)"),
|
||||
("tune_enthrone_bonus", "number", 0.25, "", "[B§4.4] 阴司敕封自动全局恩典 +25%(与 m08 商品字面重叠,见 provenance P-4)"),
|
||||
("tune_skip_interval", "number", 5, "", "[B§4.2] 每 5 级宿慧 → 前 3 档班底各 +25 级(m04 可加快为 4)"),
|
||||
("tune_skip_gens", "number", 3, "", "[B§4.2] 跳过机制覆盖前 3 档班底"),
|
||||
("tune_skip_levels", "number", 25, "", "[B§4.2] 跳过机制每档 +25 级"),
|
||||
("tune_enthrone_rebirths", "number", 2, "", "[B§4.4] 阴司敕封触发:转世 ≥2(约第 2-3 轮,累计 8-12h 为其 pacing 后果)"),
|
||||
("tune_restart_curve", "text", 0, "1:0.6:0.4:0.3", "[B§4.3] 重开曲线目标(第 5 轮触底 ≥0.25×);模拟器门 #13 断言"),
|
||||
("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 数值)"),
|
||||
]
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# [B§7] 事件 4 条(倍率走独立瞬时区,不进全局乘区)
|
||||
# 参数列语义(schema description 同步声明):
|
||||
# p_mult 倍率参数:output_mult=全产出倍率;click_output_mult=产出倍率部分
|
||||
# p_clicks 次数参数:click_challenge=需点击次数;click_output_mult=点击倍率;
|
||||
# choice=冤气增益下限(随殿序 lerp 上限见 p_window_sec)
|
||||
# p_window_sec 窗口参数:click_challenge=窗口秒;choice=冤气增益上限
|
||||
# p_penalty/p_penalty_dur_sec click_challenge 失败惩罚(倍率/持续秒)
|
||||
# opt_a_*/opt_b_* choice 事件二选一(type + param)
|
||||
# ---------------------------------------------------------------------------
|
||||
EVENTS = [
|
||||
# (id, name_cn, name_en[临时], trig_min, trig_max, dur, type,
|
||||
# p_mult, p_clicks, p_window, p_pen, p_pen_dur,
|
||||
# opt_a_type, opt_a_param, opt_b_type, opt_b_param, flavor)
|
||||
("ev_01_baigui", "百鬼夜行", "Night of the Hundred Ghosts", 8, 15, 60, "output_mult",
|
||||
5, 0, 0, 0, 0, "none", 0, "none", 0,
|
||||
"百鬼倾巢而出,阴阳秩序暂时失序"),
|
||||
("ev_02_naoxiang", "阴兵闹饷", "Ghost Mutiny", 10, 18, 15, "click_challenge",
|
||||
0, 10, 15, 0.5, 60, "none", 0, "none", 0,
|
||||
"鬼卒也要三牲祭,不然哗变"),
|
||||
("ev_03_shensu", "冤魂申诉", "Grievance of the Wronged", 6, 12, 45, "choice",
|
||||
0, 15, 40, 0, 0, "merit_burst_sec", 30, "wrath_gain", 0,
|
||||
"他跪在地上喊冤枉。你选择……"),
|
||||
("ev_04_tiangou", "天狗食月", "Heavenly Dog Eats the Moon", 20, 35, 30, "click_output_mult",
|
||||
2, 10, 0, 0, 0, "none", 0, "none", 0,
|
||||
"天狗噬月,阴气最盛——动手"),
|
||||
]
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# [B§6/§5.3] per-hall 色板行(hex 为工程起始值,W5 look-dev 冻结评审校准;
|
||||
# 四色面积纪律 [B] 墨65/骨白25/朱砂≤8/靛≤7)
|
||||
# ---------------------------------------------------------------------------
|
||||
INK_HEX, BONE_HEX, CINNABAR_HEX, INDIGO_HEX = "#1A1A1A", "#F2EDE4", "#B4402C", "#2C4A6E"
|
||||
PALETTES = [
|
||||
# (id, hall_id, name_cn, accent_hex, accent_name_cn, flow)
|
||||
("pal_hall_01", "hall_01_qinguang", "冷靛", "#3D5A80", "冷靛", "bleed_left"),
|
||||
("pal_hall_02", "hall_02_chujiang", "冰青", "#8FC1C9", "冰青", "seep_down"),
|
||||
("pal_hall_03", "hall_03_songdi", "暗红", "#7E2E2E", "暗红", "bleed_left"),
|
||||
("pal_hall_04", "hall_04_wuguan", "锈橙", "#A85C32", "锈橙", "seep_down"),
|
||||
("pal_hall_05", "hall_05_yama", "玄黑", "#141414", "玄黑", "bleed_left"),
|
||||
("pal_hall_06", "hall_06_biancheng", "铅灰", "#6E7276", "铅灰", "seep_down"),
|
||||
("pal_hall_07", "hall_07_taishan", "烬红", "#8C3A2B", "烬红", "bleed_left"),
|
||||
("pal_hall_08", "hall_08_dushi", "墨绿", "#2F4A3E", "墨绿", "seep_down"),
|
||||
("pal_hall_09", "hall_09_pingdeng", "黛紫", "#4A3A5C", "黛紫", "bleed_left"),
|
||||
("pal_hall_10", "hall_10_zhuanlun", "骨白+金", "#C9A227", "暗金", "seep_down"),
|
||||
]
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# CSV 列定义(顺序 = schema required 顺序 = build JSON 键序)
|
||||
# ---------------------------------------------------------------------------
|
||||
GEN_COLS = ["id", "display_cn", "display_en", "base_cost", "cost_ratio", "base_rate",
|
||||
"unlock_hall", "unlock_gate", "sprite_row"]
|
||||
HALL_COLS = ["id", "no", "king_cn", "king_en", "motif_cn", "motif_en", "target_min",
|
||||
"gate_merit", "unlock_gen", "unlock_gate", "palette_id", "is_prestige_gate"]
|
||||
UPGRADE_COLS = ["id", "name_cn", "name_en", "type", "target_gen", "unlock_hall",
|
||||
"effect_type", "effect_param", "requires", "cost", "desc_cn"]
|
||||
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",
|
||||
"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",
|
||||
"duration_sec", "type", "p_mult", "p_clicks", "p_window_sec",
|
||||
"p_penalty", "p_penalty_dur_sec", "opt_a_type", "opt_a_param",
|
||||
"opt_b_type", "opt_b_param", "flavor_cn"]
|
||||
META_COLS = ["id", "name_cn", "name_en", "effect_type", "target_id", "effect_param",
|
||||
"cost_suihui", "max_rank", "condition", "desc_cn"]
|
||||
TUNING_COLS = ["id", "kind", "num", "text", "desc_cn"]
|
||||
|
||||
|
||||
def fnum(v) -> str:
|
||||
"""数值列序列化:整数不带小数点;小数保留(禁科学计数法)。"""
|
||||
if isinstance(v, bool):
|
||||
return "true" if v else "false"
|
||||
if isinstance(v, int):
|
||||
return str(v)
|
||||
f = float(v)
|
||||
if f == int(f) and abs(f) < 1e15:
|
||||
return str(int(f))
|
||||
return repr(f)
|
||||
|
||||
|
||||
def build_rows():
|
||||
"""返回 {sheet: (columns, rows)};rows 为 dict 列表(键序 = 列序)。"""
|
||||
sheets = {}
|
||||
|
||||
# --- generators([B§1]) -------------------------------------------------
|
||||
rows = []
|
||||
for i, (gid, cn, en, c0, r, hall, gate) in enumerate(GENERATORS, start=1):
|
||||
rows.append({
|
||||
"id": gid, "display_cn": cn, "display_en": en,
|
||||
"base_cost": c0, "cost_ratio": r, "base_rate": gen_rate(i),
|
||||
"unlock_hall": hall, "unlock_gate": gate,
|
||||
"sprite_row": "res://assets/sprites/units/%s" % gid,
|
||||
})
|
||||
sheets["generators"] = (GEN_COLS, rows)
|
||||
|
||||
# --- upgrades([B§2],108 条 = 效率72 + 点击12 + 路引18 + 离线6) ----------
|
||||
rows = []
|
||||
for i, (gid, cn, en, c0, r, hall, gate) in enumerate(GENERATORS, start=1):
|
||||
for n in range(1, 7):
|
||||
rows.append({
|
||||
"id": "up_eff_%s_%d" % (gid, n),
|
||||
"name_cn": "%s · 历练 %s" % (cn, ROMAN[n - 1]),
|
||||
"name_en": "%s · Training %s" % (en, ROMAN[n - 1]),
|
||||
"type": "efficiency", "target_gen": gid, "unlock_hall": hall,
|
||||
"effect_type": "gen_output_add", "effect_param": 1.0, "requires": "none",
|
||||
"cost": eff_upgrade_cost(c0, r, n),
|
||||
"desc_cn": "该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)",
|
||||
})
|
||||
for i in range(4):
|
||||
rows.append({
|
||||
"id": "up_click_%d" % (i + 1), "name_cn": CLICK_MULT_NAMES[i],
|
||||
"name_en": "Gesture · Force %s" % ROMAN[i],
|
||||
"type": "click", "target_gen": "none", "unlock_hall": "none",
|
||||
"effect_type": "click_mult", "effect_param": 2, "requires": "none",
|
||||
"cost": CLICK_MULT_COSTS[i],
|
||||
"desc_cn": "点击价值 ×2(点击区,软上限 ×16)",
|
||||
})
|
||||
for i in range(8):
|
||||
rows.append({
|
||||
"id": "up_click_%d" % (i + 5), "name_cn": CLICK_ADD_NAMES[i],
|
||||
"name_en": "Gesture · Soul-Draw %d" % (i + 1),
|
||||
"type": "click", "target_gen": "none", "unlock_hall": "none",
|
||||
"effect_type": "click_output_pct", "effect_param": 0.01, "requires": "none",
|
||||
"cost": CLICK_ADD_COSTS[i],
|
||||
"desc_cn": "点击附加 +1% 当前产出/s(仪式与殿王战弹药)",
|
||||
})
|
||||
# 路引 · 10 条班底入职文书(ID 与 generators.unlock_gate 引用一致,W1 已分配)
|
||||
for i, (gid, cn, en, c0, r, hall, gate) in enumerate(GENERATORS[2:], start=3):
|
||||
name_cn = "幽冥天子 · 敕封诏书" if gid == "gen_12_tianzi" else "%s · 入职文书" % cn
|
||||
name_en = "Enthronement Edict" if gid == "gen_12_tianzi" else "%s · Hiring Letter" % en
|
||||
req = "enthrone_ready" if gid == "gen_12_tianzi" else "none" # P-12
|
||||
rows.append({
|
||||
"id": gate, "name_cn": name_cn, "name_en": name_en,
|
||||
"type": "gate_hire", "target_gen": gid, "unlock_hall": hall,
|
||||
"effect_type": "unlock", "effect_param": 0, "requires": req,
|
||||
"cost": int(round(c0 * 2.0 / 3.0)), # P-6:对 g03 精确复现 [B§2.3] 的 800
|
||||
"desc_cn": "编制闸门:购入后该班底可购置(需先过对应殿门)",
|
||||
})
|
||||
for sgid, scn, sen, shall, scost in SYSTEM_GATES:
|
||||
rows.append({
|
||||
"id": sgid, "name_cn": scn, "name_en": sen,
|
||||
"type": "gate_system", "target_gen": "none", "unlock_hall": shall,
|
||||
"effect_type": "unlock", "effect_param": 0, "requires": "none", "cost": scost,
|
||||
"desc_cn": "系统闸门:纯解锁零效果([B§2.3];成本外推 = 殿门阈值÷2,P-5)",
|
||||
})
|
||||
for i in range(6):
|
||||
rows.append({
|
||||
"id": "up_offline_%d" % (i + 1),
|
||||
"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",
|
||||
"cost": OFFLINE_COSTS[i],
|
||||
"desc_cn": "离线效率 ×1.15(作用于衰减表全部时段;6 级满 ×2.31)",
|
||||
})
|
||||
sheets["upgrades"] = (UPGRADE_COLS, rows)
|
||||
|
||||
# --- halls([B§6]) -------------------------------------------------------
|
||||
rows = []
|
||||
for (hid, no, kcn, ken, mcn, men, tmin, gate_merit, ugen, ugate, pal, pres) in HALLS:
|
||||
rows.append({
|
||||
"id": hid, "no": no, "king_cn": kcn, "king_en": ken,
|
||||
"motif_cn": mcn, "motif_en": men, "target_min": tmin,
|
||||
"gate_merit": gate_merit, "unlock_gen": ugen, "unlock_gate": ugate,
|
||||
"palette_id": pal, "is_prestige_gate": pres,
|
||||
})
|
||||
sheets["halls"] = (HALL_COLS, rows)
|
||||
|
||||
# --- hall_palettes([B§6 色板列] + 架构 §5.3;hex 工程起始值) -------------
|
||||
rows = []
|
||||
for i, (pid, hid, name, accent, accent_cn, flow) in enumerate(PALETTES, start=1):
|
||||
rows.append({
|
||||
"id": pid, "hall_id": hid, "name_cn": name,
|
||||
"ink_hex": INK_HEX, "bone_hex": BONE_HEX, "cinnabar_hex": CINNABAR_HEX,
|
||||
"indigo_hex": INDIGO_HEX, "accent_hex": accent, "accent_name_cn": accent_cn,
|
||||
"flow": flow, "lut_index": i,
|
||||
"ink_pct": 65, "bone_pct": 25, "cinnabar_max_pct": 8, "indigo_max_pct": 7,
|
||||
})
|
||||
sheets["hall_palettes"] = (PALETTE_COLS, rows)
|
||||
|
||||
# --- boss_battles([B§5]) -------------------------------------------------
|
||||
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,
|
||||
"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,
|
||||
})
|
||||
sheets["boss_battles"] = (BOSS_COLS, rows)
|
||||
|
||||
# --- events([B§7]) -------------------------------------------------------
|
||||
rows = []
|
||||
for (eid, cn, en, tmin, tmax, dur, etype, p_mult, p_clicks, p_window,
|
||||
p_pen, p_pen_dur, oa_type, oa_param, ob_type, ob_param, flavor) in EVENTS:
|
||||
rows.append({
|
||||
"id": eid, "name_cn": cn, "name_en": en,
|
||||
"trigger_min_min": tmin, "trigger_max_min": tmax, "duration_sec": dur,
|
||||
"type": etype, "p_mult": p_mult, "p_clicks": p_clicks,
|
||||
"p_window_sec": p_window, "p_penalty": p_pen, "p_penalty_dur_sec": p_pen_dur,
|
||||
"opt_a_type": oa_type, "opt_a_param": oa_param,
|
||||
"opt_b_type": ob_type, "opt_b_param": ob_param, "flavor_cn": flavor,
|
||||
})
|
||||
sheets["events"] = (EVENT_COLS, rows)
|
||||
|
||||
# --- meta_upgrades([B§2.5]) ---------------------------------------------
|
||||
rows = []
|
||||
for (mid, cn, en, etype, target, param, cost, max_rank, cond, desc) in META_UPGRADES:
|
||||
rows.append({
|
||||
"id": mid, "name_cn": cn, "name_en": en, "effect_type": etype,
|
||||
"target_id": target, "effect_param": param, "cost_suihui": cost,
|
||||
"max_rank": max_rank, "condition": cond, "desc_cn": desc,
|
||||
})
|
||||
sheets["meta_upgrades"] = (META_COLS, rows)
|
||||
|
||||
# --- balance_tuning -------------------------------------------------------
|
||||
rows = []
|
||||
for (tid, kind, num, text, desc) in TUNING:
|
||||
rows.append({
|
||||
"id": tid, "kind": kind, "num": num, "text": text, "desc_cn": desc,
|
||||
})
|
||||
sheets["balance_tuning"] = (TUNING_COLS, rows)
|
||||
|
||||
return sheets
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 自检:Bible 表值断言(任何一条失败 → 中止,不写盘)
|
||||
# ---------------------------------------------------------------------------
|
||||
def self_check(sheets) -> None:
|
||||
gens = sheets["generators"][1]
|
||||
# [B§1] 速率阶梯:gen2 起 8 的纯幂
|
||||
for i, g in enumerate(gens):
|
||||
n = i + 1
|
||||
expect = 0.1 if n == 1 else float(8 ** (n - 2))
|
||||
assert float(g["base_rate"]) == expect, "rate %s" % g["id"]
|
||||
assert float(gens[11]["base_rate"]) == 1073741824.0, "gen12 rate = 8^10"
|
||||
# [B§1] 首级回本 = C0/rate:tier2-6 ∈ [120,293]
|
||||
paybacks = [g["base_cost"] / float(g["base_rate"]) for g in gens]
|
||||
for i in range(1, 6): # tier2..tier6(0-indexed 1..5)
|
||||
assert 120.0 <= paybacks[i] <= 293.0, "payback tier%d = %.1f" % (i + 1, paybacks[i])
|
||||
# [B§1] 全表单调递增(tier7+ 递增是设计意图)
|
||||
for i in range(1, len(paybacks) - 1):
|
||||
assert paybacks[i] < paybacks[i + 1], "payback 单调性 @tier%d" % (i + 1)
|
||||
assert abs(paybacks[1] - 120.0) < 0.5, "gen2 回本 120s"
|
||||
assert abs(paybacks[5] - 293.0) < 0.5, "gen6 回本 293s"
|
||||
assert abs(paybacks[11] - 1118.0) < 0.5, "gen12 回本 1118s"
|
||||
# [B§6] 殿门阈值
|
||||
halls = sheets["halls"][1]
|
||||
expect_gates = [2000, 30000, 300000, 2000000, 10000000, 40000000,
|
||||
120000000, 350000000, 800000000, 1500000000]
|
||||
for h, eg in zip(halls, expect_gates):
|
||||
assert h["gate_merit"] == eg, "gate %s" % h["id"]
|
||||
assert sum(1 for h in halls if h["is_prestige_gate"] == "true") == 1
|
||||
assert halls[-1]["is_prestige_gate"] == "true"
|
||||
# 升级 108 条 + meta 12 条([B§2] 配比)
|
||||
ups = sheets["upgrades"][1]
|
||||
assert len(ups) == 108, "upgrades 行数 %d" % len(ups)
|
||||
from collections import Counter
|
||||
c = Counter(u["type"] for u in ups)
|
||||
assert c["efficiency"] == 72 and c["click"] == 12 and c["gate_hire"] == 10 \
|
||||
and c["gate_system"] == 8 and c["offline"] == 6, dict(c)
|
||||
# 效率成本抽检:gen1 n=1 = round(15×1.13^14)
|
||||
eff1 = [u for u in ups if u["id"] == "up_eff_gen_01_youhun_1"][0]
|
||||
assert eff1["cost"] == int(round(15 * 1.13 ** 14)), "eff gen1 n1 cost"
|
||||
# 入职文书 g03 = 800([B§2.3] 唯一钉值)
|
||||
g03 = [u for u in ups if u["id"] == "up_gate_guizu"][0]
|
||||
assert g03["cost"] == 800, "g03 gate cost = 800(P-6 复现)"
|
||||
# 跨表引用完整性:generators.unlock_gate ⊆ upgrades.id
|
||||
up_ids = {u["id"] for u in ups}
|
||||
for g in gens:
|
||||
if g["unlock_gate"] != "none":
|
||||
assert g["unlock_gate"] in up_ids, "dangling %s" % g["unlock_gate"]
|
||||
hall_ids = {h["id"] for h in halls}
|
||||
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] 殿系数
|
||||
bosses = sheets["boss_battles"][1]
|
||||
assert len(bosses) == 10
|
||||
for b, h in zip(bosses, halls):
|
||||
assert b["hall_id"] == h["id"]
|
||||
assert b["hp_factor"] == 15 # [B§11 差异 #4]:50→15
|
||||
# 冤煞成本公式 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
|
||||
assert tune["tune_wrath_cost_base"]["num"] == 8
|
||||
assert tune["tune_offline_cap_h"]["num"] == 72
|
||||
# P-14:超度除数 + 敕封宿慧门槛(GDD §3.4)
|
||||
assert tune["tune_souls_div"]["num"] == 10000
|
||||
assert tune["tune_enthrone_suihui_req"]["num"] == 60
|
||||
assert tune["tune_enthrone_rebirths"]["num"] == 2
|
||||
assert tune["tune_suihui_div"]["num"] == 1000000
|
||||
assert tune["tune_suihui_mult"]["num"] == 5
|
||||
assert tune["tune_suihui_global_pct"]["num"] == 0.03
|
||||
assert tune["tune_enthrone_bonus"]["num"] == 0.25
|
||||
assert tune["tune_skip_interval"]["num"] == 5
|
||||
assert tune["tune_skip_gens"]["num"] == 3
|
||||
assert tune["tune_skip_levels"]["num"] == 25
|
||||
assert tune["tune_click_base"]["num"] == 1
|
||||
assert tune["tune_click_zone_cap"]["num"] == 16
|
||||
assert tune["tune_gen_zone_cap"]["num"] == 8
|
||||
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"
|
||||
# meta 12 条
|
||||
metas = sheets["meta_upgrades"][1]
|
||||
assert len(metas) == 12
|
||||
assert [m["cost_suihui"] for m in metas] == [3, 5, 8, 12, 6, 10, 15, 20, 10, 8, 5, 25]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 序列化:CSV(utf-8-sig + LF,与 W1 generators.csv 一致)与 canonical JSON
|
||||
# (逐字节复刻 SheetImporter._canonical_json:tab 缩进 / 键序 = 列序 /
|
||||
# 整数无小数点 / 积分浮点写成整数 / 字符串 JSON.stringify 原样中文)
|
||||
# ---------------------------------------------------------------------------
|
||||
def to_cell(v) -> str:
|
||||
if isinstance(v, bool):
|
||||
return "true" if v else "false"
|
||||
if isinstance(v, str):
|
||||
return v
|
||||
return fnum(v)
|
||||
|
||||
|
||||
def csv_quote(s: str) -> str:
|
||||
if any(ch in s for ch in [",", '"', "\n", "\r"]):
|
||||
return '"' + s.replace('"', '""') + '"'
|
||||
return s
|
||||
|
||||
|
||||
def write_csv(path: str, cols, rows) -> None:
|
||||
lines = [",".join(csv_quote(c) for c in cols)]
|
||||
for r in rows:
|
||||
lines.append(",".join(csv_quote(to_cell(r[c])) for c in cols))
|
||||
with open(path, "w", encoding="utf-8-sig", newline="\n") as f:
|
||||
f.write("\n".join(lines) + "\n")
|
||||
|
||||
|
||||
def canon_json(value, indent: str = "") -> str:
|
||||
if isinstance(value, dict):
|
||||
if not value:
|
||||
return "{}"
|
||||
lines = ["%s\t%s: %s" % (indent, json_str(k), canon_json(v, indent + "\t"))
|
||||
for k, v in value.items()]
|
||||
return "{\n" + ",\n".join(lines) + "\n" + indent + "}"
|
||||
if isinstance(value, (list, tuple)):
|
||||
if not value:
|
||||
return "[]"
|
||||
lines = ["%s\t%s" % (indent, canon_json(v, indent + "\t")) for v in value]
|
||||
return "[\n" + ",\n".join(lines) + "\n" + indent + "]"
|
||||
if isinstance(value, bool):
|
||||
return "true" if value else "false"
|
||||
if isinstance(value, int):
|
||||
return str(value)
|
||||
if isinstance(value, float):
|
||||
if value == int(value) and abs(value) < 1e15:
|
||||
return str(int(value))
|
||||
return repr(value)
|
||||
return json_str(str(value))
|
||||
|
||||
|
||||
def json_str(s: str) -> str:
|
||||
"""复刻 Godot JSON.stringify:转义 \" 与 \\ 与控制字符;中文原样。"""
|
||||
out = ['"']
|
||||
for ch in s:
|
||||
if ch == '"':
|
||||
out.append('\\"')
|
||||
elif ch == "\\":
|
||||
out.append("\\\\")
|
||||
elif ch == "\n":
|
||||
out.append("\\n")
|
||||
elif ch == "\r":
|
||||
out.append("\\r")
|
||||
elif ch == "\t":
|
||||
out.append("\\t")
|
||||
elif ord(ch) < 0x20:
|
||||
out.append("\\u%04x" % ord(ch))
|
||||
else:
|
||||
out.append(ch)
|
||||
out.append('"')
|
||||
return "".join(out)
|
||||
|
||||
|
||||
def typed_row(row: dict) -> dict:
|
||||
"""模拟 SheetImporter._convert:数字列按 CSV 字符串 round-trip
|
||||
(含 '.' → float,否则 int),保证 canonical JSON 字节一致。"""
|
||||
out = {}
|
||||
for k, v in row.items():
|
||||
if isinstance(v, bool):
|
||||
out[k] = v
|
||||
elif isinstance(v, (int, float)):
|
||||
s = fnum(v)
|
||||
out[k] = float(s) if "." in s else int(s)
|
||||
else:
|
||||
out[k] = v
|
||||
return out
|
||||
|
||||
|
||||
def write_build(path: str, sheet: str, cols, rows, schema_version: int = 0) -> None:
|
||||
doc = {
|
||||
"sheet": sheet,
|
||||
"schema_version": schema_version,
|
||||
"columns": list(cols),
|
||||
"rows": [typed_row(r) for r in rows],
|
||||
}
|
||||
with open(path, "w", encoding="utf-8", newline="\n") as f:
|
||||
f.write(canon_json(doc) + "\n")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
sheets = build_rows()
|
||||
self_check(sheets)
|
||||
os.makedirs(SHEETS_DIR, exist_ok=True)
|
||||
os.makedirs(BUILD_DIR, exist_ok=True)
|
||||
for sheet, (cols, rows) in sheets.items():
|
||||
write_csv(os.path.join(SHEETS_DIR, sheet + ".csv"), cols, rows)
|
||||
write_build(os.path.join(BUILD_DIR, sheet + ".json"), sheet, cols, rows)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,40 @@
|
||||
extends RefCounted
|
||||
|
||||
# 用例:BalanceData 载入 + 跨表引用 + CSV/Bible 关键值(数据门)
|
||||
|
||||
func run(a, data) -> void:
|
||||
a.true_(data.load_errors.is_empty(), "load_errors 为空(%s)" % str(data.load_errors))
|
||||
a.eq(data.generators.size(), 12, "12 班底")
|
||||
a.eq(data.upgrades.size(), 108, "108 升级")
|
||||
a.eq(data.meta_upgrades.size(), 12, "12 宿慧商品")
|
||||
a.eq(data.halls.size(), 10, "10 殿")
|
||||
a.eq(data.palettes.size(), 10, "10 色板")
|
||||
a.eq(data.bosses.size(), 10, "10 殿王战")
|
||||
a.eq(data.events.size(), 4, "4 事件")
|
||||
# 关键数值(Balance Bible v1.0 逐值一致)
|
||||
a.near(data.generators[11].base_rate, 1073741824.0, 0.5, "gen12 rate = 8^10")
|
||||
a.near(data.generators[1].payback_seconds(), 120.0, 0.5, "gen2 回本 120s")
|
||||
a.near(data.generators[5].payback_seconds(), 293.0, 0.5, "gen6 回本 293s")
|
||||
a.near(data.generators[11].payback_seconds(), 1118.0, 0.5, "gen12 回本 1118s")
|
||||
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])")
|
||||
# 敕封诏书(P-12:requires = enthrone_ready)
|
||||
var edict := data.upgrade("up_gate_tianzi")
|
||||
a.true_(edict != null, "敕封诏书存在")
|
||||
a.eq(edict.requires, "enthrone_ready", "诏书 requires = enthrone_ready")
|
||||
a.eq(edict.cost, 800000000000, "诏书成本 800B")
|
||||
# 调参数(balance_tuning)
|
||||
a.near(data.tune_num("tune_souls_div", 0.0), 10000.0, 1e-9, "超度除数 10000")
|
||||
a.near(data.tune_num("tune_enthrone_suihui_req", 0.0), 60.0, 1e-9, "敕封宿慧门槛 60")
|
||||
a.near(data.tune_num("tune_enthrone_rebirths", 0.0), 2.0, 1e-9, "敕封转世门槛 2")
|
||||
a.eq(data.tune_text("tune_restart_curve", ""), "1:0.6:0.4:0.3", "重开曲线文本")
|
||||
a.near(data.tune_num("tune_wrath_layers", 0.0), 22.0, 1e-9, "冤煞 22 层")
|
||||
a.near(data.tune_num("tune_click_base", 0.0), 1.0, 1e-9, "点击基础值 1")
|
||||
# 跨表引用完整性(schema 管单表;这里查跨表)
|
||||
a.true_(data.check_references().is_empty(), "跨表引用无悬挂(%s)" % str(data.check_references()))
|
||||
# 每殿恰一场殿王战 + 色板
|
||||
for h in data.halls:
|
||||
a.true_(data.boss_for_hall(h.id) != null, "%s 有 boss 行" % h.id)
|
||||
a.true_(data.palette_for_hall(h.id) != null, "%s 有色板行" % h.id)
|
||||
@@ -0,0 +1,59 @@
|
||||
extends RefCounted
|
||||
|
||||
# 用例:核心公式(MultiplierSet / PrestigeModel / OfflineModel)
|
||||
|
||||
func run(a, _data) -> void:
|
||||
# ---- MultiplierSet 三乘区 + 软上限 ----
|
||||
var m := MultiplierSet.new(16.0, 8.0, 64.0)
|
||||
m.set_click_levels(4, 0.0, 0.01)
|
||||
a.near(m.click_mult(), 16.0, 1e-9, "点击区 4 级 = ×16")
|
||||
m.set_click_levels(5, 0.0, 0.01)
|
||||
a.near(m.click_mult(), 16.0, 1e-9, "点击区 5 级仍钳 ×16")
|
||||
m.set_gen_eff("gen_01_youhun", 6)
|
||||
a.near(m.gen_mult("gen_01_youhun"), 7.0, 1e-9, "班底区 6 级 = ×7")
|
||||
m.set_gen_eff("gen_01_youhun", 99)
|
||||
a.near(m.gen_mult("gen_01_youhun"), 8.0, 1e-9, "班底区钳 ×8")
|
||||
m.set_global_add(63.0)
|
||||
a.near(m.global_mult(), 64.0, 1e-9, "全局区 ×64")
|
||||
m.set_global_add(100.0)
|
||||
a.near(m.global_mult(), 64.0, 1e-9, "全局区钳 ×64")
|
||||
m.set_click_levels(4, 8.0, 0.01)
|
||||
a.near(m.click_value(1.0, 1000.0), 96.0, 1e-6, "click_value = 1×16 + 1000×8%")
|
||||
|
||||
# ---- PrestigeModel ----
|
||||
a.eq(PrestigeModel.suihui_gain(1.5e9, 1e6, 5.0, 0.0, 0.0), 193, "宿慧 1.5B → 193")
|
||||
a.eq(PrestigeModel.suihui_gain(1.25e7, 1e6, 5.0, 0.0, 0.0), 17, "宿慧 12.5M → 17")
|
||||
a.eq(PrestigeModel.suihui_gain(1.5e9, 1e6, 5.0, 0.0, 0.3), 135, "宿慧 30% 削减 → 135")
|
||||
a.eq(PrestigeModel.suihui_gain(1.5e9, 1e6, 5.0, 0.45, 0.0), 279, "宿慧 m03×3 → 279")
|
||||
a.eq(PrestigeModel.suihui_gain(0.0, 1e6, 5.0, 0.0, 0.0), 0, "零累计 → 0 宿慧")
|
||||
a.eq(PrestigeModel.wrath_layer_cost(1, 8.0, 1.8), 8, "怨煞层 1 成本 8")
|
||||
a.eq(PrestigeModel.wrath_layer_cost(2, 8.0, 1.8), 28, "怨煞层 2 成本 28")
|
||||
a.eq(PrestigeModel.wrath_layer_cost(22, 8.0, 1.8), 2087, "怨煞层 22 成本 2087")
|
||||
a.near(PrestigeModel.wrath_penalty(10, 0.03, 0.3, 0.0), 0.3, 1e-9, "冤气削减封顶 30%")
|
||||
a.near(PrestigeModel.wrath_penalty(10, 0.03, 0.3, 0.15), 0.15, 1e-9, "m07 改 15%")
|
||||
a.near(PrestigeModel.wrath_penalty(5, 0.03, 0.3, 0.0), 0.15, 1e-9, "5 层 = 15%")
|
||||
a.eq(PrestigeModel.skip_count(23, 5), 4, "skip 23/5 = 4")
|
||||
a.eq(PrestigeModel.skip_count(23, 4), 5, "skip 23/4 = 5(m04)")
|
||||
a.near(PrestigeModel.restart_target(0), 1.0, 1e-9, "重开曲线 R1")
|
||||
a.near(PrestigeModel.restart_target(3), 0.3, 1e-9, "重开曲线 R4")
|
||||
a.near(PrestigeModel.restart_target(4), 0.25, 1e-9, "重开曲线 R5 触底")
|
||||
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)
|
||||
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)")
|
||||
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)
|
||||
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)")
|
||||
|
||||
# ---- BigNumber 红线 ----
|
||||
a.true_(BigNumber.assert_within(1e13, "test"), "1e13 不过红线")
|
||||
a.true_(not BigNumber.assert_within(1e15, "test"), "1e15 触红线")
|
||||
@@ -0,0 +1,159 @@
|
||||
extends RefCounted
|
||||
|
||||
# 用例:EconomyState 状态机(点击/购买/殿门/冤气/prestige/敕封)
|
||||
|
||||
func run(a, data) -> void:
|
||||
_test_first_purchase(a, data)
|
||||
_test_hall_gate(a, data)
|
||||
_test_upgrades_and_meta(a, data)
|
||||
_test_wrath(a, data)
|
||||
_test_prestige(a, data)
|
||||
_test_enthrone(a, data)
|
||||
_test_event_and_offline(a, data)
|
||||
|
||||
|
||||
# ---- 首购循环:15 点击 → 买 gen1 → 产出 0.1/s(GDD demo 验证点)----
|
||||
func _test_first_purchase(a, data) -> void:
|
||||
var e := EconomyState.new(data)
|
||||
a.near(e.merit, 0.0, 1e-9, "初始阴德 0")
|
||||
a.near(e.output_per_sec(), 0.0, 1e-9, "初始产出 0")
|
||||
for _i in 15:
|
||||
e.click()
|
||||
a.near(e.merit, 15.0, 1e-9, "15 次点击 = 15 阴德")
|
||||
a.near(e.click_value(), 1.0, 1e-9, "点击价值 = 1")
|
||||
var r: Dictionary = e.buy_generator(0, 1)
|
||||
a.true_(bool(r.get("ok", false)), "15 阴德可买游魂×1")
|
||||
a.eq(e.gen_levels[0], 1, "游魂 1 级")
|
||||
a.near(e.merit, 0.0, 1e-9, "买后阴德 0")
|
||||
a.near(e.output_per_sec(), 0.1, 1e-9, "产出 0.1/s")
|
||||
var r2: Dictionary = e.buy_generator(0, 1)
|
||||
a.true_(not bool(r2.get("ok", false)), "0 阴德再买失败")
|
||||
e.merit = 100.0
|
||||
var r3: Dictionary = e.buy_upgrade("up_click_1")
|
||||
a.true_(bool(r3.get("ok", false)), "买指诀·倍力 I(50)")
|
||||
a.near(e.click_value(), 2.0, 1e-9, "点击价值 ×2")
|
||||
|
||||
|
||||
# ---- 殿门单计数器闸门 ----
|
||||
func _test_hall_gate(a, data) -> void:
|
||||
var e := EconomyState.new(data)
|
||||
a.false_(e.generator_unlocked(2), "gen3 初始锁定")
|
||||
e.merit_run_total = 1999.0
|
||||
e.tick(0.1)
|
||||
a.eq(e.halls_passed, 0, "1999 不过 H1")
|
||||
e.merit_run_total = 2000.0
|
||||
e.tick(0.1)
|
||||
a.eq(e.halls_passed, 1, "2000 过 H1(单计数器闸门)")
|
||||
a.true_(e.upgrade_visible(data.upgrade("up_eff_gen_03_guizu_1")), "H1 后 gen3 效率可见")
|
||||
a.true_(e.upgrade_visible(data.upgrade("up_gate_guizu")), "H1 后入职文书可见")
|
||||
a.false_(e.generator_unlocked(2), "过殿但未买文书 → gen3 仍锁")
|
||||
e.merit = 1000.0
|
||||
var r: Dictionary = e.buy_upgrade("up_gate_guizu")
|
||||
a.true_(bool(r.get("ok", false)), "买鬼卒班入职文书(800)")
|
||||
a.true_(e.generator_unlocked(2), "双闸齐 → gen3 解锁")
|
||||
var r2: Dictionary = e.buy_generator(2, 1)
|
||||
a.true_(bool(r2.get("ok", false)), "gen3 可购")
|
||||
|
||||
|
||||
# ---- 升级/宿慧商店 ----
|
||||
func _test_upgrades_and_meta(a, data) -> void:
|
||||
var e := EconomyState.new(data)
|
||||
e.suihui = 10
|
||||
var r: Dictionary = e.buy_meta("meta_01")
|
||||
a.true_(bool(r.get("ok", false)), "买枕旧梦(3 宿慧)")
|
||||
a.eq(e.suihui, 7, "扣 3 宿慧")
|
||||
a.eq(int(e.meta_ranks.get("meta_01", 0)), 1, "meta_01 阶 1")
|
||||
var r2: Dictionary = e.buy_meta("meta_01")
|
||||
a.true_(not bool(r2.get("ok", false)), "meta_01 一阶限购")
|
||||
e.suihui = 100
|
||||
var r3: Dictionary = e.buy_meta("meta_03")
|
||||
a.true_(bool(r3.get("ok", false)), "买宿慧通透(8)")
|
||||
# 效率升级进班底区
|
||||
e.merit = 100000.0
|
||||
e.gen_levels[0] = 10
|
||||
var r4: Dictionary = e.buy_upgrade("up_eff_gen_01_youhun_1")
|
||||
a.true_(bool(r4.get("ok", false)), "买游魂操练 I(83)")
|
||||
a.near(e.gen_mult(0), 2.0, 1e-9, "班底区 ×2(+100%)")
|
||||
|
||||
|
||||
# ---- 冤气线 ----
|
||||
func _test_wrath(a, data) -> void:
|
||||
var e := EconomyState.new(data)
|
||||
e.devour(100.0)
|
||||
a.near(e.wrath, 100.0, 1e-9, "吞噬 +100 冤气")
|
||||
a.eq(e.devour_count, 1, "吞噬计数")
|
||||
a.eq(e.wrath_layer_cost(), 8, "第 1 层成本 8")
|
||||
var r: Dictionary = e.buy_wrath_layer()
|
||||
a.true_(bool(r.get("ok", false)), "买第 1 层怨煞")
|
||||
a.eq(e.wrath_layers, 1, "1 层")
|
||||
a.near(e.global_mult(), 1.25, 1e-9, "全局 ×1.25(每层 +25%)")
|
||||
a.near(e.wrath_penalty(), 0.03, 1e-9, "宿慧削减 3%")
|
||||
e.wrath = 0.0
|
||||
var r2: Dictionary = e.buy_wrath_layer()
|
||||
a.true_(not bool(r2.get("ok", false)), "冤气不足买不了")
|
||||
e.wrath = 1e6
|
||||
for _i in 21:
|
||||
e.buy_wrath_layer()
|
||||
a.eq(e.wrath_layers, 22, "22 层上限")
|
||||
var r3: Dictionary = e.buy_wrath_layer()
|
||||
a.true_(not bool(r3.get("ok", false)), "满层后再买失败")
|
||||
|
||||
|
||||
# ---- 轮回转世 ----
|
||||
func _test_prestige(a, data) -> void:
|
||||
var e := EconomyState.new(data)
|
||||
a.false_(e.prestige_available(), "初始不可轮回")
|
||||
e.merit_run_total = 1.5e9
|
||||
a.true_(e.prestige_available(), "H10 门达成 → 可轮回")
|
||||
a.eq(e.suihui_gain(), 193, "1.5B → 193 宿慧")
|
||||
e.gen_levels[0] = 50
|
||||
e.merit = 123.0
|
||||
var r: Dictionary = e.do_prestige()
|
||||
a.true_(bool(r.get("ok", false)), "执行轮回")
|
||||
a.eq(e.suihui, 193, "宿慧入账")
|
||||
a.eq(e.rebirths, 1, "转世 1 次")
|
||||
a.near(e.merit, 0.0, 1e-9, "阴德清零")
|
||||
a.eq(e.gen_levels[0], 950, "跳过机制:193/5=38 ×25 = 950 级")
|
||||
a.eq(e.halls_passed, 0, "殿门进度重置")
|
||||
a.eq(e.bought.size(), 0, "升级清空")
|
||||
# 第二轮更快(宿慧全局 +3%)
|
||||
a.near(e.global_mult(), 1.0 + 193 * 0.03, 1e-9, "宿慧全局加成")
|
||||
|
||||
|
||||
# ---- 阴司敕封 + gen12 愿景档 ----
|
||||
func _test_enthrone(a, data) -> void:
|
||||
var e := EconomyState.new(data)
|
||||
e.rebirths = 2
|
||||
e.suihui = 100
|
||||
a.true_(e.enthrone_ready(), "转世 2 + 宿慧 100 → 敕封就绪")
|
||||
e.merit_run_total = 1.5e9
|
||||
e.merit = 1e12
|
||||
var r: Dictionary = e.buy_upgrade("up_gate_tianzi")
|
||||
a.true_(bool(r.get("ok", false)), "买敕封诏书(800B)")
|
||||
a.true_(e.enthrone_available(), "受敕可用")
|
||||
a.false_(e.generator_unlocked(11), "未受敕 → gen12 仍锁(P-12)")
|
||||
var r2: Dictionary = e.do_enthrone()
|
||||
a.true_(bool(r2.get("ok", false)), "执行受敕")
|
||||
a.true_(e.enthroned, "enthroned 旗标")
|
||||
a.true_(e.generator_unlocked(11), "受敕 → gen12 解锁")
|
||||
# 全局区 = 1 + 宿慧100×3% + 敕封 25% = 4.25
|
||||
a.near(e.global_mult(), 4.25, 1e-9, "全局 ×4.25(宿慧+敕封)")
|
||||
# gen12 产出接入
|
||||
e.gen_levels[11] = 1
|
||||
a.near(e.output_per_sec(), 1073741824.0 * 4.25, 1.0, "gen12 1 级产出")
|
||||
|
||||
|
||||
# ---- 事件瞬时区 + 离线倍率 ----
|
||||
func _test_event_and_offline(a, data) -> void:
|
||||
var e := EconomyState.new(data)
|
||||
e.gen_levels[0] = 10
|
||||
var base_out := e.output_per_sec()
|
||||
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")
|
||||
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 乘性链)")
|
||||
@@ -0,0 +1,19 @@
|
||||
extends RefCounted
|
||||
|
||||
# 用例:NumberFormat(core/numbers/number_format.gd)
|
||||
|
||||
func run(a, _data) -> void:
|
||||
a.eq(NumberFormat.short(999.0), "999", "short 999")
|
||||
a.eq(NumberFormat.short(1000.0), "1.00K", "short 1K")
|
||||
a.eq(NumberFormat.short(1234.0), "1.23K", "short 1234")
|
||||
a.eq(NumberFormat.short(1.5e9), "1.50B", "short 1.5B")
|
||||
a.eq(NumberFormat.short(1.5e12), "1.50T", "short 1.5T")
|
||||
a.eq(NumberFormat.short_int(1500.0), "2K", "short_int 1500 → 2K")
|
||||
a.eq(NumberFormat.short_int(1400.0), "1K", "short_int 1400 → 1K")
|
||||
a.eq(NumberFormat.duration(42.0), "42s", "duration 42s")
|
||||
a.eq(NumberFormat.duration(90.0), "1m 30s", "duration 90s")
|
||||
a.eq(NumberFormat.duration(3600.0), "1h", "duration 1h")
|
||||
a.eq(NumberFormat.duration(5100.0), "1h 25m", "duration 1h25m")
|
||||
a.eq(NumberFormat.minutes(25.0), "25m", "minutes 25")
|
||||
a.eq(NumberFormat.minutes(180.0), "3h", "minutes 180")
|
||||
a.eq(NumberFormat.minutes(125.0), "2h 5m", "minutes 125")
|
||||
@@ -0,0 +1,78 @@
|
||||
extends SceneTree
|
||||
|
||||
# ============================================================================
|
||||
# test_runner.gd —— core 层无头单测入口(GUT 未装前的自包含方案;W3 换 GUT)
|
||||
#
|
||||
# 运行:godot --headless --path . -s res://tests/unit/test_runner.gd
|
||||
# 退出码:0 = 全过;1 = 有失败;2 = 数据/环境错误。
|
||||
# 每个用例文件 = tests/unit/cases/*.gd,契约:func run(a) -> void(a = TestAssert)。
|
||||
# ============================================================================
|
||||
|
||||
class TestAssert extends RefCounted:
|
||||
var failures: Array[String] = []
|
||||
var checks := 0
|
||||
|
||||
func check(cond: bool, msg: String) -> void:
|
||||
checks += 1
|
||||
if not cond:
|
||||
failures.append(msg)
|
||||
|
||||
func eq(got: Variant, want: Variant, msg: String) -> void:
|
||||
checks += 1
|
||||
if got != want:
|
||||
failures.append("%s:got %s want %s" % [msg, str(got), str(want)])
|
||||
|
||||
func near(got: float, want: float, tol: float, msg: String) -> void:
|
||||
checks += 1
|
||||
if absf(float(got) - float(want)) > tol:
|
||||
failures.append("%s:got %.6g want %.6g(tol %g)" % [msg, got, want, tol])
|
||||
|
||||
func true_(cond: bool, msg: String) -> void:
|
||||
check(cond, msg)
|
||||
|
||||
func false_(cond: bool, msg: String) -> void:
|
||||
check(not cond, msg)
|
||||
|
||||
|
||||
const CASE_FILES: Array[String] = [
|
||||
"res://tests/unit/cases/test_number_format.gd",
|
||||
"res://tests/unit/cases/test_economy_formulas.gd",
|
||||
"res://tests/unit/cases/test_balance_data.gd",
|
||||
"res://tests/unit/cases/test_economy_state.gd",
|
||||
]
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
var data := BalanceData.load_from_dir("res://data/build/")
|
||||
if not data.load_errors.is_empty():
|
||||
for e in data.load_errors:
|
||||
printerr("test_runner:数据错误:%s" % e)
|
||||
quit(2)
|
||||
return
|
||||
var total_checks := 0
|
||||
var total_fail: Array[String] = []
|
||||
for path in CASE_FILES:
|
||||
var script: Variant = load(path)
|
||||
if script == null:
|
||||
printerr("test_runner:用例缺失 %s" % path)
|
||||
quit(2)
|
||||
return
|
||||
var case_obj: Variant = script.new()
|
||||
if not (case_obj is RefCounted) or not case_obj.has_method("run"):
|
||||
printerr("test_runner:用例契约不符 %s" % path)
|
||||
quit(2)
|
||||
return
|
||||
var a := TestAssert.new()
|
||||
case_obj.run(a, data)
|
||||
total_checks += a.checks
|
||||
for f in a.failures:
|
||||
total_fail.append("%s → %s" % [path.get_file(), f])
|
||||
print("test_runner:%s:%d 检查,%d 失败" % [path.get_file(), a.checks, a.failures.size()])
|
||||
if total_fail.is_empty():
|
||||
print("test_runner:PASS(%d 检查全过)" % total_checks)
|
||||
quit(0)
|
||||
return
|
||||
print("test_runner:FAIL(%d/%d)" % [total_fail.size(), total_checks])
|
||||
for f in total_fail:
|
||||
printerr(" ✗ %s" % f)
|
||||
quit(1)
|
||||
Reference in new issue
Block a user