feat: W2 经济核心+数据管线+灰盒 — Balance Bible v1.0 八表逐值落地,EconomyState/sim 14 门/灰盒主场景/单测(GDScript 待本机引擎验证)
This commit is contained in:
1 parent
856326a766
commit
24e5b0c4cc
58 files changed
+7099
-22
No files matched your search
@@ -0,0 +1,336 @@
|
||||
class_name BalanceData
|
||||
extends RefCounted
|
||||
|
||||
# ============================================================================
|
||||
# BalanceData —— data/build/*.json 载入器(架构 §3.1 管线第三段)
|
||||
#
|
||||
# 单向数据管线的运行时入口:
|
||||
# data/sheets/*.csv ──SheetImporter──► data/build/*.json ──本类──► def 对象
|
||||
# sim(core/)与游戏(game/DataRegistry)读同一份文件 = 零漂移(架构 §3.1 铁律②)
|
||||
#
|
||||
# 纪律:
|
||||
# - core 层:无 Node/无场景树依赖;sim 与单测可无头构造(架构 §2.2)
|
||||
# - 只读:游戏永不写 data/(铁律①)
|
||||
# - 载入即做跨表引用完整性检查(check_references),违规进 load_errors
|
||||
#
|
||||
# 知识缺口:4.7.x 超出训练数据——只用 4.x 稳定 API(FileAccess/JSON/DirAccess)。
|
||||
# ============================================================================
|
||||
|
||||
const SHEET_FILES: Array[String] = [
|
||||
"generators", "upgrades", "halls", "hall_palettes",
|
||||
"boss_battles", "events", "meta_upgrades", "balance_tuning",
|
||||
]
|
||||
|
||||
var generators: Array[GeneratorDef] = []
|
||||
var upgrades: Array[UpgradeDef] = [] # 108 条非 meta 升级
|
||||
var meta_upgrades: Array[UpgradeDef] = [] # 12 条宿慧商店(type=meta)
|
||||
var halls: Array[HallDef] = []
|
||||
var palettes: Array[PaletteDef] = []
|
||||
var bosses: Array[BossDef] = []
|
||||
var events: Array[EventDef] = []
|
||||
|
||||
var load_errors: Array[String] = []
|
||||
|
||||
var _gen_by_id := {}
|
||||
var _up_by_id := {}
|
||||
var _meta_by_id := {}
|
||||
var _hall_by_id := {}
|
||||
var _hall_by_no := {}
|
||||
var _boss_by_hall := {}
|
||||
var _palette_by_hall := {}
|
||||
var _event_by_id := {}
|
||||
var _tune := {} # id -> {"kind": String, "num": float, "text": String}
|
||||
|
||||
|
||||
## 从目录载入全部表(缺表 = load_errors,不静默)
|
||||
static func load_from_dir(dir: String) -> BalanceData:
|
||||
var bd := BalanceData.new()
|
||||
for sheet in SHEET_FILES:
|
||||
var path := "%s/%s.json" % [dir, sheet]
|
||||
if not FileAccess.file_exists(path):
|
||||
bd.load_errors.append("缺少 build 产物:%s(先运行 SheetImporter import_all)" % path)
|
||||
continue
|
||||
var text := FileAccess.get_file_as_string(path)
|
||||
var doc: Variant = JSON.parse_string(text)
|
||||
if not (doc is Dictionary):
|
||||
bd.load_errors.append("JSON 解析失败:%s" % path)
|
||||
continue
|
||||
var rows: Variant = doc.get("rows", [])
|
||||
if not (rows is Array):
|
||||
bd.load_errors.append("%s:缺少 rows 数组" % path)
|
||||
continue
|
||||
match sheet:
|
||||
"generators": bd._load_generators(rows)
|
||||
"upgrades": bd._load_upgrades(rows)
|
||||
"halls": bd._load_halls(rows)
|
||||
"hall_palettes": bd._load_palettes(rows)
|
||||
"boss_battles": bd._load_bosses(rows)
|
||||
"events": bd._load_events(rows)
|
||||
"meta_upgrades": bd._load_meta(rows)
|
||||
"balance_tuning": bd._load_tuning(rows)
|
||||
bd._build_indexes()
|
||||
bd.load_errors.append_array(bd.check_references())
|
||||
return bd
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# 各表载入(字段名与 CSV 列同名;JSON 数字按 int/float 到达,统一 float() 收敛)
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
func _load_generators(rows: Array) -> void:
|
||||
for r in rows:
|
||||
var d := GeneratorDef.new()
|
||||
d.id = str(r.get("id", ""))
|
||||
d.display_cn = str(r.get("display_cn", ""))
|
||||
d.display_en = str(r.get("display_en", ""))
|
||||
d.base_cost = float(r.get("base_cost", 0))
|
||||
d.cost_ratio = float(r.get("cost_ratio", 1))
|
||||
d.base_rate = float(r.get("base_rate", 0))
|
||||
d.unlock_hall = str(r.get("unlock_hall", "none"))
|
||||
d.unlock_gate = str(r.get("unlock_gate", "none"))
|
||||
d.sprite_row = str(r.get("sprite_row", ""))
|
||||
generators.append(d)
|
||||
|
||||
|
||||
func _load_upgrades(rows: Array) -> void:
|
||||
for r in rows:
|
||||
var d := UpgradeDef.new()
|
||||
d.id = str(r.get("id", ""))
|
||||
d.name_cn = str(r.get("name_cn", ""))
|
||||
d.name_en = str(r.get("name_en", ""))
|
||||
d.type = str(r.get("type", ""))
|
||||
d.target_gen = str(r.get("target_gen", "none"))
|
||||
d.unlock_hall = str(r.get("unlock_hall", "none"))
|
||||
d.effect_type = str(r.get("effect_type", ""))
|
||||
d.effect_param = float(r.get("effect_param", 0))
|
||||
d.requires = str(r.get("requires", "none"))
|
||||
d.cost = int(r.get("cost", 0))
|
||||
d.desc_cn = str(r.get("desc_cn", ""))
|
||||
upgrades.append(d)
|
||||
|
||||
|
||||
func _load_halls(rows: Array) -> void:
|
||||
for r in rows:
|
||||
var d := HallDef.new()
|
||||
d.id = str(r.get("id", ""))
|
||||
d.no = int(r.get("no", 0))
|
||||
d.king_cn = str(r.get("king_cn", ""))
|
||||
d.king_en = str(r.get("king_en", ""))
|
||||
d.motif_cn = str(r.get("motif_cn", ""))
|
||||
d.motif_en = str(r.get("motif_en", ""))
|
||||
d.target_min = float(r.get("target_min", 0))
|
||||
d.gate_merit = float(r.get("gate_merit", 0))
|
||||
d.unlock_gen = str(r.get("unlock_gen", "none"))
|
||||
d.unlock_gate = str(r.get("unlock_gate", "none"))
|
||||
d.palette_id = str(r.get("palette_id", ""))
|
||||
d.is_prestige_gate = bool(r.get("is_prestige_gate", false))
|
||||
halls.append(d)
|
||||
|
||||
|
||||
func _load_palettes(rows: Array) -> void:
|
||||
for r in rows:
|
||||
var d := PaletteDef.new()
|
||||
d.id = str(r.get("id", ""))
|
||||
d.hall_id = str(r.get("hall_id", ""))
|
||||
d.name_cn = str(r.get("name_cn", ""))
|
||||
d.ink_hex = str(r.get("ink_hex", "#1A1A1A"))
|
||||
d.bone_hex = str(r.get("bone_hex", "#F2EDE4"))
|
||||
d.cinnabar_hex = str(r.get("cinnabar_hex", "#B4402C"))
|
||||
d.indigo_hex = str(r.get("indigo_hex", "#2C4A6E"))
|
||||
d.accent_hex = str(r.get("accent_hex", "#1A1A1A"))
|
||||
d.accent_name_cn = str(r.get("accent_name_cn", ""))
|
||||
d.flow = str(r.get("flow", "seep_down"))
|
||||
d.lut_index = int(r.get("lut_index", 0))
|
||||
d.ink_pct = int(r.get("ink_pct", 65))
|
||||
d.bone_pct = int(r.get("bone_pct", 25))
|
||||
d.cinnabar_max_pct = int(r.get("cinnabar_max_pct", 8))
|
||||
d.indigo_max_pct = int(r.get("indigo_max_pct", 7))
|
||||
palettes.append(d)
|
||||
|
||||
|
||||
func _load_bosses(rows: Array) -> void:
|
||||
for r in rows:
|
||||
var d := BossDef.new()
|
||||
d.id = str(r.get("id", ""))
|
||||
d.hall_id = str(r.get("hall_id", ""))
|
||||
d.hall_factor = float(r.get("hall_factor", 1))
|
||||
d.hp_factor = float(r.get("hp_factor", 15))
|
||||
d.army_dps_factor = float(r.get("army_dps_factor", 1))
|
||||
d.intro_sec = float(r.get("intro_sec", 3))
|
||||
d.intro_dmg_souls_factor = float(r.get("intro_dmg_souls_factor", 0.5))
|
||||
d.intro_dmg_cap_ratio = float(r.get("intro_dmg_cap_ratio", 0.2))
|
||||
d.active_sec = float(r.get("active_sec", 30))
|
||||
d.combo_window_sec = float(r.get("combo_window_sec", 0.6))
|
||||
d.combo_step = float(r.get("combo_step", 0.02))
|
||||
d.combo_cap = float(r.get("combo_cap", 0.6))
|
||||
d.retry_cooldown_sec = float(r.get("retry_cooldown_sec", 60))
|
||||
d.reward_hp_ratio = float(r.get("reward_hp_ratio", 0.2))
|
||||
bosses.append(d)
|
||||
|
||||
|
||||
func _load_events(rows: Array) -> void:
|
||||
for r in rows:
|
||||
var d := EventDef.new()
|
||||
d.id = str(r.get("id", ""))
|
||||
d.name_cn = str(r.get("name_cn", ""))
|
||||
d.name_en = str(r.get("name_en", ""))
|
||||
d.trigger_min_min = float(r.get("trigger_min_min", 1))
|
||||
d.trigger_max_min = float(r.get("trigger_max_min", 1))
|
||||
d.duration_sec = float(r.get("duration_sec", 0))
|
||||
d.type = str(r.get("type", ""))
|
||||
d.p_mult = float(r.get("p_mult", 0))
|
||||
d.p_clicks = float(r.get("p_clicks", 0))
|
||||
d.p_window_sec = float(r.get("p_window_sec", 0))
|
||||
d.p_penalty = float(r.get("p_penalty", 0))
|
||||
d.p_penalty_dur_sec = float(r.get("p_penalty_dur_sec", 0))
|
||||
d.opt_a_type = str(r.get("opt_a_type", "none"))
|
||||
d.opt_a_param = float(r.get("opt_a_param", 0))
|
||||
d.opt_b_type = str(r.get("opt_b_type", "none"))
|
||||
d.opt_b_param = float(r.get("opt_b_param", 0))
|
||||
d.flavor_cn = str(r.get("flavor_cn", ""))
|
||||
events.append(d)
|
||||
|
||||
|
||||
func _load_meta(rows: Array) -> void:
|
||||
for r in rows:
|
||||
var d := UpgradeDef.new()
|
||||
d.id = str(r.get("id", ""))
|
||||
d.name_cn = str(r.get("name_cn", ""))
|
||||
d.name_en = str(r.get("name_en", ""))
|
||||
d.type = "meta"
|
||||
d.target_gen = str(r.get("target_id", ""))
|
||||
d.unlock_hall = "none"
|
||||
d.effect_type = str(r.get("effect_type", ""))
|
||||
d.effect_param = float(r.get("effect_param", 0))
|
||||
d.requires = str(r.get("requires", "none"))
|
||||
d.cost = int(r.get("cost_suihui", 0))
|
||||
d.cost_is_suihui = true
|
||||
d.max_rank = int(r.get("max_rank", 1))
|
||||
d.condition = str(r.get("condition", "none"))
|
||||
d.desc_cn = str(r.get("desc_cn", ""))
|
||||
meta_upgrades.append(d)
|
||||
|
||||
|
||||
func _load_tuning(rows: Array) -> void:
|
||||
for r in rows:
|
||||
var id := str(r.get("id", ""))
|
||||
if id == "":
|
||||
continue
|
||||
_tune[id] = {
|
||||
"kind": str(r.get("kind", "number")),
|
||||
"num": float(r.get("num", 0)),
|
||||
"text": str(r.get("text", "")),
|
||||
}
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# 索引与查询
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
func _build_indexes() -> void:
|
||||
for d in generators:
|
||||
_gen_by_id[d.id] = d
|
||||
for d in upgrades:
|
||||
_up_by_id[d.id] = d
|
||||
for d in meta_upgrades:
|
||||
_meta_by_id[d.id] = d
|
||||
for d in halls:
|
||||
_hall_by_id[d.id] = d
|
||||
_hall_by_no[d.no] = d
|
||||
for d in bosses:
|
||||
_boss_by_hall[d.hall_id] = d
|
||||
for d in palettes:
|
||||
_palette_by_hall[d.hall_id] = d
|
||||
for d in events:
|
||||
_event_by_id[d.id] = d
|
||||
|
||||
|
||||
func generator(id: String) -> GeneratorDef:
|
||||
return _gen_by_id.get(id)
|
||||
|
||||
|
||||
func upgrade(id: String) -> UpgradeDef:
|
||||
return _up_by_id.get(id)
|
||||
|
||||
|
||||
func meta(id: String) -> UpgradeDef:
|
||||
return _meta_by_id.get(id)
|
||||
|
||||
|
||||
func hall(id_or_no: Variant) -> HallDef:
|
||||
if id_or_no is int:
|
||||
return _hall_by_no.get(id_or_no)
|
||||
return _hall_by_id.get(str(id_or_no))
|
||||
|
||||
|
||||
func hall_by_no(no: int) -> HallDef:
|
||||
return _hall_by_no.get(no)
|
||||
|
||||
|
||||
func boss_for_hall(hall_id: String) -> BossDef:
|
||||
return _boss_by_hall.get(hall_id)
|
||||
|
||||
|
||||
func palette_for_hall(hall_id: String) -> PaletteDef:
|
||||
return _palette_by_hall.get(hall_id)
|
||||
|
||||
|
||||
func event(id: String) -> EventDef:
|
||||
return _event_by_id.get(id)
|
||||
|
||||
|
||||
## 全局调参数(balance_tuning.csv);缺失返回 default 并记 load_errors
|
||||
func tune_num(key: String, default_value: float) -> float:
|
||||
var row: Variant = _tune.get(key)
|
||||
if row == null:
|
||||
load_errors.append("tune 缺失:%s(用默认 %s)" % [key, str(default_value)])
|
||||
return default_value
|
||||
return float(row.num)
|
||||
|
||||
|
||||
func tune_text(key: String, default_value: String) -> String:
|
||||
var row: Variant = _tune.get(key)
|
||||
if row == null:
|
||||
load_errors.append("tune 缺失:%s(用默认 %s)" % [key, default_value])
|
||||
return default_value
|
||||
return str(row.text)
|
||||
|
||||
|
||||
func has_tune(key: String) -> bool:
|
||||
return _tune.has(key)
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# 跨表引用完整性(schema 管单表;跨表在这里查)
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
func check_references() -> Array[String]:
|
||||
var errs: Array[String] = []
|
||||
for g in generators:
|
||||
if g.unlock_hall != "none" and _hall_by_id.get(g.unlock_hall) == null:
|
||||
errs.append("generators.%s.unlock_hall 引用了不存在的殿:%s" % [g.id, g.unlock_hall])
|
||||
if g.unlock_gate != "none" and _up_by_id.get(g.unlock_gate) == null:
|
||||
errs.append("generators.%s.unlock_gate 引用了不存在的路引:%s" % [g.id, g.unlock_gate])
|
||||
for u in upgrades:
|
||||
if u.target_gen != "none" and _gen_by_id.get(u.target_gen) == null:
|
||||
errs.append("upgrades.%s.target_gen 引用了不存在的班底:%s" % [u.id, u.target_gen])
|
||||
if u.unlock_hall != "none" and _hall_by_id.get(u.unlock_hall) == null:
|
||||
errs.append("upgrades.%s.unlock_hall 引用了不存在的殿:%s" % [u.id, u.unlock_hall])
|
||||
for h in halls:
|
||||
if h.unlock_gen != "none" and _gen_by_id.get(h.unlock_gen) == null:
|
||||
errs.append("halls.%s.unlock_gen 引用了不存在的班底:%s" % [h.id, h.unlock_gen])
|
||||
if h.unlock_gate != "none" and _up_by_id.get(h.unlock_gate) == null:
|
||||
errs.append("halls.%s.unlock_gate 引用了不存在的路引:%s" % [h.id, h.unlock_gate])
|
||||
if _palette_by_hall.get(h.id) == null:
|
||||
errs.append("halls.%s 缺少色板行(hall_palettes.csv)" % h.id)
|
||||
for b in bosses:
|
||||
if _hall_by_id.get(b.hall_id) == null:
|
||||
errs.append("boss_battles.%s.hall_id 引用了不存在的殿:%s" % [b.id, b.hall_id])
|
||||
for m in meta_upgrades:
|
||||
if m.target_gen != "" and _gen_by_id.get(m.target_gen) == null:
|
||||
errs.append("meta_upgrades.%s.target_id 引用了不存在的班底:%s" % [m.id, m.target_gen])
|
||||
# 每殿恰一场殿王战
|
||||
for h in halls:
|
||||
if _boss_by_hall.get(h.id) == null:
|
||||
errs.append("殿 %s 缺少 boss_battles 行" % h.id)
|
||||
return errs
|
||||
Reference in new issue
Block a user