Files
spellforge/docs_dev/specs/2026-07-22-designer-tuple-list-editor-design.md

7.6 KiB
Raw Permalink Blame History

游戏设计器 整数元组列表编辑器(edges / aether_thresholds 去 JSON)— 设计

  • 日期:2026-07-22
  • 状态:设计已确认(方案),待细化评审
  • 范围:编辑器工具层 addons/game_designer/游戏运行时代码与数据文件格式零改动

1. 背景与问题

游戏设计器仍有两处「可变长度小整数元组列表」用手写 JSON 文本框,易配置错、可读性差:

  • Core edges(电路边) —— core_tab.gd电路边(CIRCUITJSON) TextEdit。CIRCUIT 拓扑法杖的槽位有向连线:[{"from":0,"to":1},{"from":0,"to":2}]。当前仅 circuit_fork 使用。
  • 平衡 aether_thresholds(碎片阈值) —— balance_tab.gd碎片奖励阈值(JSON [[波次,碎片],...] TextEdit。元进度奖励表:[[1,0],[5,3],[10,8],[15,14],[20,20]] = [波次, 累计碎片]。

法术 其它(JSON)逃生舱(收纳 schema 之外的未知键,本质装任意键、无法字段化),保持不动,不在本次范围。

目标

用一个共享的行编辑器替代这两处 JSON 文本:每行 N 个 SpinBox + 🗑 删行,底部 加行。


2. 新组件:tuple_list_editor.gd

新建 addons/game_designer/tuple_list_editor.gd@tool extends VBoxContainer,可复用行编辑器。

2.1 数据模型

  • 对外数据 = Array[Array[number]](行的数组,每行一组数值)。调用方负责与自身 JSON 结构(dict / array)互转。

2.2 API

## col_specs: Array[Dictionary],每列一项:
##   {"label": String, "min": float, "max": float, "step": float, "int": bool}
##   label 可为 ""(不显示列标签);int=true 时 get_values 返回该列为整数
func setup(col_specs: Array, add_label: String = " 加行") -> void
func set_values(rows: Array) -> void      # rows: Array[Array];清空并重建
func get_values() -> Array                # 返回 Array[Array],按列 int 规格转 int

2.3 行为

  • setup:记录列规格,创建内部 _rows_box: VBoxContainer + 底部 按钮(点按追加一空行,值取各列 default 或 min)。
  • 每行:一个 HBoxContainer,含(可选列标签 Label +N 个 SpinBoxsize_flags_horizontal = SIZE_EXPAND_FILL+ 尾部 🗑 按钮。🗑 处理器:_rows_box.remove_child(row); row.queue_free()立即移出树,保证随后的 get_values 不再计入)。
  • set_values:先立即 remove_child + free 所有旧行(不用 queue_free,避免同帧残留),再逐行 _add_row
  • get_values:遍历 _rows_box 现存行,收集每行 SpinBox 值;intint(v),否则 float(v)

2.4 边界

  • 无排序/校验(YAGNI);行顺序即用户可见顺序。
  • 不做去重(edges/aether 语义允许重复行;调用方如需可自行处理,本次不做)。

3. 集成:core_tab.gdedges

  • 成员:var _edges: TextEditvar _edges: <TupleListEditor 实例>(类型标注用 Control 或不标注,经 .new() 创建)。
  • _ready():把原
    var el := Label.new(); el.text = "电路边(CIRCUITJSON)"; add_child(el)
    _edges = TextEdit.new(); _edges.custom_minimum_size = Vector2(0, 50)
    _edges.placeholder_text = '[{"from":0,"to":1},{"from":0,"to":2}]'
    add_child(_edges)
    
    替换为:
    var el := Label.new(); el.text = "电路边(CIRCUITfrom → to)"; add_child(el)
    _edges = preload("res://addons/game_designer/tuple_list_editor.gd").new()
    _edges.setup([
        {"label": "from", "min": 0, "max": 63, "step": 1, "int": true},
        {"label": "→ to", "min": 0, "max": 63, "step": 1, "int": true}], " 加边")
    add_child(_edges)
    
  • _on_select()_edges.text = JSON.stringify(d.get("edges", []))
    var erows := []
    for e in d.get("edges", []):
        erows.append([int(e.get("from", 0)), int(e.get("to", 0))])
    _edges.set_values(erows)
    
  • _on_apply():原
    var edges = JSON.parse_string(_edges.text) if _edges.text.strip_edges() != "" else []
    if not (edges is Array):
        edges = []
    
    替换为:
    var edges := []
    for r in _edges.get_values():
        edges.append({"from": r[0], "to": r[1]})
    
    (后续 _data[cid] = { ... "edges": edges, ... } 不变。)
  • 其余(拓扑/矩阵行列等)不变。max=63 为槽位上限的宽松值(当前 slot_count ≤ 8,给足冗余)。

4. 集成:balance_tab.gdaether_thresholds

  • 成员:var _aether: TextEditvar _aether: <TupleListEditor 实例>
  • _ready():把原
    var al := Label.new(); al.text = "碎片奖励阈值 aether_thresholdsJSON [[波次,碎片],...]"; add_child(al)
    _aether = TextEdit.new(); _aether.custom_minimum_size = Vector2(0, 50)
    _aether.text = JSON.stringify(_data.get("aether_thresholds", [[1, 0], [5, 3], [10, 8], [15, 14], [20, 20]]))
    add_child(_aether)
    
    替换为:
    var al := Label.new(); al.text = "碎片奖励阈值 aether_thresholds(波次 → 碎片)"; add_child(al)
    _aether = preload("res://addons/game_designer/tuple_list_editor.gd").new()
    _aether.setup([
        {"label": "波次", "min": 1, "max": 9999, "step": 1, "int": true},
        {"label": "碎片", "min": 0, "max": 9999, "step": 1, "int": true}], " 加行")
    add_child(_aether)
    _aether.set_values(_data.get("aether_thresholds", [[1, 0], [5, 3], [10, 8], [15, 14], [20, 20]]))
    
  • _save():原
    var thresholds = JSON.parse_string(_aether.text) if _aether.text.strip_edges() != "" else []
    if not (thresholds is Array):
        thresholds = []
    
    替换为:
    var thresholds := _aether.get_values()
    
    out["aether_thresholds"] = thresholds 不变。)

