# 整数元组列表编辑器（edges / aether_thresholds 去 JSON）实现计划

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** 新增可复用行编辑器 `tuple_list_editor.gd`，把 Core `edges`（电路边）与平衡 `aether_thresholds`（碎片阈值）从手写 JSON 改为 SpinBox 行编辑；spell `其它(JSON)` 不动。

**Architecture:** 一个 `@tool VBoxContainer` 组件（`setup`/`set_values`/`get_values`），数据模型 `Array[Array[int]]`；core_tab 在 `{from,to}` 字典与 `[from,to]` 数组间转换，balance_tab 直接用 `[波次,碎片]` 数组。运行时代码/数据格式零改动。

**Tech Stack:** Godot 4.6 (Mono) / GDScript `@tool`；验证用 godot-mcp-pro（`validate_script` / `execute_editor_script` / `reload_project`）。

**Spec:** `docs_dev/specs/2026-07-22-designer-tuple-list-editor-design.md`

**验证说明：** 沿用既有模式——每个改动文件 `validate_script` 校验；最后 `execute_editor_script` 对 `cores.json`/`balance.json` 回读往返（Task 4）。⚠️ core_tab/balance_tab 当前在编辑器 dock 里以旧编译类驻留，回读前需**重启编辑器**清 GDScript 类缓存（同上一增量，需用户确认）。

---

### Task 1: 新建 `tuple_list_editor.gd`

**Files:**
- Create: `addons/game_designer/tuple_list_editor.gd`

- [ ] **Step 1: 写入完整脚本**

创建 `addons/game_designer/tuple_list_editor.gd`（保留 TAB 缩进）：

```gdscript
## 整数元组列表编辑器（@tool）— 每行 N 个 SpinBox + 🗑 删行，底部 ➕ 加行。
## 用于 Core edges（from/to）、balance aether_thresholds（波次/碎片）等可变长小整数元组列表。
## 数据模型：Array[Array]（行数组）；调用方负责与自身 JSON 结构互转。
@tool
extends VBoxContainer

var _cols: Array = []            # [{label,min,max,step,int,default}]
var _rows_box: VBoxContainer
var _add_label: String = "➕ 加行"

func setup(col_specs: Array, add_label: String = "➕ 加行") -> void:
	_cols = col_specs
	_add_label = add_label
	_rows_box = VBoxContainer.new()
	add_child(_rows_box)
	var add_btn := Button.new()
	add_btn.text = _add_label
	add_btn.pressed.connect(_on_add)
	add_child(add_btn)

func set_values(rows: Array) -> void:
	if _rows_box == null:
		return
	for c in _rows_box.get_children():
		_rows_box.remove_child(c)
		c.free()
	for r in rows:
		_add_row(r)

func get_values() -> Array:
	var out: Array = []
	if _rows_box == null:
		return out
	for h in _rows_box.get_children():
		var row: Array = []
		var ci := 0
		for ch in h.get_children():
			if ch is SpinBox:
				var v: float = (ch as SpinBox).value
				var is_int: bool = bool(_cols[ci].get("int", true)) if ci < _cols.size() else true
				row.append(int(v) if is_int else v)
				ci += 1
		out.append(row)
	return out

func _on_add() -> void:
	_add_row([])

func _add_row(vals: Array) -> void:
	var h := HBoxContainer.new()
	for i in _cols.size():
		var c: Dictionary = _cols[i]
		var lbl := String(c.get("label", ""))
		if lbl != "":
			var l := Label.new(); l.text = lbl
			h.add_child(l)
		var sp := SpinBox.new()
		sp.min_value = c.get("min", 0)
		sp.max_value = c.get("max", 100000)
		sp.step = c.get("step", 1)
		sp.value = float(vals[i]) if i < vals.size() else float(c.get("default", c.get("min", 0)))
		sp.size_flags_horizontal = Control.SIZE_EXPAND_FILL
		h.add_child(sp)
	var del := Button.new()
	del.text = "🗑"
	del.pressed.connect(func(): _rows_box.remove_child(h); h.queue_free())
	h.add_child(del)
	_rows_box.add_child(h)
```

