merge(spell-editor): 法术编辑器并入 game_designer + 字段化表单
This commit is contained in:
@@ -94,8 +94,7 @@ daihaoRogue/
|
|||||||
|
|
||||||
| 插件 | 位置 | 功能 |
|
| 插件 | 位置 | 功能 |
|
||||||
|:---|:---|:---|
|
|:---|:---|:---|
|
||||||
| **🎮 游戏设计器** | `addons/game_designer/` · 底部面板 | 一站式:法术 / 敌人 / 波次 / Core / 平衡 / 共鸣 / 状态 共 7 个表单页 |
|
| **🎮 游戏设计器** | `addons/game_designer/` · 底部面板 | 一站式:法术 / 敌人 / 波次 / Core / 平衡 / 共鸣 / 状态 共 7 个表单页(法术为字段化表单) |
|
||||||
| **法术编辑器** | `addons/spell_editor/` | 单独的法术 meta 字段编辑(已并入设计器) |
|
|
||||||
| **Godot MCP** | `addons/godot_mcp/` | AI 辅助开发(截图 / 输入 / 场景检查服务) |
|
| **Godot MCP** | `addons/godot_mcp/` | AI 辅助开发(截图 / 输入 / 场景检查服务) |
|
||||||
|
|
||||||
> 修改后需 **F5 重启**生效(内容在 `_ready()` 时加载)。用法见 [`docs/handbook/07_game_designer.md`](docs/handbook/07_game_designer.md)。
|
> 修改后需 **F5 重启**生效(内容在 `_ready()` 时加载)。用法见 [`docs/handbook/07_game_designer.md`](docs/handbook/07_game_designer.md)。
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ func _init() -> void:
|
|||||||
tabs.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
tabs.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||||
add_child(tabs)
|
add_child(tabs)
|
||||||
|
|
||||||
# 法术(复用现有法术编辑器停靠脚本)
|
# 法术(字段化表单 tab)
|
||||||
_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/enemy_tab.gd").new())
|
||||||
_add_tab(tabs, "🌊 波次", preload("res://addons/game_designer/wave_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())
|
_add_tab(tabs, "🪄 Core", preload("res://addons/game_designer/core_tab.gd").new())
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://bousxyhirtljs
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
[plugin]
|
|
||||||
name="Spell Editor (法术编辑器)"
|
|
||||||
description="可视化法术编辑器:策划/开发无需改代码即可新增、编辑、删除法术,保存为 res://data/spells.json,游戏运行时自动加载。"
|
|
||||||
author="Base Games"
|
|
||||||
version="1.0.0"
|
|
||||||
script="spell_editor.gd"
|
|
||||||
@@ -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)
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
uid://dkk2bspn83h8s
|
|
||||||
@@ -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
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
uid://qmds48f8kkm0
|
|
||||||
@@ -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。
|
> **⚠️ 实现现状 (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`,**无需写代码**。
|
||||||
|
|
||||||
### 法术定义位置
|
### 法术定义位置
|
||||||
|
|
||||||
|
|||||||
@@ -4,24 +4,22 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 1. 启用插件
|
## 1. 打开编辑器
|
||||||
|
|
||||||
插件位于 `res://addons/spell_editor/`,默认已在 `project.godot` 启用。若未显示:
|
法术编辑现已并入 **游戏设计器**(`res://addons/game_designer/`,默认已在 `project.godot` 启用)。
|
||||||
|
|
||||||
```
|
```
|
||||||
项目 → 项目设置 → 插件 → 勾选 "Spell Editor (法术编辑器)"
|
Godot 编辑器 → 底部面板 → 🎮 游戏设计器 → ✨ 法术 标签页
|
||||||
```
|
```
|
||||||
|
|
||||||
启用后,编辑器**左下停靠区**会出现「法术编辑器」标签页。
|
> 独立的「法术编辑器」左下停靠面板已移除,法术统一在游戏设计器内维护。修改插件后可能需**重启 Godot 编辑器**让面板刷新。
|
||||||
|
|
||||||
> 首次启用或修改插件后,可能需要**重启 Godot 编辑器**让停靠面板出现。
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2. 界面说明
|
## 2. 界面说明
|
||||||
|
|
||||||
```
|
```
|
||||||
✨ 法术编辑器
|
✨ 法术编辑器 — 字段化表单
|
||||||
┌────────────────────────┐
|
┌────────────────────────┐
|
||||||
│ 法术列表(ItemList) │ ← 所有 data/spells.json 中的法术
|
│ 法术列表(ItemList) │ ← 所有 data/spells.json 中的法术
|
||||||
│ [ACTION] action_frost… │
|
│ [ACTION] action_frost… │
|
||||||
@@ -29,19 +27,21 @@
|
|||||||
└────────────────────────┘
|
└────────────────────────┘
|
||||||
[➕新增] [⧉复制] [🗑删除]
|
[➕新增] [⧉复制] [🗑删除]
|
||||||
────────────────────────
|
────────────────────────
|
||||||
ID(唯一) : [______]
|
ID(唯一)/ 类型[ACTION ▾]/ 显示名 / 描述 / 元素标签(逗号)
|
||||||
类型 : [ACTION ▾]
|
── 通用 meta ──
|
||||||
显示名(tr 键) : [______]
|
蓝耗 mana_cost / 商店价 shop_cost / 解锁价 unlock_cost
|
||||||
描述(tr 键) : [______]
|
── 动态字段(随类型/子类型自动切换)──
|
||||||
元素标签(逗号) : [tag:ice]
|
ACTION:动作种类[弹道 / zone 毒池 / summon 召唤] + 对应字段
|
||||||
meta(JSON) : [______]
|
(伤害 / 速度 / 存活 / 半径 / 伤害类型▾ / 施加状态▾ / …)
|
||||||
[______]
|
MODIFIER / TRIGGER / LOGIC(logic_op▾ 子选择)各自字段
|
||||||
[插入该类型 meta 模板]
|
── 其它(JSON) ── ← schema 之外的键,保留不丢
|
||||||
────────────────────────
|
────────────────────────
|
||||||
[✓应用到列表] [💾保存文件] [↻重载]
|
[✓应用到列表] [💾保存文件] [↻重载]
|
||||||
状态:……
|
状态:……
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> **不再手写 meta JSON**:字段随「类型」及 ACTION 的「动作种类」/ LOGIC 的「logic_op」自动切换;只有 schema 未覆盖的非常规键才会出现在「其它(JSON)」框里(保留不丢)。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 3. 新增一个法术(操作流程)
|
## 3. 新增一个法术(操作流程)
|
||||||
@@ -51,7 +51,7 @@ meta(JSON) : [______]
|
|||||||
3. 选 **类型**(ACTION / MODIFIER / TRIGGER / LOGIC)
|
3. 选 **类型**(ACTION / MODIFIER / TRIGGER / LOGIC)
|
||||||
4. 填 **显示名 / 描述**(建议填 `tr()` 键,如 `SPELL_LIGHTNING_NAME`,见 [04 本地化](04_localization.md))
|
4. 填 **显示名 / 描述**(建议填 `tr()` 键,如 `SPELL_LIGHTNING_NAME`,见 [04 本地化](04_localization.md))
|
||||||
5. 填 **元素标签**(共鸣用,如 `tag:lightning`;多个用逗号)
|
5. 填 **元素标签**(共鸣用,如 `tag:lightning`;多个用逗号)
|
||||||
6. 编辑 **meta**(JSON)——点「插入该类型 meta 模板」获取起始模板,改数值
|
6. 填 **meta 字段**——通用区(蓝耗/商店价/解锁价)常驻;下方动态区随类型(及 ACTION 动作种类 / LOGIC logic_op)自动列出对应字段,直接填数值即可
|
||||||
7. 点 **✓应用到列表** → 校验并写入内存
|
7. 点 **✓应用到列表** → 校验并写入内存
|
||||||
8. 点 **💾保存文件** → 写入 `res://data/spells.json`
|
8. 点 **💾保存文件** → 写入 `res://data/spells.json`
|
||||||
9. 运行游戏(F5)→ 新法术自动出现在商店池,可购买施放
|
9. 运行游戏(F5)→ 新法术自动出现在商店池,可购买施放
|
||||||
@@ -62,6 +62,8 @@ meta(JSON) : [______]
|
|||||||
|
|
||||||
## 4. meta 字段速查(按类型)
|
## 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(弹道)
|
### ACTION(弹道)
|
||||||
```json
|
```json
|
||||||
{ "base_damage": 5.0, "speed": 350.0, "lifetime": 4.0, "radius": 6.0,
|
{ "base_damage": 5.0, "speed": 350.0, "lifetime": 4.0, "radius": 6.0,
|
||||||
@@ -105,7 +107,7 @@ meta(JSON) : [______]
|
|||||||
## 5. 数据流(原理)
|
## 5. 数据流(原理)
|
||||||
|
|
||||||
```
|
```
|
||||||
法术编辑器 dock ──保存──▶ res://data/spells.json ──游戏启动加载──▶ SpellRegistry
|
法术编辑器 tab ──保存──▶ res://data/spells.json ──游戏启动加载──▶ SpellRegistry
|
||||||
(编辑器内) (纯数据,可 git 版本管理) (注册到法术 VM)
|
(编辑器内) (纯数据,可 git 版本管理) (注册到法术 VM)
|
||||||
│
|
│
|
||||||
商店抽取 / 背包装备 / 法术 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/enemies.json` | `enemy_manager.gd` `_SPEED`/`_ARMOR` |
|
||||||
| 波次编辑器(可加) | `data/waves.json` | `wave_manager.gd` `WAVE_CONFIG` |
|
| 波次编辑器(可加) | `data/waves.json` | `wave_manager.gd` `WAVE_CONFIG` |
|
||||||
| Core 编辑器(可加) | `data/cores.json` | `wand_preset.gd` `make_*_core` |
|
| Core 编辑器(可加) | `data/cores.json` | `wand_preset.gd` `make_*_core` |
|
||||||
| 共鸣配方编辑器(可加) | `data/resonance.json` | `spell_evaluator.gd` `_resonance_recipes` |
|
| 共鸣配方编辑器(可加) | `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 一一对应,且与节点系统集成更紧。
|
> **Godot 编辑器扩展能力**:`EditorPlugin`(停靠面板/工具栏/底部面板)、`@tool`(脚本在编辑器运行)、`EditorInspectorPlugin`(自定义检视器)、自定义 `Resource`(`.tres` 自带表单)、`EditorScript`(一次性脚本)。与 Unity 的 EditorWindow/CustomEditor/ScriptableObject 一一对应,且与节点系统集成更紧。
|
||||||
|
|||||||
+1
-1
@@ -65,7 +65,7 @@ project/assembly_name="Rogue"
|
|||||||
|
|
||||||
[editor_plugins]
|
[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]
|
[gui]
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user