From 4fee7082872a2cf86f07eba7498b8d7ebbb254c4 Mon Sep 17 00:00:00 2001 From: Joywayer Date: Wed, 22 Jul 2026 12:05:56 +0800 Subject: [PATCH] =?UTF-8?q?docs(designer):=20=E6=95=B4=E6=95=B0=E5=85=83?= =?UTF-8?q?=E7=BB=84=E5=88=97=E8=A1=A8=E7=BC=96=E8=BE=91=E5=99=A8=E5=AE=9E?= =?UTF-8?q?=E7=8E=B0=E8=AE=A1=E5=88=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../2026-07-22-designer-tuple-list-editor.md | 336 ++++++++++++++++++ 1 file changed, 336 insertions(+) create mode 100644 docs_dev/plans/2026-07-22-designer-tuple-list-editor.md diff --git a/docs_dev/plans/2026-07-22-designer-tuple-list-editor.md b/docs_dev/plans/2026-07-22-designer-tuple-list-editor.md new file mode 100644 index 0000000..6021b27 --- /dev/null +++ b/docs_dev/plans/2026-07-22-designer-tuple-list-editor.md @@ -0,0 +1,336 @@ +# 整数元组列表编辑器(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: 成员变量类型** + +old: +```gdscript +var _edges: TextEdit +``` +new: +```gdscript +var _edges: Control +``` + +- [ ] **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: 成员变量类型** + +old: +```gdscript + var _aether: TextEdit +``` +new: +```gdscript + var _aether: Control +``` + +(注意:该 `var` 声明位于文件顶部成员区,缩进为单 TAB;按文件实际缩进匹配。) + +- [ ] **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→Control(Task 2/3 定义,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 视为等价。