diff --git a/addons/game_designer/attribute_tab.gd b/addons/game_designer/attribute_tab.gd index 3d511d9..9d3e98d 100644 --- a/addons/game_designer/attribute_tab.gd +++ b/addons/game_designer/attribute_tab.gd @@ -13,6 +13,7 @@ const ATTR_ORDER = ["cpu_limit", "move_speed", "cast_delay_mod", "hp_max"] var _data: Dictionary = {} var _rows: Dictionary = {} # attr_id → {"base": SpinBox, "soft": SpinBox, "hard": SpinBox, "combine": OptionButton} var _status: Label +var _loaded: bool = false # 加载失败时禁止写回,见 _save func _ready() -> void: add_child(UI.header("📊 属性编辑器 — 玩家属性 base / soft / hard / combine")) @@ -22,29 +23,54 @@ func _ready() -> void: var l := Label.new(); l.text = h; l.modulate = Color(0.7, 0.8, 1.0) grid.add_child(l) for attr_id in ATTR_ORDER: + # _load 已保证:_loaded 时每个条目都是 Dictionary,否则 _data 为空 var d: Dictionary = _data.get(attr_id, {}) grid.add_child(UI.cell_label(String(d.get("display_name", attr_id)))) - var sp_base := UI.spin(-99999.0, 99999.0, 0.01, float(d.get("base", 0.0))) - var sp_soft := UI.spin(-99999.0, 99999.0, 0.01, float(d.get("soft", 0.0))) - var sp_hard := UI.spin(-99999.0, 99999.0, 0.01, float(d.get("hard", 0.0))) - var idx: int = COMBINE_KEYS.find(String(d.get("combine", "hybrid"))) + # 四个属性按定义均非负,故 min = 0;且 min 取 0 可避开 Range 的抵消误差(见 _clean) + var sp_base := UI.spin(0.0, 99999.0, 0.01, float(d.get("base", 0.0))) + var sp_soft := UI.spin(0.0, 99999.0, 0.01, float(d.get("soft", 0.0))) + var sp_hard := UI.spin(0.0, 99999.0, 0.01, float(d.get("hard", 0.0))) + var combine_key := String(d.get("combine", "hybrid")) + var idx: int = COMBINE_KEYS.find(combine_key) + if idx < 0 and not d.is_empty(): + # 不硬拒(打字错误不该让整页打不开),但必须留下诊断:保存会把它改写成 hybrid + push_warning("attribute_tab: 「%s」的 combine「%s」无法识别,下拉已回落到 hybrid,保存将覆盖原值" % [attr_id, combine_key]) var op := UI.opt(COMBINE_LABELS, idx if idx >= 0 else 0) grid.add_child(sp_base); grid.add_child(sp_soft); grid.add_child(sp_hard); grid.add_child(op) _rows[attr_id] = {"base": sp_base, "soft": sp_soft, "hard": sp_hard, "combine": op} add_child(HSeparator.new()) var tip := Label.new() - tip.text = "hard=0 在 hybrid 下表示不钳制;inverse 的 hard 是「下限」,填 0 即钳到 0(与 hybrid 相反);add_int 拒绝 pct 加成。" + tip.text = "hard=0 在 hybrid / add_int 下表示不钳制;inverse 的 hard 是「下限」,填 0 即钳到 0(与 hybrid 相反);add_int 拒绝 pct 加成;soft 本期不参与计算(留给货架 C)。" + # dock 的 ScrollContainer 禁用横向滚动,不换行的话这句会被右侧裁掉(正是最需要看到的半句) + tip.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART tip.modulate = Color(0.7, 0.7, 0.7) add_child(tip) add_child(UI.btn("💾 保存 attributes.json", _save)) _status = UI.status_label(); add_child(_status) +## 加载失败必须留痕并禁止写回:本页的零值载荷(move_speed.base = 0 等)是合法 JSON, +## PlayerStats 会照单全收,游戏表现为玩家不能移动 / 瞬间死亡且毫无诊断。 func _load() -> void: var d = UI.load_json(PATH) - _data = d if d is Dictionary else {} + _loaded = d is Dictionary + if not _loaded: + push_error("attribute_tab: 无法加载 %s(缺失或格式错误)" % PATH) + _data = {} + return + # 逐条守卫,同 player_stats.gd 的既有做法:畸形条目(如 "hp_max": 5)会让下面的 + # Dictionary 赋值硬崩、dock 停在半构建状态;整个文件拒绝加载并禁止写回 + for k in d: + if not (d[k] is Dictionary): + push_error("attribute_tab: %s 的「%s」应为对象,整个文件已拒绝加载" % [PATH, k]) + _loaded = false + _data = {} + return + _data = d -## SpinBox 按 step 吸附会引入双精度噪声(0.01 → 0.00999999999476), -## 直接写回会把噪声固化进 JSON;抹到 1e-6 精度,远细于本页任何有意义的调参粒度。 +## Range 用 round((v - min) / step) * step + min 吸附取值。min = -99999 与 0.01 的 step +## 相差七个数量级,此式在此发生抵消误差(0.01 → 0.00999999999476),直接写回会把噪声 +## 固化进 JSON。本页已把 min 收紧到 0.0,当前 12 个值全部逐位精确、并不依赖本函数; +## 保留它是给将来真需要宽区间(含负值)的属性的廉价保险。 static func _clean(v: float) -> float: return snappedf(v, 0.000001) @@ -59,11 +85,15 @@ func _collect() -> Dictionary: entry["base"] = _clean(r["base"].value) entry["soft"] = _clean(r["soft"].value) entry["hard"] = _clean(r["hard"].value) - entry["combine"] = COMBINE_KEYS[r["combine"].selected] # 存 key,不存双语显示串 + # 存 key,不存双语显示串;selected 恒为 0..2(UI.opt 构造时已 clampi,本页此后不再赋值) + entry["combine"] = COMBINE_KEYS[r["combine"].selected] out[attr_id] = entry return out func _save() -> void: + if not _loaded: + UI.set_status(_status, "✗ 定义未加载,拒绝写回", true) + return if UI.save_json(PATH, _collect()): UI.set_status(_status, "💾 已保存(重启 F5 生效)") else: