# 法术编辑器并入 game_designer + 字段化表单 — 设计

- 日期：2026-07-21
- 状态：设计已确认，待实现
- 范围：编辑器工具层（`addons/`）+ 文档，**游戏运行时代码零改动**

---

## 1. 背景与问题

当前有两个已启用的编辑器插件：

- `addons/spell_editor/`（独立插件，左下停靠面板，入口 `spell_editor.gd` → `spell_dock.gd`）
- `addons/game_designer/`（底部面板，`designer_panel.gd` 用 `TabContainer` 整合多个 tab）

`designer_panel.gd` 已经把 `spell_dock.gd` 复用为「✨ 法术」标签页 → **法术编辑器同时出现在两处，入口冗余**。

同时，`spell_dock.gd` 的 `meta` 字段是一个**自由格式 JSON TextEdit**，随法术类型/子类型变化很大，容易配置错误、可读性差 —— 与 `core_tab.gd`（字段化输入框）风格不一致。

### 目标

1. **并入**：法术编辑只保留在 `game_designer` 底部面板的 tab 里；删除独立 `spell_editor` 插件。
2. **字段化**：用按类型动态切换的输入框表单替代原始 JSON `meta`，对齐 `core_tab.gd` 风格。

---

## 2. 结构变更（并入）

- 新建 `addons/game_designer/spell_tab.gd`，结构对齐 `core_tab.gd`：
  - `@tool extends VBoxContainer`
  - `const UI = preload("res://addons/game_designer/designer_ui.gd")`
  - 复用 `UI.spin / line / opt / btn / cell_label / header / status_label / set_status / load_json / save_json`
  - 布局：标题 → 列表(ItemList) → `➕新增 / ⧉复制 / 🗑删除` → 表单区 → `✓应用到列表 / 💾保存文件 / ↻重载` → 状态行
- `designer_panel.gd` 第 12 行：
  - 由 `preload("res://addons/spell_editor/spell_dock.gd").new()`
  - 改为 `preload("res://addons/game_designer/spell_tab.gd").new()`
- **删除独立插件**：
  - 从 `project.godot` `[editor_plugins] enabled` 数组移除 `"res://addons/spell_editor/plugin.cfg"`
  - 删除整个 `addons/spell_editor/` 目录（`spell_editor.gd(.uid)`、`spell_dock.gd(.uid)`、`plugin.cfg`）
- 数据路径与文件格式不变：`res://data/spells.json`；`SpellRegistry` 运行时加载逻辑零改动。

---

## 3. 动态表单（核心）

### 3.1 固定区（所有类型）
`ID`(LineEdit，唯一) / `类型`(OptionButton: ACTION/MODIFIER/TRIGGER/LOGIC) / `显示名`(LineEdit) / `描述`(LineEdit) / `元素标签`(LineEdit，逗号分隔)。

### 3.2 通用 meta 区（所有类型常驻）
`mana_cost`(int) / `shop_cost`(int) / `unlock_cost`(int)。

### 3.3 动态 meta 区
一个占位容器（如 `VBoxContainer`），随 `类型` 变化（ACTION 额外看 `action_kind` 子选择、LOGIC 额外看 `logic_op` 子选择）**销毁并重建其子节点**。字段 schema：

| 类型 | 子选择 | 字段（控件类型） |
|---|---|---|
| ACTION | 弹道（默认，action_kind 省略） | base_damage(float) speed(float) lifetime(float) radius(float) damage_type(enum) apply_status_id(enum,含「无」) apply_combo_mark(bool) pierce(int) |
| ACTION | zone 毒池（action_kind="zone"） | radius(float) status_id(enum) duration(float) tick_interval(float) |
| ACTION | summon 召唤（action_kind="summon"） | lifetime(float) range(float) fire_interval(float) base_damage(float) speed(float) damage_type(enum) |
| MODIFIER | — | damage_add(float) damage_mult(float) spread_add(int) multicast(int) pierce(int) heavy_cost(int) mana_leech(int) |
| TRIGGER | — | base_damage(float) speed(float) lifetime(float) radius(float) damage_type(enum) |
| LOGIC | every_n_shots | n(int) reg(int) |
| LOGIC | if_hp_below | threshold(float, 0–1) |
| LOGIC | if_enemy_nearby | range(float) |
| LOGIC | loop | count(int) |

- ACTION 的 `action_kind` 用一个 OptionButton 子选择器：`弹道 / zone 毒池 / summon 召唤`；选「弹道」时不写 `action_kind` 键。
- LOGIC 的 `logic_op` 用一个 OptionButton 子选择器：`every_n_shots / if_hp_below / if_enemy_nearby / loop`；始终写入 `logic_op` 字符串。

### 3.4 枚举下拉（整数存储）
标签来自域枚举，控件用 `UI.opt(...)`，存整数值：