5. 组件边界与改动清单

文件 改动
tuple_list_editor.gd(新建) 可复用整数元组行编辑器(setup/set_values/get_values
core_tab.gd edges 文本框 → 行编辑器;dict↔array 边界转换
balance_tab.gd aether_thresholds 文本框 → 行编辑器

不动:spell 其它(JSON)、运行时脚本、数据文件格式与内容、其它 tab。


6. 验证

  1. validate_script 三文件全绿;reload_project + get_editor_errors 无报错(GDScript 类缓存问题可能需重启编辑器,同上一增量)。
  2. 回读往返(execute_editor_script):
    • core:实例化 core_tab,对 cores.json 每个 core _on_select_on_apply 组装的 edges 与原一致(circuit_fork 两条边;无 edges 的 core 结果为空数组、原本也无该键——比较时空数组 vs 缺键视为等价)。
    • balance:实例化 balance_tab_ready 后)→ _aether.get_values()balance.jsonaether_thresholds 逐元素一致。
  3. 目测:Core 页电路边为 from/to 行 + 加/删;平衡页碎片阈值为 波次/碎片 行 + 加/删(重启编辑器后由用户确认)。

7. 范围外(YAGNI

  • 不动 spell 其它(JSON)(逃生舱)。
  • 行编辑器不做排序/去重/跨行校验。
  • 不改数据文件内容(现有 edges/aether 原样回读)。

8. 验收标准

  1. Core 页「电路边」为 from→to 的 SpinBox 行,可加/删;circuit_fork 正确显示两条边 (0→1, 0→2),应用保存后 cores.jsonedges 不变。
  2. 平衡页「碎片阈值」为 波次/碎片 的 SpinBox 行,可加/删;显示 5 行,保存后 balance.jsonaether_thresholds 不变。
  3. validate_script 无报错;core 与 balance 回读往返一致。
  4. spell 其它(JSON) 仍在、未被改动。