- [ ] **Step 2: 校验**

调用 `mcp__godot-mcp-pro__validate_script`，`path = "res://addons/game_designer/tuple_list_editor.gd"`。Expected: 通过。

- [ ] **Step 3: 提交**

```bash
git add addons/game_designer/tuple_list_editor.gd
git commit -m "feat(designer): 新增整数元组列表行编辑器组件"
```

---

### Task 2: `core_tab.gd` — edges 改行编辑器

**Files:**
- Modify: `addons/game_designer/core_tab.gd`

（4 处 Edit，精确匹配，保留 TAB 缩进。）

- [ ] **Step 1: 成员变量类型（改为不标注，动态派发 setup/get_values）**

old:
```gdscript
var _edges: TextEdit
```
new:
```gdscript
var _edges
```

- [ ] **Step 2: `_ready()` —— edges 文本框换行编辑器**

old:
```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)
```
new:
```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)
```

- [ ] **Step 3: `_on_select()` —— 载入边**

old:
```gdscript
	_edges.text = JSON.stringify(d.get("edges", []))
```
new:
```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)
```

- [ ] **Step 4: `_on_apply()` —— 组装边**

old:
```gdscript
	var edges = JSON.parse_string(_edges.text) if _edges.text.strip_edges() != "" else []
	if not (edges is Array):
		edges = []
```
new:
```gdscript
	var edges := []
	for r in _edges.get_values():
		edges.append({"from": r[0], "to": r[1]})
```

- [ ] **Step 5: 校验**

`mcp__godot-mcp-pro__validate_script`，`path = "res://addons/game_designer/core_tab.gd"`。Expected: 通过。

- [ ] **Step 6: 提交**

```bash
git add addons/game_designer/core_tab.gd
git commit -m "feat(designer): Core 电路边改行编辑器（去 JSON）"
```

---

### Task 3: `balance_tab.gd` — aether_thresholds 改行编辑器

**Files:**
- Modify: `addons/game_designer/balance_tab.gd`

- [ ] **Step 1: 成员变量类型（改为不标注，动态派发 setup/get_values）**

old:
```gdscript
var _aether: TextEdit
```
new:
```gdscript
var _aether
```

（注意：`_aether` 成员声明在文件顶部成员区，无前导缩进；`balance_tab.gd` 的 `var _aether: TextEdit` 在第 15 行左右，顶格。）

- [ ] **Step 2: `_ready()` —— aether 文本框换行编辑器**

old:
```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)
```
new:
```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]]))
```

- [ ] **Step 3: `_save()` —— 读取阈值**

old:
```gdscript
	var thresholds = JSON.parse_string(_aether.text) if _aether.text.strip_edges() != "" else []
	if not (thresholds is Array):
		thresholds = []
```
new:
```gdscript
	var thresholds := _aether.get_values()
```

- [ ] **Step 4: 校验**

`mcp__godot-mcp-pro__validate_script`，`path = "res://addons/game_designer/balance_tab.gd"`。Expected: 通过。

- [ ] **Step 5: 提交**

```bash
git add addons/game_designer/balance_tab.gd
git commit -m "feat(designer): 平衡碎片阈值改行编辑器（去 JSON）"
```

---

### Task 4: 全量验证（重启 + 回读往返）

**Files:**
- 无（MCP 验证）

- [ ] **Step 1: 重启编辑器清类缓存**

core_tab/balance_tab 在 dock 中以旧类驻留，需重启。**先征得用户同意**，然后调用 `mcp__godot-mcp-pro__execute_editor_script`，`code = "EditorInterface.restart_editor(true)"`（连接会断开，等待 ~20s 重连）。

- [ ] **Step 2: 重连后查错**

`mcp__godot-mcp-pro__reload_project`（如需）→ `mcp__godot-mcp-pro__get_editor_errors`（max_lines 40）。Expected: 无与本次相关报错。

