From d7e051ffde19400ac3d2fd80201d2548b365b586 Mon Sep 17 00:00:00 2001 From: Joywayer Date: Tue, 21 Jul 2026 16:04:53 +0800 Subject: [PATCH 1/5] =?UTF-8?q?feat(spell-editor):=20=E6=96=B0=E5=A2=9E=20?= =?UTF-8?q?game=5Fdesigner=20=E6=B3=95=E6=9C=AF=20tab=EF=BC=88=E5=AD=97?= =?UTF-8?q?=E6=AE=B5=E5=8C=96=E8=A1=A8=E5=8D=95=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- addons/game_designer/spell_tab.gd | 330 ++++++++++++++++++++++++++++++ 1 file changed, 330 insertions(+) create mode 100644 addons/game_designer/spell_tab.gd diff --git a/addons/game_designer/spell_tab.gd b/addons/game_designer/spell_tab.gd new file mode 100644 index 0000000..c0be1e0 --- /dev/null +++ b/addons/game_designer/spell_tab.gd @@ -0,0 +1,330 @@ +## 法术编辑器标签(game_designer)— 字段化表单,替代原始 JSON meta。 +## 数据持久化到 res://data/spells.json,运行时由 SpellRegistry 加载。 +## 枚举标签来源:DamageType(scripts/autoloads/damage_type.gd)、StatusID(scripts/autoloads/status_id.gd); +## 若域枚举新增值,需同步 DMG_LABELS / ST_LABELS。 +@tool +extends VBoxContainer + +const UI = preload("res://addons/game_designer/designer_ui.gd") +const PATH := "res://data/spells.json" +const TYPE_NAMES := ["ACTION", "MODIFIER", "TRIGGER", "LOGIC"] +const ACTION_KINDS := ["弹道", "zone 毒池", "summon 召唤"] # 0=默认(不写 action_kind) 1=zone 2=summon +const LOGIC_OPS := ["every_n_shots", "if_hp_below", "if_enemy_nearby", "loop"] +const DMG_LABELS := ["物理", "火", "冰", "雷", "毒", "奥术"] # index = damage_type 整数 +const ST_LABELS := ["无", "燃烧", "冻结", "中毒", "潮湿", "沾油", "眩晕", "连击", "易伤"] # index = status 整数 + +var _data: Dictionary = {} +var _cur: String = "" + +var _list: ItemList +var _id: LineEdit +var _type: OptionButton +var _name: LineEdit +var _desc: LineEdit +var _tags: LineEdit +var _mana: SpinBox +var _shop: SpinBox +var _unlock: SpinBox +var _akind_row: HBoxContainer +var _akind: OptionButton +var _lop_row: HBoxContainer +var _lop: OptionButton +var _dyn: VBoxContainer +var _extra: TextEdit +var _status: Label +var _field_ctrls: Dictionary = {} # key -> control(当前动态字段) + +func _ready() -> void: + add_child(UI.header("✨ 法术编辑器 — 字段化表单")) + _load() + _list = ItemList.new() + _list.custom_minimum_size = Vector2(0, 110) + _list.item_selected.connect(_on_select) + add_child(_list) + var brow := HBoxContainer.new(); add_child(brow) + brow.add_child(UI.btn("➕新增", _on_new)) + brow.add_child(UI.btn("⧉复制", _on_dup)) + brow.add_child(UI.btn("🗑删除", _on_delete)) + add_child(HSeparator.new()) + + _id = _row("ID(唯一)", UI.line("")) + _type = _row("类型", UI.opt(TYPE_NAMES, 0)) + _type.item_selected.connect(_on_type_or_sub_changed) + _name = _row("显示名", UI.line("", "tr 键或文本")) + _desc = _row("描述", UI.line("", "tr 键或文本")) + _tags = _row("元素标签", UI.line("", "逗号分隔,如 tag:fire")) + add_child(HSeparator.new()) + + _mana = _row("蓝耗 mana_cost", UI.spin(0, 100000, 1, 0)) + _shop = _row("商店价 shop_cost", UI.spin(0, 100000, 1, 20)) + _unlock = _row("解锁价 unlock_cost", UI.spin(0, 100000, 1, 0)) + + _akind = UI.opt(ACTION_KINDS, 0) + _akind.item_selected.connect(_on_type_or_sub_changed) + _akind_row = _labeled_row("动作种类", _akind) + _lop = UI.opt(LOGIC_OPS, 0) + _lop.item_selected.connect(_on_type_or_sub_changed) + _lop_row = _labeled_row("逻辑 logic_op", _lop) + + add_child(Label.new()) + _dyn = VBoxContainer.new(); add_child(_dyn) + + var el := Label.new(); el.text = "其它(JSON)"; add_child(el) + _extra = TextEdit.new(); _extra.custom_minimum_size = Vector2(0, 44) + _extra.placeholder_text = "{}(schema 之外的键,保留不丢)" + add_child(_extra) + + add_child(HSeparator.new()) + var srow := HBoxContainer.new(); add_child(srow) + srow.add_child(UI.btn("✓应用到列表", _on_apply)) + srow.add_child(UI.btn("💾保存文件", _save)) + srow.add_child(UI.btn("↻重载", func(): _load(); _refresh(); UI.set_status(_status, "已重载"))) + _status = UI.status_label(); add_child(_status) + + _refresh() + _rebuild_dynamic() + +func _row(label: String, ctrl: Control) -> Control: + var h := HBoxContainer.new() + h.add_child(UI.cell_label(label)) + h.add_child(ctrl) + add_child(h) + return ctrl + +func _labeled_row(label: String, ctrl: Control) -> HBoxContainer: + var h := HBoxContainer.new() + h.add_child(UI.cell_label(label)) + h.add_child(ctrl) + add_child(h) + return h + +# ── 数据 ─────────────────────────────── +func _load() -> void: + var d = UI.load_json(PATH) + _data = d if d is Dictionary else {} + +func _refresh() -> void: + _list.clear() + var ids: Array = _data.keys(); ids.sort() + for sid in ids: + var t := int(_data[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) + +# ── schema ───────────────────────────── +func _dynamic_schema() -> Array: + match _type.selected: + 0: + match _akind.selected: + 1: return [ + {"k": "radius", "l": "半径 radius", "w": "f", "d": 200.0}, + {"k": "status_id", "l": "状态 status_id", "w": "st", "d": 0}, + {"k": "duration", "l": "持续 duration", "w": "f", "d": 5.0}, + {"k": "tick_interval", "l": "tick 间隔", "w": "f", "d": 1.0}] + 2: return [ + {"k": "lifetime", "l": "存活 lifetime", "w": "f", "d": 20.0}, + {"k": "range", "l": "射程 range", "w": "f", "d": 350.0}, + {"k": "fire_interval", "l": "开火间隔", "w": "f", "d": 0.8}, + {"k": "base_damage", "l": "伤害 base_damage", "w": "f", "d": 4.0}, + {"k": "speed", "l": "速度 speed", "w": "f", "d": 360.0}, + {"k": "damage_type", "l": "伤害类型", "w": "dmg", "d": 0}] + _: return [ + {"k": "base_damage", "l": "伤害 base_damage", "w": "f", "d": 5.0}, + {"k": "speed", "l": "速度 speed", "w": "f", "d": 350.0}, + {"k": "lifetime", "l": "存活 lifetime", "w": "f", "d": 4.0}, + {"k": "radius", "l": "半径 radius", "w": "f", "d": 6.0}, + {"k": "damage_type", "l": "伤害类型", "w": "dmg", "d": 0}, + {"k": "apply_status_id", "l": "施加状态", "w": "st", "d": 0, "opt": true}, + {"k": "apply_combo_mark", "l": "连击标记", "w": "b", "d": false, "opt": true}, + {"k": "pierce", "l": "穿透 pierce", "w": "i", "d": 0, "opt": true}] + 1: return [ + {"k": "damage_add", "l": "伤害+ damage_add", "w": "f", "d": 0.0, "opt": true}, + {"k": "damage_mult", "l": "伤害× damage_mult", "w": "f", "d": 1.0, "opt": true}, + {"k": "spread_add", "l": "多发 spread_add", "w": "i", "d": 0, "opt": true}, + {"k": "multicast", "l": "额外施法 multicast", "w": "i", "d": 0, "opt": true}, + {"k": "pierce", "l": "穿透 pierce", "w": "i", "d": 0, "opt": true}, + {"k": "heavy_cost", "l": "扣血 heavy_cost", "w": "i", "d": 0, "opt": true}, + {"k": "mana_leech", "l": "击杀回蓝 mana_leech", "w": "i", "d": 0, "opt": true}] + 2: return [ + {"k": "base_damage", "l": "伤害 base_damage", "w": "f", "d": 3.0}, + {"k": "speed", "l": "速度 speed", "w": "f", "d": 250.0}, + {"k": "lifetime", "l": "存活 lifetime", "w": "f", "d": 4.0}, + {"k": "radius", "l": "半径 radius", "w": "f", "d": 6.0}, + {"k": "damage_type", "l": "伤害类型", "w": "dmg", "d": 0}] + 3: + match _lop.selected: + 0: return [ + {"k": "n", "l": "n(每 n 次)", "w": "i", "d": 3}, + {"k": "reg", "l": "reg(寄存器)", "w": "i", "d": 0, "opt": true}] + 1: return [ + {"k": "threshold", "l": "阈值 threshold", "w": "f", "d": 0.5, "min": 0.0, "max": 1.0}] + 2: return [ + {"k": "range", "l": "范围 range", "w": "f", "d": 200.0}] + 3: return [ + {"k": "count", "l": "循环次数 count", "w": "i", "d": 2}] + return [] + +func _rebuild_dynamic() -> void: + _akind_row.visible = _type.selected == 0 + _lop_row.visible = _type.selected == 3 + for c in _dyn.get_children(): + c.queue_free() + _field_ctrls.clear() + for f in _dynamic_schema(): + var ctrl := _make_ctrl(f) + _field_ctrls[f["k"]] = ctrl + var h := HBoxContainer.new() + h.add_child(UI.cell_label(f["l"])) + h.add_child(ctrl) + _dyn.add_child(h) + +func _make_ctrl(f: Dictionary) -> Control: + match f["w"]: + "i": return UI.spin(f.get("min", 0), f.get("max", 100000), 1, f["d"]) + "f": return UI.spin(f.get("min", 0.0), f.get("max", 100000.0), 0.05, f["d"]) + "b": + var cb := CheckBox.new(); cb.button_pressed = bool(f["d"]); return cb + "dmg": return UI.opt(DMG_LABELS, int(f["d"])) + "st": return UI.opt(ST_LABELS, int(f["d"])) + return UI.line("") + +func _get_ctrl(ctrl: Control): + if ctrl is SpinBox: return (ctrl as SpinBox).value + if ctrl is CheckBox: return (ctrl as CheckBox).button_pressed + if ctrl is OptionButton: return (ctrl as OptionButton).selected + return null + +func _set_ctrl(ctrl: Control, v) -> void: + if ctrl is SpinBox: (ctrl as SpinBox).value = float(v) + elif ctrl is CheckBox: (ctrl as CheckBox).button_pressed = bool(v) + elif ctrl is OptionButton: (ctrl as OptionButton).selected = int(v) + +# ── 选中 → 填表 ───────────────────────── +func _on_select(idx: int) -> void: + _cur = String(_list.get_item_metadata(idx)) + var d: Dictionary = _data.get(_cur, {}) + var meta: Dictionary = d.get("meta", {}) + _id.text = _cur + _type.selected = clampi(int(d.get("type", 0)), 0, 3) + _name.text = String(d.get("display_name", "")) + _desc.text = String(d.get("description", "")) + _tags.text = ",".join(d.get("element_tags", [])) + var ak := String(meta.get("action_kind", "")) + _akind.selected = (1 if ak == "zone" else (2 if ak == "summon" else 0)) + _lop.selected = maxi(0, LOGIC_OPS.find(String(meta.get("logic_op", "every_n_shots")))) + _mana.value = float(meta.get("mana_cost", 0)) + _shop.value = float(meta.get("shop_cost", 20)) + _unlock.value = float(meta.get("unlock_cost", 0)) + _rebuild_dynamic() + for k in _field_ctrls: + if meta.has(k): + _set_ctrl(_field_ctrls[k], meta[k]) + _extra.text = _compute_extra(meta) + +func _known_keys() -> Dictionary: + var ks := {"mana_cost": true, "shop_cost": true, "unlock_cost": true, + "action_kind": true, "logic_op": true} + for f in _dynamic_schema(): + ks[f["k"]] = true + return ks + +func _compute_extra(meta: Dictionary) -> String: + var known := _known_keys() + var leftover := {} + for k in meta: + if not known.has(k): + leftover[k] = meta[k] + return "" if leftover.is_empty() else JSON.stringify(leftover, " ") + +func _on_type_or_sub_changed(_i: int = 0) -> void: + _rebuild_dynamic() + +# ── 读表 → meta ───────────────────────── +func _read_meta() -> Variant: + var meta := {} + meta["mana_cost"] = int(_mana.value) + meta["shop_cost"] = int(_shop.value) + if int(_unlock.value) > 0: + meta["unlock_cost"] = int(_unlock.value) + if _type.selected == 0 and _akind.selected == 1: + meta["action_kind"] = "zone" + elif _type.selected == 0 and _akind.selected == 2: + meta["action_kind"] = "summon" + if _type.selected == 3: + meta["logic_op"] = LOGIC_OPS[_lop.selected] + for f in _dynamic_schema(): + var ctrl: Control = _field_ctrls.get(f["k"]) + if ctrl == null: continue + var v = _get_ctrl(ctrl) + if f["w"] == "i" or f["w"] == "dmg" or f["w"] == "st": + v = int(v) + elif f["w"] == "f": + v = float(v) + if f.get("opt", false) and _is_default(v, f["d"]): + continue + meta[f["k"]] = v + var extra_txt := _extra.text.strip_edges() + if extra_txt != "": + var parsed = JSON.parse_string(extra_txt) + if not (parsed is Dictionary): + return null + for k in parsed: + if not meta.has(k): + meta[k] = parsed[k] + return meta + +func _is_default(v, d) -> bool: + if v is float or d is float: + return absf(float(v) - float(d)) < 0.0001 + return v == d + +# ── 增删改查 ──────────────────────────── +func _on_apply() -> void: + var sid := _id.text.strip_edges() + if sid == "": + UI.set_status(_status, "✗ ID 不能为空", true); return + var meta = _read_meta() + if not (meta is Dictionary): + UI.set_status(_status, "✗ 其它(JSON) 不是合法对象", true); return + var tags: Array = [] + for t in _tags.text.split(",", false): + var s := t.strip_edges() + if s != "": tags.append(s) + if _cur != "" and _cur != sid and _data.has(_cur): + _data.erase(_cur) + _data[sid] = {"type": _type.selected, "display_name": _name.text, + "description": _desc.text, "element_tags": tags, "meta": meta} + _cur = sid + _refresh() + UI.set_status(_status, "✓ 已应用『%s』(记得保存)" % sid) + +func _on_new() -> void: + var sid := "new_spell"; var i := 1 + while _data.has(sid): sid = "new_spell_%d" % i; i += 1 + _data[sid] = {"type": 0, "display_name": sid, "description": "", + "element_tags": [], "meta": {"base_damage": 5.0, "speed": 350.0, + "lifetime": 4.0, "radius": 6.0, "damage_type": 0, "mana_cost": 5, "shop_cost": 20}} + _refresh(); _select_id(sid) + +func _on_dup() -> void: + if _cur == "" or not _data.has(_cur): return + var sid := _cur + "_copy"; var i := 1 + while _data.has(sid): sid = "%s_copy%d" % [_cur, i]; i += 1 + _data[sid] = _data[_cur].duplicate(true) + _refresh(); _select_id(sid) + +func _on_delete() -> void: + if _cur != "" and _data.has(_cur): + _data.erase(_cur); _cur = ""; _refresh() + UI.set_status(_status, "已删除(记得保存)") + +func _save() -> void: + if UI.save_json(PATH, _data): + UI.set_status(_status, "💾 已保存 %d 条法术(F5 生效)" % _data.size()) + else: + UI.set_status(_status, "✗ 保存失败", true) + +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 From 3e189206d54564984f32077bff5e78ee14896cfd Mon Sep 17 00:00:00 2001 From: Joywayer Date: Tue, 21 Jul 2026 16:07:26 +0800 Subject: [PATCH 2/5] =?UTF-8?q?refactor(spell-editor):=20=E5=B9=B6?= =?UTF-8?q?=E5=85=A5=20game=5Fdesigner=20tab=EF=BC=8C=E7=A7=BB=E9=99=A4?= =?UTF-8?q?=E7=8B=AC=E7=AB=8B=20spell=5Feditor=20=E6=8F=92=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- addons/game_designer/designer_panel.gd | 2 +- addons/spell_editor/plugin.cfg | 6 - addons/spell_editor/spell_dock.gd | 214 ------------------------ addons/spell_editor/spell_dock.gd.uid | 1 - addons/spell_editor/spell_editor.gd | 17 -- addons/spell_editor/spell_editor.gd.uid | 1 - project.godot | 2 +- 7 files changed, 2 insertions(+), 241 deletions(-) delete mode 100644 addons/spell_editor/plugin.cfg delete mode 100644 addons/spell_editor/spell_dock.gd delete mode 100644 addons/spell_editor/spell_dock.gd.uid delete mode 100644 addons/spell_editor/spell_editor.gd delete mode 100644 addons/spell_editor/spell_editor.gd.uid diff --git a/addons/game_designer/designer_panel.gd b/addons/game_designer/designer_panel.gd index d8d504c..a191bab 100644 --- a/addons/game_designer/designer_panel.gd +++ b/addons/game_designer/designer_panel.gd @@ -9,7 +9,7 @@ func _init() -> void: add_child(tabs) # 法术(复用现有法术编辑器停靠脚本) - _add_tab(tabs, "✨ 法术", preload("res://addons/spell_editor/spell_dock.gd").new()) + _add_tab(tabs, "✨ 法术", preload("res://addons/game_designer/spell_tab.gd").new()) _add_tab(tabs, "👾 敌人", preload("res://addons/game_designer/enemy_tab.gd").new()) _add_tab(tabs, "🌊 波次", preload("res://addons/game_designer/wave_tab.gd").new()) _add_tab(tabs, "🪄 Core", preload("res://addons/game_designer/core_tab.gd").new()) diff --git a/addons/spell_editor/plugin.cfg b/addons/spell_editor/plugin.cfg deleted file mode 100644 index 2e569b4..0000000 --- a/addons/spell_editor/plugin.cfg +++ /dev/null @@ -1,6 +0,0 @@ -[plugin] -name="Spell Editor (法术编辑器)" -description="可视化法术编辑器:策划/开发无需改代码即可新增、编辑、删除法术,保存为 res://data/spells.json,游戏运行时自动加载。" -author="Base Games" -version="1.0.0" -script="spell_editor.gd" diff --git a/addons/spell_editor/spell_dock.gd b/addons/spell_editor/spell_dock.gd deleted file mode 100644 index 6813815..0000000 --- a/addons/spell_editor/spell_dock.gd +++ /dev/null @@ -1,214 +0,0 @@ -## 法术编辑器停靠面板(@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) diff --git a/addons/spell_editor/spell_dock.gd.uid b/addons/spell_editor/spell_dock.gd.uid deleted file mode 100644 index a95158d..0000000 --- a/addons/spell_editor/spell_dock.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://dkk2bspn83h8s diff --git a/addons/spell_editor/spell_editor.gd b/addons/spell_editor/spell_editor.gd deleted file mode 100644 index 15ec956..0000000 --- a/addons/spell_editor/spell_editor.gd +++ /dev/null @@ -1,17 +0,0 @@ -## 法术编辑器插件入口(EditorPlugin) -## 在编辑器左下停靠区添加「法术编辑器」面板。 -@tool -extends EditorPlugin - -var _dock: Control = null - -func _enter_tree() -> void: - _dock = preload("res://addons/spell_editor/spell_dock.gd").new() - _dock.name = "法术编辑器" - add_control_to_dock(EditorPlugin.DOCK_SLOT_LEFT_BR, _dock) - -func _exit_tree() -> void: - if _dock: - remove_control_from_docks(_dock) - _dock.free() - _dock = null diff --git a/addons/spell_editor/spell_editor.gd.uid b/addons/spell_editor/spell_editor.gd.uid deleted file mode 100644 index e948aca..0000000 --- a/addons/spell_editor/spell_editor.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://qmds48f8kkm0 diff --git a/project.godot b/project.godot index 069bc73..df97170 100644 --- a/project.godot +++ b/project.godot @@ -65,7 +65,7 @@ project/assembly_name="Rogue" [editor_plugins] -enabled=PackedStringArray("res://addons/game_designer/plugin.cfg", "res://addons/godot_mcp/plugin.cfg", "res://addons/spell_editor/plugin.cfg") +enabled=PackedStringArray("res://addons/game_designer/plugin.cfg", "res://addons/godot_mcp/plugin.cfg") [gui] From 29efa986c58722a5f3a643bd8ad487dfdc25f9cb Mon Sep 17 00:00:00 2001 From: Joywayer Date: Tue, 21 Jul 2026 16:08:13 +0800 Subject: [PATCH 3/5] =?UTF-8?q?chore(spell-editor):=20=E8=BF=BD=E8=B8=AA?= =?UTF-8?q?=20spell=5Ftab.gd.uid=EF=BC=88=E4=B8=8E=E5=85=B6=E5=AE=83=20tab?= =?UTF-8?q?=20=E4=B8=80=E8=87=B4=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- addons/game_designer/spell_tab.gd.uid | 1 + 1 file changed, 1 insertion(+) create mode 100644 addons/game_designer/spell_tab.gd.uid diff --git a/addons/game_designer/spell_tab.gd.uid b/addons/game_designer/spell_tab.gd.uid new file mode 100644 index 0000000..78e03f9 --- /dev/null +++ b/addons/game_designer/spell_tab.gd.uid @@ -0,0 +1 @@ +uid://bousxyhirtljs From ed89387f2efd76d0c105e4392e380dfcd4da0912 Mon Sep 17 00:00:00 2001 From: Joywayer Date: Tue, 21 Jul 2026 16:09:33 +0800 Subject: [PATCH 4/5] =?UTF-8?q?docs(spell-editor):=20=E6=89=8B=E5=86=8C?= =?UTF-8?q?=E6=94=B9=E4=B8=BA=E5=AD=97=E6=AE=B5=E5=8C=96=E8=A1=A8=E5=8D=95?= =?UTF-8?q?=20+=20game=5Fdesigner=20=E5=85=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/handbook/06_spell_editor.md | 40 +++++++++++++++++--------------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/docs/handbook/06_spell_editor.md b/docs/handbook/06_spell_editor.md index 1ec58fa..586ed84 100644 --- a/docs/handbook/06_spell_editor.md +++ b/docs/handbook/06_spell_editor.md @@ -4,24 +4,22 @@ --- -## 1. 启用插件 +## 1. 打开编辑器 -插件位于 `res://addons/spell_editor/`,默认已在 `project.godot` 启用。若未显示: +法术编辑现已并入 **游戏设计器**(`res://addons/game_designer/`,默认已在 `project.godot` 启用)。 ``` -项目 → 项目设置 → 插件 → 勾选 "Spell Editor (法术编辑器)" +Godot 编辑器 → 底部面板 → 🎮 游戏设计器 → ✨ 法术 标签页 ``` -启用后,编辑器**左下停靠区**会出现「法术编辑器」标签页。 - -> 首次启用或修改插件后,可能需要**重启 Godot 编辑器**让停靠面板出现。 +> 独立的「法术编辑器」左下停靠面板已移除,法术统一在游戏设计器内维护。修改插件后可能需**重启 Godot 编辑器**让面板刷新。 --- ## 2. 界面说明 ``` -✨ 法术编辑器 +✨ 法术编辑器 — 字段化表单 ┌────────────────────────┐ │ 法术列表(ItemList) │ ← 所有 data/spells.json 中的法术 │ [ACTION] action_frost… │ @@ -29,19 +27,21 @@ └────────────────────────┘ [➕新增] [⧉复制] [🗑删除] ──────────────────────── -ID(唯一) : [______] -类型 : [ACTION ▾] -显示名(tr 键) : [______] -描述(tr 键) : [______] -元素标签(逗号) : [tag:ice] -meta(JSON) : [______] - [______] -[插入该类型 meta 模板] +ID(唯一)/ 类型[ACTION ▾]/ 显示名 / 描述 / 元素标签(逗号) +── 通用 meta ── +蓝耗 mana_cost / 商店价 shop_cost / 解锁价 unlock_cost +── 动态字段(随类型/子类型自动切换)── +ACTION:动作种类[弹道 / zone 毒池 / summon 召唤] + 对应字段 + (伤害 / 速度 / 存活 / 半径 / 伤害类型▾ / 施加状态▾ / …) +MODIFIER / TRIGGER / LOGIC(logic_op▾ 子选择)各自字段 +── 其它(JSON) ── ← schema 之外的键,保留不丢 ──────────────────────── [✓应用到列表] [💾保存文件] [↻重载] 状态:…… ``` +> **不再手写 meta JSON**:字段随「类型」及 ACTION 的「动作种类」/ LOGIC 的「logic_op」自动切换;只有 schema 未覆盖的非常规键才会出现在「其它(JSON)」框里(保留不丢)。 + --- ## 3. 新增一个法术(操作流程) @@ -51,7 +51,7 @@ meta(JSON) : [______] 3. 选 **类型**(ACTION / MODIFIER / TRIGGER / LOGIC) 4. 填 **显示名 / 描述**(建议填 `tr()` 键,如 `SPELL_LIGHTNING_NAME`,见 [04 本地化](04_localization.md)) 5. 填 **元素标签**(共鸣用,如 `tag:lightning`;多个用逗号) -6. 编辑 **meta**(JSON)——点「插入该类型 meta 模板」获取起始模板,改数值 +6. 填 **meta 字段**——通用区(蓝耗/商店价/解锁价)常驻;下方动态区随类型(及 ACTION 动作种类 / LOGIC logic_op)自动列出对应字段,直接填数值即可 7. 点 **✓应用到列表** → 校验并写入内存 8. 点 **💾保存文件** → 写入 `res://data/spells.json` 9. 运行游戏(F5)→ 新法术自动出现在商店池,可购买施放 @@ -62,6 +62,8 @@ meta(JSON) : [______] ## 4. meta 字段速查(按类型) +> 下列字段现由表单**按类型自动呈现**,无需手写 JSON;本速查作为字段语义参考。`damage_type`(0物理/1火/2冰/3雷/4毒/5奥术)与 `apply_status_id`/`status_id`(0无/1燃烧/2冻结/3中毒/4潮湿/5沾油/6眩晕/7连击/8易伤)为整数枚举,表单以下拉呈现。 + ### ACTION(弹道) ```json { "base_damage": 5.0, "speed": 350.0, "lifetime": 4.0, "radius": 6.0, @@ -105,7 +107,7 @@ meta(JSON) : [______] ## 5. 数据流(原理) ``` -法术编辑器 dock ──保存──▶ res://data/spells.json ──游戏启动加载──▶ SpellRegistry +法术编辑器 tab ──保存──▶ res://data/spells.json ──游戏启动加载──▶ SpellRegistry (编辑器内) (纯数据,可 git 版本管理) (注册到法术 VM) │ 商店抽取 / 背包装备 / 法术 VM 编译施放 @@ -123,12 +125,12 @@ meta(JSON) : [______] | 工具 | 数据文件 | 对应硬编码 | |:---|:---|:---| -| **法术编辑器**(已实现) | `data/spells.json` | `wand_preset.gd` | +| **法术编辑器**(已实现,game_designer tab) | `data/spells.json` | `wand_preset.gd` | | 敌人编辑器(可加) | `data/enemies.json` | `enemy_manager.gd` `_SPEED`/`_ARMOR` | | 波次编辑器(可加) | `data/waves.json` | `wave_manager.gd` `WAVE_CONFIG` | | Core 编辑器(可加) | `data/cores.json` | `wand_preset.gd` `make_*_core` | | 共鸣配方编辑器(可加) | `data/resonance.json` | `spell_evaluator.gd` `_resonance_recipes` | -实现参考 `addons/spell_editor/spell_dock.gd`:`@tool` 脚本 + `EditorPlugin.add_control_to_dock` + JSON 读写 + 对应 Manager 的 `_load_json_*()` 加载方法。 +实现参考 `addons/game_designer/spell_tab.gd`(字段化表单)与 `core_tab.gd`:`@tool` 脚本 + 复用 `designer_ui.gd` 辅助 + schema 驱动的动态字段 + JSON 读写 + 对应 Manager 的 `_load_json_*()` 加载方法。 > **Godot 编辑器扩展能力**:`EditorPlugin`(停靠面板/工具栏/底部面板)、`@tool`(脚本在编辑器运行)、`EditorInspectorPlugin`(自定义检视器)、自定义 `Resource`(`.tres` 自带表单)、`EditorScript`(一次性脚本)。与 Unity 的 EditorWindow/CustomEditor/ScriptableObject 一一对应,且与节点系统集成更紧。 From ac8a4e439258bda143751002c1d93383b551f407 Mon Sep 17 00:00:00 2001 From: Joywayer Date: Tue, 21 Jul 2026 16:15:06 +0800 Subject: [PATCH 5/5] =?UTF-8?q?docs(spell-editor):=20=E6=B8=85=E7=90=86?= =?UTF-8?q?=E5=AF=B9=E5=B7=B2=E5=88=A0=E9=99=A4=20spell=5Feditor=20?= =?UTF-8?q?=E7=9B=AE=E5=BD=95=E7=9A=84=E6=82=AC=E6=8C=82=E5=BC=95=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 3 +-- addons/game_designer/designer_panel.gd | 2 +- docs/handbook/02_content_authoring.md | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 3ef2e90..36b30eb 100644 --- a/README.md +++ b/README.md @@ -94,8 +94,7 @@ daihaoRogue/ | 插件 | 位置 | 功能 | |:---|:---|:---| -| **🎮 游戏设计器** | `addons/game_designer/` · 底部面板 | 一站式:法术 / 敌人 / 波次 / Core / 平衡 / 共鸣 / 状态 共 7 个表单页 | -| **法术编辑器** | `addons/spell_editor/` | 单独的法术 meta 字段编辑(已并入设计器) | +| **🎮 游戏设计器** | `addons/game_designer/` · 底部面板 | 一站式:法术 / 敌人 / 波次 / Core / 平衡 / 共鸣 / 状态 共 7 个表单页(法术为字段化表单) | | **Godot MCP** | `addons/godot_mcp/` | AI 辅助开发(截图 / 输入 / 场景检查服务) | > 修改后需 **F5 重启**生效(内容在 `_ready()` 时加载)。用法见 [`docs/handbook/07_game_designer.md`](docs/handbook/07_game_designer.md)。 diff --git a/addons/game_designer/designer_panel.gd b/addons/game_designer/designer_panel.gd index a191bab..0124e3f 100644 --- a/addons/game_designer/designer_panel.gd +++ b/addons/game_designer/designer_panel.gd @@ -8,7 +8,7 @@ func _init() -> void: tabs.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) add_child(tabs) - # 法术(复用现有法术编辑器停靠脚本) + # 法术(字段化表单 tab) _add_tab(tabs, "✨ 法术", preload("res://addons/game_designer/spell_tab.gd").new()) _add_tab(tabs, "👾 敌人", preload("res://addons/game_designer/enemy_tab.gd").new()) _add_tab(tabs, "🌊 波次", preload("res://addons/game_designer/wave_tab.gd").new()) diff --git a/docs/handbook/02_content_authoring.md b/docs/handbook/02_content_authoring.md index 142c5c7..578ec1d 100644 --- a/docs/handbook/02_content_authoring.md +++ b/docs/handbook/02_content_authoring.md @@ -12,7 +12,7 @@ > **⚠️ 实现现状 (2026-07-20 审计)**:法术定义唯一来源是 `res://data/spells.json`,由 `SpellRegistry` 启动时加载。`wand_preset.gd` **已不再定义任何法术**(文件头注:「法术已迁至 SpellRegistry」),下文 `make_frost_bolt` / `make_spell_by_id` / `get_all_spell_ids` 等工厂函数**在代码中已不存在**,请勿添加。详见审计报告 ../../docs_dev/doc_code_audit_2026-07-20.md。 -> 💡 **策划优先用可视化工具**:在 [07 游戏设计器](07_game_designer.md) 的「法术」页签(或独立的 [06 法术编辑器](06_spell_editor.md))用表单新增/编辑法术,保存到 `data/spells.json`,**无需写代码**。 +> 💡 **策划优先用可视化工具**:在 [07 游戏设计器](07_game_designer.md) 的「法术」页签(字段化表单,用法详见 [06 法术编辑器](06_spell_editor.md))新增/编辑法术,保存到 `data/spells.json`,**无需写代码**。 ### 法术定义位置