Files
game-code-4/src/tools/gen_balance_sheets.py
T

646 lines
39 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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())