- `damage_type`（`scripts/autoloads/damage_type.gd`）：0 物理 / 1 火 / 2 冰 / 3 雷 / 4 毒 / 5 奥术
- `status`（`apply_status_id` / `status_id`，`scripts/autoloads/status_id.gd`）：0 无 / 1 燃烧 / 2 冻结 / 3 中毒 / 4 潮湿 / 5 沾油 / 6 眩晕 / 7 连击 / 8 易伤
  - 下拉 index 即整数值（0 = 无 = 不写该键）。

> 枚举标签在 `spell_tab.gd` 内以常量数组硬编码，index 对齐上述整数值；若域枚举新增值，需同步更新此数组（在文件顶部注释标注来源）。

---

## 4. 数据安全（防丢键 / 保持简洁）

### 4.1 「其它(JSON)」逃生舱
动态区下方放一个小 `TextEdit`（标签「其它(JSON)」）。

- **加载**（选中法术）：把 `meta` 中 schema **已知键**填入对应控件；**剩余未知键**打包成 JSON 字符串填入此框（无剩余则空）。
- **应用**：最终 `meta` = 通用字段 + 当前类型/子类型动态字段 + 解析「其它(JSON)」得到的键（后者仅补充未被 schema 覆盖的键；若解析失败则报错、拒绝应用）。

作用：游戏日后新增 meta 键、或某法术带非常规键时，编辑器不会静默丢弃。

### 4.2 可选键清洁写出
以下键在值为默认（0 / false / "" / 未设）时**不写入** JSON，保持与现有 `spells.json` 一致的简洁：
`unlock_cost`(=0) / `apply_status_id`(=0) / `apply_combo_mark`(=false) / `pierce`(=0) / `damage_mult`(=1.0，乘数默认值，等于 1.0 时不写) / `spread_add`(=0) / `multicast`(=0) / `heavy_cost`(=0) / `mana_leech`(=0) / `reg`(=0)。

> `mana_cost` / `shop_cost` 始终写入（含 0，等离子风暴 `mana_cost:0 shop_cost:0` 是有意义的显式 0）。

### 4.3 回读验证目标
现有 `data/spells.json` 全部 22 条必须能被正确回读进新表单（含 zone、summon、全部 4 种 logic_op），再次「应用+保存」后语义等价（允许可选默认键因 §4.2 被省略，属预期简化）。

---

## 5. 组件边界

| 单元 | 职责 | 依赖 |
|---|---|---|
| `spell_tab.gd` | 法术 tab 全部 UI + 增删改查 + schema 驱动的表单构建/读写 | `designer_ui.gd`；`data/spells.json` |
| `designer_panel.gd` | 只改一行 preload 指向新 tab | `spell_tab.gd` |
| `project.godot` | 移除 spell_editor 插件启用项 | — |

`spell_tab.gd` 内部建议拆出：`_schema_for(type, sub)` 返回字段描述数组；`_rebuild_dynamic()` 依 schema 建控件；`_read_form()`/`_write_form(meta)` 在控件与 meta 字典间搬运。schema 表是唯一事实源，加/改字段只动这张表。

---

## 6. 文档

更新 `docs/handbook/06_spell_editor.md`：

- 入口改为「Godot 编辑器底部面板 → 🎮 游戏设计器 → ✨ 法术 标签页」。
- 删除「1. 启用插件（左下停靠区）」相关描述。
- 界面示意图改为字段化表单（固定区 + 通用区 + 动态区 + 其它JSON）。
- 「meta 字段速查」保留（作为字段语义参考），补一句「现由表单按类型呈现」。

---

## 7. 范围外（YAGNI）

- 不动其它 tab（敌人/波次/Core/共鸣/状态/平衡）。
- 不动游戏运行时代码（`SpellRegistry` / VM / `wand_preset.gd`）。
- 不做字段级校验规则引擎（超范围）；仅做 ID 非空、JSON 合法性、枚举 index 合法性等基本校验。
- 不做 tag 的下拉选择器（元素标签仍为逗号分隔文本，沿用现状）。

---

## 8. 验收标准

1. Godot 编辑器仅在底部「🎮 游戏设计器 → ✨ 法术」出现法术编辑，左下不再有独立「法术编辑器」停靠面板。
2. `addons/spell_editor/` 目录已删除，`project.godot` 不再引用它，编辑器加载无报错。
3. 逐一选中 22 条现有法术，字段正确回读（类型/子类型/枚举/数值/其它JSON 均对）。
4. 新增一条各类型法术（含 zone、summon、每种 logic_op），应用+保存后 `spells.json` 合法且游戏 F5 能加载。
5. 改 ID 时旧键被移除；复制/删除正常。
6. 「其它(JSON)」非法时应用被拒绝并给出错误状态，不写坏数据。
