## 属性编辑器标签 — 玩家属性的 base / soft / hard / combine ## 数据:res://data/attributes.json(权威属性定义见 docs/design/numerical_design.md §1.1) @tool extends VBoxContainer const UI = preload("res://addons/game_designer/designer_ui.gd") const PATH = "res://data/attributes.json" # 显示与存储解耦:LABELS 仅供下拉显示,KEYS 才写进 JSON,二者 index 对齐 const COMBINE_KEYS = ["hybrid", "inverse", "add_int"] const COMBINE_LABELS = ["混合 hybrid", "反向 inverse", "整数加 add_int"] 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 func _ready() -> void: add_child(UI.header("📊 属性编辑器 — 玩家属性 base / soft / hard / combine")) _load() var grid := GridContainer.new(); grid.columns = 5; add_child(grid) for h in ["属性", "基准 base", "软上限 soft", "硬上限 hard", "合成 combine"]: 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: 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"))) 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.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) func _load() -> void: var d = UI.load_json(PATH) _data = d if d is Dictionary else {} ## SpinBox 按 step 吸附会引入双精度噪声(0.01 → 0.00999999999476), ## 直接写回会把噪声固化进 JSON;抹到 1e-6 精度,远细于本页任何有意义的调参粒度。 static func _clean(v: float) -> float: return snappedf(v, 0.000001) ## 表单 → 字典(合并式:以 _data 为基底保留未知键) func _collect() -> Dictionary: var out: Dictionary = _data.duplicate(true) for attr_id in ATTR_ORDER: var r: Dictionary = _rows.get(attr_id, {}) if r.is_empty(): continue var entry: Dictionary = out.get(attr_id, {}).duplicate(true) 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,不存双语显示串 out[attr_id] = entry return out func _save() -> void: if UI.save_json(PATH, _collect()): UI.set_status(_status, "💾 已保存(重启 F5 生效)") else: UI.set_status(_status, "✗ 保存失败", true)