# 游戏设计器 整数元组列表编辑器(edges / aether_thresholds 去 JSON)— 设计 - 日期:2026-07-22 - 状态:设计已确认(方案),待细化评审 - 范围:编辑器工具层 `addons/game_designer/`,**游戏运行时代码与数据文件格式零改动** --- ## 1. 背景与问题 游戏设计器仍有两处「可变长度小整数元组列表」用手写 JSON 文本框,易配置错、可读性差: - **Core `edges`(电路边)** —— `core_tab.gd` 的 `电路边(CIRCUIT,JSON)` 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 ```gdscript ## 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 个 SpinBox(`size_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 值;`int` 列 `int(v)`,否则 `float(v)`。 ### 2.4 边界 - 无排序/校验(YAGNI);行顺序即用户可见顺序。 - 不做去重(edges/aether 语义允许重复行;调用方如需可自行处理,本次不做)。 --- ## 3. 集成:`core_tab.gd`(edges) - 成员:`var _edges: TextEdit` → `var _edges: `(类型标注用 `Control` 或不标注,经 `.new()` 创建)。 - `_ready()`:把原 ```gdscript var el := Label.new(); el.text = "电路边(CIRCUIT,JSON)"; 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) ``` 替换为: ```gdscript var el := Label.new(); el.text = "电路边(CIRCUIT,from → 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", []))` → ```gdscript 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()`:原 ```gdscript var edges = JSON.parse_string(_edges.text) if _edges.text.strip_edges() != "" else [] if not (edges is Array): edges = [] ``` 替换为: ```gdscript 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.gd`(aether_thresholds) - 成员:`var _aether: TextEdit` → `var _aether: `。 - `_ready()`:把原 ```gdscript var al := Label.new(); al.text = "碎片奖励阈值 aether_thresholds(JSON [[波次,碎片],...])"; 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) ``` 替换为: ```gdscript 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()`:原 ```gdscript var thresholds = JSON.parse_string(_aether.text) if _aether.text.strip_edges() != "" else [] if not (thresholds is Array): thresholds = [] ``` 替换为: ```gdscript 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.json` 的 `aether_thresholds` 逐元素一致。 3. 目测:Core 页电路边为 from/to 行 + 加/删;平衡页碎片阈值为 波次/碎片 行 + 加/删(重启编辑器后由用户确认)。 ## 7. 范围外(YAGNI) - 不动 spell `其它(JSON)`(逃生舱)。 - 行编辑器不做排序/去重/跨行校验。 - 不改数据文件内容(现有 edges/aether 原样回读)。 ## 8. 验收标准 1. Core 页「电路边」为 from→to 的 SpinBox 行,可加/删;`circuit_fork` 正确显示两条边 (0→1, 0→2),应用保存后 `cores.json` 的 `edges` 不变。 2. 平衡页「碎片阈值」为 波次/碎片 的 SpinBox 行,可加/删;显示 5 行,保存后 `balance.json` 的 `aether_thresholds` 不变。 3. `validate_script` 无报错;core 与 balance 回读往返一致。 4. spell `其它(JSON)` 仍在、未被改动。