#!/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())