## 法术编辑器停靠面板(@tool)— 策划/开发可视化新增·编辑·删除法术 ## 数据持久化到 res://data/spells.json,游戏运行时由 SpellRegistry 加载 @tool extends VBoxContainer const DATA_PATH: String = "res://data/spells.json" const TYPE_NAMES: Array = ["ACTION", "MODIFIER", "TRIGGER", "LOGIC"] ## meta 模板(按类型“插入模板”用) const META_TEMPLATES: Dictionary = { 0: '{ "base_damage": 5.0, "speed": 350.0, "lifetime": 4.0, "radius": 6.0, "damage_type": 0, "mana_cost": 5, "shop_cost": 20 }', 1: '{ "damage_add": 10.0, "mana_cost": 10, "shop_cost": 20 }', 2: '{ "base_damage": 3.0, "speed": 250.0, "lifetime": 4.0, "radius": 6.0, "damage_type": 0, "mana_cost": 10, "shop_cost": 30 }', 3: '{ "logic_op": "every_n_shots", "n": 3, "reg": 0, "mana_cost": 0, "shop_cost": 24 }', } var _spells: Dictionary = {} # { id: {type, display_name, description, element_tags, meta} } var _cur_id: String = "" # UI 引用 var _list: ItemList var _id_edit: LineEdit var _type_opt: OptionButton var _name_edit: LineEdit var _desc_edit: LineEdit var _tags_edit: LineEdit var _meta_edit: TextEdit var _status: Label func _ready() -> void: _build_ui() _load_data() _refresh_list() func _build_ui() -> void: custom_minimum_size = Vector2(300, 400) add_theme_constant_override("separation", 4) var title := Label.new() title.text = "✨ 法术编辑器" title.add_theme_font_size_override("font_size", 16) add_child(title) # 法术列表 _list = ItemList.new() _list.custom_minimum_size = Vector2(0, 120) _list.size_flags_vertical = Control.SIZE_EXPAND_FILL _list.item_selected.connect(_on_select) add_child(_list) var row := HBoxContainer.new() add_child(row) row.add_child(_btn("➕ 新增", _on_new)) row.add_child(_btn("⧉ 复制", _on_dup)) row.add_child(_btn("🗑 删除", _on_delete)) add_child(HSeparator.new()) # 表单 _id_edit = _field("ID(唯一)") _type_opt = OptionButton.new() for n in TYPE_NAMES: _type_opt.add_item(n) _labeled("类型", _type_opt) _name_edit = _field("显示名(tr 键或文本)") _desc_edit = _field("描述(tr 键或文本)") _tags_edit = _field("元素标签(逗号分隔,如 tag:water)") var meta_lbl := Label.new() meta_lbl.text = "meta(JSON,因类型而异)" add_child(meta_lbl) _meta_edit = TextEdit.new() _meta_edit.custom_minimum_size = Vector2(0, 90) _meta_edit.placeholder_text = '{ "base_damage": 5.0, ... }' add_child(_meta_edit) add_child(_btn("插入该类型 meta 模板", _on_template)) add_child(HSeparator.new()) var save_row := HBoxContainer.new() add_child(save_row) save_row.add_child(_btn("✓ 应用到列表", _on_apply)) save_row.add_child(_btn("💾 保存文件", _on_save_file)) save_row.add_child(_btn("↻ 重载", func(): _load_data(); _refresh_list(); _set_status("已重载"))) _status = Label.new() _status.add_theme_font_size_override("font_size", 11) _status.modulate = Color(0.6, 0.9, 0.6) add_child(_status) func _btn(text: String, cb: Callable) -> Button: var b := Button.new() b.text = text b.pressed.connect(cb) return b func _field(placeholder: String) -> LineEdit: var le := LineEdit.new() le.placeholder_text = placeholder add_child(le) return le func _labeled(label: String, ctrl: Control) -> void: var l := Label.new() l.text = label add_child(l) add_child(ctrl) # ── 数据 ──────────────────────────────────────────────────── func _load_data() -> void: _spells = {} if FileAccess.file_exists(DATA_PATH): var parsed = JSON.parse_string(FileAccess.get_file_as_string(DATA_PATH)) if parsed is Dictionary: _spells = parsed func _refresh_list() -> void: _list.clear() var ids: Array = _spells.keys() ids.sort() for sid in ids: var t: int = int(_spells[sid].get("type", 0)) _list.add_item("[%s] %s" % [TYPE_NAMES[clampi(t, 0, 3)], sid]) _list.set_item_metadata(_list.item_count - 1, sid) func _on_select(idx: int) -> void: _cur_id = String(_list.get_item_metadata(idx)) var d: Dictionary = _spells.get(_cur_id, {}) _id_edit.text = _cur_id _type_opt.selected = clampi(int(d.get("type", 0)), 0, 3) _name_edit.text = String(d.get("display_name", "")) _desc_edit.text = String(d.get("description", "")) _tags_edit.text = ",".join(d.get("element_tags", [])) _meta_edit.text = JSON.stringify(d.get("meta", {}), " ") func _on_apply() -> void: var sid: String = _id_edit.text.strip_edges() if sid == "": _set_status("✗ ID 不能为空", true); return var meta = JSON.parse_string(_meta_edit.text) if not (meta is Dictionary): _set_status("✗ meta 不是合法 JSON 对象", true); return var tags: Array = [] for t in _tags_edit.text.split(",", false): var s: String = t.strip_edges() if s != "": tags.append(s) # 改了 ID:移除旧键 if _cur_id != "" and _cur_id != sid and _spells.has(_cur_id): _spells.erase(_cur_id) _spells[sid] = { "type": _type_opt.selected, "display_name": _name_edit.text, "description": _desc_edit.text, "element_tags": tags, "meta": meta, } _cur_id = sid _refresh_list() _set_status("✓ 已应用『%s』到列表(记得保存文件)" % sid) func _on_save_file() -> void: DirAccess.make_dir_recursive_absolute("res://data") var f := FileAccess.open(DATA_PATH, FileAccess.WRITE) if f: f.store_string(JSON.stringify(_spells, " ")) f.close() _set_status("💾 已保存 %d 条法术 → %s" % [_spells.size(), DATA_PATH]) else: _set_status("✗ 写入失败", true) func _on_new() -> void: var base := "new_spell" var sid := base var i := 1 while _spells.has(sid): sid = "%s_%d" % [base, i]; i += 1 _spells[sid] = {"type": 0, "display_name": sid, "description": "", "element_tags": [], "meta": JSON.parse_string(META_TEMPLATES[0])} _refresh_list() _select_id(sid) func _on_dup() -> void: if _cur_id == "" or not _spells.has(_cur_id): return var sid := _cur_id + "_copy" var i := 1 while _spells.has(sid): sid = "%s_copy%d" % [_cur_id, i]; i += 1 _spells[sid] = _spells[_cur_id].duplicate(true) _refresh_list() _select_id(sid) func _on_delete() -> void: if _cur_id != "" and _spells.has(_cur_id): _spells.erase(_cur_id) _cur_id = "" _refresh_list() _set_status("已删除(记得保存文件)") func _on_template() -> void: _meta_edit.text = String(META_TEMPLATES.get(_type_opt.selected, "{}")) func _select_id(sid: String) -> void: for i in _list.item_count: if String(_list.get_item_metadata(i)) == sid: _list.select(i) _on_select(i) return func _set_status(msg: String, is_err: bool = false) -> void: if _status: _status.text = msg _status.modulate = Color(1.0, 0.5, 0.5) if is_err else Color(0.6, 0.9, 0.6)