- [ ] **Step 3: Core edges 回读往返**

`mcp__godot-mcp-pro__execute_editor_script`，`code`：
```gdscript
var tab = load("res://addons/game_designer/core_tab.gd").new()
EditorInterface.get_base_control().add_child(tab)
var raw = JSON.parse_string(FileAccess.get_file_as_string("res://data/cores.json"))
var fails = []
for cid in raw:
	var found = false
	for i in tab._list.item_count:
		if String(tab._list.get_item_metadata(i)) == cid:
			tab._on_select(i); found = true; break
	if not found:
		fails.append(cid + ":未在列表"); continue
	var got = []
	for r in tab._edges.get_values():
		got.append([int(r[0]), int(r[1])])
	var orig = raw[cid].get("edges", [])
	if got.size() != orig.size():
		fails.append("%s:edges数 %d!=%d" % [cid, got.size(), orig.size()])
	else:
		for i in got.size():
			if got[i][0] != int(orig[i].get("from",0)) or got[i][1] != int(orig[i].get("to",0)):
				fails.append("%s:edge#%d %s vs %s" % [cid, i, str(got[i]), str(orig[i])])
_mcp_print("CORE FAILS=%d %s" % [fails.size(), str(fails)])
tab.queue_free()
```
Expected: `CORE FAILS=0 []`。（`circuit_fork` 两条边 [0,1]/[0,2]；其余 core 无 edges → got 空、orig 空数组，size 均 0，等价。）

- [ ] **Step 4: Balance aether 回读往返**

`mcp__godot-mcp-pro__execute_editor_script`，`code`：
```gdscript
var tab = load("res://addons/game_designer/balance_tab.gd").new()
EditorInterface.get_base_control().add_child(tab)
var raw = JSON.parse_string(FileAccess.get_file_as_string("res://data/balance.json"))
var orig = raw.get("aether_thresholds", [])
var got = tab._aether.get_values()
var fails = []
if got.size() != orig.size():
	fails.append("size %d!=%d" % [got.size(), orig.size()])
else:
	for i in got.size():
		if int(got[i][0]) != int(orig[i][0]) or int(got[i][1]) != int(orig[i][1]):
			fails.append("row#%d %s!=%s" % [i, str(got[i]), str(orig[i])])
_mcp_print("BALANCE FAILS=%d %s" % [fails.size(), str(fails)])
tab.queue_free()
```
Expected: `BALANCE FAILS=0 []`（5 行 [[1,0],[5,3],[10,8],[15,14],[20,20]]）。

- [ ] **Step 5: 目测（用户确认）**

重启后由用户在 Core 页/平衡页确认：电路边为 from/to 行、平衡碎片为 波次/碎片 行，均可 ➕加/🗑删。若 Step 3/4 触发修复则相应 `git add && git commit`；否则本任务无提交。

---

## Self-Review

**Spec 覆盖：**
- §2 组件 `tuple_list_editor.gd`（setup/set_values/get_values/加删行）→ Task 1 ✅
- §3 core edges 集成（dict↔array）→ Task 2 ✅
- §4 balance aether 集成 → Task 3 ✅
- §6 验证（validate + 回读往返 core/balance）→ Task 4 ✅
- §7 spell 其它(JSON) 不动 → 无任务触碰 ✅

**占位符扫描：** 无 TBD/TODO；每步含完整代码或精确命令。✅

**类型/命名一致性：** `_edges`/`_aether` 由 `TextEdit` 改为**不标注**（动态派发 setup/get_values，避免 Control 无此方法的编译错；Task 4 回读用 `tab._edges`/`tab._aether`）；`setup`/`set_values`/`get_values`（Task 1 定义，Task 2/3/4 调用一致）；行数据模型 `Array[Array]` 全程一致。✅

**已知取舍：** LINEAR/MATRIX 法杖应用后仍会写 `edges:[]`（既有行为，非本次引入）；回读把「空数组 == 缺 edges 键」按 size 0 视为等价。
