From 24e5b0c4ccfa25abac4f658a94aa258eb31b9814 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A8=8B=E5=9F=BA=E5=B2=A9=EF=BC=88engineering-lead?= =?UTF-8?q?=EF=BC=89?= Date: Mon, 19 Oct 2026 17:44:53 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20W2=20=E7=BB=8F=E6=B5=8E=E6=A0=B8?= =?UTF-8?q?=E5=BF=83+=E6=95=B0=E6=8D=AE=E7=AE=A1=E7=BA=BF+=E7=81=B0?= =?UTF-8?q?=E7=9B=92=20=E2=80=94=20Balance=20Bible=20v1.0=20=E5=85=AB?= =?UTF-8?q?=E8=A1=A8=E9=80=90=E5=80=BC=E8=90=BD=E5=9C=B0=EF=BC=8CEconomySt?= =?UTF-8?q?ate/sim=2014=20=E9=97=A8/=E7=81=B0=E7=9B=92=E4=B8=BB=E5=9C=BA?= =?UTF-8?q?=E6=99=AF/=E5=8D=95=E6=B5=8B=EF=BC=88GDScript=20=E5=BE=85?= =?UTF-8?q?=E6=9C=AC=E6=9C=BA=E5=BC=95=E6=93=8E=E9=AA=8C=E8=AF=81=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- data/build/balance_tuning.json | 223 ++++ data/build/boss_battles.json | 182 +++ data/build/events.json | 101 ++ data/build/generators.json | 18 +- data/build/hall_palettes.json | 193 +++ data/build/halls.json | 160 +++ data/build/meta_upgrades.json | 162 +++ data/build/upgrades.json | 1423 +++++++++++++++++++++ data/schema/balance_tuning.schema.json | 40 + data/schema/boss_battles.schema.json | 67 + data/schema/events.schema.json | 61 + data/schema/generators.schema.json | 6 +- data/schema/hall_palettes.schema.json | 63 + data/schema/halls.schema.json | 79 ++ data/schema/meta_upgrades.schema.json | 64 + data/schema/upgrades.schema.json | 81 ++ data/sheets/balance_tuning.csv | 31 + data/sheets/boss_battles.csv | 11 + data/sheets/events.csv | 5 + data/sheets/generators.csv | 18 +- data/sheets/hall_palettes.csv | 11 + data/sheets/halls.csv | 11 + data/sheets/meta_upgrades.csv | 13 + data/sheets/upgrades.csv | 109 ++ project.godot | 7 +- src/core/data/balance_data.gd | 336 +++++ src/core/data/boss_def.gd | 39 + src/core/data/event_def.gd | 37 + src/core/data/hall_def.gd | 23 + src/core/data/palette_def.gd | 32 + src/core/economy/economy_state.gd | 635 +++++++++ src/core/economy/generator_def.gd | 51 + src/core/economy/multiplier_set.gd | 83 ++ src/core/economy/offline_model.gd | 58 + src/core/economy/prestige_model.gd | 61 + src/core/economy/upgrade_def.gd | 42 + src/core/numbers/big_number.gd | 36 + src/core/numbers/number_format.gd | 78 ++ src/core/sim/gates.gd | 221 ++++ src/core/sim/sim_main.gd | 85 ++ src/core/sim/simulator.gd | 124 ++ src/core/sim/strategies/greedy.gd | 67 + src/core/sim/strategies/novice.gd | 25 + src/core/sim/strategies/random.gd | 40 + src/core/sim/strategies/strategy_base.gd | 57 + src/game/data_registry.gd | 89 ++ src/game/game_state.gd | 138 ++ src/presentation/hud/hud_root.gd | 171 +++ src/presentation/stage/army_band.gd | 122 ++ src/presentation/stage/hall_band.gd | 87 ++ src/presentation/stage/stage_root.gd | 239 ++++ src/presentation/stage/stage_root.tscn | 6 + src/tools/gen_balance_sheets.py | 645 ++++++++++ tests/unit/cases/test_balance_data.gd | 40 + tests/unit/cases/test_economy_formulas.gd | 59 + tests/unit/cases/test_economy_state.gd | 159 +++ tests/unit/cases/test_number_format.gd | 19 + tests/unit/test_runner.gd | 78 ++ 58 files changed, 7099 insertions(+), 22 deletions(-) create mode 100644 data/build/balance_tuning.json create mode 100644 data/build/boss_battles.json create mode 100644 data/build/events.json create mode 100644 data/build/hall_palettes.json create mode 100644 data/build/halls.json create mode 100644 data/build/meta_upgrades.json create mode 100644 data/build/upgrades.json create mode 100644 data/schema/balance_tuning.schema.json create mode 100644 data/schema/boss_battles.schema.json create mode 100644 data/schema/events.schema.json create mode 100644 data/schema/hall_palettes.schema.json create mode 100644 data/schema/halls.schema.json create mode 100644 data/schema/meta_upgrades.schema.json create mode 100644 data/schema/upgrades.schema.json create mode 100644 data/sheets/balance_tuning.csv create mode 100644 data/sheets/boss_battles.csv create mode 100644 data/sheets/events.csv create mode 100644 data/sheets/hall_palettes.csv create mode 100644 data/sheets/halls.csv create mode 100644 data/sheets/meta_upgrades.csv create mode 100644 data/sheets/upgrades.csv create mode 100644 src/core/data/balance_data.gd create mode 100644 src/core/data/boss_def.gd create mode 100644 src/core/data/event_def.gd create mode 100644 src/core/data/hall_def.gd create mode 100644 src/core/data/palette_def.gd create mode 100644 src/core/economy/economy_state.gd create mode 100644 src/core/economy/generator_def.gd create mode 100644 src/core/economy/multiplier_set.gd create mode 100644 src/core/economy/offline_model.gd create mode 100644 src/core/economy/prestige_model.gd create mode 100644 src/core/economy/upgrade_def.gd create mode 100644 src/core/numbers/big_number.gd create mode 100644 src/core/numbers/number_format.gd create mode 100644 src/core/sim/gates.gd create mode 100644 src/core/sim/sim_main.gd create mode 100644 src/core/sim/simulator.gd create mode 100644 src/core/sim/strategies/greedy.gd create mode 100644 src/core/sim/strategies/novice.gd create mode 100644 src/core/sim/strategies/random.gd create mode 100644 src/core/sim/strategies/strategy_base.gd create mode 100644 src/game/data_registry.gd create mode 100644 src/game/game_state.gd create mode 100644 src/presentation/hud/hud_root.gd create mode 100644 src/presentation/stage/army_band.gd create mode 100644 src/presentation/stage/hall_band.gd create mode 100644 src/presentation/stage/stage_root.gd create mode 100644 src/presentation/stage/stage_root.tscn create mode 100644 src/tools/gen_balance_sheets.py create mode 100644 tests/unit/cases/test_balance_data.gd create mode 100644 tests/unit/cases/test_economy_formulas.gd create mode 100644 tests/unit/cases/test_economy_state.gd create mode 100644 tests/unit/cases/test_number_format.gd create mode 100644 tests/unit/test_runner.gd diff --git a/data/build/balance_tuning.json b/data/build/balance_tuning.json new file mode 100644 index 0000000..dea4ce0 --- /dev/null +++ b/data/build/balance_tuning.json @@ -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 数值)" + } + ] +} diff --git a/data/build/boss_battles.json b/data/build/boss_battles.json new file mode 100644 index 0000000..5fe6812 --- /dev/null +++ b/data/build/boss_battles.json @@ -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 + } + ] +} diff --git a/data/build/events.json b/data/build/events.json new file mode 100644 index 0000000..e282f3b --- /dev/null +++ b/data/build/events.json @@ -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": "天狗噬月,阴气最盛——动手" + } + ] +} diff --git a/data/build/generators.json b/data/build/generators.json index 17aba54..bfa678e 100644 --- a/data/build/generators.json +++ b/data/build/generators.json @@ -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" diff --git a/data/build/hall_palettes.json b/data/build/hall_palettes.json new file mode 100644 index 0000000..bda9cae --- /dev/null +++ b/data/build/hall_palettes.json @@ -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 + } + ] +} diff --git a/data/build/halls.json b/data/build/halls.json new file mode 100644 index 0000000..e399900 --- /dev/null +++ b/data/build/halls.json @@ -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" + } + ] +} diff --git a/data/build/meta_upgrades.json b/data/build/meta_upgrades.json new file mode 100644 index 0000000..cba5bfa --- /dev/null +++ b/data/build/meta_upgrades.json @@ -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%" + } + ] +} diff --git a/data/build/upgrades.json b/data/build/upgrades.json new file mode 100644 index 0000000..8fb4c11 --- /dev/null +++ b/data/build/upgrades.json @@ -0,0 +1,1423 @@ +{ + "sheet": "upgrades", + "schema_version": 0, + "columns": [ + "id", + "name_cn", + "name_en", + "type", + "target_gen", + "unlock_hall", + "effect_type", + "effect_param", + "requires", + "cost", + "desc_cn" + ], + "rows": [ + { + "id": "up_eff_gen_01_youhun_1", + "name_cn": "游魂 · 历练 I", + "name_en": "Wandering Souls · Training I", + "type": "efficiency", + "target_gen": "gen_01_youhun", + "unlock_hall": "none", + "effect_type": "gen_output_add", + "effect_param": 1, + "requires": "none", + "cost": 83, + "desc_cn": "该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)" + }, + { + "id": "up_eff_gen_01_youhun_2", + "name_cn": "游魂 · 历练 II", + "name_en": "Wandering Souls · Training II", + "type": "efficiency", + "target_gen": "gen_01_youhun", + "unlock_hall": "none", + "effect_type": "gen_output_add", + "effect_param": 1, + "requires": "none", + "cost": 135, + "desc_cn": "该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)" + }, + { + "id": "up_eff_gen_01_youhun_3", + "name_cn": "游魂 · 历练 III", + "name_en": "Wandering Souls · Training III", + "type": "efficiency", + "target_gen": "gen_01_youhun", + "unlock_hall": "none", + "effect_type": "gen_output_add", + "effect_param": 1, + "requires": "none", + "cost": 221, + "desc_cn": "该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)" + }, + { + "id": "up_eff_gen_01_youhun_4", + "name_cn": "游魂 · 历练 IV", + "name_en": "Wandering Souls · Training IV", + "type": "efficiency", + "target_gen": "gen_01_youhun", + "unlock_hall": "none", + "effect_type": "gen_output_add", + "effect_param": 1, + "requires": "none", + "cost": 360, + "desc_cn": "该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)" + }, + { + "id": "up_eff_gen_01_youhun_5", + "name_cn": "游魂 · 历练 V", + "name_en": "Wandering Souls · Training V", + "type": "efficiency", + "target_gen": "gen_01_youhun", + "unlock_hall": "none", + "effect_type": "gen_output_add", + "effect_param": 1, + "requires": "none", + "cost": 587, + "desc_cn": "该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)" + }, + { + "id": "up_eff_gen_01_youhun_6", + "name_cn": "游魂 · 历练 VI", + "name_en": "Wandering Souls · Training VI", + "type": "efficiency", + "target_gen": "gen_01_youhun", + "unlock_hall": "none", + "effect_type": "gen_output_add", + "effect_param": 1, + "requires": "none", + "cost": 957, + "desc_cn": "该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)" + }, + { + "id": "up_eff_gen_02_yinbing_1", + "name_cn": "阴兵 · 历练 I", + "name_en": "Ghost Soldiers · Training I", + "type": "efficiency", + "target_gen": "gen_02_yinbing", + "unlock_hall": "none", + "effect_type": "gen_output_add", + "effect_param": 1, + "requires": "none", + "cost": 751, + "desc_cn": "该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)" + }, + { + "id": "up_eff_gen_02_yinbing_2", + "name_cn": "阴兵 · 历练 II", + "name_en": "Ghost Soldiers · Training II", + "type": "efficiency", + "target_gen": "gen_02_yinbing", + "unlock_hall": "none", + "effect_type": "gen_output_add", + "effect_param": 1, + "requires": "none", + "cost": 1269, + "desc_cn": "该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)" + }, + { + "id": "up_eff_gen_02_yinbing_3", + "name_cn": "阴兵 · 历练 III", + "name_en": "Ghost Soldiers · Training III", + "type": "efficiency", + "target_gen": "gen_02_yinbing", + "unlock_hall": "none", + "effect_type": "gen_output_add", + "effect_param": 1, + "requires": "none", + "cost": 2143, + "desc_cn": "该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)" + }, + { + "id": "up_eff_gen_02_yinbing_4", + "name_cn": "阴兵 · 历练 IV", + "name_en": "Ghost Soldiers · Training IV", + "type": "efficiency", + "target_gen": "gen_02_yinbing", + "unlock_hall": "none", + "effect_type": "gen_output_add", + "effect_param": 1, + "requires": "none", + "cost": 3620, + "desc_cn": "该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)" + }, + { + "id": "up_eff_gen_02_yinbing_5", + "name_cn": "阴兵 · 历练 V", + "name_en": "Ghost Soldiers · Training V", + "type": "efficiency", + "target_gen": "gen_02_yinbing", + "unlock_hall": "none", + "effect_type": "gen_output_add", + "effect_param": 1, + "requires": "none", + "cost": 6114, + "desc_cn": "该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)" + }, + { + "id": "up_eff_gen_02_yinbing_6", + "name_cn": "阴兵 · 历练 VI", + "name_en": "Ghost Soldiers · Training VI", + "type": "efficiency", + "target_gen": "gen_02_yinbing", + "unlock_hall": "none", + "effect_type": "gen_output_add", + "effect_param": 1, + "requires": "none", + "cost": 10326, + "desc_cn": "该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)" + }, + { + "id": "up_eff_gen_03_guizu_1", + "name_cn": "鬼卒班 · 历练 I", + "name_en": "Ghost Constables · Training I", + "type": "efficiency", + "target_gen": "gen_03_guizu", + "unlock_hall": "hall_01_qinguang", + "effect_type": "gen_output_add", + "effect_param": 1, + "requires": "none", + "cost": 8491, + "desc_cn": "该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)" + }, + { + "id": "up_eff_gen_03_guizu_2", + "name_cn": "鬼卒班 · 历练 II", + "name_en": "Ghost Constables · Training II", + "type": "efficiency", + "target_gen": "gen_03_guizu", + "unlock_hall": "hall_01_qinguang", + "effect_type": "gen_output_add", + "effect_param": 1, + "requires": "none", + "cost": 14851, + "desc_cn": "该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)" + }, + { + "id": "up_eff_gen_03_guizu_3", + "name_cn": "鬼卒班 · 历练 III", + "name_en": "Ghost Constables · Training III", + "type": "efficiency", + "target_gen": "gen_03_guizu", + "unlock_hall": "hall_01_qinguang", + "effect_type": "gen_output_add", + "effect_param": 1, + "requires": "none", + "cost": 25974, + "desc_cn": "该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)" + }, + { + "id": "up_eff_gen_03_guizu_4", + "name_cn": "鬼卒班 · 历练 IV", + "name_en": "Ghost Constables · Training IV", + "type": "efficiency", + "target_gen": "gen_03_guizu", + "unlock_hall": "hall_01_qinguang", + "effect_type": "gen_output_add", + "effect_param": 1, + "requires": "none", + "cost": 45428, + "desc_cn": "该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)" + }, + { + "id": "up_eff_gen_03_guizu_5", + "name_cn": "鬼卒班 · 历练 V", + "name_en": "Ghost Constables · Training V", + "type": "efficiency", + "target_gen": "gen_03_guizu", + "unlock_hall": "hall_01_qinguang", + "effect_type": "gen_output_add", + "effect_param": 1, + "requires": "none", + "cost": 79454, + "desc_cn": "该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)" + }, + { + "id": "up_eff_gen_03_guizu_6", + "name_cn": "鬼卒班 · 历练 VI", + "name_en": "Ghost Constables · Training VI", + "type": "efficiency", + "target_gen": "gen_03_guizu", + "unlock_hall": "hall_01_qinguang", + "effect_type": "gen_output_add", + "effect_param": 1, + "requires": "none", + "cost": 138966, + "desc_cn": "该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)" + }, + { + "id": "up_eff_gen_04_biliao_1", + "name_cn": "勾魂笔僚 · 历练 I", + "name_en": "Death-Scribe Clerks · Training I", + "type": "efficiency", + "target_gen": "gen_04_biliao", + "unlock_hall": "hall_02_chujiang", + "effect_type": "gen_output_add", + "effect_param": 1, + "requires": "none", + "cost": 84908, + "desc_cn": "该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)" + }, + { + "id": "up_eff_gen_04_biliao_2", + "name_cn": "勾魂笔僚 · 历练 II", + "name_en": "Death-Scribe Clerks · Training II", + "type": "efficiency", + "target_gen": "gen_04_biliao", + "unlock_hall": "hall_02_chujiang", + "effect_type": "gen_output_add", + "effect_param": 1, + "requires": "none", + "cost": 148505, + "desc_cn": "该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)" + }, + { + "id": "up_eff_gen_04_biliao_3", + "name_cn": "勾魂笔僚 · 历练 III", + "name_en": "Death-Scribe Clerks · Training III", + "type": "efficiency", + "target_gen": "gen_04_biliao", + "unlock_hall": "hall_02_chujiang", + "effect_type": "gen_output_add", + "effect_param": 1, + "requires": "none", + "cost": 259737, + "desc_cn": "该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)" + }, + { + "id": "up_eff_gen_04_biliao_4", + "name_cn": "勾魂笔僚 · 历练 IV", + "name_en": "Death-Scribe Clerks · Training IV", + "type": "efficiency", + "target_gen": "gen_04_biliao", + "unlock_hall": "hall_02_chujiang", + "effect_type": "gen_output_add", + "effect_param": 1, + "requires": "none", + "cost": 454282, + "desc_cn": "该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)" + }, + { + "id": "up_eff_gen_04_biliao_5", + "name_cn": "勾魂笔僚 · 历练 V", + "name_en": "Death-Scribe Clerks · Training V", + "type": "efficiency", + "target_gen": "gen_04_biliao", + "unlock_hall": "hall_02_chujiang", + "effect_type": "gen_output_add", + "effect_param": 1, + "requires": "none", + "cost": 794541, + "desc_cn": "该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)" + }, + { + "id": "up_eff_gen_04_biliao_6", + "name_cn": "勾魂笔僚 · 历练 VI", + "name_en": "Death-Scribe Clerks · Training VI", + "type": "efficiency", + "target_gen": "gen_04_biliao", + "unlock_hall": "hall_02_chujiang", + "effect_type": "gen_output_add", + "effect_param": 1, + "requires": "none", + "cost": 1389658, + "desc_cn": "该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)" + }, + { + "id": "up_eff_gen_05_tangpu_1", + "name_cn": "孟婆汤铺 · 历练 I", + "name_en": "Meng Po's Soup Stall · Training I", + "type": "efficiency", + "target_gen": "gen_05_tangpu", + "unlock_hall": "hall_03_songdi", + "effect_type": "gen_output_add", + "effect_param": 1, + "requires": "none", + "cost": 958502, + "desc_cn": "该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)" + }, + { + "id": "up_eff_gen_05_tangpu_2", + "name_cn": "孟婆汤铺 · 历练 II", + "name_en": "Meng Po's Soup Stall · Training II", + "type": "efficiency", + "target_gen": "gen_05_tangpu", + "unlock_hall": "hall_03_songdi", + "effect_type": "gen_output_add", + "effect_param": 1, + "requires": "none", + "cost": 1735502, + "desc_cn": "该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)" + }, + { + "id": "up_eff_gen_05_tangpu_3", + "name_cn": "孟婆汤铺 · 历练 III", + "name_en": "Meng Po's Soup Stall · Training III", + "type": "efficiency", + "target_gen": "gen_05_tangpu", + "unlock_hall": "hall_03_songdi", + "effect_type": "gen_output_add", + "effect_param": 1, + "requires": "none", + "cost": 3142368, + "desc_cn": "该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)" + }, + { + "id": "up_eff_gen_05_tangpu_4", + "name_cn": "孟婆汤铺 · 历练 IV", + "name_en": "Meng Po's Soup Stall · Training IV", + "type": "efficiency", + "target_gen": "gen_05_tangpu", + "unlock_hall": "hall_03_songdi", + "effect_type": "gen_output_add", + "effect_param": 1, + "requires": "none", + "cost": 5689695, + "desc_cn": "该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)" + }, + { + "id": "up_eff_gen_05_tangpu_5", + "name_cn": "孟婆汤铺 · 历练 V", + "name_en": "Meng Po's Soup Stall · Training V", + "type": "efficiency", + "target_gen": "gen_05_tangpu", + "unlock_hall": "hall_03_songdi", + "effect_type": "gen_output_add", + "effect_param": 1, + "requires": "none", + "cost": 10301985, + "desc_cn": "该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)" + }, + { + "id": "up_eff_gen_05_tangpu_6", + "name_cn": "孟婆汤铺 · 历练 VI", + "name_en": "Meng Po's Soup Stall · Training VI", + "type": "efficiency", + "target_gen": "gen_05_tangpu", + "unlock_hall": "hall_03_songdi", + "effect_type": "gen_output_add", + "effect_param": 1, + "requires": "none", + "cost": 18653180, + "desc_cn": "该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)" + }, + { + "id": "up_eff_gen_06_nanmin_1", + "name_cn": "枉死城难民团 · 历练 I", + "name_en": "Refugee Camp of the Wrongfully Dead · Training I", + "type": "efficiency", + "target_gen": "gen_06_nanmin", + "unlock_hall": "hall_04_wuguan", + "effect_type": "gen_output_add", + "effect_param": 1, + "requires": "none", + "cost": 9585022, + "desc_cn": "该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)" + }, + { + "id": "up_eff_gen_06_nanmin_2", + "name_cn": "枉死城难民团 · 历练 II", + "name_en": "Refugee Camp of the Wrongfully Dead · Training II", + "type": "efficiency", + "target_gen": "gen_06_nanmin", + "unlock_hall": "hall_04_wuguan", + "effect_type": "gen_output_add", + "effect_param": 1, + "requires": "none", + "cost": 17355017, + "desc_cn": "该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)" + }, + { + "id": "up_eff_gen_06_nanmin_3", + "name_cn": "枉死城难民团 · 历练 III", + "name_en": "Refugee Camp of the Wrongfully Dead · Training III", + "type": "efficiency", + "target_gen": "gen_06_nanmin", + "unlock_hall": "hall_04_wuguan", + "effect_type": "gen_output_add", + "effect_param": 1, + "requires": "none", + "cost": 31423678, + "desc_cn": "该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)" + }, + { + "id": "up_eff_gen_06_nanmin_4", + "name_cn": "枉死城难民团 · 历练 IV", + "name_en": "Refugee Camp of the Wrongfully Dead · Training IV", + "type": "efficiency", + "target_gen": "gen_06_nanmin", + "unlock_hall": "hall_04_wuguan", + "effect_type": "gen_output_add", + "effect_param": 1, + "requires": "none", + "cost": 56896947, + "desc_cn": "该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)" + }, + { + "id": "up_eff_gen_06_nanmin_5", + "name_cn": "枉死城难民团 · 历练 V", + "name_en": "Refugee Camp of the Wrongfully Dead · Training V", + "type": "efficiency", + "target_gen": "gen_06_nanmin", + "unlock_hall": "hall_04_wuguan", + "effect_type": "gen_output_add", + "effect_param": 1, + "requires": "none", + "cost": 103019852, + "desc_cn": "该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)" + }, + { + "id": "up_eff_gen_06_nanmin_6", + "name_cn": "枉死城难民团 · 历练 VI", + "name_en": "Refugee Camp of the Wrongfully Dead · Training VI", + "type": "efficiency", + "target_gen": "gen_06_nanmin", + "unlock_hall": "hall_04_wuguan", + "effect_type": "gen_output_add", + "effect_param": 1, + "requires": "none", + "cost": 186531799, + "desc_cn": "该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)" + }, + { + "id": "up_eff_gen_07_shuban_1", + "name_cn": "阴律司书办 · 历练 I", + "name_en": "Clerks of the Nether Court · Training I", + "type": "efficiency", + "target_gen": "gen_07_shuban", + "unlock_hall": "hall_05_yama", + "effect_type": "gen_output_add", + "effect_param": 1, + "requires": "none", + "cost": 108089451, + "desc_cn": "该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)" + }, + { + "id": "up_eff_gen_07_shuban_2", + "name_cn": "阴律司书办 · 历练 II", + "name_en": "Clerks of the Nether Court · Training II", + "type": "efficiency", + "target_gen": "gen_07_shuban", + "unlock_hall": "hall_05_yama", + "effect_type": "gen_output_add", + "effect_param": 1, + "requires": "none", + "cost": 202547440, + "desc_cn": "该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)" + }, + { + "id": "up_eff_gen_07_shuban_3", + "name_cn": "阴律司书办 · 历练 III", + "name_en": "Clerks of the Nether Court · Training III", + "type": "efficiency", + "target_gen": "gen_07_shuban", + "unlock_hall": "hall_05_yama", + "effect_type": "gen_output_add", + "effect_param": 1, + "requires": "none", + "cost": 379551056, + "desc_cn": "该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)" + }, + { + "id": "up_eff_gen_07_shuban_4", + "name_cn": "阴律司书办 · 历练 IV", + "name_en": "Clerks of the Nether Court · Training IV", + "type": "efficiency", + "target_gen": "gen_07_shuban", + "unlock_hall": "hall_05_yama", + "effect_type": "gen_output_add", + "effect_param": 1, + "requires": "none", + "cost": 711235870, + "desc_cn": "该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)" + }, + { + "id": "up_eff_gen_07_shuban_5", + "name_cn": "阴律司书办 · 历练 V", + "name_en": "Clerks of the Nether Court · Training V", + "type": "efficiency", + "target_gen": "gen_07_shuban", + "unlock_hall": "hall_05_yama", + "effect_type": "gen_output_add", + "effect_param": 1, + "requires": "none", + "cost": 1332775800, + "desc_cn": "该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)" + }, + { + "id": "up_eff_gen_07_shuban_6", + "name_cn": "阴律司书办 · 历练 VI", + "name_en": "Clerks of the Nether Court · Training VI", + "type": "efficiency", + "target_gen": "gen_07_shuban", + "unlock_hall": "hall_05_yama", + "effect_type": "gen_output_add", + "effect_param": 1, + "requires": "none", + "cost": 2497471526, + "desc_cn": "该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)" + }, + { + "id": "up_eff_gen_08_bingfang_1", + "name_cn": "幽都兵坊 · 历练 I", + "name_en": "Armory of the Dark Capital · Training I", + "type": "efficiency", + "target_gen": "gen_08_bingfang", + "unlock_hall": "hall_06_biancheng", + "effect_type": "gen_output_add", + "effect_param": 1, + "requires": "none", + "cost": 1080894509, + "desc_cn": "该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)" + }, + { + "id": "up_eff_gen_08_bingfang_2", + "name_cn": "幽都兵坊 · 历练 II", + "name_en": "Armory of the Dark Capital · Training II", + "type": "efficiency", + "target_gen": "gen_08_bingfang", + "unlock_hall": "hall_06_biancheng", + "effect_type": "gen_output_add", + "effect_param": 1, + "requires": "none", + "cost": 2025474395, + "desc_cn": "该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)" + }, + { + "id": "up_eff_gen_08_bingfang_3", + "name_cn": "幽都兵坊 · 历练 III", + "name_en": "Armory of the Dark Capital · Training III", + "type": "efficiency", + "target_gen": "gen_08_bingfang", + "unlock_hall": "hall_06_biancheng", + "effect_type": "gen_output_add", + "effect_param": 1, + "requires": "none", + "cost": 3795510563, + "desc_cn": "该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)" + }, + { + "id": "up_eff_gen_08_bingfang_4", + "name_cn": "幽都兵坊 · 历练 IV", + "name_en": "Armory of the Dark Capital · Training IV", + "type": "efficiency", + "target_gen": "gen_08_bingfang", + "unlock_hall": "hall_06_biancheng", + "effect_type": "gen_output_add", + "effect_param": 1, + "requires": "none", + "cost": 7112358700, + "desc_cn": "该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)" + }, + { + "id": "up_eff_gen_08_bingfang_5", + "name_cn": "幽都兵坊 · 历练 V", + "name_en": "Armory of the Dark Capital · Training V", + "type": "efficiency", + "target_gen": "gen_08_bingfang", + "unlock_hall": "hall_06_biancheng", + "effect_type": "gen_output_add", + "effect_param": 1, + "requires": "none", + "cost": 13327758001, + "desc_cn": "该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)" + }, + { + "id": "up_eff_gen_08_bingfang_6", + "name_cn": "幽都兵坊 · 历练 VI", + "name_en": "Armory of the Dark Capital · Training VI", + "type": "efficiency", + "target_gen": "gen_08_bingfang", + "unlock_hall": "hall_06_biancheng", + "effect_type": "gen_output_add", + "effect_param": 1, + "requires": "none", + "cost": 24974715257, + "desc_cn": "该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)" + }, + { + "id": "up_eff_gen_09_wodi_1", + "name_cn": "十殿卧底 · 历练 I", + "name_en": "Sleeper Agents in the Ten Courts · Training I", + "type": "efficiency", + "target_gen": "gen_09_wodi", + "unlock_hall": "hall_07_taishan", + "effect_type": "gen_output_add", + "effect_param": 1, + "requires": "none", + "cost": 12176692765, + "desc_cn": "该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)" + }, + { + "id": "up_eff_gen_09_wodi_2", + "name_cn": "十殿卧底 · 历练 II", + "name_en": "Sleeper Agents in the Ten Courts · Training II", + "type": "efficiency", + "target_gen": "gen_09_wodi", + "unlock_hall": "hall_07_taishan", + "effect_type": "gen_output_add", + "effect_param": 1, + "requires": "none", + "cost": 23607901124, + "desc_cn": "该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)" + }, + { + "id": "up_eff_gen_09_wodi_3", + "name_cn": "十殿卧底 · 历练 III", + "name_en": "Sleeper Agents in the Ten Courts · Training III", + "type": "efficiency", + "target_gen": "gen_09_wodi", + "unlock_hall": "hall_07_taishan", + "effect_type": "gen_output_add", + "effect_param": 1, + "requires": "none", + "cost": 45770473659, + "desc_cn": "该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)" + }, + { + "id": "up_eff_gen_09_wodi_4", + "name_cn": "十殿卧底 · 历练 IV", + "name_en": "Sleeper Agents in the Ten Courts · Training IV", + "type": "efficiency", + "target_gen": "gen_09_wodi", + "unlock_hall": "hall_07_taishan", + "effect_type": "gen_output_add", + "effect_param": 1, + "requires": "none", + "cost": 88738776396, + "desc_cn": "该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)" + }, + { + "id": "up_eff_gen_09_wodi_5", + "name_cn": "十殿卧底 · 历练 V", + "name_en": "Sleeper Agents in the Ten Courts · Training V", + "type": "efficiency", + "target_gen": "gen_09_wodi", + "unlock_hall": "hall_07_taishan", + "effect_type": "gen_output_add", + "effect_param": 1, + "requires": "none", + "cost": 172044766126, + "desc_cn": "该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)" + }, + { + "id": "up_eff_gen_09_wodi_6", + "name_cn": "十殿卧底 · 历练 VI", + "name_en": "Sleeper Agents in the Ten Courts · Training VI", + "type": "efficiency", + "target_gen": "gen_09_wodi", + "unlock_hall": "hall_07_taishan", + "effect_type": "gen_output_add", + "effect_param": 1, + "requires": "none", + "cost": 333556566289, + "desc_cn": "该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)" + }, + { + "id": "up_eff_gen_10_xingzhe_1", + "name_cn": "轮回裂隙行者 · 历练 I", + "name_en": "Rift Walkers of the Wheel · Training I", + "type": "efficiency", + "target_gen": "gen_10_xingzhe", + "unlock_hall": "hall_08_dushi", + "effect_type": "gen_output_add", + "effect_param": 1, + "requires": "none", + "cost": 121766927654, + "desc_cn": "该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)" + }, + { + "id": "up_eff_gen_10_xingzhe_2", + "name_cn": "轮回裂隙行者 · 历练 II", + "name_en": "Rift Walkers of the Wheel · Training II", + "type": "efficiency", + "target_gen": "gen_10_xingzhe", + "unlock_hall": "hall_08_dushi", + "effect_type": "gen_output_add", + "effect_param": 1, + "requires": "none", + "cost": 236079011240, + "desc_cn": "该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)" + }, + { + "id": "up_eff_gen_10_xingzhe_3", + "name_cn": "轮回裂隙行者 · 历练 III", + "name_en": "Rift Walkers of the Wheel · Training III", + "type": "efficiency", + "target_gen": "gen_10_xingzhe", + "unlock_hall": "hall_08_dushi", + "effect_type": "gen_output_add", + "effect_param": 1, + "requires": "none", + "cost": 457704736595, + "desc_cn": "该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)" + }, + { + "id": "up_eff_gen_10_xingzhe_4", + "name_cn": "轮回裂隙行者 · 历练 IV", + "name_en": "Rift Walkers of the Wheel · Training IV", + "type": "efficiency", + "target_gen": "gen_10_xingzhe", + "unlock_hall": "hall_08_dushi", + "effect_type": "gen_output_add", + "effect_param": 1, + "requires": "none", + "cost": 887387763957, + "desc_cn": "该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)" + }, + { + "id": "up_eff_gen_10_xingzhe_5", + "name_cn": "轮回裂隙行者 · 历练 V", + "name_en": "Rift Walkers of the Wheel · Training V", + "type": "efficiency", + "target_gen": "gen_10_xingzhe", + "unlock_hall": "hall_08_dushi", + "effect_type": "gen_output_add", + "effect_param": 1, + "requires": "none", + "cost": 1720447661255, + "desc_cn": "该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)" + }, + { + "id": "up_eff_gen_10_xingzhe_6", + "name_cn": "轮回裂隙行者 · 历练 VI", + "name_en": "Rift Walkers of the Wheel · Training VI", + "type": "efficiency", + "target_gen": "gen_10_xingzhe", + "unlock_hall": "hall_08_dushi", + "effect_type": "gen_output_add", + "effect_param": 1, + "requires": "none", + "cost": 3335565662885, + "desc_cn": "该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)" + }, + { + "id": "up_eff_gen_11_yinhun_1", + "name_cn": "六道引魂人 · 历练 I", + "name_en": "Soul Guides of the Six Paths · Training I", + "type": "efficiency", + "target_gen": "gen_11_yinhun", + "unlock_hall": "hall_09_pingdeng", + "effect_type": "gen_output_add", + "effect_param": 1, + "requires": "none", + "cost": 1370372723145, + "desc_cn": "该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)" + }, + { + "id": "up_eff_gen_11_yinhun_2", + "name_cn": "六道引魂人 · 历练 II", + "name_en": "Soul Guides of the Six Paths · Training II", + "type": "efficiency", + "target_gen": "gen_11_yinhun", + "unlock_hall": "hall_09_pingdeng", + "effect_type": "gen_output_add", + "effect_param": 1, + "requires": "none", + "cost": 2748062154037, + "desc_cn": "该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)" + }, + { + "id": "up_eff_gen_11_yinhun_3", + "name_cn": "六道引魂人 · 历练 III", + "name_en": "Soul Guides of the Six Paths · Training III", + "type": "efficiency", + "target_gen": "gen_11_yinhun", + "unlock_hall": "hall_09_pingdeng", + "effect_type": "gen_output_add", + "effect_param": 1, + "requires": "none", + "cost": 5510796789008, + "desc_cn": "该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)" + }, + { + "id": "up_eff_gen_11_yinhun_4", + "name_cn": "六道引魂人 · 历练 IV", + "name_en": "Soul Guides of the Six Paths · Training IV", + "type": "efficiency", + "target_gen": "gen_11_yinhun", + "unlock_hall": "hall_09_pingdeng", + "effect_type": "gen_output_add", + "effect_param": 1, + "requires": "none", + "cost": 11051016879340, + "desc_cn": "该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)" + }, + { + "id": "up_eff_gen_11_yinhun_5", + "name_cn": "六道引魂人 · 历练 V", + "name_en": "Soul Guides of the Six Paths · Training V", + "type": "efficiency", + "target_gen": "gen_11_yinhun", + "unlock_hall": "hall_09_pingdeng", + "effect_type": "gen_output_add", + "effect_param": 1, + "requires": "none", + "cost": 22161037458512, + "desc_cn": "该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)" + }, + { + "id": "up_eff_gen_11_yinhun_6", + "name_cn": "六道引魂人 · 历练 VI", + "name_en": "Soul Guides of the Six Paths · Training VI", + "type": "efficiency", + "target_gen": "gen_11_yinhun", + "unlock_hall": "hall_09_pingdeng", + "effect_type": "gen_output_add", + "effect_param": 1, + "requires": "none", + "cost": 44440397349832, + "desc_cn": "该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)" + }, + { + "id": "up_eff_gen_12_tianzi_1", + "name_cn": "幽冥天子 · 历练 I", + "name_en": "Emperor of the Netherworld · Training I", + "type": "efficiency", + "target_gen": "gen_12_tianzi", + "unlock_hall": "none", + "effect_type": "gen_output_add", + "effect_param": 1, + "requires": "none", + "cost": 15407021574586, + "desc_cn": "该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)" + }, + { + "id": "up_eff_gen_12_tianzi_2", + "name_cn": "幽冥天子 · 历练 II", + "name_en": "Emperor of the Netherworld · Training II", + "type": "efficiency", + "target_gen": "gen_12_tianzi", + "unlock_hall": "none", + "effect_type": "gen_output_add", + "effect_param": 1, + "requires": "none", + "cost": 31947999937062, + "desc_cn": "该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)" + }, + { + "id": "up_eff_gen_12_tianzi_3", + "name_cn": "幽冥天子 · 历练 III", + "name_en": "Emperor of the Netherworld · Training III", + "type": "efficiency", + "target_gen": "gen_12_tianzi", + "unlock_hall": "none", + "effect_type": "gen_output_add", + "effect_param": 1, + "requires": "none", + "cost": 66247372669492, + "desc_cn": "该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)" + }, + { + "id": "up_eff_gen_12_tianzi_4", + "name_cn": "幽冥天子 · 历练 IV", + "name_en": "Emperor of the Netherworld · Training IV", + "type": "efficiency", + "target_gen": "gen_12_tianzi", + "unlock_hall": "none", + "effect_type": "gen_output_add", + "effect_param": 1, + "requires": "none", + "cost": 137370551967459, + "desc_cn": "该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)" + }, + { + "id": "up_eff_gen_12_tianzi_5", + "name_cn": "幽冥天子 · 历练 V", + "name_en": "Emperor of the Netherworld · Training V", + "type": "efficiency", + "target_gen": "gen_12_tianzi", + "unlock_hall": "none", + "effect_type": "gen_output_add", + "effect_param": 1, + "requires": "none", + "cost": 284851576559723, + "desc_cn": "该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)" + }, + { + "id": "up_eff_gen_12_tianzi_6", + "name_cn": "幽冥天子 · 历练 VI", + "name_en": "Emperor of the Netherworld · Training VI", + "type": "efficiency", + "target_gen": "gen_12_tianzi", + "unlock_hall": "none", + "effect_type": "gen_output_add", + "effect_param": 1, + "requires": "none", + "cost": 590668229154242, + "desc_cn": "该班底产出 +100%(加法;6 级满 = ×7,软上限 ×8)" + }, + { + "id": "up_click_1", + "name_cn": "指诀 · 倍力 I", + "name_en": "Gesture · Force I", + "type": "click", + "target_gen": "none", + "unlock_hall": "none", + "effect_type": "click_mult", + "effect_param": 2, + "requires": "none", + "cost": 50, + "desc_cn": "点击价值 ×2(点击区,软上限 ×16)" + }, + { + "id": "up_click_2", + "name_cn": "指诀 · 倍力 II", + "name_en": "Gesture · Force II", + "type": "click", + "target_gen": "none", + "unlock_hall": "none", + "effect_type": "click_mult", + "effect_param": 2, + "requires": "none", + "cost": 500, + "desc_cn": "点击价值 ×2(点击区,软上限 ×16)" + }, + { + "id": "up_click_3", + "name_cn": "指诀 · 倍力 III", + "name_en": "Gesture · Force III", + "type": "click", + "target_gen": "none", + "unlock_hall": "none", + "effect_type": "click_mult", + "effect_param": 2, + "requires": "none", + "cost": 5000, + "desc_cn": "点击价值 ×2(点击区,软上限 ×16)" + }, + { + "id": "up_click_4", + "name_cn": "指诀 · 倍力 IV", + "name_en": "Gesture · Force IV", + "type": "click", + "target_gen": "none", + "unlock_hall": "none", + "effect_type": "click_mult", + "effect_param": 2, + "requires": "none", + "cost": 50000, + "desc_cn": "点击价值 ×2(点击区,软上限 ×16)" + }, + { + "id": "up_click_5", + "name_cn": "指诀 · 引阴 I", + "name_en": "Gesture · Soul-Draw 1", + "type": "click", + "target_gen": "none", + "unlock_hall": "none", + "effect_type": "click_output_pct", + "effect_param": 0.01, + "requires": "none", + "cost": 100000, + "desc_cn": "点击附加 +1% 当前产出/s(仪式与殿王战弹药)" + }, + { + "id": "up_click_6", + "name_cn": "指诀 · 引阴 II", + "name_en": "Gesture · Soul-Draw 2", + "type": "click", + "target_gen": "none", + "unlock_hall": "none", + "effect_type": "click_output_pct", + "effect_param": 0.01, + "requires": "none", + "cost": 1000000, + "desc_cn": "点击附加 +1% 当前产出/s(仪式与殿王战弹药)" + }, + { + "id": "up_click_7", + "name_cn": "指诀 · 引阴 III", + "name_en": "Gesture · Soul-Draw 3", + "type": "click", + "target_gen": "none", + "unlock_hall": "none", + "effect_type": "click_output_pct", + "effect_param": 0.01, + "requires": "none", + "cost": 10000000, + "desc_cn": "点击附加 +1% 当前产出/s(仪式与殿王战弹药)" + }, + { + "id": "up_click_8", + "name_cn": "指诀 · 引阴 IV", + "name_en": "Gesture · Soul-Draw 4", + "type": "click", + "target_gen": "none", + "unlock_hall": "none", + "effect_type": "click_output_pct", + "effect_param": 0.01, + "requires": "none", + "cost": 100000000, + "desc_cn": "点击附加 +1% 当前产出/s(仪式与殿王战弹药)" + }, + { + "id": "up_click_9", + "name_cn": "指诀 · 引阴 V", + "name_en": "Gesture · Soul-Draw 5", + "type": "click", + "target_gen": "none", + "unlock_hall": "none", + "effect_type": "click_output_pct", + "effect_param": 0.01, + "requires": "none", + "cost": 1000000000, + "desc_cn": "点击附加 +1% 当前产出/s(仪式与殿王战弹药)" + }, + { + "id": "up_click_10", + "name_cn": "指诀 · 引阴 VI", + "name_en": "Gesture · Soul-Draw 6", + "type": "click", + "target_gen": "none", + "unlock_hall": "none", + "effect_type": "click_output_pct", + "effect_param": 0.01, + "requires": "none", + "cost": 10000000000, + "desc_cn": "点击附加 +1% 当前产出/s(仪式与殿王战弹药)" + }, + { + "id": "up_click_11", + "name_cn": "指诀 · 引阴 VII", + "name_en": "Gesture · Soul-Draw 7", + "type": "click", + "target_gen": "none", + "unlock_hall": "none", + "effect_type": "click_output_pct", + "effect_param": 0.01, + "requires": "none", + "cost": 100000000000, + "desc_cn": "点击附加 +1% 当前产出/s(仪式与殿王战弹药)" + }, + { + "id": "up_click_12", + "name_cn": "指诀 · 引阴 VIII", + "name_en": "Gesture · Soul-Draw 8", + "type": "click", + "target_gen": "none", + "unlock_hall": "none", + "effect_type": "click_output_pct", + "effect_param": 0.01, + "requires": "none", + "cost": 1000000000000, + "desc_cn": "点击附加 +1% 当前产出/s(仪式与殿王战弹药)" + }, + { + "id": "up_gate_guizu", + "name_cn": "鬼卒班 · 入职文书", + "name_en": "Ghost Constables · Hiring Letter", + "type": "gate_hire", + "target_gen": "gen_03_guizu", + "unlock_hall": "hall_01_qinguang", + "effect_type": "unlock", + "effect_param": 0, + "requires": "none", + "cost": 800, + "desc_cn": "编制闸门:购入后该班底可购置(需先过对应殿门)" + }, + { + "id": "up_gate_biliao", + "name_cn": "勾魂笔僚 · 入职文书", + "name_en": "Death-Scribe Clerks · Hiring Letter", + "type": "gate_hire", + "target_gen": "gen_04_biliao", + "unlock_hall": "hall_02_chujiang", + "effect_type": "unlock", + "effect_param": 0, + "requires": "none", + "cost": 8000, + "desc_cn": "编制闸门:购入后该班底可购置(需先过对应殿门)" + }, + { + "id": "up_gate_tangpu", + "name_cn": "孟婆汤铺 · 入职文书", + "name_en": "Meng Po's Soup Stall · Hiring Letter", + "type": "gate_hire", + "target_gen": "gen_05_tangpu", + "unlock_hall": "hall_03_songdi", + "effect_type": "unlock", + "effect_param": 0, + "requires": "none", + "cost": 80000, + "desc_cn": "编制闸门:购入后该班底可购置(需先过对应殿门)" + }, + { + "id": "up_gate_nanmin", + "name_cn": "枉死城难民团 · 入职文书", + "name_en": "Refugee Camp of the Wrongfully Dead · Hiring Letter", + "type": "gate_hire", + "target_gen": "gen_06_nanmin", + "unlock_hall": "hall_04_wuguan", + "effect_type": "unlock", + "effect_param": 0, + "requires": "none", + "cost": 800000, + "desc_cn": "编制闸门:购入后该班底可购置(需先过对应殿门)" + }, + { + "id": "up_gate_shuban", + "name_cn": "阴律司书办 · 入职文书", + "name_en": "Clerks of the Nether Court · Hiring Letter", + "type": "gate_hire", + "target_gen": "gen_07_shuban", + "unlock_hall": "hall_05_yama", + "effect_type": "unlock", + "effect_param": 0, + "requires": "none", + "cost": 8000000, + "desc_cn": "编制闸门:购入后该班底可购置(需先过对应殿门)" + }, + { + "id": "up_gate_bingfang", + "name_cn": "幽都兵坊 · 入职文书", + "name_en": "Armory of the Dark Capital · Hiring Letter", + "type": "gate_hire", + "target_gen": "gen_08_bingfang", + "unlock_hall": "hall_06_biancheng", + "effect_type": "unlock", + "effect_param": 0, + "requires": "none", + "cost": 80000000, + "desc_cn": "编制闸门:购入后该班底可购置(需先过对应殿门)" + }, + { + "id": "up_gate_wodi", + "name_cn": "十殿卧底 · 入职文书", + "name_en": "Sleeper Agents in the Ten Courts · Hiring Letter", + "type": "gate_hire", + "target_gen": "gen_09_wodi", + "unlock_hall": "hall_07_taishan", + "effect_type": "unlock", + "effect_param": 0, + "requires": "none", + "cost": 800000000, + "desc_cn": "编制闸门:购入后该班底可购置(需先过对应殿门)" + }, + { + "id": "up_gate_xingzhe", + "name_cn": "轮回裂隙行者 · 入职文书", + "name_en": "Rift Walkers of the Wheel · Hiring Letter", + "type": "gate_hire", + "target_gen": "gen_10_xingzhe", + "unlock_hall": "hall_08_dushi", + "effect_type": "unlock", + "effect_param": 0, + "requires": "none", + "cost": 8000000000, + "desc_cn": "编制闸门:购入后该班底可购置(需先过对应殿门)" + }, + { + "id": "up_gate_yinhun", + "name_cn": "六道引魂人 · 入职文书", + "name_en": "Soul Guides of the Six Paths · Hiring Letter", + "type": "gate_hire", + "target_gen": "gen_11_yinhun", + "unlock_hall": "hall_09_pingdeng", + "effect_type": "unlock", + "effect_param": 0, + "requires": "none", + "cost": 80000000000, + "desc_cn": "编制闸门:购入后该班底可购置(需先过对应殿门)" + }, + { + "id": "up_gate_tianzi", + "name_cn": "幽冥天子 · 敕封诏书", + "name_en": "Enthronement Edict", + "type": "gate_hire", + "target_gen": "gen_12_tianzi", + "unlock_hall": "none", + "effect_type": "unlock", + "effect_param": 0, + "requires": "enthrone_ready", + "cost": 800000000000, + "desc_cn": "编制闸门:购入后该班底可购置(需先过对应殿门)" + }, + { + "id": "up_gate_wrath", + "name_cn": "开通 · 冤气系统", + "name_en": "Unlock: Wrath System", + "type": "gate_system", + "target_gen": "none", + "unlock_hall": "hall_01_qinguang", + "effect_type": "unlock", + "effect_param": 0, + "requires": "none", + "cost": 1000, + "desc_cn": "系统闸门:纯解锁零效果([B§2.3];成本外推 = 殿门阈值÷2,P-5)" + }, + { + "id": "up_gate_events", + "name_cn": "开通 · 事件系统", + "name_en": "Unlock: Event System", + "type": "gate_system", + "target_gen": "none", + "unlock_hall": "hall_02_chujiang", + "effect_type": "unlock", + "effect_param": 0, + "requires": "none", + "cost": 15000, + "desc_cn": "系统闸门:纯解锁零效果([B§2.3];成本外推 = 殿门阈值÷2,P-5)" + }, + { + "id": "up_gate_bulk10", + "name_cn": "开通 · 批量购置(×10)", + "name_en": "Unlock: Bulk Buy ×10", + "type": "gate_system", + "target_gen": "none", + "unlock_hall": "hall_03_songdi", + "effect_type": "unlock", + "effect_param": 0, + "requires": "none", + "cost": 150000, + "desc_cn": "系统闸门:纯解锁零效果([B§2.3];成本外推 = 殿门阈值÷2,P-5)" + }, + { + "id": "up_gate_offline", + "name_cn": "开通 · 离线收益", + "name_en": "Unlock: Offline Income", + "type": "gate_system", + "target_gen": "none", + "unlock_hall": "hall_04_wuguan", + "effect_type": "unlock", + "effect_param": 0, + "requires": "none", + "cost": 1000000, + "desc_cn": "系统闸门:纯解锁零效果([B§2.3];成本外推 = 殿门阈值÷2,P-5)" + }, + { + "id": "up_gate_prestige", + "name_cn": "开通 · 轮回预告", + "name_en": "Unlock: Rebirth Preview", + "type": "gate_system", + "target_gen": "none", + "unlock_hall": "hall_06_biancheng", + "effect_type": "unlock", + "effect_param": 0, + "requires": "none", + "cost": 20000000, + "desc_cn": "系统闸门:纯解锁零效果([B§2.3];成本外推 = 殿门阈值÷2,P-5)" + }, + { + "id": "up_gate_boss_retry", + "name_cn": "开通 · 殿王再战", + "name_en": "Unlock: Boss Rematch", + "type": "gate_system", + "target_gen": "none", + "unlock_hall": "hall_07_taishan", + "effect_type": "unlock", + "effect_param": 0, + "requires": "none", + "cost": 60000000, + "desc_cn": "系统闸门:纯解锁零效果([B§2.3];成本外推 = 殿门阈值÷2,P-5)" + }, + { + "id": "up_gate_bulkmax", + "name_cn": "开通 · 批量购置(×尽)", + "name_en": "Unlock: Bulk Buy Max", + "type": "gate_system", + "target_gen": "none", + "unlock_hall": "hall_08_dushi", + "effect_type": "unlock", + "effect_param": 0, + "requires": "none", + "cost": 175000000, + "desc_cn": "系统闸门:纯解锁零效果([B§2.3];成本外推 = 殿门阈值÷2,P-5)" + }, + { + "id": "up_gate_enthrone", + "name_cn": "开通 · 敕封预告", + "name_en": "Unlock: Enthronement Preview", + "type": "gate_system", + "target_gen": "none", + "unlock_hall": "hall_09_pingdeng", + "effect_type": "unlock", + "effect_param": 0, + "requires": "none", + "cost": 400000000, + "desc_cn": "系统闸门:纯解锁零效果([B§2.3];成本外推 = 殿门阈值÷2,P-5)" + }, + { + "id": "up_offline_1", + "name_cn": "离线收益 · 沉淀 I", + "name_en": "Offline Income · Settling I", + "type": "offline", + "target_gen": "none", + "unlock_hall": "hall_04_wuguan", + "effect_type": "offline_mult", + "effect_param": 1.15, + "requires": "none", + "cost": 2000, + "desc_cn": "离线效率 ×1.15(作用于衰减表全部时段;6 级满 ×2.31)" + }, + { + "id": "up_offline_2", + "name_cn": "离线收益 · 沉淀 II", + "name_en": "Offline Income · Settling II", + "type": "offline", + "target_gen": "none", + "unlock_hall": "hall_04_wuguan", + "effect_type": "offline_mult", + "effect_param": 1.15, + "requires": "none", + "cost": 20000, + "desc_cn": "离线效率 ×1.15(作用于衰减表全部时段;6 级满 ×2.31)" + }, + { + "id": "up_offline_3", + "name_cn": "离线收益 · 沉淀 III", + "name_en": "Offline Income · Settling III", + "type": "offline", + "target_gen": "none", + "unlock_hall": "hall_04_wuguan", + "effect_type": "offline_mult", + "effect_param": 1.15, + "requires": "none", + "cost": 200000, + "desc_cn": "离线效率 ×1.15(作用于衰减表全部时段;6 级满 ×2.31)" + }, + { + "id": "up_offline_4", + "name_cn": "离线收益 · 沉淀 IV", + "name_en": "Offline Income · Settling IV", + "type": "offline", + "target_gen": "none", + "unlock_hall": "hall_04_wuguan", + "effect_type": "offline_mult", + "effect_param": 1.15, + "requires": "none", + "cost": 2000000, + "desc_cn": "离线效率 ×1.15(作用于衰减表全部时段;6 级满 ×2.31)" + }, + { + "id": "up_offline_5", + "name_cn": "离线收益 · 沉淀 V", + "name_en": "Offline Income · Settling V", + "type": "offline", + "target_gen": "none", + "unlock_hall": "hall_04_wuguan", + "effect_type": "offline_mult", + "effect_param": 1.15, + "requires": "none", + "cost": 20000000, + "desc_cn": "离线效率 ×1.15(作用于衰减表全部时段;6 级满 ×2.31)" + }, + { + "id": "up_offline_6", + "name_cn": "离线收益 · 沉淀 VI", + "name_en": "Offline Income · Settling VI", + "type": "offline", + "target_gen": "none", + "unlock_hall": "hall_04_wuguan", + "effect_type": "offline_mult", + "effect_param": 1.15, + "requires": "none", + "cost": 200000000, + "desc_cn": "离线效率 ×1.15(作用于衰减表全部时段;6 级满 ×2.31)" + } + ] +} diff --git a/data/schema/balance_tuning.schema.json b/data/schema/balance_tuning.schema.json new file mode 100644 index 0000000..5a0c994 --- /dev/null +++ b/data/schema/balance_tuning.schema.json @@ -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 +} diff --git a/data/schema/boss_battles.schema.json b/data/schema/boss_battles.schema.json new file mode 100644 index 0000000..0a1d585 --- /dev/null +++ b/data/schema/boss_battles.schema.json @@ -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 +} diff --git a/data/schema/events.schema.json b/data/schema/events.schema.json new file mode 100644 index 0000000..587ce8a --- /dev/null +++ b/data/schema/events.schema.json @@ -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 +} diff --git a/data/schema/generators.schema.json b/data/schema/generators.schema.json index fc35727..8c82353 100644 --- a/data/schema/generators.schema.json +++ b/data/schema/generators.schema.json @@ -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", diff --git a/data/schema/hall_palettes.schema.json b/data/schema/hall_palettes.schema.json new file mode 100644 index 0000000..74071db --- /dev/null +++ b/data/schema/hall_palettes.schema.json @@ -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 +} diff --git a/data/schema/halls.schema.json b/data/schema/halls.schema.json new file mode 100644 index 0000000..4965d06 --- /dev/null +++ b/data/schema/halls.schema.json @@ -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 +} diff --git a/data/schema/meta_upgrades.schema.json b/data/schema/meta_upgrades.schema.json new file mode 100644 index 0000000..e514eb5 --- /dev/null +++ b/data/schema/meta_upgrades.schema.json @@ -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 +} diff --git a/data/schema/upgrades.schema.json b/data/schema/upgrades.schema.json new file mode 100644 index 0000000..afd47dc --- /dev/null +++ b/data/schema/upgrades.schema.json @@ -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__<1-6> / up_click_<1-12> / up_offline_<1-6> / up_gate_(入职文书 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 +} diff --git a/data/sheets/balance_tuning.csv b/data/sheets/balance_tuning.csv new file mode 100644 index 0000000..c089418 --- /dev/null +++ b/data/sheets/balance_tuning.csv @@ -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 数值) diff --git a/data/sheets/boss_battles.csv b/data/sheets/boss_battles.csv new file mode 100644 index 0000000..ebbb889 --- /dev/null +++ b/data/sheets/boss_battles.csv @@ -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 diff --git a/data/sheets/events.csv b/data/sheets/events.csv new file mode 100644 index 0000000..2833f6e --- /dev/null +++ b/data/sheets/events.csv @@ -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,天狗噬月,阴气最盛——动手 diff --git a/data/sheets/generators.csv b/data/sheets/generators.csv index ea5963a..e0dc36a 100644 --- a/data/sheets/generators.csv +++ b/data/sheets/generators.csv @@ -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 diff --git a/data/sheets/hall_palettes.csv b/data/sheets/hall_palettes.csv new file mode 100644 index 0000000..0b9e9c4 --- /dev/null +++ b/data/sheets/hall_palettes.csv @@ -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 diff --git a/data/sheets/halls.csv b/data/sheets/halls.csv new file mode 100644 index 0000000..e787845 --- /dev/null +++ b/data/sheets/halls.csv @@ -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 diff --git a/data/sheets/meta_upgrades.csv b/data/sheets/meta_upgrades.csv new file mode 100644 index 0000000..08e7c6a --- /dev/null +++ b/data/sheets/meta_upgrades.csv @@ -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% diff --git a/data/sheets/upgrades.csv b/data/sheets/upgrades.csv new file mode 100644 index 0000000..405c500 --- /dev/null +++ b/data/sheets/upgrades.csv @@ -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) diff --git a/project.godot b/project.godot index 7a9fcd9..3a2823d 100644 --- a/project.godot +++ b/project.godot @@ -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 diff --git a/src/core/data/balance_data.gd b/src/core/data/balance_data.gd new file mode 100644 index 0000000..2011a1e --- /dev/null +++ b/src/core/data/balance_data.gd @@ -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 diff --git a/src/core/data/boss_def.gd b/src/core/data/boss_def.gd new file mode 100644 index 0000000..10d90bf --- /dev/null +++ b/src/core/data/boss_def.gd @@ -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 diff --git a/src/core/data/event_def.gd b/src/core/data/event_def.gd new file mode 100644 index 0000000..2851f70 --- /dev/null +++ b/src/core/data/event_def.gd @@ -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 diff --git a/src/core/data/hall_def.gd b/src/core/data/hall_def.gd new file mode 100644 index 0000000..828be49 --- /dev/null +++ b/src/core/data/hall_def.gd @@ -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) diff --git a/src/core/data/palette_def.gd b/src/core/data/palette_def.gd new file mode 100644 index 0000000..155884a --- /dev/null +++ b/src/core/data/palette_def.gd @@ -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) diff --git a/src/core/economy/economy_state.gd b/src/core/economy/economy_state.gd new file mode 100644 index 0000000..be2e367 --- /dev/null +++ b/src/core/economy/economy_state.gd @@ -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, + } diff --git a/src/core/economy/generator_def.gd b/src/core/economy/generator_def.gd new file mode 100644 index 0000000..ef3e76d --- /dev/null +++ b/src/core/economy/generator_def.gd @@ -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 diff --git a/src/core/economy/multiplier_set.gd b/src/core/economy/multiplier_set.gd new file mode 100644 index 0000000..afd4e39 --- /dev/null +++ b/src/core/economy/multiplier_set.gd @@ -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 diff --git a/src/core/economy/offline_model.gd b/src/core/economy/offline_model.gd new file mode 100644 index 0000000..253ede2 --- /dev/null +++ b/src/core/economy/offline_model.gd @@ -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"]) diff --git a/src/core/economy/prestige_model.gd b/src/core/economy/prestige_model.gd new file mode 100644 index 0000000..d167420 --- /dev/null +++ b/src/core/economy/prestige_model.gd @@ -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 diff --git a/src/core/economy/upgrade_def.gd b/src/core/economy/upgrade_def.gd new file mode 100644 index 0000000..c0dabd5 --- /dev/null +++ b/src/core/economy/upgrade_def.gd @@ -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" diff --git a/src/core/numbers/big_number.gd b/src/core/numbers/big_number.gd new file mode 100644 index 0000000..b861489 --- /dev/null +++ b/src/core/numbers/big_number.gd @@ -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] diff --git a/src/core/numbers/number_format.gd b/src/core/numbers/number_format.gd new file mode 100644 index 0000000..6df5064 --- /dev/null +++ b/src/core/numbers/number_format.gd @@ -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) diff --git a/src/core/sim/gates.gd b/src/core/sim/gates.gd new file mode 100644 index 0000000..b63df64 --- /dev/null +++ b/src/core/sim/gates.gd @@ -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) diff --git a/src/core/sim/sim_main.gd b/src/core/sim/sim_main.gd new file mode 100644 index 0000000..4988aae --- /dev/null +++ b/src/core/sim/sim_main.gd @@ -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-.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] diff --git a/src/core/sim/simulator.gd b/src/core/sim/simulator.gd new file mode 100644 index 0000000..ee37ee8 --- /dev/null +++ b/src/core/sim/simulator.gd @@ -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 diff --git a/src/core/sim/strategies/greedy.gd b/src/core/sim/strategies/greedy.gd new file mode 100644 index 0000000..119c927 --- /dev/null +++ b/src/core/sim/strategies/greedy.gd @@ -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) diff --git a/src/core/sim/strategies/novice.gd b/src/core/sim/strategies/novice.gd new file mode 100644 index 0000000..2249310 --- /dev/null +++ b/src/core/sim/strategies/novice.gd @@ -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) diff --git a/src/core/sim/strategies/random.gd b/src/core/sim/strategies/random.gd new file mode 100644 index 0000000..c3dbf2b --- /dev/null +++ b/src/core/sim/strategies/random.gd @@ -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) diff --git a/src/core/sim/strategies/strategy_base.gd b/src/core/sim/strategies/strategy_base.gd new file mode 100644 index 0000000..bfd6382 --- /dev/null +++ b/src/core/sim/strategies/strategy_base.gd @@ -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 diff --git a/src/game/data_registry.gd b/src/game/data_registry.gd new file mode 100644 index 0000000..4a665dc --- /dev/null +++ b/src/game/data_registry.gd @@ -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) diff --git a/src/game/game_state.gd b/src/game/game_state.gd new file mode 100644 index 0000000..91e9a37 --- /dev/null +++ b/src/game/game_state.gd @@ -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() diff --git a/src/presentation/hud/hud_root.gd b/src/presentation/hud/hud_root.gd new file mode 100644 index 0000000..8701bff --- /dev/null +++ b/src/presentation/hud/hud_root.gd @@ -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 diff --git a/src/presentation/stage/army_band.gd b/src/presentation/stage/army_band.gd new file mode 100644 index 0000000..9758207 --- /dev/null +++ b/src/presentation/stage/army_band.gd @@ -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") diff --git a/src/presentation/stage/hall_band.gd b/src/presentation/stage/hall_band.gd new file mode 100644 index 0000000..66a65fe --- /dev/null +++ b/src/presentation/stage/hall_band.gd @@ -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 = "未解锁" diff --git a/src/presentation/stage/stage_root.gd b/src/presentation/stage/stage_root.gd new file mode 100644 index 0000000..19783e7 --- /dev/null +++ b/src/presentation/stage/stage_root.gd @@ -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 = "阴司敕封:幽冥天子可用了。" diff --git a/src/presentation/stage/stage_root.tscn b/src/presentation/stage/stage_root.tscn new file mode 100644 index 0000000..6a07ce8 --- /dev/null +++ b/src/presentation/stage/stage_root.tscn @@ -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") diff --git a/src/tools/gen_balance_sheets.py b/src/tools/gen_balance_sheets.py new file mode 100644 index 0000000..fb39a5f --- /dev/null +++ b/src/tools/gen_balance_sheets.py @@ -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()) diff --git a/tests/unit/cases/test_balance_data.gd b/tests/unit/cases/test_balance_data.gd new file mode 100644 index 0000000..21b3583 --- /dev/null +++ b/tests/unit/cases/test_balance_data.gd @@ -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) diff --git a/tests/unit/cases/test_economy_formulas.gd b/tests/unit/cases/test_economy_formulas.gd new file mode 100644 index 0000000..5c749b3 --- /dev/null +++ b/tests/unit/cases/test_economy_formulas.gd @@ -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 触红线") diff --git a/tests/unit/cases/test_economy_state.gd b/tests/unit/cases/test_economy_state.gd new file mode 100644 index 0000000..f4f7788 --- /dev/null +++ b/tests/unit/cases/test_economy_state.gd @@ -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 乘性链)") diff --git a/tests/unit/cases/test_number_format.gd b/tests/unit/cases/test_number_format.gd new file mode 100644 index 0000000..deda06b --- /dev/null +++ b/tests/unit/cases/test_number_format.gd @@ -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") diff --git a/tests/unit/test_runner.gd b/tests/unit/test_runner.gd new file mode 100644 index 0000000..6efd7ad --- /dev/null +++ b/tests/unit/test_runner.gd @@ -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)