docs(designer): 整数元组列表编辑器设计(edges/aether 去 JSON)

This commit is contained in:
2026-07-22 12:02:46 +08:00
parent 2e9b4be702
commit 0967b3f10b
@@ -0,0 +1,160 @@
# 游戏设计器 整数元组列表编辑器(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
```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: <TupleListEditor 实例>`(类型标注用 `Control` 或不标注,经 `.new()` 创建)。
- `_ready()`:把原
```gdscript
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)
```
替换为:
```gdscript
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", []))` →
```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: <TupleListEditor 实例>`。
- `_ready()`:把原
```gdscript
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)
```
替换为:
```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)` 仍在、未被改动。