## 法术编辑器标签(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 := ["弹道 Projectile", "毒池 Zone", "召唤 Summon"] # 0=默认(不写 action_kind) 1=zone 2=summon const LOGIC_OPS := ["every_n_shots", "if_hp_below", "if_enemy_nearby", "loop"] # 存储 key,不变 const LOGIC_OP_LABELS := ["每 N 次 every_n_shots", "生命低于 if_hp_below", "附近有敌 if_enemy_nearby", "循环 loop"] # 下拉显示 const DMG_LABELS := ["物理 Physical", "火 Fire", "冰 Ice", "雷 Lightning", "毒 Poison", "奥术 Arcane"] # index = damage_type 整数 const ST_LABELS := ["无 None", "燃烧 Burn", "冻结 Freeze", "中毒 Poison", "潮湿 Wet", "沾油 Oily", "眩晕 Stun", "连击 Combo", "易伤 Vuln"] # index = status 整数 var _data: Dictionary = {} var _cur: String = "" var _list: ItemList var _id: LineEdit var _type: OptionButton var _name: LineEdit var _desc: LineEdit var _elem_checks: Array = [] var _custom_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 键或文本")) var erow := HBoxContainer.new() erow.add_child(UI.cell_label("元素标签")) _elem_checks.clear() for i in UI.ELEM_LABELS.size(): var cb := CheckBox.new(); cb.text = UI.ELEM_LABELS[i] _elem_checks.append(cb); erow.add_child(cb) add_child(erow) _custom_tags = _row("自定义标签", UI.line("", "非元素 tag,逗号分隔")) 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_OP_LABELS, 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": "bounce", "l": "弹跳 bounce", "w": "i", "d": 0, "opt": true}, {"k": "homing", "l": "归航 homing(弧度/秒)", "w": "f", "d": 0.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", "")) var etags: Array = d.get("element_tags", []) for i in _elem_checks.size(): _elem_checks[i].button_pressed = UI.ELEM_TAGS[i] in etags var custom_list := [] for t in etags: if not (String(t) in UI.ELEM_TAGS): custom_list.append(String(t)) _custom_tags.text = ",".join(custom_list) 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 i in _elem_checks.size(): if _elem_checks[i].button_pressed: tags.append(UI.ELEM_TAGS[i]) for t in _custom_tags.text.split(",", false): var s := t.strip_edges() if s != "" and not (s in tags): 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