Files
spellforge/addons/spell_editor/spell_dock.gd
T
joywayerandClaude Opus 4.8 7bc13a7be6 feat(mana): 游戏设计器支持 mana 字段(补 Mana MVP 配套)
- Core 编辑器加「蓝上限/回蓝」两栏(_on_select 读 / _on_apply 写 / _on_new 默认),
  修复此前编辑 Core 保存会静默丢失 mana_max/mana_regen 的数据丢失洞
- 法术编辑器 4 个 meta 模板加 mana_cost,新法术默认有合理蓝耗
- handbook 07 补 Core 蓝池字段 + 法术 mana_cost 说明

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 10:31:01 +08:00

215 lines
6.7 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## 法术编辑器停靠面板(@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 = "metaJSON,因类型而异)"
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)