646 lines
39 KiB
Python
646 lines
39 KiB
Python
#!/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())
|