12 KiB
整数元组列表编辑器(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 缩进):
## 整数元组列表编辑器(@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: 提交
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:
var _edges: TextEdit
new:
var _edges
- Step 2:
_ready()—— edges 文本框换行编辑器
old:
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:
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:
_edges.text = JSON.stringify(d.get("edges", []))
new:
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:
var edges = JSON.parse_string(_edges.text) if _edges.text.strip_edges() != "" else []
if not (edges is Array):
edges = []
new:
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: 提交
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:
var _aether: TextEdit
new:
var _aether
(注意:_aether 成员声明在文件顶部成员区,无前导缩进;balance_tab.gd 的 var _aether: TextEdit 在第 15 行左右,顶格。)
- Step 2:
_ready()—— aether 文本框换行编辑器
old:
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:
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:
var thresholds = JSON.parse_string(_aether.text) if _aether.text.strip_edges() != "" else []
if not (thresholds is Array):
thresholds = []
new:
var thresholds := _aether.get_values()
- Step 4: 校验
mcp__godot-mcp-pro__validate_script,path = "res://addons/game_designer/balance_tab.gd"。Expected: 通过。
- Step 5: 提交
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:
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:
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 视为等价。